refactor MoneroWalletManager class to function module; fix settings page imports

This commit is contained in:
2026-05-26 03:08:58 +08:00
parent b5f4a80f5c
commit 6fe9e6306b
7 changed files with 575 additions and 613 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ import { join } from 'path'
import { getENV } from './lib/get-env'
import { createEvmHandlers } from './rpc/evm'
import { createMoneroHandlers } from './rpc/monero'
import { MoneroWalletManager } from './lib/monero'
import type { MoneroWalletState } from './lib/monero'
import { canPromptTouchID, promptTouchID } from './lib/biometric'
import { sqlite } from './lib/sqlite'
import { evmWallets } from './lib/db/schema'
+22 -1
View File
@@ -1,2 +1,23 @@
export * from './binary'
export { MoneroWalletManager } from './wallet'
export {
createMoneroWalletState,
start,
stop,
createWallet,
restoreWallet,
openWallet,
closeWallet,
getBalance,
getAddress,
getAccounts,
getTransactions,
getTransferDetails,
getHeight,
getFeeEstimate,
getDaemonHeight,
transfer,
isWalletOpen,
isConnected,
listWallets,
} from './wallet'
export type { MoneroWalletState } from './wallet'
+295 -411
View File
@@ -6,459 +6,343 @@ import {
} from 'monero-ts'
import { getBinaryPath, getWalletDir } from './binary'
import { existsSync, readdirSync } from 'fs'
import { join } from 'path'
const DEFAULT_DAEMON = 'xmr-node.cakewallet.com:18081'
export class MoneroWalletManager {
private wallet: MoneroWalletRpc | null = null
private process: Bun.Subprocess | null = null
private rpcPort: number
private rpcUser: string
private rpcPassword: string
private daemonAddress: string = DEFAULT_DAEMON
export type MoneroWalletState = {
wallet: MoneroWalletRpc | null
process: Bun.Subprocess | null
rpcPort: number
rpcUser: string
rpcPassword: string
daemonAddress: string
}
constructor() {
this.rpcPort = 28084
this.rpcUser = 'koins'
this.rpcPassword = crypto.randomUUID()
export function createMoneroWalletState(): MoneroWalletState {
return {
wallet: null,
process: null,
rpcPort: 28084,
rpcUser: 'koins',
rpcPassword: crypto.randomUUID(),
daemonAddress: DEFAULT_DAEMON,
}
}
listWallets(): string[] {
const dir = getWalletDir()
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'}`,
)
return wallets
}
function rpcUrl(state: MoneroWalletState): string {
return `http://127.0.0.1:${state.rpcPort}`
}
function rpcConnection(state: MoneroWalletState): MoneroRpcConnection {
return new MoneroRpcConnection({
uri: rpcUrl(state),
username: state.rpcUser,
password: state.rpcPassword,
})
}
private get rpcUrl() {
return `http://127.0.0.1:${this.rpcPort}`
}
async function rawRpc(state: MoneroWalletState, method: string, params: Record<string, any> = {}): Promise<any> {
const conn = rpcConnection(state)
const res = await conn.sendJsonRequest(method, params)
if (res?.error) throw new Error(res.error.message || JSON.stringify(res.error))
return res?.result
}
private get rpcConnection(): MoneroRpcConnection {
return new MoneroRpcConnection({
uri: this.rpcUrl,
username: this.rpcUser,
password: this.rpcPassword,
})
async function waitForRpc(state: MoneroWalletState, timeoutMs = 30000): Promise<void> {
const start = Date.now()
let lastErr = ''
while (Date.now() - start < timeoutMs) {
try {
await Bun.connect({
hostname: '127.0.0.1',
port: state.rpcPort,
socket: {
open(s) { s.end() },
close() {},
data() {},
error() {},
},
})
console.log(`[monero] RPC ready after ${Date.now() - start}ms`)
return
} catch (e) {
lastErr = String(e)
}
await new Promise((r) => setTimeout(r, 500))
}
throw new Error(`monero-wallet-rpc failed to start within ${timeoutMs}ms: ${lastErr}`)
}
async start(daemonAddress: string = DEFAULT_DAEMON) {
if (!existsSync(getBinaryPath()))
throw new Error(
'monero-wallet-rpc not installed. Call downloadBinary() first.',
)
this.daemonAddress = daemonAddress
export async function start(state: MoneroWalletState, daemonAddress: string = DEFAULT_DAEMON) {
if (!existsSync(getBinaryPath()))
throw new Error('monero-wallet-rpc not installed. Call downloadBinary() first.')
state.daemonAddress = daemonAddress
console.log(`[monero] starting wallet-rpc process...`)
console.log(`[monero] daemon address: ${this.daemonAddress}`)
console.log(`[monero] wallet dir: ${getWalletDir()}`)
console.log(`[monero] starting wallet-rpc process...`)
console.log(`[monero] daemon address: ${state.daemonAddress}`)
console.log(`[monero] wallet dir: ${getWalletDir()}`)
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}`,
'--trusted-daemon',
]
const cmd = [
getBinaryPath(),
'--daemon-address', state.daemonAddress,
'--rpc-bind-port', String(state.rpcPort),
'--rpc-bind-ip', '127.0.0.1',
'--wallet-dir', getWalletDir(),
'--rpc-login', `${state.rpcUser}:${state.rpcPassword}`,
'--trusted-daemon',
]
console.log(`[monero] spawning: ${cmd.join(' ')}`)
this.process = Bun.spawn(cmd, {
env: { ...process.env, LANG: 'en_US.UTF-8' },
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 pid: ${this.process.pid}`)
console.log(`[monero] spawning: ${cmd.join(' ')}`)
state.process = Bun.spawn(cmd, {
env: { ...process.env, LANG: 'en_US.UTF-8' },
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 pid: ${state.process.pid}`)
const stdout = this.process.stdout.getReader()
;(async () => {
while (true) {
const { done, value } = await stdout.read()
if (done) break
console.log(
`[monero:stdout] ${new TextDecoder().decode(value)}`,
)
}
})()
const stdout = state.process.stdout.getReader()
;(async () => {
while (true) {
const { done, value } = await stdout.read()
if (done) break
console.log(`[monero:stdout] ${new TextDecoder().decode(value)}`)
}
})()
const stderr = this.process.stderr.getReader()
;(async () => {
while (true) {
const { done, value } = await stderr.read()
if (done) break
console.log(
`[monero:stderr] ${new TextDecoder().decode(value)}`,
)
}
})()
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
console.log(`[monero] MoneroWalletRpc connected successfully`)
}
const stderr = state.process.stderr.getReader()
;(async () => {
while (true) {
const { done, value } = await stderr.read()
if (done) break
console.log(`[monero:stderr] ${new TextDecoder().decode(value)}`)
}
})()
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))
return res?.result
}
console.log(`[monero] waiting for RPC server to become available...`)
await waitForRpc(state)
console.log(`[monero] RPC server is ready`)
private async waitForRpc(timeoutMs = 30000): Promise<void> {
const start = Date.now()
let lastErr = ''
while (Date.now() - start < timeoutMs) {
try {
await Bun.connect({
hostname: '127.0.0.1',
port: this.rpcPort,
socket: {
open(s) {
s.end()
},
close() {},
data() {},
error() {},
},
})
console.log(
`[monero] RPC ready after ${Date.now() - start}ms`,
)
return
} catch (e) {
lastErr = String(e)
}
await new Promise((r) => setTimeout(r, 500))
}
throw new Error(
`monero-wallet-rpc failed to start within ${timeoutMs}ms: ${lastErr}`,
)
}
console.log(`[monero] connecting MoneroWalletRpc client...`)
state.wallet = await connectToWalletRpc(rpcUrl(state), state.rpcUser, state.rpcPassword) as MoneroWalletRpc
console.log(`[monero] MoneroWalletRpc connected successfully`)
}
async stop() {
console.log(`[monero] stopping wallet manager...`)
if (this.process) {
console.log(
`[monero] killing wallet-rpc process (pid ${this.process.pid})`,
)
this.process.kill()
this.process = null
}
this.wallet = null
console.log(`[monero] wallet manager stopped`)
export async function stop(state: MoneroWalletState) {
console.log(`[monero] stopping wallet manager...`)
if (state.process) {
console.log(`[monero] killing wallet-rpc process (pid ${state.process.pid})`)
state.process.kill()
state.process = null
}
state.wallet = null
console.log(`[monero] wallet manager stopped`)
}
async createWallet(name: string, password: string) {
console.log(`[monero] creating wallet: ${name}`)
const result = await this.rawRpc('create_wallet', {
filename: name,
password,
language: 'English',
})
if (!this.wallet) throw new Error('Wallet RPC not started')
const address = await this.wallet.getAddress(0, 0)
console.log(`[monero] wallet created: ${name} -> ${address}`)
return { mnemonic: result.mnemonic, address }
}
export async function createWallet(state: MoneroWalletState, name: string, password: string) {
console.log(`[monero] creating wallet: ${name}`)
const result = await rawRpc(state, 'create_wallet', { filename: name, password, language: 'English' })
if (!state.wallet) throw new Error('Wallet RPC not started')
const address = await state.wallet.getAddress(0, 0)
console.log(`[monero] wallet created: ${name} -> ${address}`)
return { mnemonic: result.mnemonic, address }
}
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,
seed: mnemonic,
restore_height: restoreHeight ?? 0,
language: 'English',
})
console.log(`[monero] wallet restored: ${name}`)
}
export async function restoreWallet(state: MoneroWalletState, name: string, password: string, mnemonic: string, restoreHeight?: number) {
console.log(`[monero] restoring wallet: ${name} (height: ${restoreHeight ?? 0})`)
await rawRpc(state, 'restore_deterministic_wallet', {
filename: name, password, seed: mnemonic, restore_height: restoreHeight ?? 0, language: 'English',
})
console.log(`[monero] wallet restored: ${name}`)
}
async openWallet(name: string, password: string) {
console.log(`[monero] opening wallet: ${name}`)
if (!this.wallet) throw new Error('Wallet RPC not started')
await this.wallet.openWallet(name, password)
console.log(`[monero] wallet opened: ${name}`)
}
export async function openWallet(state: MoneroWalletState, name: string, password: string) {
console.log(`[monero] opening wallet: ${name}`)
if (!state.wallet) throw new Error('Wallet RPC not started')
await state.wallet.openWallet(name, password)
console.log(`[monero] wallet opened: ${name}`)
}
async closeWallet() {
console.log(`[monero] closing current wallet`)
if (!this.wallet) throw new Error('Wallet RPC not started')
await this.wallet.close()
console.log(`[monero] wallet closed`)
}
export async function closeWallet(state: MoneroWalletState) {
console.log(`[monero] closing current wallet`)
if (!state.wallet) throw new Error('Wallet RPC not started')
await state.wallet.close()
console.log(`[monero] wallet closed`)
}
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
console.log(`[monero] balance: ${bal} (unlocked: ${unlocked})`)
return { balance: bal, unlocked }
}
export async function getBalance(state: MoneroWalletState): Promise<{ balance: bigint; unlocked: bigint }> {
if (!state.wallet) throw new Error('Wallet RPC not started')
const balance = await state.wallet.getBalance() as bigint
const unlocked = await state.wallet.getUnlockedBalance() as bigint
console.log(`[monero] balance: ${balance} (unlocked: ${unlocked})`)
return { balance, 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
console.log(`[monero] address: ${address}`)
return address
}
export async function getAddress(state: MoneroWalletState, accountIdx = 0, subIdx = 0): Promise<string> {
if (!state.wallet) throw new Error('Wallet RPC not started')
const address = await state.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`,
)
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)}...`,
)
const has0 = subs.some((s: any) => s.getIndex() === 0)
if (!has0) {
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()}`,
)
}
export async function getAccounts(state: MoneroWalletState): Promise<any[]> {
if (!state.wallet) throw new Error('Wallet RPC not started')
const accounts = await state.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)}...`)
const has0 = subs.some((s: any) => s.getIndex() === 0)
if (!has0) 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()}`)
}
}
return accounts?.map((a) => a.toJson()) ?? []
}
return accounts?.map((a) => a.toJson()) ?? []
}
async getTransactions(accountIndex?: number): Promise<any[]> {
if (!this.wallet) throw new Error('Wallet RPC not started')
const params: Record<string, any> = {
in: true,
out: true,
pending: false,
pool: false,
export async function getTransactions(state: MoneroWalletState, accountIndex?: number): Promise<any[]> {
if (!state.wallet) throw new Error('Wallet RPC not started')
const params: Record<string, any> = { in: true, out: true, pending: false, pool: false }
if (accountIndex !== undefined) params.account_index = accountIndex
const result = await rawRpc(state, '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,
})
}
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,
})
}
}
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
}
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 getTransferDetails(txid: string): Promise<any> {
if (!this.wallet) throw new Error('Wallet RPC not started')
const result = await this.rawRpc('get_transfer_by_txid', { txid })
if (!result?.transfer) return null
const t = result.transfer
const isIn = t.type === 'in'
const isOut = t.type === 'out'
const dests =
t.destinations?.map((d: any) => ({
address: d.address,
amount: d.amount?.toString() ?? '0',
})) ?? []
const subaddrIndices = isIn
? [
{
major: t.subaddr_index?.major ?? 0,
minor: t.subaddr_index?.minor ?? 0,
},
]
: (t.subaddr_indices ?? []).map((s: any) => ({
major: s.major ?? 0,
minor: s.minor ?? 0,
}))
return {
hash: t.txid,
direction: isIn ? ('in' as const) : ('out' as const),
amount: t.amount?.toString() ?? '0',
fee: t.fee?.toString() ?? '0',
height: t.height ?? 0,
timestamp: String(t.timestamp ?? 0),
confirmations: t.confirmations ?? 0,
unlockTime: t.unlock_time ?? 0,
locked: t.locked ?? false,
doubleSpend: t.double_spend_seen ?? false,
note: t.note,
paymentId: t.payment_id ?? '',
destinations: dests,
subaddrIndices,
}
export async function getTransferDetails(state: MoneroWalletState, txid: string): Promise<any> {
if (!state.wallet) throw new Error('Wallet RPC not started')
const result = await rawRpc(state, 'get_transfer_by_txid', { txid })
if (!result?.transfer) return null
const t = result.transfer
const isIn = t.type === 'in'
const isOut = t.type === 'out'
const dests = t.destinations?.map((d: any) => ({ address: d.address, amount: d.amount?.toString() ?? '0' })) ?? []
const subaddrIndices = isIn
? [{ major: t.subaddr_index?.major ?? 0, minor: t.subaddr_index?.minor ?? 0 }]
: (t.subaddr_indices ?? []).map((s: any) => ({ major: s.major ?? 0, minor: s.minor ?? 0 }))
return {
hash: t.txid,
direction: isIn ? ('in' as const) : ('out' as const),
amount: t.amount?.toString() ?? '0',
fee: t.fee?.toString() ?? '0',
height: t.height ?? 0,
timestamp: String(t.timestamp ?? 0),
confirmations: t.confirmations ?? 0,
unlockTime: t.unlock_time ?? 0,
locked: t.locked ?? false,
doubleSpend: t.double_spend_seen ?? false,
note: t.note,
paymentId: t.payment_id ?? '',
destinations: dests,
subaddrIndices,
}
}
async getHeight(): Promise<number> {
if (!this.wallet) throw new Error('Wallet RPC not started')
const height = (await this.wallet.getHeight()) as number
console.log(`[monero] wallet height: ${height}`)
return height
export async function getHeight(state: MoneroWalletState): Promise<number> {
if (!state.wallet) throw new Error('Wallet RPC not started')
const height = await state.wallet.getHeight() as number
console.log(`[monero] wallet height: ${height}`)
return height
}
export async function getFeeEstimate(state: MoneroWalletState): Promise<{ fee: string; fees: string[]; estimatedFee: string }> {
try {
const conn = new MoneroRpcConnection({ uri: `http://${state.daemonAddress}` })
const res = await conn.sendJsonRequest('get_fee_estimate', { grace_blocks: 10 })
if (res?.error) throw new Error(res.error.message)
console.log('[rpc][get_fee_estimate][response]', res)
const result = res?.result
const txSizeKb = 2.5
const feePerKb = BigInt(result.fee ?? '0')
const fees = (result.fees ?? []).map((f: string) => BigInt(f).toString())
const estimated = (feePerKb * BigInt(Math.round(txSizeKb * 10))) / 10n
return { fee: feePerKb.toString(), fees, estimatedFee: estimated.toString() }
} catch (e) {
console.log('[monero] getFeeEstimate error:', e)
return { fee: '0', fees: ['0', '0', '0', '0'], estimatedFee: '0' }
}
}
async getFeeEstimate(): Promise<{
fee: string
fees: string[]
estimatedFee: string
}> {
try {
const conn = new MoneroRpcConnection({
uri: `http://${this.daemonAddress}`,
})
const res = await conn.sendJsonRequest('get_fee_estimate', {
grace_blocks: 10,
})
if (res?.error) throw new Error(res.error.message)
console.log('[rpc][get_fee_estimate][response]', res)
const result = res?.result
const txSizeKb = 2.5
const feePerKb = BigInt(result.fee ?? '0')
const fees = (result.fees ?? []).map((f: string) =>
BigInt(f).toString(),
)
const estimated =
(feePerKb * BigInt(Math.round(txSizeKb * 10))) / 10n
return {
fee: feePerKb.toString(),
fees,
estimatedFee: estimated.toString(),
}
} catch (e) {
console.log('[monero] getFeeEstimate error:', e)
return {
fee: '0',
fees: ['0', '0', '0', '0'],
estimatedFee: '0',
}
}
export async function getDaemonHeight(state: MoneroWalletState): Promise<number> {
if (!state.wallet) throw new Error('Wallet RPC not started')
try {
const daemon = await connectToDaemonRpc(`http://${state.daemonAddress}`)
const height = await daemon.getHeight() as number
console.log(`[monero] daemon height: ${height}`)
return height
} catch (e) {
console.log(`[monero] failed to get daemon height:`, e)
return 0
}
}
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
console.log(`[monero] daemon height: ${height}`)
return height
} catch (e) {
console.log(`[monero] failed to get daemon height:`, e)
return 0
}
}
export async function transfer(
state: MoneroWalletState,
address: string,
amount: bigint,
priority: number = 0,
accountIndex: number = 0,
): Promise<{ txHash: string; fee: string; amount: string }> {
if (!state.wallet) throw new Error('Wallet RPC not started')
console.log(`[monero] transfer: ${address} amount=${amount.toString()} priority=${priority} account=${accountIndex}`)
const result = await rawRpc(state, 'transfer', {
destinations: [{ address, amount: amount.toString() }],
priority,
account_index: accountIndex,
get_tx_key: true,
get_tx_hex: false,
get_tx_metadata: false,
})
console.log(`[monero] transfer result: tx_hash=${result.tx_hash} fee=${result.fee} amount=${result.amount}`)
return { txHash: result.tx_hash, fee: result.fee?.toString() ?? '0', amount: result.amount?.toString() ?? amount.toString() }
}
async transfer(
address: string,
amount: bigint,
priority: number = 0,
accountIndex: number = 0,
): Promise<{ txHash: string; fee: string; amount: string }> {
if (!this.wallet) throw new Error('Wallet RPC not started')
console.log(
`[monero] transfer: ${address} amount=${amount.toString()} priority=${priority} account=${accountIndex}`,
)
const result = await this.rawRpc('transfer', {
destinations: [{ address, amount: amount.toString() }],
priority,
account_index: accountIndex,
get_tx_key: true,
get_tx_hex: false,
get_tx_metadata: false,
})
console.log(
`[monero] transfer result: tx_hash=${result.tx_hash} fee=${result.fee} amount=${result.amount}`,
)
return {
txHash: result.tx_hash,
fee: result.fee?.toString() ?? '0',
amount: result.amount?.toString() ?? amount.toString(),
}
}
export function listWallets(): string[] {
const dir = getWalletDir()
if (!existsSync(dir)) return []
return readdirSync(dir)
.filter((f) => f.endsWith('.keys'))
.map((f) => f.replace(/\.keys$/, ''))
}
async isWalletOpen(): Promise<boolean> {
if (!this.wallet) return false
try {
await this.wallet.getHeight()
return true
} catch {
return false
}
export async function isWalletOpen(state: MoneroWalletState): Promise<boolean> {
if (!state.wallet) return false
try {
await state.wallet.getHeight()
return true
} catch {
return false
}
}
async isConnected(): Promise<boolean> {
if (!this.wallet) return false
try {
const daemon = await connectToDaemonRpc(
`http://${this.daemonAddress}`,
)
await daemon.getHeight()
return true
} catch (e) {
console.log(`[monero] connection check failed:`, e)
return false
}
export async function isConnected(state: MoneroWalletState): Promise<boolean> {
if (!state.wallet) return false
try {
const daemon = await connectToDaemonRpc(`http://${state.daemonAddress}`)
await daemon.getHeight()
return true
} catch (e) {
console.log(`[monero] connection check failed:`, e)
return false
}
}
+65 -146
View File
@@ -1,20 +1,30 @@
import {
isBinaryInstalled,
downloadBinary,
MoneroWalletManager,
createMoneroWalletState,
start as walletStart,
stop as walletStop,
createWallet as walletCreate,
restoreWallet as walletRestore,
openWallet as walletOpen,
getBalance as walletGetBalance,
getTransactions as walletGetTransactions,
getAccounts as walletGetAccounts,
listWallets,
getTransferDetails as walletGetTransferDetails,
getHeight as walletGetHeight,
getDaemonHeight as walletGetDaemonHeight,
getFeeEstimate as walletGetFeeEstimate,
transfer as walletTransfer,
isWalletOpen,
isConnected,
} from '../../lib/monero'
import type { MoneroWalletState } from '../../lib/monero'
export function createMoneroHandlers(state: {
manager: MoneroWalletManager | null
downloading: boolean
}) {
export function createMoneroHandlers(state: { manager: MoneroWalletState | null; downloading: boolean }) {
return {
moneroBinaryStatus: async () => {
const status = {
installed: isBinaryInstalled(),
downloading: state.downloading,
error: undefined,
}
const status = { installed: isBinaryInstalled(), downloading: state.downloading }
console.log('[rpc] moneroBinaryStatus:', status)
return status
},
@@ -31,117 +41,62 @@ export function createMoneroHandlers(state: {
state.downloading = false
}
},
moneroStart: async ({
daemonAddress,
}: {
daemonAddress?: string
}) => {
moneroStart: async ({ daemonAddress }: { daemonAddress?: string }) => {
console.log('[rpc] moneroStart:', { daemonAddress })
if (!state.manager) {
state.manager = new MoneroWalletManager()
state.manager = createMoneroWalletState()
}
try {
await state.manager.start(daemonAddress)
await walletStart(state.manager, daemonAddress)
} catch (e) {
console.log('[rpc] moneroStart error:', e)
return { running: false, walletOpen: false, connected: false }
}
const connected = await state.manager.isConnected()
const connected = await isConnected(state.manager)
console.log('[rpc] moneroStart complete, connected:', connected)
return { running: true, walletOpen: false, connected }
},
moneroStop: async () => {
console.log('[rpc] moneroStop')
if (state.manager) {
await state.manager.stop()
await walletStop(state.manager)
state.manager = null
}
},
moneroCreateWallet: async ({
name,
password,
}: {
name: string
password: string
}) => {
moneroCreateWallet: async ({ name, password }: { name: string; password: string }) => {
console.log('[rpc] moneroCreateWallet:', name)
if (!state.manager)
throw new Error('Monero wallet RPC not started')
const result = await state.manager.createWallet(name, password)
if (!state.manager) throw new Error('Monero wallet RPC not started')
const result = await walletCreate(state.manager, name, password)
console.log('[rpc] moneroCreateWallet complete')
return result
},
moneroRestoreWallet: async ({
name,
password,
mnemonic,
restoreHeight,
}: {
name: string
password: string
mnemonic: string
restoreHeight?: number
}) => {
moneroRestoreWallet: async ({ name, password, mnemonic, restoreHeight }: { name: string; password: string; mnemonic: string; restoreHeight?: number }) => {
console.log('[rpc] moneroRestoreWallet:', name)
if (!state.manager)
throw new Error('Monero wallet RPC not started')
await state.manager.restoreWallet(
name,
password,
mnemonic,
restoreHeight,
)
const address = await state.manager.getAddress()
console.log(
'[rpc] moneroRestoreWallet complete, address:',
address,
)
if (!state.manager) throw new Error('Monero wallet RPC not started')
await walletRestore(state.manager, name, password, mnemonic, restoreHeight)
const address = await walletGetAddress(state.manager)
console.log('[rpc] moneroRestoreWallet complete, address:', address)
return { address }
},
moneroOpenWallet: async ({
name,
password,
}: {
name: string
password: string
}) => {
moneroOpenWallet: async ({ name, password }: { name: string; password: string }) => {
console.log('[rpc] moneroOpenWallet:', name)
if (!state.manager)
throw new Error('Monero wallet RPC not started')
await state.manager.openWallet(name, password)
if (!state.manager) throw new Error('Monero wallet RPC not started')
await walletOpen(state.manager, name, password)
},
moneroGetBalance: async () => {
console.log('[rpc] moneroGetBalance')
if (!state.manager)
throw new Error('Monero wallet RPC not started')
const { balance, unlocked } = await state.manager.getBalance()
const address = await state.manager.getAddress()
const height = await state.manager.getHeight()
const daemonHeight = await state.manager.getDaemonHeight()
console.log('[rpc] balance:', {
balance: balance.toString(),
unlocked: unlocked.toString(),
address,
height,
daemonHeight,
})
return {
balance: balance.toString(),
unlocked: unlocked.toString(),
address,
height,
daemonHeight,
}
if (!state.manager) throw new Error('Monero wallet RPC not started')
const { balance, unlocked } = await walletGetBalance(state.manager)
const address = await walletGetAddress(state.manager)
const height = await walletGetHeight(state.manager)
const daemonHeight = await walletGetDaemonHeight(state.manager)
console.log('[rpc] balance:', { balance: balance.toString(), unlocked: unlocked.toString(), address, height, daemonHeight })
return { balance: balance.toString(), unlocked: unlocked.toString(), address, height, daemonHeight }
},
moneroGetTransactions: async ({
accountIndex,
}: {
accountIndex?: number
}) => {
moneroGetTransactions: async ({ accountIndex }: { accountIndex?: number }) => {
console.log('[rpc] moneroGetTransactions', { accountIndex })
if (!state.manager)
throw new Error('Monero wallet RPC not started')
const txs = await state.manager.getTransactions(accountIndex)
if (!state.manager) throw new Error('Monero wallet RPC not started')
const txs = await walletGetTransactions(state.manager, accountIndex)
console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`)
return txs
},
@@ -150,85 +105,49 @@ export function createMoneroHandlers(state: {
console.log('[rpc] moneroWalletStatus: not running')
return { running: false, walletOpen: false, connected: false }
}
const connected = await state.manager.isConnected()
const walletOpen = await state.manager.isWalletOpen()
console.log(
'[rpc] moneroWalletStatus: running, walletOpen:',
walletOpen,
'connected:',
connected,
)
const connected = await isConnected(state.manager)
const walletOpen = await isWalletOpen(state.manager)
console.log('[rpc] moneroWalletStatus: running, walletOpen:', walletOpen, 'connected:', connected)
return { running: true, walletOpen, connected }
},
moneroGetAccounts: async () => {
console.log('[rpc] moneroGetAccounts')
if (!state.manager)
throw new Error('Monero wallet RPC not started')
const accounts = await state.manager.getAccounts()
console.log(
`[rpc] moneroGetAccounts: ${accounts.length} accounts`,
)
if (!state.manager) throw new Error('Monero wallet RPC not started')
const accounts = await walletGetAccounts(state.manager)
console.log(`[rpc] moneroGetAccounts: ${accounts.length} accounts`)
return accounts
},
moneroListWallets: async () => {
console.log('[rpc] moneroListWallets')
if (!state.manager)
throw new Error('Monero wallet RPC not started')
const wallets = state.manager.listWallets()
console.log(
`[rpc] moneroListWallets: ${wallets.join(', ') || 'none'}`,
)
if (!state.manager) throw new Error('Monero wallet RPC not started')
const wallets = listWallets()
console.log(`[rpc] moneroListWallets: ${wallets.join(', ') || 'none'}`)
return wallets
},
moneroTransfer: async ({
address,
amount,
priority,
accountIndex,
}: {
address: string
amount: string
priority?: number
accountIndex?: number
}) => {
console.log('[rpc] moneroTransfer:', {
address,
amount,
priority,
})
if (!state.manager)
throw new Error('Monero wallet RPC not started')
const result = await state.manager.transfer(
address,
BigInt(amount),
priority ?? 0,
accountIndex ?? 0,
)
moneroTransfer: async ({ address, amount, priority, accountIndex }: { address: string; amount: string; priority?: number; accountIndex?: number }) => {
console.log('[rpc] moneroTransfer:', { address, amount, priority })
if (!state.manager) throw new Error('Monero wallet RPC not started')
const result = await walletTransfer(state.manager, address, BigInt(amount), priority ?? 0, accountIndex ?? 0)
console.log('[rpc] moneroTransfer complete:', result.txHash)
return result
},
moneroGetTransferDetails: async ({ txid }: { txid: string }) => {
console.log('[rpc] moneroGetTransferDetails:', txid)
if (!state.manager)
throw new Error('Monero wallet RPC not started')
const details = await state.manager.getTransferDetails(txid)
console.log(
'[rpc] moneroGetTransferDetails complete:',
details?.hash,
)
if (!state.manager) throw new Error('Monero wallet RPC not started')
const details = await walletGetTransferDetails(state.manager, txid)
console.log('[rpc] moneroGetTransferDetails complete:', details?.hash)
return details
},
moneroGetFeeEstimate: async () => {
console.log('[rpc] moneroGetFeeEstimate')
if (!state.manager)
throw new Error('Monero wallet RPC not started')
if (!state.manager) throw new Error('Monero wallet RPC not started')
try {
const fee = await state.manager.getFeeEstimate()
const fee = await walletGetFeeEstimate(state.manager)
console.log('[monero][gas][fee]', fee)
return fee
} catch (error) {
console.log('[monero][gas][error]', error)
return null
return
}
},
}