433 lines
14 KiB
Markdown
433 lines
14 KiB
Markdown
# 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
|
|
|
|
```c
|
|
// Optional — runtime is created lazily on first FFI call.
|
|
salvium_ffi_init();
|
|
```
|
|
|
|
## 2. Connect to Daemon
|
|
|
|
```c
|
|
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:
|
|
|
|
```c
|
|
int synced = salvium_daemon_is_synchronized(daemon);
|
|
// 1 = synced, 0 = still syncing, -1 = error
|
|
```
|
|
|
|
## 3. Create / Open Wallet
|
|
|
|
Three options — all return an opaque `void*` handle (null on error).
|
|
|
|
### 3a. From 25-word mnemonic
|
|
|
|
```c
|
|
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
|
|
|
|
```c
|
|
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)
|
|
|
|
```c
|
|
// 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
|
|
|
|
```c
|
|
void* wallet = salvium_wallet_import_blob(blob_json, pin, "/path/to/wallet.db");
|
|
```
|
|
|
|
## 4. Sync
|
|
|
|
### The call
|
|
|
|
```c
|
|
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:
|
|
|
|
```c
|
|
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)
|
|
|
|
```c
|
|
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;
|
|
}
|
|
}
|
|
```
|
|
|
|
| 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 |
|
|
|
|
### 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
|
|
|
|
```c
|
|
salvium_wallet_reset_sync_height(wallet, 0); // reset to genesis
|
|
salvium_wallet_sync(wallet, daemon, callback); // full rescan
|
|
```
|
|
|
|
## 5. Query Balance
|
|
|
|
```c
|
|
// 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
|
|
|
|
```c
|
|
// 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 |
|
|
| `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. Query Outputs (UTXOs)
|
|
|
|
```c
|
|
// 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) |
|
|
|
|
## 8. Subaddresses
|
|
|
|
```c
|
|
// 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.
|
|
|
|
## 9. Other Wallet Queries
|
|
|
|
```c
|
|
// 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
|
|
```
|
|
|
|
## 10. Staking
|
|
|
|
```c
|
|
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) |
|
|
|
|
## 11. Cleanup
|
|
|
|
**Close in reverse order.** Always close handles when done.
|
|
|
|
```c
|
|
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:
|
|
|
|
```c
|
|
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)
|
|
|
|
```c
|
|
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-rpc/src/daemon.rs` | RPC client |
|
|
| `crates/salvium-sync-bench/src/main.rs` | Working Rust sync example |
|