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] }}
>
-
+
+ Lightning Balance
+
-
-
-
-
-
- {prettyPrintAmount(balance()?.confirmed)} SAT
-
-
-
-
-
- {prettyPrintAmount(balance()?.unconfirmed)} SAT
-
+
+
+
+
+
+ On-Chain 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 (
-
+
+
+
)
}
@@ -156,27 +158,29 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
return (
<>
-
+
+
+
{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!