mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-22 09:12:05 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d40a7cb01 | ||
|
|
0a14ca08a4 | ||
|
|
198141b75a | ||
|
|
6f25ec4b68 | ||
|
|
cb416d26c0 | ||
|
|
376094c6c2 | ||
|
|
f566d44252 | ||
|
|
0e7b45214f | ||
|
|
58366b5442 | ||
|
|
f97e7016c5 | ||
|
|
f82c85712c | ||
|
|
5f18bc578b | ||
|
|
e72a6b50e4 | ||
|
|
66c99ae442 | ||
|
|
5e04125bc3 | ||
|
|
fd3fd43800 | ||
|
|
5184da7c26 | ||
|
|
595caffb51 | ||
|
|
6b00f0fd5b | ||
|
|
e4a427788f | ||
|
|
27a2825a43 | ||
|
|
3d453c335f | ||
|
|
9a85a2a4ef | ||
|
|
443fb233ae | ||
|
|
63b8168c45 | ||
|
|
1e5dae56af | ||
|
|
553d6fd4b0 | ||
|
|
ab98bf05da | ||
|
|
6806b4a197 | ||
|
|
4ae74aed59 | ||
|
|
98e1a5b40f | ||
|
|
279c9b7bea | ||
|
|
56be614980 | ||
|
|
584037ebdd | ||
|
|
44230b73f7 | ||
|
|
b7f1eba6bd | ||
|
|
35bc3025d6 | ||
|
|
6f6531428a | ||
|
|
cb5e962167 | ||
|
|
33da626f78 | ||
|
|
e9766630ed | ||
|
|
9b6adbf4ec | ||
|
|
5f09447da4 |
@@ -19,6 +19,7 @@ start-fullstack-test.sh
|
||||
start-free-main.sh
|
||||
start-local-mainnet.sh.save
|
||||
start-local-testnet.sh.save
|
||||
temp.sh
|
||||
|
||||
coverage
|
||||
start-my-infra
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# bch-api
|
||||
|
||||
[](https://greenkeeper.io/)
|
||||
|
||||
[](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
|
||||
[full stack of BCH software](https://troutsblog.com/research/bitcoin-cash/how-to-bch-full-stack-developer).
|
||||
|
||||
- [API Documentation](https://api.bchjs.cash/docs/)
|
||||
- [BCH Example Applications](https://github.com/Permissionless-Software-Foundation/bch-js-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.
|
||||
- [API Documentation](https://fullstack.cash/documentation)
|
||||
- [Example Code](https://fullstack.cash/examples)
|
||||
|
||||
Have questions? Need help? Join our community support
|
||||
Have questions? Need help? Join our community support:
|
||||
[Telegram channel](https://t.me/bch_js_toolkit)
|
||||
|
||||
## Features
|
||||
@@ -96,4 +94,4 @@ Have questions? Need help? Join our community support
|
||||
[Telegram channel](https://t.me/bch_js_toolkit)
|
||||
|
||||
## License
|
||||
[MIT](LICENSE.md)
|
||||
[MIT](./LICENSE.md)
|
||||
|
||||
@@ -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-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."
|
||||
|
||||
@@ -16,9 +16,9 @@ USER safeuser
|
||||
|
||||
# Clone the repository
|
||||
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
|
||||
RUN npm install
|
||||
RUN npm install --silent
|
||||
|
||||
# Generate documentation
|
||||
RUN npm run docs
|
||||
|
||||
@@ -13,9 +13,9 @@ USER safeuser
|
||||
|
||||
# Clone the repository
|
||||
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
|
||||
RUN npm install
|
||||
RUN npm install --silent
|
||||
|
||||
# Generate documentation
|
||||
RUN npm run docs
|
||||
|
||||
Generated
+178
-153
@@ -147,106 +147,6 @@
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@chris.troutner/bch-js": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@chris.troutner/bch-js/-/bch-js-3.0.1.tgz",
|
||||
"integrity": "sha512-XTOCFeMCBThXlGxdWsmW6OmfZBqa3XtsvFGE3hIapWfvoCUd6fSyegn9G1mXod236D3qKTW/Vk/NBwGlc7Y1YQ==",
|
||||
"requires": {
|
||||
"@uppy/core": "^1.10.4",
|
||||
"@uppy/tus": "^1.5.12",
|
||||
"apidoc": "^0.17.7",
|
||||
"assert": "^2.0.0",
|
||||
"axios": "^0.19.0",
|
||||
"bc-bip68": "^1.0.5",
|
||||
"bch-wallet-bridge.js": "github:web3bch/bch-wallet-bridge.js#master",
|
||||
"bchaddrjs-slp": "^0.2.5",
|
||||
"bigi": "^1.4.2",
|
||||
"bignumber.js": "^9.0.0",
|
||||
"bip-schnorr": "^0.3.0",
|
||||
"bip21": "github:Bitcoin-com/bip21",
|
||||
"bip32-utils": "github:Bitcoin-com/bip32-utils#0.13.1",
|
||||
"bip38": "^2.0.2",
|
||||
"bip39": "^3.0.2",
|
||||
"bip66": "^1.1.5",
|
||||
"bitcoincash-ops": "github:christroutner/bitcoincash-ops",
|
||||
"bitcoincashjs-lib": "github:christroutner/bitcoincashjs-lib",
|
||||
"bitcoinjs-message": "^2.0.0",
|
||||
"bs58": "^4.0.1",
|
||||
"buffer": "^5.1.0",
|
||||
"cashaddrjs": "^0.3.3",
|
||||
"chalk": "^2.3.0",
|
||||
"clear": "0.1.0",
|
||||
"coininfo": "github:Bitcoin-com/coininfo",
|
||||
"commander": "^3.0.0",
|
||||
"cp-file": "^7.0.0",
|
||||
"ecurve": "^1.0.6",
|
||||
"figlet": "^1.2.0",
|
||||
"git-clone": "^0.1.0",
|
||||
"ini": "^1.3.5",
|
||||
"mkdirp": "^0.5.1",
|
||||
"node-cmd": "^3.0.0",
|
||||
"node-emoji": "^1.8.1",
|
||||
"qrcode": "^1.4.1",
|
||||
"randombytes": "^2.0.6",
|
||||
"repl.history": "^0.1.4",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"satoshi-bitcoin": "^1.0.4",
|
||||
"slp-mdm": "0.0.6",
|
||||
"slp-parser": "0.0.4",
|
||||
"socket.io": "^2.1.1",
|
||||
"socket.io-client": "^2.1.1",
|
||||
"touch": "^3.1.0",
|
||||
"wif": "^2.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"apidoc": {
|
||||
"version": "0.17.7",
|
||||
"resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.17.7.tgz",
|
||||
"integrity": "sha512-9Wf4bRPwCuWOIOxR42dDnsXnFw+rhJg5VrMQK+KmNxJwyIh30UqX6gvjjXSG6YO74MqE87F18bbQXUENK9dPGg==",
|
||||
"requires": {
|
||||
"apidoc-core": "~0.8.2",
|
||||
"commander": "^2.19.0",
|
||||
"fs-extra": "^7.0.0",
|
||||
"lodash": "^4.17.10",
|
||||
"markdown-it": "^8.3.1",
|
||||
"winston": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"fs-extra": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
|
||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@istanbuljs/load-nyc-config": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz",
|
||||
@@ -465,6 +365,127 @@
|
||||
"@types/node": ">= 8"
|
||||
}
|
||||
},
|
||||
"@psf/bch-js": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@psf/bch-js/-/bch-js-3.5.5.tgz",
|
||||
"integrity": "sha512-bZs1p6VNDBYJPJuKuYD4h2Wsp+gt38181XlVX+2Yq8JGFOvf5ZRSrkqcd0LYvfiP8f9IA46aaAtTaLQT1KQsJw==",
|
||||
"requires": {
|
||||
"@uppy/core": "^1.10.4",
|
||||
"@uppy/tus": "^1.5.12",
|
||||
"apidoc": "^0.17.7",
|
||||
"assert": "^2.0.0",
|
||||
"axios": "^0.19.0",
|
||||
"bc-bip68": "^1.0.5",
|
||||
"bch-wallet-bridge.js": "github:web3bch/bch-wallet-bridge.js#master",
|
||||
"bchaddrjs-slp": "^0.2.5",
|
||||
"bigi": "^1.4.2",
|
||||
"bignumber.js": "^9.0.0",
|
||||
"bip-schnorr": "^0.3.0",
|
||||
"bip21": "github:Bitcoin-com/bip21",
|
||||
"bip32-utils": "github:Bitcoin-com/bip32-utils#0.13.1",
|
||||
"bip38": "^2.0.2",
|
||||
"bip39": "^3.0.2",
|
||||
"bip66": "^1.1.5",
|
||||
"bitcoincash-ops": "github:christroutner/bitcoincash-ops",
|
||||
"bitcoincashjs-lib": "github:christroutner/bitcoincashjs-lib#master",
|
||||
"bitcoinjs-message": "^2.0.0",
|
||||
"bs58": "^4.0.1",
|
||||
"buffer": "^5.1.0",
|
||||
"cashaddrjs": "^0.3.3",
|
||||
"chalk": "^2.3.0",
|
||||
"clear": "0.1.0",
|
||||
"coininfo": "github:Bitcoin-com/coininfo",
|
||||
"commander": "^3.0.0",
|
||||
"cp-file": "^7.0.0",
|
||||
"ecurve": "^1.0.6",
|
||||
"figlet": "^1.2.0",
|
||||
"git-clone": "^0.1.0",
|
||||
"ini": "^1.3.5",
|
||||
"mkdirp": "^0.5.1",
|
||||
"node-cmd": "^3.0.0",
|
||||
"node-emoji": "^1.8.1",
|
||||
"qrcode": "^1.4.1",
|
||||
"randombytes": "^2.0.6",
|
||||
"repl.history": "^0.1.4",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"satoshi-bitcoin": "^1.0.4",
|
||||
"slp-mdm": "0.0.6",
|
||||
"slp-parser": "0.0.4",
|
||||
"socket.io": "^2.1.1",
|
||||
"socket.io-client": "^2.1.1",
|
||||
"touch": "^3.1.0",
|
||||
"wif": "^2.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"apidoc": {
|
||||
"version": "0.17.7",
|
||||
"resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.17.7.tgz",
|
||||
"integrity": "sha512-9Wf4bRPwCuWOIOxR42dDnsXnFw+rhJg5VrMQK+KmNxJwyIh30UqX6gvjjXSG6YO74MqE87F18bbQXUENK9dPGg==",
|
||||
"requires": {
|
||||
"apidoc-core": "~0.8.2",
|
||||
"commander": "^2.19.0",
|
||||
"fs-extra": "^7.0.0",
|
||||
"lodash": "^4.17.10",
|
||||
"markdown-it": "^8.3.1",
|
||||
"winston": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"bitcoincashjs-lib": {
|
||||
"version": "github:christroutner/bitcoincashjs-lib#715823b5d6eb3e2961e715605ad1464f3223da0b",
|
||||
"from": "github:christroutner/bitcoincashjs-lib#master",
|
||||
"requires": {
|
||||
"bech32": "^1.1.2",
|
||||
"bigi": "^1.4.0",
|
||||
"bip66": "^1.1.0",
|
||||
"bitcoincash-ops": "github:christroutner/bitcoincash-ops",
|
||||
"bs58check": "^2.0.0",
|
||||
"create-hash": "^1.1.0",
|
||||
"create-hmac": "^1.1.3",
|
||||
"ecurve": "^1.0.0",
|
||||
"merkle-lib": "^2.0.10",
|
||||
"pushdata-bitcoin": "github:Bitcoin-com/pushdata-bitcoin#1.2.1",
|
||||
"randombytes": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"typeforce": "^1.18.0",
|
||||
"varuint-bitcoin": "^1.0.4",
|
||||
"wif": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"fs-extra": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
|
||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@semantic-release/commit-analyzer": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-7.0.0.tgz",
|
||||
@@ -872,6 +893,14 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/tus-js-client/-/tus-js-client-1.8.0.tgz",
|
||||
"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": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-1.4.5.tgz",
|
||||
@@ -1402,6 +1431,21 @@
|
||||
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
|
||||
"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": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -2266,27 +2310,6 @@
|
||||
"resolved": "https://registry.npmjs.org/bitcoincashjs/-/bitcoincashjs-0.1.14.tgz",
|
||||
"integrity": "sha512-x0wMVSCZ56ZQnSQ+Xim7XfS0IYiOyOwo0pdMCmR+cDNB7zRhICFuH5i3lNdLSgUw1e7Pc3FmUlC9wruQNlYMnA=="
|
||||
},
|
||||
"bitcoincashjs-lib": {
|
||||
"version": "github:christroutner/bitcoincashjs-lib#b03ca974d4fd548cb42c7a522863e5d9be6051ef",
|
||||
"from": "github:christroutner/bitcoincashjs-lib",
|
||||
"requires": {
|
||||
"bech32": "^1.1.2",
|
||||
"bigi": "^1.4.0",
|
||||
"bip66": "^1.1.0",
|
||||
"bitcoincash-ops": "github:christroutner/bitcoincash-ops",
|
||||
"bs58check": "^2.0.0",
|
||||
"create-hash": "^1.1.0",
|
||||
"create-hmac": "^1.1.3",
|
||||
"ecurve": "^1.0.0",
|
||||
"merkle-lib": "^2.0.10",
|
||||
"pushdata-bitcoin": "github:Bitcoin-com/pushdata-bitcoin#1.2.1",
|
||||
"randombytes": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"typeforce": "^1.18.0",
|
||||
"varuint-bitcoin": "^1.0.4",
|
||||
"wif": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"bitcoinjs-message": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bitcoinjs-message/-/bitcoinjs-message-2.1.0.tgz",
|
||||
@@ -2301,42 +2324,28 @@
|
||||
}
|
||||
},
|
||||
"bitcore-lib": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-8.21.0.tgz",
|
||||
"integrity": "sha512-Z1lDqSReD/Ah/n7qEsR564AlNImwYeB1MQ0fnOD6e2piNsTXQ3cSjgH93MxVARpX4LRkEev2XBnYCJ4NcYTQdQ==",
|
||||
"version": "8.22.2",
|
||||
"resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-8.22.2.tgz",
|
||||
"integrity": "sha512-atl/RN7x2R/JQMaIgUzX0EQZ+I/d7fTaOoS2/5k+H5POGc2vygWZ9MWpGJi3T9Yb5jM/cT5aFsrDq8s8l6lqgQ==",
|
||||
"requires": {
|
||||
"bech32": "=1.1.3",
|
||||
"bn.js": "=4.11.8",
|
||||
"bs58": "^4.0.1",
|
||||
"buffer-compare": "=1.1.1",
|
||||
"elliptic": "=6.4.0",
|
||||
"elliptic": "^6.5.3",
|
||||
"inherits": "=2.0.1",
|
||||
"lodash": "=4.17.15"
|
||||
"lodash": "^4.17.20"
|
||||
},
|
||||
"dependencies": {
|
||||
"elliptic": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz",
|
||||
"integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=",
|
||||
"requires": {
|
||||
"bn.js": "^4.4.0",
|
||||
"brorand": "^1.0.1",
|
||||
"hash.js": "^1.0.0",
|
||||
"hmac-drbg": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"minimalistic-crypto-utils": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
|
||||
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2381,9 +2390,9 @@
|
||||
}
|
||||
},
|
||||
"bl": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
|
||||
"integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz",
|
||||
"integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==",
|
||||
"requires": {
|
||||
"readable-stream": "^2.3.5",
|
||||
"safe-buffer": "^5.1.1"
|
||||
@@ -4424,11 +4433,22 @@
|
||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
||||
},
|
||||
"electrum-cash": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/electrum-cash/-/electrum-cash-1.0.1.tgz",
|
||||
"integrity": "sha512-snMgRt6JzsHdCdf+Un1rLj4RXjje1WuFVTx3Xx+6mitEibYalmD+x0ts146VF4Ki3+42DQKXtQXl0bzCAY7Jtw==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/electrum-cash/-/electrum-cash-2.0.2.tgz",
|
||||
"integrity": "sha512-b/mLctjwnmfp4cuvJbl9wgCxf4GWWiKPYZXEtekoyGPviZwOe2bnMC7LGOxAMpOW0YyU4W12kK6rbqskQc6EkA==",
|
||||
"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": {
|
||||
@@ -7551,6 +7571,11 @@
|
||||
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
|
||||
"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": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||
@@ -9042,9 +9067,9 @@
|
||||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
|
||||
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==",
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
|
||||
"dev": true
|
||||
},
|
||||
"node-gyp-build": {
|
||||
|
||||
+3
-3
@@ -17,7 +17,7 @@
|
||||
"test": "npm run lint && npm run test-v3",
|
||||
"lint": "standard --env mocha --fix",
|
||||
"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:slpdb": "mocha --timeout 25000 test/v3/integration/slp*.js",
|
||||
"coverage": "nyc report --reporter=text-lcov | coveralls",
|
||||
@@ -28,7 +28,7 @@
|
||||
"node": ">=10.15.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chris.troutner/bch-js": "^3.0.1",
|
||||
"@psf/bch-js": "^3.5.5",
|
||||
"apidoc": "^0.23.0",
|
||||
"axios": "^0.19.0",
|
||||
"bitcore-lib-cash": "^8.20.3",
|
||||
@@ -37,7 +37,7 @@
|
||||
"cors": "^2.8.3",
|
||||
"debug": "~4.1.1",
|
||||
"dotenv": "^8.0.0",
|
||||
"electrum-cash": "^1.0.1",
|
||||
"electrum-cash": "^2.0.2",
|
||||
"express": "^4.15.5",
|
||||
"express-basic-auth": "^1.1.3",
|
||||
"express-rate-limit": "^5.0.0",
|
||||
|
||||
@@ -116,13 +116,20 @@ class RateLimits {
|
||||
let pointsToConsume = _this.calcPoints(decoded)
|
||||
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
|
||||
// apply paid-access rate limits.
|
||||
const origin = req.get('origin')
|
||||
if (
|
||||
origin &&
|
||||
(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
|
||||
res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
|
||||
|
||||
+14
-14
@@ -23,24 +23,24 @@
|
||||
target="_blank"
|
||||
>REST API</a
|
||||
>, 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
|
||||
href="https://troutsblog.com/research/bitcoin-cash/how-to-bch-full-stack-developer"
|
||||
href="https://fullstack.cash/documentation"
|
||||
target="_blank"
|
||||
>building full stack applications on BCH</a
|
||||
>educational resources</a
|
||||
>.
|
||||
</p><br />
|
||||
<p>
|
||||
A REST API can work with any programming language, but bch-api is intended
|
||||
to be paired with the
|
||||
<a
|
||||
href="https://www.npmjs.com/package/@chris.troutner/bch-js"
|
||||
href="https://www.npmjs.com/package/@psf/bch-js"
|
||||
target="_blank"
|
||||
>npm bch-js</a
|
||||
>
|
||||
JavaScript library.
|
||||
<a
|
||||
href="https://github.com/Permissionless-Software-Foundation/bch-js-examples"
|
||||
href="https://fullstack.cash/examples"
|
||||
target="_blank"
|
||||
>Here is example code</a
|
||||
>
|
||||
@@ -53,7 +53,7 @@
|
||||
</p><br />
|
||||
<p>
|
||||
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
|
||||
>
|
||||
in order to unlock higher rate limits.
|
||||
@@ -66,7 +66,7 @@
|
||||
<h3>bch-api</h3>
|
||||
<ul>
|
||||
<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
|
||||
</a>
|
||||
</li>
|
||||
@@ -80,14 +80,14 @@
|
||||
<br />
|
||||
|
||||
<li>
|
||||
<a href="https://tapi.bchjs.cash">
|
||||
<a href="https://tapi.fullstack.cash">
|
||||
testnet REST API
|
||||
</a>
|
||||
</li>
|
||||
<br />
|
||||
|
||||
<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
|
||||
</a>
|
||||
</li>
|
||||
@@ -114,14 +114,14 @@
|
||||
</li>
|
||||
<br />
|
||||
<li>
|
||||
<a href="https://bchjs.cash/" target="_blank">
|
||||
<a href="https://fullstack.cash/cashstrap" target="_blank">
|
||||
Run your own infrastructure
|
||||
</a>
|
||||
</li>
|
||||
<br />
|
||||
|
||||
<li>
|
||||
<a href="https://github.com/bitcoinbook/bitcoinbook#chapters" target="_blank">
|
||||
<a href="https://fullstack.cash/documentation" target="_blank">
|
||||
Mastering Bitcoin (great free reference)
|
||||
</a>
|
||||
</li>
|
||||
@@ -135,7 +135,7 @@
|
||||
<br />
|
||||
|
||||
<li>
|
||||
<a href="https://developer.bitcoin.com/faucets/bch/" target="_blank">
|
||||
<a href="https://fullstack.cash/faucet/" target="_blank">
|
||||
Testnet Faucet
|
||||
</a>
|
||||
</li>
|
||||
@@ -148,7 +148,7 @@
|
||||
<h3>bch-js</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://www.npmjs.com/package/@chris.troutner/bch-js"
|
||||
<a href="https://www.npmjs.com/package/@psf/bch-js"
|
||||
target="_blank">
|
||||
npm library
|
||||
</a>
|
||||
@@ -163,7 +163,7 @@
|
||||
<br />
|
||||
|
||||
<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
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -23,7 +23,7 @@ const router = express.Router()
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
let _this
|
||||
|
||||
+341
-3
@@ -10,7 +10,7 @@ const axios = require('axios')
|
||||
const util = require('util')
|
||||
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 wlogger = require('../../util/winston-logging')
|
||||
@@ -19,7 +19,7 @@ const config = require('../../../config')
|
||||
const RouteUtils = require('../../util/route-utils')
|
||||
const routeUtils = new RouteUtils()
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
let _this
|
||||
@@ -50,6 +50,10 @@ class Electrum {
|
||||
_this.router.get('/', _this.root)
|
||||
_this.router.get('/utxos/:address', _this.getUtxos)
|
||||
_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.post('/balance', _this.balanceBulk)
|
||||
_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
|
||||
// to be a cash address, and input validation to have already been done by
|
||||
// parent, calling function.
|
||||
@@ -911,7 +1247,9 @@ class Electrum {
|
||||
const address = _this.bitcore.Address.fromString(addrStr)
|
||||
// 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}`)
|
||||
|
||||
const scripthash = _this.bitcore.crypto.Hash.sha256(script.toBuffer())
|
||||
|
||||
@@ -15,7 +15,7 @@ const config = require('../../../config')
|
||||
const RouteUtils = require('../../util/route-utils')
|
||||
const routeUtils = new RouteUtils()
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
// Use Blockbook for getting indexer data.
|
||||
|
||||
@@ -17,7 +17,7 @@ const routeUtils = new RouteUtils()
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
let _this
|
||||
|
||||
@@ -11,7 +11,7 @@ const express = require('express')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// const BCHJS = require('@chris.troutner/bch-js')
|
||||
// const BCHJS = require('@psf/bch-js')
|
||||
// const bchjs = new BCHJS()
|
||||
|
||||
// let _this
|
||||
|
||||
@@ -10,7 +10,7 @@ const wlogger = require('../../util/winston-logging')
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const axios = require('axios')
|
||||
|
||||
const SLPSDK = require('@chris.troutner/bch-js')
|
||||
const SLPSDK = require('@psf/bch-js')
|
||||
const SLP = new SLPSDK()
|
||||
|
||||
class Slpdb {
|
||||
|
||||
+79
-169
@@ -13,7 +13,7 @@ const Slpdb = require('./services/slpdb')
|
||||
// const strftime = require('strftime')
|
||||
const wlogger = require('../../util/winston-logging')
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
// Used to convert error messages to strings, to safely pass to users.
|
||||
@@ -64,10 +64,6 @@ class Slp {
|
||||
_this.router.get('/balancesForAddress/:address', _this.balancesForAddress)
|
||||
_this.router.post('/balancesForAddress', _this.balancesForAddressBulk)
|
||||
_this.router.get('/balancesForToken/:tokenId', _this.balancesForTokenSingle)
|
||||
_this.router.get(
|
||||
'/balance/:address/:tokenId',
|
||||
_this.balancesForAddressByTokenID
|
||||
)
|
||||
_this.router.get('/convert/:address', _this.convertAddressSingle)
|
||||
_this.router.post('/convert', _this.convertAddressBulk)
|
||||
_this.router.post('/validateTxid', _this.validateBulk)
|
||||
@@ -83,6 +79,7 @@ class Slp {
|
||||
_this.txsByAddressSingle
|
||||
)
|
||||
_this.router.post('/generateSendOpReturn', _this.generateSendOpReturn)
|
||||
_this.router.post('/hydrateUtxos', _this.hydrateUtxos)
|
||||
}
|
||||
|
||||
// DRY error handler.
|
||||
@@ -830,170 +827,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.
|
||||
* @apiName Convert address to slpAddr, cashAddr and legacy.
|
||||
@@ -1687,6 +1520,83 @@ class Slp {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /slp/hydrateUtxos/ hydrateUtxos
|
||||
* @apiName SLP hydrateUtxos
|
||||
* @apiGroup SLP
|
||||
* @apiDescription Hydrate UTXO data with SLP information.
|
||||
*
|
||||
* Expects an array of UTXO objects as input. Returns an array of equal size.
|
||||
* Returns UTXO data hydrated with token information. If the UTXO does not
|
||||
* belong to a SLP transaction, it will return an isValid property set to
|
||||
* false. If the UTXO is part of an SLP transaction, it will return the UTXO
|
||||
* object with additional SLP information attached. An isValid property will
|
||||
* be included. If its value is true, the UTXO is a valid SLP UTXO. If the
|
||||
* value is null, then SLPDB has not yet processed that txid and validity has
|
||||
* not been confirmed.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X POST "https://api.fullstack.cash/v3/slp/hydrateUtxos" -H "accept:application/json" -H "Content-Type: application/json" -d '{"utxos":[{"txid": "d56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56","vout": 3, "value": "6816", "height": 606848, "confirmations": 13, "satoshis": 6816}, {"txid": "d56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56","vout": 2, "value": "546", "height": 606848, "confirmations": 13, "satoshis": 546}]}'
|
||||
*
|
||||
*
|
||||
*/
|
||||
async hydrateUtxos (req, res, next) {
|
||||
try {
|
||||
const utxos = req.body.utxos
|
||||
|
||||
// Validate inputs
|
||||
if (!Array.isArray(utxos)) {
|
||||
res.status(422)
|
||||
return res.json({
|
||||
error: 'Input must be an array.'
|
||||
})
|
||||
}
|
||||
|
||||
if (!utxos.length) {
|
||||
res.status(422)
|
||||
return res.json({
|
||||
error: 'Array should not be empty'
|
||||
})
|
||||
}
|
||||
|
||||
if (utxos.length > 20) {
|
||||
res.status(422)
|
||||
return res.json({
|
||||
error: 'Array too long, max length is 20'
|
||||
})
|
||||
}
|
||||
|
||||
// Loop through each address and query the UTXOs for that element.
|
||||
for (let i = 0; i < utxos.length; i++) {
|
||||
const theseUtxos = utxos[i].utxos
|
||||
|
||||
// Get SLP token details.
|
||||
const details = await _this.bchjs.SLP.Utils.tokenUtxoDetails(theseUtxos)
|
||||
// console.log('details : ', details)
|
||||
|
||||
// Replace the original UTXO data with the hydrated data.
|
||||
utxos[i].utxos = details
|
||||
}
|
||||
|
||||
res.status(200)
|
||||
return res.json({ slpUtxos: utxos })
|
||||
} catch (err) {
|
||||
wlogger.error('Error in slp.js/hydrateUtxos().', err)
|
||||
|
||||
// Decode the error message.
|
||||
const { msg, status } = routeUtils.decodeError(err)
|
||||
if (msg) {
|
||||
res.status(status)
|
||||
return res.json({ error: msg })
|
||||
}
|
||||
|
||||
res.status(500)
|
||||
return res.json({
|
||||
error: 'Error in hydrateUtxos()'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Slp
|
||||
|
||||
@@ -12,7 +12,7 @@ const blockbook = new Blockbook()
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
// const BCHJS_TESTNET = 'https://testnet.bchjs.cash/v3/'
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const router = express.Router()
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
// Connect the route endpoints to their handler functions.
|
||||
|
||||
@@ -10,7 +10,7 @@ const wlogger = require('./winston-logging')
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
// let _this
|
||||
|
||||
+496
-2
@@ -2,7 +2,7 @@
|
||||
TESTS FOR THE ELECTRUMX.JS LIBRARY
|
||||
|
||||
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
|
||||
was an easy fix.
|
||||
|
||||
@@ -30,6 +30,18 @@ const mockData = require('./mocks/electrumx-mock')
|
||||
const util = require('util')
|
||||
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', () => {
|
||||
let req, res
|
||||
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', () => {
|
||||
// root route handler.
|
||||
const root = electrumxRoute.root
|
||||
@@ -94,7 +117,7 @@ describe('#ElectrumX Router', () => {
|
||||
})
|
||||
|
||||
describe('#addressToScripthash', () => {
|
||||
it('should accurately return a scripthash', () => {
|
||||
it('should accurately return a scripthash for a P2PKH address', () => {
|
||||
const addr = 'bitcoincash:qpr270a5sxphltdmggtj07v4nskn9gmg9yx4m5h7s4'
|
||||
|
||||
const scripthash = electrumxRoute.addressToScripthash(addr)
|
||||
@@ -104,6 +127,17 @@ describe('#ElectrumX Router', () => {
|
||||
|
||||
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', () => {
|
||||
@@ -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', () => {
|
||||
it('should throw error for invalid address', async () => {
|
||||
try {
|
||||
|
||||
@@ -22,6 +22,9 @@ if (!process.env.SLPDB_URL) {
|
||||
const SLP = require('../../../src/routes/v3/slp')
|
||||
const slp = new SLP()
|
||||
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
const { mockReq, mockRes } = require('../mocks/express-mocks')
|
||||
|
||||
describe('#slp', () => {
|
||||
@@ -102,7 +105,7 @@ describe('#slp', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateSendOpReturn', () => {
|
||||
describe('#generateSendOpReturn', () => {
|
||||
it('should return OP_RETURN script', async () => {
|
||||
req.body.tokenUtxos = [
|
||||
{
|
||||
@@ -121,4 +124,93 @@ describe('#slp', () => {
|
||||
assert.isNumber(result.outputs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#hydrateUtxos', () => {
|
||||
it('should return utxo details', async () => {
|
||||
const utxos = [
|
||||
{
|
||||
utxos: [
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 3,
|
||||
value: '6816',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 6816
|
||||
},
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 2,
|
||||
value: '546',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 546
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
req.body.utxos = utxos
|
||||
const result = await slp.hydrateUtxos(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// Test the general structure of the output.
|
||||
assert.isArray(result.slpUtxos)
|
||||
assert.equal(result.slpUtxos.length, 1)
|
||||
assert.equal(result.slpUtxos[0].utxos.length, 2)
|
||||
|
||||
// Test the non-slp UTXO.
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'txid')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'vout')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'value')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'height')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'confirmations')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'satoshis')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'isValid')
|
||||
assert.equal(result.slpUtxos[0].utxos[0].isValid, false)
|
||||
|
||||
// Test the slp UTXO.
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'txid')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'vout')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'value')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'height')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'confirmations')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'satoshis')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'isValid')
|
||||
assert.equal(result.slpUtxos[0].utxos[1].isValid, true)
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'transactionType')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenId')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenTicker')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenName')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenDocumentUrl')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenDocumentHash')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'decimals')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenType')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenQty')
|
||||
})
|
||||
|
||||
it('should process data directly from Electrumx', async () => {
|
||||
const addrs = [
|
||||
'bitcoincash:qq6mvsm7l92d77zpymmltvaw09p5uzghyuyx7spygg',
|
||||
'bitcoincash:qpjdrs8qruzh8xvusdfmutjx62awcepnhyperm3g89',
|
||||
'bitcoincash:qzygn28zpgeemnptkn26xzyuzzfu9l8f9vfvq7kptk'
|
||||
]
|
||||
|
||||
const utxos = await bchjs.Electrumx.utxo(addrs)
|
||||
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
||||
|
||||
req.body.utxos = utxos.utxos
|
||||
const result = await slp.hydrateUtxos(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// Test the general structure of the output.
|
||||
assert.isArray(result.slpUtxos)
|
||||
assert.equal(result.slpUtxos.length, 3)
|
||||
assert.equal(result.slpUtxos[0].utxos.length, 1)
|
||||
assert.equal(result.slpUtxos[1].utxos.length, 1)
|
||||
assert.equal(result.slpUtxos[2].utxos.length, 2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 = {
|
||||
utxos,
|
||||
balance,
|
||||
txHistory,
|
||||
mempool
|
||||
mempool,
|
||||
txDetails,
|
||||
blockHeaders
|
||||
}
|
||||
|
||||
+147
@@ -888,6 +888,153 @@ describe('#SLP', () => {
|
||||
assert.isNumber(result.outputs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#hydrateUtxos', () => {
|
||||
it('should throw error if input is not an array.', async () => {
|
||||
req.body.utxos = 'test'
|
||||
|
||||
const result = await slpRoute.hydrateUtxos(req, res)
|
||||
// console.log(`result: `, result)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(result.error, 'Input must be an array')
|
||||
})
|
||||
|
||||
it('should throw error if Array is empty', async () => {
|
||||
req.body.utxos = []
|
||||
|
||||
const result = await slpRoute.hydrateUtxos(req, res)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(result.error, 'Array should not be empty')
|
||||
})
|
||||
|
||||
it('should throw error if Array is too long', async () => {
|
||||
const utxo = {
|
||||
txid:
|
||||
'bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90',
|
||||
vout: 3,
|
||||
amount: 0.00002015,
|
||||
satoshis: 2015,
|
||||
height: 594892,
|
||||
confirmations: 5
|
||||
}
|
||||
|
||||
const utxos = []
|
||||
|
||||
// Populate array with 21 utxos
|
||||
for (let i = 0; i < 21; i++) {
|
||||
utxos.push(utxo)
|
||||
}
|
||||
|
||||
req.body.utxos = utxos
|
||||
|
||||
const result = await slpRoute.hydrateUtxos(req, res)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(result.error, 'Array too long, max length is 20')
|
||||
})
|
||||
|
||||
it('should return utxo details', async () => {
|
||||
const utxos = [
|
||||
{
|
||||
utxos: [
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 3,
|
||||
value: '6816',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 6816
|
||||
},
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 2,
|
||||
value: '546',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 546
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// Mock the external network call.
|
||||
sandbox.stub(slpRoute.bchjs.SLP.Utils, 'tokenUtxoDetails').resolves([
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 3,
|
||||
value: '6816',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 6816,
|
||||
isValid: false
|
||||
},
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 2,
|
||||
value: '546',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 546,
|
||||
utxoType: 'token',
|
||||
transactionType: 'send',
|
||||
tokenId:
|
||||
'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d',
|
||||
tokenTicker: 'TAP',
|
||||
tokenName: 'Thoughts and Prayers',
|
||||
tokenDocumentUrl: '',
|
||||
tokenDocumentHash: '',
|
||||
decimals: 0,
|
||||
tokenType: 1,
|
||||
tokenQty: 5,
|
||||
isValid: true
|
||||
}
|
||||
])
|
||||
|
||||
req.body.utxos = utxos
|
||||
const result = await slpRoute.hydrateUtxos(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// Test the general structure of the output.
|
||||
assert.isArray(result.slpUtxos)
|
||||
assert.equal(result.slpUtxos.length, 1)
|
||||
assert.equal(result.slpUtxos[0].utxos.length, 2)
|
||||
|
||||
// Test the non-slp UTXO.
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'txid')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'vout')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'value')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'height')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'confirmations')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'satoshis')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'isValid')
|
||||
assert.equal(result.slpUtxos[0].utxos[0].isValid, false)
|
||||
|
||||
// Test the slp UTXO.
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'txid')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'vout')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'value')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'height')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'confirmations')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'satoshis')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'isValid')
|
||||
assert.equal(result.slpUtxos[0].utxos[1].isValid, true)
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'transactionType')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenId')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenTicker')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenName')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenDocumentUrl')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenDocumentHash')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'decimals')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenType')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenQty')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
|
||||
+31
-58
@@ -135,11 +135,12 @@ describe('#Util', () => {
|
||||
// Mock the RPC call for unit tests.
|
||||
if (process.env.TEST === 'unit') {
|
||||
nock(`${process.env.RPC_BASEURL}`)
|
||||
.post(uri => uri.includes('/'))
|
||||
.post((uri) => uri.includes('/'))
|
||||
.reply(200, { result: mockData.mockAddress })
|
||||
}
|
||||
|
||||
req.params.address = 'bitcoincash:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5hzljcrnd'
|
||||
req.params.address =
|
||||
'bitcoincash:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5hzljcrnd'
|
||||
|
||||
const result = await validateAddress(req, res)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
@@ -255,7 +256,7 @@ describe('#Util', () => {
|
||||
// Mock the RPC call for unit tests.
|
||||
if (process.env.TEST === 'unit') {
|
||||
nock(`${process.env.RPC_BASEURL}`)
|
||||
.post(uri => uri.includes('/'))
|
||||
.post((uri) => uri.includes('/'))
|
||||
.reply(200, { result: mockData.mockAddress })
|
||||
}
|
||||
|
||||
@@ -281,7 +282,7 @@ describe('#Util', () => {
|
||||
// Mock the RPC call for unit tests.
|
||||
if (process.env.TEST === 'unit') {
|
||||
nock(`${process.env.RPC_BASEURL}`)
|
||||
.post(uri => uri.includes('/'))
|
||||
.post((uri) => uri.includes('/'))
|
||||
.times(2)
|
||||
.reply(200, { result: mockData.mockAddress })
|
||||
}
|
||||
@@ -356,17 +357,11 @@ describe('#Util', () => {
|
||||
// Mock the RPC call for unit tests.
|
||||
|
||||
sandbox
|
||||
.stub(
|
||||
utilRouteInst.blockbook,
|
||||
'balanceFromBlockbook'
|
||||
)
|
||||
.stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
|
||||
.resolves(mockData.mockBalance)
|
||||
|
||||
sandbox
|
||||
.stub(
|
||||
utilRouteInst.blockbook,
|
||||
'utxosFromBlockbook'
|
||||
)
|
||||
.stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
|
||||
.resolves(mockData.mockUtxos)
|
||||
|
||||
sandbox
|
||||
@@ -389,51 +384,41 @@ describe('#Util', () => {
|
||||
|
||||
assert.equal(result, 'test-txid')
|
||||
})
|
||||
}
|
||||
|
||||
it('should return balance if balance-only is true', async () => {
|
||||
// Mock the RPC call for unit tests.
|
||||
if (process.env.TEST === 'unit') {
|
||||
it('should return balance if balance-only is true', async () => {
|
||||
// Mock the RPC call for unit tests.
|
||||
|
||||
sandbox
|
||||
.stub(
|
||||
utilRouteInst.blockbook,
|
||||
'balanceFromBlockbook'
|
||||
)
|
||||
.stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
|
||||
.resolves(mockData.mockBalance)
|
||||
}
|
||||
|
||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
||||
// to the network.
|
||||
sandbox
|
||||
.stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction')
|
||||
.resolves('test-txid')
|
||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
||||
// to the network.
|
||||
sandbox
|
||||
.stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction')
|
||||
.resolves('test-txid')
|
||||
|
||||
req.body = {
|
||||
wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt',
|
||||
balanceOnly: true
|
||||
}
|
||||
req.body = {
|
||||
wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt',
|
||||
balanceOnly: true
|
||||
}
|
||||
|
||||
const result = await utilRouteInst.sweepWif(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
const result = await utilRouteInst.sweepWif(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
assert.isNumber(result)
|
||||
})
|
||||
assert.isNumber(result)
|
||||
})
|
||||
}
|
||||
|
||||
// Unit tests only
|
||||
if (process.env.TEST === 'unit') {
|
||||
it('should generate transaction for valid BCH-only sweep', async () => {
|
||||
sandbox
|
||||
.stub(
|
||||
utilRouteInst.blockbook,
|
||||
'balanceFromBlockbook'
|
||||
)
|
||||
.stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
|
||||
.resolves(mockData.mockBalance)
|
||||
|
||||
sandbox
|
||||
.stub(
|
||||
utilRouteInst.blockbook,
|
||||
'utxosFromBlockbook'
|
||||
)
|
||||
.stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
|
||||
.resolves(mockData.mockUtxos)
|
||||
|
||||
// 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 () => {
|
||||
sandbox
|
||||
.stub(
|
||||
utilRouteInst.blockbook,
|
||||
'balanceFromBlockbook'
|
||||
)
|
||||
.stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
|
||||
.resolves(mockData.mockBalance)
|
||||
|
||||
sandbox
|
||||
.stub(
|
||||
utilRouteInst.blockbook,
|
||||
'utxosFromBlockbook'
|
||||
)
|
||||
.stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
|
||||
.resolves(mockData.mockUtxos)
|
||||
|
||||
// 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 () => {
|
||||
sandbox
|
||||
.stub(
|
||||
utilRouteInst.blockbook,
|
||||
'balanceFromBlockbook'
|
||||
)
|
||||
.stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
|
||||
.resolves(mockData.mockBalance)
|
||||
|
||||
sandbox
|
||||
.stub(
|
||||
utilRouteInst.blockbook,
|
||||
'utxosFromBlockbook'
|
||||
)
|
||||
.stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
|
||||
.resolves(mockData.mockThreeUtxos)
|
||||
|
||||
// Force token utxo to appear as regular BCH utxo.
|
||||
|
||||
Reference in New Issue
Block a user