diff --git a/src/components/DLCList.tsx b/src/components/DLCList.tsx new file mode 100644 index 00000000..025fcefd --- /dev/null +++ b/src/components/DLCList.tsx @@ -0,0 +1,94 @@ +import { Collapsible } from "@kobalte/core"; +import { Contract } from "@mutinywallet/mutiny-wasm"; +import { createResource, For, Suspense } from "solid-js"; + +import { Button, InnerCard, VStack } from "~/components"; +import { useMegaStore } from "~/state/megaStore"; + +type RefetchDLCsType = ( + info?: unknown +) => Contract[] | Promise | null | undefined; + +function DLCItem(props: { dlc: Contract; refetch: RefetchDLCsType }) { + const [state, _] = useMegaStore(); + + const handleRejectDLC = async () => { + await state.mutiny_wallet?.reject_dlc_offer(props.dlc.id); + await props.refetch(); + }; + + const handleAcceptDLC = async () => { + await state.mutiny_wallet?.accept_dlc_offer(props.dlc.id); + }; + + const handleCloseDLC = async () => { + const userInput = prompt("Enter oracle sigs:"); + if (userInput != null) { + await state.mutiny_wallet?.close_dlc( + props.dlc.id, + userInput.trim() + ); + } + }; + + return ( + + +

+ {">"} {props.dlc.id} +

+
+ + +
+                        {JSON.stringify(props.dlc, null, 2)}
+                    
+ + + +
+
+
+ ); +} + +export function DLCsList() { + const [state, _] = useMegaStore(); + + const getDLCs = async () => { + return (await state.mutiny_wallet?.list_dlcs()) as Promise; + }; + + const [dlcs, { refetch }] = createResource(getDLCs); + + return ( + <> + + {/* By wrapping this in a suspense I don't cause the page to jump to the top */} + + + No DLCs found.} + > + {(dlc) => } + + + + + + + ); +} diff --git a/src/components/index.ts b/src/components/index.ts index 0323d923..26065451 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -15,6 +15,7 @@ export * from "./ContactForm"; export * from "./ContactViewer"; export * from "./DecryptDialog"; export * from "./DeleteEverything"; +export * from "./DLCList"; export * from "./ErrorDisplay"; export * from "./Fee"; export * from "./I18nProvider"; diff --git a/src/router.tsx b/src/router.tsx index 6111c5f4..7712e4f2 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -23,6 +23,7 @@ import { Channels, Connections, Currency, + DLC, EmergencyKit, Encrypt, Gift, @@ -109,6 +110,7 @@ export function Router() { + diff --git a/src/routes/settings/DLC.tsx b/src/routes/settings/DLC.tsx new file mode 100644 index 00000000..858e97c3 --- /dev/null +++ b/src/routes/settings/DLC.tsx @@ -0,0 +1,287 @@ +import { TextField } from "@kobalte/core"; +import { MutinyWallet, OracleAnnouncement } from "@mutinywallet/mutiny-wasm"; +import { createEffect, createSignal, For, Show } from "solid-js"; + +import { + AmountCard, + BackLink, + Button, + DefaultMain, + DLCsList, + InnerCard, + LargeHeader, + MiniStringShower, + MutinyWalletGuard, + NavBar, + SafeArea, + SimpleDialog +} from "~/components"; +import { useI18n } from "~/i18n/context"; +import { useMegaStore } from "~/state/megaStore"; + +type CreateDlcState = "start" | "amount" | "payouts" | "confirm"; + +type OutcomePayout = { + outcome: string; + payout: { + offer: number; + accept: number; + }; +}; + +function PayoutSlider( + disabled: boolean, + outcome: string, + payouts: OutcomePayout[], + max: number, + setSliderValue: (outcome: string, value: string) => void +) { + return ( + <> + + + + op.outcome == outcome)?.payout.offer || + 0 + } + min="0" + max={max} + list="tickmarks" + onChange={(e) => setSliderValue(outcome, e.currentTarget.value)} + /> + + ); +} + +export function DLC() { + const i18n = useI18n(); + const [state, _] = useMegaStore(); + + const [pubkey, setPubkey] = createSignal(""); + const [oracleAnn, setOracleAnn] = createSignal(""); + const [amountSats, setAmountSats] = createSignal(0); + const [payouts, setPayouts] = createSignal([]); + const [creationState, setCreationState] = + createSignal("start"); + const [decodedAnnouncement, setDecodedAnnouncement] = createSignal< + OracleAnnouncement | undefined + >(undefined); + + // Handle state changes + createEffect(() => { + if (creationState() == "start" && oracleAnn().length > 0) { + // todo handle errors + const decoded = MutinyWallet.decode_oracle_announcement( + oracleAnn() + ); + setDecodedAnnouncement(decoded); + setCreationState("amount"); + } else if (creationState() == "amount" && amountSats() > 0) { + const payouts: OutcomePayout[] = + decodedAnnouncement()!.outcomes.map((outcome) => ({ + outcome, + payout: { + offer: amountSats(), + accept: amountSats() + } + })); + setPayouts(payouts); + setCreationState("payouts"); + } + }); + + const onSubmit = async (e: SubmitEvent) => { + e.preventDefault(); + + const pk = pubkey().trim(); + const oracle = oracleAnn().trim(); + const size = amountSats(); + const outcomePayouts = payouts(); + + console.log("Sending DLC offer: " + JSON.stringify(outcomePayouts)); + + const id = await state.mutiny_wallet?.send_dlc_offer( + BigInt(size), + { outcomePayouts }, + oracle, + pk + ); + + // clear state + setPubkey(""); + setOracleAnn(""); + setAmountSats(0); + setPayouts([]); + setCreationState("start"); + setDecodedAnnouncement(undefined); + + console.log("DLC contract id: " + id || "none"); + }; + + const onSetPayouts = async (e: SubmitEvent) => { + e.preventDefault(); + console.log("Setting payouts"); + setCreationState("confirm"); + }; + + const setSliderValue = (outcome: string, value: string) => { + const outcomePayouts = payouts(); + const payout = outcomePayouts.find((op) => op.outcome == outcome); + if (payout) { + const index = outcomePayouts.indexOf(payout); + payout.payout.offer = Number(value); + // accept payout is amountsSats * 2 - offer, we multiply by 2 since both parties need to put up the same amount + payout.payout.accept = Number(amountSats()) * 2 - Number(value); + outcomePayouts[index] = payout; + console.log("Setting payouts: " + JSON.stringify(outcomePayouts)); + setPayouts(outcomePayouts); + } + }; + + return ( + + + + + DLC Testing + +
+ + Error parsing Oracle Announcement + + } + > + {(outcome) => + PayoutSlider( + false, + outcome, + payouts(), + Number(amountSats()) * 2, + setSliderValue + ) + } + + +
+
+ + Create DLC Offer +
+ + + Pubkey + + + + + + Oracle Announcement + + + + 0 || + creationState() == "amount" + } + > + + + 0 && + creationState() == "confirm" + } + > + + Error parsing Oracle Announcement + + } + > + {(outcome) => + PayoutSlider( + true, + outcome, + payouts(), + Number(amountSats()) * 2, + setSliderValue + ) + } + + + +
+
+ + + My DLC Key: + + +
+ +
+
+ ); +} diff --git a/src/routes/settings/Root.tsx b/src/routes/settings/Root.tsx index 54b62165..f9de5ea6 100644 --- a/src/routes/settings/Root.tsx +++ b/src/routes/settings/Root.tsx @@ -155,6 +155,10 @@ export function Settings() { { href: "/settings/syncnostrcontacts", text: "Sync Nostr Contacts" + }, + { + href: "/settings/dlc", + text: "DLC Testing" } ]} /> diff --git a/src/routes/settings/index.ts b/src/routes/settings/index.ts index 35c5ff23..2657f872 100644 --- a/src/routes/settings/index.ts +++ b/src/routes/settings/index.ts @@ -4,6 +4,7 @@ export * from "./Backup"; export * from "./Channels"; export * from "./Connections"; export * from "./Currency"; +export * from "./DLC"; export * from "./EmergencyKit"; export * from "./Encrypt"; export * from "./Gift";