Add per-account transaction filtering and QR codes for Monero
This commit is contained in:
@@ -395,6 +395,14 @@ const rpc = BrowserView.defineRPC<RPC>({
|
||||
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<RPC>({
|
||||
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) {
|
||||
|
||||
@@ -274,45 +274,35 @@ export class MoneroWalletManager {
|
||||
return accounts?.map((a) => a.toJson()) ?? []
|
||||
}
|
||||
|
||||
async getTransactions(): Promise<any[]> {
|
||||
async getTransactions(accountIndex?: number): Promise<any[]> {
|
||||
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<string, any> = {
|
||||
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<number> {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { electrobun } from '$lib/electrobun.js'
|
||||
|
||||
let { text, size = 128 }: { text: string; size?: number } = $props()
|
||||
|
||||
let svgContent = $state('')
|
||||
|
||||
$effect(() => {
|
||||
if (!text || !electrobun.rpc) return
|
||||
svgContent = ''
|
||||
electrobun.rpc.request
|
||||
.generateQrCode({ text, size })
|
||||
.then((svg) => {
|
||||
svgContent = svg
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if svgContent}
|
||||
{@html svgContent}
|
||||
{/if}
|
||||
@@ -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[] }
|
||||
|
||||
@@ -39,6 +39,7 @@ export const MoneroWallet = () => {
|
||||
let wallets = $state<string[]>([])
|
||||
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,
|
||||
|
||||
@@ -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 @@
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="mb-4 space-y-1">
|
||||
<p class="font-medium text-xs">Address</p>
|
||||
<p class="font-mono text-xs break-all">{w.address}</p>
|
||||
<div class="flex items-start gap-3">
|
||||
<QrCode text={w.address} size={128} />
|
||||
<div class="min-w-0 flex-1 space-y-1">
|
||||
<p class="font-medium text-xs">Address</p>
|
||||
<p class="font-mono text-xs break-all">{w.address}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 font-medium text-xs">Balance</p>
|
||||
<p class="font-mono text-lg">{w.balance} XMR</p>
|
||||
{#if w.unlockedAtomic !== w.balAtomic}
|
||||
@@ -236,6 +242,19 @@
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Transactions</CardTitle>
|
||||
{#if w.accounts.length > 0}
|
||||
<CardDescription>
|
||||
<div class="flex flex-wrap gap-1.5 mt-1">
|
||||
{#each w.accounts as acct}
|
||||
<button
|
||||
onclick={() => { w.selectedAccountIndex = acct.index; w.refresh() }}
|
||||
class="rounded-md px-2 py-0.5 text-xs transition-colors {w.selectedAccountIndex === acct.index ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground hover:bg-muted/80'}">
|
||||
Account {acct.index}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</CardDescription>
|
||||
{/if}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#if w.loading}
|
||||
@@ -299,15 +318,20 @@
|
||||
{#if account.subaddresses.length > 1}
|
||||
<div class="space-y-1 border-t border-border pt-2">
|
||||
<p class="text-xs text-muted-foreground mb-1">Subaddresses</p>
|
||||
{#each account.subaddresses as sub}
|
||||
<div class="rounded-sm bg-background px-2 py-1.5 text-xs">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="font-medium">#{sub.index}{sub.label ? ` - ${sub.label}` : ''}</p>
|
||||
<p class="text-muted-foreground">{atomicToXmr(sub.balance)} XMR</p>
|
||||
{#each account.subaddresses as sub}
|
||||
<div class="rounded-sm bg-background px-2 py-1.5 text-xs">
|
||||
<div class="flex items-start gap-2">
|
||||
<QrCode text={sub.address} size={64} />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="font-medium">#{sub.index}{sub.label ? ` - ${sub.label}` : ''}</p>
|
||||
<p class="text-muted-foreground">{atomicToXmr(sub.balance)} XMR</p>
|
||||
</div>
|
||||
<p class="font-mono text-muted-foreground/50 truncate mt-0.5">{sub.address}</p>
|
||||
</div>
|
||||
<p class="font-mono text-muted-foreground/50 truncate">{sub.address}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user