Some checks failed
ci / gate (push) Has been cancelled
Restoring a phrase now scans for the account numbers it already uses, instead of opening account 1 and leaving the rest to be guessed at. Each number is derived under both schemes, the chain is asked in one request which of those addresses exist (a System::Account entry is there for an account that has ever been funded), and the scan carries on while a batch held any, stopping after a gap of 20 empty numbers, as the mobile wallet's import does. Numbers found are opened and remembered like a hand-added one, so they come back on the next unlock. The same scan is a "Find accounts" button on every open wallet with a phrase, and it says what it found. A wallet with no phrase, or a chain that is not connected, offers no scan; a failure during restore is swallowed, since the wallet is open either way and the button retries. On the dev node: 7 DEV sent from the CLI to account 3 of the dev phrase, then that phrase restored here. Without pressing anything the wallet opened accounts 1, 2 and 3 under both schemes, account 3 showing the 7 DEV, after looking at 40 numbers; settings recorded 1 and 2. On a freshly created phrase, Find accounts looked at 20 numbers, added nothing, and said so. Closes #69 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW
161 lines
5.0 KiB
TypeScript
161 lines
5.0 KiB
TypeScript
// React Query wrappers around the command client. Server state (what Rust
|
|
// knows) lives in the query cache; component state stays local.
|
|
|
|
import { useEffect } from 'react'
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import type { ChainId } from './generated/ChainId'
|
|
import type { Settings } from './generated/Settings'
|
|
import { onBalancesUpdated, onSessionLocked, subscribe } from './events'
|
|
import {
|
|
appInfo,
|
|
balances,
|
|
balancesWatch,
|
|
chainConnect,
|
|
chainStatus,
|
|
chainsList,
|
|
lock,
|
|
reversiblePending,
|
|
sessionStatus,
|
|
settingsGet,
|
|
settingsSet,
|
|
unlock,
|
|
walletAddAccount,
|
|
walletDiscoverAccounts,
|
|
walletsList,
|
|
} from './wallet'
|
|
|
|
export const queryKeys = {
|
|
appInfo: ['app-info'] as const,
|
|
settings: ['settings'] as const,
|
|
chains: ['chains'] as const,
|
|
session: ['session'] as const,
|
|
wallets: ['wallets'] as const,
|
|
chainStatus: (chain: ChainId) => ['chain-status', chain] as const,
|
|
balances: (chain: ChainId) => ['balances', chain] as const,
|
|
pendingReversible: (chain: ChainId) => ['pending-reversible', chain] as const,
|
|
}
|
|
|
|
export function useAppInfo() {
|
|
return useQuery({ queryKey: queryKeys.appInfo, queryFn: appInfo, staleTime: Infinity })
|
|
}
|
|
|
|
export function useSettings() {
|
|
return useQuery({ queryKey: queryKeys.settings, queryFn: settingsGet, staleTime: Infinity })
|
|
}
|
|
|
|
export function useSetSettings() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (s: Settings) => settingsSet(s),
|
|
onSuccess: (s) => qc.setQueryData(queryKeys.settings, s),
|
|
})
|
|
}
|
|
|
|
export function useChains() {
|
|
return useQuery({ queryKey: queryKeys.chains, queryFn: chainsList, staleTime: Infinity })
|
|
}
|
|
|
|
export function useWallets() {
|
|
return useQuery({ queryKey: queryKeys.wallets, queryFn: walletsList })
|
|
}
|
|
|
|
export function useSession() {
|
|
return useQuery({ queryKey: queryKeys.session, queryFn: sessionStatus, refetchInterval: 15_000 })
|
|
}
|
|
|
|
export function useUnlock() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: ({ name, password }: { name: string; password: string }) => unlock(name, password),
|
|
onSuccess: (s) => qc.setQueryData(queryKeys.session, s),
|
|
})
|
|
}
|
|
|
|
/** Add the next account to an open wallet; the session and settings both change. */
|
|
export function useAddAccount() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (wallet: string) => walletAddAccount(wallet),
|
|
onSuccess: (s) => {
|
|
qc.setQueryData(queryKeys.session, s)
|
|
qc.invalidateQueries({ queryKey: queryKeys.settings })
|
|
},
|
|
})
|
|
}
|
|
|
|
/** Scan a wallet's phrase for account numbers already used on `chain`. */
|
|
export function useDiscoverAccounts() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: ({ chain, wallet }: { chain: ChainId; wallet: string }) =>
|
|
walletDiscoverAccounts(chain, wallet),
|
|
onSuccess: (scan) => {
|
|
qc.setQueryData(queryKeys.session, scan.session)
|
|
qc.invalidateQueries({ queryKey: queryKeys.settings })
|
|
},
|
|
})
|
|
}
|
|
|
|
/** Lock one wallet (pass its name) or every open wallet (pass nothing). */
|
|
export function useLock() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (wallet?: string) => lock(wallet),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.session }),
|
|
})
|
|
}
|
|
|
|
/// Connect the chain (idempotent on the Rust side) and poll its status.
|
|
export function useChainStatus(chain: ChainId | undefined) {
|
|
return useQuery({
|
|
queryKey: chain ? queryKeys.chainStatus(chain) : ['chain-status', 'none'],
|
|
queryFn: () => chainConnect(chain!).then(() => chainStatus(chain!)),
|
|
enabled: !!chain,
|
|
refetchInterval: 5_000,
|
|
})
|
|
}
|
|
|
|
/// Balances of the open wallet on `chain`, refreshed by Rust on every head.
|
|
export function useBalances(chain: ChainId | undefined, unlocked: boolean) {
|
|
const qc = useQueryClient()
|
|
useEffect(() => {
|
|
if (!chain || !unlocked) return
|
|
balancesWatch(chain).catch(() => undefined)
|
|
return subscribe(
|
|
onBalancesUpdated((b) => {
|
|
if (b.length > 0 && b[0].chain === chain) qc.setQueryData(queryKeys.balances(chain), b)
|
|
}),
|
|
)
|
|
}, [chain, unlocked, qc])
|
|
return useQuery({
|
|
queryKey: chain ? queryKeys.balances(chain) : ['balances', 'none'],
|
|
queryFn: () => balances(chain!),
|
|
enabled: !!chain && unlocked,
|
|
retry: 2,
|
|
retryDelay: 1_500,
|
|
})
|
|
}
|
|
|
|
/// Drop the session in the cache when Rust says the idle rule fired.
|
|
export function useSessionLockedListener() {
|
|
const qc = useQueryClient()
|
|
useEffect(() => {
|
|
return subscribe(onSessionLocked(() => qc.invalidateQueries({ queryKey: queryKeys.session })))
|
|
}, [qc])
|
|
}
|
|
|
|
/// Reversible transfers still pending from any of `addresses` on `chain`.
|
|
export function usePendingReversible(chain: ChainId | undefined, addresses: string[]) {
|
|
return useQuery({
|
|
queryKey: chain
|
|
? [...queryKeys.pendingReversible(chain), addresses]
|
|
: ['pending-reversible', 'none'],
|
|
queryFn: async () => {
|
|
const lists = await Promise.all(addresses.map((a) => reversiblePending(chain!, a)))
|
|
return lists.flat()
|
|
},
|
|
enabled: !!chain && addresses.length > 0,
|
|
refetchInterval: 5_000,
|
|
})
|
|
}
|