OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet commits / 4c521d5

Move all lib files to hyper-wallet-lib submodule
- Remove duplicate .cjs files from lib/ - Update imports to use lib/hyper-wallet-lib/ path - All shared code now in single submodule Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
4c521d50e28760943d45d6de9b7108994a5403dc
by plankatron <fleeky@gmail.com> · 2026-01-30T22:59:55.000Z

Changed files (22) +465 -5183

hyper-wallet-advanced-cli.cjs +10 -10
……@@ -36,7 +36,7 @@ const {
3636 deleteTransaction,
3737 // Transaction status updates (Phase 6.7)
3838 updateTransactionStatus
39} = require('./lib/wallet-storage.cjs')
39} = require('./lib/hyper-wallet-lib/wallet-storage.cjs')
4040const {
4141 generateWalletMnemonic,
4242 saveWalletMnemonic,
……@@ -44,13 +44,13 @@ const {
4444 initializeWDK,
4545 getFirstAddress,
4646 walletMnemonicExists
47} = require('./lib/wdk-integration.cjs')
47} = require('./lib/hyper-wallet-lib/wdk-integration.cjs')
4848const {
4949 generateNewAddress,
5050 listAddresses: listAddressesFromManager,
5151 getCurrentAddress: getCurrentAddressFromManager
52} = require('./lib/address-manager.cjs')
53const { loadConfig } = require('./lib/config-loader.cjs')
52} = require('./lib/hyper-wallet-lib/address-manager.cjs')
53const { loadConfig } = require('./lib/hyper-wallet-lib/config-loader.cjs')
5454// UTXO manager functions (not needed for WDK-based balance queries)
5555// const {
5656// storeUTXO: storeUTXOHelper,
……@@ -58,12 +58,12 @@ const { loadConfig } = require('./lib/config-loader.cjs')
5858// calculateBalance,
5959// getSpendableUTXOs,
6060// formatBalance
61// } = require('./lib/utxo-manager.cjs')
61// } = require('./lib/hyper-wallet-lib/utxo-manager.cjs')
6262// const {
6363// getUTXOsForAddresses,
6464// getFeeRates,
6565// checkConnection
66// } = require('./lib/blockchain-api.cjs')
66// } = require('./lib/hyper-wallet-lib/blockchain-api.cjs')
6767
6868// Load configuration from file
6969const config = loadConfig()
……@@ -894,8 +894,8 @@ async function cmdSyncHistory(args) {
894894 await initializeStorage(identity)
895895
896896 // Load configuration
897 const { getConfig } = require('./lib/config.cjs')
898 const { fetchTransactions } = require('./lib/blockchain-providers.cjs')
897 const { getConfig } = require('./lib/hyper-wallet-lib/config.cjs')
898 const { fetchTransactions } = require('./lib/hyper-wallet-lib/blockchain-providers.cjs')
899899 const config = getConfig()
900900
901901 try {
……@@ -1399,8 +1399,8 @@ async function cmdDaemon(args) {
13991399 * Manage blockchain providers and configuration
14001400 */
14011401async function cmdSettings(args) {
1402 const { getConfig, updateConfig, saveConfig, DEFAULT_CONFIG } = require('./lib/config.cjs')
1403 const { getAvailableProviders, getProviderInfo } = require('./lib/blockchain-providers.cjs')
1402 const { getConfig, updateConfig, saveConfig, DEFAULT_CONFIG } = require('./lib/hyper-wallet-lib/config.cjs')
1403 const { getAvailableProviders, getProviderInfo } = require('./lib/hyper-wallet-lib/blockchain-providers.cjs')
14041404
14051405 const subcommand = args[1]
14061406
lib/address-manager.cjs +0 -91
……@@ -1,91 +0,0 @@
1// lib/address-manager.cjs - Address Generation & Management (Phase 6.2)
2// CRITICAL: Load bare-node-runtime FIRST before any other requires
3require('bare-node-runtime/global')
4
5const {
6 initializeWallets,
7 storeAddress,
8 getAddresses,
9 getNextAddressIndex,
10 getCurrentAddress: getLatestAddress
11} = require('./wallet-storage.cjs')
12
13/**
14 * Generate new receiving address and store in Autobase
15 * @param {string} chain - 'bitcoin' or 'ethereum'
16 * @param {Object} config - { testnet: boolean }
17 * @returns {Object} - { address, path, index, chain }
18 */
19async function generateNewAddress(chain, config = { testnet: true }) {
20 // Normalize chain name
21 const chainNormalized = chain.toLowerCase()
22 if (chainNormalized !== 'bitcoin' && chainNormalized !== 'ethereum') {
23 throw new Error(`Unsupported chain: ${chain}. Use 'bitcoin' or 'ethereum'`)
24 }
25
26 // Initialize WDK
27 const { wdk } = await initializeWallets(config)
28
29 // Get next address index
30 const index = await getNextAddressIndex(chainNormalized)
31
32 // Determine derivation path fragment for WDK getAccountByPath
33 // Bitcoin (BIP-84): m/84'/coinType'/0'/0/{index}
34 // Ethereum (BIP-44): m/44'/60'/0'/0/{index}
35 // WDK expects path fragment like "0'/0/{index}"
36 const pathFragment = `0'/0/${index}`
37
38 // Get account using custom derivation path
39 const account = await wdk.getAccountByPath(chainNormalized, pathFragment)
40 const address = await account.getAddress()
41
42 // Build full path for display
43 const coinType = chainNormalized === 'bitcoin'
44 ? (config.testnet ? "1'" : "0'")
45 : "60'"
46
47 const path = chainNormalized === 'bitcoin'
48 ? `m/84'/${coinType}/${pathFragment}`
49 : `m/44'/${coinType}/${pathFragment}`
50
51 // Store address in Autobase (syncs to all devices)
52 await storeAddress({
53 chain: chainNormalized,
54 network: config.testnet ? 'testnet' : 'mainnet',
55 address,
56 path,
57 index
58 })
59
60 return { address, path, index, chain: chainNormalized }
61}
62
63/**
64 * List all addresses for a chain
65 * @param {string} chain - 'bitcoin', 'ethereum', or 'all'
66 * @returns {Array} - Array of address objects sorted by index
67 */
68async function listAddresses(chain = 'all') {
69 const chainNormalized = chain.toLowerCase()
70 return await getAddresses(chainNormalized)
71}
72
73/**
74 * Get current (latest) address for a chain
75 * @param {string} chain - 'bitcoin' or 'ethereum'
76 * @returns {Object|null} - Latest address or null if none exist
77 */
78async function getCurrentAddress(chain) {
79 const chainNormalized = chain.toLowerCase()
80 if (chainNormalized !== 'bitcoin' && chainNormalized !== 'ethereum') {
81 throw new Error(`Unsupported chain: ${chain}. Use 'bitcoin' or 'ethereum'`)
82 }
83
84 return await getLatestAddress(chainNormalized)
85}
86
87module.exports = {
88 generateNewAddress,
89 listAddresses,
90 getCurrentAddress
91}
lib/address-validator.cjs +0 -269
……@@ -1,269 +0,0 @@
1/**
2 * FILE: lib/address-validator.cjs
3 * STATUS: ACTIVE
4 * RUNTIME: Bare
5 * MODULE: CommonJS
6 *
7 * PURPOSE:
8 * Address validation with basic format checking
9 *
10 * USED BY:
11 * - wallet-server.cjs (transaction and contact validation)
12 *
13 * KEY EXPORTS:
14 * - validateAddress(address, chain, network) - Generic validator
15 * - validateBitcoinAddress(address, network) - Bitcoin with Base58Check/Bech32
16 * - validateEthereumAddress(address) - Ethereum format check only
17 *
18 * VALIDATION:
19 * - Bitcoin: Full checksum validation (Base58Check and Bech32/Bech32m)
20 * - Ethereum: Format check only (EIP-55 checksum disabled - Pear module resolution issues)
21 * - Returns: { valid: boolean, reason?: string, warning?: string }
22 *
23 * NOTE: EIP-55 checksum validation disabled due to Pear module resolution bug with @noble/hashes
24 */
25
26const crypto = require('bare-crypto')
27
28/**
29 * Bitcoin Base58 alphabet (no 0, O, I, l to avoid confusion)
30 */
31const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
32
33/**
34 * Decode Base58 string to buffer
35 * @param {string} str - Base58 encoded string
36 * @returns {Buffer} Decoded bytes
37 */
38function base58Decode(str) {
39 const bytes = []
40 for (let i = 0; i < str.length; i++) {
41 const char = str[i]
42 let value = BASE58_ALPHABET.indexOf(char)
43 if (value === -1) {
44 throw new Error('Invalid Base58 character')
45 }
46
47 for (let j = 0; j < bytes.length; j++) {
48 value += bytes[j] * 58
49 bytes[j] = value & 0xff
50 value >>= 8
51 }
52
53 while (value > 0) {
54 bytes.push(value & 0xff)
55 value >>= 8
56 }
57 }
58
59 // Add leading zeros
60 for (let i = 0; i < str.length && str[i] === '1'; i++) {
61 bytes.push(0)
62 }
63
64 return Buffer.from(bytes.reverse())
65}
66
67/**
68 * Verify Base58Check checksum
69 * @param {Buffer} payload - Decoded Base58 bytes
70 * @returns {boolean} True if checksum valid
71 */
72function verifyBase58Check(payload) {
73 if (payload.length < 5) return false
74
75 const data = payload.slice(0, -4)
76 const checksum = payload.slice(-4)
77
78 // Double SHA-256 hash
79 const hash1 = crypto.createHash('sha256').update(data).digest()
80 const hash2 = crypto.createHash('sha256').update(hash1).digest()
81
82 // Compare first 4 bytes
83 return checksum.equals(hash2.slice(0, 4))
84}
85
86/**
87 * Bech32 character set
88 */
89const BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
90
91/**
92 * Bech32 polymod for checksum
93 */
94function bech32Polymod(values) {
95 const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
96 let chk = 1
97
98 for (const value of values) {
99 const top = chk >> 25
100 chk = ((chk & 0x1ffffff) << 5) ^ value
101
102 for (let i = 0; i < 5; i++) {
103 if ((top >> i) & 1) {
104 chk ^= GEN[i]
105 }
106 }
107 }
108
109 return chk
110}
111
112/**
113 * Expand HRP (Human Readable Part) for Bech32
114 */
115function bech32HrpExpand(hrp) {
116 const result = []
117 for (let i = 0; i < hrp.length; i++) {
118 result.push(hrp.charCodeAt(i) >> 5)
119 }
120 result.push(0)
121 for (let i = 0; i < hrp.length; i++) {
122 result.push(hrp.charCodeAt(i) & 31)
123 }
124 return result
125}
126
127/**
128 * Verify Bech32 checksum (SegWit v0)
129 */
130function verifyBech32(hrp, data) {
131 return bech32Polymod(bech32HrpExpand(hrp).concat(data)) === 1
132}
133
134/**
135 * Verify Bech32m checksum (SegWit v1+)
136 */
137function verifyBech32m(hrp, data) {
138 return bech32Polymod(bech32HrpExpand(hrp).concat(data)) === 0x2bc830a3
139}
140
141/**
142 * Decode Bech32 address
143 */
144function bech32Decode(address) {
145 address = address.toLowerCase()
146
147 const pos = address.lastIndexOf('1')
148 if (pos < 1 || pos + 7 > address.length || address.length > 90) {
149 throw new Error('Invalid Bech32 address')
150 }
151
152 const hrp = address.slice(0, pos)
153 const data = []
154
155 for (let i = pos + 1; i < address.length; i++) {
156 const d = BECH32_CHARSET.indexOf(address[i])
157 if (d === -1) {
158 throw new Error('Invalid Bech32 character')
159 }
160 data.push(d)
161 }
162
163 return { hrp, data }
164}
165
166/**
167 * Validate Bitcoin address with checksum
168 * @param {string} address - Bitcoin address
169 * @param {string} network - 'testnet' or 'mainnet'
170 * @returns {Object} { valid: boolean, reason?: string }
171 */
172function validateBitcoinAddress(address, network = 'testnet') {
173 // P2PKH (1... or m...) and P2SH (3... or 2...)
174 if (address.match(/^[123mn][a-zA-HJ-NP-Z0-9]{25,34}$/)) {
175 try {
176 const decoded = base58Decode(address)
177
178 if (!verifyBase58Check(decoded)) {
179 return { valid: false, reason: 'Invalid Base58Check checksum' }
180 }
181
182 const version = decoded[0]
183
184 // Check version byte matches network
185 const validVersions = network === 'testnet' ? [0x6f, 0xc4] : [0x00, 0x05]
186 if (!validVersions.includes(version)) {
187 return { valid: false, reason: `Invalid network for address format (expected ${network})` }
188 }
189
190 return { valid: true }
191 } catch (err) {
192 return { valid: false, reason: `Base58 decode error: ${err.message}` }
193 }
194 }
195
196 // Bech32 (bc1... or tb1...)
197 if (address.match(/^(bc1|tb1)[a-z0-9]{39,87}$/)) {
198 try {
199 const prefix = network === 'testnet' ? 'tb' : 'bc'
200 if (!address.startsWith(prefix)) {
201 return { valid: false, reason: `Invalid network prefix (expected ${prefix}1...)` }
202 }
203
204 const { hrp, data } = bech32Decode(address)
205
206 if (hrp !== prefix) {
207 return { valid: false, reason: 'Network mismatch in HRP' }
208 }
209
210 // Try Bech32 (v0) first, then Bech32m (v1+)
211 const validBech32 = verifyBech32(hrp, data)
212 const validBech32m = verifyBech32m(hrp, data)
213
214 if (!validBech32 && !validBech32m) {
215 return { valid: false, reason: 'Invalid Bech32/Bech32m checksum' }
216 }
217
218 return { valid: true }
219 } catch (err) {
220 return { valid: false, reason: `Bech32 decode error: ${err.message}` }
221 }
222 }
223
224 return { valid: false, reason: 'Unrecognized Bitcoin address format' }
225}
226
227/**
228 * Validate Ethereum address (format only, NO EIP-55 checksum)
229 * @param {string} address - Ethereum address (0x...)
230 * @returns {Object} { valid: boolean, reason?: string, warning?: string }
231 */
232function validateEthereumAddress(address) {
233 // Must start with 0x and be 40 hex chars
234 if (!address.match(/^0x[a-fA-F0-9]{40}$/)) {
235 return { valid: false, reason: 'Invalid Ethereum address format (expected 0x + 40 hex chars)' }
236 }
237
238 // EIP-55 checksum validation DISABLED
239 // bare-crypto doesn't support keccak256, and installing @noble/hashes breaks Pear module resolution
240 // Accepting all properly formatted addresses regardless of checksum
241 return { valid: true, warning: 'EIP-55 checksum validation disabled (Pear module resolution limitation)' }
242}
243
244/**
245 * Generic address validator (dispatches to chain-specific validator)
246 * @param {string} address - Blockchain address
247 * @param {string} chain - 'bitcoin' or 'ethereum'
248 * @param {string} network - 'testnet' or 'mainnet'
249 * @returns {Object} { valid: boolean, reason?: string, warning?: string }
250 */
251function validateAddress(address, chain, network = 'testnet') {
252 if (!address || typeof address !== 'string') {
253 return { valid: false, reason: 'Address is required' }
254 }
255
256 if (chain === 'bitcoin') {
257 return validateBitcoinAddress(address, network)
258 } else if (chain === 'ethereum') {
259 return validateEthereumAddress(address)
260 } else {
261 return { valid: false, reason: `Unsupported chain: ${chain}` }
262 }
263}
264
265module.exports = {
266 validateAddress,
267 validateBitcoinAddress,
268 validateEthereumAddress
269}
lib/amount-validator.cjs +0 -158
……@@ -1,158 +0,0 @@
1/**
2 * FILE: lib/amount-validator.cjs
3 * STATUS: ACTIVE
4 * RUNTIME: Bare
5 * MODULE: CommonJS
6 *
7 * PURPOSE:
8 * Amount validation with range checks and format validation
9 *
10 * USED BY:
11 * - wallet-server.cjs (transaction validation)
12 *
13 * KEY EXPORTS:
14 * - validateAmount(amountStr, chain) - Format and range validation
15 * - validateSufficientBalance(amount, balance, fee, chain) - Balance check
16 * - formatAmount(amount, chain) - Human-readable formatting
17 *
18 * VALIDATION:
19 * - Checks for positive amounts
20 * - Validates BigInt format (no decimals, scientific notation)
21 * - Enforces maximum limits (21M BTC, 1B ETH)
22 * - Balance sufficiency checks
23 */
24
25/**
26 * Maximum supply limits (prevent overflow attacks)
27 */
28const MAX_BTC = 21_000_000n * 100_000_000n // 21 million BTC in satoshi
29const MAX_ETH = 1_000_000_000n * 1_000_000_000_000_000_000n // 1 billion ETH in wei
30
31/**
32 * Validate amount string format and range
33 * @param {string|number} amountStr - Amount in smallest unit (satoshi/wei)
34 * @param {string} chain - 'bitcoin' or 'ethereum'
35 * @returns {Object} { valid: boolean, amount?: bigint, reason?: string }
36 */
37function validateAmount(amountStr, chain) {
38 // Check if amount is provided
39 if (amountStr === undefined || amountStr === null || amountStr === '') {
40 return { valid: false, reason: 'Amount is required' }
41 }
42
43 // Try to convert to BigInt
44 let amount
45 try {
46 // Remove any spaces or underscores (common in large numbers)
47 const cleaned = String(amountStr).replace(/[\s_]/g, '')
48
49 // Check for scientific notation (not supported by BigInt)
50 if (cleaned.includes('e') || cleaned.includes('E')) {
51 return { valid: false, reason: 'Amount cannot be in scientific notation (use smallest unit: satoshi/wei)' }
52 }
53
54 // Check for decimal point (BigInt doesn't support decimals)
55 if (cleaned.includes('.')) {
56 return { valid: false, reason: 'Decimal values not supported (use smallest unit: satoshi/wei)' }
57 }
58
59 // Check for negative sign
60 if (cleaned.includes('-')) {
61 return { valid: false, reason: 'Amount cannot be negative' }
62 }
63
64 amount = BigInt(cleaned)
65 } catch (err) {
66 return { valid: false, reason: `Invalid amount format: ${err.message}` }
67 }
68
69 // Check if non-negative (allow zero for balance checks)
70 if (amount < 0n) {
71 return { valid: false, reason: 'Amount cannot be negative' }
72 }
73
74 // Check maximum limits (prevent overflow)
75 if (chain === 'bitcoin' && amount > MAX_BTC) {
76 return { valid: false, reason: 'Amount exceeds maximum (21M BTC = 2,100,000,000,000,000 satoshi)' }
77 }
78 if (chain === 'ethereum' && amount > MAX_ETH) {
79 return { valid: false, reason: 'Amount exceeds maximum (1B ETH)' }
80 }
81
82 return { valid: true, amount }
83}
84
85/**
86 * Validate sufficient balance for transaction
87 * @param {bigint} amount - Amount to send
88 * @param {bigint} balance - Available balance
89 * @param {bigint} fee - Transaction fee
90 * @param {string} chain - Blockchain identifier
91 * @returns {Object} { valid: boolean, reason?: string }
92 */
93function validateSufficientBalance(amount, balance, fee, chain) {
94 const total = amount + fee
95
96 if (total > balance) {
97 return {
98 valid: false,
99 reason: `Insufficient balance: need ${formatAmount(total, chain)}, have ${formatAmount(balance, chain)}`
100 }
101 }
102
103 return { valid: true }
104}
105
106/**
107 * Format amount for display (human-readable)
108 * @param {bigint|string} amount - Amount in smallest unit
109 * @param {string} chain - Blockchain identifier
110 * @returns {string} Formatted amount with unit
111 */
112function formatAmount(amount, chain) {
113 const amountBigInt = typeof amount === 'string' ? BigInt(amount) : amount
114
115 if (chain === 'bitcoin') {
116 // 1 BTC = 100,000,000 satoshi (1e8)
117 const btc = Number(amountBigInt) / 1e8
118 return `${btc.toFixed(8)} BTC`
119 } else if (chain === 'ethereum') {
120 // 1 ETH = 1,000,000,000,000,000,000 wei (1e18)
121 const eth = Number(amountBigInt) / 1e18
122 return `${eth.toFixed(18)} ETH`
123 }
124
125 return String(amountBigInt)
126}
127
128/**
129 * Parse human-readable amount to smallest unit
130 * @param {string} amountStr - Amount in BTC/ETH
131 * @param {string} chain - Blockchain identifier
132 * @returns {bigint} Amount in smallest unit (satoshi/wei)
133 * @throws {Error} If amount is invalid
134 */
135function parseAmount(amountStr, chain) {
136 const amount = parseFloat(amountStr)
137
138 if (isNaN(amount) || amount < 0) {
139 throw new Error('Invalid amount')
140 }
141
142 if (chain === 'bitcoin') {
143 // Convert BTC to satoshi
144 return BigInt(Math.floor(amount * 1e8))
145 } else if (chain === 'ethereum') {
146 // Convert ETH to wei
147 return BigInt(Math.floor(amount * 1e18))
148 }
149
150 throw new Error(`Unsupported chain: ${chain}`)
151}
152
153module.exports = {
154 validateAmount,
155 validateSufficientBalance,
156 formatAmount,
157 parseAmount
158}
lib/auth.cjs +0 -144
……@@ -1,144 +0,0 @@
1/**
2 * FILE: lib/auth.cjs
3 * STATUS: ACTIVE
4 * RUNTIME: Bare
5 * MODULE: CommonJS
6 *
7 * PURPOSE:
8 * API authentication system for wallet-server.cjs
9 * Generates and verifies Bearer tokens for HTTP API access
10 *
11 * USED BY:
12 * - wallet-server.cjs (authentication middleware)
13 *
14 * KEY EXPORTS:
15 * - generateToken() - Generate secure random token
16 * - saveToken() - Save token to file
17 * - loadToken() - Load token from file
18 * - verifyToken() - Verify Bearer token from request
19 *
20 * SECURITY:
21 * - 32-byte random tokens (256-bit security)
22 * - Token file mode 0600 (owner read/write only)
23 * - Bearer token authentication (RFC 6750)
24 */
25
26const crypto = require('hypercore-crypto')
27const fs = require('bare-fs')
28const path = require('bare-path')
29const os = require('bare-os')
30
31const STORAGE_DIR = path.join(os.homedir(), '.hyper-wallet')
32const TOKEN_FILE = path.join(STORAGE_DIR, '.api-token')
33
34/**
35 * Generate secure random API token
36 * Uses hypercore-crypto for cryptographically secure random bytes
37 *
38 * @returns {string} - Hex-encoded 32-byte token (64 hex characters)
39 */
40function generateToken() {
41 const randomBytes = crypto.randomBytes(32)
42 return randomBytes.toString('hex')
43}
44
45/**
46 * Save API token to file with secure permissions
47 *
48 * @param {string} token - API token to save
49 */
50function saveToken(token) {
51 // Ensure storage directory exists
52 if (!fs.existsSync(STORAGE_DIR)) {
53 fs.mkdirSync(STORAGE_DIR, { recursive: true, mode: 0o700 })
54 }
55
56 // Write token with mode 0600 (owner read/write only)
57 fs.writeFileSync(TOKEN_FILE, token, { mode: 0o600 })
58}
59
60/**
61 * Load API token from file
62 *
63 * @returns {string|null} - Token if exists, null otherwise
64 */
65function loadToken() {
66 if (!fs.existsSync(TOKEN_FILE)) {
67 return null
68 }
69
70 try {
71 return fs.readFileSync(TOKEN_FILE, 'utf8').trim()
72 } catch (err) {
73 console.error('Error loading API token:', err.message)
74 return null
75 }
76}
77
78/**
79 * Verify Bearer token from HTTP request
80 *
81 * @param {object} req - HTTP request object
82 * @returns {boolean} - True if token is valid, false otherwise
83 */
84function verifyToken(req) {
85 // Get Authorization header
86 const authHeader = req.headers.authorization
87
88 // Check if header exists and has Bearer scheme
89 if (!authHeader || !authHeader.startsWith('Bearer ')) {
90 return false
91 }
92
93 // Extract token (remove "Bearer " prefix)
94 const token = authHeader.slice(7)
95
96 // Load stored token
97 const storedToken = loadToken()
98
99 // Verify token matches
100 if (!storedToken) {
101 return false
102 }
103
104 // Use constant-time comparison to prevent timing attacks
105 // Note: crypto.timingSafeEqual doesn't exist in Bare runtime
106 // Implement manual constant-time comparison
107 if (token.length !== storedToken.length) {
108 return false
109 }
110
111 let diff = 0
112 for (let i = 0; i < token.length; i++) {
113 diff |= token.charCodeAt(i) ^ storedToken.charCodeAt(i)
114 }
115
116 return diff === 0
117}
118
119/**
120 * Delete API token (for security - regenerate on demand)
121 */
122function deleteToken() {
123 if (fs.existsSync(TOKEN_FILE)) {
124 fs.unlinkSync(TOKEN_FILE)
125 }
126}
127
128/**
129 * Check if API token exists
130 *
131 * @returns {boolean} - True if token file exists
132 */
133function tokenExists() {
134 return fs.existsSync(TOKEN_FILE)
135}
136
137module.exports = {
138 generateToken,
139 saveToken,
140 loadToken,
141 verifyToken,
142 deleteToken,
143 tokenExists
144}
lib/blockchain-api.cjs +0 -166
……@@ -1,166 +0,0 @@
1/**
2 * Blockchain API - Phase 6.3
3 *
4 * Wrapper around WDK blockchain queries.
5 * Provides unified interface for querying UTXOs, transactions, and fees.
6 */
7
8/**
9 * Query UTXOs for a Bitcoin address
10 * @param {Object} btcWallet - WDK Bitcoin wallet instance
11 * @param {string} address - Bitcoin address (tb1q... for testnet)
12 * @returns {Array} Array of UTXO objects
13 */
14async function getUTXOsForAddress(btcWallet, address) {
15 try {
16 const utxos = await btcWallet.getUTXOs(address)
17
18 // Normalize UTXO format
19 return utxos.map(utxo => ({
20 txid: utxo.txid,
21 vout: utxo.vout,
22 address: address,
23 value: utxo.value, // satoshis
24 scriptPubKey: utxo.scriptPubKey || null,
25 confirmations: utxo.confirmations || 0,
26 blockHeight: utxo.height || null
27 }))
28 } catch (err) {
29 console.error(`Error fetching UTXOs for ${address}:`, err.message)
30 return []
31 }
32}
33
34/**
35 * Query UTXOs for multiple addresses
36 * @param {Object} btcWallet - WDK Bitcoin wallet instance
37 * @param {Array} addresses - Array of address strings
38 * @returns {Array} Combined array of UTXOs from all addresses
39 */
40async function getUTXOsForAddresses(btcWallet, addresses) {
41 const allUTXOs = []
42
43 for (const address of addresses) {
44 const utxos = await getUTXOsForAddress(btcWallet, address)
45 allUTXOs.push(...utxos)
46 }
47
48 return allUTXOs
49}
50
51/**
52 * Get current fee rates from network
53 * @param {Object} btcWallet - WDK Bitcoin wallet instance
54 * @returns {Object} Fee rates in sat/vbyte
55 */
56async function getFeeRates(btcWallet) {
57 try {
58 const rates = await btcWallet.getFeeRates()
59
60 return {
61 low: rates.low || 1,
62 medium: rates.medium || 5,
63 high: rates.high || 10
64 }
65 } catch (err) {
66 console.error('Error fetching fee rates:', err.message)
67
68 // Return default rates on error
69 return {
70 low: 1,
71 medium: 5,
72 high: 10
73 }
74 }
75}
76
77/**
78 * Get transaction details from blockchain
79 * @param {Object} btcWallet - WDK Bitcoin wallet instance
80 * @param {string} txid - Transaction ID
81 * @returns {Object|null} Transaction details or null if not found
82 */
83async function getTransaction(btcWallet, txid) {
84 try {
85 const tx = await btcWallet.getTransaction(txid)
86
87 return {
88 txid: tx.txid,
89 confirmations: tx.confirmations || 0,
90 blockHeight: tx.blockHeight || null,
91 blockHash: tx.blockHash || null,
92 timestamp: tx.timestamp || null,
93 inputs: tx.inputs || [],
94 outputs: tx.outputs || [],
95 fee: tx.fee || null
96 }
97 } catch (err) {
98 console.error(`Error fetching transaction ${txid}:`, err.message)
99 return null
100 }
101}
102
103/**
104 * Get balance for a single address (direct query)
105 * @param {Object} btcWallet - WDK Bitcoin wallet instance
106 * @param {string} address - Bitcoin address
107 * @returns {number} Balance in satoshis
108 */
109async function getAddressBalance(btcWallet, address) {
110 try {
111 const balance = await btcWallet.getBalance(address)
112 return balance || 0
113 } catch (err) {
114 console.error(`Error fetching balance for ${address}:`, err.message)
115 return 0
116 }
117}
118
119/**
120 * Broadcast transaction to network
121 * @param {Object} btcWallet - WDK Bitcoin wallet instance
122 * @param {string} txHex - Signed transaction hex
123 * @returns {Object} Broadcast result
124 */
125async function broadcastTransaction(btcWallet, txHex) {
126 try {
127 const result = await btcWallet.broadcast(txHex)
128
129 return {
130 success: true,
131 txid: result.txid,
132 message: 'Transaction broadcasted successfully'
133 }
134 } catch (err) {
135 return {
136 success: false,
137 error: err.message,
138 message: 'Failed to broadcast transaction'
139 }
140 }
141}
142
143/**
144 * Check if blockchain API is reachable
145 * @param {Object} btcWallet - WDK Bitcoin wallet instance
146 * @returns {boolean} True if API is reachable
147 */
148async function checkConnection(btcWallet) {
149 try {
150 // Try to get fee rates as a connectivity check
151 await btcWallet.getFeeRates()
152 return true
153 } catch (err) {
154 return false
155 }
156}
157
158module.exports = {
159 getUTXOsForAddress,
160 getUTXOsForAddresses,
161 getFeeRates,
162 getTransaction,
163 getAddressBalance,
164 broadcastTransaction,
165 checkConnection
166}
lib/blockchain-providers.cjs +0 -394
……@@ -1,394 +0,0 @@
1// lib/blockchain-providers.cjs - Blockchain API providers
2require('bare-node-runtime/global')
3
4/**
5 * Ethereum providers configuration
6 * Supports multiple providers with automatic fallback
7 */
8const ETHEREUM_PROVIDERS = {
9 blockscout: {
10 name: 'Blockscout',
11 needsApiKey: false,
12 testnetUrl: 'https://eth-sepolia.blockscout.com/api',
13 mainnetUrl: 'https://eth.blockscout.com/api',
14 testnet4Url: null, // Blockscout doesn't have testnet4
15
16 getTxListUrl (address, network, apiKey) {
17 const baseUrl = network === 'testnet' ? this.testnetUrl : this.mainnetUrl
18 return `${baseUrl}?module=account&action=txlist&address=${address}&startblock=0&endblock=latest&page=1&offset=100&sort=desc`
19 },
20
21 // NEW: Get ERC-20 token transaction list
22 getTokenTxListUrl (address, network, contractAddress = null) {
23 const baseUrl = network === 'testnet' ? this.testnetUrl : this.mainnetUrl
24 let url = `${baseUrl}?module=account&action=tokentx&address=${address}&startblock=0&endblock=latest&page=1&offset=100&sort=desc`
25 if (contractAddress) {
26 url += `&contractaddress=${contractAddress}`
27 }
28 return url
29 },
30
31 parseResponse (data) {
32 if (data.status === '1' && Array.isArray(data.result)) {
33 return data.result.map(tx => ({
34 hash: tx.hash,
35 from: tx.from,
36 to: tx.to,
37 value: tx.value,
38 fee: tx.gasUsed && tx.gasPrice ? (BigInt(tx.gasUsed) * BigInt(tx.gasPrice)).toString() : '0',
39 blockNumber: parseInt(tx.blockNumber) || 0,
40 timestamp: parseInt(tx.timeStamp) || 0,
41 status: tx.txreceipt_status === '1' ? 'confirmed' : 'failed'
42 }))
43 }
44 throw new Error('Invalid response format')
45 },
46
47 // NEW: Parse token transaction response
48 parseTokenResponse (data) {
49 if (data.status === '1' && Array.isArray(data.result)) {
50 return data.result.map(tx => ({
51 hash: tx.hash,
52 from: tx.from,
53 to: tx.to,
54 value: tx.value, // Token amount in smallest unit
55 tokenSymbol: tx.tokenSymbol,
56 tokenName: tx.tokenName,
57 tokenDecimal: parseInt(tx.tokenDecimal) || 0,
58 contractAddress: tx.contractAddress,
59 fee: tx.gasUsed && tx.gasPrice ? (BigInt(tx.gasUsed) * BigInt(tx.gasPrice)).toString() : '0',
60 blockNumber: parseInt(tx.blockNumber) || 0,
61 timestamp: parseInt(tx.timeStamp) || 0,
62 status: tx.txreceipt_status === '1' ? 'confirmed' : 'failed'
63 }))
64 }
65 throw new Error('Invalid response format')
66 }
67 },
68
69 etherscan: {
70 name: 'Etherscan',
71 needsApiKey: true,
72 testnetUrl: 'https://api.etherscan.io/v2/api',
73 mainnetUrl: 'https://api.etherscan.io/api',
74 testnet4Url: null,
75
76 getTxListUrl (address, network, apiKey) {
77 if (!apiKey) throw new Error('Etherscan requires API key')
78
79 const baseUrl = network === 'testnet' ? this.testnetUrl : this.mainnetUrl
80 const chainId = network === 'testnet' ? '11155111' : '1' // Sepolia : Mainnet
81
82 if (network === 'testnet') {
83 // Etherscan v2 API for testnet
84 return `${baseUrl}?chainid=${chainId}&module=account&action=txlist&address=${address}&startblock=0&endblock=latest&page=1&offset=100&sort=desc&apikey=${apiKey}`
85 } else {
86 // Regular API for mainnet
87 return `${baseUrl}?module=account&action=txlist&address=${address}&startblock=0&endblock=latest&page=1&offset=100&sort=desc&apikey=${apiKey}`
88 }
89 },
90
91 parseResponse (data) {
92 // Same format as Blockscout
93 return ETHEREUM_PROVIDERS.blockscout.parseResponse(data)
94 }
95 },
96
97 alchemy: {
98 name: 'Alchemy',
99 needsApiKey: true,
100 testnetUrl: 'https://eth-sepolia.g.alchemy.com/v2',
101 mainnetUrl: 'https://eth-mainnet.g.alchemy.com/v2',
102 testnet4Url: null,
103
104 getTxListUrl (address, network, apiKey) {
105 if (!apiKey) throw new Error('Alchemy requires API key')
106
107 const baseUrl = network === 'testnet' ? this.testnetUrl : this.mainnetUrl
108 // Alchemy uses a different API format
109 return `${baseUrl}/${apiKey}`
110 },
111
112 async fetchTransactions (address, network, apiKey) {
113 // Alchemy uses JSON-RPC, not REST
114 const baseUrl = network === 'testnet' ? this.testnetUrl : this.mainnetUrl
115 const url = `${baseUrl}/${apiKey}`
116
117 const response = await fetch(url, {
118 method: 'POST',
119 headers: { 'Content-Type': 'application/json' },
120 body: JSON.stringify({
121 jsonrpc: '2.0',
122 method: 'alchemy_getAssetTransfers',
123 params: [{
124 fromBlock: '0x0',
125 toBlock: 'latest',
126 fromAddress: address,
127 category: ['external', 'internal'],
128 maxCount: '0x64' // 100 in hex
129 }],
130 id: 1
131 })
132 })
133
134 return response.json()
135 },
136
137 parseResponse (data) {
138 // Alchemy has different format
139 if (data.result && Array.isArray(data.result.transfers)) {
140 return data.result.transfers.map(tx => ({
141 hash: tx.hash,
142 from: tx.from,
143 to: tx.to,
144 value: tx.value ? (parseFloat(tx.value) * 1e18).toString() : '0',
145 fee: '0', // Not provided by Alchemy
146 blockNumber: tx.blockNum ? parseInt(tx.blockNum, 16) : 0,
147 timestamp: 0, // Not provided
148 status: 'confirmed'
149 }))
150 }
151 throw new Error('Invalid Alchemy response format')
152 }
153 }
154}
155
156/**
157 * Bitcoin providers configuration
158 */
159const BITCOIN_PROVIDERS = {
160 electrum: {
161 name: 'Electrum',
162 needsApiKey: false,
163 usesNativeWDK: true // Uses WDK's built-in getTransfers() method
164 },
165
166 blockstream: {
167 name: 'Blockstream',
168 needsApiKey: false,
169 testnetUrl: 'https://blockstream.info/testnet/api',
170 mainnetUrl: 'https://blockstream.info/api',
171 testnet4Url: 'https://blockstream.info/testnet4/api',
172
173 getTxListUrl (address, network) {
174 let baseUrl
175 if (network === 'testnet4') {
176 baseUrl = this.testnet4Url
177 } else if (network === 'testnet') {
178 baseUrl = this.testnetUrl
179 } else {
180 baseUrl = this.mainnetUrl
181 }
182 return `${baseUrl}/address/${address}/txs`
183 },
184
185 parseResponse (data) {
186 if (!Array.isArray(data)) throw new Error('Invalid response')
187
188 return data.map(tx => ({
189 txid: tx.txid,
190 hash: tx.txid,
191 value: tx.vout.reduce((sum, out) => sum + out.value, 0),
192 fee: tx.fee || 0,
193 blockHeight: tx.status?.block_height || 0,
194 timestamp: tx.status?.block_time || 0,
195 status: tx.status?.confirmed ? 'confirmed' : 'pending'
196 }))
197 }
198 },
199
200 mempool: {
201 name: 'Mempool.space',
202 needsApiKey: false,
203 testnetUrl: 'https://mempool.space/testnet/api',
204 mainnetUrl: 'https://mempool.space/api',
205 testnet4Url: 'https://mempool.space/testnet4/api',
206
207 getTxListUrl (address, network) {
208 let baseUrl
209 if (network === 'testnet4') {
210 baseUrl = this.testnet4Url
211 } else if (network === 'testnet') {
212 baseUrl = this.testnetUrl
213 } else {
214 baseUrl = this.mainnetUrl
215 }
216 return `${baseUrl}/address/${address}/txs`
217 },
218
219 parseResponse (data) {
220 // Same format as Blockstream
221 return BITCOIN_PROVIDERS.blockstream.parseResponse(data)
222 }
223 }
224}
225
226/**
227 * Fetch transactions using configured provider with automatic fallback
228 * @param {string} chain - 'bitcoin' or 'ethereum'
229 * @param {string} address - Blockchain address
230 * @param {string} network - 'testnet', 'testnet4', or 'mainnet'
231 * @param {Object} config - Configuration object from getConfig()
232 * @returns {Promise<Array|Object>} - Array of transactions or { useNativeWDK: true }
233 */
234async function fetchTransactions (chain, address, network, config) {
235 const providers = chain === 'ethereum' ? ETHEREUM_PROVIDERS : BITCOIN_PROVIDERS
236
237 // Get primary provider from config
238 const primaryProviderName = config.blockchain[chain].provider
239 const apiKey = config.blockchain[chain].apiKey
240
241 // Build provider list (primary first, then fallbacks)
242 const providerList = [
243 primaryProviderName,
244 ...Object.keys(providers).filter(p => p !== primaryProviderName)
245 ]
246
247 let lastError = null
248
249 for (const providerName of providerList) {
250 const provider = providers[providerName]
251 if (!provider) {
252 console.log(` ⚠️ Unknown provider: ${providerName}`)
253 continue
254 }
255
256 try {
257 // Skip if provider needs API key but we don't have one
258 if (provider.needsApiKey && !apiKey) {
259 console.log(` ⚠️ Skipping ${provider.name} (API key required)`)
260 continue
261 }
262
263 // If provider uses native WDK method, return signal
264 if (provider.usesNativeWDK) {
265 return { useNativeWDK: true, provider: provider.name }
266 }
267
268 console.log(` 📡 Fetching from ${provider.name}...`)
269
270 // Handle Alchemy's special API
271 if (providerName === 'alchemy' && provider.fetchTransactions) {
272 const data = await provider.fetchTransactions(address, network, apiKey)
273 const transactions = provider.parseResponse(data)
274 console.log(` ✅ ${provider.name}: ${transactions.length} transactions`)
275 return transactions
276 }
277
278 // Standard HTTP API
279 const url = provider.getTxListUrl(address, network, apiKey)
280 const response = await fetch(url)
281
282 if (!response.ok) {
283 throw new Error(`HTTP ${response.status}: ${response.statusText}`)
284 }
285
286 const data = await response.json()
287 const transactions = provider.parseResponse(data)
288
289 console.log(` ✅ ${provider.name}: ${transactions.length} transactions`)
290 return transactions
291 } catch (err) {
292 console.log(` ⚠️ ${provider.name} failed: ${err.message}`)
293 lastError = err
294 continue
295 }
296 }
297
298 // All providers failed
299 throw new Error(`All providers failed. Last error: ${lastError?.message}`)
300}
301
302/**
303 * Fetch ERC-20 token transactions for an Ethereum address
304 * @param {string} address - Ethereum address
305 * @param {string} network - 'testnet' or 'mainnet'
306 * @param {Object} config - Configuration object from getConfig()
307 * @param {string} contractAddress - Optional: filter by token contract address
308 * @returns {Promise<Array>} - Array of token transactions
309 */
310async function fetchTokenTransactions (address, network, config, contractAddress = null) {
311 const providers = ETHEREUM_PROVIDERS
312
313 // Get primary provider from config
314 const primaryProviderName = config.blockchain.ethereum.provider
315 const apiKey = config.blockchain.ethereum.apiKey
316
317 // Build provider list (primary first, then fallbacks)
318 const providerList = [
319 primaryProviderName,
320 ...Object.keys(providers).filter(p => p !== primaryProviderName)
321 ]
322
323 let lastError = null
324
325 for (const providerName of providerList) {
326 const provider = providers[providerName]
327 if (!provider || !provider.getTokenTxListUrl) {
328 console.log(` ⚠️ ${providerName} doesn't support token transactions`)
329 continue
330 }
331
332 try {
333 // Skip if provider needs API key but we don't have one
334 if (provider.needsApiKey && !apiKey) {
335 console.log(` ⚠️ Skipping ${provider.name} (API key required)`)
336 continue
337 }
338
339 console.log(` 📡 Fetching token txs from ${provider.name}...`)
340
341 // Get token transaction list URL
342 const url = provider.getTokenTxListUrl(address, network, contractAddress)
343 const response = await fetch(url)
344
345 if (!response.ok) {
346 throw new Error(`HTTP ${response.status}: ${response.statusText}`)
347 }
348
349 const data = await response.json()
350 const transactions = provider.parseTokenResponse(data)
351
352 console.log(` ✅ ${provider.name}: ${transactions.length} token transactions`)
353 return transactions
354 } catch (err) {
355 console.log(` ⚠️ ${provider.name} failed: ${err.message}`)
356 lastError = err
357 continue
358 }
359 }
360
361 // All providers failed - return empty array instead of throwing
362 console.log(` ⚠️ All token tx providers failed: ${lastError?.message}`)
363 return []
364}
365
366/**
367 * Get list of available providers for a chain
368 * @param {string} chain - 'bitcoin' or 'ethereum'
369 * @returns {Array} - Array of provider names
370 */
371function getAvailableProviders (chain) {
372 const providers = chain === 'ethereum' ? ETHEREUM_PROVIDERS : BITCOIN_PROVIDERS
373 return Object.keys(providers)
374}
375
376/**
377 * Get provider info
378 * @param {string} chain - 'bitcoin' or 'ethereum'
379 * @param {string} providerName - Provider name
380 * @returns {Object} - Provider configuration
381 */
382function getProviderInfo (chain, providerName) {
383 const providers = chain === 'ethereum' ? ETHEREUM_PROVIDERS : BITCOIN_PROVIDERS
384 return providers[providerName] || null
385}
386
387module.exports = {
388 ETHEREUM_PROVIDERS,
389 BITCOIN_PROVIDERS,
390 fetchTransactions,
391 fetchTokenTransactions,
392 getAvailableProviders,
393 getProviderInfo
394}
lib/config-loader.cjs +0 -96
……@@ -1,96 +0,0 @@
1// lib/config-loader.cjs - Configuration Loader
2require('bare-node-runtime/global')
3
4const fs = require('bare-fs')
5const path = require('bare-path')
6
7/**
8 * Load configuration from file
9 * Looks for config/default.json relative to project root
10 * @param {string} configPath - Optional path to config file
11 * @returns {Object} - Configuration object
12 */
13function loadConfig(configPath) {
14 // Default path: config/default.json in project root
15 const defaultPath = path.join(__dirname, '..', 'config', 'default.json')
16 const filePath = configPath || defaultPath
17
18 try {
19 if (!fs.existsSync(filePath)) {
20 console.warn(`⚠️ Config file not found: ${filePath}`)
21 console.warn('Using default configuration')
22 return getDefaultConfig()
23 }
24
25 const content = fs.readFileSync(filePath, 'utf8')
26 const config = JSON.parse(content)
27
28 // Convert flat config to format expected by wdk-integration.cjs
29 return normalizeConfig(config)
30 } catch (err) {
31 console.error(`Failed to load config from ${filePath}:`, err.message)
32 console.warn('Falling back to default configuration')
33 return getDefaultConfig()
34 }
35}
36
37/**
38 * Normalize config to expected format
39 * Converts config/default.json format to internal format used by wdk-integration
40 * @param {Object} config - Raw config from file
41 * @returns {Object} - Normalized config
42 */
43function normalizeConfig(config) {
44 const normalized = {
45 storagePath: config.storagePath || '~/.hyper-wallet/storage',
46 network: config.network || 'testnet',
47 protocols: config.protocols || ['wdk'],
48
49 // Convert network flag to boolean flags expected by wdk-integration.cjs
50 testnet: config.network === 'testnet',
51 testnet4: config.network === 'testnet4',
52 mainnet: config.network === 'mainnet',
53
54 // WDK-specific config
55 wdk: config.wdk || {}
56 }
57
58 return normalized
59}
60
61/**
62 * Get default configuration (fallback)
63 * Used when config file is missing or invalid
64 * @returns {Object} - Default configuration
65 */
66function getDefaultConfig() {
67 return {
68 storagePath: '~/.hyper-wallet/storage',
69 network: 'testnet',
70 protocols: ['wdk'],
71 testnet: true,
72 testnet4: false,
73 mainnet: false,
74 wdk: {
75 chains: ['ethereum', 'bitcoin'],
76 networks: {
77 ethereum: {
78 name: 'sepolia',
79 provider: 'https://ethereum-sepolia-rpc.publicnode.com',
80 chainId: 11155111
81 },
82 bitcoin: {
83 network: 'testnet',
84 host: 'electrum.blockstream.info',
85 port: 60001
86 }
87 }
88 }
89 }
90}
91
92module.exports = {
93 loadConfig,
94 normalizeConfig,
95 getDefaultConfig
96}
lib/config.cjs +0 -233
……@@ -1,233 +0,0 @@
1// lib/config.cjs - Configuration management system
2// CRITICAL: Load bare-node-runtime FIRST before any other requires
3require('bare-node-runtime/global')
4
5const fs = require('bare-fs')
6const path = require('bare-path')
7const os = require('bare-os')
8
9const CONFIG_DIR = path.join(os.homedir(), '.hyper-wallet')
10const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
11
12// Default configuration
13const DEFAULT_CONFIG = {
14 version: 1,
15 network: 'testnet',
16
17 blockchain: {
18 ethereum: {
19 provider: 'blockscout', // Default: no API key needed!
20 apiKey: null,
21 rpcUrl: null
22 },
23 bitcoin: {
24 provider: 'electrum',
25 host: 'electrum.blockstream.info',
26 port: 60001
27 }
28 },
29
30 ui: {
31 theme: 'dark',
32 autoLock: 30, // 30 minutes
33 currency: 'USD'
34 },
35
36 sync: {
37 autoSync: true,
38 syncInterval: 15 // 15 minutes
39 },
40
41 security: {
42 requirePasswordOnSend: true,
43 sessionTimeout: 30 // 30 minutes
44 }
45}
46
47/**
48 * Deep merge objects
49 * Later objects override earlier ones
50 */
51function mergeDeep (...objects) {
52 const result = {}
53
54 for (const obj of objects) {
55 if (!obj) continue
56
57 for (const key in obj) {
58 if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
59 result[key] = mergeDeep(result[key] || {}, obj[key])
60 } else {
61 result[key] = obj[key]
62 }
63 }
64 }
65
66 return result
67}
68
69/**
70 * Load user config file
71 * Creates default if doesn't exist
72 */
73function loadUserConfig () {
74 // Create directory if needed
75 if (!fs.existsSync(CONFIG_DIR)) {
76 fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 })
77 }
78
79 // Create default config if doesn't exist
80 if (!fs.existsSync(CONFIG_FILE)) {
81 saveConfig(DEFAULT_CONFIG)
82 return DEFAULT_CONFIG
83 }
84
85 try {
86 const data = fs.readFileSync(CONFIG_FILE, 'utf8')
87 const userConfig = JSON.parse(data)
88
89 // Merge with defaults (in case new fields added in updates)
90 return mergeDeep(DEFAULT_CONFIG, userConfig)
91 } catch (err) {
92 console.error('⚠️ Error loading config, using defaults:', err.message)
93 return DEFAULT_CONFIG
94 }
95}
96
97/**
98 * Load config from environment variables
99 * Overrides user config
100 */
101function loadEnvConfig () {
102 const envConfig = {}
103
104 if (process.env.WALLET_NETWORK) {
105 envConfig.network = process.env.WALLET_NETWORK
106 }
107
108 if (process.env.ETHEREUM_PROVIDER) {
109 envConfig.blockchain = envConfig.blockchain || {}
110 envConfig.blockchain.ethereum = envConfig.blockchain.ethereum || {}
111 envConfig.blockchain.ethereum.provider = process.env.ETHEREUM_PROVIDER
112 }
113
114 if (process.env.ETHERSCAN_API_KEY) {
115 envConfig.blockchain = envConfig.blockchain || {}
116 envConfig.blockchain.ethereum = envConfig.blockchain.ethereum || {}
117 envConfig.blockchain.ethereum.apiKey = process.env.ETHERSCAN_API_KEY
118 }
119
120 if (process.env.ETHEREUM_RPC_URL) {
121 envConfig.blockchain = envConfig.blockchain || {}
122 envConfig.blockchain.ethereum = envConfig.blockchain.ethereum || {}
123 envConfig.blockchain.ethereum.rpcUrl = process.env.ETHEREUM_RPC_URL
124 }
125
126 // Legacy support: Check .etherscan-api-key file
127 const legacyKeyFile = path.join(CONFIG_DIR, '.etherscan-api-key')
128 if (fs.existsSync(legacyKeyFile)) {
129 try {
130 const apiKey = fs.readFileSync(legacyKeyFile, 'utf8').trim()
131 if (apiKey) {
132 envConfig.blockchain = envConfig.blockchain || {}
133 envConfig.blockchain.ethereum = envConfig.blockchain.ethereum || {}
134 envConfig.blockchain.ethereum.apiKey = apiKey
135 // Don't override provider if already set
136 if (!envConfig.blockchain.ethereum.provider) {
137 envConfig.blockchain.ethereum.provider = 'etherscan'
138 }
139 }
140 } catch (err) {
141 // Ignore error reading legacy file
142 }
143 }
144
145 return envConfig
146}
147
148/**
149 * Get complete configuration
150 * Merges defaults + user config + env vars
151 */
152function getConfig () {
153 const defaults = DEFAULT_CONFIG
154 const user = loadUserConfig()
155 const env = loadEnvConfig()
156
157 return mergeDeep(defaults, user, env)
158}
159
160/**
161 * Save configuration to file
162 */
163function saveConfig (config) {
164 // Ensure directory exists
165 if (!fs.existsSync(CONFIG_DIR)) {
166 fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 })
167 }
168
169 const data = JSON.stringify(config, null, 2)
170 fs.writeFileSync(CONFIG_FILE, data, { mode: 0o600 })
171}
172
173/**
174 * Update specific config value
175 * Example: updateConfig('blockchain.ethereum.provider', 'etherscan')
176 */
177function updateConfig (path, value) {
178 const config = loadUserConfig()
179
180 // Set nested value
181 const parts = path.split('.')
182 let current = config
183
184 for (let i = 0; i < parts.length - 1; i++) {
185 if (!current[parts[i]]) current[parts[i]] = {}
186 current = current[parts[i]]
187 }
188
189 current[parts[parts.length - 1]] = value
190
191 saveConfig(config)
192 return config
193}
194
195/**
196 * Migrate legacy .etherscan-api-key file to config.json
197 * Called automatically on first config load
198 */
199function migrateLegacyConfig () {
200 const legacyKeyFile = path.join(CONFIG_DIR, '.etherscan-api-key')
201
202 if (fs.existsSync(legacyKeyFile)) {
203 try {
204 const apiKey = fs.readFileSync(legacyKeyFile, 'utf8').trim()
205
206 if (apiKey) {
207 // Update config
208 const config = loadUserConfig()
209 config.blockchain.ethereum.apiKey = apiKey
210 config.blockchain.ethereum.provider = 'etherscan' // They were using Etherscan
211 saveConfig(config)
212
213 // Rename old file (don't delete, for safety)
214 fs.renameSync(legacyKeyFile, legacyKeyFile + '.migrated')
215
216 console.log('✅ Migrated Etherscan API key to config.json')
217 console.log(` Old file renamed to ${path.basename(legacyKeyFile)}.migrated`)
218 }
219 } catch (err) {
220 console.error('⚠️ Error migrating legacy config:', err.message)
221 }
222 }
223}
224
225module.exports = {
226 getConfig,
227 saveConfig,
228 updateConfig,
229 migrateLegacyConfig,
230 DEFAULT_CONFIG,
231 CONFIG_FILE,
232 CONFIG_DIR
233}
lib/create-wallet-server.cjs +0 -37
……@@ -1,37 +0,0 @@
1// Wrapper to create wallet HTTP server (reusing terminal version logic)
2// This allows desktop and terminal versions to share the same API implementation
3
4const http = require('bare-http1')
5const path = require('bare-path')
6
7// Import the terminal version's wallet server
8// Since lib/ is symlinked to ../hyper-wallet/lib/, we can require the parent's server
9const parentWalletServer = path.join(__dirname, '../../hyper-wallet/wallet-server.cjs')
10
11module.exports = async function createServer(port = 4004) {
12 // For now, just start the HTTP server directly
13 // TODO: Extract server creation logic from wallet-server.cjs into a reusable function
14
15 const server = http.createServer((req, res) => {
16 res.setHeader('Access-Control-Allow-Origin', '*')
17 res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
18 res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
19
20 if (req.method === 'OPTIONS') {
21 res.statusCode = 200
22 res.end()
23 return
24 }
25
26 res.statusCode = 200
27 res.setHeader('Content-Type', 'application/json')
28 res.end(JSON.stringify({ status: 'ok', message: 'API placeholder - use terminal version server logic' }))
29 })
30
31 return new Promise((resolve, reject) => {
32 server.listen(port, 'localhost', (err) => {
33 if (err) reject(err)
34 else resolve(server)
35 })
36 })
37}
lib/encryption.cjs +0 -167
……@@ -1,167 +0,0 @@
1/**
2 * FILE: lib/encryption.cjs
3 * STATUS: ACTIVE
4 * RUNTIME: Bare
5 * MODULE: CommonJS
6 *
7 * PURPOSE:
8 * Password-based encryption for mnemonics at rest
9 * Uses PBKDF2 for key derivation and AES-256-GCM for encryption
10 *
11 * USED BY:
12 * - wallet-server.cjs (mnemonic encryption/decryption)
13 * - hyper-wallet-advanced-cli.cjs (mnemonic encryption/decryption)
14 *
15 * KEY EXPORTS:
16 * - encrypt(plaintext, password) - Encrypt with password
17 * - decrypt(encrypted, password) - Decrypt with password
18 *
19 * SECURITY:
20 * - PBKDF2-SHA256 for key derivation (100k iterations, bare-crypto compatible)
21 * - AES-256-GCM authenticated encryption
22 * - Random salt and IV per encryption
23 * - Auth tag prevents tampering
24 *
25 * NOTE: Using PBKDF2 instead of scrypt due to bare-crypto limitations
26 */
27
28const crypto = require('bare-crypto')
29const b4a = require('b4a')
30
31// PBKDF2 parameters (bare-crypto compatible)
32const PBKDF2_ITERATIONS = 100000 // NIST recommended minimum
33const PBKDF2_KEYLEN = 32 // 256-bit key
34const PBKDF2_DIGEST = 'sha256' // Hash algorithm
35
36// AES-GCM parameters
37const IV_LENGTH = 12 // 96-bit IV (recommended for GCM)
38const SALT_LENGTH = 32 // 256-bit salt
39const TAG_LENGTH = 16 // 128-bit auth tag
40
41/**
42 * Derive encryption key from password using PBKDF2
43 * (bare-crypto compatible - scrypt not available)
44 *
45 * @param {string} password - User password
46 * @param {Buffer} salt - Random salt
47 * @returns {Buffer} - 256-bit derived key
48 */
49function deriveKey(password, salt) {
50 // Derive key using PBKDF2
51 // Note: This is CPU-intensive (intentional security feature)
52 const key = crypto.pbkdf2Sync(
53 password,
54 salt,
55 PBKDF2_ITERATIONS,
56 PBKDF2_KEYLEN,
57 PBKDF2_DIGEST
58 )
59
60 return key
61}
62
63/**
64 * Encrypt plaintext with password
65 *
66 * @param {string} plaintext - Text to encrypt (e.g., mnemonic)
67 * @param {string} password - Encryption password
68 * @returns {string} - Base64-encoded encrypted data with salt and IV
69 *
70 * Format: base64(salt + iv + ciphertext + tag)
71 */
72function encrypt(plaintext, password) {
73 // Generate random salt and IV
74 const salt = crypto.randomBytes(SALT_LENGTH)
75 const iv = crypto.randomBytes(IV_LENGTH)
76
77 // Derive encryption key
78 const key = deriveKey(password, salt)
79
80 // Create cipher
81 const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
82
83 // Encrypt plaintext
84 const plaintextBuffer = b4a.from(plaintext, 'utf8')
85 const encrypted = b4a.concat([
86 cipher.update(plaintextBuffer),
87 cipher.final()
88 ])
89
90 // Get authentication tag
91 const tag = cipher.getAuthTag()
92
93 // Concatenate: salt + iv + ciphertext + tag
94 const result = b4a.concat([salt, iv, encrypted, tag])
95
96 // Return as base64
97 return b4a.toString(result, 'base64')
98}
99
100/**
101 * Decrypt encrypted data with password
102 *
103 * @param {string} encrypted - Base64-encoded encrypted data
104 * @param {string} password - Decryption password
105 * @returns {string} - Decrypted plaintext
106 * @throws {Error} - If password is wrong or data is corrupted
107 */
108function decrypt(encrypted, password) {
109 // Decode from base64
110 const data = b4a.from(encrypted, 'base64')
111
112 // Extract components
113 const salt = data.slice(0, SALT_LENGTH)
114 const iv = data.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH)
115 const ciphertext = data.slice(SALT_LENGTH + IV_LENGTH, data.length - TAG_LENGTH)
116 const tag = data.slice(data.length - TAG_LENGTH)
117
118 // Derive decryption key
119 const key = deriveKey(password, salt)
120
121 // Create decipher
122 const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv)
123 decipher.setAuthTag(tag)
124
125 try {
126 // Decrypt ciphertext
127 const decrypted = b4a.concat([
128 decipher.update(ciphertext),
129 decipher.final()
130 ])
131
132 return b4a.toString(decrypted, 'utf8')
133 } catch (err) {
134 // Auth tag verification failed or wrong password
135 throw new Error('Decryption failed - wrong password or corrupted data')
136 }
137}
138
139/**
140 * Check if encrypted data format is valid
141 *
142 * @param {string} encrypted - Base64-encoded encrypted data
143 * @returns {boolean} - True if format is valid
144 */
145function isValidFormat(encrypted) {
146 try {
147 const data = b4a.from(encrypted, 'base64')
148 const minLength = SALT_LENGTH + IV_LENGTH + TAG_LENGTH
149 return data.length >= minLength
150 } catch {
151 return false
152 }
153}
154
155module.exports = {
156 encrypt,
157 decrypt,
158 isValidFormat,
159
160 // Export constants for testing
161 PBKDF2_ITERATIONS,
162 PBKDF2_KEYLEN,
163 PBKDF2_DIGEST,
164 IV_LENGTH,
165 SALT_LENGTH,
166 TAG_LENGTH
167}
lib/hyper-wallet-lib +1 -1
……@@ -1 +1 @@
1Subproject commit 4eb36c062547f6852a47b08f2399f2c3e081311d
1Subproject commit 629b2aec61dfbef0f3b85ca8ca280d6937929160
lib/input-sanitizer.cjs +0 -227
……@@ -1,227 +0,0 @@
1/**
2 * FILE: lib/input-sanitizer.cjs
3 * STATUS: ACTIVE
4 * RUNTIME: Bare
5 * MODULE: CommonJS
6 *
7 * PURPOSE:
8 * Input sanitization to prevent injection attacks and validate user inputs
9 *
10 * USED BY:
11 * - wallet-server.cjs (all user input validation)
12 *
13 * KEY EXPORTS:
14 * - sanitizeString(str, maxLength) - Generic string sanitization
15 * - sanitizeContactName(name) - Contact name validation
16 * - sanitizeDeviceName(name) - Device name validation
17 * - sanitizeNotes(notes) - Notes/description validation
18 * - validateChain(chain) - Chain identifier validation
19 * - validateNetwork(network) - Network identifier validation
20 *
21 * PROTECTION:
22 * - Removes null bytes (string termination attacks)
23 * - Enforces length limits (DoS prevention)
24 * - Validates character sets (injection prevention)
25 * - Trims whitespace (normalization)
26 */
27
28/**
29 * Generic string sanitization
30 * @param {string} str - Input string
31 * @param {number} maxLength - Maximum allowed length
32 * @returns {string} Sanitized string
33 * @throws {Error} If input is invalid
34 */
35function sanitizeString(str, maxLength = 1000) {
36 if (typeof str !== 'string') {
37 throw new Error('Input must be a string')
38 }
39
40 // Trim whitespace
41 let sanitized = str.trim()
42
43 // Truncate to max length (sanitize, don't throw)
44 if (sanitized.length > maxLength) {
45 sanitized = sanitized.slice(0, maxLength)
46 }
47
48 // Remove null bytes (can cause string termination attacks)
49 sanitized = sanitized.replace(/\0/g, '')
50
51 return sanitized
52}
53
54/**
55 * Sanitize contact name
56 * Allows: alphanumeric, spaces, and common punctuation
57 * @param {string} name - Contact name
58 * @returns {string} Sanitized name
59 * @throws {Error} If name is invalid
60 */
61function sanitizeContactName(name) {
62 const sanitized = sanitizeString(name, 100)
63
64 if (sanitized.length === 0) {
65 throw new Error('Name cannot be empty')
66 }
67
68 // Only allow safe characters (alphanumeric, spaces, common punctuation)
69 if (!sanitized.match(/^[a-zA-Z0-9\s\-_.,'()]+$/)) {
70 throw new Error('Name contains invalid characters (allowed: letters, numbers, spaces, -_.\'(),)')
71 }
72
73 return sanitized
74}
75
76/**
77 * Sanitize device name
78 * Allows: alphanumeric, spaces, hyphens, underscores
79 * @param {string} name - Device name
80 * @returns {string} Sanitized name
81 * @throws {Error} If name is invalid
82 */
83function sanitizeDeviceName(name) {
84 const sanitized = sanitizeString(name, 50)
85
86 if (sanitized.length === 0) {
87 throw new Error('Device name cannot be empty')
88 }
89
90 // Only allow alphanumeric, spaces, hyphens, underscores
91 if (!sanitized.match(/^[a-zA-Z0-9\s\-_]+$/)) {
92 throw new Error('Device name contains invalid characters (allowed: letters, numbers, spaces, -_)')
93 }
94
95 return sanitized
96}
97
98/**
99 * Sanitize notes/description fields
100 * Allows: alphanumeric, spaces, and extended punctuation
101 * @param {string} notes - Notes text
102 * @returns {string} Sanitized notes
103 * @throws {Error} If notes are invalid
104 */
105function sanitizeNotes(notes) {
106 // Check for non-string types
107 if (typeof notes !== 'string') {
108 throw new Error('Notes must be a string')
109 }
110
111 // Notes can be empty strings
112 if (notes.trim().length === 0) {
113 return ''
114 }
115
116 let sanitized = sanitizeString(notes, 500)
117
118 // Remove dangerous characters (< and >) instead of throwing
119 sanitized = sanitized.replace(/[<>]/g, '')
120
121 return sanitized
122}
123
124/**
125 * Validate chain identifier
126 * @param {string} chain - Chain name
127 * @returns {string|boolean} Validated chain or false if invalid
128 */
129function validateChain(chain) {
130 if (typeof chain !== 'string') {
131 return false
132 }
133
134 const validChains = ['bitcoin', 'ethereum']
135 const lowerChain = chain.toLowerCase()
136
137 if (!validChains.includes(lowerChain)) {
138 return false
139 }
140
141 return lowerChain
142}
143
144/**
145 * Validate network identifier
146 * @param {string} network - Network name
147 * @returns {string|boolean} Validated network or false if invalid
148 */
149function validateNetwork(network) {
150 if (typeof network !== 'string') {
151 return false
152 }
153
154 const validNetworks = ['testnet', 'mainnet', 'sepolia']
155 const lowerNetwork = network.toLowerCase()
156
157 if (!validNetworks.includes(lowerNetwork)) {
158 return false
159 }
160
161 return lowerNetwork
162}
163
164/**
165 * Sanitize hex string (for keys, hashes, etc.)
166 * @param {string} hex - Hex string
167 * @param {number} expectedLength - Expected length (0 = any length)
168 * @returns {string} Sanitized hex string (lowercase)
169 * @throws {Error} If hex is invalid
170 */
171function sanitizeHex(hex, expectedLength = 0) {
172 if (typeof hex !== 'string') {
173 throw new Error('Hex string must be a string')
174 }
175
176 // Remove 0x prefix if present
177 let sanitized = hex.toLowerCase()
178 if (sanitized.startsWith('0x')) {
179 sanitized = sanitized.slice(2)
180 }
181
182 // Remove non-hex characters instead of throwing
183 sanitized = sanitized.replace(/[^0-9a-f]/g, '')
184
185 // Check length if specified
186 if (expectedLength > 0 && sanitized.length !== expectedLength) {
187 throw new Error(`Invalid hex length (expected ${expectedLength}, got ${sanitized.length})`)
188 }
189
190 return sanitized
191}
192
193/**
194 * Validate invite code format
195 * @param {string} inviteCode - Base64 invite code
196 * @returns {string|boolean} Validated invite code or false if invalid
197 */
198function validateInviteCode(inviteCode) {
199 if (typeof inviteCode !== 'string') {
200 return false
201 }
202
203 const sanitized = inviteCode.trim()
204
205 // Check for valid base64 characters
206 if (!sanitized.match(/^[A-Za-z0-9+/]+=*$/)) {
207 return false
208 }
209
210 // Check reasonable length (should be ~200-300 chars for signed invite)
211 if (sanitized.length < 50 || sanitized.length > 1000) {
212 return false
213 }
214
215 return sanitized
216}
217
218module.exports = {
219 sanitizeString,
220 sanitizeContactName,
221 sanitizeDeviceName,
222 sanitizeNotes,
223 validateChain,
224 validateNetwork,
225 sanitizeHex,
226 validateInviteCode
227}
lib/pairing-protocol.cjs +0 -210
……@@ -1,210 +0,0 @@
1// lib/pairing-protocol.cjs - Device pairing protocol using Protomux
2
3const Protomux = require('protomux')
4const b4a = require('b4a')
5const c = require('compact-encoding')
6
7// Protocol identifier
8const PAIRING_PROTOCOL = 'hyper-wallet-pairing'
9
10/**
11 * Create pairing channel for device that is adding another device (server side)
12 * @param {Connection} conn - Hyperswarm connection
13 * @param {string} identityMnemonic - Identity mnemonic (24 words) to send to joining device
14 * @param {string} walletMnemonic - Wallet mnemonic (12 words) to send to joining device
15 * @param {string} deviceName - Name of this device
16 * @returns {Promise<Object>} { remoteDeviceName, remoteWriterKey }
17 */
18async function createPairingServer(conn, identityMnemonic, walletMnemonic, deviceName) {
19 return new Promise((resolve, reject) => {
20 const mux = Protomux.from(conn)
21 const channel = mux.createChannel({
22 protocol: PAIRING_PROTOCOL,
23 id: null
24 })
25
26 // Message types
27 const messages = {
28 // 0: Request pairing (client → server)
29 request: channel.addMessage({
30 encoding: {
31 preencode(state, m) {
32 c.string.preencode(state, m.deviceName)
33 c.buffer.preencode(state, m.writerKey)
34 },
35 encode(state, m) {
36 c.string.encode(state, m.deviceName)
37 c.buffer.encode(state, m.writerKey)
38 },
39 decode(state) {
40 return {
41 deviceName: c.string.decode(state),
42 writerKey: c.buffer.decode(state)
43 }
44 }
45 },
46 onmessage(msg) {
47 console.log(`📨 Received pairing request from: ${msg.deviceName}`)
48
49 // Send BOTH mnemonics to joining device
50 messages.response.send({
51 identityMnemonic,
52 walletMnemonic,
53 serverDeviceName: deviceName,
54 approved: true
55 })
56
57 // Resolve with remote device info
58 resolve({
59 remoteDeviceName: msg.deviceName,
60 remoteWriterKey: msg.writerKey
61 })
62 }
63 }),
64
65 // 1: Send mnemonics (server → client)
66 response: channel.addMessage({
67 encoding: {
68 preencode(state, m) {
69 c.string.preencode(state, m.identityMnemonic)
70 c.string.preencode(state, m.walletMnemonic)
71 c.string.preencode(state, m.serverDeviceName)
72 c.bool.preencode(state, m.approved)
73 },
74 encode(state, m) {
75 c.string.encode(state, m.identityMnemonic)
76 c.string.encode(state, m.walletMnemonic)
77 c.string.encode(state, m.serverDeviceName)
78 c.bool.encode(state, m.approved)
79 },
80 decode(state) {
81 return {
82 identityMnemonic: c.string.decode(state),
83 walletMnemonic: c.string.decode(state),
84 serverDeviceName: c.string.decode(state),
85 approved: c.bool.decode(state)
86 }
87 }
88 }
89 })
90 }
91
92 channel.open()
93
94 // Timeout after 60 seconds
95 const timeout = setTimeout(() => {
96 reject(new Error('Pairing timeout - no response from joining device'))
97 }, 60000)
98
99 // Clear timeout when resolved
100 const originalResolve = resolve
101 resolve = (value) => {
102 clearTimeout(timeout)
103 originalResolve(value)
104 }
105 })
106}
107
108/**
109 * Create pairing channel for device that is joining (client side)
110 * @param {Connection} conn - Hyperswarm connection
111 * @param {Buffer} writerKey - Writer key of this device
112 * @param {string} deviceName - Name of this device
113 * @returns {Promise<Object>} { identityMnemonic, walletMnemonic } received from server
114 */
115async function createPairingClient(conn, writerKey, deviceName) {
116 return new Promise((resolve, reject) => {
117 const mux = Protomux.from(conn)
118 const channel = mux.createChannel({
119 protocol: PAIRING_PROTOCOL,
120 id: null
121 })
122
123 // Message types
124 const messages = {
125 // 0: Request pairing (client → server)
126 request: channel.addMessage({
127 encoding: {
128 preencode(state, m) {
129 c.string.preencode(state, m.deviceName)
130 c.buffer.preencode(state, m.writerKey)
131 },
132 encode(state, m) {
133 c.string.encode(state, m.deviceName)
134 c.buffer.encode(state, m.writerKey)
135 },
136 decode(state) {
137 return {
138 deviceName: c.string.decode(state),
139 writerKey: c.buffer.decode(state)
140 }
141 }
142 }
143 }),
144
145 // 1: Receive mnemonics (server → client)
146 response: channel.addMessage({
147 encoding: {
148 preencode(state, m) {
149 c.string.preencode(state, m.identityMnemonic)
150 c.string.preencode(state, m.walletMnemonic)
151 c.string.preencode(state, m.serverDeviceName)
152 c.bool.preencode(state, m.approved)
153 },
154 encode(state, m) {
155 c.string.encode(state, m.identityMnemonic)
156 c.string.encode(state, m.walletMnemonic)
157 c.string.encode(state, m.serverDeviceName)
158 c.bool.encode(state, m.approved)
159 },
160 decode(state) {
161 return {
162 identityMnemonic: c.string.decode(state),
163 walletMnemonic: c.string.decode(state),
164 serverDeviceName: c.string.decode(state),
165 approved: c.bool.decode(state)
166 }
167 }
168 },
169 onmessage(msg) {
170 if (!msg.approved) {
171 reject(new Error('Pairing rejected by server'))
172 return
173 }
174
175 console.log(`✅ Received mnemonics from: ${msg.serverDeviceName}`)
176 resolve({
177 identityMnemonic: msg.identityMnemonic,
178 walletMnemonic: msg.walletMnemonic
179 })
180 }
181 })
182 }
183
184 channel.open()
185
186 // Send pairing request immediately (channel is ready after open())
187 console.log(`📤 Sending pairing request as: ${deviceName}`)
188 messages.request.send({
189 deviceName,
190 writerKey
191 })
192
193 // Timeout after 60 seconds
194 const timeout = setTimeout(() => {
195 reject(new Error('Pairing timeout - no response from server'))
196 }, 60000)
197
198 // Clear timeout when resolved
199 const originalResolve = resolve
200 resolve = (value) => {
201 clearTimeout(timeout)
202 originalResolve(value)
203 }
204 })
205}
206
207module.exports = {
208 createPairingServer,
209 createPairingClient
210}
lib/qr-svg.cjs +0 -32
……@@ -1,32 +0,0 @@
1// QR Code generator using SVG (no fs dependency)
2
3// Polyfill TextEncoder for Bare runtime
4if (typeof global.TextEncoder === 'undefined') {
5 const { TextEncoder, TextDecoder } = require('bare-encoding')
6 global.TextEncoder = TextEncoder
7 global.TextDecoder = TextDecoder
8}
9
10const QRCode = require('qrcode/lib/core/qrcode')
11const SvgRenderer = require('qrcode/lib/renderer/svg')
12
13async function generateQRCodeSVG(text, options = {}) {
14 console.log('QRCode type:', typeof QRCode)
15 console.log('QRCode.create type:', typeof QRCode?.create)
16 console.log('QRCode keys:', Object.keys(QRCode || {}))
17
18 // Create QR code data structure
19 const qrData = QRCode.create(text, options)
20
21 // Render to SVG
22 const svg = await SvgRenderer.render(qrData, {
23 ...options,
24 type: 'svg'
25 })
26
27 // Convert SVG string to data URL
28 const svgDataURL = `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`
29 return svgDataURL
30}
31
32module.exports = { generateQRCodeSVG }
lib/rate-limiter.cjs +0 -265
……@@ -1,265 +0,0 @@
1/**
2 * FILE: lib/rate-limiter.cjs
3 * STATUS: ACTIVE
4 * RUNTIME: Bare
5 * MODULE: CommonJS
6 *
7 * PURPOSE:
8 * Rate limiting middleware to prevent abuse and spam
9 *
10 * USED BY:
11 * - wallet-server.cjs (rate limiting middleware)
12 *
13 * KEY EXPORTS:
14 * - createRateLimiter() - Create rate limiter instance
15 * - checkRateLimit(req, endpoint) - Check if request is within limits
16 *
17 * RATE LIMITS:
18 * - /api/send: 10 tx per minute
19 * - /api/export-identity: 3 per hour
20 * - /api/export-wallet: 3 per hour
21 * - /api/device/add: 5 per 5 minutes
22 * - /api/device/join: 5 per 5 minutes
23 * - default: 100 per minute
24 */
25
26// Rate limit configurations (per IP address)
27const RATE_LIMITS = {
28 '/api/send': { window: 60000, max: 10 }, // 10 transactions per minute
29 '/api/export-identity': { window: 3600000, max: 3 }, // 3 per hour
30 '/api/export-wallet': { window: 3600000, max: 3 }, // 3 per hour
31 '/api/device/add': { window: 300000, max: 5 }, // 5 per 5 minutes
32 '/api/device/join': { window: 300000, max: 5 }, // 5 per 5 minutes
33 '/api/estimate-fee': { window: 60000, max: 50 }, // 50 per minute
34 '/api/transactions/sync': { window: 300000, max: 10 }, // 10 per 5 minutes
35 'default': { window: 60000, max: 100 } // 100 per minute (general)
36}
37
38/**
39 * Rate limiter class
40 * Tracks request counts per IP and endpoint
41 */
42class RateLimiter {
43 constructor() {
44 // Map: IP → Map: endpoint → [timestamps]
45 this.requests = new Map()
46 }
47
48 /**
49 * Check if request is within rate limits
50 * @param {string} ip - Client IP address
51 * @param {string} endpoint - API endpoint path
52 * @returns {Object} { allowed: boolean, remaining: number, resetIn: number, headers: Object }
53 */
54 checkLimit(ip, endpoint) {
55 // Get rate limit for this endpoint (or default)
56 const limit = RATE_LIMITS[endpoint] || RATE_LIMITS.default
57 const now = Date.now()
58
59 // Initialize IP tracking if needed
60 if (!this.requests.has(ip)) {
61 this.requests.set(ip, new Map())
62 }
63
64 const ipRequests = this.requests.get(ip)
65
66 // Initialize endpoint tracking if needed
67 if (!ipRequests.has(endpoint)) {
68 ipRequests.set(endpoint, [])
69 }
70
71 const timestamps = ipRequests.get(endpoint)
72
73 // Remove timestamps outside the time window
74 const recentRequests = timestamps.filter(ts => now - ts < limit.window)
75
76 // Check if limit exceeded
77 if (recentRequests.length >= limit.max) {
78 const oldestRequest = Math.min(...recentRequests)
79 const resetIn = limit.window - (now - oldestRequest)
80 const resetTime = Math.floor((now + resetIn) / 1000)
81
82 return {
83 allowed: false,
84 remaining: 0,
85 resetIn: Math.ceil(resetIn / 1000), // seconds
86 limit: limit.max,
87 window: Math.floor(limit.window / 1000), // seconds
88 headers: {
89 'X-RateLimit-Limit': String(limit.max),
90 'X-RateLimit-Remaining': '0',
91 'X-RateLimit-Reset': String(resetTime)
92 }
93 }
94 }
95
96 // Add current request timestamp
97 recentRequests.push(now)
98 ipRequests.set(endpoint, recentRequests)
99
100 const resetTime = Math.floor((now + limit.window) / 1000)
101
102 return {
103 allowed: true,
104 remaining: limit.max - recentRequests.length,
105 resetIn: Math.floor(limit.window / 1000), // seconds
106 limit: limit.max,
107 window: Math.floor(limit.window / 1000), // seconds
108 headers: {
109 'X-RateLimit-Limit': String(limit.max),
110 'X-RateLimit-Remaining': String(limit.max - recentRequests.length),
111 'X-RateLimit-Reset': String(resetTime)
112 }
113 }
114 }
115
116 /**
117 * Get current status for an IP/endpoint combination
118 * @param {string} ip - Client IP address
119 * @param {string} endpoint - API endpoint path
120 * @returns {Object} Current rate limit status
121 */
122 getStatus(ip, endpoint) {
123 const limit = RATE_LIMITS[endpoint] || RATE_LIMITS.default
124 const now = Date.now()
125
126 if (!this.requests.has(ip)) {
127 return {
128 remaining: limit.max,
129 limit: limit.max,
130 window: Math.floor(limit.window / 1000)
131 }
132 }
133
134 const ipRequests = this.requests.get(ip)
135 if (!ipRequests.has(endpoint)) {
136 return {
137 remaining: limit.max,
138 limit: limit.max,
139 window: Math.floor(limit.window / 1000)
140 }
141 }
142
143 const timestamps = ipRequests.get(endpoint)
144 const recentRequests = timestamps.filter(ts => now - ts < limit.window)
145
146 return {
147 remaining: Math.max(0, limit.max - recentRequests.length),
148 limit: limit.max,
149 window: Math.floor(limit.window / 1000)
150 }
151 }
152
153 /**
154 * Clean up old entries (garbage collection)
155 * Call periodically to prevent memory buildup
156 */
157 cleanup() {
158 const now = Date.now()
159 let cleaned = 0
160
161 for (const [ip, endpoints] of this.requests.entries()) {
162 for (const [endpoint, timestamps] of endpoints.entries()) {
163 const limit = RATE_LIMITS[endpoint] || RATE_LIMITS.default
164 const recentRequests = timestamps.filter(ts => now - ts < limit.window)
165
166 if (recentRequests.length === 0) {
167 endpoints.delete(endpoint)
168 cleaned++
169 } else {
170 endpoints.set(endpoint, recentRequests)
171 }
172 }
173
174 if (endpoints.size === 0) {
175 this.requests.delete(ip)
176 }
177 }
178
179 if (cleaned > 0) {
180 console.log(`🧹 Rate limiter cleanup: removed ${cleaned} stale entries`)
181 }
182 }
183}
184
185// Create global rate limiter instance
186const rateLimiter = new RateLimiter()
187
188// Cleanup every 5 minutes
189setInterval(() => rateLimiter.cleanup(), 5 * 60 * 1000)
190
191/**
192 * Extract client IP from request
193 * @param {Request} req - HTTP request
194 * @returns {string} Client IP address
195 */
196function getClientIP(req) {
197 // Check X-Forwarded-For header (if behind proxy)
198 const forwardedFor = req.headers['x-forwarded-for']
199 if (forwardedFor) {
200 // Take first IP if multiple
201 return forwardedFor.split(',')[0].trim()
202 }
203
204 // Use socket remote address
205 return req.socket.remoteAddress || '0.0.0.0'
206}
207
208/**
209 * Normalize endpoint path for rate limiting
210 * Removes query params and route params
211 * @param {string} url - Request URL
212 * @returns {string} Normalized endpoint
213 */
214function normalizeEndpoint(url) {
215 // Remove query string
216 const path = url.split('?')[0]
217
218 // Normalize parameterized routes
219 // /api/balance/testnet-bitcoin → /api/balance/:chain
220 // /api/address/testnet-bitcoin → /api/address/:chain
221 // /api/transactions/bitcoin → /api/transactions/:chain
222 // /api/device/add/abc123 → /api/device/add/:sessionId
223
224 if (path.match(/^\/api\/balance\//)) return '/api/balance/:chain'
225 if (path.match(/^\/api\/address\//)) return '/api/address/:chain'
226 if (path.match(/^\/api\/qrcode\//)) return '/api/qrcode/:chain'
227 if (path.match(/^\/api\/transactions\/[^/]+$/)) return '/api/transactions/:chain'
228 if (path.match(/^\/api\/transaction\//)) return '/api/transaction/:chain/:hash'
229 if (path.match(/^\/api\/contacts\//)) return '/api/contacts/:id'
230 if (path.match(/^\/api\/device\/add\/[^/]+$/)) return '/api/device/add/:sessionId'
231 if (path.match(/^\/api\/settings\/providers\//)) return '/api/settings/providers/:chain'
232
233 return path
234}
235
236/**
237 * Check rate limit for request
238 * @param {Request} req - HTTP request
239 * @returns {Object} Rate limit result
240 */
241function checkRateLimit(req) {
242 const ip = getClientIP(req)
243 const endpoint = normalizeEndpoint(req.url)
244
245 return rateLimiter.checkLimit(ip, endpoint)
246}
247
248/**
249 * Get rate limit status for request
250 * @param {Request} req - HTTP request
251 * @returns {Object} Current status
252 */
253function getRateLimitStatus(req) {
254 const ip = getClientIP(req)
255 const endpoint = normalizeEndpoint(req.url)
256
257 return rateLimiter.getStatus(ip, endpoint)
258}
259
260module.exports = {
261 checkRateLimit,
262 getRateLimitStatus,
263 RateLimiter,
264 RATE_LIMITS
265}
lib/secure-storage.cjs +0 -70
……@@ -1,70 +0,0 @@
1// lib/secure-storage.cjs - File-based Mnemonic Storage (Bare Runtime)
2//
3// NOTE: This is a simplified version for Bare runtime.
4// Uses file-based storage instead of OS keychain.
5// In production, consider using encrypted storage.
6
7const fs = require('fs')
8const path = require('path')
9const os = require('os')
10
11const STORAGE_DIR = path.join(os.homedir(), '.hyper-wallet')
12const MNEMONIC_FILE = path.join(STORAGE_DIR, '.mnemonic')
13
14/**
15 * Ensure storage directory exists
16 */
17function ensureStorageDir() {
18 if (!fs.existsSync(STORAGE_DIR)) {
19 fs.mkdirSync(STORAGE_DIR, { recursive: true, mode: 0o700 })
20 }
21}
22
23/**
24 * Store mnemonic in file
25 * @param {String} mnemonic - BIP39 mnemonic phrase
26 */
27async function storeMnemonic(mnemonic) {
28 ensureStorageDir()
29 fs.writeFileSync(MNEMONIC_FILE, mnemonic, { mode: 0o600 })
30}
31
32/**
33 * Retrieve mnemonic from file
34 * @returns {String|null} - Mnemonic or null if not found
35 */
36async function loadMnemonic() {
37 try {
38 if (!fs.existsSync(MNEMONIC_FILE)) {
39 return null
40 }
41 return fs.readFileSync(MNEMONIC_FILE, 'utf8').trim()
42 } catch (err) {
43 return null
44 }
45}
46
47/**
48 * Delete mnemonic file
49 */
50async function deleteMnemonic() {
51 if (fs.existsSync(MNEMONIC_FILE)) {
52 fs.unlinkSync(MNEMONIC_FILE)
53 }
54}
55
56/**
57 * Check if wallet is initialized (mnemonic file exists)
58 * @returns {Boolean}
59 */
60async function isInitialized() {
61 const mnemonic = await loadMnemonic()
62 return mnemonic !== null
63}
64
65module.exports = {
66 storeMnemonic,
67 loadMnemonic,
68 deleteMnemonic,
69 isInitialized
70}
lib/utxo-manager.cjs +0 -157
……@@ -1,157 +0,0 @@
1/**
2 * UTXO Manager - Phase 6.3
3 *
4 * Manages Unspent Transaction Outputs (UTXOs) for Bitcoin wallet.
5 * Tracks UTXOs in Autobase for multi-device synchronization.
6 */
7
8const b4a = require('b4a')
9
10/**
11 * Create UTXO key for Autobase storage
12 * Format: utxo:{chain}:{txid}:{vout}
13 */
14function createUTXOKey(chain, txid, vout) {
15 return `utxo:${chain}:${txid}:${vout}`
16}
17
18/**
19 * Store UTXO in Autobase
20 * @param {Object} utxo - UTXO data from blockchain
21 * @param {string} chain - Chain identifier (bitcoin, ethereum)
22 * @param {Function} storeUTXOFn - Backend storeUTXO function
23 */
24async function storeUTXO(utxo, chain, storeUTXOFn) {
25 const utxoData = {
26 chain,
27 txid: utxo.txid,
28 vout: utxo.vout,
29 address: utxo.address,
30 amount: utxo.value, // satoshis
31 scriptPubKey: utxo.scriptPubKey || null,
32 confirmations: utxo.confirmations || 0,
33 blockHeight: utxo.blockHeight || null,
34 spendable: true,
35 spent: false,
36 spentTxid: null,
37 timestamp: Date.now()
38 }
39
40 await storeUTXOFn(utxoData)
41 return utxoData
42}
43
44/**
45 * Mark UTXO as spent
46 * @param {string} txid - Transaction ID
47 * @param {number} vout - Output index
48 * @param {string} spentTxid - Transaction that spent this UTXO
49 * @param {string} chain - Chain identifier
50 * @param {Function} updateUTXOFn - Backend updateUTXO function
51 */
52async function markUTXOSpent(txid, vout, spentTxid, chain, updateUTXOFn) {
53 await updateUTXOFn(chain, txid, vout, {
54 spent: true,
55 spendable: false,
56 spentTxid,
57 spentAt: Date.now()
58 })
59}
60
61/**
62 * Calculate balance from UTXOs
63 * @param {Array} utxos - Array of UTXO objects
64 * @returns {Object} Balance breakdown
65 */
66function calculateBalance(utxos) {
67 const confirmed = utxos
68 .filter(u => u.confirmations >= 6 && !u.spent && u.spendable)
69 .reduce((sum, u) => sum + u.amount, 0)
70
71 const unconfirmed = utxos
72 .filter(u => u.confirmations > 0 && u.confirmations < 6 && !u.spent && u.spendable)
73 .reduce((sum, u) => sum + u.amount, 0)
74
75 const pending = utxos
76 .filter(u => u.confirmations === 0 && !u.spent && u.spendable)
77 .reduce((sum, u) => sum + u.amount, 0)
78
79 return {
80 confirmed, // 6+ confirmations
81 unconfirmed, // 1-5 confirmations
82 pending, // 0 confirmations
83 total: confirmed + unconfirmed + pending,
84 spendable: confirmed + unconfirmed // Don't include pending in spendable
85 }
86}
87
88/**
89 * Get spendable UTXOs (confirmed + unconfirmed, not spent)
90 * @param {Array} utxos - Array of UTXO objects
91 * @param {boolean} confirmedOnly - Only return confirmed UTXOs (6+ confirmations)
92 * @returns {Array} Spendable UTXOs sorted by amount (largest first)
93 */
94function getSpendableUTXOs(utxos, confirmedOnly = false) {
95 const minConfirmations = confirmedOnly ? 6 : 1
96
97 return utxos
98 .filter(u =>
99 u.confirmations >= minConfirmations &&
100 !u.spent &&
101 u.spendable
102 )
103 .sort((a, b) => b.amount - a.amount) // Largest first for coin selection
104}
105
106/**
107 * Format satoshis to BTC display
108 * @param {number} satoshis - Amount in satoshis
109 * @returns {string} Formatted BTC amount
110 */
111function formatBTC(satoshis) {
112 const btc = (satoshis / 100000000).toFixed(8)
113 return btc.replace(/\.?0+$/, '') || '0' // Remove trailing zeros
114}
115
116/**
117 * Format balance display
118 * @param {Object} balance - Balance object from calculateBalance
119 * @param {string} symbol - Currency symbol (BTC, ETH, etc.)
120 * @returns {string} Formatted balance string
121 */
122function formatBalance(balance, symbol = 'BTC') {
123 const lines = []
124
125 if (balance.confirmed > 0) {
126 lines.push(`Confirmed: ${formatBTC(balance.confirmed)} ${symbol} (${balance.confirmed.toLocaleString()} sats)`)
127 }
128
129 if (balance.unconfirmed > 0) {
130 lines.push(`Unconfirmed: ${formatBTC(balance.unconfirmed)} ${symbol} (${balance.unconfirmed.toLocaleString()} sats)`)
131 }
132
133 if (balance.pending > 0) {
134 lines.push(`Pending: ${formatBTC(balance.pending)} ${symbol} (${balance.pending.toLocaleString()} sats)`)
135 }
136
137 if (lines.length === 0) {
138 return `Balance: 0 ${symbol}`
139 }
140
141 if (balance.total !== balance.confirmed) {
142 lines.push(`─────────────────────────────────────`)
143 lines.push(`Total: ${formatBTC(balance.total)} ${symbol} (${balance.total.toLocaleString()} sats)`)
144 }
145
146 return lines.join('\n')
147}
148
149module.exports = {
150 createUTXOKey,
151 storeUTXO,
152 markUTXOSpent,
153 calculateBalance,
154 getSpendableUTXOs,
155 formatBTC,
156 formatBalance
157}
lib/wallet-storage.cjs +0 -2230
……@@ -1,2230 +0,0 @@
1// lib/wallet-context-autobase.cjs - Multi-device storage with Autobase
2
3require('bare-node-runtime/global')
4const path = require('bare-path')
5const os = require('bare-os')
6const fs = require('bare-fs')
7const Corestore = require('corestore')
8const Hyperbee = require('hyperbee')
9const Autobase = require('autobase')
10const Hyperswarm = require('hyperswarm')
11const crypto = require('hypercore-crypto')
12const b4a = require('b4a')
13const IdentityKey = require('keet-identity-key')
14const { createPairingServer, createPairingClient } = require('./pairing-protocol.cjs')
15const { loadWalletMnemonic, initializeWDK } = require('./wdk-integration.cjs')
16const { fetchTransactions, fetchTokenTransactions } = require('./blockchain-providers.cjs')
17const { getConfig } = require('./config.cjs')
18
19// Storage configuration
20const STORAGE_DIR = path.join(os.homedir(), '.hyper-wallet', 'storage')
21const DEVICES_FILE = path.join(os.homedir(), '.hyper-wallet', '.devices')
22const PENDING_FILE = path.join(os.homedir(), '.hyper-wallet', '.pending')
23
24// Singleton instances
25let store = null
26let autobase = null
27let txHistoryDb = null
28let swarm = null
29let identityKeyPair = null
30let connections = new Set() // Track active connections
31
32// Initialization lock to prevent concurrent initialization
33let initializationPromise = null
34
35// WDK singleton instances (Phase 6.1)
36let wdkInstance = null
37let walletManagers = {} // { bitcoin, ethereum }
38
39/**
40 * Apply function for Autobase
41 * Processes operations from all input cores and builds Hyperbee view
42 *
43 * Pattern based on holepunchto/autobase examples:
44 * - nodes: array of node objects from all input cores
45 * - view: already a Hyperbee instance (created by open())
46 * - base: the Autobase instance (for operations like addWriter)
47 * - IMPORTANT: Must parse JSON manually (no valueEncoding in Autobase)
48 */
49async function apply(nodes, view, base) {
50 // Use batch for efficiency when processing multiple operations
51 const b = view.batch({ update: false })
52
53 for (const node of nodes) {
54 // Parse JSON manually (autobase doesn't use valueEncoding)
55 const value = JSON.parse(node.value)
56
57 // Handle writer management operations
58 if (value.addWriter) {
59 // Add writer to Autobase inputs using the host.addWriter() API
60 const writerKey = b4a.from(value.addWriter, 'hex')
61 await base.addWriter(writerKey, { indexer: true })
62 continue
63 }
64
65 if (value.removeWriter) {
66 // Writer removal marker
67 // Note: Autobase doesn't actually support removing inputs
68 // This is just a marker that the device has been revoked
69 // The device list manages the actual removal
70 continue
71 }
72
73 // Handle transaction operations
74 if (value.type === 'put-tx') {
75 // Store value as JSON string
76 await b.put(value.key, JSON.stringify(value.value))
77 } else if (value.type === 'del-tx') {
78 await b.del(value.key)
79 }
80
81 // Handle address operations (Phase 6.2)
82 if (value.type === 'put-address') {
83 await b.put(value.key, JSON.stringify(value.value))
84 } else if (value.type === 'del-address') {
85 await b.del(value.key)
86 }
87
88 // Handle UTXO operations (Phase 6.3)
89 if (value.type === 'put-utxo') {
90 await b.put(value.key, JSON.stringify(value.value))
91 } else if (value.type === 'del-utxo') {
92 await b.del(value.key)
93 }
94
95 // Handle transaction storage (Phase 6.6)
96 if (value.type === 'store-transaction') {
97 await b.put(value.key, JSON.stringify(value.value))
98 } else if (value.type === 'delete-transaction') {
99 await b.del(value.key)
100 }
101
102 // Handle contact storage
103 if (value.type === 'put-contact') {
104 await b.put(value.key, JSON.stringify(value.value))
105 } else if (value.type === 'del-contact') {
106 await b.del(value.key)
107 }
108 }
109
110 await b.flush()
111}
112
113/**
114 * Initialize storage with Autobase multi-writer support
115 * @param {Object} identity - keet-identity-key instance
116 */
117async function initializeStorage(identity) {
118 // Return existing instance if already initialized
119 if (store && autobase && txHistoryDb) {
120 return { store, autobase, txHistoryDb, swarm }
121 }
122
123 // If initialization is in progress, wait for it
124 if (initializationPromise) {
125 console.log('⏳ Storage initialization in progress, waiting...')
126 return await initializationPromise
127 }
128
129 // Start new initialization
130 console.log('🔧 Initializing storage (singleton)')
131 initializationPromise = _initializeStorage(identity)
132
133 try {
134 const result = await initializationPromise
135 return result
136 } finally {
137 // Clear the promise after initialization completes (success or failure)
138 initializationPromise = null
139 }
140}
141
142/**
143 * Internal storage initialization (wrapped by singleton lock)
144 */
145async function _initializeStorage(identity) {
146 // Store identity keypair for attestation
147 identityKeyPair = identity.identityKeyPair
148
149 // Initialize Corestore
150 store = new Corestore(STORAGE_DIR)
151 await store.ready()
152
153 // View class for Autobase
154 // Pattern from autobase-example
155 class WalletView {
156 open(store) {
157 const core = store.get('view')
158 // Use binary encoding - we'll handle JSON manually
159 return new Hyperbee(core, {
160 keyEncoding: 'utf-8',
161 valueEncoding: 'binary'
162 })
163 }
164
165 async apply(nodes, view, base) {
166 return apply(nodes, view, base)
167 }
168
169 close(view) {
170 return view.close()
171 }
172 }
173
174 // Get or create namespace for this autobase
175 const namespace = await Autobase.getLocalKey(store)
176
177 // Initialize Autobase with Corestore
178 // First param: Namespaced corestore
179 // Second param: bootstrap key (null for new, or existing autobase key)
180 // Third param: View instance with open/apply/close methods
181 // Note: No valueEncoding - handle JSON manually
182 autobase = new Autobase(store.namespace(namespace), null, new WalletView())
183
184 await autobase.ready()
185
186 // Add existing devices as writers by appending operations
187 // Autobase will process these operations and add the writers automatically
188 const devices = loadDevices()
189 for (const device of devices) {
190 // Skip if this is the local writer (already added automatically)
191 if (!b4a.equals(device.key, autobase.local.key)) {
192 // Append addWriter operation - Autobase processes this internally
193 const op = { addWriter: b4a.toString(device.key, 'hex') }
194 await autobase.append(JSON.stringify(op))
195 }
196 }
197
198 // Wait for operations to be processed
199 if (devices.length > 1) {
200 await autobase.update()
201 }
202
203 // Get view as Hyperbee (already created by open())
204 txHistoryDb = autobase.view
205
206 // Initialize Hyperswarm for P2P
207 swarm = new Hyperswarm()
208
209 // Track connections (Phase 6.7: Auto-replication enabled)
210 swarm.on('connection', (conn, info) => {
211 connections.add(conn)
212 console.log(`📡 Connected to peer (${connections.size} connection${connections.size !== 1 ? 's' : ''})`)
213
214 // Auto-replicate on all connections (Phase 6.7)
215 // This enables real-time multi-device sync
216 store.replicate(conn)
217
218 // Handle disconnection
219 conn.on('close', () => {
220 connections.delete(conn)
221 console.log(`📡 Peer disconnected (${connections.size} connection${connections.size !== 1 ? 's' : ''})`)
222 })
223 })
224
225 // Join discovery key for this wallet
226 const discoveryKey = crypto.discoveryKey(identityKeyPair.publicKey)
227 const discovery = swarm.join(discoveryKey, { server: true, client: true })
228
229 // Flush in background - don't block initialization on DHT announcement
230 discovery.flushed().then(() => {
231 console.log('📡 DHT discovery announced')
232 }).catch(err => {
233 console.warn('⚠️ DHT discovery flush failed:', err.message)
234 })
235
236 console.log('✅ Storage initialized successfully')
237 return { store, autobase, txHistoryDb, swarm }
238}
239
240/**
241 * Store a transaction in Autobase
242 * Operations are appended to local input core and merged by Autobase
243 */
244async function storeTx(tx) {
245 if (!autobase) {
246 throw new Error('Storage not initialized')
247 }
248
249 // Append operation to Autobase
250 // Note: No valueEncoding in Autobase - must JSON.stringify manually
251 const op = {
252 type: 'put-tx',
253 key: `tx:${tx.timestamp}:${tx.hash}`,
254 value: {
255 hash: tx.hash,
256 chain: tx.chain,
257 to: tx.to,
258 amount: tx.amount,
259 fee: tx.fee,
260 timestamp: tx.timestamp,
261 status: tx.status || 'pending'
262 }
263 }
264
265 await autobase.append(JSON.stringify(op))
266
267 // Wait for view to update
268 await new Promise(resolve => setTimeout(resolve, 100))
269}
270
271/**
272 * Get transaction history from Hyperbee view
273 */
274async function getTxHistory(chain = null) {
275 if (!txHistoryDb) {
276 throw new Error('Storage not initialized')
277 }
278
279 const transactions = []
280
281 for await (const entry of txHistoryDb.createReadStream()) {
282 // Parse JSON manually (binary encoding)
283 const tx = JSON.parse(entry.value)
284
285 if (chain && tx.chain !== chain) {
286 continue
287 }
288
289 transactions.push(tx)
290 }
291
292 // Sort by timestamp (newest first)
293 transactions.sort((a, b) => b.timestamp - a.timestamp)
294
295 return transactions
296}
297
298/**
299 * Get specific transaction by hash
300 */
301async function getTx(hash) {
302 if (!txHistoryDb) {
303 throw new Error('Storage not initialized')
304 }
305
306 for await (const entry of txHistoryDb.createReadStream()) {
307 // Parse JSON manually (binary encoding)
308 const tx = JSON.parse(entry.value)
309 if (tx.hash === hash) {
310 return tx
311 }
312 }
313
314 return null
315}
316
317/**
318 * Update transaction status
319 */
320async function updateTxStatus(hash, status) {
321 const tx = await getTx(hash)
322 if (!tx) return
323
324 tx.status = status
325 await storeTx(tx)
326}
327
328// ============================================================================
329// WDK WALLET MANAGEMENT (Phase 6.2)
330// ============================================================================
331
332/**
333 * Initialize WDK wallets singleton
334 * @param {Object} config - Configuration { testnet: boolean }
335 * @returns {Object} - { wdk } - WDK instance with registered wallets
336 */
337async function initializeWallets(config = { testnet: true }) {
338 if (wdkInstance) {
339 return { wdk: wdkInstance }
340 }
341
342 const walletMnemonic = loadWalletMnemonic()
343 if (!walletMnemonic) {
344 throw new Error('Wallet mnemonic not found. Run: bare hyper-wallet-autobase.cjs init')
345 }
346
347 const { wdk } = await initializeWDK(walletMnemonic, config)
348
349 wdkInstance = wdk
350 return { wdk: wdkInstance }
351}
352
353/**
354 * Store address in Autobase
355 * @param {Object} addressData - { chain, network, address, path, index }
356 */
357async function storeAddress(addressData) {
358 if (!autobase) {
359 throw new Error('Storage not initialized')
360 }
361
362 const op = {
363 type: 'put-address',
364 key: `address:${addressData.chain}:${addressData.index}`,
365 value: {
366 chain: addressData.chain,
367 network: addressData.network,
368 address: addressData.address,
369 path: addressData.path,
370 index: addressData.index,
371 label: addressData.label || '', // Pet-name/label for address
372 createdAt: addressData.createdAt || Date.now(),
373 device: getCurrentDevice()?.name || 'unknown',
374 isArchived: addressData.isArchived || false, // For hiding old addresses
375 isCurrent: addressData.isCurrent || false // Mark current receiving address
376 }
377 }
378
379 await autobase.append(JSON.stringify(op))
380
381 // Wait for view to update
382 await new Promise(resolve => setTimeout(resolve, 100))
383}
384
385/**
386 * Get all addresses for a chain
387 * @param {string} chain - 'bitcoin' or 'ethereum' or 'all'
388 * @returns {Array} - Array of address objects sorted by index
389 */
390async function getAddresses(chain) {
391 if (!txHistoryDb) {
392 throw new Error('Storage not initialized')
393 }
394
395 const addresses = []
396 const prefix = chain === 'all' ? 'address:' : `address:${chain}:`
397
398 for await (const entry of txHistoryDb.createReadStream()) {
399 if (entry.key.startsWith(prefix)) {
400 addresses.push(JSON.parse(entry.value))
401 }
402 }
403
404 // For Ethereum, also include ERC-20 token addresses (they share the same address space)
405 // When querying 'testnet-ethereum', also return 'testnet-ethereum-usdt' addresses
406 if (!chain.includes('all') && (chain.includes('ethereum') && !chain.includes('-usdt'))) {
407 const tokenPrefix = `${prefix}usdt:`
408 for await (const entry of txHistoryDb.createReadStream()) {
409 if (entry.key.startsWith(tokenPrefix)) {
410 const addr = JSON.parse(entry.value)
411 // Check if we already have this index (don't duplicate)
412 if (!addresses.find(a => a.index === addr.index)) {
413 addresses.push(addr)
414 }
415 }
416 }
417 }
418
419 return addresses.sort((a, b) => a.index - b.index)
420}
421
422/**
423 * Get next address index for a chain
424 * @param {string} chain - 'bitcoin' or 'ethereum'
425 * @returns {number} - Next index to use (0 if no addresses exist)
426 */
427async function getNextAddressIndex(chain) {
428 const addresses = await getAddresses(chain)
429 if (addresses.length === 0) return 0
430 return Math.max(...addresses.map(a => a.index)) + 1
431}
432
433/**
434 * Get current (latest) address for a chain
435 * @param {string} chain - 'bitcoin' or 'ethereum'
436 * @returns {Object|null} - Latest address or null if none exist
437 */
438async function getCurrentAddress(chain) {
439 let addresses = await getAddresses(chain)
440
441 // For ERC-20 tokens (like USDT), fall back to base Ethereum addresses
442 // since they share the same address space
443 if (addresses.length === 0 && chain.includes('-usdt')) {
444 const baseChain = chain.replace('-usdt', '')
445 addresses = await getAddresses(baseChain)
446 }
447
448 if (addresses.length === 0) return null
449
450 // Find address marked as current, or return latest
451 const current = addresses.find(a => a.isCurrent)
452 return current || addresses[addresses.length - 1]
453}
454
455/**
456 * Update address label
457 * @param {string} chain - 'bitcoin' or 'ethereum'
458 * @param {number} index - Address index
459 * @param {string} label - New label/pet-name
460 */
461async function updateAddressLabel(chain, index, label) {
462 if (!autobase) {
463 throw new Error('Storage not initialized')
464 }
465
466 // Get existing address
467 const addresses = await getAddresses(chain)
468 const existing = addresses.find(a => a.index === index)
469 if (!existing) {
470 throw new Error(`Address not found: ${chain}:${index}`)
471 }
472
473 // Update with new label
474 await storeAddress({
475 ...existing,
476 label
477 })
478
479 return { ...existing, label }
480}
481
482/**
483 * Set address as current receiving address
484 * @param {string} chain - 'bitcoin' or 'ethereum'
485 * @param {number} index - Address index to set as current
486 */
487async function setCurrentAddress(chain, index) {
488 if (!autobase) {
489 throw new Error('Storage not initialized')
490 }
491
492 const addresses = await getAddresses(chain)
493
494 // Find the address to set as current
495 const targetAddr = addresses.find(a => a.index === index)
496 if (!targetAddr) {
497 throw new Error(`Address not found: ${chain}:${index}`)
498 }
499
500 // Update all addresses: unset current on others, set on target
501 for (const addr of addresses) {
502 if (addr.index === index) {
503 await storeAddress({ ...addr, isCurrent: true })
504 } else if (addr.isCurrent) {
505 await storeAddress({ ...addr, isCurrent: false })
506 }
507 }
508
509 return { ...targetAddr, isCurrent: true }
510}
511
512/**
513 * Archive address (hide from UI)
514 * @param {string} chain - 'bitcoin' or 'ethereum'
515 * @param {number} index - Address index
516 */
517async function archiveAddress(chain, index) {
518 if (!autobase) {
519 throw new Error('Storage not initialized')
520 }
521
522 const addresses = await getAddresses(chain)
523 const existing = addresses.find(a => a.index === index)
524 if (!existing) {
525 throw new Error(`Address not found: ${chain}:${index}`)
526 }
527
528 await storeAddress({
529 ...existing,
530 isArchived: true
531 })
532
533 return { ...existing, isArchived: true }
534}
535
536// ============================================================================
537// UTXO MANAGEMENT (Phase 6.3)
538// ============================================================================
539
540/**
541 * Store UTXO in Autobase
542 * @param {Object} utxoData - { chain, txid, vout, address, amount, ... }
543 */
544async function storeUTXO(utxoData) {
545 if (!autobase) {
546 throw new Error('Storage not initialized')
547 }
548
549 const op = {
550 type: 'put-utxo',
551 key: `utxo:${utxoData.chain}:${utxoData.txid}:${utxoData.vout}`,
552 value: utxoData
553 }
554
555 await autobase.append(JSON.stringify(op))
556
557 // Wait for view to update
558 await new Promise(resolve => setTimeout(resolve, 100))
559}
560
561/**
562 * Get all UTXOs for a chain
563 * @param {string} chain - 'bitcoin' or 'ethereum' or 'all'
564 * @returns {Array} - Array of UTXO objects
565 */
566async function getUTXOs(chain) {
567 if (!txHistoryDb) {
568 throw new Error('Storage not initialized')
569 }
570
571 const utxos = []
572 const prefix = chain === 'all' ? 'utxo:' : `utxo:${chain}:`
573
574 for await (const entry of txHistoryDb.createReadStream()) {
575 if (entry.key.startsWith(prefix)) {
576 utxos.push(JSON.parse(entry.value))
577 }
578 }
579
580 return utxos
581}
582
583/**
584 * Update UTXO (e.g., mark as spent)
585 * @param {string} chain - Chain identifier
586 * @param {string} txid - Transaction ID
587 * @param {number} vout - Output index
588 * @param {Object} updates - Fields to update
589 */
590async function updateUTXO(chain, txid, vout, updates) {
591 if (!autobase) {
592 throw new Error('Storage not initialized')
593 }
594
595 // Get existing UTXO
596 const key = `utxo:${chain}:${txid}:${vout}`
597 const existing = await txHistoryDb.get(key)
598
599 if (!existing) {
600 throw new Error(`UTXO not found: ${key}`)
601 }
602
603 const utxoData = JSON.parse(existing.value)
604
605 // Merge updates
606 const updated = {
607 ...utxoData,
608 ...updates
609 }
610
611 const op = {
612 type: 'put-utxo',
613 key,
614 value: updated
615 }
616
617 await autobase.append(JSON.stringify(op))
618
619 // Wait for view to update
620 await new Promise(resolve => setTimeout(resolve, 100))
621}
622
623/**
624 * Delete UTXO from storage
625 * @param {string} chain - Chain identifier
626 * @param {string} txid - Transaction ID
627 * @param {number} vout - Output index
628 */
629async function deleteUTXO(chain, txid, vout) {
630 if (!autobase) {
631 throw new Error('Storage not initialized')
632 }
633
634 const op = {
635 type: 'del-utxo',
636 key: `utxo:${chain}:${txid}:${vout}`
637 }
638
639 await autobase.append(JSON.stringify(op))
640
641 // Wait for view to update
642 await new Promise(resolve => setTimeout(resolve, 100))
643}
644
645// ============================================================================
646// TRANSACTION STORAGE (Phase 6.6)
647// ============================================================================
648
649/**
650 * Store a transaction in Autobase
651 * @param {Object} tx - Transaction details
652 * @param {string} tx.hash - Transaction ID
653 * @param {string} tx.chain - Chain (bitcoin, ethereum, etc)
654 * @param {string} tx.direction - Direction (sent, received)
655 * @param {string} tx.from - From address
656 * @param {string} tx.to - To address
657 * @param {bigint|number} tx.amount - Amount in smallest unit
658 * @param {bigint|number} tx.fee - Fee in smallest unit
659 * @param {string} tx.status - Status (pending, confirmed, failed)
660 * @param {number} tx.timestamp - Unix timestamp
661 * @param {number} tx.blockHeight - Block height (if confirmed)
662 */
663async function storeTransaction(tx) {
664 if (!autobase) {
665 throw new Error('Storage not initialized')
666 }
667
668 const transaction = {
669 hash: tx.hash,
670 chain: tx.chain,
671 network: tx.network || 'testnet', // Add network field (testnet/mainnet)
672 direction: tx.direction,
673 from: tx.from,
674 to: tx.to,
675 amount: typeof tx.amount === 'string' ? tx.amount : String(tx.amount), // Ensure string
676 fee: typeof tx.fee === 'string' ? tx.fee : String(tx.fee || 0),
677 status: tx.status || 'pending',
678 timestamp: tx.timestamp || Date.now(),
679 blockHeight: tx.blockHeight || null
680 }
681
682 const op = {
683 type: 'store-transaction',
684 key: `tx:${tx.chain}:${tx.hash}`,
685 value: transaction
686 }
687
688 await autobase.append(JSON.stringify(op))
689
690 // Wait for view to update
691 await new Promise(resolve => setTimeout(resolve, 100))
692}
693
694/**
695 * Get all transactions for a chain
696 * @param {string} chain - Chain name (bitcoin, ethereum, all)
697 * @returns {Promise<Array>} - Array of transactions sorted by timestamp (newest first)
698 */
699async function getTransactions(chain = 'all') {
700 if (!txHistoryDb) {
701 throw new Error('Storage not initialized')
702 }
703
704 const transactions = []
705 const prefix = chain === 'all' ? 'tx:' : `tx:${chain}:`
706
707 for await (const data of txHistoryDb.createReadStream({ gte: prefix, lt: prefix + '\xff' })) {
708 try {
709 const tx = JSON.parse(data.value)
710 // Convert string amounts back to numbers for display
711 tx.amount = BigInt(tx.amount)
712 tx.fee = tx.fee ? BigInt(tx.fee) : 0n
713 transactions.push(tx)
714 } catch (err) {
715 console.error('Error parsing transaction:', err.message)
716 }
717 }
718
719 // Sort by timestamp, newest first
720 transactions.sort((a, b) => b.timestamp - a.timestamp)
721
722 return transactions
723}
724
725/**
726 * Get a specific transaction by hash
727 * @param {string} chain - Chain name
728 * @param {string} hash - Transaction hash
729 * @returns {Promise<Object|null>} - Transaction object or null
730 */
731async function getTransaction(chain, hash) {
732 if (!txHistoryDb) {
733 throw new Error('Storage not initialized')
734 }
735
736 try {
737 const data = await txHistoryDb.get(`tx:${chain}:${hash}`)
738 if (!data) return null
739 const tx = JSON.parse(data.value)
740
741 // Handle case where tx or its fields might be null/undefined
742 if (!tx) {
743 return null
744 }
745
746 // Safely convert amount and fee to BigInt
747 tx.amount = tx.amount ? BigInt(tx.amount) : 0n
748 tx.fee = tx.fee ? BigInt(tx.fee) : 0n
749 return tx
750 } catch (err) {
751 if (err.code === 'LEVEL_NOT_FOUND') {
752 return null
753 }
754 // Handle corrupted/malformed transaction data - treat as not found
755 // The corrupted entry will be overwritten by the new correct entry
756 if (err instanceof SyntaxError) {
757 return null
758 }
759 throw err
760 }
761}
762
763/**
764 * Delete transaction (for repair/cleanup)
765 * @param {string} chain - Chain name
766 * @param {string} hash - Transaction hash
767 */
768async function deleteTransaction(chain, hash) {
769 if (!autobase) {
770 throw new Error('Storage not initialized')
771 }
772
773 const op = {
774 type: 'delete-transaction',
775 key: `tx:${chain}:${hash}`
776 }
777
778 await autobase.append(JSON.stringify(op))
779
780 // Wait longer for view to update and propagate delete
781 await new Promise(resolve => setTimeout(resolve, 500))
782}
783
784/**
785 * Update transaction status (Phase 6.7)
786 * @param {string} chain - Chain name
787 * @param {string} hash - Transaction hash
788 * @param {Object} updates - Fields to update (status, blockHeight, confirmations)
789 */
790async function updateTransactionStatus(chain, hash, updates) {
791 if (!autobase) {
792 throw new Error('Storage not initialized')
793 }
794
795 // Get existing transaction
796 const existing = await getTransaction(chain, hash)
797 if (!existing) {
798 throw new Error('Transaction not found')
799 }
800
801 // Merge updates, ensuring BigInt values are converted to strings
802 const updated = {
803 hash: existing.hash,
804 chain: existing.chain,
805 direction: existing.direction,
806 from: existing.from,
807 to: existing.to,
808 amount: existing.amount.toString(),
809 fee: existing.fee ? existing.fee.toString() : '0',
810 status: updates.status !== undefined ? updates.status : existing.status,
811 timestamp: existing.timestamp,
812 blockHeight: updates.blockHeight !== undefined ? updates.blockHeight : existing.blockHeight
813 }
814
815 const op = {
816 type: 'store-transaction',
817 key: `tx:${chain}:${hash}`,
818 value: updated
819 }
820
821 await autobase.append(JSON.stringify(op))
822
823 // Wait for view to update
824 await new Promise(resolve => setTimeout(resolve, 100))
825}
826
827/**
828 * Sync transactions from WDK to local Hyperbee storage
829 * Fetches transaction history using Electrum for Bitcoin and provider for Ethereum
830 * @param {Object} wdk - WDK instance
831 * @param {Object} config - Configuration with chains to sync
832 * @returns {Promise<Object>} - Sync results { synced, errors }
833 */
834async function syncTransactionsFromWDK(wdk, config = {}) {
835 if (!wdk || !txHistoryDb) {
836 throw new Error('WDK or storage not initialized')
837 }
838
839 const chains = config.chains || ['bitcoin', 'ethereum']
840 const results = {
841 synced: 0,
842 errors: [],
843 chains: {}
844 }
845
846 for (const chain of chains) {
847 try {
848 // Determine base chain for WDK (ethereum-usdt -> ethereum)
849 const baseChain = chain.replace(/-usdt$/, '')
850
851 // Get current address index from storage
852 // For USDT, check both ethereum-usdt and ethereum (they share addresses)
853 let currentAddr = await getCurrentAddress(`${config.network ? config.network + '-' : ''}${chain}`)
854 if (!currentAddr && chain.includes('usdt')) {
855 // Try base chain if token chain not found
856 currentAddr = await getCurrentAddress(`${config.network ? config.network + '-' : ''}${baseChain}`)
857 }
858 const accountIndex = currentAddr ? currentAddr.index : 0
859
860 // Get WDK account for this chain at the correct index
861 const account = await wdk.getAccount(baseChain, accountIndex)
862 if (!account) {
863 results.errors.push(`No account found for chain: ${chain}`)
864 continue
865 }
866
867 // Get current address
868 const currentAddress = await account.getAddress()
869
870 let chainSynced = 0
871 let chainSkipped = 0
872 let transactions = []
873
874 // Chain-specific transaction fetching
875 if (chain === 'bitcoin') {
876 // Bitcoin: Use Electrum client directly
877 if (!account._electrumClient || typeof account._electrumClient.blockchainScripthash_getHistory !== 'function') {
878 // Electrum client not available or method doesn't exist
879 // TODO: Use blockchain explorer API (Blockstream) instead
880 results.errors.push(`${chain}: Electrum client unavailable - Bitcoin transaction sync temporarily disabled`)
881 continue
882 }
883
884 // Get script hash
885 const scriptHash = await account._getScriptHash()
886
887 // Fetch transaction history from Electrum
888 const history = await account._electrumClient.blockchainScripthash_getHistory(scriptHash)
889
890 // Process each transaction
891 for (const item of history || []) {
892 const existing = await getTransaction(chain, item.tx_hash)
893 if (existing) {
894 chainSkipped++
895 continue
896 }
897
898 // Fetch full transaction details
899 const txHex = await account._electrumClient.blockchainTransaction_get(item.tx_hash, true)
900
901 // Determine direction and amount (simplified - would need full parsing)
902 const direction = item.height > 0 ? 'received' : 'sent'
903
904 const tx = {
905 hash: item.tx_hash,
906 chain,
907 network: config.network || 'testnet',
908 direction,
909 from: direction === 'received' ? '(external)' : currentAddress,
910 to: direction === 'received' ? currentAddress : '(unknown)',
911 amount: '0', // Would need to parse tx to get amount
912 fee: txHex.fee ? String(txHex.fee) : '0',
913 status: item.height > 0 ? 'confirmed' : 'pending',
914 timestamp: Date.now(),
915 blockHeight: item.height > 0 ? item.height : null
916 }
917
918 await storeTransaction(tx)
919 chainSynced++
920 results.synced++
921 }
922 } else if (baseChain === 'ethereum') {
923 // Ethereum (including USDT): Fetch transactions from blockchain provider
924 try {
925 const config = getConfig()
926 const network = config.network || 'testnet'
927
928 // Determine if this is a token (USDT) or native ETH
929 const isToken = chain.includes('-usdt')
930 let providerTxs = []
931
932 if (isToken) {
933 // Fetch ERC-20 token transactions (USDT)
934 console.log(` 📡 Fetching token transactions for ${chain}...`)
935 // USDT contract address on Sepolia testnet
936 const USDT_CONTRACT = network === 'testnet'
937 ? '0x7169D38820dfd117C3FA1f22a697dBA58d90BA06'
938 : null // TODO: Add mainnet USDT contract
939 providerTxs = await fetchTokenTransactions(currentAddress, network, config, USDT_CONTRACT)
940 } else {
941 // Fetch native ETH transactions
942 console.log(` 📡 Fetching ETH transactions for ${chain}...`)
943 providerTxs = await fetchTransactions(baseChain, currentAddress, network, config)
944 }
945
946 for (const tx of providerTxs) {
947 // Check if already stored
948 const existing = await getTransaction(chain, tx.hash)
949 if (existing) {
950 chainSkipped++
951 continue
952 }
953
954 // Determine direction
955 const direction = tx.to.toLowerCase() === currentAddress.toLowerCase() ? 'received' : 'sent'
956
957 // Store transaction
958 const storedTx = {
959 hash: tx.hash,
960 chain,
961 network,
962 direction,
963 from: tx.from,
964 to: tx.to,
965 amount: tx.value,
966 fee: tx.fee,
967 status: tx.status,
968 timestamp: tx.timestamp * 1000, // Convert to milliseconds
969 blockHeight: tx.blockNumber
970 }
971
972 await storeTransaction(storedTx)
973 chainSynced++
974 results.synced++
975 }
976 } catch (err) {
977 console.error(`Error fetching ${chain} transactions:`, err.message)
978 results.errors.push(`${chain}: ${err.message}`)
979 }
980 }
981
982 results.chains[chain] = {
983 synced: chainSynced,
984 skipped: chainSkipped
985 }
986
987 } catch (err) {
988 console.error(`Error syncing ${chain} transactions:`, err.message)
989 results.errors.push(`${chain}: ${err.message}`)
990 }
991 }
992
993 return results
994}
995
996// ============================================================================
997// DEVICE MANAGEMENT
998// ============================================================================
999
1000/**
1001 * Load device list from file
1002 */
1003function loadDevices() {
1004 if (!fs.existsSync(DEVICES_FILE)) {
1005 return []
1006 }
1007
1008 const data = fs.readFileSync(DEVICES_FILE, 'utf8')
1009 const devices = JSON.parse(data)
1010
1011 // Convert hex strings back to Buffers
1012 return devices.map(d => ({
1013 ...d,
1014 key: b4a.from(d.key, 'hex'),
1015 proof: d.proof ? b4a.from(d.proof, 'hex') : null
1016 }))
1017}
1018
1019/**
1020 * Save device list to file
1021 */
1022function saveDevices(devices) {
1023 const dir = path.dirname(DEVICES_FILE)
1024 if (!fs.existsSync(dir)) {
1025 fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
1026 }
1027
1028 // Convert Buffers to hex strings for JSON
1029 const serializable = devices.map(d => ({
1030 ...d,
1031 key: b4a.toString(d.key, 'hex'),
1032 proof: d.proof ? b4a.toString(d.proof, 'hex') : null
1033 }))
1034
1035 fs.writeFileSync(DEVICES_FILE, JSON.stringify(serializable, null, 2), { mode: 0o600 })
1036}
1037
1038// ============================================================================
1039// CONTACT MANAGEMENT
1040// ============================================================================
1041
1042/**
1043 * Add a contact to the address book
1044 * @param {Object} contact - Contact details
1045 * @param {string} contact.address - Blockchain address
1046 * @param {string} contact.chain - Chain name (bitcoin, ethereum)
1047 * @param {string} contact.name - Display name
1048 * @param {string} [contact.notes] - Optional notes
1049 * @returns {Promise<Object>} - Created contact with ID
1050 */
1051async function addContact({ address, chain, name, notes = '' }) {
1052 if (!autobase) {
1053 throw new Error('Storage not initialized')
1054 }
1055
1056 // Generate unique ID for contact
1057 const id = crypto.randomBytes(16).toString('hex')
1058
1059 const contact = {
1060 id,
1061 address,
1062 chain,
1063 name,
1064 notes,
1065 addedAt: Date.now(),
1066 addedBy: getCurrentDevice()?.key ? b4a.toString(getCurrentDevice().key, 'hex').slice(0, 16) : 'unknown',
1067 lastUsedAt: null,
1068 metadata: {
1069 favorite: false,
1070 category: null
1071 }
1072 }
1073
1074 const op = {
1075 type: 'put-contact',
1076 key: `contact:${id}`,
1077 value: contact
1078 }
1079
1080 await autobase.append(JSON.stringify(op))
1081
1082 // Wait for view to update
1083 await new Promise(resolve => setTimeout(resolve, 100))
1084
1085 return contact
1086}
1087
1088/**
1089 * Get all contacts, optionally filtered by chain
1090 * @param {string} [chain] - Chain name to filter by (optional)
1091 * @returns {Promise<Array>} - Array of contacts sorted by name
1092 */
1093/**
1094 * Migrate a contact to new multi-address format with IDs
1095 * @param {Object} contact - Contact to migrate
1096 * @returns {Object} - Migrated contact
1097 */
1098function migrateContactFormat(contact) {
1099 // Already in new format with proper IDs - check if ALL addresses have IDs
1100 if (contact.addresses && Array.isArray(contact.addresses)) {
1101 // Check if any address is missing an ID
1102 const needsMigration = contact.addresses.some(addr => !addr.id)
1103
1104 if (!needsMigration) {
1105 // All addresses have IDs, no migration needed
1106 return contact
1107 }
1108
1109 // Some addresses missing IDs - add them
1110 const migratedAddresses = contact.addresses.map(addr => {
1111 if (!addr.id) {
1112 return {
1113 ...addr,
1114 id: crypto.randomBytes(8).toString('hex'),
1115 network: addr.network || getConfig().network || 'testnet',
1116 label: addr.label || '',
1117 addedAt: addr.addedAt || Date.now()
1118 }
1119 }
1120 return addr
1121 })
1122
1123 return {
1124 ...contact,
1125 addresses: migratedAddresses
1126 }
1127 }
1128
1129 // Old single-address format - migrate to multi-address
1130 if (contact.address && contact.chain) {
1131 const config = getConfig()
1132 const defaultNetwork = config.network || 'testnet'
1133
1134 return {
1135 ...contact,
1136 addresses: [{
1137 id: crypto.randomBytes(8).toString('hex'),
1138 chain: contact.chain,
1139 address: contact.address,
1140 network: contact.network || defaultNetwork,
1141 label: '',
1142 addedAt: contact.addedAt || Date.now()
1143 }],
1144 // Remove old fields
1145 address: undefined,
1146 chain: undefined
1147 }
1148 }
1149
1150 // Contact with no addresses - return as is
1151 return contact
1152}
1153
1154async function getContacts(chain = null) {
1155 if (!txHistoryDb) {
1156 throw new Error('Storage not initialized')
1157 }
1158
1159 const contacts = []
1160 const prefix = 'contact:'
1161
1162 for await (const data of txHistoryDb.createReadStream({ gte: prefix, lt: prefix + '\xff' })) {
1163 try {
1164 let contact = JSON.parse(data.value)
1165
1166 // Migrate contact format if needed
1167 contact = migrateContactFormat(contact)
1168
1169 // Filter by chain if specified (check both old and new format)
1170 if (chain) {
1171 // New format: check if any address matches chain
1172 if (contact.addresses && Array.isArray(contact.addresses)) {
1173 const hasChain = contact.addresses.some(addr => addr.chain === chain)
1174 if (!hasChain) continue
1175 } else if (contact.chain && contact.chain !== chain) {
1176 // Old format fallback
1177 continue
1178 }
1179 }
1180
1181 contacts.push(contact)
1182 } catch (err) {
1183 console.error('Error parsing contact:', err.message)
1184 }
1185 }
1186
1187 // Sort by name (case-insensitive)
1188 contacts.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
1189
1190 return contacts
1191}
1192
1193/**
1194 * Find a contact by name or address
1195 * @param {string} nameOrAddress - Contact name or blockchain address
1196 * @param {string} [chain] - Chain to search (optional, makes search faster)
1197 * @returns {Promise<Object|null>} - Contact object or null
1198 */
1199async function findContact(nameOrAddress, chain = null) {
1200 const contacts = await getContacts(chain)
1201
1202 // Try exact name match first (case-insensitive)
1203 const byName = contacts.find(c =>
1204 c.name.toLowerCase() === nameOrAddress.toLowerCase()
1205 )
1206 if (byName) return byName
1207
1208 // Try exact address match (check in addresses array)
1209 const byAddress = contacts.find(c => {
1210 if (c.addresses && Array.isArray(c.addresses)) {
1211 return c.addresses.some(addr =>
1212 addr.address.toLowerCase() === nameOrAddress.toLowerCase()
1213 )
1214 }
1215 return false
1216 })
1217 if (byAddress) return byAddress
1218
1219 // Try partial name match
1220 const byPartial = contacts.find(c =>
1221 c.name.toLowerCase().includes(nameOrAddress.toLowerCase())
1222 )
1223
1224 return byPartial || null
1225}
1226
1227/**
1228 * Get a contact by ID
1229 * @param {string} id - Contact ID
1230 * @returns {Promise<Object|null>} - Contact object or null
1231 */
1232async function getContactById(id) {
1233 if (!txHistoryDb) {
1234 throw new Error('Storage not initialized')
1235 }
1236
1237 try {
1238 const data = await txHistoryDb.get(`contact:${id}`)
1239 if (!data) return null
1240
1241 let contact = JSON.parse(data.value)
1242
1243 // Migrate contact format if needed
1244 contact = migrateContactFormat(contact)
1245
1246 return contact
1247 } catch (err) {
1248 if (err.code === 'LEVEL_NOT_FOUND') {
1249 return null
1250 }
1251 throw err
1252 }
1253}
1254
1255/**
1256 * Update a contact
1257 * @param {string} id - Contact ID
1258 * @param {Object} updates - Fields to update
1259 * @returns {Promise<Object>} - Updated contact
1260 */
1261async function updateContact(id, updates) {
1262 if (!autobase) {
1263 throw new Error('Storage not initialized')
1264 }
1265
1266 const existing = await getContactById(id)
1267 if (!existing) {
1268 throw new Error('Contact not found')
1269 }
1270
1271 const updated = {
1272 ...existing,
1273 ...updates,
1274 id: existing.id, // Preserve ID
1275 addedAt: existing.addedAt, // Preserve creation time
1276 modifiedAt: Date.now()
1277 }
1278
1279 const op = {
1280 type: 'put-contact',
1281 key: `contact:${id}`,
1282 value: updated
1283 }
1284
1285 await autobase.append(JSON.stringify(op))
1286
1287 // Wait for view to update
1288 await new Promise(resolve => setTimeout(resolve, 100))
1289
1290 return updated
1291}
1292
1293/**
1294 * Delete a contact
1295 * @param {string} id - Contact ID
1296 */
1297async function deleteContact(id) {
1298 if (!autobase) {
1299 throw new Error('Storage not initialized')
1300 }
1301
1302 const op = {
1303 type: 'del-contact',
1304 key: `contact:${id}`
1305 }
1306
1307 await autobase.append(JSON.stringify(op))
1308
1309 // Wait for view to update
1310 await new Promise(resolve => setTimeout(resolve, 100))
1311}
1312
1313/**
1314 * Find contact by address (helper for transaction display)
1315 * UPDATED: Supports both old (single address) and new (multiple addresses) format
1316 * @param {string} address - Blockchain address
1317 * @param {string} chain - Chain name
1318 * @returns {Promise<Object|null>} - Contact or null
1319 */
1320async function findContactByAddress(address, chain) {
1321 const contacts = await getContacts()
1322
1323 return contacts.find(c => {
1324 // Old format: contact.address
1325 if (c.address && c.address.toLowerCase() === address.toLowerCase() && c.chain === chain) {
1326 return true
1327 }
1328
1329 // New format: contact.addresses[]
1330 if (c.addresses && Array.isArray(c.addresses)) {
1331 return c.addresses.some(addr =>
1332 addr.chain === chain && addr.address.toLowerCase() === address.toLowerCase()
1333 )
1334 }
1335
1336 return false
1337 }) || null
1338}
1339
1340/**
1341 * Add an address to an existing contact (supports multiple addresses per chain)
1342 * @param {string} contactId - Contact ID
1343 * @param {Object} addressInfo - Address details
1344 * @param {string} addressInfo.chain - Chain name (bitcoin, ethereum, polygon, etc.)
1345 * @param {string} addressInfo.address - Blockchain address
1346 * @param {string} addressInfo.network - Network (testnet, mainnet, sepolia, etc.)
1347 * @param {string} [addressInfo.label] - Optional label (e.g., "Hardware wallet", "Hot wallet")
1348 * @returns {Promise<Object>} - Updated contact
1349 */
1350async function addAddressToContact(contactId, { chain, address, network, label = '' }) {
1351 if (!autobase) {
1352 throw new Error('Storage not initialized')
1353 }
1354
1355 const existing = await getContactById(contactId)
1356 if (!existing) {
1357 throw new Error('Contact not found')
1358 }
1359
1360 // Migrate old format to new format if needed
1361 let addresses = []
1362 if (existing.addresses && Array.isArray(existing.addresses)) {
1363 addresses = existing.addresses
1364 } else if (existing.address && existing.chain) {
1365 // Convert old single-address format (add ID to migrated address)
1366 addresses = [{
1367 id: crypto.randomBytes(8).toString('hex'),
1368 chain: existing.chain,
1369 address: existing.address,
1370 network: existing.network || network,
1371 label: ''
1372 }]
1373 }
1374
1375 // Check if EXACT same address already exists (same address + same chain)
1376 const exactMatch = addresses.find(a =>
1377 a.chain === chain && a.address.toLowerCase() === address.toLowerCase()
1378 )
1379 if (exactMatch) {
1380 throw new Error(`This exact address already exists for ${chain}`)
1381 }
1382
1383 // Generate unique ID for this address
1384 const addressId = crypto.randomBytes(8).toString('hex')
1385
1386 // Add new address with unique ID
1387 addresses.push({
1388 id: addressId,
1389 chain,
1390 address,
1391 network,
1392 label: label || '',
1393 addedAt: Date.now()
1394 })
1395
1396 // Update contact with new addresses array
1397 const updated = {
1398 ...existing,
1399 addresses,
1400 // Remove old format fields
1401 address: undefined,
1402 chain: undefined,
1403 modifiedAt: Date.now()
1404 }
1405
1406 const op = {
1407 type: 'put-contact',
1408 key: `contact:${contactId}`,
1409 value: updated
1410 }
1411
1412 await autobase.append(JSON.stringify(op))
1413
1414 // Wait for view to update
1415 await new Promise(resolve => setTimeout(resolve, 100))
1416
1417 return updated
1418}
1419
1420/**
1421 * Remove an address from a contact by address ID
1422 * @param {string} contactId - Contact ID
1423 * @param {string} addressId - Address ID to remove
1424 * @returns {Promise<Object>} - Updated contact
1425 */
1426async function removeAddressFromContact(contactId, addressId) {
1427 if (!autobase) {
1428 throw new Error('Storage not initialized')
1429 }
1430
1431 const existing = await getContactById(contactId)
1432 if (!existing) {
1433 throw new Error('Contact not found')
1434 }
1435
1436 // Migrate old format to new format if needed
1437 let addresses = []
1438 if (!existing.addresses || !Array.isArray(existing.addresses)) {
1439 // Old format: single address
1440 if (existing.address && existing.chain) {
1441 // Get default network from config for migration
1442 const config = getConfig()
1443 const defaultNetwork = config.network || 'testnet'
1444
1445 addresses = [{
1446 id: crypto.randomBytes(8).toString('hex'),
1447 chain: existing.chain,
1448 address: existing.address,
1449 network: existing.network || defaultNetwork,
1450 label: '',
1451 addedAt: existing.addedAt || Date.now()
1452 }]
1453 } else {
1454 throw new Error('Contact has no addresses')
1455 }
1456 } else {
1457 addresses = existing.addresses
1458 }
1459
1460 // Filter out the address with this ID
1461 const newAddresses = addresses.filter(a => a.id !== addressId)
1462
1463 if (newAddresses.length === addresses.length) {
1464 throw new Error(`No address found with ID: ${addressId}`)
1465 }
1466
1467 if (newAddresses.length === 0) {
1468 throw new Error('Cannot remove last address. Delete contact instead.')
1469 }
1470
1471 const updated = {
1472 ...existing,
1473 addresses: newAddresses,
1474 // Remove old format fields
1475 address: undefined,
1476 chain: undefined,
1477 modifiedAt: Date.now()
1478 }
1479
1480 const op = {
1481 type: 'put-contact',
1482 key: `contact:${contactId}`,
1483 value: updated
1484 }
1485
1486 await autobase.append(JSON.stringify(op))
1487
1488 // Wait for view to update
1489 await new Promise(resolve => setTimeout(resolve, 100))
1490
1491 return updated
1492}
1493
1494/**
1495 * Bootstrap initial device (this device)
1496 * Creates attestation proof for the first device
1497 */
1498async function bootstrapDevice(identity, deviceName) {
1499 if (!autobase) {
1500 throw new Error('Storage not initialized')
1501 }
1502
1503 // With Autobase, the local writer is autobase.local
1504 const localWriter = autobase.local
1505 await localWriter.ready()
1506
1507 // Create bootstrap proof (self-attestation)
1508 const proof = await identity.bootstrap(localWriter.key)
1509
1510 const device = {
1511 name: deviceName,
1512 key: localWriter.key,
1513 identityKey: identityKeyPair.publicKey, // Shared identity key
1514 proof: proof,
1515 addedAt: Date.now(),
1516 isLocal: true
1517 }
1518
1519 const devices = loadDevices()
1520
1521 // Check if already bootstrapped
1522 const existing = devices.find(d => b4a.equals(d.key, localWriter.key))
1523 if (existing) {
1524 return existing
1525 }
1526
1527 devices.push(device)
1528 saveDevices(devices)
1529
1530 return device
1531}
1532
1533/**
1534 * Create invite for another device to join
1535 * Returns invite code that includes discovery key and autobase bootstrap key
1536 */
1537async function createInvite(identity) {
1538 if (!identityKeyPair) {
1539 throw new Error('Identity not loaded')
1540 }
1541
1542 if (!autobase) {
1543 throw new Error('Storage not initialized')
1544 }
1545
1546 const now = Date.now()
1547 const INVITE_EXPIRATION = 15 * 60 * 1000 // 15 minutes
1548
1549 // Create invite data (without signature first)
1550 const inviteData = {
1551 discoveryKey: b4a.toString(crypto.discoveryKey(identityKeyPair.publicKey), 'hex'),
1552 identityKey: b4a.toString(identityKeyPair.publicKey, 'hex'),
1553 autobaseKey: b4a.toString(autobase.key, 'hex'),
1554 timestamp: now,
1555 expiresAt: now + INVITE_EXPIRATION
1556 }
1557
1558 // Sign the invite data with identity secret key
1559 const message = b4a.from(JSON.stringify(inviteData))
1560 const signature = crypto.sign(message, identityKeyPair.secretKey)
1561
1562 // Add signature to invite
1563 const signedInvite = {
1564 ...inviteData,
1565 signature: b4a.toString(signature, 'hex')
1566 }
1567
1568 // Encode as base64
1569 const inviteStr = b4a.toString(b4a.from(JSON.stringify(signedInvite)), 'base64')
1570
1571 console.log(`✅ Created signed invite (expires in 15 minutes)`)
1572 return inviteStr
1573}
1574
1575/**
1576 * Join wallet using invite code
1577 * Receives mnemonic from primary device over secure Protomux channel
1578 * Initializes Autobase with bootstrap key from invite
1579 *
1580 * NOTE: This reinitializes storage! Close existing storage first.
1581 * @param {string} inviteCode - Invite code from primary device
1582 * @param {string} deviceName - Name for this device
1583 * @returns {Object} { device, mnemonic } - Device info and received mnemonic
1584 */
1585async function joinWallet(inviteCode, deviceName) {
1586 // Decode invite
1587 let invite
1588 try {
1589 const inviteJson = b4a.toString(b4a.from(inviteCode, 'base64'))
1590 invite = JSON.parse(inviteJson)
1591 } catch (err) {
1592 throw new Error('Invalid invite code format')
1593 }
1594
1595 // Verify invite has required fields
1596 if (!invite.discoveryKey || !invite.identityKey || !invite.autobaseKey ||
1597 !invite.timestamp || !invite.expiresAt || !invite.signature) {
1598 throw new Error('Invalid invite: missing required fields')
1599 }
1600
1601 // Check expiration (15 minutes)
1602 const now = Date.now()
1603 if (now > invite.expiresAt) {
1604 const expirationAge = Math.floor((now - invite.expiresAt) / 1000 / 60) // minutes ago
1605 throw new Error(`Invite code expired ${expirationAge} minutes ago. Request a new invite.`)
1606 }
1607
1608 // Verify signature
1609 const { signature, ...inviteData } = invite
1610 const message = b4a.from(JSON.stringify(inviteData))
1611 const sig = b4a.from(signature, 'hex')
1612 const pubKey = b4a.from(invite.identityKey, 'hex')
1613
1614 if (!crypto.verify(message, sig, pubKey)) {
1615 throw new Error('Invalid invite signature - invite may be forged or corrupted')
1616 }
1617
1618 console.log('✅ Invite signature verified')
1619 console.log(`⏰ Invite expires in ${Math.floor((invite.expiresAt - now) / 1000 / 60)} minutes`)
1620
1621 // Initialize Corestore for temporary writer key
1622 if (!store) {
1623 store = new Corestore(STORAGE_DIR)
1624 await store.ready()
1625 }
1626
1627 // Get namespace
1628 const namespace = await Autobase.getLocalKey(store)
1629
1630 // View class for Autobase
1631 class WalletView {
1632 open(store) {
1633 const core = store.get('view')
1634 return new Hyperbee(core, {
1635 keyEncoding: 'utf-8',
1636 valueEncoding: 'binary'
1637 })
1638 }
1639
1640 async apply(nodes, view, base) {
1641 return apply(nodes, view, base)
1642 }
1643
1644 close(view) {
1645 return view.close()
1646 }
1647 }
1648
1649 // Initialize Autobase with BOOTSTRAP KEY from invite
1650 const bootstrapKey = b4a.from(invite.autobaseKey, 'hex')
1651 autobase = new Autobase(store.namespace(namespace), bootstrapKey, new WalletView())
1652 await autobase.ready()
1653
1654 // Get our local writer key (to send to primary device)
1655 const localWriterKey = autobase.local.key
1656
1657 // Get view
1658 txHistoryDb = autobase.view
1659
1660 // Initialize Hyperswarm for pairing
1661 if (!swarm) {
1662 swarm = new Hyperswarm()
1663 }
1664
1665 console.log('📡 Connecting to primary device...')
1666
1667 // Set up connection handler BEFORE joining swarm (avoid race condition)
1668 const { conn, identityMnemonic, walletMnemonic } = await new Promise((resolve, reject) => {
1669 let timeout = null
1670
1671 const handler = async (conn) => {
1672 try {
1673 if (timeout) clearTimeout(timeout)
1674 swarm.off('connection', handler)
1675
1676 console.log('✅ Connected to primary device')
1677 console.log('🔐 Starting pairing protocol...')
1678
1679 // Receive mnemonics via pairing protocol
1680 const { identityMnemonic, walletMnemonic } = await createPairingClient(conn, localWriterKey, deviceName)
1681
1682 // Start replication AFTER pairing completes
1683 store.replicate(conn)
1684
1685 resolve({ conn, identityMnemonic, walletMnemonic })
1686 } catch (err) {
1687 if (timeout) clearTimeout(timeout)
1688 reject(err)
1689 }
1690 }
1691
1692 swarm.on('connection', handler)
1693
1694 // Timeout after 60 seconds
1695 timeout = setTimeout(() => {
1696 swarm.off('connection', handler)
1697 reject(new Error('Connection timeout'))
1698 }, 60000)
1699
1700 // Join discovery key AFTER setting up listener (avoid race condition)
1701 const discoveryKey = b4a.from(invite.discoveryKey, 'hex')
1702 swarm.join(discoveryKey, { server: true, client: true }).flushed().catch(reject)
1703 })
1704
1705 console.log('✅ Received wallet mnemonics')
1706 console.log('')
1707
1708 // Create identity from received identity mnemonic
1709 const identity = await IdentityKey.from({ mnemonic: identityMnemonic })
1710 identityKeyPair = identity.identityKeyPair
1711
1712 // Create bootstrap proof for this device
1713 const proof = await identity.bootstrap(localWriterKey)
1714
1715 // Save local device info
1716 const device = {
1717 name: deviceName,
1718 key: localWriterKey,
1719 publicKey: b4a.toString(identityKeyPair.publicKey, 'hex'),
1720 proof: proof,
1721 addedAt: Date.now(),
1722 isLocal: true
1723 }
1724
1725 const devices = [device]
1726 saveDevices(devices)
1727
1728 // NOTE: Don't try to add ourselves as a writer here - we can't append until
1729 // we're already a writer (chicken-egg problem). The primary device handles
1730 // adding our writer via the addDevice function during pairing.
1731
1732 return { device, identityMnemonic, walletMnemonic }
1733}
1734
1735/**
1736 * Add another device's writer to Autobase
1737 * Called after attestation is verified
1738 */
1739async function addDevice(deviceKey, deviceName, proof) {
1740 const remoteWriter = store.get(deviceKey)
1741 await remoteWriter.ready()
1742
1743 // Add to Autobase by appending addWriter operation
1744 const op = { addWriter: b4a.toString(deviceKey, 'hex') }
1745 await autobase.append(JSON.stringify(op))
1746
1747 // Save to device list
1748 const devices = loadDevices()
1749 devices.push({
1750 name: deviceName,
1751 key: deviceKey,
1752 proof: proof,
1753 addedAt: Date.now(),
1754 isLocal: false
1755 })
1756 saveDevices(devices)
1757}
1758
1759/**
1760 * Get current device info
1761 */
1762function getCurrentDevice() {
1763 if (!autobase) return null
1764
1765 const devices = loadDevices()
1766 return devices.find(d => d.isLocal && b4a.equals(d.key, autobase.local.key))
1767}
1768
1769/**
1770 * List all devices
1771 */
1772function listDevices() {
1773 return loadDevices()
1774}
1775
1776/**
1777 * Remove device (revoke access)
1778 * Note: Autobase doesn't have removeInput, so we append a removeWriter operation
1779 */
1780async function removeDevice(deviceKey) {
1781 if (!autobase) {
1782 throw new Error('Storage not initialized')
1783 }
1784
1785 let devices = loadDevices()
1786
1787 const device = devices.find(d => b4a.equals(d.key, deviceKey))
1788 if (!device) {
1789 throw new Error('Device not found')
1790 }
1791
1792 // Prevent removing local device
1793 if (device.isLocal) {
1794 throw new Error('Cannot remove local device')
1795 }
1796
1797 // Append removeWriter operation - Autobase will process this
1798 // Note: Autobase doesn't have built-in removeInput, so we use custom operation
1799 const op = {
1800 removeWriter: b4a.toString(deviceKey, 'hex'),
1801 timestamp: Date.now()
1802 }
1803 await autobase.append(JSON.stringify(op))
1804
1805 // Wait for operation to be processed
1806 await autobase.update()
1807 await new Promise(resolve => setTimeout(resolve, 500))
1808
1809 // Remove from device list
1810 devices = devices.filter(d => !b4a.equals(d.key, deviceKey))
1811 saveDevices(devices)
1812}
1813
1814/**
1815 * Deduplicate devices in the device list
1816 * Removes duplicate entries keeping only the most recent one per writer key
1817 */
1818function deduplicateDevices() {
1819 let devices = loadDevices()
1820 const seen = new Map()
1821 const deduplicated = []
1822
1823 // Sort by addedAt (newest first)
1824 devices.sort((a, b) => b.addedAt - a.addedAt)
1825
1826 for (const device of devices) {
1827 const writerKeyHex = b4a.toString(device.key, 'hex')
1828
1829 if (!seen.has(writerKeyHex)) {
1830 seen.set(writerKeyHex, true)
1831 deduplicated.push(device)
1832 }
1833 }
1834
1835 // Sort back by addedAt (oldest first) for consistent ordering
1836 deduplicated.sort((a, b) => a.addedAt - b.addedAt)
1837
1838 const removed = devices.length - deduplicated.length
1839
1840 if (removed > 0) {
1841 saveDevices(deduplicated)
1842 }
1843
1844 return { removed, total: deduplicated.length }
1845}
1846
1847/**
1848 * Get connection status
1849 */
1850function getConnectionStatus() {
1851 return {
1852 connected: connections.size > 0,
1853 count: connections.size,
1854 connections: Array.from(connections).map(conn => ({
1855 remotePublicKey: conn.remotePublicKey ? b4a.toString(conn.remotePublicKey, 'hex').slice(0, 16) + '...' : 'unknown'
1856 }))
1857 }
1858}
1859
1860/**
1861 * Wait for incoming connections (for accepting join requests)
1862 * Returns when a new device connects
1863 * @param {number} timeout - Timeout in ms (Infinity = wait forever)
1864 */
1865async function waitForConnection(timeout = 30000) {
1866 return new Promise((resolve, reject) => {
1867 let timer = null
1868
1869 // Only set timeout if not infinite
1870 if (timeout !== Infinity && timeout > 0) {
1871 timer = setTimeout(() => {
1872 swarm.off('connection', handler)
1873 reject(new Error('Timeout waiting for connection'))
1874 }, timeout)
1875 }
1876
1877 const handler = (conn) => {
1878 if (timer) clearTimeout(timer)
1879 swarm.off('connection', handler)
1880 resolve(conn)
1881 }
1882
1883 swarm.on('connection', handler)
1884 })
1885}
1886
1887/**
1888 * Wait for a new device to connect and auto-approve it
1889 * Sends mnemonics to joining device over secure Protomux channel
1890 * @param {number} timeout - Timeout in ms (0 = infinite)
1891 * @param {string} identityMnemonic - Identity mnemonic to send to joining device
1892 * @param {string} walletMnemonic - Wallet mnemonic to send to joining device
1893 * @returns {Object} { writerKey, deviceName }
1894 */
1895async function waitForNewDevice(timeout = 120000, identityMnemonic = null, walletMnemonic = null) {
1896 if (!identityMnemonic || !walletMnemonic) {
1897 throw new Error('Both identity and wallet mnemonics required to pair new device')
1898 }
1899
1900 // Get current device info
1901 const currentDevice = getCurrentDevice()
1902 const myDeviceName = currentDevice ? currentDevice.name : 'Primary Device'
1903
1904 // Wait for connection (infinite if timeout=0)
1905 const actualTimeout = timeout === 0 ? Infinity : timeout
1906 const conn = await waitForConnection(actualTimeout)
1907
1908 console.log('✅ Device connected, exchanging pairing information...')
1909
1910 // Start pairing server protocol - this will send BOTH mnemonics
1911 const { remoteDeviceName, remoteWriterKey } = await createPairingServer(
1912 conn,
1913 identityMnemonic,
1914 walletMnemonic,
1915 myDeviceName
1916 )
1917
1918 console.log(`🔍 Received device info: ${remoteDeviceName}`)
1919 console.log(` Writer: ${b4a.toString(remoteWriterKey, 'hex').slice(0, 16)}...`)
1920 console.log('')
1921
1922 // Start replication AFTER pairing protocol completes
1923 store.replicate(conn)
1924
1925 // Auto-approve the writer
1926 console.log('🔐 Auto-approving writer...')
1927
1928 // Append addWriter operation
1929 const op = {
1930 addWriter: b4a.toString(remoteWriterKey, 'hex'),
1931 timestamp: Date.now()
1932 }
1933 await autobase.append(JSON.stringify(op))
1934 await autobase.update()
1935
1936 // Wait for operation to be processed
1937 await new Promise(resolve => setTimeout(resolve, 1000))
1938
1939 // Check for duplicate writer key (prevent re-adding same device)
1940 const devices = loadDevices()
1941 const writerKeyHex = b4a.toString(remoteWriterKey, 'hex')
1942 const existing = devices.find(d => b4a.toString(d.key, 'hex') === writerKeyHex)
1943
1944 if (existing) {
1945 console.log(`⚠️ Device already exists: ${existing.name} (writer: ${writerKeyHex.slice(0, 16)}...)`)
1946 console.log(' Skipping duplicate entry')
1947 return { writerKey: remoteWriterKey, deviceName: remoteDeviceName, isDuplicate: true }
1948 }
1949
1950 // Get identity key (shared across all devices using same identity mnemonic)
1951 const identity = await IdentityKey.from({ mnemonic: identityMnemonic })
1952 const identityKey = identity.identityKeyPair.publicKey
1953
1954 // Save to device list with both keys
1955 devices.push({
1956 name: remoteDeviceName,
1957 key: remoteWriterKey,
1958 identityKey: identityKey, // Shared identity key
1959 proof: null,
1960 addedAt: Date.now(),
1961 isLocal: false
1962 })
1963 saveDevices(devices)
1964
1965 return { writerKey: remoteWriterKey, deviceName: remoteDeviceName, isDuplicate: false }
1966}
1967
1968/**
1969 * Save pending writer to file with attestation proof
1970 * @param {Buffer} writerKey - The writer key
1971 * @param {Buffer} proof - Optional attestation proof
1972 */
1973function savePendingWriter(writerKey, proof = null) {
1974 const dir = path.dirname(PENDING_FILE)
1975 if (!fs.existsSync(dir)) {
1976 fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
1977 }
1978
1979 const pending = loadPendingWriters()
1980 const keyHex = b4a.toString(writerKey, 'hex')
1981
1982 // Check if already exists
1983 const existing = pending.find(p => p.key === keyHex)
1984 if (!existing) {
1985 pending.push({
1986 key: keyHex,
1987 proof: proof ? b4a.toString(proof, 'hex') : null,
1988 addedAt: Date.now()
1989 })
1990 }
1991
1992 fs.writeFileSync(PENDING_FILE, JSON.stringify(pending, null, 2), { mode: 0o600 })
1993}
1994
1995/**
1996 * Load pending writers from file
1997 * Returns array of {key, proof, addedAt} objects
1998 */
1999function loadPendingWriters() {
2000 if (!fs.existsSync(PENDING_FILE)) {
2001 return []
2002 }
2003
2004 const data = fs.readFileSync(PENDING_FILE, 'utf8')
2005 return JSON.parse(data)
2006}
2007
2008/**
2009 * Remove a pending writer from the file
2010 */
2011function removePendingWriter(writerKey) {
2012 const pending = loadPendingWriters()
2013 const keyHex = b4a.toString(writerKey, 'hex')
2014 const filtered = pending.filter(p => p.key !== keyHex)
2015
2016 if (filtered.length === 0) {
2017 // Remove file if empty
2018 if (fs.existsSync(PENDING_FILE)) {
2019 fs.unlinkSync(PENDING_FILE)
2020 }
2021 } else {
2022 fs.writeFileSync(PENDING_FILE, JSON.stringify(filtered, null, 2), { mode: 0o600 })
2023 }
2024}
2025
2026/**
2027 * Get list of pending writers (connected but not approved)
2028 * Returns array of {key, proof} objects with Buffers
2029 */
2030function getPendingWriters() {
2031 const pendingData = loadPendingWriters()
2032
2033 // Convert hex strings back to Buffers
2034 return pendingData.map(p => ({
2035 key: b4a.from(p.key, 'hex'),
2036 proof: p.proof ? b4a.from(p.proof, 'hex') : null,
2037 addedAt: p.addedAt
2038 }))
2039}
2040
2041/**
2042 * Approve a pending writer (add to Autobase)
2043 * @param {Buffer} writerKey - The writer key to approve
2044 * @param {string} deviceName - Optional name for the device
2045 * @param {boolean} skipVerification - Skip attestation verification (for testing)
2046 */
2047async function approveWriter(writerKey, deviceName = 'Unknown Device', skipVerification = false) {
2048 if (!autobase) {
2049 throw new Error('Storage not initialized')
2050 }
2051
2052 // Check if already added
2053 const devices = loadDevices()
2054 const existing = devices.find(d => b4a.equals(d.key, writerKey))
2055 if (existing) {
2056 throw new Error('Writer already approved')
2057 }
2058
2059 console.log('🔍 Approving writer:', b4a.toString(writerKey, 'hex').slice(0, 16) + '...')
2060
2061 // Get attestation proof from pending writers
2062 const pending = getPendingWriters()
2063 const pendingWriter = pending.find(p => b4a.equals(p.key, writerKey))
2064
2065 let proof = null
2066 if (pendingWriter && pendingWriter.proof) {
2067 proof = pendingWriter.proof
2068
2069 // Verify attestation proof
2070 if (!skipVerification) {
2071 console.log('🔐 Verifying attestation proof...')
2072
2073 try {
2074 const verified = IdentityKey.verify(proof)
2075
2076 if (!verified) {
2077 throw new Error('Attestation verification failed')
2078 }
2079
2080 console.log('✅ Attestation verified')
2081 } catch (err) {
2082 console.log('❌ Attestation verification failed:', err.message)
2083 console.log('')
2084 console.log('⚠️ WARNING: Proof verification failed!')
2085 console.log(' This device may not be authorized.')
2086 console.log('')
2087 console.log('To approve anyway (UNSAFE), set skipVerification=true')
2088 console.log('')
2089 throw new Error('Attestation verification failed')
2090 }
2091 } else {
2092 console.log('⚠️ Skipping attestation verification (unsafe mode)')
2093 }
2094 } else {
2095 console.log('⚠️ No attestation proof found')
2096 console.log(' Continuing without verification (Phase 5.4 limitation)')
2097 }
2098
2099 // Append addWriter operation - Autobase processes this internally
2100 const op = {
2101 addWriter: b4a.toString(writerKey, 'hex'),
2102 timestamp: Date.now()
2103 }
2104 await autobase.append(JSON.stringify(op))
2105
2106 // Wait for operation to be processed
2107 await autobase.update()
2108
2109 console.log('✅ Writer added to Autobase')
2110
2111 // Wait for view to update
2112 await new Promise(resolve => setTimeout(resolve, 1000))
2113
2114 // Save to device list
2115 devices.push({
2116 name: deviceName,
2117 key: writerKey,
2118 proof: proof,
2119 addedAt: Date.now(),
2120 isLocal: false
2121 })
2122 saveDevices(devices)
2123
2124 // Remove from pending list
2125 removePendingWriter(writerKey)
2126
2127 console.log('✅ Device approved:', deviceName)
2128
2129 return writerKey
2130}
2131
2132/**
2133 * Enable replication for device sync mode
2134 * Call this to start replicating on new connections
2135 */
2136function enableReplication() {
2137 if (!swarm || !store) {
2138 throw new Error('Storage not initialized')
2139 }
2140
2141 // Add replication handler for new connections
2142 swarm.on('connection', (conn) => {
2143 store.replicate(conn)
2144 })
2145
2146 // Replicate on existing connections
2147 for (const conn of connections) {
2148 store.replicate(conn)
2149 }
2150}
2151
2152/**
2153 * Close storage and network
2154 */
2155async function closeStorage() {
2156 if (swarm) {
2157 await swarm.destroy()
2158 swarm = null
2159 }
2160 if (txHistoryDb) {
2161 await txHistoryDb.close()
2162 txHistoryDb = null
2163 }
2164 if (autobase) {
2165 await autobase.close()
2166 autobase = null
2167 }
2168 if (store) {
2169 await store.close()
2170 store = null
2171 }
2172 connections.clear()
2173}
2174
2175module.exports = {
2176 initializeStorage,
2177 storeTx,
2178 getTxHistory,
2179 getTx,
2180 updateTxStatus,
2181 // WDK wallet functions (Phase 6.2)
2182 initializeWallets,
2183 storeAddress,
2184 getAddresses,
2185 getNextAddressIndex,
2186 getCurrentAddress,
2187 updateAddressLabel,
2188 setCurrentAddress,
2189 archiveAddress,
2190 // UTXO functions (Phase 6.3)
2191 storeUTXO,
2192 getUTXOs,
2193 updateUTXO,
2194 deleteUTXO,
2195 // Transaction storage (Phase 6.6)
2196 storeTransaction,
2197 getTransactions,
2198 getTransaction,
2199 deleteTransaction,
2200 // Transaction status updates (Phase 6.7)
2201 updateTransactionStatus,
2202 // Transaction sync
2203 syncTransactionsFromWDK,
2204 // Device management
2205 bootstrapDevice,
2206 createInvite,
2207 joinWallet,
2208 addDevice,
2209 getCurrentDevice,
2210 listDevices,
2211 removeDevice,
2212 deduplicateDevices,
2213 getConnectionStatus,
2214 waitForNewDevice,
2215 getPendingWriters,
2216 approveWriter,
2217 enableReplication,
2218 // Contact management
2219 addContact,
2220 getContacts,
2221 findContact,
2222 getContactById,
2223 updateContact,
2224 deleteContact,
2225 findContactByAddress,
2226 // NEW: Multi-address contact support
2227 addAddressToContact,
2228 removeAddressFromContact,
2229 closeStorage
2230}
lib/wdk-integration.cjs +0 -214
……@@ -1,214 +0,0 @@
1// lib/wdk-integration.cjs - WDK Integration Layer
2// CRITICAL: Load bare-node-runtime FIRST before any other requires
3require('bare-node-runtime/global')
4
5const fs = require('bare-fs')
6const path = require('bare-path')
7const os = require('bare-os')
8
9// Import WDK packages directly
10// bare-node-runtime/global provides Node.js compatibility for Bare runtime
11const WDK = require('@tetherto/wdk')
12const WalletManagerBtc = require('@tetherto/wdk-wallet-btc')
13const WalletManagerEvm = require('@tetherto/wdk-wallet-evm')
14
15// Import encryption module
16const { encrypt, decrypt, isValidFormat } = require('./encryption.cjs')
17
18// Storage paths
19const STORAGE_DIR = path.join(os.homedir(), '.hyper-wallet')
20const WALLET_MNEMONIC_FILE = path.join(STORAGE_DIR, '.wallet-mnemonic')
21
22/**
23 * Generate new wallet mnemonic (12 words)
24 * Uses WDK.getRandomSeedPhrase() - official WDK method
25 * @returns {string} - 12-word BIP-39 mnemonic
26 */
27function generateWalletMnemonic() {
28 return WDK.default.getRandomSeedPhrase()
29}
30
31/**
32 * Save wallet mnemonic to file (with password-based encryption)
33 * Stored separately from identity mnemonic for security separation
34 * @param {string} mnemonic - 12-word mnemonic
35 * @param {string} password - Encryption password (min 8 chars)
36 */
37function saveWalletMnemonic(mnemonic, password) {
38 if (!fs.existsSync(STORAGE_DIR)) {
39 fs.mkdirSync(STORAGE_DIR, { recursive: true, mode: 0o700 })
40 }
41
42 // Require password for new mnemonics
43 if (!password || password.length < 8) {
44 throw new Error('Password must be at least 8 characters')
45 }
46
47 // Encrypt mnemonic before saving
48 const encrypted = encrypt(mnemonic, password)
49 fs.writeFileSync(WALLET_MNEMONIC_FILE, encrypted, { mode: 0o600 })
50}
51
52/**
53 * Load wallet mnemonic from file (with password-based decryption)
54 * @param {string} password - Decryption password
55 * @returns {string|null} - 12-word mnemonic or null if not exists
56 */
57function loadWalletMnemonic(password) {
58 if (!fs.existsSync(WALLET_MNEMONIC_FILE)) {
59 return null
60 }
61
62 const encrypted = fs.readFileSync(WALLET_MNEMONIC_FILE, 'utf8').trim()
63
64 // Check if this is plaintext (legacy format) - contains spaces and 12 words
65 if (encrypted.includes(' ') && encrypted.split(' ').length === 12) {
66 console.warn('⚠️ WARNING: Wallet mnemonic is stored in plaintext (legacy format)')
67 console.warn('⚠️ Please set a password to encrypt your mnemonics')
68 return encrypted
69 }
70
71 // Decrypt with password
72 if (!password) {
73 throw new Error('Password required to decrypt wallet mnemonic')
74 }
75
76 try {
77 return decrypt(encrypted, password)
78 } catch (err) {
79 throw new Error('Invalid password or corrupted mnemonic data')
80 }
81}
82
83/**
84 * Initialize WDK with wallet mnemonic
85 * Creates WDK instance and registers Bitcoin + Ethereum wallets
86 * @param {string} walletMnemonic - 12-word BIP-39 mnemonic
87 * @param {Object} config - Configuration { testnet: boolean }
88 * @returns {Object} - { wdk } - WDK instance with registered wallets
89 */
90async function initializeWDK(walletMnemonic, config = { testnet: true, testnet4: false }) {
91 try {
92 // Initialize WDK with mnemonic and register wallets (chainable)
93 const wdk = new WDK.default(walletMnemonic)
94 .registerWallet('bitcoin', WalletManagerBtc.default, {
95 network: config.testnet4 ? 'testnet4' : (config.testnet ? 'testnet' : 'bitcoin'),
96 host: config.testnet4 ? 'blackie.c3-soft.com' : 'electrum.blockstream.info',
97 port: config.testnet4 ? 50001 : (config.testnet ? 60001 : 50001)
98 })
99 .registerWallet('ethereum', WalletManagerEvm.default, {
100 provider: config.testnet
101 ? 'https://ethereum-sepolia-rpc.publicnode.com'
102 : 'https://eth.drpc.org'
103 })
104
105 return { wdk }
106 } catch (err) {
107 // Graceful error handling with clear user feedback
108 if (err.message.includes('network') || err.message.includes('provider')) {
109 throw new Error('Cannot connect to blockchain network. Check internet connection.')
110 } else if (err.message.includes('mnemonic') || err.message.includes('seed')) {
111 throw new Error('Invalid wallet mnemonic format.')
112 } else {
113 throw new Error(`WDK initialization failed: ${err.message}`)
114 }
115 }
116}
117
118/**
119 * Get first address for testing/display
120 * Uses account index 0, address index 0
121 * @param {Object} wdk - WDK instance
122 * @param {string} chain - 'bitcoin' or 'ethereum'
123 * @returns {string} - First derived address
124 */
125async function getFirstAddress(wdk, chain = 'bitcoin') {
126 try {
127 const account = await wdk.getAccount(chain, 0)
128 return await account.getAddress()
129 } catch (err) {
130 throw new Error(`Failed to derive ${chain} address: ${err.message}`)
131 }
132}
133
134/**
135 * Check if wallet mnemonic exists
136 * @returns {boolean}
137 */
138function walletMnemonicExists() {
139 return fs.existsSync(WALLET_MNEMONIC_FILE)
140}
141
142/**
143 * Token contract addresses for ERC-20 tokens
144 * Format: { 'chain-token': { mainnet: 'address', testnet: 'address' } }
145 */
146const TOKEN_CONTRACTS = {
147 'ethereum-usdt': {
148 mainnet: '0xdac17f958d2ee523a2206206994597c13d831ec7',
149 testnet: '0x7169D38820dfd117C3FA1f22a697dBA58d90BA06' // Sepolia USDT
150 },
151 'ethereum-usdc': {
152 mainnet: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
153 testnet: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' // Sepolia USDC
154 }
155}
156
157/**
158 * Check if a chain parameter refers to a token (e.g., 'ethereum-usdt')
159 * @param {string} chain - Chain identifier
160 * @returns {boolean}
161 */
162function isTokenChain(chain) {
163 return chain.includes('-') && chain !== 'testnet' && chain !== 'mainnet'
164}
165
166/**
167 * Get base chain from token chain identifier
168 * @param {string} chain - Chain identifier (e.g., 'ethereum-usdt' or 'ethereum')
169 * @returns {string} - Base chain (e.g., 'ethereum')
170 */
171function getBaseChain(chain) {
172 if (!isTokenChain(chain)) {
173 return chain
174 }
175 // Extract base chain from 'ethereum-usdt' -> 'ethereum'
176 return chain.split('-')[0]
177}
178
179/**
180 * Get token contract information
181 * @param {string} chain - Token chain identifier (e.g., 'ethereum-usdt')
182 * @param {boolean} isTestnet - Whether to use testnet contracts
183 * @returns {Object} - Token info { contractAddress, symbol, decimals }
184 */
185function getTokenInfo(chain, isTestnet = true) {
186 const tokenContracts = TOKEN_CONTRACTS[chain]
187 if (!tokenContracts) {
188 throw new Error(`Unsupported token: ${chain}`)
189 }
190
191 const contractAddress = isTestnet ? tokenContracts.testnet : tokenContracts.mainnet
192
193 // Extract token symbol from chain (ethereum-usdt -> USDT)
194 const symbol = chain.split('-')[1]?.toUpperCase() || 'TOKEN'
195
196 return {
197 contractAddress, // Changed from 'contract' to match usage in wallet-server.cjs
198 symbol,
199 decimals: 6 // USDT/USDC use 6 decimals
200 }
201}
202
203module.exports = {
204 generateWalletMnemonic,
205 saveWalletMnemonic,
206 loadWalletMnemonic,
207 initializeWDK,
208 getFirstAddress,
209 walletMnemonicExists,
210 isTokenChain,
211 getBaseChain,
212 getTokenInfo,
213 TOKEN_CONTRACTS
214}
package-lock.json +442 -0
……@@ -14,6 +14,7 @@
1414 "autobase": "^7.25.1",
1515 "b4a": "^1.6.7",
1616 "corestore": "^7.7.0",
17 "hyper-wallet-lib": "file:lib/hyper-wallet-lib",
1718 "hyperbee": "^2.22.0",
1819 "hypercore": "^10.37.8",
1920 "hypercore-crypto": "^3.4.2",
……@@ -24,6 +25,29 @@
2425 "qrcode": "^1.5.4"
2526 }
2627 },
28 "../packages/hyper-wallet-lib": {
29 "version": "1.0.0",
30 "extraneous": true,
31 "dependencies": {
32 "keet-identity-key": "^3.1.0",
33 "keytar": "^7.9.0"
34 },
35 "peerDependencies": {
36 "bare-fs": "*",
37 "bare-path": "*"
38 }
39 },
40 "lib/hyper-wallet-lib": {
41 "version": "1.0.0",
42 "dependencies": {
43 "keet-identity-key": "^3.1.0",
44 "keytar": "^7.9.0"
45 },
46 "peerDependencies": {
47 "bare-fs": "*",
48 "bare-path": "*"
49 }
50 },
2751 "node_modules/@adraffy/ens-normalize": {
2852 "version": "1.10.1",
2953 "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz",
……@@ -608,6 +632,7 @@
608632 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.3.tgz",
609633 "integrity": "sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ==",
610634 "license": "Apache-2.0",
635 "peer": true,
611636 "dependencies": {
612637 "bare-events": "^2.5.4",
613638 "bare-path": "^3.0.0",
……@@ -872,6 +897,7 @@
872897 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
873898 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
874899 "license": "Apache-2.0",
900 "peer": true,
875901 "dependencies": {
876902 "bare-os": "^3.0.1"
877903 }
……@@ -1243,6 +1269,26 @@
12431269 "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==",
12441270 "license": "MIT"
12451271 },
1272 "node_modules/base64-js": {
1273 "version": "1.5.1",
1274 "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
1275 "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
1276 "funding": [
1277 {
1278 "type": "github",
1279 "url": "https://github.com/sponsors/feross"
1280 },
1281 {
1282 "type": "patreon",
1283 "url": "https://www.patreon.com/feross"
1284 },
1285 {
1286 "type": "consulting",
1287 "url": "https://feross.org/support"
1288 }
1289 ],
1290 "license": "MIT"
1291 },
12461292 "node_modules/bech32": {
12471293 "version": "2.0.0",
12481294 "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
……@@ -1404,6 +1450,31 @@
14041450 "b4a": "^1.5.0"
14051451 }
14061452 },
1453 "node_modules/bl": {
1454 "version": "4.1.0",
1455 "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
1456 "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
1457 "license": "MIT",
1458 "dependencies": {
1459 "buffer": "^5.5.0",
1460 "inherits": "^2.0.4",
1461 "readable-stream": "^3.4.0"
1462 }
1463 },
1464 "node_modules/bl/node_modules/readable-stream": {
1465 "version": "3.6.2",
1466 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
1467 "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
1468 "license": "MIT",
1469 "dependencies": {
1470 "inherits": "^2.0.3",
1471 "string_decoder": "^1.1.1",
1472 "util-deprecate": "^1.0.1"
1473 },
1474 "engines": {
1475 "node": ">= 6"
1476 }
1477 },
14071478 "node_modules/blind-relay": {
14081479 "version": "1.4.0",
14091480 "resolved": "https://registry.npmjs.org/blind-relay/-/blind-relay-1.4.0.tgz",
……@@ -1475,6 +1546,30 @@
14751546 "bs58": "^5.0.0"
14761547 }
14771548 },
1549 "node_modules/buffer": {
1550 "version": "5.7.1",
1551 "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
1552 "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
1553 "funding": [
1554 {
1555 "type": "github",
1556 "url": "https://github.com/sponsors/feross"
1557 },
1558 {
1559 "type": "patreon",
1560 "url": "https://www.patreon.com/feross"
1561 },
1562 {
1563 "type": "consulting",
1564 "url": "https://feross.org/support"
1565 }
1566 ],
1567 "license": "MIT",
1568 "dependencies": {
1569 "base64-js": "^1.3.1",
1570 "ieee754": "^1.1.13"
1571 }
1572 },
14781573 "node_modules/buffer-equals": {
14791574 "version": "1.0.4",
14801575 "resolved": "https://registry.npmjs.org/buffer-equals/-/buffer-equals-1.0.4.tgz",
……@@ -1546,6 +1641,12 @@
15461641 "node": ">=6"
15471642 }
15481643 },
1644 "node_modules/chownr": {
1645 "version": "1.1.4",
1646 "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
1647 "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
1648 "license": "ISC"
1649 },
15491650 "node_modules/cipher-base": {
15501651 "version": "1.0.7",
15511652 "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz",
……@@ -1746,6 +1847,30 @@
17461847 "node": ">=0.10.0"
17471848 }
17481849 },
1850 "node_modules/decompress-response": {
1851 "version": "6.0.0",
1852 "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
1853 "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
1854 "license": "MIT",
1855 "dependencies": {
1856 "mimic-response": "^3.1.0"
1857 },
1858 "engines": {
1859 "node": ">=10"
1860 },
1861 "funding": {
1862 "url": "https://github.com/sponsors/sindresorhus"
1863 }
1864 },
1865 "node_modules/deep-extend": {
1866 "version": "0.6.0",
1867 "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
1868 "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
1869 "license": "MIT",
1870 "engines": {
1871 "node": ">=4.0.0"
1872 }
1873 },
17491874 "node_modules/define-data-property": {
17501875 "version": "1.1.4",
17511876 "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
……@@ -1763,6 +1888,15 @@
17631888 "url": "https://github.com/sponsors/ljharb"
17641889 }
17651890 },
1891 "node_modules/detect-libc": {
1892 "version": "2.1.2",
1893 "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1894 "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1895 "license": "Apache-2.0",
1896 "engines": {
1897 "node": ">=8"
1898 }
1899 },
17661900 "node_modules/device-file": {
17671901 "version": "2.3.1",
17681902 "resolved": "https://registry.npmjs.org/device-file/-/device-file-2.3.1.tgz",
……@@ -1875,6 +2009,15 @@
18752009 "hyperschema": "^1.19.0"
18762010 }
18772011 },
2012 "node_modules/end-of-stream": {
2013 "version": "1.4.5",
2014 "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
2015 "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
2016 "license": "MIT",
2017 "dependencies": {
2018 "once": "^1.4.0"
2019 }
2020 },
18782021 "node_modules/es-define-property": {
18792022 "version": "1.0.1",
18802023 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
……@@ -1976,6 +2119,15 @@
19762119 "safe-buffer": "^5.1.1"
19772120 }
19782121 },
2122 "node_modules/expand-template": {
2123 "version": "2.0.3",
2124 "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
2125 "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
2126 "license": "(MIT OR WTFPL)",
2127 "engines": {
2128 "node": ">=6"
2129 }
2130 },
19792131 "node_modules/fast-fifo": {
19802132 "version": "1.3.2",
19812133 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
……@@ -2044,6 +2196,12 @@
20442196 "streamx": "^2.13.0"
20452197 }
20462198 },
2199 "node_modules/fs-constants": {
2200 "version": "1.0.0",
2201 "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
2202 "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
2203 "license": "MIT"
2204 },
20472205 "node_modules/fs-native-extensions": {
20482206 "version": "1.4.5",
20492207 "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.4.5.tgz",
……@@ -2136,6 +2294,12 @@
21362294 "node": ">= 0.4"
21372295 }
21382296 },
2297 "node_modules/github-from-package": {
2298 "version": "0.0.0",
2299 "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
2300 "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
2301 "license": "MIT"
2302 },
21392303 "node_modules/gopd": {
21402304 "version": "1.2.0",
21412305 "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
……@@ -2235,6 +2399,10 @@
22352399 "minimalistic-crypto-utils": "^1.0.1"
22362400 }
22372401 },
2402 "node_modules/hyper-wallet-lib": {
2403 "resolved": "lib/hyper-wallet-lib",
2404 "link": true
2405 },
22382406 "node_modules/hyperbee": {
22392407 "version": "2.27.3",
22402408 "resolved": "https://registry.npmjs.org/hyperbee/-/hyperbee-2.27.3.tgz",
……@@ -2419,6 +2587,26 @@
24192587 "unslab": "^1.3.0"
24202588 }
24212589 },
2590 "node_modules/ieee754": {
2591 "version": "1.2.1",
2592 "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
2593 "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
2594 "funding": [
2595 {
2596 "type": "github",
2597 "url": "https://github.com/sponsors/feross"
2598 },
2599 {
2600 "type": "patreon",
2601 "url": "https://www.patreon.com/feross"
2602 },
2603 {
2604 "type": "consulting",
2605 "url": "https://feross.org/support"
2606 }
2607 ],
2608 "license": "BSD-3-Clause"
2609 },
24222610 "node_modules/index-encoder": {
24232611 "version": "3.4.0",
24242612 "resolved": "https://registry.npmjs.org/index-encoder/-/index-encoder-3.4.0.tgz",
……@@ -2434,6 +2622,12 @@
24342622 "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
24352623 "license": "ISC"
24362624 },
2625 "node_modules/ini": {
2626 "version": "1.3.8",
2627 "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
2628 "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
2629 "license": "ISC"
2630 },
24372631 "node_modules/is-callable": {
24382632 "version": "1.2.7",
24392633 "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
……@@ -2514,6 +2708,17 @@
25142708 "sodium-universal": "^5.0.1"
25152709 }
25162710 },
2711 "node_modules/keytar": {
2712 "version": "7.9.0",
2713 "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz",
2714 "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==",
2715 "hasInstallScript": true,
2716 "license": "MIT",
2717 "dependencies": {
2718 "node-addon-api": "^4.3.0",
2719 "prebuild-install": "^7.0.1"
2720 }
2721 },
25172722 "node_modules/locate-path": {
25182723 "version": "5.0.0",
25192724 "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
……@@ -2561,6 +2766,18 @@
25612766 "safe-buffer": "^5.1.2"
25622767 }
25632768 },
2769 "node_modules/mimic-response": {
2770 "version": "3.1.0",
2771 "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
2772 "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
2773 "license": "MIT",
2774 "engines": {
2775 "node": ">=10"
2776 },
2777 "funding": {
2778 "url": "https://github.com/sponsors/sindresorhus"
2779 }
2780 },
25642781 "node_modules/minimalistic-assert": {
25652782 "version": "1.0.1",
25662783 "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
……@@ -2573,6 +2790,21 @@
25732790 "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
25742791 "license": "MIT"
25752792 },
2793 "node_modules/minimist": {
2794 "version": "1.2.8",
2795 "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
2796 "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
2797 "license": "MIT",
2798 "funding": {
2799 "url": "https://github.com/sponsors/ljharb"
2800 }
2801 },
2802 "node_modules/mkdirp-classic": {
2803 "version": "0.5.3",
2804 "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
2805 "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
2806 "license": "MIT"
2807 },
25762808 "node_modules/mutexify": {
25772809 "version": "1.4.0",
25782810 "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz",
……@@ -2594,12 +2826,36 @@
25942826 "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==",
25952827 "license": "ISC"
25962828 },
2829 "node_modules/napi-build-utils": {
2830 "version": "2.0.0",
2831 "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
2832 "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
2833 "license": "MIT"
2834 },
25972835 "node_modules/nat-sampler": {
25982836 "version": "1.0.1",
25992837 "resolved": "https://registry.npmjs.org/nat-sampler/-/nat-sampler-1.0.1.tgz",
26002838 "integrity": "sha512-yQvyNN7xbqR8crTKk3U8gRgpcV1Az+vfCEijiHu9oHHsnIl8n3x+yXNHl42M6L3czGynAVoOT9TqBfS87gDdcw==",
26012839 "license": "MIT"
26022840 },
2841 "node_modules/node-abi": {
2842 "version": "3.87.0",
2843 "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
2844 "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
2845 "license": "MIT",
2846 "dependencies": {
2847 "semver": "^7.3.5"
2848 },
2849 "engines": {
2850 "node": ">=10"
2851 }
2852 },
2853 "node_modules/node-addon-api": {
2854 "version": "4.3.0",
2855 "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
2856 "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
2857 "license": "MIT"
2858 },
26032859 "node_modules/noise-curve-ed": {
26042860 "version": "2.1.0",
26052861 "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz",
……@@ -2622,6 +2878,15 @@
26222878 "sodium-universal": "^5.0.0"
26232879 }
26242880 },
2881 "node_modules/once": {
2882 "version": "1.4.0",
2883 "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
2884 "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
2885 "license": "ISC",
2886 "dependencies": {
2887 "wrappy": "1"
2888 }
2889 },
26252890 "node_modules/p-limit": {
26262891 "version": "7.1.1",
26272892 "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.1.1.tgz",
……@@ -2739,6 +3004,32 @@
27393004 "node": ">= 0.4"
27403005 }
27413006 },
3007 "node_modules/prebuild-install": {
3008 "version": "7.1.3",
3009 "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
3010 "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
3011 "license": "MIT",
3012 "dependencies": {
3013 "detect-libc": "^2.0.0",
3014 "expand-template": "^2.0.3",
3015 "github-from-package": "0.0.0",
3016 "minimist": "^1.2.3",
3017 "mkdirp-classic": "^0.5.3",
3018 "napi-build-utils": "^2.0.0",
3019 "node-abi": "^3.3.0",
3020 "pump": "^3.0.0",
3021 "rc": "^1.2.7",
3022 "simple-get": "^4.0.0",
3023 "tar-fs": "^2.0.0",
3024 "tunnel-agent": "^0.6.0"
3025 },
3026 "bin": {
3027 "prebuild-install": "bin.js"
3028 },
3029 "engines": {
3030 "node": ">=10"
3031 }
3032 },
27423033 "node_modules/process-nextick-args": {
27433034 "version": "2.0.1",
27443035 "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
……@@ -2781,6 +3072,16 @@
27813072 "protomux": "^3.10.1"
27823073 }
27833074 },
3075 "node_modules/pump": {
3076 "version": "3.0.3",
3077 "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
3078 "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
3079 "license": "MIT",
3080 "dependencies": {
3081 "end-of-stream": "^1.1.0",
3082 "once": "^1.3.1"
3083 }
3084 },
27843085 "node_modules/punycode": {
27853086 "version": "2.3.1",
27863087 "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
……@@ -2881,6 +3182,21 @@
28813182 "safe-buffer": "^5.1.0"
28823183 }
28833184 },
3185 "node_modules/rc": {
3186 "version": "1.2.8",
3187 "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
3188 "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
3189 "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
3190 "dependencies": {
3191 "deep-extend": "^0.6.0",
3192 "ini": "~1.3.0",
3193 "minimist": "^1.2.0",
3194 "strip-json-comments": "~2.0.1"
3195 },
3196 "bin": {
3197 "rc": "cli.js"
3198 }
3199 },
28843200 "node_modules/readable-stream": {
28853201 "version": "2.3.8",
28863202 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
……@@ -3048,6 +3364,18 @@
30483364 "node": ">=4.0.0"
30493365 }
30503366 },
3367 "node_modules/semver": {
3368 "version": "7.7.3",
3369 "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
3370 "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
3371 "license": "ISC",
3372 "bin": {
3373 "semver": "bin/semver.js"
3374 },
3375 "engines": {
3376 "node": ">=10"
3377 }
3378 },
30513379 "node_modules/set-blocking": {
30523380 "version": "2.0.0",
30533381 "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
……@@ -3138,6 +3466,51 @@
31383466 "simdle-native": "^1.1.1"
31393467 }
31403468 },
3469 "node_modules/simple-concat": {
3470 "version": "1.0.1",
3471 "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
3472 "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
3473 "funding": [
3474 {
3475 "type": "github",
3476 "url": "https://github.com/sponsors/feross"
3477 },
3478 {
3479 "type": "patreon",
3480 "url": "https://www.patreon.com/feross"
3481 },
3482 {
3483 "type": "consulting",
3484 "url": "https://feross.org/support"
3485 }
3486 ],
3487 "license": "MIT"
3488 },
3489 "node_modules/simple-get": {
3490 "version": "4.0.1",
3491 "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
3492 "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
3493 "funding": [
3494 {
3495 "type": "github",
3496 "url": "https://github.com/sponsors/feross"
3497 },
3498 {
3499 "type": "patreon",
3500 "url": "https://www.patreon.com/feross"
3501 },
3502 {
3503 "type": "consulting",
3504 "url": "https://feross.org/support"
3505 }
3506 ],
3507 "license": "MIT",
3508 "dependencies": {
3509 "decompress-response": "^6.0.0",
3510 "once": "^1.3.1",
3511 "simple-concat": "^1.0.0"
3512 }
3513 },
31413514 "node_modules/sodium-hmac": {
31423515 "version": "2.1.0",
31433516 "resolved": "https://registry.npmjs.org/sodium-hmac/-/sodium-hmac-2.1.0.tgz",
……@@ -3240,6 +3613,15 @@
32403613 "node": ">=8"
32413614 }
32423615 },
3616 "node_modules/strip-json-comments": {
3617 "version": "2.0.1",
3618 "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
3619 "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
3620 "license": "MIT",
3621 "engines": {
3622 "node": ">=0.10.0"
3623 }
3624 },
32433625 "node_modules/sub-encoder": {
32443626 "version": "2.1.3",
32453627 "resolved": "https://registry.npmjs.org/sub-encoder/-/sub-encoder-2.1.3.tgz",
……@@ -3250,6 +3632,48 @@
32503632 "codecs": "^3.1.0"
32513633 }
32523634 },
3635 "node_modules/tar-fs": {
3636 "version": "2.1.4",
3637 "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
3638 "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
3639 "license": "MIT",
3640 "dependencies": {
3641 "chownr": "^1.1.1",
3642 "mkdirp-classic": "^0.5.2",
3643 "pump": "^3.0.0",
3644 "tar-stream": "^2.1.4"
3645 }
3646 },
3647 "node_modules/tar-stream": {
3648 "version": "2.2.0",
3649 "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
3650 "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
3651 "license": "MIT",
3652 "dependencies": {
3653 "bl": "^4.0.3",
3654 "end-of-stream": "^1.4.1",
3655 "fs-constants": "^1.0.0",
3656 "inherits": "^2.0.3",
3657 "readable-stream": "^3.1.1"
3658 },
3659 "engines": {
3660 "node": ">=6"
3661 }
3662 },
3663 "node_modules/tar-stream/node_modules/readable-stream": {
3664 "version": "3.6.2",
3665 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
3666 "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
3667 "license": "MIT",
3668 "dependencies": {
3669 "inherits": "^2.0.3",
3670 "string_decoder": "^1.1.1",
3671 "util-deprecate": "^1.0.1"
3672 },
3673 "engines": {
3674 "node": ">= 6"
3675 }
3676 },
32533677 "node_modules/text-decoder": {
32543678 "version": "1.2.3",
32553679 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
……@@ -3318,6 +3742,18 @@
33183742 "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
33193743 "license": "0BSD"
33203744 },
3745 "node_modules/tunnel-agent": {
3746 "version": "0.6.0",
3747 "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
3748 "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
3749 "license": "Apache-2.0",
3750 "dependencies": {
3751 "safe-buffer": "^5.0.1"
3752 },
3753 "engines": {
3754 "node": "*"
3755 }
3756 },
33213757 "node_modules/typed-array-buffer": {
33223758 "version": "1.0.3",
33233759 "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
……@@ -3480,6 +3916,12 @@
34803916 "node": ">=8"
34813917 }
34823918 },
3919 "node_modules/wrappy": {
3920 "version": "1.0.2",
3921 "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
3922 "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
3923 "license": "ISC"
3924 },
34833925 "node_modules/ws": {
34843926 "version": "8.17.1",
34853927 "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
wallet-server.cjs +12 -12
……@@ -42,7 +42,7 @@ const {
4242 removeAddressFromContact,
4343 // Transaction syncing
4444 syncTransactionsFromWDK
45} = require('./lib/wallet-storage.cjs')
45} = require('./lib/hyper-wallet-lib/wallet-storage.cjs')
4646
4747const IdentityKey = require('keet-identity-key')
4848const b4a = require('b4a')
……@@ -52,39 +52,39 @@ const {
5252 generateWalletMnemonic,
5353 saveWalletMnemonic,
5454 initializeWDK
55} = require('./lib/wdk-integration.cjs')
55} = require('./lib/hyper-wallet-lib/wdk-integration.cjs')
5656
57const { loadConfig } = require('./lib/config-loader.cjs')
58const { getConfig, updateConfig, saveConfig, DEFAULT_CONFIG } = require('./lib/config.cjs')
59const { getAvailableProviders, getProviderInfo } = require('./lib/blockchain-providers.cjs')
57const { loadConfig } = require('./lib/hyper-wallet-lib/config-loader.cjs')
58const { getConfig, updateConfig, saveConfig, DEFAULT_CONFIG } = require('./lib/hyper-wallet-lib/config.cjs')
59const { getAvailableProviders, getProviderInfo } = require('./lib/hyper-wallet-lib/blockchain-providers.cjs')
6060const {
6161 generateToken,
6262 saveToken,
6363 loadToken,
6464 verifyToken,
6565 tokenExists
66} = require('./lib/auth.cjs')
66} = require('./lib/hyper-wallet-lib/auth.cjs')
6767
6868const {
6969 encrypt,
7070 decrypt,
7171 isValidFormat
72} = require('./lib/encryption.cjs')
72} = require('./lib/hyper-wallet-lib/encryption.cjs')
7373
7474const {
7575 checkRateLimit,
7676 getRateLimitStatus
77} = require('./lib/rate-limiter.cjs')
77} = require('./lib/hyper-wallet-lib/rate-limiter.cjs')
7878
7979const {
8080 validateAddress
81} = require('./lib/address-validator.cjs')
81} = require('./lib/hyper-wallet-lib/address-validator.cjs')
8282
8383const {
8484 validateAmount,
8585 validateSufficientBalance,
8686 formatAmount
87} = require('./lib/amount-validator.cjs')
87} = require('./lib/hyper-wallet-lib/amount-validator.cjs')
8888
8989const {
9090 sanitizeContactName,
……@@ -93,7 +93,7 @@ const {
9393 validateChain,
9494 validateNetwork,
9595 validateInviteCode
96} = require('./lib/input-sanitizer.cjs')
96} = require('./lib/hyper-wallet-lib/input-sanitizer.cjs')
9797
9898// Load configuration from file
9999const config = loadConfig()
……@@ -505,7 +505,7 @@ const routes = {
505505 return sendError(res, 'Wallet not initialized', 400)
506506 }
507507
508 const { initializeWDK } = require('./lib/wdk-integration.cjs')
508 const { initializeWDK } = require('./lib/hyper-wallet-lib/wdk-integration.cjs')
509509 const { wdk } = await initializeWDK(walletMnemonic, { testnet: true })
510510
511511 // Sync transactions