Files
koins/apps/desktop/src/lib/states/monero-wallet.svelte.ts
T

442 lines
9.1 KiB
TypeScript

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<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('')
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<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({
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<MoneroSendResult> => {
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<MoneroSendResult> => {
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()