Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5df3ae7058 | ||
|
|
7b4ba84fee | ||
|
|
20f29faa80 | ||
|
|
c0f6417925 | ||
|
|
c61fea938a | ||
|
|
bdfee2a553 | ||
|
|
1c5e5b617f | ||
|
|
11e0bcdaf6 | ||
|
|
4270d380fb |
@@ -4,7 +4,7 @@ export default {
|
|||||||
app: {
|
app: {
|
||||||
name: 'koins',
|
name: 'koins',
|
||||||
identifier: 'koins.tzgyn.com',
|
identifier: 'koins.tzgyn.com',
|
||||||
version: '0.0.5',
|
version: '0.0.10',
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
// Vite builds to dist/, we copy from there
|
// Vite builds to dist/, we copy from there
|
||||||
|
|||||||
@@ -41,13 +41,40 @@ function rpcConnection(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withTimeout<T>(
|
||||||
|
promise: Promise<T>,
|
||||||
|
ms: number,
|
||||||
|
label: string,
|
||||||
|
): Promise<T> {
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
const timer = setTimeout(
|
||||||
|
() => reject(new Error(`${label} timed out after ${ms}ms`)),
|
||||||
|
ms,
|
||||||
|
)
|
||||||
|
promise.then(
|
||||||
|
(v) => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
resolve(v)
|
||||||
|
},
|
||||||
|
(e) => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
reject(e)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function rawRpc(
|
async function rawRpc(
|
||||||
state: MoneroWalletState,
|
state: MoneroWalletState,
|
||||||
method: string,
|
method: string,
|
||||||
params: Record<string, any> = {},
|
params: Record<string, any> = {},
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const conn = rpcConnection(state)
|
const conn = rpcConnection(state)
|
||||||
const res = await conn.sendJsonRequest(method, params)
|
const res = await withTimeout(
|
||||||
|
conn.sendJsonRequest(method, params),
|
||||||
|
30000,
|
||||||
|
`wallet-rpc ${method}`,
|
||||||
|
)
|
||||||
if (res?.error)
|
if (res?.error)
|
||||||
throw new Error(res.error.message || JSON.stringify(res.error))
|
throw new Error(res.error.message || JSON.stringify(res.error))
|
||||||
return res?.result
|
return res?.result
|
||||||
@@ -62,7 +89,11 @@ async function waitForRpc(
|
|||||||
while (Date.now() - start < timeoutMs) {
|
while (Date.now() - start < timeoutMs) {
|
||||||
try {
|
try {
|
||||||
const conn = rpcConnection(state)
|
const conn = rpcConnection(state)
|
||||||
const res = await conn.sendJsonRequest('get_version', {})
|
const res = await withTimeout(
|
||||||
|
conn.sendJsonRequest('get_version', {}),
|
||||||
|
5000,
|
||||||
|
'wallet-rpc probe',
|
||||||
|
)
|
||||||
if (res?.result?.version) {
|
if (res?.result?.version) {
|
||||||
console.log(`[monero] RPC ready after ${Date.now() - start}ms`)
|
console.log(`[monero] RPC ready after ${Date.now() - start}ms`)
|
||||||
return
|
return
|
||||||
@@ -290,7 +321,11 @@ export async function createWallet(
|
|||||||
language: 'English',
|
language: 'English',
|
||||||
})
|
})
|
||||||
if (!state.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
const address = await state.wallet.getAddress(0, 0)
|
const address = (await withTimeout(
|
||||||
|
state.wallet.getAddress(0, 0),
|
||||||
|
30000,
|
||||||
|
'wallet get_address',
|
||||||
|
)) as string
|
||||||
console.log(`[monero] wallet created: ${name} -> ${address}`)
|
console.log(`[monero] wallet created: ${name} -> ${address}`)
|
||||||
return { mnemonic: result.mnemonic, address }
|
return { mnemonic: result.mnemonic, address }
|
||||||
}
|
}
|
||||||
@@ -329,7 +364,7 @@ export async function openWallet(
|
|||||||
state.wallet = await connectClient(state)
|
state.wallet = await connectClient(state)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await state.wallet.openWallet(name, password)
|
await withTimeout(state.wallet.openWallet(name, password), 30000, `open_wallet ${name}`)
|
||||||
log(`[monero] wallet opened: ${name}`)
|
log(`[monero] wallet opened: ${name}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(`[monero] wallet open failed (${name})`, e)
|
logError(`[monero] wallet open failed (${name})`, e)
|
||||||
@@ -348,8 +383,16 @@ export async function getBalance(
|
|||||||
state: MoneroWalletState,
|
state: MoneroWalletState,
|
||||||
): Promise<{ balance: bigint; unlocked: bigint }> {
|
): Promise<{ balance: bigint; unlocked: bigint }> {
|
||||||
if (!state.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
const balance = (await state.wallet.getBalance()) as bigint
|
const balance = (await withTimeout(
|
||||||
const unlocked = (await state.wallet.getUnlockedBalance()) as bigint
|
state.wallet.getBalance(),
|
||||||
|
30000,
|
||||||
|
'wallet get_balance',
|
||||||
|
)) as bigint
|
||||||
|
const unlocked = (await withTimeout(
|
||||||
|
state.wallet.getUnlockedBalance(),
|
||||||
|
30000,
|
||||||
|
'wallet get_unlocked_balance',
|
||||||
|
)) as bigint
|
||||||
console.log(`[monero] balance: ${balance} (unlocked: ${unlocked})`)
|
console.log(`[monero] balance: ${balance} (unlocked: ${unlocked})`)
|
||||||
return { balance, unlocked }
|
return { balance, unlocked }
|
||||||
}
|
}
|
||||||
@@ -360,9 +403,10 @@ export async function getAddress(
|
|||||||
subIdx = 0,
|
subIdx = 0,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!state.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
const address = (await state.wallet.getAddress(
|
const address = (await withTimeout(
|
||||||
accountIdx,
|
state.wallet.getAddress(accountIdx, subIdx),
|
||||||
subIdx,
|
30000,
|
||||||
|
'wallet get_address',
|
||||||
)) as string
|
)) as string
|
||||||
console.log(`[monero] address: ${address}`)
|
console.log(`[monero] address: ${address}`)
|
||||||
return address
|
return address
|
||||||
@@ -398,7 +442,11 @@ export async function getAccounts(
|
|||||||
state: MoneroWalletState,
|
state: MoneroWalletState,
|
||||||
): Promise<any[]> {
|
): Promise<any[]> {
|
||||||
if (!state.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
const accounts = (await state.wallet.getAccounts(true)) as any[]
|
const accounts = (await withTimeout(
|
||||||
|
state.wallet.getAccounts(true),
|
||||||
|
30000,
|
||||||
|
'wallet get_accounts',
|
||||||
|
)) as any[]
|
||||||
console.log(`[monero] accounts: ${accounts?.length ?? 0} returned`)
|
console.log(`[monero] accounts: ${accounts?.length ?? 0} returned`)
|
||||||
if (accounts?.length) {
|
if (accounts?.length) {
|
||||||
for (const acct of accounts) {
|
for (const acct of accounts) {
|
||||||
@@ -543,7 +591,11 @@ export async function getHeight(
|
|||||||
state: MoneroWalletState,
|
state: MoneroWalletState,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
if (!state.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
const height = (await state.wallet.getHeight()) as number
|
const height = (await withTimeout(
|
||||||
|
state.wallet.getHeight(),
|
||||||
|
30000,
|
||||||
|
'wallet get_height',
|
||||||
|
)) as number
|
||||||
console.log(`[monero] wallet height: ${height}`)
|
console.log(`[monero] wallet height: ${height}`)
|
||||||
return height
|
return height
|
||||||
}
|
}
|
||||||
@@ -587,7 +639,11 @@ export async function getDaemonHeight(
|
|||||||
const daemon = await connectToDaemonRpc(
|
const daemon = await connectToDaemonRpc(
|
||||||
`http://${state.daemonAddress}`,
|
`http://${state.daemonAddress}`,
|
||||||
)
|
)
|
||||||
const height = (await daemon.getHeight()) as number
|
const height = (await withTimeout(
|
||||||
|
daemon.getHeight(),
|
||||||
|
15000,
|
||||||
|
'daemon get_height',
|
||||||
|
)) as number
|
||||||
console.log(`[monero] daemon height: ${height}`)
|
console.log(`[monero] daemon height: ${height}`)
|
||||||
return height
|
return height
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -665,9 +721,13 @@ export async function isWalletOpen(
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!state.wallet) return false
|
if (!state.wallet) return false
|
||||||
try {
|
try {
|
||||||
await state.wallet.getHeight()
|
await withTimeout(state.wallet.getHeight(), 8000, 'wallet get_height')
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
// A timeout usually means the wallet-rpc is busy syncing, not that
|
||||||
|
// the wallet is closed. Assume still-open so the UI doesn't flip to
|
||||||
|
// "closed" and try to re-open an already-open wallet.
|
||||||
|
if (e instanceof Error && e.message.includes('timed out')) return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -677,10 +737,12 @@ export async function isConnected(
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!state.wallet) return false
|
if (!state.wallet) return false
|
||||||
try {
|
try {
|
||||||
const daemon = await connectToDaemonRpc(
|
const daemon = await withTimeout(
|
||||||
`http://${state.daemonAddress}`,
|
connectToDaemonRpc(`http://${state.daemonAddress}`),
|
||||||
|
8000,
|
||||||
|
'daemon connect',
|
||||||
)
|
)
|
||||||
await daemon.getHeight()
|
await withTimeout(daemon.getHeight(), 8000, 'daemon get_height')
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`[monero] connection check failed:`, e)
|
console.log(`[monero] connection check failed:`, e)
|
||||||
|
|||||||
@@ -108,12 +108,19 @@ export function createMoneroHandlers(state: {
|
|||||||
console.log('[rpc] moneroRestoreWallet:', name)
|
console.log('[rpc] moneroRestoreWallet:', name)
|
||||||
if (!state.manager)
|
if (!state.manager)
|
||||||
throw new Error('Monero wallet RPC not started')
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
// Default to ~2 days of history instead of a full genesis scan
|
||||||
|
// (which can take hours against a remote daemon).
|
||||||
|
let height = restoreHeight
|
||||||
|
if (!height || height === 0) {
|
||||||
|
const tip = await walletGetDaemonHeight(state.manager)
|
||||||
|
if (tip > 1440) height = tip - 1440
|
||||||
|
}
|
||||||
await walletRestore(
|
await walletRestore(
|
||||||
state.manager,
|
state.manager,
|
||||||
name,
|
name,
|
||||||
password,
|
password,
|
||||||
mnemonic,
|
mnemonic,
|
||||||
restoreHeight,
|
height,
|
||||||
)
|
)
|
||||||
const address = await walletGetAddress(state.manager)
|
const address = await walletGetAddress(state.manager)
|
||||||
console.log(
|
console.log(
|
||||||
@@ -144,6 +151,7 @@ export function createMoneroHandlers(state: {
|
|||||||
console.log('[rpc] moneroGetBalance')
|
console.log('[rpc] moneroGetBalance')
|
||||||
if (!state.manager)
|
if (!state.manager)
|
||||||
throw new Error('Monero wallet RPC not started')
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
try {
|
||||||
const { balance, unlocked } = await walletGetBalance(
|
const { balance, unlocked } = await walletGetBalance(
|
||||||
state.manager,
|
state.manager,
|
||||||
)
|
)
|
||||||
@@ -164,6 +172,10 @@ export function createMoneroHandlers(state: {
|
|||||||
height,
|
height,
|
||||||
daemonHeight,
|
daemonHeight,
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logError('[rpc] moneroGetBalance failed', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
},
|
},
|
||||||
moneroGetTransactions: async ({
|
moneroGetTransactions: async ({
|
||||||
accountIndex,
|
accountIndex,
|
||||||
@@ -173,12 +185,17 @@ export function createMoneroHandlers(state: {
|
|||||||
console.log('[rpc] moneroGetTransactions', { accountIndex })
|
console.log('[rpc] moneroGetTransactions', { accountIndex })
|
||||||
if (!state.manager)
|
if (!state.manager)
|
||||||
throw new Error('Monero wallet RPC not started')
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
try {
|
||||||
const txs = await walletGetTransactions(
|
const txs = await walletGetTransactions(
|
||||||
state.manager,
|
state.manager,
|
||||||
accountIndex,
|
accountIndex,
|
||||||
)
|
)
|
||||||
console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`)
|
console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`)
|
||||||
return txs
|
return txs
|
||||||
|
} catch (e) {
|
||||||
|
logError('[rpc] moneroGetTransactions failed', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
},
|
},
|
||||||
moneroWalletStatus: async () => {
|
moneroWalletStatus: async () => {
|
||||||
if (!state.manager) {
|
if (!state.manager) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
electrobun,
|
electrobun,
|
||||||
|
frontendLog,
|
||||||
|
frontendLogError,
|
||||||
type MoneroTxEntry,
|
type MoneroTxEntry,
|
||||||
type MoneroAccountEntry,
|
type MoneroAccountEntry,
|
||||||
type MoneroSendResult,
|
type MoneroSendResult,
|
||||||
@@ -25,7 +27,7 @@ export const atomicToXmr = (atomic: string): string => {
|
|||||||
return frac ? `${whole}.${frac}` : `${whole}`
|
return frac ? `${whole}.${frac}` : `${whole}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MoneroWallet = () => {
|
export const createMoneroWallet = () => {
|
||||||
let accountType = $state<'monero' | null>(null)
|
let accountType = $state<'monero' | null>(null)
|
||||||
let ready = $state(false)
|
let ready = $state(false)
|
||||||
let biometricAvailable = $state(false)
|
let biometricAvailable = $state(false)
|
||||||
@@ -51,6 +53,29 @@ export const MoneroWallet = () => {
|
|||||||
let passwordRequired = $state(false)
|
let passwordRequired = $state(false)
|
||||||
let startInFlight: Promise<{ running: boolean; connected: boolean }> | null = null
|
let startInFlight: Promise<{ running: boolean; connected: boolean }> | null = null
|
||||||
let loginInFlight: Promise<void> | null = null
|
let loginInFlight: Promise<void> | null = null
|
||||||
|
let refreshInFlight: Promise<void> | null = null
|
||||||
|
let syncing = $state(false)
|
||||||
|
let syncRetryTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let price = $state<string | null>(null)
|
||||||
|
let priceInFlight = false
|
||||||
|
|
||||||
|
const rpcTimeout = <T>(promise: Promise<T>, ms: number, label: string): Promise<T> =>
|
||||||
|
new Promise<T>((resolve, reject) => {
|
||||||
|
const timer = setTimeout(
|
||||||
|
() => reject(new Error(`${label} timed out after ${ms}ms`)),
|
||||||
|
ms,
|
||||||
|
)
|
||||||
|
promise.then(
|
||||||
|
(v) => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
resolve(v)
|
||||||
|
},
|
||||||
|
(e) => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
reject(e)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const beginOpening = () => {
|
const beginOpening = () => {
|
||||||
openingDepth += 1
|
openingDepth += 1
|
||||||
@@ -80,7 +105,10 @@ export const MoneroWallet = () => {
|
|||||||
return authed === true
|
return authed === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let initPromise: Promise<void> | null = null
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
|
if (initPromise) return initPromise
|
||||||
|
initPromise = (async () => {
|
||||||
await checkStatus()
|
await checkStatus()
|
||||||
if (installed && !running && !downloading) {
|
if (installed && !running && !downloading) {
|
||||||
await start()
|
await start()
|
||||||
@@ -88,6 +116,8 @@ export const MoneroWallet = () => {
|
|||||||
}
|
}
|
||||||
await checkBiometric()
|
await checkBiometric()
|
||||||
ready = true
|
ready = true
|
||||||
|
})()
|
||||||
|
return initPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
@@ -101,6 +131,11 @@ export const MoneroWallet = () => {
|
|||||||
if (installed && !running) {
|
if (installed && !running) {
|
||||||
await start()
|
await start()
|
||||||
await checkStatus()
|
await checkStatus()
|
||||||
|
} else if (startInFlight) {
|
||||||
|
// a start is already in flight (e.g. triggered by init);
|
||||||
|
// wait for it so we don't call open before the server is up
|
||||||
|
await startInFlight.catch(() => {})
|
||||||
|
await checkStatus()
|
||||||
}
|
}
|
||||||
if (!running) {
|
if (!running) {
|
||||||
error = 'Wallet server failed to start. Please try again.'
|
error = 'Wallet server failed to start. Please try again.'
|
||||||
@@ -198,14 +233,22 @@ export const MoneroWallet = () => {
|
|||||||
const checkStatus = async () => {
|
const checkStatus = async () => {
|
||||||
if (!electrobun.rpc) return
|
if (!electrobun.rpc) return
|
||||||
const [binStatus] = await tryCatch(
|
const [binStatus] = await tryCatch(
|
||||||
|
rpcTimeout(
|
||||||
electrobun.rpc.request.moneroBinaryStatus({}),
|
electrobun.rpc.request.moneroBinaryStatus({}),
|
||||||
|
15000,
|
||||||
|
'moneroBinaryStatus',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if (binStatus) {
|
if (binStatus) {
|
||||||
installed = binStatus.installed
|
installed = binStatus.installed
|
||||||
downloading = binStatus.downloading
|
downloading = binStatus.downloading
|
||||||
}
|
}
|
||||||
const [status] = await tryCatch(
|
const [status] = await tryCatch(
|
||||||
|
rpcTimeout(
|
||||||
electrobun.rpc.request.moneroWalletStatus({}),
|
electrobun.rpc.request.moneroWalletStatus({}),
|
||||||
|
30000,
|
||||||
|
'moneroWalletStatus',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if (status) {
|
if (status) {
|
||||||
running = status.running
|
running = status.running
|
||||||
@@ -357,13 +400,35 @@ export const MoneroWallet = () => {
|
|||||||
if (remember) await moneroStorePassword(target, password)
|
if (remember) await moneroStorePassword(target, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scheduleSyncRetry = () => {
|
||||||
|
if (syncRetryTimer) clearTimeout(syncRetryTimer)
|
||||||
|
syncRetryTimer = setTimeout(async () => {
|
||||||
|
syncRetryTimer = null
|
||||||
|
if (walletOpen && !loading) await refresh()
|
||||||
|
}, 15000)
|
||||||
|
}
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
if (!electrobun.rpc) return
|
const rpc = electrobun.rpc
|
||||||
|
if (!rpc) return
|
||||||
|
if (refreshInFlight) return refreshInFlight
|
||||||
|
refreshInFlight = (async () => {
|
||||||
loading = true
|
loading = true
|
||||||
|
let failed = false
|
||||||
|
const wasSyncing = syncing
|
||||||
|
let lastError: unknown = null
|
||||||
try {
|
try {
|
||||||
const [bal] = await tryCatch(
|
const [bal, balErr] = await tryCatch(
|
||||||
electrobun.rpc.request.moneroGetBalance({}),
|
rpcTimeout(
|
||||||
|
rpc.request.moneroGetBalance({}),
|
||||||
|
20000,
|
||||||
|
'moneroGetBalance',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
if (balErr) {
|
||||||
|
failed = true
|
||||||
|
lastError = balErr
|
||||||
|
}
|
||||||
if (bal) {
|
if (bal) {
|
||||||
balAtomic = bal.balance
|
balAtomic = bal.balance
|
||||||
unlockedAtomic = bal.unlocked
|
unlockedAtomic = bal.unlocked
|
||||||
@@ -371,17 +436,64 @@ export const MoneroWallet = () => {
|
|||||||
height = bal.height
|
height = bal.height
|
||||||
daemonHeight = bal.daemonHeight
|
daemonHeight = bal.daemonHeight
|
||||||
}
|
}
|
||||||
const [result] = await tryCatch(
|
const [result, txErr] = await tryCatch(
|
||||||
electrobun.rpc.request.moneroGetTransactions({
|
rpcTimeout(
|
||||||
|
rpc.request.moneroGetTransactions({
|
||||||
accountIndex: selectedAccountIndex,
|
accountIndex: selectedAccountIndex,
|
||||||
}),
|
}),
|
||||||
|
20000,
|
||||||
|
'moneroGetTransactions',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
if (txErr) {
|
||||||
|
failed = true
|
||||||
|
lastError = txErr
|
||||||
|
}
|
||||||
txs = result ?? []
|
txs = result ?? []
|
||||||
await fetchAccounts()
|
const ok = await fetchAccounts()
|
||||||
|
if (!ok) {
|
||||||
|
failed = true
|
||||||
|
lastError = new Error('moneroGetAccounts failed')
|
||||||
|
}
|
||||||
|
if (failed) {
|
||||||
|
// The wallet-rpc is likely busy syncing; keep the stale
|
||||||
|
// balance and retry shortly instead of showing an error.
|
||||||
|
syncing = true
|
||||||
|
scheduleSyncRetry()
|
||||||
|
frontendLogError(
|
||||||
|
`monero refresh failed (height ${height}/${daemonHeight})`,
|
||||||
|
lastError,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (wasSyncing) {
|
||||||
|
frontendLog(`monero sync complete at height ${height}`)
|
||||||
|
}
|
||||||
|
syncing = false
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : 'Monero refresh failed'
|
syncing = true
|
||||||
|
scheduleSyncRetry()
|
||||||
|
frontendLogError(
|
||||||
|
`monero refresh failed (height ${height}/${daemonHeight})`,
|
||||||
|
e,
|
||||||
|
)
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
|
refreshInFlight = null
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
return refreshInFlight
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchPrice = async () => {
|
||||||
|
const rpc = electrobun.rpc
|
||||||
|
if (!rpc || priceInFlight) return
|
||||||
|
priceInFlight = true
|
||||||
|
try {
|
||||||
|
const [p] = await tryCatch(rpc.request.fetchMoneroPrice({}))
|
||||||
|
if (p) price = p.usd
|
||||||
|
} finally {
|
||||||
|
priceInFlight = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,6 +613,12 @@ export const MoneroWallet = () => {
|
|||||||
get passwordRequired() {
|
get passwordRequired() {
|
||||||
return passwordRequired
|
return passwordRequired
|
||||||
},
|
},
|
||||||
|
get syncing() {
|
||||||
|
return syncing
|
||||||
|
},
|
||||||
|
get price() {
|
||||||
|
return price
|
||||||
|
},
|
||||||
get error() {
|
get error() {
|
||||||
return error
|
return error
|
||||||
},
|
},
|
||||||
@@ -550,6 +668,7 @@ export const MoneroWallet = () => {
|
|||||||
autoUnlock,
|
autoUnlock,
|
||||||
unlockWallet,
|
unlockWallet,
|
||||||
refresh,
|
refresh,
|
||||||
|
fetchPrice,
|
||||||
send,
|
send,
|
||||||
sendAll,
|
sendAll,
|
||||||
createAccount,
|
createAccount,
|
||||||
@@ -557,4 +676,15 @@ export const MoneroWallet = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Singleton: calling MoneroWallet() always returns the same instance, so
|
||||||
|
// no component can ever create a second, disconnected wallet state.
|
||||||
|
type MoneroWalletStore = ReturnType<typeof createMoneroWallet>
|
||||||
|
|
||||||
|
let moneroWalletInstance: MoneroWalletStore | null = null
|
||||||
|
|
||||||
|
export const MoneroWallet = (): MoneroWalletStore => {
|
||||||
|
if (!moneroWalletInstance) moneroWalletInstance = createMoneroWallet()
|
||||||
|
return moneroWalletInstance
|
||||||
|
}
|
||||||
|
|
||||||
export const moneroWallet = MoneroWallet()
|
export const moneroWallet = MoneroWallet()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const xrpNetwork = {
|
|||||||
explorerAddressUrl: 'https://xrpscan.com/account/',
|
explorerAddressUrl: 'https://xrpscan.com/account/',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const XrpWallet = () => {
|
export const createXrpWallet = () => {
|
||||||
let accountType = $state<'xrp' | null>(null)
|
let accountType = $state<'xrp' | null>(null)
|
||||||
let ready = $state(false)
|
let ready = $state(false)
|
||||||
let biometricAvailable = $state(false)
|
let biometricAvailable = $state(false)
|
||||||
@@ -29,6 +29,9 @@ export const XrpWallet = () => {
|
|||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
let error = $state('')
|
let error = $state('')
|
||||||
let wallets = $state<XrpWalletInfo[]>([])
|
let wallets = $state<XrpWalletInfo[]>([])
|
||||||
|
let balances = $state<Record<string, { balance: number; funded: boolean }>>({})
|
||||||
|
let balancesLoading = $state(false)
|
||||||
|
let fetchBalancesInFlight = false
|
||||||
let currentWalletId = $state<string | null>(null)
|
let currentWalletId = $state<string | null>(null)
|
||||||
let currentPasswordHash = $state<string | null>(null)
|
let currentPasswordHash = $state<string | null>(null)
|
||||||
|
|
||||||
@@ -73,7 +76,10 @@ export const XrpWallet = () => {
|
|||||||
biometricAvailable = ok === true
|
biometricAvailable = ok === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let initPromise: Promise<void> | null = null
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
|
if (initPromise) return initPromise
|
||||||
|
initPromise = (async () => {
|
||||||
if (!electrobun.rpc) return
|
if (!electrobun.rpc) return
|
||||||
const [walletList] = await tryCatch(
|
const [walletList] = await tryCatch(
|
||||||
electrobun.rpc.request.xrpListWallets({}),
|
electrobun.rpc.request.xrpListWallets({}),
|
||||||
@@ -86,6 +92,8 @@ export const XrpWallet = () => {
|
|||||||
}
|
}
|
||||||
await checkBiometric()
|
await checkBiometric()
|
||||||
ready = true
|
ready = true
|
||||||
|
})()
|
||||||
|
return initPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
@@ -319,6 +327,34 @@ export const XrpWallet = () => {
|
|||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchBalances = async () => {
|
||||||
|
const rpc = electrobun.rpc
|
||||||
|
if (!rpc || fetchBalancesInFlight) return
|
||||||
|
if (wallets.length === 0) return
|
||||||
|
fetchBalancesInFlight = true
|
||||||
|
balancesLoading = true
|
||||||
|
try {
|
||||||
|
const [p] = await tryCatch(rpc.request.fetchXrpPrice({}))
|
||||||
|
if (p) price = p.usd
|
||||||
|
await Promise.allSettled(
|
||||||
|
wallets.map(async (w) => {
|
||||||
|
const [res] = await tryCatch(
|
||||||
|
rpc.request.xrpGetBalance({ address: w.address }),
|
||||||
|
)
|
||||||
|
if (res) {
|
||||||
|
balances[w.address] = {
|
||||||
|
balance: parseFloat(res.balance),
|
||||||
|
funded: res.funded,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
balancesLoading = false
|
||||||
|
fetchBalancesInFlight = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const send = async (
|
const send = async (
|
||||||
to: string,
|
to: string,
|
||||||
amount: string,
|
amount: string,
|
||||||
@@ -399,6 +435,12 @@ export const XrpWallet = () => {
|
|||||||
get wallets() {
|
get wallets() {
|
||||||
return wallets
|
return wallets
|
||||||
},
|
},
|
||||||
|
get balances() {
|
||||||
|
return balances
|
||||||
|
},
|
||||||
|
get balancesLoading() {
|
||||||
|
return balancesLoading
|
||||||
|
},
|
||||||
get currentWalletId() {
|
get currentWalletId() {
|
||||||
return currentWalletId
|
return currentWalletId
|
||||||
},
|
},
|
||||||
@@ -424,6 +466,7 @@ export const XrpWallet = () => {
|
|||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
refresh,
|
refresh,
|
||||||
|
fetchBalances,
|
||||||
createWallet,
|
createWallet,
|
||||||
importWallet,
|
importWallet,
|
||||||
lock,
|
lock,
|
||||||
@@ -442,4 +485,15 @@ export const XrpWallet = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Singleton: calling XrpWallet() always returns the same instance, so no
|
||||||
|
// component can ever create a second, disconnected wallet state.
|
||||||
|
type XrpWalletStore = ReturnType<typeof createXrpWallet>
|
||||||
|
|
||||||
|
let xrpWalletInstance: XrpWalletStore | null = null
|
||||||
|
|
||||||
|
export const XrpWallet = (): XrpWalletStore => {
|
||||||
|
if (!xrpWalletInstance) xrpWalletInstance = createXrpWallet()
|
||||||
|
return xrpWalletInstance
|
||||||
|
}
|
||||||
|
|
||||||
export const xrpWallet = XrpWallet()
|
export const xrpWallet = XrpWallet()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
|
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
|
||||||
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
|
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
|
||||||
import { electrobun } from '$lib/electrobun.js'
|
|
||||||
import { navigate } from 'sv-router/generated'
|
import { navigate } from 'sv-router/generated'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import {
|
import {
|
||||||
@@ -17,29 +16,19 @@
|
|||||||
import Loader from '$lib/components/loader.svelte'
|
import Loader from '$lib/components/loader.svelte'
|
||||||
import { frontendLog, frontendLogError } from '$lib/electrobun.js'
|
import { frontendLog, frontendLogError } from '$lib/electrobun.js'
|
||||||
|
|
||||||
let xrpPrice = $state<string | null>(null)
|
|
||||||
let xmrPrice = $state<string | null>(null)
|
|
||||||
let xrpBalances = $state<Record<string, { balance: number; funded: boolean }>>({})
|
|
||||||
let loadingXrp = $state(true)
|
|
||||||
let loadingXmr = $state(true)
|
|
||||||
let fetchXrpInFlight = false
|
|
||||||
|
|
||||||
let feError = $state<string | null>(null)
|
let feError = $state<string | null>(null)
|
||||||
|
|
||||||
const usd = (amount: string, price: string | null) =>
|
|
||||||
price ? parseFloat(amount) * parseFloat(price) : 0
|
|
||||||
|
|
||||||
const xrpTotalUsd = $derived.by(() => {
|
const xrpTotalUsd = $derived.by(() => {
|
||||||
const price = xrpPrice
|
const p = xrpWallet.price
|
||||||
return xrpWallet.wallets.reduce((sum, w) => {
|
return xrpWallet.wallets.reduce((sum, w) => {
|
||||||
const b = xrpBalances[w.address]?.balance ?? 0
|
const b = xrpWallet.balances[w.address]?.balance ?? 0
|
||||||
return sum + (price ? b * parseFloat(price) : 0)
|
return sum + (p ? b * parseFloat(p) : 0)
|
||||||
}, 0)
|
}, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
const xrpTotalBalance = $derived.by(() =>
|
const xrpTotalBalance = $derived.by(() =>
|
||||||
xrpWallet.wallets.reduce((sum, w) => {
|
xrpWallet.wallets.reduce((sum, w) => {
|
||||||
return sum + (xrpBalances[w.address]?.balance ?? 0)
|
return sum + (xrpWallet.balances[w.address]?.balance ?? 0)
|
||||||
}, 0),
|
}, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,8 +36,8 @@
|
|||||||
moneroWallet.walletOpen ? parseFloat(moneroWallet.balance || '0') : 0,
|
moneroWallet.walletOpen ? parseFloat(moneroWallet.balance || '0') : 0,
|
||||||
)
|
)
|
||||||
const xmrTotalUsd = $derived(
|
const xmrTotalUsd = $derived(
|
||||||
moneroWallet.walletOpen && xmrPrice
|
moneroWallet.walletOpen && moneroWallet.price
|
||||||
? xmrBalance * parseFloat(xmrPrice)
|
? xmrBalance * parseFloat(moneroWallet.price)
|
||||||
: 0,
|
: 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -70,80 +59,21 @@
|
|||||||
maximumFractionDigits: dp,
|
maximumFractionDigits: dp,
|
||||||
})
|
})
|
||||||
|
|
||||||
async function fetchXrp() {
|
|
||||||
if (fetchXrpInFlight) return
|
|
||||||
fetchXrpInFlight = true
|
|
||||||
loadingXrp = true
|
|
||||||
const rpc = electrobun.rpc
|
|
||||||
if (!rpc) {
|
|
||||||
loadingXrp = false
|
|
||||||
fetchXrpInFlight = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const p = await rpc.request.fetchXrpPrice({})
|
|
||||||
if (p) xrpPrice = p.usd
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
await Promise.allSettled(
|
|
||||||
xrpWallet.wallets.map(async (w) => {
|
|
||||||
const res = await rpc.request.xrpGetBalance({
|
|
||||||
address: w.address,
|
|
||||||
})
|
|
||||||
xrpBalances[w.address] = {
|
|
||||||
balance: parseFloat(res.balance),
|
|
||||||
funded: res.funded,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
loadingXrp = false
|
|
||||||
fetchXrpInFlight = false
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchXmr() {
|
|
||||||
loadingXmr = true
|
|
||||||
const rpc = electrobun.rpc
|
|
||||||
if (!rpc) {
|
|
||||||
loadingXmr = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const p = await rpc.request.fetchMoneroPrice({})
|
|
||||||
xmrPrice = p?.usd ?? null
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
loadingXmr = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
frontendLog('dashboard mounted')
|
frontendLog('dashboard mounted')
|
||||||
moneroWallet.init().catch((e) => frontendLogError('moneroWallet.init() failed', e))
|
|
||||||
xrpWallet.init().catch((e) => frontendLogError('xrpWallet.init() failed', e))
|
|
||||||
fetchXmr()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Wait for the XRP wallet list to load, then fetch balances.
|
// Balances live in the stores so they survive navigation; the effects
|
||||||
|
// only trigger background refetches.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (xrpWallet.ready && xrpWallet.wallets.length > 0) {
|
if (xrpWallet.ready && xrpWallet.wallets.length > 0) {
|
||||||
fetchXrp()
|
xrpWallet.fetchBalances()
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Self-heal: if the monero wallet list never got loaded (e.g. the server
|
|
||||||
// was starting while the dashboard rendered), retry login once.
|
|
||||||
$effect(() => {
|
|
||||||
if (
|
|
||||||
moneroWallet.ready &&
|
|
||||||
!moneroWallet.walletOpen &&
|
|
||||||
!moneroWallet.opening &&
|
|
||||||
moneroWallet.wallets.length === 0 &&
|
|
||||||
!moneroWallet.error
|
|
||||||
) {
|
|
||||||
void moneroWallet.login()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (moneroWallet.walletOpen) {
|
if (moneroWallet.walletOpen) {
|
||||||
fetchXmr()
|
moneroWallet.fetchPrice()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -205,7 +135,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col items-center pt-4 pb-2">
|
<div class="flex flex-col items-center pt-4 pb-2">
|
||||||
<p class="text-xs text-muted-foreground">Total Portfolio Value</p>
|
<p class="text-xs text-muted-foreground">Total Portfolio Value</p>
|
||||||
{#if loadingXrp || loadingXmr || moneroWallet.opening || (moneroWallet.wallets.length > 0 && !moneroWallet.walletOpen)}
|
{#if moneroWallet.opening || (moneroWallet.wallets.length > 0 && !moneroWallet.walletOpen) || (xrpWallet.wallets.length > 0 && !xrpWallet.price) || (moneroWallet.walletOpen && !moneroWallet.price)}
|
||||||
<div class="mt-2 h-9 w-44 rounded-md bg-muted animate-pulse"></div>
|
<div class="mt-2 h-9 w-44 rounded-md bg-muted animate-pulse"></div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="mt-1 text-3xl font-semibold tabular-nums">
|
<p class="mt-1 text-3xl font-semibold tabular-nums">
|
||||||
@@ -229,16 +159,16 @@
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="flex flex-1 flex-col justify-center">
|
<CardContent class="flex flex-1 flex-col justify-center">
|
||||||
{#if !xrpWallet.ready || loadingXrp || (xrpWallet.wallets.length > 0 && Object.keys(xrpBalances).length === 0)}
|
{#if !xrpWallet.ready || (xrpWallet.wallets.length > 0 && Object.keys(xrpWallet.balances).length === 0)}
|
||||||
<div class="h-14 flex items-center"><Loader /></div>
|
<div class="h-14 flex items-center"><Loader /></div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="font-mono text-xl tabular-nums">{fmtBal(xrpTotalBalance)}</p>
|
<p class="font-mono text-xl tabular-nums">{fmtBal(xrpTotalBalance)}</p>
|
||||||
{#if xrpPrice}
|
{#if xrpWallet.price}
|
||||||
<p class="mt-0.5 text-sm text-muted-foreground tabular-nums">
|
<p class="mt-0.5 text-sm text-muted-foreground tabular-nums">
|
||||||
${fmtUsd(xrpTotalUsd)}
|
${fmtUsd(xrpTotalUsd)}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-xs text-muted-foreground">
|
<p class="mt-2 text-xs text-muted-foreground">
|
||||||
${fmtUsd(parseFloat(xrpPrice) )} / XRP
|
${fmtUsd(parseFloat(xrpWallet.price) )} / XRP
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -266,12 +196,16 @@
|
|||||||
<CardContent class="flex flex-1 flex-col justify-center">
|
<CardContent class="flex flex-1 flex-col justify-center">
|
||||||
{#if moneroWallet.walletOpen}
|
{#if moneroWallet.walletOpen}
|
||||||
<p class="font-mono text-xl tabular-nums">{fmtBal(xmrBalance, 4)}</p>
|
<p class="font-mono text-xl tabular-nums">{fmtBal(xmrBalance, 4)}</p>
|
||||||
{#if xmrPrice}
|
{#if moneroWallet.syncing}
|
||||||
|
<p class="mt-0.5 text-xs text-muted-foreground">Syncing…</p>
|
||||||
|
{:else if moneroWallet.price}
|
||||||
<p class="mt-0.5 text-sm text-muted-foreground tabular-nums">
|
<p class="mt-0.5 text-sm text-muted-foreground tabular-nums">
|
||||||
${fmtUsd(xmrTotalUsd)}
|
${fmtUsd(xmrTotalUsd)}
|
||||||
</p>
|
</p>
|
||||||
|
{/if}
|
||||||
|
{#if moneroWallet.price}
|
||||||
<p class="mt-2 text-xs text-muted-foreground">
|
<p class="mt-2 text-xs text-muted-foreground">
|
||||||
${fmtUsd(parseFloat(xmrPrice) )} / XMR
|
${fmtUsd(parseFloat(moneroWallet.price) )} / XMR
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if moneroWallet.passwordRequired && moneroWallet.wallets.length > 0}
|
{:else if moneroWallet.passwordRequired && moneroWallet.wallets.length > 0}
|
||||||
@@ -299,8 +233,6 @@
|
|||||||
<Loader />
|
<Loader />
|
||||||
<p class="text-sm text-muted-foreground">Opening wallet...</p>
|
<p class="text-sm text-muted-foreground">Opening wallet...</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if loadingXmr}
|
|
||||||
<div class="h-14 flex items-center"><Loader /></div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<Button
|
<Button
|
||||||
onclick={() => navigate('/monero')}
|
onclick={() => navigate('/monero')}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
if (!xrpWallet.ready) await xrpWallet.init()
|
if (!xrpWallet.ready) await xrpWallet.init()
|
||||||
if (!moneroWallet.ready) await moneroWallet.init()
|
if (!moneroWallet.ready) await moneroWallet.init()
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
|
xrpWallet.login(),
|
||||||
xrpWallet.autoUnlock(),
|
xrpWallet.autoUnlock(),
|
||||||
moneroWallet.login(),
|
moneroWallet.login(),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -24,8 +24,6 @@
|
|||||||
let unlocking = $state(false)
|
let unlocking = $state(false)
|
||||||
let unlockError = $state('')
|
let unlockError = $state('')
|
||||||
|
|
||||||
let initStarted = false
|
|
||||||
|
|
||||||
async function unlockWithPassword() {
|
async function unlockWithPassword() {
|
||||||
if (unlocking || !walletPassword.trim()) return
|
if (unlocking || !walletPassword.trim()) return
|
||||||
unlocking = true
|
unlocking = true
|
||||||
@@ -48,16 +46,6 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (!initStarted) {
|
|
||||||
initStarted = true
|
|
||||||
w.init().then(async () => {
|
|
||||||
if (w.accountType === 'monero') return
|
|
||||||
await w.login()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (w.running && !w.walletOpen && w.wallets.length > 0 && !w.loading && !w.opening) {
|
if (w.running && !w.walletOpen && w.wallets.length > 0 && !w.loading && !w.opening) {
|
||||||
w.autoUnlock(w.wallets[0])
|
w.autoUnlock(w.wallets[0])
|
||||||
|
|||||||
@@ -15,10 +15,6 @@
|
|||||||
const w = moneroWallet
|
const w = moneroWallet
|
||||||
|
|
||||||
let moneroSettingsPw = $state('')
|
let moneroSettingsPw = $state('')
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
w.init()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto max-w-xl">
|
<div class="mx-auto max-w-xl">
|
||||||
|
|||||||
@@ -203,7 +203,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex flex-col items-center py-2">
|
<div class="flex flex-col items-center py-2">
|
||||||
{#if w.loading}
|
{#if w.loading && !w.syncing}
|
||||||
<div class="h-12 w-40 rounded-lg bg-muted animate-pulse"></div>
|
<div class="h-12 w-40 rounded-lg bg-muted animate-pulse"></div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-sm text-muted-foreground">Balance</p>
|
<p class="text-sm text-muted-foreground">Balance</p>
|
||||||
@@ -223,6 +223,14 @@
|
|||||||
Unlocked: {w.unlocked} XMR
|
Unlocked: {w.unlocked} XMR
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if w.syncing}
|
||||||
|
<div class="mt-2 flex items-center gap-2">
|
||||||
|
<Loader />
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Syncing wallet — balance updates when the scan completes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="mt-2 flex items-center gap-1.5">
|
<div class="mt-2 flex items-center gap-1.5">
|
||||||
<div class="size-2 rounded-full {w.connected ? '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">
|
<span class="text-xs text-muted-foreground">
|
||||||
|
|||||||
@@ -22,18 +22,6 @@
|
|||||||
let inputUnlockPassword = $state('')
|
let inputUnlockPassword = $state('')
|
||||||
let newSeed = $state<string | null>(null)
|
let newSeed = $state<string | null>(null)
|
||||||
|
|
||||||
let initStarted = false
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (!initStarted) {
|
|
||||||
initStarted = true
|
|
||||||
w.init().then(async () => {
|
|
||||||
if (w.accountType === 'xrp') return
|
|
||||||
await w.login()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (w.accountType === 'xrp' && w.currentWalletId && w.seed && !newSeed) {
|
if (w.accountType === 'xrp' && w.currentWalletId && w.seed && !newSeed) {
|
||||||
navigate('/xrp/wallet/:id', {
|
navigate('/xrp/wallet/:id', {
|
||||||
|
|||||||
Reference in New Issue
Block a user