Skip to content

Library for client side construction and signing of xmw/wow txs

Notifications You must be signed in to change notification settings

Such-Software/smirk-wasm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

smirk-wasm

Monero/Wownero transaction construction for browser extensions, compiled to WebAssembly.

Overview

This crate provides client-side cryptographic operations for Monero and Wownero wallets running in browser extensions. The spend key never leaves the client - the backend only provides blockchain data.

Features

  • Address validation - Parse and validate Monero/Wownero addresses
  • Key image computation - Compute key images to verify spent outputs (client-side balance verification)
  • Transaction parsing - Decode and inspect transactions
  • Fee estimation - Estimate transaction fees
  • Transaction signing - Construct and sign transactions locally (XMR and WOW)

Wownero Support

Wownero transactions are fully supported with the following differences from Monero:

Property Monero (XMR) Wownero (WOW)
RCT Type 6 (ClsagBulletproofPlus) 8 (BulletproofPlus)
Ring Size 16 (15 decoys + 1 real) 22 (21 decoys + 1 real)
Commitment Format Full commitment C/8 (scaled by INV_EIGHT)
Network Prefix 4 (mainnet) Wo (mainnet)

The signing implementation handles these differences automatically based on the network parameter.

Architecture

┌─────────────────────┐     ┌─────────────────────┐
│  Browser Extension  │     │      Backend        │
│                     │     │                     │
│  ┌───────────────┐  │     │  ┌───────────────┐  │
│  │  smirk-wasm   │  │     │  │     LWS       │  │
│  │  (~165KB)     │  │     │  │  (Monero)     │  │
│  │               │  │     │  │               │  │
│  │ - Keys        │◄─┼─────┼──┤ - Outputs     │  │
│  │ - Signing     │  │     │  │ - Decoys      │  │
│  │ - Addresses   │──┼─────┼──► - Broadcast   │  │
│  └───────────────┘  │     │  └───────────────┘  │
│                     │     │                     │
│  Spend key stays    │     │  No access to       │
│  here               │     │  spend key          │
└─────────────────────┘     └─────────────────────┘

Building

Prerequisites

  • Rust (stable) with wasm32-unknown-unknown target
  • wasm-bindgen-cli
# Add WASM target
rustup target add wasm32-unknown-unknown

# Install wasm-bindgen CLI
cargo install wasm-bindgen-cli

Build

# Quick build (uses build.sh)
./build.sh

# Or manually:
cargo build --target wasm32-unknown-unknown --release
wasm-bindgen --target web --out-dir pkg \
  target/wasm32-unknown-unknown/release/smirk_wasm.wasm

Output

After building:

  • pkg/smirk_wasm.js - JavaScript module
  • pkg/smirk_wasm.d.ts - TypeScript definitions
  • pkg/smirk_wasm_bg.wasm - WebAssembly binary (~380KB)

Testing

Rust unit tests

cargo test

Browser testing

# Build first
./build.sh

# Serve with any static server
python3 -m http.server 8080

# Open http://localhost:8080/test.html

Usage

import init, {
  test,
  version,
  validate_address,
  estimate_fee,
  sign_transaction,
  compute_key_image
} from './pkg/smirk_wasm.js';

async function main() {
  // Initialize WASM
  await init();

  // Verify loaded
  console.log(test()); // "smirk-wasm ready"

  // Validate address
  const result = JSON.parse(validate_address(
    '888tNkZrPN6JsEgekjMnABU4TBzc...'
  ));
  if (result.success) {
    console.log(result.data.network); // "mainnet"
  }

  // Estimate fee (2 inputs, 2 outputs)
  const fee = JSON.parse(estimate_fee(2, 2, 20n, 10000n));
  console.log(fee.data); // fee in atomic units

  // Sign transaction
  const txResult = JSON.parse(sign_transaction(JSON.stringify({
    inputs: [/* from get_unspent_outs + get_random_outs */],
    destinations: [{ address: '...', amount: 1000000 }],
    change_address: '...',
    fee_per_byte: 20,
    fee_mask: 10000,
    view_key: '...', // hex
    spend_key: '...', // hex
    network: 'mainnet'
  })));
  if (txResult.success) {
    console.log(txResult.data.tx_hex);  // signed tx ready for broadcast
    console.log(txResult.data.tx_hash); // transaction hash
    console.log(txResult.data.fee);     // actual fee
  }
}

API

All functions return JSON strings:

interface Result<T> {
  success: boolean;
  data?: T;
  error?: string;
}

Core Functions

Function Description
test() Returns "smirk-wasm ready" if loaded
version() Returns crate version

Address Functions

validate_address(address: string) -> string

Validates a Monero address and returns its components.

// Returns:
{
  valid: boolean;
  network: "mainnet" | "testnet" | "stagenet";
  is_subaddress: boolean;
  has_payment_id: boolean;
  spend_key: string;  // hex
  view_key: string;   // hex
}

Key Functions

derive_key_image(output_key, spend_key, key_offset) -> string

Computes the key image for an output. All arguments are 32-byte hex strings.

Returns the key image as a hex string.

Transaction Functions

parse_tx(hex_data: string) -> string

Parses a transaction from hex.

// Returns:
{
  inputs: number;
  outputs: number;
  version: number;
}

estimate_fee(inputs, outputs, fee_per_byte, fee_mask) -> string

Estimates transaction fee.

  • inputs - Number of inputs (u32)
  • outputs - Number of outputs including change (u32)
  • fee_per_byte - Fee per byte from LWS (u64/bigint)
  • fee_mask - Fee rounding mask from LWS (u64/bigint)

Returns estimated fee in atomic units.

sign_transaction(params_json: string) -> string

Builds and signs a transaction.

Input format:

{
  inputs: [{
    output: {
      amount: number,      // atomic units
      public_key: string,  // hex
      tx_pub_key: string,  // hex
      index: number,       // output index in tx
      global_index: number // global output index
    },
    decoys: [{             // XMR: 15 decoys (ring 16), WOW: 21 decoys (ring 22)
      global_index: number,
      public_key: string,  // hex
      rct: string          // hex commitment
    }]
  }],
  destinations: [{ address: string, amount: number }],
  change_address: string,
  fee_per_byte: number,
  fee_mask: number,
  view_key: string,        // hex, 64 chars
  spend_key: string,       // hex, 64 chars
  network: "mainnet" | "testnet" | "stagenet" | "wownero"
}

Returns:

{
  tx_hex: string,   // signed transaction ready for broadcast
  tx_hash: string,  // transaction hash
  fee: number       // actual fee in atomic units
}

derive_output_key_image(view_key, spend_key, tx_pub_key, output_index, output_key) -> string

Derives the key image for a specific output when you have the output's public key.

compute_key_image(view_key, spend_key, tx_pub_key, output_index) -> string

Computes the key image for an output without requiring the output public key. This is useful for verifying LWS spent_outputs where only tx_pub_key and out_index are provided.

The function:

  1. Derives the one-time private key: x = Hs(a*R || outputIndex) + b
  2. Computes the output public key: P = x * G
  3. Returns the key image: KI = x * Hp(P)

This uses monero-oxide's Point::biased_hash for the Hp() operation, which is Monero's hash_to_ec (ge_fromfe_frombytes_vartime).

Project Structure

smirk-wasm/
├── Cargo.toml
├── build.sh
├── README.md
├── test.html
└── src/
    ├── lib.rs          # Main module, re-exports
    ├── result.rs       # WasmResult type
    ├── address.rs      # Address validation
    ├── keys.rs         # Key image derivation
    ├── output.rs       # Output derivation (key_offset, commitment_mask)
    ├── transaction.rs  # Transaction parsing
    ├── signing.rs      # Transaction signing
    └── tests.rs        # Unit tests

Dependencies

License

MIT

About

Library for client side construction and signing of xmw/wow txs

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published