diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 196ef8f..a9a891a 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -20,6 +20,7 @@ "electrobun": "^1.18.1", "ethers": "^6.16.0", "mode-watcher": "^1.1.0", + "monero-ts": "^0.11.8", "sv-router": "latest", "viem": "^2.48.11" }, diff --git a/apps/desktop/src/bun/index.ts b/apps/desktop/src/bun/index.ts index 66bae5d..f80d7a9 100644 --- a/apps/desktop/src/bun/index.ts +++ b/apps/desktop/src/bun/index.ts @@ -21,6 +21,13 @@ import { db } from './lib/db' import { join } from 'path' import { getENV } from './lib/get-env' import { getClient, getTransactionDetails } from './lib/viem' +import { + isBinaryInstalled, + downloadBinary, + getBinaryPath, + getBinaryDir, + MoneroWalletManager, +} from './lib/monero' const env = getENV() @@ -169,6 +176,71 @@ type RPC = { } response: void } + moneroBinaryStatus: { + params: {} + response: { + installed: boolean + downloading: boolean + error?: string + } + } + moneroDownloadBinary: { + params: {} + response: void + } + moneroStart: { + params: { daemonAddress?: string } + response: { + running: boolean + walletOpen: boolean + walletName?: string + connected: boolean + } + } + moneroStop: { + params: {} + response: void + } + moneroCreateWallet: { + params: { name: string; password: string } + response: { mnemonic: string; address: string } + } + moneroRestoreWallet: { + params: { name: string; password: string; mnemonic: string; restoreHeight?: number } + response: { address: string } + } + moneroOpenWallet: { + params: { name: string; password: string } + response: void + } + moneroGetBalance: { + params: {} + response: { + balance: string + unlocked: string + address: string + height: number + } + } + moneroGetTransactions: { + params: {} + response: Array<{ + hash: string + amount: string + timestamp: string + direction: 'in' | 'out' + height: number + }> + } + moneroWalletStatus: { + params: {} + response: { + running: boolean + walletOpen: boolean + walletName?: string + connected: boolean + } + } } messages: {} }> @@ -274,8 +346,11 @@ function mapTransfer(t: any): TxEntry { } } +let moneroManager: MoneroWalletManager | null = null +let moneroDownloading = false + const rpc = BrowserView.defineRPC({ - maxRequestTime: 10000, + maxRequestTime: 120000, handlers: { requests: { getSecret: async ({ name, service }) => { @@ -441,6 +516,112 @@ const rpc = BrowserView.defineRPC({ return null } }, + moneroBinaryStatus: async () => { + const status = { installed: isBinaryInstalled(), downloading: moneroDownloading } + console.log('[rpc] moneroBinaryStatus:', status) + return status + }, + moneroDownloadBinary: async () => { + console.log('[rpc] moneroDownloadBinary: starting download...') + moneroDownloading = true + try { + await downloadBinary() + console.log('[rpc] moneroDownloadBinary: complete') + } catch (e) { + console.log('[rpc] moneroDownloadBinary failed:', e) + throw e + } finally { + moneroDownloading = false + } + }, + moneroStart: async ({ daemonAddress }) => { + console.log('[rpc] moneroStart:', { daemonAddress }) + if (!moneroManager) { + moneroManager = new MoneroWalletManager() + } + try { + await moneroManager.start(daemonAddress) + } catch (e) { + console.log('[rpc] moneroStart error:', e) + return { running: false, walletOpen: false, connected: false } + } + const connected = await moneroManager.isConnected() + console.log('[rpc] moneroStart complete, connected:', connected) + return { running: true, walletOpen: false, connected } + }, + moneroStop: async () => { + console.log('[rpc] moneroStop') + if (moneroManager) { + await moneroManager.stop() + moneroManager = null + } + }, + moneroCreateWallet: async ({ name, password }) => { + console.log('[rpc] moneroCreateWallet:', name) + if (!moneroManager) throw new Error('Monero wallet RPC not started') + const result = await moneroManager.createWallet(name, password) + console.log('[rpc] moneroCreateWallet complete') + return result + }, + moneroRestoreWallet: async ({ name, password, mnemonic, restoreHeight }) => { + console.log('[rpc] moneroRestoreWallet:', name) + if (!moneroManager) throw new Error('Monero wallet RPC not started') + await moneroManager.restoreWallet(name, password, mnemonic, restoreHeight) + const address = await moneroManager.getAddress() + console.log('[rpc] moneroRestoreWallet complete, address:', address) + return { address } + }, + moneroOpenWallet: async ({ name, password }) => { + console.log('[rpc] moneroOpenWallet:', name) + if (!moneroManager) throw new Error('Monero wallet RPC not started') + await moneroManager.openWallet(name, password) + }, + moneroGetBalance: async () => { + console.log('[rpc] moneroGetBalance') + if (!moneroManager) throw new Error('Monero wallet RPC not started') + const { balance, unlocked } = await moneroManager.getBalance() + const address = await moneroManager.getAddress() + const height = await moneroManager.getHeight() + const daemonHeight = await moneroManager.getDaemonHeight() + 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 () => { + console.log('[rpc] moneroGetTransactions') + if (!moneroManager) throw new Error('Monero wallet RPC not started') + const txs = await moneroManager.getTransactions() + const result = (txs || []).map((tx: any) => ({ + hash: tx.hash, + amount: tx.amount?.toString() ?? '0', + timestamp: String(tx.timestamp ?? 0), + direction: tx.direction === 'in' ? 'in' as const : 'out' as const, + height: tx.height ?? 0, + })) + console.log(`[rpc] moneroGetTransactions: ${result.length} txs`) + return result + }, + moneroWalletStatus: async () => { + if (!moneroManager) { + console.log('[rpc] moneroWalletStatus: not running') + return { running: false, walletOpen: false, connected: false } + } + const connected = await moneroManager.isConnected() + console.log('[rpc] moneroWalletStatus: running, connected:', connected) + return { running: true, walletOpen: true, connected } + }, + moneroGetAccounts: async () => { + console.log('[rpc] moneroGetAccounts') + if (!moneroManager) throw new Error('Monero wallet RPC not started') + const accounts = await moneroManager.getAccounts() + console.log(`[rpc] moneroGetAccounts: ${accounts.length} accounts`) + return accounts + }, }, messages: {}, }, diff --git a/apps/desktop/src/bun/lib/monero/binary.ts b/apps/desktop/src/bun/lib/monero/binary.ts new file mode 100644 index 0000000..418e561 --- /dev/null +++ b/apps/desktop/src/bun/lib/monero/binary.ts @@ -0,0 +1,110 @@ +import { existsSync, mkdirSync } from 'fs' +import { join } from 'path' +import envPaths from 'env-paths' +import { getENV } from '../get-env' + +const env = getENV() +const { data } = envPaths(env.appName, { suffix: '' }) + +export const MONERO_VERSION = '0.18.5.0' + +const PLATFORM_MAP: Record> = { + darwin: { arm64: 'mac-armv8', x64: 'mac-x64' }, + linux: { arm64: 'linux-armv8', x64: 'linux-x64' }, +} + +function getPlatform(): string { + const os = PLATFORM_MAP[process.platform] + if (!os) + throw new Error( + `Unsupported platform: ${process.platform} ${process.arch}`, + ) + const arch = os[process.arch] + if (!arch) + throw new Error( + `Unsupported arch: ${process.arch} on ${process.platform}`, + ) + return arch +} + +export function getDownloadUrl(): string { + return `https://downloads.getmonero.org/cli/monero-${getPlatform()}-v${MONERO_VERSION}.tar.bz2` +} + +export function getBinaryDir(): string { + const dir = join(data, 'monero-bin') + mkdirSync(dir, { recursive: true }) + return dir +} + +export function getBinaryPath(): string { + return join(getBinaryDir(), 'monero-wallet-rpc') +} + +export function getWalletDir(): string { + const dir = join(data, 'monero-wallets') + mkdirSync(dir, { recursive: true }) + return dir +} + +export function isBinaryInstalled(): boolean { + return existsSync(getBinaryPath()) +} + +export async function downloadBinary(): Promise { + const url = getDownloadUrl() + const archivePath = join( + getBinaryDir(), + `monero-v${MONERO_VERSION}.tar.bz2`, + ) + + console.log(`Downloading Monero CLI ${MONERO_VERSION}...`) + console.log(`URL: ${url}`) + const response = await fetch(url) + if (!response.ok) + throw new Error( + `Download failed: ${response.status} ${response.statusText}`, + ) + + const total = Number(response.headers.get('content-length') ?? 0) + console.log(`Download size: ${(total / 1024 / 1024).toFixed(1)} MB`) + + const reader = response.body?.getReader() + if (!reader) throw new Error('No response body') + + const chunks: Uint8Array[] = [] + let received = 0 + while (true) { + const { done, value } = await reader.read() + if (done) break + chunks.push(value) + received += value.length + const pct = total ? ((received / total) * 100).toFixed(0) : '?' + console.log( + `Download progress: ${pct}% (${(received / 1024 / 1024).toFixed(1)} MB)`, + ) + } + + const buffer = new Uint8Array(received) + let offset = 0 + for (const chunk of chunks) { + buffer.set(chunk, offset) + offset += chunk.length + } + + await Bun.write(archivePath, buffer) + console.log(`Downloaded to ${archivePath}`) + + console.log('Extracting...') + const result = Bun.spawnSync(['tar', '-xjf', archivePath, '--strip-components=1', '-C', getBinaryDir()]) + if (result.exitCode !== 0) throw new Error(`Extraction failed: ${result.stderr.toString()}`) + + Bun.spawnSync(['rm', archivePath]) + + const binaryDst = getBinaryPath() + if (!existsSync(binaryDst)) throw new Error(`monero-wallet-rpc not found after extraction`) + + Bun.spawnSync(['chmod', '+x', binaryDst]) + + console.log(`Installed monero-wallet-rpc at ${binaryDst}`) +} diff --git a/apps/desktop/src/bun/lib/monero/index.ts b/apps/desktop/src/bun/lib/monero/index.ts new file mode 100644 index 0000000..7ce2b1d --- /dev/null +++ b/apps/desktop/src/bun/lib/monero/index.ts @@ -0,0 +1,2 @@ +export * from './binary' +export { MoneroWalletManager } from './wallet' diff --git a/apps/desktop/src/bun/lib/monero/wallet.ts b/apps/desktop/src/bun/lib/monero/wallet.ts new file mode 100644 index 0000000..227615e --- /dev/null +++ b/apps/desktop/src/bun/lib/monero/wallet.ts @@ -0,0 +1,225 @@ +import { MoneroWalletRpc, MoneroRpcConnection, connectToWalletRpc, connectToDaemonRpc } from 'monero-ts' +import { getBinaryPath, getWalletDir } from './binary' +import { existsSync } from 'fs' + +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 + + constructor() { + this.rpcPort = 28084 + this.rpcUser = 'koins' + this.rpcPassword = crypto.randomUUID() + } + + private get rpcUrl() { + return `http://127.0.0.1:${this.rpcPort}` + } + + private get rpcConnection(): MoneroRpcConnection { + return new MoneroRpcConnection({ + uri: this.rpcUrl, + username: this.rpcUser, + password: this.rpcPassword, + }) + } + + async start(daemonAddress: string = DEFAULT_DAEMON) { + if (!existsSync(getBinaryPath())) 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 = {}): Promise { + 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 + } + + private async waitForRpc(timeoutMs = 30000): Promise { + 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}`) + } + + 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`) + } + + 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 } + } + + 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}`) + } + + 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}`) + } + + 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`) + } + + 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 } + } + + async getAddress(accountIdx = 0, subIdx = 0): Promise { + 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 + } + + async getAccounts(): Promise { + 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`) + return accounts?.map(a => a.toJson()) ?? [] + } + + async getTransactions(): Promise { + if (!this.wallet) throw new Error('Wallet RPC not started') + const txs = await this.wallet.getTxs() as any[] + console.log(`[monero] transactions: ${txs?.length ?? 0} returned`) + return txs ?? [] + } + + async getHeight(): Promise { + 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 + } + + async getDaemonHeight(): Promise { + 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 + } + } + + async isConnected(): Promise { + if (!this.wallet) return false + try { + await this.wallet.getHeight() + return true + } catch (e) { + console.log(`[monero] connection check failed:`, e) + return false + } + } +} diff --git a/apps/desktop/src/lib/electrobun.ts b/apps/desktop/src/lib/electrobun.ts index 3ef86f6..b207dec 100644 --- a/apps/desktop/src/lib/electrobun.ts +++ b/apps/desktop/src/lib/electrobun.ts @@ -1,5 +1,57 @@ import { Electroview } from 'electrobun/view' +export type MoneroBalance = { + balance: string + unlocked: string + address: string + height: number + daemonHeight: number +} + +export type MoneroTxEntry = { + hash: string + amount: string + timestamp: string + direction: 'in' | 'out' + height: number + note?: string +} + +export type MoneroBinaryStatus = { + installed: boolean + downloading: boolean + error?: string +} + +export type MoneroWalletStatus = { + running: boolean + walletOpen: boolean + walletName?: string + connected: boolean +} + +export type MoneroSubaddressEntry = { + accountIndex: number + index: number + address: string + label: string + balance: string + unlockedBalance: string + numUnspentOutputs: number + isUsed: boolean + numBlocksToUnlock: number +} + +export type MoneroAccountEntry = { + index: number + primaryAddress: string + label: string + balance: string + unlockedBalance: string + tag?: string + subaddresses: MoneroSubaddressEntry[] +} + export type TxEntry = { hash: string timeStamp: string @@ -82,6 +134,68 @@ type RPC = { } response: void } + moneroBinaryStatus: { + params: {} + response: MoneroBinaryStatus + } + moneroDownloadBinary: { + params: {} + response: void + } + moneroStart: { + params: { + daemonAddress?: string + } + response: MoneroWalletStatus + } + moneroStop: { + params: {} + response: void + } + moneroCreateWallet: { + params: { + name: string + password: string + } + response: { + mnemonic: string + address: string + } + } + moneroRestoreWallet: { + params: { + name: string + password: string + mnemonic: string + restoreHeight?: number + } + response: { + address: string + } + } + moneroOpenWallet: { + params: { + name: string + password: string + } + response: void + } + moneroGetBalance: { + params: {} + response: MoneroBalance + } + moneroGetTransactions: { + params: {} + response: MoneroTxEntry[] + } + moneroWalletStatus: { + params: {} + response: MoneroWalletStatus + } + moneroGetAccounts: { + params: {} + response: MoneroAccountEntry[] + } } messages: {} } diff --git a/apps/desktop/src/lib/states/wallet.svelte.ts b/apps/desktop/src/lib/states/wallet.svelte.ts index 8227092..7790c96 100644 --- a/apps/desktop/src/lib/states/wallet.svelte.ts +++ b/apps/desktop/src/lib/states/wallet.svelte.ts @@ -1,5 +1,5 @@ import { JsonRpcProvider, HDNodeWallet, formatEther } from 'ethers' -import { electrobun, type TxEntry } from '$lib/electrobun' +import { electrobun, type TxEntry, type MoneroTxEntry, type MoneroAccountEntry } from '$lib/electrobun' import { tryCatch } from '@koins/utils' export const networks = [ @@ -27,6 +27,14 @@ export const networks = [ chainid: '1', explorerUrl: 'https://etherscan.io/tx/', }, + { + id: 'monero', + name: 'Monero', + rpc: '', + symbol: 'XMR', + chainid: '', + explorerUrl: 'https://moneroblocks.info/tx/', + }, ] as const export type NetworkId = (typeof networks)[number]['id'] @@ -38,6 +46,13 @@ export type TokenBalance = { logo?: string } +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 Wallet = () => { let seed = $state('') let address = $state('') @@ -51,8 +66,24 @@ export const Wallet = () => { let apiKey = $state('') let transactions = $state([]) + let moneroRunning = $state(false) + let moneroWalletOpen = $state(false) + let moneroConnected = $state(false) + let moneroBalAtomic = $state('0') + let moneroUnlockedAtomic = $state('0') + let moneroAddress = $state('') + let moneroHeight = $state(0) + let moneroDaemonHeight = $state(0) + let moneroTxs = $state([]) + let moneroInstalled = $state(false) + let moneroDownloading = $state(false) + let moneroWalletName = $state('') + let moneroAccounts = $state([]) + const net = () => networks.find((n) => n.id === network)! + const isMonero = () => network === 'monero' + const init = async () => { if (!electrobun.rpc) return const [raw, rawError] = await tryCatch( @@ -95,7 +126,11 @@ export const Wallet = () => { const switchNetwork = async (id: NetworkId) => { network = id - if (seed) await refresh() + if (id === 'monero') { + await checkMoneroStatus() + } else if (seed) { + await refresh() + } } const saveVault = async (phrase: string) => { @@ -163,6 +198,19 @@ export const Wallet = () => { loading = false } + const moneroFetchAccounts = async () => { + if (!electrobun.rpc) return false + const [accounts, error] = await tryCatch( + electrobun.rpc.request.moneroGetAccounts({}), + ) + if (error) { + console.log('moneroFetchAccounts error:', error) + return false + } + moneroAccounts = accounts ?? [] + return true + } + const saveApiKey = async (key: string) => { await electrobun.rpc?.request.setSecret({ service: 'koins', @@ -185,58 +233,148 @@ export const Wallet = () => { transactions = txs ?? [] } + const checkMoneroStatus = async () => { + if (!electrobun.rpc) return + const [binStatus] = await tryCatch( + electrobun.rpc.request.moneroBinaryStatus({}), + ) + if (binStatus) { + moneroInstalled = binStatus.installed + moneroDownloading = binStatus.downloading + } + const [status] = await tryCatch( + electrobun.rpc.request.moneroWalletStatus({}), + ) + if (status) { + moneroRunning = status.running + moneroWalletOpen = status.walletOpen + moneroConnected = status.connected + } + } + + const moneroDownload = async () => { + if (!electrobun.rpc) return + moneroDownloading = true + try { + await electrobun.rpc.request.moneroDownloadBinary({}) + moneroInstalled = true + } finally { + moneroDownloading = false + } + } + + const moneroStart = async () => { + if (!electrobun.rpc) return + const status = await electrobun.rpc.request.moneroStart({}) + moneroRunning = status.running + moneroConnected = status.connected + } + + const moneroStop = async () => { + if (!electrobun.rpc) return + await electrobun.rpc.request.moneroStop({}) + moneroRunning = false + moneroWalletOpen = false + moneroConnected = false + moneroAddress = '' + moneroBalAtomic = '0' + moneroUnlockedAtomic = '0' + moneroTxs = [] + moneroAccounts = [] + } + + const moneroCreateWallet = async (name: string, password: string) => { + if (!electrobun.rpc) throw new Error('RPC not available') + const result = await electrobun.rpc.request.moneroCreateWallet({ name, password }) + moneroWalletName = name + moneroWalletOpen = true + moneroAddress = result.address + await moneroRefresh() + return result + } + + const moneroRestoreWallet = async (name: string, password: string, mnemonic: string, restoreHeight?: number) => { + if (!electrobun.rpc) throw new Error('RPC not available') + const result = await electrobun.rpc.request.moneroRestoreWallet({ + name, password, mnemonic, restoreHeight, + }) + moneroWalletName = name + moneroWalletOpen = true + moneroAddress = result.address + await moneroRefresh() + } + + const moneroOpenWallet = async (name: string, password: string) => { + if (!electrobun.rpc) return + await electrobun.rpc.request.moneroOpenWallet({ name, password }) + moneroWalletName = name + moneroWalletOpen = true + await moneroRefresh() + } + + const moneroRefresh = async () => { + if (!electrobun.rpc) return + loading = true + try { + const [bal] = await tryCatch( + electrobun.rpc.request.moneroGetBalance({}), + ) + if (bal) { + moneroBalAtomic = bal.balance + moneroUnlockedAtomic = bal.unlocked + moneroAddress = bal.address + moneroHeight = bal.height + moneroDaemonHeight = bal.daemonHeight + } + const [txs] = await tryCatch( + electrobun.rpc.request.moneroGetTransactions({}), + ) + moneroTxs = txs ?? [] + await moneroFetchAccounts() + } catch (e) { + console.log('monero refresh error', e) + error = e instanceof Error ? e.message : 'Monero refresh failed' + } finally { + loading = false + } + } + return { get isLocked() { - return vaultExists && !seed - }, - get seed() { - return seed - }, - get address() { - return address - }, - get balance() { - return balance - }, - get tokenBalances() { - return tokenBalances - }, - get network() { - return network - }, - get chainid() { - return net().chainid - }, - get networkName() { - return net().name - }, - get symbol() { - return net().symbol - }, - get explorerUrl() { - return net().explorerUrl - }, - get loading() { - return loading - }, - get error() { - return error - }, - get vaultExists() { - return vaultExists - }, - get ready() { - return ready - }, - get networks() { - return networks - }, - get apiKey() { - return apiKey - }, - get transactions() { - return transactions + return vaultExists && !seed && network !== 'monero' }, + get seed() { return seed }, + get address() { return address }, + get balance() { return balance }, + get tokenBalances() { return tokenBalances }, + get network() { return network }, + get chainid() { return net().chainid }, + get networkName() { return net().name }, + get symbol() { return net().symbol }, + get explorerUrl() { return net().explorerUrl }, + get loading() { return loading }, + get error() { return error }, + get vaultExists() { return vaultExists }, + get ready() { return ready }, + get networks() { return networks }, + get apiKey() { return apiKey }, + get transactions() { return transactions }, + isMonero, + get moneroRunning() { return moneroRunning }, + get moneroWalletOpen() { return moneroWalletOpen }, + get moneroConnected() { return moneroConnected }, + get moneroBalAtomic() { return moneroBalAtomic }, + get moneroUnlockedAtomic() { return moneroUnlockedAtomic }, + get moneroBalance() { return atomicToXmr(moneroBalAtomic) }, + get moneroUnlocked() { return atomicToXmr(moneroUnlockedAtomic) }, + get moneroAddress() { return moneroAddress }, + get moneroHeight() { return moneroHeight }, + get moneroDaemonHeight() { return moneroDaemonHeight }, + get moneroTxs() { return moneroTxs }, + get moneroInstalled() { return moneroInstalled }, + get moneroDownloading() { return moneroDownloading }, + get moneroWalletName() { return moneroWalletName }, + get moneroAccounts() { return moneroAccounts }, refresh, init, lock, @@ -244,5 +382,14 @@ export const Wallet = () => { switchNetwork, saveApiKey, fetchTxHistory, + checkMoneroStatus, + moneroDownload, + moneroStart, + moneroStop, + moneroCreateWallet, + moneroRestoreWallet, + moneroOpenWallet, + moneroRefresh, + moneroFetchAccounts, } } diff --git a/apps/desktop/src/routes/index.svelte b/apps/desktop/src/routes/index.svelte index d3cb74c..9140167 100644 --- a/apps/desktop/src/routes/index.svelte +++ b/apps/desktop/src/routes/index.svelte @@ -1,9 +1,10 @@