Skip to content

Commit 6a3dba3

Browse files
OttoAllmendingerbitgobot
authored andcommitted
feat(abstract-wasm-coin): add pluggable WASM coin adapter module
Introduces modules/abstract-wasm-coin with: - WasmCoinAdapter interface (deriveAddress, parseTransaction, buildTransaction) — no account-lib dependency - AbstractWasmCoin wrapper with capability checking - WasmCoinRegistry for adapter registration and dispatch - Demo adapters for wasm-dot, wasm-solana, wasm-ton - Unit tests covering registry, capability guards, and dispatch BitGoJS coin modules retain explain logic, verification policy, and product semantics; WASM packages own chain-native parsing and building primitives only. Ticket: T1-3431 Session-Id: bcc064c3-f85f-4618-8733-8b5cd729b515 Task-Id: f1f9f55c-e450-4e7c-ab54-825b699fd955
1 parent d68cfc4 commit 6a3dba3

15 files changed

Lines changed: 637 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
.idea
3+
public
4+
dist
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
require: 'tsx'
2+
timeout: '60000'
3+
reporter: 'min'
4+
reporter-option:
5+
- 'cdn=true'
6+
- 'json=false'
7+
exit: true
8+
spec: ['test/unit/**/*.ts']
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "@bitgo/abstract-wasm-coin",
3+
"version": "1.0.0",
4+
"description": "BitGo SDK pluggable WASM coin adapter interface",
5+
"main": "./dist/src/index.js",
6+
"types": "./dist/src/index.d.ts",
7+
"scripts": {
8+
"build": "npm run prepare",
9+
"build-ts": "yarn tsc --build --incremental --verbose .",
10+
"fmt": "prettier --write .",
11+
"check-fmt": "prettier --check '**/*.{ts,js,json}'",
12+
"clean": "rm -rf ./dist",
13+
"lint": "eslint --quiet .",
14+
"prepare": "npm run build-ts",
15+
"test": "npm run coverage",
16+
"coverage": "nyc -- npm run unit-test",
17+
"unit-test": "mocha"
18+
},
19+
"author": "BitGo SDK Team <sdkteam@bitgo.com>",
20+
"license": "MIT",
21+
"engines": {
22+
"node": ">=20"
23+
},
24+
"repository": {
25+
"type": "git",
26+
"url": "https://github.com/BitGo/BitGoJS.git",
27+
"directory": "modules/abstract-wasm-coin"
28+
},
29+
"lint-staged": {
30+
"*.{js,ts}": [
31+
"yarn prettier --write",
32+
"yarn eslint --fix"
33+
]
34+
},
35+
"publishConfig": {
36+
"access": "public"
37+
},
38+
"nyc": {
39+
"extension": [
40+
".ts"
41+
]
42+
},
43+
"dependencies": {
44+
"@bitgo/wasm-dot": "^1.7.0",
45+
"@bitgo/wasm-solana": "^2.6.0",
46+
"@bitgo/wasm-ton": "^1.1.1"
47+
},
48+
"devDependencies": {
49+
"@bitgo/sdk-test": "^9.1.44"
50+
},
51+
"gitHead": "18e460ddf02de2dbf13c2aa243478188fb539f0c",
52+
"files": [
53+
"dist"
54+
]
55+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type {
2+
WasmCoinAdapter,
3+
WasmCoinCapability,
4+
WasmDerivedAddress,
5+
WasmParsedTransaction,
6+
WasmBuiltTransaction,
7+
} from './types';
8+
9+
/**
10+
* Abstract wrapper that dispatches calls to a registered WasmCoinAdapter.
11+
*
12+
* Callers use this class instead of talking to adapters directly so that:
13+
* - capability checks are centralized and consistent
14+
* - the registry can return a uniform type regardless of which adapter is loaded
15+
* - future cross-cutting concerns (telemetry, error wrapping) have one place to live
16+
*/
17+
export class AbstractWasmCoin {
18+
constructor(private readonly adapter: WasmCoinAdapter) {}
19+
20+
get coin(): string {
21+
return this.adapter.coin;
22+
}
23+
24+
hasCapability(cap: WasmCoinCapability): boolean {
25+
return this.adapter.capabilities.has(cap);
26+
}
27+
28+
deriveAddress(params: unknown): Promise<WasmDerivedAddress> {
29+
if (!this.adapter.deriveAddress) {
30+
throw new Error(`${this.adapter.coin}: deriveAddress is not supported`);
31+
}
32+
return Promise.resolve(this.adapter.deriveAddress(params as never));
33+
}
34+
35+
parseTransaction(params: unknown): Promise<WasmParsedTransaction> {
36+
if (!this.adapter.parseTransaction) {
37+
throw new Error(`${this.adapter.coin}: parseTransaction is not supported`);
38+
}
39+
return Promise.resolve(this.adapter.parseTransaction(params as never));
40+
}
41+
42+
buildTransaction(params: unknown): Promise<WasmBuiltTransaction> {
43+
if (!this.adapter.buildTransaction) {
44+
throw new Error(`${this.adapter.coin}: buildTransaction is not supported`);
45+
}
46+
return Promise.resolve(this.adapter.buildTransaction(params as never));
47+
}
48+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Polkadot WASM adapter for abstract-wasm-coin.
3+
*
4+
* Wraps @bitgo/wasm-dot and exposes:
5+
* - parseTransaction — decodes a hex-encoded extrinsic into normalized outputs/inputs
6+
*
7+
* Explain logic (type derivation, staking destination sentinel, batch pattern
8+
* detection, proxy deposit cost) stays in sdk-coin-dot/wasmParser.ts.
9+
* This adapter is a thin translation layer only.
10+
*/
11+
import { DotTransaction, parseTransaction } from '@bitgo/wasm-dot';
12+
import type { Material } from '@bitgo/wasm-dot';
13+
import type { WasmCoinAdapter, WasmCoinCapability, WasmParsedTransaction } from '../types';
14+
15+
// =============================================================================
16+
// Adapter-specific param/result types
17+
// =============================================================================
18+
19+
export interface DotParseParams {
20+
/** Hex-encoded extrinsic bytes. */
21+
txHex: string;
22+
/** Runtime material (genesisHash, specVersion, metadata, etc.) required for parsing. */
23+
material: Material;
24+
senderAddress?: string;
25+
}
26+
27+
export interface DotParsedTransaction extends WasmParsedTransaction {
28+
raw: ReturnType<typeof parseTransaction>;
29+
}
30+
31+
// =============================================================================
32+
// Adapter implementation
33+
// =============================================================================
34+
35+
export const dotAdapter: WasmCoinAdapter<never, never, DotParseParams, DotParsedTransaction> = {
36+
coin: 'dot',
37+
capabilities: new Set<WasmCoinCapability>(['parseTransaction']),
38+
39+
parseTransaction(params: DotParseParams): DotParsedTransaction {
40+
const tx = DotTransaction.fromHex(params.txHex, params.material);
41+
const context = {
42+
material: params.material,
43+
sender: params.senderAddress,
44+
};
45+
const parsed = parseTransaction(tx, context);
46+
47+
// Extract the most basic output: dest + value from a transfer, if present.
48+
// Explain policy (staking, batch, proxy) stays in sdk-coin-dot.
49+
const outputs: Array<{ address: string; amount: string }> = [];
50+
const inputs: Array<{ address: string; amount: string }> = [];
51+
52+
const args = (parsed.method.args ?? {}) as Record<string, unknown>;
53+
const key = `${parsed.method.pallet}.${parsed.method.name}`;
54+
if (
55+
key === 'balances.transfer' ||
56+
key === 'balances.transferKeepAlive' ||
57+
key === 'balances.transferAllowDeath'
58+
) {
59+
const dest = String(args.dest ?? '');
60+
const value = String(args.value ?? '0');
61+
if (dest) {
62+
outputs.push({ address: dest, amount: value });
63+
if (params.senderAddress) {
64+
inputs.push({ address: params.senderAddress, amount: value });
65+
}
66+
}
67+
}
68+
69+
return {
70+
id: parsed.id ?? undefined,
71+
inputs,
72+
outputs,
73+
raw: parsed,
74+
};
75+
},
76+
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Solana WASM adapter for abstract-wasm-coin.
3+
*
4+
* Wraps @bitgo/wasm-solana and exposes:
5+
* - parseTransaction — decodes a base64-encoded transaction into normalized outputs/inputs
6+
* - buildTransaction — not yet implemented (wasm-solana building is handled inside sdk-coin-sol)
7+
*
8+
* Explain logic, fee policy, token naming, and product semantics deliberately
9+
* stay in sdk-coin-sol. This adapter is a thin translation layer only.
10+
*/
11+
import { Transaction, parseTransaction } from '@bitgo/wasm-solana';
12+
import type { WasmCoinAdapter, WasmCoinCapability, WasmParsedTransaction } from '../types';
13+
14+
// =============================================================================
15+
// Adapter-specific param/result types
16+
// =============================================================================
17+
18+
export interface SolanaParseParams {
19+
/** Base64-encoded transaction bytes. */
20+
txBase64: string;
21+
}
22+
23+
export interface SolanaParsedTransaction extends WasmParsedTransaction {
24+
raw: ReturnType<typeof parseTransaction>;
25+
}
26+
27+
// =============================================================================
28+
// Adapter implementation
29+
// =============================================================================
30+
31+
export const solanaAdapter: WasmCoinAdapter<never, never, SolanaParseParams, SolanaParsedTransaction> = {
32+
coin: 'sol',
33+
capabilities: new Set<WasmCoinCapability>(['parseTransaction']),
34+
35+
parseTransaction(params: SolanaParseParams): SolanaParsedTransaction {
36+
const txBytes = Buffer.from(params.txBase64, 'base64');
37+
const tx = Transaction.fromBytes(txBytes);
38+
const parsed = parseTransaction(tx);
39+
40+
// Extract normalized outputs (Transfer instructions only — explain logic stays in sdk-coin-sol)
41+
const outputs: Array<{ address: string; amount: string }> = [];
42+
const inputs: Array<{ address: string; amount: string }> = [];
43+
44+
for (const instr of parsed.instructionsData) {
45+
switch (instr.type) {
46+
case 'Transfer':
47+
outputs.push({ address: instr.toAddress, amount: String(instr.amount) });
48+
inputs.push({ address: instr.fromAddress, amount: String(instr.amount) });
49+
break;
50+
case 'TokenTransfer':
51+
outputs.push({ address: instr.toAddress, amount: String(instr.amount) });
52+
inputs.push({ address: instr.fromAddress, amount: String(instr.amount) });
53+
break;
54+
}
55+
}
56+
57+
// Transaction ID: first signature, undefined when unsigned (all-zeros base58)
58+
const ALL_ZEROS = '1111111111111111111111111111111111111111111111111111111111111111';
59+
const sig = parsed.signatures[0];
60+
const id = sig && sig !== ALL_ZEROS ? sig : undefined;
61+
62+
return { id, inputs, outputs, raw: parsed };
63+
},
64+
};
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* TON WASM adapter for abstract-wasm-coin.
3+
*
4+
* Wraps @bitgo/wasm-ton and exposes:
5+
* - deriveAddress — normalizes a raw TON address to bounceable EQ... form
6+
* - parseTransaction — decodes a base64-encoded TON transaction into normalized outputs/inputs
7+
*
8+
* Explain logic, verification policy, and product semantics stay in sdk-coin-ton.
9+
* This adapter is a thin translation layer only.
10+
*/
11+
import { Transaction as WasmTonTransaction, parseTransaction, decode as wasmDecode, encode as wasmEncode } from '@bitgo/wasm-ton';
12+
import type { WasmCoinAdapter, WasmCoinCapability, WasmDerivedAddress, WasmParsedTransaction } from '../types';
13+
14+
// =============================================================================
15+
// Adapter-specific param/result types
16+
// =============================================================================
17+
18+
export interface TonAddressParams {
19+
/** Raw address string (any TON format: EQ..., UQ..., workchain:hash hex). */
20+
address: string;
21+
/** When true (default), return the bounceable EQ... address. */
22+
bounceable?: boolean;
23+
}
24+
25+
export interface TonDerivedAddress extends WasmDerivedAddress {
26+
raw: { workchainId: number; addressHash: Uint8Array };
27+
}
28+
29+
export interface TonParseParams {
30+
/** Base64-encoded TON transaction bytes. */
31+
txBase64: string;
32+
/** When true (default), return bounceable EQ... destination addresses. */
33+
toAddressBounceable?: boolean;
34+
}
35+
36+
export interface TonParsedTransaction extends WasmParsedTransaction {
37+
raw: ReturnType<typeof parseTransaction>;
38+
}
39+
40+
// =============================================================================
41+
// Adapter implementation
42+
// =============================================================================
43+
44+
export const tonAdapter: WasmCoinAdapter<TonAddressParams, TonDerivedAddress, TonParseParams, TonParsedTransaction> = {
45+
coin: 'ton',
46+
capabilities: new Set<WasmCoinCapability>(['deriveAddress', 'parseTransaction']),
47+
48+
deriveAddress(params: TonAddressParams): TonDerivedAddress {
49+
const bounceable = params.bounceable !== false;
50+
const decoded = wasmDecode(params.address);
51+
const address = wasmEncode(decoded.workchainId, decoded.addressHash, bounceable);
52+
return {
53+
address,
54+
raw: { workchainId: decoded.workchainId, addressHash: decoded.addressHash },
55+
};
56+
},
57+
58+
parseTransaction(params: TonParseParams): TonParsedTransaction {
59+
const toAddressBounceable = params.toAddressBounceable !== false;
60+
const tx = WasmTonTransaction.fromBytes(Buffer.from(params.txBase64, 'base64'));
61+
const parsed = parseTransaction(tx);
62+
63+
const outputs: Array<{ address: string; amount: string }> = [];
64+
const inputs: Array<{ address: string; amount: string }> = [];
65+
66+
for (const action of parsed.sendActions) {
67+
const address = toAddressBounceable ? action.destinationBounceable : action.destination;
68+
const amount = String(action.jettonTransfer ? action.jettonTransfer.amount : action.amount);
69+
outputs.push({ address, amount });
70+
}
71+
72+
// TON is account-model: inputs mirror total output from the sender.
73+
const totalAmount = outputs.reduce((sum, o) => sum + BigInt(o.amount), 0n);
74+
if (outputs.length > 0) {
75+
// Sender address not available from the parsed payload at this layer;
76+
// callers that need it should use the raw field.
77+
inputs.push({ address: '', amount: String(totalAmount) });
78+
}
79+
80+
return { id: tx.id, inputs, outputs, raw: parsed };
81+
},
82+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Demo: Polkadot WASM adapter usage through the abstract-wasm-coin registry.
3+
*/
4+
import type { Material } from '@bitgo/wasm-dot';
5+
import { defaultRegistry } from '../registry';
6+
import { dotAdapter } from '../adapters/dot';
7+
8+
defaultRegistry.register(dotAdapter);
9+
10+
export async function parseDotTransaction(txHex: string, material: Material, senderAddress?: string) {
11+
const wasm = defaultRegistry.get('dot');
12+
return wasm.parseTransaction({ txHex, material, senderAddress });
13+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Demo: Solana WASM adapter usage through the abstract-wasm-coin registry.
3+
*
4+
* Shows that BitGoJS code can call the same entry point regardless of chain:
5+
* registry.get('sol').parseTransaction(...)
6+
*/
7+
import { defaultRegistry } from '../registry';
8+
import { solanaAdapter } from '../adapters/solana';
9+
10+
// Register the adapter once at startup
11+
defaultRegistry.register(solanaAdapter);
12+
13+
export async function parseSolanaTransaction(txBase64: string) {
14+
const wasm = defaultRegistry.get('sol');
15+
return wasm.parseTransaction({ txBase64 });
16+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Demo: TON WASM adapter usage through the abstract-wasm-coin registry.
3+
*
4+
* Shows both primitives: address normalization and transaction parsing,
5+
* called through the same uniform interface as Solana and Polkadot.
6+
*/
7+
import { defaultRegistry } from '../registry';
8+
import { tonAdapter } from '../adapters/ton';
9+
10+
defaultRegistry.register(tonAdapter);
11+
12+
export async function normalizeTonAddress(address: string, bounceable = true) {
13+
const wasm = defaultRegistry.get('ton');
14+
return wasm.deriveAddress({ address, bounceable });
15+
}
16+
17+
export async function parseTonTransaction(txBase64: string, toAddressBounceable = true) {
18+
const wasm = defaultRegistry.get('ton');
19+
return wasm.parseTransaction({ txBase64, toAddressBounceable });
20+
}

0 commit comments

Comments
 (0)