Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
743700b82c | ||
|
|
8953596194 | ||
|
|
5df3ae7058 | ||
|
|
7b4ba84fee | ||
|
|
20f29faa80 | ||
|
|
c0f6417925 | ||
|
|
c61fea938a | ||
|
|
bdfee2a553 | ||
|
|
1c5e5b617f | ||
|
|
11e0bcdaf6 | ||
|
|
4270d380fb | ||
|
|
af6251669a | ||
|
|
6eec1f998d | ||
|
|
d9ca8a0047 |
@@ -1,9 +1,10 @@
|
||||
# Instructions for AI assistants
|
||||
|
||||
- Never commit changes unless explicitly told to do so. Always ask first or wait for the user's command.
|
||||
- "Apply the changes" / "apply it" / "do it" does NOT mean commit. It means make the file changes. Before any commit (main repo, tap repo, anywhere), stop and ask for explicit permission, and list which files will be committed. Wait for a clear "yes" / "commit it" before running `git commit`.
|
||||
- "Apply the changes" / "apply it" / "do it" does NOT mean commit. It means make the file changes. Before any commit (main repo, tap repo, anywhere), stop and ask for explicit permission, and list which files will be committed. Wait for a clear "yes" / "commit it" / "commit and push" before running `git commit`.
|
||||
- Never manually edit `.router/router.ts` — sv-router auto-generates it from the filesystem. Just create the route file in the correct filesystem location.
|
||||
- Only commit when explicitly told to do so. When asked to commit, always write a single simple line commit message.
|
||||
- When asked to publish, always rebuild the DMG first (`bun run build:stable` from `apps/desktop`), wait for notarization to complete, verify with `xcrun stapler validate`, then publish the fresh DMG. Never publish a stale build.
|
||||
- Never publish without explicit permission. Before any publish action — running `bun run build:stable`, `gh release upload`, updating the Homebrew cask, or pushing the tap repo — describe the intended action (what will be built, what SHA, which release) and ask for permission. Once you have asked and received a clear affirmative (any clear yes, "go", "publish", etc.), you may proceed — the affirmative applies to the action you described, and you must still pause before each subsequent publish step (e.g. upload, cask update) to ask for permission again.
|
||||
- At the end of every response, always surface a "Pending from you" section listing anything you need from the user (decisions, permissions, credentials, confirming a plan, etc.). If nothing is pending, say "Nothing pending."
|
||||
- Never publish without explicit permission. Before any publish action — running `bun run build:stable`, `gh release upload`, updating the Homebrew cask, or pushing the tap repo — describe the intended action (what will be built, what SHA, which release) and ask for permission. Once you have asked and received a clear affirmative (any clear yes, "go", "publish", etc.), you may proceed through the whole pipeline without pausing between steps: build, notarize, verify, upload to the GitHub release, update the Homebrew cask, and push the tap repo. One permission covers the entire publish flow. You must still get a separate permission before starting each new publish cycle.
|
||||
- Permission to commit ("commit", "commit and push", or similar) authorizes `git commit` and also `git push` of the committed changes. Only push the branches/files described in the permission.
|
||||
- At the end of every response, always surface a "Pending from you" section listing anything you need from the user (decisions, permissions, credentials, confirming a plan, etc.). If nothing is pending, say "Nothing pending." List each pending item as a numbered entry (`1.`, `2.`, …) so the user can approve or act on them by number (e.g. "go ahead with 1, 2").
|
||||
|
||||
@@ -4,7 +4,7 @@ export default {
|
||||
app: {
|
||||
name: 'koins',
|
||||
identifier: 'koins.tzgyn.com',
|
||||
version: '0.0.3',
|
||||
version: '0.0.11',
|
||||
},
|
||||
build: {
|
||||
// Vite builds to dist/, we copy from there
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import {
|
||||
MoneroWalletRpc,
|
||||
MoneroRpcConnection,
|
||||
connectToWalletRpc,
|
||||
connectToDaemonRpc,
|
||||
} from 'monero-ts'
|
||||
import { MoneroWalletRpc, MoneroRpcConnection, connectToWalletRpc, connectToDaemonRpc } from 'monero-ts'
|
||||
import { getBinaryPath, getWalletDir } from './binary'
|
||||
import { existsSync, readdirSync } from 'fs'
|
||||
import { log, logError } from '../logger'
|
||||
|
||||
const DEFAULT_DAEMON = 'xmr-node.cakewallet.com:18081'
|
||||
|
||||
@@ -45,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(
|
||||
state: MoneroWalletState,
|
||||
method: string,
|
||||
params: Record<string, any> = {},
|
||||
): Promise<any> {
|
||||
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)
|
||||
throw new Error(res.error.message || JSON.stringify(res.error))
|
||||
return res?.result
|
||||
@@ -65,20 +88,17 @@ async function waitForRpc(
|
||||
let lastErr = ''
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
await Bun.connect({
|
||||
hostname: '127.0.0.1',
|
||||
port: state.rpcPort,
|
||||
socket: {
|
||||
open(s) {
|
||||
s.end()
|
||||
},
|
||||
close() {},
|
||||
data() {},
|
||||
error() {},
|
||||
},
|
||||
})
|
||||
const conn = rpcConnection(state)
|
||||
const res = await withTimeout(
|
||||
conn.sendJsonRequest('get_version', {}),
|
||||
5000,
|
||||
'wallet-rpc probe',
|
||||
)
|
||||
if (res?.result?.version) {
|
||||
console.log(`[monero] RPC ready after ${Date.now() - start}ms`)
|
||||
return
|
||||
}
|
||||
lastErr = JSON.stringify(res)
|
||||
} catch (e) {
|
||||
lastErr = String(e)
|
||||
}
|
||||
@@ -89,6 +109,28 @@ async function waitForRpc(
|
||||
)
|
||||
}
|
||||
|
||||
async function connectClient(
|
||||
state: MoneroWalletState,
|
||||
): Promise<MoneroWalletRpc> {
|
||||
let lastErr = ''
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
const client = (await connectToWalletRpc(
|
||||
rpcUrl(state),
|
||||
state.rpcUser,
|
||||
state.rpcPassword,
|
||||
)) as MoneroWalletRpc
|
||||
console.log(`[monero] MoneroWalletRpc connected (attempt ${attempt})`)
|
||||
return client
|
||||
} catch (e) {
|
||||
lastErr = String(e)
|
||||
console.log(`[monero] connectToWalletRpc attempt ${attempt} failed:`, e)
|
||||
if (attempt < 3) await new Promise((r) => setTimeout(r, 1000))
|
||||
}
|
||||
}
|
||||
throw new Error(`Failed to connect to monero-wallet-rpc: ${lastErr}`)
|
||||
}
|
||||
|
||||
export async function start(
|
||||
state: MoneroWalletState,
|
||||
daemonAddress: string = DEFAULT_DAEMON,
|
||||
@@ -169,6 +211,9 @@ export async function start(
|
||||
`[monero] wallet-rpc process exited with code ${exitCode}`,
|
||||
error?.message ?? '',
|
||||
)
|
||||
log(
|
||||
`[monero] wallet-rpc process exited with code ${exitCode} ${error?.message ?? ''}`,
|
||||
)
|
||||
if (state.saveTimer) {
|
||||
clearInterval(state.saveTimer)
|
||||
state.saveTimer = null
|
||||
@@ -222,12 +267,9 @@ export async function start(
|
||||
console.log(`[monero] RPC server is ready`)
|
||||
|
||||
console.log(`[monero] connecting MoneroWalletRpc client...`)
|
||||
state.wallet = (await connectToWalletRpc(
|
||||
rpcUrl(state),
|
||||
state.rpcUser,
|
||||
state.rpcPassword,
|
||||
)) as MoneroWalletRpc
|
||||
state.wallet = await connectClient(state)
|
||||
console.log(`[monero] MoneroWalletRpc connected successfully`)
|
||||
log(`[monero] wallet-rpc started, RPC ready`)
|
||||
}
|
||||
|
||||
export async function stop(state: MoneroWalletState) {
|
||||
@@ -241,21 +283,21 @@ export async function stop(state: MoneroWalletState) {
|
||||
if (state.wallet) {
|
||||
await rawRpc(state, 'store')
|
||||
console.log(`[monero] wallet saved before stop`)
|
||||
log(`[monero] wallet saved before stop`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`[monero] wallet save before stop failed:`, e)
|
||||
}
|
||||
if (state.process) {
|
||||
console.log(
|
||||
`[monero] sending SIGTERM to wallet-rpc (pid ${state.process.pid})`,
|
||||
)
|
||||
state.process.kill()
|
||||
await Promise.race([state.process.exited, sleep(3000)])
|
||||
if (state.process.exitCode === null) {
|
||||
const proc = state.process
|
||||
console.log(`[monero] sending SIGTERM to wallet-rpc (pid ${proc.pid})`)
|
||||
proc.kill()
|
||||
await Promise.race([proc.exited, sleep(3000)])
|
||||
if (proc.exitCode === null) {
|
||||
console.log(
|
||||
`[monero] wallet-rpc did not exit gracefully, sending SIGKILL`,
|
||||
)
|
||||
state.process.kill('SIGKILL')
|
||||
proc.kill('SIGKILL')
|
||||
}
|
||||
state.process = null
|
||||
}
|
||||
@@ -279,7 +321,11 @@ export async function createWallet(
|
||||
language: 'English',
|
||||
})
|
||||
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}`)
|
||||
return { mnemonic: result.mnemonic, address }
|
||||
}
|
||||
@@ -310,9 +356,20 @@ export async function openWallet(
|
||||
password: string,
|
||||
) {
|
||||
console.log(`[monero] opening wallet: ${name}`)
|
||||
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||
await state.wallet.openWallet(name, password)
|
||||
console.log(`[monero] wallet opened: ${name}`)
|
||||
// Self-heal: the client connection may have been lost while the
|
||||
// wallet-rpc process is still running (e.g. stale-process cleanup).
|
||||
if (!state.wallet) {
|
||||
if (!state.process) throw new Error('Wallet RPC not started')
|
||||
log(`[monero] reconnecting wallet client before open (${name})`)
|
||||
state.wallet = await connectClient(state)
|
||||
}
|
||||
try {
|
||||
await withTimeout(state.wallet.openWallet(name, password), 30000, `open_wallet ${name}`)
|
||||
log(`[monero] wallet opened: ${name}`)
|
||||
} catch (e) {
|
||||
logError(`[monero] wallet open failed (${name})`, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export async function closeWallet(state: MoneroWalletState) {
|
||||
@@ -326,8 +383,16 @@ export async function getBalance(
|
||||
state: MoneroWalletState,
|
||||
): Promise<{ balance: bigint; unlocked: bigint }> {
|
||||
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||
const balance = (await state.wallet.getBalance()) as bigint
|
||||
const unlocked = (await state.wallet.getUnlockedBalance()) as bigint
|
||||
const balance = (await withTimeout(
|
||||
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})`)
|
||||
return { balance, unlocked }
|
||||
}
|
||||
@@ -338,9 +403,10 @@ export async function getAddress(
|
||||
subIdx = 0,
|
||||
): Promise<string> {
|
||||
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||
const address = (await state.wallet.getAddress(
|
||||
accountIdx,
|
||||
subIdx,
|
||||
const address = (await withTimeout(
|
||||
state.wallet.getAddress(accountIdx, subIdx),
|
||||
30000,
|
||||
'wallet get_address',
|
||||
)) as string
|
||||
console.log(`[monero] address: ${address}`)
|
||||
return address
|
||||
@@ -376,7 +442,11 @@ export async function getAccounts(
|
||||
state: MoneroWalletState,
|
||||
): Promise<any[]> {
|
||||
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`)
|
||||
if (accounts?.length) {
|
||||
for (const acct of accounts) {
|
||||
@@ -521,7 +591,11 @@ export async function getHeight(
|
||||
state: MoneroWalletState,
|
||||
): Promise<number> {
|
||||
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}`)
|
||||
return height
|
||||
}
|
||||
@@ -565,7 +639,11 @@ export async function getDaemonHeight(
|
||||
const daemon = await connectToDaemonRpc(
|
||||
`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}`)
|
||||
return height
|
||||
} catch (e) {
|
||||
@@ -643,9 +721,13 @@ export async function isWalletOpen(
|
||||
): Promise<boolean> {
|
||||
if (!state.wallet) return false
|
||||
try {
|
||||
await state.wallet.getHeight()
|
||||
await withTimeout(state.wallet.getHeight(), 8000, 'wallet get_height')
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -655,10 +737,12 @@ export async function isConnected(
|
||||
): Promise<boolean> {
|
||||
if (!state.wallet) return false
|
||||
try {
|
||||
const daemon = await connectToDaemonRpc(
|
||||
`http://${state.daemonAddress}`,
|
||||
const daemon = await withTimeout(
|
||||
connectToDaemonRpc(`http://${state.daemonAddress}`),
|
||||
8000,
|
||||
'daemon connect',
|
||||
)
|
||||
await daemon.getHeight()
|
||||
await withTimeout(daemon.getHeight(), 8000, 'daemon get_height')
|
||||
return true
|
||||
} catch (e) {
|
||||
console.log(`[monero] connection check failed:`, e)
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
isConnected,
|
||||
} from '../../lib/monero'
|
||||
import type { MoneroWalletState } from '../../lib/monero'
|
||||
import { log, logError } from '../../lib/logger'
|
||||
|
||||
export function createMoneroHandlers(state: {
|
||||
manager: MoneroWalletState | null
|
||||
@@ -64,10 +65,12 @@ export function createMoneroHandlers(state: {
|
||||
await walletStart(state.manager, daemonAddress)
|
||||
} catch (e) {
|
||||
console.log('[rpc] moneroStart error:', e)
|
||||
logError('[rpc] moneroStart error', e)
|
||||
return { running: false, walletOpen: false, connected: false }
|
||||
}
|
||||
const connected = await isConnected(state.manager)
|
||||
console.log('[rpc] moneroStart complete, connected:', connected)
|
||||
log(`[rpc] moneroStart complete, connected: ${connected}`)
|
||||
return { running: true, walletOpen: false, connected }
|
||||
},
|
||||
moneroStop: async () => {
|
||||
@@ -105,12 +108,19 @@ export function createMoneroHandlers(state: {
|
||||
console.log('[rpc] moneroRestoreWallet:', name)
|
||||
if (!state.manager)
|
||||
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(
|
||||
state.manager,
|
||||
name,
|
||||
password,
|
||||
mnemonic,
|
||||
restoreHeight,
|
||||
height,
|
||||
)
|
||||
const address = await walletGetAddress(state.manager)
|
||||
console.log(
|
||||
@@ -129,12 +139,19 @@ export function createMoneroHandlers(state: {
|
||||
console.log('[rpc] moneroOpenWallet:', name)
|
||||
if (!state.manager)
|
||||
throw new Error('Monero wallet RPC not started')
|
||||
try {
|
||||
await walletOpen(state.manager, name, password)
|
||||
log(`[rpc] moneroOpenWallet ok: ${name}`)
|
||||
} catch (e) {
|
||||
logError(`[rpc] moneroOpenWallet failed (${name})`, e)
|
||||
throw e
|
||||
}
|
||||
},
|
||||
moneroGetBalance: async () => {
|
||||
console.log('[rpc] moneroGetBalance')
|
||||
if (!state.manager)
|
||||
throw new Error('Monero wallet RPC not started')
|
||||
try {
|
||||
const { balance, unlocked } = await walletGetBalance(
|
||||
state.manager,
|
||||
)
|
||||
@@ -155,6 +172,10 @@ export function createMoneroHandlers(state: {
|
||||
height,
|
||||
daemonHeight,
|
||||
}
|
||||
} catch (e) {
|
||||
logError('[rpc] moneroGetBalance failed', e)
|
||||
throw e
|
||||
}
|
||||
},
|
||||
moneroGetTransactions: async ({
|
||||
accountIndex,
|
||||
@@ -164,12 +185,17 @@ export function createMoneroHandlers(state: {
|
||||
console.log('[rpc] moneroGetTransactions', { accountIndex })
|
||||
if (!state.manager)
|
||||
throw new Error('Monero wallet RPC not started')
|
||||
try {
|
||||
const txs = await walletGetTransactions(
|
||||
state.manager,
|
||||
accountIndex,
|
||||
)
|
||||
console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`)
|
||||
return txs
|
||||
} catch (e) {
|
||||
logError('[rpc] moneroGetTransactions failed', e)
|
||||
throw e
|
||||
}
|
||||
},
|
||||
moneroWalletStatus: async () => {
|
||||
if (!state.manager) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
electrobun,
|
||||
frontendLog,
|
||||
frontendLogError,
|
||||
type MoneroTxEntry,
|
||||
type MoneroAccountEntry,
|
||||
type MoneroSendResult,
|
||||
@@ -25,7 +27,7 @@ export const atomicToXmr = (atomic: string): string => {
|
||||
return frac ? `${whole}.${frac}` : `${whole}`
|
||||
}
|
||||
|
||||
export const MoneroWallet = () => {
|
||||
export const createMoneroWallet = () => {
|
||||
let accountType = $state<'monero' | null>(null)
|
||||
let ready = $state(false)
|
||||
let biometricAvailable = $state(false)
|
||||
@@ -51,6 +53,30 @@ export const MoneroWallet = () => {
|
||||
let passwordRequired = $state(false)
|
||||
let startInFlight: Promise<{ running: boolean; connected: boolean }> | 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 autoRefreshTimer: ReturnType<typeof setInterval> | 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 = () => {
|
||||
openingDepth += 1
|
||||
@@ -80,7 +106,10 @@ export const MoneroWallet = () => {
|
||||
return authed === true
|
||||
}
|
||||
|
||||
let initPromise: Promise<void> | null = null
|
||||
const init = async () => {
|
||||
if (initPromise) return initPromise
|
||||
initPromise = (async () => {
|
||||
await checkStatus()
|
||||
if (installed && !running && !downloading) {
|
||||
await start()
|
||||
@@ -88,6 +117,8 @@ export const MoneroWallet = () => {
|
||||
}
|
||||
await checkBiometric()
|
||||
ready = true
|
||||
})()
|
||||
return initPromise
|
||||
}
|
||||
|
||||
const login = async () => {
|
||||
@@ -95,11 +126,21 @@ export const MoneroWallet = () => {
|
||||
if (loginInFlight) return loginInFlight
|
||||
loginInFlight = (async () => {
|
||||
accountType = 'monero'
|
||||
error = ''
|
||||
beginOpening()
|
||||
try {
|
||||
if (installed && !running) {
|
||||
await start()
|
||||
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) {
|
||||
error = 'Wallet server failed to start. Please try again.'
|
||||
return
|
||||
}
|
||||
await listWallets()
|
||||
if (wallets.length > 0) {
|
||||
@@ -193,14 +234,22 @@ export const MoneroWallet = () => {
|
||||
const checkStatus = async () => {
|
||||
if (!electrobun.rpc) return
|
||||
const [binStatus] = await tryCatch(
|
||||
rpcTimeout(
|
||||
electrobun.rpc.request.moneroBinaryStatus({}),
|
||||
15000,
|
||||
'moneroBinaryStatus',
|
||||
),
|
||||
)
|
||||
if (binStatus) {
|
||||
installed = binStatus.installed
|
||||
downloading = binStatus.downloading
|
||||
}
|
||||
const [status] = await tryCatch(
|
||||
rpcTimeout(
|
||||
electrobun.rpc.request.moneroWalletStatus({}),
|
||||
30000,
|
||||
'moneroWalletStatus',
|
||||
),
|
||||
)
|
||||
if (status) {
|
||||
running = status.running
|
||||
@@ -352,13 +401,60 @@ export const MoneroWallet = () => {
|
||||
if (remember) await moneroStorePassword(target, password)
|
||||
}
|
||||
|
||||
const scheduleSyncRetry = () => {
|
||||
if (syncRetryTimer) clearTimeout(syncRetryTimer)
|
||||
syncRetryTimer = setTimeout(async () => {
|
||||
syncRetryTimer = null
|
||||
if (walletOpen && !loading) await refresh()
|
||||
}, 15000)
|
||||
}
|
||||
|
||||
const stopAutoRefresh = () => {
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer)
|
||||
autoRefreshTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh runs in the store so the dashboard's connection/sync state
|
||||
// stays live regardless of which page is mounted.
|
||||
const startAutoRefresh = () => {
|
||||
if (autoRefreshTimer || !walletOpen) return
|
||||
autoRefreshTimer = setInterval(() => {
|
||||
if (walletOpen && !loading) refresh()
|
||||
}, 30000)
|
||||
}
|
||||
|
||||
// $effect needs a root outside a component; the root lives for the
|
||||
// lifetime of the singleton store.
|
||||
$effect.root(() => {
|
||||
$effect(() => {
|
||||
if (walletOpen && !loading) startAutoRefresh()
|
||||
else if (!walletOpen) stopAutoRefresh()
|
||||
})
|
||||
})
|
||||
|
||||
const refresh = async () => {
|
||||
if (!electrobun.rpc) return
|
||||
const rpc = electrobun.rpc
|
||||
if (!rpc) return
|
||||
if (refreshInFlight) return refreshInFlight
|
||||
refreshInFlight = (async () => {
|
||||
loading = true
|
||||
let failed = false
|
||||
const wasSyncing = syncing
|
||||
let lastError: unknown = null
|
||||
try {
|
||||
const [bal] = await tryCatch(
|
||||
electrobun.rpc.request.moneroGetBalance({}),
|
||||
const [bal, balErr] = await tryCatch(
|
||||
rpcTimeout(
|
||||
rpc.request.moneroGetBalance({}),
|
||||
20000,
|
||||
'moneroGetBalance',
|
||||
),
|
||||
)
|
||||
if (balErr) {
|
||||
failed = true
|
||||
lastError = balErr
|
||||
}
|
||||
if (bal) {
|
||||
balAtomic = bal.balance
|
||||
unlockedAtomic = bal.unlocked
|
||||
@@ -366,17 +462,64 @@ export const MoneroWallet = () => {
|
||||
height = bal.height
|
||||
daemonHeight = bal.daemonHeight
|
||||
}
|
||||
const [result] = await tryCatch(
|
||||
electrobun.rpc.request.moneroGetTransactions({
|
||||
const [result, txErr] = await tryCatch(
|
||||
rpcTimeout(
|
||||
rpc.request.moneroGetTransactions({
|
||||
accountIndex: selectedAccountIndex,
|
||||
}),
|
||||
20000,
|
||||
'moneroGetTransactions',
|
||||
),
|
||||
)
|
||||
if (txErr) {
|
||||
failed = true
|
||||
lastError = txErr
|
||||
}
|
||||
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) {
|
||||
error = e instanceof Error ? e.message : 'Monero refresh failed'
|
||||
syncing = true
|
||||
scheduleSyncRetry()
|
||||
frontendLogError(
|
||||
`monero refresh failed (height ${height}/${daemonHeight})`,
|
||||
e,
|
||||
)
|
||||
} finally {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,6 +639,12 @@ export const MoneroWallet = () => {
|
||||
get passwordRequired() {
|
||||
return passwordRequired
|
||||
},
|
||||
get syncing() {
|
||||
return syncing
|
||||
},
|
||||
get price() {
|
||||
return price
|
||||
},
|
||||
get error() {
|
||||
return error
|
||||
},
|
||||
@@ -545,6 +694,7 @@ export const MoneroWallet = () => {
|
||||
autoUnlock,
|
||||
unlockWallet,
|
||||
refresh,
|
||||
fetchPrice,
|
||||
send,
|
||||
sendAll,
|
||||
createAccount,
|
||||
@@ -552,4 +702,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()
|
||||
|
||||
@@ -13,7 +13,7 @@ export const xrpNetwork = {
|
||||
explorerAddressUrl: 'https://xrpscan.com/account/',
|
||||
} as const
|
||||
|
||||
export const XrpWallet = () => {
|
||||
export const createXrpWallet = () => {
|
||||
let accountType = $state<'xrp' | null>(null)
|
||||
let ready = $state(false)
|
||||
let biometricAvailable = $state(false)
|
||||
@@ -29,6 +29,9 @@ export const XrpWallet = () => {
|
||||
let loading = $state(false)
|
||||
let error = $state('')
|
||||
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 currentPasswordHash = $state<string | null>(null)
|
||||
|
||||
@@ -73,7 +76,10 @@ export const XrpWallet = () => {
|
||||
biometricAvailable = ok === true
|
||||
}
|
||||
|
||||
let initPromise: Promise<void> | null = null
|
||||
const init = async () => {
|
||||
if (initPromise) return initPromise
|
||||
initPromise = (async () => {
|
||||
if (!electrobun.rpc) return
|
||||
const [walletList] = await tryCatch(
|
||||
electrobun.rpc.request.xrpListWallets({}),
|
||||
@@ -86,6 +92,8 @@ export const XrpWallet = () => {
|
||||
}
|
||||
await checkBiometric()
|
||||
ready = true
|
||||
})()
|
||||
return initPromise
|
||||
}
|
||||
|
||||
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 (
|
||||
to: string,
|
||||
amount: string,
|
||||
@@ -399,6 +435,12 @@ export const XrpWallet = () => {
|
||||
get wallets() {
|
||||
return wallets
|
||||
},
|
||||
get balances() {
|
||||
return balances
|
||||
},
|
||||
get balancesLoading() {
|
||||
return balancesLoading
|
||||
},
|
||||
get currentWalletId() {
|
||||
return currentWalletId
|
||||
},
|
||||
@@ -424,6 +466,7 @@ export const XrpWallet = () => {
|
||||
login,
|
||||
logout,
|
||||
refresh,
|
||||
fetchBalances,
|
||||
createWallet,
|
||||
importWallet,
|
||||
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()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { moneroWallet } from '$lib/states/monero-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 { onMount } from 'svelte'
|
||||
import {
|
||||
@@ -17,28 +16,19 @@
|
||||
import Loader from '$lib/components/loader.svelte'
|
||||
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 feError = $state<string | null>(null)
|
||||
|
||||
const usd = (amount: string, price: string | null) =>
|
||||
price ? parseFloat(amount) * parseFloat(price) : 0
|
||||
|
||||
const xrpTotalUsd = $derived.by(() => {
|
||||
const price = xrpPrice
|
||||
const p = xrpWallet.price
|
||||
return xrpWallet.wallets.reduce((sum, w) => {
|
||||
const b = xrpBalances[w.address]?.balance ?? 0
|
||||
return sum + (price ? b * parseFloat(price) : 0)
|
||||
const b = xrpWallet.balances[w.address]?.balance ?? 0
|
||||
return sum + (p ? b * parseFloat(p) : 0)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
const xrpTotalBalance = $derived.by(() =>
|
||||
xrpWallet.wallets.reduce((sum, w) => {
|
||||
return sum + (xrpBalances[w.address]?.balance ?? 0)
|
||||
return sum + (xrpWallet.balances[w.address]?.balance ?? 0)
|
||||
}, 0),
|
||||
)
|
||||
|
||||
@@ -46,8 +36,8 @@
|
||||
moneroWallet.walletOpen ? parseFloat(moneroWallet.balance || '0') : 0,
|
||||
)
|
||||
const xmrTotalUsd = $derived(
|
||||
moneroWallet.walletOpen && xmrPrice
|
||||
? xmrBalance * parseFloat(xmrPrice)
|
||||
moneroWallet.walletOpen && moneroWallet.price
|
||||
? xmrBalance * parseFloat(moneroWallet.price)
|
||||
: 0,
|
||||
)
|
||||
|
||||
@@ -57,6 +47,16 @@
|
||||
const hasXmr = $derived(moneroWallet.wallets.length > 0)
|
||||
const noWallets = $derived(!hasXrp && !hasXmr)
|
||||
|
||||
const moneroDot = $derived(
|
||||
!moneroWallet.running
|
||||
? 'bg-muted'
|
||||
: !moneroWallet.connected
|
||||
? 'bg-red-500'
|
||||
: moneroWallet.syncing || moneroWallet.height < moneroWallet.daemonHeight
|
||||
? 'bg-yellow-500 animate-pulse'
|
||||
: 'bg-emerald-500',
|
||||
)
|
||||
|
||||
const fmtUsd = (v: number) =>
|
||||
v.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
@@ -69,57 +69,21 @@
|
||||
maximumFractionDigits: dp,
|
||||
})
|
||||
|
||||
async function fetchXrp() {
|
||||
loadingXrp = true
|
||||
const rpc = electrobun.rpc
|
||||
if (!rpc) {
|
||||
loadingXrp = 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
|
||||
}
|
||||
|
||||
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(() => {
|
||||
frontendLog('dashboard mounted')
|
||||
moneroWallet.init().catch((e) => frontendLogError('moneroWallet.init() failed', e))
|
||||
xrpWallet.init().then(async () => {
|
||||
await fetchXrp()
|
||||
})
|
||||
fetchXmr()
|
||||
|
||||
// Balances live in the stores so they survive navigation; the effects
|
||||
// only trigger background refetches.
|
||||
$effect(() => {
|
||||
if (xrpWallet.ready && xrpWallet.wallets.length > 0) {
|
||||
xrpWallet.fetchBalances()
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (moneroWallet.walletOpen) {
|
||||
fetchXmr()
|
||||
moneroWallet.fetchPrice()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -181,7 +145,7 @@
|
||||
{:else}
|
||||
<div class="flex flex-col items-center pt-4 pb-2">
|
||||
<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>
|
||||
{:else}
|
||||
<p class="mt-1 text-3xl font-semibold tabular-nums">
|
||||
@@ -200,21 +164,19 @@
|
||||
<img src="/icons/xrp.png" alt="" class="size-5 rounded-full" />
|
||||
<CardTitle class="text-base">XRP</CardTitle>
|
||||
</div>
|
||||
<CardDescription class="text-xs">
|
||||
{xrpWallet.wallets.length} wallet{xrpWallet.wallets.length !== 1 ? 's' : ''}
|
||||
</CardDescription>
|
||||
<CardDescription class="text-xs">XRP Ledger</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-1 flex-col justify-center">
|
||||
{#if loadingXrp}
|
||||
{#if !xrpWallet.ready || (xrpWallet.wallets.length > 0 && Object.keys(xrpWallet.balances).length === 0)}
|
||||
<div class="h-14 flex items-center"><Loader /></div>
|
||||
{:else}
|
||||
<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">
|
||||
${fmtUsd(xrpTotalUsd)}
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-muted-foreground">
|
||||
${fmtUsd(parseFloat(xrpPrice) )} / XRP
|
||||
${fmtUsd(parseFloat(xrpWallet.price) )} / XRP
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -231,23 +193,28 @@
|
||||
<img src="/icons/monero.png" alt="" class="size-5 rounded-full" />
|
||||
<CardTitle class="text-base">Monero</CardTitle>
|
||||
</div>
|
||||
<CardDescription class="text-xs">
|
||||
{moneroWallet.walletOpen
|
||||
? moneroWallet.walletName || 'Wallet open'
|
||||
: moneroWallet.wallets.length > 0
|
||||
? `${moneroWallet.wallets.length} wallet${moneroWallet.wallets.length !== 1 ? 's' : ''}`
|
||||
: 'Not set up'}
|
||||
<CardDescription class="flex items-center gap-1.5 text-xs">
|
||||
<span class="size-2 shrink-0 rounded-full {moneroDot}"></span>
|
||||
{#if !moneroWallet.running}
|
||||
Server offline
|
||||
{:else if !moneroWallet.connected}
|
||||
Connecting to daemon…
|
||||
{:else}
|
||||
{fmtBal(moneroWallet.height, 0)} / {fmtBal(moneroWallet.daemonHeight, 0)}
|
||||
{/if}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-1 flex-col justify-center">
|
||||
{#if moneroWallet.walletOpen}
|
||||
<p class="font-mono text-xl tabular-nums">{fmtBal(xmrBalance, 4)}</p>
|
||||
{#if xmrPrice}
|
||||
{#if !moneroWallet.syncing && moneroWallet.price}
|
||||
<p class="mt-0.5 text-sm text-muted-foreground tabular-nums">
|
||||
${fmtUsd(xmrTotalUsd)}
|
||||
</p>
|
||||
{/if}
|
||||
{#if moneroWallet.price}
|
||||
<p class="mt-2 text-xs text-muted-foreground">
|
||||
${fmtUsd(parseFloat(xmrPrice) )} / XMR
|
||||
${fmtUsd(parseFloat(moneroWallet.price) )} / XMR
|
||||
</p>
|
||||
{/if}
|
||||
{:else if moneroWallet.passwordRequired && moneroWallet.wallets.length > 0}
|
||||
@@ -275,8 +242,6 @@
|
||||
<Loader />
|
||||
<p class="text-sm text-muted-foreground">Opening wallet...</p>
|
||||
</div>
|
||||
{:else if loadingXmr}
|
||||
<div class="h-14 flex items-center"><Loader /></div>
|
||||
{:else}
|
||||
<Button
|
||||
onclick={() => navigate('/monero')}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
if (!xrpWallet.ready) await xrpWallet.init()
|
||||
if (!moneroWallet.ready) await moneroWallet.init()
|
||||
await Promise.allSettled([
|
||||
xrpWallet.login(),
|
||||
xrpWallet.autoUnlock(),
|
||||
moneroWallet.login(),
|
||||
])
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
let unlocking = $state(false)
|
||||
let unlockError = $state('')
|
||||
|
||||
let initStarted = false
|
||||
|
||||
async function unlockWithPassword() {
|
||||
if (unlocking || !walletPassword.trim()) return
|
||||
unlocking = true
|
||||
@@ -48,16 +46,6 @@
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (!initStarted) {
|
||||
initStarted = true
|
||||
w.init().then(async () => {
|
||||
if (w.accountType === 'monero') return
|
||||
await w.login()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (w.running && !w.walletOpen && w.wallets.length > 0 && !w.loading && !w.opening) {
|
||||
w.autoUnlock(w.wallets[0])
|
||||
|
||||
@@ -15,10 +15,6 @@
|
||||
const w = moneroWallet
|
||||
|
||||
let moneroSettingsPw = $state('')
|
||||
|
||||
$effect(() => {
|
||||
w.init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-xl">
|
||||
|
||||
@@ -154,7 +154,6 @@
|
||||
$effect(() => {
|
||||
if (w.walletOpen && !autoRefreshTimer) {
|
||||
autoRefreshTimer = setInterval(() => {
|
||||
w.refresh()
|
||||
electrobun.rpc?.request
|
||||
.fetchMoneroPrice({})
|
||||
.then((p) => (moneroPrice = p))
|
||||
@@ -203,7 +202,7 @@
|
||||
{:else}
|
||||
<div class="space-y-6">
|
||||
<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>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground">Balance</p>
|
||||
@@ -223,6 +222,14 @@
|
||||
Unlocked: {w.unlocked} XMR
|
||||
</p>
|
||||
{/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="size-2 rounded-full {w.connected ? 'bg-green-500' : 'bg-red-500'}"></div>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
|
||||
@@ -22,18 +22,6 @@
|
||||
let inputUnlockPassword = $state('')
|
||||
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(() => {
|
||||
if (w.accountType === 'xrp' && w.currentWalletId && w.seed && !newSeed) {
|
||||
navigate('/xrp/wallet/:id', {
|
||||
|
||||
Reference in New Issue
Block a user