Skip to content

feat: add wasm-dot package for Polkadot transaction building and decoding#145

Merged
lcovar merged 3 commits into
masterfrom
BTC-0.dot-wasm
Feb 26, 2026
Merged

feat: add wasm-dot package for Polkadot transaction building and decoding#145
lcovar merged 3 commits into
masterfrom
BTC-0.dot-wasm

Conversation

@lcovar

@lcovar lcovar commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

Summary

add wasm-dot package for Polkadot/Substrate transaction building, parsing, and signing. follows wasm-utxo conventions: Uint8Array only, signing separate from parsing, get wasm() for internal access.

API

three entry points:

  1. DotTransaction.fromBytes(bytes) — deserialize for signing
  2. parseTransaction(bytes, context) — decode into structured data
  3. explainTransaction(bytes, options) — high-level explanation with type derivation
// signing
const tx = DotTransaction.fromBytes(txBytes);
tx.addSignature(signature, pubkey);
const signedBytes = tx.toBytes();

// parsing
const parsed = parseTransaction(txBytes, { material });
console.log(parsed.method.pallet); // "balances"
console.log(parsed.method.name);   // "transferKeepAlive"

// explaining
const explained = explainTransaction(txBytes, { context: { material } });
console.log(explained.type); // TransactionType.Send
console.log(explained.outputs); // [{ address, amount }]

architecture

Rust (parser.rs, builder/) → SCALE encoding/decoding
TS (explain.ts)            → business logic (type derivation, output extraction)

rust handles SCALE binary format (compact integers, MultiAddress, era encoding, recursive batch/proxy calls). typescript handles explain layer — deriving transaction types from pallet+method, extracting outputs/inputs. business logic changes don't require WASM rebuilds.

what's included

builder — build from declarative intents:

  • transfer, transferAll, stake, unstake, withdrawUnbonded, chill
  • addProxy, removeProxy
  • batch / batchAll with nested call encoding
  • accepts JS BigInt for amounts (u64 — matches wasm-solana)
  • mortal/immortal eras, tips, nonce

parser — decode SCALE extrinsics:

  • extracts sender, nonce, tip, era, method (pallet + name + decoded args)
  • hardcoded call resolution for Polkadot, Kusama, Westend
  • dynamic metadata-based resolution for any Substrate chain
  • recursive parsing for utility.batch and proxy.proxy (depth-limited, max 10)

explain — structured transaction explanation:

  • derives TransactionType: Send, StakingActivate, StakingUnlock, StakingWithdraw, StakingUnvote, StakingClaim, AddressInitialization, Batch, Unknown
  • recursive output/input extraction through batch and proxy wrappers
  • computes outputAmount (sum of non-ALL outputs)

conventions followed

  • Uint8Array everywhere (no hex methods on Transaction)
  • parsing separate from Transaction class (standalone parseTransaction function)
  • get wasm() accessor (not getInner())
  • DotTransaction.fromBytes() for signing (no top-level parseTransaction that returns Transaction)
  • callers do their own base conversions

test coverage

  • 28 Rust unit tests — parser, address encoding, call encoding, type serialization
  • 30 TypeScript integration tests — build+explain round-trips for all transaction types, batch output extraction, unsigned transactions, metadata fields

test plan

  • cargo test — 28 passing
  • cargo fmt + cargo clippy — clean
  • npm run build — WASM + TypeScript compilation
  • npm test — 30 passing
  • npx prettier --check — clean

Tickets

  • BTC-3064 — Transaction deserialization and signing
  • BTC-3065 — Transaction parsing and call data decoding
  • BTC-3066 — Transaction explanation
  • BTC-3067 — Transaction building

@lcovar lcovar force-pushed the BTC-0.dot-wasm branch 2 times, most recently from 58a71f1 to 751d3e2 Compare February 19, 2026 22:33
@lcovar lcovar changed the title temp: add wasm-dot package for Polkadot feat: add wasm-dot package for Polkadot transaction building and decoding Feb 19, 2026
@lcovar lcovar force-pushed the BTC-0.dot-wasm branch 11 times, most recently from 3be05b1 to fe73139 Compare February 20, 2026 09:17
…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
@lcovar lcovar marked this pull request as ready for review February 20, 2026 20:21
@lcovar lcovar requested a review from a team as a code owner February 20, 2026 20:21

@OttoAllmendinger OttoAllmendinger left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/wasm-dot/js/transaction.ts Outdated
Comment thread packages/wasm-dot/js/transaction.ts Outdated
Comment thread packages/wasm-dot/js/transaction.ts Outdated
Comment thread packages/wasm-dot/js/transaction.ts Outdated
Comment thread packages/wasm-dot/js/transaction.ts Outdated
Comment thread packages/wasm-dot/js/parser.ts Outdated
Comment thread packages/wasm-dot/js/parser.ts Outdated
Comment thread packages/wasm-dot/js/transaction.ts Outdated
Comment thread packages/wasm-dot/js/transaction.ts
Comment thread packages/wasm-dot/js/transaction.ts Outdated
lcovar added a commit that referenced this pull request Feb 23, 2026
remove hex methods, move parse() to standalone function, use get wasm()

changes per otto's review:
- remove Transaction.fromHex(), .toHex(), .toBroadcastFormat(), .callDataHex, .signablePayloadHex()
- remove _context field and .parse() method from Transaction
- rename parseTransactionData → parseTransaction (the actual parser)
- rename getInner() → get wasm() (@internal)
- remove top-level parseTransaction function (callers use DotTransaction.fromBytes directly)
- move createParseContext to parser.ts (single source)
- update tests: DotTransaction.fromBytes() for signing, parseTransaction() for decoded data
- callers do their own base conversions (Uint8Array only on public API)

separation of concerns:
- Transaction.fromBytes(bytes) - deserialize for signing
- parseTransaction(bytes, context?) - decode into structured data
- explainTransaction(bytes, options) - high-level explanation

BTC-0
lcovar added a commit that referenced this pull request Feb 23, 2026
remove hex methods, move parse() to standalone function, use get wasm()

changes per otto's review:
- remove Transaction.fromHex(), .toHex(), .toBroadcastFormat(), .callDataHex, .signablePayloadHex()
- remove _context field and .parse() method from Transaction
- rename parseTransactionData → parseTransaction (the actual parser)
- rename getInner() → get wasm() (@internal)
- remove top-level parseTransaction function (callers use DotTransaction.fromBytes directly)
- move createParseContext to parser.ts (single source)
- update tests: DotTransaction.fromBytes() for signing, parseTransaction() for decoded data
- callers do their own base conversions (Uint8Array only on public API)

separation of concerns:
- Transaction.fromBytes(bytes) - deserialize for signing
- parseTransaction(bytes, context?) - decode into structured data
- explainTransaction(bytes, options) - high-level explanation

BTC-0

@OttoAllmendinger OttoAllmendinger left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

much better

Comment thread packages/wasm-dot/js/transaction.ts Outdated
specVersion: number;
/** Transaction format version */
txVersion: number;
/** Runtime metadata bytes (hex encoded) - required for encoding calls */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

( ͡° ͜ʖ ͡°)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seriously consider Uint8Array

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hesitating a bit since its a string anyway when we fetch from the node so ill just be doing a back and forth conversion but i understand your hate for strings

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well do we end up decoding it anyway or does it stay a string the whole time?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah it gets decoded to bytes so might as well start out with bytes.

Comment thread packages/wasm-dot/js/types.ts
remove hex methods, move parse() to standalone function, use get wasm()

changes per otto's review:
- remove Transaction.fromHex(), .toHex(), .toBroadcastFormat(), .callDataHex, .signablePayloadHex()
- remove _context field and .parse() method from Transaction
- rename parseTransactionData → parseTransaction (the actual parser)
- rename getInner() → get wasm() (@internal)
- remove top-level parseTransaction function (callers use DotTransaction.fromBytes directly)
- move createParseContext to parser.ts (single source)
- update tests: DotTransaction.fromBytes() for signing, parseTransaction() for decoded data
- callers do their own base conversions (Uint8Array only on public API)

separation of concerns:
- Transaction.fromBytes(bytes) - deserialize for signing
- parseTransaction(bytes, context?) - decode into structured data
- explainTransaction(bytes, options) - high-level explanation

BTC-0
replace magic number parameter with strongly-typed AddressFormat enum.
improves type safety and makes the API self-documenting.

BTC-3064
@lcovar lcovar merged commit 71af785 into master Feb 26, 2026
7 checks passed
@lcovar lcovar deleted the BTC-0.dot-wasm branch February 26, 2026 21:05
lcovar added a commit that referenced this pull request Feb 26, 2026
document 7 architectural patterns enforced in code reviews:
- prefer Uint8Array, avoid unnecessary base conversions
- bigint for monetary amounts (conversions are caller's responsibility)
- as const arrays for union types, not magic strings
- builders return Transaction objects, not bytes
- parsing separate from Transaction (standalone parseTransaction)
- use wrapper classes over raw WASM bindings
- consistent wrapper API (fromBytes, toBytes, getId, get wasm())

distilled from recurring review comments across wasm-utxo, wasm-solana,
and wasm-dot. ref: PR #145 discussion.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants