Binding Patterns
Dash Platform's core logic is written in Rust. But many developers build applications
in JavaScript -- browser-based wallets, Node.js services, React Native apps. The
wasm-dpp package bridges these worlds by compiling Rust types to WebAssembly and
exposing them as JavaScript classes.
This chapter covers the patterns used to create these bindings: the wrapper struct
pattern, naming conventions, buffer handling at the boundary, getter/setter patterns,
and the Inner trait.
The Problem
Rust and JavaScript have fundamentally different type systems. Rust has ownership,
lifetimes, and zero-cost abstractions. JavaScript has garbage collection, prototype
chains, and dynamic types. wasm-bindgen bridges the gap, but it requires careful
manual work to make the resulting JavaScript API feel natural.
The core challenge: you have a Rust type like Identity with methods like id(),
balance(), and public_keys(). You need to expose it to JavaScript as a class
with methods like getId(), getBalance(), and getPublicKeys() -- following
JavaScript naming conventions while maintaining Rust's safety guarantees.
The Wrapper Struct Pattern
Every Rust type exposed to JavaScript gets a wrapper struct. Here is IdentityWasm
from packages/wasm-dpp/src/identity/identity.rs:
#![allow(unused)] fn main() { #[wasm_bindgen(js_name=Identity)] #[derive(Clone)] pub struct IdentityWasm { inner: Identity, metadata: Option<Metadata>, } }
The pattern has three parts:
#[wasm_bindgen(js_name=Identity)]-- tellswasm_bindgento expose this struct asIdentityin JavaScript, notIdentityWasm.inner: Identity-- the real Rust type, hidden from JavaScript.- Additional fields -- any extra state needed at the WASM boundary (like
metadatahere, which is managed separately in JS).
Why a Wrapper?
You cannot put #[wasm_bindgen] directly on Identity for several reasons:
Identityis defined inrs-dpp, a different crate. You cannot add attributes to types in other crates.Identitymay contain types thatwasm_bindgencannot handle (nested enums, complex generics, trait objects).- The JavaScript API should have camelCase methods (
getId), not Rust-style snake_case (id). - Some conversions (like turning
Identifierinto aBuffer) only make sense at the WASM boundary.
From/Into Conversions
Every wrapper implements bidirectional conversion:
#![allow(unused)] fn main() { impl From<IdentityWasm> for Identity { fn from(identity: IdentityWasm) -> Self { identity.inner } } impl From<Identity> for IdentityWasm { fn from(identity: Identity) -> Self { Self { inner: identity, metadata: None, } } } }
This lets internal Rust code work with the real Identity type while the WASM
boundary works with IdentityWasm. The conversion is zero-cost -- it just moves
the inner value.
JavaScript Method Naming
Methods use #[wasm_bindgen(js_name=...)] to follow JavaScript conventions:
#![allow(unused)] fn main() { #[wasm_bindgen(js_class=Identity)] impl IdentityWasm { #[wasm_bindgen(js_name=getId)] pub fn get_id(&self) -> IdentifierWrapper { self.inner.id().into() } #[wasm_bindgen(js_name=setId)] pub fn set_id(&mut self, id: IdentifierWrapper) { self.inner.set_id(id.into()); } #[wasm_bindgen(js_name=getBalance)] pub fn get_balance(&self) -> u64 { self.inner.balance() } #[wasm_bindgen(js_name=setBalance)] pub fn set_balance(&mut self, balance: u64) { self.inner.set_balance(balance); } } }
Note the #[wasm_bindgen(js_class=Identity)] on the impl block -- this associates
the methods with the Identity JavaScript class (the js_name from the struct).
In JavaScript, these become:
const identity = new Identity(platformVersion);
const id = identity.getId();
identity.setBalance(1000n);
Getter Properties
For simple values, you can use JavaScript getter syntax:
#![allow(unused)] fn main() { #[wasm_bindgen(getter)] pub fn balance(&self) -> u64 { self.inner.balance() } }
In JavaScript, this becomes a property access:
const bal = identity.balance; // no parentheses
Platform uses both patterns -- getBalance() method and balance getter -- for
the same value. This provides flexibility: the getter is concise for reading, the
method is consistent for tools that expect a getter/setter pair.
Constructors
The #[wasm_bindgen(constructor)] attribute creates a JavaScript constructor:
#![allow(unused)] fn main() { #[wasm_bindgen(constructor)] pub fn new(platform_version: u32) -> Result<IdentityWasm, JsValue> { let platform_version = &PlatformVersion::get(platform_version) .map_err(|e| JsValue::from(e.to_string()))?; Identity::default_versioned(platform_version) .map(Into::into) .map_err(from_dpp_err) } }
Notice the error handling: Rust Result becomes a JavaScript throw. The
from_dpp_err function converts Rust ProtocolError into JavaScript error objects.
Buffer Handling at the Boundary
Binary data crosses the WASM boundary as Buffer (a custom type that wraps
Uint8Array):
#![allow(unused)] fn main() { #[wasm_bindgen(js_name=toBuffer)] pub fn to_buffer(&self) -> Result<Buffer, JsValue> { let bytes = PlatformSerializable::serialize_to_bytes( &self.inner.clone() ).with_js_error()?; Ok(Buffer::from_bytes(&bytes)) } #[wasm_bindgen(js_name=fromBuffer)] pub fn from_buffer(buffer: Vec<u8>) -> Result<IdentityWasm, JsValue> { let identity: Identity = PlatformDeserializable::deserialize_from_bytes(buffer.as_slice()) .with_js_error()?; Ok(identity.into()) } }
The toBuffer/fromBuffer pair is the standard serialization interface. Every WASM
type that needs to be stored or transmitted implements this pair.
Handling Complex Types: Arrays and Objects
JavaScript arrays and objects require special handling. For collections of public keys:
#![allow(unused)] fn main() { #[wasm_bindgen(js_name=getPublicKeys)] pub fn get_public_keys(&self) -> Vec<JsValue> { self.inner .public_keys() .values() .cloned() .map(IdentityPublicKeyWasm::from) // Rust -> Wrapper .map(JsValue::from) // Wrapper -> JsValue .collect() } #[wasm_bindgen(js_name=setPublicKeys)] pub fn set_public_keys(&mut self, public_keys: js_sys::Array) -> Result<usize, JsValue> { if public_keys.length() == 0 { return Err("Must use array of PublicKeys".into()); } let public_keys = public_keys .iter() .map(|key| { key.to_wasm::<IdentityPublicKeyWasm>("IdentityPublicKey") .map(|key| { let key = IdentityPublicKey::from(key.to_owned()); (key.id(), key) }) }) .collect::<Result<_, _>>()?; self.inner.set_public_keys(public_keys); Ok(self.inner.public_keys().len()) } }
The to_wasm::<T>("TypeName") helper extracts a Rust wrapper from a JsValue,
validating that it is the correct type.
JSON Serialization
Most WASM types provide toJSON and toObject methods:
#![allow(unused)] fn main() { #[wasm_bindgen(js_name=toJSON)] pub fn to_json(&self) -> Result<JsValue, JsValue> { let mut value = self.inner.to_object().with_js_error()?; // Convert identifiers to Base58 strings for readability value.replace_at_paths( dpp::identity::IDENTIFIER_FIELDS_RAW_OBJECT, ReplacementType::TextBase58, ).map_err(|e| e.to_string())?; // Convert binary key data to Base64 let public_keys = value .get_array_mut_ref(dpp::identity::property_names::PUBLIC_KEYS) .map_err(|e| e.to_string())?; for key in public_keys.iter_mut() { key.replace_at_paths( dpp::identity::identity_public_key::BINARY_DATA_FIELDS, ReplacementType::TextBase64, ).map_err(|e| e.to_string())?; } let json = value.try_into_validating_json() .map_err(|e| e.to_string())? .to_string(); js_sys::JSON::parse(&json) } }
The toJSON method applies human-readable encoding (Base58 for identifiers, Base64
for binary data) before converting to a JavaScript object. This is the format used
in APIs and debugging tools.
The Inner Trait
To standardize wrapper access, Platform defines an Inner trait:
#![allow(unused)] fn main() { impl Inner for IdentityWasm { type InnerItem = Identity; fn into_inner(self) -> Self::InnerItem { self.inner } fn inner(&self) -> &Self::InnerItem { &self.inner } fn inner_mut(&mut self) -> &mut Self::InnerItem { &mut self.inner } } }
This trait provides a consistent way for other Rust code in the WASM layer to access the unwrapped type without knowing the wrapper's internal structure.
Rules
Do:
- Follow the wrapper struct pattern:
TypeWasm { inner: Type }. - Use
js_nameon the struct andjs_classon theimplblock. - Implement
From<Type> for TypeWasmandFrom<TypeWasm> for Type. - Use
Buffer::from_bytesfor binary data crossing the boundary. - Implement
toBuffer/fromBufferfor any type that needs serialization. - Use
from_dpp_errorwith_js_error()for error conversion. - Implement the
Innertrait for consistent wrapper access.
Don't:
- Expose Rust types directly to WASM -- always wrap them.
- Use Rust naming conventions (
get_id) in the JavaScript API -- usejs_name=getId. - Return
Result<T, ProtocolError>from WASM methods -- convert toResult<T, JsValue>. - Forget to handle empty arrays and invalid inputs with clear error messages.
- Expose internal fields that do not make sense in JavaScript (like
ArcorMutex).