From cc32fdb20ffe7caee595d637f739752b0ef6933e Mon Sep 17 00:00:00 2001 From: TZGyn Date: Sun, 24 May 2026 19:43:32 +0800 Subject: [PATCH] Add Monero send page with transfer RPC and account selection --- apps/desktop/src/bun/index.ts | 17 ++ apps/desktop/src/bun/lib/monero/wallet.ts | 24 ++ apps/desktop/src/lib/electrobun.ts | 1 + apps/desktop/src/lib/rpc-schema.ts | 7 + .../src/lib/states/monero-wallet.svelte.ts | 20 +- apps/desktop/src/routes/monero/index.svelte | 3 + apps/desktop/src/routes/monero/send.svelte | 209 ++++++++++++++++++ 7 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/routes/monero/send.svelte diff --git a/apps/desktop/src/bun/index.ts b/apps/desktop/src/bun/index.ts index 1e7a963..f95d11b 100644 --- a/apps/desktop/src/bun/index.ts +++ b/apps/desktop/src/bun/index.ts @@ -712,6 +712,23 @@ const rpc = BrowserView.defineRPC({ ) return wallets }, + moneroTransfer: async ({ address, amount, priority, accountIndex }) => { + console.log('[rpc] moneroTransfer:', { + address, + amount, + priority, + }) + if (!moneroManager) + throw new Error('Monero wallet RPC not started') + const result = await moneroManager.transfer( + address, + BigInt(amount), + priority ?? 0, + accountIndex ?? 0, + ) + console.log('[rpc] moneroTransfer complete:', result.txHash) + return result + }, evmCreateWallet: async ({ name, phrase, passwordHash }) => { console.log('[rpc] evmCreateWallet:', name) const id = crypto.randomUUID() diff --git a/apps/desktop/src/bun/lib/monero/wallet.ts b/apps/desktop/src/bun/lib/monero/wallet.ts index b6734af..4bc2b6b 100644 --- a/apps/desktop/src/bun/lib/monero/wallet.ts +++ b/apps/desktop/src/bun/lib/monero/wallet.ts @@ -337,6 +337,30 @@ export class MoneroWalletManager { } } + async transfer( + address: string, + amount: bigint, + priority: number = 0, + accountIndex: number = 0, + ): Promise<{ txHash: string; fee: string; amount: string }> { + if (!this.wallet) throw new Error('Wallet RPC not started') + console.log(`[monero] transfer: ${address} amount=${amount.toString()} priority=${priority} account=${accountIndex}`) + const result = await this.rawRpc('transfer', { + destinations: [{ address, amount: amount.toString() }], + priority, + account_index: accountIndex, + get_tx_key: true, + get_tx_hex: false, + get_tx_metadata: false, + }) + console.log(`[monero] transfer result: tx_hash=${result.tx_hash} fee=${result.fee} amount=${result.amount}`) + return { + txHash: result.tx_hash, + fee: result.fee?.toString() ?? '0', + amount: result.amount?.toString() ?? amount.toString(), + } + } + async isWalletOpen(): Promise { if (!this.wallet) return false try { diff --git a/apps/desktop/src/lib/electrobun.ts b/apps/desktop/src/lib/electrobun.ts index be80181..def2c6d 100644 --- a/apps/desktop/src/lib/electrobun.ts +++ b/apps/desktop/src/lib/electrobun.ts @@ -7,6 +7,7 @@ export type { MoneroWalletStatus, MoneroSubaddressEntry, MoneroAccountEntry, + MoneroSendResult, TxEntry, TransactionDetails, TokenBalanceResult, diff --git a/apps/desktop/src/lib/rpc-schema.ts b/apps/desktop/src/lib/rpc-schema.ts index dce9685..23ef36b 100644 --- a/apps/desktop/src/lib/rpc-schema.ts +++ b/apps/desktop/src/lib/rpc-schema.ts @@ -40,6 +40,12 @@ export type MoneroSubaddressEntry = { numBlocksToUnlock: number } +export type MoneroSendResult = { + txHash: string + fee: string + amount: string +} + export type MoneroAccountEntry = { index: number primaryAddress: string @@ -129,6 +135,7 @@ export type RPC = { moneroWalletStatus: { params: {}; response: MoneroWalletStatus } moneroGetAccounts: { params: {}; response: MoneroAccountEntry[] } moneroListWallets: { params: {}; response: string[] } + moneroTransfer: { params: { address: string; amount: string; priority?: number; accountIndex?: number }; response: MoneroSendResult } evmCreateWallet: { params: { name: string; phrase: string; passwordHash?: string }; response: { id: string; name: string; createdAt: string } } evmListWallets: { params: {}; response: EvmWalletInfo[] } evmGetSeed: { params: { vaultKey: string }; response: string } diff --git a/apps/desktop/src/lib/states/monero-wallet.svelte.ts b/apps/desktop/src/lib/states/monero-wallet.svelte.ts index 9465dc8..c9ced62 100644 --- a/apps/desktop/src/lib/states/monero-wallet.svelte.ts +++ b/apps/desktop/src/lib/states/monero-wallet.svelte.ts @@ -1,4 +1,4 @@ -import { electrobun, type MoneroTxEntry, type MoneroAccountEntry } from '$lib/electrobun' + import { electrobun, type MoneroTxEntry, type MoneroAccountEntry, type MoneroSendResult } from '$lib/electrobun' import { tryCatch } from '@koins/utils' export const moneroNetwork = { @@ -229,6 +229,23 @@ export const MoneroWallet = () => { } } + const send = async ( + address: string, + amountAtomic: string, + priority?: number, + accountIndex?: number, + ): Promise => { + if (!electrobun.rpc) throw new Error('RPC not available') + const result = await electrobun.rpc.request.moneroTransfer({ + address, + amount: amountAtomic, + priority, + accountIndex, + }) + await refresh() + return result + } + return { get accountType() { return accountType }, get ready() { return ready }, @@ -273,6 +290,7 @@ export const MoneroWallet = () => { restoreWallet, openWallet, refresh, + send, } } diff --git a/apps/desktop/src/routes/monero/index.svelte b/apps/desktop/src/routes/monero/index.svelte index ddaeaa2..d13bb2d 100644 --- a/apps/desktop/src/routes/monero/index.svelte +++ b/apps/desktop/src/routes/monero/index.svelte @@ -219,6 +219,9 @@ {/if} Refresh + diff --git a/apps/desktop/src/routes/monero/send.svelte b/apps/desktop/src/routes/monero/send.svelte new file mode 100644 index 0000000..e378c30 --- /dev/null +++ b/apps/desktop/src/routes/monero/send.svelte @@ -0,0 +1,209 @@ + + +
+ + + + + Send XMR + + Balance: {atomicToXmr(unlockedAtomic)} XMR + {#if unlockedAtomic !== balanceAtomic} + (locked: {atomicToXmr( + ( + BigInt(balanceAtomic) - BigInt(unlockedAtomic) + ).toString(), + )} XMR) + {/if} + + + +
{ + e.preventDefault() + handleSend() + }} + class="space-y-4"> +
+ + +
+ +
+ + +
+ +
+ +
+ + + XMR + +
+ {#if exceedsBalance} +

+ Amount exceeds unlocked balance +

+ {/if} +

+ {new Intl.NumberFormat().format(Number(amountAtomic))} atomic + units +

+
+ +
+ + +
+ + +
+ + {#if sendError} +

{sendError}

+ {/if} + + {#if sentResult} +
+

+ Transaction Sent +

+

+ Hash: {sentResult.txHash} +

+

+ Fee: {atomicToXmr(sentResult.fee)} XMR +

+
+ {/if} +
+
+