Compare commits

...
313 Commits
Author SHA1 Message Date
Chris Troutner f987d4fdf2 Merge pull request #198 from Permissionless-Software-Foundation/ct-unstable
fix(electrumx.getBalance()): Allowing ecash addresses to pass through
2022-07-10 19:10:44 -07:00
Chris Troutner acdee61a1b fix(electrumx.getBalance()): Allowing ecash addresses to pass through 2022-07-10 19:09:12 -07:00
Chris Troutner e2d920fcc7 Merge pull request #197 from Permissionless-Software-Foundation/ct-unstable
fix(psf-slp-indexer getAddress): Letting ecash addrs pass through
2022-07-10 18:26:46 -07:00
Chris Troutner 4f74d9486b fix(psf-slp-indexer getAddress): Letting ecash addrs pass through 2022-07-10 18:25:31 -07:00
Chris Troutner 04c9267c3b Merge branch 'master' of https://github.com/Permissionless-Software-Foundation/bch-api 2022-07-10 15:33:21 -07:00
Chris Troutner c58e3e0ea9 Updating gitignore 2022-07-10 15:33:17 -07:00
Chris Troutner 4e18f86e98 Merge pull request #196 from Permissionless-Software-Foundation/ct-unstable
fix(body size): Increasing POST body size allowed
2022-06-10 06:47:50 -07:00
Chris Troutner 0d00d340cf fix(body size): Increasing POST body size allowed 2022-06-10 06:46:16 -07:00
Chris Troutner 767a0fd38b Minor clarification to comment 2022-05-15 07:51:37 -07:00
Chris Troutner e2bcb9783e Merge pull request #195 from Permissionless-Software-Foundation/ct-unstable
fix(bch-js): Trying improved bch-js instance in psf-slp-indexer.js
2022-05-10 16:28:12 -07:00
Chris Troutner 5d9565b1d2 fix(bch-js): Trying improved bch-js instance in psf-slp-indexer.js 2022-05-10 16:14:55 -07:00
Chris Troutner 7e9cac8a72 Merge pull request #194 from Permissionless-Software-Foundation/ct-unstable
fix(getMutableData): Adding additional debuging to console
2022-05-10 15:11:39 -07:00
Chris Troutner 8aad3b6b97 fix(bch-js): Updating to v6.3.4 2022-05-10 15:10:51 -07:00
Chris Troutner d1c35044b4 fix(getMutableData): Adding additional debuging to console 2022-05-10 14:57:01 -07:00
Chris Troutner 7f995e39e4 Merge pull request #193 from Permissionless-Software-Foundation/ct-unstable
fix(docker): Limiting size of logging
2022-05-10 07:06:19 -07:00
Chris Troutner f138d32022 fix(docker): Limiting size of logging 2022-05-10 07:05:34 -07:00
Chris Troutner 26d530158a Merge pull request #192 from Permissionless-Software-Foundation/ct-unstable
fix(token data): Returning CID and not actual content
2022-05-07 20:50:11 -07:00
Chris Troutner a3adf1eab5 fix(token data): Returning CID and not actual content 2022-05-07 20:49:13 -07:00
Chris Troutner 805c6da906 Updating bch-js 2022-05-07 20:35:50 -07:00
Chris Troutner bd0ae5fa74 Merge pull request #189 from Permissionless-Software-Foundation/feat-getTokenData
fix(getTokenData): Updated to reflect changes in PS002 spec
2022-05-02 12:19:58 -07:00
Chris Troutner 777c12776e feat(getTokenData): PS002 mutable data retrieval 2022-05-02 12:18:44 -07:00
Daniel Gonzalez cdf81baf0f feat(tests): Updated tests for getTokenData() in bch-api 2022-05-01 16:46:26 -04:00
Chris Troutner 6b602a1f66 Merge pull request #191 from Permissionless-Software-Foundation/ct-unstable
Updating README
2022-04-28 12:06:12 -07:00
Chris Troutner 77a541733d fixing readme 2022-04-28 12:05:25 -07:00
Chris Troutner 2b219901bd Updating README 2022-04-28 11:59:48 -07:00
Chris Troutner 92c257ceb3 Updating README 2022-04-28 11:59:01 -07:00
Chris Troutner db0a4c466a fix(README): Updating README 2022-04-28 11:52:03 -07:00
Chris Troutner 776628ae9e Editing readme 2022-04-28 10:22:09 -07:00
Chris Troutner 7a3e03c4b7 Editing readme 2022-04-28 10:09:52 -07:00
Chris Troutner 7459f7aff6 Editing readme 2022-04-28 10:06:12 -07:00
Chris Troutner e2dea9518c Merge pull request #190 from Permissionless-Software-Foundation/ct-unstable
fix(api-doc): Updated api-doc
2022-04-28 09:02:47 -07:00
Chris Troutner 8d83431034 fix(api-doc): Updated api-doc 2022-04-28 08:37:58 -07:00
Chris Troutner 160366c17a fix(getTokenData): Updated to reflect changes in PS002 spec 2022-04-26 08:35:36 -07:00
Chris Troutner ec6bb25278 Merge pull request #188 from Permissionless-Software-Foundation/ct-unstable
fix(logs): Removing debugging screen spam
2022-04-25 15:32:14 -07:00
Chris Troutner 293213dc33 fix(logs): Removing debugging screen spam 2022-04-25 15:31:24 -07:00
Chris Troutner 0772782d20 Merge pull request #187 from Permissionless-Software-Foundation/ct-unstable
fix(electrumx): Allowing POST balance to circumvent cache
2022-04-25 15:26:00 -07:00
Chris Troutner 6c977a912e fix(electrumx): Allowing POST balance to circumvent cache 2022-04-25 15:22:49 -07:00
Chris Troutner d60f1f117e Merge pull request #186 from Permissionless-Software-Foundation/ct-unstable
fix(electrumx): more debugging
2022-04-24 18:01:42 -07:00
Chris Troutner 4dea95c631 fix(electrumx): more debugging 2022-04-24 18:00:56 -07:00
Chris Troutner eb47d422ab Merge pull request #185 from Permissionless-Software-Foundation/ct-unstable
fix(rate-limiting): Adding debugging for most used endpoint
2022-04-24 17:48:37 -07:00
Chris Troutner 302f3c6a0e fix(rate-limiting): Adding debugging for most used endpoint 2022-04-24 17:41:59 -07:00
Chris Troutner fd897a8fb0 Merge pull request #184 from Permissionless-Software-Foundation/dh-get-token-data
feat(endpoint): Added getTokenData() method to bch-api
2022-04-17 07:59:16 -07:00
Chris Troutner cc8a8e0947 Adding curl example 2022-04-17 07:58:33 -07:00
Chris Troutner 9669d0b16a Minor edits 2022-04-17 07:11:34 -07:00
Daniel Gonzalez dbc4e3e731 feat(endpoint): Added getTokenData() method to bch-api 2022-04-15 18:25:12 -04:00
Chris Troutner 88231cb303 Merge pull request #183 from Permissionless-Software-Foundation/ct-unstable
fix(rate limits): Decreasing ANON rate limits from 20 to 10 RPM
2022-04-11 17:03:13 -07:00
Chris Troutner a33661ddcf fix(rate limits): Decreasing ANON rate limits from 20 to 10 RPM 2022-04-11 17:01:50 -07:00
Chris Troutner 6c0a7e29c1 Merge pull request #182 from Permissionless-Software-Foundation/ct-unstable
fix(rate-limits): Further slowing down anon freeloaders
2022-04-08 07:38:08 -07:00
Chris Troutner 56c52ab6ed Fixing unit tests 2022-04-08 07:35:46 -07:00
Chris Troutner d3cf02122c fix(rate-limits): Further slowing down anon freeloaders 2022-04-08 07:28:12 -07:00
Chris Troutner f253211a2f Merge pull request #181 from Permissionless-Software-Foundation/ct-unstable
fix(rate-limits): Adding delay to 422 response for anons too
2022-04-04 11:04:54 -07:00
Chris Troutner 9958821371 fix(rate-limits): Adding delay to 422 response for anons too 2022-04-04 10:51:28 -07:00
Chris Troutner d5f7426a99 Merge pull request #180 from Permissionless-Software-Foundation/ct-unstable
fix(rate-limits): Adding artifical delay to anon requests
2022-04-04 10:27:43 -07:00
Chris Troutner 0c7a816842 fix(rate-limits): Adding artifical delay to anon requests 2022-04-04 10:24:45 -07:00
Chris Troutner 6603a004c4 Merge pull request #179 from Permissionless-Software-Foundation/ct-unstable
fix(logs): Logging the result of rate limiting
2022-03-28 14:53:50 -07:00
Chris Troutner d4df475eee fix(logs): Logging the result of rate limiting 2022-03-28 14:52:43 -07:00
Chris Troutner e7e9723783 Merge pull request #178 from Permissionless-Software-Foundation/ct-unstable
Removing SLPDB and v4 routes
2022-03-13 08:35:07 -07:00
Chris Troutner 51c4272b54 feat(SLPDB): Removing SLPDB routes and tests 2022-03-13 08:33:52 -07:00
Chris Troutner 7e97765e14 feat(ninsight): Removing ninsight library from v5 2022-03-13 08:01:22 -07:00
Chris Troutner 29b51e9437 feat(v4): Deleting all v4 routes and tests 2022-03-13 07:59:42 -07:00
Chris Troutner b472d15848 Changing version to 2.0.0 2022-03-13 07:55:36 -07:00
Chris Troutner f13727094a Merge pull request #177 from Permissionless-Software-Foundation/ct-unstable
fix(PsfSlpIndexer): Adding optional tx history flag to slp/tx endpoint
2022-02-06 13:20:31 -08:00
Chris Troutner 882bb065b3 Fixing typo 2022-02-06 13:04:02 -08:00
Chris Troutner c7fbf5ea9f fix(PsfSlpIndexer): Adding optional tx history flag to slp/tx endpoint 2022-02-06 13:02:16 -08:00
Chris Troutner 6f9f3ee087 Merge pull request #176 from Permissionless-Software-Foundation/ct-unstable
fix(slpdb): Commenting out more failing integration tests
2022-02-01 10:06:53 -08:00
Chris Troutner 1062c50f4f fix(slpdb): Commenting out more failing integration tests 2022-02-01 09:06:10 -08:00
Chris Troutner 578efc1f7b Merge pull request #175 from Permissionless-Software-Foundation/ct-unstable
fix(slpdb): Commenting out more failing integration tests
2022-02-01 07:00:41 -08:00
Chris Troutner c9befa0634 fix(slpdb): Commenting out more failing integration tests 2022-02-01 06:59:15 -08:00
Chris Troutner dd3152b60e Merge pull request #174 from Permissionless-Software-Foundation/ct-unstable
fix(slpdb): Commenting out failing SLPDB integration tests
2022-02-01 06:10:35 -08:00
Chris Troutner cec49a17e9 fix(slpdb): Commenting out failing SLPDB integration tests 2022-02-01 06:09:14 -08:00
Chris Troutner e78cb6b102 Merge pull request #173 from Permissionless-Software-Foundation/ct-unstable
fix(integration tests): Removing two broken integration tests
2022-01-23 07:54:03 -08:00
Chris Troutner ad65ee7791 fix(integration tests): Removing two broken integration tests 2022-01-23 07:52:38 -08:00
Chris Troutner 3a862d7b51 Merge pull request #172 from Permissionless-Software-Foundation/ct-unstable
Updating production script
2022-01-17 17:15:25 -08:00
Chris Troutner 22eb41db13 Making cleanup-images.sh executable 2022-01-17 17:13:32 -08:00
Chris Troutner f80fde9a81 Adding docker cleanup script 2022-01-17 15:29:17 -08:00
Chris Troutner 55cb03b627 Adding v4 fulcrum settings back in 2022-01-17 15:28:28 -08:00
Chris Troutner 47e2d5e739 fix(production): Updating defaults 2022-01-17 15:22:08 -08:00
Chris Troutner 553054e18c Merge pull request #171 from Permissionless-Software-Foundation/dh-endpoints
feat(REST): Added REST API endpoint to bch-api for the psf-slp-indexer
2022-01-16 06:41:33 -08:00
Chris Troutner 3a1ad53a42 Fixing integration tests 2022-01-16 06:39:37 -08:00
Daniel Gonzalez 4255d98c71 Fixed unit tests 2022-01-14 22:29:01 -04:00
Daniel Gonzalez 9aea5533f8 Changed the use for _this 2022-01-14 19:28:44 -04:00
Daniel Gonzalez ea3801165c finished endpoints 2022-01-14 18:32:49 -04:00
Chris Troutner b72ed28d49 Added linting 2022-01-13 09:35:02 -08:00
Daniel Gonzalez cd9f2b5292 feat(REST): Added REST API endpoint to bch-api for the psf-slp-indexer 2022-01-13 13:05:03 -04:00
Chris Troutner 8946b414e8 Merge pull request #170 from Permissionless-Software-Foundation/ct-unstable
Updating linting dependencies. fixing lint errors.
2022-01-08 16:49:21 -08:00
Chris Troutner 6eba44641f fix(psf-slp-indexer): sketching out new library 2022-01-08 16:41:01 -08:00
Chris Troutner cbbb7f76e4 fix(linting): Updating linting dependencies and fixing linting issues 2022-01-08 15:52:43 -08:00
Chris Troutner 7df1a1a1ed fix(bcash): Moved env var check so it does not prevent bch-api from starting 2022-01-08 14:24:35 -08:00
Chris Troutner 1b6bc5acc8 Merge pull request #169 from Permissionless-Software-Foundation/ct-unstable
Increasing timeout when talking to slpdb
2022-01-05 10:08:20 -08:00
Chris Troutner 2e181cf6ee Increasing timeout when talking to slpdb 2022-01-05 10:06:57 -08:00
Chris Troutner 73cfb8c23a Merge pull request #168 from Permissionless-Software-Foundation/ct-unstable
fix(env vars): Adding new env vars to example shell script
2021-11-09 08:10:01 -08:00
Chris Troutner 074610161f fix(env vars): Adding new env vars to example shell script 2021-11-09 08:07:41 -08:00
Chris Troutner 7c177f8013 Updating docker startup script 2021-10-27 08:31:03 -07:00
Chris Troutner 3d6a8e30fd Adding flag to disable rate limits to docker mainnet startup script 2021-10-27 08:09:46 -07:00
Chris Troutner c8fe6f0bfd Merge pull request #165 from Permissionless-Software-Foundation/dh-bcash-getutxos
feat(bcash): Added getUtxos for bcash to bch-api
2021-10-23 09:45:32 -07:00
Daniel Gonzalez 96b5e0e408 feat(bcash): Added getUtxos for bcash to bch-api 2021-10-22 18:53:22 -04:00
Chris Troutner 6ca014e160 Merge pull request #164 from Permissionless-Software-Foundation/ct-unstable
feat(bcash-slp): Created new library for bcash SLP
2021-10-18 05:42:28 -07:00
Chris Troutner 233e210131 fix(bcash): Adding scaffolding for bcash library 2021-10-18 05:40:32 -07:00
Chris Troutner e55d421f13 feat(bcash-slp): Created new library for bcash SLP 2021-10-17 20:31:09 -07:00
Chris Troutner 1b1bf36a89 Merge pull request #163 from Permissionless-Software-Foundation/ct-unstable
fix(Docker): Fixing broken Docker file
2021-10-03 05:38:28 -07:00
Chris Troutner 8de5399818 fix(Docker): Fixing broken Docker file 2021-10-03 05:37:16 -07:00
Chris Troutner 4f4984a982 Merge pull request #162 from Permissionless-Software-Foundation/ct-unstable
feat(getBlockHash): Reimplementing lost getBlockHash method in Blockc…
2021-09-21 07:41:44 -07:00
Chris Troutner d9fc84f136 feat(getBlockHash): Reimplementing lost getBlockHash method in Blockchain lib 2021-09-21 07:39:18 -07:00
Chris Troutner d444666dec Merge pull request #160 from Permissionless-Software-Foundation/ct-unstable
fix(tests): Fixing broken JWT token test
2021-08-16 08:30:31 -07:00
Chris Troutner c8f3649004 fix(tests): Fixing broken JWT token test 2021-08-16 08:26:39 -07:00
Chris Troutner ee876ce8e4 Merge pull request #159 from Permissionless-Software-Foundation/ct-unstable
fix(bch-js): Bumping to v4.20.7
2021-08-09 10:47:44 -07:00
Chris Troutner fbd02802f0 Merge branch 'master' into ct-unstable 2021-08-09 10:45:32 -07:00
Chris Troutner 6e72879103 fix(bch-js): Bumping to v4.20.7 2021-08-09 10:44:43 -07:00
Chris Troutner 2817300ad6 Merge pull request #158 from Permissionless-Software-Foundation/ct-unstable
fix(debug): Removing debugging statements
2021-08-07 12:45:43 -07:00
Chris Troutner 4aa7cad552 fix(debug): Removing debugging statements 2021-08-07 12:44:14 -07:00
Chris Troutner 5a9401aeb1 Merge pull request #157 from Permissionless-Software-Foundation/ct-unstable
fix(jwt): Getting closer to root cause
2021-08-07 12:23:27 -07:00
Chris Troutner 0ae02790a2 fix(jwt): Getting closer to root cause 2021-08-07 12:21:25 -07:00
Chris Troutner 4d3adab9e0 Merge pull request #156 from Permissionless-Software-Foundation/ct-unstable
fix(jwt): A little more jwt debugging
2021-08-07 12:16:21 -07:00
Chris Troutner 91f06b6311 Merge branch 'master' into ct-unstable 2021-08-07 12:14:23 -07:00
Chris Troutner 2fc175d818 fix(jwt): A little more jwt debugging 2021-08-07 12:14:09 -07:00
Chris Troutner 731861cb1d Merge pull request #155 from Permissionless-Software-Foundation/ct-unstable
fix(rate limits): Digging deeper into rate limit debugging
2021-08-07 11:55:28 -07:00
Chris Troutner 9a01f5ffe6 Merge branch 'master' into ct-unstable 2021-08-07 11:45:53 -07:00
Chris Troutner c62c515474 fix(rate limits): Digging deeper into rate limit debugging 2021-08-07 11:45:42 -07:00
Chris Troutner ab198b958e Merge pull request #154 from Permissionless-Software-Foundation/ct-unstable
fix(rate limits): Adding object for debugging rate limit issues
2021-08-07 11:35:29 -07:00
Chris Troutner 2618008247 fix(rate limits): Adding object for debugging rate limit issues 2021-08-07 11:32:04 -07:00
Chris Troutner df27ba2063 Merge pull request #153 from Permissionless-Software-Foundation/ct-unstable
Fixing unit tests
2021-08-03 11:41:02 -07:00
Chris Troutner f19e70ae40 feat(sweep): Removing sweep utility from v5/util route 2021-08-03 11:36:23 -07:00
Chris Troutner 63fa97c998 fix(electrumx): Fixing unit tests for v5 Electrumx route 2021-08-03 11:30:18 -07:00
Chris Troutner 61b1b8ebdf Merge pull request #152 from Permissionless-Software-Foundation/ct-unstable
fix(eCash): Fixing price feed
2021-08-03 09:35:46 -07:00
Chris Troutner de6c19e877 fix(eCash): Fixing price feed 2021-08-03 09:23:10 -07:00
Chris Troutner 66efb76f94 fix(v5): Changed references for v5 route documentation to v5 2021-08-03 09:00:26 -07:00
Chris Troutner 1250a40ff0 Merge pull request #151 from Permissionless-Software-Foundation/ct-unstable
feat(jwt): Created new route for debugging JWT tokens
2021-07-12 10:45:04 -07:00
Chris Troutner 6786ca9ee5 feat(jwt): Created new route for debugging JWT tokens 2021-07-12 10:44:07 -07:00
Chris Troutner e96b9e7a44 Merge pull request #149 from Permissionless-Software-Foundation/ct-unstable
fix(v5 Fulcrum): Requiring FULCRUM_API env var when starting server
2021-06-16 18:48:12 -07:00
Chris Troutner d29f99965f fix(v5 Fulcrum): Requiring FULCRUM_API env var when starting server 2021-06-16 18:33:12 -07:00
Chris Troutner 9825ec1bab Merge pull request #147 from Permissionless-Software-Foundation/dh-v5-dsproof-endpoint
feat(dsproof): Created v5 GET dsproof endpoint
2021-06-09 07:38:37 -07:00
Chris Troutner fb61cfeca3 Merge branch 'master' into dh-v5-dsproof-endpoint 2021-06-09 07:08:52 -07:00
Chris Troutner 5f9a635ab5 Merge pull request #146 from Permissionless-Software-Foundation/dh-v5-fulcrum-unconfirmed
feat(unconfirmed): Created v5 Fulcrum /unconfirmed endpoints
2021-06-09 07:08:16 -07:00
Daniel Gonzalez 41c5504846 feat(dsproof): Created v5 GET dsproof endpoint 2021-06-08 20:45:03 -04:00
Daniel Gonzalez e5f9455fbb feat(unconfirmed): Created v5 Fulcrum /unconfirmed endpoints 2021-06-07 23:39:51 -04:00
Chris Troutner 20cb48caff Merge pull request #145 from Permissionless-Software-Foundation/dh-v5-fulcrum-transactions
feat(txs): Created v5 Fulcrum /transactions endpoints in bch-api
2021-06-05 08:35:26 -07:00
Daniel Gonzalez f241aed5b2 feat(txs): Created v5 Fulcrum /transactions endpoints in bch-api 2021-06-04 21:43:45 -04:00
Chris Troutner 87c2faa6af Merge pull request #144 from Permissionless-Software-Foundation/dh-v5-fulcrum-block
feat(block): Created v5 Fulcrum /block endpoints in bch-api
2021-06-04 08:07:05 -07:00
Daniel Gonzalez 10a840f48d feat(block): Created v5 Fulcrum /block endpoints in bch-api 2021-06-02 23:43:25 -04:00
Chris Troutner 61f9531046 Merge pull request #143 from Permissionless-Software-Foundation/dh-fulcrum-tx-endpoint
feat(tx): Created v5 Fulcrum /tx endpoints in bch-api
2021-06-02 07:10:45 -07:00
Daniel Gonzalez 1ab408a757 feat(tx): Created v5 Fulcrum /tx endpoints in bch-api 2021-06-01 22:20:57 -04:00
Chris Troutner 84df2be3b8 Merge pull request #142 from Permissionless-Software-Foundation/dh-v5-utxo
feat(utxo): Created v5 Fulcrum utxo endpoints in bch-api
2021-05-29 06:51:37 -07:00
Daniel Gonzalez 17376d9dba feat(utxo): Created v5 Fulcrum utxo endpoints in bch-api 2021-05-28 21:12:21 -04:00
Chris Troutner 4289cf4f7c Merge pull request #141 from Permissionless-Software-Foundation/dh-v5-fulcrum
feat(V5): Created v5 Fulcrum POST balance endpoint in bch-api
2021-05-23 07:53:47 -07:00
Chris Troutner f04449be72 fix(electrumx): Fixing context issue around 'this' 2021-05-23 07:52:44 -07:00
Daniel Gonzalez a0f9d065f9 linting 2021-05-22 21:33:16 -04:00
Daniel Gonzalez 20f2e20bcb feat(V5): Created v5 Fulcrum POST balance endpoint in bch-api 2021-05-22 20:55:24 -04:00
Chris Troutner b410b501a1 Merge pull request #140 from Permissionless-Software-Foundation/ct-unstable
fix(startup): Fixing bug in startup
2021-05-19 12:14:22 -07:00
Chris Troutner 31d430e30e fix(startup): Fixing bug in startup 2021-05-19 12:13:11 -07:00
Chris Troutner 82d8e39ca4 Merge pull request #139 from Permissionless-Software-Foundation/ct-unstable
First tests for v5 electrumx route.
2021-05-19 12:04:30 -07:00
Chris Troutner e60f4ff144 fix(v5 electrumx): Added first unit and integration tests for new electrumx route 2021-05-19 12:01:42 -07:00
Chris Troutner 156ea0ef30 v5 tests call v5 routes 2021-05-19 08:01:17 -07:00
Chris Troutner 7ec6435f37 Merge pull request #138 from Permissionless-Software-Foundation/ct-unstable
feat(v5): Adding v5 route
2021-05-19 06:53:53 -07:00
Chris Troutner 6da385c90e feat(v5): Adding v5 route 2021-05-18 22:05:28 -07:00
Chris Troutner 7eca556322 Merge pull request #137 from Permissionless-Software-Foundation/fix-error-return
Fix error response format
2021-05-18 12:41:56 -07:00
Stoyan Zhekov 99a6f88a92 Add error response integration tests 2021-05-16 11:40:17 +09:00
Stoyan Zhekov ef4a3299ed Fix error response format 2021-05-16 11:16:59 +09:00
Chris Troutner be2fd18f5c Merge pull request #136 from Permissionless-Software-Foundation/ct-unstable
fix(getNftGroup): Fixing typo in return value
2021-05-15 11:15:33 -07:00
Chris Troutner cfcbdb0429 fix(getNftGroup): Fixing typo in return value 2021-05-15 11:14:04 -07:00
Chris Troutner 9463afcbc3 Merge pull request #135 from Permissionless-Software-Foundation/ct-unstable
fix(send tx): Fixing bug in the sendTxOptions()
2021-05-13 10:25:15 -07:00
Chris Troutner 13a1e1b1db fix(send tx): Fixing bug in the sendTxOptions() 2021-05-13 10:23:01 -07:00
Chris Troutner 6e19738d1e Merge pull request #134 from Permissionless-Software-Foundation/ct-unstable
feat(send transaction): Allowing specific node for broadcasting txs
2021-05-13 09:38:34 -07:00
Chris Troutner 2a3baf39b9 feat(send transaction): Allowing specific node for broadcasting txs 2021-05-13 09:27:50 -07:00
Chris Troutner d58c98030d Merge pull request #132 from Permissionless-Software-Foundation/nft-get-calls
NFT group and children GET calls + tests
2021-05-09 21:27:01 -07:00
Chris Troutner 5c4f6802b3 Ensured integration tests pass for abc and bchn 2021-05-09 21:23:32 -07:00
Chris Troutner fd51f82dbc Got integration tests in BCH passing 2021-05-09 16:42:41 -07:00
Chris Troutner a77cb3c249 Merge branch 'master' into nft-get-calls 2021-05-09 15:40:38 -07:00
Chris Troutner 015c33faff Merge pull request #133 from Permissionless-Software-Foundation/ct-unstable
Updating example start shell script
2021-05-09 15:39:36 -07:00
Chris Troutner 738c643c1b formatting 2021-05-08 21:12:58 -07:00
Chris Troutner 69ce039882 fix(shell script): Fixing example to use 10000 rate limit points 2021-05-08 19:48:12 -07:00
Chris Troutner 1642603b78 Adding shell-script folder 2021-05-08 19:00:00 -07:00
Stoyan Zhekov 85a170073d NFT group and children GET calls + tests 2021-05-05 12:08:34 +09:00
Chris Troutner a8882bd618 Merge pull request #131 from Permissionless-Software-Foundation/ct-unstable
fix(winston): Switching max files from 5 days to 250 files
2021-04-29 10:25:38 -07:00
Chris Troutner a6b139e0c4 fix(winston): Switching max files from 5 days to 250 files 2021-04-29 10:24:28 -07:00
Chris Troutner 5880f14e07 Adding curl example for jwt token, to README 2021-04-29 08:54:18 -07:00
Chris Troutner 9bf271b4c2 Merge pull request #130 from Permissionless-Software-Foundation/ct-unstable
fix(rate limits): Making internal rate limits an env var
2021-04-20 16:01:45 -07:00
Chris Troutner 82af58e8b3 fix(rate limits): Making internal rate limits an env var 2021-04-20 15:57:37 -07:00
Chris Troutner 6d00681d6e Merge pull request #129 from Permissionless-Software-Foundation/joey
Removing BigNumber reset
2021-04-13 11:07:15 -07:00
Chris Troutner 59043bc154 Updating npm dependencies 2021-04-13 10:59:42 -07:00
Chris Troutner ffb63125e1 Merge pull request #128 from BytesOfMan/remove-bignumber-decimal-mod
URGENT: Remove bignumberjs mod to 8 decimal places
2021-04-13 10:57:46 -07:00
Joey King a3cd832c63 Remove bignumberjs mod to 8 decimal places 2021-04-12 13:27:45 -07:00
Chris Troutner 12d88e8846 Adding info about rate limits. 2021-04-05 12:08:39 -07:00
Chris Troutner 17fe219773 Merge pull request #126 from Permissionless-Software-Foundation/ct-unstable
fix(error handling): Hanling 429 rate limit error better with hyrateU…
2021-04-01 08:47:44 -08:00
Chris Troutner d76c663f25 fix(error handling): Hanling 429 rate limit error better with hyrateUtxos() 2021-04-01 09:46:33 -07:00
Chris Troutner b1655e843e Merge pull request #125 from Permissionless-Software-Foundation/ct-unstable
fix(route-utils): Adding unit tests for route-utils
2021-03-31 04:06:16 -08:00
Chris Troutner af5d44c278 fix(route-utils): Adding unit tests for route-utils 2021-03-31 05:04:35 -07:00
Chris Troutner 0d9caca8b0 Merge pull request #124 from Permissionless-Software-Foundation/ct-unstable
fix(blockbook): Removing final piece of blockbook
2021-03-30 06:53:22 -08:00
Chris Troutner 0cdb9572f1 fix(blockbook): Removing final piece of blockbook 2021-03-30 07:52:02 -07:00
Chris Troutner 8507abd5db Merge pull request #123 from Permissionless-Software-Foundation/dh-apidocs
fix(docs): Fixed bch-api API docs
2021-03-30 06:40:10 -08:00
Daniel Gonzalez fae9a75f6a fix(docs): Fixed bch-api API docs 2021-03-29 21:04:51 -04:00
Chris Troutner fc6da588ee Merge pull request #120 from Permissionless-Software-Foundation/dh-getblock-endpoint
feat(getblock): Added getblock endpoint
2021-03-29 12:17:45 -08:00
Chris Troutner 6f8c00bab9 Making sure integration tests work 2021-03-29 13:08:55 -07:00
Chris Troutner a2ab88adab Merge branch 'master' into dh-getblock-endpoint 2021-03-29 12:13:16 -07:00
Chris Troutner 60528dbd6e Merge pull request #122 from Permissionless-Software-Foundation/timeout
fix(timout): Increasing API timeout from 30 seconds to 5 minutes
2021-03-26 18:11:44 -08:00
Chris Troutner cebf1ea1d6 fix(timout): Increasing API timeout from 30 seconds to 5 minutes 2021-03-26 19:10:33 -07:00
Chris Troutner 11a62af39b Merge pull request #121 from Permissionless-Software-Foundation/ct-unstable
fix(rate limits): Quieting noisy tests
2021-03-26 08:01:45 -07:00
Chris Troutner 83aeabd913 fix(rate limits): Quieting noisy tests 2021-03-26 07:50:41 -07:00
Daniel Gonzalez 7acb778470 feat(getblock): Added getblock endpoint 2021-03-25 17:30:30 -04:00
Chris Troutner 766925c9a8 Merge pull request #119 from Permissionless-Software-Foundation/ct-unstable
Fixing test for BVT
2021-03-16 11:07:00 -07:00
Chris Troutner 51c37e1371 Merge branch 'master' into ct-unstable 2021-03-16 11:03:16 -07:00
Chris Troutner 244acfb26b fix(test): Fixing test for BVT 2021-03-16 11:03:06 -07:00
Chris Troutner d03303728c Merge pull request #118 from Permissionless-Software-Foundation/ct-unstable
fix(test): still trying to fix the same test
2021-03-16 10:33:10 -07:00
Chris Troutner 9fa16aea78 fix(test): still trying to fix the same test 2021-03-16 10:28:38 -07:00
Chris Troutner 29864bcf66 Merge pull request #117 from Permissionless-Software-Foundation/ct-unstable
fix(test): Getting test to pass on BVT
2021-03-16 10:13:06 -07:00
Chris Troutner 03a5d8076e Merge branch 'master' into ct-unstable 2021-03-16 10:10:18 -07:00
Chris Troutner 9d8f214098 fix(test): Getting test to pass on BVT 2021-03-16 10:10:05 -07:00
Chris Troutner 4b68a75a41 Merge pull request #116 from Permissionless-Software-Foundation/ct-unstable
fix(tests): Fixing failing test in BVT
2021-03-16 09:54:41 -07:00
Chris Troutner 901189ae42 fix(tests): Fixing failing test in BVT 2021-03-16 09:53:13 -07:00
Chris Troutner 62bba1d79f Merge pull request #115 from Permissionless-Software-Foundation/ct-unstable
Improved error handling of hydrateUtxos
2021-03-16 09:07:39 -07:00
Chris Troutner 9c735e3c5a Removing more debugging statements 2021-03-16 09:00:47 -07:00
Chris Troutner 62600fdd62 Backing down on some of the debugger statements 2021-03-16 08:56:14 -07:00
Chris Troutner 27e5fb8728 fix(route-utils): Adding handler for nginx 429 error 2021-03-16 08:48:15 -07:00
Chris Troutner 93811ca331 fix(route-utils): Removing old route-utils and using new route-utils 2021-03-16 08:43:43 -07:00
Chris Troutner e37858fd19 debugging 2021-03-11 09:04:26 -08:00
Chris Troutner 3ddbb728b4 debugging 2021-03-11 08:52:02 -08:00
Chris Troutner ef49b85495 debugging 2021-03-11 08:48:22 -08:00
Chris Troutner be6550686f debugging 2021-03-11 08:46:20 -08:00
Chris Troutner 8de2e9e10b debugging 2021-03-11 08:44:24 -08:00
Chris Troutner 5650afb22f fix(decodeError): Adding additional error handling 2021-03-11 08:40:48 -08:00
Chris Troutner 6540171263 Merge pull request #113 from Permissionless-Software-Foundation/ct-unstable
fix(rate limits): Bumping resolution to 10,000 points per minute
2021-03-10 12:04:42 -08:00
Chris Troutner fa1f87c5c8 fix(rate limits): Bumping resolution to 10,000 points per minute 2021-03-10 10:09:18 -08:00
Chris Troutner 13580d1081 Merge pull request #112 from Permissionless-Software-Foundation/ct-new-rate-limits
Major refactor to rate limit handling
2021-03-09 12:52:09 -08:00
Chris Troutner 0aa4d0ecd4 fix(rate limits): Removing old rate limits library 2021-03-09 12:43:30 -08:00
Chris Troutner a475aca8bb Removing temp branch from Dockerfile 2021-03-09 12:22:26 -08:00
Chris Troutner f792c8d90f Temp adding branch to Dockerfile 2021-03-09 09:13:29 -08:00
Chris Troutner ca9dd314dc Removed unnecessary comments 2021-03-09 08:15:26 -08:00
Chris Troutner 00a36ec509 Added hook for whitelist domains 2021-03-09 07:14:38 -08:00
Chris Troutner 2fa911c04e Added env var to turn off rate limits for local installation 2021-03-09 06:47:26 -08:00
Chris Troutner 8345311284 Got 100% test coverage of new rate limits 2021-03-08 19:47:37 -08:00
Chris Troutner 137cacd1da fix(getPublicKey): Switching from GET to POST calls, to pass usrObj 2021-03-08 18:36:27 -08:00
Chris Troutner cf86e390fb Fixing typo in getPublicKey 2021-03-08 18:07:24 -08:00
Chris Troutner ff52ca9418 fix(getPublicKey): Using array for tx to pass usrObj 2021-03-08 18:02:56 -08:00
Chris Troutner 154229d5f5 fix(hydrateUtxosWL): Adding usrObj for rate limit control 2021-03-08 17:51:04 -08:00
Chris Troutner f7125be3c8 fix(bch-js): Bumping to v4.16.1 2021-03-08 17:43:15 -08:00
Chris Troutner d38746dcbd fix(error handling): Replacing improper 429 with 400 2021-03-08 17:03:44 -08:00
Chris Troutner a1ce8bf742 fix(error handling): Replacing improper 429 with 400 2021-03-08 16:55:54 -08:00
Chris Troutner 57a69eee5b Investigating corner case 2021-03-08 14:23:12 -08:00
Chris Troutner 9de3436a61 feat(rate limits): Refactored rate limit middleware 2021-03-08 09:15:41 -08:00
Chris Troutner ddd379b3d5 Removing old rate limit middleware 2021-03-08 07:19:46 -08:00
Chris Troutner 2e1c5a0d61 Finished initial tests, ready for live testing 2021-03-07 17:39:40 -08:00
Chris Troutner 8383818c51 Got some more unit tests nailed down 2021-03-07 15:35:44 -08:00
Chris Troutner 8a28652c4d Adding debugging 2021-03-07 12:55:34 -08:00
Chris Troutner 72ba3c86c7 testing new rate limit middleware 2021-03-07 12:42:10 -08:00
Chris Troutner e6d4bb5443 Got first unit tests for new rate limit middleware 2021-03-07 12:19:30 -08:00
Chris Troutner bcfe503aa6 Got rough workflow sketched out for new rate limit handler 2021-03-07 11:25:50 -08:00
Chris Troutner bb3d020646 fix(local rate limits): Skipping basic auth and rate limits with an env var 2021-03-07 10:01:17 -08:00
Chris Troutner ff5554ab68 Added comments 2021-03-07 09:29:56 -08:00
Chris Troutner 7cafb11c0d Merge pull request #110 from Permissionless-Software-Foundation/dh-price-route
fix(route): Stablished route to get bch usd price
2021-03-04 09:42:36 -08:00
Daniel Gonzalez dce665b174 fix(route): Stablished route to get bch usd price 2021-03-04 13:35:58 -04:00
Chris Troutner 43c1ee9b69 Merge pull request #109 from Permissionless-Software-Foundation/dh-price
feat(price): Added BCH price feed to bch-api from Coinex
2021-03-03 08:26:54 -08:00
Daniel Gonzalez 4b95fa127c feat(price): Added BCH price feed to bch-api from Coinex 2021-03-02 22:02:32 -04:00
Chris Troutner 65314439d7 Merge pull request #108 from Permissionless-Software-Foundation/ct-unstable
fix(hydrateUtxos): Allow users to pass in utxoDelay
2021-03-02 08:55:38 -08:00
Chris Troutner 36a84726af fix(hydrateUtxos): Allow users to pass in utxoDelay 2021-03-02 08:54:07 -08:00
Chris Troutner 78f462c9ec Merge pull request #107 from Permissionless-Software-Foundation/ct-unstable
feat(v3 & blockbook): Deprecating v3 route and blockbook indexer
2021-03-01 15:36:51 -08:00
Chris Troutner b97d94edaf Bumping bch-js to v4.15.19 2021-03-01 11:37:23 -08:00
Chris Troutner b4d29a5856 fix(Util.sweepWif): Replaced blockbook with electrumx 2021-03-01 11:35:07 -08:00
Chris Troutner 64e59f607a Refactored Util route class 2021-03-01 10:29:49 -08:00
Chris Troutner fca0b50a2a Refactored util.js lib 2021-03-01 08:55:16 -08:00
Chris Troutner 47ddc6f186 feat(v3 & blockbook): Deprecating v3 route and blockbook indexer 2021-03-01 08:41:09 -08:00
Chris Troutner 1f1e3c781f Updating to bch-js v4.15.19 2021-03-01 08:28:51 -08:00
Chris Troutner 09f0cb6b6e Merge pull request #106 from Permissionless-Software-Foundation/ct-unstable
Improved rate-limits with bch-js v4.15.18
2021-02-27 16:18:38 -08:00
Chris Troutner fb43b3f390 fix(bch-js): Bumping to v4.15.18 2021-02-27 16:17:28 -08:00
Chris Troutner f76f00d235 Adding decatur script to gitignore 2021-02-27 08:13:44 -08:00
Chris Troutner ac96860c27 Merge pull request #105 from Permissionless-Software-Foundation/ct-unstable
feat(v3 hydrateUxos): Deprecating v3 hydrateUtxos to improve system s…
2021-02-24 18:21:47 -08:00
Chris Troutner c51290acf5 feat(v3 hydrateUxos): Deprecating v3 hydrateUtxos to improve system stability 2021-02-24 18:18:23 -08:00
Chris Troutner 2f520dcaec Merge pull request #104 from Permissionless-Software-Foundation/ct-unstable
fix(bch-js): Bumping to v4.15.8
2021-02-24 18:11:26 -08:00
Chris Troutner e0be1d308a fix(bch-js): Bumping to v4.15.8 2021-02-24 18:09:24 -08:00
Chris Troutner 9cd06630ea Merge pull request #103 from Permissionless-Software-Foundation/ct-unstable
fix(rate limits): Passing JWT token to internal calls
2021-02-24 17:10:17 -08:00
Chris Troutner 6b281e2842 fix(rate limits): Passing JWT token to internal calls 2021-02-24 17:07:24 -08:00
Chris Troutner 2e143d5a9e Merge pull request #102 from Permissionless-Software-Foundation/ct-unstable
More debugging around rate limits
2021-02-24 16:53:40 -08:00
Chris Troutner eb817da044 Merge branch 'master' into ct-unstable 2021-02-24 16:50:50 -08:00
Chris Troutner 1c88aa7d72 fix(bch-js): Bumping to v4.15.7 2021-02-24 16:50:38 -08:00
Chris Troutner a9e976cc24 Merge pull request #101 from Permissionless-Software-Foundation/ct-unstable
fix(bch-js): Upgrading to v4.14.4
2021-02-24 15:24:01 -08:00
Chris Troutner 5644b53cf8 fix(bch-js): Upgrading to v4.14.4 2021-02-24 15:22:54 -08:00
Chris Troutner 236637f0e0 Renaming 'origin' to 'ip' 2021-02-24 15:12:40 -08:00
Chris Troutner fb1bb0b7ed Merge pull request #100 from Permissionless-Software-Foundation/ct-unstable
fix(rate limit): Fine tuning rate limits for internal calls
2021-02-24 14:42:18 -08:00
Chris Troutner 8a91f3ae3a fix(rate limit): Fine tuning rate limits for internal calls 2021-02-24 14:40:16 -08:00
Chris Troutner 80dc296dee Merge pull request #99 from Permissionless-Software-Foundation/ct-unstable
fix(debugging): Adding debugging console.log
2021-02-24 14:29:35 -08:00
Chris Troutner f01c82631b fix(debugging): Adding debugging console.log 2021-02-24 14:26:10 -08:00
Chris Troutner ef2a681edb Merge pull request #98 from Permissionless-Software-Foundation/ct-unstable
fix(rate limits): Testing recursive rate limits
2021-02-24 14:15:49 -08:00
Chris Troutner 44ce5f2aa9 fix(rate limits): Testing recursive rate limits 2021-02-24 14:14:36 -08:00
Chris Troutner f52a04a337 Merge pull request #97 from Permissionless-Software-Foundation/ct-unstable
fix(bodyOrigin): Passing the original origin through the POST body
2021-02-24 13:51:53 -08:00
Chris Troutner 9aa3e5b52a fix(bodyOrigin): Passing the original origin through the POST body 2021-02-24 13:50:38 -08:00
Chris Troutner 71452f4834 Merge pull request #96 from Permissionless-Software-Foundation/ct-unstable
fix(bch-js): Updating to v4.15.2 to test rate limit idea
2021-02-24 13:37:57 -08:00
Chris Troutner 96f1cb6ad4 fix(bch-js): Updating to v4.15.2 to test rate limit idea 2021-02-24 13:36:21 -08:00
Chris Troutner 36dd64db22 Updating docker example shell script 2021-02-11 13:54:00 -08:00
Chris Troutner 05de04a569 Merge branch 'ct-unstable' 2021-02-11 12:56:49 -08:00
Chris Troutner c661799095 Commenting out debugging line 2021-02-11 12:56:35 -08:00
Chris Troutner 4f416d6423 Merge pull request #94 from Permissionless-Software-Foundation/ct-unstable
fix(slp): Fixing typo with whitelist env var
2021-02-11 12:51:46 -08:00
Chris Troutner 58dc4da92c fix(slp): Fixing typo with whitelist env var 2021-02-11 12:50:39 -08:00
Chris Troutner 7b8710d3da Merge pull request #93 from Permissionless-Software-Foundation/ct-unstable
feat(SLPDB): Adding separate passwords for GP and WL SLPDBs
2021-02-11 11:26:34 -08:00
Chris Troutner 7976909dbb feat(SLPDB): Adding separate passwords for GP and WL SLPDBs 2021-02-11 11:25:25 -08:00
Chris Troutner 24b85e410d Adding script to gitignore 2021-02-11 10:10:23 -08:00
Chris Troutner ae13d2c42e Merge pull request #92 from Permissionless-Software-Foundation/ct-unstable
fix(CD): Testing continuous deployment
2021-02-10 13:56:25 -08:00
Chris Troutner bf0fc2437a fix(CD): Testing continuous deployment 2021-02-10 13:54:16 -08:00
Chris Troutner 88d3dd12a1 Merge pull request #91 from Permissionless-Software-Foundation/ct-unstable
fix(bch-js): Bumping to v4.11.1
2021-02-09 15:17:24 -08:00
Chris Troutner cb8b4a5586 fix(bch-js): Bumping to v4.11.1 2021-02-09 15:15:35 -08:00
Chris Troutner ec0ef24508 fix(comments): Commenting out debug info 2021-02-09 15:07:52 -08:00
Chris Troutner 5a16e69719 Merge pull request #90 from Permissionless-Software-Foundation/ct-unstable
feat(hydrateUtxosWL): SLPDB with whitelist filter used to hydrate UTXOs
2021-02-09 12:20:11 -08:00
Chris Troutner 154dbe8151 feat(hydrateUtxosWL): SLPDB with whitelist filter used to hydrate UTXOs 2021-02-09 12:18:52 -08:00
Chris Troutner 785d3059c3 Merge pull request #89 from Permissionless-Software-Foundation/joey
fix(logging): Adding env vars for configuring logging
2021-01-13 09:27:55 -08:00
Chris Troutner 21b19b59c9 fix(logging): Adding env vars for configuring logging 2021-01-13 09:26:03 -08:00
Chris Troutner 588feff5db Added minor comments 2021-01-13 09:24:28 -08:00
Chris Troutner 9fafc6fba3 Merge pull request #88 from josephroyking/logging-env-var
Add logging storage params as env variables
2021-01-13 09:19:34 -08:00
Joey King 8412da15db Add logging storage params as env variables 2021-01-13 07:51:08 -06:00
Chris Troutner 54c299e7fe Merge pull request #87 from Permissionless-Software-Foundation/ct-unstable
Updating README, shell scripts, and dependencies.
2021-01-11 16:37:10 -08:00
Chris Troutner a06399cfda fix(dependencies): Removed unneeded dependencies. 2021-01-11 16:35:41 -08:00
Chris Troutner e49c488e06 Removing unneeded dependencies 2021-01-11 16:33:49 -08:00
Chris Troutner be07ffab33 Removing unneeded dependencies 2021-01-11 16:29:46 -08:00
Chris Troutner 41a5b24faf Updating README and shell script examples 2021-01-11 16:22:27 -08:00
Chris Troutner 2172d0feff Updating example shell script 2021-01-11 15:33:45 -08:00
Chris Troutner c4664ca173 Merge pull request #86 from Permissionless-Software-Foundation/ct-unstable
fix(bch-js): Bumped to v4.5.4
2021-01-10 15:40:28 -08:00
Chris Troutner 11a7915cb8 fix(bch-js): Bumped to v4.5.4 2021-01-10 15:37:15 -08:00
145 changed files with 30144 additions and 39306 deletions
+4 -1
View File
@@ -28,6 +28,9 @@ test-fullstack-abc.sh
test-fullstack-bchn.sh test-fullstack-bchn.sh
start-fullstack-abc.sh start-fullstack-abc.sh
start-fullstack-bchn.sh start-fullstack-bchn.sh
start-ss-main.sh
start-decatur-bchn.sh
shell-scripts/
coverage coverage
start-my-infra start-my-infra
@@ -39,6 +42,6 @@ logs/*
docs/* docs/*
docker/mainnet/start-local-mainnet.sh docker/mainnet/start-local-mainnet.sh
docker/testnet/start-local-testnet.sh docker/testnet/start-local-testnet.sh
start-local-bchn.sh
!logs/README.md !logs/README.md
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020 Chris Troutner Copyright (c) 2022 Permissionless Software Foundation developers
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+62 -34
View File
@@ -1,47 +1,43 @@
# bch-api # bch-api
This is a node.js JavaScript REST API server using Express.js. The purpose of this code is to create a REST API server that provides a common interface for working with a Bitcoin Cash full node and various indexers. See [this article](https://troutsblog.com/research/bitcoin-cash/how-to-bch-full-stack-developer) to learn about the 'Cash Stack'. Visit [FullStack.cash](https://fullstack.cash), sign up for a free account, and use this REST API right away with the [bch-js](https://github.com/Permissionless-Software-Foundation/bch-js) JavaScript library. [![License](https://img.shields.io/npm/l/@psf/bch-js)](https://github.com/Permissionless-Software-Foundation/bch-js/blob/master/LICENSE.md)
[![js-standard-style](https://img.shields.io/badge/javascript-standard%20code%20style-green.svg?style=flat-square)](https://github.com/feross/standard)
This repository is intended to be paired with [bch-js](https://github.com/Permissionless-Software-Foundation/bch-js), an npm JavaScript library for building Bitcoin Cash apps. This is a REST API server, written in node.js JavaScript, using the [Express.js](https://expressjs.com/) framework. The purpose of this code is to create a REST API server that provides a common interface for developers to build blockchain-based business applications. This software works with the Bitcoin Cash (BCH) and eCash (XEC) blockchains.
- High-level documentation is available on [CashStack.info](https://cashstack.info).
![Cash Stack Network Diagram](./generic-network-diagram.png) While developers are encouraged to run and manage their own infrastructure, cloud-based infrastructure is available via [FullStack.cash](https://fullstack.cash). Free and paid tiers are available. You can use this REST API right away with the [bch-js](https://github.com/Permissionless-Software-Foundation/bch-js) JavaScript library.
Both bch-api and bch-js are part of the 'Cash Stack' [full stack of BCH software](https://troutsblog.com/research/bitcoin-cash/how-to-bch-full-stack-developer). This repository is intended to be paired with [bch-js](https://github.com/Permissionless-Software-Foundation/bch-js), an npm JavaScript library for building Bitcoin Cash apps. [Code examples](https://github.com/Permissionless-Software-Foundation/bch-js-examples) for common use-cases are available, and the [minimal-slp-wallet](https://www.npmjs.com/package/minimal-slp-wallet) is a front end JavaScript library that incorporates bch-js and provides basic wallet functionality.
- [API Documentation](https://fullstack.cash/documentation) ![bch-api dependency diagram](./diagrams/bch-api-dependency-graph.png)
- [Example Code](https://fullstack.cash/examples)
## Documentation
All the software above is part of the [Cash Stack](https://cashstack.info).
- [CashStack](https://cashstack.info) high-level documentation.
- [bch-api REST API reference](https://api.fullstack.cash/docs/)
- [bch-js JavaScript API reference](https://bchjs.fullstack.cash/)
- [bch-js example code](https://github.com/Permissionless-Software-Foundation/bch-js-examples)
- [Additional documentation on FullStack.cash](https://fullstack.cash/documentation)
- [Additional examples on FullStack.cash](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) - [Telegram channel](https://t.me/bch_js_toolkit)
## Features
The following features set this repository apart from rest.bitcoin.com:
- Fine grain access is controlled with a JWT token using
[this back end auth server](https://github.com/Permissionless-Software-Foundation/jwt-bch-api) and [this front end](https://github.com/Permissionless-Software-Foundation/jwt-bch-frontend). Can be used to monetize access to the REST API.
- Typescript removed and ES8 JavaScript used instead.
- npm audit run on all dependencies.
## Live Demo
You can test a live demo of the REST API by running the
[bch-js examples](https://github.com/Permissionless-Software-Foundation/bch-js-examples).
Rate limits are 20 requests per minute, but you can increase them to 100 with a [paid account](https://fullstack.cash/pricing).
You can bootstrap your own REST API server by downloading and installing the infrastructure listed on the [CashStrap](https://fullstack.cash/cashstrap) page.
### Configure bch-js ### Configure bch-js
The live servers can be used by [bch-js](https://github.com/Permissionless-Software-Foundation/bch-js) by settings the `restURL` config property to one of these servers: The live servers can be used by [bch-js](https://github.com/Permissionless-Software-Foundation/bch-js) by settings the `restURL` config property to one of these servers:
- BCHN Mainnet REST API server: https://bchn.fullstack.cash/v3/ - BCH Mainnet REST API server: https://bchn.fullstack.cash/v5/
- ABC Mainnet REST API server: https://abc.fullstack.cash/v3/ - eCash Mainnet REST API server: https://abc.fullstack.cash/v5/
- Testnet3 REST API server: https://testnet3.fullstack.cash/v3/
- Check server status: https://metrics.fullstack.cash - Check server status: https://metrics.fullstack.cash
## Installation ## Installation
There are two installation paths, depending if you want a *development* or There are two installation paths, depending if you want a *development* or
*production* environment. You'll also need to set up the underlying infrastructure *production* environment. You'll also need to set up the underlying infrastructure
described on [this page](https://troutsblog.com/research/bitcoin-cash/how-to-bch-full-stack-developer). described on [CashStack.info](https://cashstack.info).
This code targets the Ubuntu 18.04 LTS Linux OS. This hardware target for this software is a PC running Ubuntu 18.04 LTS Linux OS or higher.
### Development ### Development
This is a standard node.js project. The installation is as follows: This is a standard node.js project. The installation is as follows:
@@ -66,7 +62,7 @@ For a production environment, a Docker container is provided in the
containers target the Ubuntu 18.04 LTS Linux OS. containers target the Ubuntu 18.04 LTS Linux OS.
- Install Docker and Docker Compose by following the commands on - Install Docker and Docker Compose by following the commands on
[this Dev Ops page](https://troutsblog.com/research/dev-ops/overview). [this Dev Ops page](https://bafybeidpuq6pgpryd22wwykhqh2etw44pzofuncm5ldczk5v733tkwk55y.ipfs.dweb.link/docs/dev-ops/overview).
- Customize the [bash script](docker/mainnet/start-local-mainnet.sh) for your - Customize the [bash script](docker/mainnet/start-local-mainnet.sh) for your
installation. installation.
@@ -77,16 +73,48 @@ installation.
- Run the Docker container with: - Run the Docker container with:
`docker-compose up` `docker-compose up -d`
## Support ## Rate Limits
Have questions? Need help? Join our community support Rate limiting of the REST API can be controlled in three different ways:
[Telegram channel](https://t.me/bch_js_toolkit) - JWT tokens for individual users
- Basic Authentication tokens for organization-wide usage.
- Whitelisting for specific domain names
## IPFS ### JWT Tokens
Copies of this repository will occasionally be uploaded and hosted on [IPFS](https://ipfs.io): The rate limits for [api.fullstack.cash](https://api.fullstack.cash) are controlled by a JWT token. You can increase your rate limits by [purchasing a JWT token](https://fullstack.cash). If you're using bch-js, [check the readme for instructions on increasing rate limits](https://github.com/Permissionless-Software-Foundation/bch-js#api-key). For interacting with bch-api directly (without bch-js), you can then include the JWT token in the HTTP header like this:
- v1.15.4: QmSAerFmDJm6KQp2HC8jsRR5MdL5X39hoUsJYvWNDvrD8m - `Authorization: Token <JWT token>`
### cURL example
Here is an example using curl for using the JWT token when making a call to the REST API:
```
export BCHJSTOKEN=eyJhbGciOiJ....
curl "https://bchn.fullstack.cash/v4/electrumx/balance/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -X GET -H "Content-Type: application/json" -H "Authorization: Token ${BCHJSTOKEN}"
```
### Basic Authentication
For private installations, Basic Authentication can be used to give everyone in an organization access to higher rate limits, while restricting use by the public. More than one Basic Authentication password can be generated by using the colon `:` to separate entries in the `PRO_PASS` environment variable. See the [bash script](docker/mainnet/start-local-mainnet.sh) for an example.
### Whitelists
Website domain names can be added to the `WHITELIST_DOMAINS` environment variable. Multiple domains can be separated with a comma `,`. Any web app making a request from that domain will be granted top-level rate limits. See the [bash script](docker/mainnet/start-local-mainnet.sh) for an example.
## Donate
This open source software is developed and maintained by the [Permissionless Software Foundation](https://psfoundation.cash). If this library provides value to you, please consider making a donation to support the PSF developers:
<div align="center">
<img src="./diagrams/donation-qr.png" />
<p>bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr</p>
</div>
## Censorship Resistance
Copies of this repository will occasionally be uploaded and hosted on the Filecoin blockchain, [IPFS](https://ipfs.io), and the [Radicle](https://radicle.xyz) p2p network.
- v1.25.1: [QmXFzFJHenymhReDc9oGAxbpmvK2fJYnisQ2pJn3gGyKKX](https://ipfs.io/ipfs/QmXFzFJHenymhReDc9oGAxbpmvK2fJYnisQ2pJn3gGyKKX)
## License ## License
[MIT](./LICENSE.md) [MIT](./LICENSE.md)
+3
View File
@@ -0,0 +1,3 @@
{
"sampleUrl": null
}
+13 -3
View File
@@ -10,13 +10,23 @@ const config = {
: 'secret-jwt-token', : 'secret-jwt-token',
// Rate Limits // Rate Limits
anonRateLimit: process.env.ANON_RATE_LIMIT ? process.env.ANON_RATE_LIMIT : 50, anonRateLimit: process.env.ANON_RATE_LIMIT
? Number(process.env.ANON_RATE_LIMIT)
: 1000,
whitelistRateLimit: process.env.WHITELIST_RATE_LIMIT whitelistRateLimit: process.env.WHITELIST_RATE_LIMIT
? process.env.WHITELIST_RATE_LIMIT ? Number(process.env.WHITELIST_RATE_LIMIT)
: 10, : 10,
internalRateLimit: process.env.INTERNAL_RATE_LIMIT
? Number(process.env.INTERNAL_RATE_LIMIT)
: 10,
pointsPerMinute: process.env.POINTS_PER_MINUTE
? Number(process.env.POINTS_PER_MINUTE)
: 10000,
whitelistDomains: process.env.WHITELIST_DOMAINS whitelistDomains: process.env.WHITELIST_DOMAINS
? process.env.WHITELIST_DOMAINS.split(',') ? process.env.WHITELIST_DOMAINS.split(',')
: ['fullstack.cash', 'psfoundation.cash'] : ['fullstack.cash', 'psfoundation.cash', '10.0.'],
restURL: process.env.LOCAL_RESTURL ? process.env.LOCAL_RESTURL : 'http://127.0.0.1:3000/v5/'
} }
module.exports = config module.exports = config
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

+36 -6
View File
@@ -1,23 +1,53 @@
FROM christroutner/ct-base-ubuntu FROM ubuntu:20.04
MAINTAINER Chris Troutner <chris.troutner@gmail.com> MAINTAINER Chris Troutner <chris.troutner@gmail.com>
RUN apt-get update -y #Update the OS and install any OS packages needed.
RUN apt-get update
RUN apt-get install -y sudo git curl nano gnupg wget
#Install Node and NPM
RUN curl -sL https://deb.nodesource.com/setup_14.x -o nodesource_setup.sh
RUN bash nodesource_setup.sh
RUN apt-get install -y nodejs build-essential
#Create the user 'safeuser' and add them to the sudo group.
RUN useradd -ms /bin/bash safeuser
RUN adduser safeuser sudo
#Set password to 'abcd8765' change value below if you want a different password
RUN echo safeuser:abcd8765 | chpasswd
#Set the working directory to be the users home directory
WORKDIR /home/safeuser
#Setup NPM for non-root global install (like on a mac)
RUN mkdir /home/safeuser/.npm-global
RUN chown -R safeuser .npm-global
RUN echo "export PATH=~/.npm-global/bin:$PATH" >> /home/safeuser/.profile
RUN runuser -l safeuser -c "npm config set prefix '~/.npm-global'"
# Update to the latest version of npm.
# Working with npm@7.21.1
RUN npm install -g npm
#FROM christroutner/ct-base-ubuntu
#MAINTAINER Chris Troutner <chris.troutner@gmail.com>
#RUN apt-get update -y
#Set the working directory to be the home directory #Set the working directory to be the home directory
WORKDIR /home/safeuser #WORKDIR /home/safeuser
# Switch to user account. # Switch to user account.
USER safeuser USER safeuser
# Prep 'sudo' commands. # Prep 'sudo' commands.
#RUN echo 'abcd8765' | sudo -S pwd #RUN echo 'abcd8765' | sudo -S pwd
# Install Redis. TODO: Make this a separate container.
# Clone the repository # Clone the repository
WORKDIR /home/safeuser WORKDIR /home/safeuser
RUN git clone https://github.com/Permissionless-Software-Foundation/bch-api RUN git clone https://github.com/Permissionless-Software-Foundation/bch-api
WORKDIR /home/safeuser/bch-api WORKDIR /home/safeuser/bch-api
RUN git checkout ct-unstable
RUN npm install --silent RUN npm install --silent
# Generate documentation # Generate documentation
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# Remove all untagged docker images.
docker rmi $(docker images | grep "^<none>" | awk '{print $3}')
+17 -3
View File
@@ -1,5 +1,9 @@
bch-api-main: version: '3.9'
build: .
services:
bch-api-main:
build:
context: ./
dockerfile: Dockerfile dockerfile: Dockerfile
#image: bitcore #image: bitcore
container_name: bch-api-main container_name: bch-api-main
@@ -9,11 +13,21 @@ bch-api-main:
- "3000:3000" - "3000:3000"
volumes: volumes:
- ../../logs:/home/safeuser/bch-api/logs - ../../logs:/home/safeuser/bch-api/logs
logging:
driver: 'json-file'
options:
max-size: '10m'
max-file: '10'
restart: always restart: always
redis-main: redis-main:
image: redis:latest image: redis:latest
container_name: redis-main container_name: redis-main
logging:
driver: 'json-file'
options:
max-size: '10m'
max-file: '10'
restart: always restart: always
ports: ports:
- "172.17.0.1:6379:6379" - "172.17.0.1:6379:6379"
+50 -12
View File
@@ -1,37 +1,75 @@
#!/bin/bash #!/bin/bash
# test # Docker containers for the below infrastructure is described here:
# https://psfoundation.cash/blog/cash-stack
# Pre-synced databases can be downloaded on the CashStrap page:
# https://fullstack.cash/cashstrap
# Which network are you using?
export NETWORK=mainnet
#export NETWORK=testnet
# Full node # Full node
export RPC_BASEURL=http://172.17.0.1:8332/ export RPC_IP=172.17.0.1:8332
export RPC_BASEURL=http://$RPC_IP/
export RPC_USERNAME=bitcoin export RPC_USERNAME=bitcoin
export RPC_PASSWORD=password export RPC_PASSWORD=password
export NETWORK=mainnet
# SLPDB # SLPDB
export SLPDB_PASS_GP=somelongpassword
export SLPDB_URL=http://172.17.0.1:12300/ export SLPDB_URL=http://172.17.0.1:12300/
export SLPDB_PASS=somelongpassword # Use the same address as SLPDB_URL if you don't have a separate whitelist server.
export SLPDB_PASS_WL=somelongpassword
# Blockbook Indexer export SLPDB_WHITELIST_URL=http://172.17.0.1:12300/
export BLOCKBOOK_URL=https://172.17.0.1:9131/ # slp-api alternative SLP validator using slp-validate:
# Allow node.js to make network calls to https using self-signed certificate. # https://github.com/Permissionless-Software-Foundation/slp-api
export NODE_TLS_REJECT_UNAUTHORIZED=0 export SLP_API_URL=http://10.0.0.5:5001/
# Mainnet Fulcrum / ElectrumX # Mainnet Fulcrum / ElectrumX
export FULCRUM_URL=172.17.0.1 export FULCRUM_URL=172.17.0.1
export FULCRUM_PORT=50002 export FULCRUM_PORT=50002
export FULCRUM_API=http://172.17.0.1:3001/v1/
# Redis DB - Used for rate limiting # Redis DB - Used for rate limiting
export REDIS_PORT=6379 export REDIS_PORT=6379
export REDIS_HOST=172.17.0.1 export REDIS_HOST=172.17.0.1
# JWT Token Secret # JWT Token Secret
# This is used to verify JWT tokens generated with jwt-bch-api:
# https://github.com/Permissionless-Software-Foundation/jwt-bch-api
export TOKENSECRET=somelongsecretvalue export TOKENSECRET=somelongsecretvalue
# So that bch-api can call bch-js locally. # So that bch-api can call bch-js locally.
export LOCAL_RESTURL=http://127.0.0.1:3000/v3/ export LOCAL_RESTURL=http://127.0.0.1:3000/v5/
# slp-api alternative SLP validator. # Basic Authentication password
export SLP_API_URL=http://10.0.0.5:5001/ export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
# Whitelisted domains. Automatically give pro-tier rate limit access to apps
# that originate froma domain on the whitelist.
export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash
# Disable rate limits
export DO_NOT_USE_RATE_LIMITS=1
# Rate Limits. Numbers are divided into 1000. e.g. 1000 / 50 = 20 RPM for ANON.
# Requests use the ANON rate limit if they fail to pass in a JWT token.
# ANON = 20 requests per minute (RPM)
export ANON_RATE_LIMIT=50
# 10 = 100 RPM
export WHITELIST_RATE_LIMIT=10
# Set logging parameters
#1m means no more than 1 megabyte
export LOG_MAX_SIZE=1m
#5d means store no more than 5 days
export LOG_MAX_FILES=5d
# (Optional) bcash full node
export BCASH_SERVER=http://localhost:3002/
# psf-slp-indexer
#export SLP_INDEXER_API=https://psf-slp-indexer.fullstack.cash/
export SLP_INDEXER_API=http://172.17.0.1:5010/
npm start npm start
+39 -12
View File
@@ -1,34 +1,61 @@
#!/bin/bash #!/bin/bash
# Docker containers for the below infrastructure is described here:
# https://psfoundation.cash/blog/cash-stack
# Pre-synced databases can be downloaded on the CashStrap page:
# https://fullstack.cash/cashstrap
# Which network are you using?
#export NETWORK=mainnet
export NETWORK=testnet
# Full node # Full node
export RPC_BASEURL=http://172.17.0.1:18332/ export RPC_IP=172.17.0.1:18332
export RPC_BASEURL=http://$RPC_IP/
export RPC_USERNAME=bitcoin export RPC_USERNAME=bitcoin
export RPC_PASSWORD=password export RPC_PASSWORD=password
export NETWORK=testnet
# SLPDB # SLPDB
export SLPDB_URL=http://172.17.0.1:13300/ export SLPDB_URL=http://172.17.0.1:13300/
export SLPDB_PASS=portlandisacityinoregon
# Blockbook Indexer export SLPDB_PASS_GP=portlandisacityinoregon
export BLOCKBOOK_URL=https://172.17.0.1:19131/ export SLPDB_PASS_WL=portlandisacityinoregon
# Allow node.js to make network calls to https using self-signed certificate. # Use the same address as SLPDB_URL if you don't have a separate whitelist server.
export NODE_TLS_REJECT_UNAUTHORIZED=0 export SLPDB_WHITELIST_URL=http://172.17.0.1:13300/
# slp-api alternative SLP validator using slp-validate:
# https://github.com/Permissionless-Software-Foundation/slp-api
export SLP_API_URL=https://slpapi-testnet3.fullstackslp.nl/
# Mainnet Fulcrum / ElectrumX # Mainnet Fulcrum / ElectrumX
export FULCRUM_URL=172.17.0.1 export FULCRUM_URL=172.17.0.1
export FULCRUM_PORT=50002 export FULCRUM_PORT=60002
export FULCRUM_API=http://172.17.0.1:3000/v1/
# Redis DB # Redis DB
export REDIS_PORT=6380 export REDIS_PORT=6380
export REDIS_HOST=172.17.0.1 export REDIS_HOST=172.17.0.1
# JWT Token Secret # JWT Token Secret
export TOKENSECRET=somelongsecretvalue export TOKENSECRET=fridayharbor
# So that bch-api can call bch-js locally. # So that bch-api can call bch-js locally.
export LOCAL_RESTURL=http://127.0.0.1:3000/v3/ export LOCAL_RESTURL=http://127.0.0.1:3000/v4/
# slp-api alternative SLP validator. # Basic Authentication password (optional)
export SLP_API_URL=http://10.0.0.5:5001/ export PRO_PASS=thejaasjhehrakseuhakuhr
# Whitelisted domains. Automatically give pro-tier rate limit access to apps
# that originate froma domain on the whitelist.
export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash
# Rate Limits. Numbers are divided into 1000. e.g. 1000 / 50 = 20 RPM for ANON.
# Requests use the ANON rate limit if they fail to pass in a JWT token.
# ANON = 20 requests per minute (RPM)
export ANON_RATE_LIMIT=50
# 10 = 100 RPM
export WHITELIST_RATE_LIMIT=1
export INTERNAL_RATE_LIMIT=100
npm start npm start
+20366 -8621
View File
File diff suppressed because it is too large Load Diff
+23 -28
View File
@@ -1,6 +1,6 @@
{ {
"name": "bch-api", "name": "bch-api",
"version": "1.16.0", "version": "2.0.0",
"description": "The REST API powering FullStack.cash", "description": "The REST API powering FullStack.cash",
"author": "Chris Troutner <chris.troutner@gmail.com>", "author": "Chris Troutner <chris.troutner@gmail.com>",
"contributors": [ "contributors": [
@@ -14,23 +14,25 @@
"scripts": { "scripts": {
"start": "node ./src/app.js", "start": "node ./src/app.js",
"dev": "nodemon ./dist/app.js", "dev": "nodemon ./dist/app.js",
"test": "npm run lint && npm run test-v4", "test": "npm run lint && npm run test-v5",
"lint": "standard --env mocha --fix", "lint": "standard --env mocha --fix",
"test-v4": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v4/", "test-v4": "export NETWORK=mainnet && nyc --reporter=text mocha --exit --timeout 60000 test/v4/",
"test:integration": "mocha test/v4/integration", "test-v5": "export NETWORK=mainnet && nyc --reporter=text mocha --exit --timeout 60000 test/v5/",
"test:integration:slpdb": "mocha --timeout 25000 -g '#validate2Single' test/v4/integration/slp*.js", "test:integration": "mocha test/v5/integration",
"test:integration:slpdb": "mocha --timeout 25000 -g '#validate2Single' test/v5/integration/slp*.js",
"test:integration:nft": "mocha --timeout 25000 -g '#nft' test/v5/integration/nft.js",
"coverage": "nyc report --reporter=text-lcov | coveralls", "coverage": "nyc report --reporter=text-lcov | coveralls",
"coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v4/", "coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v4/",
"docs": "./node_modules/.bin/apidoc -i src/routes/v4 -o docs", "docs": "./node_modules/.bin/apidoc -i src/routes/v5 -o docs",
"test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha --timeout 25000 -g '#generateSendOpReturn()' test/v4/integration/", "test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha -g '#utxosBulk' --exit --timeout 30000 test/v5/",
"test:temp2": "mocha test/v4/rate-limits.js" "test:temp2": "export SLP_INDEXER_API=http://fakeurl/api/ && export NETWORK=mainnet && mocha --exit --timeout 30000 test/v5/psf-slp-indexer"
}, },
"engines": { "engines": {
"node": ">=10.15.1" "node": ">=10.15.1"
}, },
"dependencies": { "dependencies": {
"@psf/bch-js": "^4.5.2", "@psf/bch-js": "6.3.4",
"apidoc": "^0.23.0", "apidoc": "^0.26.0",
"axios": "^0.21.1", "axios": "^0.21.1",
"bitcore-lib-cash": "^8.23.1", "bitcore-lib-cash": "^8.23.1",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
@@ -45,7 +47,6 @@
"helmet": "^3.21.2", "helmet": "^3.21.2",
"ioredis": "^4.14.1", "ioredis": "^4.14.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"level": "^6.0.0",
"mkdirp": "^1.0.0", "mkdirp": "^1.0.0",
"mocha": "^7.1.1", "mocha": "^7.1.1",
"morgan": "^1.9.1", "morgan": "^1.9.1",
@@ -53,8 +54,6 @@
"passport": "^0.4.0", "passport": "^0.4.0",
"passport-anonymous": "^1.0.1", "passport-anonymous": "^1.0.1",
"passport-http": "^0.3.0", "passport-http": "^0.3.0",
"pg": "^8.0.0",
"pg-hstore": "^2.3.2",
"rate-limiter-flexible": "^2.0.0", "rate-limiter-flexible": "^2.0.0",
"winston": "^3.2.1", "winston": "^3.2.1",
"winston-daily-rotate-file": "^4.0.0" "winston-daily-rotate-file": "^4.0.0"
@@ -63,23 +62,19 @@
"bignumber.js": "^9.0.0", "bignumber.js": "^9.0.0",
"chai": "^4.1.2", "chai": "^4.1.2",
"coveralls": "^3.0.2", "coveralls": "^3.0.2",
"eslint": "5.16.0", "eslint": "7.17.0",
"eslint-config-prettier": "^6.0.0", "eslint-config-prettier": "7.1.0",
"eslint-config-standard": "^13.0.1", "eslint-config-standard": "16.0.2",
"eslint-plugin-prettier": "^3.1.0", "eslint-plugin-node": "11.1.0",
"eslint-plugin-standard": "^4.0.0", "eslint-plugin-prettier": "3.3.1",
"eslint-plugin-standard": "4.0.0",
"fs-extra": "^9.0.0", "fs-extra": "^9.0.0",
"nock": "^12.0.0", "lodash.clonedeep": "^4.5.0",
"node-mocks-http": "^1.7.0", "nock": "^13.0.5",
"nyc": "^15.0.0", "nyc": "^15.0.0",
"prettier": "^2.0.0", "semantic-release": "^17.4.2",
"proxyquire": "^2.1.0",
"request": "^2.88.0",
"request-promise": "^4.2.2",
"semantic-release": "^17.3.0",
"sinon": "^9.0.0", "sinon": "^9.0.0",
"standard": "^14.3.1", "standard": "^16.0.4"
"typescript": "^3.1.4"
}, },
"release": { "release": {
"publish": [ "publish": [
@@ -91,6 +86,6 @@
}, },
"apidoc": { "apidoc": {
"title": "bch-api", "title": "bch-api",
"url": "https://api.fullstack.cash/v4" "url": "https://api.fullstack.cash/v5"
} }
} }
+89 -112
View File
@@ -3,7 +3,6 @@
const express = require('express') const express = require('express')
// Middleware // Middleware
// const { routeRateLimit } = require("./middleware/route-ratelimit")
const RateLimits = require('./middleware/route-ratelimit') const RateLimits = require('./middleware/route-ratelimit')
const rateLimits = new RateLimits() const rateLimits = new RateLimits()
@@ -25,63 +24,40 @@ const jwtAuth = require('./middleware/jwt-auth')
// Logging for API requests. // Logging for API requests.
const logReqInfo = require('./middleware/req-logging') const logReqInfo = require('./middleware/req-logging')
// v3 // v5
const healthCheckV3 = require('./routes/v3/health-check') const healthCheckV5 = require('./routes/v5/health-check')
const BlockchainV3 = require('./routes/v3/full-node/blockchain') const BlockchainV5 = require('./routes/v5/full-node/blockchain')
const ControlV3 = require('./routes/v3/full-node/control') const ControlV5 = require('./routes/v5/full-node/control')
const MiningV3 = require('./routes/v3/full-node/mining') const MiningV5 = require('./routes/v5/full-node/mining')
const networkV3 = require('./routes/v3/full-node/network') const networkV5 = require('./routes/v5/full-node/network')
const RawtransactionsV3 = require('./routes/v3/full-node/rawtransactions') const RawtransactionsV5 = require('./routes/v5/full-node/rawtransactions')
const utilV3 = require('./routes/v3/util') const DSProofV5 = require('./routes/v5/full-node/dsproof')
const SlpV3 = require('./routes/v3/slp') const UtilV5 = require('./routes/v5/util')
const xpubV3 = require('./routes/v3/xpub') const SlpV5 = require('./routes/v5/slp')
const BlockbookV3 = require('./routes/v3/blockbook') const xpubV5 = require('./routes/v5/xpub')
const Ninsight = require('./routes/v3/ninsight') const ElectrumXV5 = require('./routes/v5/electrumx')
const ElectrumXV3 = require('./routes/v3/electrumx') const EncryptionV5 = require('./routes/v5/encryption')
const EncryptionV3 = require('./routes/v3/encryption') const PriceV5 = require('./routes/v5/price')
const PriceV3 = require('./routes/v3/price') const JWTV5 = require('./routes/v5/jwt')
const BcashSLP = require('./routes/v5/bcash/slp')
// v4 const PsfSlpIndexer = require('./routes/v5/psf-slp-indexer')
const healthCheckV4 = require('./routes/v4/health-check')
const BlockchainV4 = require('./routes/v4/full-node/blockchain')
const ControlV4 = require('./routes/v4/full-node/control')
const MiningV4 = require('./routes/v4/full-node/mining')
const networkV4 = require('./routes/v4/full-node/network')
const RawtransactionsV4 = require('./routes/v4/full-node/rawtransactions')
const utilV4 = require('./routes/v4/util')
const SlpV4 = require('./routes/v4/slp')
const xpubV4 = require('./routes/v4/xpub')
const BlockbookV4 = require('./routes/v4/blockbook')
const ElectrumXV4 = require('./routes/v4/electrumx')
const EncryptionV4 = require('./routes/v4/encryption')
const PriceV4 = require('./routes/v4/price')
require('dotenv').config() require('dotenv').config()
// Instantiate v3 route libraries. // Instantiate v5 route libraries.
const blockchainV3 = new BlockchainV3() const blockchainV5 = new BlockchainV5()
const controlV3 = new ControlV3() const controlV5 = new ControlV5()
const miningV3 = new MiningV3() const miningV5 = new MiningV5()
const rawtransactionsV3 = new RawtransactionsV3() const rawtransactionsV5 = new RawtransactionsV5()
const slpV3 = new SlpV3() const slpV5 = new SlpV5()
const blockbookV3 = new BlockbookV3() const electrumxv5 = new ElectrumXV5()
const electrumxv3 = new ElectrumXV3() const encryptionv5 = new EncryptionV5()
electrumxv3.connect() const pricev5 = new PriceV5()
const encryptionv3 = new EncryptionV3() const utilV5 = new UtilV5({ electrumx: electrumxv5 })
const pricev3 = new PriceV3() const dsproofV5 = new DSProofV5()
const jwtV5 = new JWTV5()
// Instantiate v4 route libraries. const bcashSLP = new BcashSLP()
const blockchainV4 = new BlockchainV4() const psfSlpIndexer = new PsfSlpIndexer()
const controlV4 = new ControlV4()
const miningV4 = new MiningV4()
const rawtransactionsV4 = new RawtransactionsV4()
const slpV4 = new SlpV4()
const blockbookV4 = new BlockbookV4()
const electrumxv4 = new ElectrumXV4()
electrumxv4.connect()
const encryptionv4 = new EncryptionV4()
const pricev4 = new PriceV4()
const app = express() const app = express()
app.locals.env = process.env app.locals.env = process.env
@@ -98,12 +74,12 @@ app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'jade') app.set('view engine', 'jade')
// Mount the docs // Mount the docs
app.use('/docs', express.static(`${__dirname}/../docs`)) app.use('/docs', express.static(`${__dirname.toString()}/../docs`))
// Log each request to the console with IP addresses. // Log each request to the console with IP addresses.
// app.use(logger("dev")) // app.use(logger("dev"))
const morganFormat = const morganFormat =
':remote-addr :remote-user :method :url :status :response-time ms - :res[content-length] :user-agent' ':remote-addr :remote-user :method :url :status :response-time ms - :res[content-length] :user-agent'
app.use(logger(morganFormat)) app.use(logger(morganFormat))
// Log the same data to the winston logs. // Log the same data to the winston logs.
@@ -114,70 +90,69 @@ const logStream = {
} }
app.use(logger(morganFormat, { stream: logStream })) app.use(logger(morganFormat, { stream: logStream }))
app.use(bodyParser.json()) app.use(bodyParser.json({ limit: '50mb' }))
app.use(bodyParser.urlencoded({ extended: false })) app.use(bodyParser.urlencoded({ extended: false, limit: '50mb' }))
app.use(cookieParser()) app.use(cookieParser())
app.use(express.static(path.join(__dirname, 'public'))) app.use(express.static(path.join(__dirname, 'public')))
// Log requests for later analysis. // Log requests for later analysis.
app.use('/', logReqInfo) app.use('/', logReqInfo)
// const v2prefix = "v2" const v5prefix = 'v5'
const v3prefix = 'v3'
const v4prefix = 'v4'
// Inspect the header for a JWT token. // START Rate Limits
app.use(`/${v3prefix}/`, jwtAuth.getTokenFromHeaders)
app.use(`/${v4prefix}/`, jwtAuth.getTokenFromHeaders)
// Instantiate the authorization middleware, used to implement pro-tier rate limiting.
// Handles Anonymous and Basic Authorization schemes used by passport.js
const auth = new AuthMW() const auth = new AuthMW()
app.use(`/${v3prefix}/`, auth.mw())
app.use(`/${v4prefix}/`, auth.mw())
// Rate limit on all v3 routes // Ensure req.locals and res.locals objects exist.
// Establish and enforce rate limits. app.use(`/${v5prefix}/`, rateLimits.populateLocals)
// app.use(`/${v3prefix}/`, rateLimits.routeRateLimit)
app.use(`/${v3prefix}/`, rateLimits.rateLimitByResource) // Allow users to turn off rate limits with an environment variable.
const DO_NOT_USE_RATE_LIMITS = process.env.DO_NOT_USE_RATE_LIMITS || false
console.log(`DO_NOT_USE_RATE_LIMITS: ${DO_NOT_USE_RATE_LIMITS}`)
if (!DO_NOT_USE_RATE_LIMITS) {
console.log('Rate limits are being used')
// Inspect the header for a JWT token.
app.use(`/${v5prefix}/`, jwtAuth.getTokenFromHeaders)
// Instantiate the authorization middleware, used to implement pro-tier rate limiting.
// Handles Anonymous and Basic Authorization schemes used by passport.js
app.use(`/${v5prefix}/`, auth.mw())
// Experimental rate limits
app.use(`/${v5prefix}/`, rateLimits.applyRateLimits)
// Rate limit on all v4 routes // Rate limit on all v4 routes
// Establish and enforce rate limits. // Establish and enforce rate limits.
app.use(`/${v4prefix}/`, rateLimits.rateLimitByResource) // app.use(`/${v4prefix}/`, rateLimits.rateLimitByResource)
} else {
console.log('Rate limits are NOT being used')
}
// END Rate Limits
// Connect v3 routes // Connect v5 routes
app.use(`/${v3prefix}/` + 'health-check', healthCheckV3) app.use(`/${v5prefix}/` + 'health-check', healthCheckV5)
app.use(`/${v3prefix}/` + 'blockchain', blockchainV3.router) app.use(`/${v5prefix}/` + 'blockchain', blockchainV5.router)
app.use(`/${v3prefix}/` + 'control', controlV3.router) app.use(`/${v5prefix}/` + 'control', controlV5.router)
app.use(`/${v3prefix}/` + 'mining', miningV3.router) app.use(`/${v5prefix}/` + 'mining', miningV5.router)
app.use(`/${v3prefix}/` + 'network', networkV3) app.use(`/${v5prefix}/` + 'network', networkV5)
app.use(`/${v3prefix}/` + 'rawtransactions', rawtransactionsV3.router) app.use(`/${v5prefix}/` + 'rawtransactions', rawtransactionsV5.router)
app.use(`/${v3prefix}/` + 'util', utilV3.router) app.use(`/${v5prefix}/` + 'slp', slpV5.router)
app.use(`/${v3prefix}/` + 'slp', slpV3.router) app.use(`/${v5prefix}/` + 'xpub', xpubV5.router)
app.use(`/${v3prefix}/` + 'xpub', xpubV3.router) app.use(`/${v5prefix}/` + 'electrumx', electrumxv5.router)
app.use(`/${v3prefix}/` + 'blockbook', blockbookV3.router) app.use(`/${v5prefix}/` + 'encryption', encryptionv5.router)
app.use(`/${v3prefix}/` + 'electrumx', electrumxv3.router) app.use(`/${v5prefix}/` + 'price', pricev5.router)
app.use(`/${v3prefix}/` + 'encryption', encryptionv3.router) app.use(`/${v5prefix}/` + 'util', utilV5.router)
app.use(`/${v3prefix}/` + 'price', pricev3.router) app.use(`/${v5prefix}/` + 'dsproof', dsproofV5.router)
app.use(`/${v5prefix}/` + 'jwt', jwtV5.router)
// Connect v4 routes app.use(`/${v5prefix}/` + 'bcash/slp', bcashSLP.router)
app.use(`/${v4prefix}/` + 'health-check', healthCheckV4)
app.use(`/${v4prefix}/` + 'blockchain', blockchainV4.router)
app.use(`/${v4prefix}/` + 'control', controlV4.router)
app.use(`/${v4prefix}/` + 'mining', miningV4.router)
app.use(`/${v4prefix}/` + 'network', networkV4)
app.use(`/${v4prefix}/` + 'rawtransactions', rawtransactionsV4.router)
app.use(`/${v4prefix}/` + 'util', utilV4.router)
app.use(`/${v4prefix}/` + 'slp', slpV4.router)
app.use(`/${v4prefix}/` + 'xpub', xpubV4.router)
app.use(`/${v4prefix}/` + 'blockbook', blockbookV4.router)
app.use(`/${v4prefix}/` + 'electrumx', electrumxv4.router)
app.use(`/${v4prefix}/` + 'encryption', encryptionv4.router)
app.use(`/${v4prefix}/` + 'price', pricev4.router)
const ninsight = new Ninsight() app.use(`/${v5prefix}/` + 'psf/slp', psfSlpIndexer.router)
app.use(`/${v3prefix}/` + 'ninsight', ninsight.router)
app.use(`/${v4prefix}/` + 'ninsight', ninsight.router) // Daniel:
// app.use(`/${v5prefix}/` + 'psfslp', psfSlp.router)
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use((req, res, next) => { app.use((req, res, next) => {
@@ -225,9 +200,11 @@ server.listen(port)
server.on('error', onError) server.on('error', onError)
server.on('listening', onListening) server.on('listening', onListening)
// Set the time before a timeout error is generated. This impacts testing and // Set the time before a timeout error is generated.
// the handling of timeout errors. Is 10 seconds too agressive? // 10 seconds is way too agressive. 30 Seconds was used for a while, but with
server.setTimeout(30 * 1000) // being able to set a timeout between UTXOs for tokenUtxoDetails, the timeout
// needed to be extended.
server.setTimeout(1000 * 60 * 5) // 5 minutes
/** /**
* Normalize a port into a number, string, or false. * Normalize a port into a number, string, or false.
@@ -262,11 +239,11 @@ function onError (error) {
case 'EACCES': case 'EACCES':
console.error(`${bind} requires elevated privileges`) console.error(`${bind} requires elevated privileges`)
process.exit(1) process.exit(1)
// break break
case 'EADDRINUSE': case 'EADDRINUSE':
console.error(`${bind} is already in use`) console.error(`${bind} is already in use`)
process.exit(1) process.exit(1)
// break break
default: default:
throw error throw error
} }
@@ -282,4 +259,4 @@ function onListening () {
debug(`Listening on ${bind}`) debug(`Listening on ${bind}`)
} }
// //
// module.exports = app; // module.exports = app
+12 -8
View File
@@ -1,22 +1,25 @@
/* /*
CT 2/4/20 Note: This library handles anonymous and Basic auth. This library This library handles anonymous and Basic Authentication.
can be phased out with the chage to JWT tokens and the new rate-limit library.
Handle authorization for bypassing rate limits.
1) Default is 'Anonymous Authentication', which unlocks the freemimum tier by 1) Default is 'Anonymous Authentication', which unlocks the freemimum tier by
default. default.
2) Hard-coded 'Basic Authentication' is a token that does not expire and is 2) Hard-coded 'Basic Authentication' is a token that does not expire and is
provided to buisiness partners. provided for clients who run their own isolated infrastructure without rate
limits, but still need a way from preventing the random public from using
their API.
3) JWT-based 'Local Authentication' is used for normal users that pay to 3) JWT-based 'Local Authentication' is used for normal users that pay to
access the premium pro-tier services. access the premium pro-tier services.
This file uses the passport npm library to check the header of each REST API This file uses the passport npm library to check the header of each REST API
call for the prescence of a Basic authorization header: call for the prescence of a Basic Authentication header:
https://en.wikipedia.org/wiki/Basic_access_authentication https://en.wikipedia.org/wiki/Basic_access_authentication
If the header is found and validated, the req.locals.proLimit Boolean value If the header is found and validated, the req.locals.proLimit Boolean value
is set and passed to the route-ratelimits.ts middleware. is set and passed to the route-ratelimit.js middleware. route-ratelimit.js
is for fine-grain JWT-based rate limits. If req.locals.proLimit is set to
true, then those rate limits will be skipped.
*/ */
'use strict' 'use strict'
@@ -76,8 +79,9 @@ class AuthMW {
req.locals.proLimit = false req.locals.proLimit = false
// Evaluate the username and password and set the rate limit accordingly. // Evaluate the username and password and set the rate limit accordingly.
// if (username === "BITBOX" && password === PRO_PASS) {
if (username === 'fullstackcash') { if (username === 'fullstackcash') {
// Can set several different passwords in the environment variable.
// Loop through each one to see if one matches.
for (let i = 0; i < PRO_PASS.length; i++) { for (let i = 0; i < PRO_PASS.length; i++) {
const thisPass = PRO_PASS[i] const thisPass = PRO_PASS[i]
+2
View File
@@ -12,6 +12,8 @@
// req.locals.jwtToken property. // req.locals.jwtToken property.
const getTokenFromHeaders = (req, res, next) => { const getTokenFromHeaders = (req, res, next) => {
try { try {
// console.log('req.headers: ', req.headers)
// Only executes if the authorization header exists. // Only executes if the authorization header exists.
if (req.headers.authorization) { if (req.headers.authorization) {
// Retrieve the auth string from the header object. // Retrieve the auth string from the header object.
+1 -1
View File
@@ -40,7 +40,7 @@ const logReqInfo = function (req, res, next) {
body: req.body body: req.body
} }
wlogger.verbose(`Request: ${ip} ${method} ${url}`, dataToLog) wlogger.info(`Request: ${ip} ${method} ${url}`, dataToLog)
next() next()
} catch (err) { } catch (err) {
+410 -228
View File
@@ -1,49 +1,64 @@
/* /*
Sets the rate limits for the anonymous and paid tiers. Current rate limits: Sets the rate limits for the anonymous and paid tiers. Current rate limits:
- 1000 points in 60 seconds - 10000 points in 60 seconds
- 10 points per call for paid tier (100 RPM) - 500 points per call for anonymous tier (20 RPM)
- 50 points per call for anonymous tier (20 RPM) - 100 points per call for tier 40 (100 RPM)
- 40 points per call for tier 50 (250 RPM)
- 16 points per call for tier 60 (625 RPM)
Background: The rate limit handling is designed for these four use cases:
The rate limits below were originially coded with the idea of charging on a - Users who want to buy a JWT token for 24 hour access.
per-resource basis. However, that was confusing to end users trying to purchase - Users who want to buy different RPM tiers: 100, 250, 600
a subscription. So everything was simplied to two tiers: paid and anonymous - Basic Authentication which should not have any rate limits applied.
- Local installations that do not want any authentication or rate limits at all.
The Basic Auth use cases is considered when determining internal rate limits.
The internal rate limits should not be applied to calls from those users.
A lot of attention has been paid to passing rate-limit information for the user
when they trigger an endpoint that makes a lot of internal API calls. Examples
are hydrateUtxos() and getPublicKey(). These keeps things fair by charging the
same for 'light' API calls and 'heavy' API calls.
TODO:
- Add code for applying rate limits to whitelist domains.
*/ */
'use strict'
// Public npm libraries. // Public npm libraries.
const jwt = require('jsonwebtoken') const jwt = require('jsonwebtoken')
const Redis = require('ioredis')
const { RateLimiterRedis } = require('rate-limiter-flexible')
// local libraries. // local libraries.
const wlogger = require('../util/winston-logging') const wlogger = require('../util/winston-logging')
const config = require('../../config') const config = require('../../config')
const ANON_LIMITS = config.anonRateLimit // The amount of delay in milliseconds to add to anonymous requests, in order
const WHITELIST_RATE_LIMIT = config.whitelistRateLimit // to slow down freeloaders that are hitting the system too hard.
const WHITELIST_DOMAINS = config.whitelistDomains const ANON_DELAY = 3000
const INTERNAL_RATE_LIMIT = 1
// Redis let _this // Global pointer to instance of class, when 'this' context is lost.
// Setup Redis to track rate limits for each user.
const redisOptions = { const redisOptions = {
enableOfflineQueue: false, enableOfflineQueue: false,
port: process.env.REDIS_PORT ? process.env.REDIS_PORT : 6379, port: process.env.REDIS_PORT ? process.env.REDIS_PORT : 6379,
host: process.env.REDIS_HOST ? process.env.REDIS_HOST : '127.0.0.1' host: process.env.REDIS_HOST ? process.env.REDIS_HOST : '127.0.0.1'
} }
console.log(`redisOptions: ${JSON.stringify(redisOptions, null, 2)}`)
const Redis = require('ioredis')
const redisClient = new Redis(redisOptions) const redisClient = new Redis(redisOptions)
// Rate limiter middleware lib.
const { RateLimiterRedis } = require('rate-limiter-flexible')
const rateLimitOptions = { const rateLimitOptions = {
storeClient: redisClient, storeClient: redisClient,
points: 1000, // Number of points points: config.pointsPerMinute, // Number of points
duration: 60 // Per minute (per 60 seconds) duration: 60 // Per minute (per 60 seconds)
} }
let _this // Constants
const ANON_LIMITS = config.anonRateLimit
// const WHITELIST_RATE_LIMIT = config.whitelistRateLimit
const WHITELIST_DOMAINS = config.whitelistDomains
const WHITELIST_POINTS_TO_CONSUME = config.whitelistRateLimit
const POINTS_PER_MINUTE = config.pointsPerMinute
const INTERNAL_POINTS_TO_CONSUME = config.internalRateLimit
class RateLimits { class RateLimits {
constructor () { constructor () {
@@ -54,25 +69,388 @@ class RateLimits {
this.config = config this.config = config
} }
// This is the main middleware funciton of this library. All other functions
// support this function.
async applyRateLimits (req, res, next) {
try {
// Exit if the user has already authenticated with Basic Authentication.
if (req.locals.proLimit) {
console.log('External call, basic auth, skipping rate limits.')
wlogger.debug(
'req.locals.proLimit = true; Using Basic Authentication instead of rate limits'
)
return next()
}
// Determine if the call is an external or internal API call.
const isInternal = _this.checkInternalIp(req)
// console.log(`isInternal: ${isInternal}`)
// Determine if the call originates from another computer on the intranet.
const isWhitelistOrigin = _this.isInWhitelist(req)
// console.log('isWhitelistOrigin: ', isWhitelistOrigin)
// Handle the use case of internally-generated requests.
if (isInternal) {
// Internal API calls should pass the authentication data in through the
// the usrObj in the body.
if (req.body && req.body.usrObj) {
if (req.body.usrObj.proLimit) {
// console.log('Internal call, basic auth, skipping rate limits.')
// If this is an internal call that originated from a user using
// Basic Authentication, then skip rate-limits.
return next()
} else {
// console.log(
// 'Internal call, applying rate limits. Using JWT if available.'
// )
// Determine if user has exceeded their rate limits. Pass in the
// JWT token if one exists.
const hasExceededRateLimit = await _this.trackRateLimits(
req,
res,
req.body.usrObj.jwtToken
)
if (!hasExceededRateLimit) {
// Rate limits have not been exceeded. Processing can continue.
return next()
} else {
// trackRateLimits() returns the 'res' object with an error message
// and status code.
return hasExceededRateLimit
}
}
} else {
// This should be a corner case. Calls should not be going into this
// code path, so the system should throw up big warning signs when they
// do.
// This code path happens when an internal call is made but does not
// pass the usrObj. Legacy code needs to be refactored to use the usrObj
// and avoid this code path. This code path is 'pooled': all users
// share the same rate limits. Even at 1000 RPM, this pool will get
// exhausted easily.
// const warnMsg =
// 'Internal call. req.body.usrObj does not exist. Applying high-speed internal rate limits.'
// console.log(warnMsg)
// wlogger.info(warnMsg)
const defaultPayload = {
id: '98.76.54.32',
email: 'internal@bchtest.net',
apiLevel: 40,
rateLimit: 100,
pointsToConsume: INTERNAL_POINTS_TO_CONSUME,
duration: 30
}
// Default values, in case there is an error.
const defaultJwt = _this.generateJwtToken(defaultPayload)
// Track the rate limit for this user. Pass in the JWT token, if one
// is available.
const hasExceededRateLimit = await _this.trackRateLimits(
req,
res,
defaultJwt
)
// console.log(`hasExceededRateLimit: `, hasExceededRateLimit)
if (!hasExceededRateLimit) {
// Rate limits have not been exceeded. Processing can continue.
return next()
} else {
// trackRateLimits() returns the 'res' object with an error message
// and status code.
return hasExceededRateLimit
}
}
//
//
} else {
// Handle the normal use-case of external requests
// console.log(
// 'External call, applying rate limits. Using JWT if available.'
// )
// For calls originating from a whitelist domain, apply a high-RPM
// JWT token to the call.
if (isWhitelistOrigin) {
const defaultPayload = {
id: '77.77.77.77',
email: 'whitelist@bchtest.net',
apiLevel: 40,
rateLimit: 100,
pointsToConsume: WHITELIST_POINTS_TO_CONSUME,
duration: 30
}
// Inject the high-RPM JWT token into the call.
req.locals.jwtToken = _this.generateJwtToken(defaultPayload)
}
// Track the rate limit for this user. Pass in the JWT token, if one
// is available.
const hasExceededRateLimit = await _this.trackRateLimits(
req,
res,
req.locals.jwtToken
)
// console.log('hasExceededRateLimit: ', hasExceededRateLimit)
if (!hasExceededRateLimit) {
// Rate limits have not been exceeded. Processing can continue.
return next()
} else {
// trackRateLimits() returns the 'res' object with an error message
// and status code.
return hasExceededRateLimit
}
}
} catch (err) {
wlogger.error('Error in route-ratelimit2.js/applyRateLimits(): ', err)
}
// By default, move to the next middleware.
next()
}
sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
// A wrapper for Redis-based rate limiter.
// Will return false if the user has not exceeded the rate limit. Otherwise
// it will return the 'res' object with an error status and message, which
// should be returned by the middleware.
async trackRateLimits (req, res, jwtToken) {
const debugInfo = {
jwtToken,
userObj: req.body.usrObj,
locals: req.locals
}
// Anonymous rate limits are used by default.
let pointsToConsume = ANON_LIMITS
// console.log('pointsToConsume: ', pointsToConsume)
let key = req.ip // Use the IP address as the key, by default.
debugInfo.ip = req.ip
// console.log('jwtToken: ', jwtToken)
try {
// Decode the JWT token if it exists
if (jwtToken) {
const decoded = _this.decodeJwtToken(jwtToken)
// console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`)
// Preferentially use the decoded ID in the JWT payload, as the key.
key = decoded.id
debugInfo.id = key
pointsToConsume = decoded.pointsToConsume
debugInfo.pointsToConsume = pointsToConsume
}
// console.log(`rate limit key: ${key}`)
// This function will throw an error if the user exceeds the rate limit.
// The 429 error response is handled by the catch().
await _this.rateLimiter.consume(key, pointsToConsume)
// Debugging
// const rateLimitData = await _this.rateLimiter.consume(key, pointsToConsume)
// console.log(`rateLimitData: `, rateLimitData)
// Add data to logs for analytics.
const logData = {
ip: req.ip,
key,
pointsToConsume,
status: 'OK'
}
wlogger.info(logData)
res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
// Add artificial delay to slow down freeloaders.
if (pointsToConsume === ANON_LIMITS) {
await this.sleep(ANON_DELAY)
}
// Signal that the user has not exceeded their rate limits.
return false
} catch (err) {
// console.log('err: ', err)
const rateLimit = Math.floor(POINTS_PER_MINUTE / pointsToConsume)
res.locals.rateLimitTriggered = true
// console.log('res.locals: ', res.locals)
// console.log(
// `rate limit debug info: ${JSON.stringify(debugInfo, null, 2)}`
// )
// Add data to logs for analytics.
const logData = {
ip: req.ip,
key,
pointsToConsume,
status: 429
}
wlogger.info(logData)
// Add artificial delay to slow down freeloaders.
if (pointsToConsume === ANON_LIMITS) {
await this.sleep(ANON_DELAY)
}
// Rate limited was triggered
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
error: `Too many requests. Your limits are currently ${rateLimit} requests per minute. Increase rate limits at https://fullstack.cash`
})
}
}
// Attempts to decode a JWT token. Returns default values if it fails.
decodeJwtToken (jwtToken) {
const defaultPayload = {
id: '123.456.789.10',
email: 'test@bchtest.net',
apiLevel: 10,
rateLimit: 3,
pointsToConsume: ANON_LIMITS,
duration: 30
}
try {
// Default values, in case there is an error.
const defaultJwt = _this.generateJwtToken(defaultPayload)
// Generate a default payload to use, if the decoding of the user-provided
// jwt fails.
let decoded = _this.jwt.verify(defaultJwt, _this.config.apiTokenSecret)
try {
decoded = _this.jwt.verify(jwtToken, _this.config.apiTokenSecret)
} catch (err) {
wlogger.error('Error in route-ratelimit2.js/decodeJwtTokens(): ', err)
}
return decoded
} catch (err) {
wlogger.error(
'Unhandled error in route-ratelimit2.js/deocdeJwtToken: ',
err
)
// Making sure there is an exp property. Not sure if this will cause an
// issue, using a hard-coded value.
defaultPayload.exp = 1574269450
return defaultPayload
}
}
// Returns a boolean if the origin of the request matches a domain in the
// whitelist.
isInWhitelist (req) {
try {
const retVal = false // Default value.
// Retrieve the origin.
const origin = req.get('origin')
if (!process.env.TEST) console.log('origin:', origin)
// If the origin is not determinable, return false.
if (!origin) return false
// console.log(`WHITELIST_DOMAINS: ${JSON.stringify(WHITELIST_DOMAINS, null, 2)}`)
for (let i = 0; i < WHITELIST_DOMAINS.length; i++) {
const thisDomain = WHITELIST_DOMAINS[i]
if (origin.includes(thisDomain)) return true
}
return retVal
} catch (err) {
wlogger.error(
'Error in route-ratelimit.js/isInWhitelist(). Returning false by default.'
)
return false
}
}
// Checks the request object to see if it's IP address matches an internal
// IP address. That means the call is an internal API call and should be
// treated differently than an external API call.
checkInternalIp (req) {
try {
// Default value
let isInternal = false
const ip = req.ip
if (ip.includes('127.0.0.1')) isInternal = true
if (ip.includes('172.17.')) isInternal = true
// TODO: Add 192.168.
return isInternal
} catch (err) {
wlogger.error(
'Error in checkInternalIp(). Returning false be default. Err: ',
err
)
return false
}
}
// Used to disconnect from the Redis DB. // Used to disconnect from the Redis DB.
// Called by unit tests so that node.js thread doesn't live forever. // Called by unit tests so that node.js thread doesn't live forever.
closeRedis () { closeRedis () {
redisClient.disconnect() redisClient.disconnect()
} }
// Clear the redis database. Used by unit tests.
async wipeRedis () { async wipeRedis () {
await redisClient.flushdb() await redisClient.flushdb()
} }
// This is the new rate limit function that uses the rate-limiter-flexible npm // Generates a JWT token for testing purposes. This is not used in production.
// library. It uses fine-grain rate limiting based on the resources being // This function mirrors the kind of JWT token that would be generated by
// consumed. // jwt-bch-api.
async rateLimitByResource (req, res, next) { generateJwtToken (payload) {
try { try {
let userId const jwtOptions = {
let decoded = {} expiresIn: '30 days'
}
// Create a req.locals object if not passed in. const token = _this.jwt.sign(
payload,
_this.config.apiTokenSecret,
jwtOptions
)
return token
} catch (err) {
console.error('Error in generateJwtToken()')
throw err
}
}
// Called when rate limits are not used.
populateLocals (req, res, next) {
try {
// Create a re*Q*.locals object if not passed in.
// req.locals.proLimit will be true if the user is using Basic Authentication.
if (!req.locals) { if (!req.locals) {
req.locals = { req.locals = {
// default values // default values
@@ -82,215 +460,19 @@ class RateLimits {
} }
} }
// Create a res.locals object if it does not exist. This is used for // Create a re*S*.locals object if it does not exist.
// debugging.
if (!res.locals) { if (!res.locals) {
res.locals = { res.locals = {
rateLimitTriggered: false rateLimitTriggered: false
} }
} }
// Decode the JWT token if one exists.
if (req.locals.jwtToken) {
try {
decoded = _this.jwt.verify(
req.locals.jwtToken,
_this.config.apiTokenSecret
)
// console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`)
userId = decoded.id
} catch (err) {
// This handler will be triggered if the JWT token does not match the
// token secret.
wlogger.error(
`Last three letters of token secret: ${_this.config.apiTokenSecret.slice(
-3
)}`
)
wlogger.error(
'Error trying to decode JWT token in route-ratelimit.js/newRateLimit(): ',
err
)
}
} else {
wlogger.debug('No JWT token found!')
}
// Default value is 50 points per request = 20 RPM
let rateLimit = ANON_LIMITS
// Only evaluate the JWT token if the user is not using Basic Authentication.
if (!req.locals.proLimit) {
// Code here for the rate limiter is adapted from this example:
// https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users
try {
// The resource being consumed: full node, indexer, SLPDB, etc.
const resource = _this.getResource(req.url)
wlogger.debug(`resource: ${resource}`)
let key = userId || req.ip
res.locals.key = key // Feedback for tests.
// const pointsToConsume = userId ? 1 : 30
decoded.resource = resource
let pointsToConsume = _this.calcPoints(decoded)
res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
// Retrieve the origin.
let origin = req.get('origin')
// Handle calls coming from the intranet.
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.
// console.log(`origin: ${JSON.stringify(origin, null, 2)}`)
// console.log(`whitelist: ${JSON.stringify(WHITELIST_DOMAINS, null, 2)}`)
const isInWhitelist = _this.isInWhitelist(origin)
if (isInWhitelist) {
pointsToConsume = WHITELIST_RATE_LIMIT
res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
}
// For internal calls, increase rate limits to as fast as possible.
if (
// Comment out the line below when running bch-js e2e rate limit tests.
key.toString().indexOf('::ffff:127.0.0.1') > -1 ||
// Do not comment out this line.
key.toString().indexOf('172.17.') > -1
) {
pointsToConsume = INTERNAL_RATE_LIMIT
res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
}
wlogger.info(
`User ${key} consuming ${pointsToConsume} point for resource ${resource}.`
)
rateLimit = Math.floor(1000 / pointsToConsume)
// Update the key so that rate limits track both the user and the resource.
key = `${key}-${resource}`
await _this.rateLimiter.consume(key, pointsToConsume)
} catch (err) {
// console.log('err: ', err)
// Used for returning data for tests.
res.locals.rateLimitTriggered = true
// console.log('res.locals: ', res.locals)
// Rate limited was triggered
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
error: `Too many requests. Your limits are currently ${rateLimit} requests per minute. Increase rate limits at https://fullstack.cash`
})
}
}
} catch (err) {
wlogger.error('Error in route-ratelimit.js/newRateLimit(): ', err)
// throw err
}
next() next()
}
// Calculates the points consumed, based on the jwt information and the route
// requested.
calcPoints (jwtInfo) {
let retVal = ANON_LIMITS // By default, use anonymous tier.
try {
// console.log(`jwtInfo: ${JSON.stringify(jwtInfo, null, 2)}`)
const apiLevel = jwtInfo.apiLevel
const resource = jwtInfo.resource
const level30Routes = ['insight', 'bitcore', 'blockbook', 'electrumx']
const level40Routes = ['slp']
wlogger.debug(`apiLevel: ${apiLevel}`)
// Only evaluate if user is using a JWT token.
if (jwtInfo.id) {
// SLP indexer routes
if (level40Routes.includes(resource)) {
if (apiLevel >= 40) retVal = 10
// else if (apiLevel >= 10) retVal = 10
else retVal = ANON_LIMITS
// Normal indexer routes
} else if (level30Routes.includes(resource)) {
if (apiLevel >= 30) retVal = 10
else retVal = ANON_LIMITS
// Full node tier
} else if (apiLevel >= 20) {
retVal = 10
// Free tier, full node only.
} else {
retVal = ANON_LIMITS
}
}
return retVal
} catch (err) { } catch (err) {
wlogger.error('Error in route-ratelimit.js/calcPoints()') console.error('Error in populateLocals(): ', err)
// throw err
retVal = ANON_LIMITS
}
return retVal
}
// This function parses the req.url property to identify what resource
// the user is requesting.
// This was created as a function so that it can be unit tested. Not sure
// what kind of variations will be seen in production.
getResource (url) {
try {
wlogger.debug(`url: ${JSON.stringify(url, null, 2)}`)
const splitUrl = url.split('/')
const resource = splitUrl[1]
return resource
} catch (err) {
wlogger.error('Error in getResource().')
throw err throw err
} }
} }
// Returns a boolean if the origin of the request matches a domain in the
// whitelist.
isInWhitelist (origin) {
try {
const retVal = false // Default value.
if (!origin) return false
// console.log(`WHITELIST_DOMAINS: ${JSON.stringify(WHITELIST_DOMAINS, null, 2)}`)
for (let i = 0; i < WHITELIST_DOMAINS.length; i++) {
const thisDomain = WHITELIST_DOMAINS[i]
if (origin.toString().indexOf(thisDomain) > -1) {
return true
}
}
return retVal
} catch (err) {
wlogger.error('Error in route-ratelimit.js/isInWhitelist(). Returning false by default.')
return false
}
}
} }
module.exports = RateLimits module.exports = RateLimits
-521
View File
@@ -1,521 +0,0 @@
/*
Blockbook API route
*/
'use strict'
const express = require('express')
const axios = require('axios')
const wlogger = require('../../util/winston-logging')
const RouteUtils = require('../../util/route-utils')
const routeUtils = new RouteUtils()
// Library for easily switching the API paths to use different instances of
// Blockbook.
const BlockbookPath = require('../../util/blockbook-path')
const BLOCKBOOKPATH = new BlockbookPath()
// BLOCKBOOKPATH.toOpenBazaar()
const router = express.Router()
// Used for processing error messages before sending them to the user.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
const BCHJS = require('@psf/bch-js')
const bchjs = new BCHJS()
let _this
class Blockbook {
constructor () {
_this = this
_this.axios = axios
_this.routeUtils = routeUtils
_this.bchjs = bchjs
_this.BLOCKBOOKPATH = BLOCKBOOKPATH
_this.router = router
_this.router.get('/', _this.root)
_this.router.get('/balance/:address', _this.balanceSingle)
_this.router.post('/balance', _this.balanceBulk)
_this.router.get('/utxos/:address', _this.utxosSingle)
_this.router.post('/utxos', _this.utxosBulk)
_this.router.get('/tx/:txid', _this.txSingle)
_this.router.post('/tx', _this.txBulk)
}
// DRY error handler.
errorHandler (err, res) {
// Attempt to decode the error message.
const { msg, status } = _this.routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
res.status(500)
return res.json({ error: util.inspect(err) })
}
// Root API endpoint. Simply acknowledges that it exists.
root (req, res, next) {
return res.json({ status: 'address' })
}
// Query the Blockbook Node API for a balance on a single BCH address.
// Returns a Promise.
async balanceFromBlockbook (thisAddress) {
try {
// console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`)
// Convert the address to a cashaddr without a prefix.
const addr = _this.bchjs.Address.toCashAddress(thisAddress)
const path = `${_this.BLOCKBOOKPATH.addrPath}${addr}`
// console.log(`path: ${path}`)
// Query the Blockbook Node API.
const options = {
method: 'get',
baseURL: path
}
const axiosResponse = await _this.axios.request(options)
const retData = axiosResponse.data
// console.log(`retData: ${util.inspect(retData)}`)
return retData
} catch (err) {
// Dev Note: Do not log error messages here. Throw them instead and let the
// parent function handle it.
wlogger.debug('Error in blockbook.js/balanceFromBlockbook()')
throw err
}
}
async balanceSingle (req, res, next) {
try {
const address = req.params.address
if (!address || address === '') {
res.status(400)
return res.json({ error: 'address can not be empty' })
}
// Reject if address is an array.
if (Array.isArray(address)) {
res.status(400)
return res.json({
error: 'address can not be an array. Use POST for bulk upload.'
})
}
wlogger.debug(
'Executing blockbook/balanceSingle with this address: ',
address
)
// Ensure the input is a valid BCH address.
try {
// const legacyAddr = bchjs.Address.toLegacyAddress(address)
_this.bchjs.Address.toLegacyAddress(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 networkIsValid = _this.routeUtils.validateNetwork(address)
if (!networkIsValid) {
res.status(400)
return res.json({
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
// Query the Blockbook Node API.
const retData = await _this.balanceFromBlockbook(address)
// Return the retrieved address information.
res.status(200)
return res.json(retData)
} catch (err) {
// Write out error to error log.
wlogger.error('Error in blockbook.js/balanceSingle().', err)
return _this.errorHandler(err, res)
}
}
// POST handler for bulk queries on address details
async balanceBulk (req, res, next) {
try {
let addresses = req.body.addresses
// const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0
// Reject if addresses is not an array.
if (!Array.isArray(addresses)) {
res.status(400)
return res.json({
error: 'addresses needs to be an array. Use GET for single address.'
})
}
// Enforce array size rate limits
if (!_this.routeUtils.validateArraySize(req, addresses)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
error: 'Array too large.'
})
}
wlogger.debug(
'Executing blockbook.js/balanceBulk with these addresses: ',
addresses
)
// Validate each element in the address array.
for (let i = 0; i < addresses.length; i++) {
const thisAddress = addresses[i]
// Ensure the input is a valid BCH address.
try {
_this.bchjs.Address.toLegacyAddress(thisAddress)
} catch (err) {
res.status(400)
return res.json({
error: `Invalid BCH address. Double check your address is valid: ${thisAddress}`
})
}
// Prevent a common user error. Ensure they are using the correct network address.
const networkIsValid = _this.routeUtils.validateNetwork(thisAddress)
if (!networkIsValid) {
res.status(400)
return res.json({
error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.`
})
}
}
// Loops through each address and creates an array of Promises, querying
// Insight API in parallel.
addresses = addresses.map(async (address, index) =>
// console.log(`address: ${address}`)
_this.balanceFromBlockbook(address)
)
// Wait for all parallel Insight requests to return.
const result = await _this.axios.all(addresses)
// Return the array of retrieved address information.
res.status(200)
return res.json(result)
} catch (err) {
wlogger.error('Error in blockbook.js/balanceBulk().', err)
return _this.errorHandler(err, res)
}
}
// Query the Blockbook API for utxos associated with a BCH address.
// Returns a Promise.
async utxosFromBlockbook (thisAddress) {
try {
// console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`)
// Convert the address to a cashaddr without a prefix.
const addr = _this.bchjs.Address.toCashAddress(thisAddress)
const path = `${_this.BLOCKBOOKPATH.utxoPath}${addr}`
// console.log(`path: ${path}`)
// Query the Blockbook API.
// Query the Blockbook Node API.
const options = {
method: 'get',
baseURL: path
}
const axiosResponse = await _this.axios.request(options)
const retData = axiosResponse.data
// console.log(`retData: ${util.inspect(retData)}`)
// Add the satoshis property to each UTXO.
for (let i = 0; i < retData.length; i++) {
retData[i].satoshis = Number(retData[i].value)
}
return retData
} catch (err) {
// Dev Note: Do not log error messages here. Throw them instead and let the
// parent function handle it.
wlogger.debug('Error in blockbook.js/utxosFromBlockbook()')
throw err
}
}
// GET handler for single balance
async utxosSingle (req, res, next) {
try {
const address = req.params.address
if (!address || address === '') {
res.status(400)
return res.json({ error: 'address can not be empty' })
}
// Reject if address is an array.
if (Array.isArray(address)) {
res.status(400)
return res.json({
error: 'address can not be an array. Use POST for bulk upload.'
})
}
wlogger.debug(
'Executing blockbook/utxosSingle with this address: ',
address
)
// Ensure the input is a valid BCH address.
try {
// const legacyAddr = bchjs.Address.toLegacyAddress(address)
_this.bchjs.Address.toLegacyAddress(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 networkIsValid = _this.routeUtils.validateNetwork(address)
if (!networkIsValid) {
res.status(400)
return res.json({
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
// Query the Blockbook API.
const retData = await _this.utxosFromBlockbook(address)
// Return the retrieved address information.
res.status(200)
return res.json(retData)
} catch (err) {
// Write out error to error log.
wlogger.error('Error in blockbook.js/utxosSingle().', err)
return _this.errorHandler(err, res)
}
}
// POST handler for bulk queries on address utxos
async utxosBulk (req, res, next) {
try {
let addresses = req.body.addresses
// const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0
// Reject if addresses is not an array.
if (!Array.isArray(addresses)) {
res.status(400)
return res.json({
error: 'addresses needs to be an array. Use GET for single address.'
})
}
// Enforce array size rate limits
if (!_this.routeUtils.validateArraySize(req, addresses)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
error: 'Array too large.'
})
}
wlogger.debug(
'Executing blockbook.js/utxosBulk with these addresses: ',
addresses
)
// Validate each element in the address array.
for (let i = 0; i < addresses.length; i++) {
const thisAddress = addresses[i]
// Ensure the input is a valid BCH address.
try {
_this.bchjs.Address.toLegacyAddress(thisAddress)
} catch (err) {
res.status(400)
return res.json({
error: `Invalid BCH address. Double check your address is valid: ${thisAddress}`
})
}
// Prevent a common user error. Ensure they are using the correct network address.
const networkIsValid = _this.routeUtils.validateNetwork(thisAddress)
if (!networkIsValid) {
res.status(400)
return res.json({
error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.`
})
}
}
// Loops through each address and creates an array of Promises, querying
// Insight API in parallel.
addresses = addresses.map(async (address, index) =>
// console.log(`address: ${address}`)
_this.utxosFromBlockbook(address)
)
// Wait for all parallel Insight requests to return.
const result = await _this.axios.all(addresses)
// Return the array of retrieved address information.
res.status(200)
return res.json(result)
} catch (err) {
wlogger.error('Error in blockbook.js/utxosBulk().', err)
return _this.errorHandler(err, res)
}
}
// Query the Blockbook Node API for transactions on a single TXID.
// Returns a Promise.
async transactionsFromBlockbook (txid) {
try {
// console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`)
const path = `${_this.BLOCKBOOKPATH.txPath}${txid}`
// console.log(`path: ${path}`)
// Query the Blockbook Node API.
const options = {
method: 'get',
baseURL: path
}
const axiosResponse = await _this.axios.request(options)
const retPromise = axiosResponse.data
// console.log(`retData: ${util.inspect(retData)}`)
return retPromise
} catch (err) {
// Dev Note: Do not log error messages here. Throw them instead and let the
// parent function handle it.
wlogger.debug('Error in blockbook.js/transactionsFromBlockbook()')
throw err
}
}
// GET handler for single transaction details.
async txSingle (req, res, next) {
try {
const txid = req.params.txid
if (!txid || txid === '') {
res.status(400)
return res.json({ error: 'txid can not be empty' })
}
// Reject if address is an array.
if (Array.isArray(txid)) {
res.status(400)
return res.json({
error: 'txid can not be an array. Use POST for bulk upload.'
})
}
// TODO: Add regex comparison of txid to ensure it's valid.
if (txid.length !== 64) {
res.status(400)
return res.json({
error: `txid must be of length 64 (not ${txid.length})`
})
}
wlogger.debug('Executing blockbook/txSingle with this txid: ', txid)
// Query the Blockbook Node API.
const retData = await _this.transactionsFromBlockbook(txid)
// Return the retrieved address information.
res.status(200)
return res.json(retData)
} catch (err) {
// Write out error to error log.
wlogger.error('Error in blockbook.js/txSingle().', err)
return _this.errorHandler(err, res)
}
}
// POST handler for bulk queries on tx details
async txBulk (req, res, next) {
try {
let txids = req.body.txids
// const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0
// Reject if txids is not an array.
if (!Array.isArray(txids)) {
res.status(400)
return res.json({
error: 'txids need to be an array. Use GET for single address.'
})
}
// 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({
error: 'Array too large.'
})
}
wlogger.debug('Executing blockbook.js/txBulk with these txids: ', txids)
// Validate each element in the txids array.
for (let i = 0; i < txids.length; i++) {
const thisTxid = txids[i]
if (!thisTxid || thisTxid === '') {
res.status(400)
return res.json({ error: 'txid can not be empty' })
}
// TODO: Add regex comparison of txid to ensure it's valid.
if (thisTxid.length !== 64) {
res.status(400)
return res.json({
error: `txid must be of length 64 (not ${thisTxid.length})`
})
}
}
// Loops through each address and creates an array of Promises, querying
// Insight API in parallel.
txids = txids.map(async (txid, index) =>
// console.log(`address: ${address}`)
_this.transactionsFromBlockbook(txid)
)
// Wait for all parallel Insight requests to return.
const result = await _this.axios.all(txids)
// Return the array of retrieved address information.
res.status(200)
return res.json(result)
} catch (err) {
wlogger.error('Error in blockbook.js/txBulk().', err)
return _this.errorHandler(err, res)
}
}
}
module.exports = Blockbook
File diff suppressed because it is too large Load Diff
-180
View File
@@ -1,180 +0,0 @@
/*
Encryption API route
*/
'use strict'
const express = require('express')
const router = express.Router()
const axios = require('axios')
const util = require('util')
const wlogger = require('../../util/winston-logging')
const config = require('../../../config')
const RouteUtils = require('../../util/route-utils')
const routeUtils = new RouteUtils()
const BCHJS = require('@psf/bch-js')
const restURL = process.env.LOCAL_RESTURL
? process.env.LOCAL_RESTURL
: 'https://api.fullstack.cash/v3/'
const bchjs = new BCHJS({ restURL })
let _this
class Encryption {
constructor () {
_this = this
_this.config = config
_this.axios = axios
_this.routeUtils = routeUtils
_this.bchjs = bchjs
// _this.blockbook = blockbook
// _this.rawTransactions = rawTransactions
_this.router = router
_this.router.get('/', _this.root)
_this.router.get('/publickey/:address', _this.getPublicKey)
}
// DRY error handler.
errorHandler (err, res) {
// Attempt to decode the error message.
const { msg, status } = _this.routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
// Handle error patterns specific to this route.
if (err.message) {
res.status(400)
return res.json({ success: false, error: err.message })
}
wlogger.error('Unhandled error in Encryption library: ', err)
// If error can be handled, return the stack trace
res.status(500)
return res.json({ error: util.inspect(err) })
}
// Root API endpoint. Simply acknowledges that it exists.
root (req, res, next) {
return res.json({ status: 'encryption' })
}
/**
* @api {get} /encryption/publickey/{addr} Get public key for a BCH address.
* @apiName Get encryption key for bch address
* @apiGroup Encryption
* @apiDescription Searches the blockchain for a public key associated with a
* BCH address. Returns an object. If successful, the publicKey property will
* contain a hexidecimal representation of the public key.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/encryption/publickey/bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" -H "accept: application/json"
*
*/
async getPublicKey (req, res, next) {
try {
const address = req.params.address
// Reject if address is an array.
if (Array.isArray(address)) {
res.status(400)
return res.json({
success: false,
error: 'address can not be an array.'
})
}
const cashAddr = _this.bchjs.Address.toCashAddress(address)
// Prevent a common user error. Ensure they are using the correct network address.
const networkIsValid = _this.routeUtils.validateNetwork(cashAddr)
if (!networkIsValid) {
res.status(400)
return res.json({
success: false,
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
wlogger.debug(
'Executing encryption/getPublicKey with this address: ',
cashAddr
)
const rawTxData = await _this.bchjs.Electrumx.transactions(cashAddr)
// console.log(`rawTxData: ${JSON.stringify(rawTxData, null, 2)}`)
// Extract just the TXIDs
const txids = rawTxData.transactions.map((elem) => elem.tx_hash)
// console.log(`txids: ${JSON.stringify(txids, null, 2)}`)
// throw error if there is no transaction history.
if (!txids || txids.length === 0) {
throw new Error('No transaction history.')
}
// Loop through the transaction history and search for the public key.
for (let i = 0; i < txids.length; i++) {
const thisTx = txids[i]
const txDetails = await _this.bchjs.RawTransactions.getRawTransaction(
thisTx,
true
)
// console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`)
const vin = txDetails.vin
// Loop through each input.
for (let j = 0; j < vin.length; j++) {
const thisVin = vin[j]
// console.log(`thisVin: ${JSON.stringify(thisVin, null, 2)}`)
// Extract the script signature.
const scriptSig = thisVin.scriptSig.asm.split(' ')
// console.log(`scriptSig: ${JSON.stringify(scriptSig, null, 2)}`)
// Extract the public key from the script signature.
const pubKey = scriptSig[scriptSig.length - 1]
// console.log(`pubKey: ${pubKey}`)
// Generate cash address from public key.
const keyBuf = Buffer.from(pubKey, 'hex')
const ec = _this.bchjs.ECPair.fromPublicKey(keyBuf)
const cashAddr2 = _this.bchjs.ECPair.toCashAddress(ec)
// console.log(`cashAddr2: ${cashAddr2}`)
// If public keys match, this is the correct public key.
if (cashAddr === cashAddr2) {
res.status(200)
return res.json({
success: true,
publicKey: pubKey
})
}
}
}
res.status(200)
return res.json({
success: false,
publicKey: 'not found'
})
} catch (err) {
wlogger.error('Error in encryption.js/getPublicKey().', err)
return _this.errorHandler(err, res)
}
}
}
module.exports = Encryption
-942
View File
@@ -1,942 +0,0 @@
/*
A library for interacting with the Full Node
*/
'use strict'
const express = require('express')
const router = express.Router()
const axios = require('axios')
const wlogger = require('../../../util/winston-logging')
const RouteUtils = require('../../../util/route-utils')
const routeUtils = new RouteUtils()
// Used to convert error messages to strings, to safely pass to users.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
const BCHJS = require('@psf/bch-js')
const bchjs = new BCHJS()
let _this
class Blockchain {
constructor () {
_this = this
this.bchjs = bchjs
this.axios = axios
this.routeUtils = routeUtils
this.router = router
this.router.get('/', this.root)
this.router.get('/getBestBlockHash', this.getBestBlockHash)
this.router.get('/getBlockchainInfo', this.getBlockchainInfo)
this.router.get('/getBlockCount', this.getBlockCount)
this.router.get('/getBlockHeader/:hash', this.getBlockHeaderSingle)
this.router.post('/getBlockHeader', this.getBlockHeaderBulk)
this.router.get('/getChainTips', this.getChainTips)
this.router.get('/getDifficulty', this.getDifficulty)
this.router.get('/getMempoolEntry/:txid', this.getMempoolEntrySingle)
this.router.post('/getMempoolEntry', this.getMempoolEntryBulk)
this.router.get(
'/getMempoolAncestors/:txid',
this.getMempoolAncestorsSingle
)
this.router.get('/getMempoolInfo', this.getMempoolInfo)
this.router.get('/getRawMempool', this.getRawMempool)
this.router.get('/getTxOut/:txid/:n', this.getTxOut)
this.router.post('/getTxOut', this.getTxOutPost)
this.router.get('/getTxOutProof/:txid', this.getTxOutProofSingle)
this.router.post('/getTxOutProof', this.getTxOutProofBulk)
this.router.get('/verifyTxOutProof/:proof', this.verifyTxOutProofSingle)
this.router.post('/verifyTxOutProof', this.verifyTxOutProofBulk)
}
root (req, res, next) {
return res.json({ status: 'blockchain' })
}
// DRY error handler.
errorHandler (err, res) {
// Attempt to decode the error message.
const { msg, status } = _this.routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
res.status(500)
return res.json({ error: util.inspect(err) })
}
/**
* @api {get} /blockchain/getBestBlockHash Get best block hash
* @apiName GetBestBlockHash
* @apiGroup Blockchain
* @apiDescription Returns the hash of the best (tip) block in the longest
* block chain.
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getBestBlockHash" -H "accept: application/json"
*
* @apiSuccess {String} bestBlockHash 000000000000000002bc884334336d99c9a9c616670a9244c6a8c1fc35aa91a1
*/
async getBestBlockHash (req, res, next) {
try {
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getbestblockhash'
options.data.method = 'getbestblockhash'
options.data.params = []
const response = await _this.axios.request(options)
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
wlogger.error('Error in blockchain.ts/getBestBlockHash().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /blockchain/getBlockchainInfo Get blockchain info
* @apiName GetBlockchainInfo
* @apiGroup Blockchain
* @apiDescription Returns an object containing various state info regarding blockchain processing.
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getBlockchainInfo" -H "accept: application/json"
*
* @apiSuccess {Object} object Object containing data
* @apiSuccess {String} object.chain "main"
* @apiSuccess {Number} object.blocks 561838
* @apiSuccess {Number} object.headers 561838
* @apiSuccess {String} object.bestblockhash "000000000000000002307dd38cd01c7308b8febfcdf5772cf087b5bb023d55bc"
* @apiSuccess {Number} object.difficulty 246585566638.1496
* @apiSuccess {String} object.mediantime 1545402693
* @apiSuccess {Number} object.verificationprogress 0.999998831622689
* @apiSuccess {Boolean} object.chainwork "000000000000000000000000000000000000000000d8c09a8ab7262080266b3e"
* @apiSuccess {Number} object.pruned false
* @apiSuccess {Array} object.softforks Array of objects
* @apiSuccess {String} object.softforks.id "bip34"
* @apiSuccess {String} object.softforks.version 2
* @apiSuccess {Object} object.softforks.reject
* @apiSuccess {String} object.softforks.reject.status true
*/
async getBlockchainInfo (req, res, next) {
try {
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getblockchaininfo'
options.data.method = 'getblockchaininfo'
options.data.params = []
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
wlogger.error('Error in blockchain.ts/getBlockchainInfo().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /blockchain/getBlockCount Get Block Count
* @apiName GetBlockCount
* @apiGroup Blockchain
* @apiDescription Returns the number of blocks in the longest blockchain.
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getBlockCount" -H "accept: application/json"
*
* @apiSuccess {Number} bestBlockCount 587665
*/
async getBlockCount (req, res, next) {
try {
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getblockcount'
options.data.method = 'getblockcount'
options.data.params = []
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/getBlockCount().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /blockchain/getBlockHeader/:hash Get single block header
* @apiName GetSingleBlockHeader
* @apiGroup Blockchain
* @apiDescription If verbose is false (default), returns a string that is
* serialized, hex-encoded data for blockheader 'hash'. If verbose is true,
* returns an Object with information about blockheader hash.
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getBlockHeader/000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201?verbose=true" -H "accept: application/json"
*
* @apiParam {String} hash block hash
* @apiParam {Boolean} verbose Return verbose data
*
* @apiSuccess {Object} object Object containing data
* @apiSuccess {String} object.hash "000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201"
* @apiSuccess {Number} object.confirmations 61839
* @apiSuccess {Number} object.height 500000
* @apiSuccess {Number} object.version 536870912
* @apiSuccess {String} object.versionHex "20000000"
* @apiSuccess {String} object.merkleroot "4af279645e1b337e655ae3286fc2ca09f58eb01efa6ab27adedd1e9e6ec19091"
* @apiSuccess {Number} object.time 1509343584
* @apiSuccess {Number} object.mediantime 1509336533
* @apiSuccess {Number} object.nonce 3604508752
* @apiSuccess {String} object.bits "1809b91a"
* @apiSuccess {Number} object.difficulty 113081236211.4533
* @apiSuccess {String} object.chainwork "0000000000000000000000000000000000000000007ae48aca46e3b449ad9714"
* @apiSuccess {String} object.previousblockhash "0000000000000000043831d6ebb013716f0580287ee5e5687e27d0ed72e6e523"
* @apiSuccess {String} object.nextblockhash "00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3"
*/
async getBlockHeaderSingle (req, res, next) {
try {
let verbose = false
if (req.query.verbose && req.query.verbose.toString() === 'true') {
verbose = true
}
const hash = req.params.hash
if (!hash || hash === '') {
res.status(400)
return res.json({ error: 'hash can not be empty' })
}
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getblockheader'
options.data.method = 'getblockheader'
options.data.params = [hash, verbose]
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/getBlockHeaderSingle().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {post} /blockchain/getBlockHeader Get multiple block headers
* @apiName GetBulkBlockHeader
* @apiGroup Blockchain
* @apiDescription If verbose is false (default), returns a string that is
* serialized, hex-encoded data for blockheader 'hash'. If verbose is true,
* returns an Object with information about blockheader hash.
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/blockchain/getBlockHeader" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"hashes\":[\"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201\",\"00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3\"],\"verbose\":true}"
*
* @apiParam {String} hash block hash
* @apiParam {Boolean} verbose Return verbose data
*
* @apiSuccess {Array} array array containing objects
* @apiSuccess {String} object.hash "000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201"
* @apiSuccess {Number} object.confirmations 61839
* @apiSuccess {Number} object.height 500000
* @apiSuccess {Number} object.version 536870912
* @apiSuccess {String} object.versionHex "20000000"
* @apiSuccess {String} object.merkleroot "4af279645e1b337e655ae3286fc2ca09f58eb01efa6ab27adedd1e9e6ec19091"
* @apiSuccess {Number} object.time 1509343584
* @apiSuccess {Number} object.mediantime 1509336533
* @apiSuccess {Number} object.nonce 3604508752
* @apiSuccess {String} object.bits "1809b91a"
* @apiSuccess {Number} object.difficulty 113081236211.4533
* @apiSuccess {String} object.chainwork "0000000000000000000000000000000000000000007ae48aca46e3b449ad9714"
* @apiSuccess {String} object.previousblockhash "0000000000000000043831d6ebb013716f0580287ee5e5687e27d0ed72e6e523"
* @apiSuccess {String} object.nextblockhash "00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3"
*/
async getBlockHeaderBulk (req, res, next) {
try {
const hashes = req.body.hashes
const verbose = req.body.verbose ? req.body.verbose : false
if (!Array.isArray(hashes)) {
res.status(400)
return res.json({
error: 'hashes needs to be an array. Use GET for single hash.'
})
}
// Enforce array size rate limits
if (!routeUtils.validateArraySize(req, hashes)) {
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({
error: 'Array too large.'
})
}
wlogger.debug(
'Executing blockchain/getBlockHeaderBulk with these hashes: ',
hashes
)
// Validate each hash in the array.
for (let i = 0; i < hashes.length; i++) {
const hash = hashes[i]
if (hash.length !== 64) {
res.status(400)
return res.json({ error: `This is not a hash: ${hash}` })
}
}
// Axios options
const options = _this.routeUtils.getAxiosOptions()
// Loop through each hash and creates an array of requests to call in parallel
const promises = hashes.map(async (hash) => {
options.data.id = 'getblockheader'
options.data.method = 'getblockheader'
options.data.params = [hash, verbose]
return _this.axios.request(options)
})
const axiosResult = await _this.axios.all(promises)
// Extract the data component from the axios response.
const result = axiosResult.map((x) => x.data.result)
res.status(200)
return res.json(result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/getBlockHeaderBulk().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /blockchain/getChainTips Get Chain Tips
* @apiName getChainTips
* @apiGroup Blockchain
* @apiDescription Return information about all known tips in the block tree,
* including the main chain as well as orphaned branches.
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getChainTips" -H "accept: application/json"
*
*/
async getChainTips (req, res, next) {
try {
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getchaintips'
options.data.method = 'getchaintips'
options.data.params = []
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/getChainTips().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /blockchain/getDifficulty Get difficulty
* @apiName getDifficulty
* @apiGroup Blockchain
* @apiDescription Get the current difficulty value, used to regulate mining
* power on the network.
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getDifficulty" -H "accept: application/json"
*
*/
async getDifficulty (req, res, next) {
try {
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getdifficulty'
options.data.method = 'getdifficulty'
options.data.params = []
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/getDifficulty().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /blockchain/getMempoolEntry/:txid Get single mempool entry
* @apiName getMempoolEntry
* @apiGroup Blockchain
* @apiDescription Returns mempool data for given transaction. TXID must be in
* mempool (unconfirmed)
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getMempoolEntry/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
*
*/
async getMempoolEntrySingle (req, res, next) {
try {
// Validate input parameter
const txid = req.params.txid
if (!txid || txid === '') {
res.status(400)
return res.json({ error: 'txid can not be empty' })
}
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getmempoolentry'
options.data.method = 'getmempoolentry'
options.data.params = [txid]
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/getMempoolEntrySingle().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {post} /blockchain/getMempoolEntry Get bulk mempool entry
* @apiName getMempoolEntryBulk
* @apiGroup Blockchain
* @apiDescription Returns mempool data for multiple transactions
*
* @apiExample Example usage:
* curl -X POST https://api.fullstack.cash/v3/blockchain/getMempoolEntry -H "Content-Type: application/json" -d "{\"txids\":[\"a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1\",\"5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e\"]}"
*/
async getMempoolEntryBulk (req, res, next) {
try {
const txids = req.body.txids
if (!Array.isArray(txids)) {
res.status(400)
return res.json({
error: 'txids needs to be an array. Use GET for single txid.'
})
}
// Enforce array size rate limits
if (!routeUtils.validateArraySize(req, txids)) {
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({
error: 'Array too large.'
})
}
wlogger.debug(
'Executing blockchain/getMempoolEntry with these txids: ',
txids
)
// Validate each element in the array
for (let i = 0; i < txids.length; i++) {
const txid = txids[i]
if (txid.length !== 64) {
res.status(400)
return res.json({ error: 'This is not a txid' })
}
}
// Axios options
const options = _this.routeUtils.getAxiosOptions()
// Loop through each txid and creates an array of requests to call in parallel
const promises = txids.map(async (txid) => {
options.data.id = 'getmempoolentry'
options.data.method = 'getmempoolentry'
options.data.params = [txid]
return _this.axios.request(options)
})
const axiosResult = await _this.axios.all(promises)
// Extract the data component from the axios response.
const result = axiosResult.map((x) => x.data.result)
res.status(200)
return res.json(result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/getMempoolEntryBulk().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /blockchain/getMempoolAncestors/:txid Get Mempool Ancestors
* @apiName getMempoolAncestors
* @apiGroup Blockchain
* @apiDescription Returns mempool ancestors data for given TXID. It must be in
* mempool (unconfirmed). This call is handy to tell if a UTXO is bumping up
* against the 25 ancestor chain-limit.
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getMempoolAncestors/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
*
*/
async getMempoolAncestorsSingle (req, res, next) {
try {
// Validate input parameter
const txid = req.params.txid
if (!txid || txid === '') {
res.status(400)
return res.json({ error: 'txid can not be empty' })
}
let verbose = req.params.verbose
if (verbose === undefined) verbose = false
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getmempoolancestors'
options.data.method = 'getmempoolancestors'
options.data.params = [txid, verbose]
const response = await _this.axios.request(options)
// console.log(`response: ${util.inspect(response)}`)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/getMempoolAncestorsSingle().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /blockchain/getMempoolInfo Get mempool info
* @apiName getMempoolInfo
* @apiGroup Blockchain
* @apiDescription Returns details on the active state of the TX memory pool.
*
* @apiExample Example usage:
* curl -X GET https://api.fullstack.cash/v3/getMempoolInfo -H "accept: application/json"
*
*/
async getMempoolInfo (req, res, next) {
try {
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getmempoolinfo'
options.data.method = 'getmempoolinfo'
options.data.params = []
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/getMempoolInfo().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /blockchain/getRawMempool Get mempool info
* @apiName getMempoolInfo
* @apiGroup Blockchain
* @apiDescription Returns details on the active state of the TX memory pool.
*
* @apiExample Example usage:
* curl -X GET https://api.fullstack.cash/v3/getMempoolInfo -H "accept: application/json"
*
*/
/**
* @api {get} /blockchain/getRawMempool/?verbose= Get raw mempool
* @apiName getRawMempool
* @apiGroup Blockchain
* @apiDescription Returns all transaction ids in memory pool as a json array
* of string transaction ids.
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/getRawMempool/?verbose=true" -H "accept: application/json"
*
* @apiParam {Boolean} verbose Return verbose data
*
*/
async getRawMempool (req, res, next) {
try {
// Axios options
const options = _this.routeUtils.getAxiosOptions()
let verbose = false
if (req.query.verbose && req.query.verbose === 'true') verbose = true
options.data.id = 'getrawmempool'
options.data.method = 'getrawmempool'
options.data.params = [verbose]
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/getRawMempool().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /blockchain/getTxOut/:txid/:n?mempool= Get Tx Out
* @apiName getTxOut
* @apiGroup Blockchain
* @apiDescription Returns details about an unspent transaction output.
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getTxOut/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33/0?mempool=false" -H "accept: application/json"
*
* @apiParam {String} txid Transaction id (required)
* @apiParam {Number} n Output number (required)
* @apiParam {Boolean} mempool Check mempool or not (optional)
*
*/
// Returns details about an unspent transaction output.
async getTxOut (req, res, next) {
try {
// Validate input parameter
const txid = req.params.txid
if (!txid || txid === '') {
res.status(400)
return res.json({ error: 'txid can not be empty' })
}
let n = req.params.n
if (n === undefined || n === '') {
res.status(400)
return res.json({ error: 'n can not be empty' })
}
n = parseInt(n)
let includeMempool = false
if (req.query.includeMempool && req.query.includeMempool === 'true') {
includeMempool = true
}
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'gettxout'
options.data.method = 'gettxout'
options.data.params = [txid, n, includeMempool]
// console.log(`requestConfig: ${JSON.stringify(requestConfig, null, 2)}`)
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/getTxOut().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {post} /blockchain/getTxOut Validate a UTXO
* @apiName getTxOut
* @apiGroup Blockchain
* @apiDescription Returns details about an unspent transaction output (UTXO).
*
* @apiExample Example usage:
* curl "https://api.fullstack.cash/v3/blockchain/getTxOut/" -X POST -H "Content-Type: application/json" --data-binary '{"txid":"d5228d2cdc77fbe5a9aa79f19b0933b6802f9f0067f42847fc4fe343664723e5","vout":0,"mempool":true}'
*
* @apiParam {String} txid Transaction id (required)
* @apiParam {Number} vout of transaction (required)
* @apiParam {Boolean} mempool Check mempool or not (optional)
*
*/
// Returns details about an unspent transaction output.
async getTxOutPost (req, res, next) {
try {
const txid = req.body.txid
let n = req.body.vout
const mempool = req.body.mempool ? req.body.mempool : true
// Validate input parameter
if (!txid || txid === '') {
res.status(400)
return res.json({ error: 'txid can not be empty' })
}
if (n === undefined || n === '') {
res.status(400)
return res.json({ error: 'vout can not be empty' })
}
n = parseInt(n)
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'gettxout'
options.data.method = 'gettxout'
options.data.params = [txid, n, mempool]
// console.log(`requestConfig: ${JSON.stringify(requestConfig, null, 2)}`)
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/getTxOutPost().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /blockchain/getTxOutProofSingle/:txid Get Tx Out Proof
* @apiName getTxOutProofSingle
* @apiGroup Blockchain
* @apiDescription Returns a hex-encoded proof that 'txid' was included in a block.
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getTxOutProofSingle/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
*
* @apiParam {String} txid Transaction id (required)
*
*/
async getTxOutProofSingle (req, res, next) {
try {
// Validate input parameter
const txid = req.params.txid
if (!txid || txid === '') {
res.status(400)
return res.json({ error: 'txid can not be empty' })
}
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'gettxoutproof'
options.data.method = 'gettxoutproof'
options.data.params = [[txid]]
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/getTxOutProofSingle().', err)
return _this.errorHandler(err, res)
}
}
// Returns a hex-encoded proof that 'txid' was included in a block.
async getTxOutProofBulk (req, res, next) {
try {
const txids = req.body.txids
// Reject if txids is not an array.
if (!Array.isArray(txids)) {
res.status(400)
return res.json({
error: 'txids needs to be an array. Use GET for single txid.'
})
}
// Enforce array size rate limits
if (!routeUtils.validateArraySize(req, txids)) {
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({
error: 'Array too large.'
})
}
// Axios options
const options = _this.routeUtils.getAxiosOptions()
// Validate each element in the array.
for (let i = 0; i < txids.length; i++) {
const txid = txids[i]
if (txid.length !== 64) {
res.status(400)
return res.json({
error: `Invalid txid. Double check your txid is valid: ${txid}`
})
}
}
wlogger.debug(
'Executing blockchain/getTxOutProof with these txids: ',
txids
)
// Loop through each txid and creates an array of requests to call in parallel
const promises = txids.map(async (txid) => {
options.data.id = 'gettxoutproof'
options.data.method = 'gettxoutproof'
options.data.params = [[txid]]
return _this.axios.request(options)
})
// Wait for all parallel promisses to resolve.
const axiosResult = await _this.axios.all(promises)
// Extract the data component from the axios response.
const result = axiosResult.map((x) => x.data.result)
res.status(200)
return res.json(result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/getTxOutProofBulk().', err)
return _this.errorHandler(err, res)
}
}
async verifyTxOutProofSingle (req, res, next) {
try {
// Validate input parameter
const proof = req.params.proof
if (!proof || proof === '') {
res.status(400)
return res.json({ error: 'proof can not be empty' })
}
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'verifytxoutproof'
options.data.method = 'verifytxoutproof'
options.data.params = [req.params.proof]
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/verifyTxOutProofSingle().', err)
return _this.errorHandler(err, res)
}
}
async verifyTxOutProofBulk (req, res, next) {
try {
const proofs = req.body.proofs
// Reject if proofs is not an array.
if (!Array.isArray(proofs)) {
res.status(400)
return res.json({
error: 'proofs needs to be an array. Use GET for single proof.'
})
}
// Enforce array size rate limits
if (!routeUtils.validateArraySize(req, proofs)) {
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({
error: 'Array too large.'
})
}
// Axios options
const options = _this.routeUtils.getAxiosOptions()
// Validate each element in the array.
for (let i = 0; i < proofs.length; i++) {
const proof = proofs[i]
if (!proof || proof === '') {
res.status(400)
return res.json({ error: `proof can not be empty: ${proof}` })
}
}
wlogger.debug(
'Executing blockchain/verifyTxOutProof with these proofs: ',
proofs
)
// Loop through each proof and creates an array of requests to call in parallel
const promises = proofs.map(async (proof) => {
options.data.id = 'verifytxoutproof'
options.data.method = 'verifytxoutproof'
options.data.params = [proof]
return _this.axios.request(options)
})
// Wait for all parallel promisses to resolve.
const axiosResult = await _this.axios.all(promises)
// Extract the data component from the axios response.
const result = axiosResult.map((x) => x.data.result[0])
res.status(200)
return res.json(result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/verifyTxOutProofBulk().', err)
return _this.errorHandler(err, res)
}
}
}
module.exports = Blockchain
-32
View File
@@ -1,32 +0,0 @@
/*
A library for interacting with the Bitcoin.com ninsight (not Insight) indexer.
*/
'use strict'
const express = require('express')
// const axios = require('axios')
// const routeUtils = require('./route-utils')
// const wlogger = require('../../util/winston-logging')
const router = express.Router()
// const BCHJS = require('@psf/bch-js')
// const bchjs = new BCHJS()
// let _this
class Ninsight {
constructor () {
// _this = this
this.router = router
this.router.get('/', this.root)
}
root (req, res, next) {
return res.json({ status: 'ninsight' })
}
}
module.exports = Ninsight
-155
View File
@@ -1,155 +0,0 @@
/*
This price route is really just a wrapper for another API price endpoint.
The main reason for hosting this price wrapper is so that the price can
be accessible over Tor. Tor is typically blocked by other REST API servers.
*/
'use strict'
const express = require('express')
const axios = require('axios')
const wlogger = require('../../util/winston-logging')
const util = require('util')
const RouteUtils = require('../../util/route-utils')
const routeUtils = new RouteUtils()
let _this // Global context for 'this' instance of the Class.
class Price {
constructor () {
_this = this
this.axios = axios
this.routeUtils = routeUtils
this.priceUrl = 'https://api.coinbase.com/v2/exchange-rates?currency=BCH'
this.coinexPriceUrl =
'https://api.coinex.com/v1/market/ticker?market=bchausdt'
this.router = express.Router()
this.router.get('/', _this.root)
this.router.get('/usd', _this.getUSD)
this.router.get('/rates', _this.getBCHRate)
this.router.get('/bchausd', _this.getBCHAUSD)
}
// DRY error handler.
errorHandler (err, res) {
// Attempt to decode the error message.
const { msg, status } = _this.routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
res.status(500)
return res.json({ error: util.inspect(err) })
}
// Root API endpoint. Simply acknowledges that it exists.
root (req, res, next) {
return res.json({ status: 'price' })
}
/**
* @api {get} /price/usd Get the USD price of BCH
* @apiName Get the USD price of BCH
* @apiGroup Price
* @apiDescription Get the USD price of BCH from Coinbase.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/price/usd" -H "accept: application/json"
*
*/
async getUSD (req, res, next) {
try {
// Request options
const opt = {
method: 'get',
baseURL: _this.priceUrl,
timeout: 15000
}
const response = await axios.request(opt)
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
return res.json({ usd: Number(response.data.data.rates.USD) })
} catch (err) {
// Write out error to error log.
wlogger.error('Error in price.js/getUSD().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /price/usd Get rates for several different currencies
* @apiName Get rates for several different currencies
* @apiGroup Price
* @apiDescription Get rates for several different currencies from Coinbase.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/price/rates" -H "accept: application/json"
*
*/
// Get rates for several different currencies
async getBCHRate (req, res, next) {
try {
// Request options
const opt = {
method: 'get',
baseURL: _this.priceUrl,
timeout: 15000
}
const response = await axios.request(opt)
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
return res.json(response.data.data.rates)
} catch (err) {
// Write out error to error log.
wlogger.error('Error in price.js/getUSD().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /price/bchausd Get the USD price of BCHA
* @apiName Get the USD price of BCHA
* @apiGroup Price
* @apiDescription Get the USD price of BCHA from Coinex.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/price/bchausd" -H "accept: application/json"
*
*/
async getBCHAUSD (req, res, next) {
try {
// Request options
const opt = {
method: 'get',
baseURL: _this.coinexPriceUrl,
timeout: 15000
}
const response = await axios.request(opt)
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
const price = Number(response.data.data.ticker.last)
return res.json({ usd: price })
} catch (err) {
// Write out error to error log.
wlogger.error('Error in price.js/getBCHAUSD().', err)
return _this.errorHandler(err, res)
}
}
}
module.exports = Price
-183
View File
@@ -1,183 +0,0 @@
/*
A private library of utility functions used by several different routes.
*/
'use strict'
const axios = require('axios')
const wlogger = require('../../util/winston-logging')
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
const BCHJS = require('@psf/bch-js')
const bchjs = new BCHJS()
module.exports = {
validateNetwork, // Prevents a common user error
setEnvVars, // Allows RPC variables to be set dynamically based on changing env vars.
decodeError, // Extract and interpret error messages.
validateArraySize, // Ensure the passed array meets rate limiting requirements.
getAxiosOptions
}
// This function expects the Request Express.js object and an array as input.
// The array is then validated against freemium and pro-tier rate limiting
// requirements. A boolean is returned to indicate if the array size if valid
// or not.
function validateArraySize (req, array) {
const FREEMIUM_INPUT_SIZE = 20
const PRO_INPUT_SIZE = 20
if (req.locals && req.locals.proLimit) {
if (array.length <= PRO_INPUT_SIZE) return true
} else if (array.length <= FREEMIUM_INPUT_SIZE) {
return true
}
return false
}
// Returns true if user-provided cash address matches the correct network,
// mainnet or testnet. If NETWORK env var is not defined, it returns false.
// This prevent a common user-error issue that is easy to make: passing a
// testnet address into rest.bitcoin.com or passing a mainnet address into
// trest.bitcoin.com.
function validateNetwork (addr) {
try {
const network = process.env.NETWORK
// Return false if NETWORK is not defined.
if (!network || network === '') {
console.log('Warning: NETWORK environment variable is not defined!')
return false
}
// Convert the user-provided address to a cashaddress, for easy detection
// of the intended network.
const cashAddr = bchjs.Address.toCashAddress(addr)
// Return true if the network and address both match testnet
const addrIsTest = bchjs.Address.isTestnetAddress(cashAddr)
if (network === 'testnet' && addrIsTest) return true
// Return true if the network and address both match mainnet
const addrIsMain = bchjs.Address.isMainnetAddress(cashAddr)
if (network === 'mainnet' && addrIsMain) return true
return false
} catch (err) {
wlogger.error('Error in validateNetwork()')
return false
}
}
// Dynamically set these based on env vars. Allows unit testing.
function setEnvVars () {
const BitboxHTTP = axios.create({
baseURL: process.env.RPC_BASEURL,
timeout: 15000
})
const username = process.env.RPC_USERNAME
const password = process.env.RPC_PASSWORD
const requestConfig = {
method: 'post',
auth: {
username: username,
password: password
},
data: {
jsonrpc: '1.0'
}
}
return { BitboxHTTP, username, password, requestConfig }
}
// Axios options used when calling axios.post() to talk with a full node.
function getAxiosOptions () {
return {
method: 'post',
baseURL: process.env.RPC_BASEURL,
timeout: 15000,
auth: {
username: process.env.RPC_USERNAME,
password: process.env.RPC_PASSWORD
},
data: {
jsonrpc: '1.0'
}
}
}
// Error messages returned by a full node can be burried pretty deep inside the
// error object returned by Axios. This function attempts to extract and interpret
// error messages.
// Returns an object. If successful, obj.msg is a string.
// If there is a failure, obj.msg is false.
function decodeError (err) {
try {
// Attempt to extract the full node error message.
if (
err.response &&
err.response.data &&
err.response.data.error &&
err.response.data.error.message
) {
return { msg: err.response.data.error.message, status: 400 }
}
// Attempt to extract the Insight error message
if (err.response && err.response.data) {
return { msg: err.response.data, status: err.response.status }
}
// console.log(`err.message: ${err.message}`)
// console.log(`err: `, err)
// Attempt to detect a network connection error.
if (err.message && err.message.indexOf('ENOTFOUND') > -1) {
return {
msg:
'Network error: Could not communicate with full node or other external service.',
status: 503
}
}
// Different kind of network error
if (err.message && err.message.indexOf('ENETUNREACH') > -1) {
return {
msg:
'Network error: Could not communicate with full node or other external service.',
status: 503
}
}
// Different kind of network error
if (err.message && err.message.indexOf('EAI_AGAIN') > -1) {
return {
msg:
'Network error: Could not communicate with full node or other external service.',
status: 503
}
}
// Axios timeout (aborted) error, or service is down (connection refused).
if (
err.code &&
(err.code === 'ECONNABORTED' || err.code === 'ECONNREFUSED')
) {
return {
msg:
'Network error: Could not communicate with full node or other external service.',
status: 503
}
}
return { msg: false, status: 500 }
} catch (err) {
wlogger.error('unhandled error in route-utils.js/decodeError(): ', err)
return { msg: false, status: 500 }
}
}
-351
View File
@@ -1,351 +0,0 @@
const axios = require('axios')
const SLPSDK = require('@psf/bch-js')
const SLP = new SLPSDK()
class Slpdb {
// Gets transaction history for all tokens for an address. Can also specify
// block height, but defaults to 0.
async getHistoricalSlpTransactions (addressList, fromBlock = 0) {
// Build SLPDB or query from addressList
const orQueryArray = []
for (const address of addressList) {
const cashAddress = SLP.SLP.Address.toCashAddress(address)
const slpAddress = SLP.SLP.Address.toSLPAddress(address)
const cashQuery = {
'in.e.a': cashAddress.slice(12)
}
const slpQuery = {
'slp.detail.outputs.address': slpAddress
}
orQueryArray.push(cashQuery)
orQueryArray.push(slpQuery)
}
const query = {
v: 3,
q: {
find: {
db: ['c', 'u'],
$query: {
$or: orQueryArray,
'slp.valid': true,
'blk.i': {
$not: {
$lte: fromBlock
}
}
},
$orderby: {
'blk.i': -1
}
},
project: {
_id: 0,
'tx.h': 1,
'in.i': 1,
'in.e': 1,
'out.e': 1,
'out.a': 1,
'slp.detail': 1,
blk: 1
},
limit: 500
}
}
const result = await this.runQuery(query)
// console.log(`result.data: ${JSON.stringify(result.data, null, 2)}`)
let transactions = []
// Add confirmed transactions
if (result.data && result.data.c) {
transactions = transactions.concat(result.data.c)
}
// Add unconfirmed transactions
if (result.data && result.data.u) {
transactions = transactions.concat(result.data.u)
}
return transactions
}
async getTokenStats (tokenId) {
const [
totalMinted,
totalBurned,
tokenDetails,
circulatingSupply
] = await Promise.all([
this.getTotalMinted(tokenId),
this.getTotalBurned(tokenId),
this.getTokenDetails(tokenId),
this.getTotalCirculating(tokenId)
])
tokenDetails.totalMinted = tokenDetails.initialTokenQty + totalMinted
tokenDetails.totalBurned = totalBurned
// tokenDetails.circulatingSupply =
// tokenDetails.totalMinted - tokenDetails.totalBurned
tokenDetails.circulatingSupply = circulatingSupply
return tokenDetails
}
generateCredentials () {
// Generate the Basic Authentication header for a private instance of SLPDB.
const SLPDB_PASS = process.env.SLPDB_PASS
? process.env.SLPDB_PASS
: 'BITBOX'
const username = 'BITBOX'
const password = SLPDB_PASS
const combined = `${username}:${password}`
var base64Credential = Buffer.from(combined).toString('base64')
var readyCredential = `Basic ${base64Credential}`
const options = {
headers: {
authorization: readyCredential,
timeout: 30000
}
}
return options
}
async runQuery (query) {
const queryString = JSON.stringify(query)
const queryBase64 = Buffer.from(queryString).toString('base64')
const url = `${process.env.SLPDB_URL}q/${queryBase64}`
const options = this.generateCredentials()
const response = await axios.get(url, options)
return response
}
async getTotalMinted (tokenId) {
const query = {
v: 3,
q: {
db: ['g'],
aggregate: [
{
$match: {
'tokenDetails.tokenIdHex': tokenId,
'graphTxn.outputs.status': {
$in: [
'BATON_SPENT_IN_MINT',
'BATON_UNSPENT',
'BATON_SPENT_NOT_IN_MINT'
]
}
}
},
{
$unwind: '$graphTxn.outputs'
},
{
$group: {
_id: null,
count: {
$sum: '$graphTxn.outputs.slpAmount'
}
}
}
],
limit: 1
}
}
const result = await this.runQuery(query)
if (!result.data.g.length) {
return 0
}
return parseFloat(result.data.g[0].count)
}
async getTotalCirculating (tokenId) {
const query = {
v: 3,
q: {
db: ['g'],
aggregate: [
{
$match: {
'tokenDetails.tokenIdHex': tokenId,
'graphTxn.outputs': {
$elemMatch: {
status: 'UNSPENT',
slpAmount: { $gte: 0 }
}
}
}
},
{ $unwind: '$graphTxn.outputs' },
{
$match: {
'graphTxn.outputs.status': 'UNSPENT',
'graphTxn.outputs.slpAmount': { $gte: 0 }
}
},
{
$group: {
_id: null,
circulating_supply: {
$sum: '$graphTxn.outputs.slpAmount'
}
}
}
],
limit: 100000
}
}
const result = await this.runQuery(query)
// console.log(`result.data: ${JSON.stringify(result.data, null, 2)}`)
if (!result.data.g.length) {
return 0
}
return parseFloat(result.data.g[0].circulating_supply)
}
async getTotalBurned (tokenId) {
const query = {
v: 3,
q: {
db: ['g'],
aggregate: [
{
$match: {
'tokenDetails.tokenIdHex': tokenId,
'graphTxn.outputs.status': {
$in: [
'SPENT_NON_SLP',
'BATON_SPENT_INVALID_SLP',
'SPENT_INVALID_SLP',
'BATON_SPENT_NON_SLP',
'MISSING_BCH_VOUT',
'BATON_MISSING_BCH_VOUT',
'BATON_SPENT_NOT_IN_MINT',
'EXCESS_INPUT_BURNED'
]
}
}
},
{
$unwind: '$graphTxn.outputs'
},
{
$match: {
'graphTxn.outputs.status': {
$in: [
'SPENT_NON_SLP',
'BATON_SPENT_INVALID_SLP',
'SPENT_INVALID_SLP',
'BATON_SPENT_NON_SLP',
'MISSING_BCH_VOUT',
'BATON_MISSING_BCH_VOUT',
'BATON_SPENT_NOT_IN_MINT',
'EXCESS_INPUT_BURNED'
]
}
}
},
{
$group: {
_id: null,
count: {
$sum: '$graphTxn.outputs.slpAmount'
}
}
}
],
limit: 1
}
}
const result = await this.runQuery(query)
if (!result.data.g.length) {
return 0
}
return parseFloat(result.data.g[0].count)
}
async getTokenDetails (tokenId) {
const query = {
v: 3,
q: {
db: ['t'],
find: {
$query: {
'tokenDetails.tokenIdHex': tokenId
}
},
project: { tokenDetails: 1, tokenStats: 1, _id: 0 },
limit: 1
}
}
const result = await this.runQuery(query)
if (!result.data.t.length) {
throw new Error('Token could not be found')
}
const token = this.formatTokenOutput(result.data.t[0])
return token
}
formatTokenOutput (token) {
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
token.tokenDetails.id = token.tokenDetails.tokenIdHex
delete token.tokenDetails.tokenIdHex
token.tokenDetails.documentHash = token.tokenDetails.documentSha256Hex
delete token.tokenDetails.documentSha256Hex
token.tokenDetails.initialTokenQty = parseFloat(
token.tokenDetails.genesisOrMintQuantity
)
delete token.tokenDetails.genesisOrMintQuantity
delete token.tokenDetails.transactionType
delete token.tokenDetails.batonVout
delete token.tokenDetails.sendOutputs
token.tokenDetails.blockCreated = token.tokenStats.block_created
token.tokenDetails.blockLastActiveSend =
token.tokenStats.block_last_active_send
token.tokenDetails.blockLastActiveMint =
token.tokenStats.block_last_active_mint
token.tokenDetails.txnsSinceGenesis =
token.tokenStats.qty_valid_txns_since_genesis
token.tokenDetails.validAddresses =
token.tokenStats.qty_valid_token_addresses
token.tokenDetails.mintingBatonStatus =
token.tokenStats.minting_baton_status
delete token.tokenStats.block_last_active_send
delete token.tokenStats.block_last_active_mint
delete token.tokenStats.qty_valid_txns_since_genesis
delete token.tokenStats.qty_valid_token_addresses
token.tokenDetails.timestampUnix = token.tokenDetails.timestamp_unix
delete token.tokenDetails.timestamp_unix
return token.tokenDetails
}
}
module.exports = Slpdb
-1690
View File
File diff suppressed because it is too large Load Diff
-604
View File
@@ -1,604 +0,0 @@
'use strict'
const express = require('express')
const router = express.Router()
const axios = require('axios')
const routeUtils = require('./route-utils')
const wlogger = require('../../util/winston-logging')
const Blockbook = require('./blockbook')
const blockbook = new Blockbook()
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
const BCHJS = require('@psf/bch-js')
const bchjs = new BCHJS()
// const BCHJS_TESTNET = 'https://testnet.bchjs.cash/v3/'
// const bchjsHTTP = axios.create({
// baseURL: process.env.RPC_BASEURL
// })
// const username = process.env.RPC_USERNAME
// const password = process.env.RPC_PASSWORD
// const requestConfig = {
// method: 'post',
// auth: {
// username: username,
// password: password
// },
// data: {
// jsonrpc: '1.0'
// }
// }
let _this
class UtilRoute {
constructor () {
this.bchjs = bchjs
this.blockbook = blockbook
_this = this
}
root (req, res, next) {
return res.json({ status: 'util' })
}
/**
* @api {get} /util/validateAddress/{address} Get information about single bitcoin cash address.
* @apiName Information about single bitcoin cash address
* @apiGroup Util
* @apiDescription Returns information about single bitcoin cash address.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/util/validateAddress/bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c" -H "accept: application/json"
*
*
*/
async validateAddressSingle (req, res, next) {
try {
const address = req.params.address
if (!address || address === '') {
res.status(400)
return res.json({ error: 'address can not be empty' })
}
const {
BitboxHTTP,
// username,
// password,
requestConfig
} = routeUtils.setEnvVars()
requestConfig.data.id = 'validateaddress'
requestConfig.data.method = 'validateaddress'
requestConfig.data.params = [address]
const response = await BitboxHTTP(requestConfig)
return res.json(response.data.result)
} catch (err) {
// Attempt to decode the error message.
const { msg, status } = routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
wlogger.error('Error in util.ts/validateAddressSingle().', err)
res.status(500)
return res.json({ error: util.inspect(err) })
}
}
/**
* @api {post} /util/validateAddress Get information about bulk bitcoin cash addresses..
* @apiName Information about bulk bitcoin cash addresses.
* @apiGroup Util
* @apiDescription Returns information about bulk bitcoin cash addresses..
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/util/validateAddress" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c","bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0"]}'
* curl -X POST "https://api.fullstack.cash/v3/util/validateAddress" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c","bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0"],"from": 1, "to": 5}'
*
*
*/
async validateAddressBulk (req, res, next) {
try {
const addresses = req.body.addresses
// Reject if addresses is not an array.
if (!Array.isArray(addresses)) {
res.status(400)
return res.json({
error: 'addresses needs to be an array. Use GET for single address.'
})
}
// Enforce array size rate limits
if (!routeUtils.validateArraySize(req, addresses)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
error: 'Array too large.'
})
}
// Validate each element in the array.
for (let i = 0; i < addresses.length; i++) {
const address = addresses[i]
// Ensure the input is a valid BCH address.
try {
bchjs.Address.toLegacyAddress(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 networkIsValid = routeUtils.validateNetwork(address)
if (!networkIsValid) {
res.status(400)
return res.json({
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
}
wlogger.debug('Executing util/validate with these addresses: ', addresses)
const {
BitboxHTTP,
// username,
// password,
requestConfig
} = routeUtils.setEnvVars()
// Loop through each address and creates an array of requests to call in parallel
const promises = addresses.map(async address => {
requestConfig.data.id = 'validateaddress'
requestConfig.data.method = 'validateaddress'
requestConfig.data.params = [address]
return BitboxHTTP(requestConfig)
})
// Wait for all parallel Insight requests to return.
const axiosResult = await axios.all(promises)
// Retrieve the data part of the result.
const result = axiosResult.map(x => x.data.result)
res.status(200)
return res.json(result)
} catch (err) {
// Attempt to decode the error message.
const { msg, status } = routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
wlogger.error('Error in util.ts/validateAddressSingle().', err)
res.status(500)
return res.json({ error: util.inspect(err) })
}
}
/**
* @api {post} /util/sweep Sweep BCH and tokens
* @apiName Sweep BCH and tokens from a paper wallet
* @apiGroup Util
* @apiDescription This function can be used to check the BCH balance of a
* paper wallet. It can also be used to sweep BCH and tokens from a paper
* wallet and send them to a destination address.
*
* Note: It does not yet support multiple token classes on the same paper wallet.
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/util/sweep" -H "accept: application/json" -H "Content-Type: application/json" -d '{"wif":"Kz52sdXLAiKtH82iAFRi6aTZanWtW5Eyv37KWdpY6tTv7pjoHste", "balanceOnly": true}'
* curl -X POST "https://api.fullstack.cash/v3/util/sweep" -H "accept: application/json" -H "Content-Type: application/json" -d '{"wif":"Kz52sdXLAiKtH82iAFRi6aTZanWtW5Eyv37KWdpY6tTv7pjoHste", "toAddr": "bitcoincash:qpt8m4kqu963geedyrur6pdggqmv5kxwnq0rn322qu"}'
*
*
*/
async sweepWif (req, res, next) {
try {
// Validate input
const wif = req.body.wif
const toAddr = req.body.toAddr
const balanceOnly = req.body.balanceOnly
if (typeof wif !== 'string' || wif.length !== 52) {
res.status(400)
return res.json({
error: 'WIF needs to a proper compressed WIF starting with K or L'
})
}
if (!balanceOnly) {
// Only throw error if balanceOnly is false or undefined.
if (!toAddr || toAddr === '') {
res.status(400)
return res.json({ error: 'address can not be empty' })
}
}
wlogger.debug('Executing util/sweepWif with this address: ', toAddr)
// Generate a private and public key pair from the WIF.
const ecPair = bchjs.ECPair.fromWIF(wif)
const fromAddr = bchjs.ECPair.toCashAddress(ecPair)
// Get a balance on the public address
const balances = await _this.blockbook.balanceFromBlockbook(fromAddr)
// console.log(`balances: ${JSON.stringify(balances, null, 2)}`)
// Total balance is the sum of the confirmed and unconfirmed balance.
const totalBalance =
Number(balances.balance) + Number(balances.unconfirmedBalance)
// Exit if balance is zero.
if (isNaN(totalBalance) || totalBalance === 0) {
res.status(422)
return res.json({ error: 'No balance found at BCH address.' })
}
// Exit if this is a balance-only call.
if (balanceOnly) {
res.status(200)
return res.json(totalBalance)
}
// Get all UTXOs help by the address.
const utxos = await _this.blockbook.utxosFromBlockbook(fromAddr)
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
const tokenUtxos = []
const bchUtxos = []
// Exit if there are no UTXOs.
if (utxos.length === 0) {
res.status(422)
return res.json({ error: 'No utxos found.' })
}
// Figure out which UTXOs are associated with SLP tokens.
const isTokenUtxo = await _this.bchjs.SLP.Utils.tokenUtxoDetails(utxos)
// console.log(`isTokenUtxo: ${JSON.stringify(isTokenUtxo, null, 2)}`)
// Separate the bch and token UTXOs.
for (let i = 0; i < utxos.length; i++) {
// Filter based on isTokenUtxo.
if (!isTokenUtxo[i]) bchUtxos.push(utxos[i])
else tokenUtxos.push(isTokenUtxo[i])
}
// console.log(
// `bchUtxos.length: ${bchUtxos.length}, tokenUtxos.length: ${tokenUtxos.length}`
// )
// Throw error if no BCH to move tokens.
if (bchUtxos.length === 0 && tokenUtxos.length > 0) {
res.status(422)
return res.json({
error:
'Tokens found, but no BCH UTXOs found. Add BCH to wallet to move tokens.'
})
}
// console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`)
const options = {
ecPair,
utxos,
fromAddr,
toAddr,
bchUtxos,
tokenUtxos
}
let hex
// Choose the sweeping algorithm, based on if there are tokens or not.
if (tokenUtxos.length === 0) hex = await _this._sweepBCH(options)
else hex = await _this._sweepTokens(options, bchUtxos, tokenUtxos)
// console.log(`hex: ${hex}`)
// Throw error if there is more than one token class.
// Generate a transaction to move tokens and BCH.
// Broadcast the transaction.
const txid = _this.bchjs.RawTransactions.sendRawTransaction([hex])
res.status(200)
return res.json(txid)
} catch (err) {
// Attempt to decode the error message.
const { msg, status } = routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
// Catch the specific case of multiple tokens.
if (
err.message &&
err.message.indexOf('Multiple token classes detected') > -1
) {
res.status(422)
return res.json({ error: err.message })
}
wlogger.error('Error in util.js/sweepWif().', err)
console.error('Error in util.js/sweepWif().', err)
res.status(500)
return res.json({ error: err.message })
}
}
// Sweep BCH only from a private WIF.
async _sweepBCH (options) {
try {
// const wif = flags.wif
// const toAddr = flags.address
const ecPair = options.ecPair
const toAddr = options.toAddr
// const fromAddr = this.BITBOX.ECPair.toCashAddress(ecPair)
//
// // Get the UTXOs for that address.
// let utxos = await this.BITBOX.Blockbook.utxo(fromAddr)
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
let utxos = options.utxos
// Ensure all utxos have the satoshis property.
utxos = utxos.map(x => {
x.satoshis = Number(x.value)
return x
})
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
// instance of transaction builder
let transactionBuilder
if (options.testnet) {
transactionBuilder = new _this.bchjs.TransactionBuilder('testnet')
} else transactionBuilder = new _this.bchjs.TransactionBuilder()
let originalAmount = 0
// Loop through all UTXOs.
for (let i = 0; i < utxos.length; i++) {
const utxo = utxos[i]
originalAmount = originalAmount + utxo.satoshis
transactionBuilder.addInput(utxo.txid, utxo.vout)
}
if (originalAmount < 546) {
throw new Error(
'Original amount less than the dust limit. Not enough BCH to send.'
)
}
// get byte count to calculate fee. paying 1 sat/byte
const byteCount = _this.bchjs.BitcoinCash.getByteCount(
{ P2PKH: utxos.length },
{ P2PKH: 1 }
)
const fee = Math.ceil(1.1 * byteCount)
// amount to send to receiver. It's the original amount - 1 sat/byte for tx size
const sendAmount = originalAmount - fee
// add output w/ address and amount to send
transactionBuilder.addOutput(
_this.bchjs.Address.toLegacyAddress(toAddr),
sendAmount
)
// Loop through each input and sign
let redeemScript
for (var i = 0; i < utxos.length; i++) {
const utxo = utxos[i]
transactionBuilder.sign(
i,
ecPair,
redeemScript,
transactionBuilder.hashTypes.SIGHASH_ALL,
utxo.satoshis
)
}
// build tx
const tx = transactionBuilder.build()
// output rawhex
const hex = tx.toHex()
return hex
} catch (err) {
wlogger.error('Error in util.js/sweepBCH().')
throw err
}
}
// Sweep BCH and tokens from a WIF.
async _sweepTokens (options) {
try {
// const { ecPair, utxos, fromAddr, toAddr, bchUtxos, tokenUtxos } = options
const { ecPair, utxos, toAddr, bchUtxos, tokenUtxos } = options
// Input validation
if (!Array.isArray(bchUtxos) || bchUtxos.length === 0) {
throw new Error('bchUtxos need to be an array with one UTXO.')
}
if (!Array.isArray(tokenUtxos) || tokenUtxos.length === 0) {
throw new Error('tokenUtxos need to be an array with one UTXO.')
}
// if (flags.testnet)
// this.BITBOX = new config.BCHLIB({ restURL: config.TESTNET_REST })
// console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`)
// Ensure there is only one class of token in the wallet. Throw an error if
// there is more than one.
const tokenId = tokenUtxos[0].tokenId
const otherTokens = tokenUtxos.filter(x => x.tokenId !== tokenId)
if (otherTokens.length > 0) {
throw new Error(
'Multiple token classes detected. This function only supports a single class of token.'
)
}
// instance of transaction builder
let transactionBuilder
if (options.testnet) {
transactionBuilder = new _this.bchjs.TransactionBuilder('testnet')
} else transactionBuilder = new _this.bchjs.TransactionBuilder()
// Combine all the UTXOs into a single array.
const allUtxos = utxos
// console.log(`allUtxos: ${JSON.stringify(allUtxos, null, 2)}`)
// Loop through all UTXOs.
let originalAmount = 0
for (let i = 0; i < allUtxos.length; i++) {
const utxo = allUtxos[i]
originalAmount = originalAmount + utxo.satoshis
transactionBuilder.addInput(utxo.txid, utxo.vout)
}
if (originalAmount < 300) {
throw new Error(
'Not enough BCH to send. Send more BCH to the wallet to pay miner fees.'
)
}
// get byte count to calculate fee. paying 1 sat
// Note: This may not be totally accurate. Just guessing on the byteCount size.
// const byteCount = this.BITBOX.BitcoinCash.getByteCount(
// { P2PKH: 3 },
// { P2PKH: 5 }
// )
// //console.log(`byteCount: ${byteCount}`)
// const satoshisPerByte = 1.1
// const txFee = Math.floor(satoshisPerByte * byteCount)
// console.log(`txFee: ${txFee} satoshis\n`)
const txFee = 500
// amount to send back to the sending address. It's the original amount - 1 sat/byte for tx size
const remainder = originalAmount - txFee - 546
if (remainder < 1) {
throw new Error('Selected UTXO does not have enough satoshis')
}
// console.log(`remainder: ${remainder}`)
// Tally up the quantity of tokens
let tokenQty = 0
for (let i = 0; i < tokenUtxos.length; i++) {
tokenQty += tokenUtxos[i].tokenQty
}
// console.log(`tokenQty: ${tokenQty}`)
// Generate the OP_RETURN entry for an SLP SEND transaction.
// console.log(`Generating op-return.`)
const {
script,
outputs
} = _this.bchjs.SLP.TokenType1.generateSendOpReturn(tokenUtxos, tokenQty)
// console.log(`token outputs: ${outputs}`)
// Since we are sweeping all tokens from the WIF, there generateOpReturn()
// function should only compute 1 token output. If it returns 2, then there
// is something unexpected happening.
if (outputs > 1) {
throw new Error(
'More than one class of token detected. Sweep feature not supported.'
)
}
// Add OP_RETURN as first output.
const data = _this.bchjs.Script.encode(script)
transactionBuilder.addOutput(data, 0)
// Send dust transaction representing tokens being sent.
transactionBuilder.addOutput(
_this.bchjs.Address.toLegacyAddress(toAddr),
546
)
// Last output: send remaining BCH
transactionBuilder.addOutput(
_this.bchjs.Address.toLegacyAddress(toAddr),
remainder
)
// console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`)
// Sign each UTXO being consumed.
let redeemScript
for (let i = 0; i < allUtxos.length; i++) {
const thisUtxo = allUtxos[i]
// console.log(`thisUtxo: ${JSON.stringify(thisUtxo, null, 2)}`)
transactionBuilder.sign(
i,
ecPair,
redeemScript,
transactionBuilder.hashTypes.SIGHASH_ALL,
thisUtxo.satoshis
)
}
// build tx
const tx = transactionBuilder.build()
// output rawhex
const hex = tx.toHex()
// console.log(`Transaction raw hex: `)
// console.log(hex)
return hex
} catch (err) {
wlogger.error('Error in util.js/sweepBCH().')
throw err
}
}
}
const utilRoute = new UtilRoute()
router.get('/', utilRoute.root)
router.get('/validateAddress/:address', utilRoute.validateAddressSingle)
router.post('/validateAddress', utilRoute.validateAddressBulk)
router.post('/sweep', utilRoute.sweepWif)
module.exports = {
router,
// testableComponents: {
// root,
// validateAddressSingle,
// validateAddressBulk,
// sweepWif
// }
UtilRoute
}
-521
View File
@@ -1,521 +0,0 @@
/*
Blockbook API route
*/
'use strict'
const express = require('express')
const axios = require('axios')
const wlogger = require('../../util/winston-logging')
const RouteUtils = require('../../util/route-utils')
const routeUtils = new RouteUtils()
// Library for easily switching the API paths to use different instances of
// Blockbook.
const BlockbookPath = require('../../util/blockbook-path')
const BLOCKBOOKPATH = new BlockbookPath()
// BLOCKBOOKPATH.toOpenBazaar()
const router = express.Router()
// Used for processing error messages before sending them to the user.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
const BCHJS = require('@psf/bch-js')
const bchjs = new BCHJS()
let _this
class Blockbook {
constructor () {
_this = this
_this.axios = axios
_this.routeUtils = routeUtils
_this.bchjs = bchjs
_this.BLOCKBOOKPATH = BLOCKBOOKPATH
_this.router = router
_this.router.get('/', _this.root)
_this.router.get('/balance/:address', _this.balanceSingle)
_this.router.post('/balance', _this.balanceBulk)
_this.router.get('/utxos/:address', _this.utxosSingle)
_this.router.post('/utxos', _this.utxosBulk)
_this.router.get('/tx/:txid', _this.txSingle)
_this.router.post('/tx', _this.txBulk)
}
// DRY error handler.
errorHandler (err, res) {
// Attempt to decode the error message.
const { msg, status } = _this.routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
res.status(500)
return res.json({ error: util.inspect(err) })
}
// Root API endpoint. Simply acknowledges that it exists.
root (req, res, next) {
return res.json({ status: 'address' })
}
// Query the Blockbook Node API for a balance on a single BCH address.
// Returns a Promise.
async balanceFromBlockbook (thisAddress) {
try {
// console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`)
// Convert the address to a cashaddr without a prefix.
const addr = _this.bchjs.Address.toCashAddress(thisAddress)
const path = `${_this.BLOCKBOOKPATH.addrPath}${addr}`
// console.log(`path: ${path}`)
// Query the Blockbook Node API.
const options = {
method: 'get',
baseURL: path
}
const axiosResponse = await _this.axios.request(options)
const retData = axiosResponse.data
// console.log(`retData: ${util.inspect(retData)}`)
return retData
} catch (err) {
// Dev Note: Do not log error messages here. Throw them instead and let the
// parent function handle it.
wlogger.debug('Error in blockbook.js/balanceFromBlockbook()')
throw err
}
}
async balanceSingle (req, res, next) {
try {
const address = req.params.address
if (!address || address === '') {
res.status(400)
return res.json({ error: 'address can not be empty' })
}
// Reject if address is an array.
if (Array.isArray(address)) {
res.status(400)
return res.json({
error: 'address can not be an array. Use POST for bulk upload.'
})
}
wlogger.debug(
'Executing blockbook/balanceSingle with this address: ',
address
)
// Ensure the input is a valid BCH address.
try {
// const legacyAddr = bchjs.Address.toLegacyAddress(address)
_this.bchjs.Address.toLegacyAddress(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 networkIsValid = _this.routeUtils.validateNetwork(address)
if (!networkIsValid) {
res.status(400)
return res.json({
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
// Query the Blockbook Node API.
const retData = await _this.balanceFromBlockbook(address)
// Return the retrieved address information.
res.status(200)
return res.json(retData)
} catch (err) {
// Write out error to error log.
wlogger.error('Error in blockbook.js/balanceSingle().', err)
return _this.errorHandler(err, res)
}
}
// POST handler for bulk queries on address details
async balanceBulk (req, res, next) {
try {
let addresses = req.body.addresses
// const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0
// Reject if addresses is not an array.
if (!Array.isArray(addresses)) {
res.status(400)
return res.json({
error: 'addresses needs to be an array. Use GET for single address.'
})
}
// Enforce array size rate limits
if (!_this.routeUtils.validateArraySize(req, addresses)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
error: 'Array too large.'
})
}
wlogger.debug(
'Executing blockbook.js/balanceBulk with these addresses: ',
addresses
)
// Validate each element in the address array.
for (let i = 0; i < addresses.length; i++) {
const thisAddress = addresses[i]
// Ensure the input is a valid BCH address.
try {
_this.bchjs.Address.toLegacyAddress(thisAddress)
} catch (err) {
res.status(400)
return res.json({
error: `Invalid BCH address. Double check your address is valid: ${thisAddress}`
})
}
// Prevent a common user error. Ensure they are using the correct network address.
const networkIsValid = _this.routeUtils.validateNetwork(thisAddress)
if (!networkIsValid) {
res.status(400)
return res.json({
error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.`
})
}
}
// Loops through each address and creates an array of Promises, querying
// Insight API in parallel.
addresses = addresses.map(async (address, index) =>
// console.log(`address: ${address}`)
_this.balanceFromBlockbook(address)
)
// Wait for all parallel Insight requests to return.
const result = await _this.axios.all(addresses)
// Return the array of retrieved address information.
res.status(200)
return res.json(result)
} catch (err) {
wlogger.error('Error in blockbook.js/balanceBulk().', err)
return _this.errorHandler(err, res)
}
}
// Query the Blockbook API for utxos associated with a BCH address.
// Returns a Promise.
async utxosFromBlockbook (thisAddress) {
try {
// console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`)
// Convert the address to a cashaddr without a prefix.
const addr = _this.bchjs.Address.toCashAddress(thisAddress)
const path = `${_this.BLOCKBOOKPATH.utxoPath}${addr}`
// console.log(`path: ${path}`)
// Query the Blockbook API.
// Query the Blockbook Node API.
const options = {
method: 'get',
baseURL: path
}
const axiosResponse = await _this.axios.request(options)
const retData = axiosResponse.data
// console.log(`retData: ${util.inspect(retData)}`)
// Add the satoshis property to each UTXO.
for (let i = 0; i < retData.length; i++) {
retData[i].satoshis = Number(retData[i].value)
}
return retData
} catch (err) {
// Dev Note: Do not log error messages here. Throw them instead and let the
// parent function handle it.
wlogger.debug('Error in blockbook.js/utxosFromBlockbook()')
throw err
}
}
// GET handler for single balance
async utxosSingle (req, res, next) {
try {
const address = req.params.address
if (!address || address === '') {
res.status(400)
return res.json({ error: 'address can not be empty' })
}
// Reject if address is an array.
if (Array.isArray(address)) {
res.status(400)
return res.json({
error: 'address can not be an array. Use POST for bulk upload.'
})
}
wlogger.debug(
'Executing blockbook/utxosSingle with this address: ',
address
)
// Ensure the input is a valid BCH address.
try {
// const legacyAddr = bchjs.Address.toLegacyAddress(address)
_this.bchjs.Address.toLegacyAddress(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 networkIsValid = _this.routeUtils.validateNetwork(address)
if (!networkIsValid) {
res.status(400)
return res.json({
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
// Query the Blockbook API.
const retData = await _this.utxosFromBlockbook(address)
// Return the retrieved address information.
res.status(200)
return res.json(retData)
} catch (err) {
// Write out error to error log.
wlogger.error('Error in blockbook.js/utxosSingle().', err)
return _this.errorHandler(err, res)
}
}
// POST handler for bulk queries on address utxos
async utxosBulk (req, res, next) {
try {
let addresses = req.body.addresses
// const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0
// Reject if addresses is not an array.
if (!Array.isArray(addresses)) {
res.status(400)
return res.json({
error: 'addresses needs to be an array. Use GET for single address.'
})
}
// Enforce array size rate limits
if (!_this.routeUtils.validateArraySize(req, addresses)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
error: 'Array too large.'
})
}
wlogger.debug(
'Executing blockbook.js/utxosBulk with these addresses: ',
addresses
)
// Validate each element in the address array.
for (let i = 0; i < addresses.length; i++) {
const thisAddress = addresses[i]
// Ensure the input is a valid BCH address.
try {
_this.bchjs.Address.toLegacyAddress(thisAddress)
} catch (err) {
res.status(400)
return res.json({
error: `Invalid BCH address. Double check your address is valid: ${thisAddress}`
})
}
// Prevent a common user error. Ensure they are using the correct network address.
const networkIsValid = _this.routeUtils.validateNetwork(thisAddress)
if (!networkIsValid) {
res.status(400)
return res.json({
error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.`
})
}
}
// Loops through each address and creates an array of Promises, querying
// Insight API in parallel.
addresses = addresses.map(async (address, index) =>
// console.log(`address: ${address}`)
_this.utxosFromBlockbook(address)
)
// Wait for all parallel Insight requests to return.
const result = await _this.axios.all(addresses)
// Return the array of retrieved address information.
res.status(200)
return res.json(result)
} catch (err) {
wlogger.error('Error in blockbook.js/utxosBulk().', err)
return _this.errorHandler(err, res)
}
}
// Query the Blockbook Node API for transactions on a single TXID.
// Returns a Promise.
async transactionsFromBlockbook (txid) {
try {
// console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`)
const path = `${_this.BLOCKBOOKPATH.txPath}${txid}`
// console.log(`path: ${path}`)
// Query the Blockbook Node API.
const options = {
method: 'get',
baseURL: path
}
const axiosResponse = await _this.axios.request(options)
const retPromise = axiosResponse.data
// console.log(`retData: ${util.inspect(retData)}`)
return retPromise
} catch (err) {
// Dev Note: Do not log error messages here. Throw them instead and let the
// parent function handle it.
wlogger.debug('Error in blockbook.js/transactionsFromBlockbook()')
throw err
}
}
// GET handler for single transaction details.
async txSingle (req, res, next) {
try {
const txid = req.params.txid
if (!txid || txid === '') {
res.status(400)
return res.json({ error: 'txid can not be empty' })
}
// Reject if address is an array.
if (Array.isArray(txid)) {
res.status(400)
return res.json({
error: 'txid can not be an array. Use POST for bulk upload.'
})
}
// TODO: Add regex comparison of txid to ensure it's valid.
if (txid.length !== 64) {
res.status(400)
return res.json({
error: `txid must be of length 64 (not ${txid.length})`
})
}
wlogger.debug('Executing blockbook/txSingle with this txid: ', txid)
// Query the Blockbook Node API.
const retData = await _this.transactionsFromBlockbook(txid)
// Return the retrieved address information.
res.status(200)
return res.json(retData)
} catch (err) {
// Write out error to error log.
wlogger.error('Error in blockbook.js/txSingle().', err)
return _this.errorHandler(err, res)
}
}
// POST handler for bulk queries on tx details
async txBulk (req, res, next) {
try {
let txids = req.body.txids
// const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0
// Reject if txids is not an array.
if (!Array.isArray(txids)) {
res.status(400)
return res.json({
error: 'txids need to be an array. Use GET for single address.'
})
}
// 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({
error: 'Array too large.'
})
}
wlogger.debug('Executing blockbook.js/txBulk with these txids: ', txids)
// Validate each element in the txids array.
for (let i = 0; i < txids.length; i++) {
const thisTxid = txids[i]
if (!thisTxid || thisTxid === '') {
res.status(400)
return res.json({ error: 'txid can not be empty' })
}
// TODO: Add regex comparison of txid to ensure it's valid.
if (thisTxid.length !== 64) {
res.status(400)
return res.json({
error: `txid must be of length 64 (not ${thisTxid.length})`
})
}
}
// Loops through each address and creates an array of Promises, querying
// Insight API in parallel.
txids = txids.map(async (txid, index) =>
// console.log(`address: ${address}`)
_this.transactionsFromBlockbook(txid)
)
// Wait for all parallel Insight requests to return.
const result = await _this.axios.all(txids)
// Return the array of retrieved address information.
res.status(200)
return res.json(result)
} catch (err) {
wlogger.error('Error in blockbook.js/txBulk().', err)
return _this.errorHandler(err, res)
}
}
}
module.exports = Blockbook
-116
View File
@@ -1,116 +0,0 @@
'use strict'
const express = require('express')
const router = express.Router()
const axios = require('axios')
// const routeUtils = require('../route-utils')
const wlogger = require('../../../util/winston-logging')
const RouteUtils = require('../../../util/route-utils')
const routeUtils = new RouteUtils()
// Used for processing error messages before sending them to the user.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
let _this
class Control {
constructor () {
_this = this
_this.axios = axios
_this.routeUtils = routeUtils
_this.router = router
_this.router.get('/', _this.root)
_this.router.get('/getNetworkInfo', _this.getNetworkInfo)
}
root (req, res, next) {
return res.json({ status: 'control' })
}
// DRY error handler.
errorHandler (err, res) {
// Attempt to decode the error message.
const { msg, status } = _this.routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
res.status(500)
return res.json({ error: util.inspect(err) })
}
/**
* @api {get} /control/getnetworkinfo Get Network Info
* @apiName GetNetworkInfo
* @apiGroup Control
* @apiDescription RPC call which gets basic full node information.
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/control/getnetworkinfo" -H "accept: application/json"
*
*/
async getNetworkInfo (req, res, next) {
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getnetworkinfo'
options.data.method = 'getnetworkinfo'
options.data.params = []
try {
// const response = await BitboxHTTP(requestConfig)
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (error) {
wlogger.error('Error in control.ts/getNetworkInfo().', error)
return _this.errorHandler(error, res)
}
}
// router.get('/getMemoryInfo', (req, res, next) => {
// BitboxHTTP({
// method: 'post',
// auth: {
// username: username,
// password: password
// },
// data: {
// jsonrpc: "1.0",
// id:"getmemoryinfo",
// method: "getmemoryinfo"
// }
// })
// .then((response) => {
// res.json(response.data.result);
// })
// .catch((error) => {
// res.send(error.response.data.error.message);
// });
// });
//
// router.get('/help', (req, res, next) => {
// BITBOX.Control.help()
// .then((result) => {
// res.json(result);
// }, (err) => { console.log(err);
// });
// });
//
// router.post('/stop', (req, res, next) => {
// BITBOX.Control.stop()
// .then((result) => {
// res.json(result);
// }, (err) => { console.log(err);
// });
// });
}
module.exports = Control
-169
View File
@@ -1,169 +0,0 @@
'use strict'
const express = require('express')
const router = express.Router()
const axios = require('axios')
const RouteUtils = require('../../../util/route-utils')
const routeUtils = new RouteUtils()
const wlogger = require('../../../util/winston-logging')
// Used to convert error messages to strings, to safely pass to users.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
let _this
class Mining {
constructor () {
_this = this
_this.axios = axios
_this.routeUtils = routeUtils
_this.router = router
_this.router.get('/', _this.root)
_this.router.get('/getMiningInfo', _this.getMiningInfo)
_this.router.get('/getNetworkHashPS', _this.getNetworkHashPS)
}
root (req, res, next) {
return res.json({ status: 'mining' })
}
// DRY error handler.
errorHandler (err, res) {
// Attempt to decode the error message.
const { msg, status } = _this.routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
res.status(500)
return res.json({ error: util.inspect(err) })
}
// router.get('/getBlockTemplate/:templateRequest', (req, res, next) => {
// BitboxHTTP({
// method: 'post',
// auth: {
// username: username,
// password: password
// },
// data: {
// jsonrpc: "1.0",
// id:"getblocktemplate",
// method: "getblocktemplate",
// params: [
// req.params.templateRequest
// ]
// }
// })
// .then((response) => {
// res.json(response.data.result);
// })
// .catch((error) => {
// res.send(error.response.data.error.message);
// });
// });
/**
* @api {get} /mining/getMiningInfo Get Mining Info.
* @apiName Mining info.
* @apiGroup Mining
* @apiDescription Returns a json object containing mining-related information.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/mining/getMiningInfo" -H "accept: application/json"
*
*
*/
async getMiningInfo (req, res, next) {
try {
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getmininginfo'
options.data.method = 'getmininginfo'
options.data.params = []
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
wlogger.error('Error in mining.ts/getMiningInfo().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /mining/getNetworkHashps?nblocks=&height= Get Estimated network hashes per second.
* @apiName Estimated network hashes per second.
* @apiGroup Mining
* @apiDescription Returns the estimated network hashes per second based on the last n blocks. Pass in [blocks] to override # of blocks, -1 specifies since last difficulty change. Pass in [height] to estimate the network speed at the time when a certain block was found.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/mining/getNetworkHashps?nblocks=120&height=-1" -H "accept: application/json"
*
*
*/
async getNetworkHashPS (req, res, next) {
try {
let nblocks = 120 // Default
let height = -1 // Default
if (req.query.nblocks) nblocks = parseInt(req.query.nblocks)
if (req.query.height) height = parseInt(req.query.height)
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getnetworkhashps'
options.data.method = 'getnetworkhashps'
options.data.params = [nblocks, height]
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
wlogger.error('Error in mining.ts/getNetworkHashPS().', err)
return _this.errorHandler(err, res)
}
}
// router.post('/submitBlock/:hex', (req, res, next) => {
// let parameters = '';
// if(req.query.parameters && req.query.parameters !== '') {
// parameters = true;
// }
//
// BitboxHTTP({
// method: 'post',
// auth: {
// username: username,
// password: password
// },
// data: {
// jsonrpc: "1.0",
// id:"submitblock",
// method: "submitblock",
// params: [
// req.params.hex,
// parameters
// ]
// }
// })
// .then((response) => {
// res.json(response.data.result);
// })
// .catch((error) => {
// res.send(error.response.data.error.message);
// });
// });
}
module.exports = Mining
-168
View File
@@ -1,168 +0,0 @@
'use strict'
const express = require('express')
const router = express.Router()
router.get('/', async (req, res, next) => {
res.json({ status: 'network' })
})
// router.post('/addNode/:node/:command', (req, res, next) => {
// BITBOX.Network.addNode(req.params.node, req.params.command)
// .then((result) => {
// res.json(result);
// }, (err) => { console.log(err);
// });
// });
//
// router.post('/clearBanned', (req, res, next) => {
// BITBOX.Network.clearBanned()
// .then((result) => {
// res.json(result);
// }, (err) => { console.log(err);
// });
// });
//
// router.post('/disconnectNode/:address/:nodeid', (req, res, next) => {
// BITBOX.Network.disconnectNode(req.params.address, req.params.nodeid)
// .then((result) => {
// res.json(result);
// }, (err) => { console.log(err);
// });
// });
//
// router.get('/getAddedNodeInfo/:node', (req, res, next) => {
// BITBOX.Network.getAddedNodeInfo(req.params.node)
// .then((result) => {
// res.json(result);
// }, (err) => { console.log(err);
// });
// });
//
// router.get('/getConnectionCount', (req, res, next) => {
// BitboxHTTP({
// method: 'post',
// auth: {
// username: username,
// password: password
// },
// data: {
// jsonrpc: "1.0",
// id:"getconnectioncount",
// method: "getconnectioncount"
// }
// })
// .then((response) => {
// res.json(response.data.result);
// })
// .catch((error) => {
// res.send(error.response.data.error.message);
// });
// });
//
// router.get('/getNetTotals', (req, res, next) => {
// BitboxHTTP({
// method: 'post',
// auth: {
// username: username,
// password: password
// },
// data: {
// jsonrpc: "1.0",
// id:"getnettotals",
// method: "getnettotals"
// }
// })
// .then((response) => {
// res.json(response.data.result);
// })
// .catch((error) => {
// res.send(error.response.data.error.message);
// });
// });
//
// router.get('/getNetworkInfo', (req, res, next) => {
// BitboxHTTP({
// method: 'post',
// auth: {
// username: username,
// password: password
// },
// data: {
// jsonrpc: "1.0",
// id:"getnetworkinfo",
// method: "getnetworkinfo"
// }
// })
// .then((response) => {
// res.json(response.data.result);
// })
// .catch((error) => {
// res.send(error.response.data.error.message);
// });
// });
//
// router.get('/getPeerInfo', (req, res, next) => {
// BitboxHTTP({
// method: 'post',
// auth: {
// username: username,
// password: password
// },
// data: {
// jsonrpc: "1.0",
// id:"getpeerinfo",
// method: "getpeerinfo"
// }
// })
// .then((response) => {
// res.json(response.data.result);
// })
// .catch((error) => {
// res.send(error.response.data.error.message);
// });
// });
//
// router.get('/ping', (req, res, next) => {
// BitboxHTTP({
// method: 'post',
// auth: {
// username: username,
// password: password
// },
// data: {
// jsonrpc: "1.0",
// id:"ping",
// method: "ping"
// }
// })
// .then((response) => {
// res.json(JSON.stringify(response.data.result));
// })
// .catch((error) => {
// res.send(error.response.data.error.message);
// });
// });
//
// router.post('/setBan/:subnet/:command', (req, res, next) => {
// // TODO finish this
// BITBOX.Network.getConnectionCount(req.params.subnet, req.params.command)
// .then((result) => {
// res.json(result);
// }, (err) => { console.log(err);
// });
// });
//
// router.post('/setNetworkActive/:state', (req, res, next) => {
// let state = true;
// if(req.params.state && req.params.state === 'false') {
// state = false;
// }
// BITBOX.Network.getConnectionCount(state)
// .then((result) => {
// res.json(result);
// }, (err) => { console.log(err);
// });
// });
module.exports = router
-570
View File
@@ -1,570 +0,0 @@
'use strict'
const express = require('express')
const router = express.Router()
const axios = require('axios')
const RouteUtils = require('../../../util/route-utils')
const routeUtils = new RouteUtils()
const wlogger = require('../../../util/winston-logging')
// Used to convert error messages to strings, to safely pass to users.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
let _this
class RawTransactions {
constructor () {
_this = this
// Encapsulate external dependencies.
this.axios = axios
this.routeUtils = routeUtils
// Define Express routes.
this.router = router
this.router.get('/', this.root)
this.router.get(
'/decodeRawTransaction/:hex',
this.decodeRawTransactionSingle
)
this.router.post('/decodeRawTransaction', this.decodeRawTransactionBulk)
this.router.get('/decodeScript/:hex', this.decodeScriptSingle)
this.router.post('/decodeScript', this.decodeScriptBulk)
this.router.post('/getRawTransaction', this.getRawTransactionBulk)
this.router.get('/getRawTransaction/:txid', this.getRawTransactionSingle)
this.router.post('/sendRawTransaction', this.sendRawTransactionBulk)
this.router.get('/sendRawTransaction/:hex', this.sendRawTransactionSingle)
}
root (req, res, next) {
return res.json({ status: 'rawtransactions' })
}
// DRY error handler.
errorHandler (err, res) {
// Attempt to decode the error message.
const { msg, status } = _this.routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
res.status(500)
return res.json({ error: util.inspect(err) })
}
// Decode transaction hex into a JSON object.
// GET
/**
* @api {get} /rawtransactions/decodeRawTransaction/{hex} Decode Single Raw Transaction.
* @apiName Decode Single Raw Transaction
* @apiGroup Raw Transaction
* @apiDescription Return a JSON object representing the serialized, hex-encoded transaction.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/rawtransactions/decodeRawTransaction/02000000010e991f7ccec410f27d333f737f149b5d3be6728687da81072e638aed0063a176010000006b483045022100cd20443b0af090053450bc4ab00d563d4ac5955bb36e0135b00b8a96a19f233302205047f2c70a08c6ef4b76f2d198b33a31d17edfaa7e1e9e865894da0d396009354121024d4e7f522f67105b7bf5f9dbe557e7b2244613fdfcd6fe09304f93877328f6beffffffff02a0860100000000001976a9140ee020c07f39526ac5505c54fa1ab98490979b8388acb5f0f70b000000001976a9143a9b2b0c12fe722fcf653b6ef5dcc38732d6ff5188ac00000000" -H "accept: application/json"
*/
async decodeRawTransactionSingle (req, res, next) {
try {
const hex = req.params.hex
// Throw an error if hex is empty.
if (!hex || hex === '') {
res.status(400)
return res.json({ error: 'hex can not be empty' })
}
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'decoderawtransaction'
options.data.method = 'decoderawtransaction'
options.data.params = [hex]
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error(
'Error in rawtransactions.ts/decodeRawTransactionSingle().',
err
)
return _this.errorHandler(err, res)
}
}
/**
* @api {post} /rawtransactions/decodeRawTransaction Decode Bulk Raw Transactions.
* @apiName Decode Bulk Raw Transactions
* @apiGroup Raw Transaction
* @apiDescription Return bulk hex encoded transaction.
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/rawtransactions/decodeRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
*
*
*/
async decodeRawTransactionBulk (req, res, next) {
try {
const hexes = req.body.hexes
if (!Array.isArray(hexes)) {
res.status(400)
return res.json({ error: 'hexes must be an array' })
}
// Enforce array size rate limits
if (!_this.routeUtils.validateArraySize(req, hexes)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
error: 'Array too large.'
})
}
// const results = []
// Validate each element in the address array.
for (let i = 0; i < hexes.length; i++) {
const thisHex = hexes[i]
// Reject if id is empty
if (!thisHex || thisHex === '') {
res.status(400)
return res.json({ error: 'Encountered empty hex' })
}
}
const options = _this.routeUtils.getAxiosOptions()
// Loop through each height and creates an array of requests to call in parallel
const promises = hexes.map(async hex => {
options.data.id = 'decoderawtransaction'
options.data.method = 'decoderawtransaction'
options.data.params = [hex]
return _this.axios.request(options)
})
// Wait for all parallel Insight requests to return.
const axiosResult = await _this.axios.all(promises)
// Retrieve the data part of the result.
const result = axiosResult.map(x => x.data.result)
res.status(200)
return res.json(result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/getRawTransaction: `, err)
wlogger.error(
'Error in rawtransactions.ts/decodeRawTransactionBulk().',
err
)
return _this.errorHandler(err, res)
}
}
// Decode a raw transaction from hex to assembly.
// GET single
/**
* @api {get} /rawtransactions/decodeScript/{hex} Decode Single Script.
* @apiName Decode Single Script
* @apiGroup Raw Transaction
* @apiDescription Decode a hex-encoded script.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/rawtransactions/decodeScript/4830450221009a51e00ec3524a7389592bc27bea4af5104a59510f5f0cfafa64bbd5c164ca2e02206c2a8bbb47eabdeed52f17d7df668d521600286406930426e3a9415fe10ed592012102e6e1423f7abde8b70bca3e78a7d030e5efabd3eb35c19302542b5fe7879c1a16" -H "accept: application/json"
*
*
*/
async decodeScriptSingle (req, res, next) {
try {
const hex = req.params.hex
// Throw an error if hex is empty.
if (!hex || hex === '') {
res.status(400)
return res.json({ error: 'hex can not be empty' })
}
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'decodescript'
options.data.method = 'decodescript'
options.data.params = [hex]
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeScript: `, err)
wlogger.error('Error in rawtransactions.ts/decodeScriptSingle().', err)
return _this.errorHandler(err, res)
}
}
// Decode a raw transaction from hex to assembly.
// POST bulk
/**
* @api {post} /rawtransactions/decodeScript Bulk Decode Script.
* @apiName Bulk Decode Script
* @apiGroup Raw Transaction
* @apiDescription Decode multiple hex-encoded scripts.
*
*
* @apiExample Example usage:
*curl -X POST "https://api.fullstack.cash/v4/rawtransactions/decodeScript" -H "accept:" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
*
*
*/
async decodeScriptBulk (req, res, next) {
try {
const hexes = req.body.hexes
// Validation
if (!Array.isArray(hexes)) {
res.status(400)
return res.json({ error: 'hexes must be an array' })
}
// Enforce array size rate limits
if (!_this.routeUtils.validateArraySize(req, hexes)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
error: 'Array too large.'
})
}
// Validate each hex in the array
for (let i = 0; i < hexes.length; i++) {
const hex = hexes[i]
// Throw an error if hex is empty.
if (!hex || hex === '') {
res.status(400)
return res.json({ error: 'Encountered empty hex' })
}
}
const options = _this.routeUtils.getAxiosOptions()
// Loop through each hex and create an array of promises
const promises = hexes.map(async hex => {
options.data.id = 'decodescript'
options.data.method = 'decodescript'
options.data.params = [hex]
const response = await _this.axios.request(options)
return response
})
// Wait for all parallel promises to return.
const resolved = await Promise.all(promises)
// Retrieve the data from each resolved promise.
const result = resolved.map(x => x.data.result)
res.status(200)
return res.json(result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeScript: `, err)
wlogger.error('Error in rawtransactions.ts/decodeScriptBulk().', err)
return _this.errorHandler(err, res)
}
}
// Retrieve raw transactions details from the full node.
async getRawTransactionsFromNode (txid, verbose) {
try {
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getrawtransaction'
options.data.method = 'getrawtransaction'
options.data.params = [txid, verbose]
const response = await _this.axios.request(options)
return response.data.result
} catch (err) {
wlogger.error('Error in rawtransactions.ts/getRawTransactionsFromNode().')
throw err
}
}
// Get a JSON object breakdown of transaction details.
// POST
/**
* @api {post} /rawtransactions/getRawTransaction Get Bulk Raw Transactions.
* @apiName Get Bulk Raw Transactions.
* @apiGroup Raw Transaction
* @apiDescription Return the raw transaction data for multiple transactions. If verbose is 'true', returns an Object with information about 'txid'. If verbose is 'false' or omitted, returns a string that is serialized, hex-encoded data for 'txid'.
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/rawtransactions/getRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1","5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e"],"verbose":true}'
*
*/
async getRawTransactionBulk (req, res, next) {
try {
let verbose = 0
if (req.body.verbose) verbose = 1
const txids = req.body.txids
if (!Array.isArray(txids)) {
res.status(400)
return res.json({ error: 'txids must be an array' })
}
// 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({
error: 'Array too large.'
})
}
// stub response object
// const returnResponse = {
// status: 100,
// json: {
// error: ''
// }
// }
// Validate each txid in the array.
for (let i = 0; i < txids.length; i++) {
const txid = txids[i]
if (!txid || txid === '') {
res.status(400)
return res.json({ error: 'Encountered empty TXID' })
}
if (txid.length !== 64) {
res.status(400)
return res.json({
error: `parameter 1 must be of length 64 (not ${txid.length})`
})
}
}
// Loop through each txid and create an array of promises
const promises = txids.map(async txid =>
_this.getRawTransactionsFromNode(txid, verbose)
)
// Wait for all parallel promises to return.
const axiosResult = await _this.axios.all(promises)
res.status(200)
return res.json(axiosResult)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/getRawTransaction: `, err)
wlogger.error('Error in rawtransactions.ts/getRawTransactionBulk().', err)
return _this.errorHandler(err, res)
}
}
// Get a JSON object breakdown of transaction details.
// GET
/**
* @api {get} /rawtransactions/getRawTransaction/{txid} Return the raw transaction data.
* @apiName Get Raw Transaction
* @apiGroup Raw Transaction
* @apiDescription return the raw transaction data. If verbose is 'true', returns an Object with information about 'txid'. If verbose is 'false' or omitted, returns a string that is serialized, hex-encoded data for 'txid'.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/rawtransactions/getRawTransaction/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33?verbose=true" -H "accept: application/json"
*
*
*/
async getRawTransactionSingle (req, res, next) {
try {
let verbose = 0
if (req.query.verbose === 'true') verbose = 1
const txid = req.params.txid
if (!txid || txid === '') {
res.status(400)
return res.json({ error: 'txid can not be empty' })
}
if (txid.length !== 64) {
res.status(400)
return res.json({
error: `parameter 1 must be of length 64 (not ${txid.length})`
})
}
const data = await _this.getRawTransactionsFromNode(txid, verbose)
return res.json(data)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/getRawTransaction: `, err)
wlogger.error(
'Error in rawtransactions.ts/getRawTransactionSingle().',
err
)
return _this.errorHandler(err, res)
}
}
// Transmit a raw transaction to the BCH network.
/**
* @api {post} /rawtransactions/sendRawTransaction Send Bulk Raw Transactions.
* @apiName Send Bulk Raw Transactions
* @apiGroup Raw Transaction
* @apiDescription Submits multiple raw transaction (serialized, hex-encoded) to local node and network.
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/rawtransactions/sendRawTransaction" -H "accept:application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
*
*
*/
async sendRawTransactionBulk (req, res, next) {
try {
// Validation
const hexes = req.body.hexes
// Reject if input is not an array
if (!Array.isArray(hexes)) {
res.status(400)
return res.json({ error: 'hex must be an array' })
}
const options = _this.routeUtils.getAxiosOptions()
// Enforce array size rate limits
if (!_this.routeUtils.validateArraySize(req, hexes)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
error: 'Array too large.'
})
}
// Validate each element
for (let i = 0; i < hexes.length; i++) {
const hex = hexes[i]
if (hex === '') {
res.status(400)
return res.json({
error: 'Encountered empty hex'
})
}
}
// Dev Note CT 1/31/2019:
// Sending the 'sendrawtrnasaction' RPC call to a full node in parallel will
// not work. Testing showed that the full node will return the same TXID for
// different TX hexes. I believe this is by design, to prevent double spends.
// In parallel, we are essentially asking the node to broadcast a new TX before
// it's finished broadcast the previous one. Serial execution is required.
// How to send TX hexes in parallel the WRONG WAY:
/*
// Collect an array of promises.
const promises = hexes.map(async (hex: any) => {
requestConfig.data.id = "sendrawtransaction"
requestConfig.data.method = "sendrawtransaction"
requestConfig.data.params = [hex]
return await BitboxHTTP(requestConfig)
})
// Wait for all parallel Insight requests to return.
const axiosResult: Array<any> = await axios.all(promises)
// Retrieve the data part of the result.
const result = axiosResult.map(x => x.data.result)
*/
// Sending them serially.
const result = []
for (let i = 0; i < hexes.length; i++) {
const hex = hexes[i]
options.data.id = 'sendrawtransaction'
options.data.method = 'sendrawtransaction'
options.data.params = [hex]
const rpcResult = await _this.axios.request(options)
result.push(rpcResult.data.result)
}
res.status(200)
return res.json(result)
} catch (err) {
wlogger.error(
'Error in rawtransactions.ts/sendRawTransactionBulk().',
err
)
return _this.errorHandler(err, res)
}
}
// Transmit a raw transaction to the BCH network.
/**
* @api {get} /rawtransactions/sendRawTransaction/{hex} Send Single Raw Transaction.
* @apiName Send Single Raw Transaction
* @apiGroup Raw Transaction
* @apiDescription Submits single raw transaction (serialized, hex-encoded) to local node and network.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/rawtransactions/sendRawTransaction/01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000" -H "accept: "
*
*
*/
async sendRawTransactionSingle (req, res, next) {
try {
const hex = req.params.hex // URL parameter
// Reject if input is not an array or a string
if (typeof hex !== 'string') {
res.status(400)
return res.json({ error: 'hex must be a string' })
}
// Validation
if (hex === '') {
res.status(400)
return res.json({
error: 'Encountered empty hex'
})
}
const options = _this.routeUtils.getAxiosOptions()
// RPC call
options.data.id = 'sendrawtransaction'
options.data.method = 'sendrawtransaction'
options.data.params = [hex]
const rpcResult = await _this.axios.request(options)
const result = rpcResult.data.result
res.status(200)
return res.json(result)
} catch (err) {
wlogger.error(
'Error in rawtransactions.ts/sendRawTransactionSingle().',
err
)
return _this.errorHandler(err, res)
}
}
}
module.exports = RawTransactions
-16
View File
@@ -1,16 +0,0 @@
/*
This health-check API can be used to test the server for aliveness and
readiness.
*/
'use strict'
const express = require('express')
const router = express.Router()
/* GET home page. */
router.get('/', (req, res, next) => {
res.json({ status: true })
})
module.exports = router
-32
View File
@@ -1,32 +0,0 @@
/*
A library for interacting with the Bitcoin.com ninsight (not Insight) indexer.
*/
'use strict'
const express = require('express')
// const axios = require('axios')
// const routeUtils = require('./route-utils')
// const wlogger = require('../../util/winston-logging')
const router = express.Router()
// const BCHJS = require('@psf/bch-js')
// const bchjs = new BCHJS()
// let _this
class Ninsight {
constructor () {
// _this = this
this.router = router
this.router.get('/', this.root)
}
root (req, res, next) {
return res.json({ status: 'ninsight' })
}
}
module.exports = Ninsight
-192
View File
@@ -1,192 +0,0 @@
/*
A private library of utility functions used by several different routes.
*/
'use strict'
const axios = require('axios')
const wlogger = require('../../util/winston-logging')
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
const BCHJS = require('@psf/bch-js')
const bchjs = new BCHJS()
module.exports = {
validateNetwork, // Prevents a common user error
setEnvVars, // Allows RPC variables to be set dynamically based on changing env vars.
decodeError, // Extract and interpret error messages.
validateArraySize, // Ensure the passed array meets rate limiting requirements.
getAxiosOptions
}
// This function expects the Request Express.js object and an array as input.
// The array is then validated against freemium and pro-tier rate limiting
// requirements. A boolean is returned to indicate if the array size if valid
// or not.
function validateArraySize (req, array) {
const FREEMIUM_INPUT_SIZE = 20
const PRO_INPUT_SIZE = 20
if (req.locals && req.locals.proLimit) {
if (array.length <= PRO_INPUT_SIZE) return true
} else if (array.length <= FREEMIUM_INPUT_SIZE) {
return true
}
return false
}
// Returns true if user-provided cash address matches the correct network,
// mainnet or testnet. If NETWORK env var is not defined, it returns false.
// This prevent a common user-error issue that is easy to make: passing a
// testnet address into rest.bitcoin.com or passing a mainnet address into
// trest.bitcoin.com.
function validateNetwork (addr) {
try {
const network = process.env.NETWORK
// Return false if NETWORK is not defined.
if (!network || network === '') {
console.log('Warning: NETWORK environment variable is not defined!')
return false
}
// Convert the user-provided address to a cashaddress, for easy detection
// of the intended network.
const cashAddr = bchjs.Address.toCashAddress(addr)
// Return true if the network and address both match testnet
const addrIsTest = bchjs.Address.isTestnetAddress(cashAddr)
if (network === 'testnet' && addrIsTest) return true
// Return true if the network and address both match mainnet
const addrIsMain = bchjs.Address.isMainnetAddress(cashAddr)
if (network === 'mainnet' && addrIsMain) return true
return false
} catch (err) {
wlogger.error('Error in validateNetwork()')
return false
}
}
// Dynamically set these based on env vars. Allows unit testing.
function setEnvVars () {
const BitboxHTTP = axios.create({
baseURL: process.env.RPC_BASEURL,
timeout: 15000
})
const username = process.env.RPC_USERNAME
const password = process.env.RPC_PASSWORD
const requestConfig = {
method: 'post',
auth: {
username: username,
password: password
},
data: {
jsonrpc: '1.0'
}
}
return { BitboxHTTP, username, password, requestConfig }
}
// Axios options used when calling axios.post() to talk with a full node.
function getAxiosOptions () {
return {
method: 'post',
baseURL: process.env.RPC_BASEURL,
timeout: 15000,
auth: {
username: process.env.RPC_USERNAME,
password: process.env.RPC_PASSWORD
},
data: {
jsonrpc: '1.0'
}
}
}
// Error messages returned by a full node can be burried pretty deep inside the
// error object returned by Axios. This function attempts to extract and interpret
// error messages.
// Returns an object. If successful, obj.msg is a string.
// If there is a failure, obj.msg is false.
function decodeError (err) {
try {
// Attempt to extract the full node error message.
if (
err.response &&
err.response.data &&
err.response.data.error &&
err.response.data.error.message
) {
return { msg: err.response.data.error.message, status: 400 }
}
// Attempt to extract the Insight error message
if (err.response && err.response.data) {
return { msg: err.response.data, status: err.response.status }
}
// console.log(`err.message: ${err.message}`)
// console.log(`err: `, err)
// Attempt to detect a network connection error.
if (err.message && err.message.indexOf('ENOTFOUND') > -1) {
return {
msg:
'Network error: Could not communicate with full node or other external service.',
status: 503
}
}
// Different kind of network error
if (err.message && err.message.indexOf('ENETUNREACH') > -1) {
return {
msg:
'Network error: Could not communicate with full node or other external service.',
status: 503
}
}
// Different kind of network error
if (err.message && err.message.indexOf('EAI_AGAIN') > -1) {
return {
msg:
'Network error: Could not communicate with full node or other external service.',
status: 503
}
}
// Axios timeout (aborted) error, or service is down (connection refused).
if (
err.code &&
(err.code === 'ECONNABORTED' || err.code === 'ECONNREFUSED')
) {
return {
msg:
'Network error: Could not communicate with full node or other external service.',
status: 503
}
}
// Handle general Error objects.
if (err.message) {
return {
message: err.message,
status: 422
}
}
return { msg: false, status: 500 }
} catch (err) {
console.error('unhandled error in route-utils.js/decodeError(): ', err)
wlogger.error('unhandled error in route-utils.js/decodeError(): ', err)
return { msg: false, status: 500 }
}
}
-351
View File
@@ -1,351 +0,0 @@
const axios = require('axios')
const SLPSDK = require('@psf/bch-js')
const SLP = new SLPSDK()
class Slpdb {
// Gets transaction history for all tokens for an address. Can also specify
// block height, but defaults to 0.
async getHistoricalSlpTransactions (addressList, fromBlock = 0) {
// Build SLPDB or query from addressList
const orQueryArray = []
for (const address of addressList) {
const cashAddress = SLP.SLP.Address.toCashAddress(address)
const slpAddress = SLP.SLP.Address.toSLPAddress(address)
const cashQuery = {
'in.e.a': cashAddress.slice(12)
}
const slpQuery = {
'slp.detail.outputs.address': slpAddress
}
orQueryArray.push(cashQuery)
orQueryArray.push(slpQuery)
}
const query = {
v: 3,
q: {
find: {
db: ['c', 'u'],
$query: {
$or: orQueryArray,
'slp.valid': true,
'blk.i': {
$not: {
$lte: fromBlock
}
}
},
$orderby: {
'blk.i': -1
}
},
project: {
_id: 0,
'tx.h': 1,
'in.i': 1,
'in.e': 1,
'out.e': 1,
'out.a': 1,
'slp.detail': 1,
blk: 1
},
limit: 500
}
}
const result = await this.runQuery(query)
// console.log(`result.data: ${JSON.stringify(result.data, null, 2)}`)
let transactions = []
// Add confirmed transactions
if (result.data && result.data.c) {
transactions = transactions.concat(result.data.c)
}
// Add unconfirmed transactions
if (result.data && result.data.u) {
transactions = transactions.concat(result.data.u)
}
return transactions
}
async getTokenStats (tokenId) {
const [
totalMinted,
totalBurned,
tokenDetails,
circulatingSupply
] = await Promise.all([
this.getTotalMinted(tokenId),
this.getTotalBurned(tokenId),
this.getTokenDetails(tokenId),
this.getTotalCirculating(tokenId)
])
tokenDetails.totalMinted = tokenDetails.initialTokenQty + totalMinted
tokenDetails.totalBurned = totalBurned
// tokenDetails.circulatingSupply =
// tokenDetails.totalMinted - tokenDetails.totalBurned
tokenDetails.circulatingSupply = circulatingSupply
return tokenDetails
}
generateCredentials () {
// Generate the Basic Authentication header for a private instance of SLPDB.
const SLPDB_PASS = process.env.SLPDB_PASS
? process.env.SLPDB_PASS
: 'BITBOX'
const username = 'BITBOX'
const password = SLPDB_PASS
const combined = `${username}:${password}`
var base64Credential = Buffer.from(combined).toString('base64')
var readyCredential = `Basic ${base64Credential}`
const options = {
headers: {
authorization: readyCredential,
timeout: 30000
}
}
return options
}
async runQuery (query) {
const queryString = JSON.stringify(query)
const queryBase64 = Buffer.from(queryString).toString('base64')
const url = `${process.env.SLPDB_URL}q/${queryBase64}`
const options = this.generateCredentials()
const response = await axios.get(url, options)
return response
}
async getTotalMinted (tokenId) {
const query = {
v: 3,
q: {
db: ['g'],
aggregate: [
{
$match: {
'tokenDetails.tokenIdHex': tokenId,
'graphTxn.outputs.status': {
$in: [
'BATON_SPENT_IN_MINT',
'BATON_UNSPENT',
'BATON_SPENT_NOT_IN_MINT'
]
}
}
},
{
$unwind: '$graphTxn.outputs'
},
{
$group: {
_id: null,
count: {
$sum: '$graphTxn.outputs.slpAmount'
}
}
}
],
limit: 1
}
}
const result = await this.runQuery(query)
if (!result.data.g.length) {
return 0
}
return parseFloat(result.data.g[0].count)
}
async getTotalCirculating (tokenId) {
const query = {
v: 3,
q: {
db: ['g'],
aggregate: [
{
$match: {
'tokenDetails.tokenIdHex': tokenId,
'graphTxn.outputs': {
$elemMatch: {
status: 'UNSPENT',
slpAmount: { $gte: 0 }
}
}
}
},
{ $unwind: '$graphTxn.outputs' },
{
$match: {
'graphTxn.outputs.status': 'UNSPENT',
'graphTxn.outputs.slpAmount': { $gte: 0 }
}
},
{
$group: {
_id: null,
circulating_supply: {
$sum: '$graphTxn.outputs.slpAmount'
}
}
}
],
limit: 100000
}
}
const result = await this.runQuery(query)
// console.log(`result.data: ${JSON.stringify(result.data, null, 2)}`)
if (!result.data.g.length) {
return 0
}
return parseFloat(result.data.g[0].circulating_supply)
}
async getTotalBurned (tokenId) {
const query = {
v: 3,
q: {
db: ['g'],
aggregate: [
{
$match: {
'tokenDetails.tokenIdHex': tokenId,
'graphTxn.outputs.status': {
$in: [
'SPENT_NON_SLP',
'BATON_SPENT_INVALID_SLP',
'SPENT_INVALID_SLP',
'BATON_SPENT_NON_SLP',
'MISSING_BCH_VOUT',
'BATON_MISSING_BCH_VOUT',
'BATON_SPENT_NOT_IN_MINT',
'EXCESS_INPUT_BURNED'
]
}
}
},
{
$unwind: '$graphTxn.outputs'
},
{
$match: {
'graphTxn.outputs.status': {
$in: [
'SPENT_NON_SLP',
'BATON_SPENT_INVALID_SLP',
'SPENT_INVALID_SLP',
'BATON_SPENT_NON_SLP',
'MISSING_BCH_VOUT',
'BATON_MISSING_BCH_VOUT',
'BATON_SPENT_NOT_IN_MINT',
'EXCESS_INPUT_BURNED'
]
}
}
},
{
$group: {
_id: null,
count: {
$sum: '$graphTxn.outputs.slpAmount'
}
}
}
],
limit: 1
}
}
const result = await this.runQuery(query)
if (!result.data.g.length) {
return 0
}
return parseFloat(result.data.g[0].count)
}
async getTokenDetails (tokenId) {
const query = {
v: 3,
q: {
db: ['t'],
find: {
$query: {
'tokenDetails.tokenIdHex': tokenId
}
},
project: { tokenDetails: 1, tokenStats: 1, _id: 0 },
limit: 1
}
}
const result = await this.runQuery(query)
if (!result.data.t.length) {
throw new Error('Token could not be found')
}
const token = this.formatTokenOutput(result.data.t[0])
return token
}
formatTokenOutput (token) {
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
token.tokenDetails.id = token.tokenDetails.tokenIdHex
delete token.tokenDetails.tokenIdHex
token.tokenDetails.documentHash = token.tokenDetails.documentSha256Hex
delete token.tokenDetails.documentSha256Hex
token.tokenDetails.initialTokenQty = parseFloat(
token.tokenDetails.genesisOrMintQuantity
)
delete token.tokenDetails.genesisOrMintQuantity
delete token.tokenDetails.transactionType
delete token.tokenDetails.batonVout
delete token.tokenDetails.sendOutputs
token.tokenDetails.blockCreated = token.tokenStats.block_created
token.tokenDetails.blockLastActiveSend =
token.tokenStats.block_last_active_send
token.tokenDetails.blockLastActiveMint =
token.tokenStats.block_last_active_mint
token.tokenDetails.txnsSinceGenesis =
token.tokenStats.qty_valid_txns_since_genesis
token.tokenDetails.validAddresses =
token.tokenStats.qty_valid_token_addresses
token.tokenDetails.mintingBatonStatus =
token.tokenStats.minting_baton_status
delete token.tokenStats.block_last_active_send
delete token.tokenStats.block_last_active_mint
delete token.tokenStats.qty_valid_txns_since_genesis
delete token.tokenStats.qty_valid_token_addresses
token.tokenDetails.timestampUnix = token.tokenDetails.timestamp_unix
delete token.tokenDetails.timestamp_unix
return token.tokenDetails
}
}
module.exports = Slpdb
-2060
View File
File diff suppressed because it is too large Load Diff
-604
View File
@@ -1,604 +0,0 @@
'use strict'
const express = require('express')
const router = express.Router()
const axios = require('axios')
const routeUtils = require('./route-utils')
const wlogger = require('../../util/winston-logging')
const Blockbook = require('./blockbook')
const blockbook = new Blockbook()
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
const BCHJS = require('@psf/bch-js')
const bchjs = new BCHJS()
// const BCHJS_TESTNET = 'https://testnet.bchjs.cash/v4/'
// const bchjsHTTP = axios.create({
// baseURL: process.env.RPC_BASEURL
// })
// const username = process.env.RPC_USERNAME
// const password = process.env.RPC_PASSWORD
// const requestConfig = {
// method: 'post',
// auth: {
// username: username,
// password: password
// },
// data: {
// jsonrpc: '1.0'
// }
// }
let _this
class UtilRoute {
constructor () {
this.bchjs = bchjs
this.blockbook = blockbook
_this = this
}
root (req, res, next) {
return res.json({ status: 'util' })
}
/**
* @api {get} /util/validateAddress/{address} Get information about single bitcoin cash address.
* @apiName Information about single bitcoin cash address
* @apiGroup Util
* @apiDescription Returns information about single bitcoin cash address.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/util/validateAddress/bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c" -H "accept: application/json"
*
*
*/
async validateAddressSingle (req, res, next) {
try {
const address = req.params.address
if (!address || address === '') {
res.status(400)
return res.json({ error: 'address can not be empty' })
}
const {
BitboxHTTP,
// username,
// password,
requestConfig
} = routeUtils.setEnvVars()
requestConfig.data.id = 'validateaddress'
requestConfig.data.method = 'validateaddress'
requestConfig.data.params = [address]
const response = await BitboxHTTP(requestConfig)
return res.json(response.data.result)
} catch (err) {
// Attempt to decode the error message.
const { msg, status } = routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
wlogger.error('Error in util.ts/validateAddressSingle().', err)
res.status(500)
return res.json({ error: util.inspect(err) })
}
}
/**
* @api {post} /util/validateAddress Get information about bulk bitcoin cash addresses..
* @apiName Information about bulk bitcoin cash addresses.
* @apiGroup Util
* @apiDescription Returns information about bulk bitcoin cash addresses..
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/util/validateAddress" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c","bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0"]}'
* curl -X POST "https://api.fullstack.cash/v4/util/validateAddress" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c","bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0"],"from": 1, "to": 5}'
*
*
*/
async validateAddressBulk (req, res, next) {
try {
const addresses = req.body.addresses
// Reject if addresses is not an array.
if (!Array.isArray(addresses)) {
res.status(400)
return res.json({
error: 'addresses needs to be an array. Use GET for single address.'
})
}
// Enforce array size rate limits
if (!routeUtils.validateArraySize(req, addresses)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
error: 'Array too large.'
})
}
// Validate each element in the array.
for (let i = 0; i < addresses.length; i++) {
const address = addresses[i]
// Ensure the input is a valid BCH address.
try {
bchjs.Address.toLegacyAddress(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 networkIsValid = routeUtils.validateNetwork(address)
if (!networkIsValid) {
res.status(400)
return res.json({
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
}
wlogger.debug('Executing util/validate with these addresses: ', addresses)
const {
BitboxHTTP,
// username,
// password,
requestConfig
} = routeUtils.setEnvVars()
// Loop through each address and creates an array of requests to call in parallel
const promises = addresses.map(async address => {
requestConfig.data.id = 'validateaddress'
requestConfig.data.method = 'validateaddress'
requestConfig.data.params = [address]
return BitboxHTTP(requestConfig)
})
// Wait for all parallel Insight requests to return.
const axiosResult = await axios.all(promises)
// Retrieve the data part of the result.
const result = axiosResult.map(x => x.data.result)
res.status(200)
return res.json(result)
} catch (err) {
// Attempt to decode the error message.
const { msg, status } = routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
wlogger.error('Error in util.ts/validateAddressSingle().', err)
res.status(500)
return res.json({ error: util.inspect(err) })
}
}
/**
* @api {post} /util/sweep Sweep BCH and tokens
* @apiName Sweep BCH and tokens from a paper wallet
* @apiGroup Util
* @apiDescription This function can be used to check the BCH balance of a
* paper wallet. It can also be used to sweep BCH and tokens from a paper
* wallet and send them to a destination address.
*
* Note: It does not yet support multiple token classes on the same paper wallet.
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/util/sweep" -H "accept: application/json" -H "Content-Type: application/json" -d '{"wif":"Kz52sdXLAiKtH82iAFRi6aTZanWtW5Eyv37KWdpY6tTv7pjoHste", "balanceOnly": true}'
* curl -X POST "https://api.fullstack.cash/v4/util/sweep" -H "accept: application/json" -H "Content-Type: application/json" -d '{"wif":"Kz52sdXLAiKtH82iAFRi6aTZanWtW5Eyv37KWdpY6tTv7pjoHste", "toAddr": "bitcoincash:qpt8m4kqu963geedyrur6pdggqmv5kxwnq0rn322qu"}'
*
*
*/
async sweepWif (req, res, next) {
try {
// Validate input
const wif = req.body.wif
const toAddr = req.body.toAddr
const balanceOnly = req.body.balanceOnly
if (typeof wif !== 'string' || wif.length !== 52) {
res.status(400)
return res.json({
error: 'WIF needs to a proper compressed WIF starting with K or L'
})
}
if (!balanceOnly) {
// Only throw error if balanceOnly is false or undefined.
if (!toAddr || toAddr === '') {
res.status(400)
return res.json({ error: 'address can not be empty' })
}
}
wlogger.debug('Executing util/sweepWif with this address: ', toAddr)
// Generate a private and public key pair from the WIF.
const ecPair = bchjs.ECPair.fromWIF(wif)
const fromAddr = bchjs.ECPair.toCashAddress(ecPair)
// Get a balance on the public address
const balances = await _this.blockbook.balanceFromBlockbook(fromAddr)
// console.log(`balances: ${JSON.stringify(balances, null, 2)}`)
// Total balance is the sum of the confirmed and unconfirmed balance.
const totalBalance =
Number(balances.balance) + Number(balances.unconfirmedBalance)
// Exit if balance is zero.
if (isNaN(totalBalance) || totalBalance === 0) {
res.status(422)
return res.json({ error: 'No balance found at BCH address.' })
}
// Exit if this is a balance-only call.
if (balanceOnly) {
res.status(200)
return res.json(totalBalance)
}
// Get all UTXOs help by the address.
const utxos = await _this.blockbook.utxosFromBlockbook(fromAddr)
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
const tokenUtxos = []
const bchUtxos = []
// Exit if there are no UTXOs.
if (utxos.length === 0) {
res.status(422)
return res.json({ error: 'No utxos found.' })
}
// Figure out which UTXOs are associated with SLP tokens.
const isTokenUtxo = await _this.bchjs.SLP.Utils.tokenUtxoDetails(utxos)
// console.log(`isTokenUtxo: ${JSON.stringify(isTokenUtxo, null, 2)}`)
// Separate the bch and token UTXOs.
for (let i = 0; i < utxos.length; i++) {
// Filter based on isTokenUtxo.
if (!isTokenUtxo[i]) bchUtxos.push(utxos[i])
else tokenUtxos.push(isTokenUtxo[i])
}
// console.log(
// `bchUtxos.length: ${bchUtxos.length}, tokenUtxos.length: ${tokenUtxos.length}`
// )
// Throw error if no BCH to move tokens.
if (bchUtxos.length === 0 && tokenUtxos.length > 0) {
res.status(422)
return res.json({
error:
'Tokens found, but no BCH UTXOs found. Add BCH to wallet to move tokens.'
})
}
// console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`)
const options = {
ecPair,
utxos,
fromAddr,
toAddr,
bchUtxos,
tokenUtxos
}
let hex
// Choose the sweeping algorithm, based on if there are tokens or not.
if (tokenUtxos.length === 0) hex = await _this._sweepBCH(options)
else hex = await _this._sweepTokens(options, bchUtxos, tokenUtxos)
// console.log(`hex: ${hex}`)
// Throw error if there is more than one token class.
// Generate a transaction to move tokens and BCH.
// Broadcast the transaction.
const txid = _this.bchjs.RawTransactions.sendRawTransaction([hex])
res.status(200)
return res.json(txid)
} catch (err) {
// Attempt to decode the error message.
const { msg, status } = routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
// Catch the specific case of multiple tokens.
if (
err.message &&
err.message.indexOf('Multiple token classes detected') > -1
) {
res.status(422)
return res.json({ error: err.message })
}
wlogger.error('Error in util.js/sweepWif().', err)
console.error('Error in util.js/sweepWif().', err)
res.status(500)
return res.json({ error: err.message })
}
}
// Sweep BCH only from a private WIF.
async _sweepBCH (options) {
try {
// const wif = flags.wif
// const toAddr = flags.address
const ecPair = options.ecPair
const toAddr = options.toAddr
// const fromAddr = this.BITBOX.ECPair.toCashAddress(ecPair)
//
// // Get the UTXOs for that address.
// let utxos = await this.BITBOX.Blockbook.utxo(fromAddr)
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
let utxos = options.utxos
// Ensure all utxos have the satoshis property.
utxos = utxos.map(x => {
x.satoshis = Number(x.value)
return x
})
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
// instance of transaction builder
let transactionBuilder
if (options.testnet) {
transactionBuilder = new _this.bchjs.TransactionBuilder('testnet')
} else transactionBuilder = new _this.bchjs.TransactionBuilder()
let originalAmount = 0
// Loop through all UTXOs.
for (let i = 0; i < utxos.length; i++) {
const utxo = utxos[i]
originalAmount = originalAmount + utxo.satoshis
transactionBuilder.addInput(utxo.txid, utxo.vout)
}
if (originalAmount < 546) {
throw new Error(
'Original amount less than the dust limit. Not enough BCH to send.'
)
}
// get byte count to calculate fee. paying 1 sat/byte
const byteCount = _this.bchjs.BitcoinCash.getByteCount(
{ P2PKH: utxos.length },
{ P2PKH: 1 }
)
const fee = Math.ceil(1.1 * byteCount)
// amount to send to receiver. It's the original amount - 1 sat/byte for tx size
const sendAmount = originalAmount - fee
// add output w/ address and amount to send
transactionBuilder.addOutput(
_this.bchjs.Address.toLegacyAddress(toAddr),
sendAmount
)
// Loop through each input and sign
let redeemScript
for (var i = 0; i < utxos.length; i++) {
const utxo = utxos[i]
transactionBuilder.sign(
i,
ecPair,
redeemScript,
transactionBuilder.hashTypes.SIGHASH_ALL,
utxo.satoshis
)
}
// build tx
const tx = transactionBuilder.build()
// output rawhex
const hex = tx.toHex()
return hex
} catch (err) {
wlogger.error('Error in util.js/sweepBCH().')
throw err
}
}
// Sweep BCH and tokens from a WIF.
async _sweepTokens (options) {
try {
// const { ecPair, utxos, fromAddr, toAddr, bchUtxos, tokenUtxos } = options
const { ecPair, utxos, toAddr, bchUtxos, tokenUtxos } = options
// Input validation
if (!Array.isArray(bchUtxos) || bchUtxos.length === 0) {
throw new Error('bchUtxos need to be an array with one UTXO.')
}
if (!Array.isArray(tokenUtxos) || tokenUtxos.length === 0) {
throw new Error('tokenUtxos need to be an array with one UTXO.')
}
// if (flags.testnet)
// this.BITBOX = new config.BCHLIB({ restURL: config.TESTNET_REST })
// console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`)
// Ensure there is only one class of token in the wallet. Throw an error if
// there is more than one.
const tokenId = tokenUtxos[0].tokenId
const otherTokens = tokenUtxos.filter(x => x.tokenId !== tokenId)
if (otherTokens.length > 0) {
throw new Error(
'Multiple token classes detected. This function only supports a single class of token.'
)
}
// instance of transaction builder
let transactionBuilder
if (options.testnet) {
transactionBuilder = new _this.bchjs.TransactionBuilder('testnet')
} else transactionBuilder = new _this.bchjs.TransactionBuilder()
// Combine all the UTXOs into a single array.
const allUtxos = utxos
// console.log(`allUtxos: ${JSON.stringify(allUtxos, null, 2)}`)
// Loop through all UTXOs.
let originalAmount = 0
for (let i = 0; i < allUtxos.length; i++) {
const utxo = allUtxos[i]
originalAmount = originalAmount + utxo.satoshis
transactionBuilder.addInput(utxo.txid, utxo.vout)
}
if (originalAmount < 300) {
throw new Error(
'Not enough BCH to send. Send more BCH to the wallet to pay miner fees.'
)
}
// get byte count to calculate fee. paying 1 sat
// Note: This may not be totally accurate. Just guessing on the byteCount size.
// const byteCount = this.BITBOX.BitcoinCash.getByteCount(
// { P2PKH: 3 },
// { P2PKH: 5 }
// )
// //console.log(`byteCount: ${byteCount}`)
// const satoshisPerByte = 1.1
// const txFee = Math.floor(satoshisPerByte * byteCount)
// console.log(`txFee: ${txFee} satoshis\n`)
const txFee = 500
// amount to send back to the sending address. It's the original amount - 1 sat/byte for tx size
const remainder = originalAmount - txFee - 546
if (remainder < 1) {
throw new Error('Selected UTXO does not have enough satoshis')
}
// console.log(`remainder: ${remainder}`)
// Tally up the quantity of tokens
let tokenQty = 0
for (let i = 0; i < tokenUtxos.length; i++) {
tokenQty += tokenUtxos[i].tokenQty
}
// console.log(`tokenQty: ${tokenQty}`)
// Generate the OP_RETURN entry for an SLP SEND transaction.
// console.log(`Generating op-return.`)
const {
script,
outputs
} = _this.bchjs.SLP.TokenType1.generateSendOpReturn(tokenUtxos, tokenQty)
// console.log(`token outputs: ${outputs}`)
// Since we are sweeping all tokens from the WIF, there generateOpReturn()
// function should only compute 1 token output. If it returns 2, then there
// is something unexpected happening.
if (outputs > 1) {
throw new Error(
'More than one class of token detected. Sweep feature not supported.'
)
}
// Add OP_RETURN as first output.
const data = _this.bchjs.Script.encode(script)
transactionBuilder.addOutput(data, 0)
// Send dust transaction representing tokens being sent.
transactionBuilder.addOutput(
_this.bchjs.Address.toLegacyAddress(toAddr),
546
)
// Last output: send remaining BCH
transactionBuilder.addOutput(
_this.bchjs.Address.toLegacyAddress(toAddr),
remainder
)
// console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`)
// Sign each UTXO being consumed.
let redeemScript
for (let i = 0; i < allUtxos.length; i++) {
const thisUtxo = allUtxos[i]
// console.log(`thisUtxo: ${JSON.stringify(thisUtxo, null, 2)}`)
transactionBuilder.sign(
i,
ecPair,
redeemScript,
transactionBuilder.hashTypes.SIGHASH_ALL,
thisUtxo.satoshis
)
}
// build tx
const tx = transactionBuilder.build()
// output rawhex
const hex = tx.toHex()
// console.log(`Transaction raw hex: `)
// console.log(hex)
return hex
} catch (err) {
wlogger.error('Error in util.js/sweepBCH().')
throw err
}
}
}
const utilRoute = new UtilRoute()
router.get('/', utilRoute.root)
router.get('/validateAddress/:address', utilRoute.validateAddressSingle)
router.post('/validateAddress', utilRoute.validateAddressBulk)
router.post('/sweep', utilRoute.sweepWif)
module.exports = {
router,
// testableComponents: {
// root,
// validateAddressSingle,
// validateAddressBulk,
// sweepWif
// }
UtilRoute
}
-80
View File
@@ -1,80 +0,0 @@
/*
xpub route
*/
'use strict'
const express = require('express')
// const axios = require('axios')
const routeUtils = require('./route-utils')
const wlogger = require('../../util/winston-logging')
// const router = express.Router()
const router = express.Router()
// Used for processing error messages before sending them to the user.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
const BCHJS = require('@psf/bch-js')
const bchjs = new BCHJS()
// Connect the route endpoints to their handler functions.
router.get('/', root)
router.get('/fromXPub/:xpub', fromXPubSingle)
// Root API endpoint. Simply acknowledges that it exists.
function root (req, res, next) {
return res.json({ status: 'address' })
}
async function fromXPubSingle (req, res, next) {
try {
const xpub = req.params.xpub
const hdPath = req.query.hdPath ? req.query.hdPath : '0'
if (!xpub || xpub === '') {
res.status(400)
return res.json({ error: 'xpub can not be empty' })
}
// Reject if xpub is an array.
if (Array.isArray(xpub)) {
res.status(400)
return res.json({
error: 'xpub can not be an array. Use POST for bulk upload.'
})
}
wlogger.debug('Executing address/fromXPub with this xpub: ', xpub)
const cashAddr = bchjs.Address.fromXPub(xpub, hdPath)
const legacyAddr = bchjs.Address.toLegacyAddress(cashAddr)
res.status(200)
return res.json({
cashAddress: cashAddr,
legacyAddress: legacyAddr
})
} catch (err) {
// Attempt to decode the error message.
const { msg, status } = routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
// Write out error to error log.
wlogger.error('Error in address.ts/fromXPubSingle().', err)
res.status(500)
return res.json({ error: util.inspect(err) })
}
}
module.exports = {
router,
testableComponents: {
root,
fromXPubSingle
}
}
+196
View File
@@ -0,0 +1,196 @@
/*
bcash node routes for working with SLP tokens.
*/
'use strict'
const express = require('express')
const router = express.Router()
const axios = require('axios')
const util = require('util')
// const bitcore = require('bitcore-lib-cash')
// 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')
const config = require('../../../../config')
const RouteUtils = require('../../../util/route-utils')
const routeUtils = new RouteUtils()
const BCHJS = require('@psf/bch-js')
const bchjs = new BCHJS()
let _this
class BcashSlp {
constructor () {
_this = this
this.config = config
this.axios = axios
this.routeUtils = routeUtils
this.bchjs = bchjs
// _this.bitcore = bitcore
this.router = router
this.router.get('/', this.root)
this.router.get('/utxos/:address', this.getUtxos)
}
/**
* @api {get} /bcash/utxos/{addr} Get utxos for a single address.
* @apiName UTXOs for a single address
* @apiGroup bcash
* @apiDescription Returns an object with UTXOs associated with an address.
* This UTXOs will be hydrated with token information.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v5/bcash/utxos/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json"
*
*/
// GET handler for single balance
async getUtxos (req, res, next) {
try {
const address = req.params.address
this.bcashServer = process.env.BCASH_SERVER
if (!this.bcashServer) {
// console.warn('FULCRUM_API env var not set. Can not connect to Fulcrum indexer.')
throw new Error(
'BCASH_SERVER env var not set. Can not connect to bcash full node.'
)
}
// Reject if address is an array.
if (Array.isArray(address)) {
res.status(400)
return res.json({
success: false,
error: 'address can not be an array. Use POST for bulk upload.'
})
}
const cashAddr = _this.bchjs.Address.toCashAddress(address)
// Prevent a common user error. Ensure they are using the correct network address.
const networkIsValid = _this.routeUtils.validateNetwork(cashAddr)
if (!networkIsValid) {
res.status(400)
return res.json({
success: false,
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
// TODO: Customize this function below here for bcash, based on this gist:
// https://gist.github.com/christroutner/dcb25a895900e557d7b43341ee85fa79
wlogger.debug(
'Executing bcash/slp.js/getUtxos(). with this address: ',
cashAddr
)
// Get data from ElectrumX server.
const response = await _this.axios.get(
`${_this.bcashServer}coin/address/${cashAddr}?slp=true`
)
// Get address UTXOs
const utxos = response.data
// Hydrate UTXOs
const hydratedUtxos = await _this.hydrateUTXOS(utxos)
res.status(200)
return res.json(hydratedUtxos)
} catch (err) {
// Write out error to error log.
wlogger.error('Error in bcash/slp.js/getUtxos().', err)
return _this.errorHandler(err, res)
}
}
// Maps and filter the SLP UTXOs of an UTXOs array
// get information about the SLP UTXOs
async hydrateUTXOS (UTXOS) {
try {
if (!Array.isArray(UTXOS)) {
throw new Error('UTXOs must be an array of slp utxos')
}
const slpUtxos = UTXOS.filter((val) => val.slp)
const hydrated = []
// Find information of each token
for (let i = 0; i < slpUtxos.length; i++) {
const slp = slpUtxos[i].slp
const info = await _this.getTokenInfo(slp.tokenId)
const obj = Object.assign(slp, info)
slpUtxos[i].slp = obj
hydrated.push(slpUtxos[i])
}
return hydrated
} catch (error) {
console.log(error)
throw error
}
}
// Get information of a token
async getTokenInfo (tokenId) {
try {
if (!tokenId || typeof tokenId !== 'string') {
throw new Error('tokenId must be string')
}
this.bcashServer = process.env.BCASH_SERVER
if (!this.bcashServer) {
// console.warn('FULCRUM_API env var not set. Can not connect to Fulcrum indexer.')
throw new Error(
'BCASH_SERVER env var not set. Can not connect to bcash full node.'
)
}
const result = await _this.axios.get(
`${_this.bcashServer}token/${tokenId}`
)
const tokenInfo = result.data
// console.log('token info', tokenInfo)
return tokenInfo
} catch (error) {
console.log(error)
throw error
}
}
// DRY error handler.
errorHandler (err, res) {
// Attempt to decode the error message.
const { msg, status } = _this.routeUtils.decodeError(err)
// console.log('errorHandler msg: ', msg)
// console.log('errorHandler status: ', status)
if (msg) {
res.status(status)
return res.json({ success: false, error: msg })
}
// Handle error patterns specific to this route.
if (err.message) {
res.status(400)
return res.json({ success: false, error: err.message })
}
// If error can be handled, return the stack trace
res.status(500)
return res.json({ error: util.inspect(err) })
}
// Root API endpoint. Simply acknowledges that it exists.
root (req, res, next) {
return res.json({ status: 'bcash-slp' })
}
}
module.exports = BcashSlp
File diff suppressed because it is too large Load Diff
@@ -18,7 +18,7 @@ const routeUtils = new RouteUtils()
const BCHJS = require('@psf/bch-js') const BCHJS = require('@psf/bch-js')
const restURL = process.env.LOCAL_RESTURL const restURL = process.env.LOCAL_RESTURL
? process.env.LOCAL_RESTURL ? process.env.LOCAL_RESTURL
: 'https://api.fullstack.cash/v4/' : 'https://api.fullstack.cash/v5/'
const bchjs = new BCHJS({ restURL }) const bchjs = new BCHJS({ restURL })
let _this let _this
@@ -76,7 +76,7 @@ class Encryption {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/encryption/publickey/bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/encryption/publickey/bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" -H "accept: application/json"
* *
*/ */
async getPublicKey (req, res, next) { async getPublicKey (req, res, next) {
@@ -92,6 +92,15 @@ class Encryption {
}) })
} }
// Generate a user object that can be passed along with internal calls
// from bch-js.
const usrObj = {
ip: req._remoteAddress,
jwtToken: req.locals.jwtToken,
proLimit: req.locals.proLimit,
apiLevel: req.locals.apiLevel
}
const cashAddr = _this.bchjs.Address.toCashAddress(address) const cashAddr = _this.bchjs.Address.toCashAddress(address)
// Prevent a common user error. Ensure they are using the correct network address. // Prevent a common user error. Ensure they are using the correct network address.
@@ -105,16 +114,17 @@ class Encryption {
}) })
} }
// console.log(
wlogger.debug( wlogger.debug(
'Executing encryption/getPublicKey with this address: ', 'Executing encryption/getPublicKey with this address: ',
cashAddr cashAddr
) )
const rawTxData = await _this.bchjs.Electrumx.transactions(cashAddr) const rawTxData = await _this.bchjs.Electrumx.transactions([cashAddr], usrObj)
// console.log(`rawTxData: ${JSON.stringify(rawTxData, null, 2)}`) // console.log(`rawTxData: ${JSON.stringify(rawTxData, null, 2)}`)
// Extract just the TXIDs // Extract just the TXIDs
const txids = rawTxData.transactions.map((elem) => elem.tx_hash) const txids = rawTxData.transactions[0].transactions.map((elem) => elem.tx_hash)
// console.log(`txids: ${JSON.stringify(txids, null, 2)}`) // console.log(`txids: ${JSON.stringify(txids, null, 2)}`)
// throw error if there is no transaction history. // throw error if there is no transaction history.
@@ -127,12 +137,13 @@ class Encryption {
const thisTx = txids[i] const thisTx = txids[i]
const txDetails = await _this.bchjs.RawTransactions.getRawTransaction( const txDetails = await _this.bchjs.RawTransactions.getRawTransaction(
thisTx, [thisTx],
true true,
usrObj
) )
// console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`) // console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`)
const vin = txDetails.vin const vin = txDetails[0].vin
// Loop through each input. // Loop through each input.
for (let j = 0; j < vin.length; j++) { for (let j = 0; j < vin.length; j++) {
@@ -170,6 +181,7 @@ class Encryption {
publicKey: 'not found' publicKey: 'not found'
}) })
} catch (err) { } catch (err) {
// console.log('Error in encryption.js/getPublicKey().', err)
wlogger.error('Error in encryption.js/getPublicKey().', err) wlogger.error('Error in encryption.js/getPublicKey().', err)
return _this.errorHandler(err, res) return _this.errorHandler(err, res)
@@ -53,6 +53,8 @@ class Blockchain {
this.router.post('/getTxOutProof', this.getTxOutProofBulk) this.router.post('/getTxOutProof', this.getTxOutProofBulk)
this.router.get('/verifyTxOutProof/:proof', this.verifyTxOutProofSingle) this.router.get('/verifyTxOutProof/:proof', this.verifyTxOutProofSingle)
this.router.post('/verifyTxOutProof', this.verifyTxOutProofBulk) this.router.post('/verifyTxOutProof', this.verifyTxOutProofBulk)
this.router.post('/getBlock', this.getBlock)
this.router.get('/getBlockHash/:height', this.getBlockHash)
} }
root (req, res, next) { root (req, res, next) {
@@ -80,7 +82,7 @@ class Blockchain {
* block chain. * block chain.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getBestBlockHash" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/blockchain/getBestBlockHash" -H "accept: application/json"
* *
* @apiSuccess {String} bestBlockHash 000000000000000002bc884334336d99c9a9c616670a9244c6a8c1fc35aa91a1 * @apiSuccess {String} bestBlockHash 000000000000000002bc884334336d99c9a9c616670a9244c6a8c1fc35aa91a1
*/ */
@@ -111,7 +113,7 @@ class Blockchain {
* @apiDescription Returns an object containing various state info regarding blockchain processing. * @apiDescription Returns an object containing various state info regarding blockchain processing.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getBlockchainInfo" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/blockchain/getBlockchainInfo" -H "accept: application/json"
* *
* @apiSuccess {Object} object Object containing data * @apiSuccess {Object} object Object containing data
* @apiSuccess {String} object.chain "main" * @apiSuccess {String} object.chain "main"
@@ -155,7 +157,7 @@ class Blockchain {
* @apiDescription Returns the number of blocks in the longest blockchain. * @apiDescription Returns the number of blocks in the longest blockchain.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getBlockCount" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/blockchain/getBlockCount" -H "accept: application/json"
* *
* @apiSuccess {Number} bestBlockCount 587665 * @apiSuccess {Number} bestBlockCount 587665
*/ */
@@ -188,7 +190,7 @@ class Blockchain {
* returns an Object with information about blockheader hash. * returns an Object with information about blockheader hash.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getBlockHeader/000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201?verbose=true" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/blockchain/getBlockHeader/000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201?verbose=true" -H "accept: application/json"
* *
* @apiParam {String} hash block hash * @apiParam {String} hash block hash
* @apiParam {Boolean} verbose Return verbose data * @apiParam {Boolean} verbose Return verbose data
@@ -249,7 +251,7 @@ class Blockchain {
* returns an Object with information about blockheader hash. * returns an Object with information about blockheader hash.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/blockchain/getBlockHeader" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"hashes\":[\"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201\",\"00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3\"],\"verbose\":true}" * curl -X POST "https://api.fullstack.cash/v5/blockchain/getBlockHeader" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"hashes\":[\"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201\",\"00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3\"],\"verbose\":true}"
* *
* @apiParam {String} hash block hash * @apiParam {String} hash block hash
* @apiParam {Boolean} verbose Return verbose data * @apiParam {Boolean} verbose Return verbose data
@@ -284,7 +286,7 @@ class Blockchain {
// Enforce array size rate limits // Enforce array size rate limits
if (!routeUtils.validateArraySize(req, hashes)) { if (!routeUtils.validateArraySize(req, hashes)) {
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330 res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -341,7 +343,7 @@ class Blockchain {
* including the main chain as well as orphaned branches. * including the main chain as well as orphaned branches.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getChainTips" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/blockchain/getChainTips" -H "accept: application/json"
* *
*/ */
async getChainTips (req, res, next) { async getChainTips (req, res, next) {
@@ -372,7 +374,7 @@ class Blockchain {
* power on the network. * power on the network.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getDifficulty" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/blockchain/getDifficulty" -H "accept: application/json"
* *
*/ */
async getDifficulty (req, res, next) { async getDifficulty (req, res, next) {
@@ -404,7 +406,7 @@ class Blockchain {
* mempool (unconfirmed) * mempool (unconfirmed)
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getMempoolEntry/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/blockchain/getMempoolEntry/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
* *
*/ */
async getMempoolEntrySingle (req, res, next) { async getMempoolEntrySingle (req, res, next) {
@@ -442,7 +444,7 @@ class Blockchain {
* @apiDescription Returns mempool data for multiple transactions * @apiDescription Returns mempool data for multiple transactions
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X POST https://api.fullstack.cash/v4/blockchain/getMempoolEntry -H "Content-Type: application/json" -d "{\"txids\":[\"a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1\",\"5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e\"]}" * curl -X POST https://api.fullstack.cash/v5/blockchain/getMempoolEntry -H "Content-Type: application/json" -d "{\"txids\":[\"a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1\",\"5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e\"]}"
*/ */
async getMempoolEntryBulk (req, res, next) { async getMempoolEntryBulk (req, res, next) {
try { try {
@@ -457,7 +459,7 @@ class Blockchain {
// Enforce array size rate limits // Enforce array size rate limits
if (!routeUtils.validateArraySize(req, txids)) { if (!routeUtils.validateArraySize(req, txids)) {
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330 res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -515,7 +517,7 @@ class Blockchain {
* against the 25 ancestor chain-limit. * against the 25 ancestor chain-limit.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getMempoolAncestors/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/blockchain/getMempoolAncestors/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
* *
*/ */
async getMempoolAncestorsSingle (req, res, next) { async getMempoolAncestorsSingle (req, res, next) {
@@ -557,7 +559,7 @@ class Blockchain {
* @apiDescription Returns details on the active state of the TX memory pool. * @apiDescription Returns details on the active state of the TX memory pool.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET https://api.fullstack.cash/v4/getMempoolInfo -H "accept: application/json" * curl -X GET https://api.fullstack.cash/v5/getMempoolInfo -H "accept: application/json"
* *
*/ */
async getMempoolInfo (req, res, next) { async getMempoolInfo (req, res, next) {
@@ -587,7 +589,7 @@ class Blockchain {
* @apiDescription Returns details on the active state of the TX memory pool. * @apiDescription Returns details on the active state of the TX memory pool.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET https://api.fullstack.cash/v4/getMempoolInfo -H "accept: application/json" * curl -X GET https://api.fullstack.cash/v5/getMempoolInfo -H "accept: application/json"
* *
*/ */
@@ -599,7 +601,7 @@ class Blockchain {
* of string transaction ids. * of string transaction ids.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/getRawMempool/?verbose=true" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/getRawMempool/?verbose=true" -H "accept: application/json"
* *
* @apiParam {Boolean} verbose Return verbose data * @apiParam {Boolean} verbose Return verbose data
* *
@@ -635,7 +637,7 @@ class Blockchain {
* @apiDescription Returns details about an unspent transaction output. * @apiDescription Returns details about an unspent transaction output.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getTxOut/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33/0?mempool=false" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/blockchain/getTxOut/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33/0?mempool=false" -H "accept: application/json"
* *
* @apiParam {String} txid Transaction id (required) * @apiParam {String} txid Transaction id (required)
* @apiParam {Number} n Output number (required) * @apiParam {Number} n Output number (required)
@@ -692,7 +694,7 @@ class Blockchain {
* @apiDescription Returns details about an unspent transaction output (UTXO). * @apiDescription Returns details about an unspent transaction output (UTXO).
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl "https://api.fullstack.cash/v4/blockchain/getTxOut/" -X POST -H "Content-Type: application/json" --data-binary '{"txid":"d5228d2cdc77fbe5a9aa79f19b0933b6802f9f0067f42847fc4fe343664723e5","vout":0,"mempool":true}' * curl "https://api.fullstack.cash/v5/blockchain/getTxOut/" -X POST -H "Content-Type: application/json" --data-binary '{"txid":"d5228d2cdc77fbe5a9aa79f19b0933b6802f9f0067f42847fc4fe343664723e5","vout":0,"mempool":true}'
* *
* @apiParam {String} txid Transaction id (required) * @apiParam {String} txid Transaction id (required)
* @apiParam {Number} vout of transaction (required) * @apiParam {Number} vout of transaction (required)
@@ -746,7 +748,7 @@ class Blockchain {
* @apiDescription Returns a hex-encoded proof that 'txid' was included in a block. * @apiDescription Returns a hex-encoded proof that 'txid' was included in a block.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getTxOutProofSingle/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/blockchain/getTxOutProofSingle/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
* *
* @apiParam {String} txid Transaction id (required) * @apiParam {String} txid Transaction id (required)
* *
@@ -794,7 +796,7 @@ class Blockchain {
// Enforce array size rate limits // Enforce array size rate limits
if (!routeUtils.validateArraySize(req, txids)) { if (!routeUtils.validateArraySize(req, txids)) {
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330 res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -888,7 +890,7 @@ class Blockchain {
// Enforce array size rate limits // Enforce array size rate limits
if (!routeUtils.validateArraySize(req, proofs)) { if (!routeUtils.validateArraySize(req, proofs)) {
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330 res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -937,6 +939,93 @@ class Blockchain {
return _this.errorHandler(err, res) return _this.errorHandler(err, res)
} }
} }
/**
* @api {post} /blockchain/getBlock/ Get block details
* @apiName getBlock
* @apiGroup Blockchain
* @apiDescription Returns block details
*
* @apiExample Example usage:
* curl "https://api.fullstack.cash/v5/blockchain/getblock/" -X POST -H "Content-Type: application/json" --data-binary '{"blockhash":"000000000000000002a5fe0bdd6e3f04342a975c0f55e57f97e73bb90041676b","verbosity":0 }'
*
* @apiParam {String} blockhash Block hash (required)
* @apiParam {Number} verbosity Default 1 (optional)
*
*/
async getBlock (req, res, next) {
try {
// Validate input parameter
const blockhash = req.body.blockhash
let verbosity = req.body.verbosity
// Default to a value of 1 if another verbosity level is not defined.
if (!verbosity && verbosity !== 0) verbosity = 1
if (!blockhash || blockhash === '') {
res.status(400)
return res.json({ error: 'blockhash can not be empty' })
}
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getblock'
options.data.method = 'getblock'
options.data.params = [blockhash, verbosity]
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.js/getBlock()', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {post} /blockchain/getBlockHash/ Get block hash
* @apiName getBlockHash
* @apiGroup Blockchain
* @apiDescription Returns the hash of a block, given its block height.
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v5/blockchain/getBlockHash/544444" -H "accept: application/json"
*
* @apiParam {String} height Block height (required)
*
*
*/
async getBlockHash (req, res, next) {
try {
// Validate input parameter
const height = req.params.height
if (!height || height === '') {
res.status(400)
return res.json({ error: 'height can not be empty' })
}
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getblockhash'
options.data.method = 'getblockhash'
options.data.params = [parseInt(height)]
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.js/getBlockHash()', err)
return _this.errorHandler(err, res)
}
}
} }
module.exports = Blockchain module.exports = Blockchain
@@ -53,7 +53,7 @@ class Control {
* @apiDescription RPC call which gets basic full node information. * @apiDescription RPC call which gets basic full node information.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/control/getnetworkinfo" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/control/getnetworkinfo" -H "accept: application/json"
* *
*/ */
async getNetworkInfo (req, res, next) { async getNetworkInfo (req, res, next) {
@@ -75,42 +75,6 @@ class Control {
return _this.errorHandler(error, res) return _this.errorHandler(error, res)
} }
} }
// router.get('/getMemoryInfo', (req, res, next) => {
// BitboxHTTP({
// method: 'post',
// auth: {
// username: username,
// password: password
// },
// data: {
// jsonrpc: "1.0",
// id:"getmemoryinfo",
// method: "getmemoryinfo"
// }
// })
// .then((response) => {
// res.json(response.data.result);
// })
// .catch((error) => {
// res.send(error.response.data.error.message);
// });
// });
//
// router.get('/help', (req, res, next) => {
// BITBOX.Control.help()
// .then((result) => {
// res.json(result);
// }, (err) => { console.log(err);
// });
// });
//
// router.post('/stop', (req, res, next) => {
// BITBOX.Control.stop()
// .then((result) => {
// res.json(result);
// }, (err) => { console.log(err);
// });
// });
} }
module.exports = Control module.exports = Control
+95
View File
@@ -0,0 +1,95 @@
'use strict'
const express = require('express')
const router = express.Router()
const axios = require('axios')
const wlogger = require('../../../util/winston-logging')
const RouteUtils = require('../../../util/route-utils')
const routeUtils = new RouteUtils()
// Used to convert error messages to strings, to safely pass to users.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
let _this
class DSProof {
constructor () {
_this = this
_this.axios = axios
_this.routeUtils = routeUtils
_this.router = router
_this.router.get('/', this.root)
_this.router.get('/getdsproof/:txid', _this.getDSProof)
}
// DRY error handler.
errorHandler (err, res) {
// Attempt to decode the error message.
const { msg, status } = _this.routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
res.status(500)
return res.json({ error: util.inspect(err) })
}
root (req, res, next) {
return res.json({ status: 'dsproof' })
}
/**
* @api {get} /dsproof/getdsproof/:txid Get Double-Spend Proof.
* @apiName DS Proof.
* @apiGroup DSProof
* @apiDescription Get information for a double-spend proof.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v5/dsproof/getdsproof/a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d" -H "accept: application/json"
*
*
*/
async getDSProof (req, res, next) {
try {
const txid = req.params.txid
let verbose = 2 // default
if (req.query.verbose === 'true') verbose = 3
if (!txid || txid === '') {
res.status(400)
return res.json({
success: false,
error: 'txid can not be empty'
})
}
if (txid.length !== 64) {
res.status(400)
return res.json({
success: false,
error: `txid must be of length 64 (not ${txid.length})`
})
}
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getdsproof'
options.data.method = 'getdsproof'
options.data.params = [txid, verbose]
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
wlogger.error('Error in dsproof.js/getDSProof().', err)
return _this.errorHandler(err, res)
}
}
}
module.exports = DSProof
@@ -77,7 +77,7 @@ class Mining {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/mining/getMiningInfo" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/mining/getMiningInfo" -H "accept: application/json"
* *
* *
*/ */
@@ -107,7 +107,7 @@ class Mining {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/mining/getNetworkHashps?nblocks=120&height=-1" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/mining/getNetworkHashps?nblocks=120&height=-1" -H "accept: application/json"
* *
* *
*/ */
@@ -65,7 +65,7 @@ class RawTransactions {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/rawtransactions/decodeRawTransaction/02000000010e991f7ccec410f27d333f737f149b5d3be6728687da81072e638aed0063a176010000006b483045022100cd20443b0af090053450bc4ab00d563d4ac5955bb36e0135b00b8a96a19f233302205047f2c70a08c6ef4b76f2d198b33a31d17edfaa7e1e9e865894da0d396009354121024d4e7f522f67105b7bf5f9dbe557e7b2244613fdfcd6fe09304f93877328f6beffffffff02a0860100000000001976a9140ee020c07f39526ac5505c54fa1ab98490979b8388acb5f0f70b000000001976a9143a9b2b0c12fe722fcf653b6ef5dcc38732d6ff5188ac00000000" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/rawtransactions/decodeRawTransaction/02000000010e991f7ccec410f27d333f737f149b5d3be6728687da81072e638aed0063a176010000006b483045022100cd20443b0af090053450bc4ab00d563d4ac5955bb36e0135b00b8a96a19f233302205047f2c70a08c6ef4b76f2d198b33a31d17edfaa7e1e9e865894da0d396009354121024d4e7f522f67105b7bf5f9dbe557e7b2244613fdfcd6fe09304f93877328f6beffffffff02a0860100000000001976a9140ee020c07f39526ac5505c54fa1ab98490979b8388acb5f0f70b000000001976a9143a9b2b0c12fe722fcf653b6ef5dcc38732d6ff5188ac00000000" -H "accept: application/json"
*/ */
async decodeRawTransactionSingle (req, res, next) { async decodeRawTransactionSingle (req, res, next) {
try { try {
@@ -103,7 +103,7 @@ class RawTransactions {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/rawtransactions/decodeRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}' * curl -X POST "https://api.fullstack.cash/v5/rawtransactions/decodeRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
* *
* *
*/ */
@@ -118,7 +118,7 @@ class RawTransactions {
// Enforce array size rate limits // Enforce array size rate limits
if (!_this.routeUtils.validateArraySize(req, hexes)) { if (!_this.routeUtils.validateArraySize(req, hexes)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -140,7 +140,7 @@ class RawTransactions {
const options = _this.routeUtils.getAxiosOptions() const options = _this.routeUtils.getAxiosOptions()
// Loop through each height and creates an array of requests to call in parallel // Loop through each height and creates an array of requests to call in parallel
const promises = hexes.map(async hex => { const promises = hexes.map(async (hex) => {
options.data.id = 'decoderawtransaction' options.data.id = 'decoderawtransaction'
options.data.method = 'decoderawtransaction' options.data.method = 'decoderawtransaction'
options.data.params = [hex] options.data.params = [hex]
@@ -152,7 +152,7 @@ class RawTransactions {
const axiosResult = await _this.axios.all(promises) const axiosResult = await _this.axios.all(promises)
// Retrieve the data part of the result. // Retrieve the data part of the result.
const result = axiosResult.map(x => x.data.result) const result = axiosResult.map((x) => x.data.result)
res.status(200) res.status(200)
return res.json(result) return res.json(result)
@@ -177,7 +177,7 @@ class RawTransactions {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/rawtransactions/decodeScript/4830450221009a51e00ec3524a7389592bc27bea4af5104a59510f5f0cfafa64bbd5c164ca2e02206c2a8bbb47eabdeed52f17d7df668d521600286406930426e3a9415fe10ed592012102e6e1423f7abde8b70bca3e78a7d030e5efabd3eb35c19302542b5fe7879c1a16" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/rawtransactions/decodeScript/4830450221009a51e00ec3524a7389592bc27bea4af5104a59510f5f0cfafa64bbd5c164ca2e02206c2a8bbb47eabdeed52f17d7df668d521600286406930426e3a9415fe10ed592012102e6e1423f7abde8b70bca3e78a7d030e5efabd3eb35c19302542b5fe7879c1a16" -H "accept: application/json"
* *
* *
*/ */
@@ -218,7 +218,7 @@ class RawTransactions {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
*curl -X POST "https://api.fullstack.cash/v3/rawtransactions/decodeScript" -H "accept:" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}' *curl -X POST "https://api.fullstack.cash/v5/rawtransactions/decodeScript" -H "accept:" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
* *
* *
*/ */
@@ -234,7 +234,7 @@ class RawTransactions {
// Enforce array size rate limits // Enforce array size rate limits
if (!_this.routeUtils.validateArraySize(req, hexes)) { if (!_this.routeUtils.validateArraySize(req, hexes)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -254,7 +254,7 @@ class RawTransactions {
const options = _this.routeUtils.getAxiosOptions() const options = _this.routeUtils.getAxiosOptions()
// Loop through each hex and create an array of promises // Loop through each hex and create an array of promises
const promises = hexes.map(async hex => { const promises = hexes.map(async (hex) => {
options.data.id = 'decodescript' options.data.id = 'decodescript'
options.data.method = 'decodescript' options.data.method = 'decodescript'
options.data.params = [hex] options.data.params = [hex]
@@ -267,7 +267,7 @@ class RawTransactions {
const resolved = await Promise.all(promises) const resolved = await Promise.all(promises)
// Retrieve the data from each resolved promise. // Retrieve the data from each resolved promise.
const result = resolved.map(x => x.data.result) const result = resolved.map((x) => x.data.result)
res.status(200) res.status(200)
return res.json(result) return res.json(result)
@@ -308,7 +308,7 @@ class RawTransactions {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/rawtransactions/getRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1","5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e"],"verbose":true}' * curl -X POST "https://api.fullstack.cash/v5/rawtransactions/getRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1","5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e"],"verbose":true}'
* *
*/ */
async getRawTransactionBulk (req, res, next) { async getRawTransactionBulk (req, res, next) {
@@ -324,20 +324,12 @@ class RawTransactions {
// Enforce array size rate limits // Enforce array size rate limits
if (!_this.routeUtils.validateArraySize(req, txids)) { if (!_this.routeUtils.validateArraySize(req, txids)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
} }
// stub response object
// const returnResponse = {
// status: 100,
// json: {
// error: ''
// }
// }
// Validate each txid in the array. // Validate each txid in the array.
for (let i = 0; i < txids.length; i++) { for (let i = 0; i < txids.length; i++) {
const txid = txids[i] const txid = txids[i]
@@ -356,7 +348,7 @@ class RawTransactions {
} }
// Loop through each txid and create an array of promises // Loop through each txid and create an array of promises
const promises = txids.map(async txid => const promises = txids.map(async (txid) =>
_this.getRawTransactionsFromNode(txid, verbose) _this.getRawTransactionsFromNode(txid, verbose)
) )
@@ -383,7 +375,7 @@ class RawTransactions {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/rawtransactions/getRawTransaction/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33?verbose=true" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/rawtransactions/getRawTransaction/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33?verbose=true" -H "accept: application/json"
* *
* *
*/ */
@@ -429,7 +421,7 @@ class RawTransactions {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/rawtransactions/sendRawTransaction" -H "accept:application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}' * curl -X POST "https://api.fullstack.cash/v5/rawtransactions/sendRawTransaction" -H "accept:application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
* *
* *
*/ */
@@ -444,11 +436,12 @@ class RawTransactions {
return res.json({ error: 'hex must be an array' }) return res.json({ error: 'hex must be an array' })
} }
const options = _this.routeUtils.getAxiosOptions() let options = _this.routeUtils.getAxiosOptions()
options = _this.sendTxOptions(options)
// Enforce array size rate limits // Enforce array size rate limits
if (!_this.routeUtils.validateArraySize(req, hexes)) { if (!_this.routeUtils.validateArraySize(req, hexes)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -522,7 +515,7 @@ class RawTransactions {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/rawtransactions/sendRawTransaction/01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000" -H "accept: " * curl -X GET "https://api.fullstack.cash/v5/rawtransactions/sendRawTransaction/01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000" -H "accept: "
* *
* *
*/ */
@@ -544,7 +537,8 @@ class RawTransactions {
}) })
} }
const options = _this.routeUtils.getAxiosOptions() let options = _this.routeUtils.getAxiosOptions()
options = _this.sendTxOptions(options)
// RPC call // RPC call
options.data.id = 'sendrawtransaction' options.data.id = 'sendrawtransaction'
@@ -565,6 +559,31 @@ class RawTransactions {
return _this.errorHandler(err, res) return _this.errorHandler(err, res)
} }
} }
// This method modifies the default axios options. It attempts to inject
// a specific full node to use when broadcasting transactions. This is useful
// because it leverages the built-in protections that a full node has against
// accidental double spends. It mitigates a corner-case when rapidly spending
// TXs on load balanced nodes. By piping all TX sends through a single node,
// accidental double spends can be reduced.
sendTxOptions (options) {
try {
const sendUrl = process.env.RPC_SENDURL
if (sendUrl !== 'undefined' && sendUrl !== undefined) {
// console.log(`original options: ${JSON.stringify(options, null, 2)}`)
options.baseURL = process.env.RPC_SENDURL
// console.log(`modified options: ${JSON.stringify(options, null, 2)}`)
}
return options
} catch (err) {
wlogger.error('Error in rawtransactions.js/sendTxOptions()')
throw err
}
}
} }
module.exports = RawTransactions module.exports = RawTransactions
+95
View File
@@ -0,0 +1,95 @@
/*
Utility functions for troubleshooting a JWT token.
*/
'use strict'
// Public npm libraries.
const jwt = require('jsonwebtoken')
const axios = require('axios')
const express = require('express')
const router = express.Router()
const config = require('../../../config')
const wlogger = require('../../util/winston-logging')
const RouteUtils = require('../../util/route-utils')
const routeUtils = new RouteUtils()
// Used for processing error messages before sending them to the user.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
let _this
class JWT {
constructor () {
_this = this
_this.axios = axios
_this.routeUtils = routeUtils
_this.jwt = jwt
_this.config = config
_this.router = router
_this.router.get('/', _this.root)
_this.router.post('/info', _this.jwtInfo)
}
root (req, res, next) {
return res.json({ status: 'jwt' })
}
// DRY error handler.
errorHandler (err, res) {
// Attempt to decode the error message.
const { msg, status } = _this.routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
res.status(500)
return res.json({ error: util.inspect(err) })
}
/**
* @api {post} /jwt/info Get JWT Info
* @apiName GetJWTInfo
* @apiGroup JWT
* @apiDescription
* Get info on your JWT token. Useful for debugging rate limit issues with your
* JWT token.
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v5/jwt/info" -H "accept: application/json" -H "Content-Type: application/json" -d '{"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwZGRmZmI2NzRhM2Q2MDAxOTY3NjE1NCIsImVtYWlsIjoiY2hyaXNAYmNodGVzdC5uZXQiLCJhcGlMZXZlbCI6NjAsInJhdGVMaW1pdCI6MywicG9pbnRzVG9Db25zdW1lIjoxNiwiZHVyYXRpb24iOjMwLCJpYXQiOjE2MjU0MzQ2MzYsImv5cCI6MTYyODAyNjYzNn0.1WLugTkQKVG0yMXD1h5nxfho3gRzvSvs8NMa9obVhPM"}'
*
*/
async jwtInfo (req, res, next) {
try {
const jwtIn = req.body.jwt
// console.log('jwt: ', jwtIn)
// console.log('_this.config.apiTokenSecret: ', _this.config.apiTokenSecret)
const decoded = _this.jwt.verify(jwtIn, _this.config.apiTokenSecret)
// console.log('decoded: ', decoded)
const expiration = new Date(decoded.exp * 1000)
decoded.expiration = expiration.toISOString()
const createdAt = new Date(decoded.iat * 1000)
decoded.createdAt = createdAt.toISOString()
res.status(200)
return res.json(decoded)
} catch (error) {
// console.log('Error in jwt.js/jwtInfo().', error)
wlogger.error('Error in jwt.js/jwtInfo().', error)
return _this.errorHandler(error, res)
}
}
}
module.exports = JWT
@@ -25,13 +25,17 @@ class Price {
this.priceUrl = 'https://api.coinbase.com/v2/exchange-rates?currency=BCH' this.priceUrl = 'https://api.coinbase.com/v2/exchange-rates?currency=BCH'
this.coinexPriceUrl = this.coinexPriceUrl =
'https://api.coinex.com/v1/market/ticker?market=bchausdt' 'https://api.coinex.com/v1/market/ticker?market=xecusdt'
this.bchCoinexPriceUrl =
'https://api.coinex.com/v1/market/ticker?market=bchusdt'
this.router = express.Router() this.router = express.Router()
this.router.get('/', _this.root) this.router.get('/', _this.root)
this.router.get('/usd', _this.getUSD) this.router.get('/usd', _this.getUSD)
this.router.get('/rates', _this.getBCHRate) this.router.get('/rates', _this.getBCHRate)
this.router.get('/bchausd', _this.getBCHAUSD) this.router.get('/bchausd', _this.getBCHAUSD)
this.router.get('/bchusd', _this.getBCHUSD)
} }
// DRY error handler. // DRY error handler.
@@ -60,7 +64,7 @@ class Price {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/price/usd" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/price/usd" -H "accept: application/json"
* *
*/ */
async getUSD (req, res, next) { async getUSD (req, res, next) {
@@ -92,7 +96,7 @@ class Price {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/price/rates" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/price/rates" -H "accept: application/json"
* *
*/ */
// Get rates for several different currencies // Get rates for several different currencies
@@ -125,7 +129,7 @@ class Price {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/price/bchausd" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/price/bchausd" -H "accept: application/json"
* *
*/ */
async getBCHAUSD (req, res, next) { async getBCHAUSD (req, res, next) {
@@ -150,6 +154,40 @@ class Price {
return _this.errorHandler(err, res) return _this.errorHandler(err, res)
} }
} }
/**
* @api {get} /price/bchusd Get the USD price of BCH
* @apiName Get the USD price of BCH
* @apiGroup Price
* @apiDescription Get the USD price of BCH from Coinex.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v5/price/bchusd" -H "accept: application/json"
*
*/
async getBCHUSD (req, res, next) {
try {
// Request options
const opt = {
method: 'get',
baseURL: _this.bchCoinexPriceUrl,
timeout: 15000
}
const response = await axios.request(opt)
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
const price = Number(response.data.data.ticker.last)
return res.json({ usd: price })
} catch (err) {
// Write out error to error log.
wlogger.error('Error in price.js/getBCHUSD().', err)
return _this.errorHandler(err, res)
}
}
} }
module.exports = Price module.exports = Price
+474
View File
@@ -0,0 +1,474 @@
/*
Routes for interacting with the psf-slp-indexer
*/
// Public npm libraries
const express = require('express')
const router = express.Router()
const axios = require('axios')
const util = require('util')
const BCHJS = require('@psf/bch-js')
// Local libraries
const RouteUtils = require('../../util/route-utils')
const config = require('../../../config')
const routeUtils = new RouteUtils()
const bchjs = new BCHJS({ restURL: config.restURL })
let _this
class PsfSlpIndexer {
constructor () {
// Encapsulate dependencies
this.axios = axios
this.router = router
this.routeUtils = routeUtils
this.config = config
this.bchjs = bchjs
// Define routes
this.router.get('/', this.root)
this.router.get('/status', this.getStatus)
this.router.post('/address', this.getAddress)
this.router.post('/txid', this.getTxid)
this.router.post('/token', this.getTokenStats)
this.router.post('/token/data', this.getTokenData)
_this = this
}
// Root API endpoint. Simply acknowledges that it exists.
root (req, res, next) {
return res.json({ status: 'psf-slp-indexer' })
}
/**
* @api {get} /psf/slp/status/ Indexer Status.
* @apiName SLP indexer status.
* @apiGroup PSF SLP
* @apiDescription Return SLP indexer status
*
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:3000/v5/psf/slp/status
*
*
*/
async getStatus (req, res, next) {
try {
// Verify env var is set for interacting with the indexer.
_this.checkEnvVar()
const response = await _this.axios.get(
`${_this.psfSlpIndexerApi}slp/status/`
)
res.status(200)
return res.json(response.data)
} catch (err) {
return _this.errorHandler(err, res)
}
}
/**
* @api {post} /psf/slp/address/ SLP balance for address.
* @apiName SLP balance for address.
* @apiGroup PSF SLP
* @apiDescription Return SLP balance for address
*
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "address": "bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n" }' localhost:3000/v5/psf/slp/address
*
*
*/
async getAddress (req, res, next) {
try {
// Verify env var is set for interacting with the indexer.
_this.checkEnvVar()
// Validate the input data.
const address = req.body.address
if (!address || address === '') {
res.status(400)
return res.json({
success: false,
error: 'address can not be empty'
})
}
if (!address.includes('ecash')) {
// Ensure the input is a valid BCH address.
try {
_this.bchjs.SLP.Address.toCashAddress(address)
} catch (err) {
res.status(400)
return res.json({
success: false,
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({
success: false,
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
}
const response = await _this.axios.post(
`${_this.psfSlpIndexerApi}slp/address/`,
{ address }
)
res.status(200)
return res.json(response.data)
} catch (err) {
// console.log('err', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {post} /psf/slp/txid/ SLP transaction data.
* @apiName SLP transaction data.
* @apiGroup PSF SLP
* @apiDescription Return slp transaction data.
*
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "txid": "f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315" }' localhost:3000/v5/psf/slp/txid
*
*
*/
async getTxid (req, res, next) {
try {
// Verify env var is set for interacting with the indexer.
_this.checkEnvVar()
const txid = req.body.txid
if (!txid || txid === '') {
res.status(400)
return res.json({
success: false,
error: 'txid can not be empty'
})
}
if (txid.length !== 64) {
res.status(400)
return res.json({
success: false,
error: 'This is not a txid'
})
}
const response = await _this.axios.post(
`${_this.psfSlpIndexerApi}slp/tx/`,
{ txid }
)
res.status(200)
return res.json(response.data)
} catch (err) {
// console.log('err', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {post} /psf/slp/token List stats for a single slp token.
* @apiName List stats for a single slp token.
* @apiGroup PSF SLP
* @apiDescription Return list stats for a single slp token.
*
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "tokenId": "a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2" }' localhost:3000/v5/psf/slp/token
*
*
*/
async getTokenStats (req, res, next) {
try {
// Verify env var is set for interacting with the indexer.
_this.checkEnvVar()
const tokenId = req.body.tokenId
if (!tokenId || tokenId === '') {
res.status(400)
return res.json({
success: false,
error: 'tokenId can not be empty'
})
}
// Flag to toggle tx history of the token.
let withTxHistory = false
if (req.body.withTxHistory) withTxHistory = true
const response = await _this.axios.post(
`${_this.psfSlpIndexerApi}slp/token/`,
{ tokenId, withTxHistory }
)
res.status(200)
return res.json(response.data)
} catch (err) {
return _this.errorHandler(err, res)
}
}
/**
* @api {post} /psf/slp/token/data Get token data
* @apiName Get token data
* @apiGroup PSF SLP
* @apiDescription Get mutable and immutable data if the token contains them.
*
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "tokenId": "f055256b938f1ecfa270459d6f12c7c8c82b66d3263c03d5074445a2b1a498a3" }' localhost:3000/v5/psf/slp/token/data
*
*
*/
// Get mutable and immutable data for a token, if the token was created with
// such data.
//
// Example:
// curl -H "Content-Type: application/json" -X POST -d '{ "tokenId": "f055256b938f1ecfa270459d6f12c7c8c82b66d3263c03d5074445a2b1a498a3" }' localhost:3000/v5/psf/slp/token/data
async getTokenData (req, res, next) {
try {
// Verify env var is set for interacting with the indexer.
_this.checkEnvVar()
const tokenData = {}
const tokenId = req.body.tokenId
if (!tokenId || tokenId === '') {
res.status(400)
return res.json({
success: false,
error: 'tokenId can not be empty'
})
}
// get token stats from the Genesis TX of the token.
const withTxHistory = false
const response = await _this.axios.post(
`${_this.psfSlpIndexerApi}slp/token/`,
{ tokenId, withTxHistory }
)
// console.log('response', response.data)
const tokenStats = response.data.tokenData
tokenData.genesisData = tokenStats
// try to get immutable data
try {
// const immutableData = await _this.getCIDData(tokenStats.documentUri)
const immutableData = tokenStats.documentUri
tokenData.immutableData = immutableData
} catch (error) {
tokenData.immutableData = ''
}
// try to get mutable data
try {
const mutableData = await _this.getMutableData(tokenStats.documentHash)
tokenData.mutableData = mutableData
} catch (error) {
tokenData.mutableData = ''
}
res.status(200)
return res.json(tokenData)
} catch (err) {
console.log('Error in getTokenData(): ', err)
return _this.errorHandler(err, res)
}
}
// Retrieves the mutable data associated with a token document hash.
async getMutableData (documentHash) {
try {
if (!documentHash || typeof documentHash !== 'string') {
throw new Error(
'documentHash string required when calling mutableData().'
)
}
// Get the OP_RETURN data and decode it.
const mutableData = await _this.decodeOpReturn(documentHash)
const jsonData = JSON.parse(mutableData)
// console.log(`jsonData: ${JSON.stringify(jsonData, null, 2)}`)
const mutableDataAddr = jsonData.mda
// Gets the mutable data address (MDA) transaction history.
const transactions = await _this.bchjs.Electrumx.transactions(mutableDataAddr)
const mdaTxs = transactions.transactions
// console.log(`mdaTxs: ${JSON.stringify(mdaTxs, null, 2)}`)
let data = false
// Maps each transaction of the mutableDataAddr.
// If it finds an OP_RETURN, decode it. Exit the loop once the first
// valid mutable data entry is found.
// Start with the newest TXID entry and scan the history to find the first
// entry with an IPFS CID.
for (let i = mdaTxs.length - 1; i > -1; i--) {
const tx = mdaTxs[i]
const txid = tx.tx_hash
console.log(`Retrieving and decoding txid ${txid}`)
data = await _this.decodeOpReturn(txid)
// console.log('data: ', data)
// Try parse the OP_RETURN data to a JSON object.
if (data) {
try {
// console.log('Mutable Data : ', data)
// Convert the OP_RETURN data to a JSON object.
const obj = JSON.parse(data)
console.log(`obj: ${JSON.stringify(obj, null, 2)}`)
// Keep searching if this TX does not have a cid value.
if (!obj.cid) continue
// Ensure data was generated by the MDA
const txData = await this.bchjs.Transaction.get(txid)
const vinAddress = txData.txData.vin[0].address
// Skip entry if it was not made by the MDA private key.
if (mutableDataAddr !== vinAddress) {
console.log('Data is not generated by MDA, skipping.')
data = false
continue
}
break
} catch (error) {
continue
}
}
}
if (!data) {
console.log('Mutable Data Not found ')
}
// Get the CID:
const obj = JSON.parse(data)
const cid = obj.cid
if (!cid) {
throw new Error('CID could not be found in OP_RETURN data')
}
const result = cid
// const result = await _this.getCIDData(cid)
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
return result
} catch (err) {
// console.log('Error in getMutableData().')
console.log('Error in getMutableData(): ', err)
throw err
}
}
// Decodes the OP_RETURN of a transaction if this exists
async decodeOpReturn (txid) {
try {
if (!txid || typeof txid !== 'string') {
throw new Error('txid must be a string.')
}
// get transaction data
const txData = await _this.bchjs.Electrumx.txData(txid)
let data = false
// Maps the vout of the transaction in search of an OP_RETURN
for (let i = 0; i < txData.details.vout.length; i++) {
const vout = txData.details.vout[i]
const script = _this.bchjs.Script.toASM(
Buffer.from(vout.scriptPubKey.hex, 'hex')
).split(' ')
// Exit on the first OP_RETURN found.
if (script[0] === 'OP_RETURN') {
data = Buffer.from(script[1], 'hex').toString('ascii')
break
}
}
return data
} catch (error) {
console.log('Error in decodeOpReturn().')
throw error
}
}
// Get the immutable data stored in the documentUrl field of the token.
async getCIDData (cid) {
try {
if (!cid || typeof cid !== 'string') {
throw new Error('cid must be a string.')
}
// Assuming that CID starts with ipfs://. Cutting out that prefix.
cid = cid.substring(7)
const dataUrl = `https://${cid}.ipfs.dweb.link/data.json`
console.log(`dataUrl: ${dataUrl}`)
const response = await _this.axios.get(dataUrl)
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
return response.data
} catch (error) {
console.log('Error in getCIDData(): ', error)
throw error
}
}
// Check the the environment variable is set correctly.
checkEnvVar () {
_this.psfSlpIndexerApi = process.env.SLP_INDEXER_API
if (!_this.psfSlpIndexerApi) {
throw new Error(
'SLP_INDEXER_API env var not set. Can not connect to PSF SLP indexer.'
)
}
}
// DRY error handler.
errorHandler (err, res) {
// Attempt to decode the error message.
const { msg, status } = _this.routeUtils.decodeError(err)
// console.log('errorHandler msg: ', msg)
// console.log('errorHandler status: ', status)
if (msg) {
res.status(status)
return res.json({ success: false, error: msg })
}
// Handle error patterns specific to this route.
if (err.message) {
res.status(400)
return res.json({ success: false, error: err.message })
}
// If error can be handled, return the stack trace
res.status(500)
return res.json({ error: util.inspect(err) })
}
}
module.exports = PsfSlpIndexer
+540
View File
@@ -0,0 +1,540 @@
'use strict'
const express = require('express')
const router = express.Router()
const axios = require('axios')
const BigNumber = require('bignumber.js')
const RouteUtils = require('../../util/route-utils')
const routeUtils = new RouteUtils()
// const strftime = require('strftime')
const wlogger = require('../../util/winston-logging')
// Instantiate a local copy of bch-js using the local REST API server.
const LOCAL_RESTURL = process.env.LOCAL_RESTURL
? process.env.LOCAL_RESTURL
: 'https://api.fullstack.cash/v5/'
const BCHJS = require('@psf/bch-js')
// const BCHJS = require('../../../../bch-js')
const bchjs = new BCHJS({ restURL: LOCAL_RESTURL })
// Used to convert error messages to strings, to safely pass to users.
const util = require('util')
util.inspect.defaultOptions = { depth: 5 }
// Setup JSON RPC
// const BitboxHTTP = axios.create({
// baseURL: process.env.RPC_BASEURL
// })
// const username = process.env.RPC_USERNAME
// const password = process.env.RPC_PASSWORD
// Determine the Access password for a private instance of SLPDB.
// https://gist.github.com/christroutner/fc717ca704dec3dded8b52fae387eab2
// Password for General Purpose (GP) SLPDB.
const SLPDB_PASS_GP = process.env.SLPDB_PASS_GP
? process.env.SLPDB_PASS_GP
: 'BITBOX'
// Password for Whitelist (WL) SLPDB.
const SLPDB_PASS_WL = process.env.SLPDB_PASS_WL
? process.env.SLPDB_PASS_WL
: 'BITBOX'
// const rawtransactions = require('./full-node/rawtransactions')
const RawTransactions = require('./full-node/rawtransactions')
const rawTransactions = new RawTransactions()
// Setup REST and TREST URLs used by slpjs
// Dev note: this allows for unit tests to mock the URL.
if (!process.env.REST_URL) {
process.env.REST_URL = 'https://bchn.fullstack.cash/v5/'
}
if (!process.env.TREST_URL) {
process.env.TREST_URL = 'https://testnet.fullstack.cash/v5/'
}
let _this
class Slp {
constructor () {
_this = this
// Encapsulate external libraries.
_this.axios = axios
_this.routeUtils = routeUtils
_this.BigNumber = BigNumber
_this.bchjs = bchjs
_this.rawTransactions = rawTransactions
_this.router = router
_this.router.get('/', _this.root)
_this.router.get('/convert/:address', _this.convertAddressSingle)
_this.router.post('/convert', _this.convertAddressBulk)
_this.router.get('/validateTxid2/:txid', _this.validate2Single)
_this.router.get('/whitelist', _this.getSlpWhitelist)
_this.router.post('/generateSendOpReturn', _this.generateSendOpReturn)
}
// DRY error handler.
errorHandler (err, res) {
// console.error('Entering slp.js/errorHandler(). err: ', err)
// Attempt to decode the error message.
const { msg, status } = _this.routeUtils.decodeError(err)
console.log('slp.js/errorHandler msg from decodeError: ', msg)
console.log('slp.js/errorHandler status from decodeError: ', status)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
res.status(500)
return res.json({ error: util.inspect(err) })
}
formatTokenOutput (token) {
token.tokenDetails.id = token.tokenDetails.tokenIdHex
delete token.tokenDetails.tokenIdHex
token.tokenDetails.documentHash = token.tokenDetails.documentSha256Hex
delete token.tokenDetails.documentSha256Hex
token.tokenDetails.initialTokenQty = parseFloat(
token.tokenDetails.genesisOrMintQuantity
)
delete token.tokenDetails.genesisOrMintQuantity
delete token.tokenDetails.transactionType
delete token.tokenDetails.batonVout
delete token.tokenDetails.sendOutputs
if (token.tokenDetails.versionType === 65 && token.nftParentId) {
token.tokenDetails.nftParentId = token.nftParentId
}
token.tokenDetails.blockCreated = token.tokenStats.block_created
token.tokenDetails.blockLastActiveSend =
token.tokenStats.block_last_active_send
token.tokenDetails.blockLastActiveMint =
token.tokenStats.block_last_active_mint
token.tokenDetails.txnsSinceGenesis =
token.tokenStats.qty_valid_txns_since_genesis
token.tokenDetails.validAddresses =
token.tokenStats.qty_valid_token_addresses
token.tokenDetails.totalMinted = parseFloat(
token.tokenStats.qty_token_minted
)
token.tokenDetails.totalBurned = parseFloat(
token.tokenStats.qty_token_burned
)
token.tokenDetails.circulatingSupply = parseFloat(
token.tokenStats.qty_token_circulating_supply
)
token.tokenDetails.mintingBatonStatus =
token.tokenStats.minting_baton_status
delete token.tokenStats.block_last_active_send
delete token.tokenStats.block_last_active_mint
delete token.tokenStats.qty_valid_txns_since_genesis
delete token.tokenStats.qty_valid_token_addresses
return token
}
root (req, res, next) {
return res.json({ status: 'slp' })
}
/**
* @api {get} /slp/convert/{address} Convert address to slpAddr, cashAddr and legacy.
* @apiName Convert address to slpAddr, cashAddr and legacy.
* @apiGroup SLP
* @apiDescription Convert address to slpAddr, cashAddr and legacy.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v5/slp/convert/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m" -H "accept:application/json"
*
*
*/
async convertAddressSingle (req, res, next) {
try {
const address = req.params.address
// Validate input
if (!address || address === '') {
res.status(400)
return res.json({ error: 'address can not be empty' })
}
const slpAddr = _this.bchjs.SLP.Address.toSLPAddress(address)
const obj = {
slpAddress: '',
cashAddress: '',
legacyAddress: ''
}
obj.slpAddress = slpAddr
obj.cashAddress = _this.bchjs.SLP.Address.toCashAddress(slpAddr)
obj.legacyAddress = _this.bchjs.SLP.Address.toLegacyAddress(
obj.cashAddress
)
res.status(200)
return res.json(obj)
} catch (err) {
wlogger.error('Error in slp.ts/convertAddressSingle().', err)
return _this.errorHandler(err, res)
// return res.json({
// error: `Error in /address/convert/:address: ${err.message}`
// })
}
}
/**
* @api {post} /slp/convert/ Convert multiple addresses to cash, legacy and simpleledger format.
* @apiName Convert multiple addresses to cash, legacy and simpleledger format.
* @apiGroup SLP
* @apiDescription Convert multiple addresses to cash, legacy and simpleledger format.
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v5/slp/convert" -H "accept:application/json" -H "Content-Type: application/json" -d '{"addresses":["simpleledger:qrxa0unrn67rtn85v7asfddhhth43ecnxua0antk2l"]}'
*
*
*/
async convertAddressBulk (req, res, next) {
const addresses = req.body.addresses
// Reject if hashes is not an array.
if (!Array.isArray(addresses)) {
res.status(400)
return res.json({
error: 'addresses needs to be an array. Use GET for single address.'
})
}
// Enforce array size rate limits
if (!_this.routeUtils.validateArraySize(req, addresses)) {
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
error: 'Array too large.'
})
}
// Convert each address in the array.
const convertedAddresses = []
for (let i = 0; i < addresses.length; i++) {
const address = addresses[i]
// Validate input
if (!address || address === '') {
res.status(400)
return res.json({ error: 'address can not be empty' })
}
const slpAddr = _this.bchjs.SLP.Address.toSLPAddress(address)
const obj = {
slpAddress: '',
cashAddress: '',
legacyAddress: ''
}
obj.slpAddress = slpAddr
obj.cashAddress = _this.bchjs.SLP.Address.toCashAddress(slpAddr)
obj.legacyAddress = _this.bchjs.SLP.Address.toLegacyAddress(
obj.cashAddress
)
convertedAddresses.push(obj)
}
res.status(200)
return res.json(convertedAddresses)
}
/**
* @api {get} /slp/validateTxid2/{txid} Validate 2 Single
* @apiName Validate a single SLP transaction by txid using slp-validate.
* @apiGroup SLP
* @apiDescription Validate single SLP transaction by txid, using slp-validate.
* Slower, less efficient method of validating an SLP TXID using the slp-validate
* npm library. This method is independent of SLPDB and can be used as a fall-back
* when SLPDB returns 'null' values.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v5/slp/validateTxid2/f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a" -H "accept:application/json"
*
*
*/
async validate2Single (req, res, next) {
try {
const txid = req.params.txid
// Validate input
if (!txid || txid === '') {
res.status(400)
return res.json({ error: 'txid can not be empty' })
}
wlogger.debug(
'Executing slp/validate2Single/:txid with this txid: ',
txid
)
// null by default.
// Default return value.
const result = {
txid: txid,
isValid: null,
msg: ''
}
// Request options
const opt = {
method: 'get',
baseURL: `${process.env.SLP_API_URL}slp/validate/${txid}`,
timeout: 10000 // Exit after 10 seconds.
}
const tokenRes = await _this.axios.request(opt)
// console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data, null, 2)}`)
// console.log(`tokenRes: `, tokenRes)
// Overwrite the default value with the result from slp-api.
result.isValid = tokenRes.data.isValid
res.status(200)
return res.json(result)
} catch (err) {
// console.log('validate2Single error: ', err)
wlogger.error('Error in slp.ts/validate2Single().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /slp/whitelist SLP token whitelist.
* @apiName SLP token whitelist
* @apiGroup SLP
* @apiDescription Get tokens that are on the whitelist.
* SLPDB is typically used to validate SLP transactions. It can become unstable
* during periods of high network usage. A second SLPDB has been implemented
* that is much more stable, because it only tracks a whitelist of SLP tokens.
* This endpoint will return information on the SLP tokens that are included
* in that whitelist.
*
* For tokens on the whitelist, the /slp/validateTxid3 endpoints can be used.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v5/slp/whitelist" -H "accept:application/json"
*
*/
async getSlpWhitelist (req, res, next) {
try {
const list = [
{
name: 'USDH',
tokenId: 'c4b0d62156b3fa5c8f3436079b5394f7edc1bef5dc1cd2f9d0c4d46f82cca479'
},
{
name: 'SPICE',
tokenId: '4de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf'
},
{
name: 'PSF',
tokenId: '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
},
{
name: 'TROUT',
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
},
{
name: 'PSFTEST',
tokenId: 'd0ef4de95b78222bfee2326ab11382f4439aa0855936e2fe6ac129a8d778baa0'
}
]
res.status(200)
return res.json(list)
} catch (err) {
wlogger.error('Error in slp.ts/whitelist().', err)
return _this.errorHandler(err, res)
}
}
// Generates a Basic Authorization header for slpserve.
generateCredentialsGP () {
// Generate the Basic Authentication header for a private instance of SLPDB.
const username = 'BITBOX'
const password = SLPDB_PASS_GP
const combined = `${username}:${password}`
// console.log(`combined: ${combined}`)
const base64Credential = Buffer.from(combined).toString('base64')
const readyCredential = `Basic ${base64Credential}`
const options = {
headers: {
authorization: readyCredential
},
timeout: 45000
}
return options
}
// Generates a Basic Authorization header for slpserve.
generateCredentialsWL () {
// Generate the Basic Authentication header for a private instance of SLPDB.
const username = 'BITBOX'
const password = SLPDB_PASS_WL
const combined = `${username}:${password}`
// console.log(`combined: ${combined}`)
const base64Credential = Buffer.from(combined).toString('base64')
const readyCredential = `Basic ${base64Credential}`
const options = {
headers: {
authorization: readyCredential
},
timeout: 15000
}
return options
}
// Format the response from SLPDB into an object.
async formatToRestObject (slpDBFormat) {
// console.log(`slpDBFormat.data: ${JSON.stringify(slpDBFormat.data, null, 2)}`)
const transaction = slpDBFormat.data.u.length
? slpDBFormat.data.u[0]
: slpDBFormat.data.c[0]
// const inputs = transaction.in
// const outputs = transaction.out
const tokenOutputs = transaction.slp.detail.outputs
const sendOutputs = ['0']
tokenOutputs.map((x) => {
const string = parseFloat(x.amount) * 100000000
sendOutputs.push(string.toString())
return 1 // Making the linter happy
})
// Because you are not using Insight API indexer, you do not get the
// sending addresses from an indexer or from the node.
// However, they are available from the SLPDB output.
const tokenInputs = transaction.in
// Collect the input addresses
const sendInputs = []
for (let i = 0; i < tokenInputs.length; i += 1) {
const tokenInput = tokenInputs[i]
const sendInput = {}
sendInput.address = tokenInput.e.a
sendInputs.push(sendInput)
}
const obj = {
tokenInfo: {
versionType: transaction.slp.detail.versionType,
tokenName: transaction.slp.detail.name,
tokenTicker: transaction.slp.detail.symbol,
transactionType: transaction.slp.detail.transactionType,
tokenIdHex: transaction.slp.detail.tokenIdHex,
sendOutputs: sendOutputs,
sendInputsFull: sendInputs,
sendOutputsFull: transaction.slp.detail.outputs
},
tokenIsValid: transaction.slp.valid
}
return obj
}
/**
* @api {post} /slp/generateSendOpReturn/ generateSendOpReturn
* @apiName SLP generateSendOpReturn
* @apiGroup SLP
* @apiDescription Generate the hex required for a SLP Send OP_RETURN.
*
* This will return a hexidecimal representation of the OP_RETURN code that
* can be used to generate an SLP Send transaction. The number of outputs
* (1 or 2) will also be returned.
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v5/slp/generateSendOpReturn" -H "accept:application/json" -H "Content-Type: application/json" -d '{"tokenUtxos":[{"tokenId": "0a321bff9761f28e06a268b14711274bb77617410a16807bd0437ef234a072b1","decimals": 0, "tokenQty": 2}], "sendQty": 1.5}'
*
*
*/
// Get OP_RETURN script and outputs
async generateSendOpReturn (req, res, next) {
try {
const tokenUtxos = req.body.tokenUtxos
// console.log(`tokenUtxos: `, tokenUtxos)
const _sendQty = req.body.sendQty
const sendQty = Number(_sendQty)
// console.log(`sendQty: `, sendQty)
// Reject if tokenUtxos is not an array.
if (!Array.isArray(tokenUtxos)) {
res.status(400)
return res.json({
error: 'tokenUtxos needs to be an array.'
})
}
// Reject if tokenUtxos array is empty.
if (!tokenUtxos.length) {
res.status(400)
return res.json({
error: 'tokenUtxos array can not be empty.'
})
}
// Reject if sendQty is not an number.
if (!sendQty) {
res.status(400)
return res.json({
error: 'sendQty must be a number.'
})
}
// console.log('sendQty: ', sendQty)
// console.log(`tokenUtxos: `, tokenUtxos)
const opReturn = await _this.bchjs.SLP.TokenType1.generateSendOpReturn(
tokenUtxos,
sendQty
)
const script = opReturn.script.toString('hex')
// console.log(`script: ${script}`)
res.status(200)
return res.json({ script, outputs: opReturn.outputs })
} catch (err) {
console.log('err: ', err)
wlogger.error('Error in slp.js/generateSendOpReturn().', 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 /generateSendOpReturn()'
})
}
}
}
module.exports = Slp
+200
View File
@@ -0,0 +1,200 @@
'use strict'
const express = require('express')
const router = express.Router()
const axios = require('axios')
const RouteUtils = require('../../util/route-utils')
const routeUtils = new RouteUtils()
const wlogger = require('../../util/winston-logging')
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
const BCHJS = require('@psf/bch-js')
const bchjs = new BCHJS()
// let _this
class UtilRoute {
constructor (utilConfig) {
this.bchjs = bchjs
// this.blockbook = blockbook
if (!utilConfig) {
throw new Error(
'Must pass a config object when instantiating the Util library.'
)
}
if (!utilConfig.electrumx) {
throw new Error(
'Must pass an instance of Electrumx when instantiating the Util library.'
)
}
this.electrumx = utilConfig.electrumx
this.router = router
this.router.get('/', this.root)
this.router.get('/validateAddress/:address', this.validateAddressSingle)
this.router.post('/validateAddress', this.validateAddressBulk)
// this.router.post('/sweep', this.sweepWif)
// _this = this
}
root (req, res, next) {
return res.json({ status: 'util' })
}
/**
* @api {get} /util/validateAddress/{address} Get information about single bitcoin cash address.
* @apiName Information about single bitcoin cash address
* @apiGroup Util
* @apiDescription Returns information about single bitcoin cash address.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v5/util/validateAddress/bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c" -H "accept: application/json"
*
*
*/
async validateAddressSingle (req, res, next) {
try {
const address = req.params.address
if (!address || address === '') {
res.status(400)
return res.json({ error: 'address can not be empty' })
}
const {
BitboxHTTP,
// username,
// password,
requestConfig
} = routeUtils.setEnvVars()
requestConfig.data.id = 'validateaddress'
requestConfig.data.method = 'validateaddress'
requestConfig.data.params = [address]
const response = await BitboxHTTP(requestConfig)
return res.json(response.data.result)
} catch (err) {
// Attempt to decode the error message.
const { msg, status } = routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
wlogger.error('Error in util.ts/validateAddressSingle().', err)
res.status(500)
return res.json({ error: util.inspect(err) })
}
}
/**
* @api {post} /util/validateAddress Get information about bulk bitcoin cash addresses..
* @apiName Information about bulk bitcoin cash addresses.
* @apiGroup Util
* @apiDescription Returns information about bulk bitcoin cash addresses..
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v5/util/validateAddress" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c","bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0"]}'
* curl -X POST "https://api.fullstack.cash/v5/util/validateAddress" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c","bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0"],"from": 1, "to": 5}'
*
*
*/
async validateAddressBulk (req, res, next) {
try {
const addresses = req.body.addresses
// Reject if addresses is not an array.
if (!Array.isArray(addresses)) {
res.status(400)
return res.json({
error: 'addresses needs to be an array. Use GET for single address.'
})
}
// Enforce array size rate limits
if (!routeUtils.validateArraySize(req, addresses)) {
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
error: 'Array too large.'
})
}
// Validate each element in the array.
for (let i = 0; i < addresses.length; i++) {
const address = addresses[i]
// Ensure the input is a valid BCH address.
try {
bchjs.Address.toLegacyAddress(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 networkIsValid = routeUtils.validateNetwork(address)
if (!networkIsValid) {
res.status(400)
return res.json({
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
}
wlogger.debug('Executing util/validate with these addresses: ', addresses)
const {
BitboxHTTP,
// username,
// password,
requestConfig
} = routeUtils.setEnvVars()
// Loop through each address and creates an array of requests to call in parallel
const promises = addresses.map(async (address) => {
requestConfig.data.id = 'validateaddress'
requestConfig.data.method = 'validateaddress'
requestConfig.data.params = [address]
return BitboxHTTP(requestConfig)
})
// Wait for all parallel Insight requests to return.
const axiosResult = await axios.all(promises)
// Retrieve the data part of the result.
const result = axiosResult.map((x) => x.data.result)
res.status(200)
return res.json(result)
} catch (err) {
// Attempt to decode the error message.
const { msg, status } = routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
wlogger.error('Error in util.ts/validateAddressSingle().', err)
res.status(500)
return res.json({ error: util.inspect(err) })
}
}
}
module.exports = UtilRoute
@@ -5,8 +5,10 @@
'use strict' 'use strict'
const express = require('express') const express = require('express')
// const axios = require('axios')
const routeUtils = require('./route-utils') const RouteUtils = require('../../util/route-utils')
const routeUtils = new RouteUtils()
const wlogger = require('../../util/winston-logging') const wlogger = require('../../util/winston-logging')
// const router = express.Router() // const router = express.Router()
+52
View File
@@ -154,6 +154,35 @@ class RouteUtils {
} }
} }
// Handle 429 errors
if (err.error) {
console.log('decodeError: err: ', err)
// Error thrown by nginx (usually the SLPDB load balancer.)
if (err.error.includes('429 Too Many Requests')) {
const internalMsg =
'429 error thrown by nginx caught by route-utils.js/decodeError()'
console.error(internalMsg)
wlogger.error(internalMsg)
return {
msg: '429 Too Many Requests',
status: 429
}
} else if (err.error.includes('Too many requests')) {
// Error is being thrown by bch-api rate limit middleware.
return {
msg: '429 Too Many Requests',
status: 429
}
} else if (err.error.includes('Network error:')) {
return {
msg: err.error,
status: 503
}
}
}
// Handle general Error objects. // Handle general Error objects.
if (err.message) { if (err.message) {
return { return {
@@ -169,6 +198,29 @@ class RouteUtils {
return { msg: false, status: 500 } return { msg: false, status: 500 }
} }
} }
// Dynamically set these based on env vars. Allows unit testing.
setEnvVars () {
const BitboxHTTP = axios.create({
baseURL: process.env.RPC_BASEURL,
timeout: 15000
})
const username = process.env.RPC_USERNAME
const password = process.env.RPC_PASSWORD
const requestConfig = {
method: 'post',
auth: {
username: username,
password: password
},
data: {
jsonrpc: '1.0'
}
}
return { BitboxHTTP, username, password, requestConfig }
}
} }
module.exports = RouteUtils module.exports = RouteUtils
+19 -7
View File
@@ -6,18 +6,30 @@
'use strict' 'use strict'
var winston = require('winston') const winston = require('winston')
require('winston-daily-rotate-file') require('winston-daily-rotate-file')
var NETWORK = process.env.NETWORK const NETWORK = process.env.NETWORK
// Default 1 Megabyte
const LOG_MAX_SIZE = process.env.LOG_MAX_SIZE ? process.env.LOG_MAX_SIZE : '1m'
// Default 5 days.
// This was causing a problem with popularity. Creating over a gigabyte of files.
// var LOG_MAX_FILES = process.env.LOG_MAX_FILES ? process.env.LOG_MAX_FILES : '5d'
// 250 files @ 1Meg each = 250 megs
const LOG_MAX_FILES = process.env.LOG_MAX_FILES
? process.env.LOG_MAX_FILES
: '250'
// Configure daily-rotation transport. // Configure daily-rotation transport.
var transport = new winston.transports.DailyRotateFile({ const transport = new winston.transports.DailyRotateFile({
filename: `${__dirname}/../../logs/rest-${NETWORK}-%DATE%.log`, filename: `${__dirname.toString()}/../../logs/rest-${NETWORK}-%DATE%.log`,
datePattern: 'YYYY-MM-DD', datePattern: 'YYYY-MM-DD',
zippedArchive: false, zippedArchive: false,
maxSize: '1m', maxSize: LOG_MAX_SIZE,
maxFiles: '5d', maxFiles: LOG_MAX_FILES,
format: winston.format.combine( format: winston.format.combine(
winston.format.timestamp(), winston.format.timestamp(),
winston.format.json() winston.format.json()
@@ -29,7 +41,7 @@ transport.on('rotate', function (oldFilename, newFilename) {
}) })
// This controls what goes into the log FILES // This controls what goes into the log FILES
var wlogger = winston.createLogger({ const wlogger = winston.createLogger({
level: 'verbose', level: 'verbose',
format: winston.format.json(), format: winston.format.json(),
transports: [ transports: [
+52 -20
View File
@@ -1,37 +1,69 @@
#!/bin/bash #!/bin/bash
# Full node # Docker containers for the below infrastructure is described here:
export RPC_BASEURL=http://<full node ip>:8332/ # https://CashStack.info
export RPC_USERNAME=<RPC username> # Pre-synced databases can be downloaded on the CashStrap page:
export RPC_PASSWORD=<RPC password> # https://fullstack.cash/cashstrap
# Which network are you using?
export NETWORK=mainnet export NETWORK=mainnet
#export NETWORK=testnet
# SLPDB # Full node
export SLPDB_URL=http://<SLPDB IP>:12300/ export RPC_IP=<ip>:8332
export SLPDB_PASS=somelongpassword export RPC_BASEURL=http://$RPC_IP/
export RPC_USERNAME=bitcoin
export RPC_PASSWORD=password
# Use the same address as SLPDB_URL if you don't have a separate whitelist server. # (optional) If load-balancing multiple full nodes, it's best to pick a single
export SLPDB_WHITELIST_URL=http://<SLPDB IP>:12300/ # one for broadcasting transactions to the network. Prevents accidental
# double spends.
#export RPC_SENDURL=http://$RPC_IP/
# slp-api alternative SLP validator using slp-validate: # psf-slp-indxer
# https://github.com/Permissionless-Software-Foundation/slp-api export SLP_INDEXER_API=http://<ip>:<port>/
export SLP_API_URL=http://10.0.0.5:5001/
# Blockbook
export BLOCKBOOK_URL=https://<Blockbook IP>:9131/
# Allow node.js to make network calls to https using self-signed certificate.
export NODE_TLS_REJECT_UNAUTHORIZED=0
# Mainnet Fulcrum / ElectrumX # Mainnet Fulcrum / ElectrumX
export FULCRUM_URL=192.168.0.6 export FULCRUM_API=http://172.17.0.1:3001/v1/
export FULCRUM_PORT=50002
# Redis DB - Used for rate limiting - customize to your own Redis installation.
export REDIS_PORT=6379
export REDIS_HOST=172.17.0.1
# JWT Token Secret
# This is used to verify JWT tokens generated with jwt-bch-api:
# https://github.com/Permissionless-Software-Foundation/jwt-bch-api
export TOKENSECRET=somelongpassword export TOKENSECRET=somelongpassword
# So that bch-api can call bch-js locally. # So that bch-api can call bch-js locally.
export LOCAL_RESTURL=http://127.0.0.1:3000/v3/ export LOCAL_RESTURL=http://127.0.0.1:3000/v5/
# Basic Authentication password # Basic Authentication password
export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
# Whitelisted domains. Automatically give pro-tier rate limit access to apps
# that originate froma domain on the whitelist.
export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash
# Uncomment the line below if you do not want to use rate limits
#export export DO_NOT_USE_RATE_LIMITS=1
# Rate Limits. Numbers are divided into 1000. e.g. 10000 / 500 = 20 RPM for ANON.
# Requests use the ANON rate limit if they fail to pass in a JWT token.
# ANON = 20 requests per minute (RPM)
export ANON_RATE_LIMIT=500
# 10 = 1000 RPM
export WHITELIST_RATE_LIMIT=100
# Set logging parameters
#1m means no more than 1 megabyte
export LOG_MAX_SIZE=1m
#5d means store no more than 5 days
export LOG_MAX_FILES=5d
# (Optional) if using a bcash node, set this variable. Otherwise leave it as-is.
export BCASH_SERVER=http://localhost
# PSF SLP indexer
export SLP_INDEXER_API=https://psf-slp-indexer.fullstack.cash/
npm start npm start
File diff suppressed because it is too large Load Diff
-1122
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-45
View File
@@ -1,45 +0,0 @@
/*
Integration tests for the price library.
*/
'use strict'
const assert = require('chai').assert
// const axios = require('axios')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
const Price = require('../../../src/routes/v3/price')
const price = new Price()
const { mockReq, mockRes } = require('../mocks/express-mocks')
describe('#price', () => {
let req, res
beforeEach(() => {
// Mock the req and res objects used by Express routes.
req = mockReq
res = mockRes
})
describe('#getUSD', () => {
it('should get the USD price', async () => {
const result = await price.getUSD(req, res)
console.log(`result: ${util.inspect(result)}`)
assert.isNumber(result.usd)
})
})
describe('#getBCHAUSD', () => {
it('should get the USD price of BCHA', async () => {
const result = await price.getBCHAUSD(req, res)
console.log(`result: ${util.inspect(result)}`)
assert.isNumber(result.usd)
})
})
})
-164
View File
@@ -1,164 +0,0 @@
/*
These tests have been deprecated. To test bch-api rate limits, run the e2e
tests in the bch-js repository.
*/
'use strict'
const chai = require('chai')
const assert = chai.assert
const axios = require('axios')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
// const SERVER = `http://192.168.0.36:12400/v3/`
const SERVER = 'http://localhost:3000/v3/'
// const SERVER = 'https://api.fullstack.cash/v3/'
//
// const TEST_JWT =
// 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlODhhY2JmMDIyMWMxMDAxMmFkOTNmZiIsImVtYWlsIjoiY2hyaXMudHJvdXRuZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjQwLCJyYXRlTGltaXQiOjMsImlhdCI6MTYwMDYyODk1MSwiZXhwIjoxNjAzMjIwOTUxfQ.JPXDJQsxJFtCGZjHOd-hRfJuY41Ef_FQ4ET06CtYdNk'
describe('#JWT rate limits', () => {
it('should get control/getNetworkInfo() with no auth', async () => {
const options = {
method: 'GET',
url: `${SERVER}control/getNetworkInfo`
}
const result = await axios(options)
// console.log(`result.status: ${result.status}`)
// console.log(`result.data: ${util.inspect(result.data)}`)
assert.equal(result.status, 200)
assert.hasAnyKeys(result.data, ['version'])
})
it('should trigger rate-limit handler if rate limits exceeds 20 request per minute', async () => {
try {
const options = {
method: 'GET',
url: `${SERVER}control/getNetworkInfo`
}
const promises = []
for (let i = 0; i < 30; i++) {
const promise = axios(options)
promises.push(promise)
}
await Promise.all(promises)
assert.fail('Unexpected result!')
} catch (err) {
console.log('err: ', err)
assert.equal(err.response.status, 429)
assert.include(err.response.data.error, 'Too many requests')
}
})
// it('should not trigger rate-limit handler if correct pro-tier password is used', async () => {
// try {
// const username = 'BITBOX'
//
// // Pro-tier is accessed by using the right password.
// const password = 'BITBOX'
// // const password = "something"
//
// const combined = `${username}:${password}`
// const base64Credential = Buffer.from(combined).toString('base64')
// const readyCredential = `Basic ${base64Credential}`
//
// const options = {
// method: 'GET',
// url: `${SERVER}control/getNetworkInfo`,
// headers: { Authorization: readyCredential }
// }
//
// const promises = []
// for (let i = 0; i < 30; i++) {
// const promise = axios(options)
// promises.push(promise)
// }
//
// await Promise.all(promises)
//
// assert.equal(true, true, 'Not throwing an error is a pass!')
// } catch (err) {
// // console.log(`err.response: ${util.inspect(err.response)}`)
//
// assert.equal(
// true,
// false,
// 'This error handler should not have been triggered. Is the password correct?'
// )
// }
// })
// it('should trigger rate-limit handler if rate limits exceeds pro-tier limit', async () => {
// try {
// const username = 'BITBOX'
//
// // Pro-tier is accessed by using the right password.
// const password = 'BITBOX'
// // const password = "something"
//
// const combined = `${username}:${password}`
// const base64Credential = Buffer.from(combined).toString('base64')
// const readyCredential = `Basic ${base64Credential}`
//
// // Actual rate limit is 60 per minute X 4 nodes = 240 rpm.
// const options = {
// method: 'GET',
// url: `${SERVER}control/getNetworkInfo`,
// headers: { Authorization: readyCredential }
// }
//
// const promises = []
// for (let i = 0; i < 80; i++) {
// const promise = axios(options)
// promises.push(promise)
// }
//
// await Promise.all(promises)
//
// assert.equal(true, false, 'Unexpected result!')
// } catch (err) {
// // console.log(`err.response: ${util.inspect(err.response)}`)
//
// assert.equal(err.response.status, 429)
// assert.include(err.response.data.error, 'Too many requests')
// }
// })
// it('should unlock pro-tier for a valid JWT token', async () => {
// try {
// // Actual rate limit is 60 per minute X 4 nodes = 240 rpm.
// const options = {
// method: 'GET',
// url: `${SERVER}control/`,
// headers: {
// Authorization: `Token ${TEST_JWT}`
// }
// }
//
// const promises = []
// for (let i = 0; i < 60; i++) {
// const promise = axios(options)
// promises.push(promise)
// }
//
// await Promise.all(promises)
//
// // assert.equal(true, false, "Unexpected result!")
// assert.equal(true, true, 'Not throwing an error is a pass!')
// } catch (err) {
// console.log(`err.response: ${util.inspect(err.response)}`)
//
// assert.equal(true, false, 'Unexpected result!')
// }
// // Override default timeout for this test.
// }).timeout(20000)
})
-261
View File
@@ -1,261 +0,0 @@
/*
These integration tests need to be run against a live SLPDB. They query
against a live SLPDB and test the results against known token stats.
*/
'use strict'
const assert = require('chai').assert
// const axios = require('axios')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
// Exit if SLPDB URL is not defined.
if (!process.env.SLPDB_URL) {
throw new Error(
'SLPDB_URL and SLPDB_PASS must be defined in order to run these tests.'
)
}
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', () => {
let req, res
beforeEach(() => {
// Mock the req and res objects used by Express routes.
req = mockReq
res = mockRes
})
describe('#tokenStats', () => {
it('should get token stats for token with no mint baton', async () => {
req.params.tokenId =
'497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7'
// 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
const result = await slp.tokenStats(req, res)
// console.log(`result: ${util.inspect(result)}`)
// Assert that expected properties exist.
assert.property(result, 'decimals')
assert.property(result, 'timestamp')
assert.property(result, 'versionType')
assert.property(result, 'documentUri')
assert.property(result, 'symbol')
assert.property(result, 'name')
assert.property(result, 'containsBaton')
assert.property(result, 'id')
assert.property(result, 'documentHash')
assert.property(result, 'initialTokenQty')
assert.property(result, 'blockCreated')
assert.property(result, 'blockLastActiveSend')
assert.property(result, 'blockLastActiveMint')
assert.property(result, 'txnsSinceGenesis')
assert.property(result, 'validAddresses')
assert.property(result, 'mintingBatonStatus')
assert.property(result, 'timestampUnix')
assert.property(result, 'totalMinted')
assert.property(result, 'totalBurned')
assert.property(result, 'circulatingSupply')
// baton was never created.
assert.equal(result.containsBaton, false)
})
it('should get token stats for token with a mint baton', async () => {
req.params.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
const result = await slp.tokenStats(req, res)
// console.log(`result: ${util.inspect(result)}`)
// Assert that expected properties exist.
assert.property(result, 'decimals')
assert.property(result, 'timestamp')
assert.property(result, 'versionType')
assert.property(result, 'documentUri')
assert.property(result, 'symbol')
assert.property(result, 'name')
assert.property(result, 'containsBaton')
assert.property(result, 'id')
assert.property(result, 'documentHash')
assert.property(result, 'initialTokenQty')
assert.property(result, 'blockCreated')
assert.property(result, 'blockLastActiveSend')
assert.property(result, 'blockLastActiveMint')
assert.property(result, 'txnsSinceGenesis')
assert.property(result, 'validAddresses')
assert.property(result, 'mintingBatonStatus')
assert.property(result, 'timestampUnix')
assert.property(result, 'totalMinted')
assert.property(result, 'totalBurned')
assert.property(result, 'circulatingSupply')
// baton was created.
assert.equal(result.containsBaton, true)
})
})
describe('#generateSendOpReturn', () => {
it('should return OP_RETURN script', async () => {
req.body.tokenUtxos = [
{
tokenId:
'0a321bff9761f28e06a268b14711274bb77617410a16807bd0437ef234a072b1',
decimals: 0,
tokenQty: 2
}
]
req.body.sendQty = 1.5
const result = await slp.generateSendOpReturn(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['script', 'outputs'])
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)
})
})
describe('#validate2Single', () => {
it('should invalidate a known invalid TXID', async () => {
const txid =
'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a'
req.params.txid = txid
const result = await slp.validate2Single(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result.txid, txid)
assert.equal(result.isValid, false)
})
it('should validate a known valid TXID', async () => {
const txid =
'3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488'
req.params.txid = txid
const result = await slp.validate2Single(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result.txid, txid)
assert.equal(result.isValid, true)
})
// CT 10-11-2020: This test is valid, but because of the cacheing built
// into slp-validate, it will not consistently pass or fail. To manually
// Run this test, re-start slp-api or find a token txid with a long DAG.
// it('should cancel if validation takes too long', async () => {
// const txid =
// 'eacb1085dfa296fef6d4ae2c0f4529a1bef096dd2325bdcc6dcb5241b3bdb579'
//
// req.params.txid = txid
// const result = await slp.validate2Single(req, res)
// // console.log(`result: ${JSON.stringify(result, null, 2)}`)
//
// assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
// assert.include(
// result.error,
// 'Could not communicate with full node',
// 'Error message expected'
// )
// })
})
})
-31
View File
@@ -1,31 +0,0 @@
/*
These integration tests need to be run against a live SLPDB. They query
against a live SLPDB and test the results against known token stats.
*/
'use strict'
// const chai = require('chai')
// const assert = chai.assert
// const axios = require('axios')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
// Exit if SLPDB URL is not defined.
if (!process.env.SLPDB_URL) {
throw new Error('SLPDB_URL and SLPDB_PASS must be defined in order to run these tests.')
}
const SLPDB = require('../../../src/routes/v3/services/slpdb')
const slpdb = new SLPDB()
describe('#slpdb', () => {
describe('#getTotalCirculating', () => {
it('should get circulating supply', async () => {
const result = await slpdb.getTotalCirculating('b10677aef051b73e6b170c1c0824da33a3e0680ab5a01cd8d76aa77840fccfb4')
console.log('result: ', result)
})
})
})
-300
View File
@@ -1,300 +0,0 @@
/*
This library contains mocking data for running unit tests on the address route.
*/
'use strict'
const mockBlockHash =
'00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79'
const mockBlockchainInfo = {
chain: 'test',
blocks: 1267694,
headers: 1267694,
bestblockhash:
'000000000000013eaf80d1e157e32804c36a58ad0bb26ca59833880c80298780',
difficulty: 4105763.969035785,
mediantime: 1542131305,
verificationprogress: 0.9999968911571566,
chainwork: '00000000000000000000000000000000000000000000003f0443b0b5f02ce255',
pruned: false,
softforks: [
{ id: 'bip34', version: 2, reject: { status: true } },
{ id: 'bip66', version: 3, reject: { status: true } },
{ id: 'bip65', version: 4, reject: { status: true } }
],
bip9_softforks: {
csv: {
status: 'active',
startTime: 1456790400,
timeout: 1493596800,
since: 770112
}
}
}
const mockChainTips = [
{
height: 1267696,
hash: '000000000000035bfc43a642ebbff8cfc1e88b1d564de8b0a6c7e2797eeafb21',
branchlen: 0,
status: 'active'
},
{
height: 1267581,
hash: '000000000001bd12e4124207682563135b21353ca3087bc6b0c409f7f9e1da91',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1267375,
hash: '00000000702036979df70236bcc45dfc72d43d5d0e6834007afa1fa627e49587',
branchlen: 1036,
status: 'headers-only'
},
{
height: 1266979,
hash: '00000000e851abbde2174ccdc5c2508b909f81f23c4b7abeac864eee1a4891c7',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1266973,
hash: '000000007e1324d2900ed70947f89ab8fd4a267e87c9244c333215659a7370d5',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1266967,
hash: '000000000f37f12843d4800f50ec4de44dce0432e4d448242d43ff04a7a2948d',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1266963,
hash: '00000000ae11ec99e88898cbd72567cb4cb79e7fc13243357bef79632769deb4',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1266962,
hash: '000000004f97851259b4460a049e44fa5bd4beadee04a85bf81bd2e90c4f24f9',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1266950,
hash: '00000000d12a0e4a827f73ecaadb563df7e99817294939f2be8e943079dc60b2',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1266944,
hash: '00000000612d16af273217818e8e7ac5014a19f68ed04e1bc897b1be0a9c744f',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1266941,
hash: '0000000000002b91cdb15cacf3368da2c9ffce511d64092193b507dad75f4a27',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1266896,
hash: '000000000002745f67a66617b1f9f65c2dfb4c4b8a9ed9b1320b98d3ca46cbe5',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1266481,
hash: '0000000000036babee3ba7654cddb29a9fe5e85c9fbeb44bb53d5ffb694b9670',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1266452,
hash: '00000000502c3ab1490e0839780a4b441bbe21507c454545941e6adcde73c2ff',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1266336,
hash: '000000001c9a0b7cd9eb2210f69d5a9a3f4546ba3707deb40993d729190582d7',
branchlen: 10,
status: 'headers-only'
},
{
height: 1266097,
hash: '000000000028ac0fbd18afe6f001f70cb7549bd2b3082d754dde88ee236368a2',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1265522,
hash: '00000000000000c6df7a2063030e6ce51399e470cc46e4bf6d67aa3f0feca98d',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1265470,
hash: '0000000000168e6442a595a02340fccf8dc2b0e8912c632d39e1f870685f6d7c',
branchlen: 2,
status: 'valid-fork'
},
{
height: 1265275,
hash: '000000008bc2b37ae6af1b886b52e7e1c1122c12badc4ae6c13df64f789267cf',
branchlen: 11027,
status: 'headers-only'
},
{
height: 1265257,
hash: '00000000005bc3e9973d7273211eab02ae02549c7ebae48764c0bd648d4d7bbe',
branchlen: 114,
status: 'valid-fork'
},
{
height: 1265254,
hash: '0000000000e5b909d3541857315e9ee103b042d9ba661f25cc3e8fb25a96c8ce',
branchlen: 111,
status: 'valid-fork'
},
{
height: 1265245,
hash: '000000000066eb8832f6f990baceb379536f437216e22475a6c1b9133d250cb8',
branchlen: 102,
status: 'valid-fork'
},
{
height: 1264455,
hash: '00000000000001befc1bb17a23208d0b77b2449e37093ce8fbb5e346b19cafcc',
branchlen: 1,
status: 'valid-headers'
},
{
height: 1264373,
hash: '00000000000001ed3747e04fb95054cac0358fb4d8009d1999fc3ea34413e84a',
branchlen: 175,
status: 'valid-fork'
},
{
height: 1263912,
hash: '00000000000004635aecbcb97edcd6f5396483338838cb40da804eb354c68bde',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1262906,
hash: '00000000b31643d92a3e7c38e9755844fc8607e3f1055580a8ff28854e8a8ece',
branchlen: 1,
status: 'valid-fork'
},
{
height: 1255749,
hash: '000000001dde5b015a99137f4cba87871370f4b6fcfbcc8b126547e6c33177da',
branchlen: 129,
status: 'headers-only'
},
{
height: 1188789,
hash: '00000000af942ce4eb60b3213cbcb7c98a7330f1f8f1adb4b6376f5e822e15b2',
branchlen: 92,
status: 'headers-only'
}
]
const mockMempoolInfo = {
size: 87,
bytes: 16816,
usage: 66408,
maxmempool: 300000000,
mempoolminfee: 0
}
const mockRawMempool = [
'db045bc3bd1088fa91f5ebb05c35cb9e2a91a22377f79b465cc6920b9893123c',
'6b3df7febf2b9834f1409155f88b866dd516b36376eae00e2b455df82e290405'
]
const mockBlockHeaderConcise =
'000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c'
const mockBlockHeader = {
hash: '00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900',
confirmations: 7,
height: 1272859,
version: 2147418112,
versionHex: '7fff0000',
merkleroot:
'846f45604ec6cde2528933305dba80971e9ff0874f74d2811c7c690b2c32706d',
time: 1544207139,
mediantime: 1544202884,
nonce: 641041470,
bits: '1a09faea',
difficulty: 1681035.704111868,
chainwork: '00000000000000000000000000000000000000000000003fc4752e608403be04',
previousblockhash:
'0000000000003e467ebcb7e906645676c2f71b9ac520d6508bea45789b7c217d',
nextblockhash:
'00000000000006899041cdfd6c0b73a97730c362346dde479b77414ad7f25ace',
nTx: 'some value'
}
const mockTxOut = {
bestblock: '00000000003a7f19730dd9f172d7466658a5c8833dd03ed8f4ba36e3d857b5e7',
confirmations: 10,
value: 0.0001,
scriptPubKey: {
asm:
'OP_DUP OP_HASH160 0ee020c07f39526ac5505c54fa1ab98490979b83 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a9140ee020c07f39526ac5505c54fa1ab98490979b8388ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: ['bchtest:qq8wqgxq0uu4y6k92pw9f7s6hxzfp9umsvtg39pzqf']
},
coinbase: false
}
const mockTxOutProof =
'000000200798039affceb7a381e15dc62443c286efe7cae852393b656807000000000000cf9a7d6c654fd1c4558229a619a550ff749b373fa0f0027eed68b1abf6175d5c07c50f5cffff001d0452403b34000000070d0765da76980d542b5670d27ccda9e717073270f8921ad017deb7ee83fe6c4c22830a48cc8392197f1742a3d81805bd2aa4eb7469f38c74f674429d682cb87efcff1713fd0c388a50f0d11adf05363397ef79d16cdb786d47c941c2cb89f2588f42aced9c05f4203b4440616a9e4266ec47909b9580a643f402b5d4e82cc3cddd73df05a33abf6af3975d973d3033d10c4168a73d58d01f685a96d5a2519cd0de9c77faf3f8725ddf155cbdc8ab8102f173e2d0a0d74767f3bff22f588158d6d5bc71c079c7659cc864819c43877635007650502eb4060fcc9feec3280a41d602ad0a'
const mockAncestors = [
'070504de968fba8a23a1362fd7fc0546a420663f2bcc18099d894a41ecf9cce5',
'20af6b7fcd21a3f38bb65d32fe2d66fb1ae554ed9c00eaf1b17d7e81362381a9',
'227e08663a812cf44d91106b09fc8493fe1a2b7c146f30ca09ebd86c9b84b2a5',
'228833be79d4a833165ea199be160efcaeb9325e71e9455f424b27abc9688ba8',
'2de27df02001e7243435cef6d41ea2312b94a5964912eb21466b936852c0b822',
'2ef76ae690d6f6c9559e20358a75a0ce02250b9b8e80e0aa6f782576239f292a',
'3e314678f52d52694726e6da88b94c9f0ea086d2f9af297b01d014b62247839f',
'4773f840d5255f16c0e5effce48316d8b8824a1d3ddd5da275c5fd99e7b3ee42',
'4804b97112da07e2c187516a915dd4cbd4a3814f3c900b8d08eb1d508ab0ac47',
'4ca69f31081a256cb4711315336fe4358d8f04b67792ca5ee17b5f4089a2446a',
'6ac17dce10ae3a3d1d8e6f42546ad9a4bb981e62cba8d48bab03e01b9ea38ae3',
'6ce71182ef341c481a90d8b081a99d9788bdd91a917f9a6594fdfad2c2024a14',
'8413343ac973fb0968ae1809faa8f0ddf18252975481daa683e88b04afa66ccd',
'9430300f3d7ecdabe39671b6fd0e9c36f763f01a202e619c070554ddc5e8713e',
'94f82b5a2cd1647d8168807ccfdebe41931ea1ab789a050d615f08000a63b624',
'9848db6a7d2835002ca7e1097a685d75eb629f44df94007674635b45d0e97199',
'98d809993353df6c4d55fda5e7b0ed2e383601cef1180a9aa9c87e67e8f4dd94',
'9f5513a35ae8fad3dd061865a97d8127c914e3464527641c089993c06d092535',
'a7201ac90abd2aa42112faa809181c45047b6988a2f649a70a1d5bfb95f46884',
'a9f1284710c655357412a062b01efaeb174820613784ccbfe44b33b86a8bdbc4',
'b814f44c9c8ee24a218b9f295a8c1831106b70c15ffe8aeb7534e10d99935fd0',
'e130ccdcc94dc5eaa4ae37fa50e3ee96283b0e157616bc6e28454b64bae34f48',
'e68dad4a7292105cfa84fcaef7f99e5d4f2ece9613ca625d4d2ebf61efa84118',
'fe94caf5da672be3772d2304a6272eb8bc3d3f5cb4a886f39b7981e1485cf74b'
]
module.exports = {
mockBlockHash,
mockBlockchainInfo,
mockChainTips,
mockMempoolInfo,
mockRawMempool,
mockBlockHeaderConcise,
mockBlockHeader,
mockTxOut,
mockTxOutProof,
mockAncestors
}
-122
View File
@@ -1,122 +0,0 @@
/*
Mocking data for electrumx unit tests
*/
'use strict'
const utxos = [
{
height: 604392,
tx_hash: '7774e449c5a3065144cefbc4c0c21e6b69c987f095856778ef9f45ddd8ae1a41',
tx_pos: 0,
value: 1000
},
{
height: 630834,
tx_hash: '4fe60a51e0d8f5134bfd8e5f872d6e502d7f01b28a6afebb27f4438a4f638d53',
tx_pos: 0,
value: 6000
}
]
const balance = {
confirmed: 7000,
unconfirmed: 0
}
const txHistory = [
{
height: 601861,
tx_hash: '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d'
}
]
const mempool = [
{
tx_hash: '45381031132c57b2ff1cbe8d8d3920cf9ed25efd9a0beb764bdb2f24c7d1c7e3',
height: 0,
fee: 24310
}
]
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,
txDetails,
blockHeaders
}
-182
View File
@@ -1,182 +0,0 @@
/*
Mock data for the encryption test library
*/
const mockNoSendTx = {
txid: 'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023',
hash: 'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023',
version: 2,
size: 226,
locktime: 0,
vin: [
{
txid: '681cc1d392975a7e78cbd490b8a940d7eb029c554f2cee332e3a7119d77fb189',
vout: 1,
scriptSig: {
asm:
'30450221008a7c86ce3a9f5765573440143a00631bb5afa9a58b24674a7fa794bb11ffffc30220397dd4c672c3e0dbe0c4eb6390b5727c5fdc3a42cf3ae275ac13b44d27bb234f[ALL|FORKID] 02bc9fc26ae2bd9a9a14e9baeda4b4fdd95a00bcbf375732c647488b3ce82a2fd7',
hex:
'4830450221008a7c86ce3a9f5765573440143a00631bb5afa9a58b24674a7fa794bb11ffffc30220397dd4c672c3e0dbe0c4eb6390b5727c5fdc3a42cf3ae275ac13b44d27bb234f412102bc9fc26ae2bd9a9a14e9baeda4b4fdd95a00bcbf375732c647488b3ce82a2fd7'
},
sequence: 4294967295
}
],
vout: [
{
value: 0.000006,
n: 0,
scriptPubKey: {
asm:
'OP_DUP OP_HASH160 3c79ba54317c4be9cbfa5aaf357f74b97f3d9696 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a9143c79ba54317c4be9cbfa5aaf357f74b97f3d969688ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: ['bitcoincash:qq78nwj5x97yh6wtlfd27dtlwjuh70vkjc59h8tgtg']
}
},
{
value: 0.00003664,
n: 1,
scriptPubKey: {
asm:
'OP_DUP OP_HASH160 58502a73888e0a386fc69b44850af4b2a86ac9a6 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a91458502a73888e0a386fc69b44850af4b2a86ac9a688ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: ['bitcoincash:qpv9q2nn3z8q5wr0c6d5fpg27je2s6kf5cavp4ney0']
}
}
],
hex:
'020000000189b17fd719713a2e33ee2c4f559c02ebd740a9b890d4cb787e5a9792d3c11c68010000006b4830450221008a7c86ce3a9f5765573440143a00631bb5afa9a58b24674a7fa794bb11ffffc30220397dd4c672c3e0dbe0c4eb6390b5727c5fdc3a42cf3ae275ac13b44d27bb234f412102bc9fc26ae2bd9a9a14e9baeda4b4fdd95a00bcbf375732c647488b3ce82a2fd7ffffffff0258020000000000001976a9143c79ba54317c4be9cbfa5aaf357f74b97f3d969688ac500e0000000000001976a91458502a73888e0a386fc69b44850af4b2a86ac9a688ac00000000'
}
const mockFulcrumTxHistory = {
success: true,
transactions: [
{
height: 511463,
tx_hash:
'eff00a9538487ff44243c75fb13de19b5783454c42c81b9aff9afbfd09cbaec3'
},
{
height: 511464,
tx_hash:
'7e9aa7a74de2b30200a2d6fc748ff35a0c753221444194f720bb7f61ef1d9153'
},
{
height: 513373,
tx_hash:
'6960255abe64893073921e96bf3c053c82686e0fc22a565494fbe2a31e766975'
},
{
height: 513373,
tx_hash:
'9ea667bcfc9cd337bd6c5583d8094c1b1942bd2015d95b54189deac5070eeff0'
},
{
height: 560481,
tx_hash:
'ecc1b51bac767880382bf3190ff17abf78d0936843a022a943d871116ed50368'
},
{
height: 560615,
tx_hash:
'b3792d28377b975560e1b6f09e48aeff8438d4c6969ca578bd406393bd50bd7d'
},
{
height: 561568,
tx_hash:
'8bc2134c7e48e56e1769b3d7c4c1e3a0acc68e1e58160eee6fa67f3208c07262'
},
{
height: 561569,
tx_hash:
'ceb0cab0e37b59caf3ca29e1a698d19ff47f2827dd09cb2f3b91b9100b1dad1c'
},
{
height: 561572,
tx_hash:
'0f9b49cafeb9ae1d741cdb12137c92816aa8470944c270a78ba2e610bd59190d'
},
{
height: 561582,
tx_hash:
'e4a0ac48ff3f42fc342717a2a3d34248e5e85bae79d59bd20e1b60e61b1c500f'
},
{
height: 562106,
tx_hash:
'1afcc63b244182647909539ebe3f4a44b8ea4120a95edb8d9eebe5347b9491bb'
},
{
height: 562106,
tx_hash:
'c42f8f16d3baa2ee343ea89ef110dfe094992379d08edd30887b8ca7ee671c9a'
}
]
}
const mockTxDetails2 = {
txid: '7e9aa7a74de2b30200a2d6fc748ff35a0c753221444194f720bb7f61ef1d9153',
hash: '7e9aa7a74de2b30200a2d6fc748ff35a0c753221444194f720bb7f61ef1d9153',
version: 1,
size: 221,
locktime: 0,
vin: [
{
txid: 'eff00a9538487ff44243c75fb13de19b5783454c42c81b9aff9afbfd09cbaec3',
vout: 0,
scriptSig: {
asm:
'30440220150e7c9b646fe1f7f576456c9808c89a710e8fbb753e9f5ca6ed2d39a0b69ba602202212968163d8dfe35eec5975fce38440082029cb2691a3e0ad1714f9c412c47a[ALL|FORKID] 044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c',
hex:
'4730440220150e7c9b646fe1f7f576456c9808c89a710e8fbb753e9f5ca6ed2d39a0b69ba602202212968163d8dfe35eec5975fce38440082029cb2691a3e0ad1714f9c412c47a4141044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c'
},
sequence: 4294967295
}
],
vout: [
{
value: 0.01266023,
n: 0,
scriptPubKey: {
asm: 'OP_HASH160 91bacd6785e3f8c78d5f29b4020b94c753444130 OP_EQUAL',
hex: 'a91491bacd6785e3f8c78d5f29b4020b94c75344413087',
reqSigs: 1,
type: 'scripthash',
addresses: ['bitcoincash:pzgm4nt8sh3l33udtu5mgqstjnr4x3zpxqrrrndnw5']
}
}
],
hex:
'0100000001c3aecb09fdfb9aff9a1bc8424c4583579be13db15fc74342f47f4838950af0ef000000008a4730440220150e7c9b646fe1f7f576456c9808c89a710e8fbb753e9f5ca6ed2d39a0b69ba602202212968163d8dfe35eec5975fce38440082029cb2691a3e0ad1714f9c412c47a4141044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2cffffffff01675113000000000017a91491bacd6785e3f8c78d5f29b4020b94c7534441308700000000',
blockhash: '000000000000000001aab6818b4ad0379a3f7a13940d9b3d4838b3dea1d0a083',
confirmations: 149222,
time: 1515088493,
blocktime: 1515088493
}
const mockFulcrumNoTxHistory = {
success: true,
transactions: []
}
const mockFulcrumNoSendBalance = {
success: true,
transactions: [
{
height: 633578,
tx_hash:
'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023'
}
]
}
module.exports = {
mockNoSendTx,
mockFulcrumTxHistory,
mockTxDetails2,
mockFulcrumNoTxHistory,
mockFulcrumNoSendBalance
}
-545
View File
@@ -1,545 +0,0 @@
/*
This library contains mocking data for running unit tests.
*/
'use strict'
const mockList = {
t: [
{
tokenDetails: {
tokenIdHex:
'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb',
documentUri: '',
documentSha256: '',
symbol: 'NAKAMOTO',
name: 'NAKAMOTO',
decimals: 8
},
tokenStats: {
qty_valid_txns_since_genesis: 241,
qty_valid_token_utxos: 151,
qty_valid_token_addresses: 113,
qty_token_circulating_supply: '20995990',
qty_token_burned: '4010',
qty_token_minted: '21000000',
qty_satoshis_locked_up: 81900
}
}
]
}
const mockSingleToken = {
t: [
{
tokenDetails: {
decimals: 0,
tokenIdHex:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
timestamp: '2019-06-14 06:31:37',
timestamp_unix: 1560493897,
transactionType: 'GENESIS',
versionType: 1,
documentUri: 'ot@ot.com',
documentSha256Hex: null,
symbol: 'OT',
name: 'Test First SLP oasis Token',
batonVout: 2,
containsBaton: true,
genesisOrMintQuantity: '10000',
sendOutputs: null
},
tokenStats: {
block_created: 1308634,
block_last_active_send: 1308655,
block_last_active_mint: 1308636,
qty_valid_txns_since_genesis: 11,
qty_valid_token_utxos: 9,
qty_valid_token_addresses: 2,
qty_token_minted: '20000',
qty_token_burned: '0',
qty_token_circulating_supply: '20000',
qty_satoshis_locked_up: 4914,
minting_baton_status: 'ALIVE'
}
}
]
}
const mockSingleTokenError = {
t: []
}
const mockSingleAddress = {
g: [
{
_id: 'f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f',
balanceString: '0.002382',
slpAddress: 'simpleledger:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5me5fdrdn'
}
],
t: [
{
tokenDetails: {
decimals: 6,
tokenIdHex:
'f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f'
}
}
]
}
const mockTx = {
txid: '57b3082a2bf269b3d6f40fee7fb9c664e8256a88ca5ee2697c05b9457822d446',
version: 2,
locktime: 0,
vin: [
{
txid: '61e71554a3dc18158f30d9e8f5c9b6641a789690b32302899f81cbea9fe3bb49',
vout: 2,
sequence: 4294967295,
n: 0,
scriptSig: {
hex:
'4730440220409e79fec552f01203f41d3d621ae3db89c720af261c8268ce5f0453de009f5d022001e7ffefeba7b0716d32ea55cb6ace267b6ee9cbcc8a017bb9c3b6acf7889418412103c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585',
asm:
'30440220409e79fec552f01203f41d3d621ae3db89c720af261c8268ce5f0453de009f5d022001e7ffefeba7b0716d32ea55cb6ace267b6ee9cbcc8a017bb9c3b6acf7889418[ALL|FORKID] 03c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585'
},
value: 546,
legacyAddress: 'mw22g57T9YA7MZQQu5eBDKj3PKTdt99oDL',
cashAddress: 'bchtest:qz4qnxcxwvmacgye8wlakhz0835x0w3vtvaga95c09'
},
{
txid: '61e71554a3dc18158f30d9e8f5c9b6641a789690b32302899f81cbea9fe3bb49',
vout: 1,
sequence: 4294967295,
n: 1,
scriptSig: {
hex:
'483045022100a743bee56c99bd103be48a78fa4c7342100815d9d2448dbe6e1d338c3a13b241022066728b5279fc22eef5cd019582ff34771e29175835fc98aa3168a1548fd78ac8412103c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585',
asm:
'3045022100a743bee56c99bd103be48a78fa4c7342100815d9d2448dbe6e1d338c3a13b241022066728b5279fc22eef5cd019582ff34771e29175835fc98aa3168a1548fd78ac8[ALL|FORKID] 03c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585'
},
value: 546,
legacyAddress: 'mw22g57T9YA7MZQQu5eBDKj3PKTdt99oDL',
cashAddress: 'bchtest:qz4qnxcxwvmacgye8wlakhz0835x0w3vtvaga95c09'
},
{
txid: '61e71554a3dc18158f30d9e8f5c9b6641a789690b32302899f81cbea9fe3bb49',
vout: 3,
sequence: 4294967295,
n: 2,
scriptSig: {
hex:
'483045022100821473902eec5f1ce7d43b1ba7f9ec453bfe8b8dfc3de3e0723c883ab109922f02206162960e80618531fab2c16aee260fddd7979bab62471c8686af7de75f8732ec412103c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585',
asm:
'3045022100821473902eec5f1ce7d43b1ba7f9ec453bfe8b8dfc3de3e0723c883ab109922f02206162960e80618531fab2c16aee260fddd7979bab62471c8686af7de75f8732ec[ALL|FORKID] 03c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585'
},
value: 9997521,
legacyAddress: 'mw22g57T9YA7MZQQu5eBDKj3PKTdt99oDL',
cashAddress: 'bchtest:qz4qnxcxwvmacgye8wlakhz0835x0w3vtvaga95c09'
}
],
vout: [
{
value: '0.00000000',
n: 0,
scriptPubKey: {
hex:
'6a04534c500001010453454e44207ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796080000000049504f80080000001c71e64280',
asm:
'OP_RETURN 5262419 1 1145980243 7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796 0000000049504f80 0000001c71e64280'
},
spentTxId: null,
spentIndex: null,
spentHeight: null
},
{
value: '0.00000546',
n: 1,
scriptPubKey: {
hex: '76a914396b8e57ad0cb58d30e2992f22047b3c20377aa688ac',
asm:
'OP_DUP OP_HASH160 396b8e57ad0cb58d30e2992f22047b3c20377aa6 OP_EQUALVERIFY OP_CHECKSIG',
addresses: ['mkkZf7T3fU3vHSzNPy51HBmM46ghN1gnN9'],
type: 'pubkeyhash'
},
spentTxId: null,
spentIndex: null,
spentHeight: null
},
{
value: '0.00000546',
n: 2,
scriptPubKey: {
hex: '76a914aa099b067337dc20993bbfdb5c4f3c6867ba2c5b88ac',
asm:
'OP_DUP OP_HASH160 aa099b067337dc20993bbfdb5c4f3c6867ba2c5b OP_EQUALVERIFY OP_CHECKSIG',
addresses: ['mw22g57T9YA7MZQQu5eBDKj3PKTdt99oDL'],
type: 'pubkeyhash'
},
spentTxId: null,
spentIndex: null,
spentHeight: null
},
{
value: '0.09996891',
n: 3,
scriptPubKey: {
hex: '76a914aa099b067337dc20993bbfdb5c4f3c6867ba2c5b88ac',
asm:
'OP_DUP OP_HASH160 aa099b067337dc20993bbfdb5c4f3c6867ba2c5b OP_EQUALVERIFY OP_CHECKSIG',
addresses: ['mw22g57T9YA7MZQQu5eBDKj3PKTdt99oDL'],
type: 'pubkeyhash'
},
spentTxId: null,
spentIndex: null,
spentHeight: null
}
],
blockhash: '000000000000ce978accc64a6bb567acf0c653c202309a0f8e220149bf0c6968',
blockheight: 1287490,
confirmations: 746,
time: 1550855104,
blocktime: 1550855104,
valueOut: 0.09997983,
size: 628,
valueIn: 0.09998613,
fees: 0.0000063,
tokenInfo: {
versionType: 1,
transactionType: 'SEND',
tokenIdHex:
'7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796',
sendOutputs: ['0', '1230000000', '122170000000']
},
tokenIsValid: true
}
const mockConvert = {
slpAddress: 'slptest:qz35h5mfa8w2pqma2jq06lp7dnv5fxkp2shlcycvd5',
cashAddress: 'bchtest:qz35h5mfa8w2pqma2jq06lp7dnv5fxkp2svtllzmlf',
legacyAddress: 'mvQPGnzRT6gMWASZBMg7NcT3vmvsSKSQtf'
}
const mockTokenDetails = {
tokenIdHex:
'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb',
documentUri: '',
symbol: 'NAKAMOTO',
name: 'NAKAMOTO',
decimals: 8,
timestamp: '',
containsBaton: true,
versionType: 1
}
const mockTokenStats = {
qty_valid_txns_since_genesis: 241,
qty_valid_token_utxos: 151,
qty_valid_token_addresses: 113,
qty_token_circulating_supply: '20995990',
qty_token_burned: '4010',
qty_token_minted: '21000000',
qty_satoshis_locked_up: 81900
}
const mockBalance = {
_id: 'simpleledger:qp9d8mn8ypryfvea2mev0ggc3wg6plpn4suuaeuss3',
token_balance: '1000'
}
const mockTransactions = [
{
txid: 'a302f045be8efa1cd982833a7f187ff4fac8baac36da0c887eb2787d8b45e2af',
tokenDetails: {
valid: true,
detail: {
decimals: null,
tokenIdHex:
'495322b37d6b2eae81f045eda612b95870a0c2b6069c58f70cf8ef4e6a9fd43a',
timestamp: null,
transactionType: 'MINT',
versionType: 1,
documentUri: null,
documentSha256Hex: null,
symbol: null,
name: null,
batonVout: 2,
containsBaton: true,
genesisOrMintQuantity: {
$numberDecimal: '1000'
},
sendOutputs: null
},
invalidReason: null,
schema_version: 30
}
}
]
const mockFoobar = {
c: [],
u: []
}
const mockSingleValidTxid = {
c: [
{
_id: '5d965fc27f1cf2184ca2fe73',
tx: {
h: '77872738b6bddee6c0cbdb9509603de20b15d4f6b26602f629417aec2f5d5e8d'
},
slp: {
valid: true,
invalidReason: null
}
}
],
u: []
}
const mockTwoValidTxid = {
c: [
{
_id: '5d965fc27f1cf2184ca2fe73',
tx: {
h: '77872738b6bddee6c0cbdb9509603de20b15d4f6b26602f629417aec2f5d5e8d'
},
slp: {
valid: true,
invalidReason: null
}
},
{
_id: '5d71e69758380a002c492a90',
tx: {
h: '552112f9e458dc7d1d8b328b0a6685e8af74a64b60b6846e7c86407f27f47e42'
},
slp: {
valid: true,
invalidReason: null
}
}
],
u: []
}
const mockTwoRedundentTxid = {
c: [
{
_id: '5db99c72a391ae2afd604bde',
tx: {
h: 'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56'
},
slp: {
valid: true,
invalidReason: null
}
}
],
u: []
}
const mockTxHistory = [
{
tx: {
h: '3a7646b3976a8745928c7192c8dde989bfa275fa6d7bce950180ab8002cf6cef'
},
in: [
{
i: 0,
e: {
h: '7bdd586ebd1e5f3dfd5295ab6e896c48b25855ab77a72c035ce1e7aacc965c2d',
i: 1,
s:
'RzBEAiAsu5o9EnxgZChjxKygxhGLhlfxJJbQfd4PP/vp2od3fAIgPpMqg+Sp4YpiInQZ6weJwELq8LEIrHXJpueBz7yF5gRBIQLsyL3809mRphc/+tPAcTQO5bi0kqZRFKpKIHXE3qN8Rw==',
a: 'simpleledger:qrrrpqmdkggpnw0czwg0jgcjd7yhu25jy5zxh2gqdq'
}
},
{
i: 1,
e: {
h: 'df49feff24dc34a10a44a0cbd7c908d964801ec7218512c84574fbce698535f0',
i: 3,
s:
'SDBFAiEAhW3zbKTlPrOXD2E2oEcNof6vCMPGYIg8vOVSE9c8IakCIFLg/gxydG9eL8HMAzkScGElKWJRnfhVMpMHU3evNcrZQSEDRS7F+pSC8OxSldsT4FctJLZBU7f2+FDiE05ae1xqtN0=',
a: 'simpleledger:qpkpeqfslejw5pptzcy25h2jxhsc9k0vts43n26up0'
}
}
],
out: [
{
e: {
v: 0,
i: 0,
s:
'agRTTFAAAQEEU0VORCA46XxdfTWFosvz+VgMgsozmF+csIRdTcziIMtwn5U4sAgAAAAAAJiWgAgAAAAAJHfU+g=='
}
},
{
e: {
v: 546,
i: 1,
s: 'dqkUqo4lVohqOK6rDd7nIIkFQ6Uf41aIrA==',
a: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza'
}
},
{
e: {
v: 546,
i: 2,
s: 'dqkUgYSfxf90wVkERCiOMyh16iB4d92IrA==',
a: 'simpleledger:qzqcf879la6vzkgygs5guvegwh4zq7rhm5nu0pljjl'
}
},
{
e: {
v: 21849,
i: 3,
s: 'dqkUxjCDbbIQGbn4E5D5IxJviX4qkiWIrA==',
a: 'simpleledger:qrrrpqmdkggpnw0czwg0jgcjd7yhu25jy5zxh2gqdq'
}
}
],
slp: {
detail: {
decimals: 8,
tokenIdHex:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
transactionType: 'SEND',
versionType: 1,
documentUri: 'psfoundation.cash',
documentSha256Hex: null,
symbol: 'PSF',
name: 'Permissionless Software Foundation',
txnBatonVout: null,
txnContainsBaton: false,
outputs: [
{
address: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza',
amount: '0.1'
},
{
address: 'simpleledger:qzqcf879la6vzkgygs5guvegwh4zq7rhm5nu0pljjl',
amount: '6.11833082'
}
]
}
},
blk: {
h: '000000000000000001a47bf337ca211c1e7218234d0d70117c64d6f213f5dacb',
i: 634833,
t: 1589300214
}
},
{
tx: {
h: '3b11b48cab1e7c8384facf482b3f6bfe659a58245ab4aa4147b42b5cb2a5fac5'
},
in: [
{
i: 0,
e: {
h: 'e4e1e1f6b502cbd42f69b919634cea3e37fc8595e6ca800e0a7d8f6dcdfb249e',
i: 3,
s:
'SDBFAiEAjCGRmU28x24LMTwg5XqC+fLf3zGhTYCsSWpmF8Eshq0CIAJhxAKMbwKWC4ASmjWzplpno2ch+hGNUD6HNWOdbd4SQSECeRsZo5Fl29g0A9bfJo1E/WIdowWBsLblyxWnEB7ViFE=',
a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7'
}
},
{
i: 1,
e: {
h: '438420345dd8b7fb4ea74aaf2e3090f44899bf3c71662243946a441de6dae720',
i: 2,
s:
'SDBFAiEAhmLYL4QY3mvZo3/i0XG9PJAYMruw3MaGOLJ3Z4si7hwCIAaaGwRnj/cZK3L/KKFBoMtItpGRZ10GowNN//vgn6PKQSECeRsZo5Fl29g0A9bfJo1E/WIdowWBsLblyxWnEB7ViFE=',
a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7'
}
}
],
out: [
{
e: {
v: 0,
i: 0,
s:
'agRTTFAAAQEEU0VORCCk+1wtoaoGTiUBikP5FlBABx2emEuhkMIip/WQU6+EsggAAAAAAA9CQAgAAAAXQnHEwA=='
}
},
{
e: {
v: 546,
i: 1,
s: 'dqkUqo4lVohqOK6rDd7nIIkFQ6Uf41aIrA==',
a: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza'
}
},
{
e: {
v: 546,
i: 2,
s: 'dqkUWQQVny9pv6Y+76cSYzoNltwufoiIrA==',
a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7'
}
},
{
e: {
v: 43074,
i: 3,
s: 'dqkUWQQVny9pv6Y+76cSYzoNltwufoiIrA==',
a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7'
}
}
],
slp: {
detail: {
decimals: 2,
tokenIdHex:
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
transactionType: 'SEND',
versionType: 1,
documentUri: 'troutsblog.com',
documentSha256Hex: null,
symbol: 'TROUT',
name: "Trout's test token",
txnBatonVout: null,
txnContainsBaton: false,
outputs: [
{
address: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza',
amount: '10000'
},
{
address: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7',
amount: '998990000'
}
]
}
},
blk: {
h: '000000000000000002a4ee2ed6fe8a764ffef0b2556f545ca0b3ef39ecd2b7c4',
i: 634832,
t: 1589297145
}
}
]
module.exports = {
mockList,
mockSingleToken,
mockConvert,
mockTokenDetails,
mockTokenStats,
mockTx,
mockBalance,
mockTransactions,
mockSingleTokenError,
mockSingleAddress,
mockFoobar,
mockSingleValidTxid,
mockTwoValidTxid,
mockTwoRedundentTxid,
mockTxHistory
}
-280
View File
@@ -1,280 +0,0 @@
/*
TESTS FOR THE PRICE.JS LIBRARY
This test file uses the environment variable TEST to switch between unit
and integration tests. By default, TEST is set to 'unit'. Set this variable
to 'integration' to run the tests against BCH mainnet.
*/
'use strict'
const chai = require('chai')
const assert = chai.assert
const sinon = require('sinon')
const Price = require('../../src/routes/v3/price')
let uut
// Mocking data.
const { mockReq, mockRes } = require('./mocks/express-mocks')
const mockData = require('./mocks/price-mock')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
describe('#PriceRouter', () => {
let req, res
let sandbox
before(() => {
// Set default environment variables for unit tests.
if (!process.env.TEST) process.env.TEST = 'unit'
})
// Setup the mocks before each test.
beforeEach(() => {
// Mock the req and res objects used by Express routes.
req = mockReq
res = mockRes
// Explicitly reset the parmas and body.
req.params = {}
req.body = {}
req.query = {}
sandbox = sinon.createSandbox()
uut = new Price()
})
afterEach(() => {
// Restore Sandbox
sandbox.restore()
})
after(() => {})
describe('#root', async () => {
// root route handler.
// const root = controlRoute.testableComponents.root
it('should respond to GET for base route', async () => {
const result = uut.root(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(result.status, 'price', 'Returns static string')
})
})
describe('#getUSD', () => {
// const getNetworkInfo = controlRoute.testableComponents.getNetworkInfo
it('should throw 500 when network issues', async () => {
uut.priceUrl = 'http://fakeurl/api/'
await uut.getUSD(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.isAbove(
res.statusCode,
499,
'HTTP status code 500 or greater expected.'
)
// console.log(res)
assert.include(
res.output.error,
'Network error: Could not communicate with full node or other external service'
)
})
it('returns proper error when downstream service stalls', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
const result = await uut.getUSD(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
assert.include(
result.error,
'Could not communicate with full node',
'Error message expected'
)
})
it('returns proper error when downstream service is down', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
const result = await uut.getUSD(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
assert.include(
result.error,
'Could not communicate with full node',
'Error message expected'
)
})
it('should get the USD price of BCH', async () => {
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox
.stub(uut.axios, 'request')
.resolves({ data: mockData.mockCoinbaseFeed })
}
const result = await uut.getUSD(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.isNumber(result.usd)
})
})
describe('#getBCHRate', () => {
it('should throw 500 when network issues', async () => {
uut.priceUrl = 'http://fakeurl/api/'
await uut.getBCHRate(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.isAbove(
res.statusCode,
499,
'HTTP status code 500 or greater expected.'
)
// console.log(res)
assert.include(
res.output.error,
'Network error: Could not communicate with full node or other external service'
)
})
it('returns proper error when downstream service stalls', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
const result = await uut.getBCHRate(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
assert.include(
result.error,
'Could not communicate with full node',
'Error message expected'
)
})
it('returns proper error when downstream service is down', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
const result = await uut.getBCHRate(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
assert.include(
result.error,
'Could not communicate with full node',
'Error message expected'
)
})
it('should get several rates for BCH', async () => {
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox
.stub(uut.axios, 'request')
.resolves({ data: mockData.mockCoinbaseFeed })
}
const result = await uut.getBCHRate(req, res)
// console.log(`result: ${util.inspect(result)}`)
// assert.isNumber(result)
assert.property(result, 'USD')
assert.property(result, 'CAD')
})
})
describe('#errorHandler', () => {
it('should handle unexpected errors', () => {
sandbox.stub(uut.routeUtils, 'decodeError').returns({ msg: false })
const result = uut.errorHandler(new Error('test error'), res)
// console.log('result: ', result)
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
assert.property(result, 'error')
})
})
describe('#getBCHAUSD', () => {
// const getNetworkInfo = controlRoute.testableComponents.getNetworkInfo
it('should throw 500 when network issues', async () => {
uut.coinexPriceUrl = 'http://fakeurl/api/'
await uut.getBCHAUSD(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.isAbove(
res.statusCode,
499,
'HTTP status code 500 or greater expected.'
)
// console.log(res)
assert.include(
res.output.error,
'Network error: Could not communicate with full node or other external service'
)
})
it('returns proper error when downstream service stalls', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
const result = await uut.getBCHAUSD(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
assert.include(
result.error,
'Could not communicate with full node',
'Error message expected'
)
})
it('returns proper error when downstream service is down', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
const result = await uut.getBCHAUSD(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
assert.include(
result.error,
'Could not communicate with full node',
'Error message expected'
)
})
it('should get the USD price of BCH', async () => {
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox
.stub(uut.axios, 'request')
.resolves({ data: mockData.mockCoinexFeed })
}
const result = await uut.getBCHAUSD(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.isNumber(result.usd)
})
})
})
-460
View File
@@ -1,460 +0,0 @@
'use strict'
const chai = require('chai')
const assert = chai.assert
// const nock = require('nock') // HTTP mocking
const sinon = require('sinon')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
// Mocking data.
const { mockReq, mockRes, mockNext } = require('./mocks/express-mocks')
// Libraries under test
const RateLimits = require('../../src/middleware/route-ratelimit')
let rateLimits = new RateLimits()
// const controlRoute = require('../../src/routes/v3/full-node/control')
const jwtAuth = require('../../src/middleware/jwt-auth')
let req, res, next
// let originalEnvVars // Used during transition from integration to unit tests.
// JWT token used in tests.
const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYWRlM2Y1NzM5ZTZjMGZmMDM0YjlhMSIsImlhdCI6MTU3MTY3NzQ1MCwiZXhwIjoxNTc0MjY5NDUwfQ.SSz7F7ETyBB3eoNG2VKCzPOhddtB-vrtmEoj7PxicrQ'
describe('#route-ratelimits & jwt-auth', () => {
let sandbox
before(async () => {
// Save existing environment variables.
// originalEnvVars = {
// BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL,
// RPC_BASEURL: process.env.RPC_BASEURL,
// RPC_USERNAME: process.env.RPC_USERNAME,
// RPC_PASSWORD: process.env.RPC_PASSWORD
// }
if (!process.env.JWT_AUTH_SERVER) { process.env.JWT_AUTH_SERVER = 'http://fakeurl.com/' }
// Wipe the Redis DB, which prevents false negatives when running integration
// tests back-to-back.
await rateLimits.wipeRedis()
})
// Setup the mocks before each test.
beforeEach(() => {
// Mock the req and res objects used by Express routes.
req = Object.assign({}, mockReq)
res = Object.assign({}, mockRes)
next = mockNext
// Explicitly reset the parmas and body.
req.params = {}
req.body = {}
req.query = {}
req.locals = {}
sandbox = sinon.createSandbox()
})
afterEach(() => {
sandbox.restore()
})
after(() => {
rateLimits.closeRedis()
})
describe('#jwt-auth.js', () => {
describe('#getTokenFromHeaders', () => {
it('should populate the req.locals object correctly', () => {
// Initialize req.locals
req.locals = {
proLimit: false,
apiLevel: 0
}
const header = `Token ${jwt}`
req.headers.authorization = header
jwtAuth.getTokenFromHeaders(req, res, next)
// console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`)
assert.property(req.locals, 'proLimit')
assert.property(req.locals, 'apiLevel')
assert.property(req.locals, 'jwtToken')
assert.equal(req.locals.jwtToken, jwt)
})
})
})
describe('#getResource', () => {
it('should decode a blockchain request', () => {
const url =
'/blockchain/getTxOut/62a3ea958a463a372bc0caf2c374a7f60be9c624be63a0db8db78f05809df6d8/0?include_mempool=true'
const result = rateLimits.getResource(url)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result, 'blockchain')
})
})
describe('#calcPoints', () => {
it('should return 50 points for anonymous user', () => {
const result = rateLimits.calcPoints()
// console.log(`result: ${result}`)
assert.equal(result, 50)
})
it('should return 50 points for free tier requesting full node access', () => {
const jwtInfo = {
apiLevel: 10,
resource: 'blockchain',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 50)
})
it('should return 50 points for free tier requesting indexer access', () => {
const jwtInfo = {
apiLevel: 10,
resource: 'blockbook',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 50)
})
it('should return 50 points for free tier requesting SLPDB access', () => {
const jwtInfo = {
apiLevel: 10,
resource: 'slp',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 50)
})
it('should return 10 points for full node tier requesting full node access', () => {
const jwtInfo = {
apiLevel: 20,
resource: 'blockchain',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it('should return 50 points for full-node tier requesting indexer access', () => {
const jwtInfo = {
apiLevel: 20,
resource: 'blockbook',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 50)
})
it('should return 50 points for full node tier requesting SLPDB access', () => {
const jwtInfo = {
apiLevel: 20,
resource: 'slp',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 50)
})
it('should return 10 point for indexer tier requesting full node access', () => {
const jwtInfo = {
apiLevel: 30,
resource: 'blockchain',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it('should return 10 points for indexer tier requesting indexer access', () => {
const jwtInfo = {
apiLevel: 30,
resource: 'blockbook',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it('should return 50 points for indexer tier requesting SLPDB access', () => {
const jwtInfo = {
apiLevel: 30,
resource: 'slp',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 50)
})
it('should return 10 point for SLP tier requesting full node access', () => {
const jwtInfo = {
apiLevel: 40,
resource: 'blockchain',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it('should return 10 points for SLP tier requesting indexer access', () => {
const jwtInfo = {
apiLevel: 40,
resource: 'blockbook',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it('should return 10 points for SLP tier requesting SLPDB access', () => {
const jwtInfo = {
apiLevel: 40,
resource: 'slp',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
})
describe('#rateLimitByResource', () => {
// NOTE: this test will fail if you run multiple integration tests in a
// short period. Because it talks to the Redis DB.
it('should pass through rate-limit middleware', async () => {
req.baseUrl = '/v3'
req.path = '/control/getNetworkInfo'
req.url = req.path
req.method = 'GET'
// Call the route twice to trigger the rate handling.
await rateLimits.rateLimitByResource(req, res, next)
await rateLimits.rateLimitByResource(req, res, next)
// next() will be called if rate-limit is not triggered
assert.equal(next.called, true)
})
it('should trigger rate-limit handler if rate limits exceeds 5 request per minute', async () => {
req.baseUrl = '/v3'
req.path = '/control/getNetworkInfo'
req.url = req.path
req.method = 'GET'
for (let i = 0; i < 5; i++) {
next.reset() // reset the stubbed next() function.
await rateLimits.rateLimitByResource(req, res, next)
// console.log(`next() called: ${next.called}`)
}
// Note: next() will be called unless the rate-limit kicks in.
assert.equal(
next.called,
false,
'next should not be called if rate limit was triggered.'
)
})
it('should NOT trigger rate-limit for free-tier at 5 RPM', async () => {
// Create a new instance of the rate limit so we start with zeroed tracking.
rateLimits = new RateLimits()
req.baseUrl = '/v3'
req.path = '/control/getNetworkInfo'
req.url = req.path
req.method = 'GET'
req.locals.jwtToken = 'some-token'
const jwtInfo = {
apiLevel: 10,
id: '5e3a0415eb29a962da2708b1'
}
// Mock the call to the jwt library.
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
for (let i = 0; i < 5; i++) {
next.reset() // reset the stubbed next() function.
await rateLimits.rateLimitByResource(req, res, next)
// console.log(`next() called: ${next.called}`)
}
// console.log(`req.locals after test: ${util.inspect(req.locals)}`)
// Note: next() will be called unless the rate-limit kicks in.
assert.equal(
next.called,
true,
'next should be called if rate limit was not triggered.'
)
})
it('should trigger rate-limit for free tier after 20 RPM', async () => {
// Create a new instance of the rate limit so we start with zeroed tracking.
rateLimits = new RateLimits()
req.baseUrl = '/v3'
req.path = '/control/getNetworkInfo'
req.url = req.path
req.method = 'GET'
req.locals.jwtToken = 'some-token'
const jwtInfo = {
apiLevel: 10,
id: '5e3a0415eb29a962da2708b2'
}
// Mock the call to the jwt library.
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
for (let i = 0; i < 22; i++) {
next.reset() // reset the stubbed next() function.
await rateLimits.rateLimitByResource(req, res, next)
// console.log(`next() called: ${next.called}`)
}
// Note: next() will be called unless the rate-limit kicks in.
assert.equal(
next.called,
false,
'next should not be called if rate limit was triggered.'
)
})
it('should NOT trigger rate-limit handler for indexer-tier at 25 RPM', async () => {
// Create a new instance of the rate limit so we start with zeroed tracking.
rateLimits = new RateLimits()
req.baseUrl = '/v3'
req.path = '/control/getNetworkInfo'
req.url = req.path
req.method = 'GET'
req.locals.jwtToken = 'some-token'
const jwtInfo = {
apiLevel: 20,
id: '5e3a0415eb29a962da2708b3'
}
// Mock the call to the jwt library.
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
for (let i = 0; i < 25; i++) {
next.reset() // reset the stubbed next() function.
await rateLimits.rateLimitByResource(req, res, next)
// console.log(`next() called: ${next.called}`)
}
// console.log(`req.locals after test: ${util.inspect(req.locals)}`)
// Note: next() will be called unless the rate-limit kicks in.
assert.equal(
next.called,
true,
'next should be called if rate limit was not triggered.'
)
})
it('should still rate-limit at a higher RPM for pro-tier', async () => {
// Create a new instance of the rate limit so we start with zeroed tracking.
rateLimits = new RateLimits()
req.baseUrl = '/v3'
req.path = '/control/getNetworkInfo'
req.url = req.path
req.method = 'GET'
req.locals.jwtToken = 'some-token'
const jwtInfo = {
apiLevel: 20,
id: '5e3a0415eb29a962da2708b5'
}
// Mock the call to the jwt library.
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
for (let i = 0; i < 150; i++) {
next.reset() // reset the stubbed next() function.
await rateLimits.rateLimitByResource(req, res, next)
// console.log(`next() called: ${next.called}`)
}
// console.log(`req.locals after test: ${util.inspect(req.locals)}`)
// Note: next() will be called unless the rate-limit kicks in.
assert.equal(
next.called,
false,
'next should NOT be called if rate limit was triggered.'
)
})
it('should handle misconfigured token secret', async () => {
// Create a new instance of the rate limit so we start with zeroed tracking.
rateLimits = new RateLimits()
req.baseUrl = '/v3'
req.path = '/control/getNetworkInfo'
req.url = req.path
req.method = 'GET'
req.locals.jwtToken = 'some-token'
next.reset() // reset the stubbed next() function.
await rateLimits.rateLimitByResource(req, res, next)
// Issues with token secret should treat incoming requests as anonymous
// calls with 30 points or 3 RPM.
assert.equal(res.locals.pointsToConsume, 50)
})
})
})
// Generates a Basic authorization header.
// function generateAuthHeader (pass) {
// // https://en.wikipedia.org/wiki/Basic_access_authentication
// const username = 'BITBOX'
// const combined = `${username}:${pass}`
//
// var base64Credential = Buffer.from(combined).toString('base64')
// var readyCredential = `Basic ${base64Credential}`
//
// return readyCredential
// }
-1655
View File
File diff suppressed because it is too large Load Diff
-143
View File
@@ -1,143 +0,0 @@
/*
TESTS FOR THE XPUB.JS LIBRARY
This test file uses the environment variable TEST to switch between unit
and integration tests. By default, TEST is set to 'unit'. Set this variable
to 'integration' to run the tests against BCH mainnet.
*/
'use strict'
const chai = require('chai')
const assert = chai.assert
const xpubRoute = require('../../src/routes/v3/xpub')
const nock = require('nock') // HTTP mocking
// let originalUrl // Used during transition from integration to unit tests.
// Mocking data.
const { mockReq, mockRes } = require('./mocks/express-mocks')
// const mockData = require('./mocks/address-mock')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
describe('#XPUBRouter', () => {
let req, res
before(() => {
// Set default environment variables for unit tests.
if (!process.env.TEST) process.env.TEST = 'unit'
// if (process.env.TEST === "unit")
// process.env.BITCOINCOM_BASEURL = "http://fakeurl/api
})
// Setup the mocks before each test.
beforeEach(() => {
// Mock the req and res objects used by Express routes.
req = mockReq
res = mockRes
// Explicitly reset the parmas and body.
req.params = {}
req.body = {}
req.query = {}
// Activate nock if it's inactive.
if (!nock.isActive()) nock.activate()
})
afterEach(() => {
// Clean up HTTP mocks.
nock.cleanAll() // clear interceptor list.
nock.restore()
})
after(() => {
// process.env.BITCOINCOM_BASEURL = originalUrl
})
describe('#root', () => {
// root route handler.
const root = xpubRoute.testableComponents.root
it('should respond to GET for base route', async () => {
const result = root(req, res)
assert.equal(result.status, 'address', 'Returns static string')
})
})
describe('#FromXPubSingle', () => {
// details route handler.
const fromXPubSingle = xpubRoute.testableComponents.fromXPubSingle
it('should throw 400 if xpub is empty', async () => {
const result = await fromXPubSingle(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'xpub can not be empty')
})
it('should error on an array', async () => {
req.params.xpub = [
'tpubDHTK2jqg73w3GwoiHfAMbMYML1HN8FhrUxD9rFgbSgHXdwwrY6pAFqKDfUHhqw7vreaZty5hPGjb1S7ZPQeMmu6TFHAKfY9tJpYbvaGjPRM'
]
const result = await fromXPubSingle(req, res)
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
assert.include(
result.error,
'xpub can not be an array',
'Proper error message'
)
})
/*
it("should throw 500 when network issues", async () => {
const savedUrl = process.env.BITCOINCOM_BASEURL
try {
req.params.xpub = `tpubDHTK2jqg73w3GwoiHfAMbMYML1HN8FhrUxD9rFgbSgHXdwwrY6pAFqKDfUHhqw7vreaZty5hPGjb1S7ZPQeMmu6TFHAKfY9tJpYbvaGjPRM`
// Switch the Insight URL to something that will error out.
process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/"
const result = await fromXPubSingle(req, res)
// Restore the saved URL.
process.env.BITCOINCOM_BASEURL = savedUrl
assert.equal(res.statusCode, 500, "HTTP status code 500 expected.")
assert.include(result.error, "ENOTFOUND", "Error message expected")
} catch (err) {
// Restore the saved URL.
process.env.BITCOINCOM_BASEURL = savedUrl
}
})
*/
it('should create an address from xpub', async () => {
req.params.xpub = 'tpubDHTK2jqg73w3GwoiHfAMbMYML1HN8FhrUxD9rFgbSgHXdwwrY6pAFqKDfUHhqw7vreaZty5hPGjb1S7ZPQeMmu6TFHAKfY9tJpYbvaGjPRM'
// Mock the Insight URL for unit tests.
// TODO add unit test
// if (process.env.TEST === "unit") {
// nock(`${process.env.BITCOINCOM_BASEURL}`)
// .get(
// `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=0`
// )
// .reply(200, mockData.mockTransactions)
// }
// Call the details API.
const result = await fromXPubSingle(req, res)
// console.log(`result: ${util.inspect(result)}`)
// Assert that required fields exist in the returned object.
assert.exists(result.legacyAddress)
assert.exists(result.cashAddress)
})
})
})
File diff suppressed because it is too large Load Diff
-1122
View File
File diff suppressed because it is too large Load Diff
-164
View File
@@ -1,164 +0,0 @@
/*
TESTS FOR THE CONTROL.JS LIBRARY
This test file uses the environment variable TEST to switch between unit
and integration tests. By default, TEST is set to 'unit'. Set this variable
to 'integration' to run the tests against BCH mainnet.
*/
'use strict'
const chai = require('chai')
const assert = chai.assert
const ControlRoute = require('../../src/routes/v4/full-node/control')
const uut = new ControlRoute()
const sinon = require('sinon')
let originalEnvVars // Used during transition from integration to unit tests.
// Mocking data.
const { mockReq, mockRes } = require('./mocks/express-mocks')
const mockData = require('./mocks/control-mock')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
describe('#ControlRouter', () => {
let req, res
let sandbox
before(() => {
// Save existing environment variables.
originalEnvVars = {
BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL,
RPC_BASEURL: process.env.RPC_BASEURL,
RPC_USERNAME: process.env.RPC_USERNAME,
RPC_PASSWORD: process.env.RPC_PASSWORD
}
// Set default environment variables for unit tests.
})
// Setup the mocks before each test.
beforeEach(() => {
// Mock the req and res objects used by Express routes.
req = mockReq
res = mockRes
// Explicitly reset the parmas and body.
req.params = {}
req.body = {}
req.query = {}
sandbox = sinon.createSandbox()
})
afterEach(() => {
// Restore Sandbox
sandbox.restore()
})
after(() => {
// Restore any pre-existing environment variables.
process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL
process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL
process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME
process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD
})
describe('#root', async () => {
// root route handler.
// const root = controlRoute.testableComponents.root
it('should respond to GET for base route', async () => {
const result = uut.root(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(result.status, 'control', 'Returns static string')
})
})
describe('#GetNetworkInfo', () => {
// const getNetworkInfo = controlRoute.testableComponents.getNetworkInfo
it('should throw 500 when network issues', async () => {
// Save the existing RPC URL.
const savedUrl = process.env.RPC_BASEURL
// Manipulate the URL to cause a 500 network error.
process.env.RPC_BASEURL = 'http://fakeurl/api/'
await uut.getNetworkInfo(req, res)
// console.log(`result: ${util.inspect(result)}`)
// Restore the saved URL.
process.env.RPC_BASEURL = savedUrl
assert.isAbove(
res.statusCode,
499,
'HTTP status code 500 or greater expected.'
)
// assert.include(result.error, "ENOTFOUND", "Error message expected")
})
it('returns proper error when downstream service stalls', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
const result = await uut.getNetworkInfo(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
assert.include(
result.error,
'Could not communicate with full node',
'Error message expected'
)
})
it('returns proper error when downstream service is down', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
const result = await uut.getNetworkInfo(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
assert.include(
result.error,
'Could not communicate with full node',
'Error message expected'
)
})
it('should get info on the full node', async () => {
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox
.stub(uut.axios, 'request')
.resolves({ data: { result: mockData.mockGetNetworkInfo } })
}
const result = await uut.getNetworkInfo(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAnyKeys(result, [
'version',
'subversion',
'protocolversion',
'localservices',
'localrelay',
'timeoffset',
'networkactive',
'connections',
'networks',
'relayfee',
'excessutxocharge',
'localaddresses',
'warnings'
])
})
})
})
-60
View File
@@ -1,60 +0,0 @@
/*
End-to-end tests for the Address endpoint.
These tests assume that the repo is running locally and pointed at TESTNET
*/
'use strict'
const rp = require('request-promise')
// const rawtransactions = require('../../../dist/routes/v2/address')
/*
This unconfirmed utxo test needs to be expanded in the future to automatically
create a transaction. For now, that is done manually and the address provided
as a constant.
1. Start rest running locally, pointed at TESTNET.
2. Fund each address with a small amount of tBCH
3. Run this program with `node address.js`
4. If successful, it will return the unconfirmed UTXOs.
*/
const addr1 = 'bchtest:qpdcp5pv7qphu5tsjfgwezld9n9aq9ue6swgqpf45c'
const addr2 = 'bchtest:qzpvx999fagau0xqmvu3xvll9evapge7u5hcgeknzv'
async function testSingleUnconfirmed () {
try {
const options = {
method: 'GET',
uri: `http://localhost:3000/v2/address/unconfirmed/${addr1}`,
resolveWithFullResponse: true,
json: true
}
await rp(options)
// console.log(`result.body: ${JSON.stringify(result.body, null, 2)}`)
} catch (err) {
// console.log(`Error: `, err)
}
}
testSingleUnconfirmed()
async function testDoubleUnconfirmed () {
try {
const options = {
method: 'POST',
uri: 'http://localhost:3000/v2/address/unconfirmed',
resolveWithFullResponse: true,
json: true,
body: {
addresses: [addr1, addr2]
}
}
await rp(options)
// console.log(`result.body: ${JSON.stringify(result.body, null, 2)}`)
} catch (err) {
// console.log(`Error: `, err)
}
}
testDoubleUnconfirmed()
-146
View File
@@ -1,146 +0,0 @@
/*
TESTS FOR THE ENCRYPTION.JS LIBRARY
This test file uses the environment variable TEST to switch between unit
and integration tests. By default, TEST is set to 'unit'. Set this variable
to 'integration' to run the tests against BCH mainnet.
To-Do:
*/
'use strict'
const chai = require('chai')
const assert = chai.assert
const sinon = require('sinon')
// Set default environment variables for unit tests.
if (!process.env.TEST) process.env.TEST = 'unit'
// Only load blockbook library after setting BLOCKBOOK_URL env var.
const EncryptionRoute = require('../../src/routes/v4/encryption')
const encryptionRoute = new EncryptionRoute()
// Mocking data.
const { mockReq, mockRes } = require('./mocks/express-mocks')
const mockData = require('./mocks/encryption-mocks')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
describe('#Encryption Router', () => {
let req, res
let sandbox
before(() => {
// console.log(`Testing type is: ${process.env.TEST}`)
if (!process.env.NETWORK) process.env.NETWORK = 'testnet'
})
// Setup the mocks before each test.
beforeEach(() => {
// Mock the req and res objects used by Express routes.
req = mockReq
res = mockRes
// Explicitly reset the parmas and body.
req.params = {}
req.body = {}
req.query = {}
sandbox = sinon.createSandbox()
})
afterEach(() => {
sandbox.restore()
})
after(() => {})
describe('#root', () => {
// root route handler.
const root = encryptionRoute.root
it('should respond to GET for base route', async () => {
const result = root(req, res)
assert.equal(result.status, 'encryption', 'Returns static string')
})
})
describe('#getPublicKey', () => {
it('should get public key from blockchain', async () => {
req.params.address =
'bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0'
// Mock the Insight URL for unit tests.
if (process.env.TEST === 'unit') {
sandbox
.stub(encryptionRoute.bchjs.Electrumx, 'transactions')
.resolves(mockData.mockFulcrumTxHistory)
sandbox
.stub(encryptionRoute.bchjs.RawTransactions, 'getRawTransaction')
.resolves(mockData.mockTxDetails2)
}
const result = await encryptionRoute.getPublicKey(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'publicKey')
assert.equal(
result.publicKey,
'044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c'
)
})
it('should return false for address with no tx history', async () => {
req.params.address =
'bitcoincash:qqwfpk04ecf69wuprj9yjys9rla5mk7rj5j8uthqel'
// Mock the Insight URL for unit tests.
if (process.env.TEST === 'unit') {
sandbox
.stub(encryptionRoute.bchjs.Electrumx, 'transactions')
.resolves(mockData.mockFulcrumNoTxHistory)
}
const result = await encryptionRoute.getPublicKey(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, false)
assert.property(result, 'error')
assert.include(result.error, 'No transaction history')
})
it('should return false for address with no send history', async () => {
req.params.address =
'bitcoincash:qq78nwj5x97yh6wtlfd27dtlwjuh70vkjc59h8tgtg'
// Mock the Insight URL for unit tests.
if (process.env.TEST === 'unit') {
sandbox
.stub(encryptionRoute.bchjs.Electrumx, 'transactions')
.resolves(mockData.mockFulcrumNoSendBalance)
sandbox
.stub(encryptionRoute.bchjs.RawTransactions, 'getRawTransaction')
.resolves(mockData.mockNoSendTx)
}
const result = await encryptionRoute.getPublicKey(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, false)
assert.property(result, 'publicKey')
assert.include(result.publicKey, 'not found')
})
})
})
-29
View File
@@ -1,29 +0,0 @@
'use strict'
/**
* Read more about panda here: https://panda-suite.github.io/
*/
const panda = require('pandacash-core')
const runLocalNode = done => {
const server = panda.server({
// always the same mnemonic
// mnemonic: "cigar magnet ocean purchase travel damp snack alone theme budget wagon wrong",
seedAccounts: true,
enableLogs: false,
debug: false
})
server.listen({
port: 48332,
walletPort: 48333
}, (err, pandaCashCore) => {
if (err) return console.error(err)
done()
})
}
module.exports = {
runLocalNode
}
-7
View File
@@ -1,7 +0,0 @@
This directory contains integration tests. To run them requires that
a local copy of rest.bitcoin.com is running. The tests in this directory are
exactly the same calls that end users would use for their app to interact
directly with rest.bitcoin.com.
At present, this directory focuses on raw-transations. But more integration tests
will be added.
-191
View File
@@ -1,191 +0,0 @@
/*
These integration tests are intended to be run against a live local copy of
rest.bitcoin.com. They exercise the endpoints in the same way the SDK or
end-user application would. These tests were created to replace the parts
removed from the swagger UI, that otherwise would have excersiced these endpoints.
TODO:
-/rawtransactions/sendRawTransaction is more appropropriate for an e2e test,
so it is omitted here.
*/
'use strict'
const chai = require('chai')
const assert = chai.assert
// const rawtransactions = require('../../../dist/routes/v2/rawtransactions')
const rp = require('request-promise')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
// const mockData = require('../mocks/raw-transactions-mocks')
describe('#Raw-Transactions', () => {
describe('#root', () => {
it('should return root', async () => {
const options = {
method: 'GET',
uri: 'http://localhost:3000/v2/rawtransactions/',
resolveWithFullResponse: true,
json: true
}
const result = await rp(options)
// console.log(`result: ${JSON.stringify(result, null, 0)}`)
assert.equal(result.body.status, 'rawtransactions')
})
})
describe('#whCreateTx', () => {
it('should return tx hex', async () => {
const minIn = {
txid:
'f7ed9cf23dee85910f6269c9a101a75fcfd2f3c6fc81f17fad824ff7aaf99ab2',
vout: 1
}
const options = {
method: 'PUT',
uri: 'http://localhost:3000/v2/rawtransactions/create',
resolveWithFullResponse: true,
json: true,
body: {
// inputs: [mockData.mockWHCreateInput],
inputs: [minIn],
outputs: {}
}
}
const result = await rp(options)
// console.log(`result.body: ${JSON.stringify(result.body, null, 0)}`)
assert.equal(
result.body,
'0200000001b29af9aaf74f82ad7ff181fcc6f3d2cf5fa701a1c969620f9185ee3df29cedf70100000000ffffffff0000000000'
)
})
})
describe('#whOpReturn', () => {
it('should return tx hex', async () => {
const options = {
method: 'PUT',
uri: 'http://localhost:3000/v2/rawtransactions/opreturn',
resolveWithFullResponse: true,
json: true,
body: {
rawtx: '01000000000000000000',
payload: '00000000000000020000000006dac2c0'
}
}
const result = await rp(options)
// console.log(`result.body: ${JSON.stringify(result.body, null, 0)}`)
assert.equal(
result.body,
'0100000000010000000000000000166a140877686300000000000000020000000006dac2c000000000'
)
})
})
describe('#whReference', () => {
it('should return tx hex', async () => {
const options = {
method: 'PUT',
uri: 'http://localhost:3000/v2/rawtransactions/reference',
resolveWithFullResponse: true,
json: true,
body: {
rawtx:
'0100000001a7a9402ecd77f3c9f745793c9ec805bfa2e14b89877581c734c774864247e6f50400000000ffffffff03aa0a0000000000001976a9146d18edfe073d53f84dd491dae1379f8fb0dfe5d488ac5c0d0000000000004751210252ce4bdd3ce38b4ebbc5a6e1343608230da508ff12d23d85b58c964204c4cef3210294cc195fc096f87d0f813a337ae7e5f961b1c8a18f1f8604a909b3a5121f065b52aeaa0a0000000000001976a914946cb2e08075bcbaf157e47bcb67eb2b2339d24288ac00000000',
destination: 'bitcoincash:qrn60nerx5zug4u4hal06atep3lzhtecvy4pxk75lf',
amount: 0.005
}
}
const result = await rp(options)
// console.log(`result.body: ${JSON.stringify(result.body, null, 0)}`)
assert.isString(result.body)
})
})
describe('#whChangeOutput', () => {
it('should return tx hex', async () => {
const options = {
method: 'PUT',
uri: 'http://localhost:3000/v2/rawtransactions/change',
resolveWithFullResponse: true,
json: true,
body: {
rawtx:
'0100000001b15ee60431ef57ec682790dec5a3c0d83a0c360633ea8308fbf6d5fc10a779670400000000ffffffff025c0d00000000000047512102f3e471222bb57a7d416c82bf81c627bfcd2bdc47f36e763ae69935bba4601ece21021580b888ff56feb27f17f08802ebed26258c23697d6a462d43fc13b565fda2dd52aeaa0a0000000000001976a914946cb2e08075bcbaf157e47bcb67eb2b2339d24288ac00000000',
destination: 'bitcoincash:qrn60nerx5zug4u4hal06atep3lzhtecvy4pxk75lf',
prevtxs: [
{
txid:
'6779a710fcd5f6fb0883ea3306360c3ad8c0a3c5de902768ec57ef3104e65eb1',
vout: 4,
scriptPubKey:
'76a9147b25205fd98d462880a3e5b0541235831ae959e588ac',
value: 0.00068257
}
],
fee: 0.000035
}
}
const result = await rp(options)
// console.log(`result.body: ${JSON.stringify(result.body, null, 0)}`)
assert.isString(result.body)
})
})
describe('#whInput', () => {
it('should return tx hex', async () => {
const options = {
method: 'PUT',
uri: 'http://localhost:3000/v2/rawtransactions/input',
resolveWithFullResponse: true,
json: true,
body: {
txid:
'b006729017df05eda586df9ad3f8ccfee5be340aadf88155b784d1fc0e8342ee',
n: 0
}
}
const result = await rp(options)
// console.log(`result.body: ${JSON.stringify(result.body, null, 0)}`)
assert.isString(result.body)
})
})
describe('#getRawTransaction', () => {
it('should return tx hex', async () => {
const options = {
method: 'POST',
uri: 'http://localhost:3000/v2/rawtransactions/getRawTransaction',
resolveWithFullResponse: true,
json: true,
body: {
txids: [
'0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098'
],
verbose: true
}
}
const result = await rp(options)
// console.log(`result.body: ${util.inspect(result.body)}`)
assert.isArray(result.body)
})
})
})
-160
View File
@@ -1,160 +0,0 @@
/*
This library contains mocking data for running unit tests on the address route.
*/
'use strict'
const mockAddressDetails = {
addrStr: '1Fg4r9iDrEkCcDmHTy2T79EusNfhyQpu7W',
balance: 0.00126419,
balanceSat: 126419,
totalReceived: 0.02175868,
totalReceivedSat: 2175868,
totalSent: 0.02049449,
totalSentSat: 2049449,
unconfirmedBalance: 0,
unconfirmedBalanceSat: 0,
unconfirmedTxApperances: 0,
txApperances: 3,
transactions: [
'2dc053f55a666a3d2a08b1c680b704d62a55506d14ad884add87edcc56b9277d',
'544c15ce35c0f2e808d28f29d6587f1ec9276233e29856b7f2938cf0daef0026',
'81039b1d7b855b133f359f9dc65f776bd105650153a941675fedc504228ddbd3'
],
legacyAddress: '1Fg4r9iDrEkCcDmHTy2T79EusNfhyQpu7W',
cashAddress: 'bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'
}
const mockUtxoDetails = [
{
txid: '15f6a584080b04911121fbaca7bfcf3dd64ef2bfa5a01daf31e05a296c3e5e9e',
vout: 286,
amount: 0.00001,
satoshis: 1000,
height: 546083,
confirmations: 3490
},
{
txid: '15f6a584080b04911121fbaca7bfcf3dd64ef2bfa5a01daf31e05a296c3e5e9e',
vout: 287,
amount: 0.00001,
satoshis: 1000,
height: 546083,
confirmations: 3490
},
{
txid: '15f6a584080b04911121fbaca7bfcf3dd64ef2bfa5a01daf31e05a296c3e5e9e',
vout: 288,
amount: 0.00001,
satoshis: 1000,
height: 546083,
confirmations: 3490
}
]
const mockUnconfirmed = [
{
address: '1EzdL6TBbkNhnB2fYiBaKmcs5fxaoqwdAp',
txid: '000c00a90fb5031da6e02f7625df2f8b35a4c16e6feb9fc72293e67e5ff75786',
vout: 0,
scriptPubKey: '76a914997fabcd94a1e2aaa13a7664362e5e7b96c169a988ac',
amount: 0.00999626,
satoshis: 999626,
confirmations: 0,
ts: 1537989425
}
]
const mockTransactions = {
pagesTotal: 1,
txs: [
{
txid: '000c00a90fb5031da6e02f7625df2f8b35a4c16e6feb9fc72293e67e5ff75786',
version: 2,
locktime: 549565,
vin: [
{
txid:
'45c891c6d44619fc85716ba1b593aa83ebc1e500fe611b1ab98531ea203a0f21',
vout: 209,
sequence: 4294967294,
n: 0,
scriptSig: {
hex:
'47304402205b136f348bedae61a87c0500979d47ab02c76f0e4aba866b94c4931fccb5d7dc0220757a141660475b0cd4cb8b1f54b1ace95105f5bea775e817b115fb589fe4477541210246daec651c506f353daa7468714399c773df42ac4d663022d0e446a3331ac64a',
asm:
'304402205b136f348bedae61a87c0500979d47ab02c76f0e4aba866b94c4931fccb5d7dc0220757a141660475b0cd4cb8b1f54b1ace95105f5bea775e817b115fb589fe44775[ALL|FORKID] 0246daec651c506f353daa7468714399c773df42ac4d663022d0e446a3331ac64a'
},
addr: '14TdovsQUL69xL5g3zSzhLpmDb93d9rU9m',
valueSat: 1979534,
value: 0.01979534,
doubleSpentTxID: null
},
{
txid:
'6e5fa79547112a912adf6082c975dc80e282045193512ea369adb6bac7420674',
vout: 147,
sequence: 4294967294,
n: 1,
scriptSig: {
hex:
'473044022056674ac83a37d54ffec2c6d33cfc2ae2256f821b8bd818e1b6783356b49f3d28022046d3dd147c8fc51f2da5d16996a3cea1d7314e327aa1ae1017418737ed20136a4121038f23af565f68d8455e07e5c33c5cfc5114de2f478f035db23eb02f8ff5fe7f6e',
asm:
'3044022056674ac83a37d54ffec2c6d33cfc2ae2256f821b8bd818e1b6783356b49f3d28022046d3dd147c8fc51f2da5d16996a3cea1d7314e327aa1ae1017418737ed20136a[ALL|FORKID] 038f23af565f68d8455e07e5c33c5cfc5114de2f478f035db23eb02f8ff5fe7f6e'
},
addr: '1CEtC2fjELvmAzTd6MWuHb1gvuSq8x3Xpb',
valueSat: 10466,
value: 0.00010466,
doubleSpentTxID: null
}
],
vout: [
{
value: '0.00999626',
n: 0,
scriptPubKey: {
hex: '76a914997fabcd94a1e2aaa13a7664362e5e7b96c169a988ac',
asm:
'OP_DUP OP_HASH160 997fabcd94a1e2aaa13a7664362e5e7b96c169a9 OP_EQUALVERIFY OP_CHECKSIG',
addresses: ['1EzdL6TBbkNhnB2fYiBaKmcs5fxaoqwdAp'],
type: 'pubkeyhash'
},
spentTxId: null,
spentIndex: null,
spentHeight: null
},
{
value: '0.00990000',
n: 1,
scriptPubKey: {
hex: '76a914e3335e4d6babe61ea58311293c1c6bfe802801cb88ac',
asm:
'OP_DUP OP_HASH160 e3335e4d6babe61ea58311293c1c6bfe802801cb OP_EQUALVERIFY OP_CHECKSIG',
addresses: ['1MiKvgaQTFCTEZbSDMZgw9ahaB52Cr4ofb'],
type: 'pubkeyhash'
},
spentTxId: null,
spentIndex: null,
spentHeight: null
}
],
blockhash:
'00000000000000000158b1a2883688873ec5ea076e3b0f576bcdfe1fd277880f',
blockheight: 549601,
confirmations: 5,
time: 1537990026,
blocktime: 1537990026,
valueOut: 0.01989626,
size: 372,
valueIn: 0.0199,
fees: 0.00000374
}
]
}
module.exports = {
mockAddressDetails,
mockUtxoDetails,
mockUnconfirmed,
mockTransactions
}
-35
View File
@@ -1,35 +0,0 @@
/*
This library contains mocking data for running unit tests on the bitcore route.
*/
'use strict'
const mockBalance = {
confirmed: 10000000,
unconfirmed: 0,
balance: 10000000
}
const mockUtxos = [
{
_id: '5cf2c31a33bd46a95ec7e730',
chain: 'BCH',
network: 'testnet',
coinbase: false,
mintIndex: 1,
spentTxid: '',
mintTxid:
'5fe9b74056319a8c87f45cc745030715a6180758b94938dbf90d639d55652392',
mintHeight: 1265275,
spentHeight: -2,
address: 'qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4',
script: '76a9140e5b4ad9008bb9a027b7e2d0ef958914e12db20788ac',
value: 10000000,
confirmations: -1
}
]
module.exports = {
mockBalance,
mockUtxos
}
-42
View File
@@ -1,42 +0,0 @@
/*
This library contains mocking data for running unit tests on the address route.
*/
'use strict'
const mockBlockDetails = {
hash: '00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79',
size: 1319,
height: 1267544,
version: 536870912,
merkleroot:
'c2cd01ec2cf149acc7631385f89ae103d1a2ad212ab810c862d9680a811618ce',
tx: [
'52bfa89d449fef8070ec33e7a61f5ec8b5417ff62b050cd6a144ebce9557e0fa',
'8988e8742d2523f667c2d1374861919e3a82903e059fec75c10795da2303b93b',
'41def004f22b196d675566a6983ecf97611e72c48375b8ba779983d5ccd369d7',
'8ca8efb06abb94395f8331f87e101504dcf66aee828f2111922f056971fa7502',
'9ab409cbb203be7cf22bd6598d787fcdd5b417d885b72207ecc7253470770649'
],
time: 1542048973,
nonce: 3936284753,
bits: '1a03c148',
difficulty: 4467892.99177529,
chainwork: '00000000000000000000000000000000000000000000003ee27b503a064045bd',
confirmations: 3,
previousblockhash:
'00000000000001891202cbe18729a02a8763cf04da0ca7aded6e6c7d9b500785',
nextblockhash:
'000000000000039b93b3f3403a0eb21150904b15229d998cf3788fd8bf192cc2',
reward: 0.78125,
isMainChain: true,
poolInfo: {}
}
const mockBlockHash =
'00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79'
module.exports = {
mockBlockDetails,
mockBlockHash
}
-75
View File
@@ -1,75 +0,0 @@
/*
This library contains mocking data for running unit tests on the blockbook route.
*/
'use strict'
const mockBalance = {
page: 1,
totalPages: 1,
itemsOnPage: 1000,
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf',
balance: '10000000',
totalReceived: '10000000',
totalSent: '0',
unconfirmedBalance: '0',
unconfirmedTxs: 0,
txs: 1,
txids: ['5fe9b74056319a8c87f45cc745030715a6180758b94938dbf90d639d55652392']
}
const mockUtxos = [
{
txid: '5fe9b74056319a8c87f45cc745030715a6180758b94938dbf90d639d55652392',
vout: 1,
value: '10000000',
height: 1265275,
confirmations: 42704
}
]
const mockTx = {
txid: '5fe9b74056319a8c87f45cc745030715a6180758b94938dbf90d639d55652392',
version: 2,
vin: [
{
txid: '85ddb8215fc3701a493cf1c450644c5ef32c55aaa2f48ae2d008944394f3e4d3',
sequence: 4294967295,
n: 0,
addresses: ['bchtest:qqmd9unmhkpx4pkmr6fkrr8rm6y77vckjvqe8aey35'],
value: '16983000648',
hex:
'47304402202378e55f4d02bb932498deef22dfc1f7a4984858c3b55017e225dd567172252e0220373d27710b5d42a72ac9725959f1605a912fee0ae86a7ad36e7d6d796f14ca29412103c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792'
}
],
vout: [
{
value: '16973000422',
n: 0,
spent: true,
hex: '76a91436d2f27bbd826a86db1e93618ce3de89ef33169388ac',
addresses: ['bchtest:qqmd9unmhkpx4pkmr6fkrr8rm6y77vckjvqe8aey35']
},
{
value: '10000000',
n: 1,
hex: '76a9140e5b4ad9008bb9a027b7e2d0ef958914e12db20788ac',
addresses: ['bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4']
}
],
blockHash: '00000000005242edac4635ac2375a454e801cc1be8b131b622328089731e5e30',
blockHeight: 1265275,
confirmations: 65402,
blockTime: 1540912733,
value: '16983000422',
valueIn: '16983000648',
fees: '226',
hex:
'0200000001d3e4f394439408d0e28af4a2aa552cf35e4c6450c4f13c491a70c35f21b8dd85000000006a47304402202378e55f4d02bb932498deef22dfc1f7a4984858c3b55017e225dd567172252e0220373d27710b5d42a72ac9725959f1605a912fee0ae86a7ad36e7d6d796f14ca29412103c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792ffffffff02e66eabf3030000001976a91436d2f27bbd826a86db1e93618ce3de89ef33169388ac80969800000000001976a9140e5b4ad9008bb9a027b7e2d0ef958914e12db20788ac00000000'
}
module.exports = {
mockBalance,
mockUtxos,
mockTx
}
-45
View File
@@ -1,45 +0,0 @@
/*
This library contains mocking data for running unit tests on the address route.
*/
'use strict'
const mockGetInfo = {
version: 170200,
protocolversion: 70015,
walletversion: 160300,
balance: 0,
blocks: 1266726,
timeoffset: 0,
connections: 8,
proxy: '',
difficulty: 1,
testnet: true,
keypoololdest: 1536331195,
keypoolsize: 2000,
paytxfee: 0,
relayfee: 0.00001,
errors: 'Warning: unknown new rules activated (versionbit 28)'
}
const mockGetNetworkInfo = {
version: 190700,
subversion: '/Bitcoin ABC:0.19.7(EB32.0)/',
protocolversion: 70015,
localservices: '0000000000000425',
localrelay: true,
timeoffset: 0,
networkactive: true,
connections: 23,
networks: [{}, {}, {}],
relayfee: 0.00001,
excessutxocharge: 0,
localaddresses: [],
warnings:
"Warning: Unknown block versions being mined! It's possible unknown rules are in effect"
}
module.exports = {
mockGetInfo,
mockGetNetworkInfo
}
-122
View File
@@ -1,122 +0,0 @@
/*
Mocking data for electrumx unit tests
*/
'use strict'
const utxos = [
{
height: 604392,
tx_hash: '7774e449c5a3065144cefbc4c0c21e6b69c987f095856778ef9f45ddd8ae1a41',
tx_pos: 0,
value: 1000
},
{
height: 630834,
tx_hash: '4fe60a51e0d8f5134bfd8e5f872d6e502d7f01b28a6afebb27f4438a4f638d53',
tx_pos: 0,
value: 6000
}
]
const balance = {
confirmed: 7000,
unconfirmed: 0
}
const txHistory = [
{
height: 601861,
tx_hash: '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d'
}
]
const mempool = [
{
tx_hash: '45381031132c57b2ff1cbe8d8d3920cf9ed25efd9a0beb764bdb2f24c7d1c7e3',
height: 0,
fee: 24310
}
]
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,
txDetails,
blockHeaders
}
-89
View File
@@ -1,89 +0,0 @@
/*
Contains mocks of Express req and res objects.
*/
'use strict'
const sinon = require('sinon')
// Inspect JS Objects.
const util = require('util')
util.inspect.defaultOptions = {
showHidden: true,
colors: true
}
// mock for res.send()
function fakeSend (arg) {
// console.log(`res.send: ${util.inspect(arg)}`);
mockRes.output = arg
return arg
}
// mock for res.json()
function fakeJson (arg) {
// console.log(`res.json: ${util.inspect(arg)}`);
mockRes.output = arg
return arg
}
// mock for res.setStatus(num)
const setStatusCode = arg => {
mockRes.statusCode = arg
}
const mockReq = {
accepts: sinon.stub().returns({}),
acceptsCharsets: sinon.stub().returns({}),
acceptsEncodings: sinon.stub().returns({}),
acceptsLanguages: sinon.stub().returns({}),
body: {},
flash: sinon.stub().returns({}),
get: sinon.stub().returns({}),
is: sinon.stub().returns({}),
params: {},
query: {},
session: {},
locals: {},
headers: {}
}
const mockRes = {
append: sinon.stub().returns({}),
attachement: sinon.stub().returns({}),
clearCookie: sinon.stub().returns({}),
cookie: sinon.stub().returns({}),
download: sinon.stub().returns({}),
end: sinon.stub().returns({}),
get: sinon.stub().returns({}),
headersSent: sinon.stub().returns({}),
json: sinon.stub().callsFake(fakeJson),
jsonp: sinon.stub().returns({}),
links: sinon.stub().returns({}),
locals: {},
location: sinon.stub().returns({}),
output: null, // Used for retrieving output data.
redirect: sinon.stub().returns({}),
render: sinon.stub().returns({}),
send: sinon.stub().callsFake(fakeSend),
sendFile: sinon.stub().returns({}),
sendStatus: sinon.stub().returns({}),
set: sinon.stub().returns({}),
status: sinon.stub().callsFake(setStatusCode),
statusCode: null, // Default value before calling stats();
type: sinon.stub().returns({}),
vary: sinon.stub().returns({}),
write: sinon.stub().returns({}),
setHeader: sinon.stub().returns({}),
format: sinon.stub().returns({})
}
// Dev-Note on Rate Limits: Since next() is mocked, I can call the Sinon untility
// functions on it, like called(), to see if this stub was called.
const mockNext = sinon.stub().returns()
module.exports = {
mockReq,
mockRes,
mockNext
}
-21
View File
@@ -1,21 +0,0 @@
/*
This library contains mocking data for running unit tests on the address route.
*/
'use strict'
const mockMiningInfo = {
blocks: 1270185,
currentblocksize: 0,
currentblocktx: 0,
difficulty: 1,
warnings:
"Warning: Unknown block versions being mined! It's possible unknown rules are in effect",
networkhashps: 517410290.9365583,
pooledtx: 5,
chain: 'test'
}
module.exports = {
mockMiningInfo
}
-241
View File
@@ -1,241 +0,0 @@
/*
This library contains mocking data for running unit tests on the address route.
*/
'use strict'
const mockCoinbaseFeed = {
data: {
currency: 'BCH',
rates: {
AED: '918.11634',
AFN: '19208.6584998',
ALGO: '823.28722002635056355',
ALL: '26365.4673517',
AMD: '120379.9761886',
ANG: '448.6632494',
AOA: '162813.68075',
ARS: '19392.84240565',
ATOM: '45.3670932026499700525',
AUD: '354.4385981',
AWG: '449.91',
AZN: '425.539875',
BAL: '18.824215193276753072',
BAM: '415.45514235',
BAND: '40.7768732564399590085',
BAT: '1168.9711697428450012',
BBD: '499.9',
BCH: '1.0',
BDT: '21194.1523216',
BGN: '415.36691',
BHD: '94.2501462',
BIF: '483670.78174295',
BMD: '249.95',
BND: '339.227141',
BOB: '1725.8732563',
BRL: '1401.494645',
BSD: '249.95',
BSV: '1.567361207606150942625',
BTC: '0.021275',
BTN: '18323.91073475',
BWP: '2859.81217315',
BYN: '640.5913561',
BYR: '6405913.561',
BZD: '503.8107177',
CAD: '329.6765515',
CDF: '490377.5463717',
CGLD: '121.6805004503079241715',
CHF: '227.5939721',
CLF: '7.13432285',
CLP: '196860.63474705',
CNH: '1669.50978125',
CNY: '1670.21589',
COMP: '2.429765723728978241',
COP: '961722.29431455',
CRC: '150790.4338804',
CUC: '250.01273745',
CVE: '23620.275',
CZK: '5790.09175',
DAI: '247.842348666936355635',
DASH: '3.338230383973288798',
DJF: '44495.8985401',
DKK: '1580.608815',
DOP: '14597.4179324',
DZD: '32214.830745',
EEK: '3652.69206545',
EGP: '3924.839875',
EOS: '96.8423091824874092665',
ERN: '3749.25274945',
ETB: '9313.6508972',
ETC: '45.04821122825989111',
ETH: '0.658777328255340453815',
EUR: '212.33',
FJD: '534.9054975',
FKP: '193.01413945',
GBP: '193.115',
GEL: '807.3385',
GGP: '193.01413945',
GHS: '1453.70845015',
GIP: '193.01413945',
GMD: '12934.9125',
GNF: '2447970.79115335',
GTQ: '1944.576007',
GYD: '52260.2868518',
HKD: '1937.122498',
HNL: '6129.328889',
HRK: '1610.802775',
HTG: '15746.55080985',
HUF: '77512.6058777',
IDR: '3675351.03275',
ILS: '846.1232415',
IMP: '193.01413945',
INR: '18345.655135',
IQD: '298009.02802165',
ISK: '34745.5495',
JEP: '193.01413945',
JMD: '36409.6696094',
JOD: '177.21455',
JPY: '26360.601825',
KES: '27184.562',
KGS: '20308.0755724',
KHR: '1025880.7533059',
KMF: '105029.03124175',
KNC: '274.68542227594924615',
KRW: '284915.23080495',
KWD: '76.4532063',
KYD: '208.2943328',
KZT: '107014.42658325',
LAK: '2310183.71158185',
LBP: '378370.54450325',
LINK: '22.894634759817007489',
LKR: '46090.95971405',
LRC: '1457.8594342373869446',
LRD: '48852.72175115',
LSL: '4122.3008749',
LTC: '5.2062070401999589123',
LTL: '806.05000775',
LVL: '164.03243695',
LYD: '341.5926678',
MAD: '2298.47346335',
MDL: '4228.5806147',
MGA: '982179.7557538',
MKD: '13087.73767885',
MKR: '0.43764476902003812305',
MMK: '322679.7621378',
MNT: '709349.8756452',
MOP: '1995.2583685',
MRO: '89232.15',
MTL: '170.9003131',
MUR: '9974.21650765',
MVR: '3849.23',
MWK: '188665.67006785',
MXN: '5298.265135',
MYR: '1036.517655',
MZN: '18229.3548997',
NAD: '4134.173',
NGN: '95481.03472305',
NIO: '8698.6554209',
NMR: '8.624556609112112958',
NOK: '2334.203066',
NPR: '29318.1806909',
NZD: '379.2351378',
OMG: '74.464123456422320125',
OMR: '96.23549905',
OXT: '1033.27821413807369045',
PAB: '249.95',
PEN: '896.6071427',
PGK: '874.90073485',
PHP: '12136.85639315',
PKR: '40613.9575836',
PLN: '971.640633',
PYG: '1756126.21474795',
QAR: '910.1304375',
REN: '769.6689761354887733',
REP: '18.2912550311013566725',
REPV2: '18.1582473904586151205',
RON: '1035.54285',
RSD: '24943.76025',
RUB: '19402.093805',
RWF: '244237.3512583',
SAR: '937.65668115',
SBD: '2019.805958',
SCR: '4576.6749819',
SEK: '2207.698372',
SGD: '339.57582125',
SHP: '193.01413945',
SLL: '2490751.74625075',
SOS: '145044.39506805',
SRD: '3537.7923',
SSP: '32558.487',
STD: '5250063.0293496',
SVC: '2187.04525345',
SZL: '4118.75383445',
THB: '7800.314625',
TJS: '2579.48624955',
TMT: '874.825',
TND: '688.2998125',
TOP: '579.953986',
TRY: '1969.70598',
TTD: '1696.3476626',
TWD: '7179.16412995',
TZS: '579879.84308155',
UAH: '7090.3476468',
UGX: '935325.80066935',
UMA: '29.276720351390922145',
UNI: '78.76161966283282792',
USD: '249.95',
USDC: '249.95',
UYU: '10724.30446005',
UZS: '2592145.88136715',
VEF: '62109486.17813795',
VES: '113073216.80555545',
VND: '5792476.0820382',
VUV: '28582.2184128',
WST: '655.5403657',
XAF: '139318.05656485',
XAG: '10.233577875',
XAU: '0.1313462255',
XCD: '675.5023725',
XDR: '177.0940741',
XLM: '2935.3219224332811736',
XOF: '139318.05656485',
XPD: '0.1065011955',
XPF: '25344.75028595',
XPT: '0.290981792',
XRP: '1016.6768354687817832',
XTZ: '114.010080507218287957',
YER: '62574.97275195',
YFI: '0.0181099566723181033515',
ZAR: '4130.57372',
ZEC: '3.88966697790227161905',
ZMK: '1313006.15998725',
ZMW: '5048.6210738',
ZRX: '642.804422350408735165',
ZWL: '80483.9'
}
}
}
const mockCoinexFeed = {
code: 0,
data: {
date: 1605649499848,
ticker: {
vol: '26717.39062407',
low: '10.4000',
open: '11.8000',
high: '19.0029',
last: '18.5000',
buy: '18.0100',
buy_amount: '200.00000000',
sell: '18.5000',
sell_amount: '47.89580474'
}
},
message: 'OK'
}
module.exports = {
mockCoinbaseFeed,
mockCoinexFeed
}
-157
View File
@@ -1,157 +0,0 @@
/*
This library contains mocking data for running unit tests.
*/
'use strict'
const mockDecodeRawTransaction = {
txid: 'a332237d82a2543af1b0e1ae3c8cea1610c290ebcaf084a7e9894a61de0be988',
hash: 'a332237d82a2543af1b0e1ae3c8cea1610c290ebcaf084a7e9894a61de0be988',
size: 226,
version: 2,
locktime: 0,
vin: [
{
txid: '21cced645eab150585ed7ca7c96edebab5793cc0a3b3b286c42fd7d6d798b5b9',
vout: 1,
scriptSig: {
asm:
'3045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd41390[ALL|FORKID] 0360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413d',
hex:
'483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413d'
},
sequence: 4294967295
}
],
vout: [
{
value: 0.0001,
n: 0,
scriptPubKey: {
asm:
'OP_DUP OP_HASH160 eb4b180def88e3f5625b2d8ae2c098ff7d85f664 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: [Array]
}
},
{
value: 0.09989752,
n: 1,
scriptPubKey: {
asm:
'OP_DUP OP_HASH160 eb4b180def88e3f5625b2d8ae2c098ff7d85f664 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: [Array]
}
}
]
}
const mockDecodeScript = {
asm:
'0 0 -57 OP_NOP6 OP_LSHIFT OP_UNKNOWN OP_UNKNOWN OP_UNKNOWN c486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa OP_2OVER OP_NUMEQUALVERIFY OP_OR OP_INVERT OP_UNKNOWN OP_UNKNOWN 2ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab 67c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b [error]',
type: 'nonstandard',
p2sh: 'bchtest:pzy6dwfy6yf373w0dr05a6flfqksurjhwcl3awhdvm'
}
const mockRawTransactionConcise =
'02000000014e6b52500110b1c30315b85805fb274f0f4afceffc1589f889b27709e59e987d000000006a473044022052762770baa71c1a0b9544ad0f1ea343d32c22aa87c5f8397b6852f464c15b1e02201f4390745cb470e21e0e3c14229f39fef55ea0643a4c997f99d9f3501eae09b7412103c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792ffffffff02d1778f950a0000001976a91436d2f27bbd826a86db1e93618ce3de89ef33169388ac80969800000000001976a914152ea3cd65f18cb8fa9146c84ea1a97af8f051de88ac00000000'
const mockRawTransactionVerbose = {
hex:
'02000000014e6b52500110b1c30315b85805fb274f0f4afceffc1589f889b27709e59e987d000000006a473044022052762770baa71c1a0b9544ad0f1ea343d32c22aa87c5f8397b6852f464c15b1e02201f4390745cb470e21e0e3c14229f39fef55ea0643a4c997f99d9f3501eae09b7412103c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792ffffffff02d1778f950a0000001976a91436d2f27bbd826a86db1e93618ce3de89ef33169388ac80969800000000001976a914152ea3cd65f18cb8fa9146c84ea1a97af8f051de88ac00000000',
txid: 'bd320377db7026a3dd5c7ec444596c0ee18fc25c4f34ee944adc03e432ce1971',
hash: 'bd320377db7026a3dd5c7ec444596c0ee18fc25c4f34ee944adc03e432ce1971',
size: 225,
version: 2,
locktime: 0,
vin: [
{
txid: '7d989ee50977b289f88915fceffc4a0f4f27fb0558b81503c3b1100150526b4e',
vout: 0,
scriptSig: {
asm:
'3044022052762770baa71c1a0b9544ad0f1ea343d32c22aa87c5f8397b6852f464c15b1e02201f4390745cb470e21e0e3c14229f39fef55ea0643a4c997f99d9f3501eae09b7[ALL|FORKID] 03c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792',
hex:
'473044022052762770baa71c1a0b9544ad0f1ea343d32c22aa87c5f8397b6852f464c15b1e02201f4390745cb470e21e0e3c14229f39fef55ea0643a4c997f99d9f3501eae09b7412103c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792'
},
sequence: 4294967295
}
],
vout: [
{
value: 454.58880465,
n: 0,
scriptPubKey: {
asm:
'OP_DUP OP_HASH160 36d2f27bbd826a86db1e93618ce3de89ef331693 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a91436d2f27bbd826a86db1e93618ce3de89ef33169388ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: ['bchtest:qqmd9unmhkpx4pkmr6fkrr8rm6y77vckjvqe8aey35']
}
},
{
value: 0.1,
n: 1,
scriptPubKey: {
asm:
'OP_DUP OP_HASH160 152ea3cd65f18cb8fa9146c84ea1a97af8f051de OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a914152ea3cd65f18cb8fa9146c84ea1a97af8f051de88ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: ['bchtest:qq2jag7dvhccew86j9rvsn4p49a03uz3mcpw3d6aca']
}
}
],
blockhash: '000000000000026fa244de975ca89ea08008aa566564ce2e8ebb3144361b601b',
confirmations: 125,
time: 1542646373,
blocktime: 1542646373
}
const mockWHDecode = {
txid: 'f8a9857fe3b8a288b5fcafb1b0fc196731f433add6d962f77acd7c10b970ff89',
fee: '500',
sendingaddress: 'bchtest:qzjtnzcvzxx7s0na88yrg3zl28wwvfp97538sgrrmr',
referenceaddress: 'bchtest:qzjtnzcvzxx7s0na88yrg3zl28wwvfp97538sgrrmr',
ismine: false,
version: 0,
type_int: 0,
type: 'Simple Send',
propertyid: 368,
precision: '8',
amount: '10.00000000',
valid: true,
blockhash: '0000000046ba0bcef78caaa4492622176bbc563cf249ab52340a0449bc8e26f6',
blocktime: 1542814183,
positioninblock: 57,
block: 1269008,
confirmations: 2
}
const mockWHCreateInput = {
txid: 'f7ed9cf23dee85910f6269c9a101a75fcfd2f3c6fc81f17fad824ff7aaf99ab2',
vout: 1,
scriptPubKey: '76a914a4b98b0c118de83e7d39c834445f51dce62425f588ac',
amount: 0.09984138,
satoshis: 9984138,
height: 1269011,
confirmations: 4,
legacyAddress: 'mvXwPH74hW2yVTWwDwzsjGoaUAqJvWk7ZJ',
cashAddress: 'bchtest:qzjtnzcvzxx7s0na88yrg3zl28wwvfp97538sgrrmr',
value: 0.09984138
}
module.exports = {
mockDecodeRawTransaction,
mockDecodeScript,
mockRawTransactionConcise,
mockRawTransactionVerbose,
mockWHDecode,
mockWHCreateInput
}
-71
View File
@@ -1,71 +0,0 @@
/*
Mocks used for unit tests that interact with slpjs.
*/
'use strict'
// const sinon = require('sinon')
// const proxyquire = require('proxyquire')
const BigNumber = require('bignumber.js')
const slpMocks = require('./slp-mocks')
// Mock the BitboxNetwork class.
class BitboxNetwork {
// constructor () {}
async getAllSlpBalancesAndUtxos (address) {
return {
satoshis_available_bch: 9996891,
satoshis_in_slp_baton: 546,
satoshis_in_slp_token: 546,
satoshis_in_invalid_token_dag: 0,
satoshis_in_invalid_baton_dag: 0,
slpTokenBalances: {
'7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796': new BigNumber(
123400000000
)
},
slpTokenUtxos: {
'7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796': []
},
slpBatonUtxos: {
'7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796': []
},
nonSlpUtxos: [{}],
invalidTokenUtxos: [],
invalidBatonUtxos: []
}
}
async getTokenInformation (txid) {
BigNumber.set({ DECIMAL_PLACES: 8, ROUNDING_MODE: 4 })
const obj = {
versionType: 1,
transactionType: 0,
symbol: 'SLPSDK',
name: 'SLP SDK example using BITBOX',
documentUri: 'developer.bitcoin.com',
documentSha256: null,
decimals: 8,
batonVout: 2,
containsBaton: true,
genesisOrMintQuantity: new BigNumber(123400000000)
}
return obj
}
async getTransactionDetails (txid) {
return slpMocks.mockTx
}
}
// Mock the slpjs library.
const slpjs = {
BitboxNetwork,
slp: {},
validator: {}
}
module.exports = slpjs
-74
View File
@@ -1,74 +0,0 @@
/*
This library contains mocking data for running unit tests on the address route.
*/
'use strict'
/*
const mockDetails = {
txid: "6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40",
version: 2,
locktime: 0,
vin: [Array],
vout: [Array],
blockhash: "00000000e7232ff12462dedf9c11985f5b54202515277c337ccc59812758f28b",
blockheight: 1270188,
confirmations: 2,
time: 1543436253,
blocktime: 1543436253,
valueOut: 450.78867333,
size: 226,
valueIn: 450.78867559,
fees: 0.00000226
}
*/
const mockDetails = {
txid: '6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40',
version: 2,
locktime: 0,
vin: [
{
txid: '273d616d1c48f4b075c497f36ffdc79da5c8d6ed75485808b3599aac504f8525',
vout: 0,
sequence: 4294967295,
n: 0,
scriptSig: [{}],
addr: 'bchtest:qqmd9unmhkpx4pkmr6fkrr8rm6y77vckjvqe8aey35',
valueSat: 45078867559,
value: 450.78867559,
doubleSpentTxID: null
}
],
vout: [
{
value: '450.68867333',
n: 0,
scriptPubKey: [{}],
spentTxId: null,
spentIndex: null,
spentHeight: null
},
{
value: '0.10000000',
n: 1,
scriptPubKey: [{}],
spentTxId: null,
spentIndex: null,
spentHeight: null
}
],
blockhash: '00000000e7232ff12462dedf9c11985f5b54202515277c337ccc59812758f28b',
blockheight: 1270188,
confirmations: 3,
time: 1543436253,
blocktime: 1543436253,
valueOut: 450.78867333,
size: 226,
valueIn: 450.78867559,
fees: 0.00000226
}
module.exports = {
mockDetails
}
-130
View File
@@ -1,130 +0,0 @@
/*
This library contains mocking data for running unit tests on the address route.
*/
'use strict'
const mockAddress = {
isvalid: true,
address: 'bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y',
scriptPubKey: '76a914016a935f87e8deeab04501889b22d87e4e52db0988ac',
ismine: false,
iswatchonly: false,
isscript: false
}
const mockBalance = {
page: 1,
totalPages: 1,
itemsOnPage: 1000,
address: 'bitcoincash:qzp7gdl52edm24xlpkyqnza9rv43u3mdxyc77j3u6k',
balance: '2546',
totalReceived: '2546',
totalSent: '0',
unconfirmedBalance: '0',
unconfirmedTxs: 0,
txs: 2,
txids: [
'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56',
'44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6'
]
}
const mockUtxos = [
{
txid: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56',
vout: 0,
value: '2000',
height: 605873,
confirmations: 298,
satoshis: 2000
},
{
txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
vout: 1,
value: '546',
height: 605873,
confirmations: 298,
satoshis: 546
}
]
const mockThreeUtxos = [
{
txid: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56',
vout: 0,
value: '2000',
height: 605873,
confirmations: 298,
satoshis: 2000
},
{
txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
vout: 1,
value: '546',
height: 605873,
confirmations: 298,
satoshis: 546
},
{
txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
vout: 1,
value: '546',
height: 605873,
confirmations: 298,
satoshis: 546
}
]
const mockIsTokenUtxos = [
false,
{
txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
vout: 1,
value: '546',
height: 605873,
confirmations: 298,
satoshis: 546,
utxoType: 'token',
transactionType: 'send',
tokenId: '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7',
tokenTicker: 'TOK-CH',
tokenName: 'TokyoCash',
tokenDocumentUrl: '',
tokenDocumentHash: '',
decimals: 8,
tokenQty: 2
}
]
const tapUtxo = {
txid: '2e030df12390186baf817fa2760540b886511e04bc520e88f6b4c2124cc2a7d4',
vout: 1,
value: '546',
height: 606564,
confirmations: 3,
satoshis: 546,
utxoType: 'token',
transactionType: 'send',
tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d',
tokenTicker: 'TAP',
tokenName: 'Thoughts and Prayers',
tokenDocumentUrl: '',
tokenDocumentHash: '',
decimals: 0,
tokenQty: 1
}
const tokensOnly = [mockIsTokenUtxos[1], tapUtxo]
const multipleTokens = [false, mockIsTokenUtxos[1], tapUtxo]
module.exports = {
mockAddress,
mockBalance,
mockUtxos,
mockThreeUtxos,
mockIsTokenUtxos,
tokensOnly,
multipleTokens
}
-484
View File
@@ -1,484 +0,0 @@
'use strict'
const chai = require('chai')
const assert = chai.assert
// const nock = require('nock') // HTTP mocking
const sinon = require('sinon')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
// Mocking data.
const { mockReq, mockRes, mockNext } = require('./mocks/express-mocks')
// Libraries under test
const RateLimits = require('../../src/middleware/route-ratelimit')
let rateLimits = new RateLimits()
// const controlRoute = require('../../src/routes/v4/full-node/control')
const jwtAuth = require('../../src/middleware/jwt-auth')
let req, res, next
// let originalEnvVars // Used during transition from integration to unit tests.
// JWT token used in tests.
const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYWRlM2Y1NzM5ZTZjMGZmMDM0YjlhMSIsImlhdCI6MTU3MTY3NzQ1MCwiZXhwIjoxNTc0MjY5NDUwfQ.SSz7F7ETyBB3eoNG2VKCzPOhddtB-vrtmEoj7PxicrQ'
describe('#route-ratelimits & jwt-auth', () => {
let sandbox
before(async () => {
// Save existing environment variables.
// originalEnvVars = {
// BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL,
// RPC_BASEURL: process.env.RPC_BASEURL,
// RPC_USERNAME: process.env.RPC_USERNAME,
// RPC_PASSWORD: process.env.RPC_PASSWORD
// }
if (!process.env.JWT_AUTH_SERVER) { process.env.JWT_AUTH_SERVER = 'http://fakeurl.com/' }
// Wipe the Redis DB, which prevents false negatives when running integration
// tests back-to-back.
await rateLimits.wipeRedis()
})
// Setup the mocks before each test.
beforeEach(() => {
// Mock the req and res objects used by Express routes.
req = Object.assign({}, mockReq)
res = Object.assign({}, mockRes)
next = mockNext
// Explicitly reset the parmas and body.
req.params = {}
req.body = {}
req.query = {}
req.locals = {}
sandbox = sinon.createSandbox()
})
afterEach(() => {
sandbox.restore()
})
after(() => {
rateLimits.closeRedis()
})
describe('#jwt-auth.js', () => {
describe('#getTokenFromHeaders', () => {
it('should populate the req.locals object correctly', () => {
// Initialize req.locals
req.locals = {
proLimit: false,
apiLevel: 0
}
const header = `Token ${jwt}`
req.headers.authorization = header
jwtAuth.getTokenFromHeaders(req, res, next)
// console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`)
assert.property(req.locals, 'proLimit')
assert.property(req.locals, 'apiLevel')
assert.property(req.locals, 'jwtToken')
assert.equal(req.locals.jwtToken, jwt)
})
})
})
describe('#getResource', () => {
it('should decode a blockchain request', () => {
const url =
'/blockchain/getTxOut/62a3ea958a463a372bc0caf2c374a7f60be9c624be63a0db8db78f05809df6d8/0?include_mempool=true'
const result = rateLimits.getResource(url)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result, 'blockchain')
})
})
describe('#calcPoints', () => {
it('should return 50 points for anonymous user', () => {
const result = rateLimits.calcPoints()
// console.log(`result: ${result}`)
assert.equal(result, 50)
})
it('should return 50 points for free tier requesting full node access', () => {
const jwtInfo = {
apiLevel: 10,
resource: 'blockchain',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 50)
})
it('should return 50 points for free tier requesting indexer access', () => {
const jwtInfo = {
apiLevel: 10,
resource: 'blockbook',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 50)
})
it('should return 50 points for free tier requesting SLPDB access', () => {
const jwtInfo = {
apiLevel: 10,
resource: 'slp',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 50)
})
it('should return 10 points for full node tier requesting full node access', () => {
const jwtInfo = {
apiLevel: 20,
resource: 'blockchain',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it('should return 50 points for full-node tier requesting indexer access', () => {
const jwtInfo = {
apiLevel: 20,
resource: 'blockbook',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 50)
})
it('should return 50 points for full node tier requesting SLPDB access', () => {
const jwtInfo = {
apiLevel: 20,
resource: 'slp',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 50)
})
it('should return 10 point for indexer tier requesting full node access', () => {
const jwtInfo = {
apiLevel: 30,
resource: 'blockchain',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it('should return 10 points for indexer tier requesting indexer access', () => {
const jwtInfo = {
apiLevel: 30,
resource: 'blockbook',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it('should return 50 points for indexer tier requesting SLPDB access', () => {
const jwtInfo = {
apiLevel: 30,
resource: 'slp',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 50)
})
it('should return 10 point for SLP tier requesting full node access', () => {
const jwtInfo = {
apiLevel: 40,
resource: 'blockchain',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it('should return 10 points for SLP tier requesting indexer access', () => {
const jwtInfo = {
apiLevel: 40,
resource: 'blockbook',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it('should return 10 points for SLP tier requesting SLPDB access', () => {
const jwtInfo = {
apiLevel: 40,
resource: 'slp',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
})
describe('#rateLimitByResource', () => {
// NOTE: this test will fail if you run multiple integration tests in a
// short period. Because it talks to the Redis DB.
it('should pass through rate-limit middleware', async () => {
req.baseUrl = '/v4'
req.path = '/control/getNetworkInfo'
req.url = req.path
req.method = 'GET'
// Call the route twice to trigger the rate handling.
await rateLimits.rateLimitByResource(req, res, next)
await rateLimits.rateLimitByResource(req, res, next)
// next() will be called if rate-limit is not triggered
assert.equal(next.called, true)
})
it('should trigger rate-limit handler if rate limits exceeds 5 request per minute', async () => {
req.baseUrl = '/v4'
req.path = '/control/getNetworkInfo'
req.url = req.path
req.method = 'GET'
for (let i = 0; i < 5; i++) {
next.reset() // reset the stubbed next() function.
await rateLimits.rateLimitByResource(req, res, next)
// console.log(`next() called: ${next.called}`)
}
// Note: next() will be called unless the rate-limit kicks in.
assert.equal(
next.called,
false,
'next should not be called if rate limit was triggered.'
)
})
it('should NOT trigger rate-limit for free-tier at 5 RPM', async () => {
// Create a new instance of the rate limit so we start with zeroed tracking.
rateLimits = new RateLimits()
req.baseUrl = '/v4'
req.path = '/control/getNetworkInfo'
req.url = req.path
req.method = 'GET'
req.locals.jwtToken = 'some-token'
const jwtInfo = {
apiLevel: 10,
id: '5e3a0415eb29a962da2708b1'
}
// Mock the call to the jwt library.
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
for (let i = 0; i < 5; i++) {
next.reset() // reset the stubbed next() function.
await rateLimits.rateLimitByResource(req, res, next)
// console.log(`next() called: ${next.called}`)
}
// console.log(`req.locals after test: ${util.inspect(req.locals)}`)
// Note: next() will be called unless the rate-limit kicks in.
assert.equal(
next.called,
true,
'next should be called if rate limit was not triggered.'
)
})
it('should trigger rate-limit for free tier after 20 RPM', async () => {
// Create a new instance of the rate limit so we start with zeroed tracking.
rateLimits = new RateLimits()
req.baseUrl = '/v4'
req.path = '/control/getNetworkInfo'
req.url = req.path
req.method = 'GET'
req.locals.jwtToken = 'some-token'
const jwtInfo = {
apiLevel: 10,
id: '5e3a0415eb29a962da2708b2'
}
// Mock the call to the jwt library.
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
for (let i = 0; i < 22; i++) {
next.reset() // reset the stubbed next() function.
await rateLimits.rateLimitByResource(req, res, next)
// console.log(`next() called: ${next.called}`)
}
// Note: next() will be called unless the rate-limit kicks in.
assert.equal(
next.called,
false,
'next should not be called if rate limit was triggered.'
)
})
it('should NOT trigger rate-limit handler for indexer-tier at 25 RPM', async () => {
// Create a new instance of the rate limit so we start with zeroed tracking.
rateLimits = new RateLimits()
req.baseUrl = '/v4'
req.path = '/control/getNetworkInfo'
req.url = req.path
req.method = 'GET'
req.locals.jwtToken = 'some-token'
const jwtInfo = {
apiLevel: 20,
id: '5e3a0415eb29a962da2708b3'
}
// Mock the call to the jwt library.
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
for (let i = 0; i < 25; i++) {
next.reset() // reset the stubbed next() function.
await rateLimits.rateLimitByResource(req, res, next)
// console.log(`next() called: ${next.called}`)
}
// console.log(`req.locals after test: ${util.inspect(req.locals)}`)
// Note: next() will be called unless the rate-limit kicks in.
assert.equal(
next.called,
true,
'next should be called if rate limit was not triggered.'
)
})
it('should still rate-limit at a higher RPM for pro-tier', async () => {
// Create a new instance of the rate limit so we start with zeroed tracking.
rateLimits = new RateLimits()
req.baseUrl = '/v4'
req.path = '/control/getNetworkInfo'
req.url = req.path
req.method = 'GET'
req.locals.jwtToken = 'some-token'
const jwtInfo = {
apiLevel: 20,
id: '5e3a0415eb29a962da2708b5'
}
// Mock the call to the jwt library.
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
for (let i = 0; i < 150; i++) {
next.reset() // reset the stubbed next() function.
await rateLimits.rateLimitByResource(req, res, next)
// console.log(`next() called: ${next.called}`)
}
// console.log(`req.locals after test: ${util.inspect(req.locals)}`)
// Note: next() will be called unless the rate-limit kicks in.
assert.equal(
next.called,
false,
'next should NOT be called if rate limit was triggered.'
)
})
it('should handle misconfigured token secret', async () => {
// Create a new instance of the rate limit so we start with zeroed tracking.
rateLimits = new RateLimits()
req.baseUrl = '/v4'
req.path = '/control/getNetworkInfo'
req.url = req.path
req.method = 'GET'
req.locals.jwtToken = 'some-token'
next.reset() // reset the stubbed next() function.
await rateLimits.rateLimitByResource(req, res, next)
// Issues with token secret should treat incoming requests as anonymous
// calls with 30 points or 3 RPM.
assert.equal(res.locals.pointsToConsume, 50)
})
})
describe('#isInWhitelist', () => {
it('should return false when no argument is passed in', () => {
const result = rateLimits.isInWhitelist()
assert.equal(result, false)
})
it('should return false when origin is not in the whitelist', () => {
const origin = 'blah.com'
const result = rateLimits.isInWhitelist(origin)
assert.equal(result, false)
})
it('should return true when origin is in the whitelist', () => {
const origin = 'message.fullstack.cash'
const result = rateLimits.isInWhitelist(origin)
assert.equal(result, true)
})
})
})
// Generates a Basic authorization header.
// function generateAuthHeader (pass) {
// // https://en.wikipedia.org/wiki/Basic_access_authentication
// const username = 'BITBOX'
// const combined = `${username}:${pass}`
//
// var base64Credential = Buffer.from(combined).toString('base64')
// var readyCredential = `Basic ${base64Credential}`
//
// return readyCredential
// }
File diff suppressed because it is too large Load Diff
-2102
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More