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:
@@ -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,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'
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user