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 { getENV } from './lib/get-env'
|
||||||
import { createEvmHandlers } from './rpc/evm'
|
import { createEvmHandlers } from './rpc/evm'
|
||||||
import { createMoneroHandlers } from './rpc/monero'
|
import { createMoneroHandlers } from './rpc/monero'
|
||||||
import { MoneroWalletManager } from './lib/monero'
|
import type { MoneroWalletState } from './lib/monero'
|
||||||
import { canPromptTouchID, promptTouchID } from './lib/biometric'
|
import { canPromptTouchID, promptTouchID } from './lib/biometric'
|
||||||
import { sqlite } from './lib/sqlite'
|
import { sqlite } from './lib/sqlite'
|
||||||
import { evmWallets } from './lib/db/schema'
|
import { evmWallets } from './lib/db/schema'
|
||||||
|
|||||||
@@ -1,2 +1,23 @@
|
|||||||
export * from './binary'
|
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,286 +6,210 @@ import {
|
|||||||
} from 'monero-ts'
|
} 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'
|
|
||||||
|
|
||||||
const DEFAULT_DAEMON = 'xmr-node.cakewallet.com:18081'
|
const DEFAULT_DAEMON = 'xmr-node.cakewallet.com:18081'
|
||||||
|
|
||||||
export class MoneroWalletManager {
|
export type MoneroWalletState = {
|
||||||
private wallet: MoneroWalletRpc | null = null
|
wallet: MoneroWalletRpc | null
|
||||||
private process: Bun.Subprocess | null = null
|
process: Bun.Subprocess | null
|
||||||
private rpcPort: number
|
rpcPort: number
|
||||||
private rpcUser: string
|
rpcUser: string
|
||||||
private rpcPassword: string
|
rpcPassword: string
|
||||||
private daemonAddress: string = DEFAULT_DAEMON
|
daemonAddress: string
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
export function createMoneroWalletState(): MoneroWalletState {
|
||||||
this.rpcPort = 28084
|
return {
|
||||||
this.rpcUser = 'koins'
|
wallet: null,
|
||||||
this.rpcPassword = crypto.randomUUID()
|
process: null,
|
||||||
|
rpcPort: 28084,
|
||||||
|
rpcUser: 'koins',
|
||||||
|
rpcPassword: crypto.randomUUID(),
|
||||||
|
daemonAddress: DEFAULT_DAEMON,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
listWallets(): string[] {
|
function rpcUrl(state: MoneroWalletState): string {
|
||||||
const dir = getWalletDir()
|
return `http://127.0.0.1:${state.rpcPort}`
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
private get rpcUrl() {
|
function rpcConnection(state: MoneroWalletState): MoneroRpcConnection {
|
||||||
return `http://127.0.0.1:${this.rpcPort}`
|
|
||||||
}
|
|
||||||
|
|
||||||
private get rpcConnection(): MoneroRpcConnection {
|
|
||||||
return new MoneroRpcConnection({
|
return new MoneroRpcConnection({
|
||||||
uri: this.rpcUrl,
|
uri: rpcUrl(state),
|
||||||
username: this.rpcUser,
|
username: state.rpcUser,
|
||||||
password: this.rpcPassword,
|
password: state.rpcPassword,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(daemonAddress: string = DEFAULT_DAEMON) {
|
async function rawRpc(state: MoneroWalletState, method: string, params: Record<string, any> = {}): Promise<any> {
|
||||||
if (!existsSync(getBinaryPath()))
|
const conn = rpcConnection(state)
|
||||||
throw new Error(
|
|
||||||
'monero-wallet-rpc not installed. Call downloadBinary() first.',
|
|
||||||
)
|
|
||||||
this.daemonAddress = daemonAddress
|
|
||||||
|
|
||||||
console.log(`[monero] starting wallet-rpc process...`)
|
|
||||||
console.log(`[monero] daemon address: ${this.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',
|
|
||||||
]
|
|
||||||
|
|
||||||
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}`)
|
|
||||||
|
|
||||||
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 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`)
|
|
||||||
}
|
|
||||||
|
|
||||||
private async rawRpc(
|
|
||||||
method: string,
|
|
||||||
params: Record<string, any> = {},
|
|
||||||
): Promise<any> {
|
|
||||||
const conn = this.rpcConnection
|
|
||||||
const res = await conn.sendJsonRequest(method, params)
|
const res = await conn.sendJsonRequest(method, params)
|
||||||
if (res?.error)
|
if (res?.error) throw new Error(res.error.message || JSON.stringify(res.error))
|
||||||
throw new Error(res.error.message || JSON.stringify(res.error))
|
|
||||||
return res?.result
|
return res?.result
|
||||||
}
|
}
|
||||||
|
|
||||||
private async waitForRpc(timeoutMs = 30000): Promise<void> {
|
async function waitForRpc(state: MoneroWalletState, timeoutMs = 30000): Promise<void> {
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
let lastErr = ''
|
let lastErr = ''
|
||||||
while (Date.now() - start < timeoutMs) {
|
while (Date.now() - start < timeoutMs) {
|
||||||
try {
|
try {
|
||||||
await Bun.connect({
|
await Bun.connect({
|
||||||
hostname: '127.0.0.1',
|
hostname: '127.0.0.1',
|
||||||
port: this.rpcPort,
|
port: state.rpcPort,
|
||||||
socket: {
|
socket: {
|
||||||
open(s) {
|
open(s) { s.end() },
|
||||||
s.end()
|
|
||||||
},
|
|
||||||
close() {},
|
close() {},
|
||||||
data() {},
|
data() {},
|
||||||
error() {},
|
error() {},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
console.log(
|
console.log(`[monero] RPC ready after ${Date.now() - start}ms`)
|
||||||
`[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(
|
throw new Error(`monero-wallet-rpc failed to start within ${timeoutMs}ms: ${lastErr}`)
|
||||||
`monero-wallet-rpc failed to start within ${timeoutMs}ms: ${lastErr}`,
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async stop() {
|
export async function start(state: MoneroWalletState, daemonAddress: string = DEFAULT_DAEMON) {
|
||||||
console.log(`[monero] stopping wallet manager...`)
|
if (!existsSync(getBinaryPath()))
|
||||||
if (this.process) {
|
throw new Error('monero-wallet-rpc not installed. Call downloadBinary() first.')
|
||||||
console.log(
|
state.daemonAddress = daemonAddress
|
||||||
`[monero] killing wallet-rpc process (pid ${this.process.pid})`,
|
|
||||||
)
|
|
||||||
this.process.kill()
|
|
||||||
this.process = null
|
|
||||||
}
|
|
||||||
this.wallet = null
|
|
||||||
console.log(`[monero] wallet manager stopped`)
|
|
||||||
}
|
|
||||||
|
|
||||||
async createWallet(name: string, password: string) {
|
console.log(`[monero] starting wallet-rpc process...`)
|
||||||
console.log(`[monero] creating wallet: ${name}`)
|
console.log(`[monero] daemon address: ${state.daemonAddress}`)
|
||||||
const result = await this.rawRpc('create_wallet', {
|
console.log(`[monero] wallet dir: ${getWalletDir()}`)
|
||||||
filename: name,
|
|
||||||
password,
|
const cmd = [
|
||||||
language: 'English',
|
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(' ')}`)
|
||||||
|
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 ?? '')
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
console.log(`[monero] wallet-rpc pid: ${state.process.pid}`)
|
||||||
const address = await this.wallet.getAddress(0, 0)
|
|
||||||
|
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 = state.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 waitForRpc(state)
|
||||||
|
console.log(`[monero] RPC server is ready`)
|
||||||
|
|
||||||
|
console.log(`[monero] connecting MoneroWalletRpc client...`)
|
||||||
|
state.wallet = await connectToWalletRpc(rpcUrl(state), state.rpcUser, state.rpcPassword) as MoneroWalletRpc
|
||||||
|
console.log(`[monero] MoneroWalletRpc connected successfully`)
|
||||||
|
}
|
||||||
|
|
||||||
|
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`)
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`)
|
console.log(`[monero] wallet created: ${name} -> ${address}`)
|
||||||
return { mnemonic: result.mnemonic, address }
|
return { mnemonic: result.mnemonic, address }
|
||||||
}
|
}
|
||||||
|
|
||||||
async restoreWallet(
|
export async function restoreWallet(state: MoneroWalletState, name: string, password: string, mnemonic: string, restoreHeight?: number) {
|
||||||
name: string,
|
console.log(`[monero] restoring wallet: ${name} (height: ${restoreHeight ?? 0})`)
|
||||||
password: string,
|
await rawRpc(state, 'restore_deterministic_wallet', {
|
||||||
mnemonic: string,
|
filename: name, password, seed: mnemonic, restore_height: restoreHeight ?? 0, language: 'English',
|
||||||
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}`)
|
console.log(`[monero] wallet restored: ${name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async openWallet(name: string, password: string) {
|
export async function openWallet(state: MoneroWalletState, name: string, password: string) {
|
||||||
console.log(`[monero] opening wallet: ${name}`)
|
console.log(`[monero] opening wallet: ${name}`)
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
await this.wallet.openWallet(name, password)
|
await state.wallet.openWallet(name, password)
|
||||||
console.log(`[monero] wallet opened: ${name}`)
|
console.log(`[monero] wallet opened: ${name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeWallet() {
|
export async function closeWallet(state: MoneroWalletState) {
|
||||||
console.log(`[monero] closing current wallet`)
|
console.log(`[monero] closing current wallet`)
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
await this.wallet.close()
|
await state.wallet.close()
|
||||||
console.log(`[monero] wallet closed`)
|
console.log(`[monero] wallet closed`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBalance(): Promise<{ balance: bigint; unlocked: bigint }> {
|
export async function getBalance(state: MoneroWalletState): Promise<{ balance: bigint; unlocked: bigint }> {
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
const bal = (await this.wallet.getBalance()) as bigint
|
const balance = await state.wallet.getBalance() as bigint
|
||||||
const unlocked =
|
const unlocked = await state.wallet.getUnlockedBalance() as bigint
|
||||||
(await this.wallet.getUnlockedBalance()) as bigint
|
console.log(`[monero] balance: ${balance} (unlocked: ${unlocked})`)
|
||||||
console.log(`[monero] balance: ${bal} (unlocked: ${unlocked})`)
|
return { balance, unlocked }
|
||||||
return { balance: bal, unlocked }
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async getAddress(accountIdx = 0, subIdx = 0): Promise<string> {
|
export async function getAddress(state: MoneroWalletState, accountIdx = 0, subIdx = 0): Promise<string> {
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
const address = (await this.wallet.getAddress(
|
const address = await state.wallet.getAddress(accountIdx, subIdx) as string
|
||||||
accountIdx,
|
|
||||||
subIdx,
|
|
||||||
)) as string
|
|
||||||
console.log(`[monero] address: ${address}`)
|
console.log(`[monero] address: ${address}`)
|
||||||
return address
|
return address
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAccounts(): Promise<any[]> {
|
export async function getAccounts(state: MoneroWalletState): Promise<any[]> {
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
const accounts = (await this.wallet.getAccounts(true)) as any[]
|
const accounts = await state.wallet.getAccounts(true) as any[]
|
||||||
console.log(
|
console.log(`[monero] accounts: ${accounts?.length ?? 0} returned`)
|
||||||
`[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(
|
console.log(`[monero] account ${acct.getIndex()}: balance=${acct.getBalance()?.toString() ?? '0'} subs=${subs.length} primary=${(acct.getPrimaryAddress() ?? '').substring(0, 16)}...`)
|
||||||
`[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(
|
console.log(`[monero] sub ${sub.getIndex()}: addr=${(sub.getAddress() ?? '').substring(0, 16)}... label=${sub.getLabel() ?? ''} balance=${sub.getBalance()?.toString() ?? '0'} used=${sub.getIsUsed()}`)
|
||||||
`[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[]> {
|
export async function getTransactions(state: MoneroWalletState, accountIndex?: number): Promise<any[]> {
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
const params: Record<string, any> = {
|
const params: Record<string, any> = { in: true, out: true, pending: false, pool: false }
|
||||||
in: true,
|
if (accountIndex !== undefined) params.account_index = accountIndex
|
||||||
out: true,
|
const result = await rawRpc(state, 'get_transfers', params)
|
||||||
pending: false,
|
|
||||||
pool: false,
|
|
||||||
}
|
|
||||||
if (accountIndex !== undefined) {
|
|
||||||
params.account_index = accountIndex
|
|
||||||
}
|
|
||||||
const result = await this.rawRpc('get_transfers', params)
|
|
||||||
const all: any[] = []
|
const all: any[] = []
|
||||||
for (const dir of ['in', 'out'] as const) {
|
for (const dir of ['in', 'out'] as const) {
|
||||||
const transfers = result[dir] ?? []
|
const transfers = result[dir] ?? []
|
||||||
@@ -294,45 +218,28 @@ export class MoneroWalletManager {
|
|||||||
hash: t.txid,
|
hash: t.txid,
|
||||||
amount: t.amount?.toString() ?? '0',
|
amount: t.amount?.toString() ?? '0',
|
||||||
timestamp: String(t.timestamp ?? 0),
|
timestamp: String(t.timestamp ?? 0),
|
||||||
direction:
|
direction: dir === 'in' ? ('in' as const) : ('out' as const),
|
||||||
dir === 'in' ? ('in' as const) : ('out' as const),
|
|
||||||
height: t.height ?? 0,
|
height: t.height ?? 0,
|
||||||
note: t.note,
|
note: t.note,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
all.sort(
|
all.sort((a: any, b: any) => Number(b.timestamp) - Number(a.timestamp))
|
||||||
(a: any, b: any) => Number(b.timestamp) - Number(a.timestamp),
|
console.log(`[monero] transactions: ${all.length} returned${accountIndex !== undefined ? ` for account ${accountIndex}` : ''}`)
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
`[monero] transactions: ${all.length} returned${accountIndex !== undefined ? ` for account ${accountIndex}` : ''}`,
|
|
||||||
)
|
|
||||||
return all
|
return all
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTransferDetails(txid: string): Promise<any> {
|
export async function getTransferDetails(state: MoneroWalletState, txid: string): Promise<any> {
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
const result = await this.rawRpc('get_transfer_by_txid', { txid })
|
const result = await rawRpc(state, 'get_transfer_by_txid', { txid })
|
||||||
if (!result?.transfer) return null
|
if (!result?.transfer) return null
|
||||||
const t = result.transfer
|
const t = result.transfer
|
||||||
const isIn = t.type === 'in'
|
const isIn = t.type === 'in'
|
||||||
const isOut = t.type === 'out'
|
const isOut = t.type === 'out'
|
||||||
const dests =
|
const dests = t.destinations?.map((d: any) => ({ address: d.address, amount: d.amount?.toString() ?? '0' })) ?? []
|
||||||
t.destinations?.map((d: any) => ({
|
|
||||||
address: d.address,
|
|
||||||
amount: d.amount?.toString() ?? '0',
|
|
||||||
})) ?? []
|
|
||||||
const subaddrIndices = isIn
|
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 }))
|
||||||
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 {
|
return {
|
||||||
hash: t.txid,
|
hash: t.txid,
|
||||||
direction: isIn ? ('in' as const) : ('out' as const),
|
direction: isIn ? ('in' as const) : ('out' as const),
|
||||||
@@ -349,78 +256,56 @@ export class MoneroWalletManager {
|
|||||||
destinations: dests,
|
destinations: dests,
|
||||||
subaddrIndices,
|
subaddrIndices,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getHeight(): Promise<number> {
|
export async function getHeight(state: MoneroWalletState): Promise<number> {
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
const height = (await this.wallet.getHeight()) as number
|
const height = await state.wallet.getHeight() as number
|
||||||
console.log(`[monero] wallet height: ${height}`)
|
console.log(`[monero] wallet height: ${height}`)
|
||||||
return height
|
return height
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFeeEstimate(): Promise<{
|
export async function getFeeEstimate(state: MoneroWalletState): Promise<{ fee: string; fees: string[]; estimatedFee: string }> {
|
||||||
fee: string
|
|
||||||
fees: string[]
|
|
||||||
estimatedFee: string
|
|
||||||
}> {
|
|
||||||
try {
|
try {
|
||||||
const conn = new MoneroRpcConnection({
|
const conn = new MoneroRpcConnection({ uri: `http://${state.daemonAddress}` })
|
||||||
uri: `http://${this.daemonAddress}`,
|
const res = await conn.sendJsonRequest('get_fee_estimate', { grace_blocks: 10 })
|
||||||
})
|
|
||||||
const res = await conn.sendJsonRequest('get_fee_estimate', {
|
|
||||||
grace_blocks: 10,
|
|
||||||
})
|
|
||||||
if (res?.error) throw new Error(res.error.message)
|
if (res?.error) throw new Error(res.error.message)
|
||||||
console.log('[rpc][get_fee_estimate][response]', res)
|
console.log('[rpc][get_fee_estimate][response]', res)
|
||||||
const result = res?.result
|
const result = res?.result
|
||||||
const txSizeKb = 2.5
|
const txSizeKb = 2.5
|
||||||
const feePerKb = BigInt(result.fee ?? '0')
|
const feePerKb = BigInt(result.fee ?? '0')
|
||||||
const fees = (result.fees ?? []).map((f: string) =>
|
const fees = (result.fees ?? []).map((f: string) => BigInt(f).toString())
|
||||||
BigInt(f).toString(),
|
const estimated = (feePerKb * BigInt(Math.round(txSizeKb * 10))) / 10n
|
||||||
)
|
return { fee: feePerKb.toString(), fees, estimatedFee: estimated.toString() }
|
||||||
const estimated =
|
|
||||||
(feePerKb * BigInt(Math.round(txSizeKb * 10))) / 10n
|
|
||||||
return {
|
|
||||||
fee: feePerKb.toString(),
|
|
||||||
fees,
|
|
||||||
estimatedFee: estimated.toString(),
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('[monero] getFeeEstimate error:', e)
|
console.log('[monero] getFeeEstimate error:', e)
|
||||||
return {
|
return { fee: '0', fees: ['0', '0', '0', '0'], estimatedFee: '0' }
|
||||||
fee: '0',
|
|
||||||
fees: ['0', '0', '0', '0'],
|
|
||||||
estimatedFee: '0',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getDaemonHeight(): Promise<number> {
|
export async function getDaemonHeight(state: MoneroWalletState): Promise<number> {
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
try {
|
try {
|
||||||
const daemon = await connectToDaemonRpc(
|
const daemon = await connectToDaemonRpc(`http://${state.daemonAddress}`)
|
||||||
`http://${this.daemonAddress}`,
|
const height = await daemon.getHeight() as number
|
||||||
)
|
|
||||||
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) {
|
||||||
console.log(`[monero] failed to get daemon height:`, e)
|
console.log(`[monero] failed to get daemon height:`, e)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async transfer(
|
export async function transfer(
|
||||||
|
state: MoneroWalletState,
|
||||||
address: string,
|
address: string,
|
||||||
amount: bigint,
|
amount: bigint,
|
||||||
priority: number = 0,
|
priority: number = 0,
|
||||||
accountIndex: number = 0,
|
accountIndex: number = 0,
|
||||||
): Promise<{ txHash: string; fee: string; amount: string }> {
|
): Promise<{ txHash: string; fee: string; amount: string }> {
|
||||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
console.log(
|
console.log(`[monero] transfer: ${address} amount=${amount.toString()} priority=${priority} account=${accountIndex}`)
|
||||||
`[monero] transfer: ${address} amount=${amount.toString()} priority=${priority} account=${accountIndex}`,
|
const result = await rawRpc(state, 'transfer', {
|
||||||
)
|
|
||||||
const result = await this.rawRpc('transfer', {
|
|
||||||
destinations: [{ address, amount: amount.toString() }],
|
destinations: [{ address, amount: amount.toString() }],
|
||||||
priority,
|
priority,
|
||||||
account_index: accountIndex,
|
account_index: accountIndex,
|
||||||
@@ -428,37 +313,36 @@ export class MoneroWalletManager {
|
|||||||
get_tx_hex: false,
|
get_tx_hex: false,
|
||||||
get_tx_metadata: false,
|
get_tx_metadata: false,
|
||||||
})
|
})
|
||||||
console.log(
|
console.log(`[monero] transfer result: tx_hash=${result.tx_hash} fee=${result.fee} amount=${result.amount}`)
|
||||||
`[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() }
|
||||||
)
|
}
|
||||||
return {
|
|
||||||
txHash: result.tx_hash,
|
|
||||||
fee: result.fee?.toString() ?? '0',
|
|
||||||
amount: result.amount?.toString() ?? amount.toString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async isWalletOpen(): Promise<boolean> {
|
export function listWallets(): string[] {
|
||||||
if (!this.wallet) return false
|
const dir = getWalletDir()
|
||||||
|
if (!existsSync(dir)) return []
|
||||||
|
return readdirSync(dir)
|
||||||
|
.filter((f) => f.endsWith('.keys'))
|
||||||
|
.map((f) => f.replace(/\.keys$/, ''))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isWalletOpen(state: MoneroWalletState): Promise<boolean> {
|
||||||
|
if (!state.wallet) return false
|
||||||
try {
|
try {
|
||||||
await this.wallet.getHeight()
|
await state.wallet.getHeight()
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async isConnected(): Promise<boolean> {
|
export async function isConnected(state: MoneroWalletState): Promise<boolean> {
|
||||||
if (!this.wallet) return false
|
if (!state.wallet) return false
|
||||||
try {
|
try {
|
||||||
const daemon = await connectToDaemonRpc(
|
const daemon = await connectToDaemonRpc(`http://${state.daemonAddress}`)
|
||||||
`http://${this.daemonAddress}`,
|
|
||||||
)
|
|
||||||
await daemon.getHeight()
|
await daemon.getHeight()
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`[monero] connection check failed:`, e)
|
console.log(`[monero] connection check failed:`, e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
import {
|
import {
|
||||||
isBinaryInstalled,
|
isBinaryInstalled,
|
||||||
downloadBinary,
|
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'
|
} from '../../lib/monero'
|
||||||
|
import type { MoneroWalletState } from '../../lib/monero'
|
||||||
|
|
||||||
export function createMoneroHandlers(state: {
|
export function createMoneroHandlers(state: { manager: MoneroWalletState | null; downloading: boolean }) {
|
||||||
manager: MoneroWalletManager | null
|
|
||||||
downloading: boolean
|
|
||||||
}) {
|
|
||||||
return {
|
return {
|
||||||
moneroBinaryStatus: async () => {
|
moneroBinaryStatus: async () => {
|
||||||
const status = {
|
const status = { installed: isBinaryInstalled(), downloading: state.downloading }
|
||||||
installed: isBinaryInstalled(),
|
|
||||||
downloading: state.downloading,
|
|
||||||
error: undefined,
|
|
||||||
}
|
|
||||||
console.log('[rpc] moneroBinaryStatus:', status)
|
console.log('[rpc] moneroBinaryStatus:', status)
|
||||||
return status
|
return status
|
||||||
},
|
},
|
||||||
@@ -31,117 +41,62 @@ export function createMoneroHandlers(state: {
|
|||||||
state.downloading = false
|
state.downloading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
moneroStart: async ({
|
moneroStart: async ({ daemonAddress }: { daemonAddress?: string }) => {
|
||||||
daemonAddress,
|
|
||||||
}: {
|
|
||||||
daemonAddress?: string
|
|
||||||
}) => {
|
|
||||||
console.log('[rpc] moneroStart:', { daemonAddress })
|
console.log('[rpc] moneroStart:', { daemonAddress })
|
||||||
if (!state.manager) {
|
if (!state.manager) {
|
||||||
state.manager = new MoneroWalletManager()
|
state.manager = createMoneroWalletState()
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await state.manager.start(daemonAddress)
|
await walletStart(state.manager, daemonAddress)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('[rpc] moneroStart error:', e)
|
console.log('[rpc] moneroStart error:', e)
|
||||||
return { running: false, walletOpen: false, connected: false }
|
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)
|
console.log('[rpc] moneroStart complete, connected:', connected)
|
||||||
return { running: true, walletOpen: false, connected }
|
return { running: true, walletOpen: false, connected }
|
||||||
},
|
},
|
||||||
moneroStop: async () => {
|
moneroStop: async () => {
|
||||||
console.log('[rpc] moneroStop')
|
console.log('[rpc] moneroStop')
|
||||||
if (state.manager) {
|
if (state.manager) {
|
||||||
await state.manager.stop()
|
await walletStop(state.manager)
|
||||||
state.manager = null
|
state.manager = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
moneroCreateWallet: async ({
|
moneroCreateWallet: async ({ name, password }: { name: string; password: string }) => {
|
||||||
name,
|
|
||||||
password,
|
|
||||||
}: {
|
|
||||||
name: string
|
|
||||||
password: string
|
|
||||||
}) => {
|
|
||||||
console.log('[rpc] moneroCreateWallet:', name)
|
console.log('[rpc] moneroCreateWallet:', name)
|
||||||
if (!state.manager)
|
if (!state.manager) throw new Error('Monero wallet RPC not started')
|
||||||
throw new Error('Monero wallet RPC not started')
|
const result = await walletCreate(state.manager, name, password)
|
||||||
const result = await state.manager.createWallet(name, password)
|
|
||||||
console.log('[rpc] moneroCreateWallet complete')
|
console.log('[rpc] moneroCreateWallet complete')
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
moneroRestoreWallet: async ({
|
moneroRestoreWallet: async ({ name, password, mnemonic, restoreHeight }: { name: string; password: string; mnemonic: string; restoreHeight?: number }) => {
|
||||||
name,
|
|
||||||
password,
|
|
||||||
mnemonic,
|
|
||||||
restoreHeight,
|
|
||||||
}: {
|
|
||||||
name: string
|
|
||||||
password: string
|
|
||||||
mnemonic: string
|
|
||||||
restoreHeight?: number
|
|
||||||
}) => {
|
|
||||||
console.log('[rpc] moneroRestoreWallet:', name)
|
console.log('[rpc] moneroRestoreWallet:', name)
|
||||||
if (!state.manager)
|
if (!state.manager) throw new Error('Monero wallet RPC not started')
|
||||||
throw new Error('Monero wallet RPC not started')
|
await walletRestore(state.manager, name, password, mnemonic, restoreHeight)
|
||||||
await state.manager.restoreWallet(
|
const address = await walletGetAddress(state.manager)
|
||||||
name,
|
console.log('[rpc] moneroRestoreWallet complete, address:', address)
|
||||||
password,
|
|
||||||
mnemonic,
|
|
||||||
restoreHeight,
|
|
||||||
)
|
|
||||||
const address = await state.manager.getAddress()
|
|
||||||
console.log(
|
|
||||||
'[rpc] moneroRestoreWallet complete, address:',
|
|
||||||
address,
|
|
||||||
)
|
|
||||||
return { address }
|
return { address }
|
||||||
},
|
},
|
||||||
moneroOpenWallet: async ({
|
moneroOpenWallet: async ({ name, password }: { name: string; password: string }) => {
|
||||||
name,
|
|
||||||
password,
|
|
||||||
}: {
|
|
||||||
name: string
|
|
||||||
password: string
|
|
||||||
}) => {
|
|
||||||
console.log('[rpc] moneroOpenWallet:', name)
|
console.log('[rpc] moneroOpenWallet:', name)
|
||||||
if (!state.manager)
|
if (!state.manager) throw new Error('Monero wallet RPC not started')
|
||||||
throw new Error('Monero wallet RPC not started')
|
await walletOpen(state.manager, name, password)
|
||||||
await state.manager.openWallet(name, password)
|
|
||||||
},
|
},
|
||||||
moneroGetBalance: async () => {
|
moneroGetBalance: async () => {
|
||||||
console.log('[rpc] moneroGetBalance')
|
console.log('[rpc] moneroGetBalance')
|
||||||
if (!state.manager)
|
if (!state.manager) throw new Error('Monero wallet RPC not started')
|
||||||
throw new Error('Monero wallet RPC not started')
|
const { balance, unlocked } = await walletGetBalance(state.manager)
|
||||||
const { balance, unlocked } = await state.manager.getBalance()
|
const address = await walletGetAddress(state.manager)
|
||||||
const address = await state.manager.getAddress()
|
const height = await walletGetHeight(state.manager)
|
||||||
const height = await state.manager.getHeight()
|
const daemonHeight = await walletGetDaemonHeight(state.manager)
|
||||||
const daemonHeight = await state.manager.getDaemonHeight()
|
console.log('[rpc] balance:', { balance: balance.toString(), unlocked: unlocked.toString(), address, height, daemonHeight })
|
||||||
console.log('[rpc] balance:', {
|
return { balance: balance.toString(), unlocked: unlocked.toString(), address, height, daemonHeight }
|
||||||
balance: balance.toString(),
|
|
||||||
unlocked: unlocked.toString(),
|
|
||||||
address,
|
|
||||||
height,
|
|
||||||
daemonHeight,
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
balance: balance.toString(),
|
|
||||||
unlocked: unlocked.toString(),
|
|
||||||
address,
|
|
||||||
height,
|
|
||||||
daemonHeight,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
moneroGetTransactions: async ({
|
moneroGetTransactions: async ({ accountIndex }: { accountIndex?: number }) => {
|
||||||
accountIndex,
|
|
||||||
}: {
|
|
||||||
accountIndex?: number
|
|
||||||
}) => {
|
|
||||||
console.log('[rpc] moneroGetTransactions', { accountIndex })
|
console.log('[rpc] moneroGetTransactions', { accountIndex })
|
||||||
if (!state.manager)
|
if (!state.manager) throw new Error('Monero wallet RPC not started')
|
||||||
throw new Error('Monero wallet RPC not started')
|
const txs = await walletGetTransactions(state.manager, accountIndex)
|
||||||
const txs = await state.manager.getTransactions(accountIndex)
|
|
||||||
console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`)
|
console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`)
|
||||||
return txs
|
return txs
|
||||||
},
|
},
|
||||||
@@ -150,85 +105,49 @@ export function createMoneroHandlers(state: {
|
|||||||
console.log('[rpc] moneroWalletStatus: not running')
|
console.log('[rpc] moneroWalletStatus: not running')
|
||||||
return { running: false, walletOpen: false, connected: false }
|
return { running: false, walletOpen: false, connected: false }
|
||||||
}
|
}
|
||||||
const connected = await state.manager.isConnected()
|
const connected = await isConnected(state.manager)
|
||||||
const walletOpen = await state.manager.isWalletOpen()
|
const walletOpen = await isWalletOpen(state.manager)
|
||||||
console.log(
|
console.log('[rpc] moneroWalletStatus: running, walletOpen:', walletOpen, 'connected:', connected)
|
||||||
'[rpc] moneroWalletStatus: running, walletOpen:',
|
|
||||||
walletOpen,
|
|
||||||
'connected:',
|
|
||||||
connected,
|
|
||||||
)
|
|
||||||
return { running: true, walletOpen, connected }
|
return { running: true, walletOpen, connected }
|
||||||
},
|
},
|
||||||
moneroGetAccounts: async () => {
|
moneroGetAccounts: async () => {
|
||||||
console.log('[rpc] moneroGetAccounts')
|
console.log('[rpc] moneroGetAccounts')
|
||||||
if (!state.manager)
|
if (!state.manager) throw new Error('Monero wallet RPC not started')
|
||||||
throw new Error('Monero wallet RPC not started')
|
const accounts = await walletGetAccounts(state.manager)
|
||||||
const accounts = await state.manager.getAccounts()
|
console.log(`[rpc] moneroGetAccounts: ${accounts.length} accounts`)
|
||||||
console.log(
|
|
||||||
`[rpc] moneroGetAccounts: ${accounts.length} accounts`,
|
|
||||||
)
|
|
||||||
return accounts
|
return accounts
|
||||||
},
|
},
|
||||||
moneroListWallets: async () => {
|
moneroListWallets: async () => {
|
||||||
console.log('[rpc] moneroListWallets')
|
console.log('[rpc] moneroListWallets')
|
||||||
if (!state.manager)
|
if (!state.manager) throw new Error('Monero wallet RPC not started')
|
||||||
throw new Error('Monero wallet RPC not started')
|
const wallets = listWallets()
|
||||||
const wallets = state.manager.listWallets()
|
console.log(`[rpc] moneroListWallets: ${wallets.join(', ') || 'none'}`)
|
||||||
console.log(
|
|
||||||
`[rpc] moneroListWallets: ${wallets.join(', ') || 'none'}`,
|
|
||||||
)
|
|
||||||
return wallets
|
return wallets
|
||||||
},
|
},
|
||||||
moneroTransfer: async ({
|
moneroTransfer: async ({ address, amount, priority, accountIndex }: { address: string; amount: string; priority?: number; accountIndex?: number }) => {
|
||||||
address,
|
console.log('[rpc] moneroTransfer:', { address, amount, priority })
|
||||||
amount,
|
if (!state.manager) throw new Error('Monero wallet RPC not started')
|
||||||
priority,
|
const result = await walletTransfer(state.manager, address, BigInt(amount), priority ?? 0, accountIndex ?? 0)
|
||||||
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,
|
|
||||||
)
|
|
||||||
console.log('[rpc] moneroTransfer complete:', result.txHash)
|
console.log('[rpc] moneroTransfer complete:', result.txHash)
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
moneroGetTransferDetails: async ({ txid }: { txid: string }) => {
|
moneroGetTransferDetails: async ({ txid }: { txid: string }) => {
|
||||||
console.log('[rpc] moneroGetTransferDetails:', txid)
|
console.log('[rpc] moneroGetTransferDetails:', txid)
|
||||||
if (!state.manager)
|
if (!state.manager) throw new Error('Monero wallet RPC not started')
|
||||||
throw new Error('Monero wallet RPC not started')
|
const details = await walletGetTransferDetails(state.manager, txid)
|
||||||
const details = await state.manager.getTransferDetails(txid)
|
console.log('[rpc] moneroGetTransferDetails complete:', details?.hash)
|
||||||
console.log(
|
|
||||||
'[rpc] moneroGetTransferDetails complete:',
|
|
||||||
details?.hash,
|
|
||||||
)
|
|
||||||
return details
|
return details
|
||||||
},
|
},
|
||||||
moneroGetFeeEstimate: async () => {
|
moneroGetFeeEstimate: async () => {
|
||||||
console.log('[rpc] moneroGetFeeEstimate')
|
console.log('[rpc] moneroGetFeeEstimate')
|
||||||
if (!state.manager)
|
if (!state.manager) throw new Error('Monero wallet RPC not started')
|
||||||
throw new Error('Monero wallet RPC not started')
|
|
||||||
try {
|
try {
|
||||||
const fee = await state.manager.getFeeEstimate()
|
const fee = await walletGetFeeEstimate(state.manager)
|
||||||
console.log('[monero][gas][fee]', fee)
|
console.log('[monero][gas][fee]', fee)
|
||||||
return fee
|
return fee
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('[monero][gas][error]', error)
|
console.log('[monero][gas][error]', error)
|
||||||
return null
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { electrobun, type MoneroTxEntry, type MoneroAccountEntry, type MoneroSendResult } from '$lib/electrobun'
|
import {
|
||||||
|
electrobun,
|
||||||
|
type MoneroTxEntry,
|
||||||
|
type MoneroAccountEntry,
|
||||||
|
type MoneroSendResult,
|
||||||
|
} from '$lib/electrobun'
|
||||||
import { tryCatch } from '@koins/utils'
|
import { tryCatch } from '@koins/utils'
|
||||||
|
|
||||||
export const moneroNetwork = {
|
export const moneroNetwork = {
|
||||||
@@ -13,7 +18,10 @@ export const moneroNetwork = {
|
|||||||
export const atomicToXmr = (atomic: string): string => {
|
export const atomicToXmr = (atomic: string): string => {
|
||||||
const n = BigInt(atomic)
|
const n = BigInt(atomic)
|
||||||
const whole = n / 10_000_000_000_000n
|
const whole = n / 10_000_000_000_000n
|
||||||
const frac = (n % 10_000_000_000_000n).toString().padStart(12, '0').replace(/0+$/, '')
|
const frac = (n % 10_000_000_000_000n)
|
||||||
|
.toString()
|
||||||
|
.padStart(12, '0')
|
||||||
|
.replace(/0+$/, '')
|
||||||
return frac ? `${whole}.${frac}` : `${whole}`
|
return frac ? `${whole}.${frac}` : `${whole}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +60,9 @@ export const MoneroWallet = () => {
|
|||||||
const biometricAuth = async (): Promise<boolean> => {
|
const biometricAuth = async (): Promise<boolean> => {
|
||||||
if (!electrobun.rpc) return false
|
if (!electrobun.rpc) return false
|
||||||
const [authed] = await tryCatch(
|
const [authed] = await tryCatch(
|
||||||
electrobun.rpc.request.biometricAuth({ reason: 'Unlock wallet' }),
|
electrobun.rpc.request.biometricAuth({
|
||||||
|
reason: 'Unlock wallet',
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
return authed === true
|
return authed === true
|
||||||
}
|
}
|
||||||
@@ -81,17 +91,29 @@ export const MoneroWallet = () => {
|
|||||||
accountType = null
|
accountType = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const moneroGetStoredPassword = async (name: string): Promise<string | null> => {
|
const moneroGetStoredPassword = async (
|
||||||
|
name: string,
|
||||||
|
): Promise<string | null> => {
|
||||||
if (!electrobun.rpc) return null
|
if (!electrobun.rpc) return null
|
||||||
const [raw] = await tryCatch(
|
const [raw] = await tryCatch(
|
||||||
electrobun.rpc.request.getSecret({ service: 'koins', name: `monero_pw_${name}` }),
|
electrobun.rpc.request.getSecret({
|
||||||
|
service: 'koins',
|
||||||
|
name: `monero_pw_${name}`,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
return raw ?? null
|
return raw ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
const moneroStorePassword = async (name: string, password: string) => {
|
const moneroStorePassword = async (
|
||||||
|
name: string,
|
||||||
|
password: string,
|
||||||
|
) => {
|
||||||
if (!electrobun.rpc) return
|
if (!electrobun.rpc) return
|
||||||
await electrobun.rpc.request.setSecret({ service: 'koins', name: `monero_pw_${name}`, value: password })
|
await electrobun.rpc.request.setSecret({
|
||||||
|
service: 'koins',
|
||||||
|
name: `monero_pw_${name}`,
|
||||||
|
value: password,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchAccounts = async () => {
|
const fetchAccounts = async () => {
|
||||||
@@ -112,11 +134,17 @@ export const MoneroWallet = () => {
|
|||||||
wallets = result ?? []
|
wallets = result ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
const openExistingWallet = async (name: string, password?: string) => {
|
const openExistingWallet = async (
|
||||||
|
name: string,
|
||||||
|
password?: string,
|
||||||
|
) => {
|
||||||
if (!electrobun.rpc) throw new Error('RPC not available')
|
if (!electrobun.rpc) throw new Error('RPC not available')
|
||||||
const pw = password ?? await moneroGetStoredPassword(name)
|
const pw = password ?? (await moneroGetStoredPassword(name))
|
||||||
if (!pw) throw new Error('Password required')
|
if (!pw) throw new Error('Password required')
|
||||||
await electrobun.rpc.request.moneroOpenWallet({ name, password: pw })
|
await electrobun.rpc.request.moneroOpenWallet({
|
||||||
|
name,
|
||||||
|
password: pw,
|
||||||
|
})
|
||||||
walletName = name
|
walletName = name
|
||||||
walletOpen = true
|
walletOpen = true
|
||||||
await refresh()
|
await refresh()
|
||||||
@@ -173,9 +201,16 @@ export const MoneroWallet = () => {
|
|||||||
wallets = []
|
wallets = []
|
||||||
}
|
}
|
||||||
|
|
||||||
const createWallet = async (name: string, password: string, storePw?: boolean) => {
|
const createWallet = async (
|
||||||
|
name: string,
|
||||||
|
password: string,
|
||||||
|
storePw?: boolean,
|
||||||
|
) => {
|
||||||
if (!electrobun.rpc) throw new Error('RPC not available')
|
if (!electrobun.rpc) throw new Error('RPC not available')
|
||||||
const result = await electrobun.rpc.request.moneroCreateWallet({ name, password })
|
const result = await electrobun.rpc.request.moneroCreateWallet({
|
||||||
|
name,
|
||||||
|
password,
|
||||||
|
})
|
||||||
walletName = name
|
walletName = name
|
||||||
walletOpen = true
|
walletOpen = true
|
||||||
address = result.address
|
address = result.address
|
||||||
@@ -184,10 +219,19 @@ export const MoneroWallet = () => {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
const restoreWallet = async (name: string, password: string, mnemonic: string, restoreHeight?: number, storePw?: boolean) => {
|
const restoreWallet = async (
|
||||||
|
name: string,
|
||||||
|
password: string,
|
||||||
|
mnemonic: string,
|
||||||
|
restoreHeight?: number,
|
||||||
|
storePw?: boolean,
|
||||||
|
) => {
|
||||||
if (!electrobun.rpc) throw new Error('RPC not available')
|
if (!electrobun.rpc) throw new Error('RPC not available')
|
||||||
const result = await electrobun.rpc.request.moneroRestoreWallet({
|
const result = await electrobun.rpc.request.moneroRestoreWallet({
|
||||||
name, password, mnemonic, restoreHeight,
|
name,
|
||||||
|
password,
|
||||||
|
mnemonic,
|
||||||
|
restoreHeight,
|
||||||
})
|
})
|
||||||
walletName = name
|
walletName = name
|
||||||
walletOpen = true
|
walletOpen = true
|
||||||
@@ -250,34 +294,90 @@ export const MoneroWallet = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get accountType() { return accountType },
|
get accountType() {
|
||||||
get ready() { return ready },
|
return accountType
|
||||||
get biometricAvailable() { return biometricAvailable },
|
},
|
||||||
get running() { return running },
|
get ready() {
|
||||||
get walletOpen() { return walletOpen },
|
return ready
|
||||||
get connected() { return connected },
|
},
|
||||||
get balAtomic() { return balAtomic },
|
get biometricAvailable() {
|
||||||
get unlockedAtomic() { return unlockedAtomic },
|
return biometricAvailable
|
||||||
get address() { return address },
|
},
|
||||||
get height() { return height },
|
get running() {
|
||||||
get daemonHeight() { return daemonHeight },
|
return running
|
||||||
get txs() { return txs },
|
},
|
||||||
get installed() { return installed },
|
get walletOpen() {
|
||||||
get downloading() { return downloading },
|
return walletOpen
|
||||||
get walletName() { return walletName },
|
},
|
||||||
get accounts() { return accounts },
|
get connected() {
|
||||||
get wallets() { return wallets },
|
return connected
|
||||||
get loading() { return loading },
|
},
|
||||||
get error() { return error },
|
get balAtomic() {
|
||||||
get balance() { return atomicToXmr(balAtomic) },
|
return balAtomic
|
||||||
get unlocked() { return atomicToXmr(unlockedAtomic) },
|
},
|
||||||
set error(v: string) { error = v },
|
get unlockedAtomic() {
|
||||||
set installed(v: boolean) { installed = v },
|
return unlockedAtomic
|
||||||
set downloading(v: boolean) { downloading = v },
|
},
|
||||||
set running(v: boolean) { running = v },
|
get address() {
|
||||||
set connected(v: boolean) { connected = v },
|
return address
|
||||||
get selectedAccountIndex() { return selectedAccountIndex },
|
},
|
||||||
set selectedAccountIndex(v: number) { selectedAccountIndex = v },
|
get height() {
|
||||||
|
return height
|
||||||
|
},
|
||||||
|
get daemonHeight() {
|
||||||
|
return daemonHeight
|
||||||
|
},
|
||||||
|
get txs() {
|
||||||
|
return txs
|
||||||
|
},
|
||||||
|
get installed() {
|
||||||
|
return installed
|
||||||
|
},
|
||||||
|
get downloading() {
|
||||||
|
return downloading
|
||||||
|
},
|
||||||
|
get walletName() {
|
||||||
|
return walletName
|
||||||
|
},
|
||||||
|
get accounts() {
|
||||||
|
return accounts
|
||||||
|
},
|
||||||
|
get wallets() {
|
||||||
|
return wallets
|
||||||
|
},
|
||||||
|
get loading() {
|
||||||
|
return loading
|
||||||
|
},
|
||||||
|
get error() {
|
||||||
|
return error
|
||||||
|
},
|
||||||
|
get balance() {
|
||||||
|
return atomicToXmr(balAtomic)
|
||||||
|
},
|
||||||
|
get unlocked() {
|
||||||
|
return atomicToXmr(unlockedAtomic)
|
||||||
|
},
|
||||||
|
set error(v: string) {
|
||||||
|
error = v
|
||||||
|
},
|
||||||
|
set installed(v: boolean) {
|
||||||
|
installed = v
|
||||||
|
},
|
||||||
|
set downloading(v: boolean) {
|
||||||
|
downloading = v
|
||||||
|
},
|
||||||
|
set running(v: boolean) {
|
||||||
|
running = v
|
||||||
|
},
|
||||||
|
set connected(v: boolean) {
|
||||||
|
connected = v
|
||||||
|
},
|
||||||
|
get selectedAccountIndex() {
|
||||||
|
return selectedAccountIndex
|
||||||
|
},
|
||||||
|
set selectedAccountIndex(v: number) {
|
||||||
|
selectedAccountIndex = v
|
||||||
|
},
|
||||||
init,
|
init,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { evmWallet as wallet } from '$lib/states/evm-wallet.svelte.js'
|
import { evmWallet as wallet } from '$lib/states/evm-wallet.svelte.js'
|
||||||
|
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
|
||||||
import { Button } from '$lib/components/ui/button/index.js'
|
import { Button } from '$lib/components/ui/button/index.js'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
if (!initStarted) {
|
if (!initStarted) {
|
||||||
initStarted = true
|
initStarted = true
|
||||||
w.init()
|
w.init()
|
||||||
|
moneroWallet.init()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
|
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
|
||||||
|
import { Button } from '$lib/components/ui/button/index.js'
|
||||||
|
import { Input } from '$lib/components/ui/input/index.js'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '$lib/components/ui/card/index.js'
|
||||||
|
import Loader from '$lib/components/loader.svelte'
|
||||||
|
import { navigate } from 'sv-router/generated'
|
||||||
|
|
||||||
const w = moneroWallet
|
const w = moneroWallet
|
||||||
|
|
||||||
@@ -15,7 +26,9 @@
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Monero Settings</CardTitle>
|
<CardTitle>Monero Settings</CardTitle>
|
||||||
<CardDescription>Server and wallet preferences</CardDescription>
|
<CardDescription>
|
||||||
|
Server and wallet preferences
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
@@ -24,7 +37,10 @@
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onclick={async () => { await w.stop(); await w.start() }}
|
onclick={async () => {
|
||||||
|
await w.stop()
|
||||||
|
await w.start()
|
||||||
|
}}
|
||||||
disabled={w.loading}>
|
disabled={w.loading}>
|
||||||
{#if w.loading}
|
{#if w.loading}
|
||||||
<Loader />
|
<Loader />
|
||||||
@@ -34,23 +50,43 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if w.biometricAvailable}
|
{#if w.biometricAvailable}
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-medium mb-2">Save wallet password with Touch ID</p>
|
<p class="text-xs font-medium mb-2">
|
||||||
<p class="text-xs text-muted-foreground mb-3">Enter your wallet password to enable biometric unlock</p>
|
Save wallet password with Touch ID
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted-foreground mb-3">
|
||||||
|
Enter your wallet password to enable biometric unlock
|
||||||
|
</p>
|
||||||
<div class="flex gap-2 items-end">
|
<div class="flex gap-2 items-end">
|
||||||
<Input type="password" placeholder="Wallet password" bind:value={moneroSettingsPw} />
|
<Input
|
||||||
<Button size="sm" onclick={async () => {
|
type="password"
|
||||||
await w.moneroStorePassword(w.walletName, moneroSettingsPw)
|
placeholder="Wallet password"
|
||||||
|
bind:value={moneroSettingsPw} />
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onclick={async () => {
|
||||||
|
await w.moneroStorePassword(
|
||||||
|
w.walletName,
|
||||||
|
moneroSettingsPw,
|
||||||
|
)
|
||||||
moneroSettingsPw = ''
|
moneroSettingsPw = ''
|
||||||
w.error = ''
|
w.error = ''
|
||||||
}} disabled={!moneroSettingsPw || w.loading}>Save</Button>
|
}}
|
||||||
|
disabled={!moneroSettingsPw || w.loading}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-xs text-muted-foreground">Biometric authentication not available on this device</p>
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Biometric authentication not available on this device
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="pt-2 border-t border-border">
|
<div class="pt-2 border-t border-border">
|
||||||
<Button variant="outline" size="sm" onclick={() => navigate('/monero')}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => navigate('/monero')}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user