minimal-slp-wallet
Version:
A minimalist Bitcoin Cash (BCH) wallet npm library, for use in a web apps.
1,628 lines (1,328 loc) • 281 kB
Markdown
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