From 97488c83f7d68241f2868d81034c693bc8f9cf6f Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 15 Jul 2026 09:53:53 +0100 Subject: [PATCH] feat(wallet-ui): Ark send/receive tabs + Ark pay-with option on peer files Send modal gains an Ark tab (address/invoice/lightning-address via wallet.ark-send), Receive gains an Ark address + QR tab, and the peer file purchase confirm offers Ark alongside Cashu/Fedimint. Demo mock exposes ark_sats in wallet.ecash-balance and deducts the chosen rail on paid downloads. Co-Authored-By: Claude Fable 5 --- neode-ui/mock-backend.js | 14 +++++++++ .../src/components/ReceiveBitcoinModal.vue | 29 ++++++++++++++++--- neode-ui/src/components/SendBitcoinModal.vue | 27 +++++++++++++---- neode-ui/src/views/PeerFiles.vue | 27 +++++++++-------- 4 files changed, 75 insertions(+), 22 deletions(-) diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index a1d8a20f..b0882bf6 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -2196,6 +2196,15 @@ app.post('/rpc/v1', (req, res) => { case 'content.download-peer-paid': case 'content.download-peer-invoice': case 'content.download-peer-onchain': { + // Deduct the price from the chosen rail so demo balances react. + const paid = params?.price_sats || 0 + if (paid > 0 && method === 'content.download-peer-paid') { + if (params?.method === 'ark') walletState.ark_sats = Math.max(0, walletState.ark_sats - paid) + else if (params?.method === 'fedimint') { + const fed = (mockState.federations || [])[0] + if (fed) fed.balance_sats = Math.max(0, (fed.balance_sats || 0) - paid) + } else walletState.ecash_sats = Math.max(0, walletState.ecash_sats - paid) + } const filename = params?.filename || 'demo-content' const body = Buffer.from( `Archipelago demo — "${filename}"\n\nThis is sample paid content delivered over the ` + @@ -3330,10 +3339,15 @@ app.post('/rpc/v1', (req, res) => { // Wallet / Ecash (Fedimint) // ===================================================================== case 'wallet.ecash-balance': { + const fedSats = (mockState.federations || []).reduce((s, f) => s + (f.balance_sats || 0), 0) return res.json({ result: { balance_sats: walletState.ecash_sats, balance_msat: walletState.ecash_sats * 1000, + cashu_sats: walletState.ecash_sats, + fedimint_sats: fedSats, + ark_sats: walletState.ark_sats, + total_sats: walletState.ecash_sats + fedSats + walletState.ark_sats, token_count: walletState.ecash_tokens, federations: [ { federation_id: 'fed1-demo', name: 'Archy Signet Mint', balance_msat: walletState.ecash_sats * 1000, gateway_active: true }, diff --git a/neode-ui/src/components/ReceiveBitcoinModal.vue b/neode-ui/src/components/ReceiveBitcoinModal.vue index 68e6b4ed..a120a1c7 100644 --- a/neode-ui/src/components/ReceiveBitcoinModal.vue +++ b/neode-ui/src/components/ReceiveBitcoinModal.vue @@ -3,12 +3,12 @@
+ >{{ m === 'onchain' ? t('receiveBitcoin.onChain') : m === 'lightning' ? t('receiveBitcoin.lightning') : m === 'ecash' ? t('receiveBitcoin.ecash') : 'Ark' }}
@@ -43,6 +43,19 @@ + +
+
+ +

Your Ark address

+

{{ arkAddress }}

+ +
+
+

Generate a fresh Ark address to receive off-chain sats instantly.

+
+
+
@@ -57,7 +70,7 @@
@@ -75,15 +88,17 @@ const { t } = useI18n() defineProps<{ show: boolean }>() const emit = defineEmits<{ close: []; received: [] }>() -const receiveMethod = ref<'lightning' | 'onchain' | 'ecash'>('onchain') +const receiveMethod = ref<'lightning' | 'onchain' | 'ecash' | 'ark'>('onchain') const invoiceAmount = ref(0) const invoiceMemo = ref('') const invoiceResult = ref('') const onchainAddress = ref('') +const arkAddress = ref('') const ecashToken = ref('') const ecashResult = ref('') const onchainQrCanvas = ref(null) const lightningQrCanvas = ref(null) +const arkQrCanvas = ref(null) const processing = ref(false) const error = ref('') @@ -102,6 +117,7 @@ async function renderQr(data: string, canvas: HTMLCanvasElement | null, prefix = function close() { invoiceResult.value = '' onchainAddress.value = '' + arkAddress.value = '' ecashToken.value = '' ecashResult.value = '' error.value = '' @@ -131,6 +147,11 @@ async function receive() { } onchainAddress.value = res.address nextTick(() => renderQr(res.address, onchainQrCanvas.value, 'bitcoin:')) + } else if (receiveMethod.value === 'ark') { + const res = await rpcClient.call<{ address: string }>({ method: 'wallet.ark-address' }) + if (!res.address) throw new Error('barkd did not return an Ark address') + arkAddress.value = res.address + nextTick(() => renderQr(res.address, arkQrCanvas.value)) } else { if (!ecashToken.value.trim()) { error.value = t('receiveBitcoin.pasteAnEcashToken'); return } // The backend auto-detects the token type: a Cashu token (cashuA/B…) is diff --git a/neode-ui/src/components/SendBitcoinModal.vue b/neode-ui/src/components/SendBitcoinModal.vue index 10ead66d..ceb3ada7 100644 --- a/neode-ui/src/components/SendBitcoinModal.vue +++ b/neode-ui/src/components/SendBitcoinModal.vue @@ -3,12 +3,12 @@
+ >{{ m === 'onchain' ? t('sendBitcoin.onChain') : m === 'lightning' ? t('sendBitcoin.lightning') : m === 'ecash' ? t('sendBitcoin.ecash') : m === 'ark' ? 'Ark' : t('sendBitcoin.auto') }}
@@ -22,9 +22,9 @@
- +
@@ -39,6 +39,9 @@

{{ t('sendBitcoin.paidHash', { hash: resultHash }) }}

+
+

{{ resultArk }}

+
{{ error }}
@@ -62,13 +65,14 @@ const { t } = useI18n() const props = defineProps<{ show: boolean }>() const emit = defineEmits<{ close: []; sent: [] }>() -const sendMethod = ref<'auto' | 'lightning' | 'onchain' | 'ecash'>('auto') +const sendMethod = ref<'auto' | 'lightning' | 'onchain' | 'ecash' | 'ark'>('auto') const amount = ref(0) const dest = ref('') const processing = ref(false) const error = ref('') const resultTxid = ref('') const resultHash = ref('') +const resultArk = ref('') const ecashToken = ref('') const effectiveMethod = computed(() => { @@ -84,6 +88,7 @@ function close() { error.value = '' resultTxid.value = '' resultHash.value = '' + resultArk.value = '' ecashToken.value = '' emit('close') } @@ -99,10 +104,20 @@ async function send() { ecashToken.value = '' resultTxid.value = '' resultHash.value = '' + resultArk.value = '' const method = effectiveMethod.value try { - if (method === 'ecash') { + if (method === 'ark') { + if (!dest.value.trim()) { error.value = 'Enter an Ark address, invoice or lightning address'; return } + await rpcClient.call<{ sent: boolean }>({ + method: 'wallet.ark-send', + params: { destination: dest.value.trim(), amount_sats: amount.value }, + // Ark sends can wait on round participation. + timeout: 130000, + }) + resultArk.value = `Sent ${amount.value.toLocaleString()} sats via Ark` + } else if (method === 'ecash') { const res = await rpcClient.call<{ token: string }>({ method: 'wallet.ecash-send', params: { amount_sats: amount.value }, diff --git a/neode-ui/src/views/PeerFiles.vue b/neode-ui/src/views/PeerFiles.vue index 4f0ff5f9..fef576c6 100644 --- a/neode-ui/src/views/PeerFiles.vue +++ b/neode-ui/src/views/PeerFiles.vue @@ -441,16 +441,16 @@ switch to the other if it has enough balance. -->