Skip to content

Commit 158a9ee

Browse files
committed
fix: add toJsonFromWasm adapter and metadata-aware parsing fixes
Add toJsonFromWasm() to wasmParser.ts — maps WASM explainTransaction output to the TxData interface for tdot signed transactions. Handles all transaction types: transfers, staking, proxy, batch. Key fixes: - Proxy type key: proxy_type (snake_case from WASM) - Batch call args: transform to Polkadot.js format (delegate as {id}, value as number, payee as {variant: null}) - Immortal eraPeriod defaults to 0 - WASM dispatch guard in toJson() for tdot signed transactions BTC-0 TICKET: BTC-0
1 parent f934325 commit 158a9ee

2 files changed

Lines changed: 178 additions & 11 deletions

File tree

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import {
3838
} from './iface';
3939
import { getAddress, getDelegateAddress } from './iface_utils';
4040
import utils from './utils';
41-
import { explainDotTransaction } from './wasmParser';
41+
import { explainDotTransaction, toJsonFromWasm } from './wasmParser';
4242
import BigNumber from 'bignumber.js';
4343
import { Vec } from '@polkadot/types';
4444
import { PalletConstantMetadataV14 } from '@polkadot/types/interfaces';
@@ -162,6 +162,20 @@ export class Transaction extends BaseTransaction {
162162
if (!this._dotTransaction) {
163163
throw new InvalidTransactionError('Empty transaction');
164164
}
165+
166+
// WASM path for signed tdot transactions — validates WASM parsing against production.
167+
// Only for signed txs because toBroadcastFormat() returns the signed extrinsic (parseable).
168+
// Unsigned txs return a signing payload (different format), so they use the legacy path.
169+
if (this._coinConfig.name === 'tdot' && this._signedTransaction) {
170+
return toJsonFromWasm({
171+
txHex: this._signedTransaction,
172+
material: utils.getMaterial(this._coinConfig),
173+
senderAddress: this._sender,
174+
coinConfigName: this._coinConfig.name,
175+
referenceBlock: this._dotTransaction.blockHash,
176+
blockNumber: Number(this._dotTransaction.blockNumber),
177+
});
178+
}
165179
const decodedTx = decode(this._dotTransaction, {
166180
metadataRpc: this._dotTransaction.metadataRpc,
167181
registry: this._registry,

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

Lines changed: 163 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import {
33
explainTransaction,
44
TransactionType as WasmTransactionType,
55
type ExplainedTransaction as WasmExplainedTransaction,
6+
type ParsedMethod,
67
} from '@bitgo/wasm-dot';
7-
import type { TransactionExplanation, Material } from './iface';
8+
import type { BatchCallObject, ProxyType, TransactionExplanation, Material, TxData } from './iface';
89

910
/**
1011
* Input entry for a DOT transaction.
@@ -33,9 +34,24 @@ function mapTransactionType(wasmType: WasmTransactionType): TransactionType {
3334
return TransactionType[wasmType as keyof typeof TransactionType] ?? TransactionType.Send;
3435
}
3536

36-
/**
37-
* Options for the WASM-based DOT transaction explanation adapter.
38-
*/
37+
/** Call WASM explainTransaction, returns the raw WASM result */
38+
function callWasmExplain(params: {
39+
txHex: string;
40+
material: Material;
41+
senderAddress?: string;
42+
referenceBlock?: string;
43+
blockNumber?: number;
44+
}): WasmExplainedTransaction {
45+
return explainTransaction(params.txHex, {
46+
context: {
47+
material: params.material,
48+
sender: params.senderAddress,
49+
referenceBlock: params.referenceBlock,
50+
blockNumber: params.blockNumber,
51+
},
52+
});
53+
}
54+
3955
export interface ExplainDotTransactionParams {
4056
txHex: string;
4157
material: Material;
@@ -47,24 +63,19 @@ export interface ExplainDotTransactionParams {
4763
*
4864
* Thin adapter over @bitgo/wasm-dot's explainTransaction that maps
4965
* WASM types to BitGoJS TransactionExplanation format.
50-
* Analogous to explainSolTransaction for Solana.
5166
*/
5267
export function explainDotTransaction(params: ExplainDotTransactionParams): DotWasmExplanation {
53-
const explained: WasmExplainedTransaction = explainTransaction(params.txHex, {
54-
context: { material: params.material, sender: params.senderAddress },
55-
});
68+
const explained = callWasmExplain(params);
5669

5770
const sender = explained.sender || params.senderAddress || '';
5871
const type = mapTransactionType(explained.type);
5972
const methodName = `${explained.method.pallet}.${explained.method.name}`;
6073

61-
// Map WASM outputs to BitGoJS format
6274
const outputs = explained.outputs.map((o) => ({
6375
address: o.address,
6476
amount: o.amount === 'ALL' ? '0' : o.amount,
6577
}));
6678

67-
// Map WASM inputs to BitGoJS format (with numeric value for legacy compat)
6879
const inputs: DotInput[] = explained.inputs.map((i) => {
6980
const value = i.value === 'ALL' ? 0 : parseInt(i.value || '0', 10);
7081
return { address: i.address, value, valueString: i.value };
@@ -86,3 +97,145 @@ export function explainDotTransaction(params: ExplainDotTransactionParams): DotW
8697
inputs,
8798
};
8899
}
100+
101+
// =============================================================================
102+
// toJsonFromWasm — WASM-based replacement for toJson()
103+
// =============================================================================
104+
105+
export interface ToJsonFromWasmParams {
106+
txHex: string;
107+
material: Material;
108+
senderAddress: string;
109+
coinConfigName: string;
110+
referenceBlock?: string;
111+
blockNumber?: number;
112+
}
113+
114+
/**
115+
* Produce a TxData object using WASM parsing instead of the JS txwrapper.
116+
*
117+
* This replaces the legacy `toJson()` path for chains where WASM parsing is enabled.
118+
* The WASM parser decodes the extrinsic bytes (with metadata-aware signed extension handling),
119+
* and this function maps the result to the TxData interface that consumers expect.
120+
*/
121+
export function toJsonFromWasm(params: ToJsonFromWasmParams): TxData {
122+
const explained = callWasmExplain(params);
123+
const type = mapTransactionType(explained.type);
124+
const method = explained.method;
125+
const args = method.args as Record<string, unknown>;
126+
127+
const result: TxData = {
128+
id: explained.id || '',
129+
sender: explained.sender || params.senderAddress,
130+
referenceBlock: explained.referenceBlock || '',
131+
blockNumber: explained.blockNumber || 0,
132+
genesisHash: explained.genesisHash || '',
133+
nonce: explained.nonce,
134+
specVersion: explained.specVersion || 0,
135+
transactionVersion: explained.transactionVersion || 0,
136+
chainName: explained.chainName || '',
137+
tip: Number(explained.tip) || 0,
138+
eraPeriod: explained.era.type === 'mortal' ? (explained.era as { period: number }).period : 0,
139+
};
140+
141+
if (type === TransactionType.Send) {
142+
populateSendFields(result, method, args);
143+
} else if (type === TransactionType.StakingActivate) {
144+
populateStakingActivateFields(result, method, args, params.senderAddress);
145+
} else if (type === TransactionType.StakingUnlock) {
146+
result.amount = String(args.value ?? '');
147+
} else if (type === TransactionType.StakingWithdraw) {
148+
result.numSlashingSpans = String(args.numSlashingSpans ?? '0');
149+
} else if (type === TransactionType.StakingClaim) {
150+
result.validatorStash = String(args.validatorStash ?? '');
151+
result.claimEra = String(args.era ?? '');
152+
} else if (type === TransactionType.AddressInitialization) {
153+
populateAddressInitFields(result, method, args);
154+
} else if (type === TransactionType.Batch) {
155+
result.batchCalls = mapBatchCalls(args.calls as ParsedMethod[]);
156+
}
157+
158+
return result;
159+
}
160+
161+
function populateSendFields(result: TxData, method: ParsedMethod, args: Record<string, unknown>): void {
162+
const key = `${method.pallet}.${method.name}`;
163+
164+
if (key === 'proxy.proxy') {
165+
// Proxy-wrapped transfer
166+
const innerCall = args.call as ParsedMethod | undefined;
167+
result.owner = String(args.real ?? '');
168+
result.forceProxyType = (args.forceProxyType as ProxyType) ?? undefined;
169+
if (innerCall?.args) {
170+
const innerArgs = innerCall.args as Record<string, unknown>;
171+
result.to = String(innerArgs.dest ?? '');
172+
result.amount = String(innerArgs.value ?? '');
173+
}
174+
} else if (key === 'balances.transferAll') {
175+
result.to = String(args.dest ?? '');
176+
result.keepAlive = Boolean(args.keepAlive);
177+
} else {
178+
// transfer, transferKeepAlive, transferAllowDeath
179+
result.to = String(args.dest ?? '');
180+
result.amount = String(args.value ?? '');
181+
}
182+
}
183+
184+
function populateStakingActivateFields(
185+
result: TxData,
186+
method: ParsedMethod,
187+
args: Record<string, unknown>,
188+
senderAddress: string
189+
): void {
190+
if (method.name === 'bondExtra') {
191+
result.amount = String(args.value ?? '');
192+
} else {
193+
// bond
194+
result.controller = senderAddress;
195+
result.amount = String(args.value ?? '');
196+
result.payee = String(args.payee ?? '');
197+
}
198+
}
199+
200+
function populateAddressInitFields(result: TxData, method: ParsedMethod, args: Record<string, unknown>): void {
201+
const key = `${method.pallet}.${method.name}`;
202+
result.method = key;
203+
result.proxyType = String(args.proxy_type ?? '');
204+
result.delay = String(args.delay ?? '');
205+
206+
if (key === 'proxy.createPure') {
207+
result.index = String(args.index ?? '');
208+
} else {
209+
// addProxy, removeProxy
210+
result.owner = String(args.delegate ?? '');
211+
}
212+
}
213+
214+
function mapBatchCalls(calls: ParsedMethod[] | undefined): BatchCallObject[] {
215+
if (!calls) return [];
216+
return calls.map((call) => ({
217+
callIndex: `0x${call.palletIndex.toString(16).padStart(2, '0')}${call.methodIndex.toString(16).padStart(2, '0')}`,
218+
args: transformBatchCallArgs((call.args ?? {}) as Record<string, unknown>),
219+
}));
220+
}
221+
222+
/** Transform WASM-decoded batch call args to match the Polkadot.js format that consumers expect */
223+
function transformBatchCallArgs(args: Record<string, unknown>): Record<string, unknown> {
224+
const result: Record<string, unknown> = {};
225+
for (const [key, value] of Object.entries(args)) {
226+
if (key === 'delegate' && typeof value === 'string') {
227+
// MultiAddress Id variant: string → { id: string }
228+
result[key] = { id: value };
229+
} else if (key === 'value' && typeof value === 'string') {
230+
// Compact u128: string → number (matches Polkadot.js behavior)
231+
result[key] = Number(value);
232+
} else if (key === 'payee' && typeof value === 'string') {
233+
// Enum unit variant: "Staked" → { staked: null }
234+
const variantName = value.charAt(0).toLowerCase() + value.slice(1);
235+
result[key] = { [variantName]: null };
236+
} else {
237+
result[key] = value;
238+
}
239+
}
240+
return result;
241+
}

0 commit comments

Comments
 (0)