Files
salvium-rs/docs/wallet-sync-spec.md
T

20 KiB
Raw Blame History

Wallet Sync Integration Spec

How to use the salvium-rs FFI library to create a wallet, sync it, and read data.

All JSON fields are camelCase. Both query inputs and response outputs use camelCase (e.g. isConfirmed, incomingAmount, blockHeight). This is enforced by #[serde(rename_all = "camelCase")] on every struct.

1. Initialization

// Optional — runtime is created lazily on first FFI call.
salvium_ffi_init();

2. Connect to Daemon

void* daemon = salvium_daemon_connect("http://seed01.salvium.io:19081");
if (!daemon) {
    const char* err = salvium_last_error();  // DO NOT free this pointer
    // handle error...
}

Seed nodes:

Network URL
Mainnet http://seed01.salvium.io:19081
Testnet http://seed01.salvium.io:29081
Stagenet http://seed01.salvium.io:39081

Verify daemon is ready before syncing:

int synced = salvium_daemon_is_synchronized(daemon);
// 1 = synced, 0 = still syncing, -1 = error

Multi-Node Setup (Distributed Fetch)

The daemon handle wraps a NodePool that automatically includes 3 seed nodes for the detected network. You can add any number of custom nodes — the pool races all of them but only uses the fastest 4 for block fetching.

// Option A: Connect with a primary node (seeds included automatically)
void* daemon = salvium_daemon_connect("http://mynode:19081");

// Option B: Pool with seeds only (no primary)
void* daemon = salvium_daemon_pool_create(0);  // 0=Mainnet, 1=Testnet, 2=Stagenet

// Add custom nodes (one at a time)
salvium_daemon_add_node(daemon, "http://fast-node:19081");

// Add custom nodes (batch — JSON array)
const char* urls = "[\"http://node1:19081\",\"http://node2:19081\",\"http://node3:19081\"]";
salvium_daemon_add_nodes(daemon, (const uint8_t*)urls, strlen(urls));

// Race all nodes to measure latency (call once after setup)
salvium_daemon_force_race(daemon);

// Now sync — block fetches are automatically distributed across the fastest 4 nodes
int rc = salvium_wallet_sync(wallet, daemon, callback);

How it works:

  • force_race() probes every node in the pool via get_info and records latency
  • During sync, each batch of blocks is split across up to 4 nodes proportional to their speed
  • If a node fails its sub-range, the active node retries it automatically
  • Periodic re-racing (every 60s) adapts to changing network conditions
  • Single-node fallback when fewer than 2 nodes have latency data

FFI functions:

Function Description
salvium_daemon_add_node(handle, url) Add one node (returns 0/-1)
salvium_daemon_add_nodes(handle, json, len) Add multiple nodes from JSON array (returns 0/-1)
salvium_daemon_force_race(handle) Probe all nodes for latency (returns 0/-1)
salvium_daemon_active_node(handle) Get URL of current fastest node (free with salvium_string_free)

3. Create / Open Wallet

Three options — all return an opaque void* handle (null on error).

3a. From 25-word mnemonic

void* wallet = salvium_wallet_from_mnemonic(
    "word1 word2 ... word25",  // null-terminated C string
    0,                          // network: 0=Mainnet, 1=Testnet, 2=Stagenet
    "/path/to/wallet.db",       // database file path
    db_key,                     // uint8_t* encryption key
    db_key_len                  // size_t key length
);

3b. From 32-byte seed

void* wallet = salvium_wallet_create(
    seed_bytes,    // const uint8_t[32]
    0,             // network
    "/path/to/wallet.db",
    db_key,
    db_key_len
);

3c. From JSON keys (view-only supported)

// Full wallet:   {"seed": "hex64", "network": "mainnet"}
// View-only:     {"view_secret_key": "hex64", "spend_public_key": "hex64", "network": "mainnet"}
void* wallet = salvium_wallet_open(keys_json, "/path/to/wallet.db", db_key, db_key_len);

3d. From PIN-encrypted blob

void* wallet = salvium_wallet_import_blob(blob_json, pin, "/path/to/wallet.db");

4. Sync

The call

int rc = salvium_wallet_sync(wallet, daemon, my_callback);
// rc: 0 = success, -1 = error

This function blocks until sync is complete. It handles everything internally:

  • Fetches blocks from the daemon in adaptive batches (2-1000 blocks per HTTP request)
  • Parses each block and scans all transaction outputs for owned funds
  • Detects chain reorganizations and rolls back automatically
  • Stores matched outputs and transactions in the wallet database
  • Updates sync height per-block for crash safety

Cancellation

From another thread, call:

int rc = salvium_wallet_stop_sync(wallet);
// rc: 0 = success, -1 = error

The sync loop stops before the next batch and returns error code -1. The callback fires event type 6 (Cancelled) with the height reached. After cancelling, you can salvium_wallet_reset_sync_height and restart.

Callback (optional, may be NULL)

typedef void (*SyncCallbackFn)(
    int event_type,          // event code (see below)
    uint64_t current_height, // current scan position
    uint64_t target_height,  // chain tip
    uint32_t outputs_found,  // cumulative owned outputs found
    const char* error_msg    // null unless event_type=4 or 5
);

void my_callback(int type, uint64_t cur, uint64_t target, uint32_t outs, const char* msg) {
    switch (type) {
        case 0: printf("Started: target=%llu\n", target); break;
        case 1: printf("Progress: %llu/%llu (%u outputs)\n", cur, target, outs); break;
        case 2: printf("Complete: height=%llu\n", cur); break;
        case 3: printf("Reorg: %llu -> %llu\n", cur, target); break;
        case 4: printf("Error: %s\n", msg); break;
        case 5: printf("Parse error at %llu: %s\n", cur, msg); break;
        case 6: printf("Cancelled at %llu\n", cur); break;
        case 7: printf("Pool scan: %llu new, %llu dropped\n", cur, target); break;
    }
}
Event Code Frequency Fields
Started 0 Once at start target_height
Progress 1 Per batch (~every 2000 blocks) current_height, target_height, outputs_found
Complete 2 Once at end current_height = final height
Reorg 3 When chain fork detected current_height = old tip, target_height = fork point
Error 4 On RPC/network error error_msg = description
ParseError 5 On block parse failure current_height = block height, error_msg = details
Cancelled 6 When stop_sync called current_height = height reached
PoolScanComplete 7 After block sync completes current_height = new pool TXs found, target_height = dropped pool TXs

Event sequence for a normal sync: Started(0)Progress(1) × N → Complete(2)PoolScanComplete(7)

The mempool scan runs automatically after block sync. If it fails (daemon disconnected, etc.), event 7 is simply not emitted — block sync is unaffected.

Incremental sync

salvium_wallet_sync is always incremental. It starts from the last persisted height and only fetches new blocks. Calling it again after completion returns immediately if no new blocks exist.

Rescan

salvium_wallet_reset_sync_height(wallet, 0);  // reset to genesis
salvium_wallet_sync(wallet, daemon, callback); // full rescan

5. Query Balance

// Single asset
char* json = salvium_wallet_get_balance(wallet, "SAL", 0);
// Returns: {"balance":"123456789","unlockedBalance":"100000000","lockedBalance":"23456789"}
// Amounts are atomic units (1 SAL = 100,000,000 atomic)
// CALLER MUST FREE:
salvium_string_free(json);

// All assets
char* all = salvium_wallet_get_all_balances(wallet, 0);
// Returns: {"SAL":{"balance":"...","unlockedBalance":"...","lockedBalance":"..."}, ...}
salvium_string_free(all);

BalanceResult fields:

Field Type Description
balance string Total balance (atomic units)
unlockedBalance string Spendable balance (atomic units)
lockedBalance string Locked/immature balance (atomic units)

You MUST sync before querying balances. Without sync, the balance will be 0.

6. Query Transactions

// All confirmed transfers
char* txs = salvium_wallet_get_transfers(wallet, "{\"isConfirmed\":true}");

// Only incoming
char* txs = salvium_wallet_get_transfers(wallet, "{\"isIncoming\":true}");

// Height range
char* txs = salvium_wallet_get_transfers(wallet, "{\"minHeight\":100000,\"maxHeight\":200000}");

// Specific tx hash
char* txs = salvium_wallet_get_transfers(wallet, "{\"txHash\":\"abc123...\"}");

// Returns JSON array. ALWAYS free:
salvium_string_free(txs);

TxQuery fields (all optional, camelCase):

Field Type Description
isIncoming bool Filter incoming transfers
isOutgoing bool Filter outgoing transfers
isConfirmed bool Filter confirmed (in-block)
inPool bool Filter mempool transactions
txType i64 Transaction type code (see below)
minHeight i64 Minimum block height
maxHeight i64 Maximum block height
txHash string Exact transaction hash

TransactionRow fields in response (camelCase):

Field Type Description
txHash string Transaction hash (hex)
txPubKey string/null Transaction public key (hex)
blockHeight i64/null Block height (null if in pool)
blockTimestamp i64/null Unix timestamp
isIncoming bool Received funds
isOutgoing bool Sent funds
isConfirmed bool In a block
inPool bool Currently in the mempool
isFailed bool Dropped from mempool without confirming
incomingAmount string Atomic units received
outgoingAmount string Atomic units sent
fee string Transaction fee (atomic)
changeAmount string Change returned to self (atomic)
unlockTime string Unlock time (0 = immediate)
assetType string e.g. "SAL"
txType i64 Transaction type code (see below)
isMinerTx bool Coinbase transaction
isProtocolTx bool Protocol yield/return transaction
note string User note (empty if unset)

Transaction types (txType):

Value Meaning
1 Miner (coinbase)
2 Protocol (yield distribution, stake returns)
3 Transfer
4 Convert (SAL <-> VSD)
5 Burn
6 Stake
7 Return (stake unlock)

Display logic:

  • isIncoming && !isOutgoing — received funds, show incomingAmount
  • !isIncoming && isOutgoing — sent funds, show outgoingAmount - changeAmount
  • isIncoming && isOutgoing — self-transfer, net = incomingAmount - outgoingAmount

7. Mempool (Pending Transactions)

Mempool scanning runs automatically at the end of every salvium_wallet_sync call. Outgoing transactions are also stored immediately at broadcast — no sync needed to see your own sends.

Querying pool transactions

// All pending pool TXs
char* txs = salvium_wallet_get_transfers(wallet, "{\"inPool\":true}");

// Pending outgoing only
char* txs = salvium_wallet_get_transfers(wallet, "{\"isOutgoing\":true,\"inPool\":true}");

// Pending incoming only
char* txs = salvium_wallet_get_transfers(wallet, "{\"isIncoming\":true,\"inPool\":true}");

salvium_string_free(txs);

Pool TX field values

Field Pool TX Confirmed TX Failed TX
inPool true false false
isConfirmed false true false
isFailed false false true
blockHeight null / 0 actual height null / 0
confirmations 0 >= 0 0

TX lifecycle

  1. Broadcast — TX stored immediately with inPool: true, isConfirmed: false
  2. Mempool scan — incoming TXs from others detected and stored with same flags
  3. Block confirmed — TX updated to inPool: false, isConfirmed: true, blockHeight: N
  4. Dropped — if a pool TX disappears without confirming: inPool: false, isFailed: true, and any outputs it spent are automatically restored
void my_callback(int type, uint64_t cur, uint64_t target, uint32_t outs, const char* msg) {
    if (type == 7 && (cur > 0 || target > 0)) {
        // Pool scan found changes — refresh transfer list
        refresh_transfers();
    }
}

// Combined transfer list (pending first, then confirmed)
char* pending   = salvium_wallet_get_transfers(wallet, "{\"inPool\":true}");
char* confirmed = salvium_wallet_get_transfers(wallet, "{\"isConfirmed\":true}");
// display: pending + confirmed

8. Query Outputs (UTXOs)

// All unspent outputs
char* outs = salvium_wallet_get_outputs(wallet, "{\"isSpent\":false}");

// Unspent SAL only
char* outs = salvium_wallet_get_outputs(wallet, "{\"isSpent\":false,\"assetType\":\"SAL\"}");

salvium_string_free(outs);

OutputQuery fields (all optional, camelCase):

Field Type Description
isSpent bool Filter spent/unspent
isFrozen bool Filter frozen outputs
assetType string e.g. "SAL"
txType i64 Transaction type code
accountIndex i64 Account major index
subaddressIndex i64 Subaddress minor index
minAmount string Minimum amount (atomic)
maxAmount string Maximum amount (atomic)

9. Subaddresses

// Create a new subaddress (returns JSON: {"major":0,"minor":1,"address":"..."})
char* sub = salvium_wallet_create_subaddress(wallet, 0, "my label");
salvium_string_free(sub);

// List all subaddresses for account 0
char* subs = salvium_wallet_get_subaddresses(wallet, 0);
salvium_string_free(subs);

// Label an existing subaddress
salvium_wallet_label_subaddress(wallet, 0, 1, "new label");

Subaddresses are automatically CARROT or CryptoNote based on the current chain hardfork.

10. Other Wallet Queries

// Addresses
char* cn_addr   = salvium_wallet_get_address(wallet, 0);  // CryptoNote
char* carr_addr  = salvium_wallet_get_address(wallet, 1);  // CARROT
salvium_string_free(cn_addr);
salvium_string_free(carr_addr);

// Mnemonic (null if view-only)
char* words = salvium_wallet_get_mnemonic(wallet);
if (words) salvium_string_free(words);

// Key material as JSON
char* keys = salvium_wallet_get_keys_json(wallet);
salvium_string_free(keys);

// Can this wallet spend?
int can = salvium_wallet_can_spend(wallet);  // 1=yes, 0=no

// Current sync height
uint64_t h = salvium_wallet_sync_height(wallet);

// Network
int net = salvium_wallet_network(wallet);  // 0=main, 1=test, 2=stage

11. Staking

char* stakes = salvium_wallet_get_stakes(wallet, "locked");   // or "returned" or NULL for all
salvium_string_free(stakes);

StakeRow fields (camelCase):

Field Type Description
stakeTxHash string Stake transaction hash
stakeHeight i64/null Block height of stake
stakeTimestamp i64/null Unix timestamp of stake
amountStaked string Amount staked (atomic)
fee string Transaction fee (atomic)
assetType string e.g. "SAL"
status string "locked" or "returned"
returnTxHash string/null Return transaction hash
returnHeight i64/null Block height of return
returnAmount string Amount returned (atomic)

12. Sending Transactions

All transaction functions take a JSON params string. assetType is required — there is no default. Omitting it returns an error.

All return a JSON string on success (caller must free with salvium_string_free()), or NULL on error.

Transfer

char* result = salvium_wallet_transfer(wallet, daemon, "{\"destinations\":[{\"address\":\"Svk1...\",\"amount\":\"100000000\"}],\"assetType\":\"SAL1\"}");
salvium_string_free(result);

Params (camelCase):

Field Type Required Description
destinations array yes [{"address": "...", "amount": "..."}] (atomic units)
assetType string yes Asset to spend (e.g. "SAL1")
priority string no "low", "normal" (default), "elevated", "priority"
ringSize number no Default 16
dryRun bool no If true, build + sign but don't broadcast

Result: {"txHash": "...", "fee": "...", "amount": "..."} When dryRun is true, also includes txHex and weight.

Transfer Dry Run

Convenience wrapper — forces dryRun: true regardless of params.

char* result = salvium_wallet_transfer_dry_run(wallet, daemon, params_json);

Stake

char* result = salvium_wallet_stake(wallet, daemon, "{\"amount\":\"100000000\",\"assetType\":\"SAL1\"}");
salvium_string_free(result);

Params:

Field Type Required Description
amount string yes Amount to stake (atomic units)
assetType string yes Asset to stake
priority string no Fee priority (default "normal")
ringSize number no Default 16

Result: {"txHash": "...", "fee": "...", "amount": "...", "weight": ...}

Stake Dry Run

char* result = salvium_wallet_stake_dry_run(wallet, daemon, params_json);

Sweep

Sends all unlocked outputs of the specified asset to a single address.

char* result = salvium_wallet_sweep(wallet, daemon, "{\"address\":\"Svk1...\",\"assetType\":\"SAL1\"}");
salvium_string_free(result);

Params:

Field Type Required Description
address string yes Destination address
assetType string yes Asset to sweep
priority string no Fee priority (default "normal")
ringSize number no Default 16
dryRun bool no If true, build + sign but don't broadcast

Result: {"txHash": "...", "fee": "...", "amount": "..."}

13. Cleanup

Close in reverse order. Always close handles when done.

salvium_wallet_close(wallet);   // wallet first
salvium_daemon_close(daemon);   // daemon second

Error Handling

Every FFI function follows one of these patterns:

Return type Success Error
i32 0 -1
*mut c_char non-null string NULL
*mut c_void non-null handle NULL
u64 value u64::MAX

On error, call salvium_last_error() to get the error message:

const char* err = salvium_last_error();
// This pointer is valid until the NEXT FFI call on the same thread.
// DO NOT free it. Copy it if you need to keep it.

Memory Rules

  1. Strings returned by FFI functions (*mut c_char) MUST be freed with salvium_string_free().
  2. Handles (*mut c_void) MUST be closed with the matching _close() function.
  3. Error strings from salvium_last_error() must NOT be freed — they are owned by the library.
  4. Input strings (parameters you pass in) must be valid null-terminated UTF-8.

Complete Example (Pseudocode)

salvium_ffi_init();

// Connect
void* daemon = salvium_daemon_connect("http://seed01.salvium.io:19081");
assert(daemon != NULL);

// Wait for daemon to sync
while (salvium_daemon_is_synchronized(daemon) != 1) {
    sleep(5);
}

// Create wallet
uint8_t db_key[32];
generate_random_bytes(db_key, 32);
void* wallet = salvium_wallet_from_mnemonic(mnemonic, 0, "wallet.db", db_key, 32);
assert(wallet != NULL);

// Sync blockchain (blocks until complete)
int rc = salvium_wallet_sync(wallet, daemon, progress_callback);
assert(rc == 0);

// Read balance
char* bal = salvium_wallet_get_balance(wallet, "SAL", 0);
printf("Balance: %s\n", bal);
salvium_string_free(bal);

// Read transactions
char* txs = salvium_wallet_get_transfers(wallet, "{\"isConfirmed\":true}");
printf("Transfers: %s\n", txs);
salvium_string_free(txs);

// Cleanup
salvium_wallet_close(wallet);
salvium_daemon_close(daemon);

Source Files

File What
crates/salvium-ffi/src/lib.rs FFI entry point, runtime singleton
crates/salvium-ffi/src/wallet.rs Wallet lifecycle, sync, queries
crates/salvium-ffi/src/daemon.rs Daemon RPC handle
crates/salvium-ffi/src/error.rs Error storage, ffi_try helpers
crates/salvium-ffi/src/strings.rs String marshalling, salvium_string_free
crates/salvium-wallet/src/wallet.rs Rust wallet implementation
crates/salvium-wallet/src/sync.rs Sync engine internals
crates/salvium-wallet/src/pool_scan.rs Mempool transaction scanning
crates/salvium-rpc/src/daemon.rs RPC client
crates/salvium-sync-bench/src/main.rs Working Rust sync example