minimal-slp-wallet
Version:
A minimalist Bitcoin Cash (BCH) wallet npm library, for use in a web apps.
602 lines (499 loc) • 18.3 kB
JavaScript
/*
An npm JavaScript library for front end web apps. Implements a minimal
Bitcoin Cash wallet.
*/
/* eslint-disable no-async-promise-executor */
const BCHJS = require('@psf/bch-js')
const crypto = require('crypto-js')
// Local libraries
const SendBCH = require('./lib/send-bch')
const Utxos = require('./lib/utxos')
const Tokens = require('./lib/tokens')
const AdapterRouter = require('./lib/adapters/router')
const OpReturn = require('./lib/op-return')
const ConsolidateUtxos = require('./lib/consolidate-utxos.js')
// let this
class MinimalBCHWallet {
constructor (hdPrivateKeyOrMnemonic, advancedOptions = {}) {
this.advancedOptions = advancedOptions
// BEGIN Handle advanced options.
// HD Derivation path.
this.hdPath = this.advancedOptions.hdPath || "m/44'/245'/0'/0/0"
// bch-js options.
const bchjsOptions = {}
if (this.advancedOptions.restURL) {
bchjsOptions.restURL = advancedOptions.restURL
}
// JWT token for increased rate limits.
if (this.advancedOptions.apiToken) {
bchjsOptions.apiToken = advancedOptions.apiToken
}
// Basic Auth token for private installations of bch-api.
if (this.advancedOptions.authPass) {
bchjsOptions.authPass = advancedOptions.authPass
}
// Set the sats-per-byte fee rate.
this.fee = 1.2
if (this.advancedOptions.fee) {
this.fee = this.advancedOptions.fee
}
// END Handle advanced options.
// Encapsulae the external libraries.
this.crypto = crypto
this.BCHJS = BCHJS
this.bchjs = new BCHJS(bchjsOptions)
bchjsOptions.bchjs = this.bchjs
// Instantiate the adapter router.
if (advancedOptions.interface === 'consumer-api') {
bchjsOptions.interface = 'consumer-api'
// bchjsOptions.walletService = advancedOptions.walletService
// bchjsOptions.bchWalletApi = advancedOptions.bchWalletApi
}
this.ar = new AdapterRouter(bchjsOptions)
bchjsOptions.ar = this.ar
// Instantiate local libraries.
this.sendBch = new SendBCH(bchjsOptions)
this.utxos = new Utxos(bchjsOptions)
this.tokens = new Tokens(bchjsOptions)
this.opReturn = new OpReturn(bchjsOptions)
this.consolidateUtxos = new ConsolidateUtxos(this)
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)
// 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.getBalance = this.getBalance.bind(this)
this.getTransactions = this.getTransactions.bind(this)
this.getTxData = this.getTxData.bind(this)
this.send = this.send.bind(this)
this.sendTokens = this.sendTokens.bind(this)
this.burnTokens = this.burnTokens.bind(this)
this.listTokens = this.listTokens.bind(this)
this.sendAll = this.sendAll.bind(this)
this.burnAll = this.burnAll.bind(this)
this.getUsd = this.getUsd.bind(this)
this.sendOpReturn = this.sendOpReturn.bind(this)
this.utxoIsValid = this.utxoIsValid.bind(this)
this.getTokenData = this.getTokenData.bind(this)
this.getTokenData2 = this.getTokenData2.bind(this)
this.getKeyPair = this.getKeyPair.bind(this)
this.optimize = this.optimize.bind(this)
this.getTokenBalance = this.getTokenBalance.bind(this)
this.getPubKey = this.getPubKey.bind(this)
this.broadcast = this.broadcast.bind(this)
this.getPsfWritePrice = this.getPsfWritePrice.bind(this)
this.cid2json = this.cid2json.bind(this)
}
// Create a new wallet. Returns a promise that resolves into a wallet object.
async create (mnemonicOrWif) {
// return new Promise(async (resolve, reject) => {
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) {
const mnemonic = this.bchjs.Mnemonic.generate(128)
const rootSeedBuffer = await this.bchjs.Mnemonic.toSeed(mnemonic)
const masterHDNode = this.bchjs.HDNode.fromSeed(rootSeedBuffer)
const childNode = masterHDNode.derivePath(this.hdPath)
walletInfo.privateKey = this.bchjs.HDNode.toWIF(childNode)
walletInfo.publicKey = this.bchjs.HDNode.toPublicKey(
childNode
).toString('hex')
walletInfo.mnemonic = mnemonic
walletInfo.address = walletInfo.cashAddress = this.bchjs.HDNode.toCashAddress(
childNode
)
walletInfo.legacyAddress = this.bchjs.HDNode.toLegacyAddress(childNode)
walletInfo.hdPath = this.hdPath
//
} else {
// A WIF will start with L or K, will have no spaces, and will be 52
// characters long.
const startsWithKorL =
mnemonicOrWif &&
(mnemonicOrWif[0].toString().toLowerCase() === 'k' ||
mnemonicOrWif[0].toString().toLowerCase() === 'l')
const is52Chars = mnemonicOrWif && mnemonicOrWif.length === 52
if (startsWithKorL && is52Chars) {
// WIF Private Key
walletInfo.privateKey = mnemonicOrWif
const ecPair = this.bchjs.ECPair.fromWIF(mnemonicOrWif)
// walletInfo.publicKey = ecPair.toPublicKey().toString('hex')
walletInfo.publicKey = this.bchjs.ECPair.toPublicKey(ecPair).toString(
'hex'
)
walletInfo.mnemonic = null
walletInfo.address = walletInfo.cashAddress = this.bchjs.ECPair.toCashAddress(
ecPair
)
walletInfo.legacyAddress = this.bchjs.ECPair.toLegacyAddress(ecPair)
walletInfo.hdPath = null
} else {
// 12-word Mnemonic
const mnemonic = mnemonicOrWif || this.bchjs.Mnemonic.generate(128)
const rootSeedBuffer = await this.bchjs.Mnemonic.toSeed(mnemonic)
const masterHDNode = this.bchjs.HDNode.fromSeed(rootSeedBuffer)
const childNode = masterHDNode.derivePath(this.hdPath)
walletInfo.privateKey = this.bchjs.HDNode.toWIF(childNode)
walletInfo.publicKey = this.bchjs.HDNode.toPublicKey(
childNode
).toString('hex')
walletInfo.mnemonic = mnemonic
walletInfo.address = walletInfo.cashAddress = this.bchjs.HDNode.toCashAddress(
childNode
)
walletInfo.legacyAddress = this.bchjs.HDNode.toLegacyAddress(
childNode
)
walletInfo.hdPath = this.hdPath
}
}
// Encrypt the mnemonic if a password is provided.
if (this.advancedOptions.password) {
walletInfo.mnemonicEncrypted = this.encrypt(
mnemonicOrWif,
this.advancedOptions.password
)
}
walletInfo.slpAddress = this.bchjs.SLP.Address.toSLPAddress(
walletInfo.address
)
this.walletInfoCreated = true
this.walletInfo = walletInfo
return walletInfo
} catch (err) {
// return reject(err)
console.error('Error in create()')
throw err
}
// })
}
// Initialize is called to initialize the UTXO store, download token data, and
// get a balance of the wallet.
async initialize () {
await this.walletInfoPromise
await this.utxos.initUtxoStore(this.walletInfo.address)
this.isInitialized = true
return true
}
// Get the UTXO information for this wallet.
async getUtxos (bchAddress) {
let addr = bchAddress
// If no address is passed in, but the wallet has been initialized, use the
// wallet's address.
if (!bchAddress && this.walletInfo && this.walletInfo.cashAddress) {
addr = this.walletInfo.cashAddress
return this.utxos.initUtxoStore(addr)
}
const utxos = await this.ar.getUtxos(addr)
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
return utxos
}
// Encrypt the mnemonic of the wallet.
encrypt (mnemonic, password) {
return this.crypto.AES.encrypt(mnemonic, password).toString()
}
// Decrypt the mnemonic of the wallet.
decrypt (mnemonicEncrypted, password) {
let mnemonic
try {
mnemonic = this.crypto.AES.decrypt(mnemonicEncrypted, password).toString(
this.crypto.enc.Utf8
)
} catch (err) {
throw new Error('Wrong password')
}
return mnemonic
}
// Get the balance of the wallet.
async getBalance (inObj = {}) {
const { bchAddress } = inObj
let addr = bchAddress
// If no address is passed in, but the wallet has been initialized, use the
// wallet's address.
if (!bchAddress && this.walletInfo && this.walletInfo.cashAddress) {
addr = this.walletInfo.cashAddress
}
const balances = await this.ar.getBalance(addr)
return balances.balance.confirmed + balances.balance.unconfirmed
}
// Get transactions associated with the wallet.
// Returns an array of object. Each object has a 'tx_hash' and 'height' property.
async getTransactions (bchAddress, sortingOrder = 'DESCENDING') {
let addr = bchAddress
// If no address is passed in, but the wallet has been initialized, use the
// wallet's address.
if (!bchAddress && this.walletInfo && this.walletInfo.cashAddress) {
addr = this.walletInfo.cashAddress
}
// console.log(`Getting transactions for ${addr}`)
const data = await this.ar.getTransactions(addr, sortingOrder)
return data.transactions
}
// Get transaction data for up to 20 TXIDs. txids should be an array. Each
// element should be a string containing a TXID.
async getTxData (txids = []) {
const data = await this.ar.getTxData(txids)
return data
}
// Send BCH. Returns a promise that resolves into a TXID.
// This is a wrapper for the send-bch.js library.
send (outputs) {
try {
// console.log(
// `this.utxos.bchUtxos: ${JSON.stringify(this.utxos.bchUtxos, null, 2)}`
// )
return this.sendBch.sendBch(
outputs,
{
mnemonic: this.walletInfo.mnemonic,
cashAddress: this.walletInfo.address,
hdPath: this.walletInfo.hdPath,
fee: this.fee,
privateKey: this.walletInfo.privateKey
},
// this.utxos.bchUtxos
this.utxos.utxoStore.bchUtxos
)
} catch (err) {
console.error('Error in send()')
throw err
}
}
// Send Tokens. Returns a promise that resolves into a TXID.
// This is a wrapper for the tokens.js library.
sendTokens (output, satsPerByte, opts = {}) {
try {
// console.log(`utxoStore: ${JSON.stringify(this.utxos.utxoStore, null, 2)}`)
// If mining fee is not specified, use the value assigned in the constructor.
if (!satsPerByte) satsPerByte = this.fee
// If output was passed in as an array, use only the first element of the Array.
if (Array.isArray(output)) {
output = output[0]
}
// Combine all Type 1, Group, and NFT token UTXOs. Ignore minting batons.
const tokenUtxos = this.utxos.getSpendableTokenUtxos()
// console.log('msw tokenUtxos: ', tokenUtxos)
return this.tokens.sendTokens(
output,
this.walletInfo,
this.utxos.utxoStore.bchUtxos,
tokenUtxos,
satsPerByte,
opts
)
} catch (err) {
console.error('Error in send()')
throw err
}
}
async burnTokens (qty, tokenId, satsPerByte) {
try {
// console.log(`utxoStore: ${JSON.stringify(this.utxos.utxoStore, null, 2)}`)
// If mining fee is not specified, use the value assigned in the constructor.
if (!satsPerByte) satsPerByte = this.fee
// Combine all Type 1, Group, and NFT token UTXOs. Ignore minting batons.
const tokenUtxos = this.utxos.getSpendableTokenUtxos()
// Generate the transaction.
return this.tokens.burnTokens(
qty,
tokenId,
this.walletInfo,
this.utxos.utxoStore.bchUtxos,
tokenUtxos,
satsPerByte
)
} catch (err) {
console.error('Error in burnTokens()')
throw err
}
}
// Return information on SLP tokens held by this wallet.
listTokens (slpAddress) {
const addr = slpAddress || this.walletInfo.slpAddress
return this.tokens.listTokensFromAddress(addr)
}
// Get the balance for a specific SLP token.
getTokenBalance (inObj = {}) {
const { tokenId, slpAddress } = inObj
const addr = slpAddress || this.walletInfo.slpAddress
return this.tokens.getTokenBalance(tokenId, addr)
}
// Send BCH. Returns a promise that resolves into a TXID.
// This is a wrapper for the send-bch.js library.
sendAll (toAddress) {
try {
return this.sendBch.sendAllBch(
toAddress,
{
mnemonic: this.walletInfo.mnemonic,
cashAddress: this.walletInfo.address,
hdPath: this.walletInfo.hdPath,
fee: this.fee,
privateKey: this.walletInfo.privateKey
},
// this.utxos.bchUtxos
this.utxos.utxoStore.bchUtxos
)
} catch (err) {
console.error('Error in sendAll()')
throw err
}
}
// Burn all the SLP tokens associated to the token ID
async burnAll (tokenId) {
try {
// Combine all Type 1, Group, and NFT token UTXOs. Ignore minting batons.
const tokenUtxos = this.utxos.getSpendableTokenUtxos()
// console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`)
// Generate the transaction.
const txid = await this.tokens.burnAll(
tokenId,
this.walletInfo,
this.utxos.utxoStore.bchUtxos,
tokenUtxos
)
return txid
} catch (err) {
console.error('Error in burnAll()')
throw err
}
}
// Get the spot price of BCH in USD.
async getUsd () {
return await this.ar.getUsd()
}
// Generate and broadcast a transaction with an OP_RETURN output.
// Returns the txid of the transactions.
async sendOpReturn (
msg = '',
prefix = '6d02', // Default to memo.cash
bchOutput = [],
satsPerByte = 1.0
) {
try {
// Wait for the wallet to finish initializing.
await this.walletInfoPromise
// console.log(
// `this.utxos.utxoStore ${JSON.stringify(this.utxos.utxoStore, null, 2)}`
// )
const txid = await this.opReturn.sendOpReturn(
this.walletInfo,
this.utxos.utxoStore.bchUtxos,
msg,
prefix,
bchOutput,
satsPerByte
)
return txid
} catch (err) {
console.error('Error in sendOpReturn()')
throw err
}
}
// Validate that a UTXO can be spent.
// const utxo = {
// tx_hash: 'b94e1ff82eb5781f98296f0af2488ff06202f12ee92b0175963b8dba688d1b40',
// tx_pos: 0
// }
// isValid = await wallet.utxoIsValid(utxo)
async utxoIsValid (utxo) {
return await this.ar.utxoIsValid(utxo)
}
// Get mutable and immutable data associated with a token.
async getTokenData (tokenId, withTxHistory = false) {
return await this.ar.getTokenData(tokenId, withTxHistory)
}
// Get token icon and other media
async getTokenData2 (tokenId, updateCache) {
return await this.ar.getTokenData2(tokenId, updateCache)
}
// This method returns an object that contains a private key WIF, public key,
// public address, and the index of the HD wallet that the key pair was
// generated from. If no index is provided, it generates the root key pair
// (index 0).
async getKeyPair (hdIndex = 0) {
await this.walletInfoPromise
const mnemonic = this.walletInfo.mnemonic
if (!mnemonic) {
throw new Error('Wallet does not have a mnemonic. Can not generate a new key pair.')
}
// root seed buffer
const rootSeed = await this.bchjs.Mnemonic.toSeed(mnemonic)
const masterHDNode = this.bchjs.HDNode.fromSeed(rootSeed)
const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${hdIndex}`)
const cashAddress = this.bchjs.HDNode.toCashAddress(childNode)
console.log('Generating a new key pair for cashAddress: ', cashAddress)
const wif = this.bchjs.HDNode.toWIF(childNode)
const publicKey = this.bchjs.HDNode.toPublicKey(childNode).toString('hex')
const slpAddress = this.bchjs.SLP.Address.toSLPAddress(cashAddress)
const outObj = {
hdIndex,
wif,
publicKey,
cashAddress,
slpAddress
}
return outObj
}
// Optimize the wallet by consolidating UTXOs. This has the effect of speeding
// up all API calls and improving the UX.
async optimize (dryRun = false) {
return await this.consolidateUtxos.start({ dryRun })
}
// Get token icon and other media
async getPubKey (addr) {
try {
return await this.ar.getPubKey(addr)
} catch (err) {
console.error('Error in minimal-slp-wallet/getPubKey()')
throw err
}
}
// Broadcast a hex-encoded TX to the network
async broadcast (inObj = {}) {
try {
const { hex } = inObj
return await this.ar.sendTx(hex)
} catch (err) {
console.error('Error in minimal-slp-wallet/broadcast()')
throw err
}
}
// Get the cost in PSF tokens to write 1MB of data to the PSFFPP IPFS pinning
// network. Find out more at psffpp.com.
async getPsfWritePrice () {
try {
return await this.ar.getPsfWritePrice()
} catch (err) {
console.error('Error in minimal-slp-wallet/getPsfWritePrice()')
throw err
}
}
// Convert a CID to a JSON object.
async cid2json (inObj = {}) {
try {
const { cid } = inObj
console.log('index.js/cid2json() cid: ', cid)
return await this.ar.cid2json({ cid })
} catch (err) {
console.error('Error in minimal-slp-wallet/cid2json()')
throw err
}
}
}
module.exports = MinimalBCHWallet