Monero/Wownero transaction construction for browser extensions, compiled to WebAssembly.
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.
- 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 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.
┌─────────────────────┐ ┌─────────────────────┐
│ Browser Extension │ │ Backend │
│ │ │ │
│ ┌───────────────┐ │ │ ┌───────────────┐ │
│ │ smirk-wasm │ │ │ │ LWS │ │
│ │ (~165KB) │ │ │ │ (Monero) │ │
│ │ │ │ │ │ │ │
│ │ - Keys │◄─┼─────┼──┤ - Outputs │ │
│ │ - Signing │ │ │ │ - Decoys │ │
│ │ - Addresses │──┼─────┼──► - Broadcast │ │
│ └───────────────┘ │ │ └───────────────┘ │
│ │ │ │
│ Spend key stays │ │ No access to │
│ here │ │ spend key │
└─────────────────────┘ └─────────────────────┘
- Rust (stable) with
wasm32-unknown-unknowntarget - wasm-bindgen-cli
# Add WASM target
rustup target add wasm32-unknown-unknown
# Install wasm-bindgen CLI
cargo install wasm-bindgen-cli# 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.wasmAfter building:
pkg/smirk_wasm.js- JavaScript modulepkg/smirk_wasm.d.ts- TypeScript definitionspkg/smirk_wasm_bg.wasm- WebAssembly binary (~380KB)
cargo test# Build first
./build.sh
# Serve with any static server
python3 -m http.server 8080
# Open http://localhost:8080/test.htmlimport 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
}
}All functions return JSON strings:
interface Result<T> {
success: boolean;
data?: T;
error?: string;
}| Function | Description |
|---|---|
test() |
Returns "smirk-wasm ready" if loaded |
version() |
Returns crate version |
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
}Computes the key image for an output. All arguments are 32-byte hex strings.
Returns the key image as a hex string.
Parses a transaction from hex.
// Returns:
{
inputs: number;
outputs: number;
version: number;
}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.
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
}Derives the key image for a specific output when you have the output's public key.
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:
- Derives the one-time private key:
x = Hs(a*R || outputIndex) + b - Computes the output public key:
P = x * G - 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).
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
- monero-oxide - Pure Rust Monero implementation (MIT)
- curve25519-dalek - Elliptic curve ops
- wasm-bindgen - Rust/JS interop
MIT