feat(substrate): transfers prepared for review, priced, signed by the session, submitted and watched
ci / gate (push) Waiting to run
ci / gate (push) Waiting to run
`SubstrateAdapter` implements the chain-adapter port for Quantus. `prepare` builds the call from a chain-neutral intent, decodes it back from its own bytes through the runtime metadata into a summary a person can check (a 32-byte account renders as the chain's SS58 address, an amount in the token's units), prices it by assembling a full extrinsic with a zeroed signature of the right size and asking `TransactionPaymentApi_query_info`, and returns the signer payload for the session to sign. `submit` refuses if the runtime spec moved or the payload no longer matches, assembles with the session's signature, and streams statuses; a dispatch error is reported at inclusion, named `Pallet::Variant` where the chain names it, so nobody waits for finality to learn a transfer failed. Reversible transfers: `reversible_for` routes through `ReversibleTransfers::schedule_transfer_with_delay`; a high-security account is routed through the pallet's default-delay call whether it asked or not, since the chain's extension refuses a plain transfer from one. `pending_reversible` lists what is waiting with the execution block from the scheduler's lookup, whose task name is the tx id itself, and `CancelReversible` cancels. Three things the dev node taught, each now written into the code: - Mortality must be anchored at the *best* block. Finality on this proof-of-work chain trails the tip by a hundred blocks, and an era born at the finalized head names a birth block whose hash the runtime resolves differently: "Transaction has a bad signature", for both schemes, while immortal transactions from the same keys passed. Probed all four scheme-by-era combinations to isolate it. - Everything the UI shows is read at the best block, with the block number and the finalized height alongside so confirmations can be shown. subxt's `at_latest()` is the finalized block; the pending list read there showed the state of a hundred blocks ago while the chain's events showed every schedule landing. - subxt's runtime-update subscription died on this node with "header not found" after a reorg and took the connection with it. It is advisory now: on error the runtime version is polled every ten heads and a change forces a reconnect, which refetches metadata. Live, against `quantus-node --dev` with the funded dev accounts: a 3 DEV transfer from crystal_alice to crystal_bob is included and moves bob's balance; a reversible transfer with a 200-block window is scheduled, shows as pending with its execution block, and is cancelled; an overspend fails as `Token(FundsUnavailable)`. Fee for a plain transfer: 0.010157927 DEV. In the app: `transfer_prepare` (amount parsed from its decimal form in Rust), `transfer_submit` (signs through the session, emits `tx:status`), `transfer_discard`, `reversible_pending`, `reversible_cancel_prepare`. Closes #22 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW
This commit is contained in:
@@ -16,6 +16,11 @@ spendable: Amount,
|
||||
*/
|
||||
total: Amount, nonce: bigint, high_security: HighSecurityInfo | null,
|
||||
/**
|
||||
* The finalized block the numbers were read at.
|
||||
* The best block the numbers were read at.
|
||||
*/
|
||||
at_block: bigint | null, };
|
||||
at_block: bigint,
|
||||
/**
|
||||
* The chain's finalized height at the time, so the UI can say how
|
||||
* many confirmations deep `at_block` is.
|
||||
*/
|
||||
finalized_block: bigint | null, };
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type CallField = { name: string, value: string, };
|
||||
@@ -0,0 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CallField } from "./CallField";
|
||||
|
||||
/**
|
||||
* A call decoded from its bytes for the review screen: the pallet, the
|
||||
* call, and each argument rendered for a person (addresses as SS58,
|
||||
* amounts in the asset's units, everything else as the chain names it).
|
||||
*/
|
||||
export type CallSummary = { pallet: string, call: string, fields: Array<CallField>, };
|
||||
@@ -9,9 +9,9 @@ export type ChainStatus = { chain: ChainId, connected: boolean,
|
||||
/**
|
||||
* The endpoint currently in use, or the last one tried.
|
||||
*/
|
||||
endpoint: string | null, spec_version: number | null, transaction_version: number | null, finalized_number: bigint | null,
|
||||
endpoint: string | null, spec_version: number | null, transaction_version: number | null, best_number: bigint | null, finalized_number: bigint | null,
|
||||
/**
|
||||
* Seconds since the last finalized head arrived; `None` when never.
|
||||
* Seconds since the last best head arrived; `None` when never.
|
||||
*/
|
||||
head_age_seconds: bigint | null,
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Amount } from "./Amount";
|
||||
|
||||
/**
|
||||
* A reversible transfer still waiting to execute.
|
||||
*/
|
||||
export type PendingReversible = { tx_id: string, from: string, to: string, guardian: string, amount: Amount,
|
||||
/**
|
||||
* When it executes, as the scheduler records it: a block number, or a
|
||||
* timestamp in milliseconds, whichever the delay was given in.
|
||||
*/
|
||||
execute_at_block: bigint | null, execute_at_ms: bigint | null,
|
||||
/**
|
||||
* Whether the open wallet's account may cancel it (sender for a
|
||||
* one-time delay, guardian for a high-security account).
|
||||
*/
|
||||
cancellable_by_me: boolean, };
|
||||
@@ -0,0 +1,14 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Amount } from "./Amount";
|
||||
import type { CallSummary } from "./CallSummary";
|
||||
import type { ChainId } from "./ChainId";
|
||||
|
||||
/**
|
||||
* A transaction prepared for review: what will be signed, decoded from
|
||||
* the bytes, and what it costs. `id` is handed back to submit it.
|
||||
*/
|
||||
export type PreparedTransferInfo = { id: string, chain: ChainId, from: string, summary: CallSummary, fee: Amount, nonce: bigint,
|
||||
/**
|
||||
* Blocks the transaction stays valid for from the block it was built at.
|
||||
*/
|
||||
valid_for_blocks: bigint, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* One step in a submitted transaction's life, as emitted on `tx:status`.
|
||||
*/
|
||||
export type TxStage = { "stage": "submitted" } | { "stage": "in_block", block: string, } | { "stage": "finalized", block: string, } | { "stage": "failed", block: string | null, reason: string, } | { "stage": "dropped", reason: string, };
|
||||
@@ -0,0 +1,8 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { TxStage } from "./TxStage";
|
||||
|
||||
export type TxStatusInfo = {
|
||||
/**
|
||||
* The prepared transaction's id.
|
||||
*/
|
||||
id: string, hash: string, stage: TxStage, };
|
||||
@@ -9,6 +9,8 @@ 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 { SignatureScheme } from './generated/SignatureScheme'
|
||||
@@ -128,3 +130,42 @@ export function balances(chain: ChainId): Promise<AccountBalance[]> {
|
||||
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 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user