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 }) => {
|
openExternal: async ({ url }) => {
|
||||||
Utils.openExternal(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 }) => {
|
fetchTxHistory: async ({ address, chainid }) => {
|
||||||
const key = await Bun.secrets.get({
|
const key = await Bun.secrets.get({
|
||||||
service: 'koins',
|
service: 'koins',
|
||||||
@@ -653,25 +661,15 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
daemonHeight,
|
daemonHeight,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
moneroGetTransactions: async () => {
|
moneroGetTransactions: async ({ accountIndex }) => {
|
||||||
console.log('[rpc] moneroGetTransactions')
|
console.log('[rpc] moneroGetTransactions', { accountIndex })
|
||||||
if (!moneroManager)
|
if (!moneroManager)
|
||||||
throw new Error('Monero wallet RPC not started')
|
throw new Error('Monero wallet RPC not started')
|
||||||
const txs = await moneroManager.getTransactions()
|
const txs = await moneroManager.getTransactions(accountIndex)
|
||||||
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,
|
|
||||||
}))
|
|
||||||
console.log(
|
console.log(
|
||||||
`[rpc] moneroGetTransactions: ${result.length} txs`,
|
`[rpc] moneroGetTransactions: ${txs.length} txs`,
|
||||||
)
|
)
|
||||||
return result
|
return txs
|
||||||
},
|
},
|
||||||
moneroWalletStatus: async () => {
|
moneroWalletStatus: async () => {
|
||||||
if (!moneroManager) {
|
if (!moneroManager) {
|
||||||
|
|||||||
@@ -274,45 +274,35 @@ export class MoneroWalletManager {
|
|||||||
return accounts?.map((a) => a.toJson()) ?? []
|
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')
|
if (!this.wallet) throw new Error('Wallet RPC not started')
|
||||||
const txs = (await this.wallet.getTxs()) as any[]
|
const params: Record<string, any> = {
|
||||||
console.log(`[monero] transactions: ${txs?.length ?? 0} returned`)
|
in: true,
|
||||||
if (txs?.length) {
|
out: true,
|
||||||
for (const tx of txs.slice(0, 5)) {
|
pending: false,
|
||||||
const isIn = tx.getIsIncoming()
|
pool: false,
|
||||||
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 result = (txs || []).map((tx: any) => {
|
if (accountIndex !== undefined) {
|
||||||
const isIn = tx.getIsIncoming()
|
params.account_index = accountIndex
|
||||||
const isOut = tx.getIsOutgoing()
|
}
|
||||||
const amt = isIn
|
const result = await this.rawRpc('get_transfers', params)
|
||||||
? (tx.getIncomingAmount() ?? 0n)
|
const all: any[] = []
|
||||||
: (tx.getOutgoingAmount() ?? 0n)
|
for (const dir of ['in', 'out'] as const) {
|
||||||
const block = tx.getBlock()
|
const transfers = result[dir] ?? []
|
||||||
const ts =
|
for (const t of transfers) {
|
||||||
block?.getTimestamp() ?? tx.getReceivedTimestamp() ?? 0
|
all.push({
|
||||||
return {
|
hash: t.txid,
|
||||||
hash: tx.getHash(),
|
amount: t.amount?.toString() ?? '0',
|
||||||
amount: amt?.toString() ?? '0',
|
timestamp: String(t.timestamp ?? 0),
|
||||||
timestamp: String(ts),
|
direction: dir === 'in' ? ('in' as const) : ('out' as const),
|
||||||
direction: isIn ? ('in' as const) : ('out' as const),
|
height: t.height ?? 0,
|
||||||
height: tx.getHeight() ?? 0,
|
note: t.note,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
result.sort(
|
all.sort((a: any, b: any) => Number(b.timestamp) - Number(a.timestamp))
|
||||||
(a: any, b: any) => Number(b.timestamp) - Number(a.timestamp),
|
console.log(`[monero] transactions: ${all.length} returned${accountIndex !== undefined ? ` for account ${accountIndex}` : ''}`)
|
||||||
)
|
return all
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getHeight(): Promise<number> {
|
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[] }
|
fetchTokenBalances: { params: { address: string; chainid: string }; response: TokenBalanceResult[] }
|
||||||
fetchTransactionDetails: { params: { hash: string; chainid: string; address?: string }; response: TransactionDetails | null }
|
fetchTransactionDetails: { params: { hash: string; chainid: string; address?: string }; response: TransactionDetails | null }
|
||||||
openExternal: { params: { url: string }; response: void }
|
openExternal: { params: { url: string }; response: void }
|
||||||
|
generateQrCode: { params: { text: string; size?: number }; response: string }
|
||||||
moneroBinaryStatus: { params: {}; response: MoneroBinaryStatus }
|
moneroBinaryStatus: { params: {}; response: MoneroBinaryStatus }
|
||||||
moneroDownloadBinary: { params: {}; response: void }
|
moneroDownloadBinary: { params: {}; response: void }
|
||||||
moneroStart: { params: { daemonAddress?: string }; response: MoneroWalletStatus }
|
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 } }
|
moneroRestoreWallet: { params: { name: string; password: string; mnemonic: string; restoreHeight?: number }; response: { address: string } }
|
||||||
moneroOpenWallet: { params: { name: string; password: string }; response: void }
|
moneroOpenWallet: { params: { name: string; password: string }; response: void }
|
||||||
moneroGetBalance: { params: {}; response: MoneroBalance }
|
moneroGetBalance: { params: {}; response: MoneroBalance }
|
||||||
moneroGetTransactions: { params: {}; response: MoneroTxEntry[] }
|
moneroGetTransactions: { params: { accountIndex?: number }; response: MoneroTxEntry[] }
|
||||||
moneroWalletStatus: { params: {}; response: MoneroWalletStatus }
|
moneroWalletStatus: { params: {}; response: MoneroWalletStatus }
|
||||||
moneroGetAccounts: { params: {}; response: MoneroAccountEntry[] }
|
moneroGetAccounts: { params: {}; response: MoneroAccountEntry[] }
|
||||||
moneroListWallets: { params: {}; response: string[] }
|
moneroListWallets: { params: {}; response: string[] }
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export const MoneroWallet = () => {
|
|||||||
let wallets = $state<string[]>([])
|
let wallets = $state<string[]>([])
|
||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
let error = $state('')
|
let error = $state('')
|
||||||
|
let selectedAccountIndex = $state(0)
|
||||||
|
|
||||||
const checkBiometric = async () => {
|
const checkBiometric = async () => {
|
||||||
if (!electrobun.rpc) return
|
if (!electrobun.rpc) return
|
||||||
@@ -218,7 +219,9 @@ export const MoneroWallet = () => {
|
|||||||
daemonHeight = bal.daemonHeight
|
daemonHeight = bal.daemonHeight
|
||||||
}
|
}
|
||||||
const [result] = await tryCatch(
|
const [result] = await tryCatch(
|
||||||
electrobun.rpc.request.moneroGetTransactions({}),
|
electrobun.rpc.request.moneroGetTransactions({
|
||||||
|
accountIndex: selectedAccountIndex,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
txs = result ?? []
|
txs = result ?? []
|
||||||
await fetchAccounts()
|
await fetchAccounts()
|
||||||
@@ -273,6 +276,8 @@ export const MoneroWallet = () => {
|
|||||||
set downloading(v: boolean) { downloading = v },
|
set downloading(v: boolean) { downloading = v },
|
||||||
set running(v: boolean) { running = v },
|
set running(v: boolean) { running = v },
|
||||||
set connected(v: boolean) { connected = v },
|
set connected(v: boolean) { connected = v },
|
||||||
|
get selectedAccountIndex() { return selectedAccountIndex },
|
||||||
|
set selectedAccountIndex(v: number) { selectedAccountIndex = v },
|
||||||
init,
|
init,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import Fingerprint from '@lucide/svelte/icons/fingerprint'
|
import Fingerprint from '@lucide/svelte/icons/fingerprint'
|
||||||
import SettingsIcon from '@lucide/svelte/icons/settings'
|
import SettingsIcon from '@lucide/svelte/icons/settings'
|
||||||
import Loader from '$lib/components/loader.svelte'
|
import Loader from '$lib/components/loader.svelte'
|
||||||
|
import QrCode from '$lib/components/qr.svelte'
|
||||||
import { navigate } from 'sv-router/generated'
|
import { navigate } from 'sv-router/generated'
|
||||||
|
|
||||||
const w = moneroWallet
|
const w = moneroWallet
|
||||||
@@ -194,8 +195,13 @@
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div class="mb-4 space-y-1">
|
<div class="mb-4 space-y-1">
|
||||||
<p class="font-medium text-xs">Address</p>
|
<div class="flex items-start gap-3">
|
||||||
<p class="font-mono text-xs break-all">{w.address}</p>
|
<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="mt-2 font-medium text-xs">Balance</p>
|
||||||
<p class="font-mono text-lg">{w.balance} XMR</p>
|
<p class="font-mono text-lg">{w.balance} XMR</p>
|
||||||
{#if w.unlockedAtomic !== w.balAtomic}
|
{#if w.unlockedAtomic !== w.balAtomic}
|
||||||
@@ -236,6 +242,19 @@
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Transactions</CardTitle>
|
<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>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{#if w.loading}
|
{#if w.loading}
|
||||||
@@ -299,15 +318,20 @@
|
|||||||
{#if account.subaddresses.length > 1}
|
{#if account.subaddresses.length > 1}
|
||||||
<div class="space-y-1 border-t border-border pt-2">
|
<div class="space-y-1 border-t border-border pt-2">
|
||||||
<p class="text-xs text-muted-foreground mb-1">Subaddresses</p>
|
<p class="text-xs text-muted-foreground mb-1">Subaddresses</p>
|
||||||
{#each account.subaddresses as sub}
|
{#each account.subaddresses as sub}
|
||||||
<div class="rounded-sm bg-background px-2 py-1.5 text-xs">
|
<div class="rounded-sm bg-background px-2 py-1.5 text-xs">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-start gap-2">
|
||||||
<p class="font-medium">#{sub.index}{sub.label ? ` - ${sub.label}` : ''}</p>
|
<QrCode text={sub.address} size={64} />
|
||||||
<p class="text-muted-foreground">{atomicToXmr(sub.balance)} XMR</p>
|
<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>
|
</div>
|
||||||
<p class="font-mono text-muted-foreground/50 truncate">{sub.address}</p>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
</div>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user