Allows Android/FFI callers to interrupt an in-progress wallet sync cleanly between batches. Adds salvium_wallet_stop_sync FFI export, WalletError::Cancelled, SyncEvent::Cancelled, and WalletHandle wrapper that pairs each wallet with its cancellation flag.
15 KiB
Explorer WASM Integration Spec
How to use the salvium-explorer WASM module in a Cloudflare Worker or browser to parse blocks, analyze transactions, and scan outputs.
1. Build
# Build the wasm-bindgen package (JS glue + .wasm binary)
wasm-pack build crates/salvium-explorer --release --target bundler --out-dir pkg
# Output:
# crates/salvium-explorer/pkg/salvium_explorer.js (JS glue)
# crates/salvium-explorer/pkg/salvium_explorer_bg.wasm (WASM binary)
# crates/salvium-explorer/pkg/salvium_explorer.d.ts (TypeScript types)
# crates/salvium-explorer/pkg/package.json
Or use the build script which also produces the C ABI static library:
scripts/build-wasm.sh
# Output in prebuilt/wasm/
2. Initialize the Module
Cloudflare Worker (bundler target)
import init, { initSync } from 'salvium-explorer';
import wasmModule from 'salvium-explorer/salvium_explorer_bg.wasm';
// Synchronous init (preferred for Workers)
initSync({ module: wasmModule });
// Or async init
await init({ module_or_path: wasmModule });
Browser
import init from 'salvium-explorer';
await init(); // Fetches .wasm from same directory
After initialization, all exported functions are available as direct imports.
3. Explorer-Specific APIs
These are the 3 high-level functions unique to the explorer crate. They combine multiple salvium-crypto primitives into single calls.
3a. parse_and_analyze_tx
Parse a raw transaction binary and return enriched JSON.
import { parse_and_analyze_tx } from 'salvium-explorer';
const result: string = parse_and_analyze_tx(txBytes: Uint8Array);
const tx = JSON.parse(result);
Returns all fields from parse_transaction_bytes() plus these analysis fields:
| Field | Type | Description |
|---|---|---|
tx_type_name |
string | Human-readable TX type (e.g. "Transfer", "Miner", "Protocol") |
rct_type_name |
string | Human-readable RCT type (e.g. "RctBulletproofPlus") |
input_count |
number | Number of inputs |
output_count |
number | Number of outputs |
is_coinbase |
boolean | Whether this is a miner/coinbase TX |
is_carrot |
boolean | Whether any output uses CARROT format (type 0x04) |
key_images |
string[] | Key image hex strings from inputs |
output_keys |
string[] | Output public key hex strings |
fee |
string | Transaction fee in atomic units (decimal string) |
Error: Returns {"error": "..."} on parse failure.
Example response (trimmed):
{
"prefix": {
"version": 3,
"txType": 3,
"unlockTime": "0",
"vin": [...],
"vout": [...],
"extra": { "pubkey": "ab12...", ... }
},
"rct": {
"type": 6,
"txnFee": "24960000",
...
},
"tx_type_name": "Transfer",
"rct_type_name": "RctBulletproofPlus",
"input_count": 2,
"output_count": 2,
"is_coinbase": false,
"is_carrot": true,
"key_images": ["aabb...", "ccdd..."],
"output_keys": ["eeff...", "1122..."],
"fee": "24960000"
}
3b. parse_and_analyze_block
Parse a raw block binary and return enriched JSON.
import { parse_and_analyze_block } from 'salvium-explorer';
const result: string = parse_and_analyze_block(blockBytes: Uint8Array);
const block = JSON.parse(result);
Returns all fields from parse_block_bytes() plus:
| Field | Type | Description |
|---|---|---|
tx_count |
number | Number of transaction hashes (excluding miner tx) |
Base block fields:
| Field | Type | Description |
|---|---|---|
majorVersion |
number | Block major version |
minorVersion |
number | Block minor version |
timestamp |
number | Block timestamp (Unix) |
prevId |
string | Previous block hash (hex) |
nonce |
number | Mining nonce |
minerTx |
object | Miner transaction (full parsed TX) |
txHashes |
string[] | Transaction hashes in this block (hex) |
3c. decode_outputs_for_view_key
Scan a transaction for owned outputs using a view key pair.
import { decode_outputs_for_view_key } from 'salvium-explorer';
const result: string = decode_outputs_for_view_key(
txBytes: Uint8Array, // raw transaction binary
viewSecret: Uint8Array, // 32-byte view secret key
spendPub: Uint8Array, // 32-byte spend public key
);
const outputs = JSON.parse(result);
Parameters:
txBytes: Raw transaction blob (binary, NOT hex)viewSecret: 32-byte view secret key (the private view key)spendPub: 32-byte spend public key (the public spend key)
Returns a JSON array of owned outputs:
[
{
"output_index": 0,
"amount": "1000000000",
"output_key": "aabbccdd...",
"subaddress_major": 0,
"subaddress_minor": 0
}
]
| Field | Type | Description |
|---|---|---|
output_index |
number | Index within the transaction's outputs |
amount |
string | Amount in atomic units (1 SAL = 100,000,000) |
output_key |
string | Output public key (hex) |
subaddress_major |
number | Subaddress major index (0 = main) |
subaddress_minor |
number | Subaddress minor index (0 = main) |
Returns [] if no outputs match. Returns {"error": "..."} on failure.
How it works internally:
- Parses the transaction binary to extract
voutandextra.pubkey - Computes key derivation:
D = 8 * view_secret * tx_pub_key - For each output at index
i: derives expected keyP' = H(D, i)*G + spend_pub - Compares
P'against the actual output key — match means the output is owned
Note: This is a CryptoNote (legacy) scan only. CARROT output scanning requires the full CARROT key set — use the low-level CARROT helpers for that (see Section 5).
4. Core Parsing Functions
These do raw binary parsing without the analysis enrichment.
parse_transaction_bytes
import { parse_transaction_bytes } from 'salvium-explorer';
const json: string = parse_transaction_bytes(data: Uint8Array);
Returns the full parsed transaction as JSON. All binary fields are hex-encoded, amounts are decimal strings.
parse_block_bytes
import { parse_block_bytes } from 'salvium-explorer';
const json: string = parse_block_bytes(data: Uint8Array);
parse_extra
import { parse_extra } from 'salvium-explorer';
const json: string = parse_extra(extraBytes: Uint8Array);
Parses just the TX extra field. Returns JSON with:
pubkey: TX public key (hex)nonces: payment IDs, extra noncesadditionalPubkeys: additional TX public keys (for subaddresses)
compute_tx_prefix_hash
import { compute_tx_prefix_hash } from 'salvium-explorer';
const hash: Uint8Array = compute_tx_prefix_hash(data: Uint8Array);
// Returns 32-byte keccak256 of the TX prefix
5. Crypto Primitives Available
The explorer re-exports the full salvium-crypto function set. These are the ones most relevant for explorer use:
Hashing
keccak256(data: Uint8Array): Uint8Array // 32-byte CryptoNote hash
blake2b_hash(data: Uint8Array, outLen: number): Uint8Array
sha256(data: Uint8Array): Uint8Array
Key Operations
// CryptoNote key derivation (used by decode_outputs_for_view_key internally)
generate_key_derivation(pubKey: Uint8Array, secKey: Uint8Array): Uint8Array
derive_public_key(derivation: Uint8Array, outputIndex: number, basePub: Uint8Array): Uint8Array
// Key image
generate_key_image(pubKey: Uint8Array, secKey: Uint8Array): Uint8Array
is_valid_key_image(keyImage: Uint8Array): boolean
// Point operations
scalar_mult_base(s: Uint8Array): Uint8Array // s*G
hash_to_point(data: Uint8Array): Uint8Array // H_p(data)
Address Utilities
wasm_parse_address(address: string): string // Address → JSON
wasm_is_valid_address(address: string): boolean // Validate
wasm_describe_address(address: string): string // Human-readable description
wasm_create_address( // Create from components
network: number, // 0=mainnet, 1=testnet, 2=stagenet
format: number, // 0=CryptoNote, 1=CARROT
addrType: number, // 0=standard, 1=subaddress, 2=integrated
spendKey: Uint8Array,
viewKey: Uint8Array
): string
wasm_to_integrated_address(address: string, paymentId: Uint8Array): string
wasm_parse_address returns:
{
"network": "mainnet",
"format": "CryptoNote",
"address_type": "Standard",
"spend_public_key": "hex64",
"view_public_key": "hex64"
}
TX Type / RCT Type Names
wasm_tx_type_name(txType: number): string // e.g. "Transfer", "Miner", "Protocol"
wasm_rct_type_name(rctType: number): string // e.g. "RctBulletproofPlus"
CARROT Output Scanning (Low-Level)
For full CARROT scanning (beyond what decode_outputs_for_view_key provides):
// Build input context for a TX
make_input_context_rct(firstKeyImage: Uint8Array): Uint8Array // Regular TX
make_input_context_coinbase(blockHeight: bigint): Uint8Array // Coinbase TX
// View tag check (fast rejection)
compute_carrot_view_tag(sSrUnctx: Uint8Array, inputContext: Uint8Array, ko: Uint8Array): Uint8Array
// After view tag matches — full decryption
decrypt_carrot_amount(encAmount: Uint8Array, sSrCtx: Uint8Array, ko: Uint8Array): bigint
recover_carrot_address_spend_pubkey(ko: Uint8Array, sSrCtx: Uint8Array, commitment: Uint8Array): Uint8Array
derive_carrot_commitment_mask(sSrCtx: Uint8Array, amount: bigint, addressSpendPubkey: Uint8Array, enoteType: number): Uint8Array
// CARROT key derivation (9 keys from master secret)
derive_carrot_keys_batch(masterSecret: Uint8Array): Uint8Array // Returns 288 bytes (9 × 32)
derive_carrot_view_only_keys_batch(viewBalanceSecret: Uint8Array, accountSpendPubkey: Uint8Array): Uint8Array
// Subaddress map generation
carrot_subaddress_map_batch(
accountSpendPubkey: Uint8Array,
accountViewPubkey: Uint8Array,
generateAddressSecret: Uint8Array,
majorCount: number,
minorCount: number
): Uint8Array // Returns majorCount * minorCount * 40 bytes (32-byte key + 4-byte major + 4-byte minor each)
Verification
// Full RCT signature verification (all ring sigs + bulletproofs in one call)
verify_rct_signatures_wasm(
rctType: number,
inputCount: number,
ringSize: number,
txPrefixHash: Uint8Array,
rctBaseBytes: Uint8Array,
bpComponents: Uint8Array,
keyImagesFlat: Uint8Array,
pseudoOutsFlat: Uint8Array,
sigsFlat: Uint8Array,
ringPubkeysFlat: Uint8Array,
ringCommitmentsFlat: Uint8Array
): Uint8Array // Returns JSON with verification result
6. Typical Explorer Workflow
Block Page
import { parse_and_analyze_block, parse_and_analyze_tx } from 'salvium-explorer';
// 1. Fetch block from daemon RPC (get_block endpoint returns binary blob)
const blockBlob: Uint8Array = await fetchBlockBlob(height);
// 2. Parse + analyze the block
const block = JSON.parse(parse_and_analyze_block(blockBlob));
// block.majorVersion, block.timestamp, block.prevId, block.tx_count, ...
// 3. The miner TX is embedded in the block
const minerTx = block.minerTx;
// minerTx.prefix.vout → miner reward outputs
// 4. Fetch and parse each transaction
for (const txHash of block.txHashes) {
const txBlob: Uint8Array = await fetchTxBlob(txHash);
const tx = JSON.parse(parse_and_analyze_tx(txBlob));
// tx.tx_type_name, tx.input_count, tx.output_count, tx.fee, ...
}
Transaction Page
import { parse_and_analyze_tx, wasm_tx_type_name } from 'salvium-explorer';
const tx = JSON.parse(parse_and_analyze_tx(txBlob));
// Display summary
console.log(`Type: ${tx.tx_type_name}`); // "Transfer"
console.log(`Fee: ${tx.fee} atomic`); // "24960000"
console.log(`Inputs: ${tx.input_count}`);
console.log(`Outputs: ${tx.output_count}`);
console.log(`CARROT: ${tx.is_carrot}`);
console.log(`Coinbase: ${tx.is_coinbase}`);
// Key images (for double-spend checking)
for (const ki of tx.key_images) {
console.log(`Key image: ${ki}`);
}
// Output keys (for output lookup)
for (const ok of tx.output_keys) {
console.log(`Output key: ${ok}`);
}
Output Decoding (View Key Search)
import { decode_outputs_for_view_key } from 'salvium-explorer';
// User provides their view key + spend public key
const viewSecret = hexToBytes(viewSecretHex); // 32 bytes
const spendPub = hexToBytes(spendPubHex); // 32 bytes
const owned = JSON.parse(decode_outputs_for_view_key(txBlob, viewSecret, spendPub));
for (const out of owned) {
console.log(`Output #${out.output_index}: ${out.amount} atomic SAL`);
console.log(` Key: ${out.output_key}`);
}
Address Validation
import { wasm_is_valid_address, wasm_parse_address } from 'salvium-explorer';
if (wasm_is_valid_address(userAddress)) {
const info = JSON.parse(wasm_parse_address(userAddress));
console.log(`Network: ${info.network}, Type: ${info.address_type}`);
}
7. Data Types — All Inputs are Raw Bytes
Every function that takes transaction or block data expects raw binary (Uint8Array), not hex strings. If your daemon returns hex, decode first:
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
}
return bytes;
}
// Daemon RPC returns hex → convert before passing to WASM
const txHex: string = await daemon.getTransaction(txHash);
const txBytes: Uint8Array = hexToBytes(txHex);
const result = parse_and_analyze_tx(txBytes);
8. Error Handling
All string-returning functions follow the same pattern:
const result = JSON.parse(parse_and_analyze_tx(txBytes));
if (result.error) {
// Parse failed
console.error(result.error);
} else {
// Success — use result fields
}
For functions returning Uint8Array: an empty array (length === 0) indicates an error (invalid input, failed point decompression, etc).
For functions returning boolean: they return false on invalid input.
9. Memory / Performance Notes
- The WASM module is ~4MB (uncompressed). Cloudflare Workers supports this.
- All functions are synchronous — no async/await needed after
initSync(). - The module manages its own WASM linear memory.
Uint8Arrayinputs are copied into WASM memory and results are copied out — no manual memory management needed from JS. parse_and_analyze_txandparse_and_analyze_blockdo JSON serialization internally (serde_json). For hot paths parsing thousands of transactions, preferparse_transaction_bytesand extract only the fields you need.
Source Files
| File | What |
|---|---|
crates/salvium-explorer/src/lib.rs |
Explorer WASM APIs (3 custom + 61 re-exports) |
crates/salvium-explorer/Cargo.toml |
Crate config |
crates/salvium-explorer/pkg/ |
Built wasm-pack output (JS glue, .wasm, .d.ts) |
crates/salvium-crypto/src/lib.rs |
Underlying crypto implementations |
crates/salvium-crypto/src/wasm_ffi.rs |
C ABI static lib (alternative to wasm-bindgen) |
crates/salvium-crypto/src/tx_parse.rs |
Transaction/block binary parser |
scripts/build-wasm.sh |
Build script (wasm-pack + staticlib) |