Compare commits

...
25 Commits
Author SHA1 Message Date
Chris Troutner d9fa44328a Merge pull request #61 from Permissionless-Software-Foundation/ct-unstable
Heavily refactored transaction history JSON RPC endpoint
2022-01-28 08:15:36 -08:00
Chris Troutner efaffa8e0f fix(getTransactions()): Added unit tests 2022-01-28 08:03:35 -08:00
Chris Troutner 27a9e9f3f3 Working on unit tests 2022-01-28 06:55:50 -08:00
Chris Troutner 41a5071467 fix(JSON transactions): Updated to new algorithm 2022-01-28 06:37:18 -08:00
Chris Troutner c6c3bb5d9c More debugging of pagination 2022-01-27 19:01:57 -08:00
Chris Troutner e4977a4351 Debugging pagination 2022-01-27 18:58:28 -08:00
Chris Troutner fe3b45f6be fixing typo 2022-01-27 17:54:17 -08:00
Chris Troutner d25b240112 Debugging 2022-01-27 17:52:16 -08:00
Chris Troutner 7a89a06ddb fix(getTransactions()): Adding pagination to tx history 2022-01-27 17:47:03 -08:00
Chris Troutner 74fb65df72 more debugging 2022-01-27 15:10:46 -08:00
Chris Troutner 0b38247f5a more debugging 2022-01-27 15:05:08 -08:00
Chris Troutner 473aaae307 debugging 2022-01-27 15:00:54 -08:00
Chris Troutner 284f4e4478 Debugging and reporting errors 2022-01-27 14:58:00 -08:00
Chris Troutner 56f6dc273a Restoring bch-js 2022-01-27 09:06:39 -08:00
Chris Troutner 4bd3387bde feat(transactions): Refactored JSON RPC transactions endpoint 2022-01-27 08:47:06 -08:00
Chris Troutner 7e7e91460a fix(integration): Adding integration test script to package.json 2022-01-24 13:21:29 -08:00
Chris Troutner e5f6f24570 Created mock unit tests for BCH use cases 2022-01-24 13:13:55 -08:00
Chris Troutner 818c85e3d4 Merge branch 'master' into ct-unstable 2022-01-24 12:32:24 -08:00
Chris Troutner 651c4a1346 Debugging transactions JSON RPC endpoint 2022-01-19 15:58:25 -08:00
Chris Troutner 4b90ad5009 fix(bch-js): Upgrading to v5.2.2 2022-01-19 15:31:25 -08:00
Chris Troutner 05ddf8521a fix(bch-js): Updating to v5.2.1 2022-01-19 11:34:58 -08:00
Chris Troutner 88d1805685 Merge pull request #60 from Permissionless-Software-Foundation/ct-unstable
No-op
2022-01-18 15:38:12 -08:00
Chris Troutner ea104b5586 Rolling back debug console.logs 2022-01-18 15:31:15 -08:00
Chris Troutner 4251e2a02d more debugging 2022-01-18 15:22:26 -08:00
Chris Troutner 5e0eb017ef Debugging 2022-01-18 15:16:45 -08:00
16 changed files with 738 additions and 369 deletions
+346 -318
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -9,6 +9,7 @@
"test:all": "export SVC_ENV=test && nyc --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/",
"test:unit": "export SVC_ENV=test && mocha --exit --timeout 15000 --recursive test/unit/",
"test:e2e:auto": "export SVC_ENV=test && mocha --exit --timeout 30000 test/e2e/automated/",
"test:integration": "export SVC_ENV=test && mocha --timeout 30000 --recursive test/integration/",
"test:temp": "export SVC_ENV=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/",
"lint": "standard --env mocha --fix",
"docs": "./node_modules/.bin/apidoc -i src/ -o docs",
@@ -23,7 +24,7 @@
},
"repository": "Permissionless-Software-Foundation/ipfs-bch-wallet-service",
"dependencies": {
"@psf/bch-js": "5.2.0",
"@psf/bch-js": "5.3.0",
"axios": "0.25.0",
"bcryptjs": "2.4.3",
"glob": "7.1.6",
@@ -64,6 +65,7 @@
"eslint-plugin-prettier": "3.3.1",
"eslint-plugin-standard": "4.0.0",
"husky": "4.3.8",
"lodash.clonedeep": "^4.5.0",
"mocha": "8.2.1",
"nyc": "15.1.0",
"semantic-release": "17.4.4",
+4 -2
View File
@@ -47,8 +47,10 @@ class Adapters {
this.bchjs = new BCHJS({ restURL: this.config.apiServer })
}
// Start the IPFS node.
await this.ipfs.start({ bchjs: this.bchjs })
if (this.config.env !== 'test') {
// Start the IPFS node.
await this.ipfs.start({ bchjs: this.bchjs })
}
} catch (err) {
console.error('Error in adapters/index.js/start()')
throw err
+24 -28
View File
@@ -32,7 +32,6 @@ class BCHRPC {
this.jsonrpc = jsonrpc
this.validators = new Validators(localConfig)
this.rateLimit = new RateLimit()
// this.bchjs = new BCHJS()
this.bchjs = this.adapters.bchjs
}
@@ -92,7 +91,12 @@ class BCHRPC {
* @apiName Transactions
* @apiGroup JSON BCH
* @apiDescription This endpoint wraps the bchjs.Electrumx.transactions([]) function.
* Given the 'addresses' property this endpoint returns an object with the following properties
* There are three possible inputs:
* - address: (required) the address to query for a transaction history
* - sortOrder: (optional) will sort results in 'DECENDING' (default) or 'ASCENDING' order.
* - page: (optional) will return a 'page' of 100 results. Default is 0
*
* Given the 'address' property this endpoint returns an object with the following properties
*
* - jsonrpc: "" - jsonrpc version
* - id: "" - jsonrpc id
@@ -101,15 +105,14 @@ class BCHRPC {
* - receiver: "" - Receiver address
* - value: {} - Final result value of the petition
* - success : - Petition status
* - transactions : [] - Transactions of the provided adresses
* - transactions: [] - Transaction details
* - height : - Reference to the blockchain size
* - tx_hash: "" - Hash of the transaction
* - address : "" - Address asociated to the transactions
* - txs : [] - Transactions of the provided address
* - height : - Reference to the blockchain size
* - tx_hash: "" - Hash of the transaction
* - address : "" - Address asociated to the transactions
* - status: - HTTP Status Code
*
* @apiExample Example usage:
* {"jsonrpc":"2.0","id":"555","method":"bch","params":{ "endpoint": "transactions", "addresses": ["bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"]}}
* {"jsonrpc":"2.0","id":"555","method":"bch","params":{ "endpoint": "transactions", "addresses": ["bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"], "sortOrder": "DESCENDING", "page": 0}}
*
* @apiSuccessExample {json} Success-Response:
* {
@@ -119,17 +122,13 @@ class BCHRPC {
* "method":"bch",
* "reciever":"QmU86vLVbUY1UhziKB6rak7GPKRA2QHWvzNm2AjEvXNsT6",
* "value":{
* "address":"bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"
* "success":true,
* "transactions":[
* {
* "transactions":[
* {
* "height":631219,
* "tx_hash":"ae2daa01c8172545b5edd205ea438706bcb74e63d4084a26b9ff2a46d46dc97f"
* }
* ],
* "address":"bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"
* }
* "txs":[
* {
* "height":631219,
* "tx_hash":"ae2daa01c8172545b5edd205ea438706bcb74e63d4084a26b9ff2a46d46dc97f"
* }
* ],
* "status":200
* }
@@ -138,17 +137,14 @@ class BCHRPC {
*/
async transactions (rpcData) {
try {
// console.log('createUser rpcData: ', rpcData)
// console.log('transactions rpcData: ', rpcData)
const addrs = rpcData.payload.params.addresses
// const addr = rpcData.payload.params.address
// const sortOrder = rpcData.payload.params.sortOrder
const data = await this.bchjs.Electrumx.transactions(addrs)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
const data = await this.useCases.bch.getTransactions(rpcData)
const retObj = data
retObj.status = 200
return retObj
return data
} catch (err) {
console.error('Error in JSON RPC BCH transactions()')
// throw err
@@ -331,7 +327,7 @@ class BCHRPC {
// console.log('createUser rpcData: ', rpcData)
const addr = rpcData.payload.params.address
console.log('addr: ', addr)
// console.log('addr: ', addr)
const data = await this.bchjs.Utxo.get(addr)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
@@ -529,7 +525,7 @@ class BCHRPC {
const txid = rpcData.payload.params.txid
const data = await this.bchjs.Transaction.get(txid.toString())
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
console.log(`data: ${JSON.stringify(data, null, 2)}`)
const retObj = data
retObj.status = 200
+85
View File
@@ -0,0 +1,85 @@
/*
Use Cases for interacting with the BCH blockchain.
*/
class BCHUseCases {
constructor (localConfig = {}) {
// console.log('User localConfig: ', localConfig)
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of adapters must be passed in when instantiating BCH Use Cases library.'
)
}
// Encapsulate dependencies
this.bchjs = this.adapters.bchjs
}
// Get transaction history for an address, sorted by block height.
async getTransactions (rpcData) {
try {
console.log(
`getTransactions rpcData: ${JSON.stringify(rpcData, null, 2)}`
)
// Get the list of addresses.
const addr = rpcData.payload.params.address
// Default to descending sorting.
let sortOrder = rpcData.payload.params.sortOrder
if (!sortOrder) sortOrder = 'DESCENDING'
// console.log('sortOrder: ', sortOrder)
// Default to page 1
let page = rpcData.payload.params.page
if (!page) page = 0
console.log('page: ', page)
// Get the transaction history for the list of addresses.
const data = await this.bchjs.Electrumx.transactions([addr])
console.log(`data: ${JSON.stringify(data, null, 2)}`)
if (!data.success) {
throw new Error('Could not query Fulcrum indexer.')
}
// console.log(`transactions: ${JSON.stringify(transactions, null, 2)}`);
// Sort the transactions.
const txsArr = await this.bchjs.Electrumx.sortAllTxs(
data.transactions[0].transactions,
sortOrder
)
console.log(`txsArr: ${JSON.stringify(txsArr, null, 2)}`)
// Paginate the results
const pagedResults = this.bchjs.Util.chunk100(txsArr)
console.log(
`pagedResults[page]: ${JSON.stringify(pagedResults[page], null, 2)}`
)
const retObj = {
address: addr,
txs: pagedResults[page],
status: 200,
success: true
}
// console.log(`retObj: ${JSON.stringify(retObj, null, 2)}`)
return retObj
} catch (err) {
console.error('Error in JSON RPC BCH transactions(): ', err)
// throw err
// Return an error response
return {
success: false,
status: 422,
message: err.message,
endpoint: 'transactions'
}
}
}
}
module.exports = BCHUseCases
+3
View File
@@ -4,7 +4,9 @@
https://troutsblog.com/blog/clean-architecture
*/
// Local libraries
const UserUseCases = require('./user')
const BCHUseCases = require('./bch')
class UseCases {
constructor (localConfig = {}) {
@@ -17,6 +19,7 @@ class UseCases {
// console.log('use-cases/index.js localConfig: ', localConfig)
this.user = new UserUseCases(localConfig)
this.bch = new BCHUseCases(localConfig)
}
}
+12 -4
View File
@@ -5,10 +5,11 @@
// Public npm libraries
const BCHJS = require('@psf/bch-js')
const IpfsCoord = require('ipfs-coord')
const IPFS = require('@chris.troutner/ipfs')
const IPFS = require('ipfs-http-client')
const EventEmitter = require('events')
const { v4: uid } = require('uuid')
const jsonrpc = require('jsonrpc-lite')
const http = require('http')
let _this
@@ -88,8 +89,14 @@ class TestUtils {
async startIpfs () {
try {
const ipfsOptionsExternal = {
host: 'localhost',
port: 5001,
agent: http.Agent({ keepAlive: true, maxSockets: 2000 })
}
// Start the IPFS node.
this.ipfs = await IPFS.create()
this.ipfs = await IPFS.create(ipfsOptionsExternal)
await this.ipfs.config.profiles.apply('server')
// Start ipfs-coord.
@@ -101,7 +108,8 @@ class TestUtils {
privateLog: this.rpcHandler, // Default to console.log
isCircuitRelay: false,
apiInfo: 'none',
announceJsonLd: announceJsonLd
announceJsonLd: announceJsonLd,
nodeType: 'external'
})
await this.ipfsCoord.start()
@@ -214,7 +222,7 @@ class TestUtils {
const id = uid()
const cmd = jsonrpc.request(id, 'bch', {
endpoint: 'transactions',
addresses: ['bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj']
address: 'bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj'
})
const cmdStr = JSON.stringify(cmd)
+2 -2
View File
@@ -1,4 +1,4 @@
{
"uutAddr": "/ip4/127.0.0.1/tcp/5268/p2p/QmdBGB8S6sEr19gaAxJYjhRbz5ZmMiH5a1JyyMmaxcRKnY",
"uutId": "QmdBGB8S6sEr19gaAxJYjhRbz5ZmMiH5a1JyyMmaxcRKnY"
"uutAddr": "/ip4/88.99.188.196/tcp/4001/p2p/12D3KooWT2tDg2pHwKz84Htzh2KsfSUZbZXmoAUFnaSwUVrDsag3/p2p-circuit/ipfs/12D3KooWNuZPDb15ENgk2CYF2dByq1oK6CQ5tUpRW9CpwwNynuJY",
"uutId": "12D3KooWNuZPDb15ENgk2CYF2dByq1oK6CQ5tUpRW9CpwwNynuJY"
}
@@ -0,0 +1,85 @@
/*
Integration tests for the BCH Use Cases library
*/
// Global npm libraries
const assert = require('chai').assert
// Local libraries
const BCHUseCases = require('../../../../src/use-cases/bch')
const Adapters = require('../../../../src/adapters')
describe('#BCH', () => {
let uut
before(async () => {
const adapters = new Adapters()
await adapters.start()
uut = new BCHUseCases({ adapters })
})
describe('#getTransactions', () => {
it('should get transactions for an address, with default descending sorting', async () => {
const rpcData = {
payload: {
params: {
address: 'bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v'
}
}
}
const result = await uut.getTransactions(rpcData)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result.success, true)
assert.equal(result.status, 200)
assert.isArray(result.txs)
// Assert descending sort order.
assert.isAbove(result.txs[0].height, result.txs[1].height)
})
it('should get transactions for an address, with explicit descending sorting', async () => {
const rpcData = {
payload: {
params: {
address: 'bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v',
sortOrder: 'DESCENDING'
}
}
}
const result = await uut.getTransactions(rpcData)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result.success, true)
assert.equal(result.status, 200)
assert.isArray(result.txs)
// Assert descending sort order.
assert.isAbove(result.txs[0].height, result.txs[1].height)
})
it('should sort in ascending order', async () => {
const rpcData = {
payload: {
params: {
address: 'bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v',
sortOrder: 'ASCENDING'
}
}
}
const result = await uut.getTransactions(rpcData)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result.success, true)
assert.equal(result.status, 200)
assert.isArray(result.txs)
// Assert descending sort order.
assert.isAbove(result.txs[1].height, result.txs[0].height)
})
})
})
@@ -199,11 +199,6 @@ describe('#BCHRPC', () => {
describe('#transactions', () => {
it('should return data from bchjs', async () => {
// Mock dependencies
sandbox
.stub(uut.bchjs.Electrumx, 'transactions')
.resolves({ success: true })
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
@@ -224,7 +219,7 @@ describe('#BCHRPC', () => {
it('should return an error for invalid address', async () => {
// Force an error
sandbox
.stub(uut.bchjs.Electrumx, 'transactions')
.stub(uut.useCases.bch, 'getTransactions')
.rejects(new Error('Invalid address'))
// Generate the parsed data that the main router would pass to this
@@ -442,7 +437,11 @@ describe('#BCHRPC', () => {
describe('#pubKey', () => {
it('should return data from bchjs', async () => {
// Mock dependencies
const mock = { success: true, publicKey: '033f267fec0f7eb2b27f8c2e3052b3d03b09d36b47de4082ffb638ffb334ef0eee' }
const mock = {
success: true,
publicKey:
'033f267fec0f7eb2b27f8c2e3052b3d03b09d36b47de4082ffb638ffb334ef0eee'
}
sandbox.stub(uut.bchjs.encryption, 'getPubKey').resolves(mock)
// Generate the parsed data that the main router would pass to this
+5 -1
View File
@@ -56,7 +56,8 @@ const localdb = {
const bchjs = {
Electrumx: {
transactions: () => {},
balance: () => {}
balance: () => {},
sortAllTxs: () => {}
},
Utxo: {
get: () => {}
@@ -69,6 +70,9 @@ const bchjs = {
},
encryption: {
getPubKey: () => {}
},
Util: {
chunk100: () => {}
}
}
@@ -0,0 +1,28 @@
/*
Mock data for use-cases/bch/index.js unit tests
*/
const fulcrumOut01 = {
success: true,
transactions: [
{
transactions: [
{
height: 560430,
tx_hash:
'3e1f3e882be9c03897eeb197224bf87f312be556a89f4308fabeeeabcf9bc851'
},
{
height: 560534,
tx_hash:
'4ebbeaac51ce141e262964e3a0ce11b96ca72c0dffe9b4127ce80135f503a280'
}
],
address: 'bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v'
}
]
}
module.exports = {
fulcrumOut01
}
+10
View File
@@ -31,12 +31,22 @@ class UserUseCaseMock {
}
}
class BCH {
async getTransactions(rpcData) {
return {
success: true,
status: 200
}
}
}
class UseCasesMock {
constuctor(localConfig = {}) {
// this.user = new UserUseCaseMock(localConfig)
}
user = new UserUseCaseMock()
bch = new BCH()
}
module.exports = UseCasesMock
@@ -0,0 +1,125 @@
/*
Unit tests for the BCH Use Cases
*/
// Public npm libraries
const assert = require('chai').assert
const sinon = require('sinon')
const clone = require('lodash.clonedeep')
// Local support libraries
const BCHUseCases = require('../../../../src/use-cases/bch')
const adapters = require('../../mocks/adapters')
const mockDataLib = require('../../mocks/use-cases/bch/bch.use-cases.mocks.js')
describe('#bch-use-case', () => {
let uut
let sandbox
let mockData
beforeEach(() => {
sandbox = sinon.createSandbox()
mockData = clone(mockDataLib)
uut = new BCHUseCases({ adapters })
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new BCHUseCases()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of adapters must be passed in when instantiating BCH Use Cases library.'
)
}
})
})
describe('#getTransactions', () => {
it('should get transactions for an address', async () => {
// Mock dependencies
sandbox
.stub(uut.bchjs.Electrumx, 'transactions')
.resolves(mockData.fulcrumOut01)
sandbox
.stub(uut.bchjs.Electrumx, 'sortAllTxs')
.resolves(mockData.fulcrumOut01.transactions[0].transactions)
sandbox
.stub(uut.bchjs.Util, 'chunk100')
.returns([mockData.fulcrumOut01.transactions[0].transactions])
const address = 'bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v'
const rpcData = {
payload: {
params: {
address
}
}
}
const result = await uut.getTransactions(rpcData)
// console.log('result: ', result)
// Assert expected values and properties.
assert.equal(result.address, address)
assert.equal(result.status, 200)
assert.equal(result.success, true)
assert.isArray(result.txs)
})
it('should return error if Fulcrum communication is not possible', async () => {
// Force an error
sandbox
.stub(uut.bchjs.Electrumx, 'transactions')
.resolves({ success: false })
const address = 'bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v'
const rpcData = {
payload: {
params: {
address
}
}
}
const result = await uut.getTransactions(rpcData)
// console.log('result: ', result)
assert.equal(result.success, false)
assert.equal(result.status, 422)
assert.equal(result.message, 'Could not query Fulcrum indexer.')
assert.equal(result.endpoint, 'transactions')
})
it('should return error if there is an error', async () => {
// Force an error
sandbox
.stub(uut.bchjs.Electrumx, 'transactions')
.rejects(new Error('test error'))
const address = 'bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v'
const rpcData = {
payload: {
params: {
address
}
}
}
const result = await uut.getTransactions(rpcData)
// console.log('result: ', result)
assert.equal(result.success, false)
assert.equal(result.status, 422)
assert.equal(result.message, 'test error')
assert.equal(result.endpoint, 'transactions')
})
})
})
@@ -7,9 +7,6 @@ const assert = require('chai').assert
const sinon = require('sinon')
// Local support libraries
// const testUtils = require('../../utils/test-utils')
// Unit under test (uut)
const UseCases = require('../../../src/use-cases')
const adapters = require('../mocks/adapters')
@@ -9,9 +9,6 @@ const assert = require('chai').assert
const sinon = require('sinon')
// Local support libraries
// const testUtils = require('../../utils/test-utils')
// Unit under test (uut)
const UserLib = require('../../../src/use-cases/user')
const adapters = require('../mocks/adapters')