diff --git a/package.json b/package.json index 3f9510c8..00e3f1cd 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,8 @@ "@kobalte/core": "^0.9.8", "@kobalte/tailwindcss": "^0.5.0", "@modular-forms/solid": "^0.18.1", - "@mutinywallet/mutiny-wasm": "0.5.9", - "@mutinywallet/waila-wasm": "^0.2.6", + "@mutinywallet/mutiny-wasm": "file:../mutiny-node/mutiny-wasm/pkg", + "@mutinywallet/waila-wasm": "file:../bitcoin-waila/waila-wasm/pkg", "@solid-primitives/upload": "^0.0.111", "@solidjs/meta": "^0.29.1", "@solidjs/router": "^0.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14598a6b..672ac253 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,11 +54,11 @@ importers: specifier: ^0.18.1 version: 0.18.1(solid-js@1.8.5) '@mutinywallet/mutiny-wasm': - specifier: 0.5.9 - version: 0.5.9 + specifier: file:../mutiny-node/mutiny-wasm/pkg + version: file:../mutiny-node/mutiny-wasm/pkg '@mutinywallet/waila-wasm': - specifier: ^0.2.6 - version: 0.2.6 + specifier: file:../bitcoin-waila/waila-wasm/pkg + version: file:../bitcoin-waila/waila-wasm/pkg '@solid-primitives/upload': specifier: ^0.0.111 version: 0.0.111(solid-js@1.8.5) @@ -2570,14 +2570,6 @@ packages: solid-js: 1.8.5 dev: false - /@mutinywallet/mutiny-wasm@0.5.9: - resolution: {integrity: sha512-skSSpMprn/iA6Zsk092S1UVCkgjaCfXZXdvzVahFLDDS/89GtxyHtSsY64Oy3KFCULB6X+UfFp9nRFHtWA7sIw==} - dev: false - - /@mutinywallet/waila-wasm@0.2.6: - resolution: {integrity: sha512-qiyhaWX/zDKuh23VXIzeWGlmXne9IdRIx+ldGdse15JwEiC97OjATXMECX3Xo0tc3RTOo50cKyMlouKKRbLYpQ==} - dev: false - /@ndelangen/get-tarball@3.0.9: resolution: {integrity: sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==} dependencies: @@ -13644,6 +13636,7 @@ packages: /workbox-google-analytics@7.0.0: resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} + deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained dependencies: workbox-background-sync: 7.0.0 workbox-core: 7.0.0 @@ -13934,3 +13927,13 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + file:../bitcoin-waila/waila-wasm/pkg: + resolution: {directory: ../bitcoin-waila/waila-wasm/pkg, type: directory} + name: '@mutinywallet/waila-wasm' + dev: false + + file:../mutiny-node/mutiny-wasm/pkg: + resolution: {directory: ../mutiny-node/mutiny-wasm/pkg, type: directory} + name: '@mutinywallet/mutiny-wasm' + dev: false diff --git a/src/components/AmountEditable.tsx b/src/components/AmountEditable.tsx index 2ea426d8..37514552 100644 --- a/src/components/AmountEditable.tsx +++ b/src/components/AmountEditable.tsx @@ -41,6 +41,7 @@ export const AmountEditable: ParentComponent<{ activeMethod?: MethodChoice; methods?: MethodChoice[]; setChosenMethod?: (method: MethodChoice) => void; + isFederation?: boolean; }> = (props) => { const [state, _actions] = useMegaStore(); const [mode, setMode] = createSignal<"fiat" | "sats">("sats"); diff --git a/src/i18n/en/translations.ts b/src/i18n/en/translations.ts index 3eacc0cd..9b097ef3 100644 --- a/src/i18n/en/translations.ts +++ b/src/i18n/en/translations.ts @@ -11,6 +11,7 @@ export default { fee: "Fee", send: "Send", receive: "Receive", + reissue: "Reissue", dangit: "Dangit", back: "Back", coming_soon: "(coming soon)", @@ -115,6 +116,69 @@ export default { remember_choice: "Remember my choice next time", what_for: "What's this for?" }, + reissue: { + reissue_bitcoin: "Receive Bitcoin", + reissue_ecash: "Reissue Ecash", + edit: "Edit", + checking: "Checking", + choose_format: "Choose format", + payment_received: "Payment Received", + payment_initiated: "Payment Initiated", + receive_add_the_sender: "Add the sender for your records", + keep_mutiny_open: "Keep Mutiny open to complete the payment.", + choose_payment_format: "Choose payment format", + unified_label: "Unified", + unified_caption: + "Combines a bitcoin address and a lightning invoice. Sender chooses payment method.", + lightning_label: "Lightning invoice", + lightning_caption: + "Ideal for small transactions. Usually lower fees than on-chain.", + onchain_label: "Bitcoin address", + onchain_caption: + "On-chain, just like Satoshi did it. Ideal for very large transactions.", + unified_setup_fee: + "A lightning setup fee of {{amount}} SATS will be charged if paid over lightning.", + lightning_setup_fee: + "A lightning setup fee of {{amount}} SATS will be charged for this reissue.", + amount: "Amount", + fee: "+ Fee", + total: "Total", + spendable: "Spendable", + channel_size: "Channel size", + channel_reserve: "- Channel reserve", + error_under_min_lightning: + "Defaulting to On-chain. Amount is too small for your initial Lightning reissue.", + error_creating_unified: + "Defaulting to On-chain. Something went wrong when creating the unified address", + error_creating_address: + "Something went wrong when creating the on-chain address", + amount_editable: { + receive_too_small: + "A lightning setup fee might be deducted from the requested amount.", + setup_fee_lightning: + "A lightning setup fee will be charged if paid over lightning.", + too_big_for_beta: + "That's a lot of sats. You do know Mutiny Wallet is still in beta, yeah?", + more_than_21m: "There are only 21 million bitcoin.", + set_amount: "Set amount", + max: "MAX", + fix_amounts: { + ten_k: "10k", + one_hundred_k: "100k", + one_million: "1m" + }, + del: "DEL", + balance: "Balance" + }, + integrated_qr: { + onchain: "On-chain", + lightning: "Lightning", + unified: "Unified", + gift: "Lightning Gift" + }, + remember_choice: "Remember my choice next time", + what_for: "What's this for?" + }, send: { search: { placeholder: "Name, address, invoice", diff --git a/src/logic/waila.ts b/src/logic/waila.ts index d2a29b72..1c61fe2b 100644 --- a/src/logic/waila.ts +++ b/src/logic/waila.ts @@ -19,6 +19,7 @@ export type ParsedParams = { lightning_address?: string; nostr_wallet_auth?: string; fedimint_invite?: string; + fedimint_oob_notes?: string; is_lnurl_auth?: boolean; contact_id?: string; }; @@ -40,8 +41,8 @@ export function toParsedParams( const network = !params.network ? ourNetwork : params.network === "testnet" && ourNetwork === "signet" - ? "signet" - : params.network; + ? "signet" + : params.network; if (network !== ourNetwork) { return { @@ -67,7 +68,8 @@ export 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, + fedimint_oob_notes: params.fedimint_oob_notes } }; } diff --git a/src/router.tsx b/src/router.tsx index bb8407c6..4615f1f3 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -12,6 +12,7 @@ import { Main, NotFound, Receive, + Reissue, Scanner, Search, Send, @@ -100,6 +101,7 @@ export function Router() { + diff --git a/src/routes/FederationReissue.tsx b/src/routes/FederationReissue.tsx new file mode 100644 index 00000000..de10001d --- /dev/null +++ b/src/routes/FederationReissue.tsx @@ -0,0 +1,539 @@ +/* @refresh reload */ + +import { + MutinyBip21RawMaterials, + MutinyInvoice +} from "@mutinywallet/mutiny-wasm"; +import { useNavigate } from "@solidjs/router"; +import { + createEffect, + createMemo, + createResource, + createSignal, + Match, + onCleanup, + Show, + Suspense, + Switch +} from "solid-js"; + +import side2side from "~/assets/icons/side-to-side.svg"; +import { + ActivityDetailsModal, + AmountEditable, + AmountFiat, + AmountSats, + BackButton, + BackLink, + Button, + Checkbox, + DefaultMain, + Fee, + FeesModal, + HackActivityType, + Indicator, + InfoBox, + IntegratedQr, + LargeHeader, + MegaCheck, + MutinyWalletGuard, + NavBar, + ReceiveWarnings, + showToast, + SimpleDialog, + SimpleInput, + StyledRadioGroup, + SuccessModal, + VStack +} from "~/components"; +import { useI18n } from "~/i18n/context"; +import { useMegaStore } from "~/state/megaStore"; +import { eify, objectToSearchParams, vibrateSuccess } from "~/utils"; + +type OnChainTx = { + transaction: { + version: number; + lock_time: number; + input: Array<{ + previous_output: string; + script_sig: string; + sequence: number; + witness: Array; + }>; + output: Array<{ + value: number; + script_pubkey: string; + }>; + }; + txid: string; + received: number; + sent: number; + confirmation_time: { + height: number; + timestamp: number; + }; +}; + +export type ReceiveFlavor = "unified" | "lightning" | "onchain"; +type ReceiveState = "edit" | "show" | "paid"; +type PaidState = "lightning_paid" | "onchain_paid"; + +function FeeWarning(props: { fee: bigint; flavor: ReceiveFlavor }) { + const i18n = useI18n(); + return ( + // TODO: probably won't always be fixed 2500? + 1000n}> + + + + {i18n.t("receive.unified_setup_fee", { + amount: props.fee.toLocaleString() + })} + + + + + + {i18n.t("receive.lightning_setup_fee", { + amount: props.fee.toLocaleString() + })} + + + + + + ); +} + +export function Receive() { + const [state, actions] = useMegaStore(); + const navigate = useNavigate(); + const i18n = useI18n(); + + const [amount, setAmount] = createSignal(0n); + const [whatForInput, setWhatForInput] = createSignal(""); + + const [receiveState, setReceiveState] = createSignal("edit"); + const [bip21Raw, setBip21Raw] = createSignal(); + const [unified, setUnified] = createSignal(""); + + const [lspFee, setLspFee] = createSignal(0n); + + // The data we get after a payment + const [paymentTx, setPaymentTx] = createSignal(); + const [paymentInvoice, setPaymentInvoice] = createSignal(); + + // The flavor of the receive (defaults to unified) + const [flavor, setFlavor] = createSignal( + state.preferredInvoiceType + ); + + // loading state for the continue button + const [loading, setLoading] = createSignal(false); + const [error, setError] = createSignal(""); + + // Details Modal + const [detailsOpen, setDetailsOpen] = createSignal(false); + const [detailsKind, setDetailsKind] = createSignal(); + const [detailsId, setDetailsId] = createSignal(""); + + const RECEIVE_FLAVORS = [ + { + value: "unified", + label: i18n.t("receive.unified_label"), + caption: i18n.t("receive.unified_caption") + }, + { + value: "lightning", + label: i18n.t("receive.lightning_label"), + caption: i18n.t("receive.lightning_caption") + }, + { + value: "onchain", + label: i18n.t("receive.onchain_label"), + caption: i18n.t("receive.onchain_caption") + } + ]; + + const [rememberChoice, setRememberChoice] = createSignal(false); + + const receiveString = createMemo(() => { + if (unified() && receiveState() === "show") { + if (flavor() === "unified") { + return unified(); + } else if (flavor() === "lightning") { + return bip21Raw()?.invoice ?? ""; + } else if (flavor() === "onchain") { + return bip21Raw()?.address ?? ""; + } + } + }); + + function clearAll() { + setAmount(0n); + setReceiveState("edit"); + setBip21Raw(undefined); + setUnified(""); + setPaymentTx(undefined); + setPaymentInvoice(undefined); + setError(""); + setFlavor(state.preferredInvoiceType); + } + + function openDetailsModal() { + const paymentTxId = + paidState() === "onchain_paid" + ? paymentTx() + ? paymentTx()?.txid + : undefined + : paymentInvoice() + ? paymentInvoice()?.payment_hash + : undefined; + const kind = paidState() === "onchain_paid" ? "OnChain" : "Lightning"; + + console.log("Opening details modal: ", paymentTxId, kind); + + if (!paymentTxId) { + console.warn("No id provided to openDetailsModal"); + return; + } + if (paymentTxId !== undefined) { + setDetailsId(paymentTxId); + } + setDetailsKind(kind); + setDetailsOpen(true); + } + + async function getUnifiedQr(amount: bigint) { + console.log("get unified amount", amount); + const bigAmount = BigInt(amount); + setLoading(true); + + // Both paths use tags so we'll do this once + let tags; + + try { + tags = whatForInput() ? [whatForInput().trim()] : []; + } catch (e) { + showToast(eify(e)); + console.error(e); + setLoading(false); + return; + } + + // Happy path + // First we try to get both an invoice and an address + try { + console.log("big amount", bigAmount); + const raw = await state.mutiny_wallet?.create_bip21( + bigAmount, + tags + ); + // Save the raw info so we can watch the address and invoice + setBip21Raw(raw); + + console.log("raw", raw); + + const params = objectToSearchParams({ + amount: raw?.btc_amount, + lightning: raw?.invoice + }); + + setLoading(false); + return `bitcoin:${raw?.address}?${params}`; + } catch (e) { + console.error(e); + if (e === "Satoshi amount is invalid") { + setError(i18n.t("receive.error_under_min_lightning")); + } else { + setError(i18n.t("receive.error_creating_unified")); + } + } + + // If we didn't return before this, that means create_bip21 failed + // So now we'll just try and get an address without the invoice + try { + const raw = await state.mutiny_wallet?.get_new_address(tags); + + // Save the raw info so we can watch the address + setBip21Raw(raw); + + setFlavor("onchain"); + + // We won't meddle with a "unified" QR here + return raw?.address; + } catch (e) { + // If THAT failed we're really screwed + showToast(eify(i18n.t("receive.error_creating_address"))); + console.error(e); + } finally { + setLoading(false); + } + } + + async function onSubmit(e: Event) { + e.preventDefault(); + + await getQr(); + } + + async function getQr() { + if (amount()) { + const unifiedQr = await getUnifiedQr(amount()); + + setUnified(unifiedQr || ""); + setReceiveState("show"); + } + } + + async function checkIfPaid( + bip21?: MutinyBip21RawMaterials + ): Promise { + if (bip21) { + console.debug("checking if paid..."); + const lightning = bip21.invoice; + const address = bip21.address; + + try { + // Lightning invoice might be blank + if (lightning) { + const invoice = + await state.mutiny_wallet?.get_invoice(lightning); + + // If the invoice has a fees amount that's probably the LSP fee + if (invoice?.fees_paid) { + setLspFee(invoice.fees_paid); + } + + if (invoice && invoice.paid) { + setReceiveState("paid"); + setPaymentInvoice(invoice); + await vibrateSuccess(); + return "lightning_paid"; + } + } + + const tx = (await state.mutiny_wallet?.check_address( + address + )) as OnChainTx | undefined; + + if (tx) { + setReceiveState("paid"); + setPaymentTx(tx); + await vibrateSuccess(); + return "onchain_paid"; + } + } catch (e) { + console.error(e); + } + } + } + + function selectFlavor(flavor: string) { + setFlavor(flavor as ReceiveFlavor); + if (rememberChoice()) { + actions.setPreferredInvoiceType(flavor as ReceiveFlavor); + } + setMethodChooserOpen(false); + } + + // const [paidState, { refetch }] = createResource(bip21Raw, checkIfPaid); + + const [notes, { refetch }] = createResourc)async () => { + try { + const notes = await + } + } + + createEffect(() => { + const interval = setInterval(() => { + if (receiveState() === "show") refetch(); + }, 1000); // Poll every second + onCleanup(() => { + clearInterval(interval); + }); + }); + + const [methodChooserOpen, setMethodChooserOpen] = createSignal(false); + + return ( + + + }> + clearAll()} + title={i18n.t("receive.edit")} + showOnDesktop + /> + + {i18n.t("receive.checking")} + ) + } + > + {i18n.t("receive.receive_bitcoin")} + + + + + + + + + + + + + + + + + + setWhatForInput(e.currentTarget.value) + } + /> + + + {i18n.t("common.continue")} + + + + + + + + {error()} + + + + + {i18n.t("receive.keep_mutiny_open")} + + {/* Only show method chooser when we have an invoice */} + + setMethodChooserOpen(true)} + > + {i18n.t("receive.choose_format")} + + + setMethodChooserOpen(open)} + > + + + + + + + { + if (!open) clearAll(); + }} + onConfirm={() => { + clearAll(); + navigate("/"); + }} + > + + + + + + {receiveState() === "paid" && + paidState() === "lightning_paid" + ? i18n.t("receive.payment_received") + : i18n.t("receive.payment_initiated")} + + + + + + + + + + + + + + {/*TODO: Confirmation time estimate still not possible needs to be implemented in mutiny-node first*/} + + + {i18n.t("common.view_payment_details")} + + + + + + + + + ); +} diff --git a/src/routes/Reissue.tsx b/src/routes/Reissue.tsx new file mode 100644 index 00000000..441043f2 --- /dev/null +++ b/src/routes/Reissue.tsx @@ -0,0 +1,295 @@ +/* @refresh reload */ + +import { + MutinyBip21RawMaterials, + MutinyInvoice +} from "@mutinywallet/mutiny-wasm"; +import { useNavigate } from "@solidjs/router"; +import { + createEffect, + createMemo, + createResource, + createSignal, + Match, + onCleanup, + Show, + Switch +} from "solid-js"; + +import side2side from "~/assets/icons/side-to-side.svg"; +import { + ActivityDetailsModal, + AmountEditable, + AmountFiat, + AmountSats, + BackButton, + BackLink, + Button, + Checkbox, + DefaultMain, + Fee, + FeesModal, + HackActivityType, + Indicator, + InfoBox, + IntegratedQr, + LargeHeader, + MegaCheck, + MutinyWalletGuard, + NavBar, + ReceiveWarnings, + showToast, + SimpleDialog, + SimpleInput, + StyledRadioGroup, + SuccessModal, + VStack +} from "~/components"; +import { useI18n } from "~/i18n/context"; +import { useMegaStore } from "~/state/megaStore"; +import { eify, objectToSearchParams, vibrateSuccess } from "~/utils"; + +type OnChainTx = { + transaction: { + version: number; + lock_time: number; + input: Array<{ + previous_output: string; + script_sig: string; + sequence: number; + witness: Array; + }>; + output: Array<{ + value: number; + script_pubkey: string; + }>; + }; + txid: string; + received: number; + sent: number; + confirmation_time: { + height: number; + timestamp: number; + }; +}; + +export type ReceiveFlavor = "unified" | "lightning" | "onchain"; +type ReissueState = "show" | "success" | "failed"; +type reissueState = "lightning_paid" | "onchain_paid"; + +function FeeWarning(props: { fee: bigint; flavor: ReceiveFlavor }) { + const i18n = useI18n(); + return ( + // TODO: probably won't always be fixed 2500? + 1000n}> + + + + {i18n.t("reissue.unified_setup_fee", { + amount: props.fee.toLocaleString() + })} + + + + + + {i18n.t("reissue.lightning_setup_fee", { + amount: props.fee.toLocaleString() + })} + + + + + + ); +} + +export function Reissue() { + const [state, actions] = useMegaStore(); + const navigate = useNavigate(); + const i18n = useI18n(); + + const [amount, setAmount] = createSignal(0n); + const [whatForInput, setWhatForInput] = createSignal(""); + + const [reissueState, setReissueState] = createSignal("show"); + const [bip21Raw, setBip21Raw] = createSignal(); + const [unified, setUnified] = createSignal(""); + + const [lspFee, setLspFee] = createSignal(0n); + + // The data we get after a payment + const [paymentTx, setPaymentTx] = createSignal(); + const [paymentInvoice, setPaymentInvoice] = createSignal(); + + // The flavor of the reissue (defaults to unified) + const [flavor, setFlavor] = createSignal( + state.preferredInvoiceType + ); + + // loading state for the continue button + const [loading, setLoading] = createSignal(false); + const [error, setError] = createSignal(""); + + // Details Modal + const [detailsOpen, setDetailsOpen] = createSignal(false); + const [detailsKind, setDetailsKind] = createSignal(); + const [detailsId, setDetailsId] = createSignal(""); + + function clearAll() { + setAmount(0n); + setReissueState("show"); + setBip21Raw(undefined); + setUnified(""); + setPaymentTx(undefined); + setPaymentInvoice(undefined); + setError(""); + setFlavor(state.preferredInvoiceType); + } + + async function onSubmit(e: Event) { + e.preventDefault(); + + await reissueEcash(); + } + + async function reissueEcash() { + const currState = reissueState(); + console.log(currState); + + setLoading(true); + try { + await state.mutiny_wallet?.reissue_oob_notes(state.scan_result?.fedimint_oob_notes!); + setReissueState("success"); + const currState = reissueState(); + console.log(currState); + } catch (e) { + // TODO: (@leonardo) how to handle the errors properly ? + setReissueState("failed"); + console.log(error); + } + setLoading(false); + + } + + // const [paidState, { refetch }] = createResource(bip21Raw, checkIfPaid); + + // createEffect(() => { + // const interval = setInterval(() => { + // if (receiveState() === "show") refetch(); + // }, 1000); // Poll every second + // onCleanup(() => { + // clearInterval(interval); + // }); + // }); + + return ( + + + }> + clearAll()} + title={i18n.t("reissue.edit")} + showOnDesktop + /> + + {i18n.t("reissue.checking")} + ) + } + > + {i18n.t("reissue.reissue_ecash")} + + + + + + { }}> + + {/* */} + + + + {/* TODO: (@leonardo) add the info input field ? */} + + {i18n.t("common.continue")} + + + + + { + if (!open) clearAll(); + }} + onConfirm={() => { + clearAll(); + navigate("/"); + }} + > + + + + + + {i18n.t("reissue.payment_received")} + + + + + + + + + + {/* TODO: (@leonardo) add the FederationId ? */} + + {/* + + */} + {/*TODO: Confirmation time estimate still not possible needs to be implemented in mutiny-node first*/} + {/* + + {i18n.t("common.view_payment_details")} + + */} + + + + + + + ); +} diff --git a/src/routes/index.ts b/src/routes/index.ts index a47f92d4..616107e4 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -4,6 +4,7 @@ export * from "./Feedback"; export * from "./Gift"; export * from "./Main"; export * from "./Receive"; +export * from "./Reissue"; export * from "./Scanner"; export * from "./Send"; export * from "./Swap"; diff --git a/src/state/megaStore.tsx b/src/state/megaStore.tsx index e4e8b9e7..eab01c89 100644 --- a/src/state/megaStore.tsx +++ b/src/state/megaStore.tsx @@ -394,10 +394,16 @@ export const Provider: ParentComponent = (props) => { if (result.value?.fedimint_invite) { navigate( "/settings/federations?fedimint_invite=" + - encodeURIComponent(result.value?.fedimint_invite) + encodeURIComponent(result.value?.fedimint_invite) ); actions.setScanResult(undefined); } + if (result.value?.fedimint_oob_notes) { + actions.setScanResult(result.value); + navigate( + "/reissue" + ); + } if (result.value?.nostr_wallet_auth) { console.log( "nostr_wallet_auth", @@ -405,7 +411,7 @@ export const Provider: ParentComponent = (props) => { ); navigate( "/settings/connections/?nwa=" + - encodeURIComponent(result.value?.nostr_wallet_auth) + encodeURIComponent(result.value?.nostr_wallet_auth) ); } }
{error()}
+ {i18n.t("receive.keep_mutiny_open")} +
+ {i18n.t("common.view_payment_details")} +