minimal-xec-wallet
Version:
A minimalist eCash (XEC) wallet npm library, for use in web apps. Supports eTokens.
977 lines (831 loc) • 31.5 kB
JavaScript
/*
An npm JavaScript library for front end web apps. Implements a minimal
eCash (XEC) wallet with eToken support.
*/
/* eslint-disable no-async-promise-executor */
const { ChronikClient } = require('chronik-client')
const crypto = require('crypto-js')
// Local libraries
const SendXEC = require('./lib/send-xec')
const Utxos = require('./lib/utxos')
const AdapterRouter = require('./lib/adapters/router')
const OpReturn = require('./lib/op-return')
const ConsolidateUtxos = require('./lib/consolidate-utxos.js')
const KeyDerivation = require('./lib/key-derivation')
const HybridTokenManager = require('./lib/hybrid-token-manager')
// let this
class MinimalXECWallet {
constructor (hdPrivateKeyOrMnemonic, advancedOptions = {}) {
this.advancedOptions = advancedOptions
// BEGIN Handle advanced options.
// HD Derivation path for XEC (coin type 899)
this.hdPath = this.advancedOptions.hdPath || "m/44'/899'/0'/0/0"
// Default Chronik endpoints (working as of 2025)
const chronikOptions = {
chronikUrls: advancedOptions.chronikUrls || [
'https://chronik.e.cash',
'https://chronik.be.cash',
'https://xec.paybutton.org',
'https://chronik.pay2stay.com/xec',
'https://chronik.pay2stay.com/xec2',
'https://chronik1.alitayin.com',
'https://chronik2.alitayin.com'
]
}
// Set the fee rate (XEC uses same structure as BCH, but lower amounts)
this.fee = 1.2
if (this.advancedOptions.fee) {
this.fee = this.advancedOptions.fee
}
// Donation setting (defaults to false for security and user consent)
this.enableDonations = this.advancedOptions.enableDonations || false
// END Handle advanced options.
// Encapsulate the external libraries.
this.crypto = crypto
this.ChronikClient = ChronikClient
// Initialize key derivation
this.keyDerivation = new KeyDerivation()
// Initialize chronik client with fallback strategy - use first endpoint immediately
// The adapter router will handle connection strategy internally
this.chronik = new ChronikClient(chronikOptions.chronikUrls[0])
chronikOptions.chronik = this.chronik
// Instantiate the adapter router (it handles connection strategy internally)
this.ar = new AdapterRouter(chronikOptions)
chronikOptions.ar = this.ar
// Instantiate local libraries
this.sendXecLib = new SendXEC(chronikOptions)
this.utxos = new Utxos(chronikOptions)
this.opReturn = new OpReturn(chronikOptions)
this.consolidateUtxos = new ConsolidateUtxos(this)
this.hybridTokens = new HybridTokenManager(chronikOptions)
this.temp = []
this.isInitialized = false
// The create() function returns a promise. When it resolves, the
// walletInfoCreated flag will be set to true. The instance will also
// have a new `walletInfo` property that will contain the wallet information.
this.walletInfoCreated = false
this.walletInfoPromise = this.create(hdPrivateKeyOrMnemonic)
// Initialize WebAssembly early for better browser compatibility
this.wasmInitPromise = this._initializeWASM()
// Bind the 'this' object to all functions
this.create = this.create.bind(this)
this.initialize = this.initialize.bind(this)
this.getUtxos = this.getUtxos.bind(this)
this.getXecBalance = this.getXecBalance.bind(this)
this.getDetailedBalance = this.getDetailedBalance.bind(this)
this.getTransactions = this.getTransactions.bind(this)
this.getTxData = this.getTxData.bind(this)
this.sendXec = this.sendXec.bind(this)
this.sendETokens = this.sendETokens.bind(this) // Phase 2
this.burnETokens = this.burnETokens.bind(this) // Phase 2
this.listETokens = this.listETokens.bind(this) // Phase 2
this.sendAllXec = this.sendAllXec.bind(this)
this.burnAllETokens = this.burnAllETokens.bind(this) // Phase 2
this.getXecUsd = this.getXecUsd.bind(this)
this.sendOpReturn = this.sendOpReturn.bind(this)
this.utxoIsValid = this.utxoIsValid.bind(this)
this.getETokenData = this.getETokenData.bind(this) // Phase 2
this.getKeyPair = this.getKeyPair.bind(this)
this.optimize = this.optimize.bind(this)
this.getETokenBalance = this.getETokenBalance.bind(this) // Phase 2
this.getPubKey = this.getPubKey.bind(this)
this.broadcast = this.broadcast.bind(this)
this.cid2json = this.cid2json.bind(this)
this._validateAddress = this._validateAddress.bind(this)
this._sanitizeError = this._sanitizeError.bind(this)
this._secureWalletInfo = this._secureWalletInfo.bind(this)
this.exportPrivateKeyAsWIF = this.exportPrivateKeyAsWIF.bind(this)
this.validateWIF = this.validateWIF.bind(this)
}
// Private method to validate XEC addresses
_validateAddress (address) {
try {
if (!address || typeof address !== 'string') {
throw new Error('Address must be a non-empty string')
}
// Allow test addresses in test environment
if ((process.env.NODE_ENV === 'test' || process.env.TEST === 'unit') && address.startsWith('test-')) {
return true
}
// Only allow eCash addresses (ecash: prefix)
if (!address.startsWith('ecash:')) {
throw new Error('Invalid address format - must be eCash address (ecash: prefix)')
}
// Use ecashaddrjs to validate the eCash address
const { decodeCashAddress } = require('ecashaddrjs')
decodeCashAddress(address)
return true
} catch (err) {
throw new Error(`Address validation failed: ${err.message}`)
}
}
// Private method to sanitize error messages
_sanitizeError (error, context = '') {
const safeMessage = error.message || 'An error occurred'
// Remove potentially sensitive information from error messages
const sanitized = safeMessage
.replace(/[A-Za-z0-9+/=]{64,}/g, '[SENSITIVE_DATA_REMOVED]')
.replace(/[LK][1-9A-HJ-NP-Za-km-z]{51}/g, '[PRIVATE_KEY_REMOVED]')
.replace(/ecash:[a-z0-9]{42}/g, '[ADDRESS_REMOVED]')
return new Error(`${context ? context + ': ' : ''}${sanitized}`)
}
// Private method to create secure wallet info object
_secureWalletInfo (walletInfo) {
// Create a copy without exposing sensitive data directly
return {
mnemonic: walletInfo.mnemonic,
xecAddress: walletInfo.xecAddress,
hdPath: walletInfo.hdPath,
fee: this.fee,
// Store private key securely - consider implementing memory protection
privateKey: walletInfo.privateKey,
// Include donation setting (defaults to false for security)
enableDonations: this.advancedOptions.enableDonations || false
}
}
// Create a new wallet. Returns a promise that resolves into a wallet object.
async create (mnemonicOrWif) {
try {
// Attempt to decrypt mnemonic if password is provided.
if (mnemonicOrWif && this.advancedOptions.password) {
mnemonicOrWif = this.decrypt(
mnemonicOrWif,
this.advancedOptions.password
)
}
const walletInfo = {}
// No input. Generate a new mnemonic.
if (!mnemonicOrWif) {
// Generate new mnemonic using key derivation library
const mnemonic = this._generateMnemonic()
const { privateKey, publicKey, address } = this._deriveFromMnemonic(mnemonic)
walletInfo.privateKey = privateKey
walletInfo.publicKey = publicKey
walletInfo.mnemonic = mnemonic
walletInfo.xecAddress = address
walletInfo.hdPath = this.hdPath
} else {
// A WIF will start with L, K, 5 (mainnet) or c, 9 (testnet), will have no spaces,
// and will be 51-52 characters long.
const startsWithWIFChar =
mnemonicOrWif &&
(['k', 'l', 'c', '5', '9'].includes(mnemonicOrWif[0].toString().toLowerCase()))
const isWIFLength = mnemonicOrWif && (mnemonicOrWif.length === 51 || mnemonicOrWif.length === 52)
if (startsWithWIFChar && isWIFLength) {
// Enhanced WIF Private Key handling
if (!this.keyDerivation._isValidWIF(mnemonicOrWif)) {
throw new Error('Invalid WIF format or checksum')
}
const { privateKey, publicKey, address, isCompressed, wif } = this._deriveFromWif(mnemonicOrWif)
walletInfo.privateKey = privateKey
walletInfo.publicKey = publicKey
walletInfo.mnemonic = null
walletInfo.xecAddress = address
walletInfo.hdPath = null
walletInfo.isCompressed = isCompressed
walletInfo.wif = wif
} else if (mnemonicOrWif.length === 64 && /^[a-fA-F0-9]+$/.test(mnemonicOrWif)) {
// Hex Private Key (64 characters, all hex)
const { publicKey, address } = this._deriveFromWif(mnemonicOrWif)
walletInfo.privateKey = mnemonicOrWif
walletInfo.publicKey = publicKey
walletInfo.mnemonic = null
walletInfo.xecAddress = address
walletInfo.hdPath = null
} else {
// 12-word Mnemonic
const mnemonic = mnemonicOrWif
const { privateKey, publicKey, address } = this._deriveFromMnemonic(mnemonic)
walletInfo.privateKey = privateKey
walletInfo.publicKey = publicKey
walletInfo.mnemonic = mnemonic
walletInfo.xecAddress = address
walletInfo.hdPath = this.hdPath
}
}
// Encrypt the mnemonic if a password is provided.
if (this.advancedOptions.password && walletInfo.mnemonic) {
walletInfo.mnemonicEncrypted = this.encrypt(
walletInfo.mnemonic,
this.advancedOptions.password
)
}
this.walletInfoCreated = true
this.walletInfo = walletInfo
return walletInfo
} catch (err) {
throw this._sanitizeError(err, 'Wallet creation failed')
}
}
// Helper method to generate mnemonic
_generateMnemonic (strength = 128) {
try {
return this.keyDerivation.generateMnemonic(strength)
} catch (err) {
throw this._sanitizeError(err, 'Mnemonic generation failed')
}
}
// Helper method to derive keys from mnemonic
_deriveFromMnemonic (mnemonic) {
try {
return this.keyDerivation.deriveFromMnemonic(mnemonic, this.hdPath)
} catch (err) {
throw this._sanitizeError(err, 'HD derivation failed')
}
}
// Helper method to derive keys from WIF
_deriveFromWif (wif) {
try {
return this.keyDerivation.deriveFromWif(wif)
} catch (err) {
throw this._sanitizeError(err, 'WIF derivation failed')
}
}
// Initialize WebAssembly for browser compatibility
async _initializeWASM () {
try {
// Only initialize WASM in browser environments
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
// Dynamic import to avoid issues in Node.js environments
const wasmShim = require('./browser-shims/ecash_lib_wasm_browser')
if (wasmShim && wasmShim.init) {
console.log('Initializing WebAssembly for browser compatibility...')
await wasmShim.init()
console.log('WebAssembly initialization completed')
}
}
return true
} catch (err) {
// WASM initialization failure should not prevent wallet from working
console.warn('WebAssembly initialization failed (using fallbacks):', err.message)
return false
}
}
// Initialize is called to initialize the UTXO store, download token data, and
// get a balance of the wallet.
async initialize () {
await this.walletInfoPromise
// Ensure WASM is initialized (but don't block on it)
try {
await this.wasmInitPromise
} catch (err) {
console.warn('WASM initialization incomplete, continuing with fallbacks')
}
await this.utxos.initUtxoStore(this.walletInfo.xecAddress)
this.isInitialized = true
return true
}
// Encrypt the mnemonic of the wallet using secure key derivation.
encrypt (mnemonic, password) {
try {
// Validate inputs
if (!mnemonic || typeof mnemonic !== 'string') {
throw new Error('Invalid mnemonic provided for encryption')
}
if (!password || typeof password !== 'string' || password.length < 8) {
throw new Error('Password must be at least 8 characters long')
}
// Generate a random salt
const salt = this.crypto.lib.WordArray.random(256 / 8)
// Use PBKDF2 for key derivation with 10000 iterations
const key = this.crypto.PBKDF2(password, salt, {
keySize: 256 / 32,
iterations: 10000
})
// Generate random IV
const iv = this.crypto.lib.WordArray.random(128 / 8)
// Encrypt with AES-256-CBC
const encrypted = this.crypto.AES.encrypt(mnemonic, key, {
iv: iv,
mode: this.crypto.mode.CBC,
padding: this.crypto.pad.Pkcs7
})
// Combine salt, IV, and encrypted data
const combined = {
salt: salt.toString(),
iv: iv.toString(),
encrypted: encrypted.toString()
}
return JSON.stringify(combined)
} catch (err) {
throw new Error(`Encryption failed: ${err.message}`)
}
}
// Decrypt the mnemonic of the wallet using secure key derivation.
decrypt (mnemonicEncrypted, password) {
try {
// Validate inputs
if (!mnemonicEncrypted || typeof mnemonicEncrypted !== 'string') {
throw new Error('Invalid encrypted data provided')
}
if (!password || typeof password !== 'string') {
throw new Error('Password is required for decryption')
}
// Check if it's the old CryptoJS format (starts with base64 "U2FsdGVkX1")
if (mnemonicEncrypted.startsWith('U2FsdGVkX1')) {
return this._decryptLegacyFormat(mnemonicEncrypted, password)
}
// Parse the new encrypted data format
let combined
try {
combined = JSON.parse(mnemonicEncrypted)
} catch (parseErr) {
throw new Error('Invalid encrypted data format')
}
if (!combined.salt || !combined.iv || !combined.encrypted) {
throw new Error('Encrypted data is missing required components')
}
// Recreate the key using the stored salt
const salt = this.crypto.enc.Hex.parse(combined.salt)
const key = this.crypto.PBKDF2(password, salt, {
keySize: 256 / 32,
iterations: 10000
})
// Parse IV
const iv = this.crypto.enc.Hex.parse(combined.iv)
// Decrypt
const decrypted = this.crypto.AES.decrypt(combined.encrypted, key, {
iv: iv,
mode: this.crypto.mode.CBC,
padding: this.crypto.pad.Pkcs7
})
const mnemonic = decrypted.toString(this.crypto.enc.Utf8)
if (!mnemonic) {
throw new Error('Decryption failed - wrong password or corrupted data')
}
return mnemonic
} catch (err) {
throw new Error(`Decryption failed: ${err.message}`)
}
}
// Decrypt legacy CryptoJS format for backward compatibility
_decryptLegacyFormat (mnemonicEncrypted, password) {
try {
// Use the old CryptoJS format decryption
const decrypted = this.crypto.AES.decrypt(mnemonicEncrypted, password)
const mnemonic = decrypted.toString(this.crypto.enc.Utf8)
if (!mnemonic) {
throw new Error('Wrong password')
}
return mnemonic
} catch (err) {
// Return specific error message for wrong password
if (err.message === 'Wrong password') {
throw err
}
throw new Error('Wrong password')
}
}
// Get the UTXO information for this wallet.
async getUtxos (xecAddress) {
try {
let addr = xecAddress
// Validate address if provided
if (xecAddress) {
this._validateAddress(xecAddress)
}
// If no address is passed in, but the wallet has been initialized, use the
// wallet's address.
if (!xecAddress && this.walletInfo && this.walletInfo.xecAddress) {
addr = this.walletInfo.xecAddress
await this.utxos.initUtxoStore(addr)
return this.ar.getUtxos(addr)
}
if (!addr) {
throw new Error('No address provided and wallet not initialized')
}
const utxos = await this.ar.getUtxos(addr)
return utxos
} catch (err) {
throw this._sanitizeError(err, 'Failed to get UTXOs')
}
}
// Get the balance of the wallet in XEC.
async getXecBalance (inObj = {}) {
try {
// Handle backward compatibility: if inObj is a string, treat it as xecAddress
let xecAddress
if (typeof inObj === 'string') {
xecAddress = inObj
} else {
xecAddress = inObj.xecAddress
}
let addr = xecAddress
// Validate address if provided
if (xecAddress) {
this._validateAddress(xecAddress)
}
// If no address is passed in, but the wallet has been initialized, use the
// wallet's address.
if (!xecAddress && this.walletInfo && this.walletInfo.xecAddress) {
addr = this.walletInfo.xecAddress
}
if (!addr) {
throw new Error('No address provided and wallet not initialized')
}
const balances = await this.ar.getBalance(addr)
// Convert from satoshis to XEC (divide by 100, not 100,000,000 like BCH)
return (balances.balance.confirmed + balances.balance.unconfirmed) / 100
} catch (err) {
throw this._sanitizeError(err, 'Failed to get XEC balance')
}
}
// Get detailed balance information including confirmed and unconfirmed amounts
async getDetailedBalance (inObj = {}) {
try {
// Handle backward compatibility: if inObj is a string, treat it as xecAddress
let xecAddress
if (typeof inObj === 'string') {
xecAddress = inObj
} else {
xecAddress = inObj.xecAddress
}
let addr = xecAddress
// Validate address if provided
if (xecAddress) {
this._validateAddress(xecAddress)
}
// If no address is passed in, but the wallet has been initialized, use the
// wallet's address.
if (!xecAddress && this.walletInfo && this.walletInfo.xecAddress) {
addr = this.walletInfo.xecAddress
}
if (!addr) {
throw new Error('No address provided and wallet not initialized')
}
const balances = await this.ar.getBalance(addr)
// Convert from satoshis to XEC (divide by 100, not 100,000,000 like BCH)
const confirmed = balances.balance.confirmed / 100
const unconfirmed = balances.balance.unconfirmed / 100
const total = confirmed + unconfirmed
return {
confirmed,
unconfirmed,
total,
satoshis: {
confirmed: balances.balance.confirmed,
unconfirmed: balances.balance.unconfirmed,
total: balances.balance.confirmed + balances.balance.unconfirmed
}
}
} catch (err) {
throw this._sanitizeError(err, 'Failed to get detailed balance')
}
}
// Get transactions associated with the wallet.
async getTransactions (xecAddress, sortingOrder = 'DESCENDING') {
let addr = xecAddress
// If no address is passed in, but the wallet has been initialized, use the
// wallet's address.
if (!xecAddress && this.walletInfo && this.walletInfo.xecAddress) {
addr = this.walletInfo.xecAddress
}
const data = await this.ar.getTransactions(addr, sortingOrder)
return data.transactions
}
// Get transaction data for up to 20 TXIDs.
async getTxData (txids = []) {
const data = await this.ar.getTxData(txids)
return data
}
// Send XEC. Returns a promise that resolves into a TXID.
async sendXec (outputs) {
try {
// Wait for wallet to be initialized
await this.walletInfoPromise
if (!this.isInitialized) {
await this.initialize()
}
// Get XEC UTXOs - prefer non-token UTXOs to prevent accidental token burning
const xecOnlyUtxos = this.utxos.utxoStore.xecUtxos.filter(utxo => !utxo.token)
// If no pure XEC UTXOs available, provide helpful error
if (xecOnlyUtxos.length === 0) {
const tokenUtxoCount = this.utxos.utxoStore.xecUtxos.filter(utxo => utxo.token).length
throw new Error(`No pure XEC UTXOs available for transaction. All ${tokenUtxoCount} UTXOs contain tokens. To send XEC, first run wallet.optimize() to consolidate UTXOs and create pure XEC UTXOs, or use sendETokens() if you want to send tokens instead.`)
}
return await this.sendXecLib.sendXec(
outputs,
{
mnemonic: this.walletInfo.mnemonic,
xecAddress: this.walletInfo.xecAddress,
hdPath: this.walletInfo.hdPath,
fee: this.fee,
privateKey: this.walletInfo.privateKey,
publicKey: this.walletInfo.publicKey
},
xecOnlyUtxos
)
} catch (err) {
throw this._sanitizeError(err, 'XEC send failed')
}
}
// Send eTokens. Returns a promise that resolves into a TXID.
async sendETokens (tokenId, outputs, satsPerByte = this.fee) {
try {
// Wait for wallet to be initialized
await this.walletInfoPromise
if (!this.isInitialized) {
await this.initialize()
}
// Validate inputs
if (!tokenId || typeof tokenId !== 'string') {
throw new Error('Token ID is required and must be a string')
}
if (!Array.isArray(outputs) || outputs.length === 0) {
throw new Error('Outputs array is required and cannot be empty')
}
// Ensure UTXOs are loaded before token operations
if (!this.utxos || !this.utxos.utxoStore || !Array.isArray(this.utxos.utxoStore.xecUtxos)) {
throw new Error('Wallet UTXOs not loaded. Try calling initialize() first.')
}
// Use hybrid token manager for protocol detection and routing
return await this.hybridTokens.sendTokens(
tokenId,
outputs,
{
mnemonic: this.walletInfo.mnemonic,
xecAddress: this.walletInfo.xecAddress,
hdPath: this.walletInfo.hdPath,
fee: this.fee,
privateKey: this.walletInfo.privateKey,
publicKey: this.walletInfo.publicKey
},
this.utxos.utxoStore.xecUtxos,
satsPerByte
)
} catch (err) {
throw this._sanitizeError(err, 'eToken send failed')
}
}
// Send all XEC to an address
async sendAllXec (toAddress) {
try {
await this.walletInfoPromise
if (!this.isInitialized) {
await this.initialize()
}
return await this.sendXecLib.sendAllXec(
toAddress,
{
mnemonic: this.walletInfo.mnemonic,
xecAddress: this.walletInfo.xecAddress,
hdPath: this.walletInfo.hdPath,
fee: this.fee,
privateKey: this.walletInfo.privateKey,
publicKey: this.walletInfo.publicKey
},
this.utxos.utxoStore.xecUtxos
)
} catch (err) {
console.error('Error in sendAllXec():', err.message)
throw this._sanitizeError(err, 'Send all XEC failed')
}
}
// Send OP_RETURN transaction
async sendOpReturn (msg = '', prefix = '6d02', xecOutput = [], satsPerByte = 1.0) {
try {
await this.walletInfoPromise
if (!this.isInitialized) {
await this.initialize()
}
// Get XEC UTXOs for OP_RETURN - prefer non-token UTXOs to prevent accidental token burning
const xecOnlyUtxos = this.utxos.utxoStore.xecUtxos.filter(utxo => !utxo.token)
// If no pure XEC UTXOs available, provide helpful error
if (xecOnlyUtxos.length === 0) {
const tokenUtxoCount = this.utxos.utxoStore.xecUtxos.filter(utxo => utxo.token).length
throw new Error(`No pure XEC UTXOs available for OP_RETURN transaction. All ${tokenUtxoCount} UTXOs contain tokens. To send OP_RETURN, first run wallet.optimize() to consolidate UTXOs and create pure XEC UTXOs.`)
}
return await this.opReturn.sendOpReturn(
this.walletInfo,
xecOnlyUtxos,
msg,
prefix,
xecOutput,
satsPerByte
)
} catch (err) {
console.error('Error in sendOpReturn():', err.message)
throw this._sanitizeError(err, 'OP_RETURN send failed')
}
}
// Validate if a UTXO is still spendable
async utxoIsValid (utxo) {
try {
return await this.ar.utxoIsValid(utxo)
} catch (err) {
throw this._sanitizeError(err, 'UTXO validation failed')
}
}
// Get key pair for HD index
async getKeyPair (hdIndex = 0) {
try {
await this.walletInfoPromise
if (!this.walletInfo.mnemonic) {
throw new Error('Wallet does not have a mnemonic. Cannot generate key pair.')
}
const customPath = `m/44'/899'/0'/0/${hdIndex}`
const keyData = this.keyDerivation.deriveFromMnemonic(this.walletInfo.mnemonic, customPath)
return {
hdIndex,
wif: keyData.privateKey, // In real implementation, convert to WIF format
publicKey: keyData.publicKey,
xecAddress: keyData.address
}
} catch (err) {
throw this._sanitizeError(err, 'Key pair generation failed')
}
}
// Optimize wallet by consolidating UTXOs
async optimize (dryRun = false) {
try {
return await this.consolidateUtxos.start({ dryRun })
} catch (err) {
throw this._sanitizeError(err, 'UTXO optimization failed')
}
}
// Get public key for address
async getPubKey (addr) {
try {
return await this.ar.getPubKey(addr)
} catch (err) {
throw this._sanitizeError(err, 'Public key query failed')
}
}
// Broadcast transaction hex
async broadcast (inObj = {}) {
try {
const { hex } = inObj
if (!hex) {
throw new Error('Transaction hex is required')
}
return await this.ar.sendTx(hex)
} catch (err) {
throw this._sanitizeError(err, 'Transaction broadcast failed')
}
}
// Convert CID to JSON
async cid2json (inObj = {}) {
try {
return await this.ar.cid2json(inObj)
} catch (err) {
throw this._sanitizeError(err, 'CID to JSON conversion failed')
}
}
// Get the spot price of XEC in USD.
async getXecUsd () {
try {
return await this.ar.getXecUsd()
} catch (err) {
throw this._sanitizeError(err, 'XEC price query failed')
}
}
// eToken operations - Hybrid SLP/ALP token support
async listETokens (xecAddress) {
try {
// Wait for wallet to be initialized
await this.walletInfoPromise
// Determine address to use
let addr = xecAddress
if (!xecAddress && this.walletInfo && this.walletInfo.xecAddress) {
addr = this.walletInfo.xecAddress
}
if (!addr) {
throw new Error('No address provided and wallet not initialized')
}
// Validate address if provided
if (xecAddress) {
this._validateAddress(xecAddress)
}
// Use hybrid token manager to list tokens from address
return await this.hybridTokens.listTokensFromAddress(addr)
} catch (err) {
throw this._sanitizeError(err, 'eToken listing failed')
}
}
async getETokenBalance (inObj = {}) {
try {
// Wait for wallet to be initialized
await this.walletInfoPromise
// Extract tokenId from input object
const { tokenId, xecAddress } = inObj
if (!tokenId || typeof tokenId !== 'string') {
throw new Error('Token ID is required and must be a string')
}
// Determine address to use
let addr = xecAddress
if (!xecAddress && this.walletInfo && this.walletInfo.xecAddress) {
addr = this.walletInfo.xecAddress
}
if (!addr) {
throw new Error('No address provided and wallet not initialized')
}
// Validate address if provided
if (xecAddress) {
this._validateAddress(xecAddress)
}
// Get UTXOs for the address and calculate balance
const utxos = await this.getUtxos(addr)
return await this.hybridTokens.getTokenBalance(tokenId, utxos.utxos)
} catch (err) {
throw this._sanitizeError(err, 'eToken balance query failed')
}
}
async burnETokens (tokenId, amount, satsPerByte = this.fee) {
try {
// Wait for wallet to be initialized
await this.walletInfoPromise
if (!this.isInitialized) {
await this.initialize()
}
// Validate inputs
if (!tokenId || typeof tokenId !== 'string') {
throw new Error('Token ID is required and must be a string')
}
if (!amount || typeof amount !== 'number' || amount <= 0) {
throw new Error('Amount is required and must be a positive number')
}
// Use hybrid token manager for protocol detection and routing
return await this.hybridTokens.burnTokens(
tokenId,
amount,
{
mnemonic: this.walletInfo.mnemonic,
xecAddress: this.walletInfo.xecAddress,
hdPath: this.walletInfo.hdPath,
fee: this.fee,
privateKey: this.walletInfo.privateKey,
publicKey: this.walletInfo.publicKey
},
this.utxos.utxoStore.utxos,
satsPerByte
)
} catch (err) {
throw this._sanitizeError(err, 'eToken burn failed')
}
}
async burnAllETokens (tokenId, satsPerByte = this.fee) {
try {
// Wait for wallet to be initialized
await this.walletInfoPromise
if (!this.isInitialized) {
await this.initialize()
}
// Validate inputs
if (!tokenId || typeof tokenId !== 'string') {
throw new Error('Token ID is required and must be a string')
}
// Use hybrid token manager for protocol detection and routing
return await this.hybridTokens.burnAllTokens(
tokenId,
{
mnemonic: this.walletInfo.mnemonic,
xecAddress: this.walletInfo.xecAddress,
hdPath: this.walletInfo.hdPath,
fee: this.fee,
privateKey: this.walletInfo.privateKey,
publicKey: this.walletInfo.publicKey
},
this.utxos.utxoStore.utxos
)
} catch (err) {
throw this._sanitizeError(err, 'eToken burn all failed')
}
}
async getETokenData (tokenId, withTxHistory = false, sortOrder = 'DESCENDING') {
try {
// Validate inputs
if (!tokenId || typeof tokenId !== 'string') {
throw new Error('Token ID is required and must be a string')
}
// Use hybrid token manager to get comprehensive token data
return await this.hybridTokens.getTokenData(tokenId, withTxHistory, sortOrder)
} catch (err) {
throw this._sanitizeError(err, 'eToken data query failed')
}
}
// Export private key as WIF format
exportPrivateKeyAsWIF (compressed = true, testnet = false) {
try {
if (!this.walletInfo || !this.walletInfo.privateKey) {
throw new Error('Wallet not initialized or no private key available')
}
return this.keyDerivation.exportToWif(
this.walletInfo.privateKey,
compressed,
testnet
)
} catch (err) {
throw this._sanitizeError(err, 'WIF export failed')
}
}
// Validate WIF format (public utility method)
validateWIF (wif) {
try {
return this.keyDerivation._isValidWIF(wif)
} catch (err) {
return false
}
}
}
module.exports = MinimalXECWallet