Files
koins/apps/desktop/src/bun/rpc/monero/index.ts
T

345 lines
9.1 KiB
TypeScript

import {
isBinaryInstalled,
downloadBinary,
createMoneroWalletState,
start as walletStart,
stop as walletStop,
createWallet as walletCreate,
restoreWallet as walletRestore,
openWallet as walletOpen,
getBalance as walletGetBalance,
getAddress as walletGetAddress,
getTransactions as walletGetTransactions,
getAccounts as walletGetAccounts,
listWallets,
getTransferDetails as walletGetTransferDetails,
getHeight as walletGetHeight,
getDaemonHeight as walletGetDaemonHeight,
getFeeEstimate as walletGetFeeEstimate,
transfer as walletTransfer,
sweepAll as walletSweepAll,
createAccount as walletCreateAccount,
createSubaddress as walletCreateSubaddress,
isWalletOpen,
isConnected,
} from '../../lib/monero'
import type { MoneroWalletState } from '../../lib/monero'
import { log, logError } from '../../lib/logger'
export function createMoneroHandlers(state: {
manager: MoneroWalletState | null
downloading: boolean
}) {
return {
moneroBinaryStatus: async () => {
const status = {
installed: isBinaryInstalled(),
downloading: state.downloading,
}
console.log('[rpc] moneroBinaryStatus:', status)
return status
},
moneroDownloadBinary: async () => {
console.log('[rpc] moneroDownloadBinary: starting download...')
state.downloading = true
try {
await downloadBinary()
console.log('[rpc] moneroDownloadBinary: complete')
} catch (e) {
console.log('[rpc] moneroDownloadBinary failed:', e)
throw e
} finally {
state.downloading = false
}
},
moneroStart: async ({
daemonAddress,
}: {
daemonAddress?: string
}) => {
console.log('[rpc] moneroStart:', { daemonAddress })
if (!state.manager) {
state.manager = createMoneroWalletState()
}
try {
await walletStart(state.manager, daemonAddress)
} catch (e) {
console.log('[rpc] moneroStart error:', e)
logError('[rpc] moneroStart error', e)
return { running: false, walletOpen: false, connected: false }
}
const connected = await isConnected(state.manager)
console.log('[rpc] moneroStart complete, connected:', connected)
log(`[rpc] moneroStart complete, connected: ${connected}`)
return { running: true, walletOpen: false, connected }
},
moneroStop: async () => {
console.log('[rpc] moneroStop')
if (state.manager) {
await walletStop(state.manager)
state.manager = null
}
},
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 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
}) => {
console.log('[rpc] moneroRestoreWallet:', name)
if (!state.manager)
throw new Error('Monero wallet RPC not started')
// Default to ~2 days of history instead of a full genesis scan
// (which can take hours against a remote daemon).
let height = restoreHeight
if (!height || height === 0) {
const tip = await walletGetDaemonHeight(state.manager)
if (tip > 1440) height = tip - 1440
}
await walletRestore(
state.manager,
name,
password,
mnemonic,
height,
)
const address = await walletGetAddress(state.manager)
console.log(
'[rpc] moneroRestoreWallet complete, address:',
address,
)
return { address }
},
moneroOpenWallet: async ({
name,
password,
}: {
name: string
password: string
}) => {
console.log('[rpc] moneroOpenWallet:', name)
if (!state.manager)
throw new Error('Monero wallet RPC not started')
try {
await walletOpen(state.manager, name, password)
log(`[rpc] moneroOpenWallet ok: ${name}`)
} catch (e) {
logError(`[rpc] moneroOpenWallet failed (${name})`, e)
throw e
}
},
moneroGetBalance: async () => {
console.log('[rpc] moneroGetBalance')
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
}) => {
console.log('[rpc] moneroGetTransactions', { 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
},
moneroWalletStatus: async () => {
if (!state.manager) {
console.log('[rpc] moneroWalletStatus: not running')
return { running: false, walletOpen: false, connected: false }
}
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 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 = 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 walletTransfer(
state.manager,
address,
BigInt(amount),
priority ?? 0,
accountIndex ?? 0,
)
console.log('[rpc] moneroTransfer complete:', result.txHash)
return result
},
moneroSweepAll: async ({
address,
priority,
accountIndex,
}: {
address: string
priority?: number
accountIndex?: number
}) => {
console.log('[rpc] moneroSweepAll:', {
address,
priority,
})
if (!state.manager)
throw new Error('Monero wallet RPC not started')
const result = await walletSweepAll(
state.manager,
address,
priority ?? 0,
accountIndex ?? 0,
)
console.log('[rpc] moneroSweepAll complete:', result.txHash)
return result
},
moneroCreateAccount: async ({ label }: { label?: string }) => {
console.log('[rpc] moneroCreateAccount:', { label })
if (!state.manager) throw new Error('Monero wallet RPC not started')
const result = await walletCreateAccount(state.manager, label)
console.log('[rpc] moneroCreateAccount complete:', result)
return result
},
moneroCreateSubaddress: async ({
accountIndex,
label,
}: {
accountIndex: number
label?: string
}) => {
console.log('[rpc] moneroCreateSubaddress:', { accountIndex, label })
if (!state.manager) throw new Error('Monero wallet RPC not started')
const result = await walletCreateSubaddress(
state.manager,
accountIndex,
label,
)
console.log('[rpc] moneroCreateSubaddress complete:', result)
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 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')
try {
const fee = await walletGetFeeEstimate(state.manager)
console.log('[monero][gas][fee]', fee)
return fee
} catch (error) {
console.log('[monero][gas][error]', error)
return null
}
},
fetchMoneroPrice: async () => {
try {
const res = await fetch(
'https://api.coingecko.com/api/v3/simple/price?ids=monero&vs_currencies=usd',
{ signal: AbortSignal.timeout(10000) },
)
if (!res.ok) return null
const data = await res.json()
if (!data?.monero?.usd) return null
return { usd: String(data.monero.usd) }
} catch (error) {
console.log('[monero][price] error:', error)
return null
}
},
}
}