| … | … | @@ -7,7 +7,12 @@ |
| 7 | 7 | "#node:*": "bare:*" |
| 8 | 8 | }, |
| 9 | 9 | "scripts": { |
| 10 | "postinstall": "bash scripts/setup-pear-compat.sh" | |
| 10 | "postinstall": "bash scripts/setup-pear-compat.sh", | |
| 11 | "test": "brittle test/**/*.test.js", | |
| 12 | "test:unit": "brittle test/unit/**/*.test.js", | |
| 13 | "test:integration": "brittle test/integration/**/*.test.js", | |
| 14 | "test:regression": "brittle test/regression/**/*.test.js", | |
| 15 | "test:watch": "brittle --watch test/**/*.test.js" | |
| 11 | 16 | }, |
| 12 | 17 | "pear": { |
| 13 | 18 | "name": "hyper-wallet-desktop", |
| … | … | @@ -35,6 +40,10 @@ |
| 35 | 40 | } |
| 36 | 41 | } |
| 37 | 42 | }, |
| 43 | "devDependencies": { | |
| 44 | "brittle": "^3.5.0", | |
| 45 | "memfs": "^4.6.0" | |
| 46 | }, | |
| 38 | 47 | "dependencies": { |
| 39 | 48 | "@noble/hashes": "^1.8.0", |
| 40 | 49 | "@tetherto/wdk": "^1.0.0-beta.5", |
| … | … | @@ -0,0 +1,91 @@ |
| 1 | { | |
| 2 | "bitcoin": { | |
| 3 | "mainnet": { | |
| 4 | "p2pkh": [ | |
| 5 | "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", | |
| 6 | "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2", | |
| 7 | "12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S" | |
| 8 | ], | |
| 9 | "p2sh": [ | |
| 10 | "3J98t1WpEZ73CNmYviecrnyiWrnqRhWNLy", | |
| 11 | "342ftSRCvFHfCeFFBuz4xwbeqnDw6BGUey", | |
| 12 | "3QJmV3qfvL9SuYo34YihAf3sRCW3qSinyC" | |
| 13 | ], | |
| 14 | "bech32": [ | |
| 15 | "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", | |
| 16 | "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3" | |
| 17 | ], | |
| 18 | "bech32m": [ | |
| 19 | "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0" | |
| 20 | ] | |
| 21 | }, | |
| 22 | "testnet": { | |
| 23 | "p2pkh": [ | |
| 24 | "mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn", | |
| 25 | "mqbJWn9kx5h4FzKPJ3WpN2WzLPBRPb8Y8k", | |
| 26 | "n3ZddxzLvAY9o7184TB4c6FJasAybsw4HZ" | |
| 27 | ], | |
| 28 | "p2sh": [ | |
| 29 | "2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc", | |
| 30 | "2N5muMepJizJE1gR7FbHsiVdC2W8i3UUUgF", | |
| 31 | "2MsFDzHRUAMpjHxKyoEHU3aMCMsVtXMsfu8" | |
| 32 | ], | |
| 33 | "bech32": [ | |
| 34 | "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx", | |
| 35 | "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7" | |
| 36 | ] | |
| 37 | }, | |
| 38 | "testnet4": { | |
| 39 | "p2pkh": [ | |
| 40 | "n3ZddxzLvAY9o7184TB4c6FJasAybsw4HZ" | |
| 41 | ], | |
| 42 | "bech32": [ | |
| 43 | "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx" | |
| 44 | ] | |
| 45 | }, | |
| 46 | "invalid": [ | |
| 47 | "1InvalidChecksum123", | |
| 48 | "bc1invalidbech32", | |
| 49 | "notanaddress", | |
| 50 | "3InvalidP2SH", | |
| 51 | "", | |
| 52 | "1" | |
| 53 | ] | |
| 54 | }, | |
| 55 | "ethereum": { | |
| 56 | "mainnet": { | |
| 57 | "valid": [ | |
| 58 | "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", | |
| 59 | "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", | |
| 60 | "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", | |
| 61 | "0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF" | |
| 62 | ] | |
| 63 | }, | |
| 64 | "testnet": { | |
| 65 | "sepolia": [ | |
| 66 | "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", | |
| 67 | "0x1234567890123456789012345678901234567890" | |
| 68 | ] | |
| 69 | }, | |
| 70 | "invalid": [ | |
| 71 | "0x123", | |
| 72 | "742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", | |
| 73 | "0xINVALIDHEXCHARS", | |
| 74 | "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0TOOLONG", | |
| 75 | "", | |
| 76 | "0x" | |
| 77 | ] | |
| 78 | }, | |
| 79 | "descriptions": { | |
| 80 | "bitcoin": { | |
| 81 | "p2pkh": "Pay-to-Public-Key-Hash (starts with 1 or m/n)", | |
| 82 | "p2sh": "Pay-to-Script-Hash (starts with 3 or 2)", | |
| 83 | "bech32": "SegWit v0 (starts with bc1 or tb1)", | |
| 84 | "bech32m": "SegWit v1+ (starts with bc1p or tb1p)" | |
| 85 | }, | |
| 86 | "ethereum": { | |
| 87 | "format": "40 hexadecimal characters with 0x prefix", | |
| 88 | "eip55": "EIP-55 checksum not validated (Pear limitation)" | |
| 89 | } | |
| 90 | } | |
| 91 | } |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | { | |
| 2 | "identity": { | |
| 3 | "valid24": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", | |
| 4 | "invalid24": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon invalid", | |
| 5 | "short": "abandon abandon abandon", | |
| 6 | "empty": "", | |
| 7 | "malformed": "not a valid mnemonic phrase at all just random words here" | |
| 8 | }, | |
| 9 | "wallet": { | |
| 10 | "valid12": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", | |
| 11 | "invalid12": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon invalid", | |
| 12 | "valid12_alt": "zone zone zone zone zone zone zone zone zone zone zone zone", | |
| 13 | "short": "abandon abandon", | |
| 14 | "empty": "", | |
| 15 | "malformed": "invalid mnemonic phrase" | |
| 16 | }, | |
| 17 | "descriptions": { | |
| 18 | "valid24": "Valid 24-word BIP-39 mnemonic for identity (keet-identity-key)", | |
| 19 | "invalid24": "Invalid 24-word mnemonic (last word has invalid checksum)", | |
| 20 | "valid12": "Valid 12-word BIP-39 mnemonic for wallet", | |
| 21 | "invalid12": "Invalid 12-word mnemonic (last word has invalid checksum)" | |
| 22 | } | |
| 23 | } |
| … | … | @@ -0,0 +1,139 @@ |
| 1 | // test/helpers.js - Shared Test Utilities | |
| 2 | ||
| 3 | const crypto = require('hypercore-crypto') | |
| 4 | const b4a = require('b4a') | |
| 5 | ||
| 6 | /** | |
| 7 | * Generate random hex token for testing | |
| 8 | * @param {number} bytes - Number of bytes (default: 32) | |
| 9 | * @returns {string} - Hex string | |
| 10 | */ | |
| 11 | function generateTestToken(bytes = 32) { | |
| 12 | return b4a.toString(crypto.randomBytes(bytes), 'hex') | |
| 13 | } | |
| 14 | ||
| 15 | /** | |
| 16 | * Generate test mnemonic (NOT cryptographically secure - testing only!) | |
| 17 | * @param {number} words - Number of words (12 or 24) | |
| 18 | * @returns {string} - Space-separated mnemonic | |
| 19 | */ | |
| 20 | function generateTestMnemonic(words = 12) { | |
| 21 | // Use deterministic test mnemonics from fixtures | |
| 22 | const testWords = [ | |
| 23 | 'abandon', 'ability', 'able', 'about', 'above', 'absent', | |
| 24 | 'absorb', 'abstract', 'absurd', 'abuse', 'access', 'accident', | |
| 25 | 'account', 'accuse', 'achieve', 'acid', 'acoustic', 'acquire', | |
| 26 | 'across', 'act', 'action', 'actor', 'actress', 'actual' | |
| 27 | ] | |
| 28 | ||
| 29 | return testWords.slice(0, words).join(' ') | |
| 30 | } | |
| 31 | ||
| 32 | /** | |
| 33 | * Wait for async condition to be true | |
| 34 | * @param {Function} condition - Async function returning boolean | |
| 35 | * @param {number} timeout - Max wait time in ms (default: 5000) | |
| 36 | * @param {number} interval - Check interval in ms (default: 100) | |
| 37 | * @returns {Promise<void>} | |
| 38 | */ | |
| 39 | async function waitFor(condition, timeout = 5000, interval = 100) { | |
| 40 | const startTime = Date.now() | |
| 41 | ||
| 42 | while (Date.now() - startTime < timeout) { | |
| 43 | if (await condition()) { | |
| 44 | return | |
| 45 | } | |
| 46 | await new Promise(resolve => setTimeout(resolve, interval)) | |
| 47 | } | |
| 48 | ||
| 49 | throw new Error(`Timeout waiting for condition after ${timeout}ms`) | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Assert function throws with expected error | |
| 54 | * @param {Function} fn - Function that should throw | |
| 55 | * @param {string|RegExp} expectedError - Expected error message or pattern | |
| 56 | * @returns {Promise<void>} | |
| 57 | */ | |
| 58 | async function assertThrows(fn, expectedError) { | |
| 59 | let threw = false | |
| 60 | let error = null | |
| 61 | ||
| 62 | try { | |
| 63 | await fn() | |
| 64 | } catch (err) { | |
| 65 | threw = true | |
| 66 | error = err | |
| 67 | } | |
| 68 | ||
| 69 | if (!threw) { | |
| 70 | throw new Error('Expected function to throw but it did not') | |
| 71 | } | |
| 72 | ||
| 73 | if (expectedError instanceof RegExp) { | |
| 74 | if (!expectedError.test(error.message)) { | |
| 75 | throw new Error(`Expected error matching ${expectedError} but got: ${error.message}`) | |
| 76 | } | |
| 77 | } else if (typeof expectedError === 'string') { | |
| 78 | if (!error.message.includes(expectedError)) { | |
| 79 | throw new Error(`Expected error containing "${expectedError}" but got: ${error.message}`) | |
| 80 | } | |
| 81 | } | |
| 82 | } | |
| 83 | ||
| 84 | /** | |
| 85 | * Create temporary test directory | |
| 86 | * @returns {string} - Path to temp directory | |
| 87 | */ | |
| 88 | function createTempDir() { | |
| 89 | const os = require('bare-os') | |
| 90 | const path = require('bare-path') | |
| 91 | const fs = require('bare-fs') | |
| 92 | ||
| 93 | const tmpDir = path.join(os.tmpdir(), `hyper-wallet-test-${Date.now()}`) | |
| 94 | fs.mkdirSync(tmpDir, { recursive: true, mode: 0o700 }) | |
| 95 | return tmpDir | |
| 96 | } | |
| 97 | ||
| 98 | /** | |
| 99 | * Clean up temporary test directory | |
| 100 | * @param {string} dir - Directory to remove | |
| 101 | */ | |
| 102 | function cleanupTempDir(dir) { | |
| 103 | const fs = require('bare-fs') | |
| 104 | const path = require('bare-path') | |
| 105 | ||
| 106 | if (fs.existsSync(dir)) { | |
| 107 | const files = fs.readdirSync(dir) | |
| 108 | for (const file of files) { | |
| 109 | const filePath = path.join(dir, file) | |
| 110 | const stat = fs.statSync(filePath) | |
| 111 | ||
| 112 | if (stat.isDirectory()) { | |
| 113 | cleanupTempDir(filePath) | |
| 114 | } else { | |
| 115 | fs.unlinkSync(filePath) | |
| 116 | } | |
| 117 | } | |
| 118 | fs.rmdirSync(dir) | |
| 119 | } | |
| 120 | } | |
| 121 | ||
| 122 | /** | |
| 123 | * Sleep for specified milliseconds | |
| 124 | * @param {number} ms - Milliseconds to sleep | |
| 125 | * @returns {Promise<void>} | |
| 126 | */ | |
| 127 | function sleep(ms) { | |
| 128 | return new Promise(resolve => setTimeout(resolve, ms)) | |
| 129 | } | |
| 130 | ||
| 131 | module.exports = { | |
| 132 | generateTestToken, | |
| 133 | generateTestMnemonic, | |
| 134 | waitFor, | |
| 135 | assertThrows, | |
| 136 | createTempDir, | |
| 137 | cleanupTempDir, | |
| 138 | sleep | |
| 139 | } |
| … | … | @@ -0,0 +1,127 @@ |
| 1 | // test/mocks/fs-mock.cjs - In-Memory Filesystem Mock for Testing | |
| 2 | ||
| 3 | const { Volume } = require('memfs') | |
| 4 | ||
| 5 | /** | |
| 6 | * Create mock filesystem with initial structure | |
| 7 | * @param {Object} initialFiles - Initial file structure (optional) | |
| 8 | * @returns {Object} - Mock fs object with standard fs methods | |
| 9 | */ | |
| 10 | function createMockFS(initialFiles = {}) { | |
| 11 | // Create default hyper-wallet directory structure | |
| 12 | const defaultStructure = { | |
| 13 | '/home/user/.hyper-wallet/': null, | |
| 14 | '/tmp/': null, | |
| 15 | ...initialFiles | |
| 16 | } | |
| 17 | ||
| 18 | const vol = Volume.fromJSON(defaultStructure) | |
| 19 | ||
| 20 | return { | |
| 21 | // File read/write operations | |
| 22 | readFileSync: vol.readFileSync.bind(vol), | |
| 23 | writeFileSync: vol.writeFileSync.bind(vol), | |
| 24 | appendFileSync: vol.appendFileSync.bind(vol), | |
| 25 | readFile: vol.readFile.bind(vol), | |
| 26 | writeFile: vol.writeFile.bind(vol), | |
| 27 | appendFile: vol.appendFile.bind(vol), | |
| 28 | ||
| 29 | // File existence and stats | |
| 30 | existsSync: vol.existsSync.bind(vol), | |
| 31 | statSync: vol.statSync.bind(vol), | |
| 32 | lstatSync: vol.lstatSync.bind(vol), | |
| 33 | stat: vol.stat.bind(vol), | |
| 34 | lstat: vol.lstat.bind(vol), | |
| 35 | ||
| 36 | // Directory operations | |
| 37 | mkdirSync: vol.mkdirSync.bind(vol), | |
| 38 | rmdirSync: vol.rmdirSync.bind(vol), | |
| 39 | readdirSync: vol.readdirSync.bind(vol), | |
| 40 | mkdir: vol.mkdir.bind(vol), | |
| 41 | rmdir: vol.rmdir.bind(vol), | |
| 42 | readdir: vol.readdir.bind(vol), | |
| 43 | ||
| 44 | // File manipulation | |
| 45 | unlinkSync: vol.unlinkSync.bind(vol), | |
| 46 | renameSync: vol.renameSync.bind(vol), | |
| 47 | copyFileSync: vol.copyFileSync.bind(vol), | |
| 48 | unlink: vol.unlink.bind(vol), | |
| 49 | rename: vol.rename.bind(vol), | |
| 50 | copyFile: vol.copyFile.bind(vol), | |
| 51 | ||
| 52 | // Permissions (mocked - memfs doesn't enforce but tracks) | |
| 53 | chmodSync: vol.chmodSync.bind(vol), | |
| 54 | chownSync: vol.chownSync.bind(vol), | |
| 55 | chmod: vol.chmod.bind(vol), | |
| 56 | chown: vol.chown.bind(vol), | |
| 57 | ||
| 58 | // Access the underlying Volume for advanced operations | |
| 59 | __vol: vol, | |
| 60 | ||
| 61 | // Helper: Reset filesystem to initial state | |
| 62 | reset() { | |
| 63 | vol.reset() | |
| 64 | const newVol = Volume.fromJSON(defaultStructure) | |
| 65 | Object.assign(vol, newVol) | |
| 66 | }, | |
| 67 | ||
| 68 | // Helper: Dump current filesystem state (for debugging) | |
| 69 | dump() { | |
| 70 | return vol.toJSON() | |
| 71 | } | |
| 72 | } | |
| 73 | } | |
| 74 | ||
| 75 | /** | |
| 76 | * Create mock fs with wallet files already present | |
| 77 | * @param {Object} options - Configuration | |
| 78 | * @param {string} options.identityMnemonic - Identity mnemonic content | |
| 79 | * @param {string} options.walletMnemonic - Wallet mnemonic content | |
| 80 | * @param {string} options.token - API token content | |
| 81 | * @returns {Object} - Mock fs object | |
| 82 | */ | |
| 83 | function createMockFSWithWallet(options = {}) { | |
| 84 | const { | |
| 85 | identityMnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art', | |
| 86 | walletMnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', | |
| 87 | token = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' | |
| 88 | } = options | |
| 89 | ||
| 90 | const initialFiles = { | |
| 91 | '/home/user/.hyper-wallet/.identity-mnemonic': identityMnemonic, | |
| 92 | '/home/user/.hyper-wallet/.wallet-mnemonic': walletMnemonic, | |
| 93 | '/home/user/.hyper-wallet/.token': token, | |
| 94 | '/home/user/.hyper-wallet/devices.json': JSON.stringify([]) | |
| 95 | } | |
| 96 | ||
| 97 | return createMockFS(initialFiles) | |
| 98 | } | |
| 99 | ||
| 100 | /** | |
| 101 | * Mock fs.promises API | |
| 102 | * @param {Object} mockFS - Mock fs object from createMockFS | |
| 103 | * @returns {Object} - Promises-based fs API | |
| 104 | */ | |
| 105 | function createMockFSPromises(mockFS) { | |
| 106 | return { | |
| 107 | readFile: (...args) => Promise.resolve(mockFS.readFileSync(...args)), | |
| 108 | writeFile: (...args) => { mockFS.writeFileSync(...args); return Promise.resolve() }, | |
| 109 | appendFile: (...args) => { mockFS.appendFileSync(...args); return Promise.resolve() }, | |
| 110 | unlink: (...args) => { mockFS.unlinkSync(...args); return Promise.resolve() }, | |
| 111 | mkdir: (...args) => { mockFS.mkdirSync(...args); return Promise.resolve() }, | |
| 112 | rmdir: (...args) => { mockFS.rmdirSync(...args); return Promise.resolve() }, | |
| 113 | readdir: (...args) => Promise.resolve(mockFS.readdirSync(...args)), | |
| 114 | stat: (...args) => Promise.resolve(mockFS.statSync(...args)), | |
| 115 | lstat: (...args) => Promise.resolve(mockFS.lstatSync(...args)), | |
| 116 | rename: (...args) => { mockFS.renameSync(...args); return Promise.resolve() }, | |
| 117 | copyFile: (...args) => { mockFS.copyFileSync(...args); return Promise.resolve() }, | |
| 118 | chmod: (...args) => { mockFS.chmodSync(...args); return Promise.resolve() }, | |
| 119 | chown: (...args) => { mockFS.chownSync(...args); return Promise.resolve() } | |
| 120 | } | |
| 121 | } | |
| 122 | ||
| 123 | module.exports = { | |
| 124 | createMockFS, | |
| 125 | createMockFSWithWallet, | |
| 126 | createMockFSPromises | |
| 127 | } |
| … | … | @@ -0,0 +1,231 @@ |
| 1 | // test/mocks/wdk-mock.cjs - WDK Mock Factory for Testing | |
| 2 | ||
| 3 | const crypto = require('hypercore-crypto') | |
| 4 | const b4a = require('b4a') | |
| 5 | ||
| 6 | /** | |
| 7 | * Mock WDK class that simulates @tetherto/wdk behavior | |
| 8 | */ | |
| 9 | class MockWDK { | |
| 10 | constructor(mnemonic, options = {}) { | |
| 11 | this.mnemonic = mnemonic | |
| 12 | this.wallets = {} | |
| 13 | this.options = options | |
| 14 | } | |
| 15 | ||
| 16 | /** | |
| 17 | * Register wallet manager (chainable) | |
| 18 | * @param {string} chain - Chain name (bitcoin, ethereum) | |
| 19 | * @param {Function} Manager - Wallet manager class | |
| 20 | * @param {Object} config - Configuration | |
| 21 | * @returns {MockWDK} - This instance for chaining | |
| 22 | */ | |
| 23 | registerWallet(chain, Manager, config) { | |
| 24 | this.wallets[chain] = { | |
| 25 | Manager, | |
| 26 | config, | |
| 27 | accounts: new Map() | |
| 28 | } | |
| 29 | return this | |
| 30 | } | |
| 31 | ||
| 32 | /** | |
| 33 | * Get account for chain at index | |
| 34 | * @param {string} chain - Chain name | |
| 35 | * @param {number} accountIndex - BIP44 account index | |
| 36 | * @returns {Promise<MockAccount>} | |
| 37 | */ | |
| 38 | async getAccount(chain, accountIndex = 0) { | |
| 39 | const wallet = this.wallets[chain] | |
| 40 | if (!wallet) { | |
| 41 | throw new Error(`Wallet not registered for chain: ${chain}`) | |
| 42 | } | |
| 43 | ||
| 44 | const key = `${chain}:${accountIndex}` | |
| 45 | if (!wallet.accounts.has(key)) { | |
| 46 | wallet.accounts.set(key, new MockAccount(chain, accountIndex, this.mnemonic)) | |
| 47 | } | |
| 48 | ||
| 49 | return wallet.accounts.get(key) | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Generate random seed phrase (12 words) | |
| 54 | * @returns {string} - BIP-39 mnemonic | |
| 55 | */ | |
| 56 | static getRandomSeedPhrase() { | |
| 57 | // Generate deterministic test mnemonic from random bytes | |
| 58 | const entropy = crypto.randomBytes(16) // 128 bits for 12 words | |
| 59 | const checksum = entropy[0] % 16 // Simple checksum for testing | |
| 60 | ||
| 61 | const words = [ | |
| 62 | 'abandon', 'ability', 'able', 'about', 'above', 'absent', | |
| 63 | 'absorb', 'abstract', 'absurd', 'abuse', 'access', 'about' | |
| 64 | ] | |
| 65 | ||
| 66 | return words.join(' ') | |
| 67 | } | |
| 68 | } | |
| 69 | ||
| 70 | /** | |
| 71 | * Mock Account class that simulates WDK account operations | |
| 72 | */ | |
| 73 | class MockAccount { | |
| 74 | constructor(chain, accountIndex, mnemonic) { | |
| 75 | this.chain = chain | |
| 76 | this.accountIndex = accountIndex | |
| 77 | this.mnemonic = mnemonic | |
| 78 | ||
| 79 | // Mock balances | |
| 80 | this.balance = BigInt(chain === 'bitcoin' ? 100000000 : 1000000000000000000) // 1 BTC or 1 ETH | |
| 81 | this.tokenBalances = new Map() | |
| 82 | ||
| 83 | // Default token balances | |
| 84 | this.tokenBalances.set('0x7169D38820dfd117C3FA1f22a697dBA58d90BA06', BigInt(500000000)) // 500 USDT (6 decimals) | |
| 85 | this.tokenBalances.set('0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', BigInt(250000000)) // 250 USDC (6 decimals) | |
| 86 | } | |
| 87 | ||
| 88 | /** | |
| 89 | * Get account balance | |
| 90 | * @returns {Promise<BigInt>} | |
| 91 | */ | |
| 92 | async getBalance() { | |
| 93 | return this.balance | |
| 94 | } | |
| 95 | ||
| 96 | /** | |
| 97 | * Get address for account | |
| 98 | * @param {number} addressIndex - BIP44 address index | |
| 99 | * @returns {Promise<string>} | |
| 100 | */ | |
| 101 | async getAddress(addressIndex = 0) { | |
| 102 | if (this.chain === 'bitcoin') { | |
| 103 | // Mock Bitcoin testnet address | |
| 104 | return `tb1q${b4a.toString(crypto.randomBytes(32), 'hex').slice(0, 38)}` | |
| 105 | } else if (this.chain === 'ethereum') { | |
| 106 | // Mock Ethereum address | |
| 107 | return `0x${b4a.toString(crypto.randomBytes(20), 'hex')}` | |
| 108 | } | |
| 109 | throw new Error(`Unsupported chain: ${this.chain}`) | |
| 110 | } | |
| 111 | ||
| 112 | /** | |
| 113 | * Get token balance (ERC-20) | |
| 114 | * @param {string} contractAddress - Token contract address | |
| 115 | * @returns {Promise<BigInt>} | |
| 116 | */ | |
| 117 | async getTokenBalance(contractAddress) { | |
| 118 | return this.tokenBalances.get(contractAddress) || BigInt(0) | |
| 119 | } | |
| 120 | ||
| 121 | /** | |
| 122 | * Send transaction | |
| 123 | * @param {Object} params - Transaction parameters | |
| 124 | * @returns {Promise<Object>} - Transaction result with hash | |
| 125 | */ | |
| 126 | async sendTransaction(params) { | |
| 127 | const { to, value } = params | |
| 128 | ||
| 129 | // Validate balance | |
| 130 | if (value > this.balance) { | |
| 131 | throw new Error('Insufficient balance') | |
| 132 | } | |
| 133 | ||
| 134 | // Deduct from balance | |
| 135 | this.balance -= value | |
| 136 | ||
| 137 | // Return mock transaction | |
| 138 | return { | |
| 139 | hash: `0x${b4a.toString(crypto.randomBytes(32), 'hex')}`, | |
| 140 | from: await this.getAddress(), | |
| 141 | to, | |
| 142 | value: value.toString(), | |
| 143 | blockNumber: Math.floor(Math.random() * 1000000), | |
| 144 | timestamp: Date.now() | |
| 145 | } | |
| 146 | } | |
| 147 | ||
| 148 | /** | |
| 149 | * Quote transaction fee | |
| 150 | * @param {Object} params - Transaction parameters | |
| 151 | * @returns {Promise<Object>} - Fee estimate | |
| 152 | */ | |
| 153 | async quoteSendTransaction(params) { | |
| 154 | const { value } = params | |
| 155 | ||
| 156 | // Mock fee calculation (0.1% of amount or minimum) | |
| 157 | const calculatedFee = value / BigInt(1000) | |
| 158 | const minFee = this.chain === 'bitcoin' ? BigInt(1000) : BigInt(21000) // 1000 sats or 21000 gwei | |
| 159 | ||
| 160 | const fee = calculatedFee > minFee ? calculatedFee : minFee | |
| 161 | ||
| 162 | return { | |
| 163 | fee, | |
| 164 | feeRate: this.chain === 'bitcoin' ? '10 sat/vB' : '50 gwei', | |
| 165 | estimatedConfirmation: '10 minutes' | |
| 166 | } | |
| 167 | } | |
| 168 | ||
| 169 | /** | |
| 170 | * Transfer ERC-20 token | |
| 171 | * @param {Object} params - Transfer parameters | |
| 172 | * @returns {Promise<Object>} - Transfer result | |
| 173 | */ | |
| 174 | async transfer(params) { | |
| 175 | const { token, recipient, amount } = params | |
| 176 | ||
| 177 | // Validate token balance | |
| 178 | const tokenBalance = await this.getTokenBalance(token) | |
| 179 | if (amount > tokenBalance) { | |
| 180 | throw new Error('Insufficient token balance') | |
| 181 | } | |
| 182 | ||
| 183 | // Deduct from token balance | |
| 184 | this.tokenBalances.set(token, tokenBalance - amount) | |
| 185 | ||
| 186 | return { | |
| 187 | hash: `0x${b4a.toString(crypto.randomBytes(32), 'hex')}`, | |
| 188 | from: await this.getAddress(), | |
| 189 | to: recipient, | |
| 190 | token, | |
| 191 | amount: amount.toString(), | |
| 192 | blockNumber: Math.floor(Math.random() * 1000000), | |
| 193 | timestamp: Date.now() | |
| 194 | } | |
| 195 | } | |
| 196 | ||
| 197 | /** | |
| 198 | * Quote token transfer fee | |
| 199 | * @param {Object} params - Transfer parameters | |
| 200 | * @returns {Promise<Object>} - Fee estimate | |
| 201 | */ | |
| 202 | async quoteTransfer(params) { | |
| 203 | // ERC-20 transfer costs more gas than ETH transfer | |
| 204 | const fee = BigInt(65000) // ~65k gas units | |
| 205 | ||
| 206 | return { | |
| 207 | fee, | |
| 208 | feeRate: '50 gwei', | |
| 209 | estimatedConfirmation: '10 minutes' | |
| 210 | } | |
| 211 | } | |
| 212 | } | |
| 213 | ||
| 214 | /** | |
| 215 | * Mock WalletManager classes (Bitcoin, EVM) | |
| 216 | */ | |
| 217 | class MockWalletManagerBtc { | |
| 218 | static default = MockWalletManagerBtc | |
| 219 | } | |
| 220 | ||
| 221 | class MockWalletManagerEvm { | |
| 222 | static default = MockWalletManagerEvm | |
| 223 | } | |
| 224 | ||
| 225 | module.exports = { | |
| 226 | default: MockWDK, | |
| 227 | MockWDK, | |
| 228 | MockAccount, | |
| 229 | WalletManagerBtc: MockWalletManagerBtc, | |
| 230 | WalletManagerEvm: MockWalletManagerEvm | |
| 231 | } |
| … | … | @@ -0,0 +1,27 @@ |
| 1 | // test/setup.js - Test Environment Setup | |
| 2 | // CRITICAL: Load bare-node-runtime FIRST for Node.js compatibility in Bare | |
| 3 | require('bare-node-runtime/global') | |
| 4 | ||
| 5 | // Global test configuration | |
| 6 | global.TEST_TIMEOUT = 30000 // 30 seconds default timeout | |
| 7 | global.TEST_RETRY_ATTEMPTS = 3 // Retry flaky tests | |
| 8 | ||
| 9 | // Suppress console noise in tests (unless DEBUG=1) | |
| 10 | if (!process.env.DEBUG) { | |
| 11 | const originalConsole = { ...console } | |
| 12 | global.console = { | |
| 13 | ...originalConsole, | |
| 14 | log: () => {}, | |
| 15 | info: () => {}, | |
| 16 | debug: () => {}, | |
| 17 | warn: () => {}, | |
| 18 | // Keep error for test failures | |
| 19 | error: originalConsole.error | |
| 20 | } | |
| 21 | } | |
| 22 | ||
| 23 | // Export test environment info | |
| 24 | module.exports = { | |
| 25 | timeout: global.TEST_TIMEOUT, | |
| 26 | retries: global.TEST_RETRY_ATTEMPTS | |
| 27 | } |