Compare commits

...
37 Commits
Author SHA1 Message Date
Chris Troutner f566d44252 Merge pull request #27 from Permissionless-Software-Foundation/ct-unstable
fix(electrumx): Fixes GET tx details
2020-09-10 10:54:25 -07:00
Chris Troutner 0e7b45214f fix(elecrtrumx): Fixes GET tx details 2020-09-10 10:51:22 -07:00
Chris Troutner 58366b5442 Merge pull request #26 from Permissionless-Software-Foundation/25-fix-txdetails-get
Change url param name
2020-09-10 10:48:32 -07:00
Rosco Kalis f97e7016c5 Change url param name 2020-09-10 19:39:35 +02:00
Chris Troutner f82c85712c Merge pull request #24 from Permissionless-Software-Foundation/ct-unstable
fix(electrum-cash): Upgrading to electrum-cash v2.0.2
2020-08-29 13:28:22 -07:00
Chris Troutner 5f18bc578b fix(electrum-cash): Upgrading to electrum-cash v2.0.2 2020-08-29 13:24:22 -07:00
Chris Troutner e72a6b50e4 Merge pull request #22 from Permissionless-Software-Foundation/7-electrum-block-header
Add /block/headers GET/POST endpoints + tests and mock data
2020-08-27 17:12:02 -07:00
Chris Troutner 66c99ae442 Adding temp.sh to the gitignore 2020-08-27 16:54:59 -07:00
Rosco Kalis 5e04125bc3 Update electrum-cash version 2020-08-26 12:46:53 +02:00
Rosco Kalis fd3fd43800 Add /block/headers GET/POST endpoints + tests and mock data 2020-08-26 12:32:32 +02:00
Chris Troutner 5184da7c26 Merge pull request #23 from Permissionless-Software-Foundation/ct-unstable
fix(webhooks): testing webhooks deployment
2020-08-21 17:28:51 -07:00
Chris Troutner 595caffb51 fix(webhooks): testing webhooks deployment 2020-08-21 17:26:33 -07:00
Chris Troutner 6b00f0fd5b Merge pull request #20 from Permissionless-Software-Foundation/5-electrum-tx-data
Add get transaction details from Electrum
2020-08-17 10:33:50 -07:00
Chris Troutner e4a427788f Merge branch 'master' into 5-electrum-tx-data 2020-08-17 10:31:08 -07:00
Chris Troutner 27a2825a43 fix(Electrumx): Adding get tx details endpoints 2020-08-17 10:31:05 -07:00
Chris Troutner 3d453c335f Merge pull request #17 from rkalis/electrumx-p2sh
Enable P2SH addresses to be used with the ElectrumX endpoints
2020-08-17 08:17:50 -07:00
Chris Troutner 9a85a2a4ef Merge pull request #19 from Permissionless-Software-Foundation/ct-unstable
fix(util): Fixed failing integration test
2020-08-17 08:09:30 -07:00
Chris Troutner 443fb233ae fix(util): Fixed failing integration test 2020-08-17 08:08:54 -07:00
Rosco Kalis 63b8168c45 Add get transaction details from Electrum
- Add functions for getting a single transaction details or bulk
- Add GET and POST routes for these functions
- Add tests +mock data for these functions

- Add test helper function for stubbing a method for unit tests
- Add test helper function for asserting an API error
2020-08-16 19:43:12 +02:00
Rosco Kalis 1e5dae56af Enable P2SH addresses to be used with the ElectrumX endpoints 2020-08-14 12:15:26 +02:00
Chris Troutner 553d6fd4b0 Merge pull request #16 from Permissionless-Software-Foundation/ct-unstable
fix(slp-api): Adding increased rate limits for slp-api
2020-08-08 10:54:05 -07:00
Chris Troutner ab98bf05da fix(slp-api): Adding increased rate limits for slp-api 2020-08-08 10:48:12 -07:00
Chris Troutner 6806b4a197 Doing silent npm install 2020-08-08 10:27:34 -07:00
Chris Troutner 4ae74aed59 Merge pull request #15 from Permissionless-Software-Foundation/ct-unstable
fix(Docker): Updating dockerfile to PSF repo
2020-08-08 09:35:29 -07:00
Chris Troutner 98e1a5b40f fix(Docker): Updating dockerfile to PSF repo 2020-08-08 09:32:36 -07:00
Chris Troutner 279c9b7bea Merge pull request #14 from Permissionless-Software-Foundation/ct-unstable
Tracking origin of requests in logs
2020-08-08 08:27:52 -07:00
Chris Troutner 56be614980 Merge branch 'master' into ct-unstable 2020-08-08 08:22:33 -07:00
Chris Troutner 584037ebdd fix(origin): Tracking origin of requests in logs 2020-08-08 08:22:23 -07:00
Chris Troutner 44230b73f7 Merge pull request #13 from Permissionless-Software-Foundation/ct-unstable
fix(deploy): Testing deploy again
2020-08-07 17:18:08 -07:00
Chris Troutner b7f1eba6bd fix(deploy): Testing deploy again 2020-08-07 17:15:00 -07:00
Chris Troutner 35bc3025d6 Merge pull request #12 from Permissionless-Software-Foundation/ct-unstable
fix(deploy): Updating deploy script
2020-08-07 17:06:59 -07:00
Chris Troutner 6f6531428a fix(deploy): Updating deploy script 2020-08-07 17:00:57 -07:00
Chris Troutner cb5e962167 Merge pull request #11 from Permissionless-Software-Foundation/ct-unstable
feat(SLP balance): Removing redundent, non-functional SLP balance end…
2020-08-06 14:25:01 -07:00
Chris Troutner 33da626f78 Fixing linting issues 2020-08-06 14:21:39 -07:00
Chris Troutner e9766630ed Updating index.html 2020-08-06 14:14:10 -07:00
Chris Troutner 9b6adbf4ec Updates to readme 2020-08-06 14:06:45 -07:00
Chris Troutner 5f09447da4 feat(SLP balance): Removing redundent, non-functional SLP balance endpoint 2020-08-06 14:02:22 -07:00
14 changed files with 1022 additions and 263 deletions
+1
View File
@@ -19,6 +19,7 @@ start-fullstack-test.sh
start-free-main.sh start-free-main.sh
start-local-mainnet.sh.save start-local-mainnet.sh.save
start-local-testnet.sh.save start-local-testnet.sh.save
temp.sh
coverage coverage
start-my-infra start-my-infra
+4 -6
View File
@@ -1,6 +1,5 @@
# bch-api # bch-api
[![Greenkeeper badge](https://badges.greenkeeper.io/christroutner/bch-api.svg)](https://greenkeeper.io/)
[![Build Status](https://travis-ci.org/christroutner/bch-api.svg?branch=master)](https://travis-ci.org/christroutner/bch-api) [![Build Status](https://travis-ci.org/christroutner/bch-api.svg?branch=master)](https://travis-ci.org/christroutner/bch-api)
@@ -16,11 +15,10 @@ of [BITBOX SDK](https://github.com/Bitcoin-com/bitbox-sdk).
Both bch-api and bch-js are part of the Both bch-api and bch-js are part of the
[full stack of BCH software](https://troutsblog.com/research/bitcoin-cash/how-to-bch-full-stack-developer). [full stack of BCH software](https://troutsblog.com/research/bitcoin-cash/how-to-bch-full-stack-developer).
- [API Documentation](https://api.bchjs.cash/docs/) - [API Documentation](https://fullstack.cash/documentation)
- [BCH Example Applications](https://github.com/Permissionless-Software-Foundation/bch-js-examples) - [Example Code](https://fullstack.cash/examples)
- [slp-cli-wallet](https://github.com/christroutner/slp-cli-wallet): a hacker-friendly, command-line BCH and SLP token wallet using this REST API.
Have questions? Need help? Join our community support Have questions? Need help? Join our community support:
[Telegram channel](https://t.me/bch_js_toolkit) [Telegram channel](https://t.me/bch_js_toolkit)
## Features ## Features
@@ -96,4 +94,4 @@ Have questions? Need help? Join our community support
[Telegram channel](https://t.me/bch_js_toolkit) [Telegram channel](https://t.me/bch_js_toolkit)
## License ## License
[MIT](LICENSE.md) [MIT](./LICENSE.md)
+2
View File
@@ -11,5 +11,7 @@ export DATA="{\"ref\":\"$DEPLOY_SECRET\"}"
curl -X POST http://fullstack.cash:9000/hooks/bch-api-mainnet -H "Content-Type: application/json" -d $DATA curl -X POST http://fullstack.cash:9000/hooks/bch-api-mainnet -H "Content-Type: application/json" -d $DATA
curl -X POST http://fullstack.cash:9000/hooks/bch-api-testnet -H "Content-Type: application/json" -d $DATA curl -X POST http://fullstack.cash:9000/hooks/bch-api-testnet -H "Content-Type: application/json" -d $DATA
curl -X POST http://$FREE_SERVER:9000/hooks/bch-api-testnet -H "Content-Type: application/json" -d $DATA
curl -X POST http://$FREE_SERVER:9000/hooks/bch-api-mainnet -H "Content-Type: application/json" -d $DATA
echo "...Finished deploying to production." echo "...Finished deploying to production."
+2 -2
View File
@@ -16,9 +16,9 @@ USER safeuser
# Clone the repository # Clone the repository
WORKDIR /home/safeuser WORKDIR /home/safeuser
RUN git clone https://github.com/christroutner/bch-api RUN git clone https://github.com/Permissionless-Software-Foundation/bch-api
WORKDIR /home/safeuser/bch-api WORKDIR /home/safeuser/bch-api
RUN npm install RUN npm install --silent
# Generate documentation # Generate documentation
RUN npm run docs RUN npm run docs
+2 -2
View File
@@ -13,9 +13,9 @@ USER safeuser
# Clone the repository # Clone the repository
WORKDIR /home/safeuser WORKDIR /home/safeuser
RUN git clone https://github.com/christroutner/bch-api RUN git clone https://github.com/Permissionless-Software-Foundation/bch-api
WORKDIR /home/safeuser/bch-api WORKDIR /home/safeuser/bch-api
RUN npm install RUN npm install --silent
# Generate documentation # Generate documentation
RUN npm run docs RUN npm run docs
+43 -4
View File
@@ -872,6 +872,14 @@
"resolved": "https://registry.npmjs.org/@types/tus-js-client/-/tus-js-client-1.8.0.tgz", "resolved": "https://registry.npmjs.org/@types/tus-js-client/-/tus-js-client-1.8.0.tgz",
"integrity": "sha512-lWxu5+6qfyfwsW99GzUeJ9y9JeOSSLduKxgYMvaYM7sGTDKZsrIIHTUbHI2P016xhXtu9NxmUM3GrB4i14ie4A==" "integrity": "sha512-lWxu5+6qfyfwsW99GzUeJ9y9JeOSSLduKxgYMvaYM7sGTDKZsrIIHTUbHI2P016xhXtu9NxmUM3GrB4i14ie4A=="
}, },
"@types/ws": {
"version": "7.2.6",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.6.tgz",
"integrity": "sha512-Q07IrQUSNpr+cXU4E4LtkSIBPie5GLZyyMC1QtQYRLWz701+XcoVygGUZgvLqElq1nU4ICldMYPnexlBsg3dqQ==",
"requires": {
"@types/node": "*"
}
},
"@uppy/companion-client": { "@uppy/companion-client": {
"version": "1.4.5", "version": "1.4.5",
"resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-1.4.5.tgz", "resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-1.4.5.tgz",
@@ -1402,6 +1410,21 @@
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
}, },
"async-mutex": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.2.4.tgz",
"integrity": "sha512-fcQKOXUKMQc57JlmjBCHtkKNrfGpHyR7vu18RfuLfeTAf4hK9PgOadPR5cDrBQ682zasrLUhJFe7EKAHJOduDg==",
"requires": {
"tslib": "^2.0.0"
},
"dependencies": {
"tslib": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
}
}
},
"asynckit": { "asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -4424,11 +4447,22 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
}, },
"electrum-cash": { "electrum-cash": {
"version": "1.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/electrum-cash/-/electrum-cash-1.0.1.tgz", "resolved": "https://registry.npmjs.org/electrum-cash/-/electrum-cash-2.0.2.tgz",
"integrity": "sha512-snMgRt6JzsHdCdf+Un1rLj4RXjje1WuFVTx3Xx+6mitEibYalmD+x0ts146VF4Ki3+42DQKXtQXl0bzCAY7Jtw==", "integrity": "sha512-b/mLctjwnmfp4cuvJbl9wgCxf4GWWiKPYZXEtekoyGPviZwOe2bnMC7LGOxAMpOW0YyU4W12kK6rbqskQc6EkA==",
"requires": { "requires": {
"debug": "^4.1.1" "@types/ws": "^7.2.6",
"async-mutex": "^0.2.4",
"debug": "^4.1.1",
"isomorphic-ws": "^4.0.1",
"ws": "^7.3.1"
},
"dependencies": {
"ws": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
"integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA=="
}
} }
}, },
"elliptic": { "elliptic": {
@@ -7551,6 +7585,11 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true "dev": true
}, },
"isomorphic-ws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="
},
"isstream": { "isstream": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+2 -2
View File
@@ -17,7 +17,7 @@
"test": "npm run lint && npm run test-v3", "test": "npm run lint && npm run test-v3",
"lint": "standard --env mocha --fix", "lint": "standard --env mocha --fix",
"test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/", "test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/",
"test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/a01-electrumx.js", "test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/util.js",
"test:integration": "mocha test/v3/integration", "test:integration": "mocha test/v3/integration",
"test:integration:slpdb": "mocha --timeout 25000 test/v3/integration/slp*.js", "test:integration:slpdb": "mocha --timeout 25000 test/v3/integration/slp*.js",
"coverage": "nyc report --reporter=text-lcov | coveralls", "coverage": "nyc report --reporter=text-lcov | coveralls",
@@ -37,7 +37,7 @@
"cors": "^2.8.3", "cors": "^2.8.3",
"debug": "~4.1.1", "debug": "~4.1.1",
"dotenv": "^8.0.0", "dotenv": "^8.0.0",
"electrum-cash": "^1.0.1", "electrum-cash": "^2.0.2",
"express": "^4.15.5", "express": "^4.15.5",
"express-basic-auth": "^1.1.3", "express-basic-auth": "^1.1.3",
"express-rate-limit": "^5.0.0", "express-rate-limit": "^5.0.0",
+9 -2
View File
@@ -116,13 +116,20 @@ class RateLimits {
let pointsToConsume = _this.calcPoints(decoded) let pointsToConsume = _this.calcPoints(decoded)
res.locals.pointsToConsume = pointsToConsume // Feedback for tests. res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
// Retrieve the origin.
let origin = req.get('origin')
if (origin === undefined && key.indexOf('10.0.0.5') > -1) {
origin = 'slp-api'
}
wlogger.info(`origin: ${origin}`)
// If the request originates from one of the approved wallet apps, then // If the request originates from one of the approved wallet apps, then
// apply paid-access rate limits. // apply paid-access rate limits.
const origin = req.get('origin')
if ( if (
origin && origin &&
(origin.toString().indexOf('wallet.fullstack.cash') > -1 || (origin.toString().indexOf('wallet.fullstack.cash') > -1 ||
origin.toString().indexOf('sandbox.fullstack.cash') > -1) origin.toString().indexOf('sandbox.fullstack.cash') > -1 ||
origin === 'slp-api')
) { ) {
pointsToConsume = 1 pointsToConsume = 1
res.locals.pointsToConsume = pointsToConsume // Feedback for tests. res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
+14 -14
View File
@@ -23,24 +23,24 @@
target="_blank" target="_blank"
>REST API</a >REST API</a
>, allowing modern applications to interact with the Bitcoin Cash (BCH) >, allowing modern applications to interact with the Bitcoin Cash (BCH)
blockchain. New to the BCH blockchain? Start by reading more about blockchain. New to the BCH blockchain? Check out these
<a <a
href="https://troutsblog.com/research/bitcoin-cash/how-to-bch-full-stack-developer" href="https://fullstack.cash/documentation"
target="_blank" target="_blank"
>building full stack applications on BCH</a >educational resources</a
>. >.
</p><br /> </p><br />
<p> <p>
A REST API can work with any programming language, but bch-api is intended A REST API can work with any programming language, but bch-api is intended
to be paired with the to be paired with the
<a <a
href="https://www.npmjs.com/package/@chris.troutner/bch-js" href="https://www.npmjs.com/package/@psf/bch-js"
target="_blank" target="_blank"
>npm bch-js</a >npm bch-js</a
> >
JavaScript library. JavaScript library.
<a <a
href="https://github.com/Permissionless-Software-Foundation/bch-js-examples" href="https://fullstack.cash/examples"
target="_blank" target="_blank"
>Here is example code</a >Here is example code</a
> >
@@ -53,7 +53,7 @@
</p><br /> </p><br />
<p> <p>
While you'll be able to run through the examples without one, you should While you'll be able to run through the examples without one, you should
<a href="https://account.bchjs.cash/" target="_blank" <a href="https://fullstack.cash/login" target="_blank"
>sign-up for a free API key</a >sign-up for a free API key</a
> >
in order to unlock higher rate limits. in order to unlock higher rate limits.
@@ -66,7 +66,7 @@
<h3>bch-api</h3> <h3>bch-api</h3>
<ul> <ul>
<li> <li>
<a href="https://account.bchjs.cash" target="_blank"> <a href="https://fullstack.cash/login" target="_blank">
Get an API key to increase rate limits Get an API key to increase rate limits
</a> </a>
</li> </li>
@@ -80,14 +80,14 @@
<br /> <br />
<li> <li>
<a href="https://tapi.bchjs.cash"> <a href="https://tapi.fullstack.cash">
testnet REST API testnet REST API
</a> </a>
</li> </li>
<br /> <br />
<li> <li>
<a href="https://github.com/christroutner/bch-api" target="_blank"> <a href="https://github.com/Permissionless-Software-Foundation/bch-api" target="_blank">
bch-api GitHub repository bch-api GitHub repository
</a> </a>
</li> </li>
@@ -114,14 +114,14 @@
</li> </li>
<br /> <br />
<li> <li>
<a href="https://bchjs.cash/" target="_blank"> <a href="https://fullstack.cash/cashstrap" target="_blank">
Run your own infrastructure Run your own infrastructure
</a> </a>
</li> </li>
<br /> <br />
<li> <li>
<a href="https://github.com/bitcoinbook/bitcoinbook#chapters" target="_blank"> <a href="https://fullstack.cash/documentation" target="_blank">
Mastering Bitcoin (great free reference) Mastering Bitcoin (great free reference)
</a> </a>
</li> </li>
@@ -135,7 +135,7 @@
<br /> <br />
<li> <li>
<a href="https://developer.bitcoin.com/faucets/bch/" target="_blank"> <a href="https://fullstack.cash/faucet/" target="_blank">
Testnet Faucet Testnet Faucet
</a> </a>
</li> </li>
@@ -148,7 +148,7 @@
<h3>bch-js</h3> <h3>bch-js</h3>
<ul> <ul>
<li> <li>
<a href="https://www.npmjs.com/package/@chris.troutner/bch-js" <a href="https://www.npmjs.com/package/@psf/bch-js"
target="_blank"> target="_blank">
npm library npm library
</a> </a>
@@ -163,7 +163,7 @@
<br /> <br />
<li> <li>
<a href="https://github.com/christroutner/bch-js" target="_blank"> <a href="https://github.com/Permissionless-Software-Foundation/bch-js" target="_blank">
GitHub code repository GitHub code repository
</a> </a>
</li> </li>
+340 -2
View File
@@ -10,7 +10,7 @@ const axios = require('axios')
const util = require('util') const util = require('util')
const bitcore = require('bitcore-lib-cash') const bitcore = require('bitcore-lib-cash')
const ElectrumCash = require('electrum-cash').Client const ElectrumCash = require('electrum-cash').ElectrumClient
// const ElectrumCash = require('/home/trout/work/personal/electrum-cash/electrum.js').Client // eslint-disable-line // const ElectrumCash = require('/home/trout/work/personal/electrum-cash/electrum.js').Client // eslint-disable-line
const wlogger = require('../../util/winston-logging') const wlogger = require('../../util/winston-logging')
@@ -50,6 +50,10 @@ class Electrum {
_this.router.get('/', _this.root) _this.router.get('/', _this.root)
_this.router.get('/utxos/:address', _this.getUtxos) _this.router.get('/utxos/:address', _this.getUtxos)
_this.router.post('/utxos', _this.utxosBulk) _this.router.post('/utxos', _this.utxosBulk)
_this.router.get('/tx/data/:txid', _this.getTransactionDetails)
_this.router.post('/tx/data', _this.transactionDetailsBulk)
_this.router.get('/block/headers/:height', _this.getBlockHeaders)
_this.router.post('/block/headers', _this.blockHeadersBulk)
_this.router.get('/balance/:address', _this.getBalance) _this.router.get('/balance/:address', _this.getBalance)
_this.router.post('/balance', _this.balanceBulk) _this.router.post('/balance', _this.balanceBulk)
_this.router.get('/transactions/:address', _this.getTransactions) _this.router.get('/transactions/:address', _this.getTransactions)
@@ -324,6 +328,338 @@ class Electrum {
} }
} }
// Returns a promise that resolves to transaction details data for a txid.
// Expects input to be a txid string, and input validation to have already
// been done by parent, calling function.
async _transactionDetailsFromElectrum (txid, verbose = true) {
try {
if (!_this.isReady) {
throw new Error(
'ElectrumX server connection is not ready. Call await connectToServer() first.'
)
}
// Query the utxos from the ElectrumX server.
const electrumResponse = await _this.electrumx.request(
'blockchain.transaction.get',
txid,
verbose
)
// console.log(
// `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}`
// )
return electrumResponse
} catch (err) {
// console.log('err: ', err)
// Write out error to error log.
wlogger.error(
'Error in elecrumx.js/_transactionDetailsFromElectrum(): ',
err
)
throw err
}
}
/**
* @api {get} /electrumx/tx/data/{txid} Get transaction details for a TXID
* @apiName transaction details for a TXID
* @apiGroup ElectrumX / Fulcrum
* @apiDescription Returns an object with transaction details of the TXID
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/electrumx/tx/data/a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d" -H "accept: application/json"
*
*/
// GET handler for single transaction
async getTransactionDetails (req, res, next) {
try {
const txid = req.params.txid
const verbose = req.query.verbose
// Reject if txid is anything other than a string
if (typeof txid !== 'string') {
res.status(400)
return res.json({
success: false,
error: 'txid must be a string'
})
}
wlogger.debug(
'Executing electrumx/getTransactionDetails with this txid: ',
txid
)
// Get data from ElectrumX server.
const electrumResponse = await _this._transactionDetailsFromElectrum(
txid,
verbose
)
// console.log(`_transactionDetailsFromElectrum(): ${JSON.stringify(electrumResponse, null, 2)}`)
// Pass the error message if ElectrumX reports an error.
if (electrumResponse instanceof Error) {
res.status(400)
return res.json({
success: false,
error: electrumResponse.message
})
}
res.status(200)
return res.json({
success: true,
details: electrumResponse
})
} catch (err) {
// Write out error to error log.
wlogger.error('Error in elecrumx.js/getTransactionDetails().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {post} /electrumx/tx/data Get transaction details for an array of TXIDs
* @apiName Transaction details for an array of TXIDs
* @apiGroup ElectrumX / Fulcrum
* @apiDescription Returns an array of objects with transaction details of an array of TXIDs.
* Limited to 20 items per request.
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/electrumx/tx/data" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d","a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"], "verbose":false}'
*
*
*/
// POST handler for bulk queries on transaction details
async transactionDetailsBulk (req, res, next) {
try {
const txids = req.body.txids
const verbose = req.body.verbose || true
// Reject if txids is not an array.
if (!Array.isArray(txids)) {
res.status(400)
return res.json({
success: false,
error: 'txids needs to be an array. Use GET for single txid.'
})
}
// Enforce array size rate limits
if (!_this.routeUtils.validateArraySize(req, txids)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
success: false,
error: 'Array too large.'
})
}
wlogger.debug(
'Executing electrumx.js/transactionDetailsBulk with these txids: ',
txids
)
// Loops through each address and creates an array of Promises, querying
// the Electrum server in parallel.
const transactions = txids.map(async (txid, index) => {
// console.log(`address: ${address}`)
const details = await _this._transactionDetailsFromElectrum(
txid,
verbose
)
return { details, txid }
})
// Wait for all parallel Electrum requests to return.
const result = await Promise.all(transactions)
// Return the array of retrieved transaction details.
res.status(200)
return res.json({
success: true,
transactions: result
})
} catch (err) {
wlogger.error('Error in electrumx.js/transactionDetailsBulk().', err)
return _this.errorHandler(err, res)
}
}
// Returns a promise that resolves to block header data for a block height.
// Expects input to be a height number, and input validation to have already
// been done by parent, calling function.
async _blockHeadersFromElectrum (height, count = 1) {
try {
if (!_this.isReady) {
throw new Error(
'ElectrumX server connection is not ready. Call await connectToServer() first.'
)
}
// Query the block header from the ElectrumX server.
const electrumResponse = await _this.electrumx.request('blockchain.block.headers', height, count)
// console.log(
// `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}`
// )
const HEADER_SIZE = 80 * 2
if (!(electrumResponse instanceof Error)) {
const headers = electrumResponse.hex.match(new RegExp(`.{1,${HEADER_SIZE}}`, 'g'))
return headers
}
return electrumResponse
} catch (err) {
// console.log('err: ', err)
// Write out error to error log.
wlogger.error(
'Error in elecrumx.js/_blockHeaderFromElectrum(): ',
err
)
throw err
}
}
/**
* @api {get} /electrumx/block/headers/{height} Get `count` block headers starting at a height
* @apiName Block header data for a `count` blocks starting at a block height
* @apiGroup ElectrumX / Fulcrum
* @apiDescription Returns an array with block headers starting at the block height
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/electrumx/block/header/42?count=2" -H "accept: application/json"
*
*/
// GET handler for single block headers
async getBlockHeaders (req, res, next) {
try {
const height = Number(req.params.height)
const count = req.query.count === undefined ? 1 : Number(req.query.count)
// Reject if height is not a number
if (Number.isNaN(height) || height < 0) {
res.status(400)
return res.json({
success: false,
error: 'height must be a positive number'
})
}
// Reject if height is not a number
if (Number.isNaN(count) || count < 0) {
res.status(400)
return res.json({
success: false,
error: 'count must be a positive number'
})
}
wlogger.debug(
'Executing electrumx/getBlockHeaders with this height: ',
height
)
// Get data from ElectrumX server.
const electrumResponse = await _this._blockHeadersFromElectrum(height, count)
// console.log(`_transactionDetailsFromElectrum(): ${JSON.stringify(electrumResponse, null, 2)}`)
// Pass the error message if ElectrumX reports an error.
if (electrumResponse instanceof Error) {
res.status(400)
return res.json({
success: false,
error: electrumResponse.message
})
}
res.status(200)
return res.json({
success: true,
headers: electrumResponse
})
} catch (err) {
// Write out error to error log.
wlogger.error('Error in elecrumx.js/getBlockHeader().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {post} /electrumx/block/headers Get block headers for an array of height + count pairs
* @apiName Block headers for an array of height + count pairs
* @apiGroup ElectrumX / Fulcrum
* @apiDescription Returns an array of objects with blockheaders of an array of TXIDs.
* Limited to 20 items per request.
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/electrumx/block/headers" -H "accept: application/json" -H "Content-Type: application/json" -d '{"heights":[{ "height": 42, count: 2 }, { "height": 100, count: 5 }]}'
*
*/
// POST handler for bulk queries on block headers
async blockHeadersBulk (req, res, next) {
try {
const heights = req.body.heights
// Reject if heights is not an array.
if (!Array.isArray(heights)) {
res.status(400)
return res.json({
success: false,
error: 'heights needs to be an array. Use GET for single height.'
})
}
// Enforce array size rate limits
if (!_this.routeUtils.validateArraySize(req, heights)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
success: false,
error: 'Array too large.'
})
}
wlogger.debug(
'Executing electrumx.js/blockHeadersBulk with these txids: ',
heights
)
// Loops through each address and creates an array of Promises, querying
// the Electrum server in parallel.
const transactions = heights.map(async (obj) => {
const headers = await _this._blockHeadersFromElectrum(
obj.height,
obj.count
)
return { headers }
})
// Wait for all parallel Electrum requests to return.
const result = await Promise.all(transactions)
// Return the array of retrieved transaction details.
res.status(200)
return res.json({
success: true,
headers: result
})
} catch (err) {
wlogger.error('Error in electrumx.js/blockHeadersBulk().', err)
return _this.errorHandler(err, res)
}
}
// Returns a promise that resolves to a balance for an address. Expects input // Returns a promise that resolves to a balance for an address. Expects input
// to be a cash address, and input validation to have already been done by // to be a cash address, and input validation to have already been done by
// parent, calling function. // parent, calling function.
@@ -911,7 +1247,9 @@ class Electrum {
const address = _this.bitcore.Address.fromString(addrStr) const address = _this.bitcore.Address.fromString(addrStr)
// console.log(`address: ${address}`) // console.log(`address: ${address}`)
const script = _this.bitcore.Script.buildPublicKeyHashOut(address) const script = address.isPayToPublicKeyHash()
? _this.bitcore.Script.buildPublicKeyHashOut(address)
: _this.bitcore.Script.buildScriptHashOut(address)
// console.log(`script: ${script}`) // console.log(`script: ${script}`)
const scripthash = _this.bitcore.crypto.Hash.sha256(script.toBuffer()) const scripthash = _this.bitcore.crypto.Hash.sha256(script.toBuffer())
-168
View File
@@ -64,10 +64,6 @@ class Slp {
_this.router.get('/balancesForAddress/:address', _this.balancesForAddress) _this.router.get('/balancesForAddress/:address', _this.balancesForAddress)
_this.router.post('/balancesForAddress', _this.balancesForAddressBulk) _this.router.post('/balancesForAddress', _this.balancesForAddressBulk)
_this.router.get('/balancesForToken/:tokenId', _this.balancesForTokenSingle) _this.router.get('/balancesForToken/:tokenId', _this.balancesForTokenSingle)
_this.router.get(
'/balance/:address/:tokenId',
_this.balancesForAddressByTokenID
)
_this.router.get('/convert/:address', _this.convertAddressSingle) _this.router.get('/convert/:address', _this.convertAddressSingle)
_this.router.post('/convert', _this.convertAddressBulk) _this.router.post('/convert', _this.convertAddressBulk)
_this.router.post('/validateTxid', _this.validateBulk) _this.router.post('/validateTxid', _this.validateBulk)
@@ -830,170 +826,6 @@ class Slp {
} }
} }
/**
* @api {get} /slp/balance/{address}/{TokenId} List single slp token balance for address.
* @apiName List single slp token balance for address.
* @apiGroup SLP
* @apiDescription Returns List single slp token balance for address.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/balance/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m/1cda254d0a995c713b7955298ed246822bee487458cd9747a91d9e81d9d28125" -H "accept:application/json"
*
*
*/
// Retrieve token balances for a single token class, for a single address.
async balancesForAddressByTokenID (req, res, next) {
try {
// Validate input data.
const address = req.params.address
if (!address || address === '') {
res.status(400)
return res.json({ error: 'address can not be empty' })
}
const tokenId = req.params.tokenId
if (!tokenId || tokenId === '') {
res.status(400)
return res.json({ error: 'tokenId can not be empty' })
}
// Ensure the input is a valid BCH address.
try {
_this.bchjs.SLP.Address.toCashAddress(address)
} catch (err) {
res.status(400)
return res.json({
error: `Invalid BCH address. Double check your address is valid: ${address}`
})
}
// Prevent a common user error. Ensure they are using the correct network address.
const cashAddr = _this.bchjs.SLP.Address.toCashAddress(address)
const networkIsValid = _this.routeUtils.validateNetwork(cashAddr)
if (!networkIsValid) {
res.status(400)
return res.json({
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
// Convert input to an simpleledger: address.
const slpAddr = _this.bchjs.SLP.Address.toSlpAddress(req.params.address)
const query = {
v: 3,
q: {
db: ['g'],
aggregate: [
{
$match: {
'graphTxn.outputs': {
$elemMatch: {
address: slpAddr,
status: 'UNSPENT',
slpAmount: { $gte: 0 }
}
}
}
},
{
$unwind: '$graphTxn.outputs'
},
{
$match: {
'graphTxn.outputs.address': slpAddr,
'graphTxn.outputs.status': 'UNSPENT',
'graphTxn.outputs.slpAmount': { $gte: 0 }
}
},
{
$project: {
amount: '$graphTxn.outputs.slpAmount',
address: '$graphTxn.outputs.address',
txid: '$graphTxn.txid',
vout: '$graphTxn.outputs.vout',
tokenId: '$tokenDetails.tokenIdHex'
}
},
{
$group: {
_id: '$tokenId',
balanceString: {
$sum: '$amount'
},
slpAddress: {
$first: '$address'
}
}
}
],
limit: 10000
}
}
const options = _this.generateCredentials()
const s = JSON.stringify(query)
const b64 = Buffer.from(s).toString('base64')
const url = `${process.env.SLPDB_URL}q/${b64}`
// Get data from SLPDB.
const opt = {
method: 'get',
baseURL: url,
headers: options.headers,
timeout: options.timeout
}
const tokenRes = await _this.axios.request(opt)
console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data, null, 2)}`)
let resVal = {
cashAddress: _this.bchjs.SLP.Address.toCashAddress(slpAddr),
legacyAddress: _this.bchjs.SLP.Address.toLegacyAddress(slpAddr),
slpAddress: slpAddr,
tokenId: tokenId,
balance: 0,
balanceString: '0'
}
if (tokenRes.data.g.length > 0) {
tokenRes.data.g.forEach(async (token) => {
if (token._id === tokenId) {
resVal = {
cashAddress: _this.bchjs.SLP.Address.toCashAddress(slpAddr),
legacyAddress: _this.bchjs.SLP.Address.toLegacyAddress(slpAddr),
slpAddress: slpAddr,
tokenId: token._id,
balance: parseFloat(token.balanceString),
balanceString: token.balanceString
}
}
})
} else {
resVal = {
cashAddress: _this.bchjs.SLP.Address.toCashAddress(slpAddr),
legacyAddress: _this.bchjs.SLP.Address.toLegacyAddress(slpAddr),
slpAddress: slpAddr,
tokenId: tokenId,
balance: 0,
balanceString: '0'
}
}
res.status(200)
return res.json(resVal)
} catch (err) {
wlogger.error('Error in slp.ts/balancesForAddressByTokenID().', err)
return _this.errorHandler(err, res)
// return res.json({
// error: `Error in /balance/:address/:tokenId: ${err.message}`
// })
}
}
/** /**
* @api {get} /slp/convert/{address} Convert address to slpAddr, cashAddr and legacy. * @api {get} /slp/convert/{address} Convert address to slpAddr, cashAddr and legacy.
* @apiName Convert address to slpAddr, cashAddr and legacy. * @apiName Convert address to slpAddr, cashAddr and legacy.
+496 -2
View File
@@ -2,7 +2,7 @@
TESTS FOR THE ELECTRUMX.JS LIBRARY TESTS FOR THE ELECTRUMX.JS LIBRARY
Named with a01 prefix so that these tests are run first. Something about running Named with a01 prefix so that these tests are run first. Something about running
the Blcokbook and Blockchain tests screws up these tests. Spend a couple hours the Blcokbook and Blockchain tests screws up these tests. Spent a couple hours
debugging and couldn't isolate the source of the issue, but renaming the file debugging and couldn't isolate the source of the issue, but renaming the file
was an easy fix. was an easy fix.
@@ -30,6 +30,18 @@ const mockData = require('./mocks/electrumx-mock')
const util = require('util') const util = require('util')
util.inspect.defaultOptions = { depth: 1 } util.inspect.defaultOptions = { depth: 1 }
// A wrapper for asserting that the correct response is returned when an error
// is expected.
function expectRouteError (res, result, expectedError, code = 400) {
assert.equal(res.statusCode, code, `HTTP status code ${code} expected.`)
assert.property(result, 'error')
assert.include(result.error, expectedError)
assert.property(result, 'success')
assert.equal(result.success, false)
}
describe('#ElectrumX Router', () => { describe('#ElectrumX Router', () => {
let req, res let req, res
let sandbox let sandbox
@@ -82,6 +94,17 @@ describe('#ElectrumX Router', () => {
// //
}) })
// A wrapper for stubbing with the Sinon sandbox.
function stubMethodForUnitTests (obj, method, value) {
if (process.env.TEST !== 'unit') return false
electrumxRoute.isReady = true // Force flag.
sandbox.stub(obj, method).resolves(value)
return true
}
describe('#root', () => { describe('#root', () => {
// root route handler. // root route handler.
const root = electrumxRoute.root const root = electrumxRoute.root
@@ -94,7 +117,7 @@ describe('#ElectrumX Router', () => {
}) })
describe('#addressToScripthash', () => { describe('#addressToScripthash', () => {
it('should accurately return a scripthash', () => { it('should accurately return a scripthash for a P2PKH address', () => {
const addr = 'bitcoincash:qpr270a5sxphltdmggtj07v4nskn9gmg9yx4m5h7s4' const addr = 'bitcoincash:qpr270a5sxphltdmggtj07v4nskn9gmg9yx4m5h7s4'
const scripthash = electrumxRoute.addressToScripthash(addr) const scripthash = electrumxRoute.addressToScripthash(addr)
@@ -104,6 +127,17 @@ describe('#ElectrumX Router', () => {
assert.equal(scripthash, expectedOutput) assert.equal(scripthash, expectedOutput)
}) })
it('should accurately return a scripthash for a P2SH address', () => {
const addr = 'bitcoincash:pz0z7u9p96h2p6hfychxdrmwgdlzpk5luc5yks2wxq'
const scripthash = electrumxRoute.addressToScripthash(addr)
const expectedOutput =
'8bc2235c8e7d5634d9ec429fc0171f2c58e728d4f1e2fb7e440e313133cfa4f0'
assert.equal(scripthash, expectedOutput)
})
}) })
describe('#_utxosFromElectrumx', () => { describe('#_utxosFromElectrumx', () => {
@@ -412,6 +446,466 @@ describe('#ElectrumX Router', () => {
}) })
}) })
describe('#_transactionDetailsFromElectrum', () => {
it('should return error object for invalid txid', async () => {
const txid =
'4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb25'
stubMethodForUnitTests(
electrumxRoute.electrumx,
'request',
new Error('Invalid tx hash')
)
const result = await electrumxRoute._transactionDetailsFromElectrum(txid)
assert.instanceOf(result, Error)
assert.include(result.message, 'Invalid tx hash')
})
it('should get details for a single txid', async () => {
const txid =
'4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251'
stubMethodForUnitTests(
electrumxRoute.electrumx,
'request',
mockData.txDetails
)
const result = await electrumxRoute._transactionDetailsFromElectrum(txid)
assert.isObject(result)
assert.property(result, 'blockhash')
assert.property(result, 'hash')
assert.property(result, 'hex')
assert.property(result, 'vin')
assert.property(result, 'vout')
assert.equal(result.hash, txid)
})
})
describe('#getTransactionDetails', () => {
it('should throw 400 if txid is not a string', async () => {
req.params.txid = 5
const result = await electrumxRoute.getTransactionDetails(req, res)
expectRouteError(res, result, 'txid must be a string')
})
it('should throw 400 on array input', async () => {
req.params.txid = [
'4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251'
]
const result = await electrumxRoute.getTransactionDetails(req, res)
expectRouteError(res, result, 'txid must be a string')
})
it('should return error object for invalid txid', async () => {
req.params.txid =
'4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb25'
stubMethodForUnitTests(
electrumxRoute.electrumx,
'request',
new Error('Invalid tx hash')
)
// Call the details API.
const result = await electrumxRoute.getTransactionDetails(req, res)
expectRouteError(res, result, 'Invalid tx hash')
})
it('should get details for a single txid', async () => {
req.params.txid =
'4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251'
stubMethodForUnitTests(
electrumxRoute.electrumx,
'request',
mockData.txDetails
)
// Call the details API.
const result = await electrumxRoute.getTransactionDetails(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'details')
assert.isObject(result.details)
assert.property(result.details, 'blockhash')
assert.property(result.details, 'hash')
assert.property(result.details, 'hex')
assert.property(result.details, 'vin')
assert.property(result.details, 'vout')
assert.equal(result.details.hash, req.params.txid)
})
})
describe('#transactionDetailsBulk', () => {
it('should throw an error for an empty body', async () => {
req.body = {}
const result = await electrumxRoute.transactionDetailsBulk(req, res)
expectRouteError(res, result, 'txids needs to be an array')
})
it('should error on non-array single txid', async () => {
req.body = {
txid: '4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251'
}
const result = await electrumxRoute.transactionDetailsBulk(req, res)
expectRouteError(res, result, 'txids needs to be an array')
})
it('should NOT throw 400 error for an invalid txid', async () => {
req.body = {
txids: [
'4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb25'
]
}
stubMethodForUnitTests(
electrumxRoute.electrumx,
'request',
mockData.txDetails
)
const result = await electrumxRoute.transactionDetailsBulk(req, res)
// This should probably throw a 400 error, but to be consistent with the other
// bulk endpoints it doesn't throw. This will change in the future
// expectRouteError(res, result, 'Invalid tx hash')
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'transactions')
assert.isArray(result.transactions)
})
it('should throw 429 error if txid array is too large', async () => {
const testArray = []
for (var i = 0; i < 25; i++) testArray.push('')
req.body.txids = testArray
const result = await electrumxRoute.transactionDetailsBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
expectRouteError(res, result, 'Array too large', 429)
})
it('should get details for a single txid', async () => {
req.body = {
txids: [
'4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251'
]
}
stubMethodForUnitTests(
electrumxRoute.electrumx,
'request',
mockData.txDetails
)
// Call the details API.
const result = await electrumxRoute.transactionDetailsBulk(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'transactions')
assert.isArray(result.transactions)
assert.property(result.transactions[0], 'txid')
assert.property(result.transactions[0], 'details')
assert.property(result.transactions[0].details, 'blockhash')
assert.property(result.transactions[0].details, 'hash')
assert.property(result.transactions[0].details, 'hex')
assert.property(result.transactions[0].details, 'vin')
assert.property(result.transactions[0].details, 'vout')
})
it('should get details for multiple txids', async () => {
req.body = {
txids: [
'4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251',
'4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251'
]
}
stubMethodForUnitTests(
electrumxRoute.electrumx,
'request',
mockData.txDetails
)
// Call the details API.
const result = await electrumxRoute.transactionDetailsBulk(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)'
assert.property(result, 'success')
assert.equal(result.success, true)
assert.isArray(result.transactions)
assert.isObject(result.transactions[0].details)
assert.equal(result.transactions.length, 2, '2 outputs for 2 inputs')
})
})
describe('#_blockHeadersFromElectrum', () => {
it('should return error object for invalid block height', async () => {
const height = -10
stubMethodForUnitTests(
electrumxRoute.electrumx,
'request',
new Error('Invalid height')
)
const result = await electrumxRoute._blockHeadersFromElectrum(height, 2)
assert.instanceOf(result, Error)
assert.include(result.message, 'Invalid height')
})
it('should return error object for invalid count', async () => {
const height = 42
stubMethodForUnitTests(
electrumxRoute.electrumx,
'request',
new Error('Invalid count')
)
const result = await electrumxRoute._blockHeadersFromElectrum(height, -1)
assert.instanceOf(result, Error)
assert.include(result.message, 'Invalid count')
})
it('should get block header for a single block height', async () => {
const height = 42
const mockedResponse = { count: 2, hex: mockData.blockHeaders.join(''), max: 2016 }
stubMethodForUnitTests(
electrumxRoute.electrumx,
'request',
mockedResponse
)
const result = await electrumxRoute._blockHeadersFromElectrum(height, 2)
assert.isArray(result)
assert.deepEqual(result, mockData.blockHeaders)
})
})
describe('#getBlockheaders', () => {
it('should throw 400 if height is not a number', async () => {
req.params.height = 'Hello'
const result = await electrumxRoute.getBlockHeaders(req, res)
expectRouteError(res, result, 'height must be a positive number')
})
it('should throw 400 if height is negative', async () => {
req.params.height = -42
const result = await electrumxRoute.getBlockHeaders(req, res)
expectRouteError(res, result, 'height must be a positive number')
})
it('should throw 400 if count is not a number', async () => {
req.params.height = 42
req.query.count = 'Hello'
const result = await electrumxRoute.getBlockHeaders(req, res)
expectRouteError(res, result, 'count must be a positive number')
})
it('should throw 400 if count is negative', async () => {
req.params.height = 42
req.query.count = -10
const result = await electrumxRoute.getBlockHeaders(req, res)
expectRouteError(res, result, 'count must be a positive number')
})
it('should throw 400 on array input', async () => {
req.params.height = [42, 42]
const result = await electrumxRoute.getBlockHeaders(req, res)
expectRouteError(res, result, 'height must be a positive number')
})
it('should return error object for invalid height', async () => {
req.params.height = 1000000000
stubMethodForUnitTests(
electrumxRoute.electrumx,
'request',
new Error('Invalid height')
)
// Call the details API.
const result = await electrumxRoute.getBlockHeaders(req, res)
expectRouteError(res, result, 'Invalid height')
})
it('should get headers for a single block height with count 2', async () => {
req.params.height = 42
req.query.count = 2
stubMethodForUnitTests(
electrumxRoute,
'_blockHeadersFromElectrum',
mockData.blockHeaders
)
// Call the details API.
const result = await electrumxRoute.getBlockHeaders(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'headers')
assert.isArray(result.headers)
assert.deepEqual(result.headers, mockData.blockHeaders)
})
})
describe('#blockHeadersBulk', () => {
it('should throw an error for an empty body', async () => {
req.body = {}
const result = await electrumxRoute.blockHeadersBulk(req, res)
expectRouteError(res, result, 'heights needs to be an array')
})
it('should error on non-array single height', async () => {
req.body = {
heights: 42
}
const result = await electrumxRoute.blockHeadersBulk(req, res)
expectRouteError(res, result, 'heights needs to be an array')
})
it('should NOT throw 400 error for an invalid height', async () => {
req.body = {
heights: [
{ height: -10, count: 2 }
]
}
stubMethodForUnitTests(
electrumxRoute,
'_blockHeadersFromElectrum',
mockData.blockHeaders
)
const result = await electrumxRoute.blockHeadersBulk(req, res)
// This should probably throw a 400 error, but to be consistent with the other
// bulk endpoints it doesn't throw. This will change in the future
// expectRouteError(res, result, 'Invalid tx hash')
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'headers')
assert.isArray(result.headers)
})
it('should throw 429 error if heights array is too large', async () => {
const testArray = []
for (var i = 0; i < 25; i++) testArray.push('')
req.body.heights = testArray
const result = await electrumxRoute.blockHeadersBulk(req, res)
expectRouteError(res, result, 'Array too large', 429)
})
it('should get details for a single height', async () => {
req.body = {
heights: [
{ height: 42, count: 2 }
]
}
stubMethodForUnitTests(
electrumxRoute,
'_blockHeadersFromElectrum',
mockData.blockHeaders
)
// Call the details API.
const result = await electrumxRoute.blockHeadersBulk(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'headers')
assert.isArray(result.headers)
assert.property(result.headers[0], 'headers')
assert.isArray(result.headers[0].headers)
})
it('should get details for multiple txids', async () => {
req.body = {
heights: [
{ height: 42, count: 2 },
{ height: 42, count: 2 }
]
}
stubMethodForUnitTests(
electrumxRoute,
'_blockHeadersFromElectrum',
mockData.blockHeaders
)
// Call the details API.
const result = await electrumxRoute.blockHeadersBulk(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)'
assert.property(result, 'success')
assert.equal(result.success, true)
assert.isArray(result.headers)
assert.isArray(result.headers[0].headers)
assert.equal(result.headers.length, 2, '2 outputs for 2 inputs')
})
})
describe('#_balanceFromElectrumx', () => { describe('#_balanceFromElectrumx', () => {
it('should throw error for invalid address', async () => { it('should throw error for invalid address', async () => {
try { try {
+76 -1
View File
@@ -39,9 +39,84 @@ const mempool = [
} }
] ]
const txDetails = {
blockhash: '0000000000000000002aaf94953da3b487317508ebd1003a1d75d6d6ec2e75cc',
blocktime: 1578327094,
confirmations: 31861,
hash: '4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251',
hex: '020000000265d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667010000006441dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309ffffffff65d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667000000006441347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954ffffffff035ac355000000000017a914189ce02e332548f4804bac65cba68202c9dbf822878dfd0800000000001976a914285bb350881b21ac89724c6fb6dc914d096cd53b88acf9ef3100000000001976a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac00000000',
locktime: 0,
size: 392,
time: 1578327094,
txid: '4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251',
version: 2,
vin: [
{
scriptSig: {
asm: 'dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e[ALL|FORKID] 020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309',
hex: '41dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309'
},
sequence: 4294967295,
txid: '6796672c8f0342770e3d4ac2a3b0e4494daeb7af7997f3518a0c8402f43ed165',
vout: 1
},
{
scriptSig: {
asm: '347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f77[ALL|FORKID] 028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954',
hex: '41347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954'
},
sequence: 4294967295,
txid: '6796672c8f0342770e3d4ac2a3b0e4494daeb7af7997f3518a0c8402f43ed165',
vout: 0
}
],
vout: [
{
n: 0,
scriptPubKey: {
addresses: ['bitcoincash: pqvfecpwxvj53ayqfwkxtjaxsgpvnklcyg8xewk9hl'],
asm: 'OP_HASH160 189ce02e332548f4804bac65cba68202c9dbf822 OP_EQUAL',
hex: 'a914189ce02e332548f4804bac65cba68202c9dbf82287',
reqSigs: 1,
type: 'scripthash'
},
value: 0.0562057
},
{
n: 1,
scriptPubKey: {
addresses: ['bitcoincash: qq59hv6s3qdjrtyfwfxxldkuj9xsjmx48vrz882knz'],
asm: 'OP_DUP OP_HASH160 285bb350881b21ac89724c6fb6dc914d096cd53b OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a914285bb350881b21ac89724c6fb6dc914d096cd53b88ac',
reqSigs: 1,
type: 'pubkeyhash'
},
value: 0.00589197
},
{
n: 2,
scriptPubKey: {
addresses: ['bitcoincash: qpzlruwy4xu5rxjs3z37nsj29y7h59gwvsu4ddp0u4'],
asm: 'OP_DUP OP_HASH160 45f1f1c4a9b9419a5088a3e9c24a293d7a150e64 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac',
reqSigs: 1,
type: 'pubkeyhash'
},
value: 0.03272697
}
]
}
const blockHeaders = [
'010000008b52bbd72c2f49569059f559c1b1794de5192e4f7d6d2b03c7482bad0000000083e4f8a9d502ed0c419075c1abb5d56f878a2e9079e5612bfb76a2dc37d9c42741dd6849ffff001d2b909dd6',
'01000000f528fac1bcb685d0cd6c792320af0300a5ce15d687c7149548904e31000000004e8985a786d864f21e9cbb7cbdf4bc9265fe681b7a0893ac55a8e919ce035c2f85de6849ffff001d385ccb7c'
]
module.exports = { module.exports = {
utxos, utxos,
balance, balance,
txHistory, txHistory,
mempool mempool,
txDetails,
blockHeaders
} }
+31 -58
View File
@@ -135,11 +135,12 @@ describe('#Util', () => {
// Mock the RPC call for unit tests. // Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') { if (process.env.TEST === 'unit') {
nock(`${process.env.RPC_BASEURL}`) nock(`${process.env.RPC_BASEURL}`)
.post(uri => uri.includes('/')) .post((uri) => uri.includes('/'))
.reply(200, { result: mockData.mockAddress }) .reply(200, { result: mockData.mockAddress })
} }
req.params.address = 'bitcoincash:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5hzljcrnd' req.params.address =
'bitcoincash:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5hzljcrnd'
const result = await validateAddress(req, res) const result = await validateAddress(req, res)
// console.log(`result: ${util.inspect(result)}`) // console.log(`result: ${util.inspect(result)}`)
@@ -255,7 +256,7 @@ describe('#Util', () => {
// Mock the RPC call for unit tests. // Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') { if (process.env.TEST === 'unit') {
nock(`${process.env.RPC_BASEURL}`) nock(`${process.env.RPC_BASEURL}`)
.post(uri => uri.includes('/')) .post((uri) => uri.includes('/'))
.reply(200, { result: mockData.mockAddress }) .reply(200, { result: mockData.mockAddress })
} }
@@ -281,7 +282,7 @@ describe('#Util', () => {
// Mock the RPC call for unit tests. // Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') { if (process.env.TEST === 'unit') {
nock(`${process.env.RPC_BASEURL}`) nock(`${process.env.RPC_BASEURL}`)
.post(uri => uri.includes('/')) .post((uri) => uri.includes('/'))
.times(2) .times(2)
.reply(200, { result: mockData.mockAddress }) .reply(200, { result: mockData.mockAddress })
} }
@@ -356,17 +357,11 @@ describe('#Util', () => {
// Mock the RPC call for unit tests. // Mock the RPC call for unit tests.
sandbox sandbox
.stub( .stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
utilRouteInst.blockbook,
'balanceFromBlockbook'
)
.resolves(mockData.mockBalance) .resolves(mockData.mockBalance)
sandbox sandbox
.stub( .stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
utilRouteInst.blockbook,
'utxosFromBlockbook'
)
.resolves(mockData.mockUtxos) .resolves(mockData.mockUtxos)
sandbox sandbox
@@ -389,51 +384,41 @@ describe('#Util', () => {
assert.equal(result, 'test-txid') assert.equal(result, 'test-txid')
}) })
}
it('should return balance if balance-only is true', async () => { it('should return balance if balance-only is true', async () => {
// Mock the RPC call for unit tests. // Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox sandbox
.stub( .stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
utilRouteInst.blockbook,
'balanceFromBlockbook'
)
.resolves(mockData.mockBalance) .resolves(mockData.mockBalance)
}
// Mock sendRawTransaction() so that the hex does not actually get broadcast // Mock sendRawTransaction() so that the hex does not actually get broadcast
// to the network. // to the network.
sandbox sandbox
.stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction') .stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction')
.resolves('test-txid') .resolves('test-txid')
req.body = { req.body = {
wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt', wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt',
balanceOnly: true balanceOnly: true
} }
const result = await utilRouteInst.sweepWif(req, res) const result = await utilRouteInst.sweepWif(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`) // console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isNumber(result) assert.isNumber(result)
}) })
}
// Unit tests only // Unit tests only
if (process.env.TEST === 'unit') { if (process.env.TEST === 'unit') {
it('should generate transaction for valid BCH-only sweep', async () => { it('should generate transaction for valid BCH-only sweep', async () => {
sandbox sandbox
.stub( .stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
utilRouteInst.blockbook,
'balanceFromBlockbook'
)
.resolves(mockData.mockBalance) .resolves(mockData.mockBalance)
sandbox sandbox
.stub( .stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
utilRouteInst.blockbook,
'utxosFromBlockbook'
)
.resolves(mockData.mockUtxos) .resolves(mockData.mockUtxos)
// Force token utxo to appear as regular BCH utxo. // Force token utxo to appear as regular BCH utxo.
@@ -460,17 +445,11 @@ describe('#Util', () => {
it('should throw 422 error if no non-token UTXOs', async () => { it('should throw 422 error if no non-token UTXOs', async () => {
sandbox sandbox
.stub( .stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
utilRouteInst.blockbook,
'balanceFromBlockbook'
)
.resolves(mockData.mockBalance) .resolves(mockData.mockBalance)
sandbox sandbox
.stub( .stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
utilRouteInst.blockbook,
'utxosFromBlockbook'
)
.resolves(mockData.mockUtxos) .resolves(mockData.mockUtxos)
// Force token utxo to appear as regular BCH utxo. // Force token utxo to appear as regular BCH utxo.
@@ -502,17 +481,11 @@ describe('#Util', () => {
it('should detect and throw error for multiple token classes', async () => { it('should detect and throw error for multiple token classes', async () => {
sandbox sandbox
.stub( .stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
utilRouteInst.blockbook,
'balanceFromBlockbook'
)
.resolves(mockData.mockBalance) .resolves(mockData.mockBalance)
sandbox sandbox
.stub( .stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
utilRouteInst.blockbook,
'utxosFromBlockbook'
)
.resolves(mockData.mockThreeUtxos) .resolves(mockData.mockThreeUtxos)
// Force token utxo to appear as regular BCH utxo. // Force token utxo to appear as regular BCH utxo.