1099 lines
34 KiB
Markdown
1099 lines
34 KiB
Markdown
# salvium-js
|
|
|
|
JavaScript library for Salvium cryptocurrency - wallet generation, address handling, RPC clients, key derivation, and cryptographic utilities.
|
|
|
|
## Features
|
|
|
|
- **Wallet Generation** - Generate seeds, derive keys, create addresses
|
|
- **Mnemonic Support** - 25-word seed phrases in 12 languages
|
|
- **Key Derivation** - CryptoNote and CARROT key derivation from seeds
|
|
- **Address Creation** - Create all 18 Salvium address types
|
|
- **Subaddress Generation** - Generate CryptoNote and CARROT subaddresses
|
|
- **Integrated Addresses** - Create and parse integrated addresses with payment IDs
|
|
- **Address Validation** - Parse and validate any Salvium address
|
|
- **Transaction Scanning** - Detect owned outputs, decrypt amounts, view tags
|
|
- **Key Images** - Generate and validate key images for spend detection
|
|
- **Transaction Construction** - Pedersen commitments, CLSAG/TCLSAG signatures, serialization
|
|
- **Bulletproofs+ Range Proofs** - Pure JS proof generation AND verification (mobile-friendly)
|
|
- **RandomX Proof-of-Work** - WASM-JIT implementation with official test vectors
|
|
- **Stratum Mining Client** - Connect to mining pools with multi-threaded hashing
|
|
- **Wallet Class** - Unified wallet management with view-only mode support
|
|
- **UTXO Selection** - Multiple strategies (minimize inputs, minimize change, etc.)
|
|
- **Transaction Builder** - High-level buildTransaction() and signTransaction() API
|
|
- **Transaction Parser** - Decode and analyze transaction data
|
|
- **RPC Clients** - Full daemon and wallet RPC implementations
|
|
- **Signature Verification** - Verify message signatures (V1 and V2 formats)
|
|
- **Cryptographic Primitives** - Blake2b, Keccak-256, Ed25519, Base58
|
|
- **Multi-Network Support** - Mainnet, Testnet, Stagenet
|
|
- **TypeScript Support** - Full type definitions included
|
|
- **Minimal Dependencies** - Only @noble/curves and @noble/hashes
|
|
|
|
## Address Types Supported
|
|
|
|
| Network | Format | Standard | Integrated | Subaddress |
|
|
|---------|--------|----------|------------|------------|
|
|
| Mainnet | Legacy | SaLv... | SaLvi... | SaLvs... |
|
|
| Mainnet | CARROT | SC1... | SC1i... | SC1s... |
|
|
| Testnet | Legacy | SaLvT... | SaLvTi... | SaLvTs... |
|
|
| Testnet | CARROT | SC1T... | SC1Ti... | SC1Ts... |
|
|
| Stagenet | Legacy | SaLvS... | SaLvSi... | SaLvSs... |
|
|
| Stagenet | CARROT | SC1S... | SC1Si... | SC1Ss... |
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
npm install salvium-js
|
|
```
|
|
|
|
Or include directly in browser:
|
|
|
|
```html
|
|
<script type="module">
|
|
import salvium from './salvium-js/src/index.js';
|
|
</script>
|
|
```
|
|
|
|
## Quick Start: Generate a Wallet
|
|
|
|
```javascript
|
|
import {
|
|
generateSeed,
|
|
seedToMnemonic,
|
|
deriveKeys,
|
|
createAddress,
|
|
generateCNSubaddress,
|
|
bytesToHex
|
|
} from 'salvium-js';
|
|
|
|
// 1. Generate a cryptographically secure random seed
|
|
const seed = generateSeed(); // 32 random bytes
|
|
|
|
// 2. Convert to 25-word mnemonic for backup
|
|
const mnemonic = seedToMnemonic(seed, { language: 'english' });
|
|
console.log('Backup these words:', mnemonic);
|
|
|
|
// 3. Derive wallet keys from seed
|
|
const keys = deriveKeys(seed);
|
|
// {
|
|
// spendSecretKey: Uint8Array(32),
|
|
// spendPublicKey: Uint8Array(32),
|
|
// viewSecretKey: Uint8Array(32),
|
|
// viewPublicKey: Uint8Array(32)
|
|
// }
|
|
|
|
// 4. Create main wallet address
|
|
const address = createAddress({
|
|
network: 'mainnet',
|
|
format: 'legacy',
|
|
type: 'standard',
|
|
spendPublicKey: keys.spendPublicKey,
|
|
viewPublicKey: keys.viewPublicKey
|
|
});
|
|
console.log('Main address:', address); // SaLv...
|
|
|
|
// 5. Generate subaddresses
|
|
const subaddress = generateCNSubaddress({
|
|
network: 'mainnet',
|
|
spendPublicKey: keys.spendPublicKey,
|
|
viewSecretKey: keys.viewSecretKey,
|
|
major: 0, // account index
|
|
minor: 1 // address index
|
|
});
|
|
console.log('Subaddress:', subaddress.address); // SaLvs...
|
|
|
|
// 6. Display secret keys (keep these safe!)
|
|
console.log('Spend secret key:', bytesToHex(keys.spendSecretKey));
|
|
console.log('View secret key:', bytesToHex(keys.viewSecretKey));
|
|
```
|
|
|
|
## Restore Wallet from Mnemonic
|
|
|
|
```javascript
|
|
import { mnemonicToSeed, deriveKeys, createAddress } from 'salvium-js';
|
|
|
|
const mnemonic = 'abbey ability able about above absent ...'; // 25 words
|
|
|
|
const result = mnemonicToSeed(mnemonic, { language: 'english' });
|
|
if (result.valid) {
|
|
const keys = deriveKeys(result.seed);
|
|
const address = createAddress({
|
|
network: 'mainnet',
|
|
format: 'legacy',
|
|
type: 'standard',
|
|
spendPublicKey: keys.spendPublicKey,
|
|
viewPublicKey: keys.viewPublicKey
|
|
});
|
|
console.log('Restored address:', address);
|
|
} else {
|
|
console.error('Invalid mnemonic:', result.error);
|
|
}
|
|
```
|
|
|
|
## CARROT Address Format
|
|
|
|
```javascript
|
|
import {
|
|
generateSeed,
|
|
deriveKeys,
|
|
deriveCarrotKeys,
|
|
createAddress,
|
|
generateCarrotSubaddress,
|
|
hexToBytes,
|
|
scalarMultPoint
|
|
} from 'salvium-js';
|
|
|
|
const seed = generateSeed();
|
|
|
|
// Derive CryptoNote keys (for public keys)
|
|
const keys = deriveKeys(seed);
|
|
|
|
// Derive CARROT-specific keys
|
|
const carrotKeys = deriveCarrotKeys(seed);
|
|
// {
|
|
// masterSecret: '...', // hex string
|
|
// proveSpendKey: '...', // k_ps
|
|
// viewBalanceSecret: '...', // s_vb
|
|
// generateImageKey: '...', // k_gi
|
|
// viewIncomingKey: '...', // k_vi
|
|
// generateAddressSecret: '...' // s_ga
|
|
// }
|
|
|
|
// Create CARROT main address
|
|
const carrotAddress = createAddress({
|
|
network: 'mainnet',
|
|
format: 'carrot',
|
|
type: 'standard',
|
|
spendPublicKey: keys.spendPublicKey,
|
|
viewPublicKey: keys.viewPublicKey
|
|
});
|
|
console.log('CARROT address:', carrotAddress); // SC1...
|
|
|
|
// Generate CARROT subaddress
|
|
const carrotSub = generateCarrotSubaddress({
|
|
network: 'mainnet',
|
|
accountSpendPubkey: keys.spendPublicKey,
|
|
accountViewPubkey: keys.viewPublicKey,
|
|
generateAddressSecret: hexToBytes(carrotKeys.generateAddressSecret),
|
|
major: 0,
|
|
minor: 1
|
|
});
|
|
console.log('CARROT subaddress:', carrotSub.address); // SC1s...
|
|
```
|
|
|
|
## Integrated Addresses (with Payment ID)
|
|
|
|
```javascript
|
|
import {
|
|
toIntegratedAddress,
|
|
toStandardAddress,
|
|
getPaymentId,
|
|
generateRandomPaymentId,
|
|
createIntegratedAddressWithRandomId,
|
|
bytesToHex
|
|
} from 'salvium-js';
|
|
|
|
// Create integrated address with specific payment ID
|
|
const integrated = toIntegratedAddress('SaLv...', 'deadbeef12345678');
|
|
console.log('Integrated:', integrated); // SaLvi...
|
|
|
|
// Create with random payment ID
|
|
const result = createIntegratedAddressWithRandomId('SaLv...');
|
|
console.log('Integrated:', result.address);
|
|
console.log('Payment ID:', result.paymentIdHex);
|
|
|
|
// Extract payment ID from integrated address
|
|
const paymentId = getPaymentId(integrated);
|
|
console.log('Payment ID:', bytesToHex(paymentId));
|
|
|
|
// Get standard address from integrated
|
|
const standard = toStandardAddress(integrated);
|
|
```
|
|
|
|
## Validate and Parse Addresses
|
|
|
|
```javascript
|
|
import { isValidAddress, parseAddress, describeAddress } from 'salvium-js';
|
|
|
|
// Simple validation
|
|
if (isValidAddress('SC1...')) {
|
|
console.log('Valid!');
|
|
}
|
|
|
|
// Detailed parsing
|
|
const info = parseAddress('SC1...');
|
|
// {
|
|
// valid: true,
|
|
// network: 'mainnet',
|
|
// format: 'carrot',
|
|
// type: 'standard',
|
|
// prefix: 'SC1',
|
|
// spendPublicKey: Uint8Array(32),
|
|
// viewPublicKey: Uint8Array(32),
|
|
// paymentId: null,
|
|
// error: null
|
|
// }
|
|
|
|
// Human-readable description
|
|
console.log(describeAddress('SaLvi...'));
|
|
// "Mainnet Legacy integrated (Payment ID: abcd1234...)"
|
|
```
|
|
|
|
## Check Address Properties
|
|
|
|
```javascript
|
|
import {
|
|
isMainnet, isTestnet, isStagenet,
|
|
isCarrot, isLegacy,
|
|
isStandard, isIntegrated, isSubaddress
|
|
} from 'salvium-js';
|
|
|
|
const addr = 'SC1...';
|
|
|
|
isMainnet(addr); // true
|
|
isCarrot(addr); // true
|
|
isStandard(addr); // true
|
|
isSubaddress(addr); // false
|
|
```
|
|
|
|
## Mnemonic Seeds
|
|
|
|
```javascript
|
|
import {
|
|
seedToMnemonic,
|
|
mnemonicToSeed,
|
|
validateMnemonic,
|
|
detectLanguage,
|
|
getAvailableLanguages
|
|
} from 'salvium-js';
|
|
|
|
// Generate mnemonic from seed
|
|
const mnemonic = seedToMnemonic(seed, { language: 'english' });
|
|
// 25 words
|
|
|
|
// Convert mnemonic back to seed
|
|
const result = mnemonicToSeed(mnemonic, { language: 'english' });
|
|
if (result.valid) {
|
|
console.log('Seed:', result.seed);
|
|
}
|
|
|
|
// Auto-detect language
|
|
const detected = detectLanguage(mnemonic);
|
|
console.log('Language:', detected.language.name);
|
|
|
|
// Validate without converting
|
|
const validation = validateMnemonic(mnemonic, { language: 'auto' });
|
|
console.log('Valid:', validation.valid);
|
|
|
|
// Available languages
|
|
console.log(getAvailableLanguages());
|
|
// ['english', 'spanish', 'french', 'italian', 'german', 'portuguese',
|
|
// 'russian', 'japanese', 'chinese_simplified', 'dutch', 'esperanto', 'lojban']
|
|
```
|
|
|
|
## RPC Clients
|
|
|
|
Full-featured RPC clients for Salvium daemon and wallet services.
|
|
|
|
### Default Ports (from cryptonote_config.h)
|
|
|
|
| Service | Mainnet | Testnet | Stagenet |
|
|
|---------|---------|---------|----------|
|
|
| Daemon RPC | 19081 | 29081 | 39081 |
|
|
| ZMQ RPC | 19083 | 29083 | 39083 |
|
|
| Wallet RPC* | 19083 | 29083 | 39083 |
|
|
|
|
*Wallet RPC has no default in source - port is user-specified, conventionally daemon+1
|
|
|
|
### Daemon RPC
|
|
|
|
```javascript
|
|
import { createDaemonRPC } from 'salvium-js/rpc';
|
|
|
|
const daemon = createDaemonRPC({ url: 'http://localhost:19081' });
|
|
|
|
// Get node info
|
|
const info = await daemon.getInfo();
|
|
if (info.success) {
|
|
console.log('Height:', info.result.height);
|
|
console.log('Network hashrate:', info.result.difficulty / 120);
|
|
console.log('Synchronized:', info.result.synchronized);
|
|
}
|
|
|
|
// Get block by height
|
|
const block = await daemon.getBlockHeaderByHeight(100000);
|
|
|
|
// Get transactions
|
|
const txs = await daemon.getTransactions(['txhash1', 'txhash2']);
|
|
|
|
// Get fee estimate
|
|
const fee = await daemon.getFeeEstimate();
|
|
console.log('Fee per byte:', fee.result.fee);
|
|
|
|
// Mining
|
|
const template = await daemon.getBlockTemplate({
|
|
wallet_address: 'SaLv...',
|
|
reserve_size: 8
|
|
});
|
|
```
|
|
|
|
### Wallet RPC
|
|
|
|
```javascript
|
|
import { createWalletRPC, PRIORITY } from 'salvium-js/rpc';
|
|
|
|
const wallet = createWalletRPC({
|
|
url: 'http://localhost:19083',
|
|
username: 'user',
|
|
password: 'pass'
|
|
});
|
|
|
|
// Open wallet
|
|
await wallet.openWallet({ filename: 'mywallet', password: 'secret' });
|
|
|
|
// Get balance (amounts in atomic units, divide by 1e8 for SAL)
|
|
const balance = await wallet.getBalance();
|
|
if (balance.success) {
|
|
console.log('Balance:', balance.result.balance / 1e8, 'SAL');
|
|
console.log('Unlocked:', balance.result.unlocked_balance / 1e8, 'SAL');
|
|
}
|
|
|
|
// Get address
|
|
const addr = await wallet.getAddress();
|
|
console.log('Address:', addr.result.address);
|
|
|
|
// Send transaction (1 SAL = 100000000 atomic units)
|
|
const tx = await wallet.transfer({
|
|
destinations: [{ address: 'SaLv...', amount: 100000000 }],
|
|
priority: PRIORITY.NORMAL
|
|
});
|
|
|
|
// Get transaction history
|
|
const transfers = await wallet.getTransfers({ in: true, out: true });
|
|
|
|
// Create subaddress
|
|
const newAddr = await wallet.createAddress({ account_index: 0 });
|
|
|
|
// Close wallet
|
|
await wallet.closeWallet();
|
|
```
|
|
|
|
### RPC Client Options
|
|
|
|
```javascript
|
|
const daemon = createDaemonRPC({
|
|
url: 'http://localhost:19081',
|
|
timeout: 30000, // Request timeout in ms (default: 30000)
|
|
retries: 2, // Retry attempts (default: 2)
|
|
retryDelay: 1000, // Delay between retries in ms (default: 1000)
|
|
username: 'user', // HTTP basic auth username
|
|
password: 'pass' // HTTP basic auth password
|
|
});
|
|
```
|
|
|
|
### Available Daemon RPC Methods
|
|
|
|
- **Network**: `getInfo`, `getHeight`, `syncInfo`, `hardForkInfo`, `getNetStats`, `getConnections`, `getPeerList`
|
|
- **Blocks**: `getBlockHash`, `getBlock`, `getBlockHeaderByHash`, `getBlockHeaderByHeight`, `getBlockHeadersRange`, `getLastBlockHeader`
|
|
- **Transactions**: `getTransactions`, `getTransactionPool`, `sendRawTransaction`, `relayTx`
|
|
- **Outputs**: `getOuts`, `getOutputHistogram`, `getOutputDistribution`, `isKeyImageSpent`
|
|
- **Mining**: `getBlockTemplate`, `submitBlock`, `getMinerData`, `calcPow`
|
|
- **Fees**: `getFeeEstimate`, `getBaseFeeEstimate`, `getCoinbaseTxSum`
|
|
|
|
### Available Wallet RPC Methods
|
|
|
|
- **Wallet**: `createWallet`, `openWallet`, `closeWallet`, `restoreDeterministicWallet`, `generateFromKeys`
|
|
- **Accounts**: `getAccounts`, `createAccount`, `labelAccount`, `getAddress`, `createAddress`
|
|
- **Balance**: `getBalance`, `getTransfers`, `getTransferByTxid`, `incomingTransfers`
|
|
- **Transfers**: `transfer`, `transferSplit`, `sweepAll`, `sweepSingle`, `sweepDust`
|
|
- **Proofs**: `getTxKey`, `checkTxKey`, `getTxProof`, `checkTxProof`, `getReserveProof`
|
|
- **Keys**: `queryKey`, `getMnemonic`, `exportOutputs`, `importOutputs`, `exportKeyImages`
|
|
- **Signing**: `sign`, `verify`, `signMultisig`, `submitMultisig`
|
|
|
|
## Transaction Scanning
|
|
|
|
Detect owned outputs in transactions and decrypt amounts.
|
|
|
|
```javascript
|
|
import {
|
|
generateKeyDerivation,
|
|
derivePublicKey,
|
|
deriveSecretKey,
|
|
checkOutputOwnership,
|
|
scanOutput,
|
|
ecdhDecode,
|
|
deriveViewTag
|
|
} from 'salvium-js';
|
|
|
|
// Given a transaction with tx public key R and your view secret key a:
|
|
const derivation = generateKeyDerivation(txPubKey, viewSecretKey);
|
|
|
|
// Check if output at index n belongs to you
|
|
const outputKey = tx.outputs[n].target;
|
|
const owned = checkOutputOwnership(derivation, n, spendPublicKey, outputKey);
|
|
|
|
if (owned) {
|
|
// Decrypt the amount
|
|
const amount = ecdhDecode(encryptedAmount, derivation, n);
|
|
console.log('Received:', amount, 'atomic units');
|
|
|
|
// Derive the secret key to spend this output
|
|
const outputSecretKey = deriveSecretKey(derivation, n, spendSecretKey);
|
|
}
|
|
|
|
// Full output scan with view tag optimization
|
|
const result = scanOutput({
|
|
derivation,
|
|
outputIndex: 0,
|
|
outputPublicKey: tx.outputs[0].target,
|
|
spendPublicKey,
|
|
viewTag: tx.outputs[0].viewTag, // optional
|
|
encryptedAmount: tx.outputs[0].encryptedAmount
|
|
});
|
|
|
|
if (result.owned) {
|
|
console.log('Amount:', result.amount);
|
|
console.log('Output secret:', result.outputSecretKey);
|
|
}
|
|
```
|
|
|
|
## Key Images
|
|
|
|
Generate key images for spend detection (required for full wallet functionality).
|
|
|
|
```javascript
|
|
import {
|
|
hashToPoint,
|
|
generateKeyImage,
|
|
isValidKeyImage,
|
|
exportKeyImages,
|
|
importKeyImages
|
|
} from 'salvium-js';
|
|
|
|
// Generate key image for an output you own
|
|
// KI = x * H_p(P) where x is your output secret key, P is output public key
|
|
const keyImage = generateKeyImage(outputPublicKey, outputSecretKey);
|
|
|
|
// Validate a key image
|
|
if (isValidKeyImage(keyImage)) {
|
|
console.log('Valid key image');
|
|
}
|
|
|
|
// Export key images for view-only wallet sync
|
|
const exported = exportKeyImages([
|
|
{ keyImage: ki1, txHash: 'abc...', outputIndex: 0 },
|
|
{ keyImage: ki2, txHash: 'def...', outputIndex: 1 }
|
|
]);
|
|
|
|
// Import key images into a view-only wallet
|
|
const keyImageMap = importKeyImages(exported);
|
|
// Can now detect which outputs have been spent
|
|
```
|
|
|
|
## Transaction Construction
|
|
|
|
Build transactions with Pedersen commitments and CLSAG/TCLSAG ring signatures.
|
|
|
|
```javascript
|
|
import {
|
|
// Scalar operations
|
|
scAdd, scSub, scMul, scRandom, scInvert,
|
|
// Commitments
|
|
commit, zeroCommit, genCommitmentMask,
|
|
// Output creation
|
|
generateOutputKeys, createOutput,
|
|
// Signing (CLSAG for standard, TCLSAG for SalviumOne)
|
|
clsagSign, clsagVerify,
|
|
tclsagSign, tclsagVerify,
|
|
// Serialization
|
|
serializeTxPrefix, getTxPrefixHash, encodeVarint
|
|
} from 'salvium-js';
|
|
|
|
// Create a transaction output
|
|
const txSecretKey = scRandom(); // r - transaction secret key
|
|
const output = createOutput(
|
|
txSecretKey,
|
|
recipientViewPublicKey,
|
|
recipientSpendPublicKey,
|
|
1000000000n, // 10 SAL in atomic units
|
|
0, // output index
|
|
false // isSubaddress
|
|
);
|
|
// Returns: { outputPublicKey, txPublicKey, commitment, encryptedAmount, mask }
|
|
|
|
// Pedersen commitment: C = mask*G + amount*H
|
|
const mask = scRandom();
|
|
const commitment = commit(1000000000n, mask);
|
|
|
|
// Zero commitment for fees (public amount)
|
|
const feeCommitment = zeroCommit(10000000n); // 0.1 SAL fee
|
|
|
|
// CLSAG ring signature (standard RingCT)
|
|
const signature = clsagSign(
|
|
message, // Pre-MLSAG hash
|
|
ring, // Array of public keys (decoys + real)
|
|
secretKey, // Your secret key
|
|
commitments, // Ring member commitments
|
|
maskDiff, // Your mask - pseudo output mask
|
|
pseudoCommitment, // Pseudo output commitment
|
|
secretIndex // Your position in ring
|
|
);
|
|
|
|
// Verify CLSAG signature
|
|
const valid = clsagVerify(message, signature, ring, commitments, pseudoCommitment);
|
|
|
|
// TCLSAG ring signature (SalviumOne - uses dual generators G and T)
|
|
const tclsagSig = tclsagSign(
|
|
message, // Pre-MLSAG hash
|
|
ring, // Array of public keys
|
|
secretKeyX, // Spend secret key component
|
|
secretKeyY, // Auxiliary secret key component
|
|
commitments, // Ring member commitments
|
|
maskDiff, // Commitment mask difference
|
|
pseudoCommitment, // Pseudo output commitment
|
|
secretIndex // Your position in ring
|
|
);
|
|
// Returns { sx: [], sy: [], c1, I, D } - dual scalar arrays
|
|
|
|
// Verify TCLSAG signature
|
|
const tclsagValid = tclsagVerify(message, tclsagSig, ring, commitments, pseudoCommitment);
|
|
```
|
|
|
|
## Bulletproofs+ Range Proofs
|
|
|
|
Generate and verify range proofs that prove transaction amounts are in valid range (0 to 2^64-1) without revealing the actual amounts. **100% pure JavaScript** - no WASM required.
|
|
|
|
```javascript
|
|
import {
|
|
// Proof generation
|
|
proveRange,
|
|
proveRangeMultiple,
|
|
randomScalar,
|
|
serializeProof,
|
|
// Verification
|
|
verifyBulletproofPlus,
|
|
verifyRangeProof,
|
|
parseProof,
|
|
initGenerators,
|
|
bytesToPoint
|
|
} from 'salvium-js';
|
|
|
|
// === PROOF GENERATION ===
|
|
|
|
// Generate a range proof for a single amount
|
|
const amount = 1000000000n; // 10 SAL in atomic units
|
|
const mask = randomScalar(); // Blinding factor (commitment mask)
|
|
|
|
const proof = proveRange(amount, mask);
|
|
// proof = { V, A, A1, B, r1, s1, d1, L, R }
|
|
// V[0] is the Pedersen commitment: mask*G + amount*H
|
|
|
|
// Generate proof for multiple amounts (batched, more efficient)
|
|
const amounts = [1000000000n, 500000000n];
|
|
const masks = [randomScalar(), randomScalar()];
|
|
const batchProof = proveRangeMultiple(amounts, masks);
|
|
|
|
// Serialize for transmission
|
|
const proofBytes = serializeProof(proof);
|
|
|
|
// === VERIFICATION ===
|
|
|
|
// Verify a generated proof
|
|
const valid = verifyBulletproofPlus(proof.V, proof);
|
|
|
|
// Verify from serialized bytes (e.g., from a transaction)
|
|
const isValid = verifyRangeProof(
|
|
tx.rctSig.outPk, // Array of commitment bytes
|
|
tx.rctSig.proofBytes // Serialized Bulletproof+ proof
|
|
);
|
|
|
|
// Parse and verify separately for more control
|
|
const parsedProof = parseProof(proofBytes);
|
|
const verified = verifyBulletproofPlus(commitments, parsedProof);
|
|
```
|
|
|
|
**Performance** (pure JavaScript, no WASM):
|
|
| Operation | Time |
|
|
|-----------|------|
|
|
| Generator init (1024 pts) | ~800ms (one-time, cached) |
|
|
| Single proof generation | ~1100ms |
|
|
| Proof verification | ~300ms |
|
|
| Round-trip (generate + verify) | ~1400ms |
|
|
|
|
This is fast enough for mobile wallets (React Native on iOS/Android).
|
|
|
|
```javascript
|
|
// Serialize transaction
|
|
const tx = {
|
|
version: 2,
|
|
unlockTime: 0n,
|
|
inputs: [{ amount: 0n, keyOffsets: [100n, 50n], keyImage }],
|
|
outputs: [{ amount: 0n, target: output.outputPublicKey, viewTag: 0x42 }],
|
|
extra: { txPubKey: output.txPublicKey }
|
|
};
|
|
const serialized = serializeTxPrefix(tx);
|
|
const prefixHash = getTxPrefixHash(tx);
|
|
```
|
|
|
|
## RandomX Proof-of-Work
|
|
|
|
WASM-JIT accelerated RandomX implementation for mining and verification. Validated against [official test vectors](https://github.com/tevador/RandomX/blob/master/src/tests/tests.cpp).
|
|
|
|
```javascript
|
|
import {
|
|
RandomXContext,
|
|
rxSlowHash,
|
|
verifyHash,
|
|
checkDifficulty,
|
|
mine
|
|
} from 'salvium-js';
|
|
|
|
// === BASIC HASHING ===
|
|
|
|
// One-shot hash (creates temporary context)
|
|
const hash = await rxSlowHash('block header hash', 'block blob');
|
|
|
|
// === REUSABLE CONTEXT (Recommended) ===
|
|
|
|
// For multiple hashes with same key, reuse context
|
|
const ctx = new RandomXContext();
|
|
await ctx.init('test key 000'); // Initialize 256MB cache
|
|
|
|
const hash1 = ctx.hash('This is a test');
|
|
const hash2 = ctx.hash('Another input'); // Much faster, reuses cache
|
|
|
|
// Verify against official test vectors
|
|
console.log(ctx.hashHex('This is a test'));
|
|
// '639183aae1bf4c9a35884cb46b09cad9175f04efd7684e7262a0ac1c2f0b4e3f'
|
|
|
|
// === VERIFICATION ===
|
|
|
|
// Verify a hash
|
|
const isValid = await verifyHash(key, input, expectedHash);
|
|
|
|
// Check if hash meets difficulty target
|
|
const meetsDifficulty = checkDifficulty(hash, difficulty);
|
|
|
|
// === MINING ===
|
|
|
|
// Find nonce that meets difficulty
|
|
const result = await mine(
|
|
key, // Cache key (prev block hash)
|
|
blockBlob, // Block blob with nonce placeholder
|
|
nonceOffset, // Byte offset of nonce in blob
|
|
difficulty, // Target difficulty (BigInt)
|
|
maxNonce // Max nonce to try (default: 2^32)
|
|
);
|
|
|
|
if (result) {
|
|
console.log('Found nonce:', result.nonce);
|
|
console.log('Hash:', result.hash);
|
|
}
|
|
```
|
|
|
|
**Modes:**
|
|
- **Light mode** (default) - 256MB cache per thread, suitable for verification and mining
|
|
- **Full mode** - 2GB shared dataset, faster hashing for dedicated miners
|
|
|
|
**Multi-threaded Mining:**
|
|
```javascript
|
|
import { RandomXWorkerPool, getAvailableCores } from 'salvium-js';
|
|
|
|
const pool = new RandomXWorkerPool(getAvailableCores());
|
|
await pool.init(key);
|
|
const hash = await pool.hash(input);
|
|
pool.terminate();
|
|
```
|
|
|
|
## Verify Message Signatures
|
|
|
|
```javascript
|
|
import { verifySignature, parseSignature } from 'salvium-js';
|
|
|
|
// Verify a signature created with `sign` command in salvium-wallet-cli
|
|
const result = verifySignature(
|
|
'Hello, World!', // The original message
|
|
'SC1...', // The signer's address
|
|
'SigV2...' // The signature string
|
|
);
|
|
|
|
console.log(result);
|
|
// {
|
|
// valid: true,
|
|
// version: 2,
|
|
// keyType: 'spend', // or 'view'
|
|
// error: null
|
|
// }
|
|
```
|
|
|
|
## Cryptographic Primitives
|
|
|
|
```javascript
|
|
import { keccak256, keccak256Hex, blake2b, blake2bHex } from 'salvium-js';
|
|
|
|
// Keccak-256 (CryptoNote fast hash)
|
|
const hash = keccak256('hello'); // Uint8Array(32)
|
|
const hex = keccak256Hex('hello'); // "1c8aff950685..."
|
|
|
|
// Blake2b (CARROT key derivation)
|
|
const b2hash = blake2b(data, 32); // 32-byte output
|
|
const b2keyed = blake2b(data, 32, key); // Keyed hash (MAC)
|
|
```
|
|
|
|
## API Reference
|
|
|
|
### Wallet Generation
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `generateSeed()` | Generate 32-byte cryptographically secure random seed |
|
|
| `deriveKeys(seed)` | Derive CryptoNote keys from seed |
|
|
| `deriveCarrotKeys(seed)` | Derive CARROT keys from seed |
|
|
| `createAddress(options)` | Create address from public keys |
|
|
|
|
### Mnemonic Functions
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `seedToMnemonic(seed, options)` | Convert 32-byte seed to 25-word mnemonic |
|
|
| `mnemonicToSeed(mnemonic, options)` | Convert mnemonic to seed |
|
|
| `validateMnemonic(mnemonic, options)` | Validate mnemonic without converting |
|
|
| `detectLanguage(mnemonic)` | Auto-detect mnemonic language |
|
|
| `getAvailableLanguages()` | List supported languages |
|
|
|
|
### Subaddress Functions
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `generateCNSubaddress(options)` | Generate CryptoNote subaddress |
|
|
| `generateCarrotSubaddress(options)` | Generate CARROT subaddress |
|
|
| `generateRandomPaymentId()` | Generate 8-byte random payment ID |
|
|
|
|
### Address Functions
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `parseAddress(addr)` | Parse address, returns detailed info object |
|
|
| `isValidAddress(addr)` | Returns true if valid |
|
|
| `isMainnet(addr)` | Check if mainnet address |
|
|
| `isTestnet(addr)` | Check if testnet address |
|
|
| `isStagenet(addr)` | Check if stagenet address |
|
|
| `isCarrot(addr)` | Check if CARROT format |
|
|
| `isLegacy(addr)` | Check if legacy CryptoNote format |
|
|
| `isStandard(addr)` | Check if standard address |
|
|
| `isIntegrated(addr)` | Check if integrated address |
|
|
| `isSubaddress(addr)` | Check if subaddress |
|
|
| `getSpendPublicKey(addr)` | Extract 32-byte spend public key |
|
|
| `getViewPublicKey(addr)` | Extract 32-byte view public key |
|
|
| `getPaymentId(addr)` | Extract 8-byte payment ID (integrated only) |
|
|
| `toIntegratedAddress(addr, paymentId)` | Create integrated from standard |
|
|
| `toStandardAddress(addr)` | Extract standard from integrated |
|
|
| `describeAddress(addr)` | Human-readable description |
|
|
|
|
### Transaction Scanning Functions
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `generateKeyDerivation(pubKey, secretKey)` | Compute ECDH shared secret D = 8*s*P |
|
|
| `derivationToScalar(derivation, outputIndex)` | Derive scalar from derivation |
|
|
| `derivePublicKey(derivation, index, spendPub)` | Derive one-time public key |
|
|
| `deriveSecretKey(derivation, index, spendSec)` | Derive one-time secret key |
|
|
| `deriveViewTag(derivation, index)` | Compute view tag for fast filtering |
|
|
| `checkOutputOwnership(derivation, index, spendPub, outputPub)` | Check if output belongs to wallet |
|
|
| `scanOutput(params)` | Full output scan with amount decryption |
|
|
| `ecdhDecode(encrypted, derivation, index)` | Decrypt amount |
|
|
| `ecdhEncode(amount, derivation, index)` | Encrypt amount |
|
|
|
|
### Key Image Functions
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `hashToPoint(data)` | Hash to curve point (Elligator 2) |
|
|
| `generateKeyImage(outputPub, outputSec)` | Generate key image KI = x*H_p(P) |
|
|
| `isValidKeyImage(keyImage)` | Validate key image is on curve |
|
|
| `exportKeyImages(outputs)` | Format key images for export |
|
|
| `importKeyImages(data)` | Create lookup map from exported data |
|
|
|
|
### Transaction Construction Functions
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `scAdd(a, b)` | Add scalars mod L |
|
|
| `scSub(a, b)` | Subtract scalars mod L |
|
|
| `scMul(a, b)` | Multiply scalars mod L |
|
|
| `scRandom()` | Generate random scalar |
|
|
| `scInvert(a)` | Compute modular inverse |
|
|
| `commit(amount, mask)` | Create Pedersen commitment |
|
|
| `zeroCommit(amount)` | Create commitment with zero mask |
|
|
| `generateOutputKeys(txSec, viewPub, spendPub, index)` | Generate one-time output keys |
|
|
| `createOutput(txSec, viewPub, spendPub, amount, index)` | Create full transaction output |
|
|
| `clsagSign(message, ring, secret, commitments, mask, pseudo, index)` | Generate CLSAG signature |
|
|
| `clsagVerify(message, sig, ring, commitments, pseudo)` | Verify CLSAG signature |
|
|
| `tclsagSign(message, ring, secretX, secretY, commitments, mask, pseudo, index)` | Generate TCLSAG signature (SalviumOne) |
|
|
| `tclsagVerify(message, sig, ring, commitments, pseudo)` | Verify TCLSAG signature |
|
|
| `serializeTxPrefix(tx)` | Serialize transaction prefix |
|
|
| `getTxPrefixHash(tx)` | Compute transaction prefix hash |
|
|
| `encodeVarint(value)` | Encode integer as varint |
|
|
| `decodeVarint(bytes, offset)` | Decode varint from bytes |
|
|
|
|
### Bulletproofs+ Functions
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `proveRange(amount, mask)` | Generate range proof for single amount |
|
|
| `proveRangeMultiple(amounts, masks)` | Generate batched range proof for multiple amounts |
|
|
| `randomScalar()` | Generate cryptographically secure random scalar |
|
|
| `serializeProof(proof)` | Serialize proof to bytes for transmission |
|
|
| `verifyBulletproofPlus(V, proof)` | Verify single range proof |
|
|
| `verifyBulletproofPlusBatch(proofs)` | Batch verify multiple proofs |
|
|
| `verifyRangeProof(commitments, proofBytes)` | Verify from raw bytes |
|
|
| `initGenerators(n)` | Initialize Gi/Hi generators (cached) |
|
|
| `parseProof(proofBytes)` | Parse proof from serialized bytes |
|
|
| `multiScalarMul(scalars, points)` | Multiscalar multiplication |
|
|
| `bytesToPoint(bytes)` | Decode compressed point |
|
|
| `bytesToScalar(bytes)` | Decode scalar from bytes |
|
|
|
|
### RandomX Functions
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `RandomXContext` | Reusable context class for repeated hashing with same key |
|
|
| `rxSlowHash(key, input)` | One-shot RandomX hash computation |
|
|
| `randomxHash(key, input)` | Alias for rxSlowHash |
|
|
| `verifyHash(key, input, expected)` | Verify RandomX hash matches expected |
|
|
| `checkDifficulty(hash, difficulty)` | Check if hash meets difficulty target |
|
|
| `mine(key, blob, nonceOffset, difficulty, maxNonce, onProgress)` | Find nonce meeting difficulty |
|
|
| `calculateCommitment(input, hashIn)` | Calculate hash commitment |
|
|
| `RandomXCache` | Cache class for Argon2d-initialized memory |
|
|
| `initDatasetItem(cache, itemNumber)` | Generate single dataset item from cache |
|
|
| `RandomXVM` | Virtual machine for RandomX program execution |
|
|
| `Blake2Generator` | Pseudo-random byte generator using Blake2b |
|
|
| `generateSuperscalar(gen)` | Generate superscalar program |
|
|
| `executeSuperscalar(registers, program)` | Execute superscalar program on registers |
|
|
| `reciprocal(divisor)` | Compute reciprocal for IMUL_RCP instruction |
|
|
| `argon2d(password, salt, tCost, mCost, parallelism, outLen)` | Argon2d hash function |
|
|
| `argon2InitCache(key)` | Initialize RandomX cache with Argon2d |
|
|
|
|
### Wallet Functions
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `Wallet` | Wallet class with full and view-only modes |
|
|
| `createWallet(options?)` | Create new wallet with mnemonic |
|
|
| `restoreWallet(mnemonic, options?)` | Restore wallet from mnemonic |
|
|
| `createViewOnlyWallet(options)` | Create view-only wallet from keys |
|
|
| `wallet.getAddress()` | Get main wallet address |
|
|
| `wallet.getSubaddress(major, minor)` | Generate subaddress |
|
|
| `wallet.getBalance()` | Get balance (total, unlocked, locked) |
|
|
| `wallet.canSign()` | Check if wallet can sign transactions |
|
|
| `wallet.canScan()` | Check if wallet can scan for outputs |
|
|
| `wallet.toJSON(includeSecrets?)` | Serialize wallet to JSON |
|
|
| `Wallet.fromJSON(json)` | Restore wallet from JSON |
|
|
|
|
### UTXO Selection Functions
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `selectUTXOs(utxos, amount, options?)` | Select UTXOs for transaction |
|
|
| `UTXO_STRATEGY.MINIMIZE_INPUTS` | Strategy to minimize number of inputs |
|
|
| `UTXO_STRATEGY.MINIMIZE_CHANGE` | Strategy to minimize change amount |
|
|
| `UTXO_STRATEGY.OLDEST_FIRST` | Strategy to spend oldest UTXOs first |
|
|
| `UTXO_STRATEGY.NEWEST_FIRST` | Strategy to spend newest UTXOs first |
|
|
| `UTXO_STRATEGY.RANDOM` | Random UTXO selection |
|
|
|
|
### Transaction Builder Functions
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `buildTransaction(options)` | Build complete transaction with signatures |
|
|
| `signTransaction(unsignedTx, spendSecretKey)` | Sign an unsigned transaction |
|
|
| `prepareInputs(utxos, viewSecretKey)` | Prepare inputs with ring members |
|
|
| `estimateTransactionFee(numInputs, numOutputs, feePerByte?)` | Estimate transaction fee |
|
|
| `validateTransaction(tx)` | Validate transaction structure |
|
|
| `serializeTransaction(tx)` | Serialize transaction for broadcast |
|
|
|
|
### Transaction Parser Functions
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `parseTransaction(txData)` | Parse raw transaction bytes/hex |
|
|
| `parseExtra(extra)` | Parse transaction extra field |
|
|
| `extractTxPubKey(extra)` | Extract transaction public key |
|
|
| `extractPaymentId(extra)` | Extract payment ID from extra |
|
|
| `decodeAmount(encrypted, derivation, index)` | Decrypt encrypted amount |
|
|
| `summarizeTransaction(tx)` | Get transaction summary |
|
|
| `getTransactionHashFromParsed(tx)` | Compute hash from parsed transaction |
|
|
|
|
### Utility Functions
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `bytesToHex(bytes)` | Convert Uint8Array to hex string |
|
|
| `hexToBytes(hex)` | Convert hex string to Uint8Array |
|
|
| `keccak256(data)` | Keccak-256 hash, returns Uint8Array |
|
|
| `keccak256Hex(data)` | Keccak-256 hash, returns hex string |
|
|
| `blake2b(data, outlen, key?)` | Blake2b hash with optional key |
|
|
|
|
## Wallet Class
|
|
|
|
Unified wallet management with full and view-only modes.
|
|
|
|
```javascript
|
|
import {
|
|
Wallet,
|
|
createWallet,
|
|
restoreWallet,
|
|
createViewOnlyWallet
|
|
} from 'salvium-js';
|
|
|
|
// Create a new wallet
|
|
const { wallet, mnemonic, seed } = createWallet({ network: 'mainnet' });
|
|
console.log('Backup:', mnemonic); // 25 words
|
|
console.log('Address:', wallet.getAddress());
|
|
|
|
// Restore from mnemonic
|
|
const restored = restoreWallet('abbey ability able about...');
|
|
|
|
// Create view-only wallet (can scan, cannot spend)
|
|
const viewOnly = createViewOnlyWallet({
|
|
network: 'mainnet',
|
|
viewSecretKey: '...',
|
|
spendPublicKey: '...'
|
|
});
|
|
|
|
console.log('Can scan:', viewOnly.canScan()); // true
|
|
console.log('Can sign:', viewOnly.canSign()); // false
|
|
|
|
// Generate subaddresses
|
|
const sub = wallet.getSubaddress(0, 1); // account 0, index 1
|
|
|
|
// Serialize/deserialize
|
|
const json = wallet.toJSON();
|
|
const loaded = Wallet.fromJSON(json);
|
|
```
|
|
|
|
## UTXO Selection
|
|
|
|
Multiple strategies for selecting transaction inputs.
|
|
|
|
```javascript
|
|
import { selectUTXOs, UTXO_STRATEGY } from 'salvium-js';
|
|
|
|
const utxos = [
|
|
{ txHash: 'abc...', outputIndex: 0, amount: 1000000000n, publicKey: ... },
|
|
{ txHash: 'def...', outputIndex: 1, amount: 500000000n, publicKey: ... },
|
|
// ...
|
|
];
|
|
|
|
const result = selectUTXOs(utxos, 800000000n, {
|
|
strategy: UTXO_STRATEGY.MINIMIZE_INPUTS, // or MINIMIZE_CHANGE, OLDEST_FIRST, etc.
|
|
feePerByte: 1000n
|
|
});
|
|
|
|
console.log('Selected:', result.selectedUTXOs.length);
|
|
console.log('Total:', result.totalAmount);
|
|
console.log('Change:', result.changeAmount);
|
|
console.log('Fee:', result.estimatedFee);
|
|
```
|
|
|
|
## Transaction Builder
|
|
|
|
High-level API for building and signing transactions.
|
|
|
|
```javascript
|
|
import { buildTransaction, signTransaction, validateTransaction } from 'salvium-js';
|
|
|
|
// Build a transaction
|
|
const tx = await buildTransaction({
|
|
utxos: myUTXOs,
|
|
destinations: [
|
|
{ address: 'SaLv...', amount: 1000000000n }
|
|
],
|
|
changeAddress: myChangeAddress,
|
|
viewSecretKey: keys.viewSecretKey,
|
|
spendSecretKey: keys.spendSecretKey,
|
|
ringSize: 16
|
|
});
|
|
|
|
console.log('TX Hash:', tx.txHash);
|
|
console.log('Fee:', tx.fee);
|
|
|
|
// Validate before broadcast
|
|
const validation = validateTransaction(tx.tx);
|
|
if (!validation.valid) {
|
|
console.error('Errors:', validation.errors);
|
|
}
|
|
```
|
|
|
|
## Transaction Parser
|
|
|
|
Decode and analyze transaction data.
|
|
|
|
```javascript
|
|
import {
|
|
parseTransaction,
|
|
parseExtra,
|
|
extractTxPubKey,
|
|
extractPaymentId,
|
|
summarizeTransaction
|
|
} from 'salvium-js';
|
|
|
|
// Parse raw transaction
|
|
const tx = parseTransaction(txHex);
|
|
|
|
// Get transaction summary
|
|
const summary = summarizeTransaction(tx);
|
|
console.log('Hash:', summary.hash);
|
|
console.log('Inputs:', summary.numInputs);
|
|
console.log('Outputs:', summary.numOutputs);
|
|
console.log('Coinbase:', summary.isCoinbase);
|
|
console.log('Key images:', summary.keyImages);
|
|
|
|
// Extract extra fields
|
|
const extra = parseExtra(tx.extra);
|
|
const txPubKey = extractTxPubKey(extra);
|
|
const paymentId = extractPaymentId(extra);
|
|
```
|
|
|
|
## Stratum Mining
|
|
|
|
Connect to mining pools with the stratum protocol.
|
|
|
|
```javascript
|
|
import { createMiner } from 'salvium-js';
|
|
|
|
const miner = createMiner({
|
|
host: 'pool.example.com',
|
|
port: 3333,
|
|
wallet: 'SaLv...',
|
|
password: 'x',
|
|
threads: 4
|
|
});
|
|
|
|
miner.on('hashrate', (rate) => console.log(`${rate} H/s`));
|
|
miner.on('share', (accepted) => console.log(accepted ? 'Share accepted' : 'Share rejected'));
|
|
|
|
await miner.start();
|
|
// ... mining ...
|
|
miner.stop();
|
|
```
|
|
|
|
## Testing
|
|
|
|
```bash
|
|
# Run all tests
|
|
bun test/all.js
|
|
|
|
# Run with integration tests (requires running daemon)
|
|
bun test/all.js --integration
|
|
|
|
# Run against specific daemon
|
|
bun test/all.js --integration http://localhost:19081
|
|
```
|
|
|
|
Test coverage: 19 test suites including CLSAG/TCLSAG signing and official RandomX test vectors.
|
|
|
|
## Contributing
|
|
|
|
Contributions welcome! Please read the Salvium source code for reference:
|
|
https://github.com/salvium/salvium
|