feat: add wasm-dot package for Polkadot transaction building and decoding#145
feat: add wasm-dot package for Polkadot transaction building and decoding#145
Conversation
481b7ac to
ede6160
Compare
58a71f1 to
751d3e2
Compare
3be05b1 to
fe73139
Compare
…ding WASM-based Polkadot/Substrate transaction builder, parser, and explainer for use by sdk-coin-dot (tdot). Supports transfers, staking operations, proxy management, and batched calls. Key capabilities: - Build unsigned extrinsics from high-level intents (transfer, stake, batch, proxy) - Parse signed/unsigned extrinsics with metadata-aware signed extension decoding - Explain transactions: derive type, extract outputs/inputs, attach DOT-specific metadata - Proxy type resolution from chain metadata (works across Polkadot, Kusama, Westend) - parseTransaction returns DotTransaction with .parse() method (PSBT pattern) BTC-0
fe73139 to
19a7ab6
Compare
OttoAllmendinger
left a comment
There was a problem hiding this comment.
let's try this: I will write a high level CONVENTIONS.md file that contains these re-occurring patterns an we'll see if the agents pick it up
| /** | ||
| * Get call data as hex string | ||
| */ | ||
| get callDataHex(): string { |
There was a problem hiding this comment.
the callers can do the base conversions
| */ | ||
| export class DotTransaction { | ||
| private inner: WasmTransaction; | ||
| private _context?: ParseContext; |
There was a problem hiding this comment.
can we do without this?
| * ``` | ||
| */ | ||
| parse(context?: ParseContext): ParsedTransaction { | ||
| return parseTransactionData(this.toHex(), context ?? this._context); |
There was a problem hiding this comment.
needless base conversion
I think we can do without the context. Maybe have parse as a separate func instead of amethod
| /** | ||
| * Get the underlying WASM transaction (for advanced use) | ||
| */ | ||
| getInner(): WasmTransaction { |
There was a problem hiding this comment.
we use a different convention in wasm-utxo, I would prefer we stick to one. Maybe we should write it down as a toplevel repo document.
| /** | ||
| * Create a ParseContextJs from ParseContext | ||
| */ | ||
| function createParseContext(ctx: ParseContext): ParseContextJs { | ||
| const material = new MaterialJs( | ||
| ctx.material.genesisHash, | ||
| ctx.material.chainName, | ||
| ctx.material.specName, | ||
| ctx.material.specVersion, | ||
| ctx.material.txVersion, | ||
| ctx.material.metadata, | ||
| ); | ||
| return new ParseContextJs(material, ctx.sender ?? null); | ||
| } |
There was a problem hiding this comment.
can we move all this to parser.ts and have it distinct from Transaction?
| const ctx = context ? createParseContext(context) : undefined; | ||
|
|
||
| if (typeof input === "string") { | ||
| return ParserNamespace.parseTransactionHex(input, ctx) as ParsedTransaction; |
There was a problem hiding this comment.
we can do the hex parsing here and have a parseTransaction that works on Uint8Array
| } | ||
|
|
||
| if (input instanceof DotTransaction) { | ||
| const hex = input.toHex(); |
There was a problem hiding this comment.
needless base conversion, going through bytes is cleaner
| /** | ||
| * Get signable payload as hex | ||
| */ | ||
| signablePayloadHex(): string { |
| * @param signature - 64-byte Ed25519 signature | ||
| * @param pubkey - 32-byte public key | ||
| */ | ||
| addSignature(signature: Uint8Array, pubkey: Uint8Array): void { |
| /** | ||
| * Serialize to hex string | ||
| */ | ||
| toHex(): string { |
Summary
Add a new WASM package (
@bitgo/wasm-dot) for Polkadot/Substrate transaction building and decoding, following the same architecture aswasm-solana.Architecture
Why this split: Rust handles SCALE binary format (compact integers, MultiAddress, era encoding, recursive batch/proxy calls). TypeScript handles the explain layer — deriving transaction types from pallet+method, extracting outputs/inputs, computing amounts. This means business logic changes don't require WASM rebuilds.
What's included
Builder — build transactions from declarative intents:
transfer,transferAll,stake,unstake,withdrawUnbonded,chilladdProxy,removeProxybatch/batchAllwith nested call encodingBigIntnatively for amounts (u64— matcheswasm-solanapattern, no custom deserializers needed)Parser — decode SCALE-encoded extrinsics:
utility.batchandproxy.proxywith depth limiting (max 10)Explain — structured transaction explanation:
TransactionType: Send, StakingActivate, StakingUnlock, StakingWithdraw, StakingUnvote, StakingClaim, AddressInitialization, Batch, UnknownoutputAmount(sum of non-ALL outputs)Key design decisions
u64for amounts (notu128)serde_wasm_bindgenhandles JS BigInt → u64 natively — no custom deserializers, no TS-side.toString()conversionBatchtypeproxy.proxyderives from inner callSendTest coverage
Test plan
cargo test— 28 tests passingcargo fmt+cargo clippy— cleannpm run build— WASM + TypeScript compilationnpm test— 25 tests passingnpx prettier --check— cleannpm ci— lockfile updated with workspace entryBTC-0