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
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { electrobun, type TxEntry } from '$lib/electrobun.js' import { electrobun, type TxEntry } from '$lib/electrobun.js'
import { import {
wallet, evmWallet as wallet,
type NetworkId, type NetworkId,
} from '$lib/states/wallet.svelte.js' } from '$lib/states/evm-wallet.svelte.js'
import { Button } from '$lib/components/ui/button/index.js' import { Button } from '$lib/components/ui/button/index.js'
import { Textarea } from '$lib/components/ui/textarea/index.js' import { Textarea } from '$lib/components/ui/textarea/index.js'
import { navigate } from 'sv-router/generated' import { navigate } from 'sv-router/generated'
@@ -170,7 +170,7 @@
{:else} {:else}
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex gap-1 justify-center" role="group"> <div class="flex gap-1 justify-center" role="group">
{#each w.evmNetworks as net} {#each w.networks as net}
<button <button
class="cursor-pointer rounded-md px-2.5 py-1 text-xs font-medium transition-colors {w.network === class="cursor-pointer rounded-md px-2.5 py-1 text-xs font-medium transition-colors {w.network ===
net.id net.id
@@ -0,0 +1,321 @@
import { mnemonicToAccount } from 'viem/accounts'
import { createPublicClient, http, formatEther } from 'viem'
import { electrobun, type TxEntry } from '$lib/electrobun'
import { tryCatch } from '@koins/utils'
import { moneroWallet } from './monero-wallet.svelte.js'
export const networks = [
{
id: 'bsc',
name: 'BNB Smart Chain',
rpc: 'https://bsc-dataseed.binance.org',
symbol: 'BNB',
chainid: '56',
explorerUrl: 'https://bscscan.com/tx/',
},
{
id: 'polygon',
name: 'Polygon',
rpc: 'https://polygon-bor.publicnode.com',
symbol: 'POL',
chainid: '137',
explorerUrl: 'https://polygonscan.com/tx/',
},
{
id: 'eth',
name: 'Ethereum',
rpc: 'https://ethereum-rpc.publicnode.com',
symbol: 'ETH',
chainid: '1',
explorerUrl: 'https://etherscan.io/tx/',
},
] as const
export type NetworkId = (typeof networks)[number]['id']
export type TokenBalance = {
symbol: string
balance: string
contractAddress: string
logo?: string
}
export type AccountType = 'multi' | 'monero'
export const EvmWallet = () => {
let accountType = $state<AccountType | null>(null)
let ready = $state(false)
let biometricAvailable = $state(false)
let seed = $state('')
let address = $state('')
let balance = $state('0')
let tokenBalances = $state<TokenBalance[]>([])
let network = $state<NetworkId>('eth')
let vaultExists = $state(false)
let apiKey = $state('')
let transactions = $state<TxEntry[]>([])
let passwordHash = $state('')
let loading = $state(false)
let error = $state('')
const net = () => networks.find((n) => n.id === network)!
async function hashPassword(password: string, salt?: string): Promise<{ salt: string; hash: string }> {
const s = salt ?? Array.from(crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join('')
const enc = new TextEncoder()
const key = await crypto.subtle.importKey('raw', enc.encode(password + s), 'PBKDF2', false, ['deriveBits'])
const bits = await crypto.subtle.deriveBits({ name: 'PBKDF2', salt: enc.encode(s), iterations: 100_000, hash: 'SHA-256' }, key, 256)
const hash = Array.from(new Uint8Array(bits), b => b.toString(16).padStart(2, '0')).join('')
return { salt: s, hash }
}
const checkBiometric = async () => {
if (!electrobun.rpc) return
const [ok] = await tryCatch(
electrobun.rpc.request.biometricCanAuth({}),
)
biometricAvailable = ok === true
}
const init = async () => {
if (!electrobun.rpc) return
const [raw] = await tryCatch(
electrobun.rpc.request.getSecret({ service: 'koins', name: 'vault' }),
)
vaultExists = raw !== null && raw !== undefined
const [key] = await tryCatch(
electrobun.rpc.request.getSecret({ service: 'koins', name: 'alchemy_key' }),
)
if (key) apiKey = key
const [ph] = await tryCatch(
electrobun.rpc.request.getSecret({ service: 'koins', name: 'vault_password' }),
)
if (ph) passwordHash = ph
await moneroWallet.checkStatus()
if (moneroWallet.installed && !moneroWallet.running && !moneroWallet.downloading) {
await moneroWallet.start()
await moneroWallet.checkStatus()
}
await checkBiometric()
ready = true
}
const login = async (type: AccountType) => {
accountType = type
if (type === 'multi') {
if (seed) await switchNetwork(network)
} else if (type === 'monero') {
if (moneroWallet.installed && !moneroWallet.running) {
await moneroWallet.start()
await moneroWallet.checkStatus()
}
await moneroWallet.listWallets()
}
}
const logout = async () => {
await moneroWallet.stop()
accountType = null
seed = ''
address = ''
balance = '0'
tokenBalances = []
transactions = []
error = ''
}
const loadSeed = async () => {
if (!electrobun.rpc) return false
const [raw] = await tryCatch(
electrobun.rpc.request.getSecret({ service: 'koins', name: 'vault' }),
)
if (!raw) return false
seed = raw
await refresh()
return true
}
const unlockWithBiometrics = async () => {
if (!electrobun.rpc) return false
const [authed] = await tryCatch(
electrobun.rpc.request.biometricAuth({ reason: 'Unlock wallet' }),
)
if (!authed) return false
return loadSeed()
}
const unlockWithPassword = async (password: string) => {
if (!passwordHash) return false
try {
const { salt, hash } = JSON.parse(passwordHash)
const { hash: check } = await hashPassword(password, salt)
if (check !== hash) return false
return await loadSeed()
} catch {
return false
}
}
const setPassword = async (password: string) => {
if (!electrobun.rpc) return
const ph = await hashPassword(password)
const raw = JSON.stringify(ph)
await electrobun.rpc.request.setSecret({
service: 'koins', name: 'vault_password', value: raw,
})
passwordHash = raw
}
const saveVault = async (phrase: string, password?: string) => {
loading = true
error = ''
try {
mnemonicToAccount(phrase.trim())
await electrobun.rpc?.request.setSecret({
service: 'koins', name: 'vault', value: phrase.trim(),
})
if (password) {
const ph = await hashPassword(password)
await electrobun.rpc?.request.setSecret({
service: 'koins', name: 'vault_password', value: JSON.stringify(ph),
})
passwordHash = JSON.stringify(ph)
}
seed = phrase.trim()
vaultExists = true
await refresh()
} catch (e) {
error = e instanceof Error ? e.message : 'Invalid seed phrase'
} finally {
loading = false
}
}
const switchNetwork = async (id: NetworkId) => {
network = id
if (seed) await refresh()
}
const refresh = async () => {
if (!seed || !electrobun.rpc) return
loading = true
error = ''
tokenBalances = []
const account = mnemonicToAccount(seed)
address = account.address
const client = createPublicClient({ transport: http(net().rpc) })
const [nativeBal, nativeBalError] = await tryCatch(
client.getBalance({ address: account.address }),
)
if (nativeBalError) {
loading = false
return
}
balance = formatEther(nativeBal)
const [bals, balsError] = await tryCatch(
electrobun.rpc.request.fetchTokenBalances({
address, chainid: net().chainid,
}),
)
if (balsError) error = balsError.message
tokenBalances = bals ?? []
await fetchTxHistory()
loading = false
}
const saveApiKey = async (key: string) => {
await electrobun.rpc?.request.setSecret({
service: 'koins', name: 'alchemy_key', value: key,
})
apiKey = key
if (address) await fetchTxHistory()
}
const fetchTxHistory = async () => {
if (!address || !apiKey) {
transactions = []
return
}
const txs = await electrobun.rpc?.request.fetchTxHistory({
address, chainid: net().chainid,
})
transactions = txs ?? []
}
const lock = () => {
seed = ''
address = ''
balance = '0'
error = ''
tokenBalances = []
transactions = []
}
const reset = () => {
seed = ''
address = ''
balance = '0'
error = ''
tokenBalances = []
transactions = []
apiKey = ''
vaultExists = false
passwordHash = ''
}
const resetApp = async () => {
if (!electrobun.rpc) return false
const [ok] = await tryCatch(electrobun.rpc.request.resetApp({}))
if (!ok) return false
reset()
return true
}
return {
get accountType() { return accountType },
get ready() { return ready },
get biometricAvailable() { return biometricAvailable },
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 vaultExists() { return vaultExists },
get networks() { return networks },
get apiKey() { return apiKey },
get transactions() { return transactions },
get passwordSet() { return !!passwordHash },
get loading() { return loading },
get error() { return error },
get isLocked() { return vaultExists && !seed },
set passwordHash(v: string) { passwordHash = v },
set vaultExists(v: boolean) { vaultExists = v },
set apiKey(v: string) { apiKey = v },
set seed(v: string) { seed = v },
set error(v: string) { error = v },
init,
login,
logout,
refresh,
saveVault,
switchNetwork,
saveApiKey,
fetchTxHistory,
loadSeed,
lock,
reset,
resetApp,
unlockWithBiometrics,
unlockWithPassword,
setPassword,
}
}
export const evmWallet = EvmWallet()
@@ -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()
@@ -1,560 +0,0 @@
import { mnemonicToAccount } from 'viem/accounts'
import { createPublicClient, http, formatEther } from 'viem'
import { electrobun, type TxEntry, type MoneroTxEntry, type MoneroAccountEntry } from '$lib/electrobun'
import { tryCatch } from '@koins/utils'
export const networks = [
{
id: 'bsc',
name: 'BNB Smart Chain',
rpc: 'https://bsc-dataseed.binance.org',
symbol: 'BNB',
chainid: '56',
explorerUrl: 'https://bscscan.com/tx/',
},
{
id: 'polygon',
name: 'Polygon',
rpc: 'https://polygon-bor.publicnode.com',
symbol: 'POL',
chainid: '137',
explorerUrl: 'https://polygonscan.com/tx/',
},
{
id: 'eth',
name: 'Ethereum',
rpc: 'https://ethereum-rpc.publicnode.com',
symbol: 'ETH',
chainid: '1',
explorerUrl: 'https://etherscan.io/tx/',
},
] as const
const moneroNetwork = {
id: 'monero',
name: 'Monero',
rpc: '',
symbol: 'XMR',
chainid: '',
explorerUrl: 'https://moneroblocks.info/tx/',
} as const
export type NetworkId = (typeof networks)[number]['id']
export type TokenBalance = {
symbol: string
balance: string
contractAddress: string
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 type AccountType = 'multi' | 'monero'
export const Wallet = () => {
let accountType = $state<AccountType | null>(null)
let seed = $state('')
let address = $state('')
let balance = $state('0')
let tokenBalances = $state<TokenBalance[]>([])
let network = $state<NetworkId>('eth')
let loading = $state(false)
let error = $state('')
let vaultExists = $state(false)
let ready = $state(false)
let apiKey = $state('')
let transactions = $state<TxEntry[]>([])
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<MoneroTxEntry[]>([])
let moneroInstalled = $state(false)
let moneroDownloading = $state(false)
let biometricAvailable = $state(false)
let moneroWalletName = $state('')
let moneroAccounts = $state<MoneroAccountEntry[]>([])
let moneroWallets = $state<string[]>([])
let passwordHash = $state('')
const net = () => networks.find((n) => n.id === network)!
async function hashPassword(password: string, salt?: string): Promise<{ salt: string; hash: string }> {
const s = salt ?? Array.from(crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join('')
const enc = new TextEncoder()
const key = await crypto.subtle.importKey('raw', enc.encode(password + s), 'PBKDF2', false, ['deriveBits'])
const bits = await crypto.subtle.deriveBits({ name: 'PBKDF2', salt: enc.encode(s), iterations: 100_000, hash: 'SHA-256' }, key, 256)
const hash = Array.from(new Uint8Array(bits), b => b.toString(16).padStart(2, '0')).join('')
return { salt: s, hash }
}
const init = async () => {
if (!electrobun.rpc) return
const [raw, rawError] = await tryCatch(
electrobun.rpc.request.getSecret({
service: 'koins',
name: 'vault',
}),
)
if (rawError) {
console.log('Raw Error:', rawError)
}
vaultExists = raw !== null && raw !== undefined
const [key, keyError] = await tryCatch(
electrobun.rpc.request.getSecret({ service: 'koins', name: 'alchemy_key' }),
)
if (keyError) console.log('key error', keyError)
if (key) apiKey = key
const [ph] = await tryCatch(
electrobun.rpc.request.getSecret({ service: 'koins', name: 'vault_password' }),
)
if (ph) passwordHash = ph
await checkMoneroStatus()
if (moneroInstalled && !moneroRunning && !moneroDownloading) {
await moneroStart()
await checkMoneroStatus()
}
await checkBiometric()
ready = true
}
const login = async (type: AccountType) => {
accountType = type
if (type === 'multi') {
if (seed) await switchNetwork(network)
} else if (type === 'monero') {
if (moneroInstalled && !moneroRunning) {
await moneroStart()
await checkMoneroStatus()
}
await moneroListWallets()
}
}
const logout = async () => {
await moneroStop()
accountType = null
seed = ''
address = ''
balance = '0'
tokenBalances = []
transactions = []
}
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 switchNetwork = async (id: NetworkId) => {
network = id
if (seed) {
await refresh()
}
}
const saveVault = async (phrase: string, password?: string) => {
loading = true
error = ''
try {
mnemonicToAccount(phrase.trim())
await electrobun.rpc?.request.setSecret({
service: 'koins',
name: 'vault',
value: phrase.trim(),
})
if (password) {
const ph = await hashPassword(password)
const raw = JSON.stringify(ph)
await electrobun.rpc?.request.setSecret({
service: 'koins',
name: 'vault_password',
value: raw,
})
passwordHash = raw
}
seed = phrase.trim()
vaultExists = true
await refresh()
} catch (e) {
error = e instanceof Error ? e.message : 'Invalid seed phrase'
} finally {
loading = false
}
}
const checkBiometric = async () => {
if (!electrobun.rpc) return
const [ok] = await tryCatch(
electrobun.rpc.request.biometricCanAuth({}),
)
biometricAvailable = ok === true
}
const unlockWithBiometrics = async () => {
if (!electrobun.rpc) return false
const [authed] = await tryCatch(
electrobun.rpc.request.biometricAuth({ reason: 'Unlock wallet' }),
)
if (!authed) return false
return loadSeed()
}
const unlockWithPassword = async (password: string) => {
if (!passwordHash) return false
try {
const { salt, hash } = JSON.parse(passwordHash)
const { hash: check } = await hashPassword(password, salt)
if (check !== hash) return false
return await loadSeed()
} catch {
return false
}
}
const resetApp = async () => {
if (!electrobun.rpc) return false
const [ok] = await tryCatch(
electrobun.rpc.request.resetApp({}),
)
if (!ok) return false
seed = ''
address = ''
balance = '0'
error = ''
tokenBalances = []
transactions = []
apiKey = ''
vaultExists = false
passwordHash = ''
return true
}
const setPassword = async (password: string) => {
if (!electrobun.rpc) return
const ph = await hashPassword(password)
const raw = JSON.stringify(ph)
await electrobun.rpc.request.setSecret({
service: 'koins',
name: 'vault_password',
value: raw,
})
passwordHash = raw
}
async function loadSeed() {
if (!electrobun.rpc) return false
const [raw] = await tryCatch(
electrobun.rpc.request.getSecret({
service: 'koins',
name: 'vault',
}),
)
if (!raw) return false
seed = raw
await refresh()
return true
}
const lock = () => {
seed = ''
address = ''
balance = '0'
error = ''
tokenBalances = []
transactions = []
}
const refresh = async () => {
if (!seed || !electrobun.rpc) return
loading = true
error = ''
tokenBalances = []
const account = mnemonicToAccount(seed)
address = account.address
const client = createPublicClient({ transport: http(net().rpc) })
const [nativeBal, nativeBalError] = await tryCatch(
client.getBalance({ address: account.address }),
)
if (nativeBalError) {
loading = false
console.log('Native Balance Error', nativeBalError)
return
}
balance = formatEther(nativeBal)
const [bals, balsError] = await tryCatch(
electrobun.rpc.request.fetchTokenBalances({
address: address,
chainid: net().chainid,
}),
)
if (balsError) {
console.log('Bals Error', balsError)
error = balsError.message
}
tokenBalances = bals ?? []
fetchTxHistory()
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 moneroListWallets = async () => {
if (!electrobun.rpc) return
const [wallets] = await tryCatch(
electrobun.rpc.request.moneroListWallets({}),
)
moneroWallets = wallets ?? []
}
const moneroOpenExistingWallet = 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 })
moneroWalletName = name
moneroWalletOpen = true
await moneroRefresh()
}
const saveApiKey = async (key: string) => {
await electrobun.rpc?.request.setSecret({
service: 'koins',
name: 'alchemy_key',
value: key,
})
apiKey = key
if (address) await fetchTxHistory()
}
const fetchTxHistory = async () => {
if (!address || !apiKey) {
transactions = []
return
}
const txs = await electrobun.rpc?.request.fetchTxHistory({
address,
chainid: net().chainid,
})
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 = []
moneroWallets = []
}
const moneroCreateWallet = 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 })
moneroWalletName = name
moneroWalletOpen = true
moneroAddress = result.address
if (storePw) await moneroStorePassword(name, password)
await moneroRefresh()
return result
}
const moneroRestoreWallet = 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,
})
moneroWalletName = name
moneroWalletOpen = true
moneroAddress = result.address
if (storePw) await moneroStorePassword(name, password)
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 accountType === 'multi' && vaultExists && !seed
},
get accountType() { return accountType },
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 moneroNetwork() { return moneroNetwork },
get evmNetworks() { return networks },
get apiKey() { return apiKey },
get transactions() { return transactions },
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 },
get moneroWallets() { return moneroWallets },
get biometricAvailable() { return biometricAvailable },
get passwordSet() { return !!passwordHash },
refresh,
init,
login,
logout,
lock,
unlockWithBiometrics,
unlockWithPassword,
setPassword,
resetApp,
saveVault,
switchNetwork,
saveApiKey,
fetchTxHistory,
checkMoneroStatus,
moneroDownload,
moneroStart,
moneroStop,
moneroCreateWallet,
moneroRestoreWallet,
moneroOpenWallet,
moneroRefresh,
moneroFetchAccounts,
moneroListWallets,
moneroOpenExistingWallet,
moneroGetStoredPassword,
moneroStorePassword,
}
}
export const wallet = Wallet()
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { wallet } from '$lib/states/wallet.svelte.js' import { evmWallet as wallet } from '$lib/states/evm-wallet.svelte.js'
import { Button } from '$lib/components/ui/button/index.js' import { Button } from '$lib/components/ui/button/index.js'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card/index.js' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card/index.js'
import { navigate } from 'sv-router/generated' import { navigate } from 'sv-router/generated'
+36 -54
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { wallet, atomicToXmr } from '$lib/states/wallet.svelte.js' import { moneroWallet, atomicToXmr } 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 { Textarea } from '$lib/components/ui/textarea/index.js' import { Textarea } from '$lib/components/ui/textarea/index.js'
import { Input } from '$lib/components/ui/input/index.js' import { Input } from '$lib/components/ui/input/index.js'
@@ -17,7 +17,7 @@
import Loader from '$lib/components/loader.svelte' import Loader from '$lib/components/loader.svelte'
import { navigate } from 'sv-router/generated' import { navigate } from 'sv-router/generated'
const w = wallet const w = moneroWallet
let moneroWalletName = $state('') let moneroWalletName = $state('')
let moneroWalletPass = $state('') let moneroWalletPass = $state('')
@@ -33,7 +33,7 @@
initStarted = true initStarted = true
w.init().then(async () => { w.init().then(async () => {
if (w.accountType === 'monero') return if (w.accountType === 'monero') return
await w.login('monero') await w.login()
}) })
} }
}) })
@@ -44,52 +44,34 @@
{#if !w.ready} {#if !w.ready}
<p class="text-center text-muted-foreground text-sm mt-8">Loading...</p> <p class="text-center text-muted-foreground text-sm mt-8">Loading...</p>
{:else if !w.accountType} {:else if !w.accountType}
<Card> <p class="text-center text-muted-foreground text-sm mt-8">
<CardHeader class="text-center"> <a href="/" class="underline">Go to welcome page</a> to get started
<CardTitle>Welcome</CardTitle> </p>
<CardDescription>Choose an account type to get started</CardDescription>
</CardHeader>
<CardContent>
<div class="flex flex-col gap-3">
<Button
onclick={async () => { await w.login('multi'); navigate('/multicoin') }}
variant="outline"
class="w-full">
EVM (ETH / BSC / Polygon)
</Button>
<Button
onclick={() => w.login('monero')}
class="w-full">
Monero
</Button>
</div>
</CardContent>
</Card>
{:else if w.accountType === 'monero'} {:else if w.accountType === 'monero'}
{#if w.moneroDownloading} {#if w.downloading}
<Card> <Card>
<CardContent> <CardContent>
<p class="text-muted-foreground text-xs">Downloading Monero binary (70MB)...</p> <p class="text-muted-foreground text-xs">Downloading Monero binary (70MB)...</p>
</CardContent> </CardContent>
</Card> </Card>
{:else if !w.moneroInstalled} {:else if !w.installed}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Monero Setup</CardTitle> <CardTitle>Monero Setup</CardTitle>
<CardDescription>Download monero-wallet-rpc to get started</CardDescription> <CardDescription>Download monero-wallet-rpc to get started</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Button onclick={() => w.moneroDownload()}>Download (~70MB)</Button> <Button onclick={() => w.download()}>Download (~70MB)</Button>
</CardContent> </CardContent>
</Card> </Card>
{:else if !w.moneroRunning} {:else if !w.running}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Monero Wallet</CardTitle> <CardTitle>Monero Wallet</CardTitle>
<CardDescription>Start the wallet RPC server to connect to the Monero network</CardDescription> <CardDescription>Start the wallet RPC server to connect to the Monero network</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Button onclick={() => w.moneroStart()} disabled={w.loading}> <Button onclick={() => w.start()} disabled={w.loading}>
{#if w.loading} {#if w.loading}
<Loader /> <Loader />
{/if} {/if}
@@ -97,22 +79,22 @@
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
{:else if !w.moneroWalletOpen} {:else if !w.walletOpen}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Monero Wallet</CardTitle> <CardTitle>Monero Wallet</CardTitle>
<CardDescription> <CardDescription>
{w.moneroWallets.length > 0 {w.wallets.length > 0
? `${w.moneroWallets.length} wallet${w.moneroWallets.length > 1 ? 's' : ''} found on disk` ? `${w.wallets.length} wallet${w.wallets.length > 1 ? 's' : ''} found on disk`
: 'No existing wallets found'} : 'No existing wallets found'}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
{#if w.moneroWallets.length > 0} {#if w.wallets.length > 0}
<p class="text-xs font-medium">Select a wallet to open:</p> <p class="text-xs font-medium">Select a wallet to open:</p>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{#each w.moneroWallets as name} {#each w.wallets as name}
<Button <Button
variant={moneroSelectedWallet === name ? 'default' : 'outline'} variant={moneroSelectedWallet === name ? 'default' : 'outline'}
onclick={() => { moneroSelectedWallet = name; moneroSelectedWalletPass = '' }} onclick={() => { moneroSelectedWallet = name; moneroSelectedWalletPass = '' }}
@@ -126,10 +108,10 @@
{#if w.biometricAvailable} {#if w.biometricAvailable}
<Button <Button
onclick={async () => { onclick={async () => {
const ok = await w.unlockWithBiometrics() const ok = await w.biometricAuth()
if (ok) { if (ok) {
await w.moneroOpenExistingWallet(moneroSelectedWallet) await w.openExistingWallet(moneroSelectedWallet)
if (!w.moneroWalletOpen) w.error = 'No password saved for this wallet' if (!w.walletOpen) w.error = 'No password saved for this wallet'
} }
}} }}
disabled={w.loading}> disabled={w.loading}>
@@ -144,7 +126,7 @@
bind:value={moneroSelectedWalletPass} bind:value={moneroSelectedWalletPass}
/> />
<Button <Button
onclick={() => w.moneroOpenExistingWallet(moneroSelectedWallet, moneroSelectedWalletPass)} onclick={() => w.openExistingWallet(moneroSelectedWallet, moneroSelectedWalletPass)}
disabled={w.loading || !moneroSelectedWalletPass} disabled={w.loading || !moneroSelectedWalletPass}
> >
Open Open
@@ -169,7 +151,7 @@
<Button <Button
onclick={async () => { onclick={async () => {
const pw = moneroUseBiometric ? crypto.randomUUID() : moneroWalletPass const pw = moneroUseBiometric ? crypto.randomUUID() : moneroWalletPass
const result = await w.moneroCreateWallet(moneroWalletName, pw, moneroUseBiometric) const result = await w.createWallet(moneroWalletName, pw, moneroUseBiometric)
moneroMnemonic = result.mnemonic moneroMnemonic = result.mnemonic
}} }}
disabled={w.loading || !moneroWalletName || (!moneroUseBiometric && !moneroWalletPass)}> disabled={w.loading || !moneroWalletName || (!moneroUseBiometric && !moneroWalletPass)}>
@@ -184,7 +166,7 @@
<Input type="password" placeholder="Password" bind:value={moneroWalletPass} /> <Input type="password" placeholder="Password" bind:value={moneroWalletPass} />
{/if} {/if}
<Button <Button
onclick={() => w.moneroRestoreWallet(moneroWalletName, moneroUseBiometric ? crypto.randomUUID() : moneroWalletPass, moneroMnemonic, moneroRestoreHeight, moneroUseBiometric)} onclick={() => w.restoreWallet(moneroWalletName, moneroUseBiometric ? crypto.randomUUID() : moneroWalletPass, moneroMnemonic, moneroRestoreHeight, moneroUseBiometric)}
disabled={w.loading || !moneroWalletName || (!moneroUseBiometric && !moneroWalletPass) || !moneroMnemonic.trim()}> disabled={w.loading || !moneroWalletName || (!moneroUseBiometric && !moneroWalletPass) || !moneroMnemonic.trim()}>
Restore Wallet Restore Wallet
</Button> </Button>
@@ -198,27 +180,27 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<CardTitle>Monero Wallet</CardTitle> <CardTitle>Monero Wallet</CardTitle>
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<div class="size-2 rounded-full {w.moneroConnected ? 'bg-green-500' : 'bg-red-500'}"></div> <div class="size-2 rounded-full {w.connected ? 'bg-green-500' : 'bg-red-500'}"></div>
<span class="text-xs text-muted-foreground">{w.moneroConnected ? 'Connected' : 'Disconnected'}</span> <span class="text-xs text-muted-foreground">{w.connected ? 'Connected' : 'Disconnected'}</span>
</div> </div>
</div> </div>
<CardDescription> <CardDescription>
{#if w.moneroHeight < w.moneroDaemonHeight} {#if w.height < w.daemonHeight}
Scanning: {w.moneroHeight.toLocaleString()} / {w.moneroDaemonHeight.toLocaleString()} Scanning: {w.height.toLocaleString()} / {w.daemonHeight.toLocaleString()}
{:else} {:else}
Height: {w.moneroHeight.toLocaleString()} Height: {w.height.toLocaleString()}
{/if} {/if}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div class="mb-4 space-y-1"> <div class="mb-4 space-y-1">
<p class="font-medium text-xs">Address</p> <p class="font-medium text-xs">Address</p>
<p class="font-mono text-xs break-all">{w.moneroAddress}</p> <p class="font-mono text-xs break-all">{w.address}</p>
<p class="mt-2 font-medium text-xs">Balance</p> <p class="mt-2 font-medium text-xs">Balance</p>
<p class="font-mono text-lg">{w.moneroBalance} XMR</p> <p class="font-mono text-lg">{w.balance} XMR</p>
{#if w.moneroUnlockedAtomic !== w.moneroBalAtomic} {#if w.unlockedAtomic !== w.balAtomic}
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
Unlocked: {w.moneroUnlocked} XMR Unlocked: {w.unlocked} XMR
</p> </p>
{/if} {/if}
</div> </div>
@@ -231,7 +213,7 @@
{/if} {/if}
<div class="flex gap-2"> <div class="flex gap-2">
<Button onclick={() => { w.moneroRefresh(); w.moneroFetchAccounts(); }} disabled={w.loading}> <Button onclick={() => { w.refresh(); w.fetchAccounts(); }} disabled={w.loading}>
{#if w.loading} {#if w.loading}
<Loader /> <Loader />
{/if} {/if}
@@ -257,11 +239,11 @@
<div class="flex justify-center py-4"> <div class="flex justify-center py-4">
<Loader /> <Loader />
</div> </div>
{:else if w.moneroTxs.length === 0} {:else if w.txs.length === 0}
<p class="text-muted-foreground text-xs">No transactions found</p> <p class="text-muted-foreground text-xs">No transactions found</p>
{:else} {:else}
<div class="max-h-96 space-y-1 overflow-y-auto"> <div class="max-h-96 space-y-1 overflow-y-auto">
{#each w.moneroTxs as tx} {#each w.txs as tx}
<div class="flex items-start gap-2 rounded-md bg-muted px-3 py-2 text-xs"> <div class="flex items-start gap-2 rounded-md bg-muted px-3 py-2 text-xs">
<div class="shrink-0 mt-0.5 {tx.direction === 'in' ? 'text-green-500' : 'text-muted-foreground'}"> <div class="shrink-0 mt-0.5 {tx.direction === 'in' ? 'text-green-500' : 'text-muted-foreground'}">
{#if tx.direction === 'in'} {#if tx.direction === 'in'}
@@ -296,11 +278,11 @@
<CardHeader> <CardHeader>
<CardTitle>Accounts & Addresses</CardTitle> <CardTitle>Accounts & Addresses</CardTitle>
<CardDescription> <CardDescription>
{w.moneroAccounts.length} account{w.moneroAccounts.length !== 1 ? 's' : ''} {w.accounts.length} account{w.accounts.length !== 1 ? 's' : ''}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{#each w.moneroAccounts as account} {#each w.accounts as account}
<div class="mb-3 rounded-md bg-muted p-3"> <div class="mb-3 rounded-md bg-muted p-3">
<div class="mb-1 flex items-center justify-between"> <div class="mb-1 flex items-center justify-between">
<p class="text-xs font-medium">Account {account.index}</p> <p class="text-xs font-medium">Account {account.index}</p>
+4 -15
View File
@@ -1,18 +1,7 @@
<script lang="ts"> <script lang="ts">
import { wallet } from '$lib/states/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 { navigate } from 'sv-router/generated'
import Loader from '$lib/components/loader.svelte'
const w = wallet const w = moneroWallet
let moneroSettingsPw = $state('') let moneroSettingsPw = $state('')
@@ -35,7 +24,7 @@
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onclick={async () => { await w.moneroStop(); await w.moneroStart() }} onclick={async () => { await w.stop(); await w.start() }}
disabled={w.loading}> disabled={w.loading}>
{#if w.loading} {#if w.loading}
<Loader /> <Loader />
@@ -50,7 +39,7 @@
<div class="flex gap-2 items-end"> <div class="flex gap-2 items-end">
<Input type="password" placeholder="Wallet password" bind:value={moneroSettingsPw} /> <Input type="password" placeholder="Wallet password" bind:value={moneroSettingsPw} />
<Button size="sm" onclick={async () => { <Button size="sm" onclick={async () => {
await w.moneroStorePassword(w.moneroWalletName, moneroSettingsPw) await w.moneroStorePassword(w.walletName, moneroSettingsPw)
moneroSettingsPw = '' moneroSettingsPw = ''
w.error = '' w.error = ''
}} disabled={!moneroSettingsPw || w.loading}>Save</Button> }} disabled={!moneroSettingsPw || w.loading}>Save</Button>
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { wallet } from '$lib/states/wallet.svelte.js' import { evmWallet as wallet } from '$lib/states/evm-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,
@@ -3,7 +3,7 @@
electrobun, electrobun,
type TransactionDetails, type TransactionDetails,
} from '$lib/electrobun.js' } from '$lib/electrobun.js'
import { networks } from '$lib/states/wallet.svelte.js' import { networks } from '$lib/states/evm-wallet.svelte.js'
import { navigate, route } from 'sv-router/generated' import { navigate, route } from 'sv-router/generated'
import { Button } from '$lib/components/ui/button/index.js' import { Button } from '$lib/components/ui/button/index.js'
import { import {