Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cashu redeem #1038

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions public/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -781,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"
}
}
19 changes: 14 additions & 5 deletions src/components/Activity.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 <Shuffle />;
}

if (
props.item.labels.length > 0 &&
props.item.labels[0] === "Cashu Token Melt"
) {
return <Nut />;
}
};

const verb = () => {
Expand Down Expand Up @@ -266,7 +275,7 @@ export function UnifiedActivityItem(props: {
? primaryContact()?.image_url
: profileFromNostr()?.primal_image_url || ""
}
icon={shouldShowShuffle() ? <Shuffle /> : undefined}
icon={maybeIcon()}
primaryOnClick={handlePrimaryOnClick}
amountOnClick={click}
primaryName={
Expand Down
1 change: 1 addition & 0 deletions src/components/GenericItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export function GenericItem(props: {
<div class="flex w-full items-center gap-1 text-m-grey-400">
<Clock4 class="w-3" />
<span class="text-xs text-m-grey-400">
{/* the date might include slashes so we don't want to escape those */}
{i18n.t("common.expires", {
time: props.due,
interpolation: { escapeValue: false }
Expand Down
4 changes: 3 additions & 1 deletion src/logic/waila.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type ParsedParams = {
fedimint_invite?: string;
is_lnurl_auth?: boolean;
contact_id?: string;
cashu_token?: string;
};

export async function toParsedParams(
Expand Down Expand Up @@ -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
}
};
}
58 changes: 58 additions & 0 deletions src/routes/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,19 @@ function SingleMessage(props: {
amount: result.value.amount_sats
};
}

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
};
}
},
{
initialValue: undefined
Expand Down Expand Up @@ -159,6 +172,16 @@ function SingleMessage(props: {
);
}

function handleRedeem(token: string) {
actions.handleIncomingString(
token,
(error) => {
showToast(error);
},
payContact
);
}

return (
<div
id="message"
Expand Down Expand Up @@ -202,6 +225,41 @@ function SingleMessage(props: {
<div />
</div>
</Match>
<Match when={parsed()?.type === "cashu"}>
<div class="flex flex-col gap-2">
<Show when={parsed()?.message_without_invoice}>
<p class="!mb-0 break-words">
{parsed()?.message_without_invoice}
</p>
</Show>
<div class="flex items-center gap-2">
<Zap class="h-4 w-4" />
<span>Cashu Token</span>
</div>
<AmountSats amountSats={parsed()?.amount} />
<Show
when={
parsed()?.status !== "paid" &&
parsed()?.from === props.counterPartyNpub
}
>
<Button
intent="blue"
layout="xs"
onClick={() =>
handleRedeem(parsed()?.value || "")
}
>
Redeem
</Button>
</Show>

<Show when={parsed()?.status === "paid"}>
<p class="!mb-0 italic">Paid</p>
</Show>
<div />
</div>
</Match>
<Match when={true}>
<p class="!mb-0 !select-text break-words">
{props.dm.message}
Expand Down
134 changes: 110 additions & 24 deletions src/routes/Redeem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
BackLink,
Button,
DefaultMain,
Failure,
InfoBox,
LargeHeader,
LoadingShimmer,
Expand All @@ -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();
Expand Down Expand Up @@ -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;
}
});

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -140,6 +142,50 @@ 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 sw.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 (
<MutinyWalletGuard>
<DefaultMain>
Expand All @@ -156,14 +202,24 @@ export function Redeem() {
</div>
}
>
<Show when={decodedLnurl() && lnurlData()}>
<AmountEditable
initialAmountSats={amount() || "0"}
setAmountSats={setAmount}
onSubmit={handleLnUrlWithdrawal}
frozenAmount={fixedAmount()}
/>
</Show>
<Switch>
<Match when={decodedLnurl() && lnurlData()}>
<AmountEditable
initialAmountSats={amount() || "0"}
setAmountSats={setAmount}
onSubmit={handleLnUrlWithdrawal}
frozenAmount={fixedAmount()}
/>
</Match>
<Match when={decodedCashuToken()}>
<AmountEditable
initialAmountSats={amount() || "0"}
setAmountSats={() => {}}
onSubmit={() => {}}
frozenAmount={fixedAmount()}
/>
</Match>
</Switch>
</Suspense>
<ReceiveWarnings
amountSats={amount() || 0n}
Expand Down Expand Up @@ -193,14 +249,28 @@ export function Redeem() {
}
/>
</form> */}
<Button
disabled={!amount() || !canSend()}
intent="green"
onClick={handleLnUrlWithdrawal}
loading={loading()}
>
{i18n.t("common.continue")}
</Button>
<Switch>
<Match when={lnurlData()}>
<Button
disabled={!amount() || !lnUrlCanSend()}
intent="green"
onClick={handleLnUrlWithdrawal}
loading={loading()}
>
{i18n.t("common.continue")}
</Button>
</Match>
<Match when={decodedCashuToken()}>
<Button
disabled={!amount() || !cashuCanSend()}
intent="green"
onClick={meltCashuToken}
loading={loading()}
>
{i18n.t("common.continue")}
</Button>
</Match>
</Switch>
</VStack>
</Match>
<Match when={redeemState() === "paid"}>
Expand Down Expand Up @@ -234,7 +304,23 @@ export function Redeem() {
</div>
{/* TODO: add payment details */}
</SuccessModal>
<pre>NICE</pre>
</Match>
<Match when={redeemState() === "already_paid"}>
<SuccessModal
open={true}
setOpen={(open: boolean) => {
if (!open) clearAll();
}}
onConfirm={() => {
clearAll();
navigate("/");
}}
confirmText={i18n.t("common.dangit")}
>
<Failure
reason={i18n.t("redeem.cashu_already_spent")}
/>
</SuccessModal>
</Match>
</Switch>
</DefaultMain>
Expand Down
5 changes: 5 additions & 0 deletions src/state/megaStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
3 changes: 3 additions & 0 deletions src/utils/prettyPrintTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand Down
Loading
Loading