refactor MoneroWalletManager class to function module; fix settings page imports
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user