Files
salvium-rs/docs/explorer-spec.md
T
Matt Hess 65d596a6ea Add sync cancellation via AtomicBool flag for FFI consumers
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.
2026-02-26 02:23:12 +00:00

15 KiB
Raw Blame History

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:

  1. Parses the transaction binary to extract vout and extra.pubkey
  2. Computes key derivation: D = 8 * view_secret * tx_pub_key
  3. For each output at index i: derives expected key P' = H(D, i)*G + spend_pub
  4. 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 nonces
  • additionalPubkeys: 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}`);
}
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. Uint8Array inputs are copied into WASM memory and results are copied out — no manual memory management needed from JS.
  • parse_and_analyze_tx and parse_and_analyze_block do JSON serialization internally (serde_json). For hot paths parsing thousands of transactions, prefer parse_transaction_bytes and 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)