fix tx amount/direction mapping, add date display, sort by latest

This commit is contained in:
2026-05-17 11:35:15 +08:00
parent b4d5304587
commit 9c1b157107
2 changed files with 140 additions and 46 deletions
+137 -41
View File
@@ -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 { existsSync, readdirSync } from 'fs'
import { join } from 'path'
@@ -24,9 +29,11 @@ export class MoneroWalletManager {
if (!existsSync(dir)) return []
const files = readdirSync(dir)
const wallets = files
.filter(f => f.endsWith('.keys'))
.map(f => f.replace(/\.keys$/, ''))
console.log(`[monero] wallets on disk: ${wallets.join(', ') || 'none'}`)
.filter((f) => f.endsWith('.keys'))
.map((f) => f.replace(/\.keys$/, ''))
console.log(
`[monero] wallets on disk: ${wallets.join(', ') || 'none'}`,
)
return wallets
}
@@ -43,7 +50,10 @@ export class MoneroWalletManager {
}
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
console.log(`[monero] starting wallet-rpc process...`)
@@ -52,11 +62,16 @@ export class MoneroWalletManager {
const cmd = [
getBinaryPath(),
'--daemon-address', this.daemonAddress,
'--rpc-bind-port', String(this.rpcPort),
'--rpc-bind-ip', '127.0.0.1',
'--wallet-dir', getWalletDir(),
'--rpc-login', `${this.rpcUser}:${this.rpcPassword}`,
'--daemon-address',
this.daemonAddress,
'--rpc-bind-port',
String(this.rpcPort),
'--rpc-bind-ip',
'127.0.0.1',
'--wallet-dir',
getWalletDir(),
'--rpc-login',
`${this.rpcUser}:${this.rpcPassword}`,
'--trusted-daemon',
]
@@ -66,7 +81,10 @@ export class MoneroWalletManager {
stdout: 'pipe',
stderr: 'pipe',
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}`)
@@ -76,7 +94,9 @@ export class MoneroWalletManager {
while (true) {
const { done, value } = await stdout.read()
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) {
const { done, value } = await stderr.read()
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()
console.log(`[monero] RPC server is ready`)
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`)
}
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 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
}
@@ -113,22 +145,35 @@ export class MoneroWalletManager {
await Bun.connect({
hostname: '127.0.0.1',
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
} catch (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() {
console.log(`[monero] stopping wallet manager...`)
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 = null
}
@@ -149,8 +194,15 @@ export class MoneroWalletManager {
return { mnemonic: result.mnemonic, address }
}
async restoreWallet(name: string, password: string, mnemonic: string, restoreHeight?: number) {
console.log(`[monero] restoring wallet: ${name} (height: ${restoreHeight ?? 0})`)
async restoreWallet(
name: string,
password: string,
mnemonic: string,
restoreHeight?: number,
) {
console.log(
`[monero] restoring wallet: ${name} (height: ${restoreHeight ?? 0})`,
)
await this.rawRpc('restore_deterministic_wallet', {
filename: name,
password,
@@ -177,55 +229,95 @@ export class MoneroWalletManager {
async getBalance(): Promise<{ balance: bigint; unlocked: bigint }> {
if (!this.wallet) throw new Error('Wallet RPC not started')
const bal = await this.wallet.getBalance() as bigint
const unlocked = await this.wallet.getUnlockedBalance() as bigint
const bal = (await this.wallet.getBalance()) as bigint
const unlocked =
(await this.wallet.getUnlockedBalance()) as bigint
console.log(`[monero] balance: ${bal} (unlocked: ${unlocked})`)
return { balance: bal, unlocked }
}
async getAddress(accountIdx = 0, subIdx = 0): Promise<string> {
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}`)
return address
}
async getAccounts(): Promise<any[]> {
if (!this.wallet) throw new Error('Wallet RPC not started')
const accounts = await this.wallet.getAccounts(true) as any[]
console.log(`[monero] accounts: ${accounts?.length ?? 0} returned`)
const accounts = (await this.wallet.getAccounts(true)) as any[]
console.log(
`[monero] accounts: ${accounts?.length ?? 0} returned`,
)
if (accounts?.length) {
for (const acct of accounts) {
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)
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) {
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[]> {
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`)
if (txs?.length) {
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> {
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}`)
return height
}
@@ -233,8 +325,10 @@ export class MoneroWalletManager {
async getDaemonHeight(): Promise<number> {
if (!this.wallet) throw new Error('Wallet RPC not started')
try {
const daemon = await connectToDaemonRpc(`http://${this.daemonAddress}`)
const height = await daemon.getHeight() as number
const daemon = await connectToDaemonRpc(
`http://${this.daemonAddress}`,
)
const height = (await daemon.getHeight()) as number
console.log(`[monero] daemon height: ${height}`)
return height
} catch (e) {
@@ -256,7 +350,9 @@ export class MoneroWalletManager {
async isConnected(): Promise<boolean> {
if (!this.wallet) return false
try {
const daemon = await connectToDaemonRpc(`http://${this.daemonAddress}`)
const daemon = await connectToDaemonRpc(
`http://${this.daemonAddress}`,
)
await daemon.getHeight()
return true
} catch (e) {