diff --git a/apps/desktop/src/bun/index.ts b/apps/desktop/src/bun/index.ts index f95d11b..2f0cf97 100644 --- a/apps/desktop/src/bun/index.ts +++ b/apps/desktop/src/bun/index.ts @@ -395,6 +395,14 @@ const rpc = BrowserView.defineRPC({ openExternal: async ({ url }) => { Utils.openExternal(url) }, + generateQrCode: async ({ text, size }) => { + const { default: QRCode } = await import('qrcode') + return QRCode.toString(text, { + type: 'svg', + width: size ?? 128, + margin: 1, + }) + }, fetchTxHistory: async ({ address, chainid }) => { const key = await Bun.secrets.get({ service: 'koins', @@ -653,25 +661,15 @@ const rpc = BrowserView.defineRPC({ daemonHeight, } }, - moneroGetTransactions: async () => { - console.log('[rpc] moneroGetTransactions') + moneroGetTransactions: async ({ accountIndex }) => { + console.log('[rpc] moneroGetTransactions', { accountIndex }) if (!moneroManager) throw new Error('Monero wallet RPC not started') - const txs = await moneroManager.getTransactions() - const result = (txs || []).map((tx: any) => ({ - hash: tx.hash, - amount: tx.amount?.toString() ?? '0', - timestamp: String(tx.timestamp ?? 0), - direction: - tx.direction === 'in' - ? ('in' as const) - : ('out' as const), - height: tx.height ?? 0, - })) + const txs = await moneroManager.getTransactions(accountIndex) console.log( - `[rpc] moneroGetTransactions: ${result.length} txs`, + `[rpc] moneroGetTransactions: ${txs.length} txs`, ) - return result + return txs }, moneroWalletStatus: async () => { if (!moneroManager) { diff --git a/apps/desktop/src/bun/lib/monero/wallet.ts b/apps/desktop/src/bun/lib/monero/wallet.ts index 4bc2b6b..1bf716e 100644 --- a/apps/desktop/src/bun/lib/monero/wallet.ts +++ b/apps/desktop/src/bun/lib/monero/wallet.ts @@ -274,45 +274,35 @@ export class MoneroWalletManager { return accounts?.map((a) => a.toJson()) ?? [] } - async getTransactions(): Promise { + async getTransactions(accountIndex?: number): Promise { if (!this.wallet) throw new Error('Wallet RPC not started') - const txs = (await this.wallet.getTxs()) as any[] - console.log(`[monero] transactions: ${txs?.length ?? 0} returned`) - if (txs?.length) { - for (const tx of txs.slice(0, 5)) { - const isIn = tx.getIsIncoming() - const isOut = tx.getIsOutgoing() - const amt = isIn - ? (tx.getIncomingAmount() ?? 0n) - : (tx.getOutgoingAmount() ?? 0n) - console.log( - `[monero] tx: hash=${(tx.getHash() ?? '').substring(0, 12)} amount=${amt.toString()} dir=${isIn ? 'in' : 'out'} height=${tx.getHeight() ?? 0}`, - ) - } - if (txs.length > 5) - console.log(`[monero] ... and ${txs.length - 5} more`) + const params: Record = { + in: true, + out: true, + pending: false, + pool: false, } - const result = (txs || []).map((tx: any) => { - const isIn = tx.getIsIncoming() - const isOut = tx.getIsOutgoing() - const amt = isIn - ? (tx.getIncomingAmount() ?? 0n) - : (tx.getOutgoingAmount() ?? 0n) - const block = tx.getBlock() - const ts = - block?.getTimestamp() ?? tx.getReceivedTimestamp() ?? 0 - return { - hash: tx.getHash(), - amount: amt?.toString() ?? '0', - timestamp: String(ts), - direction: isIn ? ('in' as const) : ('out' as const), - height: tx.getHeight() ?? 0, + if (accountIndex !== undefined) { + params.account_index = accountIndex + } + const result = await this.rawRpc('get_transfers', params) + const all: any[] = [] + for (const dir of ['in', 'out'] as const) { + const transfers = result[dir] ?? [] + for (const t of transfers) { + all.push({ + hash: t.txid, + amount: t.amount?.toString() ?? '0', + timestamp: String(t.timestamp ?? 0), + direction: dir === 'in' ? ('in' as const) : ('out' as const), + height: t.height ?? 0, + note: t.note, + }) } - }) - result.sort( - (a: any, b: any) => Number(b.timestamp) - Number(a.timestamp), - ) - return result + } + all.sort((a: any, b: any) => Number(b.timestamp) - Number(a.timestamp)) + console.log(`[monero] transactions: ${all.length} returned${accountIndex !== undefined ? ` for account ${accountIndex}` : ''}`) + return all } async getHeight(): Promise { diff --git a/apps/desktop/src/lib/components/qr.svelte b/apps/desktop/src/lib/components/qr.svelte new file mode 100644 index 0000000..39e5e5a --- /dev/null +++ b/apps/desktop/src/lib/components/qr.svelte @@ -0,0 +1,21 @@ + + +{#if svgContent} + {@html svgContent} +{/if} diff --git a/apps/desktop/src/lib/rpc-schema.ts b/apps/desktop/src/lib/rpc-schema.ts index 23ef36b..83f8e33 100644 --- a/apps/desktop/src/lib/rpc-schema.ts +++ b/apps/desktop/src/lib/rpc-schema.ts @@ -123,6 +123,7 @@ export type RPC = { fetchTokenBalances: { params: { address: string; chainid: string }; response: TokenBalanceResult[] } fetchTransactionDetails: { params: { hash: string; chainid: string; address?: string }; response: TransactionDetails | null } openExternal: { params: { url: string }; response: void } + generateQrCode: { params: { text: string; size?: number }; response: string } moneroBinaryStatus: { params: {}; response: MoneroBinaryStatus } moneroDownloadBinary: { params: {}; response: void } moneroStart: { params: { daemonAddress?: string }; response: MoneroWalletStatus } @@ -131,7 +132,7 @@ export type RPC = { moneroRestoreWallet: { params: { name: string; password: string; mnemonic: string; restoreHeight?: number }; response: { address: string } } moneroOpenWallet: { params: { name: string; password: string }; response: void } moneroGetBalance: { params: {}; response: MoneroBalance } - moneroGetTransactions: { params: {}; response: MoneroTxEntry[] } + moneroGetTransactions: { params: { accountIndex?: number }; response: MoneroTxEntry[] } moneroWalletStatus: { params: {}; response: MoneroWalletStatus } moneroGetAccounts: { params: {}; response: MoneroAccountEntry[] } moneroListWallets: { params: {}; 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 c9ced62..bbc6067 100644 --- a/apps/desktop/src/lib/states/monero-wallet.svelte.ts +++ b/apps/desktop/src/lib/states/monero-wallet.svelte.ts @@ -39,6 +39,7 @@ export const MoneroWallet = () => { let wallets = $state([]) let loading = $state(false) let error = $state('') + let selectedAccountIndex = $state(0) const checkBiometric = async () => { if (!electrobun.rpc) return @@ -218,7 +219,9 @@ export const MoneroWallet = () => { daemonHeight = bal.daemonHeight } const [result] = await tryCatch( - electrobun.rpc.request.moneroGetTransactions({}), + electrobun.rpc.request.moneroGetTransactions({ + accountIndex: selectedAccountIndex, + }), ) txs = result ?? [] await fetchAccounts() @@ -273,6 +276,8 @@ export const MoneroWallet = () => { set downloading(v: boolean) { downloading = v }, set running(v: boolean) { running = v }, set connected(v: boolean) { connected = v }, + get selectedAccountIndex() { return selectedAccountIndex }, + set selectedAccountIndex(v: number) { selectedAccountIndex = v }, init, login, logout, diff --git a/apps/desktop/src/routes/monero/index.svelte b/apps/desktop/src/routes/monero/index.svelte index d13bb2d..46590a7 100644 --- a/apps/desktop/src/routes/monero/index.svelte +++ b/apps/desktop/src/routes/monero/index.svelte @@ -15,6 +15,7 @@ import Fingerprint from '@lucide/svelte/icons/fingerprint' import SettingsIcon from '@lucide/svelte/icons/settings' import Loader from '$lib/components/loader.svelte' + import QrCode from '$lib/components/qr.svelte' import { navigate } from 'sv-router/generated' const w = moneroWallet @@ -194,8 +195,13 @@
-

Address

-

{w.address}

+
+ +
+

Address

+

{w.address}

+
+

Balance

{w.balance} XMR

{#if w.unlockedAtomic !== w.balAtomic} @@ -236,6 +242,19 @@ Transactions + {#if w.accounts.length > 0} + +
+ {#each w.accounts as acct} + + {/each} +
+
+ {/if}
{#if w.loading} @@ -299,15 +318,20 @@ {#if account.subaddresses.length > 1}

Subaddresses

- {#each account.subaddresses as sub} -
-
-

#{sub.index}{sub.label ? ` - ${sub.label}` : ''}

-

{atomicToXmr(sub.balance)} XMR

+ {#each account.subaddresses as sub} +
+
+ +
+
+

#{sub.index}{sub.label ? ` - ${sub.label}` : ''}

+

{atomicToXmr(sub.balance)} XMR

+
+

{sub.address}

-

{sub.address}

- {/each} +
+ {/each}
{/if}