Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/wasm-dot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
target/
node_modules/
# we actually only track the .ts files
dist/
test/*.js
test/*.d.ts
js/*.js
js/*.d.ts
js/wasm
.vscode
pkg/
Cargo.lock
5 changes: 5 additions & 0 deletions packages/wasm-dot/.mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extensions": ["ts", "tsx", "js", "jsx"],
"spec": ["test/**/*.ts"],
"node-option": ["import=tsx/esm", "experimental-wasm-modules"]
}
4 changes: 4 additions & 0 deletions packages/wasm-dot/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
js/wasm
test/fixtures/
target/
bips/
47 changes: 47 additions & 0 deletions packages/wasm-dot/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[package]
name = "wasm-dot"
version = "0.1.0"
edition = "2021"

[package.metadata.wasm-pack.profile.release]
wasm-opt = false

[lib]
crate-type = ["cdylib", "rlib"]

[lints.clippy]
all = "warn"

[dependencies]
# WASM bindings
wasm-bindgen = "0.2"
js-sys = "0.3"

# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-wasm-bindgen = "0.6"

# SCALE codec (Substrate/Polkadot serialization)
parity-scale-codec = { version = "3.6", features = ["derive"] }

# Subxt for client-side Substrate types (WASM compatible)
# Note: We use subxt_core::ext::scale_value for dynamic call building
subxt-core = { version = "0.37", default-features = false }

# Crypto
blake2 = "0.10"
bs58 = "0.5"

# WASM random number generation support
getrandom = { version = "0.2", features = ["js"] }

# Hex encoding
hex = "0.4"

[dev-dependencies]
wasm-bindgen-test = "0.3"
hex = "0.4"

[profile.release]
strip = true
70 changes: 70 additions & 0 deletions packages/wasm-dot/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
WASM_PACK = wasm-pack
WASM_OPT = wasm-opt
WASM_PACK_FLAGS = --no-pack --weak-refs

ifdef WASM_PACK_DEV
WASM_PACK_FLAGS += --dev
endif

# Auto-detect Mac and use Homebrew LLVM for WASM compilation
# Apple's Clang doesn't support wasm32-unknown-unknown target
UNAME_S := $(shell uname -s)

ifeq ($(UNAME_S),Darwin)
# Mac detected - check for Homebrew LLVM installation
HOMEBREW_LLVM := $(shell brew --prefix llvm 2>/dev/null)

ifdef HOMEBREW_LLVM
export CC = $(HOMEBREW_LLVM)/bin/clang
export AR = $(HOMEBREW_LLVM)/bin/llvm-ar
$(info Using Homebrew LLVM: $(HOMEBREW_LLVM))
else
$(warning Homebrew LLVM not found. Install with: brew install llvm)
$(warning Continuing with system clang - may fail on Apple Silicon)
endif
endif

define WASM_PACK_COMMAND
$(WASM_PACK) build --no-opt --out-dir $(1) $(WASM_PACK_FLAGS) --target $(2)
endef

# run wasm-opt separately so we can pass `--enable-bulk-memory`
define WASM_OPT_COMMAND
$(WASM_OPT) --enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext -Oz $(1)/*.wasm -o $(1)/*.wasm
endef

define REMOVE_GITIGNORE
find $(1) -name .gitignore -delete
endef

define SHOW_WASM_SIZE
@find $(1) -name "*.wasm" -exec gzip -k {} \;
@find $(1) -name "*.wasm" -exec du -h {} \;
@find $(1) -name "*.wasm.gz" -exec du -h {} \;
@find $(1) -name "*.wasm.gz" -delete
endef

define BUILD
rm -rf $(1)
$(call WASM_PACK_COMMAND,$(1),$(2))
$(call WASM_OPT_COMMAND,$(1))
$(call REMOVE_GITIGNORE,$(1))
$(call SHOW_WASM_SIZE,$(1))
endef

.PHONY: js/wasm
js/wasm:
$(call BUILD,$@,bundler)

.PHONY: dist/esm/js/wasm
dist/esm/js/wasm:
$(call BUILD,$@,bundler)

.PHONY: dist/cjs/js/wasm
dist/cjs/js/wasm:
$(call BUILD,$@,nodejs)

.PHONY: lint
lint:
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
Binary file added packages/wasm-dot/bitgo-wasm-dot-0.0.1.tgz
Binary file not shown.
53 changes: 53 additions & 0 deletions packages/wasm-dot/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
project: ["./tsconfig.json", "./tsconfig.test.json"],
tsconfigRootDir: import.meta.dirname,
},
},
},
{
ignores: [
"dist/",
"pkg/",
"target/",
"node_modules/",
"js/wasm/",
"bundler-test/",
"cli/",
"bips/",
"*.config.js",
],
},
// Ban Node.js globals in production code
{
files: ["js/**/*.ts"],
rules: {
"no-restricted-globals": [
"error",
{
name: "Buffer",
message: "Use Uint8Array instead of Buffer for ESM compatibility.",
},
{
name: "process",
message: "Avoid Node.js process global for ESM compatibility.",
},
{
name: "__dirname",
message: "Use import.meta.url instead of __dirname for ESM.",
},
{
name: "__filename",
message: "Use import.meta.url instead of __filename for ESM.",
},
],
},
},
);
111 changes: 111 additions & 0 deletions packages/wasm-dot/js/builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Transaction building from high-level intents.
*
* Provides the `buildTransaction()` function for building DOT transactions.
* Follows wallet-platform pattern: buildTransaction(intent, context)
*/

import { BuilderNamespace } from "./wasm/wasm_dot";
import { DotTransaction } from "./transaction";
import type { TransactionIntent, BuildContext, BatchIntent } from "./types";

/**
* Build a DOT transaction from an intent and context.
*
* This function takes a declarative TransactionIntent and BuildContext,
* producing a Transaction object that can be inspected, signed, and serialized.
*
* The returned transaction is unsigned - signatures should be added via
* `addSignature()` before serializing with `toBytes()` and broadcasting.
*
* @param intent - What to do (transfer, stake, etc.)
* @param context - How to build it (sender, nonce, material, validity, referenceBlock)
* @returns A Transaction object that can be inspected, signed, and serialized
* @throws Error if the intent cannot be built (e.g., invalid addresses)
*
* @example
* ```typescript
* import { buildTransaction } from '@bitgo/wasm-dot';
*
* // Build a simple DOT transfer
* const tx = buildTransaction(
* { type: 'transfer', to: '5FHneW46...', amount: '1000000000000', keepAlive: true },
* {
* sender: '5EGoFA95omzemRssELLDjVenNZ68aXyUeqtKQScXSEBvVJkr',
* nonce: 5,
* material: {
* genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3',
* chainName: 'Polkadot',
* specName: 'polkadot',
* specVersion: 9150,
* txVersion: 9
* },
* validity: { firstValid: 1000, maxDuration: 2400 },
* referenceBlock: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3'
* }
* );
*
* // Inspect the transaction
* console.log(tx.nonce);
*
* // Get the signable payload for signing
* const payload = tx.signablePayload();
*
* // Add signature and serialize
* tx.addSignature(signerPubkey, signature);
* const txBytes = tx.toBytes();
* ```
*
* @example
* ```typescript
* // Build with batch (multiple operations)
* const tx = buildTransaction(
* {
* type: 'batch',
* calls: [
* { type: 'transfer', to: recipient, amount: '1000000000000' },
* { type: 'stake', amount: '5000000000000', payee: { type: 'staked' } }
* ],
* atomic: true
* },
* context
* );
* ```
*/
export function buildTransaction(intent: TransactionIntent, context: BuildContext): DotTransaction {
// Convert BigInt amounts to strings for JSON serialization
const serializedIntent = serializeIntent(intent);
const serializedContext = serializeContext(context);

const inner = BuilderNamespace.buildTransaction(serializedIntent, serializedContext);
return DotTransaction.fromInner(inner as any);
}

/**
* Serialize intent for WASM, converting BigInt to string
*/
function serializeIntent(intent: TransactionIntent): TransactionIntent {
if ("amount" in intent && typeof intent.amount === "bigint") {
return { ...intent, amount: intent.amount.toString() };
}
if (intent.type === "batch") {
return {
...intent,
calls: (intent as BatchIntent).calls.map(serializeIntent),
};
}
return intent;
}

/**
* Serialize context for WASM, converting BigInt to string
*/
function serializeContext(context: BuildContext): BuildContext {
return {
...context,
tip: context.tip?.toString() ?? "0",
};
}

// Re-export types for convenience
export type { TransactionIntent, BuildContext } from "./types";
34 changes: 34 additions & 0 deletions packages/wasm-dot/js/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* wasm-dot: WASM bindings for Polkadot/DOT transaction operations
*
* This module provides:
* - Transaction parsing (decode extrinsics)
* - Signature operations (add signatures to unsigned transactions)
* - Transaction building from intents (following wasm-solana pattern)
*/

import {
WasmTransaction,
ParserNamespace,
BuilderNamespace,
MaterialJs,
ValidityJs,
ParseContextJs,
} from "./wasm/wasm_dot";

// Export WASM classes for advanced usage
export {
WasmTransaction,
ParserNamespace,
ParserNamespace as DotParser,
BuilderNamespace,
MaterialJs,
ValidityJs,
ParseContextJs,
};

// Re-export types
export * from "./types";
export * from "./transaction";
export * from "./parser";
export * from "./builder";
Loading
Loading