// lib/wallet-context.js - Shared Wallet State
//
// Manages shared state across all protocols:
// - Identity (keet-identity-key)
// - Storage (Corestore)
// - Database (Hyperbee)
// - Transaction history (Autobase)
import IdentityKey from './identity.js'
import Corestore from 'corestore'
import Hyperbee from 'hyperbee'
import Autobase from 'autobase'
import path from '#path'
import os from '#os'
class WalletContext {
/**
* Create wallet context
* @param {Object} config - Configuration object
*/
constructor(config) {
this.config = config
this.protocols = {} // Initialized protocol instances
this.identity = null
this.store = null
this.db = null
this.txHistory = null
}
/**
* Initialize wallet with mnemonic
* @param {String} mnemonic - BIP39 mnemonic phrase
* @returns {WalletContext} - This instance
*/
async init(mnemonic) {
// 1. Initialize identity (keet-identity-key)
this.identity = await IdentityKey.from({ mnemonic })
// 2. Initialize storage (Corestore)
const storagePath = this.resolveStoragePath(this.config.storagePath)
// Note: Using identity seed as primaryKey for deterministic key derivation
this.store = new Corestore(storagePath, {
primaryKey: this.identity.seed,
unsafe: true // Acknowledge we're intentionally using a custom key
})
await this.store.ready()
// 3. Initialize database (Hyperbee)
const dbCore = this.store.get({ name: 'wallet-db' })
this.db = new Hyperbee(dbCore, {
keyEncoding: 'utf-8',
valueEncoding: 'json'
})
await this.db.ready()
// 4. Initialize transaction history (Autobase)
this.txHistory = new Autobase(this.store, null, {
apply: this.applyTransaction.bind(this),
open: (store) => store.get({ name: 'tx-log' })
})
await this.txHistory.ready()
console.log('✅ Wallet context initialized')
console.log(` Storage: ${storagePath}`)
console.log(` Identity: ${this.identity.identityPublicKey.toString('hex').slice(0, 16)}...`)
return this
}
/**
* Apply transaction to Autobase view
* This is called automatically by Autobase when syncing across devices
*/
async applyTransaction(nodes, view, base) {
for (const node of nodes) {
try {
const tx = JSON.parse(node.value.toString())
await view.append(JSON.stringify(tx))
} catch (err) {
console.error('Failed to apply transaction:', err)
}
}
}
/**
* Load enabled protocols
* @param {Object} enabled - Enabled protocol objects
*/
async loadProtocols(enabled) {
console.log('🔌 Loading protocols...')
const failures = []
for (const [name, protocol] of Object.entries(enabled)) {
try {
this.protocols[name] = await protocol.init(this)
console.log(` ✅ ${name}`)
} catch (err) {
failures.push({ protocol: name, error: err.message })
console.error(` ❌ ${name}: ${err.message}`)
}
}
// Check if any protocols loaded successfully
if (Object.keys(this.protocols).length === 0) {
console.error('')
console.error('❌ All protocols failed to initialize')
console.error(' Failures:')
failures.forEach(({ protocol, error }) => {
console.error(` - ${protocol}: ${error}`)
})
throw new Error('No protocols available')
}
// Warn about partial failures
if (failures.length > 0) {
console.warn('')
console.warn('⚠️ Some protocols failed to load:')
failures.forEach(({ protocol, error }) => {
console.warn(` - ${protocol}: ${error}`)
})
console.warn(` ${Object.keys(this.protocols).length} protocol(s) loaded successfully`)
} else {
console.log('✅ All protocols loaded')
}
}
/**
* Get balances from all protocols
* @returns {Object} - { protocolName: balance }
*/
async getBalances() {
const balances = {}
for (const [name, protocol] of Object.entries(this.protocols)) {
try {
balances[name] = await protocol.getBalance()
} catch (err) {
balances[name] = { error: err.message }
}
}
return balances
}
/**
* Check if device is authorized
* @param {Buffer} devicePublicKey - Device public key
* @returns {Boolean} - True if authorized
*/
async isDeviceAuthorized(devicePublicKey) {
// Get device proof
const deviceId = devicePublicKey.toString('hex').slice(0, 8)
const device = await this.db.get(`devices/${deviceId}`)
if (!device) return false
// Verify proof is valid
const verified = IdentityKey.verify(device.value.proof)
if (!verified) return false
// Check revocation list
const revoked = await this.db.get('devices/revoked')
if (revoked && revoked.value) {
const isRevoked = revoked.value.some(r =>
r.publicKey.equals(devicePublicKey)
)
if (isRevoked) return false
}
return true
}
/**
* Resolve storage path (expand ~)
* @param {String} storagePath - Path from config
* @returns {String} - Absolute path
*/
resolveStoragePath(storagePath) {
if (storagePath.startsWith('~')) {
return path.join(os.homedir(), storagePath.slice(1))
}
return storagePath
}
/**
* Close wallet (cleanup resources)
*/
async close() {
if (this.txHistory) await this.txHistory.close()
if (this.db) await this.db.close()
if (this.store) await this.store.close()
}
}
export default WalletContext