Skip to content

Commit 751d3e2

Browse files
committed
feat: add wasm-dot package for Polkadot transaction operations
WASM-based Polkadot/DOT transaction package following the same architecture as wasm-solana: Rust handles byte-level SCALE encoding/ decoding, TypeScript handles business logic and explain semantics. Supported transaction types: - Transfers: transfer, transferKeepAlive, transferAllowDeath, transferAll - Staking: bond, bondExtra, unbond, withdrawUnbonded, chill, payoutStakers - Proxy: addProxy, removeProxy, createPure, proxy.proxy (nested) - Utility: batch, batchAll (recursive inner call parsing) Key features: - buildTransaction(intent, context) — encode intents to extrinsic bytes - parseTransaction(bytes, context) — decode extrinsic bytes to structured data - explainTransaction(bytes, opts) — full explain with type derivation, output/input extraction, and metadata (tip, era, nonce) - DotTransaction class with add_signature() and signable_payload() - Dynamic pallet resolution via runtime metadata with hardcoded fallback - Dual resolution: metadata-based (preferred) and static index mapping for Polkadot, Kusama, and Westend Architecture: - Core Rust (src/*.rs): pure byte decoding, no WASM deps - WASM bindings (src/wasm/*.rs): thin #[wasm_bindgen] wrappers - TypeScript (js/*.ts): business logic, type derivation, explain semantics - Recursive batch/proxy parsing with depth limit (MAX_NESTING_DEPTH=10) - Batch count cap (MAX_BATCH_SIZE=256) against untrusted input - json_compatible() serializer for serde_json::Value → plain JS objects BTC-0
1 parent c1b45e4 commit 751d3e2

33 files changed

Lines changed: 4613 additions & 0 deletions

packages/wasm-dot/.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
target/
2+
node_modules/
3+
# we actually only track the .ts files
4+
dist/
5+
test/*.js
6+
test/*.d.ts
7+
js/*.js
8+
js/*.d.ts
9+
js/wasm
10+
.vscode
11+
pkg/
12+
Cargo.lock

packages/wasm-dot/.mocharc.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"extensions": ["ts", "tsx", "js", "jsx"],
3+
"spec": ["test/**/*.ts"],
4+
"node-option": ["import=tsx/esm", "experimental-wasm-modules"]
5+
}

packages/wasm-dot/.prettierignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
js/wasm
2+
test/fixtures/
3+
target/
4+
bips/

packages/wasm-dot/Cargo.toml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
[package]
2+
name = "wasm-dot"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[package.metadata.wasm-pack.profile.release]
7+
wasm-opt = false
8+
9+
[lib]
10+
crate-type = ["cdylib", "rlib"]
11+
12+
[lints.clippy]
13+
all = "warn"
14+
15+
[dependencies]
16+
# WASM bindings
17+
wasm-bindgen = "0.2"
18+
js-sys = "0.3"
19+
20+
# Serialization
21+
serde = { version = "1.0", features = ["derive"] }
22+
serde_json = "1.0"
23+
serde-wasm-bindgen = "0.6"
24+
25+
# SCALE codec (Substrate/Polkadot serialization)
26+
parity-scale-codec = { version = "3.6", features = ["derive"] }
27+
28+
# Subxt for client-side Substrate types (WASM compatible)
29+
# Note: We use subxt_core::ext::scale_value for dynamic call building
30+
subxt-core = { version = "0.37", default-features = false }
31+
32+
# Crypto
33+
blake2 = "0.10"
34+
bs58 = "0.5"
35+
36+
# WASM random number generation support
37+
getrandom = { version = "0.2", features = ["js"] }
38+
39+
# Hex encoding
40+
hex = "0.4"
41+
42+
[dev-dependencies]
43+
wasm-bindgen-test = "0.3"
44+
hex = "0.4"
45+
46+
[profile.release]
47+
strip = true

packages/wasm-dot/Makefile

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
WASM_PACK = wasm-pack
2+
WASM_OPT = wasm-opt
3+
WASM_PACK_FLAGS = --no-pack --weak-refs
4+
5+
ifdef WASM_PACK_DEV
6+
WASM_PACK_FLAGS += --dev
7+
endif
8+
9+
# Auto-detect Mac and use Homebrew LLVM for WASM compilation
10+
# Apple's Clang doesn't support wasm32-unknown-unknown target
11+
UNAME_S := $(shell uname -s)
12+
13+
ifeq ($(UNAME_S),Darwin)
14+
# Mac detected - check for Homebrew LLVM installation
15+
HOMEBREW_LLVM := $(shell brew --prefix llvm 2>/dev/null)
16+
17+
ifdef HOMEBREW_LLVM
18+
export CC = $(HOMEBREW_LLVM)/bin/clang
19+
export AR = $(HOMEBREW_LLVM)/bin/llvm-ar
20+
$(info Using Homebrew LLVM: $(HOMEBREW_LLVM))
21+
else
22+
$(warning Homebrew LLVM not found. Install with: brew install llvm)
23+
$(warning Continuing with system clang - may fail on Apple Silicon)
24+
endif
25+
endif
26+
27+
define WASM_PACK_COMMAND
28+
$(WASM_PACK) build --no-opt --out-dir $(1) $(WASM_PACK_FLAGS) --target $(2)
29+
endef
30+
31+
# run wasm-opt separately so we can pass `--enable-bulk-memory`
32+
define WASM_OPT_COMMAND
33+
$(WASM_OPT) --enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext -Oz $(1)/*.wasm -o $(1)/*.wasm
34+
endef
35+
36+
define REMOVE_GITIGNORE
37+
find $(1) -name .gitignore -delete
38+
endef
39+
40+
define SHOW_WASM_SIZE
41+
@find $(1) -name "*.wasm" -exec gzip -k {} \;
42+
@find $(1) -name "*.wasm" -exec du -h {} \;
43+
@find $(1) -name "*.wasm.gz" -exec du -h {} \;
44+
@find $(1) -name "*.wasm.gz" -delete
45+
endef
46+
47+
define BUILD
48+
rm -rf $(1)
49+
$(call WASM_PACK_COMMAND,$(1),$(2))
50+
$(call WASM_OPT_COMMAND,$(1))
51+
$(call REMOVE_GITIGNORE,$(1))
52+
$(call SHOW_WASM_SIZE,$(1))
53+
endef
54+
55+
.PHONY: js/wasm
56+
js/wasm:
57+
$(call BUILD,$@,bundler)
58+
59+
.PHONY: dist/esm/js/wasm
60+
dist/esm/js/wasm:
61+
$(call BUILD,$@,bundler)
62+
63+
.PHONY: dist/cjs/js/wasm
64+
dist/cjs/js/wasm:
65+
$(call BUILD,$@,nodejs)
66+
67+
.PHONY: lint
68+
lint:
69+
cargo fmt --check
70+
cargo clippy --all-targets --all-features -- -D warnings

packages/wasm-dot/eslint.config.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import eslint from "@eslint/js";
2+
import tseslint from "typescript-eslint";
3+
4+
export default tseslint.config(
5+
eslint.configs.recommended,
6+
...tseslint.configs.recommendedTypeChecked,
7+
{
8+
languageOptions: {
9+
parserOptions: {
10+
project: ["./tsconfig.json", "./tsconfig.test.json"],
11+
tsconfigRootDir: import.meta.dirname,
12+
},
13+
},
14+
},
15+
{
16+
ignores: [
17+
"dist/",
18+
"pkg/",
19+
"target/",
20+
"node_modules/",
21+
"js/wasm/",
22+
"bundler-test/",
23+
"cli/",
24+
"bips/",
25+
"*.config.js",
26+
],
27+
},
28+
// Ban Node.js globals in production code
29+
{
30+
files: ["js/**/*.ts"],
31+
rules: {
32+
"no-restricted-globals": [
33+
"error",
34+
{
35+
name: "Buffer",
36+
message: "Use Uint8Array instead of Buffer for ESM compatibility.",
37+
},
38+
{
39+
name: "process",
40+
message: "Avoid Node.js process global for ESM compatibility.",
41+
},
42+
{
43+
name: "__dirname",
44+
message: "Use import.meta.url instead of __dirname for ESM.",
45+
},
46+
{
47+
name: "__filename",
48+
message: "Use import.meta.url instead of __filename for ESM.",
49+
},
50+
],
51+
},
52+
},
53+
);

packages/wasm-dot/js/builder.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Transaction building from high-level intents.
3+
*
4+
* Provides the `buildTransaction()` function for building DOT transactions.
5+
* Follows wallet-platform pattern: buildTransaction(intent, context)
6+
*/
7+
8+
import { BuilderNamespace } from "./wasm/wasm_dot";
9+
import { DotTransaction } from "./transaction";
10+
import type { TransactionIntent, BuildContext } from "./types";
11+
12+
/**
13+
* Build a DOT transaction from an intent and context.
14+
*
15+
* This function takes a declarative TransactionIntent and BuildContext,
16+
* producing a Transaction object that can be inspected, signed, and serialized.
17+
*
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
24+
* @throws Error if the intent cannot be built (e.g., invalid addresses)
25+
*
26+
* @example
27+
* ```typescript
28+
* import { buildTransaction } from '@bitgo/wasm-dot';
29+
*
30+
* // Build a simple DOT transfer
31+
* 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+
* }
46+
* );
47+
*
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+
* ```
58+
*
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+
* },
71+
* context
72+
* );
73+
* ```
74+
*/
75+
export function buildTransaction(intent: TransactionIntent, context: BuildContext): DotTransaction {
76+
const inner = BuilderNamespace.buildTransaction(intent, context);
77+
return DotTransaction.fromInner(inner as any);
78+
}
79+
80+
// Re-export types for convenience
81+
export type { TransactionIntent, BuildContext } from "./types";

0 commit comments

Comments
 (0)