Skip to content
This repository was archived by the owner on Apr 21, 2025. It is now read-only.

Commit 9140efa

Browse files
futurepaulelnosh
andcommitted
Cashu redeem
Co-authored-by: elnosh <[email protected]>
1 parent e137e5e commit 9140efa

File tree

6 files changed

+185
-33
lines changed

6 files changed

+185
-33
lines changed

public/i18n/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@
9292
"redeem_bitcoin": "Redeem Bitcoin",
9393
"lnurl_amount_message": "Enter withdrawal amount between {{min}} and {{max}} sats",
9494
"lnurl_redeem_failed": "Withdrawal Failed",
95-
"lnurl_redeem_success": "Payment Received"
95+
"lnurl_redeem_success": "Payment Received",
96+
"cashu_already_spent": "That token has already been spent"
9697
},
9798
"request": {
9899
"request_bitcoin": "Request Bitcoin",

src/components/Activity.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TagItem } from "@mutinywallet/mutiny-wasm";
22
import { cache, createAsync, useNavigate } from "@solidjs/router";
3-
import { Plus, Save, Search, Shuffle, Users } from "lucide-solid";
3+
import { Nut, Plus, Save, Search, Shuffle, Users } from "lucide-solid";
44
import {
55
createEffect,
66
createMemo,
@@ -127,12 +127,21 @@ export function UnifiedActivityItem(props: {
127127
return filtered[0];
128128
};
129129

130-
const shouldShowShuffle = () => {
131-
return (
130+
const maybeIcon = () => {
131+
if (
132132
props.item.kind === "ChannelOpen" ||
133133
props.item.kind === "ChannelClose" ||
134134
(props.item.labels.length > 0 && props.item.labels[0] === "SWAP")
135-
);
135+
) {
136+
return <Shuffle />;
137+
}
138+
139+
if (
140+
props.item.labels.length > 0 &&
141+
props.item.labels[0] === "Cashu Token Melt"
142+
) {
143+
return <Nut />;
144+
}
136145
};
137146

138147
const verb = () => {
@@ -217,7 +226,7 @@ export function UnifiedActivityItem(props: {
217226
? primaryContact()?.image_url
218227
: profileFromNostr()?.primal_image_url || ""
219228
}
220-
icon={shouldShowShuffle() ? <Shuffle /> : undefined}
229+
icon={maybeIcon()}
221230
primaryOnClick={() =>
222231
primaryContact()?.id
223232
? navigate(`/chat/${primaryContact()?.id}`)

src/logic/waila.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type ParsedParams = {
1818
fedimint_invite?: string;
1919
is_lnurl_auth?: boolean;
2020
contact_id?: string;
21+
cashu_token?: string;
2122
};
2223

2324
export function toParsedParams(
@@ -63,7 +64,8 @@ export function toParsedParams(
6364
lightning_address: params.lightning_address,
6465
nostr_wallet_auth: params.nostr_wallet_auth,
6566
is_lnurl_auth: params.is_lnurl_auth,
66-
fedimint_invite: params.fedimint_invite_code
67+
fedimint_invite: params.fedimint_invite_code,
68+
cashu_token: params.cashu_token
6769
}
6870
};
6971
}

src/routes/Chat.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ function SingleMessage(props: {
127127
amount: result.value.amount_sats
128128
};
129129
}
130+
131+
if (result.value?.cashu_token) {
132+
return {
133+
type: "cashu",
134+
from: props.dm.from,
135+
value: result.value.cashu_token,
136+
amount: result.value.amount_sats
137+
};
138+
}
130139
},
131140
{
132141
initialValue: undefined
@@ -159,6 +168,16 @@ function SingleMessage(props: {
159168
);
160169
}
161170

171+
function handleRedeem() {
172+
actions.handleIncomingString(
173+
props.dm.message,
174+
(error) => {
175+
showToast(error);
176+
},
177+
payContact
178+
);
179+
}
180+
162181
return (
163182
<div
164183
id="message"
@@ -202,6 +221,34 @@ function SingleMessage(props: {
202221
<div />
203222
</div>
204223
</Match>
224+
<Match when={parsed()?.type === "cashu"}>
225+
<div class="flex flex-col gap-2">
226+
<div class="flex items-center gap-2">
227+
<Zap class="h-4 w-4" />
228+
<span>Cashu Token</span>
229+
</div>
230+
<AmountSats amountSats={parsed()?.amount} />
231+
<Show
232+
when={
233+
parsed()?.status !== "paid" &&
234+
parsed()?.from === props.counterPartyNpub
235+
}
236+
>
237+
<Button
238+
intent="blue"
239+
layout="xs"
240+
onClick={handleRedeem}
241+
>
242+
Redeem
243+
</Button>
244+
</Show>
245+
246+
<Show when={parsed()?.status === "paid"}>
247+
<p class="!mb-0 italic">Paid</p>
248+
</Show>
249+
<div />
250+
</div>
251+
</Match>
205252
<Match when={true}>
206253
<p class="!mb-0 !select-text break-words">
207254
{props.dm.message}

src/routes/Redeem.tsx

Lines changed: 114 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
BackLink,
1919
Button,
2020
DefaultMain,
21+
Failure,
2122
InfoBox,
2223
LargeHeader,
2324
LoadingShimmer,
@@ -33,7 +34,7 @@ import { useI18n } from "~/i18n/context";
3334
import { useMegaStore } from "~/state/megaStore";
3435
import { eify, vibrateSuccess } from "~/utils";
3536

36-
type RedeemState = "edit" | "paid";
37+
type RedeemState = "edit" | "paid" | "already_paid";
3738

3839
export function Redeem() {
3940
const [state, _actions] = useMegaStore();
@@ -66,14 +67,15 @@ export function Redeem() {
6667
setError("");
6768
}
6869

70+
//
71+
// Lnurl stuff
72+
//
6973
const [decodedLnurl] = createResource(async () => {
70-
if (state.scan_result) {
71-
if (state.scan_result.lnurl) {
72-
const decoded = await state.mutiny_wallet?.decode_lnurl(
73-
state.scan_result.lnurl
74-
);
75-
return decoded;
76-
}
74+
if (state.scan_result && state.scan_result.lnurl) {
75+
const decoded = await state.mutiny_wallet?.decode_lnurl(
76+
state.scan_result.lnurl
77+
);
78+
return decoded;
7779
}
7880
});
7981

@@ -108,7 +110,7 @@ export function Redeem() {
108110
}
109111
});
110112

111-
const canSend = createMemo(() => {
113+
const lnUrlCanSend = createMemo(() => {
112114
const lnurlParams = lnurlData();
113115
if (!lnurlParams) return false;
114116
const min = mSatsToSats(lnurlParams.min);
@@ -144,6 +146,52 @@ export function Redeem() {
144146
}
145147
}
146148

149+
//
150+
// Cashu stuff
151+
//
152+
const [decodedCashuToken] = createResource(async () => {
153+
if (state.scan_result && state.scan_result.cashu_token) {
154+
// If it's a cashu token we already have what we need
155+
const token = state.scan_result?.cashu_token;
156+
const amount = state.scan_result?.amount_sats;
157+
if (amount) {
158+
setAmount(amount);
159+
setFixedAmount(true);
160+
}
161+
162+
return token;
163+
}
164+
});
165+
166+
const cashuCanSend = createMemo(() => {
167+
if (!decodedCashuToken()) return false;
168+
if (amount() === 0n) return false;
169+
return true;
170+
});
171+
172+
async function meltCashuToken() {
173+
try {
174+
setError("");
175+
setLoading(true);
176+
if (!state.scan_result?.cashu_token) return;
177+
await state.mutiny_wallet?.melt_cashu_token(
178+
state.scan_result?.cashu_token
179+
);
180+
setRedeemState("paid");
181+
await vibrateSuccess();
182+
} catch (e) {
183+
console.error("melt_cashu_token failed", e);
184+
const err = eify(e);
185+
if (err.message === "Token has been already spent.") {
186+
setRedeemState("already_paid");
187+
} else {
188+
showToast(err);
189+
}
190+
} finally {
191+
setLoading(false);
192+
}
193+
}
194+
147195
return (
148196
<MutinyWalletGuard>
149197
<DefaultMain>
@@ -160,14 +208,24 @@ export function Redeem() {
160208
</div>
161209
}
162210
>
163-
<Show when={decodedLnurl() && lnurlData()}>
164-
<AmountEditable
165-
initialAmountSats={amount() || "0"}
166-
setAmountSats={setAmount}
167-
onSubmit={handleLnUrlWithdrawal}
168-
frozenAmount={fixedAmount()}
169-
/>
170-
</Show>
211+
<Switch>
212+
<Match when={decodedLnurl() && lnurlData()}>
213+
<AmountEditable
214+
initialAmountSats={amount() || "0"}
215+
setAmountSats={setAmount}
216+
onSubmit={handleLnUrlWithdrawal}
217+
frozenAmount={fixedAmount()}
218+
/>
219+
</Match>
220+
<Match when={decodedCashuToken()}>
221+
<AmountEditable
222+
initialAmountSats={amount() || "0"}
223+
setAmountSats={() => {}}
224+
onSubmit={() => {}}
225+
frozenAmount={fixedAmount()}
226+
/>
227+
</Match>
228+
</Switch>
171229
</Suspense>
172230
<ReceiveWarnings
173231
amountSats={amount() || "0"}
@@ -197,14 +255,28 @@ export function Redeem() {
197255
}
198256
/>
199257
</form> */}
200-
<Button
201-
disabled={!amount() || !canSend()}
202-
intent="green"
203-
onClick={handleLnUrlWithdrawal}
204-
loading={loading()}
205-
>
206-
{i18n.t("common.continue")}
207-
</Button>
258+
<Switch>
259+
<Match when={lnurlData()}>
260+
<Button
261+
disabled={!amount() || !lnUrlCanSend()}
262+
intent="green"
263+
onClick={handleLnUrlWithdrawal}
264+
loading={loading()}
265+
>
266+
{i18n.t("common.continue")}
267+
</Button>
268+
</Match>
269+
<Match when={decodedCashuToken()}>
270+
<Button
271+
disabled={!amount() || !cashuCanSend()}
272+
intent="green"
273+
onClick={meltCashuToken}
274+
loading={loading()}
275+
>
276+
{i18n.t("common.continue")}
277+
</Button>
278+
</Match>
279+
</Switch>
208280
</VStack>
209281
</Match>
210282
<Match when={redeemState() === "paid"}>
@@ -238,7 +310,23 @@ export function Redeem() {
238310
</div>
239311
{/* TODO: add payment details */}
240312
</SuccessModal>
241-
<pre>NICE</pre>
313+
</Match>
314+
<Match when={redeemState() === "already_paid"}>
315+
<SuccessModal
316+
open={true}
317+
setOpen={(open: boolean) => {
318+
if (!open) clearAll();
319+
}}
320+
onConfirm={() => {
321+
clearAll();
322+
navigate("/");
323+
}}
324+
confirmText={i18n.t("common.dangit")}
325+
>
326+
<Failure
327+
reason={i18n.t("redeem.cashu_already_spent")}
328+
/>
329+
</SuccessModal>
242330
</Match>
243331
</Switch>
244332
</DefaultMain>

src/state/megaStore.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,11 @@ export const Provider: ParentComponent = (props) => {
445445
encodeURIComponent(result.value?.nostr_wallet_auth)
446446
);
447447
}
448+
if (result.value?.cashu_token) {
449+
console.log("cashu_token", result.value?.cashu_token);
450+
actions.setScanResult(result.value);
451+
navigate("/redeem");
452+
}
448453
}
449454
},
450455
setTestFlightPromptDismissed() {

0 commit comments

Comments
 (0)