minimal-slp-wallet
Version:
A minimalist Bitcoin Cash (BCH) wallet npm library, for use in a web apps.
918 lines (708 loc) • 25.9 kB
JavaScript
/*
Unit tests for the lib/adapters/router.js library.
*/
// Public npm libraries
const assert = require('chai').assert
const sinon = require('sinon')
const BCHJS = require('@psf/bch-js')
const AdapterRouter = require('../../lib/adapters/router')
let uut
describe('#adapter-router', () => {
let sandbox
// let utxos
beforeEach(() => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs })
sandbox = sinon.createSandbox()
// mockData = Object.assign({}, mockDataLib)
// sendMockData = Object.assign({}, sendMockDataLib)
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if bch-js is not included', () => {
try {
uut = new AdapterRouter()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Must pass instance of bch-js when instantiating AdapterRouter.'
)
}
})
it('should select rest-api by default', () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs })
assert.equal(uut.interface, 'rest-api')
})
it('should select consumer-api interface', () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
assert.equal(uut.interface, 'consumer-api')
})
it('should override restURL for consumer API', () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({
bchjs,
interface: 'consumer-api',
restURL: 'fakeUrl'
})
assert.equal(uut.interface, 'consumer-api')
assert.equal(uut.bchConsumer.restURL, 'fakeUrl')
})
})
describe('#getUtxos', () => {
it('should throw an error if address is not specified', async () => {
try {
await uut.getUtxos()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Address string required when calling getUtxos()'
)
}
})
it('should use bch-js by default', async () => {
// Mock dependencies.
sandbox.stub(uut.bchjs.Utxo, 'get').resolves(['test str'])
const result = await uut.getUtxos('fake-addr')
assert.equal(result, 'test str')
})
it('should use wallet service when consumer interface is selected', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies.
sandbox.stub(uut.bchConsumer.bch, 'getUtxos').resolves(['test str'])
const result = await uut.getUtxos('fake-addr')
assert.equal(result, 'test str')
})
it('should throw error if communication error with bch-consumer', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies.
sandbox
.stub(uut.bchConsumer.bch, 'getUtxos')
.resolves([{ success: false, message: 'test error' }])
try {
await uut.getUtxos('fake-addr')
assert.fail('Unexpected code path')
} catch (err) {
assert.equal(err.message, 'test error')
}
})
it('should throw an error if an interface is not specified', async () => {
try {
uut.interface = ''
await uut.getUtxos('fake-addr')
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'this.interface is not specified')
}
})
})
describe('#sendTx', () => {
it('should throw an error hex is not specified', async () => {
try {
await uut.sendTx()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Hex encoded transaction required as input.'
)
}
})
it('should use bch-js by default', async () => {
// Mock dependencies.
sandbox
.stub(uut.bchjs.RawTransactions, 'sendRawTransaction')
.resolves(['txid-str'])
const result = await uut.sendTx('fakeHex')
assert.equal(result, 'txid-str')
})
it('should catch full-node errors thrown by bch-js', async () => {
// Mock dependencies.
sandbox
.stub(uut.bchjs.RawTransactions, 'sendRawTransaction')
.rejects({ error: 'test error' })
try {
await uut.sendTx('fakeHex')
assert.fail('Unexpected code path')
} catch (err) {
assert.equal(err.message, 'test error')
}
})
it('should use wallet service when consumer-api interface is selected', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies.
sandbox.stub(uut.bchConsumer.bch, 'sendTx').resolves({ txid: 'txid-str' })
const result = await uut.sendTx('fakeHex')
assert.equal(result, 'txid-str')
})
it('should throw error if communication error with bch-consumer', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies.
sandbox
.stub(uut.bchConsumer.bch, 'sendTx')
.resolves({ success: false, message: 'test error' })
try {
await uut.sendTx('fakeHex')
assert.fail('Unexpected code path')
} catch (err) {
assert.equal(err.message, 'test error')
}
})
it('should throw an error if an interface is not specified', async () => {
try {
uut.interface = ''
await uut.sendTx('fake-addr')
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'this.interface is not specified')
}
})
})
describe('#getBalance', () => {
it('should throw an error if address is not specified', async () => {
try {
await uut.getBalance()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Address string required when calling getBalance()'
)
}
})
it('should use bch-js by default', async () => {
// Mock dependencies.
sandbox.stub(uut.bchjs.Electrumx, 'balance').resolves('test str')
const result = await uut.getBalance('fake-addr')
assert.equal(result, 'test str')
})
it('should use wallet service when consumer interface is selected', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies.
sandbox
.stub(uut.bchConsumer.bch, 'getBalance')
.resolves({ success: true, balances: [{ balance: 'test str' }] })
const result = await uut.getBalance('fake-addr')
// console.log('result: ', result)
assert.equal(result.balance, 'test str')
})
it('should throw an error if an interface is not specified', async () => {
try {
uut.interface = ''
await uut.getBalance('fake-addr')
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'this.interface is not specified')
}
})
it('should throw errors passed from service', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies.
sandbox
.stub(uut.bchConsumer.bch, 'getBalance')
.resolves({ success: false, message: 'test-error' })
try {
await uut.getBalance('fake-addr')
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test-error')
}
})
})
describe('#getTransactions', () => {
it('should throw an error if address is not specified', async () => {
try {
await uut.getTransactions()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Address string required when calling getTransactions()'
)
}
})
it('should use bch-js by default', async () => {
// Mock dependencies.
sandbox.stub(uut.bchjs.Electrumx, 'transactions').resolves({ transactions: 'test str' })
sandbox.stub(uut.bchjs.Electrumx, 'sortAllTxs').resolves('test str')
const result = await uut.getTransactions('fake-addr')
// console.log('result: ', result)
assert.equal(result.transactions, 'test str')
})
it('should use wallet service when consumer interface is selected', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies.
sandbox.stub(uut.bchConsumer.bch, 'getTxHistory').resolves({
success: true,
txs: ['test str']
})
const result = await uut.getTransactions('fake-addr')
// console.log('result: ', result)
assert.equal(result.transactions, 'test str')
})
it('should throw an error if an interface is not specified', async () => {
try {
uut.interface = ''
await uut.getTransactions('fake-addr')
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'this.interface is not specified')
}
})
})
describe('#getTxData', () => {
it('should throw an error if txids is a string', async () => {
try {
await uut.getTxData('blah')
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Input txids must be an array of TXIDs. Up to 20.'
)
}
})
it('should use bch-js by default', async () => {
// Mock dependencies.
sandbox
.stub(uut.bchjs.PsfSlpIndexer, 'tx')
.resolves({ txData: { key: 'value' } })
const result = await uut.getTxData(['fake-txid'])
// console.log('result: ', result)
assert.isArray(result)
assert.equal(result[0].key, 'value')
})
it('should get data from bch-consumer', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies.
sandbox
.stub(uut.bchConsumer.bch, 'getTxData')
.resolves([{ key: 'value' }])
const result = await uut.getTxData(['fake-txid'])
// console.log('result: ', result)
assert.isArray(result)
assert.equal(result[0].key, 'value')
})
it('should throw an error if an interface is not specified', async () => {
try {
uut.interface = ''
await uut.getTxData(['fake-addr'])
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'this.interface is not specified')
}
})
})
describe('#getUsd', () => {
it('should get price from bch-js', async () => {
// Mock dependencies
sandbox.stub(uut.bchjs.Price, 'getUsd').resolves(100)
// Force selected interface.
uut.interface = 'rest-api'
const result = await uut.getUsd()
assert.equal(result, 100)
})
it('should get price from bch-consumer', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies
sandbox.stub(uut.bchConsumer.bch, 'getUsd').resolves(100)
// Force selected interface.
uut.interface = 'consumer-api'
const result = await uut.getUsd()
assert.equal(result, 100)
})
it('should throw an error if an interface is not specified', async () => {
try {
uut.interface = ''
await uut.getUsd()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'this.interface is not specified')
}
})
})
describe('#utxoIsValid', () => {
it('should validate UTXO from bch-js', async () => {
// Mock dependencies
sandbox.stub(uut.bchjs.Utxo, 'isValid').resolves(true)
// Force selected interface.
uut.interface = 'rest-api'
const utxo = {
tx_hash: 'b94e1ff82eb5781f98296f0af2488ff06202f12ee92b0175963b8dba688d1b40',
tx_pos: 0
}
const result = await uut.utxoIsValid(utxo)
assert.equal(result, true)
})
it('should validate UTXO from bch-consumer', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies
sandbox.stub(uut.bchConsumer.bch, 'utxoIsValid').resolves({ isValid: true })
// Force selected interface.
uut.interface = 'consumer-api'
const utxo = {
tx_hash: 'b94e1ff82eb5781f98296f0af2488ff06202f12ee92b0175963b8dba688d1b40',
tx_pos: 0
}
const result = await uut.utxoIsValid(utxo)
assert.equal(result, true)
})
it('should throw an error if an interface is not specified', async () => {
try {
uut.interface = ''
const utxo = {
tx_hash: 'b94e1ff82eb5781f98296f0af2488ff06202f12ee92b0175963b8dba688d1b40',
tx_pos: 0
}
await uut.utxoIsValid(utxo)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'this.interface is not specified')
}
})
it('should throw an error if utxo is not specified', async () => {
try {
await uut.utxoIsValid()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'utxo required as input.')
}
})
})
describe('#getTokenData', () => {
it('should get token data from bch-js', async () => {
// Mock dependencies
sandbox.stub(uut.bchjs.PsfSlpIndexer, 'getTokenData').resolves({
genesisData: {},
immutableData: {},
mutableData: {}
})
// Force selected interface.
uut.interface = 'rest-api'
const tokenId = 'c85042ab08a2099f27de880a30f9a42874202751d834c42717a20801a00aab0d'
const result = await uut.getTokenData(tokenId)
assert.property(result, 'genesisData')
assert.property(result, 'immutableData')
assert.property(result, 'mutableData')
})
it('should get token data from bch-consumer', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies
sandbox.stub(uut.bchConsumer.bch, 'getTokenData').resolves({
success: true,
tokenData: {
genesisData: {},
immutableData: {},
mutableData: {}
}
})
const tokenId = 'c85042ab08a2099f27de880a30f9a42874202751d834c42717a20801a00aab0d'
const result = await uut.getTokenData(tokenId)
assert.property(result, 'genesisData')
assert.property(result, 'immutableData')
assert.property(result, 'mutableData')
})
// it('should throw an error if an interface is not specified', async () => {
// try {
// uut.interface = ''
//
// const tokenId = 'c85042ab08a2099f27de880a30f9a42874202751d834c42717a20801a00aab0d'
//
// await uut.getTokenData(tokenId)
//
// assert.fail('Unexpected code path')
// } catch (err) {
// assert.include(err.message, 'this.interface is not specified')
// }
// })
it('should throw an error if token ID is not provided', async () => {
try {
await uut.getTokenData()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'tokenId required as input.')
}
})
it('should sent token txs to Electrum library for sorting', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
const tokenId =
'5f31905f335fa932879c5aabfd1c14ac748f6696148bd300f845ea5016ad573e'
// Mock dependencies and force desired code path.
sandbox.stub(uut.bchConsumer.bch, 'getTokenData').resolves({
success: true,
tokenData: {
genesisData: {
txs: []
},
immutableData: {},
mutableData: {}
}
})
sandbox.stub(uut.bchjs.Electrumx, 'sortAllTxs').resolves([])
const result = await uut.getTokenData(tokenId, true)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result.genesisData.txs)
})
// CT 4/6/23 Saw this error in the wild. Created an error handler for it.
it('should throw error if genesis data has no txs', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
const tokenId =
'5f31905f335fa932879c5aabfd1c14ac748f6696148bd300f845ea5016ad573e'
// Mock dependencies and force desired code path.
sandbox.stub(uut.bchConsumer.bch, 'getTokenData').resolves({
success: true,
tokenData: {
genesisData: {
},
immutableData: {},
mutableData: {}
}
})
sandbox.stub(uut.bchjs.Electrumx, 'sortAllTxs').resolves([])
try {
await uut.getTokenData(tokenId, true)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'No transaction history included with genesis data.')
}
})
// CT 4/6/23 Saw this error in the wild
it('should throw error if timeout occurs with wallet service', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies
sandbox.stub(uut.bchConsumer.bch, 'getTokenData').resolves({
success: false,
message: 'request timed out',
data: ''
})
const tokenId = 'c85042ab08a2099f27de880a30f9a42874202751d834c42717a20801a00aab0d'
try {
await uut.getTokenData(tokenId)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'request timed out')
}
})
})
describe('#getTokenData2', () => {
it('should get token data from bch-js', async () => {
// Mock dependencies
sandbox.stub(uut.bchjs.PsfSlpIndexer, 'getTokenData2').resolves({
tokenIcon: {},
tokenStats: {},
optimizedTokenIcon: {},
iconRepoCompatible: {},
ps002Compatible: {}
})
// Force selected interface.
uut.interface = 'rest-api'
const tokenId = 'c85042ab08a2099f27de880a30f9a42874202751d834c42717a20801a00aab0d'
const result = await uut.getTokenData2(tokenId)
assert.property(result, 'tokenIcon')
assert.property(result, 'tokenStats')
assert.property(result, 'optimizedTokenIcon')
assert.property(result, 'iconRepoCompatible')
assert.property(result, 'ps002Compatible')
})
it('should get token data from bch-consumer', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies
sandbox.stub(uut.bchConsumer.bch, 'getTokenData2').resolves({
tokenData: {
tokenIcon: {},
tokenStats: {},
optimizedTokenIcon: {},
iconRepoCompatible: {},
ps002Compatible: {}
}
})
const tokenId = 'c85042ab08a2099f27de880a30f9a42874202751d834c42717a20801a00aab0d'
const result = await uut.getTokenData2(tokenId)
assert.property(result, 'tokenIcon')
assert.property(result, 'tokenStats')
assert.property(result, 'optimizedTokenIcon')
assert.property(result, 'iconRepoCompatible')
assert.property(result, 'ps002Compatible')
})
it('should throw an error if an interface is not specified', async () => {
try {
uut.interface = ''
const tokenId = 'c85042ab08a2099f27de880a30f9a42874202751d834c42717a20801a00aab0d'
await uut.getTokenData2(tokenId)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'this.interface is not specified')
}
})
it('should throw an error if token ID is not provided', async () => {
try {
await uut.getTokenData2()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'tokenId required as input.')
}
})
})
describe('#getPubKey', () => {
it('should return pubkey from bch-js', async () => {
// Mock dependencies and force desired code path
sandbox.stub(uut.bchjs.encryption, 'getPubKey').resolves({
success: true,
publicKey: '033a24d13b45eaf53bebc7da5b7ee79a39615790b4fb16dab048fdcc5abd3764ef'
})
const addr =
'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d'
const result = await uut.getPubKey(addr)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isString(result)
})
it('should return pubkey from bch-consumer', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies and force desired code path
sandbox.stub(uut.bchConsumer.msg, 'getPubKey').resolves({
success: true,
status: 200,
endpoint: 'pubkey',
pubkey: {
success: true,
publicKey: '033a24d13b45eaf53bebc7da5b7ee79a39615790b4fb16dab048fdcc5abd3764ef'
}
})
const addr =
'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d'
const result = await uut.getPubKey(addr)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isString(result)
})
it('should handle address without a pubkey from bch-js', async () => {
try {
// Mock dependencies and force desired code path
sandbox.stub(uut.bchjs.encryption, 'getPubKey').rejects({
success: false,
error: 'No transaction history.'
})
const addr =
'bitcoincash:qzxeg2p27ls5mkgcy56spadfydau0cyk0y4g90sgtl'
await uut.getPubKey(addr)
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'No transaction history')
}
})
it('should handle address without a pubkey from bch-consumer', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies and force desired code path
sandbox.stub(uut.bchConsumer.msg, 'getPubKey').resolves({
success: false,
status: 422,
message: 'No transaction history.',
endpoint: 'pubkey'
})
try {
const addr =
'bitcoincash:qzxeg2p27ls5mkgcy56spadfydau0cyk0y4g90sgtl'
await uut.getPubKey(addr)
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'No transaction history')
}
})
it('should throw an error if an interface is not specified', async () => {
try {
uut.interface = ''
await uut.getPubKey('fake-addr')
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'this.interface is not specified')
}
})
it('should throw an error if token ID is not provided', async () => {
try {
await uut.getPubKey()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'addr required as input.')
}
})
})
describe('#getPsfWritePrice', () => {
it('should get price from bch-js', async () => {
// Mock dependencies
sandbox.stub(uut.bchjs.Price, 'getPsffppPrice').resolves(100)
// Force selected interface.
uut.interface = 'rest-api'
const result = await uut.getPsfWritePrice()
assert.equal(result, 100)
})
it('should get price from bch-consumer', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies
sandbox.stub(uut.bchConsumer.bch, 'getPsffppWritePrice').resolves(100)
// Force selected interface.
uut.interface = 'consumer-api'
const result = await uut.getPsfWritePrice()
assert.equal(result, 100)
})
it('should throw an error if an interface is not specified', async () => {
try {
uut.interface = ''
await uut.getPsfWritePrice()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'this.interface is not specified')
}
})
})
describe('#cid2json', () => {
it('should convert a CID to a JSON object for bch-consumer', async () => {
const bchjs = new BCHJS()
uut = new AdapterRouter({ bchjs, interface: 'consumer-api' })
// Mock dependencies and force desired code path
sandbox.stub(uut.bchConsumer.bch, 'cid2json').resolves({ key: 'value' })
// Force selected interface.
uut.interface = 'consumer-api'
const result = await uut.cid2json({ cid: 'fake-cid' })
assert.equal(result.key, 'value')
})
it('should throw an error for rest-api interface', async () => {
try {
uut.interface = 'rest-api'
await uut.cid2json({ cid: 'fake-cid' })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'cid2json() is not supported with the rest-api interface.')
}
})
it('should throw an error if no CID is provided', async () => {
try {
await uut.cid2json()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'cid required as input.')
}
})
})
})