65d596a6ea
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.
478 lines
15 KiB
Markdown
478 lines
15 KiB
Markdown
# 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
|
||
|
||
```bash
|
||
# 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:
|
||
|
||
```bash
|
||
scripts/build-wasm.sh
|
||
# Output in prebuilt/wasm/
|
||
```
|
||
|
||
## 2. Initialize the Module
|
||
|
||
### Cloudflare Worker (bundler target)
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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.
|
||
|
||
```typescript
|
||
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):**
|
||
```json
|
||
{
|
||
"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.
|
||
|
||
```typescript
|
||
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.
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"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`
|
||
|
||
```typescript
|
||
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`
|
||
|
||
```typescript
|
||
import { parse_block_bytes } from 'salvium-explorer';
|
||
const json: string = parse_block_bytes(data: Uint8Array);
|
||
```
|
||
|
||
### `parse_extra`
|
||
|
||
```typescript
|
||
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`
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
keccak256(data: Uint8Array): Uint8Array // 32-byte CryptoNote hash
|
||
blake2b_hash(data: Uint8Array, outLen: number): Uint8Array
|
||
sha256(data: Uint8Array): Uint8Array
|
||
```
|
||
|
||
### Key Operations
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
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:**
|
||
```json
|
||
{
|
||
"network": "mainnet",
|
||
"format": "CryptoNote",
|
||
"address_type": "Standard",
|
||
"spend_public_key": "hex64",
|
||
"view_public_key": "hex64"
|
||
}
|
||
```
|
||
|
||
### TX Type / RCT Type Names
|
||
|
||
```typescript
|
||
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):
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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)
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
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) |
|