Skip to content

Commit cf99374

Browse files
committed
feat: add WASM-based DOT transaction parsing and explanation
Add @bitgo/wasm-dot integration for parsing, explaining, and building DOT transactions using Rust/WASM instead of @polkadot/api. - wasmParser.ts: adapter that maps wasm-dot's explainTransaction() output to BitGoJS TransactionExplanation format (type mapping, output/input extraction, fee handling) - wasmBuilderByteComparison.ts: tests verifying WASM-built transactions produce byte-identical signing payloads to legacy txwrapper-polkadot - Covers: transfers, staking (bond/unbond/withdraw/chill), proxy (add/remove), and batch transactions BTC-0 TICKET: BTC-0
1 parent 79aad00 commit cf99374

9 files changed

Lines changed: 4828 additions & 2175 deletions

File tree

428 KB
Binary file not shown.

modules/sdk-coin-dot/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@bitgo/sdk-core": "^36.30.0",
4444
"@bitgo/sdk-lib-mpc": "^10.9.0",
4545
"@bitgo/statics": "^58.24.0",
46+
"@bitgo/wasm-dot": "file:bitgo-wasm-dot-0.0.1.tgz",
4647
"@polkadot/api": "14.1.1",
4748
"@polkadot/api-augment": "14.1.1",
4849
"@polkadot/keyring": "13.3.1",

modules/sdk-coin-dot/src/dot.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Environments,
88
ExplanationResult,
99
KeyPair,
10+
TransactionType,
1011
MPCAlgorithm,
1112
ParsedTransaction,
1213
ParseTransactionOptions,
@@ -39,6 +40,8 @@ import {
3940
Transaction,
4041
TransactionBuilderFactory,
4142
Utils,
43+
explainDotTransaction,
44+
type DotWasmExplanation,
4245
} from './lib';
4346
import '@polkadot/api-augment';
4447
import { ApiPromise, WsProvider } from '@polkadot/api';
@@ -58,14 +61,6 @@ export interface TransactionPrebuild {
5861
transaction: Interface.TxData;
5962
}
6063

61-
export interface ExplainTransactionOptions {
62-
txPrebuild: TransactionPrebuild;
63-
publicKey: string;
64-
feeInfo: {
65-
fee: string;
66-
};
67-
}
68-
6964
export interface VerifiedTransactionParameters {
7065
txHex: string;
7166
prv: string;
@@ -209,6 +204,25 @@ export class Dot extends BaseCoin {
209204
* @param unsignedTransaction
210205
*/
211206
async explainTransaction(unsignedTransaction: UnsignedTransaction): Promise<ExplanationResult> {
207+
// Testnet uses WASM-based parsing (no @polkadot/api dependency).
208+
if (this.getChain() === 'tdot') {
209+
const material = dotUtils.getMaterial(coins.get(this.getChain()));
210+
const wasmExplain = explainDotTransaction({
211+
txHex: unsignedTransaction.serializedTxHex,
212+
material,
213+
});
214+
return {
215+
...wasmExplain,
216+
type: TransactionType[wasmExplain.type],
217+
outputs: wasmExplain.outputs.map((o) => ({
218+
...o,
219+
valueString: String(o.amount),
220+
})),
221+
sequenceId: wasmExplain.nonce,
222+
blockNumber: unsignedTransaction.coinSpecific?.blockNumber,
223+
};
224+
}
225+
212226
let outputAmount = 0;
213227
unsignedTransaction.parsedTx.outputs.forEach((o) => {
214228
outputAmount += parseInt(o.valueString, 10);
@@ -239,6 +253,15 @@ export class Dot extends BaseCoin {
239253
return explanationResult;
240254
}
241255

256+
/**
257+
* Explain a DOT transaction from hex using WASM parsing.
258+
* Bypasses txwrapper-polkadot rebuild — delegates to explainDotTransaction().
259+
*/
260+
explainTransactionWithWasm(txHex: string, senderAddress?: string): DotWasmExplanation {
261+
const material = dotUtils.getMaterial(coins.get(this.getChain()));
262+
return explainDotTransaction({ txHex, material, senderAddress });
263+
}
264+
242265
verifySignTransactionParams(params: SignTransactionOptions): VerifiedTransactionParameters {
243266
const prv = params.prv;
244267

modules/sdk-coin-dot/src/lib/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ export { TransactionBuilderFactory } from './transactionBuilderFactory';
1616
export { SingletonRegistry } from './singletonRegistry';
1717
export { NativeTransferBuilder } from './nativeTransferBuilder';
1818
export { RemoveProxyBuilder } from './proxyBuilder';
19+
export { explainDotTransaction } from './wasmParser';
20+
export type { ExplainDotTransactionParams, DotWasmExplanation, DotInput } from './wasmParser';
1921
export { Interface, Utils };

modules/sdk-coin-dot/src/lib/transaction.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
} from './iface';
3939
import { getAddress, getDelegateAddress } from './iface_utils';
4040
import utils from './utils';
41+
import { explainDotTransaction } from './wasmParser';
4142
import BigNumber from 'bignumber.js';
4243
import { Vec } from '@polkadot/types';
4344
import { PalletConstantMetadataV14 } from '@polkadot/types/interfaces';
@@ -338,6 +339,17 @@ export class Transaction extends BaseTransaction {
338339

339340
/** @inheritdoc */
340341
explainTransaction(): TransactionExplanation {
342+
// Testnet uses WASM-based parsing (no @polkadot/api dependency).
343+
// This validates the WASM path against production traffic before
344+
// replacing the legacy implementation for all networks.
345+
if (this._coinConfig.name === 'tdot' && this._dotTransaction) {
346+
return explainDotTransaction({
347+
txHex: this.toBroadcastFormat(),
348+
material: utils.getMaterial(this._coinConfig),
349+
senderAddress: this._sender,
350+
});
351+
}
352+
341353
const result = this.toJson();
342354
const displayOrder = ['outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee', 'type'];
343355
const outputs: TransactionRecipient[] = [];
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { TransactionType } from '@bitgo/sdk-core';
2+
import {
3+
explainTransaction,
4+
TransactionType as WasmTransactionType,
5+
type ExplainedTransaction as WasmExplainedTransaction,
6+
} from '@bitgo/wasm-dot';
7+
import type { TransactionExplanation, Material } from './iface';
8+
9+
/**
10+
* Input entry for a DOT transaction.
11+
* For account-model chains, there's typically one input (the sender).
12+
*/
13+
export interface DotInput {
14+
address: string;
15+
value: number;
16+
valueString: string;
17+
}
18+
19+
/**
20+
* Extended explanation returned by WASM-based parsing.
21+
* Includes fields needed by wallet-platform that aren't in the base TransactionExplanation.
22+
*/
23+
export interface DotWasmExplanation extends TransactionExplanation {
24+
sender: string;
25+
nonce: number;
26+
isSigned: boolean;
27+
methodName: string;
28+
inputs: DotInput[];
29+
}
30+
31+
/** Map WASM TransactionType (string enum) to sdk-core TransactionType (numeric enum) via name lookup */
32+
function mapTransactionType(wasmType: WasmTransactionType): TransactionType {
33+
return TransactionType[wasmType as keyof typeof TransactionType] ?? TransactionType.Send;
34+
}
35+
36+
/**
37+
* Options for the WASM-based DOT transaction explanation adapter.
38+
*/
39+
export interface ExplainDotTransactionParams {
40+
txHex: string;
41+
material: Material;
42+
senderAddress?: string;
43+
}
44+
45+
/**
46+
* Explain a DOT transaction using the WASM parser.
47+
*
48+
* Thin adapter over @bitgo/wasm-dot's explainTransaction that maps
49+
* WASM types to BitGoJS TransactionExplanation format.
50+
* Analogous to explainSolTransaction for Solana.
51+
*/
52+
export function explainDotTransaction(params: ExplainDotTransactionParams): DotWasmExplanation {
53+
const explained: WasmExplainedTransaction = explainTransaction(params.txHex, {
54+
context: { material: params.material, sender: params.senderAddress },
55+
});
56+
57+
const sender = explained.sender || params.senderAddress || '';
58+
const type = mapTransactionType(explained.type);
59+
const methodName = `${explained.method.pallet}.${explained.method.name}`;
60+
61+
// Map WASM outputs to BitGoJS format
62+
const outputs = explained.outputs.map((o) => ({
63+
address: o.address,
64+
amount: o.amount === 'ALL' ? '0' : o.amount,
65+
}));
66+
67+
// Map WASM inputs to BitGoJS format (with numeric value for legacy compat)
68+
const inputs: DotInput[] = explained.inputs.map((i) => {
69+
const value = i.value === 'ALL' ? 0 : parseInt(i.value || '0', 10);
70+
return { address: i.address, value, valueString: i.value };
71+
});
72+
73+
return {
74+
displayOrder: ['outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee', 'type', 'sequenceId', 'id'],
75+
id: explained.id || '',
76+
outputs,
77+
outputAmount: explained.outputAmount,
78+
changeOutputs: [],
79+
changeAmount: '0',
80+
fee: { fee: explained.tip || '0', type: 'tip' },
81+
type,
82+
sender,
83+
nonce: explained.nonce,
84+
isSigned: explained.isSigned,
85+
methodName,
86+
inputs,
87+
};
88+
}

modules/sdk-coin-dot/test/unit/dot.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import { Dot, Tdot, KeyPair } from '../../src';
77
import * as testData from '../fixtures';
88
import { chainName, txVersion, genesisHash, specVersion } from '../resources';
99
import * as sinon from 'sinon';
10-
import { Wallet } from '@bitgo/sdk-core';
10+
import { TransactionType, Wallet } from '@bitgo/sdk-core';
11+
import { coins } from '@bitgo/statics';
12+
import { buildTransaction, type BuildContext, type Material } from '@bitgo/wasm-dot';
13+
import utils from '../../src/lib/utils';
1114

1215
describe('DOT:', function () {
1316
let bitgo: TestBitGoAPI;
@@ -152,7 +155,7 @@ describe('DOT:', function () {
152155

153156
describe('Explain Transactions:', () => {
154157
it('should explain an unsigned transfer transaction', async function () {
155-
const explainedTransaction = await basecoin.explainTransaction(testData.unsignedTransaction);
158+
const explainedTransaction = await prodCoin.explainTransaction(testData.unsignedTransaction);
156159
explainedTransaction.should.deepEqual({
157160
displayOrder: [
158161
'outputAmount',
@@ -185,6 +188,67 @@ describe('DOT:', function () {
185188
});
186189
});
187190

191+
describe('Explain Transactions (WASM):', () => {
192+
const coin = coins.get('tdot');
193+
const material = utils.getMaterial(coin);
194+
const SENDER = testData.accounts.account1.address;
195+
const RECIPIENT = testData.accounts.account2.address;
196+
197+
function wasmContext(nonce = 0): BuildContext {
198+
return {
199+
sender: SENDER,
200+
nonce,
201+
material: material as Material,
202+
validity: { firstValid: testData.westendBlock.blockNumber, maxDuration: 2400 },
203+
referenceBlock: testData.westendBlock.hash,
204+
};
205+
}
206+
207+
it('should explain a transfer via explainTransactionWithWasm', function () {
208+
const tx = buildTransaction({ type: 'transfer', to: RECIPIENT, amount: 1000000000000n }, wasmContext());
209+
const explained = basecoin.explainTransactionWithWasm(tx.toHex(), SENDER);
210+
211+
assert.strictEqual(explained.type, TransactionType.Send);
212+
assert.strictEqual(explained.outputs.length, 1);
213+
assert.strictEqual(explained.outputs[0].address, RECIPIENT);
214+
assert.strictEqual(explained.outputs[0].amount, '1000000000000');
215+
assert.strictEqual(explained.outputAmount, '1000000000000');
216+
assert.strictEqual(explained.sender, SENDER);
217+
assert.strictEqual(explained.methodName, 'balances.transferKeepAlive');
218+
});
219+
220+
it('should explain a staking bond via explainTransactionWithWasm', function () {
221+
const tx = buildTransaction({ type: 'stake', amount: 5000000000000n, payee: { type: 'stash' } }, wasmContext(1));
222+
const explained = basecoin.explainTransactionWithWasm(tx.toHex(), SENDER);
223+
224+
assert.strictEqual(explained.type, TransactionType.StakingActivate);
225+
assert.strictEqual(explained.outputs.length, 1);
226+
assert.strictEqual(explained.outputs[0].address, 'STAKING');
227+
assert.strictEqual(explained.outputs[0].amount, '5000000000000');
228+
});
229+
230+
it('should explain a transfer via coin class explainTransaction (WASM path)', async function () {
231+
const tx = buildTransaction({ type: 'transfer', to: RECIPIENT, amount: 1000000000000n }, wasmContext());
232+
const unsignedTransaction = {
233+
serializedTxHex: tx.toHex(),
234+
signableHex: tx.toHex(),
235+
derivationPath: 'm/0',
236+
parsedTx: { outputs: [], spendAmount: '0', inputs: [] },
237+
entryValues: undefined,
238+
coinSpecific: { blockNumber: 12345 },
239+
};
240+
const explained = await basecoin.explainTransaction(unsignedTransaction);
241+
242+
assert.strictEqual(explained.type, 'Send');
243+
assert.strictEqual(explained.outputs.length, 1);
244+
assert.strictEqual(explained.outputs[0].address, RECIPIENT);
245+
assert.strictEqual(explained.outputs[0].amount, '1000000000000');
246+
assert.strictEqual(explained.outputs[0].valueString, '1000000000000');
247+
assert.strictEqual(explained.sequenceId, 0);
248+
assert.strictEqual(explained.blockNumber, 12345);
249+
});
250+
});
251+
188252
describe('Recover Transactions:', () => {
189253
const sandBox = sinon.createSandbox();
190254
const destAddr = testData.accounts.account1.address;

0 commit comments

Comments
 (0)