diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47a36ea..5fb7872 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2326,7 +2326,7 @@ packages: postcss: ^8.1.0 dependencies: browserslist: 4.21.5 - caniuse-lite: 1.0.30001477 + caniuse-lite: 1.0.30001478 fraction.js: 4.2.0 normalize-range: 0.1.2 picocolors: 1.0.0 @@ -2427,8 +2427,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001477 - electron-to-chromium: 1.4.359 + caniuse-lite: 1.0.30001478 + electron-to-chromium: 1.4.360 node-releases: 2.0.10 update-browserslist-db: 1.0.10(browserslist@4.21.5) @@ -2472,8 +2472,8 @@ packages: engines: {node: '>= 6'} dev: true - /caniuse-lite@1.0.30001477: - resolution: {integrity: sha512-lZim4iUHhGcy5p+Ri/G7m84hJwncj+Kz7S5aD4hoQfslKZJgt0tHc/hafVbqHC5bbhHb+mrW2JOUHkI5KH7toQ==} + /caniuse-lite@1.0.30001478: + resolution: {integrity: sha512-gMhDyXGItTHipJj2ApIvR+iVB5hd0KP3svMWWXDvZOmjzJJassGLMfxRkQCSYgGd2gtdL/ReeiyvMSFD1Ss6Mw==} /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -2735,8 +2735,8 @@ packages: jake: 10.8.5 dev: true - /electron-to-chromium@1.4.359: - resolution: {integrity: sha512-OoVcngKCIuNXtZnsYoqlCvr0Cf3NIPzDIgwUfI9bdTFjXCrr79lI0kwQstLPZ7WhCezLlGksZk/BFAzoXC7GDw==} + /electron-to-chromium@1.4.360: + resolution: {integrity: sha512-EP/jdF15S+l3iSSzgUpUqeazvkbVFXNuVxwwLMVUSie3lUeH1HH70gKe0IS7TASB/0h5QPG2bLMzv2jelSztIQ==} /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} diff --git a/src/assets/icons/paste.svg b/src/assets/icons/paste.svg new file mode 100644 index 0000000..e409489 --- /dev/null +++ b/src/assets/icons/paste.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/Paste.tsx b/src/assets/svg/Paste.tsx new file mode 100644 index 0000000..07adc78 --- /dev/null +++ b/src/assets/svg/Paste.tsx @@ -0,0 +1,8 @@ +export function Paste() { + return ( + + + + ) +} + diff --git a/src/assets/svg/Scan.tsx b/src/assets/svg/Scan.tsx new file mode 100644 index 0000000..ef09950 --- /dev/null +++ b/src/assets/svg/Scan.tsx @@ -0,0 +1,6 @@ +export function Scan() { + return ( + + ) +} + diff --git a/src/components/Amount.tsx b/src/components/Amount.tsx new file mode 100644 index 0000000..560694b --- /dev/null +++ b/src/components/Amount.tsx @@ -0,0 +1,35 @@ +import { Show, createResource } from "solid-js" +import { useMegaStore } from "~/state/megaStore" +import { satsToUsd } from "~/utils/conversions" + +function prettyPrintAmount(n?: number | bigint): string { + if (!n || n.valueOf() === 0) { + return "0" + } + return n.toLocaleString() +} + +export function Amount(props: { amountSats: bigint | number | undefined, showFiat?: boolean }) { + + const [state, _] = useMegaStore() + + async function getPrice() { + return await state.node_manager?.get_bitcoin_price() + } + + const [price] = createResource(getPrice) + const amountInUsd = () => satsToUsd(price(), Number(props.amountSats) || 0, true) + + return ( +
+

+ {prettyPrintAmount(props.amountSats)} SAT +

+ +

+ ≈ {amountInUsd()} USD +

+
+
+ ) +} \ No newline at end of file diff --git a/src/components/BalanceBox.tsx b/src/components/BalanceBox.tsx index 40eefdf..11d38bc 100644 --- a/src/components/BalanceBox.tsx +++ b/src/components/BalanceBox.tsx @@ -1,8 +1,9 @@ import { Motion, Presence } from "@motionone/solid"; import { createResource, Show, Suspense } from "solid-js"; -import { ButtonLink } from "~/components/layout"; +import { ButtonLink, SmallHeader } from "~/components/layout"; import { useMegaStore } from "~/state/megaStore"; +import { Amount } from "./Amount"; function prettyPrintAmount(n?: number | bigint): string { if (!n || n.valueOf() === 0) { @@ -32,31 +33,31 @@ export default function BalanceBox() { transition={{ duration: 0.5, easing: [0.87, 0, 0.13, 1] }} >
-
- Balance -
+ + Lightning Balance +
-

- - -
-
- {prettyPrintAmount(balance()?.confirmed)} SAT -
- -
-
- Unconfirmed Balance -
-
- {prettyPrintAmount(balance()?.unconfirmed)} SAT -
+ + +
+ + + On-Chain Balance + + + +
+
+ Unconfirmed Balance +
+
+ {prettyPrintAmount(balance()?.unconfirmed)} SAT
- -
-
- -

+
+ +
+ +
Send diff --git a/src/components/KitchenSink.tsx b/src/components/KitchenSink.tsx index 4dcf37e..c9355ed 100644 --- a/src/components/KitchenSink.tsx +++ b/src/components/KitchenSink.tsx @@ -1,5 +1,5 @@ import { useMegaStore } from "~/state/megaStore"; -import { Card, Hr, SmallHeader, Button } from "~/components/layout"; +import { Card, Hr, SmallHeader, Button, InnerCard } from "~/components/layout"; import PeerConnectModal from "~/components/PeerConnectModal"; import { For, Show, Suspense, createResource, createSignal } from "solid-js"; import { MutinyChannel, MutinyPeer } from "@mutinywallet/node-manager"; @@ -60,19 +60,21 @@ function ConnectPeer(props: { refetchPeers: RefetchPeersType }) { }; return ( -
- - Connect Peer - - Expecting something like mutiny:abc123... - - -
+ +
+ + Connect Peer + + Expecting something like mutiny:abc123... + + +
+
) } @@ -156,27 +158,29 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) { return ( <> -
- - Pubkey - - - - Amount - - - -
+ +
+ + Pubkey + + + + Amount + + + +
+
                     {JSON.stringify(newChannel()?.outpoint, null, 2)}
diff --git a/src/components/layout/Button.tsx b/src/components/layout/Button.tsx
index 98047d1..9020d0f 100644
--- a/src/components/layout/Button.tsx
+++ b/src/components/layout/Button.tsx
@@ -3,19 +3,20 @@ import { children, JSX, ParentComponent, splitProps } from "solid-js";
 import { Dynamic } from "solid-js/web";
 import { A } from "solid-start";
 
-const button = cva(["p-4", "rounded-xl", "text-xl", "font-semibold", "disabled:opacity-50", "transition"], {
+const button = cva("p-4 rounded-xl text-xl font-semibold disabled:opacity-50 disabled:grayscale transition", {
     variants: {
         intent: {
-            active: "bg-white text-black",
-            inactive: "bg-black text-white border border-white hover:enabled:text-[#3B6CCC]",
-            blue: "bg-m-blue text-white shadow-inner-button hover:bg-m-blue-dark text-shadow-button",
-            red: "bg-m-red text-white shadow-inner-button hover:bg-m-red-dark text-shadow-button",
-            green: "bg-m-green text-white shadow-inner-button hover:bg-m-green-dark text-shadow-button",
+            active: "bg-white text-black border border-white enabled:hover:text-[#3B6CCC]",
+            inactive: "bg-black text-white border border-white enabled:hover:text-[#3B6CCC]",
+            // TODO: not sure what breaks these hover states, they work in tailwind playground
+            blue: "bg-m-blue text-white shadow-inner-button enabled:hover:bg-m-blue-dark text-shadow-button",
+            red: "bg-m-red text-white shadow-inner-button enabled:bg-m-red-dark text-shadow-button",
+            green: "bg-m-green text-white shadow-inner-button enabled:bg-m-green-dark text-shadow-button",
         },
         layout: {
             flex: "flex-1",
             pad: "px-8",
-            small: "p-1 w-auto",
+            small: "px-4 py-2 w-auto text-lg",
         },
     },
 
diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx
index e4acf43..9cc122d 100644
--- a/src/components/layout/index.tsx
+++ b/src/components/layout/index.tsx
@@ -12,7 +12,15 @@ const Card: ParentComponent<{ title?: string }> = (props) => {
             {props.title && {props.title}}
             {props.children}
         
+ ) +} +const InnerCard: ParentComponent<{ title?: string }> = (props) => { + return ( +
+ {props.title && {props.title}} + {props.children} +
) } @@ -61,4 +69,4 @@ const LoadingSpinner = () => { const Hr = () => -export { SmallHeader, Card, SafeArea, LoadingSpinner, Button, ButtonLink, Linkify, Hr, NodeManagerGuard, FullscreenLoader } +export { SmallHeader, Card, SafeArea, LoadingSpinner, Button, ButtonLink, Linkify, Hr, NodeManagerGuard, FullscreenLoader, InnerCard } diff --git a/src/root.css b/src/root.css index 287ce3d..9a52f24 100644 --- a/src/root.css +++ b/src/root.css @@ -40,3 +40,8 @@ a { #video-container .scan-region-highlight-svg { display: none; } + +/* Missing you sveltekit */ +dd { + @apply mb-8 mt-4; +} diff --git a/src/routes/Send.tsx b/src/routes/Send.tsx index 714f7e3..c1fe06d 100644 --- a/src/routes/Send.tsx +++ b/src/routes/Send.tsx @@ -1,11 +1,205 @@ +import { TextField } from "@kobalte/core"; +import { Show, createResource, createSignal } from "solid-js"; +import { Amount } from "~/components/Amount"; import NavBar from "~/components/NavBar"; -import { SafeArea } from "~/components/layout"; +import { Button, InnerCard, SafeArea, SmallHeader } from "~/components/layout"; + +import { Paste } from "~/assets/svg/Paste"; +import { Scan } from "~/assets/svg/Scan"; +import { Motion, Presence } from "@motionone/solid"; +import { useMegaStore } from "~/state/megaStore"; +import { MutinyInvoice, NodeManager } from "@mutinywallet/node-manager"; +import { bip21decode } from "~/utils/TEMPbip21"; + +type SendSource = "lightning" | "onchain"; export default function Send() { + const [state, _] = useMegaStore(); + + // These can only be set by the user + const [destination, setDestination] = createSignal(""); + const [privateLabel, setPrivateLabel] = createSignal(""); + + // These can be derived from the "destination" signal or set by the user + const [amountSats, setAmountSats] = createSignal(0n); + const [source, setSource] = createSignal("lightning"); + + // These can only be derived from the "destination" signal + const [invoice, setInvoice] = createSignal(); + const [address, setAddress] = createSignal(); + const [description, setDescription] = createSignal(); + + async function decode(source: string) { + if (!source) return; + try { + const { address, label, lightning, amount } = bip21decode(source); + + setAddress(address) + + if (lightning) { + const invoice = await state.node_manager?.decode_invoice(lightning); + if (invoice?.amount_sats) setAmountSats(invoice.amount_sats); + setInvoice(invoice) + // We can stick with default lightning because there's an invoice + setSource("lightning") + } else { + // If we can't use the lightning amount we have to use the float btc amount + const amt = NodeManager.convert_btc_to_sats(amount || 0); + setAmountSats(amt); + + // We use onchain because there's no invoice + setSource("onchain") + } + + if (label) setDescription(label); + + setInvoice(invoice) + + return invoice + + } catch (e) { + console.error("error", e) + } + } + + // IMPORTANT: pass the signal but don't "call" the signal (`destination`, not `destination()`) + const [_decodedDestination] = createResource(destination, decode); + + let labelInput!: HTMLInputElement; + + function handlePaste() { + navigator.clipboard.readText().then(text => { + setDestination(text); + labelInput.focus(); + }); + } + + async function handleSend() { + const bolt11 = invoice()?.bolt11; + if (source() === "lightning" && invoice() && bolt11) { + const nodes = await state.node_manager?.list_nodes(); + const firstNode = nodes[0] as string || "" + // If the invoice has sats use that, otherwise we pass the user-defined amount + if (invoice()?.amount_sats) { + await state.node_manager?.pay_invoice(firstNode, bolt11); + } else { + await state.node_manager?.pay_invoice(firstNode, bolt11, amountSats()); + + } + } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const txid = await state.node_manager?.send_to_address(address()!, amountSats()); + console.error(txid) + } + console.error("SENT"); + } + return (

Send Bitcoin

+
+
+ + Source + +
+
+
+ + +
+
+
+ + How Much + +
+
+ +
+
+ + Destination +
+
+ + + + +
}> + + +
+ + {source() === "onchain" && "→"} {address()} + + + + {source() === "lightning" && "→"} {invoice()?.bolt11} + + +
+ {/* {destination()} */} +
+
+ + + + + + +
+ + Description +
+
+ + {description()} +
+
+ + +
+ + Label (private) + +
+
+ labelInput = el} + class="w-full p-2 rounded-lg text-black" + placeholder="A helpful reminder of why you spent bitcoin" + /> +
+
+ + + + +
diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 1a489ae..46423f2 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,10 +1,10 @@ import App from "~/components/App"; -import { Switch, Match, Suspense, Show } from "solid-js"; +import { Switch, Match } from "solid-js"; import { WaitlistAlreadyIn } from "~/components/waitlist/WaitlistAlreadyIn"; import WaitlistForm from "~/components/waitlist/WaitlistForm"; import { useMegaStore } from "~/state/megaStore"; -import { FullscreenLoader, LoadingSpinner } from "~/components/layout"; +import { FullscreenLoader } from "~/components/layout"; export default function Home() { const [state, _] = useMegaStore(); @@ -24,6 +24,5 @@ export default function Home() { - ); } diff --git a/src/utils/TEMPbip21.ts b/src/utils/TEMPbip21.ts new file mode 100644 index 0000000..70fa483 --- /dev/null +++ b/src/utils/TEMPbip21.ts @@ -0,0 +1,18 @@ +// Take in a string that looks like this: +// bitcoin:tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe?amount=0.00001&label=heyo&lightning=lntbs10u1pjrwrdedq8dpjhjmcnp4qd60w268ve0jencwzhz048ruprkxefhj0va2uspgj4q42azdg89uupp5gngy2pqte5q5uvnwcxwl2t8fsdlla5s6xl8aar4xcsvxeus2w2pqsp5n5jp3pz3vpu92p3uswttxmw79a5lc566herwh3f2amwz2sp6f9tq9qyysgqcqpcxqrpwugv5m534ww5ukcf6sdw2m75f2ntjfh3gzeqay649256yvtecgnhjyugf74zakaf56sdh66ec9fqep2kvu6xv09gcwkv36rrkm38ylqsgpw3yfjl +// and return an object with this shape: { address: string, amount: number, label: string, lightning: string } +// using typescript type annotations +export function bip21decode(bip21: string): { address: string, amount?: number, label?: string, lightning?: string } { + const [scheme, data] = bip21.split(':') + if (scheme !== 'bitcoin') { + throw new Error('Not a bitcoin URI') + } + const [address, query] = data.split('?') + const params = new URLSearchParams(query) + return { + address, + amount: Number(params.get('amount')) || undefined, + label: params.get('label') || undefined, + lightning: params.get('lightning') || undefined + } +} \ No newline at end of file diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 1d232a9..9aeef87 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -28,7 +28,7 @@ module.exports = { backgroundImage: { 'fade-to-blue': 'linear-gradient(1.63deg, #0B215B 32.05%, rgba(11, 33, 91, 0) 84.78%)', 'subtle-fade': 'linear-gradient(180deg, #060A13 0%, #131E39 100%)', - 'richer-fade': 'linear-gradient(180deg, #050914 0%, #0A1329 100%)' + 'richer-fade': 'linear-gradient(180deg, hsla(224, 20%, 8%, 1) 0%, hsla(224, 20%, 15%, 1) 100%)' }, dropShadow: { 'blue-glow': '0px 0px 32px rgba(11, 33, 91, 0.5)', @@ -80,7 +80,6 @@ module.exports = { } } } - addUtilities(newUtilities); }), // Text shadow!