Compare commits

...
175 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
89 changed files with 27800 additions and 14076 deletions
+2 -1
View File
@@ -30,6 +30,7 @@ start-fullstack-abc.sh
start-fullstack-bchn.sh start-fullstack-bchn.sh
start-ss-main.sh start-ss-main.sh
start-decatur-bchn.sh start-decatur-bchn.sh
shell-scripts/
coverage coverage
start-my-infra start-my-infra
@@ -41,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) 2021 Permissionless Software Foundation 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
+54 -35
View File
@@ -3,48 +3,41 @@
[![License](https://img.shields.io/npm/l/@psf/bch-js)](https://github.com/Permissionless-Software-Foundation/bch-js/blob/master/LICENSE.md) [![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) [![js-standard-style](https://img.shields.io/badge/javascript-standard%20code%20style-green.svg?style=flat-square)](https://github.com/feross/standard)
This is a REST API server, written in node.js JavaScript, using [Express.js](https://expressjs.com/) framework. 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://psfoundation.cash/blog/cash-stack) 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. 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).
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. 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.
![Cash Stack Network Diagram](./generic-network-diagram.png) 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.
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). ![bch-api dependency diagram](./diagrams/bch-api-dependency-graph.png)
- [API Documentation](https://fullstack.cash/documentation) ## Documentation
- [Example Code](https://fullstack.cash/examples) 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 fix` frequently run to fix 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/v4/ - BCH Mainnet REST API server: https://bchn.fullstack.cash/v5/
- ABC Mainnet REST API server: https://abc.fullstack.cash/v4/ - eCash Mainnet REST API server: https://abc.fullstack.cash/v5/
- Testnet3 REST API server: https://testnet3.fullstack.cash/v4/
- 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://psfoundation.cash/blog/cash-stack). described on [CashStack.info](https://cashstack.info).
This code targets the Ubuntu 18.04 LTS Linux OS or higher. 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:
@@ -69,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.
@@ -80,22 +73,48 @@ installation.
- Run the Docker container with: - Run the Docker container with:
`docker-compose up` `docker-compose up -d`
## Rate Limits ## Rate Limits
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://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, you can then include the JWT token in the HTTP header like this: Rate limiting of the REST API can be controlled in three different ways:
- JWT tokens for individual users
- Basic Authentication tokens for organization-wide usage.
- Whitelisting for specific domain names
### JWT Tokens
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:
- `Authorization: Token <JWT token>` - `Authorization: Token <JWT token>`
## Support ### cURL example
Have questions? Need help? Join our community support Here is an example using curl for using the JWT token when making a call to the REST API:
[Telegram channel](https://t.me/bch_js_toolkit)
## IPFS ```
Copies of this repository will occasionally be uploaded and hosted on [IPFS](https://ipfs.io): 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) - v1.25.1: [QmXFzFJHenymhReDc9oGAxbpmvK2fJYnisQ2pJn3gGyKKX](https://ipfs.io/ipfs/QmXFzFJHenymhReDc9oGAxbpmvK2fJYnisQ2pJn3gGyKKX)
## License ## License
[MIT](./LICENSE.md) [MIT](./LICENSE.md)
a
+2 -3
View File
@@ -1,4 +1,3 @@
{ {
"sampleUrl": null "sampleUrl": null
}
}
+4 -2
View File
@@ -12,7 +12,7 @@ const config = {
// Rate Limits // Rate Limits
anonRateLimit: process.env.ANON_RATE_LIMIT anonRateLimit: process.env.ANON_RATE_LIMIT
? Number(process.env.ANON_RATE_LIMIT) ? Number(process.env.ANON_RATE_LIMIT)
: 500, : 1000,
whitelistRateLimit: process.env.WHITELIST_RATE_LIMIT whitelistRateLimit: process.env.WHITELIST_RATE_LIMIT
? Number(process.env.WHITELIST_RATE_LIMIT) ? Number(process.env.WHITELIST_RATE_LIMIT)
: 10, : 10,
@@ -24,7 +24,9 @@ const config = {
: 10000, : 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', '10.0.'] : ['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}')
+32 -18
View File
@@ -1,19 +1,33 @@
bch-api-main: version: '3.9'
build: .
dockerfile: Dockerfile
#image: bitcore
container_name: bch-api-main
links:
- redis-main
ports:
- "3000:3000"
volumes:
- ../../logs:/home/safeuser/bch-api/logs
restart: always
redis-main: services:
image: redis:latest bch-api-main:
container_name: redis-main build:
restart: always context: ./
ports: dockerfile: Dockerfile
- "172.17.0.1:6379:6379" #image: bitcore
container_name: bch-api-main
links:
- redis-main
ports:
- "3000:3000"
volumes:
- ../../logs:/home/safeuser/bch-api/logs
logging:
driver: 'json-file'
options:
max-size: '10m'
max-file: '10'
restart: always
redis-main:
image: redis:latest
container_name: redis-main
logging:
driver: 'json-file'
options:
max-size: '10m'
max-file: '10'
restart: always
ports:
- "172.17.0.1:6379:6379"
+12 -1
View File
@@ -28,6 +28,7 @@ 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
@@ -39,7 +40,7 @@ export REDIS_HOST=172.17.0.1
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/v4/ 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
@@ -48,6 +49,9 @@ export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
# that originate froma domain on the whitelist. # that originate froma domain on the whitelist.
export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash 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. # 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. # Requests use the ANON rate limit if they fail to pass in a JWT token.
# ANON = 20 requests per minute (RPM) # ANON = 20 requests per minute (RPM)
@@ -61,4 +65,11 @@ export LOG_MAX_SIZE=1m
#5d means store no more than 5 days #5d means store no more than 5 days
export LOG_MAX_FILES=5d 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
+11 -5
View File
@@ -17,29 +17,32 @@ export RPC_PASSWORD=password
# SLPDB # SLPDB
export SLPDB_URL=http://172.17.0.1:13300/ export SLPDB_URL=http://172.17.0.1:13300/
export SLPDB_PASS=somelongpassword export SLPDB_PASS=portlandisacityinoregon
export SLPDB_PASS_GP=portlandisacityinoregon
export SLPDB_PASS_WL=portlandisacityinoregon
# Use the same address as SLPDB_URL if you don't have a separate whitelist server. # Use the same address as SLPDB_URL if you don't have a separate whitelist server.
export SLPDB_WHITELIST_URL=http://172.17.0.1:13300/ export SLPDB_WHITELIST_URL=http://172.17.0.1:13300/
# slp-api alternative SLP validator using slp-validate: # slp-api alternative SLP validator using slp-validate:
# https://github.com/Permissionless-Software-Foundation/slp-api # https://github.com/Permissionless-Software-Foundation/slp-api
export SLP_API_URL=http://10.0.0.5:5001/ 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=60002 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/v4/ export LOCAL_RESTURL=http://127.0.0.1:3000/v4/
# Basic Authentication password (optional) # Basic Authentication password (optional)
export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword export PRO_PASS=thejaasjhehrakseuhakuhr
# Whitelisted domains. Automatically give pro-tier rate limit access to apps # Whitelisted domains. Automatically give pro-tier rate limit access to apps
# that originate froma domain on the whitelist. # that originate froma domain on the whitelist.
@@ -50,6 +53,9 @@ export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash
# ANON = 20 requests per minute (RPM) # ANON = 20 requests per minute (RPM)
export ANON_RATE_LIMIT=50 export ANON_RATE_LIMIT=50
# 10 = 100 RPM # 10 = 100 RPM
export WHITELIST_RATE_LIMIT=10 export WHITELIST_RATE_LIMIT=1
export INTERNAL_RATE_LIMIT=100
npm start npm start
+19486 -5387
View File
File diff suppressed because it is too large Load Diff
+18 -16
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,22 +14,24 @@
"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 --exit --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 --exit --timeout 25000 -g '#hydrateUtxos-' 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-limit2-unit.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.18.0", "@psf/bch-js": "6.3.4",
"apidoc": "^0.26.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",
@@ -60,19 +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",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"nock": "^13.0.5", "nock": "^13.0.5",
"nyc": "^15.0.0", "nyc": "^15.0.0",
"prettier": "^2.0.0",
"semantic-release": "^17.4.2", "semantic-release": "^17.4.2",
"sinon": "^9.0.0", "sinon": "^9.0.0",
"standard": "^14.3.1" "standard": "^16.0.4"
}, },
"release": { "release": {
"publish": [ "publish": [
@@ -84,6 +86,6 @@
}, },
"apidoc": { "apidoc": {
"title": "bch-api", "title": "bch-api",
"url": "https://api.fullstack.cash/v4" "url": "https://api.fullstack.cash/v5"
} }
} }
+67 -56
View File
@@ -24,35 +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')
// v4 // v5
const healthCheckV4 = require('./routes/v4/health-check') const healthCheckV5 = require('./routes/v5/health-check')
const BlockchainV4 = require('./routes/v4/full-node/blockchain') const BlockchainV5 = require('./routes/v5/full-node/blockchain')
const ControlV4 = require('./routes/v4/full-node/control') const ControlV5 = require('./routes/v5/full-node/control')
const MiningV4 = require('./routes/v4/full-node/mining') const MiningV5 = require('./routes/v5/full-node/mining')
const networkV4 = require('./routes/v4/full-node/network') const networkV5 = require('./routes/v5/full-node/network')
const RawtransactionsV4 = require('./routes/v4/full-node/rawtransactions') const RawtransactionsV5 = require('./routes/v5/full-node/rawtransactions')
const UtilV4 = require('./routes/v4/util') const DSProofV5 = require('./routes/v5/full-node/dsproof')
const SlpV4 = require('./routes/v4/slp') const UtilV5 = require('./routes/v5/util')
const xpubV4 = require('./routes/v4/xpub') const SlpV5 = require('./routes/v5/slp')
const ElectrumXV4 = require('./routes/v4/electrumx') const xpubV5 = require('./routes/v5/xpub')
const EncryptionV4 = require('./routes/v4/encryption') const ElectrumXV5 = require('./routes/v5/electrumx')
const PriceV4 = require('./routes/v4/price') const EncryptionV5 = require('./routes/v5/encryption')
const Ninsight = require('./routes/v4/ninsight') const PriceV5 = require('./routes/v5/price')
const JWTV5 = require('./routes/v5/jwt')
const BcashSLP = require('./routes/v5/bcash/slp')
const PsfSlpIndexer = require('./routes/v5/psf-slp-indexer')
require('dotenv').config() require('dotenv').config()
// Instantiate v4 route libraries. // Instantiate v5 route libraries.
const blockchainV4 = new BlockchainV4() const blockchainV5 = new BlockchainV5()
const controlV4 = new ControlV4() const controlV5 = new ControlV5()
const miningV4 = new MiningV4() const miningV5 = new MiningV5()
const rawtransactionsV4 = new RawtransactionsV4() const rawtransactionsV5 = new RawtransactionsV5()
const slpV4 = new SlpV4() const slpV5 = new SlpV5()
const electrumxv4 = new ElectrumXV4() const electrumxv5 = new ElectrumXV5()
electrumxv4.connect() const encryptionv5 = new EncryptionV5()
const encryptionv4 = new EncryptionV4() const pricev5 = new PriceV5()
const pricev4 = new PriceV4() const utilV5 = new UtilV5({ electrumx: electrumxv5 })
const utilV4 = new UtilV4({ electrumx: electrumxv4 }) const dsproofV5 = new DSProofV5()
const jwtV5 = new JWTV5()
const bcashSLP = new BcashSLP()
const psfSlpIndexer = new PsfSlpIndexer()
const app = express() const app = express()
app.locals.env = process.env app.locals.env = process.env
@@ -69,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.
@@ -85,21 +90,21 @@ 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 v4prefix = 'v4' const v5prefix = 'v5'
// START Rate Limits // START Rate Limits
const auth = new AuthMW() const auth = new AuthMW()
// Ensure req.locals and res.locals objects exist. // Ensure req.locals and res.locals objects exist.
app.use(`/${v4prefix}/`, rateLimits.populateLocals) app.use(`/${v5prefix}/`, rateLimits.populateLocals)
// Allow users to turn off rate limits with an environment variable. // 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 const DO_NOT_USE_RATE_LIMITS = process.env.DO_NOT_USE_RATE_LIMITS || false
@@ -109,39 +114,45 @@ console.log(`DO_NOT_USE_RATE_LIMITS: ${DO_NOT_USE_RATE_LIMITS}`)
if (!DO_NOT_USE_RATE_LIMITS) { if (!DO_NOT_USE_RATE_LIMITS) {
console.log('Rate limits are being used') console.log('Rate limits are being used')
// Inspect the header for a JWT token. // Inspect the header for a JWT token.
app.use(`/${v4prefix}/`, jwtAuth.getTokenFromHeaders) app.use(`/${v5prefix}/`, jwtAuth.getTokenFromHeaders)
// Instantiate the authorization middleware, used to implement pro-tier rate limiting. // Instantiate the authorization middleware, used to implement pro-tier rate limiting.
// Handles Anonymous and Basic Authorization schemes used by passport.js // Handles Anonymous and Basic Authorization schemes used by passport.js
app.use(`/${v4prefix}/`, auth.mw()) app.use(`/${v5prefix}/`, auth.mw())
// Experimental rate limits // Experimental rate limits
app.use(`/${v4prefix}/`, rateLimits.applyRateLimits) 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 { } else {
console.log('Rate limits are NOT being used') console.log('Rate limits are NOT being used')
} }
// END Rate Limits // END Rate Limits
// Connect v4 routes // Connect v5 routes
app.use(`/${v4prefix}/` + 'health-check', healthCheckV4) app.use(`/${v5prefix}/` + 'health-check', healthCheckV5)
app.use(`/${v4prefix}/` + 'blockchain', blockchainV4.router) app.use(`/${v5prefix}/` + 'blockchain', blockchainV5.router)
app.use(`/${v4prefix}/` + 'control', controlV4.router) app.use(`/${v5prefix}/` + 'control', controlV5.router)
app.use(`/${v4prefix}/` + 'mining', miningV4.router) app.use(`/${v5prefix}/` + 'mining', miningV5.router)
app.use(`/${v4prefix}/` + 'network', networkV4) app.use(`/${v5prefix}/` + 'network', networkV5)
app.use(`/${v4prefix}/` + 'rawtransactions', rawtransactionsV4.router) app.use(`/${v5prefix}/` + 'rawtransactions', rawtransactionsV5.router)
app.use(`/${v4prefix}/` + 'slp', slpV4.router) app.use(`/${v5prefix}/` + 'slp', slpV5.router)
app.use(`/${v4prefix}/` + 'xpub', xpubV4.router) app.use(`/${v5prefix}/` + 'xpub', xpubV5.router)
app.use(`/${v4prefix}/` + 'electrumx', electrumxv4.router) app.use(`/${v5prefix}/` + 'electrumx', electrumxv5.router)
app.use(`/${v4prefix}/` + 'encryption', encryptionv4.router) app.use(`/${v5prefix}/` + 'encryption', encryptionv5.router)
app.use(`/${v4prefix}/` + 'price', pricev4.router) app.use(`/${v5prefix}/` + 'price', pricev5.router)
app.use(`/${v4prefix}/` + 'util', utilV4.router) app.use(`/${v5prefix}/` + 'util', utilV5.router)
app.use(`/${v5prefix}/` + 'dsproof', dsproofV5.router)
app.use(`/${v5prefix}/` + 'jwt', jwtV5.router)
const ninsight = new Ninsight() app.use(`/${v5prefix}/` + 'bcash/slp', bcashSLP.router)
app.use(`/${v4prefix}/` + 'ninsight', ninsight.router)
app.use(`/${v5prefix}/` + 'psf/slp', psfSlpIndexer.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) => {
@@ -228,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
} }
@@ -248,4 +259,4 @@ function onListening () {
debug(`Listening on ${bind}`) debug(`Listening on ${bind}`)
} }
// //
// module.exports = app; // module.exports = app
+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) {
+59 -2
View File
@@ -1,6 +1,4 @@
/* /*
This file will replace the original rate-limit.js file.
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:
- 10000 points in 60 seconds - 10000 points in 60 seconds
- 500 points per call for anonymous tier (20 RPM) - 500 points per call for anonymous tier (20 RPM)
@@ -35,6 +33,10 @@ const { RateLimiterRedis } = require('rate-limiter-flexible')
const wlogger = require('../util/winston-logging') const wlogger = require('../util/winston-logging')
const config = require('../../config') const config = require('../../config')
// The amount of delay in milliseconds to add to anonymous requests, in order
// to slow down freeloaders that are hitting the system too hard.
const ANON_DELAY = 3000
let _this // Global pointer to instance of class, when 'this' context is lost. let _this // Global pointer to instance of class, when 'this' context is lost.
// Setup Redis to track rate limits for each user. // Setup Redis to track rate limits for each user.
@@ -154,6 +156,7 @@ class RateLimits {
res, res,
defaultJwt defaultJwt
) )
// console.log(`hasExceededRateLimit: `, hasExceededRateLimit)
if (!hasExceededRateLimit) { if (!hasExceededRateLimit) {
// Rate limits have not been exceeded. Processing can continue. // Rate limits have not been exceeded. Processing can continue.
@@ -195,6 +198,7 @@ class RateLimits {
res, res,
req.locals.jwtToken req.locals.jwtToken
) )
// console.log('hasExceededRateLimit: ', hasExceededRateLimit)
if (!hasExceededRateLimit) { if (!hasExceededRateLimit) {
// Rate limits have not been exceeded. Processing can continue. // Rate limits have not been exceeded. Processing can continue.
@@ -213,14 +217,29 @@ class RateLimits {
next() next()
} }
sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
// A wrapper for Redis-based rate limiter. // A wrapper for Redis-based rate limiter.
// Will return false if the user has not exceeded the rate limit. Otherwise // 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 // it will return the 'res' object with an error status and message, which
// should be returned by the middleware. // should be returned by the middleware.
async trackRateLimits (req, res, jwtToken) { async trackRateLimits (req, res, jwtToken) {
const debugInfo = {
jwtToken,
userObj: req.body.usrObj,
locals: req.locals
}
// Anonymous rate limits are used by default. // Anonymous rate limits are used by default.
let pointsToConsume = ANON_LIMITS let pointsToConsume = ANON_LIMITS
// console.log('pointsToConsume: ', pointsToConsume)
let key = req.ip // Use the IP address as the key, by default. let key = req.ip // Use the IP address as the key, by default.
debugInfo.ip = req.ip
// console.log('jwtToken: ', jwtToken)
try { try {
// Decode the JWT token if it exists // Decode the JWT token if it exists
@@ -230,8 +249,10 @@ class RateLimits {
// Preferentially use the decoded ID in the JWT payload, as the key. // Preferentially use the decoded ID in the JWT payload, as the key.
key = decoded.id key = decoded.id
debugInfo.id = key
pointsToConsume = decoded.pointsToConsume pointsToConsume = decoded.pointsToConsume
debugInfo.pointsToConsume = pointsToConsume
} }
// console.log(`rate limit key: ${key}`) // console.log(`rate limit key: ${key}`)
@@ -239,8 +260,26 @@ class RateLimits {
// The 429 error response is handled by the catch(). // The 429 error response is handled by the catch().
await _this.rateLimiter.consume(key, pointsToConsume) 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. 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. // Signal that the user has not exceeded their rate limits.
return false return false
} catch (err) { } catch (err) {
@@ -251,6 +290,24 @@ class RateLimits {
res.locals.rateLimitTriggered = true res.locals.rateLimitTriggered = true
// console.log('res.locals: ', res.locals) // 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 // Rate limited was triggered
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({ return res.json({
-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
-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
-2227
View File
File diff suppressed because it is too large Load Diff
-606
View File
@@ -1,606 +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')
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 (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/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(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) })
}
}
/**
* @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.electrumx._balanceFromElectrumx(fromAddr)
// console.log(`balances: ${JSON.stringify(balances, null, 2)}`)
// Total balance is the sum of the confirmed and unconfirmed balance.
const totalBalance = balances.confirmed + balances.unconfirmed
// 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.electrumx._utxosFromElectrumx(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.value
transactionBuilder.addInput(utxo.tx_hash, utxo.tx_pos)
}
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.value
)
}
// 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.value
transactionBuilder.addInput(utxo.tx_hash, utxo.tx_pos)
}
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.value
)
}
// 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
}
}
}
module.exports = UtilRoute
+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) {
@@ -54,6 +54,7 @@ class Blockchain {
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.post('/getBlock', this.getBlock)
this.router.get('/getBlockHash/:height', this.getBlockHash)
} }
root (req, res, next) { root (req, res, next) {
@@ -81,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
*/ */
@@ -112,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"
@@ -156,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
*/ */
@@ -189,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
@@ -250,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
@@ -342,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) {
@@ -373,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) {
@@ -405,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) {
@@ -443,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 {
@@ -516,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) {
@@ -558,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) {
@@ -588,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"
* *
*/ */
@@ -600,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
* *
@@ -636,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)
@@ -693,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)
@@ -747,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)
* *
@@ -946,7 +947,7 @@ class Blockchain {
* @apiDescription Returns block details * @apiDescription Returns block details
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl "https://api.fullstack.cash/v4/blockchain/getblock/" -X POST -H "Content-Type: application/json" --data-binary '{"blockhash":"000000000000000002a5fe0bdd6e3f04342a975c0f55e57f97e73bb90041676b","verbosity":0 }' * 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 {String} blockhash Block hash (required)
* @apiParam {Number} verbosity Default 1 (optional) * @apiParam {Number} verbosity Default 1 (optional)
@@ -984,6 +985,47 @@ class Blockchain {
return _this.errorHandler(err, res) 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/v4/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/v4/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/v4/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/v4/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/v4/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"]}'
* *
* *
*/ */
@@ -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/v4/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/v4/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"]}'
* *
* *
*/ */
@@ -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/v4/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) {
@@ -348,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)
) )
@@ -375,7 +375,7 @@ class RawTransactions {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/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"
* *
* *
*/ */
@@ -421,7 +421,7 @@ class RawTransactions {
* *
* *
* @apiExample Example usage: * @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"]}' * curl -X POST "https://api.fullstack.cash/v5/rawtransactions/sendRawTransaction" -H "accept:application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
* *
* *
*/ */
@@ -436,7 +436,8 @@ 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)) {
@@ -514,7 +515,7 @@ class RawTransactions {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/rawtransactions/sendRawTransaction/01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000" -H "accept: " * curl -X GET "https://api.fullstack.cash/v5/rawtransactions/sendRawTransaction/01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000" -H "accept: "
* *
* *
*/ */
@@ -536,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'
@@ -557,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,7 +25,7 @@ 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 = this.bchCoinexPriceUrl =
'https://api.coinex.com/v1/market/ticker?market=bchusdt' 'https://api.coinex.com/v1/market/ticker?market=bchusdt'
@@ -64,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) {
@@ -96,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
@@ -129,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) {
@@ -163,7 +163,7 @@ class Price {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/price/bchusd" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v5/price/bchusd" -H "accept: application/json"
* *
*/ */
async getBCHUSD (req, res, next) { async getBCHUSD (req, res, next) {
+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
View File
@@ -175,6 +175,11 @@ class RouteUtils {
msg: '429 Too Many Requests', msg: '429 Too Many Requests',
status: 429 status: 429
} }
} else if (err.error.includes('Network error:')) {
return {
msg: err.error,
status: 503
}
} }
} }
+13 -7
View File
@@ -6,20 +6,26 @@
'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 // Default 1 Megabyte
var LOG_MAX_SIZE = process.env.LOG_MAX_SIZE ? process.env.LOG_MAX_SIZE : '1m' const LOG_MAX_SIZE = process.env.LOG_MAX_SIZE ? process.env.LOG_MAX_SIZE : '1m'
// Default 5 days. // Default 5 days.
var LOG_MAX_FILES = process.env.LOG_MAX_FILES ? process.env.LOG_MAX_FILES : '5d' // 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: LOG_MAX_SIZE, maxSize: LOG_MAX_SIZE,
@@ -35,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: [
+22 -17
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# Docker containers for the below infrastructure is described here: # Docker containers for the below infrastructure is described here:
# https://psfoundation.cash/blog/cash-stack # https://CashStack.info
# Pre-synced databases can be downloaded on the CashStrap page: # Pre-synced databases can be downloaded on the CashStrap page:
# https://fullstack.cash/cashstrap # https://fullstack.cash/cashstrap
@@ -15,19 +15,16 @@ export RPC_BASEURL=http://$RPC_IP/
export RPC_USERNAME=bitcoin export RPC_USERNAME=bitcoin
export RPC_PASSWORD=password export RPC_PASSWORD=password
# SLPDB # (optional) If load-balancing multiple full nodes, it's best to pick a single
export SLPDB_PASS_GP=somelongpassword # one for broadcasting transactions to the network. Prevents accidental
export SLPDB_URL=http://<SLPDB IP>:12300/ # double spends.
# Use the same address as SLPDB_URL if you don't have a separate whitelist server. #export RPC_SENDURL=http://$RPC_IP/
export SLPDB_PASS_WL=somelongpassword
export SLPDB_WHITELIST_URL=http://<SLPDB IP>:12300/ # psf-slp-indxer
# slp-api alternative SLP validator using slp-validate: export SLP_INDEXER_API=http://<ip>:<port>/
# https://github.com/Permissionless-Software-Foundation/slp-api
export SLP_API_URL=http://10.0.0.5:5001/
# 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. # Redis DB - Used for rate limiting - customize to your own Redis installation.
export REDIS_PORT=6379 export REDIS_PORT=6379
@@ -39,7 +36,7 @@ export REDIS_HOST=172.17.0.1
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/v4/ 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
@@ -48,12 +45,14 @@ export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
# that originate froma domain on the whitelist. # that originate froma domain on the whitelist.
export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash
# Rate Limits. Numbers are divided into 1000. e.g. 1000 / 50 = 20 RPM for ANON. # 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. # Requests use the ANON rate limit if they fail to pass in a JWT token.
# ANON = 20 requests per minute (RPM) # ANON = 20 requests per minute (RPM)
export ANON_RATE_LIMIT=50 export ANON_RATE_LIMIT=500
# 10 = 100 RPM # 10 = 1000 RPM
export WHITELIST_RATE_LIMIT=10 export WHITELIST_RATE_LIMIT=100
# Set logging parameters # Set logging parameters
#1m means no more than 1 megabyte #1m means no more than 1 megabyte
@@ -61,4 +60,10 @@ export LOG_MAX_SIZE=1m
#5d means store no more than 5 days #5d means store no more than 5 days
export LOG_MAX_FILES=5d 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
-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
}
-2277
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+244
View File
@@ -0,0 +1,244 @@
/*
TESTS FOR THE bcash/slp.js LIBRARY
*/
'use strict'
const chai = require('chai')
const assert = chai.assert
const sinon = require('sinon')
const BcashSlp = require('../../src/routes/v5/bcash/slp')
// Mocking data.
const { mockReq, mockRes } = require('./mocks/express-mocks')
const mockData = require('./mocks/bcash-slp-mock')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
if (!process.env.BCASH_SERVER) process.env.BCASH_SERVER = 'http://localhost'
describe('#bcash-slp', () => {
let req, res
let sandbox
const bcashSlp = new BcashSlp()
// let electrumxRoute
before(async () => {
if (!process.env.TEST) {
process.env.TEST = 'unit'
}
console.log(`Testing type is: ${process.env.TEST}`)
if (!process.env.NETWORK) process.env.NETWORK = 'testnet'
// Connect to electrumx servers if this is an integration test.
// if (process.env.TEST === 'integration') {
// await electrumxRoute.connect()
// console.log('Connected to ElectrumX server')
// }
})
after(async () => {
// console.log(`electrumxRoute.electrumx: `, electrumxRoute.electrumx)
// Disconnect from the electrumx server if this is an integration test.
// if (process.env.TEST === 'integration') {
// await electrumxRoute.disconnect()
// console.log('Disconnected from ElectrumX server')
// }
})
// 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()
// electrumxRoute = new ElecrumxRoute()
})
afterEach(() => {
sandbox.restore()
})
after(() => {
//
})
describe('#root', () => {
// root route handler.
const root = bcashSlp.root
it('should respond to GET for base route', async () => {
const result = root(req, res)
assert.equal(result.status, 'bcash-slp', 'Returns static string')
})
})
describe('#getUtxos', () => {
it('should return UTXOs for an address, hydrated with SLP info', async () => {
sandbox.stub(bcashSlp.axios, 'get').resolves({ data: mockData.utxos })
sandbox.stub(bcashSlp, 'getTokenInfo').resolves(mockData.tokenInfo)
req.params.address =
'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk'
const utxos = await bcashSlp.getUtxos(req, res)
assert.isArray(utxos)
const utxo = utxos[0]
assert.property(utxo, 'version')
assert.property(utxo, 'height')
assert.property(utxo, 'value')
assert.property(utxo, 'script')
assert.property(utxo, 'address')
assert.property(utxo, 'coinbase')
assert.property(utxo, 'hash')
assert.property(utxo, 'index')
assert.property(utxo, 'slp')
assert.property(utxo.slp, 'tokenId')
assert.property(utxo.slp, 'ticker')
assert.property(utxo.slp, 'name')
assert.property(utxo.slp, 'uri')
assert.property(utxo.slp, 'hash')
assert.property(utxo.slp, 'decimals')
assert.property(utxo.slp, 'vout')
assert.property(utxo.slp, 'value')
assert.property(utxo.slp, 'type')
})
it('should handle error if address is array', async () => {
req.params.address = [
'bchtest:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk'
]
const result = await bcashSlp.getUtxos(req, res)
assert.include(
result.error,
'address can not be an array. Use POST for bulk upload.'
)
})
it('should throw error if address has invalid format', async () => {
sandbox.stub(bcashSlp.routeUtils, 'validateNetwork').returns(false)
req.params.address = 'qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk'
const result = await bcashSlp.getUtxos(req, res)
assert.include(
result.error,
'Invalid network. Trying to use a testnet address on mainnet'
)
})
it('should catch axios error', async () => {
sandbox.stub(bcashSlp.axios, 'get').throws(new Error('test error'))
req.params.address =
'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk'
const result = await bcashSlp.getUtxos(req, res)
assert.include(result.error, 'test error')
})
})
describe('#hydrateUTXOS', () => {
it('should hydrate slp utxos', async () => {
sandbox.stub(bcashSlp, 'getTokenInfo').resolves(mockData.tokenInfo)
const hydratedUtxos = await bcashSlp.hydrateUTXOS(mockData.utxos)
assert.isArray(hydratedUtxos)
const hydratedUtxo = hydratedUtxos[0]
assert.property(hydratedUtxo, 'version')
assert.property(hydratedUtxo, 'height')
assert.property(hydratedUtxo, 'value')
assert.property(hydratedUtxo, 'script')
assert.property(hydratedUtxo, 'address')
assert.property(hydratedUtxo, 'coinbase')
assert.property(hydratedUtxo, 'hash')
assert.property(hydratedUtxo, 'index')
assert.property(hydratedUtxo, 'slp')
assert.property(hydratedUtxo.slp, 'tokenId')
assert.property(hydratedUtxo.slp, 'ticker')
assert.property(hydratedUtxo.slp, 'name')
assert.property(hydratedUtxo.slp, 'uri')
assert.property(hydratedUtxo.slp, 'hash')
assert.property(hydratedUtxo.slp, 'decimals')
assert.property(hydratedUtxo.slp, 'vout')
assert.property(hydratedUtxo.slp, 'value')
assert.property(hydratedUtxo.slp, 'type')
})
it('should throw error if utxos is not provided', async () => {
try {
await bcashSlp.hydrateUTXOS()
assert.fail('Unexpected code path')
} catch (error) {
assert.include(error.message, 'UTXOs must be an array of slp utxos')
}
})
it('should handle error', async () => {
try {
sandbox.stub(bcashSlp, 'getTokenInfo').throws(new Error('test error'))
await bcashSlp.getTokenInfo(mockData.utxos)
assert.fail('Unexpected code path')
} catch (error) {
assert.include(error.message, 'test error')
}
})
})
describe('#getTokenInfo', () => {
it('should return SLP info for an token id', async () => {
sandbox.stub(bcashSlp.axios, 'get').resolves({ data: mockData.tokenInfo })
const tokenId =
'afd88e9afab110e7b75410417edb5c98798c08aa892cee8d97b44f2e5545a900'
const slpInfo = await bcashSlp.getTokenInfo(tokenId)
assert.isObject(slpInfo)
assert.property(slpInfo, 'tokenId')
assert.property(slpInfo, 'ticker')
assert.property(slpInfo, 'name')
assert.property(slpInfo, 'uri')
assert.property(slpInfo, 'hash')
assert.property(slpInfo, 'decimals')
})
it('should throw error if token id is not provided', async () => {
try {
await bcashSlp.getTokenInfo()
assert.fail('Unexpected code path')
} catch (error) {
assert.include(error.message, 'tokenId must be string')
}
})
it('should catch axios error', async () => {
try {
sandbox.stub(bcashSlp.axios, 'get').throws(new Error('test error'))
const tokenId =
'afd88e9afab110e7b75410417edb5c98798c08aa892cee8d97b44f2e5545a900'
await bcashSlp.getTokenInfo(tokenId)
assert.fail('Unexpected code path')
} catch (error) {
assert.include(error.message, 'test error')
}
})
})
describe('#errorHandler', () => {
it('should handle unexpected errors', () => {
sandbox.stub(bcashSlp.routeUtils, 'decodeError').returns({ msg: false })
const result = bcashSlp.errorHandler(new Error('test error'), res)
// console.log('result: ', result)
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
assert.property(result, 'error')
})
})
})
@@ -12,7 +12,7 @@ const chai = require('chai')
const assert = chai.assert const assert = chai.assert
const sinon = require('sinon') const sinon = require('sinon')
const Blockchain = require('../../src/routes/v4/full-node/blockchain') const Blockchain = require('../../src/routes/v5/full-node/blockchain')
const uut = new Blockchain() const uut = new Blockchain()
const util = require('util') const util = require('util')
@@ -456,7 +456,7 @@ describe('#BlockchainRouter', () => {
it('should throw 400 error if addresses array is too large', async () => { it('should throw 400 error if addresses array is too large', async () => {
const testArray = [] const testArray = []
for (var i = 0; i < 25; i++) testArray.push('') for (let i = 0; i < 25; i++) testArray.push('')
req.body.hashes = testArray req.body.hashes = testArray
@@ -1006,7 +1006,7 @@ describe('#BlockchainRouter', () => {
it('should throw 400 error if addresses array is too large', async () => { it('should throw 400 error if addresses array is too large', async () => {
const testArray = [] const testArray = []
for (var i = 0; i < 25; i++) testArray.push('') for (let i = 0; i < 25; i++) testArray.push('')
req.body.txids = testArray req.body.txids = testArray
@@ -1525,7 +1525,7 @@ describe('#BlockchainRouter', () => {
it('should throw 400 error if addresses array is too large', async () => { it('should throw 400 error if addresses array is too large', async () => {
const testArray = [] const testArray = []
for (var i = 0; i < 25; i++) testArray.push('') for (let i = 0; i < 25; i++) testArray.push('')
req.body.txids = testArray req.body.txids = testArray
@@ -1729,7 +1729,7 @@ describe('#BlockchainRouter', () => {
it('should throw 400 error if addresses array is too large', async () => { it('should throw 400 error if addresses array is too large', async () => {
const testArray = [] const testArray = []
for (var i = 0; i < 25; i++) testArray.push('') for (let i = 0; i < 25; i++) testArray.push('')
req.body.proofs = testArray req.body.proofs = testArray
+1 -1
View File
@@ -11,7 +11,7 @@
const chai = require('chai') const chai = require('chai')
const assert = chai.assert const assert = chai.assert
const ControlRoute = require('../../src/routes/v4/full-node/control') const ControlRoute = require('../../src/routes/v5/full-node/control')
const uut = new ControlRoute() const uut = new ControlRoute()
const sinon = require('sinon') const sinon = require('sinon')
+190
View File
@@ -0,0 +1,190 @@
/*
TESTS FOR THE DSPROOF.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 DSProofRoute = require('../../src/routes/v5/full-node/dsproof')
const uut = new DSProofRoute()
// const nock = require('nock') // HTTP mocking
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/dsproof-mocks')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
describe('#DSProof', () => {
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.
if (!process.env.TEST) process.env.TEST = 'unit'
if (process.env.TEST === 'unit') {
process.env.BITCOINCOM_BASEURL = 'http://fakeurl/api/'
process.env.RPC_BASEURL = 'http://fakeurl/api'
process.env.RPC_USERNAME = 'fakeusername'
process.env.RPC_PASSWORD = 'fakepassword'
}
})
// 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.
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, 'dsproof', 'Returns static string')
})
})
describe('#getDSProof', async () => {
it('should throw 400 error if txid is missing', async () => {
const result = await uut.getDSProof(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.include(result.error, 'txid can not be empty')
})
it('should throw 400 error if txid is invalid', async () => {
req.params.txid = 'abc123'
const result = await uut.getDSProof(req, res)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
assert.include(result.error, 'txid must be of length 64')
})
it('should throw 503 when network issues', async () => {
req.params.txid =
'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'
// Save the existing RPC URL.
const savedUrl2 = process.env.RPC_BASEURL
// Manipulate the URL to cause a 500 network error.
process.env.RPC_BASEURL = 'http://fakeurl/api/'
await uut.getDSProof(req, res)
// console.log(`result: ${util.inspect(result)}`)
// Restore the saved URL.
process.env.RPC_BASEURL = savedUrl2
assert.isAbove(
res.statusCode,
499,
'HTTP status code 500 or greater expected.'
)
// assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
})
it('returns proper error when downstream service stalls', async () => {
req.params.txid =
'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
const result = await uut.getDSProof(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 () => {
req.params.txid =
'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
const result = await uut.getDSProof(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 double-spend proof', async function () {
req.params.txid =
'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox
.stub(uut.axios, 'request')
.resolves({ data: { result: mockData.mockGetDsProof } })
} else {
return this.skip()
}
const result = await uut.getDSProof(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.property(result, 'dspid')
assert.property(result, 'txid')
assert.property(result, 'outpoint')
assert.property(result, 'path')
assert.property(result, 'descendants')
assert.property(result.outpoint, 'txid')
assert.property(result.outpoint, 'vout')
})
})
})
@@ -19,7 +19,7 @@ const sinon = require('sinon')
if (!process.env.TEST) process.env.TEST = 'unit' if (!process.env.TEST) process.env.TEST = 'unit'
// Only load blockbook library after setting BLOCKBOOK_URL env var. // Only load blockbook library after setting BLOCKBOOK_URL env var.
const EncryptionRoute = require('../../src/routes/v4/encryption') const EncryptionRoute = require('../../src/routes/v5/encryption')
const encryptionRoute = new EncryptionRoute() const encryptionRoute = new EncryptionRoute()
// Mocking data. // Mocking data.
+114
View File
@@ -0,0 +1,114 @@
/*
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 SLP = require('../../../src/routes/v4/slp')
const slp = new SLP()
const { mockReq, mockRes } = require('../mocks/express-mocks')
describe('#nft', () => {
let req, res
beforeEach(() => {
// Mock the req and res objects used by Express routes.
req = mockReq
res = mockRes
})
describe('#getNftChildren', () => {
it('should return error on non-existing NFT group token', async () => {
req.params.tokenId =
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0b'
const result = await slp.getNftChildren(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.include(
result.error,
'NFT group does not exists',
'Error message expected'
)
})
it('should return error on non-group NFT token', async () => {
req.params.tokenId =
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
const result = await slp.getNftChildren(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.include(
result.error,
'NFT group does not exists',
'Error message expected'
)
})
it('should get NFT children list', async () => {
req.params.tokenId =
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
const result = await slp.getNftChildren(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.isArray(result.nftChildren)
assert.equal(result.nftChildren.length, 2)
assert.equal(
result.nftChildren[0],
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
)
assert.equal(
result.nftChildren[1],
'928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55'
)
})
})
describe('#getNftGroup', () => {
it('should return error on non-existing NFT child token', async () => {
req.params.tokenId =
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a8'
const result = await slp.getNftGroup(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.include(
result.error,
'NFT child does not exists',
'Error message expected'
)
})
it('should get NFT group token info', async () => {
req.params.tokenId =
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
// req.params.tokenId = '928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55'
const result = await slp.getNftGroup(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.property(result, 'nftGroup')
assert.property(result.nftGroup, 'id')
assert.property(result.nftGroup, 'versionType')
assert.property(result.nftGroup, 'symbol')
assert.equal(
result.nftGroup.id,
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
)
assert.equal(result.nftGroup.versionType, 129)
assert.equal(result.nftGroup.symbol, 'PSF.TEST.GROUP')
})
})
})
+101
View File
@@ -0,0 +1,101 @@
/*
TESTS FOR THE JWT.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 JWTRoute = require('../../src/routes/v5/jwt')
const uut = new JWTRoute()
const sinon = require('sinon')
// 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('#JWTRouter', () => {
let req, res
let sandbox
before(() => {
// 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(() => {})
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, 'jwt', 'Returns static string')
})
})
describe('#jwtInfo', () => {
it('should decode the JWT token', async () => {
const jwtToken =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwZWM3NTY1YTM4ZjkyMjdlOWVjNTM3MCIsImVtYWlsIjoidGVzdHVzZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjAsInJhdGVMaW1pdCI6MywicG9pbnRzVG9Db25zdW1lIjo1MDAsImR1cmF0aW9uIjozMCwiaWF0IjoxNjI2MTA5MzQ4LCJleHAiOjE2Mjg3MDEzNDh9.hF8BKU-1SvlvQBnTNbB65ErQTtNSl-pWlRANSJY-Zb4'
req.body.jwt = jwtToken
const result = await uut.jwtInfo(req, res)
// console.log('result: ', result)
assert.equal(result.error, 'jwt expired')
// assert.property(result, 'id')
// assert.property(result, 'email')
// assert.property(result, 'apiLevel')
// assert.property(result, 'rateLimit')
// assert.property(result, 'pointsToConsume')
// assert.property(result, 'duration')
// assert.property(result, 'iat')
// assert.property(result, 'exp')
// assert.property(result, 'expiration')
// assert.property(result, 'createdAt')
})
it('should return an error with malformed JWT token', async () => {
const jwtToken =
'eyJhbGciOiJIUzI1NiLsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwZWM3NTY1YTM4ZjkyMjdlOWVjNTM3MCIsImVtYWlsIjoidGVzdHVzZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjAsInJhdGVMaW1pdCI6MywicG9pbnRzVG9Db25zdW1lIjo1MDAsImR1cmF0aW9uIjozMCwiaWF0IjoxNjI2MTA5MzQ4LCJleHAiOjE2Mjg3MDEzNDh9.hF8BKU-1SvlvQBnTNbB65ErQTtNSl-pWlRANSJY-Zb4'
req.body.jwt = jwtToken
const result = await uut.jwtInfo(req, res)
// console.log('result: ', result)
assert.equal(result.error, 'invalid token')
})
})
})
+1 -1
View File
@@ -10,7 +10,7 @@
const chai = require('chai') const chai = require('chai')
const assert = chai.assert const assert = chai.assert
const MiningRoute = require('../../src/routes/v4/full-node/mining') const MiningRoute = require('../../src/routes/v5/full-node/mining')
const uut = new MiningRoute() const uut = new MiningRoute()
// const nock = require('nock') // HTTP mocking // const nock = require('nock') // HTTP mocking
+71
View File
@@ -0,0 +1,71 @@
/*
This library contains mocking data for running unit tests on the bcash route.
*/
const tokenInfo = {
tokenId: 'c7cb019764df3a352d9433749330b4b2eb022d8fbc101e68a6943a7a58a8ee84',
ticker: 'PSI',
name: 'Psidium',
uri: '',
hash: '',
decimals: 8
}
const utxos = [
{
version: 2,
height: 707135,
value: 546,
script: '76a91466b2156f71629c89f5bf882cb3920b0e1e4d4fa888ac',
address: 'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk',
coinbase: false,
hash: '77e514d59c0c866e791d08ddab73a1f7827be5f1ca15c1e05515dac9b271b596',
index: 2,
slp: {
vout: 2,
tokenId:
'5f2914840d0fd82689bdf0a0f8194ea363b9d0e2bd69b01608e18e1c6a4b9861',
value: '9999777700000',
type: 'SEND'
}
},
{
version: 2,
height: 707135,
value: 546,
script: '76a91466b2156f71629c89f5bf882cb3920b0e1e4d4fa888ac',
address: 'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk',
coinbase: false,
hash: '350aab06bf9523c062692f62622cb28b18cff9e4aa92be5476143336da217e98',
index: 1,
slp: {
vout: 1,
tokenId:
'2d860662801067828be3c4f504ce31b2a1a36c2cdbc6d92f6160920f66b0a9ee',
value: '1',
type: 'SEND'
}
},
{
version: 2,
height: 687335,
value: 546,
script: '76a91466b2156f71629c89f5bf882cb3920b0e1e4d4fa888ac',
address: 'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk',
coinbase: false,
hash: 'b7a0f52a344b1169292c757a36c62dbdaaa4ad684e96837eec732a9a2033dfd1',
index: 1,
slp: {
vout: 1,
tokenId:
'c7cb019764df3a352d9433749330b4b2eb022d8fbc101e68a6943a7a58a8ee84',
value: '100000000',
type: 'SEND'
}
}
]
module.exports = {
tokenInfo,
utxos
}
+22
View File
@@ -0,0 +1,22 @@
/*
This library contains mocking data for running unit tests on the dsproof route.
*/
'use strict'
const mockGetDsProof = {
dspid: '6234fff2a9dbcaa50e2c50d74e1d725a2bed1757d7a05a2db7a82df659554397',
txid: 'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e',
outpoint: {
txid: '17ebef34f7e9a0692317dd6fbb43ffccff45b7d688a2a754dbccfc5e6d93ce3e',
vout: 1
},
path: ['ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'],
descendants: [
'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'
]
}
module.exports = {
mockGetDsProof
}
+221
View File
@@ -0,0 +1,221 @@
/*
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 utxosArray = {
success: true,
utxos: [
{
utxos: [
{
height: 601861,
tx_hash:
'6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d',
tx_pos: 0,
value: 1000
}
],
address: 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'
},
{
utxos: [
{
height: 604392,
tx_hash:
'7774e449c5a3065144cefbc4c0c21e6b69c987f095856778ef9f45ddd8ae1a41',
tx_pos: 0,
value: 1000
},
{
height: 630834,
tx_hash:
'4fe60a51e0d8f5134bfd8e5f872d6e502d7f01b28a6afebb27f4438a4f638d53',
tx_pos: 0,
value: 6000
}
],
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
}
]
}
const balance = {
success: true,
balance: {
confirmed: 7000,
unconfirmed: 0
}
}
const balances = {
success: true,
balances: [
{
balance: { confirmed: 7000, unconfirmed: 0 },
address: 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'
},
{
balance: { confirmed: 7000, unconfirmed: 0 },
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
}
]
}
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 txDetailsBulk = {
success: true,
transactions: [{ details: txDetails }]
}
const blockHeaders = [
'010000008b52bbd72c2f49569059f559c1b1794de5192e4f7d6d2b03c7482bad0000000083e4f8a9d502ed0c419075c1abb5d56f878a2e9079e5612bfb76a2dc37d9c42741dd6849ffff001d2b909dd6',
'01000000f528fac1bcb685d0cd6c792320af0300a5ce15d687c7149548904e31000000004e8985a786d864f21e9cbb7cbdf4bc9265fe681b7a0893ac55a8e919ce035c2f85de6849ffff001d385ccb7c'
]
const blockHeadersBulk = {
success: true,
headers: [{ headers: blockHeaders }, { headers: blockHeaders }]
}
const transactions = [
{
height: 603416,
tx_hash: 'eef683d236d88e978bd406419f144057af3fe1b62ef59162941c1a9f05ded62c'
},
{
height: 646894,
tx_hash: '4c695fae636f3e8e2edc571d11756b880ccaae744390f3950d798ce7b5e25754'
}
]
const transactionsBulk = {
success: true,
transactions: [
{
transactions,
address: 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'
},
{
transactions,
address: 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'
}
]
}
module.exports = {
utxos,
utxosArray,
balance,
txHistory,
mempool,
txDetails,
txDetailsBulk,
blockHeaders,
blockHeadersBulk,
balances,
transactions,
transactionsBulk
}
+227
View File
@@ -0,0 +1,227 @@
/*
This library contains mocking data for running unit tests on the psf-slp-indexer route.
*/
'use strict'
const tokenStats = {
tokenData: {
type: 1,
ticker: 'TP03',
name: 'Test Plugin 03',
tokenId: '13cad617d523c8eb4ab11fff19c010e0e0a4ea4360b58e0c8c955a45a146a669',
documentUri: 'fullstack.cash',
documentHash: 'i\u00043s\u0003tz}/PǚZ>T)',
decimals: 0,
mintBatonIsActive: false,
tokensInCirculationBN: '1',
tokensInCirculationStr: '1',
blockCreated: 722420,
totalBurned: '0',
totalMinted: '1',
txs: [
{
txid: '13cad617d523c8eb4ab11fff19c010e0e0a4ea4360b58e0c8c955a45a146a669',
height: 722420,
type: 'GENESIS',
qty: '1'
}
]
}
}
// txData generated by MDA
const txDataByMDA = {
txData: {
txid: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
hash: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
version: 2,
size: 339,
locktime: 0,
vin: [
{
txid: '8370db30d94761ab9a11b71ecd22541151bf6125c8c613f0f6fab8ab794565a7',
vout: 0,
scriptSig: {
asm: '304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4[ALL|FORKID] 02791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851',
hex: '47304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4412102791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851'
},
sequence: 4294967295,
address: 'bitcoincash:qrg77j4jf2pl7azgvzrz2z567ls464gkuuhplt30dp',
value: 0.00051303,
tokenQty: 0,
tokenQtyStr: '0',
tokenId: null
}
],
vout: [
{
value: 0,
n: 0,
scriptPubKey: {
asm: 'OP_RETURN 5262419 1 47454e45534953 54524f5554 54726f75742773207465737420746f6b656e 74726f757473626c6f672e636f6d 0 2 2 000000174876e800',
hex: '6a04534c500001010747454e455349530554524f55541254726f75742773207465737420746f6b656e0e74726f757473626c6f672e636f6d4c000102010208000000174876e800',
type: 'nulldata'
},
tokenQtyStr: '0',
tokenQty: 0
}
],
hex: '0200000001a7654579abb8faf6f013c6c82561bf51115422cd1eb7119aab6147d930db7083000000006a47304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4412102791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851ffffffff040000000000000000476a04534c500001010747454e455349530554524f55541254726f75742773207465737420746f6b656e0e74726f757473626c6f672e636f6d4c000102010208000000174876e80022020000000000001976a914db4d39ceb7794ffe5d06855f249e1d3a7f1b024088ac22020000000000001976a914db4d39ceb7794ffe5d06855f249e1d3a7f1b024088accec20000000000001976a9145904159f2f69bfa63eefa712633a0d96dc2e7e8888ac00000000',
blockhash: '0000000000000000009f65225a3e12e23a7ea057c869047e0f36563a1f410267',
confirmations: 97398,
time: 1581773131,
blocktime: 1581773131,
blockheight: 622414,
isSlpTx: true,
tokenTxType: 'GENESIS',
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
tokenType: 1,
tokenTicker: 'TROUT',
tokenName: "Trout's test token",
tokenDecimals: 2,
tokenUri: 'troutsblog.com',
tokenDocHash: '',
isValidSlp: true
}
}
const txData = {
txData: {
txid: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
hash: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
version: 2,
size: 339,
locktime: 0,
vin: [
{
txid: '8370db30d94761ab9a11b71ecd22541151bf6125c8c613f0f6fab8ab794565a7',
vout: 0,
scriptSig: {
asm: '304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4[ALL|FORKID] 02791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851',
hex: '47304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4412102791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851'
},
sequence: 4294967295,
address: 'bitcoincash:qrqy3kj7r822ps6628vwqq5k8hyjl6ey3y4eea2m4s',
value: 0.00051303,
tokenQty: 0,
tokenQtyStr: '0',
tokenId: null
}
],
vout: [
{
value: 0,
n: 0,
scriptPubKey: {
asm: 'OP_RETURN 5262419 1 47454e45534953 54524f5554 54726f75742773207465737420746f6b656e 74726f757473626c6f672e636f6d 0 2 2 000000174876e800',
hex: '6a04534c500001010747454e455349530554524f55541254726f75742773207465737420746f6b656e0e74726f757473626c6f672e636f6d4c000102010208000000174876e800',
type: 'nulldata'
},
tokenQtyStr: '0',
tokenQty: 0
}
],
hex: '0200000001a7654579abb8faf6f013c6c82561bf51115422cd1eb7119aab6147d930db7083000000006a47304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4412102791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851ffffffff040000000000000000476a04534c500001010747454e455349530554524f55541254726f75742773207465737420746f6b656e0e74726f757473626c6f672e636f6d4c000102010208000000174876e80022020000000000001976a914db4d39ceb7794ffe5d06855f249e1d3a7f1b024088ac22020000000000001976a914db4d39ceb7794ffe5d06855f249e1d3a7f1b024088accec20000000000001976a9145904159f2f69bfa63eefa712633a0d96dc2e7e8888ac00000000',
blockhash: '0000000000000000009f65225a3e12e23a7ea057c869047e0f36563a1f410267',
confirmations: 97398,
time: 1581773131,
blocktime: 1581773131,
blockheight: 622414,
isSlpTx: true,
tokenTxType: 'GENESIS',
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
tokenType: 1,
tokenTicker: 'TROUT',
tokenName: "Trout's test token",
tokenDecimals: 2,
tokenUri: 'troutsblog.com',
tokenDocHash: '',
isValidSlp: true
}
}
const balance = {
balance: {
utxos: [
{
txid: 'a24a6a4abf06fabd799ecea4f8fac6a9ff21e6a8dd6169a3c2ebc03665329db9',
vout: 1,
type: 'token',
qty: '1800',
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
address: 'bitcoincash:qrqy3kj7r822ps6628vwqq5k8hyjl6ey3y4eea2m4s'
}
],
txs: [
{
txid: '078b2c48ed1db0d5d5996f2889b8d847a49200d0a781f6aa6752f740f312688f',
height: 717796
},
{
txid: 'a24a6a4abf06fabd799ecea4f8fac6a9ff21e6a8dd6169a3c2ebc03665329db9',
height: 717832
}
],
balances: [
{
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
qty: '1800'
}
]
}
}
const transactions = {
success: true,
transactions: [
{
height: 734439,
tx_hash: '0bd2a8a72108659cd39a59bde89c45fff7d51c334531f036b1e102ab4b62f33f'
},
{
height: 734441,
tx_hash: '4f2837b7fff325c0442b550863de3470e016df234561d9151cbb6949fe43ac17'
},
{
height: 735564,
tx_hash: '1bfa83355839c0ef0f974463415fa983466e0286d7bcdfdd825f244e357ad1e5'
},
{
height: 735564,
tx_hash: '6eba09627175af4b50ac75fa8b3a15a9015df8a19b5b28a992f26044f4fd8891'
},
{
height: 735564,
tx_hash: 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
}]
}
const status = {
status: {
startBlockHeight: 543376,
syncedBlockHeight: 722860,
chainBlockHeight: 722679
}
}
const immutableData = {
payloadCid: 'QmY3EaRaUcc5bNuqDfc7TaeNPThBGUefbqJeJVDjCqxqFZ',
about: 'This is a placeholder'
}
const mutableData = {
tokenIcon: 'https://gateway.ipfs.io/ipfs/bafybeiehitanirn5gmhqjg44xrmdtomn4n5lu5yjoepsvgpswk5mggaw6i/LP_logo-1.png',
about: 'Mutable data managed with npm package: https://www.npmjs.com/package/slp-mutable-data'
}
const decodedOpReturn = JSON.stringify({
mda: 'bitcoincash:qrg77j4jf2pl7azgvzrz2z567ls464gkuuhplt30dp',
cid: 'bafybeie6t5uyupddc7azms737xg4hxrj7i5t5ov3lb5g2qeehaujj6ak64'
})
module.exports = {
tokenStats,
txDataByMDA,
txData,
balance,
status,
transactions,
immutableData,
mutableData,
decodedOpReturn
}
@@ -66,6 +66,69 @@ const mockSingleToken = {
] ]
} }
const mockNftGroup = {
decimals: 0,
timestamp: '2021-05-03 10:36:01',
timestamp_unix: 1620038161,
versionType: 129,
documentUri: 'psfoundation.cash',
symbol: 'PSF.TEST.GROUP',
name: 'PSF Test NFT Group',
containsBaton: true,
id: '68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a',
documentHash: null,
initialTokenQty: 1000000,
blockCreated: 686117,
totalMinted: null,
totalBurned: null,
circulatingSupply: null
}
const mockNftChildren = [
{
decimals: 0,
timestamp: '2021-05-03 11:59:30',
timestamp_unix: 1620043170,
versionType: 65,
documentUri: 'psfoundation.cash',
symbol: 'PSF.TEST.CHILD.1',
name: 'PSF Test NFT Child #1',
containsBaton: false,
id: '45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9',
documentHash: null,
initialTokenQty: 1,
nftParentId: '68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a',
blockCreated: 686130,
totalMinted: null,
totalBurned: null,
circulatingSupply: null,
// only in axios.data.t
transactionType: 'GENESIS',
tokenIdHex: '45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
},
{
decimals: 0,
timestamp: '2021-05-03 11:59:30',
timestamp_unix: 1620043170,
versionType: 65,
documentUri: 'psfoundation.cash',
symbol: 'PSF.TEST.CHILD.2',
name: 'PSF Test NFT Child #2',
containsBaton: false,
id: '928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55',
documentHash: null,
initialTokenQty: 1,
nftParentId: '68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a',
blockCreated: 686130,
totalMinted: null,
totalBurned: null,
circulatingSupply: null,
// only in axios.data.t
transactionType: 'GENESIS',
tokenIdHex: '928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55'
}
]
const mockSingleTokenError = { const mockSingleTokenError = {
t: [] t: []
} }
@@ -1038,5 +1101,7 @@ module.exports = {
mockPsfToken, mockPsfToken,
mockValidateBulk, mockValidateBulk,
mockValidate3Bulk, mockValidate3Bulk,
mockStatus mockStatus,
mockNftGroup,
mockNftChildren
} }
+1 -1
View File
@@ -13,7 +13,7 @@ const chai = require('chai')
const assert = chai.assert const assert = chai.assert
const sinon = require('sinon') const sinon = require('sinon')
const Price = require('../../src/routes/v4/price') const Price = require('../../src/routes/v5/price')
let uut let uut
// Mocking data. // Mocking data.
+931
View File
@@ -0,0 +1,931 @@
/*
TESTS FOR THE PSF-SLP-INDEXER.JS LIBRARY
*/
'use strict'
const chai = require('chai')
const assert = chai.assert
const PsfSlpIndexerRouter = require('../../src/routes/v5/psf-slp-indexer')
const uut = new PsfSlpIndexerRouter()
// const nock = require('nock') // HTTP mocking
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/psf-slp-indexer-mocks')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
describe('#PsfSlpIndexer', () => {
let req, res
let sandbox
before(() => {
// Save existing environment variables.
originalEnvVars = {
SLP_INDEXER_API: process.env.SLP_INDEXER_API
}
// Set default environment variables for unit tests.
if (!process.env.TEST) process.env.TEST = 'unit'
if (process.env.TEST === 'unit') {
process.env.SLP_INDEXER_API = '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 = {}
sandbox = sinon.createSandbox()
})
afterEach(() => {
// Restore Sandbox
sandbox.restore()
})
after(() => {
// Restore any pre-existing environment variables.
process.env.SLP_INDEXER_API = originalEnvVars.SLP_INDEXER_API
})
describe('#root', async () => {
// root route handler.
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, 'psf-slp-indexer', 'Returns static string')
})
})
describe('#getTokenStats', async () => {
it('should throw 400 error if tokenId is missing', async () => {
const result = await uut.getTokenStats(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.include(result.error, 'tokenId can not be empty')
})
it('should throw 503 when network issues', async () => {
req.body.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
// Save the existing RPC URL.
const savedUrl2 = process.env.SLP_INDEXER_API
// Manipulate the URL to cause a 500 network error.
process.env.SLP_INDEXER_API = 'http://fakeurl/api/'
await uut.getTokenStats(req, res)
// console.log(`result: ${util.inspect(result)}`)
// Restore the saved URL.
process.env.SLP_INDEXER_API = savedUrl2
assert.isAbove(
res.statusCode,
499,
'HTTP status code 500 or greater expected.'
)
// assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
})
it('returns proper error when downstream service stalls', async () => {
req.body.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
// Mock the timeout error.
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNABORTED' })
const result = await uut.getTokenStats(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 () => {
req.body.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
// Mock the timeout error.
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNREFUSED' })
const result = await uut.getTokenStats(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 tokens stats', async function () {
req.body.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.tokenStats })
} else {
return this.skip()
}
const result = await uut.getTokenStats(req, res)
const tokenData = result.tokenData
assert.property(tokenData, 'type')
assert.property(tokenData, 'ticker')
assert.property(tokenData, 'name')
assert.property(tokenData, 'tokenId')
assert.property(tokenData, 'documentUri')
assert.property(tokenData, 'documentHash')
assert.property(tokenData, 'decimals')
assert.property(tokenData, 'mintBatonIsActive')
assert.property(tokenData, 'tokensInCirculationBN')
assert.property(tokenData, 'tokensInCirculationStr')
assert.property(tokenData, 'blockCreated')
assert.property(tokenData, 'totalBurned')
assert.property(tokenData, 'totalMinted')
assert.property(tokenData, 'txs')
})
it('should GET tokens stats with tx hisroty', async function () {
req.body.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
req.body.withTxHistory = true
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.tokenStats })
} else {
return this.skip()
}
const result = await uut.getTokenStats(req, res)
const tokenData = result.tokenData
assert.property(tokenData, 'type')
assert.property(tokenData, 'ticker')
assert.property(tokenData, 'name')
assert.property(tokenData, 'tokenId')
assert.property(tokenData, 'documentUri')
assert.property(tokenData, 'documentHash')
assert.property(tokenData, 'decimals')
assert.property(tokenData, 'mintBatonIsActive')
assert.property(tokenData, 'tokensInCirculationBN')
assert.property(tokenData, 'tokensInCirculationStr')
assert.property(tokenData, 'blockCreated')
assert.property(tokenData, 'totalBurned')
assert.property(tokenData, 'totalMinted')
assert.property(tokenData, 'txs')
})
})
describe('#getTxid', async () => {
it('should throw 400 error if txid is missing', async () => {
const result = await uut.getTxid(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.include(result.error, 'txid can not be empty')
})
it('should throw 400 error if txid is invalid format', async () => {
req.body.txid = 'txid'
const result = await uut.getTxid(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.include(result.error, 'This is not a txid')
})
it('should throw 503 when network issues', async () => {
req.body.txid =
'f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315'
// Save the existing RPC URL.
const savedUrl2 = process.env.SLP_INDEXER_API
// Manipulate the URL to cause a 500 network error.
process.env.SLP_INDEXER_API = 'http://fakeurl/api/'
await uut.getTxid(req, res)
// console.log(`result: ${util.inspect(result)}`)
// Restore the saved URL.
process.env.SLP_INDEXER_API = savedUrl2
assert.isAbove(
res.statusCode,
499,
'HTTP status code 500 or greater expected.'
)
// assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
})
it('returns proper error when downstream service stalls', async () => {
req.body.txid =
'f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315'
// Mock the timeout error.
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNABORTED' })
const result = await uut.getTxid(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 () => {
req.body.txid =
'f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315'
// Mock the timeout error.
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNREFUSED' })
const result = await uut.getTxid(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 tx data', async function () {
req.body.txid =
'f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315'
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.txData })
} else {
return this.skip()
}
const result = await uut.getTxid(req, res)
const txData = result.txData
assert.property(txData, 'txid')
assert.property(txData, 'hash')
assert.property(txData, 'version')
assert.property(txData, 'size')
assert.property(txData, 'locktime')
assert.property(txData, 'vin')
assert.property(txData, 'vout')
assert.property(txData, 'hex')
assert.property(txData, 'blockhash')
assert.property(txData, 'confirmations')
assert.property(txData, 'time')
assert.property(txData, 'blocktime')
assert.property(txData, 'blockheight')
assert.property(txData, 'isSlpTx')
assert.property(txData, 'tokenTxType')
assert.property(txData, 'tokenId')
assert.property(txData, 'tokenType')
assert.property(txData, 'tokenTicker')
assert.property(txData, 'tokenName')
assert.property(txData, 'tokenDecimals')
assert.property(txData, 'tokenUri')
assert.property(txData, 'tokenDocHash')
assert.property(txData, 'isValidSlp')
})
})
describe('#getAddress', async () => {
it('should throw 400 error if address is missing', async () => {
const result = await uut.getAddress(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.include(result.error, 'address can not be empty')
})
it('should throw 400 error if address is invalid format', async () => {
req.body.address = 'address'
const result = await uut.getAddress(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.include(
result.error,
'Invalid BCH address. Double check your address is valid'
)
})
it('should throw 400 error for invalid network address', async () => {
req.body.address = 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4'
const result = await uut.getAddress(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.include(
result.error,
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
)
})
it('should throw 503 when network issues', async () => {
req.body.address =
'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n'
// Save the existing RPC URL.
const savedUrl2 = process.env.SLP_INDEXER_API
// Manipulate the URL to cause a 500 network error.
process.env.SLP_INDEXER_API = 'http://fakeurl/api/'
await uut.getAddress(req, res)
// console.log(`result: ${util.inspect(result)}`)
// Restore the saved URL.
process.env.SLP_INDEXER_API = savedUrl2
assert.isAbove(
res.statusCode,
499,
'HTTP status code 500 or greater expected.'
)
// assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
})
it('returns proper error when downstream service stalls', async () => {
req.body.address =
'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n'
// Mock the timeout error.
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNABORTED' })
const result = await uut.getAddress(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 () => {
req.body.address =
'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n'
// Mock the timeout error.
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNREFUSED' })
const result = await uut.getAddress(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 address data', async function () {
req.body.address =
'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n'
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.balance })
} else {
return this.skip()
}
const result = await uut.getAddress(req, res)
const balance = result.balance
assert.property(balance, 'utxos')
assert.property(balance, 'txs')
assert.property(balance, 'balances')
assert.isArray(balance.utxos)
assert.isArray(balance.txs)
assert.isArray(balance.balances)
})
it('should GET data for ecash address', async function () {
req.body.address =
'ecash:qr5c4hfy52zn87484cucvzle5pljz0gtr5vhtw9z09'
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.balance })
} else {
return this.skip()
}
const result = await uut.getAddress(req, res)
const balance = result.balance
assert.property(balance, 'utxos')
assert.property(balance, 'txs')
assert.property(balance, 'balances')
assert.isArray(balance.utxos)
assert.isArray(balance.txs)
assert.isArray(balance.balances)
})
})
describe('#getStatus', async () => {
it('should throw 503 when network issues', async () => {
// Save the existing RPC URL.
const savedUrl2 = process.env.SLP_INDEXER_API
// Manipulate the URL to cause a 500 network error.
process.env.SLP_INDEXER_API = 'http://fakeurl/api/'
await uut.getStatus(req, res)
// console.log(`result: ${util.inspect(result)}`)
// Restore the saved URL.
process.env.SLP_INDEXER_API = savedUrl2
assert.isAbove(
res.statusCode,
499,
'HTTP status code 500 or greater expected.'
)
// assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
})
it('returns proper error when downstream service stalls', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'get').throws({ code: 'ECONNABORTED' })
const result = await uut.getStatus(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, 'get').throws({ code: 'ECONNREFUSED' })
const result = await uut.getStatus(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 indexer status', async function () {
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(uut.axios, 'get').resolves({ data: mockData.status })
} else {
return this.skip()
}
const result = await uut.getStatus(req, res)
const status = result.status
assert.property(status, 'startBlockHeight')
assert.property(status, 'syncedBlockHeight')
assert.property(status, 'chainBlockHeight')
})
})
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.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
assert.property(result, 'error')
})
it('should handle unknow errors', () => {
sandbox.stub(uut.routeUtils, 'decodeError').returns({ msg: false })
const result = uut.errorHandler({}, res)
// console.log('result: ', result)
assert.equal(res.statusCode, 500, 'HTTP status code 500 expected.')
assert.property(result, 'error')
})
})
describe('#checkEnvVar', () => {
it('should throw errors if SLP_INDEXER_API env var is not provided', () => {
const savedSlpIndexerUrl = process.env.SLP_INDEXER_API
try {
process.env.SLP_INDEXER_API = ''
uut.checkEnvVar()
assert.fail('unexpected code path')
} catch (error) {
assert.include(
error.message,
'SLP_INDEXER_API env var not set. Can not connect to PSF SLP indexer.',
'Error message expected'
)
}
process.env.SLP_INDEXER_API = savedSlpIndexerUrl
})
})
describe('#getCIDData', () => {
it('should throw errors if cid is not provided', async () => {
try {
await uut.getCIDData()
assert.fail('unexpected code path')
} catch (error) {
assert.include(
error.message,
'cid must be a string.',
'Error message expected'
)
}
})
it('should handle axios error', async () => {
try {
sandbox.stub(uut.axios, 'get').throws(new Error('test error'))
const cid = 'bafybeigp3bfmj6woms7pywb7s7r6npcdudvsabvzne2chyspxtdendrwmy'
await uut.getCIDData(cid)
assert.fail('unexpected code path')
} catch (error) {
assert.include(
error.message,
'test error',
'Error message expected'
)
}
})
it('should return cid object data', async () => {
sandbox.stub(uut.axios, 'get').resolves({ data: mockData.immutableData })
const cid = 'bafybeigp3bfmj6woms7pywb7s7r6npcdudvsabvzne2chyspxtdendrwmy'
const result = await uut.getCIDData(cid)
assert.isObject(result)
})
})
describe('#decodeOpReturn', () => {
it('should throw errors if txid is not provided', async () => {
try {
await uut.decodeOpReturn()
assert.fail('unexpected code path')
} catch (error) {
assert.include(
error.message,
'txid must be a string.',
'Error message expected'
)
}
})
it('should handle bchjs error', async () => {
try {
sandbox.stub(uut.bchjs.Electrumx, 'txData').throws(new Error('test error'))
const txid = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
await uut.decodeOpReturn(txid)
assert.fail('unexpected code path')
} catch (error) {
assert.include(
error.message,
'test error',
'Error message expected'
)
}
})
it('should return data', async () => {
sandbox.stub(uut.bchjs.Electrumx, 'txData').resolves({ details: mockData.txData.txData })
const txid = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
const result = await uut.decodeOpReturn(txid)
assert.isString(result)
})
it('should return false if data is not found', async () => {
sandbox.stub(uut.bchjs.Electrumx, 'txData').resolves({ details: { vout: [] } })
const txid = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
const result = await uut.decodeOpReturn(txid)
assert.isFalse(result)
})
})
describe('#getTokenData', async () => {
it('should throw 400 error if tokenId is missing', async () => {
const result = await uut.getTokenData(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.include(result.error, 'tokenId can not be empty')
})
it('should throw 503 when network issues', async () => {
req.body.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
// Save the existing RPC URL.
const savedUrl2 = process.env.SLP_INDEXER_API
// Manipulate the URL to cause a 500 network error.
process.env.SLP_INDEXER_API = 'http://fakeurl/api/'
await uut.getTokenData(req, res)
// console.log(`result: ${util.inspect(result)}`)
// Restore the saved URL.
process.env.SLP_INDEXER_API = savedUrl2
assert.isAbove(
res.statusCode,
499,
'HTTP status code 500 or greater expected.'
)
// assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
})
it('returns proper error when downstream service stalls', async () => {
req.body.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
// Mock the timeout error.
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNABORTED' })
const result = await uut.getTokenData(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 () => {
req.body.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
// Mock the timeout error.
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNREFUSED' })
const result = await uut.getTokenData(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 tokens data', async function () {
req.body.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.tokenStats })
sandbox.stub(uut, 'getCIDData').resolves(mockData.immutableData)
sandbox.stub(uut, 'getMutableData').resolves(mockData.mutableData)
} else {
return this.skip()
}
const result = await uut.getTokenData(req, res)
assert.property(result, 'genesisData')
assert.property(result, 'immutableData')
assert.property(result, 'mutableData')
assert.isObject(result.genesisData)
// assert.isObject(result.immutableData)
// assert.isObject(result.mutableData)
const genesisData = result.genesisData
assert.property(genesisData, 'ticker')
assert.property(genesisData, 'name')
assert.property(genesisData, 'type')
assert.property(genesisData, 'tokenId')
assert.property(genesisData, 'documentUri')
assert.property(genesisData, 'documentHash')
assert.property(genesisData, 'decimals')
assert.property(genesisData, 'mintBatonIsActive')
assert.property(genesisData, 'tokensInCirculationBN')
assert.property(genesisData, 'tokensInCirculationStr')
assert.property(genesisData, 'blockCreated')
assert.property(genesisData, 'totalBurned')
assert.property(genesisData, 'totalMinted')
assert.property(genesisData, 'txs')
})
it('should GET tokens data if immutableData data not found', async function () {
req.body.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.tokenStats })
sandbox.stub(uut, 'getCIDData').throws(new Error('test error'))
sandbox.stub(uut, 'getMutableData').resolves(mockData.mutableData)
} else {
return this.skip()
}
const result = await uut.getTokenData(req, res)
assert.property(result, 'genesisData')
assert.property(result, 'immutableData')
assert.property(result, 'mutableData')
assert.isObject(result.genesisData)
assert.isObject(result.mutableData)
// assert.equal(result.immutableData, '')
const genesisData = result.genesisData
assert.property(genesisData, 'ticker')
assert.property(genesisData, 'name')
assert.property(genesisData, 'type')
assert.property(genesisData, 'tokenId')
assert.property(genesisData, 'documentUri')
assert.property(genesisData, 'documentHash')
assert.property(genesisData, 'decimals')
assert.property(genesisData, 'mintBatonIsActive')
assert.property(genesisData, 'tokensInCirculationBN')
assert.property(genesisData, 'tokensInCirculationStr')
assert.property(genesisData, 'blockCreated')
assert.property(genesisData, 'totalBurned')
assert.property(genesisData, 'totalMinted')
assert.property(genesisData, 'txs')
})
it('should GET tokens data if mutable data not found', async function () {
req.body.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.tokenStats })
sandbox.stub(uut, 'getCIDData').resolves(mockData.immutableData)
sandbox.stub(uut, 'getMutableData').throws(new Error('test error'))
} else {
return this.skip()
}
const result = await uut.getTokenData(req, res)
assert.property(result, 'genesisData')
assert.property(result, 'immutableData')
assert.property(result, 'mutableData')
assert.isObject(result.genesisData)
// assert.isObject(result.immutableData)
// assert.equal(result.mutableData, '')
const genesisData = result.genesisData
assert.property(genesisData, 'ticker')
assert.property(genesisData, 'name')
assert.property(genesisData, 'type')
assert.property(genesisData, 'tokenId')
assert.property(genesisData, 'documentUri')
assert.property(genesisData, 'documentHash')
assert.property(genesisData, 'decimals')
assert.property(genesisData, 'mintBatonIsActive')
assert.property(genesisData, 'tokensInCirculationBN')
assert.property(genesisData, 'tokensInCirculationStr')
assert.property(genesisData, 'blockCreated')
assert.property(genesisData, 'totalBurned')
assert.property(genesisData, 'totalMinted')
assert.property(genesisData, 'txs')
})
})
describe('#getMutableData', () => {
it('should throw errors if documentHash is not provided', async () => {
try {
await uut.getMutableData()
assert.fail('unexpected code path')
} catch (error) {
assert.include(
error.message,
'documentHash string required when calling mutableData().',
'Error message expected'
)
}
})
it('should throw errors if cid is not provided in OP Return', async () => {
try {
sandbox.stub(uut, 'decodeOpReturn').resolves('{}')
sandbox.stub(uut.bchjs.Electrumx, 'transactions').resolves(mockData.transactions)
sandbox.stub(uut, 'getCIDData').resolves(mockData.mutableData)
const documentHash = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
await uut.getMutableData(documentHash)
assert.fail('unexpected code path')
} catch (error) {
assert.include(
error.message,
'CID could not be found in OP_RETURN data',
'Error message expected'
)
}
})
it('should throw errors if data is not found', async () => {
try {
sandbox.stub(uut, 'decodeOpReturn').resolves(mockData.decodedOpReturn)
sandbox.stub(uut.bchjs.Electrumx, 'transactions').resolves({ transactions: [] })
sandbox.stub(uut, 'getCIDData').resolves(mockData.mutableData)
const documentHash = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
await uut.getMutableData(documentHash)
assert.fail('unexpected code path')
} catch (error) {
assert.include(
error.message,
'CID could not be found in OP_RETURN data',
'Error message expected'
)
}
})
it('should handle bchjs error', async () => {
try {
sandbox.stub(uut, 'decodeOpReturn').resolves(mockData.decodedOpReturn)
sandbox.stub(uut.bchjs.Electrumx, 'transactions').throws(new Error('test error'))
const documentHash = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
await uut.getMutableData(documentHash)
assert.fail('unexpected code path')
} catch (error) {
assert.include(
error.message,
'test error',
'Error message expected'
)
}
})
it('should skip mutable data if was not generated by the MDA private key', async () => {
try {
sandbox.stub(uut, 'decodeOpReturn').resolves(mockData.decodedOpReturn)
sandbox.stub(uut.bchjs.Electrumx, 'transactions').resolves(mockData.transactions)
sandbox.stub(uut.bchjs.Transaction, 'get').resolves(mockData.txData)
const documentHash = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
await uut.getMutableData(documentHash)
assert.fail('unexpected code path')
} catch (error) {
assert.include(
error.message,
'CID could not be found in OP_RETURN data',
'Error message expected'
)
}
})
it('should return mutable data', async () => {
sandbox.stub(uut, 'decodeOpReturn')
.onFirstCall().resolves(mockData.decodedOpReturn)
.onSecondCall().resolves(JSON.parse(mockData.decodedOpReturn)) // data to force an error while parsing the JSON
.onThirdCall().resolves(mockData.decodedOpReturn)
sandbox.stub(uut.bchjs.Electrumx, 'transactions').resolves(mockData.transactions)
sandbox.stub(uut.bchjs.Transaction, 'get').resolves(mockData.txDataByMDA)
sandbox.stub(uut, 'getCIDData').resolves(mockData.mutableData)
const documentHash = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
const result = await uut.getMutableData(documentHash)
assert.isString(result)
})
})
})
@@ -204,6 +204,9 @@ describe('#rate-routelimit', () => {
describe('#trackRateLimits', () => { describe('#trackRateLimits', () => {
it('should apply anonymous rate limits if no JWT token is provided', async () => { it('should apply anonymous rate limits if no JWT token is provided', async () => {
// Mock sleep function
sandbox.stub(uut, 'sleep').resolves()
req.ip = '127.0.0.1' req.ip = '127.0.0.1'
const result = await uut.trackRateLimits(req, res) const result = await uut.trackRateLimits(req, res)
@@ -278,6 +281,9 @@ describe('#rate-routelimit', () => {
}) })
it('should apply rate limits to anonymous users', async () => { it('should apply rate limits to anonymous users', async () => {
// Mock sleep function
sandbox.stub(uut, 'sleep').resolves()
req.ip = '123.456.7.8' req.ip = '123.456.7.8'
// console.log('next.callCount: ', next.callCount) // console.log('next.callCount: ', next.callCount)
@@ -302,6 +308,9 @@ describe('#rate-routelimit', () => {
}) })
it('should return 429 error when anonymous users exceed rate limit', async () => { it('should return 429 error when anonymous users exceed rate limit', async () => {
// Mock sleep function
sandbox.stub(uut, 'sleep').resolves()
req.ip = '123.456.7.8' req.ip = '123.456.7.8'
// force req.locals.jwtToken to be empty. // force req.locals.jwtToken to be empty.
@@ -317,7 +326,7 @@ describe('#rate-routelimit', () => {
assert.property(val, 'error') assert.property(val, 'error')
assert.include( assert.include(
val.error, val.error,
'Too many requests. Your limits are currently 20 requests per minute.' 'Too many requests. Your limits are currently'
) )
assert.equal(res.locals.rateLimitTriggered, true, 'Rate limits triggered') assert.equal(res.locals.rateLimitTriggered, true, 'Rate limits triggered')
@@ -14,7 +14,7 @@
const chai = require('chai') const chai = require('chai')
const assert = chai.assert const assert = chai.assert
const Rawtransactions = require('../../src/routes/v4/full-node/rawtransactions') const Rawtransactions = require('../../src/routes/v5/full-node/rawtransactions')
const uut = new Rawtransactions() const uut = new Rawtransactions()
// const nock = require('nock') // HTTP mocking // const nock = require('nock') // HTTP mocking
@@ -216,7 +216,7 @@ describe('#Raw-Transactions', () => {
it('should throw 400 error if hexes array is too large', async () => { it('should throw 400 error if hexes array is too large', async () => {
const testArray = [] const testArray = []
for (var i = 0; i < 25; i++) testArray.push('') for (let i = 0; i < 25; i++) testArray.push('')
req.body.hexes = testArray req.body.hexes = testArray
@@ -250,6 +250,7 @@ describe('#Raw-Transactions', () => {
'Proper error message' 'Proper error message'
) )
}) })
it('returns proper error when downstream service stalls', async () => { it('returns proper error when downstream service stalls', async () => {
req.body.hexes = [ req.body.hexes = [
'0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000' '0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000'
@@ -458,7 +459,7 @@ describe('#Raw-Transactions', () => {
it('should throw 400 error if hexes array is too large', async () => { it('should throw 400 error if hexes array is too large', async () => {
const testArray = [] const testArray = []
for (var i = 0; i < 25; i++) testArray.push('') for (let i = 0; i < 25; i++) testArray.push('')
req.body.hexes = testArray req.body.hexes = testArray
@@ -598,7 +599,7 @@ describe('#Raw-Transactions', () => {
it('should throw 400 error if txids array is too large', async () => { it('should throw 400 error if txids array is too large', async () => {
const testArray = [] const testArray = []
for (var i = 0; i < 25; i++) testArray.push('') for (let i = 0; i < 25; i++) testArray.push('')
req.body.txids = testArray req.body.txids = testArray
@@ -884,7 +885,7 @@ describe('#Raw-Transactions', () => {
it('should throw 400 error if hexs array is too large', async () => { it('should throw 400 error if hexs array is too large', async () => {
const testArray = [] const testArray = []
for (var i = 0; i < 25; i++) testArray.push('') for (let i = 0; i < 25; i++) testArray.push('')
req.body.hexes = testArray req.body.hexes = testArray
@@ -1046,10 +1047,12 @@ describe('#Raw-Transactions', () => {
// Save the existing RPC URL. // Save the existing RPC URL.
const savedUrl = process.env.BITCOINCOM_BASEURL const savedUrl = process.env.BITCOINCOM_BASEURL
const savedUrl2 = process.env.RPC_BASEURL const savedUrl2 = process.env.RPC_BASEURL
const savedUrl3 = process.env.RPC_SENDURL
// Manipulate the URL to cause a 500 network error. // Manipulate the URL to cause a 500 network error.
process.env.BITCOINCOM_BASEURL = 'http://fakeurl/api/' process.env.BITCOINCOM_BASEURL = 'http://fakeurl/api/'
process.env.RPC_BASEURL = 'http://fakeurl/api/' process.env.RPC_BASEURL = 'http://fakeurl/api/'
process.env.RPC_SENDURL = 'http://fakeurl/api/'
req.params.hex = req.params.hex =
'020000000136697692fed77bc4f5b6885295d0c56d1d0280fb578f445ce42be4eb6db381f2010000006a4730440220473adba0e7da14f0abf4817bbd591741ecb8da6544b998f10341f6704f5f05280220405221c626cb7edcf333367ebd469aff3f5a2169e37ee58eebb811ffc2fbc9e0412102202ff86325c5d903171fa5a2895c4efb3765105115460dc96f113048ddb69b47feffffff027a621b00000000001976a914e0a8ffc3b91e35f46618d6db90f66397989abf0588ac38041300000000001976a914a741f282af390bc7ea8c4375a3a56401d668564288ac2c330900' '020000000136697692fed77bc4f5b6885295d0c56d1d0280fb578f445ce42be4eb6db381f2010000006a4730440220473adba0e7da14f0abf4817bbd591741ecb8da6544b998f10341f6704f5f05280220405221c626cb7edcf333367ebd469aff3f5a2169e37ee58eebb811ffc2fbc9e0412102202ff86325c5d903171fa5a2895c4efb3765105115460dc96f113048ddb69b47feffffff027a621b00000000001976a914e0a8ffc3b91e35f46618d6db90f66397989abf0588ac38041300000000001976a914a741f282af390bc7ea8c4375a3a56401d668564288ac2c330900'
@@ -1059,6 +1062,7 @@ describe('#Raw-Transactions', () => {
// Restore the saved URL. // Restore the saved URL.
process.env.BITCOINCOM_BASEURL = savedUrl process.env.BITCOINCOM_BASEURL = savedUrl
process.env.RPC_BASEURL = savedUrl2 process.env.RPC_BASEURL = savedUrl2
process.env.RPC_SENDURL = savedUrl3
assert.isAbove( assert.isAbove(
res.statusCode, res.statusCode,
+274
View File
@@ -0,0 +1,274 @@
/*
TESTS FOR THE SLP.TS 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.
TODO:
-See listSingleToken() tests.
*/
'use strict'
const chai = require('chai')
const assert = chai.assert
const sinon = require('sinon')
// const proxyquire = require('proxyquire').noPreserveCache()
// const axios = require('axios')
// Save existing environment variables.
// Used during transition from integration to unit tests.
// eslint-disable-next-line no-unused-vars
let mockServerUrl
const originalEnvVars = {
BITDB_URL: process.env.BITDB_URL,
BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL,
SLPDB_URL: process.env.SLPDB_URL
}
// Set default environment variables for unit tests.
if (!process.env.TEST) process.env.TEST = 'unit'
// Block network connections for unit tests.
if (process.env.TEST === 'unit') {
process.env.BITDB_URL = 'http://fakeurl/'
process.env.BITCOINCOM_BASEURL = 'http://fakeurl/'
process.env.SLPDB_URL = 'http://fakeurl/'
// mockServerUrl = 'http://fakeurl'
}
// Prepare the slpRoute for stubbing dependcies on slpjs.
const SlpRoute = require('../../src/routes/v5/slp')
const slpRoute = new SlpRoute()
// const pathStub = {} // Used to stub methods within slpjs.
// const slpRouteStub = proxyquire('../../src/routes/v5/slp', { slpjs: pathStub })
// Mocking data.
const { mockReq, mockRes } = require('./mocks/express-mocks')
// const mockData = require('./mocks/slp-mocks')
// const slpjsMock = require('./mocks/slpjs-mocks')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
describe('#SLP', () => {
let req, res
let sandbox
before(() => {
})
// 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 = {}
req.locals = {}
sandbox = sinon.createSandbox()
})
afterEach(() => {
sandbox.restore()
})
after(() => {
// Restore any pre-existing environment variables.
process.env.BITDB_URL = originalEnvVars.BITDB_URL
process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL
process.env.SLPDB_URL = originalEnvVars.SLPDB_URL
})
describe('#root', async () => {
// root route handler.
const root = slpRoute.root
it('should respond to GET for base route', async () => {
const result = root(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(result.status, 'slp', 'Returns static string')
})
})
describe('#validate2Single', () => {
it('should throw 400 if txid is empty', async () => {
req.params.txid = ''
const result = await slpRoute.validate2Single(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'txid can not be empty')
})
/*
it('should invalidate a known invalid TXID', async () => {
if (process.env.TEST === 'unit') {
// Mock to prevent live network connection.
sandbox
.stub(slpRoute.axios, 'request')
.resolves({ data: { isValid: false } })
}
const txid =
'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a'
req.params.txid = txid
const result = await slpRoute.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 () => {
if (process.env.TEST === 'unit') {
// Mock to prevent live network connection.
sandbox
.stub(slpRoute.axios, 'request')
.resolves({ data: { isValid: true } })
}
const txid =
'3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488'
req.params.txid = txid
const result = await slpRoute.validate2Single(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result.txid, txid)
assert.equal(result.isValid, true)
})
*/
// This test can only be run as a mocked unit test. It's too inconsistent
// to run as an integration test, due to the caching built into slp-validate.
if (process.env.TEST === 'unit') {
it('should cancel if validation takes too long', async () => {
// Mock the timeout error.
sandbox.stub(slpRoute.axios, 'request').throws({
code: 'ECONNABORTED'
})
const txid =
'eacb1085dfa296fef6d4ae2c0f4529a1bef096dd2325bdcc6dcb5241b3bdb579'
req.params.txid = txid
const result = await slpRoute.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'
)
})
}
})
describe('#generateSendOpReturn()', () => {
const generateSendOpReturn = slpRoute.generateSendOpReturn
// Validate tokenUtxos input
it('should throw 400 if tokenUtxos is missing', async () => {
const result = await generateSendOpReturn(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'tokenUtxos needs to be an array.')
})
it('should throw 400 if tokenUtxos is empty', async () => {
req.body.tokenUtxos = ''
const result = await generateSendOpReturn(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'tokenUtxos needs to be an array.')
})
it('should throw 400 if tokenUtxos is not array', async () => {
req.body.tokenUtxos = 'tokenUtxos'
const result = await generateSendOpReturn(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'tokenUtxos needs to be an array.')
})
it('should throw 400 if tokenUtxos is empty array', async () => {
req.body.tokenUtxos = []
const result = await generateSendOpReturn(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'tokenUtxos array can not be empty.')
})
// Validate sendQty input
it('should throw 400 if sendQty is missing', async () => {
req.body.tokenUtxos = [{}, {}, {}]
const result = await generateSendOpReturn(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'sendQty must be a number')
})
it('should throw 400 if sendQty is empty', async () => {
req.body.tokenUtxos = [{}, {}, {}]
req.body.sendQty = ''
const result = await generateSendOpReturn(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'sendQty must be a number')
})
it('should throw 400 if sendQty is not a number', async () => {
req.body.tokenUtxos = [{}, {}, {}]
req.body.sendQty = 'sendQty'
const result = await generateSendOpReturn(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'sendQty must be a number')
})
it('should return OP_RETURN script', async () => {
req.body.tokenUtxos = [
{
tokenId: '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
decimals: 8,
tokenQty: 2
}
]
req.body.sendQty = 1.5
if (process.env.TEST === 'unit') {
sandbox
.stub(slpRoute.bchjs.SLP.TokenType1, 'generateSendOpReturn')
.resolves({ script: '<Buffer ', outputs: 2 })
}
const result = await generateSendOpReturn(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['script', 'outputs'])
assert.isNumber(result.outputs)
})
})
})
+7 -214
View File
@@ -19,8 +19,8 @@ const nock = require('nock') // HTTP mocking
const sinon = require('sinon') const sinon = require('sinon')
// Local libraries // Local libraries
const Electrumx = require('../../src/routes/v4/electrumx') // const Electrumx = require('../../src/routes/v5/electrumx')
const UtilRoute = require('../../src/routes/v4/util') const UtilRoute = require('../../src/routes/v5/util')
let originalEnvVars // Used during transition from integration to unit tests. let originalEnvVars // Used during transition from integration to unit tests.
@@ -38,8 +38,8 @@ const utilRouteInst = new UtilRoute({ electrumx: {} })
describe('#Util', () => { describe('#Util', () => {
let req, res let req, res
let sandbox let sandbox
let electrumx // let electrumx
let utilRoute // let utilRoute
before(async () => { before(async () => {
// Save existing environment variables. // Save existing environment variables.
@@ -59,7 +59,7 @@ describe('#Util', () => {
process.env.RPC_PASSWORD = 'fakepassword' process.env.RPC_PASSWORD = 'fakepassword'
} }
electrumx = new Electrumx() // electrumx = new Electrumx()
// await electrumx.connect() // await electrumx.connect()
}) })
@@ -79,7 +79,7 @@ describe('#Util', () => {
sandbox = sinon.createSandbox() sandbox = sinon.createSandbox()
utilRoute = new UtilRoute({ electrumx }) // utilRoute = new UtilRoute({ electrumx })
}) })
afterEach(() => { afterEach(() => {
@@ -203,7 +203,7 @@ describe('#Util', () => {
it('should throw 400 error if addresses array is too large', async () => { it('should throw 400 error if addresses array is too large', async () => {
const testArray = [] const testArray = []
for (var i = 0; i < 25; i++) testArray.push('') for (let i = 0; i < 25; i++) testArray.push('')
req.body.addresses = testArray req.body.addresses = testArray
@@ -322,211 +322,4 @@ describe('#Util', () => {
]) ])
}) })
}) })
describe('#sweepWif', () => {
it('should throw 400 if WIF is not included', async () => {
req.body = {}
const result = await utilRouteInst.sweepWif(req, res)
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
assert.include(
result.error,
'WIF needs to a proper compressed WIF starting with K or L',
'Proper error message'
)
})
it('should throw 400 if WIF is malformed', async () => {
req.body = {
wif: 'abc123'
}
const result = await utilRouteInst.sweepWif(req, res)
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
assert.include(
result.error,
'WIF needs to a proper compressed WIF starting with K or L',
'Proper error message'
)
})
it('should throw 400 if destination address is not included', async () => {
req.body = {
wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt'
}
const result = await utilRouteInst.sweepWif(req, res)
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
assert.include(
result.error,
'address can not be empty',
'Proper error message'
)
})
// Unit test only.
if (process.env.TEST === 'unit') {
it('should generate transaction for valid token sweep', async () => {
// Mock the RPC call for unit tests.
sandbox
.stub(utilRoute.electrumx, '_balanceFromElectrumx')
.resolves(mockData.mockBalance)
sandbox
.stub(utilRoute.electrumx, '_utxosFromElectrumx')
.resolves(mockData.mockUtxos)
sandbox
.stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails')
.resolves(mockData.mockIsTokenUtxos)
// Mock sendRawTransaction() so that the hex does not actually get broadcast
// to the network.
sandbox
.stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction')
.resolves('test-txid')
req.body = {
wif: 'L1wAGEN721LHDoiN8pLwwBb87bYrU6Gs21UPcCRR7LjKypQyVaCq',
toAddr: 'bitcoincash:qp2g4cnekxsjspccmtvh5k73mczz6273js4mjr353r'
}
const result = await utilRoute.sweepWif(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result, 'test-txid')
})
it('should return balance if balance-only is true', async () => {
// Mock the RPC call for unit tests.
sandbox
.stub(utilRoute.electrumx, '_balanceFromElectrumx')
.resolves(mockData.mockBalance)
// Mock sendRawTransaction() so that the hex does not actually get broadcast
// to the network.
sandbox
.stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction')
.resolves('test-txid')
req.body = {
wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt',
balanceOnly: true
}
const result = await utilRoute.sweepWif(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isNumber(result)
})
it('should generate transaction for valid BCH-only sweep', async () => {
sandbox
.stub(utilRoute.electrumx, '_balanceFromElectrumx')
.resolves(mockData.mockBalance)
sandbox
.stub(utilRoute.electrumx, '_utxosFromElectrumx')
.resolves(mockData.mockUtxos)
// Force token utxo to appear as regular BCH utxo.
sandbox
.stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails')
.resolves([false, false])
// Mock sendRawTransaction() so that the hex does not actually get broadcast
// to the network.
sandbox
.stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction')
.resolves('test-txid')
req.body = {
wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt',
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
}
const result = await utilRoute.sweepWif(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result, 'test-txid')
})
it('should throw 422 error if no non-token UTXOs', async () => {
sandbox
.stub(utilRoute.electrumx, '_balanceFromElectrumx')
.resolves(mockData.mockBalance)
sandbox
.stub(utilRoute.electrumx, '_utxosFromElectrumx')
.resolves(mockData.mockUtxos)
// Force token utxo to appear as regular BCH utxo.
sandbox
.stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails')
.resolves(mockData.tokensOnly)
// Mock sendRawTransaction() so that the hex does not actually get broadcast
// to the network.
sandbox
.stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction')
.resolves('test-txid')
req.body = {
wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt',
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
}
const result = await utilRoute.sweepWif(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(res.statusCode, 422)
assert.property(result, 'error')
assert.include(
result.error,
'Tokens found, but no BCH UTXOs found. Add BCH to wallet to move tokens'
)
})
it('should detect and throw error for multiple token classes', async () => {
sandbox
.stub(utilRoute.electrumx, '_balanceFromElectrumx')
.resolves(mockData.mockBalance)
sandbox
.stub(utilRoute.electrumx, '_utxosFromElectrumx')
.resolves(mockData.mockThreeUtxos)
// Force token utxo to appear as regular BCH utxo.
sandbox
.stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails')
.resolves(mockData.multipleTokens)
// Mock sendRawTransaction() so that the hex does not actually get broadcast
// to the network.
sandbox
.stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction')
.resolves('test-txid')
req.body = {
wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt',
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
}
const result = await utilRoute.sweepWif(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(res.statusCode, 422)
assert.property(result, 'error')
assert.include(
result.error,
'Multiple token classes detected. This function only supports a single class of token'
)
})
}
})
}) })
+3 -2
View File
@@ -10,7 +10,7 @@
const chai = require('chai') const chai = require('chai')
const assert = chai.assert const assert = chai.assert
const xpubRoute = require('../../src/routes/v4/xpub') const xpubRoute = require('../../src/routes/v5/xpub')
const nock = require('nock') // HTTP mocking const nock = require('nock') // HTTP mocking
// let originalUrl // Used during transition from integration to unit tests. // let originalUrl // Used during transition from integration to unit tests.
@@ -119,7 +119,8 @@ describe('#XPUBRouter', () => {
}) })
*/ */
it('should create an address from xpub', async () => { it('should create an address from xpub', async () => {
req.params.xpub = 'tpubDHTK2jqg73w3GwoiHfAMbMYML1HN8FhrUxD9rFgbSgHXdwwrY6pAFqKDfUHhqw7vreaZty5hPGjb1S7ZPQeMmu6TFHAKfY9tJpYbvaGjPRM' req.params.xpub =
'tpubDHTK2jqg73w3GwoiHfAMbMYML1HN8FhrUxD9rFgbSgHXdwwrY6pAFqKDfUHhqw7vreaZty5hPGjb1S7ZPQeMmu6TFHAKfY9tJpYbvaGjPRM'
// Mock the Insight URL for unit tests. // Mock the Insight URL for unit tests.
// TODO add unit test // TODO add unit test