diff --git a/apps/desktop/src/bun/lib/monero/index.ts b/apps/desktop/src/bun/lib/monero/index.ts index fe4ccae..b258625 100644 --- a/apps/desktop/src/bun/lib/monero/index.ts +++ b/apps/desktop/src/bun/lib/monero/index.ts @@ -17,6 +17,8 @@ export { getDaemonHeight, transfer, sweepAll, + createAccount, + createSubaddress, isWalletOpen, isConnected, listWallets, diff --git a/apps/desktop/src/bun/lib/monero/wallet.ts b/apps/desktop/src/bun/lib/monero/wallet.ts index f68be4f..29d8637 100644 --- a/apps/desktop/src/bun/lib/monero/wallet.ts +++ b/apps/desktop/src/bun/lib/monero/wallet.ts @@ -259,6 +259,32 @@ export async function getAddress( return address } +export async function createAccount( + state: MoneroWalletState, + label?: string, +): Promise<{ index: number; address: string }> { + if (!state.wallet) throw new Error('Wallet RPC not started') + const result = await rawRpc(state, 'create_account', { + label: label ?? '', + }) + console.log(`[monero] created account ${result.account_index}: ${result.address}`) + return { index: result.account_index, address: result.address } +} + +export async function createSubaddress( + state: MoneroWalletState, + accountIndex: number, + label?: string, +): Promise<{ index: number; address: string }> { + if (!state.wallet) throw new Error('Wallet RPC not started') + const result = await rawRpc(state, 'create_address', { + account_index: accountIndex, + label: label ?? '', + }) + console.log(`[monero] created subaddress ${result.address_index} for account ${accountIndex}: ${result.address}`) + return { index: result.address_index, address: result.address } +} + export async function getAccounts( state: MoneroWalletState, ): Promise { diff --git a/apps/desktop/src/bun/rpc/monero/index.ts b/apps/desktop/src/bun/rpc/monero/index.ts index fe43338..2ea61a9 100644 --- a/apps/desktop/src/bun/rpc/monero/index.ts +++ b/apps/desktop/src/bun/rpc/monero/index.ts @@ -18,6 +18,8 @@ import { getFeeEstimate as walletGetFeeEstimate, transfer as walletTransfer, sweepAll as walletSweepAll, + createAccount as walletCreateAccount, + createSubaddress as walletCreateSubaddress, isWalletOpen, isConnected, } from '../../lib/monero' @@ -256,6 +258,30 @@ export function createMoneroHandlers(state: { console.log('[rpc] moneroSweepAll complete:', result.txHash) return result }, + moneroCreateAccount: async ({ label }: { label?: string }) => { + console.log('[rpc] moneroCreateAccount:', { label }) + if (!state.manager) throw new Error('Monero wallet RPC not started') + const result = await walletCreateAccount(state.manager, label) + console.log('[rpc] moneroCreateAccount complete:', result) + return result + }, + moneroCreateSubaddress: async ({ + accountIndex, + label, + }: { + accountIndex: number + label?: string + }) => { + console.log('[rpc] moneroCreateSubaddress:', { accountIndex, label }) + if (!state.manager) throw new Error('Monero wallet RPC not started') + const result = await walletCreateSubaddress( + state.manager, + accountIndex, + label, + ) + console.log('[rpc] moneroCreateSubaddress complete:', result) + return result + }, moneroGetTransferDetails: async ({ txid }: { txid: string }) => { console.log('[rpc] moneroGetTransferDetails:', txid) if (!state.manager) diff --git a/apps/desktop/src/lib/rpc-schema.ts b/apps/desktop/src/lib/rpc-schema.ts index 8b56554..889ea3d 100644 --- a/apps/desktop/src/lib/rpc-schema.ts +++ b/apps/desktop/src/lib/rpc-schema.ts @@ -180,6 +180,8 @@ export type MoneroRpcRequests = { moneroListWallets: { params: {}; response: string[] } moneroTransfer: { params: { address: string; amount: string; priority?: number; accountIndex?: number }; response: MoneroSendResult } moneroSweepAll: { params: { address: string; priority?: number; accountIndex?: number }; response: MoneroSendResult } + moneroCreateAccount: { params: { label?: string }; response: { index: number; address: string } } + moneroCreateSubaddress: { params: { accountIndex: number; label?: string }; response: { index: number; address: string } } moneroGetTransferDetails: { params: { txid: string }; response: MoneroTransferDetails | null } moneroGetFeeEstimate: { params: {}; response: MoneroFeeEstimate | null } } diff --git a/apps/desktop/src/lib/states/monero-wallet.svelte.ts b/apps/desktop/src/lib/states/monero-wallet.svelte.ts index 004c14f..9928ffb 100644 --- a/apps/desktop/src/lib/states/monero-wallet.svelte.ts +++ b/apps/desktop/src/lib/states/monero-wallet.svelte.ts @@ -308,6 +308,23 @@ export const MoneroWallet = () => { return result } + const createAccount = async (label?: string) => { + if (!electrobun.rpc) throw new Error('RPC not available') + const result = await electrobun.rpc.request.moneroCreateAccount({ label }) + await refresh() + return result + } + + const createSubaddress = async (accountIndex: number, label?: string) => { + if (!electrobun.rpc) throw new Error('RPC not available') + const result = await electrobun.rpc.request.moneroCreateSubaddress({ + accountIndex, + label, + }) + await refresh() + return result + } + return { get accountType() { return accountType @@ -412,6 +429,8 @@ export const MoneroWallet = () => { refresh, send, sendAll, + createAccount, + createSubaddress, } } diff --git a/apps/desktop/src/routes/monero/index.svelte b/apps/desktop/src/routes/monero/index.svelte index d0b0348..6b02b4e 100644 --- a/apps/desktop/src/routes/monero/index.svelte +++ b/apps/desktop/src/routes/monero/index.svelte @@ -7,6 +7,13 @@ import { Button } from '$lib/components/ui/button/index.js' import { Textarea } from '$lib/components/ui/textarea/index.js' import { Input } from '$lib/components/ui/input/index.js' + import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + } from '$lib/components/ui/select/index.js' + import * as Dialog from '$lib/components/ui/dialog/index.js' import { Card, CardContent, @@ -31,6 +38,13 @@ } catch {} } + const chunkAddress = (addr: string) => { + const start = addr.slice(0, 6) + const mid = addr.slice(6, 12) + const end = addr.slice(-6) + return { start, mid, end } + } + let moneroWalletName = $state('') let moneroWalletPass = $state('') let moneroMnemonic = $state('') @@ -48,6 +62,12 @@ estimatedFee: string } | null>(null) + let accountDialogOpen = $state(false) + let accountLabel = $state('') + let subaddressDialogOpen = $state(false) + let subaddressAccountIndex = $state('') + let subaddressLabel = $state('') + $effect(() => { if (w.walletOpen && !autoRefreshTimer) { autoRefreshTimer = setInterval(() => w.refresh(), 30000) @@ -91,7 +111,7 @@ {:else if !w.accountType}

Go to welcome page - to get started + to get started

{:else if w.accountType === 'monero'} {#if w.downloading} @@ -283,6 +303,7 @@ {:else} + {@const a = chunkAddress(w.address)}
@@ -313,7 +334,14 @@

Address

- {w.address} + + {a.start} + + {a.mid} + ... + + {a.end} +

@@ -325,12 +353,19 @@

{/if} {#if feeEstimate} - {@const labels = ['Unimportant', 'Normal', 'Elevated', 'Priority']} + {@const labels = [ + 'Unimportant', + 'Normal', + 'Elevated', + 'Priority', + ]}
{#each feeEstimate.fees as f, i} {@const feeAtomic = (BigInt(f) * 2500n) / 1000n}

- {labels[i]}: ~{atomicToXmr(feeAtomic.toString())} XMR + {labels[i]}: ~{atomicToXmr( + feeAtomic.toString(), + )} XMR

{/each}
@@ -408,7 +443,12 @@
{#each w.txs as tx} + + + + Create Account + + Optional label for the new account + + +
+ +
+ + +
+
+
+ + + + + + + + Create Subaddress + + Account index and optional label + + +
+
+ + +
+ +
+ + +
+
+
+
+
{#each w.accounts as account} + {@const a = chunkAddress(account.primaryAddress)}

@@ -469,10 +623,16 @@

- {account.primaryAddress} + + {a.start} + + {a.mid} + ... + {a.end}

@@ -482,6 +642,7 @@ Subaddresses

{#each account.subaddresses as sub} + {@const a = chunkAddress(sub.address)}
@@ -498,12 +659,23 @@ {atomicToXmr(sub.balance)} XMR

-

- {sub.address} +

+ + {a.start} + + + {a.mid} + + + ... + + + {a.end} +

diff --git a/apps/desktop/src/routes/monero/send.svelte b/apps/desktop/src/routes/monero/send.svelte index 7ff1a83..d1c7813 100644 --- a/apps/desktop/src/routes/monero/send.svelte +++ b/apps/desktop/src/routes/monero/send.svelte @@ -56,7 +56,7 @@ amountAtomic > BigInt(unlockedAtomic), ) const canSend = $derived( - recipient.startsWith('4') && + (recipient.startsWith('4') || recipient.startsWith('8')) && recipient.length >= 95 && (sendAll || amountAtomic > 0n) && (sendAll || !exceedsBalance) &&