fix tx amount/direction mapping, add date display, sort by latest
This commit is contained in:
@@ -1,4 +1,9 @@
|
|||||||
import { MoneroWalletRpc, MoneroRpcConnection, connectToWalletRpc, connectToDaemonRpc } from 'monero-ts'
|
import {
|
||||||
|
MoneroWalletRpc,
|
||||||
|
MoneroRpcConnection,
|
||||||
|
connectToWalletRpc,
|
||||||
|
connectToDaemonRpc,
|
||||||
|
} from 'monero-ts'
|
||||||
import { getBinaryPath, getWalletDir } from './binary'
|
import { getBinaryPath, getWalletDir } from './binary'
|
||||||
import { existsSync, readdirSync } from 'fs'
|
import { existsSync, readdirSync } from 'fs'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
@@ -24,9 +29,11 @@ export class MoneroWalletManager {
|
|||||||
if (!existsSync(dir)) return []
|
if (!existsSync(dir)) return []
|
||||||
const files = readdirSync(dir)
|
const files = readdirSync(dir)
|
||||||
const wallets = files
|
const wallets = files
|
||||||
.filter(f => f.endsWith('.keys'))
|
.filter((f) => f.endsWith('.keys'))
|
||||||
.map(f => f.replace(/\.keys$/, ''))
|
.map((f) => f.replace(/\.keys$/, ''))
|
||||||
console.log(`[monero] wallets on disk: ${wallets.join(', ') || 'none'}`)
|
console.log(
|
||||||
|
`[monero] wallets on disk: ${wallets.join(', ') || 'none'}`,
|
||||||
|
)
|
||||||
return wallets
|
return wallets
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +50,10 @@ export class MoneroWalletManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async start(daemonAddress: string = DEFAULT_DAEMON) {
|
async start(daemonAddress: string = DEFAULT_DAEMON) {
|
||||||
if (!existsSync(getBinaryPath())) throw new Error('monero-wallet-rpc not installed. Call downloadBinary() first.')
|
if (!existsSync(getBinaryPath()))
|
||||||
|
throw new Error(
|
||||||
|
'monero-wallet-rpc not installed. Call downloadBinary() first.',
|
||||||
|
)
|
||||||
this.daemonAddress = daemonAddress
|
this.daemonAddress = daemonAddress
|
||||||
|
|
||||||
console.log(`[monero] starting wallet-rpc process...`)
|
console.log(`[monero] starting wallet-rpc process...`)
|
||||||
@@ -52,11 +62,16 @@ export class MoneroWalletManager {
|
|||||||
|
|
||||||
const cmd = [
|
const cmd = [
|
||||||
getBinaryPath(),
|
getBinaryPath(),
|
||||||
'--daemon-address', this.daemonAddress,
|
'--daemon-address',
|
||||||
'--rpc-bind-port', String(this.rpcPort),
|
this.daemonAddress,
|
||||||
'--rpc-bind-ip', '127.0.0.1',
|
'--rpc-bind-port',
|
||||||
'--wallet-dir', getWalletDir(),
|
String(this.rpcPort),
|
||||||
'--rpc-login', `${this.rpcUser}:${this.rpcPassword}`,
|
'--rpc-bind-ip',
|
||||||
|
'127.0.0.1',
|
||||||
|
'--wallet-dir',
|
||||||
|
getWalletDir(),
|
||||||
|
'--rpc-login',
|
||||||
|
`${this.rpcUser}:${this.rpcPassword}`,
|
||||||
'--trusted-daemon',
|
'--trusted-daemon',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -66,7 +81,10 @@ export class MoneroWalletManager {
|
|||||||
stdout: 'pipe',
|
stdout: 'pipe',
|
||||||
stderr: 'pipe',
|
stderr: 'pipe',
|
||||||
onExit(_proc, exitCode, _signalCode, error) {
|
onExit(_proc, exitCode, _signalCode, error) {
|
||||||
console.log(`[monero] wallet-rpc process exited with code ${exitCode}`, error?.message ?? '')
|
console.log(
|
||||||
|
`[monero] wallet-rpc process exited with code ${exitCode}`,
|
||||||
|
error?.message ?? '',
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
console.log(`[monero] wallet-rpc pid: ${this.process.pid}`)
|
console.log(`[monero] wallet-rpc pid: ${this.process.pid}`)
|
||||||
@@ -76,7 +94,9 @@ export class MoneroWalletManager {
|
|||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await stdout.read()
|
const { done, value } = await stdout.read()
|
||||||
if (done) break
|
if (done) break
|
||||||
console.log(`[monero:stdout] ${new TextDecoder().decode(value)}`)
|
console.log(
|
||||||
|
`[monero:stdout] ${new TextDecoder().decode(value)}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
@@ -85,23 +105,35 @@ export class MoneroWalletManager {
|
|||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await stderr.read()
|
const { done, value } = await stderr.read()
|
||||||
if (done) break
|
if (done) break
|
||||||
console.log(`[monero:stderr] ${new TextDecoder().decode(value)}`)
|
console.log(
|
||||||
|
`[monero:stderr] ${new TextDecoder().decode(value)}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
console.log(`[monero] waiting for RPC server to become available...`)
|
console.log(
|
||||||
|
`[monero] waiting for RPC server to become available...`,
|
||||||
|
)
|
||||||
await this.waitForRpc()
|
await this.waitForRpc()
|
||||||
console.log(`[monero] RPC server is ready`)
|
console.log(`[monero] RPC server is ready`)
|
||||||
|
|
||||||
console.log(`[monero] connecting MoneroWalletRpc client...`)
|
console.log(`[monero] connecting MoneroWalletRpc client...`)
|
||||||
this.wallet = await connectToWalletRpc(this.rpcUrl, this.rpcUser, this.rpcPassword) as MoneroWalletRpc
|
this.wallet = (await connectToWalletRpc(
|
||||||
|
this.rpcUrl,
|
||||||
|
this.rpcUser,
|
||||||
|
this.rpcPassword,
|
||||||
|
)) as MoneroWalletRpc
|
||||||
console.log(`[monero] MoneroWalletRpc connected successfully`)
|
console.log(`[monero] MoneroWalletRpc connected successfully`)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async rawRpc(method: string, params: Record<string, any> = {}): Promise<any> {
|
private async rawRpc(
|
||||||
|
method: string,
|
||||||
|
params: Record<string, any> = {},
|
||||||
|
): Promise<any> {
|
||||||
const conn = this.rpcConnection
|
const conn = this.rpcConnection
|
||||||
const res = await conn.sendJsonRequest(method, params)
|
const res = await conn.sendJsonRequest(method, params)
|
||||||
if (res?.error) throw new Error(res.error.message || JSON.stringify(res.error))
|
if (res?.error)
|
||||||
|
throw new Error(res.error.message || JSON.stringify(res.error))
|
||||||
return res?.result
|
return res?.result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,22 +145,35 @@ export class MoneroWalletManager {
|
|||||||
await Bun.connect({
|
await Bun.connect({
|
||||||
hostname: '127.0.0.1',
|
hostname: '127.0.0.1',
|
||||||
port: this.rpcPort,
|
port: this.rpcPort,
|
||||||
socket: { open(s) { s.end() }, close() {}, data() {}, error() {} },
|
socket: {
|
||||||
|
open(s) {
|
||||||
|
s.end()
|
||||||
|
},
|
||||||
|
close() {},
|
||||||
|
data() {},
|
||||||
|
error() {},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
console.log(`[monero] RPC ready after ${Date.now() - start}ms`)
|
console.log(
|
||||||
|
`[monero] RPC ready after ${Date.now() - start}ms`,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
lastErr = String(e)
|
lastErr = String(e)
|
||||||
}
|
}
|
||||||
await new Promise(r => setTimeout(r, 500))
|
await new Promise((r) => setTimeout(r, 500))
|
||||||
}
|
}
|
||||||
throw new Error(`monero-wallet-rpc failed to start within ${timeoutMs}ms: ${lastErr}`)
|
throw new Error(
|
||||||
|
`monero-wallet-rpc failed to start within ${timeoutMs}ms: ${lastErr}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
console.log(`[monero] stopping wallet manager...`)
|
console.log(`[monero] stopping wallet manager...`)
|
||||||
if (this.process) {
|
if (this.process) {
|
||||||
console.log(`[monero] killing wallet-rpc process (pid ${this.process.pid})`)
|
console.log(
|
||||||
|
`[monero] killing wallet-rpc process (pid ${this.process.pid})`,
|
||||||
|
)
|
||||||
this.process.kill()
|
this.process.kill()
|
||||||
this.process = null
|
this.process = null
|
||||||
}
|
}
|
||||||
@@ -149,8 +194,15 @@ export class MoneroWalletManager {
|
|||||||
return { mnemonic: result.mnemonic, address }
|
return { mnemonic: result.mnemonic, address }
|
||||||
}
|
}
|
||||||
|
|
||||||
async restoreWallet(name: string, password: string, mnemonic: string, restoreHeight?: number) {
|
async restoreWallet(
|
||||||
console.log(`[monero] restoring wallet: ${name} (height: ${restoreHeight ?? 0})`)
|
name: string,
|
||||||
|
password: string,
|
||||||
|
mnemonic: string,
|
||||||
|
restoreHeight?: number,
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`[monero] restoring wallet: ${name} (height: ${restoreHeight ?? 0})`,
|
||||||
|
)
|
||||||
await this.rawRpc('restore_deterministic_wallet', {
|
await this.rawRpc('restore_deterministic_wallet', {
|
||||||
filename: name,
|
filename: name,
|
||||||
password,
|
password,
|
||||||
@@ -177,55 +229,95 @@ export class MoneroWalletManager {
|
|||||||
|
|
||||||
async getBalance(): Promise<{ balance: bigint; unlocked: bigint }> {
|
async getBalance(): Promise<{ balance: bigint; unlocked: bigint }> {
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
if (!this.wallet) throw new Error('Wallet RPC not started')
|
||||||
const bal = await this.wallet.getBalance() as bigint
|
const bal = (await this.wallet.getBalance()) as bigint
|
||||||
const unlocked = await this.wallet.getUnlockedBalance() as bigint
|
const unlocked =
|
||||||
|
(await this.wallet.getUnlockedBalance()) as bigint
|
||||||
console.log(`[monero] balance: ${bal} (unlocked: ${unlocked})`)
|
console.log(`[monero] balance: ${bal} (unlocked: ${unlocked})`)
|
||||||
return { balance: bal, unlocked }
|
return { balance: bal, unlocked }
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAddress(accountIdx = 0, subIdx = 0): Promise<string> {
|
async getAddress(accountIdx = 0, subIdx = 0): Promise<string> {
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
if (!this.wallet) throw new Error('Wallet RPC not started')
|
||||||
const address = await this.wallet.getAddress(accountIdx, subIdx) as string
|
const address = (await this.wallet.getAddress(
|
||||||
|
accountIdx,
|
||||||
|
subIdx,
|
||||||
|
)) as string
|
||||||
console.log(`[monero] address: ${address}`)
|
console.log(`[monero] address: ${address}`)
|
||||||
return address
|
return address
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAccounts(): Promise<any[]> {
|
async getAccounts(): Promise<any[]> {
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
if (!this.wallet) throw new Error('Wallet RPC not started')
|
||||||
const accounts = await this.wallet.getAccounts(true) as any[]
|
const accounts = (await this.wallet.getAccounts(true)) as any[]
|
||||||
console.log(`[monero] accounts: ${accounts?.length ?? 0} returned`)
|
console.log(
|
||||||
|
`[monero] accounts: ${accounts?.length ?? 0} returned`,
|
||||||
|
)
|
||||||
if (accounts?.length) {
|
if (accounts?.length) {
|
||||||
for (const acct of accounts) {
|
for (const acct of accounts) {
|
||||||
const subs = acct.getSubaddresses() ?? []
|
const subs = acct.getSubaddresses() ?? []
|
||||||
console.log(`[monero] account ${acct.getIndex()}: balance=${acct.getBalance()?.toString() ?? '0'} subs=${subs.length} primary=${(acct.getPrimaryAddress() ?? '').substring(0, 16)}...`)
|
console.log(
|
||||||
|
`[monero] account ${acct.getIndex()}: balance=${acct.getBalance()?.toString() ?? '0'} subs=${subs.length} primary=${(acct.getPrimaryAddress() ?? '').substring(0, 16)}...`,
|
||||||
|
)
|
||||||
const has0 = subs.some((s: any) => s.getIndex() === 0)
|
const has0 = subs.some((s: any) => s.getIndex() === 0)
|
||||||
if (!has0) {
|
if (!has0) {
|
||||||
console.log(`[monero] sub 0 (primary): addr=${(acct.getPrimaryAddress() ?? '').substring(0, 16)}...`)
|
console.log(
|
||||||
|
`[monero] sub 0 (primary): addr=${(acct.getPrimaryAddress() ?? '').substring(0, 16)}...`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
for (const sub of subs) {
|
for (const sub of subs) {
|
||||||
console.log(`[monero] sub ${sub.getIndex()}: addr=${(sub.getAddress() ?? '').substring(0, 16)}... label=${sub.getLabel() ?? ''} balance=${sub.getBalance()?.toString() ?? '0'} used=${sub.getIsUsed()}`)
|
console.log(
|
||||||
|
`[monero] sub ${sub.getIndex()}: addr=${(sub.getAddress() ?? '').substring(0, 16)}... label=${sub.getLabel() ?? ''} balance=${sub.getBalance()?.toString() ?? '0'} used=${sub.getIsUsed()}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return accounts?.map(a => a.toJson()) ?? []
|
return accounts?.map((a) => a.toJson()) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTransactions(): Promise<any[]> {
|
async getTransactions(): 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 txs = (await this.wallet.getTxs()) as any[]
|
||||||
console.log(`[monero] transactions: ${txs?.length ?? 0} returned`)
|
console.log(`[monero] transactions: ${txs?.length ?? 0} returned`)
|
||||||
if (txs?.length) {
|
if (txs?.length) {
|
||||||
for (const tx of txs.slice(0, 5)) {
|
for (const tx of txs.slice(0, 5)) {
|
||||||
console.log(`[monero] tx: hash=${tx.hash?.substring(0, 12) ?? '?'} amount=${tx.amount?.toString() ?? '0'} dir=${tx.direction} height=${tx.height ?? 0}`)
|
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`)
|
if (txs.length > 5)
|
||||||
|
console.log(`[monero] ... and ${txs.length - 5} more`)
|
||||||
}
|
}
|
||||||
return txs ?? []
|
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
result.sort(
|
||||||
|
(a: any, b: any) => Number(b.timestamp) - Number(a.timestamp),
|
||||||
|
)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async getHeight(): Promise<number> {
|
async getHeight(): Promise<number> {
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
if (!this.wallet) throw new Error('Wallet RPC not started')
|
||||||
const height = await this.wallet.getHeight() as number
|
const height = (await this.wallet.getHeight()) as number
|
||||||
console.log(`[monero] wallet height: ${height}`)
|
console.log(`[monero] wallet height: ${height}`)
|
||||||
return height
|
return height
|
||||||
}
|
}
|
||||||
@@ -233,8 +325,10 @@ export class MoneroWalletManager {
|
|||||||
async getDaemonHeight(): Promise<number> {
|
async getDaemonHeight(): Promise<number> {
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
if (!this.wallet) throw new Error('Wallet RPC not started')
|
||||||
try {
|
try {
|
||||||
const daemon = await connectToDaemonRpc(`http://${this.daemonAddress}`)
|
const daemon = await connectToDaemonRpc(
|
||||||
const height = await daemon.getHeight() as number
|
`http://${this.daemonAddress}`,
|
||||||
|
)
|
||||||
|
const height = (await daemon.getHeight()) as number
|
||||||
console.log(`[monero] daemon height: ${height}`)
|
console.log(`[monero] daemon height: ${height}`)
|
||||||
return height
|
return height
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -256,7 +350,9 @@ export class MoneroWalletManager {
|
|||||||
async isConnected(): Promise<boolean> {
|
async isConnected(): Promise<boolean> {
|
||||||
if (!this.wallet) return false
|
if (!this.wallet) return false
|
||||||
try {
|
try {
|
||||||
const daemon = await connectToDaemonRpc(`http://${this.daemonAddress}`)
|
const daemon = await connectToDaemonRpc(
|
||||||
|
`http://${this.daemonAddress}`,
|
||||||
|
)
|
||||||
await daemon.getHeight()
|
await daemon.getHeight()
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -268,12 +268,10 @@
|
|||||||
{tx.direction === 'in' ? 'Received' : 'Sent'}
|
{tx.direction === 'in' ? 'Received' : 'Sent'}
|
||||||
</p>
|
</p>
|
||||||
<p class="font-mono text-muted-foreground">
|
<p class="font-mono text-muted-foreground">
|
||||||
{Number(tx.amount) > 0
|
{atomicToXmr(tx.amount)} XMR
|
||||||
? `${tx.direction === 'in' ? '+' : '-'}${(BigInt(tx.amount) / 10_000_000_000_000n).toString()} XMR`
|
|
||||||
: '0 XMR'}
|
|
||||||
</p>
|
</p>
|
||||||
{#if tx.height > 0}
|
{#if tx.timestamp && tx.timestamp !== '0'}
|
||||||
<p class="text-muted-foreground">Block {tx.height}</p>
|
<p class="text-muted-foreground">{new Date(Number(tx.timestamp) * 1000).toLocaleString()}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<p class="text-muted-foreground/50 truncate font-mono">
|
<p class="text-muted-foreground/50 truncate font-mono">
|
||||||
{tx.hash.slice(0, 12)}...
|
{tx.hash.slice(0, 12)}...
|
||||||
|
|||||||
Reference in New Issue
Block a user