From 6390469274b8f37653834a517d36d90ddcfa82d6 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Thu, 11 Apr 2024 17:08:12 -0500 Subject: [PATCH 1/4] Cashu redeem Co-authored-by: elnosh --- public/i18n/en.json | 3 +- src/components/Activity.tsx | 19 +++-- src/logic/waila.ts | 4 +- src/routes/Chat.tsx | 47 +++++++++++++ src/routes/Redeem.tsx | 136 +++++++++++++++++++++++++++++------- src/state/megaStore.tsx | 5 ++ 6 files changed, 183 insertions(+), 31 deletions(-) diff --git a/public/i18n/en.json b/public/i18n/en.json index bb0d2611..8ae7be3a 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -92,7 +92,8 @@ "redeem_bitcoin": "Redeem Bitcoin", "lnurl_amount_message": "Enter withdrawal amount between {{min}} and {{max}} sats", "lnurl_redeem_failed": "Withdrawal Failed", - "lnurl_redeem_success": "Payment Received" + "lnurl_redeem_success": "Payment Received", + "cashu_already_spent": "That token has already been spent" }, "request": { "request_bitcoin": "Request Bitcoin", diff --git a/src/components/Activity.tsx b/src/components/Activity.tsx index 4163f768..a2fcbf04 100644 --- a/src/components/Activity.tsx +++ b/src/components/Activity.tsx @@ -1,6 +1,6 @@ import { TagItem } from "@mutinywallet/mutiny-wasm"; import { cache, createAsync, useNavigate } from "@solidjs/router"; -import { Plus, Save, Search, Shuffle, Users } from "lucide-solid"; +import { Nut, Plus, Save, Search, Shuffle, Users } from "lucide-solid"; import { createEffect, createMemo, @@ -157,12 +157,21 @@ export function UnifiedActivityItem(props: { return filtered[0]; }; - const shouldShowShuffle = () => { - return ( + const maybeIcon = () => { + if ( props.item.kind === "ChannelOpen" || props.item.kind === "ChannelClose" || (props.item.labels.length > 0 && props.item.labels[0] === "SWAP") - ); + ) { + return ; + } + + if ( + props.item.labels.length > 0 && + props.item.labels[0] === "Cashu Token Melt" + ) { + return ; + } }; const verb = () => { @@ -266,7 +275,7 @@ export function UnifiedActivityItem(props: { ? primaryContact()?.image_url : profileFromNostr()?.primal_image_url || "" } - icon={shouldShowShuffle() ? : undefined} + icon={maybeIcon()} primaryOnClick={handlePrimaryOnClick} amountOnClick={click} primaryName={ diff --git a/src/logic/waila.ts b/src/logic/waila.ts index 41ee03f9..e5c944ca 100644 --- a/src/logic/waila.ts +++ b/src/logic/waila.ts @@ -17,6 +17,7 @@ export type ParsedParams = { fedimint_invite?: string; is_lnurl_auth?: boolean; contact_id?: string; + cashu_token?: string; }; export async function toParsedParams( @@ -63,7 +64,8 @@ export async function toParsedParams( lightning_address: params.lightning_address, nostr_wallet_auth: params.nostr_wallet_auth, is_lnurl_auth: params.is_lnurl_auth, - fedimint_invite: params.fedimint_invite_code + fedimint_invite: params.fedimint_invite_code, + cashu_token: params.cashu_token } }; } diff --git a/src/routes/Chat.tsx b/src/routes/Chat.tsx index de8e2a58..3ccabef8 100644 --- a/src/routes/Chat.tsx +++ b/src/routes/Chat.tsx @@ -126,6 +126,15 @@ function SingleMessage(props: { amount: result.value.amount_sats }; } + + if (result.value?.cashu_token) { + return { + type: "cashu", + from: props.dm.from, + value: result.value.cashu_token, + amount: result.value.amount_sats + }; + } }, { initialValue: undefined @@ -159,6 +168,16 @@ function SingleMessage(props: { ); } + function handleRedeem() { + actions.handleIncomingString( + props.dm.message, + (error) => { + showToast(error); + }, + payContact + ); + } + return (
+ +
+
+ + Cashu Token +
+ + + + + + +

Paid

+
+
+
+

{props.dm.message} diff --git a/src/routes/Redeem.tsx b/src/routes/Redeem.tsx index d166499b..7d61b193 100644 --- a/src/routes/Redeem.tsx +++ b/src/routes/Redeem.tsx @@ -18,6 +18,7 @@ import { BackLink, Button, DefaultMain, + Failure, InfoBox, LargeHeader, LoadingShimmer, @@ -33,7 +34,7 @@ import { useI18n } from "~/i18n/context"; import { useMegaStore } from "~/state/megaStore"; import { eify, vibrateSuccess } from "~/utils"; -type RedeemState = "edit" | "paid"; +type RedeemState = "edit" | "paid" | "already_paid"; export function Redeem() { const [state, _actions, sw] = useMegaStore(); @@ -67,12 +68,13 @@ export function Redeem() { setError(""); } + // + // Lnurl stuff + // const [decodedLnurl] = createResource(async () => { - if (state.scan_result) { - if (state.scan_result.lnurl) { - const decoded = await sw.decode_lnurl(state.scan_result.lnurl); - return decoded; - } + if (state.scan_result && state.scan_result.lnurl) { + const decoded = await sw.decode_lnurl(state.scan_result.lnurl); + return decoded; } }); @@ -107,7 +109,7 @@ export function Redeem() { } }); - const canSend = createMemo(() => { + const lnUrlCanSend = createMemo(() => { const lnurlParams = lnurlData(); if (!lnurlParams) return false; const min = mSatsToSats(lnurlParams.min); @@ -140,6 +142,52 @@ export function Redeem() { } } + // + // Cashu stuff + // + const [decodedCashuToken] = createResource(async () => { + if (state.scan_result && state.scan_result.cashu_token) { + // If it's a cashu token we already have what we need + const token = state.scan_result?.cashu_token; + const amount = state.scan_result?.amount_sats; + if (amount) { + setAmount(amount); + setFixedAmount(true); + } + + return token; + } + }); + + const cashuCanSend = createMemo(() => { + if (!decodedCashuToken()) return false; + if (amount() === 0n) return false; + return true; + }); + + async function meltCashuToken() { + try { + setError(""); + setLoading(true); + if (!state.scan_result?.cashu_token) return; + await state.mutiny_wallet?.melt_cashu_token( + state.scan_result?.cashu_token + ); + setRedeemState("paid"); + await vibrateSuccess(); + } catch (e) { + console.error("melt_cashu_token failed", e); + const err = eify(e); + if (err.message === "Token has been already spent.") { + setRedeemState("already_paid"); + } else { + showToast(err); + } + } finally { + setLoading(false); + } + } + return ( @@ -156,14 +204,24 @@ export function Redeem() {

} > - - - + + + + + + {}} + onSubmit={() => {}} + frozenAmount={fixedAmount()} + /> + + */} - + + + + + + + +
@@ -234,7 +306,23 @@ export function Redeem() { {/* TODO: add payment details */} -
NICE
+
+ + { + if (!open) clearAll(); + }} + onConfirm={() => { + clearAll(); + navigate("/"); + }} + confirmText={i18n.t("common.dangit")} + > + + diff --git a/src/state/megaStore.tsx b/src/state/megaStore.tsx index 8bd49230..1e818ae1 100644 --- a/src/state/megaStore.tsx +++ b/src/state/megaStore.tsx @@ -453,6 +453,11 @@ export const makeMegaStoreContext = () => { encodeURIComponent(result.value?.nostr_wallet_auth) ); } + if (result.value?.cashu_token) { + console.log("cashu_token", result.value?.cashu_token); + actions.setScanResult(result.value); + navigate("/redeem"); + } } }, setTestFlightPromptDismissed() { From f8a5d1205c11b854574fbfde1f711a93ba69a5fe Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Fri, 12 Apr 2024 17:02:12 -0500 Subject: [PATCH 2/4] handle embedded in messages --- src/routes/Chat.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/routes/Chat.tsx b/src/routes/Chat.tsx index 3ccabef8..1aee76ee 100644 --- a/src/routes/Chat.tsx +++ b/src/routes/Chat.tsx @@ -130,6 +130,10 @@ function SingleMessage(props: { if (result.value?.cashu_token) { return { type: "cashu", + message_without_invoice: props.dm.message.replace( + result.value.original, + "" + ), from: props.dm.from, value: result.value.cashu_token, amount: result.value.amount_sats @@ -168,9 +172,9 @@ function SingleMessage(props: { ); } - function handleRedeem() { + function handleRedeem(token: string) { actions.handleIncomingString( - props.dm.message, + token, (error) => { showToast(error); }, @@ -223,6 +227,11 @@ function SingleMessage(props: {
+ +

+ {parsed()?.message_without_invoice} +

+
Cashu Token @@ -237,7 +246,9 @@ function SingleMessage(props: { From fca7ed8ee8819b179114fa50d6008d0dd76a7306 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Fri, 12 Apr 2024 18:18:07 -0500 Subject: [PATCH 3/4] escape interpolated dates --- public/i18n/en.json | 3 ++- src/components/GenericItem.tsx | 1 + src/utils/prettyPrintTime.ts | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/public/i18n/en.json b/public/i18n/en.json index 8ae7be3a..e24fef04 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -782,6 +782,7 @@ "minutes_short": "{{count}}m", "nowish": "Nowish", "seconds_future": "Seconds from now", - "seconds_past": "Just now" + "seconds_past": "Just now", + "weeks_short": "{{count}}w" } } diff --git a/src/components/GenericItem.tsx b/src/components/GenericItem.tsx index 4816a99b..ae231d8f 100644 --- a/src/components/GenericItem.tsx +++ b/src/components/GenericItem.tsx @@ -114,6 +114,7 @@ export function GenericItem(props: {
+ {/* the date might include slashes so we don't want to escape those */} {i18n.t("common.expires", { time: props.due, interpolation: { escapeValue: false } diff --git a/src/utils/prettyPrintTime.ts b/src/utils/prettyPrintTime.ts index 94c04450..8f2692a7 100644 --- a/src/utils/prettyPrintTime.ts +++ b/src/utils/prettyPrintTime.ts @@ -57,6 +57,7 @@ export function veryShortTimeStamp(ts?: number | bigint) { const elapsedMinutes = Math.floor(elapsedSeconds / 60); const elapsedHours = Math.floor(elapsedMinutes / 60); const elapsedDays = Math.floor(elapsedHours / 24); + const elapsedWeeks = Math.floor(elapsedDays / 7); if (elapsedSeconds < 60) { return i18n.t("utils.nowish"); @@ -66,6 +67,8 @@ export function veryShortTimeStamp(ts?: number | bigint) { return i18n.t("utils.hours_short", { count: elapsedHours }); } else if (elapsedDays < 7) { return i18n.t("utils.days_short", { count: elapsedDays }); + } else if (elapsedDays < 30) { + return i18n.t("utils.weeks_short", { count: elapsedWeeks }); } else { const date = new Date(timestamp); const day = String(date.getDate()).padStart(2, "0"); From 2a3b4625cb3891402339922e5f72c7ff39d5fd09 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Wed, 8 May 2024 19:52:52 -0500 Subject: [PATCH 4/4] update for web worker --- src/routes/Redeem.tsx | 4 +--- src/workers/walletWorker.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/routes/Redeem.tsx b/src/routes/Redeem.tsx index 7d61b193..71a3b025 100644 --- a/src/routes/Redeem.tsx +++ b/src/routes/Redeem.tsx @@ -170,9 +170,7 @@ export function Redeem() { setError(""); setLoading(true); if (!state.scan_result?.cashu_token) return; - await state.mutiny_wallet?.melt_cashu_token( - state.scan_result?.cashu_token - ); + await sw.melt_cashu_token(state.scan_result?.cashu_token); setRedeemState("paid"); await vibrateSuccess(); } catch (e) { diff --git a/src/workers/walletWorker.ts b/src/workers/walletWorker.ts index 8f5d54b5..a2d9d1f9 100644 --- a/src/workers/walletWorker.ts +++ b/src/workers/walletWorker.ts @@ -1565,6 +1565,15 @@ export async function estimate_sweep_federation_fee( return await wallet!.estimate_sweep_federation_fee(amount); } +/** + * Calls upon a Cash mint and melts the token from it. + * @param {string} maybe_token + * @returns {Promise} + */ +export async function melt_cashu_token(maybe_token: string): Promise { + return await wallet!.melt_cashu_token(maybe_token); +} + export async function parse_params(params: string): Promise { const paramsResult = await new PaymentParams(params); // PAIN just another object rebuild