Files
wallet/ui/src/api/hooks.ts
rob thijssen fb42138651
Some checks failed
ci / gate (push) Has been cancelled
feat: find the accounts a restored phrase already uses
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
2026-09-18 09:47:01 +03:00

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,
})
}