split wallet state into evm-wallet and monero-wallet

- Move EVM state/methods to evm-wallet.svelte.ts with its own init/login/logout/accountType
- Move Monero state/methods to monero-wallet.svelte.ts with its own init/login/logout/accountType
- Each wallet is fully self-contained and pages import only their wallet's state
- evm-wallet imports monero-wallet internally for full app init (no circular dep)
- Welcome/settings/tx pages import evmWallet
- Monero pages import moneroWallet
- Delete old wallet.svelte.ts orchestrator
This commit is contained in:
2026-05-20 20:09:45 +08:00
parent 805d103785
commit c55a33e9ab
9 changed files with 646 additions and 635 deletions
@@ -0,0 +1,279 @@
import { electrobun, type MoneroTxEntry, type MoneroAccountEntry } 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 type AccountType = 'multi' | 'monero'
export const MoneroWallet = () => {
let accountType = $state<AccountType | 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<MoneroTxEntry[]>([])
let installed = $state(false)
let downloading = $state(false)
let walletName = $state('')
let accounts = $state<MoneroAccountEntry[]>([])
let wallets = $state<string[]>([])
let loading = $state(false)
let error = $state('')
const checkBiometric = async () => {
if (!electrobun.rpc) return
const [ok] = await tryCatch(
electrobun.rpc.request.biometricCanAuth({}),
)
biometricAvailable = ok === true
}
const biometricAuth = async (): Promise<boolean> => {
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<string | null> => {
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({}),
)
txs = result ?? []
await fetchAccounts()
} catch (e) {
error = e instanceof Error ? e.message : 'Monero refresh failed'
} finally {
loading = false
}
}
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 },
init,
login,
logout,
biometricAuth,
moneroGetStoredPassword,
moneroStorePassword,
fetchAccounts,
listWallets,
openExistingWallet,
checkStatus,
download,
start,
stop,
createWallet,
restoreWallet,
openWallet,
refresh,
}
}
export const moneroWallet = MoneroWallet()