diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 100a448..2afe4d2 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -19,6 +19,8 @@ "@alchemy/common": "^5.0.0", "@alchemy/wallet-apis": "^5.0.0", "@koins/utils": "workspace:*", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", "drizzle-orm": "^1.0.0-beta.22", "electrobun": "^1.18.1", "mode-watcher": "^1.1.0", @@ -34,6 +36,8 @@ "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tailwindcss/vite": "^4.2.2", "@tanstack/table-core": "^8.21.3", + "@types/d3-scale": "^4.0.9", + "@types/d3-shape": "^3.1.8", "@types/env-paths": "^2.1.2", "@types/node": "^25.7.0", "@types/qrcode": "^1.5.6", diff --git a/apps/desktop/src/bun/rpc/monero/index.ts b/apps/desktop/src/bun/rpc/monero/index.ts index 2ea61a9..8fa1bfc 100644 --- a/apps/desktop/src/bun/rpc/monero/index.ts +++ b/apps/desktop/src/bun/rpc/monero/index.ts @@ -309,5 +309,20 @@ export function createMoneroHandlers(state: { return null } }, + fetchMoneroPrice: async () => { + try { + const res = await fetch( + 'https://api.coingecko.com/api/v3/simple/price?ids=monero&vs_currencies=usd', + { signal: AbortSignal.timeout(10000) }, + ) + if (!res.ok) return null + const data = await res.json() + if (!data?.monero?.usd) return null + return { usd: String(data.monero.usd) } + } catch (error) { + console.log('[monero][price] error:', error) + return null + } + }, } } diff --git a/apps/desktop/src/lib/rpc-schema.ts b/apps/desktop/src/lib/rpc-schema.ts index 6d2fa58..5d78e00 100644 --- a/apps/desktop/src/lib/rpc-schema.ts +++ b/apps/desktop/src/lib/rpc-schema.ts @@ -186,6 +186,7 @@ export type MoneroRpcRequests = { moneroCreateSubaddress: { params: { accountIndex: number; label?: string }; response: { index: number; address: string } } moneroGetTransferDetails: { params: { txid: string }; response: MoneroTransferDetails | null } moneroGetFeeEstimate: { params: {}; response: MoneroFeeEstimate | null } + fetchMoneroPrice: { params: {}; response: { usd: string } | null } } export type RPC = { diff --git a/apps/desktop/src/lib/states/monero-wallet.svelte.ts b/apps/desktop/src/lib/states/monero-wallet.svelte.ts index 828e551..054ff79 100644 --- a/apps/desktop/src/lib/states/monero-wallet.svelte.ts +++ b/apps/desktop/src/lib/states/monero-wallet.svelte.ts @@ -17,8 +17,8 @@ export const moneroNetwork = { export const atomicToXmr = (atomic: string): string => { const n = BigInt(atomic) - const whole = n / 10_000_000_000_000n - const frac = (n % 10_000_000_000_000n) + const whole = n / 1_000_000_000_000n + const frac = (n % 1_000_000_000_000n) .toString() .padStart(12, '0') .replace(/0+$/, '') diff --git a/apps/desktop/src/routes/monero/wallet/[name].svelte b/apps/desktop/src/routes/monero/wallet/[name].svelte index 11ccd4e..4c7b61e 100644 --- a/apps/desktop/src/routes/monero/wallet/[name].svelte +++ b/apps/desktop/src/routes/monero/wallet/[name].svelte @@ -30,6 +30,10 @@ import { navigate, route } from 'sv-router/generated' import { CopyButton } from '$lib/components/ui/copy-button' import { onMount } from 'svelte' + import { LineChart } from 'layerchart' + import { scaleUtc } from 'd3-scale' + import { curveLinear } from 'd3-shape' + import * as Chart from '$lib/components/ui/chart/index.js' const w = moneroWallet @@ -48,6 +52,31 @@ fees: string[] estimatedFee: string } | null>(null) + let moneroPrice = $state<{ usd: string } | null>(null) + + const balanceHistory = $derived.by(() => { + const sorted = [...w.txs].reverse() + if (sorted.length === 0) return [] + const points: { date: Date; balance: number }[] = [] + let cum = 0n + for (const tx of sorted) { + const amt = BigInt(tx.amount) + if (tx.direction === 'in') cum += amt + else cum -= amt + points.push({ + date: new Date(Number(tx.timestamp) * 1000), + balance: parseFloat(atomicToXmr(cum.toString())), + }) + } + points.push({ + date: new Date(), + balance: parseFloat(atomicToXmr(cum.toString())), + }) + return points + }) + + $inspect(balanceHistory) + let accountDialogOpen = $state(false) let accountLabel = $state('') let subaddressDialogOpen = $state(false) @@ -61,7 +90,12 @@ $effect(() => { if (w.walletOpen && !autoRefreshTimer) { - autoRefreshTimer = setInterval(() => w.refresh(), 30000) + autoRefreshTimer = setInterval(() => { + w.refresh() + electrobun.rpc?.request + .fetchMoneroPrice({}) + .then((p) => (moneroPrice = p)) + }, 30000) } else if (!w.walletOpen && autoRefreshTimer) { clearInterval(autoRefreshTimer) autoRefreshTimer = undefined @@ -87,6 +121,9 @@ navigate('/monero') } w.refresh() + electrobun.rpc?.request + .fetchMoneroPrice({}) + .then((p) => (moneroPrice = p)) }) @@ -163,7 +200,19 @@
Balance
-{w.balance} XMR
+{w.balance} XMR
+ {#if moneroPrice} ++ ≈ ${( + parseFloat(w.balance) * parseFloat(moneroPrice.usd) + ).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} +
+ {/if} +
Unlocked: {w.unlocked} XMR
@@ -301,6 +350,69 @@
+ {#if balanceHistory.length > 1}
+