56fbea363c
─────────────────────
- Rebuild WASM binary via wasm-pack — adds batch exports that were missing
from stale binary (cn_subaddress_map_batch, carrot_subaddress_map_batch,
derive_carrot_keys_batch, compute_carrot_view_tag, decrypt_carrot_amount,
parse_extra, serialize_tx_extra, compute_tx_prefix_hash, and 8 more)
- Deprecate JS backend scalar/point ops — JS backend now provides hashing
only (keccak256, blake2b, sha256); all EC math requires WASM or FFI
- Add initCrypto() entry point that loads WASM automatically at startup
- Wire batch subaddress, CARROT key derivation, CARROT helpers, and
tx_extra parsing/serialization through all four backends (WASM/FFI/JSI/JS)
- Extend crypto provider with 15 new delegating functions for the
unified backend interface
FFI use-after-free fix (critical)
─────────────────────────────────
- Fix Bun segfault / SIGILL crash when using CRYPTO_BACKEND=ffi:
toArrayBuffer() from bun:ffi returns a zero-copy VIEW of Rust-owned
heap memory. Calling salvium_storage_free_buf() afterwards left the
Uint8Array pointing to freed memory — classic use-after-free.
With small buffers the freed memory wasn't reused (masking the bug);
at 50×200 subaddress scale (~400KB) the allocator reused immediately,
corrupting data and crashing with SIGILL at 1.11GB RSS.
- Fix: .slice() after toArrayBuffer() to copy into JS-owned memory
before freeing the Rust buffer. Applied to all 4 affected call sites:
cnSubaddressMapBatch, carrotSubaddressMapBatch, parseExtra,
serializeTxExtra.
Subaddress batch fallback hardening
────────────────────────────────────
- Log warnings (not silent catch) when batch WASM/FFI calls fail,
so the fallback to per-item JS loop is visible in console output
Rust crate (salvium-crypto)
───────────────────────────
- Add crates/salvium-crypto/src/subaddress.rs — batch CN + CARROT
subaddress map generation with #[wasm_bindgen] + #[no_mangle] FFI
- Add crates/salvium-crypto/src/carrot_keys.rs — batch CARROT key
derivation (full + view-only) from master secret
- Add crates/salvium-crypto/src/tx_format.rs — tx_extra parse/serialize
and tx prefix hash computation
- Extend ffi.rs with 15 new extern "C" entry points for the above
- Extend lib.rs with matching #[wasm_bindgen] entry points
Test suite updates
──────────────────
- Add initCrypto() to all test files that perform key derivation or
EC math (wallet-class, persistent-wallet, integration-sync, keys,
keyimage, subaddress, scanning, address, transaction, etc.)
- Fix integration-sync.test.js: always init WASM unless FFI explicitly
requested (JS backend can't do scalar/point ops post-deprecation)
- Rewrite test/full-testnet.js: replace hardcoded phase functions with
data-driven FORKS[] table covering all 10 hard forks (HF1–HF10),
WASM miner probes at every fork boundary, per-fork TX tests with
era-appropriate asset types and address formats, --resume-from and
--skip-mining CLI flags
README
──────
- Update JS backend description: "Hashing only (keccak, blake2b)"
- Add initCrypto() requirement to Quick Start and all code examples
- Add full-testnet commands to Testing section
779 lines
18 KiB
JavaScript
779 lines
18 KiB
JavaScript
#!/usr/bin/env bun
|
|
/**
|
|
* Persistent Wallet Tests
|
|
*
|
|
* Tests for persistent-wallet.js:
|
|
* - PersistentWallet class
|
|
* - Storage integration
|
|
* - Balance calculation
|
|
* - Transaction creation
|
|
*/
|
|
|
|
import {
|
|
PersistentWallet,
|
|
createPersistentWallet,
|
|
restorePersistentWallet,
|
|
openPersistentWallet
|
|
} from '../src/persistent-wallet.js';
|
|
|
|
import { MemoryStorage, WalletOutput, WalletTransaction } from '../src/wallet-store.js';
|
|
import { generateSeed, deriveKeys } from '../src/carrot.js';
|
|
import { mnemonicToSeed, seedToMnemonic } from '../src/mnemonic.js';
|
|
import { initCrypto } from '../src/crypto/index.js';
|
|
|
|
await initCrypto();
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` ✓ ${name}`);
|
|
passed++;
|
|
} catch (e) {
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` Error: ${e.message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
async function testAsync(name, fn) {
|
|
try {
|
|
await fn();
|
|
console.log(` ✓ ${name}`);
|
|
passed++;
|
|
} catch (e) {
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` Error: ${e.message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
function assert(condition, message) {
|
|
if (!condition) throw new Error(message || 'Assertion failed');
|
|
}
|
|
|
|
function assertEqual(actual, expected, message) {
|
|
if (actual !== expected) {
|
|
throw new Error(message || `Expected ${expected}, got ${actual}`);
|
|
}
|
|
}
|
|
|
|
// Mock daemon for testing
|
|
class MockDaemon {
|
|
constructor(options = {}) {
|
|
this.height = options.height || 1000;
|
|
}
|
|
|
|
async getInfo() {
|
|
return {
|
|
success: true,
|
|
result: { height: this.height, status: 'OK' }
|
|
};
|
|
}
|
|
|
|
async getBlockHeadersRange(start, end) {
|
|
return { success: true, result: { headers: [] } };
|
|
}
|
|
|
|
async getBlock(opts) {
|
|
return {
|
|
success: true,
|
|
result: { tx_hashes: [], miner_tx_hash: 'miner' }
|
|
};
|
|
}
|
|
|
|
async getTransactions() {
|
|
return { success: true, result: { txs: [] } };
|
|
}
|
|
|
|
async sendRawTransaction(blob) {
|
|
return { success: true, result: { status: 'OK' } };
|
|
}
|
|
|
|
async getOutputDistribution() {
|
|
return {
|
|
success: true,
|
|
result: { distributions: [{ amount: 1000000 }] }
|
|
};
|
|
}
|
|
|
|
async getOuts(indices) {
|
|
return {
|
|
success: true,
|
|
result: {
|
|
outs: indices.map(i => ({
|
|
key: 'aa'.repeat(32),
|
|
mask: 'bb'.repeat(32)
|
|
}))
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
console.log('=== Persistent Wallet Tests ===\n');
|
|
|
|
// ============================================================================
|
|
// Construction Tests
|
|
// ============================================================================
|
|
|
|
console.log('--- Construction ---');
|
|
|
|
test('creates PersistentWallet with options', () => {
|
|
const seed = generateSeed();
|
|
const keys = deriveKeys(seed);
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage: { type: 'memory' },
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
assert(wallet !== undefined);
|
|
assert(!wallet.isOpen());
|
|
});
|
|
|
|
test('creates with custom storage instance', () => {
|
|
const storage = new MemoryStorage();
|
|
const seed = generateSeed();
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage,
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
assertEqual(wallet._storage, storage);
|
|
});
|
|
|
|
test('creates with custom daemon instance', () => {
|
|
const daemon = new MockDaemon({ height: 5000 });
|
|
const seed = generateSeed();
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage: { type: 'memory' },
|
|
daemon
|
|
});
|
|
|
|
assertEqual(wallet._daemon, daemon);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Lifecycle Tests
|
|
// ============================================================================
|
|
|
|
console.log('\n--- Lifecycle ---');
|
|
|
|
await testAsync('open initializes wallet', async () => {
|
|
const seed = generateSeed();
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage: { type: 'memory' },
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
assert(!wallet.isOpen());
|
|
await wallet.open();
|
|
assert(wallet.isOpen());
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('close cleans up', async () => {
|
|
const seed = generateSeed();
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage: { type: 'memory' },
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
await wallet.open();
|
|
assert(wallet.isOpen());
|
|
|
|
await wallet.close();
|
|
assert(!wallet.isOpen());
|
|
});
|
|
|
|
await testAsync('open is idempotent', async () => {
|
|
const seed = generateSeed();
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage: { type: 'memory' },
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
await wallet.open();
|
|
await wallet.open(); // Should not throw
|
|
assert(wallet.isOpen());
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('close is idempotent', async () => {
|
|
const seed = generateSeed();
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage: { type: 'memory' },
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
await wallet.open();
|
|
await wallet.close();
|
|
await wallet.close(); // Should not throw
|
|
assert(!wallet.isOpen());
|
|
});
|
|
|
|
// ============================================================================
|
|
// Balance Tests
|
|
// ============================================================================
|
|
|
|
console.log('\n--- Balance ---');
|
|
|
|
await testAsync('getBalance returns 0 for empty wallet', async () => {
|
|
const seed = generateSeed();
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage: { type: 'memory' },
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
await wallet.open();
|
|
const balance = await wallet.getBalance('SAL1');
|
|
assertEqual(balance, 0n);
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('getBalance sums unspent outputs', async () => {
|
|
const seed = generateSeed();
|
|
const storage = new MemoryStorage();
|
|
await storage.open();
|
|
|
|
// Add some outputs
|
|
await storage.putOutput(new WalletOutput({
|
|
keyImage: 'ki1',
|
|
amount: 1000000000n,
|
|
assetType: 'SAL1',
|
|
isSpent: false,
|
|
blockHeight: 100
|
|
}));
|
|
await storage.putOutput(new WalletOutput({
|
|
keyImage: 'ki2',
|
|
amount: 2000000000n,
|
|
assetType: 'SAL1',
|
|
isSpent: false,
|
|
blockHeight: 101
|
|
}));
|
|
await storage.putOutput(new WalletOutput({
|
|
keyImage: 'ki3',
|
|
amount: 500000000n,
|
|
assetType: 'SAL1',
|
|
isSpent: true, // Spent, should not count
|
|
blockHeight: 102
|
|
}));
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage,
|
|
daemon: new MockDaemon({ height: 200 })
|
|
});
|
|
|
|
await wallet.open();
|
|
const balance = await wallet.getBalance('SAL1');
|
|
|
|
// 1000000000 + 2000000000 = 3000000000
|
|
assertEqual(balance, 3000000000n);
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('getBalance filters by asset type', async () => {
|
|
const seed = generateSeed();
|
|
const storage = new MemoryStorage();
|
|
await storage.open();
|
|
|
|
await storage.putOutput(new WalletOutput({
|
|
keyImage: 'ki1',
|
|
amount: 1000n,
|
|
assetType: 'SAL',
|
|
isSpent: false,
|
|
blockHeight: 100
|
|
}));
|
|
await storage.putOutput(new WalletOutput({
|
|
keyImage: 'ki2',
|
|
amount: 2000n,
|
|
assetType: 'USD',
|
|
isSpent: false,
|
|
blockHeight: 101
|
|
}));
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage,
|
|
daemon: new MockDaemon({ height: 200 })
|
|
});
|
|
|
|
await wallet.open();
|
|
|
|
const salBalance = await wallet.getBalance('SAL');
|
|
const usdBalance = await wallet.getBalance('USD');
|
|
|
|
assertEqual(salBalance, 1000n);
|
|
assertEqual(usdBalance, 2000n);
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('getUnlockedBalance excludes locked outputs', async () => {
|
|
const seed = generateSeed();
|
|
const storage = new MemoryStorage();
|
|
await storage.open();
|
|
|
|
// Unlocked output (old enough)
|
|
await storage.putOutput(new WalletOutput({
|
|
keyImage: 'ki1',
|
|
amount: 1000n,
|
|
assetType: 'SAL1',
|
|
isSpent: false,
|
|
blockHeight: 100,
|
|
unlockTime: 0n
|
|
}));
|
|
|
|
// Locked output (too recent)
|
|
await storage.putOutput(new WalletOutput({
|
|
keyImage: 'ki2',
|
|
amount: 2000n,
|
|
assetType: 'SAL1',
|
|
isSpent: false,
|
|
blockHeight: 195, // Only 5 blocks old at height 200
|
|
unlockTime: 0n
|
|
}));
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage,
|
|
daemon: new MockDaemon({ height: 200 })
|
|
});
|
|
|
|
await wallet.open();
|
|
|
|
const balance = await wallet.getBalance('SAL1');
|
|
const unlocked = await wallet.getUnlockedBalance('SAL1');
|
|
|
|
assertEqual(balance, 3000n);
|
|
assertEqual(unlocked, 1000n); // Only the old output
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('getBalances returns all asset types', async () => {
|
|
const seed = generateSeed();
|
|
const storage = new MemoryStorage();
|
|
await storage.open();
|
|
|
|
await storage.putOutput(new WalletOutput({
|
|
keyImage: 'ki1', amount: 100n, assetType: 'SAL', isSpent: false, blockHeight: 10
|
|
}));
|
|
await storage.putOutput(new WalletOutput({
|
|
keyImage: 'ki2', amount: 200n, assetType: 'USD', isSpent: false, blockHeight: 10
|
|
}));
|
|
await storage.putOutput(new WalletOutput({
|
|
keyImage: 'ki3', amount: 300n, assetType: 'EUR', isSpent: false, blockHeight: 10
|
|
}));
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage,
|
|
daemon: new MockDaemon({ height: 200 })
|
|
});
|
|
|
|
await wallet.open();
|
|
const balances = await wallet.getBalances();
|
|
|
|
assert(balances.has('SAL'));
|
|
assert(balances.has('USD'));
|
|
assert(balances.has('EUR'));
|
|
assertEqual(balances.get('SAL').balance, 100n);
|
|
assertEqual(balances.get('USD').balance, 200n);
|
|
assertEqual(balances.get('EUR').balance, 300n);
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
// ============================================================================
|
|
// Output Tests
|
|
// ============================================================================
|
|
|
|
console.log('\n--- Outputs ---');
|
|
|
|
await testAsync('getOutputs returns all outputs', async () => {
|
|
const seed = generateSeed();
|
|
const storage = new MemoryStorage();
|
|
await storage.open();
|
|
|
|
await storage.putOutput(new WalletOutput({ keyImage: 'ki1', amount: 100n }));
|
|
await storage.putOutput(new WalletOutput({ keyImage: 'ki2', amount: 200n }));
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage,
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
await wallet.open();
|
|
const outputs = await wallet.getOutputs();
|
|
|
|
assertEqual(outputs.length, 2);
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('getUnspentOutputs filters spent', async () => {
|
|
const seed = generateSeed();
|
|
const storage = new MemoryStorage();
|
|
await storage.open();
|
|
|
|
await storage.putOutput(new WalletOutput({ keyImage: 'ki1', amount: 100n, isSpent: false }));
|
|
await storage.putOutput(new WalletOutput({ keyImage: 'ki2', amount: 200n, isSpent: true }));
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage,
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
await wallet.open();
|
|
const outputs = await wallet.getUnspentOutputs();
|
|
|
|
assertEqual(outputs.length, 1);
|
|
assertEqual(outputs[0].keyImage, 'ki1');
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('freezeOutput prevents spending', async () => {
|
|
const seed = generateSeed();
|
|
const storage = new MemoryStorage();
|
|
await storage.open();
|
|
|
|
await storage.putOutput(new WalletOutput({
|
|
keyImage: 'ki1',
|
|
amount: 100n,
|
|
isSpent: false,
|
|
isFrozen: false,
|
|
blockHeight: 10
|
|
}));
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage,
|
|
daemon: new MockDaemon({ height: 200 })
|
|
});
|
|
|
|
await wallet.open();
|
|
|
|
// Before freeze
|
|
let spendable = await wallet.getSpendableOutputs();
|
|
assertEqual(spendable.length, 1);
|
|
|
|
// Freeze
|
|
await wallet.freezeOutput('ki1');
|
|
|
|
// After freeze
|
|
spendable = await wallet.getSpendableOutputs();
|
|
assertEqual(spendable.length, 0);
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('thawOutput allows spending again', async () => {
|
|
const seed = generateSeed();
|
|
const storage = new MemoryStorage();
|
|
await storage.open();
|
|
|
|
await storage.putOutput(new WalletOutput({
|
|
keyImage: 'ki1',
|
|
amount: 100n,
|
|
isSpent: false,
|
|
isFrozen: true,
|
|
blockHeight: 10
|
|
}));
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage,
|
|
daemon: new MockDaemon({ height: 200 })
|
|
});
|
|
|
|
await wallet.open();
|
|
|
|
// Before thaw
|
|
let spendable = await wallet.getSpendableOutputs();
|
|
assertEqual(spendable.length, 0);
|
|
|
|
// Thaw
|
|
await wallet.thawOutput('ki1');
|
|
|
|
// After thaw
|
|
spendable = await wallet.getSpendableOutputs();
|
|
assertEqual(spendable.length, 1);
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
// ============================================================================
|
|
// Transaction Tests
|
|
// ============================================================================
|
|
|
|
console.log('\n--- Transactions ---');
|
|
|
|
await testAsync('getTransactions returns all transactions', async () => {
|
|
const seed = generateSeed();
|
|
const storage = new MemoryStorage();
|
|
await storage.open();
|
|
|
|
await storage.putTransaction(new WalletTransaction({ txHash: 'tx1', blockHeight: 100 }));
|
|
await storage.putTransaction(new WalletTransaction({ txHash: 'tx2', blockHeight: 101 }));
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage,
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
await wallet.open();
|
|
const txs = await wallet.getTransactions();
|
|
|
|
assertEqual(txs.length, 2);
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('getTransaction returns specific tx', async () => {
|
|
const seed = generateSeed();
|
|
const storage = new MemoryStorage();
|
|
await storage.open();
|
|
|
|
await storage.putTransaction(new WalletTransaction({
|
|
txHash: 'specific_tx',
|
|
blockHeight: 500,
|
|
incomingAmount: 1000000n
|
|
}));
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage,
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
await wallet.open();
|
|
const tx = await wallet.getTransaction('specific_tx');
|
|
|
|
assertEqual(tx.txHash, 'specific_tx');
|
|
assertEqual(tx.blockHeight, 500);
|
|
assertEqual(tx.incomingAmount, 1000000n);
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('getTransaction returns null for nonexistent', async () => {
|
|
const seed = generateSeed();
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage: { type: 'memory' },
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
await wallet.open();
|
|
const tx = await wallet.getTransaction('nonexistent');
|
|
|
|
assertEqual(tx, null);
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('setTransactionNote updates note', async () => {
|
|
const seed = generateSeed();
|
|
const storage = new MemoryStorage();
|
|
await storage.open();
|
|
|
|
await storage.putTransaction(new WalletTransaction({
|
|
txHash: 'tx_with_note',
|
|
blockHeight: 100
|
|
}));
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage,
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
await wallet.open();
|
|
|
|
await wallet.setTransactionNote('tx_with_note', 'Payment for coffee');
|
|
const tx = await wallet.getTransaction('tx_with_note');
|
|
|
|
assertEqual(tx.note, 'Payment for coffee');
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
// ============================================================================
|
|
// Sync Height Tests
|
|
// ============================================================================
|
|
|
|
console.log('\n--- Sync Height ---');
|
|
|
|
await testAsync('getSyncHeight returns stored height', async () => {
|
|
const seed = generateSeed();
|
|
const storage = new MemoryStorage();
|
|
await storage.open();
|
|
await storage.setSyncHeight(5000);
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage,
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
await wallet.open();
|
|
const height = await wallet.getSyncHeight();
|
|
|
|
assertEqual(height, 5000);
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('getDaemonHeight returns daemon height', async () => {
|
|
const seed = generateSeed();
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage: { type: 'memory' },
|
|
daemon: new MockDaemon({ height: 12345 })
|
|
});
|
|
|
|
await wallet.open();
|
|
const height = await wallet.getDaemonHeight();
|
|
|
|
assertEqual(height, 12345);
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('isSynced returns true when caught up', async () => {
|
|
const seed = generateSeed();
|
|
const storage = new MemoryStorage();
|
|
await storage.open();
|
|
await storage.setSyncHeight(1000);
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage,
|
|
daemon: new MockDaemon({ height: 1000 })
|
|
});
|
|
|
|
await wallet.open();
|
|
const synced = await wallet.isSynced();
|
|
|
|
assert(synced);
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('isSynced returns false when behind', async () => {
|
|
const seed = generateSeed();
|
|
const storage = new MemoryStorage();
|
|
await storage.open();
|
|
await storage.setSyncHeight(500);
|
|
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage,
|
|
daemon: new MockDaemon({ height: 1000 })
|
|
});
|
|
|
|
await wallet.open();
|
|
const synced = await wallet.isSynced();
|
|
|
|
assert(!synced);
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
// ============================================================================
|
|
// Error Handling Tests
|
|
// ============================================================================
|
|
|
|
console.log('\n--- Error Handling ---');
|
|
|
|
await testAsync('operations throw when not open', async () => {
|
|
const seed = generateSeed();
|
|
const wallet = new PersistentWallet({
|
|
seed,
|
|
storage: { type: 'memory' },
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
let threw = false;
|
|
try {
|
|
await wallet.getBalance('SAL1');
|
|
} catch (e) {
|
|
threw = true;
|
|
assert(e.message.includes('not open'));
|
|
}
|
|
assert(threw, 'Should throw when not open');
|
|
});
|
|
|
|
// ============================================================================
|
|
// Factory Function Tests
|
|
// ============================================================================
|
|
|
|
console.log('\n--- Factory Functions ---');
|
|
|
|
await testAsync('createPersistentWallet creates and opens', async () => {
|
|
const seed = generateSeed();
|
|
const wallet = await createPersistentWallet({
|
|
seed,
|
|
storage: { type: 'memory' },
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
assert(wallet instanceof PersistentWallet);
|
|
assert(wallet.isOpen());
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
await testAsync('restorePersistentWallet restores from mnemonic', async () => {
|
|
const seed = generateSeed();
|
|
const mnemonic = seedToMnemonic(seed);
|
|
|
|
const wallet = await restorePersistentWallet(mnemonic, {
|
|
storage: { type: 'memory' },
|
|
daemon: new MockDaemon()
|
|
});
|
|
|
|
assert(wallet instanceof PersistentWallet);
|
|
assert(wallet.isOpen());
|
|
|
|
await wallet.close();
|
|
});
|
|
|
|
// ============================================================================
|
|
// Summary
|
|
// ============================================================================
|
|
|
|
console.log('\n--- Summary ---');
|
|
console.log(`Passed: ${passed}`);
|
|
console.log(`Failed: ${failed}`);
|
|
|
|
if (failed > 0) {
|
|
console.log('\n✗ Some tests failed!');
|
|
process.exit(1);
|
|
} else {
|
|
console.log('\n✓ All persistent wallet tests passed!');
|
|
process.exit(0);
|
|
}
|