diff --git a/src/root.css b/src/root.css
index 72da87c..a69d595 100644
--- a/src/root.css
+++ b/src/root.css
@@ -39,3 +39,15 @@ a {
#video-container .scan-region-highlight-svg {
display: none;
}
+
+select {
+ @apply appearance-none;
+ @apply block;
+ @apply border-[2px] focus:outline-none focus:ring-2 focus:ring-offset-2 ring-offset-black;
+ @apply font-light text-lg;
+ @apply py-4 pl-4 pr-8;
+ background-image: url("data:image/svg+xml,%3Csvg aria-hidden='true' class='w-4 h-4 ml-1' fill='white' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M5.293 7.293a1 1 0 0 1 1.414 0L10 10.586l3.293-3.293a1 1 0 1 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 0-1.414z' clip-rule='evenodd'/%3E%3C/svg%3E");
+ background-position: right 0.75rem center;
+ background-size: 20px 20px;
+ background-repeat: no-repeat;
+}
diff --git a/src/routes/Send.tsx b/src/routes/Send.tsx
index 349505a..473f41d 100644
--- a/src/routes/Send.tsx
+++ b/src/routes/Send.tsx
@@ -1,7 +1,18 @@
import { Match, Show, Switch, createEffect, createMemo, createSignal, onMount } from "solid-js";
import { Amount } from "~/components/Amount";
import NavBar from "~/components/NavBar";
-import { Button, ButtonLink, Card, DefaultMain, HStack, LargeHeader, MutinyWalletGuard, SafeArea, SmallHeader, VStack } from "~/components/layout";
+import {
+ Button,
+ ButtonLink,
+ Card,
+ DefaultMain,
+ HStack,
+ LargeHeader,
+ MutinyWalletGuard,
+ SafeArea,
+ SmallHeader,
+ VStack
+} from "~/components/layout";
import { Paste } from "~/assets/svg/Paste";
import { Scan } from "~/assets/svg/Scan";
import { useMegaStore } from "~/state/megaStore";
@@ -11,7 +22,7 @@ import { ParsedParams, toParsedParams } from "./Scanner";
import { showToast } from "~/components/Toaster";
import eify from "~/utils/eify";
import { FullscreenModal } from "~/components/layout/FullscreenModal";
-import megacheck from "~/assets/icons/megacheck.png"
+import megacheck from "~/assets/icons/megacheck.png";
import megaex from "~/assets/icons/megaex.png";
import mempoolTxUrl from "~/utils/mempoolTxUrl";
import { BackLink } from "~/components/layout/BackLink";
@@ -22,379 +33,483 @@ import { AmountCard } from "~/components/AmountCard";
import { MutinyTagItem } from "~/utils/tags";
import { BackButton } from "~/components/layout/BackButton";
-type SendSource = "lightning" | "onchain";
+export type SendSource = "lightning" | "onchain";
// const TEST_DEST = "bitcoin:tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe?amount=0.00001&label=heyo&lightning=lntbs10u1pjrwrdedq8dpjhjmcnp4qd60w268ve0jencwzhz048ruprkxefhj0va2uspgj4q42azdg89uupp5gngy2pqte5q5uvnwcxwl2t8fsdlla5s6xl8aar4xcsvxeus2w2pqsp5n5jp3pz3vpu92p3uswttxmw79a5lc566herwh3f2amwz2sp6f9tq9qyysgqcqpcxqrpwugv5m534ww5ukcf6sdw2m75f2ntjfh3gzeqay649256yvtecgnhjyugf74zakaf56sdh66ec9fqep2kvu6xv09gcwkv36rrkm38ylqsgpw3yfjl"
// const TEST_DEST_ADDRESS = "tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe"
// TODO: better success / fail type
-type SentDetails = { amount?: bigint, destination?: string, txid?: string, failure_reason?: string }
+type SentDetails = {
+ amount?: bigint;
+ destination?: string;
+ txid?: string;
+ failure_reason?: string;
+};
-function MethodChooser(props: { source: SendSource, setSource: (source: string) => void, both?: boolean }) {
- const [store, _actions] = useMegaStore();
+export function MethodChooser(props: {
+ source: SendSource;
+ setSource: (source: string) => void;
+ both?: boolean;
+}) {
+ const [store, _actions] = useMegaStore();
- const methods = createMemo(() => {
- return [
- { value: "lightning", label: "Lightning Balance", caption: store.balance?.lightning ? `${store.balance?.lightning.toLocaleString()} SATS` : "No balance" },
- { value: "onchain", label: "On-chain Balance", caption: store.balance?.confirmed ? `${store.balance?.confirmed.toLocaleString()} SATS` : "No balance" }
- ]
-
- })
- return (
-
-
-
-
-
-
-
-
-
-
-
- )
+ const methods = createMemo(() => {
+ return [
+ {
+ value: "lightning",
+ label: "Lightning Balance",
+ caption: store.balance?.lightning
+ ? `${store.balance?.lightning.toLocaleString()} SATS`
+ : "No balance"
+ },
+ {
+ value: "onchain",
+ label: "On-chain Balance",
+ caption:
+ store.balance?.confirmed || store.balance?.unconfirmed
+ ? `${(
+ (store.balance?.confirmed || 0n) + (store.balance?.unconfirmed || 0n)
+ ).toLocaleString()} SATS`
+ : "No balance"
+ }
+ ];
+ });
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
}
function DestinationInput(props: {
- fieldDestination: string,
- setFieldDestination: (destination: string) => void,
- handleDecode: () => void,
- handlePaste: () => void,
+ fieldDestination: string;
+ setFieldDestination: (destination: string) => void;
+ handleDecode: () => void;
+ handlePaste: () => void;
}) {
- return (
-
- Destination
-
- )
+ return (
+
+ Destination
+
+ );
}
function DestinationShower(props: {
- source: SendSource,
- description?: string,
- address?: string,
- invoice?: MutinyInvoice,
- nodePubkey?: string,
- lnurl?: string,
- clearAll: () => void,
+ source: SendSource;
+ description?: string;
+ address?: string;
+ invoice?: MutinyInvoice;
+ nodePubkey?: string;
+ lnurl?: string;
+ clearAll: () => void;
}) {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
}
export default function Send() {
- const [state, actions] = useMegaStore();
- const navigate = useNavigate()
+ const [state, actions] = useMegaStore();
+ const navigate = useNavigate();
- // These can only be set by the user
- const [fieldDestination, setFieldDestination] = createSignal("");
- const [destination, setDestination] = createSignal
();
+ // These can only be set by the user
+ const [fieldDestination, setFieldDestination] = createSignal("");
+ const [destination, setDestination] = 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 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 [nodePubkey, setNodePubkey] = createSignal();
- const [lnurlp, setLnurlp] = createSignal();
- const [address, setAddress] = createSignal();
- const [description, setDescription] = createSignal();
+ // These can only be derived from the "destination" signal
+ const [invoice, setInvoice] = createSignal();
+ const [nodePubkey, setNodePubkey] = createSignal();
+ const [lnurlp, setLnurlp] = createSignal();
+ const [address, setAddress] = createSignal();
+ const [description, setDescription] = createSignal();
- // Is sending / sent
- const [sending, setSending] = createSignal(false);
- const [sentDetails, setSentDetails] = createSignal();
+ // Is sending / sent
+ const [sending, setSending] = createSignal(false);
+ const [sentDetails, setSentDetails] = createSignal();
- // Tagging stuff
- const [selectedContacts, setSelectedContacts] = createSignal[]>([]);
+ // Tagging stuff
+ const [selectedContacts, setSelectedContacts] = createSignal[]>([]);
- function clearAll() {
- setDestination(undefined);
- setAmountSats(0n);
- setSource("lightning");
- setInvoice(undefined);
- setAddress(undefined);
- setDescription(undefined);
- setNodePubkey(undefined);
- setLnurlp(undefined);
- setFieldDestination("");
+ function clearAll() {
+ setDestination(undefined);
+ setAmountSats(0n);
+ setSource("lightning");
+ setInvoice(undefined);
+ setAddress(undefined);
+ setDescription(undefined);
+ setNodePubkey(undefined);
+ setLnurlp(undefined);
+ setFieldDestination("");
+ }
+
+ const feeEstimate = () => {
+ if (source() === "lightning") return undefined;
+
+ if (source() === "onchain" && amountSats() && amountSats() > 0n && address()) {
+ return state.mutiny_wallet?.estimate_tx_fee(address()!, amountSats(), undefined);
}
- const feeEstimate = () => {
- if (source() === "lightning") return undefined;
+ return undefined;
+ };
- if (source() === "onchain" && amountSats() && amountSats() > 0n && address()) {
- return state.mutiny_wallet?.estimate_tx_fee(address()!, amountSats(), undefined);
- }
-
- return undefined
+ onMount(() => {
+ if (state.scan_result) {
+ setDestination(state.scan_result);
+ actions.setScanResult(undefined);
}
+ });
- onMount(() => {
- if (state.scan_result) {
- setDestination(state.scan_result);
- actions.setScanResult(undefined);
- }
- })
+ // Rerun every time the destination changes
+ createEffect(() => {
+ const source = destination();
+ if (!source) return undefined;
+ try {
+ if (source.address) setAddress(source.address);
+ if (source.memo) setDescription(source.memo);
- // Rerun every time the destination changes
- createEffect(() => {
- const source = destination();
- if (!source) return undefined;
- try {
- if (source.address) setAddress(source.address)
- if (source.memo) setDescription(source.memo);
-
- if (source.invoice) {
- state.mutiny_wallet?.decode_invoice(source.invoice).then(invoice => {
- if (invoice?.amount_sats) setAmountSats(invoice.amount_sats);
- setInvoice(invoice)
- setSource("lightning")
- });
- } else if (source.node_pubkey) {
- setAmountSats(source.amount_sats || 0n);
- setNodePubkey(source.node_pubkey);
- setSource("lightning")
- } else if (source.lnurl) {
- state.mutiny_wallet?.decode_lnurl(source.lnurl).then((lnurlParams) => {
- if (lnurlParams.tag === "payRequest") {
- setAmountSats(source.amount_sats || 0n);
- setLnurlp(source.lnurl);
- setSource("lightning")
- }
- })
- } else {
- setAmountSats(source.amount_sats || 0n);
- setSource("onchain")
- }
- // Return the source just to trigger `decodedDestination` as not undefined
- return source
- } catch (e) {
- console.error("error", e)
- clearAll();
- }
- })
-
- function parsePaste(text: string) {
- if (text) {
- const network = state.mutiny_wallet?.get_network() || "signet";
- const result = toParsedParams(text || "", network);
- if (!result.ok) {
- showToast(result.error);
- return;
- } else {
- if (result.value?.address || result.value?.invoice || result.value?.node_pubkey || result.value?.lnurl) {
- setDestination(result.value);
- // Important! we need to clear the scan result once we've used it
- actions.setScanResult(undefined);
- }
- }
- }
- }
-
- function handleDecode() {
- const text = fieldDestination();
- parsePaste(text);
- }
-
- function handlePaste() {
- if (!navigator.clipboard.readText) return showToast(new Error("Clipboard not supported"));
-
- navigator.clipboard.readText().then(text => {
- setFieldDestination(text);
- parsePaste(text);
- }).catch((e) => {
- showToast(new Error("Failed to read clipboard: " + e.message))
+ if (source.invoice) {
+ state.mutiny_wallet?.decode_invoice(source.invoice).then((invoice) => {
+ if (invoice?.amount_sats) setAmountSats(invoice.amount_sats);
+ setInvoice(invoice);
+ setSource("lightning");
});
+ } else if (source.node_pubkey) {
+ setAmountSats(source.amount_sats || 0n);
+ setNodePubkey(source.node_pubkey);
+ setSource("lightning");
+ } else if (source.lnurl) {
+ state.mutiny_wallet?.decode_lnurl(source.lnurl).then((lnurlParams) => {
+ if (lnurlParams.tag === "payRequest") {
+ setAmountSats(source.amount_sats || 0n);
+ setLnurlp(source.lnurl);
+ setSource("lightning");
+ }
+ });
+ } else {
+ setAmountSats(source.amount_sats || 0n);
+ setSource("onchain");
+ }
+ // Return the source just to trigger `decodedDestination` as not undefined
+ return source;
+ } catch (e) {
+ console.error("error", e);
+ clearAll();
}
+ });
- async function processContacts(contacts: Partial[]): Promise {
- console.log("Processing contacts", contacts)
-
- if (contacts.length) {
- const first = contacts![0];
-
- if (!first.name) {
- console.error("Something went wrong with contact creation, proceeding anyway")
- return []
- }
-
- if (!first.id && first.name) {
- console.error("Creating new contact", first.name)
- const c = new Contact(first.name, undefined, undefined, undefined);
- const newContactId = await state.mutiny_wallet?.create_new_contact(c);
- if (newContactId) {
- return [newContactId];
- }
- }
-
- if (first.id) {
- console.error("Using existing contact", first.name, first.id)
- return [first.id];
- }
-
+ function parsePaste(text: string) {
+ if (text) {
+ const network = state.mutiny_wallet?.get_network() || "signet";
+ const result = toParsedParams(text || "", network);
+ if (!result.ok) {
+ showToast(result.error);
+ return;
+ } else {
+ if (
+ result.value?.address ||
+ result.value?.invoice ||
+ result.value?.node_pubkey ||
+ result.value?.lnurl
+ ) {
+ setDestination(result.value);
+ // Important! we need to clear the scan result once we've used it
+ actions.setScanResult(undefined);
}
-
- console.error("Something went wrong with contact creation, proceeding anyway")
- return []
-
+ }
}
+ }
- async function handleSend() {
- try {
- setSending(true);
- const bolt11 = invoice()?.bolt11;
- const sentDetails: Partial = {};
+ function handleDecode() {
+ const text = fieldDestination();
+ parsePaste(text);
+ }
- const tags = await processContacts(selectedContacts());
+ function handlePaste() {
+ if (!navigator.clipboard.readText) return showToast(new Error("Clipboard not supported"));
- if (source() === "lightning" && invoice() && bolt11) {
- const nodes = await state.mutiny_wallet?.list_nodes();
- const firstNode = nodes[0] as string || ""
- sentDetails.destination = bolt11;
- // If the invoice has sats use that, otherwise we pass the user-defined amount
- if (invoice()?.amount_sats) {
- await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, undefined, tags);
- sentDetails.amount = invoice()?.amount_sats;
- } else {
- await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, amountSats(), tags);
- sentDetails.amount = amountSats();
- }
- } else if (source() === "lightning" && nodePubkey()) {
- const nodes = await state.mutiny_wallet?.list_nodes();
- const firstNode = nodes[0] as string || ""
- const payment = await state.mutiny_wallet?.keysend(firstNode, nodePubkey()!, amountSats(), tags);
+ navigator.clipboard
+ .readText()
+ .then((text) => {
+ setFieldDestination(text);
+ parsePaste(text);
+ })
+ .catch((e) => {
+ showToast(new Error("Failed to read clipboard: " + e.message));
+ });
+ }
- // TODO: handle timeouts
- if (!payment?.paid) {
- throw new Error("Keysend failed")
- } else {
- sentDetails.amount = amountSats();
- }
- } else if (source() === "lightning" && lnurlp()) {
- const nodes = await state.mutiny_wallet?.list_nodes();
- const firstNode = nodes[0] as string || ""
- const payment = await state.mutiny_wallet?.lnurl_pay(firstNode, lnurlp()!, amountSats(), tags);
+ async function processContacts(contacts: Partial[]): Promise {
+ console.log("Processing contacts", contacts);
- if (!payment?.paid) {
- throw new Error("Lnurl Pay failed")
- } else {
- sentDetails.amount = amountSats();
- }
- } else if (source() === "onchain" && address()) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const txid = await state.mutiny_wallet?.send_to_address(address()!, amountSats(), tags);
- sentDetails.amount = amountSats();
- sentDetails.destination = address();
- // TODO: figure out if this is necessary, it takes forever
- await actions.sync();
- sentDetails.txid = txid;
- }
- setSentDetails(sentDetails as SentDetails);
- clearAll();
- } catch (e) {
- const error = eify(e)
- setSentDetails({ failure_reason: error.message });
- // TODO: figure out ux of when we want to show toast vs error screen
- // showToast(eify(e))
- console.error(e);
- } finally {
- setSending(false);
+ if (contacts.length) {
+ const first = contacts![0];
+
+ if (!first.name) {
+ console.error("Something went wrong with contact creation, proceeding anyway");
+ return [];
+ }
+
+ if (!first.id && first.name) {
+ console.error("Creating new contact", first.name);
+ const c = new Contact(first.name, undefined, undefined, undefined);
+ const newContactId = await state.mutiny_wallet?.create_new_contact(c);
+ if (newContactId) {
+ return [newContactId];
}
+ }
+
+ if (first.id) {
+ console.error("Using existing contact", first.name, first.id);
+ return [first.id];
+ }
}
- const sendButtonDisabled = createMemo(() => {
- return !destination() || sending() || amountSats() === 0n;
- })
+ console.error("Something went wrong with contact creation, proceeding anyway");
+ return [];
+ }
- return (
-
-
-
- }>
- clearAll()} title="Start Over" />
-
- Send Bitcoin
- { if (!open) setSentDetails(undefined) }}
- onConfirm={() => { setSentDetails(undefined); navigate("/"); }}
+ async function handleSend() {
+ try {
+ setSending(true);
+ const bolt11 = invoice()?.bolt11;
+ const sentDetails: Partial = {};
+
+ const tags = await processContacts(selectedContacts());
+
+ if (source() === "lightning" && invoice() && bolt11) {
+ const nodes = await state.mutiny_wallet?.list_nodes();
+ const firstNode = (nodes[0] as string) || "";
+ sentDetails.destination = bolt11;
+ // If the invoice has sats use that, otherwise we pass the user-defined amount
+ if (invoice()?.amount_sats) {
+ await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, undefined, tags);
+ sentDetails.amount = invoice()?.amount_sats;
+ } else {
+ await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, amountSats(), tags);
+ sentDetails.amount = amountSats();
+ }
+ } else if (source() === "lightning" && nodePubkey()) {
+ const nodes = await state.mutiny_wallet?.list_nodes();
+ const firstNode = (nodes[0] as string) || "";
+ const payment = await state.mutiny_wallet?.keysend(
+ firstNode,
+ nodePubkey()!,
+ amountSats(),
+ tags
+ );
+
+ // TODO: handle timeouts
+ if (!payment?.paid) {
+ throw new Error("Keysend failed");
+ } else {
+ sentDetails.amount = amountSats();
+ }
+ } else if (source() === "lightning" && lnurlp()) {
+ const nodes = await state.mutiny_wallet?.list_nodes();
+ const firstNode = (nodes[0] as string) || "";
+ const payment = await state.mutiny_wallet?.lnurl_pay(
+ firstNode,
+ lnurlp()!,
+ amountSats(),
+ tags
+ );
+
+ if (!payment?.paid) {
+ throw new Error("Lnurl Pay failed");
+ } else {
+ sentDetails.amount = amountSats();
+ }
+ } else if (source() === "onchain" && address()) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const txid = await state.mutiny_wallet?.send_to_address(address()!, amountSats(), tags);
+ sentDetails.amount = amountSats();
+ sentDetails.destination = address();
+ // TODO: figure out if this is necessary, it takes forever
+ await actions.sync();
+ sentDetails.txid = txid;
+ }
+ setSentDetails(sentDetails as SentDetails);
+ clearAll();
+ } catch (e) {
+ const error = eify(e);
+ setSentDetails({ failure_reason: error.message });
+ // TODO: figure out ux of when we want to show toast vs error screen
+ // showToast(eify(e))
+ console.error(e);
+ } finally {
+ setSending(false);
+ }
+ }
+
+ const sendButtonDisabled = createMemo(() => {
+ return !destination() || sending() || amountSats() === 0n;
+ });
+
+ return (
+
+
+
+ }>
+ clearAll()} title="Start Over" />
+
+ Send Bitcoin
+ {
+ if (!open) setSentDetails(undefined);
+ }}
+ onConfirm={() => {
+ setSentDetails(undefined);
+ navigate("/");
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ Private tags
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/routes/Storybook.tsx b/src/routes/Storybook.tsx
index f33f0e9..7839414 100644
--- a/src/routes/Storybook.tsx
+++ b/src/routes/Storybook.tsx
@@ -1,29 +1,98 @@
+import { Match, Show, Switch } from "solid-js";
import { ActivityItem } from "~/components/ActivityItem";
+import { Amount } from "~/components/Amount";
import { AmountCard } from "~/components/AmountCard";
import NavBar from "~/components/NavBar";
import { OnboardWarning } from "~/components/OnboardWarning";
import { ShareCard } from "~/components/ShareCard";
-import { Card, DefaultMain, LargeHeader, SafeArea, VStack } from "~/components/layout";
+import { Card, DefaultMain, LargeHeader, SafeArea, SmallHeader, VStack } from "~/components/layout";
+import { FullscreenModal } from "~/components/layout/FullscreenModal";
+import mempoolTxUrl from "~/utils/mempoolTxUrl";
+import megaex from "~/assets/icons/megaex.png";
+import megacheck from "~/assets/icons/megacheck.png";
-const SAMPLE = "bitcoin:tb1prqm8xtlgme0vmw5s30lgf0a4f5g4mkgsqundwmpu6thrg8zr6uvq2qrhzq?amount=0.001&lightning=lntbs1m1pj9n9xjsp5xgdrmvprtm67p7nq4neparalexlhlmtxx87zx6xeqthsplu842zspp546d6zd2seyaxpapaxx62m88yz3xueqtjmn9v6wj8y56np8weqsxqdqqnp4qdn2hj8tfknpuvdg6tz9yrf3e27ltrx9y58c24jh89lnm43yjwfc5xqrpwjcqpj9qrsgq5sdgh0m3ur5mu5hrmmag4mx9yvy86f83pd0x9ww80kgck6tac3thuzkj0mrtltaxwnlfea95h2re7tj4qsnwzxlvrdmyq2h9mgapnycpppz6k6"
+const SAMPLE =
+ "bitcoin:tb1prqm8xtlgme0vmw5s30lgf0a4f5g4mkgsqundwmpu6thrg8zr6uvq2qrhzq?amount=0.001&lightning=lntbs1m1pj9n9xjsp5xgdrmvprtm67p7nq4neparalexlhlmtxx87zx6xeqthsplu842zspp546d6zd2seyaxpapaxx62m88yz3xueqtjmn9v6wj8y56np8weqsxqdqqnp4qdn2hj8tfknpuvdg6tz9yrf3e27ltrx9y58c24jh89lnm43yjwfc5xqrpwjcqpj9qrsgq5sdgh0m3ur5mu5hrmmag4mx9yvy86f83pd0x9ww80kgck6tac3thuzkj0mrtltaxwnlfea95h2re7tj4qsnwzxlvrdmyq2h9mgapnycpppz6k6";
export default function Admin() {
- return (
-
-
- Storybook
-
-
-
-
-
+ const channelOpenResult = () => {
+ return {
+ channel: {
+ balance: 100000n,
+ reserve: 1000n,
+ outpoint: "123:0"
+ },
+ failure_reason: undefined
+ };
+ };
+
+ const setChannelOpenResult = (result: any) => {};
+
+ return (
+
+
+ Storybook
+
+
+
+
+ {/*
-
-
-
-
-
- )
-}
\ No newline at end of file
+ */}
+ {
+ if (!open) setChannelOpenResult(undefined);
+ }}
+ onConfirm={() => {
+ setChannelOpenResult(undefined);
+ // navigate("/");
+ }}
+ >
+
+
+
+
+
+
+ {channelOpenResult()?.failure_reason?.message}
+
+
+
+
+
+
+
+ Mempool Link
+
+
+ {/* {JSON.stringify(channelOpenResult()?.channel?.value, null, 2)} */}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/routes/Swap.tsx b/src/routes/Swap.tsx
new file mode 100644
index 0000000..9b2f63c
--- /dev/null
+++ b/src/routes/Swap.tsx
@@ -0,0 +1,302 @@
+import { createForm, required } from "@modular-forms/solid";
+import { MutinyChannel, MutinyPeer } from "@mutinywallet/mutiny-wasm";
+import { For, Match, Show, Switch, createResource, createSignal } from "solid-js";
+import { AmountCard } from "~/components/AmountCard";
+import NavBar from "~/components/NavBar";
+import { showToast } from "~/components/Toaster";
+import {
+ Button,
+ Card,
+ Checkbox,
+ DefaultMain,
+ LargeHeader,
+ MutinyWalletGuard,
+ SafeArea,
+ VStack
+} from "~/components/layout";
+import { BackLink } from "~/components/layout/BackLink";
+import { TextField } from "~/components/layout/TextField";
+import { MethodChooser, SendSource } from "~/routes/Send";
+import { useMegaStore } from "~/state/megaStore";
+import eify from "~/utils/eify";
+import megaex from "~/assets/icons/megaex.png";
+import megacheck from "~/assets/icons/megacheck.png";
+import { InfoBox } from "~/components/InfoBox";
+import { FullscreenModal } from "~/components/layout/FullscreenModal";
+import { useNavigate } from "solid-start";
+import mempoolTxUrl from "~/utils/mempoolTxUrl";
+import { Network } from "~/logic/mutinyWalletSetup";
+
+const CHANNEL_FEE_ESTIMATE_ADDRESS =
+ "bc1qf7546vg73ddsjznzq57z3e8jdn6gtw6au576j07kt6d9j7nz8mzsyn6lgf";
+
+type PeerConnectForm = {
+ peer: string;
+};
+
+type ChannelOpenDetails = {
+ channel?: MutinyChannel;
+ failure_reason?: Error;
+};
+
+export default function Swap() {
+ const [state, actions] = useMegaStore();
+ const navigate = useNavigate();
+
+ const [source, setSource] = createSignal("onchain");
+ const [amountSats, setAmountSats] = createSignal(0n);
+ const [useLsp, setUseLsp] = createSignal(true);
+ const [isConnecting, setIsConnecting] = createSignal(false);
+
+ const [selectedPeer, setSelectedPeer] = createSignal("");
+
+ const [channelOpenResult, setChannelOpenResult] = createSignal();
+
+ const feeEstimate = () => {
+ if (amountSats()) {
+ try {
+ return state.mutiny_wallet?.estimate_tx_fee(
+ CHANNEL_FEE_ESTIMATE_ADDRESS,
+ amountSats(),
+ undefined
+ );
+ } catch (e) {
+ console.error(e);
+ // showToast(eify(new Error("Unsufficient funds")))
+ return undefined;
+ }
+ }
+ return undefined;
+ };
+
+ const hasLsp = () => {
+ return !!localStorage.getItem("MUTINY_SETTINGS_lsp") || !!import.meta.env.VITE_LSP;
+ };
+
+ const getPeers = async () => {
+ return (await state.mutiny_wallet?.list_peers()) as Promise;
+ };
+
+ const [peers, { refetch }] = createResource(getPeers);
+
+ const [_peerForm, { Form, Field }] = createForm();
+
+ const onSubmit = async (values: PeerConnectForm) => {
+ setIsConnecting(true);
+ try {
+ const peerConnectString = values.peer.trim();
+ const nodes = await state.mutiny_wallet?.list_nodes();
+ const firstNode = (nodes[0] as string) || "";
+
+ await state.mutiny_wallet?.connect_to_peer(firstNode, peerConnectString);
+
+ await refetch();
+
+ // If peers list contains the peer we just connected to, select it
+ const peer = peers()?.find((p) => p.pubkey === peerConnectString.split("@")[0]);
+
+ if (peer) {
+ setSelectedPeer(peer.pubkey);
+ } else {
+ showToast(new Error("Peer not found"));
+ }
+ } catch (e) {
+ showToast(eify(e));
+ } finally {
+ setIsConnecting(false);
+ }
+ };
+
+ const handlePeerSelect = (
+ e: Event & {
+ currentTarget: HTMLSelectElement;
+ target: HTMLSelectElement;
+ }
+ ) => {
+ setSelectedPeer(e.currentTarget.value);
+ };
+
+ const handleSwap = async () => {
+ if (canSwap()) {
+ try {
+ const nodes = await state.mutiny_wallet?.list_nodes();
+ const firstNode = (nodes[0] as string) || "";
+
+ if (useLsp()) {
+ const new_channel = await state.mutiny_wallet?.open_channel(
+ firstNode,
+ undefined,
+ amountSats()
+ );
+
+ setChannelOpenResult({ channel: new_channel });
+ } else {
+ const new_channel = await state.mutiny_wallet?.open_channel(
+ firstNode,
+ selectedPeer(),
+ amountSats()
+ );
+
+ setChannelOpenResult({ channel: new_channel });
+ }
+ } catch (e) {
+ setChannelOpenResult({ failure_reason: eify(e) });
+ // showToast(eify(e))
+ }
+ }
+ };
+
+ const canSwap = () => {
+ const balance = (state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n);
+ return (!!selectedPeer() || !!useLsp()) && amountSats() >= 10000n && amountSats() <= balance;
+ };
+
+ const amountWarning = () => {
+ if (amountSats() < 10000n) {
+ return "It's just silly to make a channel smaller than 10,000 sats";
+ }
+
+ if (
+ amountSats() > (state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n) ||
+ !feeEstimate()
+ ) {
+ return "You don't have enough funds to make this channel";
+ }
+
+ return undefined;
+ };
+
+ const network = state.mutiny_wallet?.get_network() as Network;
+
+ return (
+
+
+
+
+ Swap to Lightning
+ {
+ if (!open) setChannelOpenResult(undefined);
+ }}
+ onConfirm={() => {
+ setChannelOpenResult(undefined);
+ navigate("/");
+ }}
+ >
+
+
+
+
+
+
+ {channelOpenResult()?.failure_reason?.message}
+
+
+
+
+
+
+
+ Mempool Link
+
+
+ {/* {JSON.stringify(channelOpenResult()?.channel?.value, null, 2)} */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0n}>
+ {amountWarning()}
+
+
+
+
+
+
+
+
+ );
+}