Merge pull request #85 from Permissionless-Software-Foundation/ct-unstable

Created JSON RPC endpoint for bulk UTXO lookup
This commit is contained in:
Chris Troutner
2023-02-08 05:43:25 -08:00
committed by GitHub
3 changed files with 364 additions and 5 deletions
+140
View File
@@ -60,6 +60,10 @@ class BCHRPC {
await this.rateLimit.limiter(rpcData.from)
return await this.utxos(rpcData)
case 'utxosBulk':
await this.rateLimit.limiter(rpcData.from)
return await this.utxosBulk(rpcData)
case 'broadcast':
await this.rateLimit.limiter(rpcData.from)
return await this.broadcast(rpcData)
@@ -366,6 +370,142 @@ class BCHRPC {
}
}
/**
* @api {JSON} /bch UTXOs Bulk
* @apiPermission public
* @apiName UTXOs Bulk
* @apiGroup JSON BCH
* @apiDescription This endpoint provides the same data as as the utxos
* endpoint, but it allows retrieval of UTXOs for up to 20 addresses at
* a time. This reduces the number of JSON RPC calls, and is very handy
* for HD wallets that need to quickly scan a lot of addresses.
*
* Given an array of addresses, this endpoint will return an object
* with the following properties
*
* - jsonrpc: "" - jsonrpc version
* - id: "" - jsonrpc id
* - result: {} - Result of the request with the RPC information
* - method: "" - Method used in the request
* - receiver: "" - Receiver address
* - value: [] - Final result value of the request
* - address: "" - the address these UTXOs are associated with
* - bchUtxos: [] - UTXOs confirmed to be spendable as normal BCH
* - nullUtxo: [] - UTXOs that did not pass SLP validation. Should be ignored and
* not spent, to be safe.
* - slpUtxos: {} - UTXOs confirmed to be colored as valid SLP tokens
* - type1: {}
* - tokens: [] - SLP token Type 1 tokens.
* - mintBatons: [] - SLP token Type 1 mint batons.
* - nft: {}
* - tokens: [] - NFT tokens
* - groupTokens: [] - NFT Group tokens, used to create NFT tokens.
* - groupMintBatons: [] - Minting baton to create more NFT Group tokens.
*
* @apiExample Example usage:
* {"jsonrpc":"2.0","id":"555","method":"bch","params":{ "endpoint": "utxos", "address": "bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"}}
*
* @apiSuccessExample {json} Success-Response:
*
* {
* "jsonrpc":"2.0",
* "id":"555",
* "result":{
* "method":"bch",
* "reciever":"QmU86vLVbUY1UhziKB6rak7GPKRA2QHWvzNm2AjEvXNsT6",
* "value":[
* {
* "address":"bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj",
* "bchUtxos":[
* {
* "height":631219,
* "tx_hash":"ae2daa01c8172545b5edd205ea438706bcb74e63d4084a26b9ff2a46d46dc97f",
* "tx_pos":0,
* "value":1000,
* "txid":"ae2daa01c8172545b5edd205ea438706bcb74e63d4084a26b9ff2a46d46dc97f",
* "vout":0,
* "isValid":false
* }
* ],
* "nullUtxos":[
*
* ],
* "slpUtxos":{
* "type1":{
* "mintBatons":[
*
* ],
* "tokens":[
*
* ]
* },
* "nft":{
* "groupMintBatons":[
*
* ],
* "groupTokens":[
*
* ],
* "tokens":[
*
* ]
* }
* }
* }
* ]
* }
* }
*
*/
async utxosBulk (rpcData) {
try {
// console.log('createUser rpcData: ', rpcData)
const addrs = rpcData.payload.params.addresses
// console.log('addr: ', addr)
// Input validation
if (!Array.isArray(addrs)) {
throw new Error('addresses parameter must be an array')
}
if (addrs.length > 20) {
throw new Error('addresses parameter must not exceed 20 elements')
}
const result = []
for (let i = 0; i < addrs.length; i++) {
const thisAddr = addrs[i]
const data = await this.bchjs.Utxo.get(thisAddr)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
result.push({
address: thisAddr,
utxos: data
})
}
const retObj = {}
retObj.status = 200
retObj.success = true
retObj.data = result
return retObj
} catch (err) {
console.error('Error in JSON RPC BCH utxosBulk()')
console.error('Error in JSON RPC utxosBulk(): ', err)
// throw err
// Return an error response
return {
success: false,
status: 422,
message: err.message,
endpoint: 'utxosBulk'
}
}
}
/**
* @api {JSON} /bch Broadcast
* @apiPermission public
@@ -118,6 +118,25 @@ describe('#BCHRPC', () => {
assert.equal(result, true)
})
it('should route to the utxosBulk method', async () => {
// Mock dependencies
sandbox.stub(uut, 'utxosBulk').resolves(true)
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const txCall = jsonrpc.request(id, 'bch', {
endpoint: 'utxosBulk'
})
const jsonStr = JSON.stringify(txCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
rpcData.from = 'Origin request'
const result = await uut.bchRouter(rpcData)
assert.equal(result, true)
})
it('should route to the broadcast method', async () => {
// Mock dependencies
sandbox.stub(uut, 'broadcast').resolves(true)
@@ -175,6 +194,63 @@ describe('#BCHRPC', () => {
assert.equal(result, true)
})
it('should route to the utxoIsValid method', async () => {
// Mock dependencies
sandbox.stub(uut, 'utxoIsValid').resolves(true)
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const rpcCall = jsonrpc.request(id, 'bch', {
endpoint: 'utxoIsValid'
})
const jsonStr = JSON.stringify(rpcCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
rpcData.from = 'Origin request'
const result = await uut.bchRouter(rpcData)
assert.equal(result, true)
})
it('should route to the getTokenData method', async () => {
// Mock dependencies
sandbox.stub(uut, 'getTokenData').resolves(true)
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const rpcCall = jsonrpc.request(id, 'bch', {
endpoint: 'getTokenData'
})
const jsonStr = JSON.stringify(rpcCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
rpcData.from = 'Origin request'
const result = await uut.bchRouter(rpcData)
assert.equal(result, true)
})
it('should route to the getTokenData2 method', async () => {
// Mock dependencies
sandbox.stub(uut, 'getTokenData2').resolves(true)
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const rpcCall = jsonrpc.request(id, 'bch', {
endpoint: 'getTokenData2'
})
const jsonStr = JSON.stringify(rpcCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
rpcData.from = 'Origin request'
const result = await uut.bchRouter(rpcData)
assert.equal(result, true)
})
it('should return 500 status on routing issue', async () => {
// Mock dependencies
sandbox.stub(uut, 'txHistory').rejects(new Error('test error'))
@@ -301,7 +377,7 @@ describe('#BCHRPC', () => {
const id = uid()
const rpcCall = jsonrpc.request(id, 'bch', {
endpoint: 'utxos',
addresses: 'testAddr'
address: 'testAddr'
})
const jsonStr = JSON.stringify(rpcCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
@@ -322,7 +398,7 @@ describe('#BCHRPC', () => {
const id = uid()
const rpcCall = jsonrpc.request(id, 'bch', {
endpoint: 'utxos',
addresses: 'testAddr'
address: 'testAddr'
})
const jsonStr = JSON.stringify(rpcCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
@@ -337,6 +413,98 @@ describe('#BCHRPC', () => {
})
})
describe('#utxosBulk', () => {
it('should return UTXO data from bchjs for multiple addresses', async () => {
// Mock dependencies
sandbox.stub(uut.bchjs.Utxo, 'get').resolves({ success: true })
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const rpcCall = jsonrpc.request(id, 'bch', {
endpoint: 'utxosBulk',
addresses: ['testAddr1', 'testAddr2']
})
const jsonStr = JSON.stringify(rpcCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const response = await uut.utxosBulk(rpcData)
// console.log('response: ', response)
assert.equal(response.success, true)
assert.equal(response.status, 200)
})
it('should return an error for invalid address', async () => {
// Force an error
sandbox.stub(uut.bchjs.Utxo, 'get').rejects(new Error('Invalid address'))
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const rpcCall = jsonrpc.request(id, 'bch', {
endpoint: 'utxosBulk',
addresses: ['testAddr1', 'testAddr2']
})
const jsonStr = JSON.stringify(rpcCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const response = await uut.utxosBulk(rpcData)
// console.log('response: ', response)
assert.equal(response.success, false)
assert.equal(response.status, 422)
assert.equal(response.message, 'Invalid address')
assert.equal(response.endpoint, 'utxosBulk')
})
it('should throw error if input is not an array', async () => {
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const rpcCall = jsonrpc.request(id, 'bch', {
endpoint: 'utxosBulk',
addresses: 'singleAddress'
})
const jsonStr = JSON.stringify(rpcCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const response = await uut.utxosBulk(rpcData)
// console.log('response: ', response)
assert.equal(response.success, false)
assert.equal(response.status, 422)
assert.equal(response.message, 'addresses parameter must be an array')
assert.equal(response.endpoint, 'utxosBulk')
})
it('should throw error if input array is larger than 20 elements', async () => {
// Generate an array larger than 20 elements
const inputArray = []
for (let i = 0; i < 25; i++) {
inputArray.push(i)
}
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const rpcCall = jsonrpc.request(id, 'bch', {
endpoint: 'utxosBulk',
addresses: inputArray
})
const jsonStr = JSON.stringify(rpcCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const response = await uut.utxosBulk(rpcData)
// console.log('response: ', response)
assert.equal(response.success, false)
assert.equal(response.status, 422)
assert.equal(response.message, 'addresses parameter must not exceed 20 elements')
assert.equal(response.endpoint, 'utxosBulk')
})
})
describe('#broadcast', () => {
it('should return data from bchjs', async () => {
// Mock dependencies
@@ -385,6 +553,32 @@ describe('#BCHRPC', () => {
assert.equal(response.message, 'Invalid data')
assert.equal(response.endpoint, 'broadcast')
})
it('should return an error if full node returns a string error', async () => {
// Force an error
sandbox
.stub(uut.bchjs.RawTransactions, 'sendRawTransaction')
// .rejects(new Error('Invalid data'))
.rejects({ error: 'Invalid data' })
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const rpcCall = jsonrpc.request(id, 'bch', {
endpoint: 'broadcast',
hex: 'testHex'
})
const jsonStr = JSON.stringify(rpcCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const response = await uut.broadcast(rpcData)
// console.log('response: ', response)
assert.equal(response.success, false)
assert.equal(response.status, 422)
assert.equal(response.message, 'Invalid data')
assert.equal(response.endpoint, 'broadcast')
})
})
describe('#txData', () => {
@@ -568,7 +762,8 @@ describe('#BCHRPC', () => {
// Mock dependencies
sandbox
.stub(uut.bchjs.Utxo, 'isValid')
.rejects(new Error('test error'))
// .rejects(new Error('test error'))
.rejects({ error: 'test error' })
const rpcData = {
payload: {
@@ -619,7 +814,8 @@ describe('#BCHRPC', () => {
// Mock dependencies
sandbox
.stub(uut.bchjs.PsfSlpIndexer, 'getTokenData')
.rejects(new Error('test error'))
// .rejects(new Error('test error'))
.rejects({ error: 'test error' })
const rpcData = {
payload: {
@@ -667,7 +863,8 @@ describe('#BCHRPC', () => {
// Mock dependencies
sandbox
.stub(uut.bchjs.PsfSlpIndexer, 'getTokenData2')
.rejects(new Error('test error'))
// .rejects(new Error('test error'))
.rejects({ error: 'test error' })
const rpcData = {
payload: {
+22
View File
@@ -55,6 +55,28 @@ describe('#server', () => {
// Not throwing an error is a success
})
it('should warn if user admin can not be created', async () => {
// Mock dependencies and force desired code path
sandbox.stub(uut.mongoose, 'connect').resolves()
sandbox.stub(uut.controllers, 'initAdapters').resolves()
sandbox.stub(uut.controllers, 'initUseCases').resolves()
sandbox.stub(uut.controllers, 'attachRESTControllers').resolves()
sandbox.stub(uut.adminLib, 'createSystemUser').rejects('test error')
sandbox.stub(uut.controllers, 'attachControllers').resolves()
uut.config.env = 'dev'
const result = await uut.startServer()
// console.log('result: ', result)
assert.property(result, 'env')
// Turn off the server.
uut.server.close()
// Restor config env
uut.config.env = 'test'
})
})
describe('#sleep', () => {