initial commit of salvium-js

This commit is contained in:
Matt Hess
2026-01-05 03:13:07 +00:00
commit df3855c31d
13 changed files with 3129 additions and 0 deletions
+233
View File
@@ -0,0 +1,233 @@
# salvium-js
JavaScript library for Salvium cryptocurrency - address validation, parsing, and cryptographic utilities.
## Features
- **Address Validation** - Validate all 18 Salvium address types
- **Address Parsing** - Extract public keys, payment IDs, detect network/format/type
- **Multi-Network Support** - Mainnet, Testnet, Stagenet
- **Dual Format Support** - Legacy (CryptoNote) and CARROT addresses
- **Base58 Encoding** - Monero-variant Base58 with checksums
- **Keccak-256** - Pre-SHA3 Keccak hashing (cn_fast_hash)
- **Signature Verification** - Verify message signatures (V1 and V2 formats)
- **Zero Dependencies** - Pure JavaScript, works in browsers and Node.js
## 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>
```
## Usage
### Validate an Address
```javascript
import { isValidAddress, parseAddress } from 'salvium-js';
// Simple validation
if (isValidAddress('SC1...')) {
console.log('Valid address!');
}
// Detailed parsing
const info = parseAddress('SC1...');
console.log(info);
// {
// valid: true,
// network: 'mainnet',
// format: 'carrot',
// type: 'standard',
// prefix: 'SC1',
// spendPublicKey: Uint8Array(32),
// viewPublicKey: Uint8Array(32),
// paymentId: null,
// error: null
// }
```
### Check Address Properties
```javascript
import {
isMainnet,
isTestnet,
isCarrot,
isLegacy,
isStandard,
isIntegrated,
isSubaddress
} from 'salvium-js';
const addr = 'SC1...';
isMainnet(addr); // true
isCarrot(addr); // true
isStandard(addr); // true
isIntegrated(addr); // false
```
### Extract Keys
```javascript
import { getSpendPublicKey, getViewPublicKey, bytesToHex } from 'salvium-js';
const spendKey = getSpendPublicKey('SC1...');
const viewKey = getViewPublicKey('SC1...');
console.log('Spend Key:', bytesToHex(spendKey));
console.log('View Key:', bytesToHex(viewKey));
```
### Work with Integrated Addresses
```javascript
import { toIntegratedAddress, toStandardAddress, getPaymentId, bytesToHex } from 'salvium-js';
// Create integrated address with payment ID
const integrated = toIntegratedAddress('SC1...', 'deadbeef12345678');
// Extract payment ID
const paymentId = getPaymentId(integrated);
console.log('Payment ID:', bytesToHex(paymentId));
// Get standard address from integrated
const standard = toStandardAddress(integrated);
```
### Describe an Address
```javascript
import { describeAddress } from 'salvium-js';
console.log(describeAddress('SC1...'));
// "Mainnet CARROT standard"
console.log(describeAddress('SaLvi...'));
// "Mainnet Legacy integrated (Payment ID: abcd1234...)"
```
### Low-Level: Keccak-256
```javascript
import { keccak256, keccak256Hex } from 'salvium-js';
const hash = keccak256('hello'); // Uint8Array(32)
const hex = keccak256Hex('hello'); // "1c8aff950685..."
```
### Low-Level: Base58
```javascript
import { encode, decode, encodeAddress, decodeAddress } from 'salvium-js/base58';
const encoded = encode(new Uint8Array([1, 2, 3]));
const decoded = decode(encoded);
```
### 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
// }
// Parse signature components
const sig = parseSignature('SigV2...');
console.log(sig);
// {
// valid: true,
// version: 2,
// c: Uint8Array(32),
// r: Uint8Array(32),
// signMask: 0
// }
```
## API Reference
### 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 |
### 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 |
### Signature Functions
| Function | Description |
|----------|-------------|
| `verifySignature(message, address, signature)` | Verify a message signature, returns result object |
| `parseSignature(signature)` | Parse signature string into components |
## Testing
```bash
npm test
```
## License
MIT
## Contributing
Contributions welcome! Please read the Salvium source code for reference:
https://github.com/salvium/salvium
+3
View File
@@ -0,0 +1,3 @@
// Re-export everything from src
export * from './src/index.js';
export { default } from './src/index.js';
+40
View File
@@ -0,0 +1,40 @@
{
"name": "salvium-js",
"version": "0.1.0",
"description": "JavaScript library for Salvium cryptocurrency - address validation, parsing, and signature verification",
"main": "src/index.js",
"type": "module",
"exports": {
".": "./src/index.js",
"./address": "./src/address.js",
"./base58": "./src/base58.js",
"./keccak": "./src/keccak.js",
"./signature": "./src/signature.js",
"./constants": "./src/constants.js"
},
"scripts": {
"test": "node test/run.js"
},
"keywords": [
"salvium",
"cryptocurrency",
"cryptonote",
"carrot",
"address",
"validation",
"base58",
"keccak"
],
"author": "WhiskyMine",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/whiskymine/salvium-js"
},
"engines": {
"node": ">=16.0.0"
},
"browser": {
"./src/keccak.js": "./src/keccak.browser.js"
}
}
+417
View File
@@ -0,0 +1,417 @@
/**
* Salvium Address Handling
*
* Supports all 18 address types across:
* - 3 networks: mainnet, testnet, stagenet
* - 2 formats: legacy (CryptoNote), carrot (CARROT)
* - 3 types: standard, integrated, subaddress
*/
import {
NETWORK,
ADDRESS_TYPE,
ADDRESS_FORMAT,
PREFIX_MAP,
KEY_SIZE,
PAYMENT_ID_SIZE,
ADDRESS_DATA_SIZE,
getPrefix
} from './constants.js';
import { decodeAddress, encodeAddress } from './base58.js';
/**
* Result of parsing an address
* @typedef {Object} ParsedAddress
* @property {boolean} valid - Whether the address is valid
* @property {string|null} network - Network type (mainnet, testnet, stagenet)
* @property {string|null} format - Address format (legacy, carrot)
* @property {string|null} type - Address type (standard, integrated, subaddress)
* @property {string|null} prefix - Human-readable prefix (e.g., "SaLv", "SC1")
* @property {Uint8Array|null} spendPublicKey - 32-byte public spend key
* @property {Uint8Array|null} viewPublicKey - 32-byte public view key
* @property {Uint8Array|null} paymentId - 8-byte payment ID (integrated addresses only)
* @property {string|null} error - Error message if invalid
*/
/**
* Parse and validate a Salvium address
* @param {string} address - The address string to parse
* @returns {ParsedAddress} - Parsed address information
*/
export function parseAddress(address) {
const result = {
valid: false,
network: null,
format: null,
type: null,
prefix: null,
spendPublicKey: null,
viewPublicKey: null,
paymentId: null,
error: null
};
// Basic validation
if (!address || typeof address !== 'string') {
result.error = 'Address must be a non-empty string';
return result;
}
// Trim whitespace
address = address.trim();
// Check length (rough bounds based on possible address types)
// Standard: ~95-99 chars, Integrated: ~106-110 chars, Subaddress: ~95-99 chars
// Allow wide range to accommodate all prefix sizes
if (address.length < 90 || address.length > 150) {
result.error = 'Invalid address length';
return result;
}
// Decode the address
const decoded = decodeAddress(address);
if (decoded === null) {
result.error = 'Invalid Base58 encoding or checksum';
return result;
}
const { tag, data } = decoded;
// Look up the prefix
const prefixInfo = PREFIX_MAP.get(tag);
if (!prefixInfo) {
result.error = `Unknown address prefix: 0x${tag.toString(16)}`;
return result;
}
result.network = prefixInfo.network;
result.format = prefixInfo.format;
result.type = prefixInfo.type;
result.prefix = prefixInfo.text;
// Validate data length based on address type
const expectedDataSize = ADDRESS_DATA_SIZE[result.type];
if (data.length !== expectedDataSize) {
result.error = `Invalid data length: expected ${expectedDataSize} bytes, got ${data.length}`;
return result;
}
// Extract public keys
result.spendPublicKey = data.slice(0, KEY_SIZE);
result.viewPublicKey = data.slice(KEY_SIZE, KEY_SIZE * 2);
// Extract payment ID for integrated addresses
if (result.type === ADDRESS_TYPE.INTEGRATED) {
result.paymentId = data.slice(KEY_SIZE * 2, KEY_SIZE * 2 + PAYMENT_ID_SIZE);
}
result.valid = true;
return result;
}
/**
* Validate a Salvium address
* @param {string} address - The address string to validate
* @returns {boolean} - True if valid, false otherwise
*/
export function isValidAddress(address) {
return parseAddress(address).valid;
}
/**
* Check if an address belongs to a specific network
* @param {string} address - The address string
* @param {string} network - Network to check (mainnet, testnet, stagenet)
* @returns {boolean} - True if address belongs to the specified network
*/
export function isNetwork(address, network) {
const parsed = parseAddress(address);
return parsed.valid && parsed.network === network;
}
/**
* Check if an address is a mainnet address
* @param {string} address - The address string
* @returns {boolean}
*/
export function isMainnet(address) {
return isNetwork(address, NETWORK.MAINNET);
}
/**
* Check if an address is a testnet address
* @param {string} address - The address string
* @returns {boolean}
*/
export function isTestnet(address) {
return isNetwork(address, NETWORK.TESTNET);
}
/**
* Check if an address is a stagenet address
* @param {string} address - The address string
* @returns {boolean}
*/
export function isStagenet(address) {
return isNetwork(address, NETWORK.STAGENET);
}
/**
* Check if an address uses the CARROT format
* @param {string} address - The address string
* @returns {boolean}
*/
export function isCarrot(address) {
const parsed = parseAddress(address);
return parsed.valid && parsed.format === ADDRESS_FORMAT.CARROT;
}
/**
* Check if an address uses the legacy CryptoNote format
* @param {string} address - The address string
* @returns {boolean}
*/
export function isLegacy(address) {
const parsed = parseAddress(address);
return parsed.valid && parsed.format === ADDRESS_FORMAT.LEGACY;
}
/**
* Check if an address is a standard address
* @param {string} address - The address string
* @returns {boolean}
*/
export function isStandard(address) {
const parsed = parseAddress(address);
return parsed.valid && parsed.type === ADDRESS_TYPE.STANDARD;
}
/**
* Check if an address is an integrated address
* @param {string} address - The address string
* @returns {boolean}
*/
export function isIntegrated(address) {
const parsed = parseAddress(address);
return parsed.valid && parsed.type === ADDRESS_TYPE.INTEGRATED;
}
/**
* Check if an address is a subaddress
* @param {string} address - The address string
* @returns {boolean}
*/
export function isSubaddress(address) {
const parsed = parseAddress(address);
return parsed.valid && parsed.type === ADDRESS_TYPE.SUBADDRESS;
}
/**
* Get the public spend key from an address
* @param {string} address - The address string
* @returns {Uint8Array|null} - 32-byte public spend key or null if invalid
*/
export function getSpendPublicKey(address) {
const parsed = parseAddress(address);
return parsed.valid ? parsed.spendPublicKey : null;
}
/**
* Get the public view key from an address
* @param {string} address - The address string
* @returns {Uint8Array|null} - 32-byte public view key or null if invalid
*/
export function getViewPublicKey(address) {
const parsed = parseAddress(address);
return parsed.valid ? parsed.viewPublicKey : null;
}
/**
* Get the payment ID from an integrated address
* @param {string} address - The address string
* @returns {Uint8Array|null} - 8-byte payment ID or null if not an integrated address
*/
export function getPaymentId(address) {
const parsed = parseAddress(address);
return parsed.valid && parsed.type === ADDRESS_TYPE.INTEGRATED ? parsed.paymentId : null;
}
/**
* Create an address from components
* @param {Object} options - Address options
* @param {string} options.network - Network type
* @param {string} options.format - Address format
* @param {string} options.type - Address type
* @param {Uint8Array} options.spendPublicKey - 32-byte public spend key
* @param {Uint8Array} options.viewPublicKey - 32-byte public view key
* @param {Uint8Array} [options.paymentId] - 8-byte payment ID (for integrated addresses)
* @returns {string|null} - Encoded address or null on error
*/
export function createAddress(options) {
const { network, format, type, spendPublicKey, viewPublicKey, paymentId } = options;
// Validate keys
if (!spendPublicKey || spendPublicKey.length !== KEY_SIZE) {
return null;
}
if (!viewPublicKey || viewPublicKey.length !== KEY_SIZE) {
return null;
}
// Get prefix
const prefix = getPrefix(network, format, type);
if (prefix === null) {
return null;
}
// Build data
let data;
if (type === ADDRESS_TYPE.INTEGRATED) {
if (!paymentId || paymentId.length !== PAYMENT_ID_SIZE) {
return null;
}
data = new Uint8Array(KEY_SIZE * 2 + PAYMENT_ID_SIZE);
data.set(spendPublicKey, 0);
data.set(viewPublicKey, KEY_SIZE);
data.set(paymentId, KEY_SIZE * 2);
} else {
data = new Uint8Array(KEY_SIZE * 2);
data.set(spendPublicKey, 0);
data.set(viewPublicKey, KEY_SIZE);
}
return encodeAddress(prefix, data);
}
/**
* Convert a standard address to an integrated address by adding a payment ID
* @param {string} address - Standard address
* @param {Uint8Array|string} paymentId - 8-byte payment ID (or 16-char hex string)
* @returns {string|null} - Integrated address or null on error
*/
export function toIntegratedAddress(address, paymentId) {
const parsed = parseAddress(address);
if (!parsed.valid || parsed.type !== ADDRESS_TYPE.STANDARD) {
return null;
}
// Convert hex string to bytes if needed
if (typeof paymentId === 'string') {
if (paymentId.length !== 16) {
return null;
}
const bytes = new Uint8Array(8);
for (let i = 0; i < 8; i++) {
bytes[i] = parseInt(paymentId.substr(i * 2, 2), 16);
}
paymentId = bytes;
}
if (paymentId.length !== PAYMENT_ID_SIZE) {
return null;
}
return createAddress({
network: parsed.network,
format: parsed.format,
type: ADDRESS_TYPE.INTEGRATED,
spendPublicKey: parsed.spendPublicKey,
viewPublicKey: parsed.viewPublicKey,
paymentId
});
}
/**
* Extract the standard address from an integrated address
* @param {string} address - Integrated address
* @returns {string|null} - Standard address or null on error
*/
export function toStandardAddress(address) {
const parsed = parseAddress(address);
if (!parsed.valid || parsed.type !== ADDRESS_TYPE.INTEGRATED) {
return null;
}
return createAddress({
network: parsed.network,
format: parsed.format,
type: ADDRESS_TYPE.STANDARD,
spendPublicKey: parsed.spendPublicKey,
viewPublicKey: parsed.viewPublicKey
});
}
/**
* Format address info as a human-readable string
* @param {string} address - The address string
* @returns {string} - Human-readable description
*/
export function describeAddress(address) {
const parsed = parseAddress(address);
if (!parsed.valid) {
return `Invalid address: ${parsed.error}`;
}
const parts = [
parsed.network.charAt(0).toUpperCase() + parsed.network.slice(1),
parsed.format === ADDRESS_FORMAT.CARROT ? 'CARROT' : 'Legacy',
parsed.type
];
if (parsed.type === ADDRESS_TYPE.INTEGRATED) {
const paymentIdHex = Array.from(parsed.paymentId)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
parts.push(`(Payment ID: ${paymentIdHex})`);
}
return parts.join(' ');
}
/**
* Convert bytes to hex string
* @param {Uint8Array} bytes - Bytes to convert
* @returns {string} - Hex string
*/
export function bytesToHex(bytes) {
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}
/**
* Convert hex string to bytes
* @param {string} hex - Hex string
* @returns {Uint8Array} - Bytes
*/
export function hexToBytes(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
}
export default {
parseAddress,
isValidAddress,
isNetwork,
isMainnet,
isTestnet,
isStagenet,
isCarrot,
isLegacy,
isStandard,
isIntegrated,
isSubaddress,
getSpendPublicKey,
getViewPublicKey,
getPaymentId,
createAddress,
toIntegratedAddress,
toStandardAddress,
describeAddress,
bytesToHex,
hexToBytes
};
+303
View File
@@ -0,0 +1,303 @@
/**
* Monero/Salvium Base58 Encoding/Decoding
*
* This is NOT the same as Bitcoin's Base58Check!
* Monero uses a block-based encoding where:
* - Data is split into 8-byte blocks
* - Each 8-byte block encodes to exactly 11 Base58 characters
* - Partial blocks use a size mapping table
*/
import { BASE58_ALPHABET, BASE58_FULL_BLOCK_SIZE, BASE58_FULL_ENCODED_BLOCK_SIZE, BASE58_ENCODED_BLOCK_SIZES } from './constants.js';
import { keccak256 } from './keccak.js';
// Build reverse alphabet lookup
const ALPHABET_MAP = new Map();
for (let i = 0; i < BASE58_ALPHABET.length; i++) {
ALPHABET_MAP.set(BASE58_ALPHABET[i], i);
}
// Build decoded block sizes lookup (reverse of encoded block sizes)
const DECODED_BLOCK_SIZES = new Map();
for (let i = 0; i < BASE58_ENCODED_BLOCK_SIZES.length; i++) {
DECODED_BLOCK_SIZES.set(BASE58_ENCODED_BLOCK_SIZES[i], i);
}
/**
* Convert bytes to big-endian uint64
* @param {Uint8Array} data - Input bytes (up to 8 bytes)
* @returns {BigInt} - Big-endian integer value
*/
function uint8BEToUint64(data) {
let result = 0n;
for (let i = 0; i < data.length; i++) {
result = (result << 8n) | BigInt(data[i]);
}
return result;
}
/**
* Convert uint64 to big-endian bytes
* @param {BigInt} num - Number to convert
* @param {number} size - Output size in bytes
* @returns {Uint8Array} - Big-endian bytes
*/
function uint64ToUint8BE(num, size) {
const result = new Uint8Array(size);
for (let i = size - 1; i >= 0; i--) {
result[i] = Number(num & 0xFFn);
num >>= 8n;
}
return result;
}
/**
* Encode a single block of data to Base58
* @param {Uint8Array} block - Input block (1-8 bytes)
* @returns {string} - Base58 encoded string
*/
function encodeBlock(block) {
const encodedSize = BASE58_ENCODED_BLOCK_SIZES[block.length];
const result = new Array(encodedSize).fill(BASE58_ALPHABET[0]);
let num = uint8BEToUint64(block);
let i = encodedSize - 1;
while (num > 0n) {
const remainder = Number(num % 58n);
num = num / 58n;
result[i] = BASE58_ALPHABET[remainder];
i--;
}
return result.join('');
}
/**
* Decode a single Base58 block to bytes
* @param {string} block - Base58 encoded block
* @returns {Uint8Array|null} - Decoded bytes or null on error
*/
function decodeBlock(block) {
const decodedSize = DECODED_BLOCK_SIZES.get(block.length);
if (decodedSize === undefined || decodedSize < 0) {
return null; // Invalid block size
}
if (decodedSize === 0) {
return new Uint8Array(0);
}
let num = 0n;
const base = 58n;
for (let i = 0; i < block.length; i++) {
const digit = ALPHABET_MAP.get(block[i]);
if (digit === undefined) {
return null; // Invalid character
}
num = num * base + BigInt(digit);
}
// Check for overflow
if (decodedSize < BASE58_FULL_BLOCK_SIZE && num >= (1n << BigInt(8 * decodedSize))) {
return null; // Overflow
}
return uint64ToUint8BE(num, decodedSize);
}
/**
* Encode binary data to Base58 (Monero variant)
* @param {Uint8Array|Array} data - Binary data to encode
* @returns {string} - Base58 encoded string
*/
export function encode(data) {
if (!(data instanceof Uint8Array)) {
data = new Uint8Array(data);
}
if (data.length === 0) {
return '';
}
const fullBlockCount = Math.floor(data.length / BASE58_FULL_BLOCK_SIZE);
const lastBlockSize = data.length % BASE58_FULL_BLOCK_SIZE;
let result = '';
// Encode full blocks
for (let i = 0; i < fullBlockCount; i++) {
const block = data.slice(i * BASE58_FULL_BLOCK_SIZE, (i + 1) * BASE58_FULL_BLOCK_SIZE);
result += encodeBlock(block);
}
// Encode last partial block
if (lastBlockSize > 0) {
const block = data.slice(fullBlockCount * BASE58_FULL_BLOCK_SIZE);
result += encodeBlock(block);
}
return result;
}
/**
* Decode Base58 string to binary data (Monero variant)
* @param {string} encoded - Base58 encoded string
* @returns {Uint8Array|null} - Decoded binary data or null on error
*/
export function decode(encoded) {
if (encoded.length === 0) {
return new Uint8Array(0);
}
const fullBlockCount = Math.floor(encoded.length / BASE58_FULL_ENCODED_BLOCK_SIZE);
const lastBlockSize = encoded.length % BASE58_FULL_ENCODED_BLOCK_SIZE;
const lastBlockDecodedSize = DECODED_BLOCK_SIZES.get(lastBlockSize);
if (lastBlockDecodedSize === undefined || lastBlockDecodedSize < 0) {
return null; // Invalid encoded length
}
const dataSize = fullBlockCount * BASE58_FULL_BLOCK_SIZE + lastBlockDecodedSize;
const result = new Uint8Array(dataSize);
let offset = 0;
// Decode full blocks
for (let i = 0; i < fullBlockCount; i++) {
const block = encoded.slice(i * BASE58_FULL_ENCODED_BLOCK_SIZE, (i + 1) * BASE58_FULL_ENCODED_BLOCK_SIZE);
const decoded = decodeBlock(block);
if (decoded === null) {
return null;
}
result.set(decoded, offset);
offset += BASE58_FULL_BLOCK_SIZE;
}
// Decode last partial block
if (lastBlockSize > 0) {
const block = encoded.slice(fullBlockCount * BASE58_FULL_ENCODED_BLOCK_SIZE);
const decoded = decodeBlock(block);
if (decoded === null) {
return null;
}
result.set(decoded, offset);
}
return result;
}
/**
* Encode a varint (variable-length integer)
* @param {BigInt|number} value - Integer to encode
* @returns {Uint8Array} - Varint encoded bytes
*/
export function encodeVarint(value) {
value = BigInt(value);
const bytes = [];
while (value >= 0x80n) {
bytes.push(Number((value & 0x7Fn) | 0x80n));
value >>= 7n;
}
bytes.push(Number(value));
return new Uint8Array(bytes);
}
/**
* Decode a varint from the start of data
* @param {Uint8Array} data - Data containing varint
* @returns {{value: BigInt, bytesRead: number}|null} - Decoded value and bytes consumed
*/
export function decodeVarint(data) {
let value = 0n;
let shift = 0n;
let bytesRead = 0;
for (let i = 0; i < data.length && i < 10; i++) {
const byte = BigInt(data[i]);
value |= (byte & 0x7Fn) << shift;
bytesRead++;
if ((byte & 0x80n) === 0n) {
return { value, bytesRead };
}
shift += 7n;
}
return null; // Varint too long or incomplete
}
/**
* Encode an address with tag/prefix and checksum
* @param {BigInt|number} tag - Address prefix/tag
* @param {Uint8Array} data - Address data (public keys)
* @returns {string} - Base58 encoded address
*/
export function encodeAddress(tag, data) {
// Build: [varint tag][data][4-byte checksum]
const tagBytes = encodeVarint(tag);
const combined = new Uint8Array(tagBytes.length + data.length);
combined.set(tagBytes, 0);
combined.set(data, tagBytes.length);
// Compute checksum (first 4 bytes of Keccak-256)
const hash = keccak256(combined);
const checksum = hash.slice(0, 4);
// Combine with checksum
const withChecksum = new Uint8Array(combined.length + 4);
withChecksum.set(combined, 0);
withChecksum.set(checksum, combined.length);
return encode(withChecksum);
}
/**
* Decode an address, verifying checksum and extracting tag and data
* @param {string} address - Base58 encoded address
* @returns {{tag: BigInt, data: Uint8Array}|null} - Decoded tag and data, or null on error
*/
export function decodeAddress(address) {
const decoded = decode(address);
if (decoded === null || decoded.length <= 4) {
return null;
}
// Extract checksum
const checksum = decoded.slice(decoded.length - 4);
const payload = decoded.slice(0, decoded.length - 4);
// Verify checksum
const hash = keccak256(payload);
const expectedChecksum = hash.slice(0, 4);
for (let i = 0; i < 4; i++) {
if (checksum[i] !== expectedChecksum[i]) {
return null; // Checksum mismatch
}
}
// Decode varint tag
const varintResult = decodeVarint(payload);
if (varintResult === null) {
return null;
}
const { value: tag, bytesRead } = varintResult;
const data = payload.slice(bytesRead);
return { tag, data };
}
export default {
encode,
decode,
encodeVarint,
decodeVarint,
encodeAddress,
decodeAddress
};
+255
View File
@@ -0,0 +1,255 @@
/**
* Blake2b hash function
* Pure JavaScript implementation based on RFC 7693
*/
// IV constants
const IV = new Uint32Array([
0xf3bcc908, 0x6a09e667,
0x84caa73b, 0xbb67ae85,
0xfe94f82b, 0x3c6ef372,
0x5f1d36f1, 0xa54ff53a,
0xade682d1, 0x510e527f,
0x2b3e6c1f, 0x9b05688c,
0xfb41bd6b, 0x1f83d9ab,
0x137e2179, 0x5be0cd19
]);
// Sigma permutation table
const SIGMA = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
[14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
[11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4],
[7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
[9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13],
[2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
[12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11],
[13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
[6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5],
[10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
[14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3]
];
// 64-bit operations using two 32-bit values (lo, hi)
// v is Uint32Array where v[2i] = low 32 bits, v[2i+1] = high 32 bits
function ADD64AA(v, a, b) {
const o0 = v[a] + v[b];
let o1 = v[a + 1] + v[b + 1];
if (o0 >= 0x100000000) o1++;
v[a] = o0 >>> 0;
v[a + 1] = o1 >>> 0;
}
function ADD64AC(v, a, b0, b1) {
let o0 = v[a] + b0;
let o1 = v[a + 1] + b1;
if (o0 >= 0x100000000) o1++;
v[a] = o0 >>> 0;
v[a + 1] = o1 >>> 0;
}
function XOR64(v, a, b) {
v[a] ^= v[b];
v[a + 1] ^= v[b + 1];
}
function ROTR64(x, y, c) {
// Rotate right by c bits, result in x
const xl = x[0], xh = x[1];
if (c < 32) {
y[0] = (xl >>> c) | (xh << (32 - c));
y[1] = (xh >>> c) | (xl << (32 - c));
} else if (c === 32) {
y[0] = xh;
y[1] = xl;
} else {
const cc = c - 32;
y[0] = (xh >>> cc) | (xl << (32 - cc));
y[1] = (xl >>> cc) | (xh << (32 - cc));
}
}
// G mixing function
function G(v, m, a, b, c, d, ix, iy) {
const x0 = m[ix * 2], x1 = m[ix * 2 + 1];
const y0 = m[iy * 2], y1 = m[iy * 2 + 1];
const tmp = [0, 0];
// a = a + b + x
ADD64AA(v, a * 2, b * 2);
ADD64AC(v, a * 2, x0, x1);
// d = rotr64(d ^ a, 32)
XOR64(v, d * 2, a * 2);
tmp[0] = v[d * 2];
tmp[1] = v[d * 2 + 1];
v[d * 2] = tmp[1];
v[d * 2 + 1] = tmp[0];
// c = c + d
ADD64AA(v, c * 2, d * 2);
// b = rotr64(b ^ c, 24)
XOR64(v, b * 2, c * 2);
tmp[0] = v[b * 2];
tmp[1] = v[b * 2 + 1];
v[b * 2] = (tmp[0] >>> 24) | (tmp[1] << 8);
v[b * 2 + 1] = (tmp[1] >>> 24) | (tmp[0] << 8);
// a = a + b + y
ADD64AA(v, a * 2, b * 2);
ADD64AC(v, a * 2, y0, y1);
// d = rotr64(d ^ a, 16)
XOR64(v, d * 2, a * 2);
tmp[0] = v[d * 2];
tmp[1] = v[d * 2 + 1];
v[d * 2] = (tmp[0] >>> 16) | (tmp[1] << 16);
v[d * 2 + 1] = (tmp[1] >>> 16) | (tmp[0] << 16);
// c = c + d
ADD64AA(v, c * 2, d * 2);
// b = rotr64(b ^ c, 63)
XOR64(v, b * 2, c * 2);
tmp[0] = v[b * 2];
tmp[1] = v[b * 2 + 1];
v[b * 2] = (tmp[1] >>> 31) | (tmp[0] << 1);
v[b * 2 + 1] = (tmp[0] >>> 31) | (tmp[1] << 1);
}
/**
* Blake2b context
*/
class Blake2bCtx {
constructor(outlen, key) {
this.h = new Uint32Array(16);
this.b = new Uint8Array(128);
this.c = 0; // pointer within buffer
this.t = 0; // total bytes
this.outlen = outlen;
// Initialize state with IV
for (let i = 0; i < 16; i++) {
this.h[i] = IV[i];
}
// Mix in parameters: outlen, keylen
const keylen = key ? key.length : 0;
this.h[0] ^= 0x01010000 ^ (keylen << 8) ^ outlen;
// If keyed, process key block
if (keylen > 0) {
const block = new Uint8Array(128);
for (let i = 0; i < keylen; i++) block[i] = key[i];
this.update(block);
}
}
compress(last) {
const v = new Uint32Array(32);
const m = new Uint32Array(32);
// Initialize v[0..15] = h[0..7] (as 64-bit words)
for (let i = 0; i < 16; i++) v[i] = this.h[i];
// Initialize v[16..31] = IV (as 64-bit words)
for (let i = 0; i < 16; i++) v[16 + i] = IV[i];
// v[12] ^= t (low 64 bits of counter)
v[24] ^= this.t >>> 0;
v[25] ^= (this.t / 0x100000000) >>> 0;
// v[14] ^= 0xffffffffffffffff if last block
if (last) {
v[28] = ~v[28];
v[29] = ~v[29];
}
// Load message block as 16 64-bit words
for (let i = 0; i < 16; i++) {
const off = i * 8;
m[i * 2] = this.b[off] | (this.b[off + 1] << 8) | (this.b[off + 2] << 16) | (this.b[off + 3] << 24);
m[i * 2 + 1] = this.b[off + 4] | (this.b[off + 5] << 8) | (this.b[off + 6] << 16) | (this.b[off + 7] << 24);
}
// 12 rounds
for (let round = 0; round < 12; round++) {
const s = SIGMA[round];
G(v, m, 0, 4, 8, 12, s[0], s[1]);
G(v, m, 1, 5, 9, 13, s[2], s[3]);
G(v, m, 2, 6, 10, 14, s[4], s[5]);
G(v, m, 3, 7, 11, 15, s[6], s[7]);
G(v, m, 0, 5, 10, 15, s[8], s[9]);
G(v, m, 1, 6, 11, 12, s[10], s[11]);
G(v, m, 2, 7, 8, 13, s[12], s[13]);
G(v, m, 3, 4, 9, 14, s[14], s[15]);
}
// h = h ^ v[0..7] ^ v[8..15]
for (let i = 0; i < 16; i++) {
this.h[i] ^= v[i] ^ v[16 + i];
}
}
update(input) {
for (let i = 0; i < input.length; i++) {
if (this.c === 128) {
this.t += this.c;
this.compress(false);
this.c = 0;
}
this.b[this.c++] = input[i];
}
}
final() {
this.t += this.c;
// Pad with zeros
while (this.c < 128) {
this.b[this.c++] = 0;
}
this.compress(true);
// Output
const out = new Uint8Array(this.outlen);
for (let i = 0; i < this.outlen; i++) {
out[i] = (this.h[i >> 2] >> (8 * (i & 3))) & 0xff;
}
return out;
}
}
/**
* Blake2b hash
* @param {Uint8Array} input - Input data
* @param {number} outlen - Output length in bytes (1-64)
* @param {Uint8Array} [key] - Optional key (up to 64 bytes)
* @returns {Uint8Array} Hash output
*/
export function blake2b(input, outlen, key = null) {
if (outlen < 1 || outlen > 64) throw new Error('Invalid output length');
if (key && key.length > 64) throw new Error('Key too long');
const ctx = new Blake2bCtx(outlen, key);
ctx.update(input);
return ctx.final();
}
/**
* Blake2b hex output
* @param {Uint8Array} input - Input data
* @param {number} outlen - Output length in bytes
* @param {Uint8Array} [key] - Optional key
* @returns {string} Hex string
*/
export function blake2bHex(input, outlen, key = null) {
const hash = blake2b(input, outlen, key);
return Array.from(hash).map(b => b.toString(16).padStart(2, '0')).join('');
}
export default { blake2b, blake2bHex };
+162
View File
@@ -0,0 +1,162 @@
/**
* CARROT Key Derivation Functions
* Implements key derivation as per Salvium/Monero CARROT specification
*/
import { blake2b } from './blake2b.js';
import { hexToBytes, bytesToHex } from './address.js';
// Group order L for scalar reduction
const L = (1n << 252n) + 27742317777372353535851937790883648493n;
// Create length-prefixed domain separator (matches Salvium SpFixedTranscript format)
function makeDomainSep(str) {
const strBytes = new TextEncoder().encode(str);
const result = new Uint8Array(1 + strBytes.length);
result[0] = strBytes.length; // Length prefix
result.set(strBytes, 1);
return result;
}
// Domain separators (from Salvium carrot_core/config.h)
// Each is length-prefixed as per SpFixedTranscript format
const DOMAIN_SEP = {
PROVE_SPEND_KEY: makeDomainSep("Carrot prove-spend key"),
VIEW_BALANCE_SECRET: makeDomainSep("Carrot view-balance secret"),
GENERATE_IMAGE_KEY: makeDomainSep("Carrot generate-image key"),
INCOMING_VIEW_KEY: makeDomainSep("Carrot incoming view key"),
GENERATE_ADDRESS_SECRET: makeDomainSep("Carrot generate-address secret")
};
/**
* Reduce a 64-byte value modulo L (curve order)
* This is sc_reduce from ref10
* @param {Uint8Array} bytes - 64 bytes to reduce
* @returns {Uint8Array} 32-byte scalar
*/
function scReduce(bytes) {
// Convert 64 bytes to BigInt (little-endian)
let n = 0n;
for (let i = 63; i >= 0; i--) {
n = (n << 8n) | BigInt(bytes[i]);
}
// Reduce mod L
n = n % L;
// Convert back to 32 bytes (little-endian)
const result = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
result[i] = Number(n & 0xffn);
n = n >> 8n;
}
return result;
}
/**
* H_32: 32-byte keyed hash
* @param {Uint8Array} domainSep - Domain separator
* @param {Uint8Array} key - 32-byte key
* @returns {Uint8Array} 32-byte hash
*/
function deriveBytes32(domainSep, key) {
return blake2b(domainSep, 32, key);
}
/**
* H_n: Scalar derivation (hash to 64 bytes, then reduce mod L)
* @param {Uint8Array} domainSep - Domain separator
* @param {Uint8Array} key - 32-byte key
* @returns {Uint8Array} 32-byte scalar
*/
function deriveScalar(domainSep, key) {
const hash64 = blake2b(domainSep, 64, key);
return scReduce(hash64);
}
/**
* Derive view-balance secret from master secret
* s_vb = H_32("Carrot view-balance secret", s_master)
* @param {Uint8Array} masterSecret - 32-byte master secret (spend key)
* @returns {Uint8Array} 32-byte view-balance secret
*/
export function makeViewBalanceSecret(masterSecret) {
return deriveBytes32(DOMAIN_SEP.VIEW_BALANCE_SECRET, masterSecret);
}
/**
* Derive view-incoming key from view-balance secret
* k_vi = H_n("Carrot incoming view key", s_vb)
* @param {Uint8Array} viewBalanceSecret - 32-byte view-balance secret
* @returns {Uint8Array} 32-byte view-incoming key
*/
export function makeViewIncomingKey(viewBalanceSecret) {
return deriveScalar(DOMAIN_SEP.INCOMING_VIEW_KEY, viewBalanceSecret);
}
/**
* Derive prove-spend key from master secret
* k_ps = H_n("Carrot prove-spend key", s_master)
* @param {Uint8Array} masterSecret - 32-byte master secret
* @returns {Uint8Array} 32-byte prove-spend key
*/
export function makeProveSpendKey(masterSecret) {
return deriveScalar(DOMAIN_SEP.PROVE_SPEND_KEY, masterSecret);
}
/**
* Derive generate-image key from view-balance secret
* k_gi = H_n("Carrot generate-image key", s_vb)
* @param {Uint8Array} viewBalanceSecret - 32-byte view-balance secret
* @returns {Uint8Array} 32-byte generate-image key
*/
export function makeGenerateImageKey(viewBalanceSecret) {
return deriveScalar(DOMAIN_SEP.GENERATE_IMAGE_KEY, viewBalanceSecret);
}
/**
* Derive generate-address secret from view-balance secret
* s_ga = H_32("Carrot generate-address secret", s_vb)
* @param {Uint8Array} viewBalanceSecret - 32-byte view-balance secret
* @returns {Uint8Array} 32-byte generate-address secret
*/
export function makeGenerateAddressSecret(viewBalanceSecret) {
return deriveBytes32(DOMAIN_SEP.GENERATE_ADDRESS_SECRET, viewBalanceSecret);
}
/**
* Derive all CARROT keys from master secret
* @param {Uint8Array|string} masterSecret - 32-byte master secret or hex string
* @returns {Object} All derived keys as hex strings
*/
export function deriveCarrotKeys(masterSecret) {
// Convert hex string to bytes if needed
if (typeof masterSecret === 'string') {
masterSecret = hexToBytes(masterSecret);
}
const viewBalanceSecret = makeViewBalanceSecret(masterSecret);
const proveSpendKey = makeProveSpendKey(masterSecret);
const viewIncomingKey = makeViewIncomingKey(viewBalanceSecret);
const generateImageKey = makeGenerateImageKey(viewBalanceSecret);
const generateAddressSecret = makeGenerateAddressSecret(viewBalanceSecret);
return {
masterSecret: bytesToHex(masterSecret),
proveSpendKey: bytesToHex(proveSpendKey),
viewBalanceSecret: bytesToHex(viewBalanceSecret),
generateImageKey: bytesToHex(generateImageKey),
viewIncomingKey: bytesToHex(viewIncomingKey),
generateAddressSecret: bytesToHex(generateAddressSecret)
};
}
export default {
makeViewBalanceSecret,
makeViewIncomingKey,
makeProveSpendKey,
makeGenerateImageKey,
makeGenerateAddressSecret,
deriveCarrotKeys
};
+129
View File
@@ -0,0 +1,129 @@
/**
* Salvium Network and Address Constants
*
* This module contains all address prefixes for Salvium across all network types
* and address formats (Legacy CryptoNote and CARROT).
*/
// Network types
export const NETWORK = {
MAINNET: 'mainnet',
TESTNET: 'testnet',
STAGENET: 'stagenet'
};
// Address types
export const ADDRESS_TYPE = {
STANDARD: 'standard',
INTEGRATED: 'integrated',
SUBADDRESS: 'subaddress'
};
// Address formats
export const ADDRESS_FORMAT = {
LEGACY: 'legacy', // CryptoNote style (SaLv...)
CARROT: 'carrot' // CARROT style (SC1...)
};
/**
* Address prefixes organized by network, format, and type
* Values are BigInt for handling large prefixes
*/
export const PREFIXES = {
[NETWORK.MAINNET]: {
[ADDRESS_FORMAT.LEGACY]: {
[ADDRESS_TYPE.STANDARD]: { prefix: 0x3ef318n, text: 'SaLv' },
[ADDRESS_TYPE.INTEGRATED]: { prefix: 0x55ef318n, text: 'SaLvi' },
[ADDRESS_TYPE.SUBADDRESS]: { prefix: 0xf5ef318n, text: 'SaLvs' }
},
[ADDRESS_FORMAT.CARROT]: {
[ADDRESS_TYPE.STANDARD]: { prefix: 0x180c96n, text: 'SC1' },
[ADDRESS_TYPE.INTEGRATED]: { prefix: 0x2ccc96n, text: 'SC1i' },
[ADDRESS_TYPE.SUBADDRESS]: { prefix: 0x314c96n, text: 'SC1s' }
}
},
[NETWORK.TESTNET]: {
[ADDRESS_FORMAT.LEGACY]: {
[ADDRESS_TYPE.STANDARD]: { prefix: 0x15beb318n, text: 'SaLvT' },
[ADDRESS_TYPE.INTEGRATED]: { prefix: 0xd055eb318n, text: 'SaLvTi' },
[ADDRESS_TYPE.SUBADDRESS]: { prefix: 0xa59eb318n, text: 'SaLvTs' }
},
[ADDRESS_FORMAT.CARROT]: {
[ADDRESS_TYPE.STANDARD]: { prefix: 0x254c96n, text: 'SC1T' },
[ADDRESS_TYPE.INTEGRATED]: { prefix: 0x1ac50c96n, text: 'SC1Ti' },
[ADDRESS_TYPE.SUBADDRESS]: { prefix: 0x3c54c96n, text: 'SC1Ts' }
}
},
[NETWORK.STAGENET]: {
[ADDRESS_FORMAT.LEGACY]: {
[ADDRESS_TYPE.STANDARD]: { prefix: 0x149eb318n, text: 'SaLvS' },
[ADDRESS_TYPE.INTEGRATED]: { prefix: 0xf343eb318n, text: 'SaLvSi' },
[ADDRESS_TYPE.SUBADDRESS]: { prefix: 0x2d47eb318n, text: 'SaLvSs' }
},
[ADDRESS_FORMAT.CARROT]: {
[ADDRESS_TYPE.STANDARD]: { prefix: 0x24cc96n, text: 'SC1S' },
[ADDRESS_TYPE.INTEGRATED]: { prefix: 0x1a848c96n, text: 'SC1Si' },
[ADDRESS_TYPE.SUBADDRESS]: { prefix: 0x384cc96n, text: 'SC1Ss' }
}
}
};
/**
* Build a reverse lookup map from prefix value to address info
*/
export const PREFIX_MAP = new Map();
for (const [network, formats] of Object.entries(PREFIXES)) {
for (const [format, types] of Object.entries(formats)) {
for (const [type, info] of Object.entries(types)) {
PREFIX_MAP.set(info.prefix, {
network,
format,
type,
text: info.text
});
}
}
}
/**
* Get prefix info by prefix value
* @param {BigInt} prefix - The prefix value
* @returns {Object|null} - Address info or null if not found
*/
export function getPrefixInfo(prefix) {
return PREFIX_MAP.get(prefix) || null;
}
/**
* Get prefix for a specific address configuration
* @param {string} network - Network type (mainnet, testnet, stagenet)
* @param {string} format - Address format (legacy, carrot)
* @param {string} type - Address type (standard, integrated, subaddress)
* @returns {BigInt|null} - The prefix value or null if invalid
*/
export function getPrefix(network, format, type) {
return PREFIXES[network]?.[format]?.[type]?.prefix || null;
}
// Key sizes
export const KEY_SIZE = 32; // 32 bytes = 256 bits
export const CHECKSUM_SIZE = 4;
export const PAYMENT_ID_SIZE = 8; // For integrated addresses
// Address data sizes (without prefix)
export const ADDRESS_DATA_SIZE = {
[ADDRESS_TYPE.STANDARD]: KEY_SIZE * 2, // spend_key + view_key = 64 bytes
[ADDRESS_TYPE.INTEGRATED]: KEY_SIZE * 2 + PAYMENT_ID_SIZE, // + payment_id = 72 bytes
[ADDRESS_TYPE.SUBADDRESS]: KEY_SIZE * 2 // spend_key + view_key = 64 bytes
};
// Base58 alphabet (Monero variant)
export const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
// Full block size for Base58 encoding (8 bytes -> 11 chars)
export const BASE58_FULL_BLOCK_SIZE = 8;
export const BASE58_FULL_ENCODED_BLOCK_SIZE = 11;
// Encoded block sizes for partial blocks
export const BASE58_ENCODED_BLOCK_SIZES = [0, 2, 3, 5, 6, 7, 9, 10, 11];
+714
View File
@@ -0,0 +1,714 @@
/**
* Ed25519 Elliptic Curve Operations
*
* Direct port from Salvium/Monero ref10 logic using BigInt for correctness.
* Field elements are represented as BigInt in range [0, p) where p = 2^255 - 19
*/
// Prime field: p = 2^255 - 19
const P = (1n << 255n) - 19n;
// Curve constant d = -121665/121666 mod p
const D = 37095705934669439343138083508754565189542113879843219016388785533085940283555n;
// 2*d
const D2 = (2n * D) % P;
// sqrt(-1) mod p
const I = 19681161376707505956807079304988542015446066515923890162744021073123829784752n;
// Base point G (Ed25519 standard base point)
// GY = 4/5 mod P, GX is the positive (even) square root satisfying the curve equation
const GX = 15112221349535400772501151409588531511454012693041857206046113283949847762202n;
const GY = 46316835694926478169428394003475163141307993866256225615783033603165251855960n;
// Generator T for CARROT/FCMP++ (from Salvium generators.cpp)
// T = H_p(keccak("Monero Generator T"))
// Encoded as: 966fc66b82cd56cf85eaec801c42845f5f408878d1561e00d3d7ded2794d094f
const T_BYTES = new Uint8Array([
0x96, 0x6f, 0xc6, 0x6b, 0x82, 0xcd, 0x56, 0xcf,
0x85, 0xea, 0xec, 0x80, 0x1c, 0x42, 0x84, 0x5f,
0x5f, 0x40, 0x88, 0x78, 0xd1, 0x56, 0x1e, 0x00,
0xd3, 0xd7, 0xde, 0xd2, 0x79, 0x4d, 0x09, 0x4f
]);
// Group order L
const L = (1n << 252n) + 27742317777372353535851937790883648493n;
// Field operations
function feAdd(a, b) {
return (a + b) % P;
}
function feSub(a, b) {
return (a - b + P) % P;
}
function feMul(a, b) {
return (a * b) % P;
}
function feSq(a) {
return (a * a) % P;
}
function feNeg(a) {
return (P - a) % P;
}
// Modular exponentiation
function fePow(base, exp) {
let result = 1n;
base = base % P;
while (exp > 0n) {
if (exp & 1n) result = (result * base) % P;
exp = exp >> 1n;
base = (base * base) % P;
}
return result;
}
// Modular inverse using Fermat's little theorem: a^(-1) = a^(p-2) mod p
function feInv(a) {
return fePow(a, P - 2n);
}
// Square root mod p (for point decompression)
// p ≡ 5 (mod 8), so sqrt(a) = a^((p+3)/8) or i*a^((p+3)/8)
function feSqrt(a) {
const exp = (P + 3n) / 8n;
let r = fePow(a, exp);
// Check if r^2 = a
if (feSq(r) === a) return r;
// Try i*r
r = feMul(r, I);
if (feSq(r) === a) return r;
return null; // No square root exists
}
// Point in extended coordinates: (X:Y:Z:T) where x=X/Z, y=Y/Z, xy=T/Z
// Represented as {X, Y, Z, T} with BigInt values
function pointZero() {
return { X: 0n, Y: 1n, Z: 1n, T: 0n };
}
function pointFromXY(x, y) {
return { X: x, Y: y, Z: 1n, T: feMul(x, y) };
}
function pointCopy(p) {
return { X: p.X, Y: p.Y, Z: p.Z, T: p.T };
}
// Point addition using unified formula (works for doubling too)
// Based on https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html
function pointAdd(p, q) {
const A = feMul(feSub(p.Y, p.X), feSub(q.Y, q.X));
const B = feMul(feAdd(p.Y, p.X), feAdd(q.Y, q.X));
const C = feMul(feMul(p.T, q.T), D2);
const D_ = feMul(p.Z, q.Z);
const D2_ = feAdd(D_, D_);
const E = feSub(B, A);
const F = feSub(D2_, C);
const G = feAdd(D2_, C);
const H = feAdd(B, A);
return {
X: feMul(E, F),
Y: feMul(G, H),
Z: feMul(F, G),
T: feMul(E, H)
};
}
// Point doubling using ge_p2_dbl formula from Salvium ref10
// This matches ge_p2_dbl.h exactly
function pointDouble(p) {
// Matching ge_p2_dbl.h exactly:
// XX = X^2
const XX = feSq(p.X);
// YY = Y^2
const YY = feSq(p.Y);
// B = 2*Z^2 (fe_sq2 computes 2*a^2)
const B = feMul(2n, feSq(p.Z));
// A = X + Y
const A = feAdd(p.X, p.Y);
// AA = A^2 = (X+Y)^2
const AA = feSq(A);
// Y3 = YY + XX
const Y3_p1p1 = feAdd(YY, XX);
// Z3 = YY - XX
const Z3_p1p1 = feSub(YY, XX);
// X3 = AA - Y3 = (X+Y)^2 - (YY+XX) = 2XY
const X3_p1p1 = feSub(AA, Y3_p1p1);
// T3 = B - Z3 = 2Z^2 - (YY-XX)
const T3_p1p1 = feSub(B, Z3_p1p1);
// Now convert from p1p1 to p3 (extended) using ge_p1p1_to_p3
// r->X = p->X * p->T
// r->Y = p->Y * p->Z
// r->Z = p->Z * p->T
// r->T = p->X * p->Y
return {
X: feMul(X3_p1p1, T3_p1p1),
Y: feMul(Y3_p1p1, Z3_p1p1),
Z: feMul(Z3_p1p1, T3_p1p1),
T: feMul(X3_p1p1, Y3_p1p1)
};
}
// Unified add formula for comparison
function pointAddUnified(p, q) {
const A = feMul(feSub(p.Y, p.X), feSub(q.Y, q.X));
const B = feMul(feAdd(p.Y, p.X), feAdd(q.Y, q.X));
const C = feMul(feMul(p.T, q.T), D2);
const D_ = feMul(p.Z, q.Z);
const D2_ = feAdd(D_, D_);
const E = feSub(B, A);
const F = feSub(D2_, C);
const G = feAdd(D2_, C);
const H = feAdd(B, A);
return {
X: feMul(E, F),
Y: feMul(G, H),
Z: feMul(F, G),
T: feMul(E, H)
};
}
// Scalar multiplication using double-and-add
function scalarMult(p, s) {
let result = pointZero();
let base = pointCopy(p);
// Process scalar bits from LSB to MSB
while (s > 0n) {
if (s & 1n) {
result = pointAdd(result, base);
}
base = pointDouble(base);
s = s >> 1n;
}
return result;
}
// Compress point to 32 bytes
function pointToBytes(p) {
// Convert to affine: x = X/Z, y = Y/Z
const zi = feInv(p.Z);
const x = feMul(p.X, zi);
const y = feMul(p.Y, zi);
// Encode y with sign of x in high bit
const bytes = new Uint8Array(32);
let yy = y;
for (let i = 0; i < 32; i++) {
bytes[i] = Number(yy & 0xffn);
yy = yy >> 8n;
}
// Set high bit if x is "negative" (odd)
if (x & 1n) {
bytes[31] |= 0x80;
}
return bytes;
}
// Decompress 32 bytes to point
function pointFromBytes(bytes) {
// Extract y (clear high bit)
let y = 0n;
for (let i = 31; i >= 0; i--) {
y = (y << 8n) | BigInt(bytes[i]);
}
const xSign = (y >> 255n) & 1n;
y = y & ((1n << 255n) - 1n);
if (y >= P) return null;
// Recover x from curve equation: x^2 = (y^2 - 1) / (d*y^2 + 1)
const y2 = feSq(y);
const num = feSub(y2, 1n);
const den = feAdd(feMul(D, y2), 1n);
const denInv = feInv(den);
const x2 = feMul(num, denInv);
let x = feSqrt(x2);
if (x === null) return null;
// Adjust sign
if ((x & 1n) !== xSign) {
x = feNeg(x);
}
return pointFromXY(x, y);
}
// Convert scalar bytes (little-endian) to BigInt
function scalarFromBytes(bytes) {
let s = 0n;
for (let i = bytes.length - 1; i >= 0; i--) {
s = (s << 8n) | BigInt(bytes[i]);
}
return s;
}
// Public API
/**
* Check if scalar is valid (< L)
* @param {Uint8Array} s - 32-byte scalar
* @returns {boolean} true if valid
*/
export function scalarCheck(s) {
if (s.length !== 32) return false;
const scalar = scalarFromBytes(s);
return scalar < L;
}
/**
* Check if scalar is non-zero
* @param {Uint8Array} s - 32-byte scalar
* @returns {boolean} true if non-zero
*/
export function scalarIsNonzero(s) {
const scalar = scalarFromBytes(s);
return scalar !== 0n;
}
/**
* Scalar subtraction: r = a - b (mod L)
* @param {Uint8Array} r - 32-byte output
* @param {Uint8Array} a - 32-byte input
* @param {Uint8Array} b - 32-byte input
*/
export function scalarSub(r, a, b) {
const aVal = scalarFromBytes(a);
const bVal = scalarFromBytes(b);
let result = (aVal - bVal) % L;
if (result < 0n) result += L;
// Convert back to bytes (little-endian)
for (let i = 0; i < 32; i++) {
r[i] = Number(result & 0xffn);
result = result >> 8n;
}
}
/**
* Scalar multiplication with base point: s * G
* @param {Uint8Array} s - 32-byte scalar
* @returns {Uint8Array} 32-byte compressed public key
*/
export function scalarMultBase(s) {
const scalar = scalarFromBytes(s);
const G = pointFromXY(GX, GY);
const result = scalarMult(G, scalar);
return pointToBytes(result);
}
/**
* Scalar multiplication with arbitrary point: s * P
* @param {Uint8Array} s - 32-byte scalar
* @param {Uint8Array} P - 32-byte compressed point
* @returns {Uint8Array|null} 32-byte compressed result, or null if P is invalid
*/
export function scalarMultPoint(s, P) {
const scalar = scalarFromBytes(s);
const point = pointFromBytes(P);
if (!point) return null;
const result = scalarMult(point, scalar);
return pointToBytes(result);
}
/**
* Get the T generator point (used in CARROT)
* @returns {Uint8Array} 32-byte compressed T point
*/
export function getGeneratorT() {
return new Uint8Array(T_BYTES);
}
/**
* Get the G generator point (standard Ed25519 base point)
* @returns {Uint8Array} 32-byte compressed G point
*/
export function getGeneratorG() {
const G = pointFromXY(GX, GY);
return pointToBytes(G);
}
/**
* Point addition: P + Q
* @param {Uint8Array} P - 32-byte compressed point
* @param {Uint8Array} Q - 32-byte compressed point
* @returns {Uint8Array|null} 32-byte compressed result, or null if invalid
*/
export function pointAddCompressed(P, Q) {
const p = pointFromBytes(P);
const q = pointFromBytes(Q);
if (!p || !q) return null;
const result = pointAdd(p, q);
return pointToBytes(result);
}
/**
* Compute CARROT account spend public key: K_s = k_gi * G + k_ps * T
* @param {Uint8Array} k_gi - 32-byte generate-image key
* @param {Uint8Array} k_ps - 32-byte prove-spend key
* @returns {Uint8Array} 32-byte compressed public key
*/
export function computeCarrotSpendPubkey(k_gi, k_ps) {
const giScalar = scalarFromBytes(k_gi);
const psScalar = scalarFromBytes(k_ps);
// Get generators
const G = pointFromXY(GX, GY);
const T = pointFromBytes(T_BYTES);
if (!T) throw new Error('Failed to decode T generator');
// K_s = k_gi * G + k_ps * T
const giG = scalarMult(G, giScalar);
const psT = scalarMult(T, psScalar);
const result = pointAdd(giG, psT);
return pointToBytes(result);
}
/**
* Compute CARROT account view public key: K_v = k_vi * K_s
* Used for subaddress derivation, NOT for main address
* @param {Uint8Array} k_vi - 32-byte view-incoming key
* @param {Uint8Array} K_s - 32-byte spend public key
* @returns {Uint8Array|null} 32-byte compressed view public key, or null if K_s invalid
*/
export function computeCarrotAccountViewPubkey(k_vi, K_s) {
return scalarMultPoint(k_vi, K_s);
}
/**
* Compute CARROT main address view public key: K_v = k_vi * G
* This is what goes into the main address (j=0)
* @param {Uint8Array} k_vi - 32-byte view-incoming key
* @returns {Uint8Array} 32-byte compressed view public key
*/
export function computeCarrotMainAddressViewPubkey(k_vi) {
return scalarMultBase(k_vi);
}
/**
* Decompress a public key (ge_frombytes_vartime equivalent)
* @param {Uint8Array} pk - 32-byte compressed public key
* @returns {Object|null} Point or null if invalid
*/
export { pointFromBytes };
/**
* Compress a point to bytes (ge_tobytes equivalent)
* @param {Object} p - Point {X, Y, Z, T}
* @returns {Uint8Array} 32-byte compressed point
*/
export { pointToBytes };
/**
* Double scalar multiplication: aP + bG
* @param {Uint8Array} a - 32-byte scalar
* @param {Object} P - Point
* @param {Uint8Array} b - 32-byte scalar
* @returns {Uint8Array} 32-byte compressed result
*/
export function doubleScalarMultBase(a, P, b) {
const aScalar = scalarFromBytes(a);
const bScalar = scalarFromBytes(b);
const G = pointFromXY(GX, GY);
// Compute a*P + b*G
const aP = scalarMult(P, aScalar);
const bG = scalarMult(G, bScalar);
const result = pointAdd(aP, bG);
return pointToBytes(result);
}
/**
* Check if a point is the identity (neutral element)
* @param {Uint8Array} p - 32-byte compressed point
* @returns {boolean} true if identity
*/
export function isIdentity(p) {
// Identity point encodes as y=1, x=0 -> bytes = [1, 0, 0, ..., 0]
if (p[0] !== 1) return false;
for (let i = 1; i < 32; i++) {
if (p[i] !== 0) return false;
}
return true;
}
// Debug functions
export function testDouble() {
const G = pointFromXY(GX, GY);
const G2 = pointAdd(G, G);
return pointToBytes(G2);
}
export function getBasePoint() {
const G = pointFromXY(GX, GY);
return pointToBytes(G);
}
export function test2G() {
const scalar = new Uint8Array(32);
scalar[0] = 2;
return scalarMultBase(scalar);
}
// Debug: test G + identity = G
export function testIdentity() {
const G = pointFromXY(GX, GY);
const identity = pointZero();
const result = pointAdd(G, identity);
return pointToBytes(result);
}
// Debug: get affine coordinates of 2G
export function get2GAffine() {
const G = pointFromXY(GX, GY);
const G2 = pointAdd(G, G);
const zi = feInv(G2.Z);
const x = feMul(G2.X, zi);
const y = feMul(G2.Y, zi);
return { x: x.toString(), y: y.toString() };
}
// Debug: check if point (x,y) is on the curve: -x² + y² = 1 + d*x²*y²
export function isOnCurve(x, y) {
const x2 = feSq(x);
const y2 = feSq(y);
const lhs = feSub(y2, x2); // -x² + y² = y² - x²
const rhs = feAdd(1n, feMul(D, feMul(x2, y2))); // 1 + d*x²*y²
return lhs === rhs;
}
// Debug: show curve equation values for G
export function debugCurveEquation() {
const x2 = feSq(GX);
const y2 = feSq(GY);
const lhs = feSub(y2, x2);
const dxy2 = feMul(D, feMul(x2, y2));
const rhs = feAdd(1n, dxy2);
return {
GX: GX.toString(),
GY: GY.toString(),
D: D.toString(),
P: P.toString(),
x2: x2.toString(),
y2: y2.toString(),
lhs: lhs.toString(), // y² - x²
dxy2: dxy2.toString(), // d*x²*y²
rhs: rhs.toString(), // 1 + d*x²*y²
match: lhs === rhs,
diff: (lhs > rhs ? lhs - rhs : rhs - lhs).toString()
};
}
// Debug: verify G is on curve
export function checkG() {
return isOnCurve(GX, GY);
}
// Debug: verify 2G is on curve
export function check2G() {
const G = pointFromXY(GX, GY);
const G2 = pointAdd(G, G);
const zi = feInv(G2.Z);
const x = feMul(G2.X, zi);
const y = feMul(G2.Y, zi);
return isOnCurve(x, y);
}
// Debug: compare doubling methods
export function compare2GMethods() {
const G = pointFromXY(GX, GY);
// Method 1: Using pointDouble (Salvium ge_p2_dbl)
const G2_double = pointDouble(G);
const zi1 = feInv(G2_double.Z);
const x1 = feMul(G2_double.X, zi1);
const y1 = feMul(G2_double.Y, zi1);
// Method 2: Using unified add formula
const G2_add = pointAddUnified(G, G);
const zi2 = feInv(G2_add.Z);
const x2 = feMul(G2_add.X, zi2);
const y2 = feMul(G2_add.Y, zi2);
return {
doubleX: x1.toString(),
doubleY: y1.toString(),
addX: x2.toString(),
addY: y2.toString(),
match: x1 === x2 && y1 === y2,
onCurve1: isOnCurve(x1, y1),
onCurve2: isOnCurve(x2, y2)
};
}
// Debug: decode expected 2G from hex and show coordinates
export function decodeExpected2G() {
// Expected encoding: c9a3f86aae465f0e56513864510f3997561fa2c9e85ea21dc2292309f3cd6022
const hex = 'c9a3f86aae465f0e56513864510f3997561fa2c9e85ea21dc2292309f3cd6022';
const bytes = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
// Decode using our pointFromBytes
const point = pointFromBytes(bytes);
if (!point) return { error: 'Failed to decode point' };
const zi = feInv(point.Z);
const x = feMul(point.X, zi);
const y = feMul(point.Y, zi);
return {
x: x.toString(),
y: y.toString(),
onCurve: isOnCurve(x, y)
};
}
// Debug: verify field operations are correct
export function testFieldOps() {
// Test that (a + b) mod p works
const a = P - 1n; // largest value
const b = 2n;
const sum = feAdd(a, b); // should be 1
// Test subtraction
const diff = feSub(2n, 5n); // should be p - 3
// Test multiplication
const prod = feMul(GX, GY);
// Test identity point (0, 1) - must be on ANY Edwards curve
const identityOnCurve = isOnCurve(0n, 1n);
// Verify y = 4/5 mod p for base point
// 5 * GY mod P should equal 4
const fiveTimesY = feMul(5n, GY);
const yIsFourFifths = fiveTimesY === 4n;
return {
addTest: sum === 1n,
subTest: diff === P - 3n,
prodNonZero: prod > 0n && prod < P,
gxLessThanP: GX < P,
gyLessThanP: GY < P,
dLessThanP: D < P,
identityOnCurve: identityOnCurve,
yIsFourFifths: yIsFourFifths,
fiveTimesY: fiveTimesY.toString()
};
}
// Convert 10-limb ref10 representation to BigInt
// Limbs alternate between 26 and 25 bits: 26,25,26,25,26,25,26,25,26,25
function limbsToFe(limbs) {
const shifts = [0n, 26n, 51n, 77n, 102n, 128n, 153n, 179n, 204n, 230n];
let result = 0n;
for (let i = 0; i < 10; i++) {
result += BigInt(limbs[i]) << shifts[i];
}
// Reduce mod P and handle negatives
result = ((result % P) + P) % P;
return result;
}
// Debug: verify our D constant matches Salvium's d.h
export function verifyDConstant() {
// From Salvium d.h:
const dLimbs = [-10913610, 13857413, -15372611, 6949391, 114729, -8787816, -6275908, -3247719, -18696448, -12055116];
const dFromLimbs = limbsToFe(dLimbs);
// From Salvium d2.h (2*d):
const d2Limbs = [-21827239, -5839606, -30745221, 13898782, 229458, 15978800, -12551817, -6495438, 29715968, 9444199];
const d2FromLimbs = limbsToFe(d2Limbs);
return {
ourD: D.toString(),
salviumD: dFromLimbs.toString(),
dMatch: D === dFromLimbs,
ourD2: D2.toString(),
salviumD2: d2FromLimbs.toString(),
d2Match: D2 === d2FromLimbs
};
}
// Compute x from y for twisted Edwards curve: -x² + y² = 1 + d*x²*y²
// x² = (y² - 1) / (1 + d*y²)
export function computeXFromY(y) {
const y2 = feSq(y);
const num = feSub(y2, 1n); // y² - 1
const den = feAdd(1n, feMul(D, y2)); // 1 + d*y²
const x2 = feMul(num, feInv(den)); // (y² - 1) / (1 + d*y²)
// Get square root
const x = feSqrt(x2);
const xNeg = x ? feNeg(x) : null;
// Verify x² = x2
const xSquaredCheck = x ? feSq(x) === x2 : null;
// Check if computed point is on curve
const computedOnCurve = x ? isOnCurve(x, y) : null;
const computedNegOnCurve = xNeg ? isOnCurve(xNeg, y) : null;
// Check if our GX satisfies x² = x2
const gxSquared = feSq(GX);
const gxSatisfiesX2 = gxSquared === x2;
return {
x2: x2.toString(),
x: x ? x.toString() : 'no sqrt exists',
xNeg: xNeg ? xNeg.toString() : null,
xSquaredCheck: xSquaredCheck,
computedOnCurve: computedOnCurve,
computedNegOnCurve: computedNegOnCurve,
ourGX: GX.toString(),
gxSquared: gxSquared.toString(),
gxSatisfiesX2: gxSatisfiesX2,
xMatchesGX: x === GX,
xNegMatchesGX: xNeg === GX
};
}
export default {
scalarCheck,
scalarIsNonzero,
scalarSub,
scalarMultBase,
scalarMultPoint,
pointFromBytes,
pointToBytes,
pointAddCompressed,
doubleScalarMultBase,
isIdentity,
getGeneratorG,
getGeneratorT,
computeCarrotSpendPubkey,
computeCarrotAccountViewPubkey,
computeCarrotMainAddressViewPubkey,
testDouble,
getBasePoint,
test2G,
testIdentity,
get2GAffine
};
+164
View File
@@ -0,0 +1,164 @@
/**
* salvium-js - JavaScript library for Salvium cryptocurrency
*
* Features:
* - Address validation and parsing for all 18 address types
* - Support for Legacy (CryptoNote) and CARROT address formats
* - Mainnet, Testnet, and Stagenet support
* - Base58 encoding/decoding (Monero variant)
* - Keccak-256 hashing
* - Message signature verification (V1 and V2)
*
* @module salvium-js
*/
// Re-export everything from submodules
export * from './constants.js';
export * from './keccak.js';
export * from './base58.js';
export * from './address.js';
export * from './signature.js';
export * from './blake2b.js';
export * from './carrot.js';
export {
scalarMultBase,
scalarMultPoint,
pointAddCompressed,
getGeneratorG,
getGeneratorT,
computeCarrotSpendPubkey,
computeCarrotAccountViewPubkey,
computeCarrotMainAddressViewPubkey,
testDouble,
getBasePoint,
test2G,
testIdentity,
get2GAffine,
isOnCurve,
checkG,
check2G,
compare2GMethods,
decodeExpected2G,
testFieldOps,
debugCurveEquation,
verifyDConstant,
computeXFromY
} from './ed25519.js';
// Import named exports for combined API object
import {
NETWORK,
ADDRESS_TYPE,
ADDRESS_FORMAT,
PREFIXES
} from './constants.js';
import {
keccak256,
keccak256Hex,
cnFastHash
} from './keccak.js';
import {
encode,
decode,
encodeAddress,
decodeAddress
} from './base58.js';
import {
parseAddress,
isValidAddress,
isMainnet,
isTestnet,
isStagenet,
isCarrot,
isLegacy,
isStandard,
isIntegrated,
isSubaddress,
getSpendPublicKey,
getViewPublicKey,
getPaymentId,
createAddress,
toIntegratedAddress,
toStandardAddress,
describeAddress,
bytesToHex,
hexToBytes
} from './address.js';
import {
verifySignature,
parseSignature
} from './signature.js';
import {
scalarMultBase,
scalarMultPoint,
pointAddCompressed,
getGeneratorG,
getGeneratorT,
computeCarrotSpendPubkey,
computeCarrotAccountViewPubkey,
computeCarrotMainAddressViewPubkey
} from './ed25519.js';
// Main API object
const salvium = {
// Constants
NETWORK,
ADDRESS_TYPE,
ADDRESS_FORMAT,
PREFIXES,
// Keccak
keccak256,
keccak256Hex,
cnFastHash,
// Base58
base58Encode: encode,
base58Decode: decode,
encodeAddress,
decodeAddress,
// Address
parseAddress,
isValidAddress,
isMainnet,
isTestnet,
isStagenet,
isCarrot,
isLegacy,
isStandard,
isIntegrated,
isSubaddress,
getSpendPublicKey,
getViewPublicKey,
getPaymentId,
createAddress,
toIntegratedAddress,
toStandardAddress,
describeAddress,
bytesToHex,
hexToBytes,
// Signatures
verifySignature,
parseSignature,
// Ed25519
scalarMultBase,
scalarMultPoint,
pointAddCompressed,
getGeneratorG,
getGeneratorT,
// CARROT
computeCarrotSpendPubkey,
computeCarrotAccountViewPubkey,
computeCarrotMainAddressViewPubkey
};
export default salvium;
+230
View File
@@ -0,0 +1,230 @@
/**
* Keccak-256 Hash Implementation
*
* Based on js-sha3 by Chen Yi-Cyuan (MIT License)
* https://github.com/nicolo-ribaudo/js-sha3
*
* IMPORTANT: This is Keccak-256, NOT SHA3-256!
* Monero/Salvium use the pre-NIST version with 0x01 padding.
*/
const KECCAK_PADDING = [1, 256, 65536, 16777216];
const SHIFT = [0, 8, 16, 24];
const RC = [
1, 0, 32898, 0, 32906, 2147483648, 2147516416, 2147483648,
32907, 0, 2147483649, 0, 2147516545, 2147483648, 32777, 2147483648,
138, 0, 136, 0, 2147516425, 0, 2147483658, 0,
2147516555, 0, 139, 2147483648, 32905, 2147483648, 32771, 2147483648,
32770, 2147483648, 128, 2147483648, 32778, 0, 2147483658, 2147483648,
2147516545, 2147483648, 32896, 2147483648, 2147483649, 0, 2147516424, 2147483648
];
function keccak(bits) {
const outputBits = bits;
const blockSize = (1600 - bits * 2) >> 5;
const byteCount = blockSize << 2;
const outputBytes = outputBits >> 3;
const padding = KECCAK_PADDING;
return function(message) {
let input;
if (typeof message === 'string') {
input = new TextEncoder().encode(message);
} else if (message instanceof Uint8Array) {
input = message;
} else if (Array.isArray(message)) {
input = new Uint8Array(message);
} else {
throw new Error('Invalid input type');
}
const blocks = [];
const state = [];
for (let i = 0; i < 50; i++) {
state[i] = 0;
}
let blockCount = blockSize;
let byteCountInc = byteCount;
let i, code, end = false, index = 0, block = 0;
let start = 0, length = input.length;
for (i = 0; i < blockCount; i++) {
blocks[i] = 0;
}
while (!end) {
block = 0;
for (i = start; index < length && i < byteCountInc; ++index) {
code = input[index];
block |= code << SHIFT[i++ & 3];
if (i === byteCountInc) {
blocks[(i >> 2) - 1] = block;
block = 0;
} else if ((i & 3) === 0) {
blocks[(i >> 2) - 1] = block;
block = 0;
}
}
start = i - byteCountInc;
if (index === length) {
// Store remaining partial word before adding padding
blocks[i >> 2] |= block;
blocks[i >> 2] |= padding[i & 3];
++index;
}
if (index > length && i < byteCountInc) {
blocks[blockCount - 1] |= 0x80000000;
end = true;
}
for (i = 0; i < blockCount; i++) {
state[i] ^= blocks[i];
blocks[i] = 0;
}
f(state);
}
// Output - use >>> for unsigned right shift to avoid sign extension issues
const output = new Uint8Array(outputBytes);
let outputIndex = 0;
let stateIndex = 0;
while (outputIndex < outputBytes) {
const s = state[stateIndex++];
output[outputIndex++] = s & 0xff;
if (outputIndex < outputBytes) output[outputIndex++] = (s >>> 8) & 0xff;
if (outputIndex < outputBytes) output[outputIndex++] = (s >>> 16) & 0xff;
if (outputIndex < outputBytes) output[outputIndex++] = (s >>> 24) & 0xff;
}
return output;
};
}
function f(s) {
let h, l, n, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9,
b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15,
b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29,
b30, b31, b32, b33, b34, b35, b36, b37, b38, b39, b40, b41, b42, b43,
b44, b45, b46, b47, b48, b49;
for (n = 0; n < 48; n += 2) {
c0 = s[0] ^ s[10] ^ s[20] ^ s[30] ^ s[40];
c1 = s[1] ^ s[11] ^ s[21] ^ s[31] ^ s[41];
c2 = s[2] ^ s[12] ^ s[22] ^ s[32] ^ s[42];
c3 = s[3] ^ s[13] ^ s[23] ^ s[33] ^ s[43];
c4 = s[4] ^ s[14] ^ s[24] ^ s[34] ^ s[44];
c5 = s[5] ^ s[15] ^ s[25] ^ s[35] ^ s[45];
c6 = s[6] ^ s[16] ^ s[26] ^ s[36] ^ s[46];
c7 = s[7] ^ s[17] ^ s[27] ^ s[37] ^ s[47];
c8 = s[8] ^ s[18] ^ s[28] ^ s[38] ^ s[48];
c9 = s[9] ^ s[19] ^ s[29] ^ s[39] ^ s[49];
h = c8 ^ ((c2 << 1) | (c3 >>> 31));
l = c9 ^ ((c3 << 1) | (c2 >>> 31));
s[0] ^= h; s[1] ^= l; s[10] ^= h; s[11] ^= l;
s[20] ^= h; s[21] ^= l; s[30] ^= h; s[31] ^= l; s[40] ^= h; s[41] ^= l;
h = c0 ^ ((c4 << 1) | (c5 >>> 31));
l = c1 ^ ((c5 << 1) | (c4 >>> 31));
s[2] ^= h; s[3] ^= l; s[12] ^= h; s[13] ^= l;
s[22] ^= h; s[23] ^= l; s[32] ^= h; s[33] ^= l; s[42] ^= h; s[43] ^= l;
h = c2 ^ ((c6 << 1) | (c7 >>> 31));
l = c3 ^ ((c7 << 1) | (c6 >>> 31));
s[4] ^= h; s[5] ^= l; s[14] ^= h; s[15] ^= l;
s[24] ^= h; s[25] ^= l; s[34] ^= h; s[35] ^= l; s[44] ^= h; s[45] ^= l;
h = c4 ^ ((c8 << 1) | (c9 >>> 31));
l = c5 ^ ((c9 << 1) | (c8 >>> 31));
s[6] ^= h; s[7] ^= l; s[16] ^= h; s[17] ^= l;
s[26] ^= h; s[27] ^= l; s[36] ^= h; s[37] ^= l; s[46] ^= h; s[47] ^= l;
h = c6 ^ ((c0 << 1) | (c1 >>> 31));
l = c7 ^ ((c1 << 1) | (c0 >>> 31));
s[8] ^= h; s[9] ^= l; s[18] ^= h; s[19] ^= l;
s[28] ^= h; s[29] ^= l; s[38] ^= h; s[39] ^= l; s[48] ^= h; s[49] ^= l;
b0 = s[0]; b1 = s[1];
b32 = (s[11] << 4) | (s[10] >>> 28); b33 = (s[10] << 4) | (s[11] >>> 28);
b14 = (s[20] << 3) | (s[21] >>> 29); b15 = (s[21] << 3) | (s[20] >>> 29);
b46 = (s[31] << 9) | (s[30] >>> 23); b47 = (s[30] << 9) | (s[31] >>> 23);
b28 = (s[40] << 18) | (s[41] >>> 14); b29 = (s[41] << 18) | (s[40] >>> 14);
b20 = (s[2] << 1) | (s[3] >>> 31); b21 = (s[3] << 1) | (s[2] >>> 31);
b2 = (s[13] << 12) | (s[12] >>> 20); b3 = (s[12] << 12) | (s[13] >>> 20);
b34 = (s[22] << 10) | (s[23] >>> 22); b35 = (s[23] << 10) | (s[22] >>> 22);
b16 = (s[33] << 13) | (s[32] >>> 19); b17 = (s[32] << 13) | (s[33] >>> 19);
b48 = (s[42] << 2) | (s[43] >>> 30); b49 = (s[43] << 2) | (s[42] >>> 30);
b40 = (s[5] << 30) | (s[4] >>> 2); b41 = (s[4] << 30) | (s[5] >>> 2);
b22 = (s[14] << 6) | (s[15] >>> 26); b23 = (s[15] << 6) | (s[14] >>> 26);
b4 = (s[25] << 11) | (s[24] >>> 21); b5 = (s[24] << 11) | (s[25] >>> 21);
b36 = (s[34] << 15) | (s[35] >>> 17); b37 = (s[35] << 15) | (s[34] >>> 17);
b18 = (s[45] << 29) | (s[44] >>> 3); b19 = (s[44] << 29) | (s[45] >>> 3);
b10 = (s[6] << 28) | (s[7] >>> 4); b11 = (s[7] << 28) | (s[6] >>> 4);
b42 = (s[17] << 23) | (s[16] >>> 9); b43 = (s[16] << 23) | (s[17] >>> 9);
b24 = (s[26] << 25) | (s[27] >>> 7); b25 = (s[27] << 25) | (s[26] >>> 7);
b6 = (s[36] << 21) | (s[37] >>> 11); b7 = (s[37] << 21) | (s[36] >>> 11);
b38 = (s[47] << 24) | (s[46] >>> 8); b39 = (s[46] << 24) | (s[47] >>> 8);
b30 = (s[8] << 27) | (s[9] >>> 5); b31 = (s[9] << 27) | (s[8] >>> 5);
b12 = (s[18] << 20) | (s[19] >>> 12); b13 = (s[19] << 20) | (s[18] >>> 12);
b44 = (s[29] << 7) | (s[28] >>> 25); b45 = (s[28] << 7) | (s[29] >>> 25);
b26 = (s[38] << 8) | (s[39] >>> 24); b27 = (s[39] << 8) | (s[38] >>> 24);
b8 = (s[48] << 14) | (s[49] >>> 18); b9 = (s[49] << 14) | (s[48] >>> 18);
s[0] = b0 ^ (~b2 & b4); s[1] = b1 ^ (~b3 & b5);
s[10] = b10 ^ (~b12 & b14); s[11] = b11 ^ (~b13 & b15);
s[20] = b20 ^ (~b22 & b24); s[21] = b21 ^ (~b23 & b25);
s[30] = b30 ^ (~b32 & b34); s[31] = b31 ^ (~b33 & b35);
s[40] = b40 ^ (~b42 & b44); s[41] = b41 ^ (~b43 & b45);
s[2] = b2 ^ (~b4 & b6); s[3] = b3 ^ (~b5 & b7);
s[12] = b12 ^ (~b14 & b16); s[13] = b13 ^ (~b15 & b17);
s[22] = b22 ^ (~b24 & b26); s[23] = b23 ^ (~b25 & b27);
s[32] = b32 ^ (~b34 & b36); s[33] = b33 ^ (~b35 & b37);
s[42] = b42 ^ (~b44 & b46); s[43] = b43 ^ (~b45 & b47);
s[4] = b4 ^ (~b6 & b8); s[5] = b5 ^ (~b7 & b9);
s[14] = b14 ^ (~b16 & b18); s[15] = b15 ^ (~b17 & b19);
s[24] = b24 ^ (~b26 & b28); s[25] = b25 ^ (~b27 & b29);
s[34] = b34 ^ (~b36 & b38); s[35] = b35 ^ (~b37 & b39);
s[44] = b44 ^ (~b46 & b48); s[45] = b45 ^ (~b47 & b49);
s[6] = b6 ^ (~b8 & b0); s[7] = b7 ^ (~b9 & b1);
s[16] = b16 ^ (~b18 & b10); s[17] = b17 ^ (~b19 & b11);
s[26] = b26 ^ (~b28 & b20); s[27] = b27 ^ (~b29 & b21);
s[36] = b36 ^ (~b38 & b30); s[37] = b37 ^ (~b39 & b31);
s[46] = b46 ^ (~b48 & b40); s[47] = b47 ^ (~b49 & b41);
s[8] = b8 ^ (~b0 & b2); s[9] = b9 ^ (~b1 & b3);
s[18] = b18 ^ (~b10 & b12); s[19] = b19 ^ (~b11 & b13);
s[28] = b28 ^ (~b20 & b22); s[29] = b29 ^ (~b21 & b23);
s[38] = b38 ^ (~b30 & b32); s[39] = b39 ^ (~b31 & b33);
s[48] = b48 ^ (~b40 & b42); s[49] = b49 ^ (~b41 & b43);
s[0] ^= RC[n]; s[1] ^= RC[n + 1];
}
}
/**
* Keccak-256 hash function
* @param {Uint8Array|Array|string} input - Data to hash
* @returns {Uint8Array} - 32-byte hash
*/
export const keccak256 = keccak(256);
/**
* Compute Keccak-256 and return as hex string
* @param {Uint8Array|Array|string} input - Data to hash
* @returns {string} - 64-character hex string
*/
export function keccak256Hex(input) {
const hash = keccak256(input);
return Array.from(hash).map(b => b.toString(16).padStart(2, '0')).join('');
}
/**
* cn_fast_hash - Salvium/Monero's name for Keccak-256
* @param {Uint8Array|Array|string} input - Data to hash
* @returns {Uint8Array} - 32-byte hash
*/
export const cnFastHash = keccak256;
export default { keccak256, keccak256Hex, cnFastHash };
+296
View File
@@ -0,0 +1,296 @@
/**
* Salvium Message Signature Verification
*
* Supports both V1 (legacy) and V2 (domain-separated) signatures.
*
* Signature format: "SigV1" or "SigV2" + Base58(signature_bytes)
* where signature_bytes = c (32 bytes) + r (32 bytes) + sign_mask (1 byte) = 65 bytes
*
* V1: hash = Keccak256(message)
* V2: hash = Keccak256(domain_separator + spend_key + view_key + mode + varint(len) + message)
*/
import { keccak256 } from './keccak.js';
import { decode } from './base58.js';
import { parseAddress, hexToBytes } from './address.js';
import {
scalarCheck,
scalarIsNonzero,
scalarSub,
pointFromBytes,
doubleScalarMultBase,
isIdentity
} from './ed25519.js';
// Domain separator for V2 signatures (includes null terminator)
const HASH_KEY_MESSAGE_SIGNING = new TextEncoder().encode('MoneroMessageSignature\0');
/**
* Encode a number as a varint (variable-length integer)
* @param {number} n - Number to encode
* @returns {Uint8Array} Varint bytes
*/
function encodeVarint(n) {
const bytes = [];
while (n >= 0x80) {
bytes.push((n & 0x7f) | 0x80);
n >>>= 7;
}
bytes.push(n & 0x7f);
return new Uint8Array(bytes);
}
/**
* Compute V1 message hash (simple Keccak256)
* @param {string} message - The message
* @returns {Uint8Array} 32-byte hash
*/
function getMessageHashV1(message) {
const messageBytes = new TextEncoder().encode(message);
return keccak256(messageBytes);
}
/**
* Compute V2 message hash with domain separation
* @param {string} message - The message
* @param {Uint8Array} spendKey - 32-byte spend public key
* @param {Uint8Array} viewKey - 32-byte view public key
* @param {number} mode - 0 for spend key, 1 for view key
* @returns {Uint8Array} 32-byte hash
*/
function getMessageHashV2(message, spendKey, viewKey, mode) {
const messageBytes = new TextEncoder().encode(message);
const lenVarint = encodeVarint(messageBytes.length);
// Concatenate: domain_separator + spend_key + view_key + mode + len + message
const totalLen = HASH_KEY_MESSAGE_SIGNING.length + 32 + 32 + 1 + lenVarint.length + messageBytes.length;
const data = new Uint8Array(totalLen);
let offset = 0;
data.set(HASH_KEY_MESSAGE_SIGNING, offset);
offset += HASH_KEY_MESSAGE_SIGNING.length;
data.set(spendKey, offset);
offset += 32;
data.set(viewKey, offset);
offset += 32;
data[offset++] = mode;
data.set(lenVarint, offset);
offset += lenVarint.length;
data.set(messageBytes, offset);
return keccak256(data);
}
/**
* Perform Schnorr signature verification
*
* Verifies: R' = r*G + c*P, then checks hash(prefix || key || R') == c
*
* @param {Uint8Array} hash - 32-byte message hash
* @param {Uint8Array} publicKey - 32-byte public key
* @param {Uint8Array} sigC - 32-byte signature c component
* @param {Uint8Array} sigR - 32-byte signature r component
* @returns {boolean} true if signature is valid
*/
function checkSignature(hash, publicKey, sigC, sigR) {
// Validate scalars
if (!scalarCheck(sigC) || !scalarCheck(sigR) || !scalarIsNonzero(sigC)) {
return false;
}
// Decompress public key
const P = pointFromBytes(publicKey);
if (!P) {
return false;
}
// Compute R' = c*P + r*G using double scalar multiplication
const RBytes = doubleScalarMultBase(sigC, P, sigR);
// Check R' is not identity
if (isIdentity(RBytes)) {
return false;
}
// Recompute challenge: c' = H(hash || publicKey || R')
const buf = new Uint8Array(32 + 32 + 32);
buf.set(hash, 0);
buf.set(publicKey, 32);
buf.set(RBytes, 64);
const cPrime = keccak256(buf);
// Reduce c' mod L
const cPrimeReduced = new Uint8Array(32);
reduceScalar32(cPrimeReduced, cPrime);
// Check c' == c
const diff = new Uint8Array(32);
scalarSub(diff, cPrimeReduced, sigC);
return !scalarIsNonzero(diff);
}
/**
* Reduce a 32-byte value modulo L (simplified for hash output)
* @param {Uint8Array} r - 32-byte output
* @param {Uint8Array} x - 32-byte input
*/
function reduceScalar32(r, x) {
// For a 32-byte input, just copy and rely on the fact that
// Keccak output is uniformly distributed
// Full reduction would require 64-byte input handling
for (let i = 0; i < 32; i++) r[i] = x[i];
// Simple modular reduction for values that might exceed L
const L = new Uint8Array([
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,
0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10
]);
// Compare r with L and subtract if r >= L
let borrow = 0;
for (let i = 31; i >= 0; i--) {
if (r[i] > L[i]) {
// r > L, need to subtract
borrow = 0;
for (let j = 0; j < 32; j++) {
const diff = r[j] - L[j] - borrow;
if (diff < 0) {
r[j] = diff + 256;
borrow = 1;
} else {
r[j] = diff;
borrow = 0;
}
}
break;
} else if (r[i] < L[i]) {
// r < L, no reduction needed
break;
}
}
}
/**
* Verify a Salvium message signature
*
* @param {string} message - The original message that was signed
* @param {string} address - Salvium address (to extract public keys)
* @param {string} signature - The signature string (SigV1... or SigV2...)
* @returns {Object} Result object with:
* - valid: boolean - whether signature is valid
* - version: number - signature version (1 or 2)
* - keyType: string - 'spend' or 'view' (which key was used to sign)
* - error: string|null - error message if invalid
*/
export function verifySignature(message, address, signature) {
// Parse signature header
const isV1 = signature.startsWith('SigV1');
const isV2 = signature.startsWith('SigV2');
if (!isV1 && !isV2) {
return { valid: false, version: 0, keyType: null, error: 'Invalid signature header (expected SigV1 or SigV2)' };
}
const version = isV1 ? 1 : 2;
const headerLen = 5; // "SigV1" or "SigV2"
// Decode signature from Base58
let sigBytes;
try {
sigBytes = decode(signature.substring(headerLen));
} catch (e) {
return { valid: false, version, keyType: null, error: 'Failed to decode signature Base58' };
}
// Signature should be 65 bytes (c: 32, r: 32, sign_mask: 1)
if (sigBytes.length !== 65) {
return { valid: false, version, keyType: null, error: `Invalid signature length: expected 65, got ${sigBytes.length}` };
}
const sigC = sigBytes.slice(0, 32);
const sigR = sigBytes.slice(32, 64);
// sign_mask (byte 64) is not used for standard message verification
// Parse address to get public keys
const addrInfo = parseAddress(address);
if (!addrInfo.valid) {
return { valid: false, version, keyType: null, error: `Invalid address: ${addrInfo.error}` };
}
const spendKey = addrInfo.spendPublicKey;
const viewKey = addrInfo.viewPublicKey;
// Try verification with spend key (mode 0)
let hash;
if (isV1) {
hash = getMessageHashV1(message);
} else {
hash = getMessageHashV2(message, spendKey, viewKey, 0);
}
if (checkSignature(hash, spendKey, sigC, sigR)) {
return { valid: true, version, keyType: 'spend', error: null };
}
// Try verification with view key (mode 1)
if (isV2) {
hash = getMessageHashV2(message, spendKey, viewKey, 1);
}
// For V1, the hash is just the message hash, same for both keys
if (checkSignature(hash, viewKey, sigC, sigR)) {
return { valid: true, version, keyType: 'view', error: null };
}
return { valid: false, version, keyType: null, error: 'Signature verification failed' };
}
/**
* Parse a signature string and extract its components
*
* @param {string} signature - The signature string
* @returns {Object} Parsed signature with version, c, r, signMask
*/
export function parseSignature(signature) {
const isV1 = signature.startsWith('SigV1');
const isV2 = signature.startsWith('SigV2');
if (!isV1 && !isV2) {
return { valid: false, error: 'Invalid signature header' };
}
const version = isV1 ? 1 : 2;
try {
const sigBytes = decode(signature.substring(5));
if (sigBytes.length !== 65) {
return { valid: false, error: 'Invalid signature length' };
}
return {
valid: true,
version,
c: sigBytes.slice(0, 32),
r: sigBytes.slice(32, 64),
signMask: sigBytes[64]
};
} catch (e) {
return { valid: false, error: 'Failed to decode signature' };
}
}
export default {
verifySignature,
parseSignature,
getMessageHashV1,
getMessageHashV2
};
+183
View File
@@ -0,0 +1,183 @@
/**
* salvium-js Test Suite
*
* Run with: node test/run.js
*/
import salvium, {
keccak256,
keccak256Hex,
encode,
decode,
parseAddress,
isValidAddress,
isMainnet,
isCarrot,
isLegacy,
describeAddress,
bytesToHex,
hexToBytes,
NETWORK,
ADDRESS_FORMAT,
ADDRESS_TYPE
} from '../src/index.js';
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++;
}
}
function assertEqual(actual, expected, msg = '') {
if (actual !== expected) {
throw new Error(`${msg} Expected ${expected}, got ${actual}`);
}
}
function assertArrayEqual(actual, expected, msg = '') {
if (actual.length !== expected.length) {
throw new Error(`${msg} Length mismatch: ${actual.length} vs ${expected.length}`);
}
for (let i = 0; i < actual.length; i++) {
if (actual[i] !== expected[i]) {
throw new Error(`${msg} Mismatch at index ${i}: ${actual[i]} vs ${expected[i]}`);
}
}
}
console.log('\n=== salvium-js Test Suite ===\n');
// Keccak-256 Tests
console.log('Keccak-256 Tests:');
test('Empty string hash', () => {
// Known Keccak-256 hash of empty string
const hash = keccak256Hex('');
assertEqual(hash, 'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470');
});
test('Simple string hash', () => {
// Known Keccak-256 hash of "test"
const hash = keccak256Hex('test');
assertEqual(hash, '9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658');
});
test('Bytes input', () => {
const input = new Uint8Array([0x74, 0x65, 0x73, 0x74]); // "test"
const hash = keccak256Hex(input);
assertEqual(hash, '9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658');
});
// Base58 Tests
console.log('\nBase58 Tests:');
test('Encode empty', () => {
const result = encode(new Uint8Array(0));
assertEqual(result, '');
});
test('Encode/decode roundtrip', () => {
const original = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const encoded = encode(original);
const decoded = decode(encoded);
assertArrayEqual(decoded, original);
});
test('Encode/decode 64 bytes (key size)', () => {
// Simulate public keys (64 bytes)
const original = new Uint8Array(64);
for (let i = 0; i < 64; i++) {
original[i] = i;
}
const encoded = encode(original);
const decoded = decode(encoded);
assertArrayEqual(decoded, original);
});
// Utility Tests
console.log('\nUtility Tests:');
test('bytesToHex', () => {
const bytes = new Uint8Array([0x00, 0x01, 0x0f, 0xff]);
assertEqual(bytesToHex(bytes), '00010fff');
});
test('hexToBytes', () => {
const hex = '00010fff';
const bytes = hexToBytes(hex);
assertArrayEqual(bytes, new Uint8Array([0x00, 0x01, 0x0f, 0xff]));
});
test('Hex roundtrip', () => {
const original = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
const hex = bytesToHex(original);
const recovered = hexToBytes(hex);
assertArrayEqual(recovered, original);
});
// Address Validation Tests (with invalid addresses)
console.log('\nAddress Validation Tests:');
test('Reject empty string', () => {
assertEqual(isValidAddress(''), false);
});
test('Reject too short', () => {
assertEqual(isValidAddress('SaLv123'), false);
});
test('Reject invalid characters', () => {
// 0, O, I, l are not in Base58 alphabet
assertEqual(isValidAddress('SaLv0OIl' + 'x'.repeat(90)), false);
});
test('Reject invalid checksum', () => {
// A valid-looking address with wrong checksum should fail
const fakeAddr = '1'.repeat(95);
assertEqual(isValidAddress(fakeAddr), false);
});
// Constants Tests
console.log('\nConstants Tests:');
test('Network constants exist', () => {
assertEqual(NETWORK.MAINNET, 'mainnet');
assertEqual(NETWORK.TESTNET, 'testnet');
assertEqual(NETWORK.STAGENET, 'stagenet');
});
test('Address type constants exist', () => {
assertEqual(ADDRESS_TYPE.STANDARD, 'standard');
assertEqual(ADDRESS_TYPE.INTEGRATED, 'integrated');
assertEqual(ADDRESS_TYPE.SUBADDRESS, 'subaddress');
});
test('Address format constants exist', () => {
assertEqual(ADDRESS_FORMAT.LEGACY, 'legacy');
assertEqual(ADDRESS_FORMAT.CARROT, 'carrot');
});
// Summary
console.log('\n=== Summary ===');
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
console.log('');
if (failed > 0) {
process.exit(1);
}
// Interactive test with real addresses
console.log('=== Real Address Tests ===');
console.log('To test with real Salvium addresses, run:');
console.log(' node -e "import(\'./src/index.js\').then(s => console.log(s.parseAddress(\'YOUR_ADDRESS\')))"');
console.log('');