import { electrobun, type MoneroTxEntry, type MoneroAccountEntry, type MoneroSendResult, } from '$lib/electrobun' import { tryCatch } from '@koins/utils' export const moneroNetwork = { id: 'monero', name: 'Monero', rpc: '', symbol: 'XMR', chainid: '', explorerUrl: 'https://moneroblocks.info/tx/', } as const export const atomicToXmr = (atomic: string): string => { const n = BigInt(atomic) const whole = n / 10_000_000_000_000n const frac = (n % 10_000_000_000_000n) .toString() .padStart(12, '0') .replace(/0+$/, '') return frac ? `${whole}.${frac}` : `${whole}` } export const MoneroWallet = () => { let accountType = $state<'monero' | null>(null) let ready = $state(false) let biometricAvailable = $state(false) let running = $state(false) let walletOpen = $state(false) let connected = $state(false) let balAtomic = $state('0') let unlockedAtomic = $state('0') let address = $state('') let height = $state(0) let daemonHeight = $state(0) let txs = $state([]) let installed = $state(false) let downloading = $state(false) let walletName = $state('') let accounts = $state([]) let wallets = $state([]) let loading = $state(false) let error = $state('') let selectedAccountIndex = $state(0) const checkBiometric = async () => { if (!electrobun.rpc) return const [ok] = await tryCatch( electrobun.rpc.request.biometricCanAuth({}), ) biometricAvailable = ok === true } const biometricAuth = async (): Promise => { if (!electrobun.rpc) return false const [authed] = await tryCatch( electrobun.rpc.request.biometricAuth({ reason: 'Unlock wallet', }), ) return authed === true } const init = async () => { await checkStatus() if (installed && !running && !downloading) { await start() await checkStatus() } await checkBiometric() ready = true } const login = async () => { accountType = 'monero' if (installed && !running) { await start() await checkStatus() } await listWallets() } const logout = async () => { await stop() accountType = null } const moneroGetStoredPassword = async ( name: string, ): Promise => { if (!electrobun.rpc) return null const [raw] = await tryCatch( electrobun.rpc.request.getSecret({ service: 'koins', name: `monero_pw_${name}`, }), ) return raw ?? null } const moneroStorePassword = async ( name: string, password: string, ) => { if (!electrobun.rpc) return await electrobun.rpc.request.setSecret({ service: 'koins', name: `monero_pw_${name}`, value: password, }) } const fetchAccounts = async () => { if (!electrobun.rpc) return false const [result, err] = await tryCatch( electrobun.rpc.request.moneroGetAccounts({}), ) if (err) return false accounts = result ?? [] return true } const listWallets = async () => { if (!electrobun.rpc) return const [result] = await tryCatch( electrobun.rpc.request.moneroListWallets({}), ) wallets = result ?? [] } const openExistingWallet = async ( name: string, password?: string, ) => { if (!electrobun.rpc) throw new Error('RPC not available') const pw = password ?? (await moneroGetStoredPassword(name)) if (!pw) throw new Error('Password required') await electrobun.rpc.request.moneroOpenWallet({ name, password: pw, }) walletName = name walletOpen = true await refresh() } const checkStatus = async () => { if (!electrobun.rpc) return const [binStatus] = await tryCatch( electrobun.rpc.request.moneroBinaryStatus({}), ) if (binStatus) { installed = binStatus.installed downloading = binStatus.downloading } const [status] = await tryCatch( electrobun.rpc.request.moneroWalletStatus({}), ) if (status) { running = status.running walletOpen = status.walletOpen connected = status.connected } } const download = async () => { if (!electrobun.rpc) return downloading = true try { await electrobun.rpc.request.moneroDownloadBinary({}) installed = true } finally { downloading = false } } const start = async () => { if (!electrobun.rpc) return const status = await electrobun.rpc.request.moneroStart({}) running = status.running connected = status.connected } const stop = async () => { if (!electrobun.rpc) return await electrobun.rpc.request.moneroStop({}) running = false walletOpen = false connected = false address = '' balAtomic = '0' unlockedAtomic = '0' txs = [] accounts = [] wallets = [] } const createWallet = async ( name: string, password: string, storePw?: boolean, ) => { if (!electrobun.rpc) throw new Error('RPC not available') const result = await electrobun.rpc.request.moneroCreateWallet({ name, password, }) walletName = name walletOpen = true address = result.address if (storePw) await moneroStorePassword(name, password) await refresh() return result } const restoreWallet = async ( name: string, password: string, mnemonic: string, restoreHeight?: number, storePw?: boolean, ) => { if (!electrobun.rpc) throw new Error('RPC not available') const result = await electrobun.rpc.request.moneroRestoreWallet({ name, password, mnemonic, restoreHeight, }) walletName = name walletOpen = true address = result.address if (storePw) await moneroStorePassword(name, password) await refresh() } const openWallet = async (name: string, password: string) => { if (!electrobun.rpc) return await electrobun.rpc.request.moneroOpenWallet({ name, password }) walletName = name walletOpen = true await refresh() } const refresh = async () => { if (!electrobun.rpc) return loading = true try { const [bal] = await tryCatch( electrobun.rpc.request.moneroGetBalance({}), ) if (bal) { balAtomic = bal.balance unlockedAtomic = bal.unlocked address = bal.address height = bal.height daemonHeight = bal.daemonHeight } const [result] = await tryCatch( electrobun.rpc.request.moneroGetTransactions({ accountIndex: selectedAccountIndex, }), ) txs = result ?? [] await fetchAccounts() } catch (e) { error = e instanceof Error ? e.message : 'Monero refresh failed' } finally { loading = false } } const send = async ( address: string, amountAtomic: string, priority?: number, accountIndex?: number, ): Promise => { if (!electrobun.rpc) throw new Error('RPC not available') const result = await electrobun.rpc.request.moneroTransfer({ address, amount: amountAtomic, priority, accountIndex, }) await refresh() return result } const sendAll = async ( address: string, priority?: number, accountIndex?: number, ): Promise => { if (!electrobun.rpc) throw new Error('RPC not available') const result = await electrobun.rpc.request.moneroSweepAll({ address, priority, accountIndex, }) await refresh() return result } const createAccount = async (label?: string) => { if (!electrobun.rpc) throw new Error('RPC not available') const result = await electrobun.rpc.request.moneroCreateAccount({ label, }) await refresh() return result } const createSubaddress = async ( accountIndex: number, label?: string, ) => { if (!electrobun.rpc) throw new Error('RPC not available') const result = await electrobun.rpc.request.moneroCreateSubaddress({ accountIndex, label, }) await refresh() return result } return { get accountType() { return accountType }, get ready() { return ready }, get biometricAvailable() { return biometricAvailable }, get running() { return running }, get walletOpen() { return walletOpen }, get connected() { return connected }, get balAtomic() { return balAtomic }, get unlockedAtomic() { return unlockedAtomic }, get address() { return address }, 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, login, logout, biometricAuth, moneroGetStoredPassword, moneroStorePassword, fetchAccounts, listWallets, openExistingWallet, checkStatus, download, start, stop, createWallet, restoreWallet, openWallet, refresh, send, sendAll, createAccount, createSubaddress, } } export const moneroWallet = MoneroWallet()