Skip to content

Commit 19ee5a9

Browse files
authored
Merge pull request #202 from BitGo/BTC-3124.dot-wasm-address-bindings
feat: expose address encode/decode/validate to WASM/JS
2 parents 7a0d3be + 318819a commit 19ee5a9

5 files changed

Lines changed: 180 additions & 0 deletions

File tree

packages/wasm-dot/js/address.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { AddressNamespace } from "./wasm/wasm_dot.js";
2+
import { AddressFormat } from "./types.js";
3+
4+
/**
5+
* Result of decoding an SS58 address
6+
*/
7+
export interface DecodedAddress {
8+
publicKey: Uint8Array;
9+
prefix: number;
10+
}
11+
12+
/**
13+
* Encode a public key to SS58 address format.
14+
*
15+
* @param publicKey - 32-byte Ed25519 public key
16+
* @param format - Address format (Polkadot, Kusama, or Substrate)
17+
* @returns SS58-encoded address string
18+
*/
19+
export function encodeSs58(publicKey: Uint8Array, format: AddressFormat): string {
20+
return AddressNamespace.encodeSs58(publicKey, format);
21+
}
22+
23+
/**
24+
* Decode an SS58 address to its public key and network prefix.
25+
*
26+
* @param address - SS58-encoded address string
27+
* @returns The decoded public key and network prefix
28+
*/
29+
export function decodeSs58(address: string): DecodedAddress {
30+
return AddressNamespace.decodeSs58(address) as DecodedAddress;
31+
}
32+
33+
/**
34+
* Validate an SS58 address.
35+
*
36+
* @param address - SS58-encoded address string
37+
* @param format - Optional expected address format to check against
38+
* @returns true if the address is valid (and matches format if provided)
39+
*/
40+
export function validateAddress(address: string, format?: AddressFormat): boolean {
41+
return AddressNamespace.validateAddress(address, format);
42+
}

packages/wasm-dot/js/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
WasmTransaction,
1212
ParserNamespace,
1313
BuilderNamespace,
14+
AddressNamespace,
1415
MaterialJs,
1516
ValidityJs,
1617
ParseContextJs,
@@ -21,6 +22,7 @@ export {
2122
WasmTransaction,
2223
ParserNamespace,
2324
BuilderNamespace,
25+
AddressNamespace,
2426
MaterialJs,
2527
ValidityJs,
2628
ParseContextJs,
@@ -31,3 +33,4 @@ export * from "./types.js";
3133
export * from "./transaction.js";
3234
export * from "./parser.js";
3335
export * from "./builder.js";
36+
export * from "./address.js";
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//! WASM bindings for address operations
2+
//!
3+
//! AddressNamespace provides static methods for SS58 address encoding,
4+
//! decoding, and validation.
5+
6+
use crate::address;
7+
use wasm_bindgen::prelude::*;
8+
9+
/// Namespace for address operations
10+
#[wasm_bindgen]
11+
pub struct AddressNamespace;
12+
13+
#[wasm_bindgen]
14+
impl AddressNamespace {
15+
/// Encode a public key to SS58 address format.
16+
///
17+
/// @param publicKey - 32-byte Ed25519 public key
18+
/// @param prefix - Network prefix (0 = Polkadot, 2 = Kusama, 42 = Substrate)
19+
/// @returns SS58-encoded address string
20+
#[wasm_bindgen(js_name = encodeSs58)]
21+
pub fn encode_ss58(public_key: &[u8], prefix: u16) -> Result<String, JsValue> {
22+
address::encode_ss58(public_key, prefix).map_err(|e| JsValue::from_str(&e.to_string()))
23+
}
24+
25+
/// Decode an SS58 address to its public key and network prefix.
26+
///
27+
/// Returns a JS object with `publicKey` (Uint8Array) and `prefix` (number).
28+
///
29+
/// @param address - SS58-encoded address string
30+
/// @returns { publicKey: Uint8Array, prefix: number }
31+
#[wasm_bindgen(js_name = decodeSs58)]
32+
pub fn decode_ss58(addr: &str) -> Result<JsValue, JsValue> {
33+
let (pubkey, prefix) =
34+
address::decode_ss58(addr).map_err(|e| JsValue::from_str(&e.to_string()))?;
35+
36+
let obj = js_sys::Object::new();
37+
let pubkey_array = js_sys::Uint8Array::from(pubkey.as_slice());
38+
js_sys::Reflect::set(&obj, &"publicKey".into(), &pubkey_array)?;
39+
js_sys::Reflect::set(&obj, &"prefix".into(), &JsValue::from(prefix))?;
40+
Ok(obj.into())
41+
}
42+
43+
/// Validate an SS58 address.
44+
///
45+
/// @param address - SS58-encoded address string
46+
/// @param prefix - Optional expected network prefix to check against
47+
/// @returns true if the address is valid (and matches prefix if provided)
48+
#[wasm_bindgen(js_name = validateAddress)]
49+
pub fn validate_address(addr: &str, prefix: Option<u16>) -> bool {
50+
address::validate_address(addr, prefix)
51+
}
52+
}

packages/wasm-dot/src/wasm/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
//! This module contains thin wrappers with #[wasm_bindgen] that delegate
44
//! to the core Rust implementations.
55
6+
pub mod address;
67
pub mod builder;
78
pub mod parser;
89
pub mod transaction;
910
pub mod try_into_js_value;
1011

1112
// Re-export WASM types
13+
pub use address::AddressNamespace;
1214
pub use builder::BuilderNamespace;
1315
pub use parser::ParserNamespace;
1416
pub use transaction::{MaterialJs, ParseContextJs, ValidityJs, WasmTransaction};

packages/wasm-dot/test/address.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import * as assert from "assert";
2+
import { encodeSs58, decodeSs58, validateAddress, AddressFormat } from "../js/index.js";
3+
4+
describe("address", () => {
5+
// Known test vector: public key → SS58 addresses
6+
const PUBLIC_KEY = new Uint8Array(
7+
Buffer.from("61b18c6dc02ddcabdeac56cb4f21a971cc41cc97640f6f85b073480008c53a0d", "hex"),
8+
);
9+
const SUBSTRATE_ADDRESS = "5EGoFA95omzemRssELLDjVenNZ68aXyUeqtKQScXSEBvVJkr";
10+
11+
describe("encodeSs58", () => {
12+
it("should encode public key to Substrate address (prefix 42)", () => {
13+
const address = encodeSs58(PUBLIC_KEY, AddressFormat.Substrate);
14+
assert.strictEqual(address, SUBSTRATE_ADDRESS);
15+
});
16+
17+
it("should encode public key to Polkadot address (prefix 0)", () => {
18+
const address = encodeSs58(PUBLIC_KEY, AddressFormat.Polkadot);
19+
assert.ok(address.startsWith("1"), "Polkadot addresses start with '1'");
20+
});
21+
22+
it("should encode public key to Kusama address (prefix 2)", () => {
23+
const address = encodeSs58(PUBLIC_KEY, AddressFormat.Kusama);
24+
assert.ok(address.length > 0);
25+
});
26+
27+
it("should throw for invalid public key length", () => {
28+
const shortKey = new Uint8Array(16);
29+
assert.throws(() => encodeSs58(shortKey, AddressFormat.Substrate));
30+
});
31+
});
32+
33+
describe("decodeSs58", () => {
34+
it("should decode Substrate address to public key and prefix", () => {
35+
const decoded = decodeSs58(SUBSTRATE_ADDRESS);
36+
assert.strictEqual(decoded.prefix, 42);
37+
assert.deepStrictEqual(new Uint8Array(decoded.publicKey), PUBLIC_KEY);
38+
});
39+
40+
it("should roundtrip encode → decode for all formats", () => {
41+
for (const format of [
42+
AddressFormat.Polkadot,
43+
AddressFormat.Kusama,
44+
AddressFormat.Substrate,
45+
]) {
46+
const address = encodeSs58(PUBLIC_KEY, format);
47+
const decoded = decodeSs58(address);
48+
assert.strictEqual(decoded.prefix, format);
49+
assert.deepStrictEqual(new Uint8Array(decoded.publicKey), PUBLIC_KEY);
50+
}
51+
});
52+
53+
it("should throw for invalid address", () => {
54+
assert.throws(() => decodeSs58("invalid"));
55+
});
56+
});
57+
58+
describe("validateAddress", () => {
59+
it("should return true for valid address without format check", () => {
60+
assert.strictEqual(validateAddress(SUBSTRATE_ADDRESS), true);
61+
});
62+
63+
it("should return true for valid address with correct format", () => {
64+
assert.strictEqual(validateAddress(SUBSTRATE_ADDRESS, AddressFormat.Substrate), true);
65+
});
66+
67+
it("should return false for valid address with wrong format", () => {
68+
assert.strictEqual(validateAddress(SUBSTRATE_ADDRESS, AddressFormat.Polkadot), false);
69+
});
70+
71+
it("should return false for invalid address", () => {
72+
assert.strictEqual(validateAddress("invalid"), false);
73+
});
74+
75+
it("should validate Polkadot mainnet addresses", () => {
76+
const polkadotAddress = encodeSs58(PUBLIC_KEY, AddressFormat.Polkadot);
77+
assert.strictEqual(validateAddress(polkadotAddress, AddressFormat.Polkadot), true);
78+
assert.strictEqual(validateAddress(polkadotAddress, AddressFormat.Substrate), false);
79+
});
80+
});
81+
});

0 commit comments

Comments
 (0)