Skip to content

Commit ea2d340

Browse files
committed
feat: redesign TransactionIntent as high-level business intents
Replace low-level call intents (Transfer, Bond, Unbond, AddProxy, etc.) with business-level intents (Payment, Consolidate, Stake, Unstake, Claim, FillNonce). The crate now handles intent-to-call composition internally, including automatic batching for multi-call operations. Composition logic: - Payment → transfer or transferKeepAlive - Consolidate → transferAll - Stake with proxyAddress → batchAll(bond, addProxy) - Stake without proxyAddress → bondExtra (top-up) - Unstake with stopStaking → batchAll(removeProxy, chill, unbond) - Unstake without stopStaking → unbond - Claim → withdrawUnbonded - FillNonce → zero-value transferKeepAlive to self Old call-level types are now internal CallIntent (not public). Includes bondExtra encoding (previously missing). 47 tests pass (18 new composition + deserialization tests). Ticket: BTC-3120
1 parent 4e269eb commit ea2d340

File tree

6 files changed

+573
-274
lines changed

6 files changed

+573
-274
lines changed

packages/wasm-dot/js/builder.ts

Lines changed: 21 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,46 @@
11
/**
2-
* Transaction building from high-level intents.
2+
* Transaction building from high-level business intents.
33
*
44
* Provides the `buildTransaction()` function for building DOT transactions.
5-
* Follows wallet-platform pattern: buildTransaction(intent, context)
5+
* The crate handles intent composition internally (e.g., stake with proxy
6+
* automatically produces a batchAll of bond + addProxy).
67
*/
78

89
import { BuilderNamespace } from "./wasm/wasm_dot.js";
910
import { DotTransaction } from "./transaction.js";
1011
import type { TransactionIntent, BuildContext } from "./types.js";
1112

1213
/**
13-
* Build a DOT transaction from an intent and context.
14+
* Build a DOT transaction from a business-level intent and context.
1415
*
15-
* This function takes a declarative TransactionIntent and BuildContext,
16-
* producing a Transaction object that can be inspected, signed, and serialized.
16+
* The intent describes *what* to do (payment, stake, etc.) and the context
17+
* provides *how* to build it (sender, nonce, material, validity).
18+
* Multi-call intents are batched automatically.
1719
*
18-
* The returned transaction is unsigned - signatures should be added via
19-
* `addSignature()` before serializing with `toBytes()` and broadcasting.
20-
*
21-
* @param intent - What to do (transfer, stake, etc.)
22-
* @param context - How to build it (sender, nonce, material, validity, referenceBlock)
23-
* @returns A Transaction object that can be inspected, signed, and serialized
20+
* @param intent - Business intent (payment, stake, unstake, claim, etc.)
21+
* @param context - Build context (sender, nonce, material, validity, referenceBlock)
22+
* @returns An unsigned DotTransaction ready for signing
2423
* @throws Error if the intent cannot be built (e.g., invalid addresses)
2524
*
2625
* @example
2726
* ```typescript
2827
* import { buildTransaction } from '@bitgo/wasm-dot';
2928
*
30-
* // Build a simple DOT transfer
29+
* // Payment
3130
* const tx = buildTransaction(
32-
* { type: 'transfer', to: '5FHneW46...', amount: 1000000000000n, keepAlive: true },
33-
* {
34-
* sender: '5EGoFA95omzemRssELLDjVenNZ68aXyUeqtKQScXSEBvVJkr',
35-
* nonce: 5,
36-
* material: {
37-
* genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3',
38-
* chainName: 'Polkadot',
39-
* specName: 'polkadot',
40-
* specVersion: 9150,
41-
* txVersion: 9
42-
* },
43-
* validity: { firstValid: 1000, maxDuration: 2400 },
44-
* referenceBlock: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3'
45-
* }
31+
* { type: 'payment', to: '5FHneW46...', amount: 1000000000000n },
32+
* context
4633
* );
4734
*
48-
* // Inspect the transaction
49-
* console.log(tx.nonce);
50-
*
51-
* // Get the signable payload for signing
52-
* const payload = tx.signablePayload();
53-
*
54-
* // Add signature and serialize
55-
* tx.addSignature(signerPubkey, signature);
56-
* const txBytes = tx.toBytes();
57-
* ```
35+
* // New stake (produces batchAll of bond + addProxy)
36+
* const stakeTx = buildTransaction(
37+
* { type: 'stake', amount: 5000000000000n, proxyAddress: '5Grwva...' },
38+
* context
39+
* );
5840
*
59-
* @example
60-
* ```typescript
61-
* // Build with batch (multiple operations)
62-
* const tx = buildTransaction(
63-
* {
64-
* type: 'batch',
65-
* calls: [
66-
* { type: 'transfer', to: recipient, amount: 1000000000000n },
67-
* { type: 'stake', amount: 5000000000000n, payee: { type: 'staked' } }
68-
* ],
69-
* atomic: true
70-
* },
41+
* // Full unstake (produces batchAll of removeProxy + chill + unbond)
42+
* const unstakeTx = buildTransaction(
43+
* { type: 'unstake', amount: 5000000000000n, stopStaking: true, proxyAddress: '5Grwva...' },
7144
* context
7245
* );
7346
* ```

packages/wasm-dot/js/types.ts

Lines changed: 52 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
/**
22
* TypeScript type definitions for wasm-dot
33
*
4-
* Follows wallet-platform pattern: buildTransaction(intent, context)
5-
* - intent: what to do (transfer, stake, etc.) - single operation
4+
* buildTransaction(intent, context)
5+
* - intent: business-level intent (payment, stake, unstake, etc.)
66
* - context: how to build it (sender, nonce, material, validity)
7+
*
8+
* The crate handles intent composition internally. For example, a stake
9+
* intent with a proxy address produces a batchAll(bond, addProxy) extrinsic.
710
*/
811

912
// =============================================================================
@@ -54,9 +57,9 @@ export interface ParseContext {
5457
material: Material;
5558
/** Sender address (optional, helps with decoding) */
5659
sender?: string;
57-
/** Reference block hash (not in extrinsic bytes pass-through for consumers) */
60+
/** Reference block hash (not in extrinsic bytes, pass-through for consumers) */
5861
referenceBlock?: string;
59-
/** Block number when transaction becomes valid (not in extrinsic bytes pass-through for consumers) */
62+
/** Block number when transaction becomes valid (not in extrinsic bytes, pass-through for consumers) */
6063
blockNumber?: number;
6164
}
6265

@@ -65,9 +68,7 @@ export interface ParseContext {
6568
// =============================================================================
6669

6770
/**
68-
* Build context - contains all non-intent data needed to build a transaction
69-
*
70-
* Matches wallet-platform's material + nonce + validity pattern.
71+
* Build context: contains all non-intent data needed to build a transaction.
7172
*/
7273
export interface BuildContext {
7374
/** Sender address (SS58 encoded) */
@@ -89,46 +90,54 @@ export interface BuildContext {
8990
// =============================================================================
9091

9192
/**
92-
* Transaction intent - a single operation to perform
93+
* Business-level transaction intent.
9394
*
94-
* Discriminated union using the `type` field.
95-
* For multiple operations, use the `batch` intent type.
95+
* Discriminated union using the `type` field. The crate handles composing
96+
* these into the correct Polkadot extrinsic calls, including automatic
97+
* batching when multiple calls are needed (e.g., stake with proxy).
9698
*/
9799
export type TransactionIntent =
98-
| TransferIntent
99-
| TransferAllIntent
100+
| PaymentIntent
101+
| ConsolidateIntent
100102
| StakeIntent
101103
| UnstakeIntent
102-
| WithdrawUnbondedIntent
103-
| ChillIntent
104-
| AddProxyIntent
105-
| RemoveProxyIntent
106-
| BatchIntent;
107-
108-
export interface TransferIntent {
109-
type: "transfer";
104+
| ClaimIntent
105+
| FillNonceIntent;
106+
107+
/** Transfer DOT to a recipient */
108+
export interface PaymentIntent {
109+
type: "payment";
110110
/** Recipient address (SS58) */
111111
to: string;
112112
/** Amount in planck */
113113
amount: bigint;
114-
/** Use transferKeepAlive (default: true) */
114+
/** Use transferKeepAlive to prevent reaping (default: true) */
115115
keepAlive?: boolean;
116116
}
117117

118-
export interface TransferAllIntent {
119-
type: "transferAll";
118+
/** Sweep all DOT to a recipient (transferAll) */
119+
export interface ConsolidateIntent {
120+
type: "consolidate";
120121
/** Recipient address (SS58) */
121122
to: string;
122-
/** Keep account alive after transfer */
123+
/** Keep sender account alive after transfer (default: true) */
123124
keepAlive?: boolean;
124125
}
125126

127+
/**
128+
* Stake DOT.
129+
*
130+
* - With `proxyAddress`: new stake, produces batchAll(bond, addProxy)
131+
* - Without `proxyAddress`: top-up, produces bondExtra
132+
*/
126133
export interface StakeIntent {
127134
type: "stake";
128135
/** Amount to stake in planck */
129136
amount: bigint;
130-
/** Where to send staking rewards */
137+
/** Reward destination (default: Staked / compound) */
131138
payee?: StakePayee;
139+
/** Proxy address for new stake. Absent means top-up (bondExtra). */
140+
proxyAddress?: string;
132141
}
133142

134143
export type StakePayee =
@@ -137,48 +146,33 @@ export type StakePayee =
137146
| { type: "controller" }
138147
| { type: "account"; address: string };
139148

149+
/**
150+
* Unstake DOT.
151+
*
152+
* - `stopStaking=true` + `proxyAddress`: full unstake, produces
153+
* batchAll(removeProxy, chill, unbond)
154+
* - `stopStaking=false`: partial unstake, produces unbond
155+
*/
140156
export interface UnstakeIntent {
141157
type: "unstake";
142158
/** Amount to unstake in planck */
143159
amount: bigint;
160+
/** Full unstake (remove proxy + chill) or partial (just unbond). Default: false */
161+
stopStaking?: boolean;
162+
/** Proxy address to remove (required when stopStaking=true) */
163+
proxyAddress?: string;
144164
}
145165

146-
export interface WithdrawUnbondedIntent {
147-
type: "withdrawUnbonded";
148-
/** Number of slashing spans (usually 0) */
166+
/** Claim (withdraw unbonded) DOT after the unbonding period */
167+
export interface ClaimIntent {
168+
type: "claim";
169+
/** Number of slashing spans (default: 0) */
149170
slashingSpans?: number;
150171
}
151172

152-
export interface ChillIntent {
153-
type: "chill";
154-
}
155-
156-
export interface AddProxyIntent {
157-
type: "addProxy";
158-
/** Delegate address (SS58) */
159-
delegate: string;
160-
/** Proxy type (Any, NonTransfer, Staking, etc.) */
161-
proxyType: string;
162-
/** Delay in blocks */
163-
delay?: number;
164-
}
165-
166-
export interface RemoveProxyIntent {
167-
type: "removeProxy";
168-
/** Delegate address (SS58) */
169-
delegate: string;
170-
/** Proxy type */
171-
proxyType: string;
172-
/** Delay in blocks */
173-
delay?: number;
174-
}
175-
176-
export interface BatchIntent {
177-
type: "batch";
178-
/** List of intents to execute */
179-
calls: TransactionIntent[];
180-
/** Use batchAll (atomic) instead of batch (default: true) */
181-
atomic?: boolean;
173+
/** Zero-value self-transfer to advance the account nonce */
174+
export interface FillNonceIntent {
175+
type: "fillNonce";
182176
}
183177

184178
// =============================================================================

0 commit comments

Comments
 (0)