Fix subaddress creation to use chain tip for CARROT detection, add CARROT subaddress derivation
This commit is contained in:
@@ -224,6 +224,42 @@ fn carrot_subaddress_spend_pubkey(
|
||||
EdwardsPoint::vartime_multiscalar_mul(&[k_subscal], &[*account_spend_pt])
|
||||
}
|
||||
|
||||
/// Derive the CARROT subaddress spend and view public keys for a given index.
|
||||
///
|
||||
/// Returns `(spend_pubkey, view_pubkey)` as 32-byte arrays.
|
||||
///
|
||||
/// For (0,0): spend = K_s, view = primary_address_view_pubkey (k_vi * G).
|
||||
/// For other indices: spend = k^j_subscal * K_s, view = k^j_subscal * K_v.
|
||||
pub fn carrot_derive_subaddress_keys(
|
||||
account_spend_pubkey: &[u8; 32],
|
||||
account_view_pubkey: &[u8; 32],
|
||||
primary_address_view_pubkey: &[u8; 32],
|
||||
generate_address_secret: &[u8; 32],
|
||||
major: u32,
|
||||
minor: u32,
|
||||
) -> ([u8; 32], [u8; 32]) {
|
||||
if major == 0 && minor == 0 {
|
||||
return (*account_spend_pubkey, *primary_address_view_pubkey);
|
||||
}
|
||||
|
||||
let spend_pt = match CompressedEdwardsY(*account_spend_pubkey).decompress() {
|
||||
Some(pt) => pt,
|
||||
None => return (*account_spend_pubkey, *account_view_pubkey),
|
||||
};
|
||||
let view_pt = match CompressedEdwardsY(*account_view_pubkey).decompress() {
|
||||
Some(pt) => pt,
|
||||
None => return (*account_spend_pubkey, *account_view_pubkey),
|
||||
};
|
||||
|
||||
let s_gen = carrot_index_extension_generator(generate_address_secret, major, minor);
|
||||
let k_subscal = carrot_subaddress_scalar(account_spend_pubkey, &s_gen, major, minor);
|
||||
|
||||
let sub_spend = EdwardsPoint::vartime_multiscalar_mul(&[k_subscal], &[spend_pt]);
|
||||
let sub_view = EdwardsPoint::vartime_multiscalar_mul(&[k_subscal], &[view_pt]);
|
||||
|
||||
(sub_spend.compress().to_bytes(), sub_view.compress().to_bytes())
|
||||
}
|
||||
|
||||
/// Generate the full CARROT subaddress map as a flat binary buffer.
|
||||
///
|
||||
/// Iterates major 0..=major_count, minor 0..=minor_count.
|
||||
|
||||
@@ -134,6 +134,9 @@ impl SyncEngine {
|
||||
|
||||
let sync_height = {
|
||||
let db = db.lock().map_err(|e| WalletError::Storage(e.to_string()))?;
|
||||
// Persist chain tip so non-sync code (e.g. subaddress creation)
|
||||
// can determine the active hardfork without daemon access.
|
||||
let _ = db.set_attribute("chain_tip_height", &top_block.to_string());
|
||||
db.get_sync_height()
|
||||
.map_err(|e| WalletError::Storage(e.to_string()))?
|
||||
};
|
||||
|
||||
@@ -706,38 +706,80 @@ impl Wallet {
|
||||
}
|
||||
|
||||
/// Derive the address string for a subaddress at (major, minor).
|
||||
///
|
||||
/// Produces a CARROT address if the CARROT hard fork is active at the
|
||||
/// chain tip height, otherwise falls back to legacy CryptoNote.
|
||||
/// The chain tip is persisted in the DB by the sync engine.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn derive_subaddress(&self, major: u32, minor: u32) -> Result<String, WalletError> {
|
||||
use salvium_types::address::create_address_raw;
|
||||
use salvium_types::consensus::is_carrot_active;
|
||||
use salvium_types::constants::{AddressFormat, AddressType};
|
||||
|
||||
let chain_tip = {
|
||||
let db = self
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| WalletError::Storage(e.to_string()))?;
|
||||
db.get_attribute("chain_tip_height")
|
||||
.map_err(|e| WalletError::Storage(e.to_string()))?
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(0)
|
||||
};
|
||||
|
||||
let use_carrot = is_carrot_active(chain_tip, self.keys.network)
|
||||
&& !self.keys.carrot.is_empty();
|
||||
|
||||
if major == 0 && minor == 0 {
|
||||
// Primary address.
|
||||
return self
|
||||
.keys
|
||||
.cn_address()
|
||||
.map_err(|e| WalletError::InvalidAddress(e.to_string()));
|
||||
return if use_carrot {
|
||||
self.keys
|
||||
.carrot_address()
|
||||
.map_err(|e| WalletError::InvalidAddress(e.to_string()))
|
||||
} else {
|
||||
self.keys
|
||||
.cn_address()
|
||||
.map_err(|e| WalletError::InvalidAddress(e.to_string()))
|
||||
};
|
||||
}
|
||||
|
||||
// Derive the subaddress spend public key.
|
||||
let spend_pub = salvium_crypto::subaddress::cn_derive_subaddress_spend_pubkey(
|
||||
&self.keys.cn.spend_public_key,
|
||||
&self.keys.cn.view_secret_key,
|
||||
major,
|
||||
minor,
|
||||
);
|
||||
if use_carrot {
|
||||
let (spend_pub, view_pub) =
|
||||
salvium_crypto::subaddress::carrot_derive_subaddress_keys(
|
||||
&self.keys.carrot.account_spend_pubkey,
|
||||
&self.keys.carrot.account_view_pubkey,
|
||||
&self.keys.carrot.primary_address_view_pubkey,
|
||||
&self.keys.carrot.generate_address_secret,
|
||||
major,
|
||||
minor,
|
||||
);
|
||||
|
||||
// Subaddresses use the main view public key.
|
||||
let addr = create_address_raw(
|
||||
self.keys.network,
|
||||
AddressFormat::Legacy,
|
||||
AddressType::Subaddress,
|
||||
&spend_pub,
|
||||
&self.keys.cn.view_public_key,
|
||||
None,
|
||||
)
|
||||
.map_err(|e| WalletError::InvalidAddress(e.to_string()))?;
|
||||
create_address_raw(
|
||||
self.keys.network,
|
||||
AddressFormat::Carrot,
|
||||
AddressType::Subaddress,
|
||||
&spend_pub,
|
||||
&view_pub,
|
||||
None,
|
||||
)
|
||||
.map_err(|e| WalletError::InvalidAddress(e.to_string()))
|
||||
} else {
|
||||
let spend_pub = salvium_crypto::subaddress::cn_derive_subaddress_spend_pubkey(
|
||||
&self.keys.cn.spend_public_key,
|
||||
&self.keys.cn.view_secret_key,
|
||||
major,
|
||||
minor,
|
||||
);
|
||||
|
||||
Ok(addr)
|
||||
create_address_raw(
|
||||
self.keys.network,
|
||||
AddressFormat::Legacy,
|
||||
AddressType::Subaddress,
|
||||
&spend_pub,
|
||||
&self.keys.cn.view_public_key,
|
||||
None,
|
||||
)
|
||||
.map_err(|e| WalletError::InvalidAddress(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Integrated addresses ────────────────────────────────────────────
|
||||
|
||||
+128
-32
@@ -2,6 +2,10 @@
|
||||
|
||||
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
|
||||
@@ -92,6 +96,19 @@ int rc = salvium_wallet_sync(wallet, daemon, my_callback);
|
||||
- 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
|
||||
@@ -111,6 +128,7 @@ void my_callback(int type, uint64_t cur, uint64_t target, uint32_t outs, const c
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -123,6 +141,7 @@ void my_callback(int type, uint64_t cur, uint64_t target, uint32_t outs, const c
|
||||
| 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
|
||||
|
||||
@@ -140,80 +159,142 @@ salvium_wallet_sync(wallet, daemon, callback); // full rescan
|
||||
```c
|
||||
// Single asset
|
||||
char* json = salvium_wallet_get_balance(wallet, "SAL", 0);
|
||||
// Returns: {"balance":"123456789","unlocked_balance":"100000000","locked_balance":"23456789"}
|
||||
// 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":"...","unlocked_balance":"...","locked_balance":"..."}, ...}
|
||||
// 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, "{\"is_confirmed\":true}");
|
||||
char* txs = salvium_wallet_get_transfers(wallet, "{\"isConfirmed\":true}");
|
||||
|
||||
// Only incoming
|
||||
char* txs = salvium_wallet_get_transfers(wallet, "{\"is_incoming\":true}");
|
||||
char* txs = salvium_wallet_get_transfers(wallet, "{\"isIncoming\":true}");
|
||||
|
||||
// Height range
|
||||
char* txs = salvium_wallet_get_transfers(wallet, "{\"min_height\":100000,\"max_height\":200000}");
|
||||
char* txs = salvium_wallet_get_transfers(wallet, "{\"minHeight\":100000,\"maxHeight\":200000}");
|
||||
|
||||
// Specific tx hash
|
||||
char* txs = salvium_wallet_get_transfers(wallet, "{\"tx_hash\":\"abc123...\"}");
|
||||
char* txs = salvium_wallet_get_transfers(wallet, "{\"txHash\":\"abc123...\"}");
|
||||
|
||||
// Returns JSON array. ALWAYS free:
|
||||
salvium_string_free(txs);
|
||||
```
|
||||
|
||||
**TxQuery fields (all optional):**
|
||||
**TxQuery fields (all optional, camelCase):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `is_incoming` | bool | Filter incoming transfers |
|
||||
| `is_outgoing` | bool | Filter outgoing transfers |
|
||||
| `is_confirmed` | bool | Filter confirmed (in-block) |
|
||||
| `in_pool` | bool | Filter mempool transactions |
|
||||
| `tx_type` | i64 | 1=miner, 2=protocol, 3=transfer |
|
||||
| `min_height` | i64 | Minimum block height |
|
||||
| `max_height` | i64 | Maximum block height |
|
||||
| `tx_hash` | string | Exact transaction hash |
|
||||
| `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:**
|
||||
**TransactionRow fields in response (camelCase):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `tx_hash` | string | Transaction hash (hex) |
|
||||
| `block_height` | i64/null | Block height (null if in pool) |
|
||||
| `block_timestamp` | i64 | Unix timestamp |
|
||||
| `is_incoming` | bool | Received funds |
|
||||
| `is_outgoing` | bool | Sent funds |
|
||||
| `is_confirmed` | bool | In a block |
|
||||
| `incoming_amount` | string | Atomic units received |
|
||||
| `outgoing_amount` | string | Atomic units sent |
|
||||
| `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) |
|
||||
| `asset_type` | string | e.g. "SAL" |
|
||||
| `tx_type` | i64 | Transaction type code |
|
||||
| `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, "{\"is_spent\":false}");
|
||||
char* outs = salvium_wallet_get_outputs(wallet, "{\"isSpent\":false}");
|
||||
|
||||
// Unspent SAL only
|
||||
char* outs = salvium_wallet_get_outputs(wallet, "{\"is_spent\":false,\"asset_type\":\"SAL\"}");
|
||||
char* outs = salvium_wallet_get_outputs(wallet, "{\"isSpent\":false,\"assetType\":\"SAL\"}");
|
||||
|
||||
salvium_string_free(outs);
|
||||
```
|
||||
|
||||
## 8. Other Wallet Queries
|
||||
**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
|
||||
@@ -240,14 +321,29 @@ uint64_t h = salvium_wallet_sync_height(wallet);
|
||||
int net = salvium_wallet_network(wallet); // 0=main, 1=test, 2=stage
|
||||
```
|
||||
|
||||
## 9. Staking
|
||||
## 10. Staking
|
||||
|
||||
```c
|
||||
char* stakes = salvium_wallet_get_stakes(wallet, "locked"); // or "returned" or NULL for all
|
||||
salvium_string_free(stakes);
|
||||
```
|
||||
|
||||
## 10. Cleanup
|
||||
**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.
|
||||
|
||||
@@ -312,7 +408,7 @@ printf("Balance: %s\n", bal);
|
||||
salvium_string_free(bal);
|
||||
|
||||
// Read transactions
|
||||
char* txs = salvium_wallet_get_transfers(wallet, "{\"is_confirmed\":true}");
|
||||
char* txs = salvium_wallet_get_transfers(wallet, "{\"isConfirmed\":true}");
|
||||
printf("Transfers: %s\n", txs);
|
||||
salvium_string_free(txs);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user