ci / gate (push) Waiting to run
wallet_core::orders is the pure part: Quoted → AwaitingDeposit → Processing → Success | Refunded | Failed, with Expired when a quote's deadline passes before a deposit is seen; it never moves backwards, a quote past its deadline expires without a network call, and the poll delay doubles from ten seconds to five minutes (five seconds while processing). Orders persist through an OrderStore port on the same SQLite file; on start the app follows every order still open, and the follower asks the provider with backoff and jitter, persists each move and emits orders:updated. Commands: swap_assets (what 1Click lists and the registry maps), swap_preview (a dry quote), swap_order_create (a wet quote persisted with its deposit address and deadline) and orders_list. The swap screen sits behind a preview setting, says plainly that every pair today is paid from an external wallet, quotes, shows the deposit address with a QR, memo and deadline, and lists orders with their states. Driven live on the dev network: BTC on bitcoin to USDC on near, 0.001 BTC quoted at 75.568313 USDC, at least 74.812629, about 805 s. Reserving a deposit address failed with 1Click's "Failed to get quote": a wet quote needs the partner credential the relay will hold (#40), which is the operator's registration. Until then the screen previews and the poller has nothing to follow; the state machine and the store are covered by unit tests, including the mid-order restart the store makes possible. Refs #39 Refs #41 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW
224 lines
7.7 KiB
TypeScript
224 lines
7.7 KiB
TypeScript
// The only file in the webview that talks to Rust. One function per Tauri
|
|
// command, each typed with the ts-rs output in ./generated, and one place
|
|
// where a Rust `WalletError` becomes a thrown `WalletError`. Screens never
|
|
// call `invoke` themselves; CI counts the occurrences in this directory.
|
|
|
|
import { invoke } from '@tauri-apps/api/core'
|
|
import type { AccountBalance } from './generated/AccountBalance'
|
|
import type { AppInfo } from './generated/AppInfo'
|
|
import type { ChainId } from './generated/ChainId'
|
|
import type { ChainProfile } from './generated/ChainProfile'
|
|
import type { ChainStatus } from './generated/ChainStatus'
|
|
import type { PendingReversible } from './generated/PendingReversible'
|
|
import type { PreparedTransferInfo } from './generated/PreparedTransferInfo'
|
|
import type { CreationStart } from './generated/CreationStart'
|
|
import type { SessionStatusInfo } from './generated/SessionStatusInfo'
|
|
import type { Settings } from './generated/Settings'
|
|
import type { AssetInfo } from './generated/AssetInfo'
|
|
import type { HistoryCursor } from './generated/HistoryCursor'
|
|
import type { Order } from './generated/Order'
|
|
import type { SwapAsk } from './generated/SwapAsk'
|
|
import type { SwapPreview } from './generated/SwapPreview'
|
|
import type { HistoryPage } from './generated/HistoryPage'
|
|
import type { SignatureScheme } from './generated/SignatureScheme'
|
|
import type { WalletSummary } from './generated/WalletSummary'
|
|
import type { WormholeSummary } from './generated/WormholeSummary'
|
|
import type { WalletError as WalletErrorShape } from './generated/WalletError'
|
|
|
|
export class WalletError extends Error {
|
|
readonly kind: WalletErrorShape['kind']
|
|
readonly detail: string
|
|
|
|
constructor(shape: WalletErrorShape) {
|
|
const detail = 'detail' in shape ? shape.detail : ''
|
|
super(detail ? `${shape.kind}: ${detail}` : shape.kind)
|
|
this.name = 'WalletError'
|
|
this.kind = shape.kind
|
|
this.detail = detail
|
|
}
|
|
}
|
|
|
|
function isWalletErrorShape(e: unknown): e is WalletErrorShape {
|
|
return (
|
|
typeof e === 'object' &&
|
|
e !== null &&
|
|
'kind' in e &&
|
|
typeof (e as { kind: unknown }).kind === 'string'
|
|
)
|
|
}
|
|
|
|
// Rust commands return `Result<T, WalletError>`; Tauri rejects the promise
|
|
// with the serialised error. Anything that is not a WalletError is a bug on
|
|
// one side of the boundary and is rethrown as-is so it is loud.
|
|
async function command<T>(name: string, args?: Record<string, unknown>): Promise<T> {
|
|
try {
|
|
return await invoke<T>(name, args)
|
|
} catch (e) {
|
|
if (isWalletErrorShape(e)) throw new WalletError(e)
|
|
throw e
|
|
}
|
|
}
|
|
|
|
export function appInfo(): Promise<AppInfo> {
|
|
return command<AppInfo>('app_info')
|
|
}
|
|
|
|
export function walletsList(): Promise<WalletSummary[]> {
|
|
return command<WalletSummary[]>('wallets_list')
|
|
}
|
|
|
|
// Creation is three steps so the phrase is shown once and then gone: begin
|
|
// returns the words and the challenge, confirm writes the wallet if the
|
|
// challenge passes, cancel forgets the phrase.
|
|
export function walletCreateBegin(scheme: SignatureScheme): Promise<CreationStart> {
|
|
return command<CreationStart>('wallet_create_begin', { scheme })
|
|
}
|
|
|
|
export function walletCreateConfirm(
|
|
name: string,
|
|
password: string,
|
|
answers: string[],
|
|
): Promise<WalletSummary> {
|
|
return command<WalletSummary>('wallet_create_confirm', { name, password, answers })
|
|
}
|
|
|
|
export function walletCreateCancel(): Promise<void> {
|
|
return command<void>('wallet_create_cancel')
|
|
}
|
|
|
|
export function walletImportPhrase(
|
|
name: string,
|
|
password: string,
|
|
phrase: string,
|
|
scheme: SignatureScheme,
|
|
): Promise<WalletSummary> {
|
|
return command<WalletSummary>('wallet_import_phrase', { name, password, phrase, scheme })
|
|
}
|
|
|
|
export function walletImportFile(path: string, password: string): Promise<WalletSummary> {
|
|
return command<WalletSummary>('wallet_import_file', { path, password })
|
|
}
|
|
|
|
export function checkphrase(address: string): Promise<string> {
|
|
return command<string>('checkphrase', { address })
|
|
}
|
|
|
|
// The session: one wallet open at a time, locked again after idling. The
|
|
// Rust side emits `session:locked` when the idle rule fires.
|
|
export function unlock(name: string, password: string): Promise<SessionStatusInfo> {
|
|
return command<SessionStatusInfo>('unlock', { name, password })
|
|
}
|
|
|
|
export function lock(): Promise<void> {
|
|
return command<void>('lock')
|
|
}
|
|
|
|
export function sessionStatus(): Promise<SessionStatusInfo> {
|
|
return command<SessionStatusInfo>('session_status')
|
|
}
|
|
|
|
export function chainsList(): Promise<ChainProfile[]> {
|
|
return command<ChainProfile[]>('chains_list')
|
|
}
|
|
|
|
export function chainConnect(chain: ChainId): Promise<ChainStatus> {
|
|
return command<ChainStatus>('chain_connect', { chain })
|
|
}
|
|
|
|
export function chainStatus(chain: ChainId): Promise<ChainStatus> {
|
|
return command<ChainStatus>('chain_status', { chain })
|
|
}
|
|
|
|
// Balances are read from finalized state. `balancesWatch` makes Rust re-read
|
|
// them on every finalized head and emit `balances:updated`.
|
|
export function balances(chain: ChainId): Promise<AccountBalance[]> {
|
|
return command<AccountBalance[]>('balances', { chain })
|
|
}
|
|
|
|
export function balancesWatch(chain: ChainId): Promise<void> {
|
|
return command<void>('balances_watch', { chain })
|
|
}
|
|
|
|
// Sending is two steps so the review screen shows what Rust decoded from the
|
|
// bytes: prepare returns the summary and fee under an id, submit signs and
|
|
// sends that id. Progress arrives as `tx:status` events.
|
|
export function transferPrepare(
|
|
chain: ChainId,
|
|
from: string,
|
|
to: string,
|
|
amount: string,
|
|
reversibleFor?: number,
|
|
): Promise<PreparedTransferInfo> {
|
|
return command<PreparedTransferInfo>('transfer_prepare', {
|
|
chain,
|
|
from,
|
|
to,
|
|
amount,
|
|
reversibleFor: reversibleFor ?? null,
|
|
})
|
|
}
|
|
|
|
export function transferSubmit(id: string): Promise<string> {
|
|
return command<string>('transfer_submit', { id })
|
|
}
|
|
|
|
export function transferDiscard(id: string): Promise<void> {
|
|
return command<void>('transfer_discard', { id })
|
|
}
|
|
|
|
export function reversiblePending(chain: ChainId, address: string): Promise<PendingReversible[]> {
|
|
return command<PendingReversible[]>('reversible_pending', { chain, address })
|
|
}
|
|
|
|
export function reversibleCancelPrepare(
|
|
chain: ChainId,
|
|
from: string,
|
|
txId: string,
|
|
): Promise<PreparedTransferInfo> {
|
|
return command<PreparedTransferInfo>('reversible_cancel_prepare', { chain, from, txId })
|
|
}
|
|
|
|
export function settingsGet(): Promise<Settings> {
|
|
return command<Settings>('settings_get')
|
|
}
|
|
|
|
export function settingsSet(settings: Settings): Promise<Settings> {
|
|
return command<Settings>('settings_set', { settings })
|
|
}
|
|
|
|
// History: one page per call, from the indexer or, when it cannot be
|
|
// reached, from the local cache with `from_cache` set.
|
|
export function historyPage(
|
|
chain: ChainId,
|
|
address: string,
|
|
before?: HistoryCursor | null,
|
|
): Promise<HistoryPage> {
|
|
return command<HistoryPage>('history_page', { chain, address, before: before ?? null })
|
|
}
|
|
|
|
// The wormhole address behind the wallet's mining rewards and what the
|
|
// zk-tree holds for it. Scans on the Rust side; can take a while the first
|
|
// time on a long tree.
|
|
export function wormholeSummary(chain: ChainId): Promise<WormholeSummary> {
|
|
return command<WormholeSummary>('wormhole_summary', { chain })
|
|
}
|
|
|
|
// Swaps, in preview: the assets the provider and the registry agree on, a
|
|
// dry quote, an order (a wet quote persisted with its deposit address),
|
|
// and the list. Rust follows open orders and emits `orders:updated`.
|
|
export function swapAssets(): Promise<AssetInfo[]> {
|
|
return command<AssetInfo[]>('swap_assets')
|
|
}
|
|
|
|
export function swapPreview(ask: SwapAsk): Promise<SwapPreview> {
|
|
return command<SwapPreview>('swap_preview', { ask })
|
|
}
|
|
|
|
export function swapOrderCreate(ask: SwapAsk): Promise<Order> {
|
|
return command<Order>('swap_order_create', { ask })
|
|
}
|
|
|
|
export function ordersList(): Promise<Order[]> {
|
|
return command<Order[]>('orders_list')
|
|
}
|