UNPKG

minimal-slp-wallet

Version:
1,628 lines (1,328 loc) 281 kB
Project Path: minimal-slp-wallet Source Tree: ``` minimal-slp-wallet ├── dist ├── index.js ├── test │ ├── unit │ │ ├── a02-send-bch-unit.js │ │ ├── a07-consolidate-utoxs-unit.js │ │ ├── a06-op-return-unit.js │ │ ├── a01-minimal-bch-wallet-unit.js │ │ ├── a05-adapters-router-unit.js │ │ ├── a03-utxos-unit.js │ │ ├── a04-tokens-unit.js │ │ └── mocks │ │ ├── consolidate-utxos-mocks.js │ │ ├── utxo-mocks.js │ │ ├── util-mocks.js │ │ └── send-bch-mocks.js │ ├── e2e │ │ ├── burn-token.js │ │ ├── basic-auth.js │ │ ├── op-return.js │ │ └── consolidate-utxos-e2e.js │ └── integration │ ├── tokens.integration.test.js │ ├── router.integration.js │ ├── op-return.integration.js │ ├── utxos.integration.test.js │ └── index.integration.test.js ├── README.md ├── lib │ ├── consolidate-utxos.js │ ├── send-bch.js │ ├── adapters │ │ └── router.js │ ├── utxos.js │ ├── tokens.js │ └── op-return.js ├── LICENSE.md ├── dev-docs │ ├── images │ │ └── dep-diagram.png │ └── README.md ├── examples │ ├── burn-all-tokens.js │ ├── send-bch.js │ ├── list-tokens.js │ ├── validate-utxo.js │ ├── get-token-data.js │ ├── send-tokens.js │ ├── create-wallet.js │ └── burn-some-tokens.js └── package.json ``` `/home/trout/work/psf/code/minimal-slp-wallet/index.js`: ```js /* An npm JavaScript library for front end web apps. Implements a minimal Bitcoin Cash wallet. */ /* eslint-disable no-async-promise-executor */ 'use strict' 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 ``` `/home/trout/work/psf/code/minimal-slp-wallet/test/unit/a02-send-bch-unit.js`: ```js /* Unit tests for the send-bch.js library. */ // npm libraries const assert = require('chai').assert const sinon = require('sinon') const BCHJS = require('@psf/bch-js') const clone = require('lodash.clonedeep') // Local libraries const SendBCH = require('../../lib/send-bch') const AdapterRouter = require('../../lib/adapters/router') let uut // Unit Under Test const mockDataLib = require('./mocks/send-bch-mocks') let mockData describe('#SendBCH', () => { let sandbox // Restore the sandbox before each test. beforeEach(() => { sandbox = sinon.createSandbox() const config = { restURL: 'https://free-main.fullstack.cash/v5/' } const bchjs = new BCHJS(config) config.bchjs = bchjs config.ar = new AdapterRouter(config) uut = new SendBCH(config) mockData = clone(mockDataLib) }) afterEach(() => sandbox.restore()) describe('#constructor', () => { it('should throw an error if instance of bch-js is not passed.', () => { try { uut = new SendBCH() assert.fail('Unexpected code path') } catch (err) { assert.include( err.message, 'Must pass instance of bch-js when instantiating SendBCH.' ) } }) it('should throw an error if instance of adapter router is not passed.', () => { try { uut = new SendBCH({ bchjs: {} }) assert.fail('Unexpected code path') } catch (err) { assert.include(err.message, 'Must pass instance of Adapter Router.') } }) }) describe('#calculateFee', () => { it('should accurately calculate a P2PKH with 1 input and 2 outputs', () => { const fee = uut.calculateFee(1, 2, 1) // console.log('fee: ', fee) assert.equal(fee, 260) }) it('should accurately calculate a P2PKH with 2 input and 2 outputs', () => { const fee = uut.calculateFee(2, 2, 1) // console.log('fee: ', fee) assert.equal(fee, 408) }) it('should accurately calculate a P2PKH with 2 input and 3 outputs', () => { const fee = uut.calculateFee(2, 3, 1) // console.log('fee: ', fee) assert.equal(fee, 442) }) it('should throw an error for bad input', () => { try { const fee = uut.calculateFee('a', 'b', 'c') console.log('fee: ', fee) assert.equal(true, false, 'unexpected result') } catch (err) { // console.log('err: ', err) assert.include( err.message, 'Invalid input. Fee could not be calculated' ) } }) }) describe('#sortUtxosBySize', () => { it('should sort UTXOs in ascending order', () => { const utxos = uut.sortUtxosBySize(mockData.exampleUtxos01.utxos) // console.log('utxos: ', utxos) const lastElem = utxos.length - 1 assert.isAbove(utxos[lastElem].value, utxos[0].value) }) it('should sort UTXOs in descending order', () => { const utxos = uut.sortUtxosBySize( mockData.exampleUtxos01.utxos, 'DESCENDING' ) // console.log('utxos: ', utxos) const lastElem = utxos.length - 1 assert.isAbove(utxos[0].value, utxos[lastElem].value) }) }) describe('#getNecessaryUtxosAndChange', () => { it('should return UTXOs to achieve single output', () => { const outputs = [ { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 600 } ] const { necessaryUtxos, change } = uut.getNecessaryUtxosAndChange( outputs, mockData.exampleUtxos01.utxos ) // console.log('necessaryUtxos: ', necessaryUtxos) // console.log('change: ', change) assert.isArray(necessaryUtxos) assert.equal(necessaryUtxos.length, 3) assert.isNumber(change) }) it('should return UTXOs to achieve multiple outputs', () => { const outputs = [ { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 12513803 }, { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 2000 } ] const { necessaryUtxos, change } = uut.getNecessaryUtxosAndChange( outputs, mockData.exampleUtxos01.utxos ) // console.log('necessaryUtxos: ', necessaryUtxos) // console.log('change: ', change) assert.isArray(necessaryUtxos) assert.equal(necessaryUtxos.length, 3) assert.isNumber(change) }) it('should throw an error if not enough BCH', () => { try { const outputs = [ { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 12525803 }, { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 2000 } ] uut.getNecessaryUtxosAndChange(outputs, mockData.exampleUtxos01.utxos) assert.equal(true, false, 'Unexpected result') } catch (err) { // console.log('err: ', err) assert.include(err.message, 'Insufficient balance') } }) it('should use custom sorting function', () => { const outputs = [ { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 600 } ] const sortingStub = sinon.stub().returnsArg(0) uut.getNecessaryUtxosAndChange( outputs, mockData.exampleUtxos01.utxos, 1.0, { utxoSortingFn: sortingStub } ) assert.ok(sortingStub.calledOnceWith(mockData.exampleUtxos01.utxos)) }) }) describe('#getKeyPairFromMnemonic', () => { it('should generate a key pair from a wallet with a mnemonic', async () => { const keyPair = await uut.getKeyPairFromMnemonic(mockData.mockWallet) // console.log(`keyPair: ${JSON.stringify(keyPair, null, 2)}`) // Ensure the output has the expected properties. assert.property(keyPair, 'compressed') assert.property(keyPair, 'network') }) it('should generate a key pair from a wallet without a mnemonic', async () => { // Force mnemonic to have a null value mockData.mockWallet.mnemonic = null const keyPair = await uut.getKeyPairFromMnemonic(mockData.mockWallet) // console.log(`keyPair: ${JSON.stringify(keyPair, null, 2)}`) // Ensure the output has the expected properties. assert.property(keyPair, 'compressed') assert.property(keyPair, 'network') }) it('should throw error if wallet has neither mnemonic or private key', async () => { try { // Force desired code path mockData.mockWallet.mnemonic = null mockData.mockWallet.privateKey = null await uut.getKeyPairFromMnemonic(mockData.mockWallet) assert.fail('Unexpected code path') } catch (err) { assert.include(err.message, 'Wallet has no mnemonic or private key!') } }) }) describe('#createTransaction', () => { it('should throw an error if UTXOs array is empty', async () => { try { const outputs = [ { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 1000 } ] await uut.createTransaction(outputs, mockData.mockWallet, []) assert.equal(true, false, 'Unexpected result') } catch (err) { // console.log('err: ', err) assert.include(err.message, 'UTXO list is empty') } }) it('should ignore change if below the dust limit', async () => { const outputs = [ { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 600 } ] const { hex, txid } = await uut.createTransaction( outputs, mockData.mockWallet, mockData.exampleUtxos01.utxos ) assert.isString(hex) assert.isString(txid) }) it('should add change output if above the dust limit', async () => { const outputs = [ { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 625 } ] const { hex, txid } = await uut.createTransaction( outputs, mockData.mockWallet, mockData.exampleUtxos01.utxos ) // console.log('hex: ', hex) // console.log('txid: ', txid) assert.isString(hex) assert.isString(txid) }) }) describe('#sendBch', () => { it('should broadcast a transaction and return a txid', async () => { const hex = '0200000002abdb671501c19d11c35473aa84547f7f3b301d6924d6c8f419a26616dc486ea3010000006b4830450221009833f7bbecd7ba4c193f1edd693e42b337cd295b7e530cab3b2210f46c6cebe102200b65bf9b9bc66992c09cb1a40a2c84b629ba48c066b9f3cc5fe713a898051b6d41210259da20750fbde4e48d48068aa93e02701554dc66b4fe83851a91023110093449ffffffffcc198a396570aebd10605cdde223356c0d8f92133560c52013ae5d43dccccf53010000006a47304402207bd190fce11a0cbf8dd8d0d987bcdd428168312f217ec61d018c3198014a786a02200b0ac3db775ea708eb9a76a9fa84fe9a68a0553408b143f52c220313cc2ecbd241210259da20750fbde4e48d48068aa93e02701554dc66b4fe83851a91023110093449ffffffff0271020000000000001976a914543dc8f7c91721da06da8c3941f79e26cfbce67288ac6c030000000000001976a9141d027f19f0e9c4e6bb4e0b5359b4d2e2f9e27d9888ac00000000' const txid = '66b7d1fced6df27feb7faf305de2e3d6470decb0276648411fd6a2f69fec8543' // Mock live network calls. sandbox.stub(uut, 'createTransaction').resolves(hex) sandbox.stub(uut.ar, 'sendTx').resolves(txid) const output = await uut.sendBch() assert.equal(output, txid) }) it('should throw an error if there is an issue with broadcasting a tx', async () => { try { const hex = '0200000002abdb671501c19d11c35473aa84547f7f3b301d6924d6c8f419a26616dc486ea3010000006b4830450221009833f7bbecd7ba4c193f1edd693e42b337cd295b7e530cab3b2210f46c6cebe102200b65bf9b9bc66992c09cb1a40a2c84b629ba48c066b9f3cc5fe713a898051b6d41210259da20750fbde4e48d48068aa93e02701554dc66b4fe83851a91023110093449ffffffffcc198a396570aebd10605cdde223356c0d8f92133560c52013ae5d43dccccf53010000006a47304402207bd190fce11a0cbf8dd8d0d987bcdd428168312f217ec61d018c3198014a786a02200b0ac3db775ea708eb9a76a9fa84fe9a68a0553408b143f52c220313cc2ecbd241210259da20750fbde4e48d48068aa93e02701554dc66b4fe83851a91023110093449ffffffff0271020000000000001976a914543dc8f7c91721da06da8c3941f79e26cfbce67288ac6c030000000000001976a9141d027f19f0e9c4e6bb4e0b5359b4d2e2f9e27d9888ac00000000' // Mock live network calls. sandbox.stub(uut, 'createTransaction').resolves(hex) sandbox.stub(uut.ar, 'sendTx').throws(new Error('error message')) await uut.sendBch() assert.equal(true, false, 'unexpected result') } catch (err) { // console.log('err: ', err) assert.include(err.message, 'error message') } }) }) describe('#createSendAllTx', () => { it('should throw an error if address is invalid type', async () => { try { const toAddress = 1 await uut.createSendAllTx(toAddress, mockData.mockWallet, []) assert.equal(true, false, 'Unexpected result') } catch (err) { // console.log('err: ', err) assert.include(err.message, 'Address to send must be a bch address') } }) it('should throw an error if UTXOs array is empty', async () => { try { const toAddress = 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h' await uut.createSendAllTx(toAddress, mockData.mockWallet, []) assert.equal(true, false, 'Unexpected result') } catch (err) { // console.log('err: ', err) assert.include(err.message, 'UTXO list is empty') } }) it('should build transaction', async () => { const toAddress = 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h' const { hex, txid } = await uut.createSendAllTx( toAddress, mockData.mockWallet, mockData.exampleUtxos01.utxos ) // console.log('hex: ', hex) // console.log('txid: ', txid) assert.isString(hex) assert.isString(txid) }) it('should use default fee if fee is not specified.', async () => { const toAddress = 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h' mockData.mockWallet.fee = undefined const { hex, txid } = await uut.createSendAllTx( toAddress, mockData.mockWallet, mockData.exampleUtxos01.utxos ) // console.log('hex: ', hex) // console.log('txid: ', txid) assert.isString(hex) assert.isString(txid) }) }) describe('#sendAllBch', () => { it('should broadcast a transaction and return a txid', async () => { const hex = '0200000002abdb671501c19d11c35473aa84547f7f3b301d6924d6c8f419a26616dc486ea3010000006b4830450221009833f7bbecd7ba4c193f1edd693e42b337cd295b7e530cab3b2210f46c6cebe102200b65bf9b9bc66992c09cb1a40a2c84b629ba48c066b9f3cc5fe713a898051b6d41210259da20750fbde4e48d48068aa93e02701554dc66b4fe83851a91023110093449ffffffffcc198a396570aebd10605cdde223356c0d8f92133560c52013ae5d43dccccf53010000006a47304402207bd190fce11a0cbf8dd8d0d987bcdd428168312f217ec61d018c3198014a786a02200b0ac3db775ea708eb9a76a9fa84fe9a68a0553408b143f52c220313cc2ecbd241210259da20750fbde4e48d48068aa93e02701554dc66b4fe83851a91023110093449ffffffff0271020000000000001976a914543dc8f7c91721da06da8c3941f79e26cfbce67288ac6c030000000000001976a9141d027f19f0e9c4e6bb4e0b5359b4d2e2f9e27d9888ac00000000' const txid = '66b7d1fced6df27feb7faf305de2e3d6470decb0276648411fd6a2f69fec8543' // Mock live network calls. sandbox.stub(uut, 'createSendAllTx').resolves(hex) sandbox.stub(uut.ar, 'sendTx').resolves(txid) const output = await uut.sendAllBch() assert.equal(output, txid) }) it('should throw an error if there is an issue with broadcasting a tx', async () => { try { const hex = '0200000002abdb671501c19d11c35473aa84547f7f3b301d6924d6c8f419a26616dc486ea3010000006b4830450221009833f7bbecd7ba4c193f1edd693e42b337cd295b7e530cab3b2210f46c6cebe102200b65bf9b9bc66992c09cb1a40a2c84b629ba48c066b9f3cc5fe713a898051b6d41210259da20750fbde4e48d48068aa93e02701554dc66b4fe83851a91023110093449ffffffffcc198a396570aebd10605cdde223356c0d8f92133560c52013ae5d43dccccf53010000006a47304402207bd190fce11a0cbf8dd8d0d987bcdd428168312f217ec61d018c3198014a786a02200b0ac3db775ea708eb9a76a9fa84fe9a68a0553408b143f52c220313cc2ecbd241210259da20750fbde4e48d48068aa93e02701554dc66b4fe83851a91023110093449ffffffff0271020000000000001976a914543dc8f7c91721da06da8c3941f79e26cfbce67288ac6c030000000000001976a9141d027f19f0e9c4e6bb4e0b5359b4d2e2f9e27d9888ac00000000' // Mock live network calls. sandbox.stub(uut, 'createSendAllTx').resolves(hex) sandbox.stub(uut.ar, 'sendTx').throws(new Error('error message')) await uut.sendAllBch() assert.equal(true, false, 'unexpected result') } catch (err) { // console.log('err: ', err) assert.include(err.message, 'error message') } }) }) }) ``` `/home/trout/work/psf/code/minimal-slp-wallet/test/unit/a07-consolidate-utoxs-unit.js`: ```js /* Unit tests for the consolidate-utxos.js library. */ // Public npm libraries const assert = require('chai').assert const sinon = require('sinon') // const BCHJS = require('@psf/bch-js') const clone = require('lodash.clonedeep') // Local libraries const ConsolidateUtxos = require('../../lib/consolidate-utxos.js') const SlpWallet = require('../../index.js') const mockDataLib = require('./mocks/consolidate-utxos-mocks') describe('#Consolidate-UTXOs', () => { let sandbox let uut let mockData beforeEach(async () => { const wallet = new SlpWallet() await wallet.walletInfoPromise uut = new ConsolidateUtxos(wallet) sandbox = sinon.createSandbox() // mockData = Object.assign({}, mockDataLib) mockData = clone(mockDataLib) }) afterEach(() => sandbox.restore()) describe('#constructor', () => { it('should throw an error if instance of wallet is not passed', () => { try { uut = new ConsolidateUtxos() assert.fail('Unexpected code path') } catch (err) { assert.include( err.message, 'Must pass an instance of the wallet.' ) } }) }) describe('#countBchUtxos', () => { it('should count the number of UTXOs', () => { uut.wallet.utxos.utxoStore = { bchUtxos: ['a', 'b'] } const result = uut.countBchUtxos() // console.log('result: ', result) assert.equal(result, 2) }) }) describe('#updateUtxos', () => { it('should update the UTXO store', async () => { // Mock dependencies and force desired code path sandbox.stub(uut.wallet, 'initialize').resolves() const result = await uut.updateUtxos() assert.equal(result, true) }) }) describe('#countTokenUtxos', () => { it('should return an array of token classes', async () => { // Mock dependencies and force desired code path uut.wallet.utxos.utxoStore = { slpUtxos: { type1: { tokens: mockData.tokenUtxos01 } } } sandbox.stub(uut.wallet.tokens, 'listTokensFromUtxos').returns(mockData.tokenList01) const result = uut.countTokenUtxos() // console.log('result: ', result) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.equal(result.length, 2) assert.equal(result[0].cnt, 1) assert.equal(result[1].cnt, 2) }) }) describe('#consolidateTokenUtxos', () => { it('should consolidate token UTXOs and return an array of TXIDs', async () => { // Mock dependencies and force desired code path sandbox.stub(uut.wallet, 'sendTokens').resolves('fake-txid') sandbox.stub(uut.bchjs.Util, 'sleep').resolves() sandbox.stub(uut, 'updateUtxos').resolves() const result = await uut.consolidateTokenUtxos(mockData.countTokenUtxosOut01) // console.log('result: ', result) assert.equal(result.length, 1) assert.equal(result[0], 'fake-txid') }) }) describe('#start', () => { it('should return expected properties and values if there are no UTXOs to consolidate', async () => { // Mock dependencies and force desired code path sandbox.stub(uut, 'updateUtxos').resolves() sandbox.stub(uut, 'countBchUtxos').returns(1) sandbox.stub(uut, 'countTokenUtxos').returns([]) const result = await uut.start() // console.log('result: ', result) // Assert that expected properties exist assert.property(result, 'bchUtxoCnt') assert.property(result, 'bchTxid') assert.property(result, 'tokenUtxos') assert.property(result, 'tokenTxids') // Assert the properties have expected values. assert.equal(result.bchUtxoCnt, 1) assert.equal(result.bchTxid, null) assert.equal(result.tokenUtxos.length, 0) assert.equal(result.tokenTxids.length, 0) }) it('should consolidate BCH and token UTXOs', async () => { // Mock dependencies and force desired code path sandbox.stub(uut, 'updateUtxos').resolves() sandbox.stub(uut, 'countBchUtxos').returns(2) sandbox.stub(uut.wallet, 'sendAll').resolves('fake-bch-txid') sandbox.stub(uut.bchjs.Util, 'sleep').resolves() sandbox.stub(uut, 'countTokenUtxos').returns(mockData.countTokenUtxosOut01) sandbox.stub(uut, 'consolidateTokenUtxos').resolves(['fake-token-txid']) const result = await uut.start() // console.log('result: ', result) // Assert that expected properties exist assert.property(result, 'bchUtxoCnt') assert.property(result, 'bchTxid') assert.property(result, 'tokenUtxos') assert.property(result, 'tokenTxids') // Assert the properties have expected values. assert.equal(result.bchUtxoCnt, 2) assert.equal(result.bchTxid, 'fake-bch-txid') assert.equal(result.tokenUtxos.length, 2) assert.equal(result.tokenTxids.length, 1) }) it('should catch and throw errors', async () => { try { // Mock dependencies and force desired code path sandbox.stub(uut.retryQueue, 'addToQueue').rejects(new Error('fake error')) await uut.start() assert.fail('Unexpected code path') } catch (err) { // console.log('err.message: ', err.message) assert.include(err.message, 'fake error') } }) }) }) ``` `/home/trout/work/psf/code/minimal-slp-wallet/test/unit/a06-op-return-unit.js`: ```js /* Unit tests for the op-return.js library. */ // Public npm libraries const assert = require('chai').assert const sinon = require('sinon') const BCHJS = require('@psf/bch-js') // Local libraries const OpReturn = require('../../lib/op-return') // const Tokens = require('../../lib/tokens') // const Utxos = require('../../lib/utxos') const AdapterRouter = require('../../lib/adapters/router') const sendMockData = require('./mocks/send-bch-mocks') describe('#OP_RETURN', () => { let sandbox let uut beforeEach(() => { const config = { restURL: 'https://api.fullstack.cash/v5/' } const bchjs = new BCHJS(config) config.bchjs = bchjs config.ar = new AdapterRouter(config) uut = new OpReturn(config) // utxos = new Utxos(config) sandbox = sinon.createSandbox() // mockData = Object.assign({}, mockDataLib) // sendMockData = Object.assign({}, sendMockDataLib) }) afterEach(() => sandbox.restore()) describe('#constructor', () => { it('should throw an error if instance of bch-js is not passed', () => { try { uut = new OpReturn() assert.fail('Unexpected code path') } catch (err) { assert.include( err.message, 'Must pass instance of bch-js when instantiating OpReturn library.' ) } }) it('should throw an error if instance of Adapter Router is not passed', () => { try { uut = new OpReturn({ bchjs: {} }) assert.fail('Unexpected code path') } catch (err) { assert.include(err.message, 'Must pass instance of Adapter Router.') } }) }) describe('#calculateFee', () => { it('should accurately calculate a P2PKH with 2 input and 2 outputs', () => { const fee = uut.calculateFee(2, 2, 3, 1) // console.log('fee: ', fee) assert.equal(fee, 421) }) it('should throw an error for bad input', () => { try { uut.calculateFee('a', 'b', 'c') // console.log('fee: ', fee) assert.fail('Unexpected code path') } catch (err) { // console.log('err: ', err) assert.include( err.message, 'Invalid input. Fee could not be calculated' ) } }) it('should calculate fee for minimum OP_RETURN size', () => { const fee = uut.calculateFee(1, 2, 3, 1) // console.log('fee: ', fee) assert.equal(fee, 273) }) it('should calculate fee for maximum OP_RETURN size', () => { const fee = uut.calculateFee(1, 2, 223, 1) // console.log('fee: ', fee) assert.equal(fee, 493) }) }) describe('#getNecessaryUtxosAndChange', () => { it('should return UTXOs to achieve single output', () => { const outputs = [ { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 600 } ] const { necessaryUtxos, change } = uut.getNecessaryUtxosAndChange( outputs, sendMockData.exampleUtxos01.utxos ) // console.log('necessaryUtxos: ', necessaryUtxos) // console.log('change: ', change) assert.isArray(necessaryUtxos) assert.equal(necessaryUtxos.length, 3) assert.isNumber(change) }) it('should return UTXOs to achieve multiple outputs', () => { const outputs = [ { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 12513803 }, { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 2000 } ] const { necessaryUtxos, change } = uut.getNecessaryUtxosAndChange( outputs, sendMockData.exampleUtxos01.utxos ) // console.log('necessaryUtxos: ', necessaryUtxos) // console.log('change: ', change) assert.isArray(necessaryUtxos) assert.equal(necessaryUtxos.length, 3) assert.isNumber(change) }) it('should throw an error if not enough BCH', () => { try { const outputs = [ { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 12525803 }, { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 2000 } ] uut.getNecessaryUtxosAndChange( outputs, sendMockData.exampleUtxos01.utxos ) assert.equal(true, false, 'Unexpected result') } catch (err) { // console.log('err: ', err) assert.include(err.message, 'Insufficient balance') } }) }) describe('#createTransaction', () => { it('should throw an error if there are no BCH UTXOs.', async () => { try { await uut.createTransaction({}, [], []) assert.equal(true, false, 'unexpecte result') } catch (err) { assert.include(err.message, 'BCH UTXO list is empty') } }) it('should generate tx with OP_RETURN', async () => { // const walletInfo = sendMockData.mockWallet const result = await uut.createTransaction( sendMockData.mockWallet, sendMockData.exampleUtxos01.utxos, 'this is a test' ) // console.log('result: ', result) assert.isString(result.hex) assert.isString(result.txid) }) it('should generate OP_RETURN tx with extra outputs', async () => { // const walletInfo = sendMockData.mockWallet const outputs = [ { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 2525803 }, { address: 'bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h', amountSat: 2000 } ] const result = await uut.createTransaction( sendMockData.mockWallet, sendMockData.exampleUtxos01.utxos, 'this is a test', '6d02', outputs ) // console.log('result: ', result) assert.isString(result.hex) assert.isString(result.txid) }) }) describe('#sendOpReturn', () => { it('should broadcast hex and return a txid', async () => { // Mock dependencies sandbox.stub(uut, 'createTransaction').resolves('fake-hex') sandbox.stub(uut.ar, 'sendTx').resolves('fake-txid') const result = await uut.sendOpReturn() assert.equal(result, 'fake-txid') }) }) }) ``` `/home/trout/work/psf/code/minimal-slp-wallet/test/unit/a01-minimal-bch-wallet-unit.js`: ```js /* Unit tests for the main library. */ // npm libraries const assert = require('chai').assert const sinon = require('sinon') // Mocking data libraries. // const mockData = require('./mocks/util-mocks') const mockUtxos = require('./mocks/utxo-mocks') // Unit under test const MinimalBCHWallet = require('../../index') describe('#index.js - Minimal BCH Wallet', () => { let sandbox, uut // Restore the sandbox before each test. beforeEach(async () => { sandbox = sinon.createSandbox() uut = new MinimalBCHWallet() await uut.walletInfoPromise }) afterEach(() => sandbox.restore()) describe('#constructor', () => { it('should create a new wallet without encrypted mnemonic', async () => { uut = new MinimalBCHWallet(undefined) await uut.walletInfoPromise // console.log('uut: ', uut) assert.property(uut, 'walletInfo') assert.property(uut, 'walletInfoPromise') assert.property(uut, 'walletInfoCreated') assert.equal(uut.walletInfoCreated, true) assert.property(uut.walletInfo, 'mnemonic') assert.isString(uut.walletInfo.mnemonic) assert.isNotEmpty(uut.walletInfo.mnemonic) assert.property(uut.walletInfo, 'privateKey') assert.isString(uut.walletInfo.privateKey) assert.isNotEmpty(uut.walletInfo.privateKey) assert.property(uut.walletInfo, 'cashAddress') assert.isString(uut.walletInfo.cashAddress) assert.isNotEmpty(uut.walletInfo.cashAddress) assert.property(uut.walletInfo, 'legacyAddress') assert.isString(uut.walletInfo.legacyAddress) assert.isNotEmpty(uut.walletInfo.legacyAddress) assert.property(uut.walletInfo, 'slpAddress') assert.isString(uut.walletInfo.slpAddress) assert.isNotEmpty(uut.walletInfo.slpAddress) assert.notProperty(uut, 'mnemonicEncrypted') assert.notProperty(uut.walletInfo, 'mnemonicEncrypted') assert.equal(uut.isInitialized, false) }) it('should create a new wallet with encrypted mnemonic', async () => { uut = new MinimalBCHWallet(null, { password: 'myStrongPassword' }) await uut.walletInfoPromise // console.log('uut: ', uut) assert.property(uut.walletInfo, 'mnemonic') assert.isString(uut.walletInfo.mnemonic) assert.isNotEmpty(uut.walletInfo.mnemonic) assert.property(uut.walletInfo, 'privateKey') assert.isString(uut.walletInfo.privateKey) assert.isNotEmpty(uut.walletInfo.privateKey) assert.property(uut.walletInfo, 'cashAddress') assert.isString(uut.walletInfo.cashAddress) assert.isNotEmpty(uut.walletInfo.cashAddress) assert.property(uut.walletInfo, 'legacyAddress') assert.isString(uut.walletInfo.legacyAddress) assert.isNotEmpty(uut.walletInfo.legacyAddress) assert.property(uut.walletIn