Compare commits

...
220 Commits
Author SHA1 Message Date
Chris Troutner ac83827fca Merge branch 'master' into fullstack 2025-12-04 15:29:13 +00:00
Chris Troutner 0811024030 Merge pull request #123 from Permissionless-Software-Foundation/dh-token-data
feat(offer): Added tokenData to offer model
2025-12-04 08:08:45 -07:00
Daniel Gonzalez 737d35463d feat(offer): Added tokenData to offer model 2025-12-03 19:21:40 -04:00
Chris Troutner 2ed1eaba9c Merge branch 'master' into fullstack 2025-11-26 14:16:56 +00:00
Chris Troutner 6dddfbdcb6 Merge pull request #122 from Permissionless-Software-Foundation/dh-ctr-offr-page
feat(counterOffer): Added new properties to counter offer model
2025-11-25 18:59:41 -08:00
Daniel Gonzalez fc2d7b4b9b feat(counterOffer): Added new properties to counter offer model 2025-11-25 20:57:44 -04:00
Chris Troutner c877898ae7 Merge pull request #121 from Permissionless-Software-Foundation/dh-track-counter-offrs
feat(offers): Track Counter Offers
2025-11-21 15:54:45 -08:00
Daniel Gonzalez 88e27f47f5 feat(offers): Track Counter Offers 2025-11-21 17:34:05 -04:00
Chris Troutner 9681def3e0 Merge branch 'master' into fullstack 2025-11-14 19:24:38 +00:00
Chris Troutner 0057007b4c Merge pull request #120 from Permissionless-Software-Foundation/dh-npub-offer
feat(nostr): Added npub to Offer model
2025-11-13 06:28:42 -08:00
Daniel Gonzalez 8aad93e572 feat(nostr): Added npub to Offer model 2025-11-11 14:48:09 -04:00
Chris Troutner fa0ec3ece2 Merge pull request #119 from Permissionless-Software-Foundation/dh-npub-order
feat(nostr): Added npub to order
2025-11-11 06:56:18 -08:00
Daniel Gonzalez 4f9f2f8370 feat(nostr): Added npub to order 2025-11-10 18:45:37 -04:00
Chris Troutner 0fb93be2cd Merge pull request #118 from Permissionless-Software-Foundation/dh-timer-deleted
feat(chats): Added timer to delete old deleted chat and post database models
2025-11-06 10:58:25 -08:00
Daniel Gonzalez 26dcd6072e feat(chats): Added timer to delete old deleted chat and post database models 2025-11-06 11:42:46 -04:00
Chris Troutner 42f228af0e Merge branch 'master' into fullstack 2025-11-05 18:50:34 +00:00
Chris Troutner 8be70baff2 Merge pull request #117 from Permissionless-Software-Foundation/dh-nostr-route
feat(nostr): Added new route for deleteChat and deletePost models
2025-10-29 09:02:51 -07:00
Daniel Gonzalez 7e5c314000 feat(nostr): Added new route for deleteChat and deletePost models 2025-10-28 19:18:18 -04:00
Chris Troutner 9669112d69 Merge pull request #116 from Permissionless-Software-Foundation/ct-unstable
fix(offer): Adding makerNpub property to db model
2025-10-26 11:14:07 -07:00
Chris Troutner 9a71763f79 fix(offer): Adding makerNpub property to db model 2025-10-26 11:12:49 -07:00
Chris Troutner 1a96eac8c9 Merge pull request #115 from Permissionless-Software-Foundation/ct-unstable
Adding deletedChat and deletedPost DB models
2025-10-25 16:03:55 -07:00
Chris Troutner a2acdef539 fix(database): Adding deletedChat and deletedPost DB models 2025-10-25 16:02:35 -07:00
Chris Troutner 58ddbbcc4e fix(updateUser): Adding util scripts for dev & prod 2025-10-25 14:35:22 -07:00
Chris Troutner 5ff33ff19f Merge remote-tracking branch 'upstream/master' into ct-unstable 2025-10-25 14:34:05 -07:00
Chris Troutner 682f599261 fixing updateUser script 2025-10-25 14:33:51 -07:00
Chris Troutner 7c26e33a8c Merge pull request #181 from Permissionless-Software-Foundation/ct-unstable
fix(util): Fixing problem with updateUser.js script
2025-10-25 14:33:12 -07:00
Chris Troutner 2595ee8295 fix(util): Fixing problem with updateUser.js script 2025-10-25 14:31:33 -07:00
Chris Troutner 531685c0e4 Fixing merge conflicts with upstream 2025-10-25 14:26:16 -07:00
Chris Troutner 476fdcd0aa Merge pull request #180 from Permissionless-Software-Foundation/ct-unstable
fix(utils): Adding script to update users password or convert to admin
2025-10-25 14:24:43 -07:00
Chris Troutner b6762b215e fix(utils): Adding script to update users password or convert to admin 2025-10-25 14:23:28 -07:00
Chris Troutner 4ac7f59378 Merge branch 'master' into fullstack 2025-10-25 18:30:01 +00:00
Chris Troutner 3cdf49bb56 Merge pull request #114 from Permissionless-Software-Foundation/dh-update-endpoint
feat(update): Added Update button REST endpoint
2025-10-25 11:25:02 -07:00
Chris Troutner 28615403b8 Formatting and adding dev comments 2025-10-25 11:22:13 -07:00
Daniel Gonzalez bb10bd819a feat(update): Added Update button REST endpoint 2025-10-24 19:58:56 -04:00
Chris Troutner 3882217e6a Merge pull request #113 from Permissionless-Software-Foundation/dh-user-data
feat(userdata): Store user data on the database
2025-10-21 12:57:45 -07:00
Chris Troutner 8cd2896115 linting 2025-10-21 12:56:23 -07:00
Chris Troutner e570519ec8 Switching default back end to free-bch 2025-10-21 12:56:07 -07:00
Chris Troutner 2f74b74110 fix(createOffer()): Handling API errors when token data can not be retrieved 2025-10-21 12:44:48 -07:00
Chris Troutner fddb5499f0 Making offer use-case unit tests less flaky 2025-10-21 12:29:23 -07:00
Daniel Gonzalez fed98d51be feat(userdata): Store user data on the database 2025-10-20 18:37:54 -04:00
Chris Troutner 8e3ee3a0fb Merge pull request #112 from Permissionless-Software-Foundation/dh-offer-model
feat(offer): Store mutable data in bch-dex Offer db model
2025-10-17 22:30:36 -07:00
Daniel Gonzalez e57faf8f26 feat(offer): Store mutable data in bch-dex Offer db model 2025-10-16 18:38:06 -04:00
Chris Troutner 725c1c39a5 Merge branch 'master' into fullstack 2025-10-09 15:09:25 +00:00
Chris Troutner 61fa6b0ed6 Merge pull request #111 from Permissionless-Software-Foundation/dh-pagination
feat(NFTs): Added pagination controls for NFTs
2025-10-09 07:21:07 -07:00
Daniel Gonzalez 99843061ab feat(NFTs): Added pagination controls for NFTs 2025-10-08 20:20:11 -04:00
Chris Troutner 3a23763800 Adding an extra property to offer db model 2025-10-04 09:44:15 -07:00
Chris Troutner 3f4d49a228 Merge pull request #110 from Permissionless-Software-Foundation/ct-unstable
fix(offer): Adding new properties to offer db model
2025-10-04 09:41:39 -07:00
Chris Troutner 88cfadbc17 fix(offer): Adding new properties to offer db model 2025-10-04 09:40:07 -07:00
Chris Troutner 6c1da41cb3 Syncing with upstream. Fixing merge conflicts 2025-10-04 09:00:38 -07:00
Chris Troutner 0e6a411012 Merge pull request #179 from Permissionless-Software-Foundation/ct-unstable
fix(IPFS): Adding API docs for the IPFS endpoints that need it
2025-10-04 08:32:55 -07:00
Chris Troutner 55e9e35c1f fix(IPFS): Adding API docs for the IPFS endpoints that need it 2025-10-04 08:31:17 -07:00
Chris Troutner 84603356ef Merge pull request #109 from Permissionless-Software-Foundation/dh-access-validation
feat(test): Added user/admin access validation to endpoint
2025-10-03 06:17:40 -07:00
Daniel Gonzalez 2f6b4dfc09 feat(test): Added user/admin access validation to endpoint 2025-10-01 17:24:29 -04:00
Chris Troutner b19af253ae Merge pull request #108 from Permissionless-Software-Foundation/dh-api-docs
fix(docs): Added api-docs to bch-dex endpoints
2025-09-30 07:03:10 -07:00
Daniel Gonzalez b6c6c186a8 fix(docs): Added api-docs to bch-dex endpoints 2025-09-29 18:58:20 -04:00
Chris Troutner ae6e708f51 Merge pull request #107 from Permissionless-Software-Foundation/ct-unstable
Reducing debug console logs
2025-09-16 10:22:09 -07:00
Chris Troutner 442f4ed752 Reducing more console logs 2025-09-16 10:04:00 -07:00
Chris Troutner 8617cf5fd0 fix(offers): Reducing console logs 2025-09-14 09:52:03 -07:00
Chris Troutner 2e00230f79 Merge branch 'master' into ct-unstable 2025-09-14 08:41:21 -07:00
Chris Troutner 44dcce6c45 Merge pull request #106 from Permissionless-Software-Foundation/dh-unit-test-coverage
feat(tests): Added more unit test coverage
2025-09-14 08:32:52 -07:00
Chris Troutner 8d43192a1c minor edits 2025-09-14 08:31:29 -07:00
Daniel Gonzalez 59e2bb473b feat(tests): Added more unit test coverage 2025-09-13 14:36:27 -04:00
Chris Troutner b4d2506e81 Tried AI generated architectural diagram 2025-09-10 06:50:35 -07:00
Chris Troutner 7290c8db30 Merge branch 'master' into fullstack 2025-08-10 18:11:09 +00:00
Chris Troutner 2af50ecf02 Merge pull request #105 from Permissionless-Software-Foundation/operator-fee
Adding required Operator fee to SWaP TX
2025-08-10 11:06:55 -07:00
Chris Troutner f88a24c1b5 Updating unit tests 2025-08-10 10:44:45 -07:00
Chris Troutner a4b4ff13d2 Handling corner cases when validating counter offer against operator fee 2025-08-09 17:54:30 -07:00
Chris Troutner 2bf4f9df28 fix(acceptCounterOffer()): Skipping Counter Offers that are missing an output for the operator 2025-08-09 16:28:56 -07:00
Chris Troutner 19ba38e8ce Adding operator fee to Offer use case, entity, and database model 2025-08-09 15:08:31 -07:00
Chris Troutner 373a52ebf1 feat(operator fee): Adding config values to ensure bch-dex operator recieves a fee 2025-08-09 15:02:29 -07:00
Chris Troutner fe82478dc4 linting 2025-08-09 14:49:41 -07:00
Chris Troutner bed281c8c3 minor edit 2025-08-09 14:49:32 -07:00
Chris Troutner 01484b4e03 fixing typo in comments 2025-08-07 07:00:58 -07:00
Chris Troutner c86512254d Merge pull request #104 from Permissionless-Software-Foundation/dh-test-coverage
feat(tests): Improved adapters test coverage
2025-08-06 09:42:17 -07:00
Daniel Gonzalez 7df9b9b8a4 feat(tests): Improved adapters test coverage 2025-08-05 16:45:37 -04:00
Chris Troutner 36e1ec1f8e Merge branch 'master' into fullstack 2025-07-28 18:19:52 +00:00
Chris Troutner 5f91395766 Merge pull request #103 from Permissionless-Software-Foundation/ct-unstable
fix(GET /offer/list/addr/:addr): Get offers by an address
2025-07-28 11:14:25 -07:00
Chris Troutner dda6cb2aa4 fix(GET /offer/list/addr/:addr): Get offers by an address 2025-07-28 11:12:04 -07:00
Chris Troutner 578aade39b Merge pull request #102 from Permissionless-Software-Foundation/ct-unstable
fix(metrics): Adding timer-controller that updates metrics of SM Acco…
2025-07-14 19:30:21 -07:00
Chris Troutner b09890a527 fix(metrics): Adding timer-controller that updates metrics of SM Accounts 2025-07-14 19:16:00 -07:00
Chris Troutner baa574a93e Merge branch 'master' into fullstack 2025-07-08 23:56:41 +00:00
Chris Troutner a32bd840b9 Merge pull request #101 from Permissionless-Software-Foundation/ct-unstable
fix(smAccount): Adding GET endpoints
2025-07-08 09:24:11 -07:00
Chris Troutner 0bdc232938 fix(smAccount): Adding GET endpoints 2025-07-08 08:58:31 -07:00
Chris Troutner 8333563d98 Merge branch 'master' into fullstack 2025-07-06 21:44:33 +00:00
Chris Troutner f543cfce73 Merge pull request #100 from Permissionless-Software-Foundation/trout-smAccount
Created Social Media account and REST endpoint
2025-07-02 17:09:59 -07:00
Chris Troutner 0f5e694682 Added GET /sm/list/all/0 endpoint 2025-07-01 18:45:53 -07:00
Chris Troutner 615638d705 Timer controller checks creates new social media DB models 2025-07-01 18:23:09 -07:00
Chris Troutner 9b46bbd7bb feat(smAccount): Tracking content creator social media accounts 2025-07-01 15:47:44 -07:00
Chris Troutner 1b58bda5a7 Merge pull request #99 from Permissionless-Software-Foundation/ct-unstable
Syncing with upstream ipfs-service-provider
2025-06-29 07:42:53 -07:00
Chris Troutner c1f15a4079 Fixing merge conflicts, commenting out broken tests 2025-06-29 07:38:58 -07:00
Chris Troutner 18ea03a4d6 Merge pull request #178 from Permissionless-Software-Foundation/dh-test-coverage
feat(tests): Increased unit test coverage
2025-06-28 07:28:04 -07:00
Daniel Gonzalez 3d911d1062 feat(tests): Increased unit test coverage 2025-06-27 20:11:27 -04:00
Chris Troutner 99fd116b6d Merge branch 'master' into fullstack 2025-06-27 16:28:28 +00:00
Chris Troutner 935ab94159 Merge pull request #98 from Permissionless-Software-Foundation/ct-unstable
fix(docker): Improved routing for dex-ui docker container
2025-06-27 09:10:45 -07:00
Chris Troutner 18dad062f6 fix(docker): Improved routing for dex-ui docker container 2025-06-27 09:09:14 -07:00
Chris Troutner 1e8fc1825c Merge pull request #97 from Permissionless-Software-Foundation/ct-unstable
Updating README
2025-06-13 12:13:46 -07:00
Chris Troutner f64795706d Editing README 2025-06-13 08:58:11 -07:00
Chris Troutner 67a9914a8b Editing README 2025-06-13 08:52:54 -07:00
Chris Troutner b01d15a67d Editing README 2025-06-13 08:52:06 -07:00
Chris Troutner e4c9d15e60 fix(README): Updating installation instructions 2025-06-13 08:51:29 -07:00
Chris Troutner ccae89ad01 Merge branch 'master' into fullstack 2025-06-06 18:53:44 +00:00
Chris Troutner 3c00ce6644 Merge pull request #96 from Permissionless-Software-Foundation/ct-unstable
fix(error): Removing confusing non-error error
2025-06-06 11:48:14 -07:00
Chris Troutner e9d1b763fd fix(error): Removing confusing non-error error 2025-06-06 11:46:43 -07:00
Chris Troutner cf88fadfdc Merge pull request #177 from Permissionless-Software-Foundation/ct-unstable
Usage: back up to DB to track usage over restarts.
2025-06-05 18:27:06 -07:00
Chris Troutner 4977ae7003 Increasing backup to 10 minutes 2025-06-05 18:25:45 -07:00
Chris Troutner f0f6970a08 Added timer controllers for managing usage stats 2025-06-05 18:25:10 -07:00
Chris Troutner fcb4acb11d feat(usage-use-cases.js): Added functions for persisting usage data to database 2025-06-04 12:26:03 -07:00
Chris Troutner 0ecae58be2 Merge branch 'master' into fullstack 2025-06-02 01:52:27 +00:00
Chris Troutner 98b5e300c0 Merge pull request #95 from Permissionless-Software-Foundation/ct-unstable
Using User wallet to create and settle Orders, not app wallet.
2025-06-01 18:49:24 -07:00
Chris Troutner 2ea062ae2b Removing e2e tests from unit tests 2025-06-01 18:47:15 -07:00
Chris Troutner c6e5eb9dc9 Updating package-lock 2025-06-01 18:42:16 -07:00
Chris Troutner 46a19b0fb1 linting 2025-06-01 18:34:14 -07:00
Chris Troutner 40cf8cc8de got tests passing again 2025-06-01 18:34:01 -07:00
Chris Troutner ddc70d6fbc feat(completeTx()): Uses user wallet to complete sale 2025-06-01 18:18:34 -07:00
Chris Troutner 625b8895b0 feat(moveTokensFromCustomWallet()): Using user wallet, not system wallet to stash new Offer UTXOs 2025-06-01 17:23:35 -07:00
Chris Troutner 5fd8d8fcd1 Adding llm-bch-dex.md for AI consuption 2025-06-01 16:18:04 -07:00
Chris Troutner 28e012feb6 Merge pull request #176 from Permissionless-Software-Foundation/e2e-tests
Separating E2E tests from Unit tests
2025-05-30 17:18:40 -07:00
Chris Troutner 958c637c24 Commenting out failing test 2025-05-30 17:17:22 -07:00
Daniel Gonzalez 34381e6e93 Improved e2e tests 2025-05-30 11:53:57 -04:00
Daniel Gonzalez 8843b3e2b0 Separate e2e tests 2025-05-28 20:52:25 -04:00
Chris Troutner 3f91653d23 feat(e2e): separating e2e from unit tests 2025-05-23 07:26:51 -07:00
Chris Troutner 5f442835e5 Customizing scrips 2025-05-20 19:38:16 +00:00
Chris Troutner aa8fd89227 Merge branch 'master' into fullstack 2025-05-19 22:45:42 +00:00
Chris Troutner 153ebc204a Merge pull request #94 from Permissionless-Software-Foundation/ct-unstable
feat(NOSTR_TOPIC): Changing default nostr topic
2025-05-19 15:40:19 -07:00
Chris Troutner 589c16550c Merge branch 'master' into ct-unstable 2025-05-19 15:37:01 -07:00
Chris Troutner 49063456b6 feat(NOSTR_TOPIC): Changing default nostr topic 2025-05-19 15:34:36 -07:00
Chris Troutner 87eb804cd1 Merge pull request #93 from Permissionless-Software-Foundation/ct-unstable
fix(util): Adding scripts for creating new users
2025-05-19 13:49:31 -07:00
Chris Troutner 23d5118b52 fix(util): Adding scripts for creating new users 2025-05-19 13:48:15 -07:00
Chris Troutner 94eb342435 Merge pull request #92 from Permissionless-Software-Foundation/ct-unstable
Syncing admin settings with upstream ipfs-service-provider
2025-05-19 13:18:38 -07:00
Chris Troutner ac326b8e34 fix(adapter/admin.js): Adding mnemonic for user 2025-05-19 13:16:32 -07:00
Chris Troutner 164a38ad20 linting 2025-05-19 13:11:23 -07:00
Chris Troutner 3300e0fc80 fix(config): Fixing merge conflict in common.js 2025-05-19 13:11:13 -07:00
Chris Troutner 3a4569c33f Merge pull request #175 from Permissionless-Software-Foundation/dh-env-var-admin
feat(admin): added env var setting so only admin can create new users
2025-05-19 13:06:40 -07:00
Daniel Gonzalez 05a0620663 feat(admin): added env var setting so only admin can create new users 2025-05-19 12:17:09 -04:00
Chris Troutner af83c06b43 Merge pull request #174 from Permissionless-Software-Foundation/ct-unstable
fix(usage): Debugging issue with usage tracking
2025-05-18 15:37:46 -07:00
Chris Troutner e6317c3282 fix(usage): Debugging issue with usage tracking 2025-05-18 15:36:32 -07:00
Chris Troutner 3da7432273 Building UI from psfoundation branch 2025-05-16 17:10:43 +00:00
Chris Troutner 61c1dd5878 Merge branch 'master' into fullstack 2025-05-16 14:56:09 +00:00
Chris Troutner c095e7af39 noop 2025-05-16 14:55:57 +00:00
Chris Troutner ddce40bb52 Merge pull request #91 from Permissionless-Software-Foundation/ct-unstable
Production improvements
2025-05-16 07:54:30 -07:00
Chris Troutner 746f9a3488 fix(production): Adding script to get users in prod env 2025-05-16 07:52:58 -07:00
Chris Troutner fb9612bb65 Using ct-unstable branch 2025-05-16 02:22:19 +00:00
Chris Troutner a611580f48 Turning off CI option 2025-05-15 19:18:41 -07:00
Chris Troutner e4a37b1c10 Merge branch 'ct-unstable' into fullstack 2025-05-16 00:58:49 +00:00
Chris Troutner 07c671ede4 fix(production): Using v3 UI 2025-05-15 17:58:29 -07:00
Chris Troutner ba4f847e57 Merge branch 'master' into fullstack 2025-05-16 00:39:57 +00:00
Chris Troutner 7c8d73810d linting 2025-05-15 07:43:24 -07:00
Chris Troutner 9829e8581c Adding sweep-funds2 script 2025-05-15 07:43:13 -07:00
Chris Troutner a12aa5609d Merge pull request #90 from Permissionless-Software-Foundation/dh-refactor-counteroffer
feat(order): refactored acceptCounterOffer()
2025-05-15 06:01:47 -07:00
Daniel Gonzalez abf834c5a8 feat(order): refactored acceptCounterOffer() 2025-05-14 20:33:25 -04:00
Chris Troutner 7ae557d143 Merge pull request #89 from Permissionless-Software-Foundation/ct-unstable
fix(debug): Adding debugging conosole.logs to bch-dex
2025-05-09 13:17:04 -07:00
Chris Troutner 5f4b0c8da3 fix(debug): Adding debugging conosole.logs to bch-dex 2025-05-09 13:15:39 -07:00
Chris Troutner 6ef197dae3 Merge pull request #88 from Permissionless-Software-Foundation/dh-create-order
feat(order): Modify createOrder() to work with the user model
2025-05-06 18:45:09 -07:00
Daniel Gonzalez fafa7eecd9 feat(order): Modify createOrder() to work with the user model 2025-05-05 18:26:04 -04:00
Chris Troutner 5ede934be5 Merge pull request #87 from Permissionless-Software-Foundation/ct-unstable
fix(wallet adapter): Binding 'this' object to class methods
2025-04-26 14:36:28 -07:00
Chris Troutner 8d036ea9ba fix(wallet adapter): Binding 'this' object to class methods 2025-04-26 14:30:54 -07:00
Chris Troutner bc6acc846a Merge pull request #86 from Permissionless-Software-Foundation/ct-unstable
Syncing with upstream
2025-04-26 14:13:25 -07:00
Chris Troutner afa050261f Merge remote-tracking branch 'upstream/master' into merge-upstream 2025-04-26 14:11:39 -07:00
Chris Troutner 1715d064fb Merge pull request #85 from Permissionless-Software-Foundation/dh-user-mnemonic
feat(user): Added mnemonic to user model
2025-04-26 12:35:37 -07:00
Daniel Gonzalez cf2f2af90f feat(user): Added mnemonic to user model 2025-04-24 12:45:55 -04:00
Chris Troutner 354321c956 Merge pull request #173 from Permissionless-Software-Foundation/ct-unstable
fix(wallet.adapter): Binding this object to all methods
2025-04-22 19:07:06 -07:00
Chris Troutner af2572a493 fix(wallet.adapter): Binding this object to all methods 2025-04-22 18:58:03 -07:00
Chris Troutner 1a5dadbf35 Merge pull request #84 from Permissionless-Software-Foundation/ct-unstable
fix(README): Updating README details
2025-04-11 16:18:37 -07:00
Chris Troutner ced7a60f14 More README edits 2025-04-11 16:15:29 -07:00
Chris Troutner 043c4a8b2b fix(README): Updating README details 2025-04-11 16:09:31 -07:00
Chris Troutner f9b4bf06ab Merge pull request #83 from Permissionless-Software-Foundation/ct-unstable
Syncing with upstream repo ipfs-service-provider
2025-04-10 18:06:27 -07:00
Chris Troutner 8048182f46 fixed tests 2025-04-10 18:04:51 -07:00
Chris Troutner cb4d1d3eba fix(ipfs-service-provider): Syncing with upstream repo 2025-04-10 17:57:44 -07:00
Chris Troutner 81e623e35f Merge branch 'ct-unstable' into fullstack 2025-03-09 01:10:05 +00:00
Chris Troutner 3209f96e7f fix(createOffer()): Removing broken NSFW check 2025-03-08 09:09:38 -08:00
Chris Troutner c65d662630 Merge pull request #172 from Permissionless-Software-Foundation/ct-unstable
fix(config): Added webRTC port & connection preference reporting
2025-02-23 10:37:30 -07:00
Chris Troutner 1b99f8afa1 fix(config): Added webRTC port & connection preference reporting 2025-02-23 10:34:00 -07:00
Chris Troutner c6f0a1317c Merge pull request #171 from Permissionless-Software-Foundation/ct-unstable
fix(prod): Switching Docker container to master branch
2025-02-23 09:57:41 -07:00
Chris Troutner fad5f8e23d fix(prod): Switching Docker container to master branch 2025-02-23 09:56:13 -07:00
Chris Troutner ec6e0f5e46 Merge pull request #170 from Permissionless-Software-Foundation/helia-v5
Helia v5
2025-02-23 09:46:10 -07:00
Chris Troutner f1caef2c0d fix(prod): Adding connection prefrence to production startup script 2025-02-23 09:41:55 -07:00
Chris Troutner 55bf9424f9 fix(libp2p): Updating deps to prevent AbortError issue 2702 2025-02-23 09:01:17 -07:00
Chris Troutner aa100c97ed fix(helia-coord): Updating to v1.7.1 2025-02-20 22:41:25 -07:00
Chris Troutner 9fa5eafe95 removing relay advetising deprecated setting 2025-02-20 20:51:40 -07:00
Chris Troutner 4d9e948e30 fix(helia): Manually porting @achingbrian PR to v5 branch 2025-02-20 20:51:09 -07:00
Chris Troutner e5cdf28e34 Catching and reporting AbortError race condition 2025-02-20 19:03:45 -07:00
Chris Troutner 1d82b36105 fix(helia): Updated to 1.7.0 with webRTC and circuit relay enabled 2025-02-16 15:27:43 -07:00
Chris Troutner b910cd1e6d fix(webRTC): Adding /p2p-circuit multiaddr + ping service 2025-02-16 09:29:18 -07:00
Chris Troutner 536861ba99 Re-enabled webRTC and circuit relays 2025-02-14 12:53:12 -07:00
Chris Troutner ef030e3dc6 fix(libp2p): Updated to v2.6.2 with node.js webrtc fix 2025-02-14 09:52:09 -07:00
Chris Troutner 3da46c3d6f Using helia-v5 branch when building docker container 2025-02-12 16:22:44 -07:00
Chris Troutner 82f5552836 feat(helia v5): Generating and saving new private key 2025-02-12 14:58:11 -07:00
Chris Troutner 4d25681c51 fix(helia v5): Changes to get node to connect to other nodes with helia v5 2025-02-12 09:22:36 -07:00
Chris Troutner 98a1631498 Merge pull request #167 from Permissionless-Software-Foundation/ct-unstable
Removing logging of usage middleware
2025-01-27 18:55:29 -07:00
Chris Troutner 8dce8cbf8d Removing logging of usage middleware 2025-01-27 18:53:17 -07:00
Chris Troutner 0b3049138b Merge pull request #166 from Permissionless-Software-Foundation/dh-usage-tests
feat(tests): Added Usage Unit Tests
2025-01-17 05:32:27 -07:00
Daniel Gonzalez d7136583b8 feat(tests): Added Usage Unit Tests 2025-01-15 17:59:09 -04:00
Chris Troutner db1da9172c Using fullstack branch of UI 2024-12-29 09:37:28 -08:00
Chris Troutner 19b13e5d6e Updating docker-compose .env 2024-12-29 09:23:05 -08:00
Chris Troutner c92ffac645 Merge pull request #82 from Permissionless-Software-Foundation/ct-unstable
Refactored to use Nostr instead of P2WDB
2024-12-29 09:16:40 -08:00
Chris Troutner 216f0b6910 fix(tests): fixed broken tests 2024-12-29 09:14:49 -08:00
Chris Troutner c31bf96986 fix(timers): Disabling unneeded timer controller designed for P2WDB issues. 2024-12-28 13:59:27 -08:00
Chris Troutner 7f8ef994a7 feat(Docker): Updating docker containers 2024-12-16 17:53:36 -08:00
Chris Troutner 72ba1c48c2 Can create and take offers using Nostr 2024-12-16 05:29:03 -08:00
Chris Troutner c721da1562 Merge pull request #165 from Permissionless-Software-Foundation/usage
Usage
2024-12-07 21:28:57 -08:00
Chris Troutner 3606a2d31b code cleanup 2024-12-07 21:27:41 -08:00
Chris Troutner 764905b041 fix(usage): Adding endpoints to analyize 24 hour usage of the REST API 2024-12-07 21:25:10 -08:00
Chris Troutner 556c6ec522 fix(usage): Adding Usage REST API 2024-12-07 20:48:08 -08:00
Chris Troutner 2a78145170 Debugging counter-offer 2024-12-07 20:33:25 -08:00
Chris Troutner 04395b70a6 Syncing with ct-unstable. Fixing merge conflict 2024-12-07 18:08:25 -08:00
Chris Troutner d15fee7c7a Merge pull request #81 from Permissionless-Software-Foundation/ct-unstable
fix(offers): Fixed workflow for creating offers from Nostr posts
2024-12-07 10:56:11 -08:00
Chris Troutner 9437b8fb80 fix(offers): Fixed workflow for creating offers from Nostr posts 2024-12-07 10:54:34 -08:00
Chris Troutner b611663379 Merge pull request #80 from Permissionless-Software-Foundation/dh-create-offr-nostr
feat(offer): Trigger createOffer() from Nostr topic
2024-12-06 09:43:58 -08:00
Chris Troutner 03b7ed980a fix(IPFS): Updating IPFS gateway for getting token data 2024-12-06 09:35:33 -08:00
Chris Troutner bf74d50bb0 linting 2024-12-04 17:12:40 -08:00
Chris Troutner 003480bcb5 fix(offer): Refactored _this in rest-api controller 2024-12-04 17:12:26 -08:00
Daniel Gonzalez 9dbd18d0d6 Increased test coverage 2024-12-04 06:51:28 -04:00
Chris Troutner b3ba7f03e4 Commenting out POST /offer/ webhook endpoint 2024-12-03 09:28:52 -08:00
Daniel Gonzalez 0a8bb52241 feat(offer): Trigger createOffer() from Nostr topic 2024-12-02 22:19:54 -04:00
Chris Troutner 5eeafd7ead Merge pull request #79 from Permissionless-Software-Foundation/dh-create-order-fix
fix(order): Refactored createOrder() to use Nostr
2024-11-29 04:02:53 -08:00
Chris Troutner df146382cb Reducing info posted to Nostr on Order 2024-11-29 04:00:10 -08:00
Daniel Gonzalez af8a7c3ff3 fix(order): Refactored createOrder() to use Nostr 2024-11-28 12:37:47 -04:00
Chris Troutner 064ac36dc7 linting 2024-04-26 14:38:17 -07:00
Chris Troutner 5e2cf86f4b Checking in code before switching branches 2024-04-26 14:38:03 -07:00
Chris Troutner 425d2ada65 Binding 'this' object to some subfunctions 2024-04-20 07:14:17 -07:00
129 changed files with 38562 additions and 5128 deletions
+27 -41
View File
@@ -1,14 +1,14 @@
# bch-dex # bch-dex
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![Join the chat at https://gitter.im/Permissionless-Software-Foundation/psf-dex-dev](https://badges.gitter.im/Permissionless-Software-Foundation/psf-dex-dev.svg)](https://gitter.im/Permissionless-Software-Foundation/psf-dex-dev?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
`bch-dex` is a decentralized exchange for permissionless trading of [SLP tokens](https://github.com/simpleledger/slp-specifications/blob/master/slp-token-type-1.md) and [NFTs](https://github.com/simpleledger/slp-specifications/blob/master/slp-nft-1.md). Trading fees are incredibly small, compared to other DEX platforms. The software is based entirely on open protocols and open source software. It's not a '.com' website that you visit, but software you run on your own computer, in the confort of your own home. So long as you have an internet connection and the technical knowledge to run this software, no one can stop you from trading tokens. `bch-dex` software is maintained by the [Permissionless Software Foundation](https://psfoundation.cash/). `bch-dex` is a decentralized exchange *protocol* for permissionless trading of [SLP tokens](https://github.com/simpleledger/slp-specifications/blob/master/slp-token-type-1.md) and [NFTs](https://github.com/simpleledger/slp-specifications/blob/master/slp-nft-1.md). Trading fees are incredibly small, compared to other DEX platforms. The software is based entirely on open protocols and open source software. `bch-dex` software is maintained by the [Permissionless Software Foundation](https://psfoundation.info).
- [High-level Overview of bch-dex](https://youtu.be/LVX8CLi4sHw) (Video) - [High-level Overview of bch-dex](https://youtu.be/LVX8CLi4sHw) (Video)
This is a prototype web service that monitors the [P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service) for trading signals, to trade BCH and SLP tokens. It's inspired by the [SWaP Protocol](https://github.com/vinarmani/swap-protocol/blob/master/swap-protocol-spec.md). This is a prototype web service that monitors one or more [Nostr relays](https://nostr.com) for trading signals, to trade BCH and SLP tokens. It's inspired by the [SWaP Protocol](https://github.com/vinarmani/swap-protocol/blob/master/swap-protocol-spec.md).
This repository contains the back end code. The user interface is contained in the [bch-dex-ui](https://github.com/Permissionless-Software-Foundation/bch-dex-ui) repository. This repository contains the back end code. The user interface is contained in the [bch-dex-ui-v3](https://github.com/Permissionless-Software-Foundation/bch-dex-ui-v3) repository.
**Warning**: This repository is under active development. Things will be constantly changing and breaking. **Warning**: This repository is under active development. Things will be constantly changing and breaking.
@@ -19,57 +19,43 @@ This repository contains the back end code. The user interface is contained in t
## Participate ## Participate
This is an open source project, and we encourage other JavaScript developers to participate in its creation and maintenance. We have two chat rooms for the community: This is an open source project, and we encourage other JavaScript developers to participate in its creation and maintenance. We have two chat rooms for the community:
- [Telegram Channel](https://t.me/psf_dex_dev) - [Telegram Channel](https://t.me/bch_js_toolkit)
- [Gitter Channel](https://gitter.im/Permissionless-Software-Foundation/psf-dex-dev)
## Installation ## Installation
Running the DEX requires composition of these different software packages: Running the DEX requires composition of these different software packages:
- bch-dex - This repository is the back end software that tracks trade data on the network, generates [Offers and Counter Offers](https://github.com/Permissionless-Software-Foundation/bch-dex/tree/ct-unstable/dev-docs#definitions), and finalizes trades by accepting Counter Offers. - bch-dex - This repository is the back end software that tracks trade data on the network, generates [Offers and Counter Offers](https://github.com/Permissionless-Software-Foundation/bch-dex/tree/ct-unstable/dev-docs#definitions), and finalizes trades by accepting Counter Offers.
- [bch-dex-ui](https://github.com/Permissionless-Software-Foundation/bch-dex-ui) is a [Gatsby](https://www.gatsbyjs.com/) web app and user interface (UI) for bch-dex. - [bch-dex-ui-v3](https://github.com/Permissionless-Software-Foundation/bch-dex-ui-v3) web app and user interface (UI) for bch-dex focused on Sellers of tokens.
- [P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service) is a censorship-resistant database used to communicate trade data between peers running bch-dex. - [MongoDB](https://www.mongodb.com/) is a database used by bch-dex to store and manage local data, user accounts, etc.
- [IPFS](https://ipfs.io/) is a censorship-resistant network for communicating data over the internet.
- [MongoDB](https://www.mongodb.com/) is a database used by both P2WDB and bch-dex to store and manage local data.
The above software is orchestrated using [Docker](https://www.docker.com/) and Docker Compose. The target operating system is Ubuntu 20+, and the target hardware is amd64 (normal desktop PCs) and the arm64 (Raspberry Pi 4). Trying to operate this software on other operating systems or hardware is possible, but not supported. The above software is orchestrated using [Docker](https://www.docker.com/) and Docker Compose. The target operating system is Ubuntu 20+, and the target hardware is amd64 (normal desktop PCs). Trying to operate this software on other operating systems or hardware is possible, but not supported.
The Raspberry Pi 4 is the primary hardware target, because this software functions best when it is always running. The Pi 4 makes a great appliance for monitoring trade signals and finalizing trades. Setup instructions:
Instructions for setting up Node.js, Docker, and Docker Compose can be found in [this Gist](https://gist.github.com/christroutner/a39f656850dc022b60f25c9663dd1cdd). Walk-through videos can also be found on the [PSF Videos page](https://psfoundation.cash/video/). - Follow the direction in [this Gist](https://gist.github.com/christroutner/a39f656850dc022b60f25c9663dd1cdd) to install Node.js, Docker, and Docker Compose.
- Clone the repository with `git clone https://github.com/Permissionless-Software-Foundation/bch-dex` and enter it with `cd bch-dex`.
Here are the steps involved in setting up bch-dex: - Install dependencies by running `npm install`
- Create a wallet:
1. Follow the direction in [this Gist](https://gist.github.com/christroutner/a39f656850dc022b60f25c9663dd1cdd) to install Node.js, Docker, and Docker Compose.
1. Clone the repository with `git clone https://github.com/Permissionless-Software-Foundation/bch-dex` and enter it with `cd bch-dex`.
1. Install dependencies by running `npm install`
1. Create a wallet:
- `cd production/scripts` - `cd production/scripts`
- `node create-wallet.js` - `node create-wallet.js`
1. Change directory to the `production/docker` or `production/rpi-docker` folder depending on your hardware target. - Change directory to the `production/docker` folder.
1. Pull the Docker images down from Docker Hub: `docker-compose pull` - Pull the Docker images down from Docker Hub: `docker-compose pull`
1. Start the Docker containers with `docker-compose up -d` - Build the back end: `docker-compose build --no-cache bch-dex`
1. Wait for the P2WDB to sync and populate bch-dex with trade data. You can monitor it with `docker logs --tail 20 -f p2wdb`. - Build the front end: `docker-compose build --no-cache dex-ui`
1. Open a web browser and navigate the `http://localhost:4500`. You'll be able to see new Offers as they come in and are detected by bch-dex. - Start the Docker containers with `docker-compose up -d`
1. To take the other side of the trade, click the `Take` button in the UI. - It will take about 10 minutes for the bch-dex to sync with the network and display tokens for sale.
1. You can add the 12-word mnemonic from the `wallet.json` file to the the web wallet, which will mirror your wallet in the UI, and allow you to perform basic wallet functions (send and receive BCH and tokens). - Open a web browser and navigate to `http://localhost:4500`.
- You will be presented with a login screen. Click the sign-up tab, enter an email and password to create an account. This information is stored locally on bch-dex, it is not set to a third party.
- Once logged in, you can retrieve your BCH and SLP addresses, to fund your wallet.
## Blockchain Infrastructure ## Blockchain Infrastructure
The P2WDB and bch-dex containers requires a 'Global Back End' (as described in [CashStack.info](https://cashstack.info)) in order to connect to the blockchain. By default, the Docker containers connect to [free-bch.fullstack.cash](https://free-bch.fullstack.cash/). Several community-provided Global Back End servers are provided and can be [viewed here](https://gist.github.com/christroutner/63c5513782181f8b8ea3eb89f7cadeb6). The back end can be changed by setting the `CONSUMER_URL` environment variable in the `docker-compose.yml` file. bch-dex requires a [Cash Stack](https://cashstack.info) back end in order to connect to the blockchain. By default, the Docker containers connect to [free-bch.fullstack.cash](https://free-bch.fullstack.cash/). Several community-provided servers are provided and can be [viewed here](https://consumers.psfoundation.info/consumers.json). The back end can be changed by setting the `CONSUMER_URL` environment variable in the `docker-compose.yml` file.
### Applying Software Updates ## Environment Variables
As this is an active project, software updates will happen frequently. To apply a software update, perform these steps.
- `DISABLE_NEW_ACCOUNTS` - If set, only the admin user can create new accounts. The sign-up tab becomes non-functional. Useful for controlling access to your instance of bch-dex.
1. Enter the `docker` or `rpi-docker` folder, depending on your hardware target. - `ADMIN_PASSWORD` - Sets the password for the admin user. This can be used with the [createUser.js script](/util/users/production/createUser.js) to add new users to the system, if the `DISABLE_NEW_ACCOUNTS` env var is set.
1. Bring down the Docker containers with `docker-compose down`.
1. Pull in the software updates with `git pull`
1. Update dependencies with `npm install`
1. Pull new Docker images with `docker-compose pull`
1. Clean up disk space by deleting old Docker images with `./cleanup-images.sh`
1. Start the Docker containers with `docker-compose up -d`
Sometimes it may be necessary to delete the databases before applying a software update. This can be done by stopping the Docker containers and deleting the `production/data` directory. When the Docker containers are restarted, they will recreate that directory. The P2WDB will re-sync and bch-dex will be populated with fresh trade data.
## License ## License
+5 -1
View File
@@ -25,6 +25,7 @@ import config from '../config/index.js' // this first.
import AdminLib from '../src/adapters/admin.js' import AdminLib from '../src/adapters/admin.js'
import errorMiddleware from '../src/controllers/rest-api/middleware/error.js' import errorMiddleware from '../src/controllers/rest-api/middleware/error.js'
import { usageMiddleware } from '../src/use-cases/usage-use-cases.js'
// import wlogger from '../src/adapters/wlogger.js' // import wlogger from '../src/adapters/wlogger.js'
import Controllers from '../src/controllers/index.js' import Controllers from '../src/controllers/index.js'
import { applyPassportMods } from '../config/passport.js' import { applyPassportMods } from '../config/passport.js'
@@ -70,6 +71,7 @@ class Server {
app.use(bodyParser()) app.use(bodyParser())
app.use(session()) app.use(session())
app.use(errorMiddleware()) app.use(errorMiddleware())
app.use(usageMiddleware())
// Used to generate the docs. // Used to generate the docs.
app.use(mount('/', serve(`${process.cwd()}/docs`))) app.use(mount('/', serve(`${process.cwd()}/docs`)))
@@ -142,7 +144,9 @@ class Server {
console.log('\nConfiguration:') console.log('\nConfiguration:')
console.log(`Circuit Relay: ${this.config.isCircuitRelay}`) console.log(`Circuit Relay: ${this.config.isCircuitRelay}`)
console.log(`IPFS TCP port: ${this.config.ipfsTcpPort}`) console.log(`IPFS TCP port: ${this.config.ipfsTcpPort}`)
console.log(`IPFS WS port: ${this.config.ipfsWsPort}\n`) console.log(`IPFS WS port: ${this.config.ipfsWsPort}`)
console.log(`IPFS WebRTC port: ${this.config.ipfsWebRtcPort}`)
console.log(`Connection preference: ${this.config.connectPref}\n`)
return app return app
} catch (err) { } catch (err) {
+16 -2
View File
@@ -48,6 +48,7 @@ export default {
consumerUrl: process.env.CONSUMER_URL consumerUrl: process.env.CONSUMER_URL
? process.env.CONSUMER_URL ? process.env.CONSUMER_URL
: 'https://free-bch.fullstack.cash', : 'https://free-bch.fullstack.cash',
// : 'https://dev-consumer.psfoundation.info',
// : 'https://wa-usa-bch-consumer.fullstackcash.nl', // : 'https://wa-usa-bch-consumer.fullstackcash.nl',
// P2WDB URL that will accept API calls from the p2wdb npm library. // P2WDB URL that will accept API calls from the p2wdb npm library.
@@ -155,6 +156,8 @@ export default {
// IPFS Ports // IPFS Ports
ipfsTcpPort: process.env.IPFS_TCP_PORT ? process.env.IPFS_TCP_PORT : 4001, ipfsTcpPort: process.env.IPFS_TCP_PORT ? process.env.IPFS_TCP_PORT : 4001,
ipfsWsPort: process.env.IPFS_WS_PORT ? process.env.IPFS_WS_PORT : 4003, ipfsWsPort: process.env.IPFS_WS_PORT ? process.env.IPFS_WS_PORT : 4003,
ipfsWebRtcPort: process.env.IPFS_WEB_RTC_PORT ? process.env.IPFS_WEB_RTC_PORT : 4005,
connectPref: process.env.CONNECT_PREF, // Used in helia-coord to select connection preference.
// Settings for production, using external go-ipfs node. // Settings for production, using external go-ipfs node.
isProduction: process.env.SVC_ENV === 'prod' ? true : false, isProduction: process.env.SVC_ENV === 'prod' ? true : false,
@@ -166,7 +169,7 @@ export default {
chatPubSubChan: 'psf-ipfs-chat-001', chatPubSubChan: 'psf-ipfs-chat-001',
// IPFS gateway // IPFS gateway
ipfsGateway: process.env.IPFS_GATEWAY ? process.env.IPFS_GATEWAY : 'https://p2wdb-gateway-678.fullstack.cash/ipfs/', ipfsGateway: process.env.IPFS_GATEWAY ? process.env.IPFS_GATEWAY : 'https://pin.fullstack.cash/ipfs/download/',
// This can add specific Circuit Relay v2 servers to connect to. // This can add specific Circuit Relay v2 servers to connect to.
bootstrapRelays: [ bootstrapRelays: [
@@ -181,5 +184,16 @@ export default {
// Nostr // Nostr
nostrRelay: process.env.NOSTR_RELAY ? process.env.NOSTR_RELAY : 'wss://nostr-relay.psfoundation.info', nostrRelay: process.env.NOSTR_RELAY ? process.env.NOSTR_RELAY : 'wss://nostr-relay.psfoundation.info',
nostrTopic: process.env.NOSTR_TOPIC ? process.env.NOSTR_TOPIC : 'bch-dex-test-topic-01' nostrTopic: process.env.NOSTR_TOPIC ? process.env.NOSTR_TOPIC : 'bch-dex-test-topic-02',
nostrGlobalFeed: process.env.NOSTR_GLOBAL_FEED ? process.env.NOSTR_GLOBAL_FEED : 'slpdex-socialmedia',
// Account Configuration
disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false,
// Admin password
adminPassword: process.env.ADMIN_PASSWORD,
// The Operator of BCH DEX can elect to receive a percentage of each sale.
operatorAddress: process.env.OPERATOR_ADDRESS ? process.env.OPERATOR_ADDRESS : 'bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr',
operatorPercentage: process.env.OPERATOR_PERCENTAGE ? parseFloat(process.env.OPERATOR_PERCENTAGE) : 2.0
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+4
View File
@@ -1,4 +1,8 @@
import Server from './bin/server.js' import Server from './bin/server.js'
const server = new Server() const server = new Server()
process.on('unhandledRejection', (reason, promise) => {
console.log(`Handling ${reason.code} error. stack: `, reason)
})
server.startServer() server.startServer()
+26701
View File
File diff suppressed because it is too large Load Diff
+1838 -4140
View File
File diff suppressed because it is too large Load Diff
+27 -24
View File
@@ -6,10 +6,11 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "node index.js", "start": "node index.js",
"test": "npm run test:all", "test": "npm run test:unit",
"test:all": "export BCH_DEX=test && c8 --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/", "test:all": "export BCH_DEX=test && c8 --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/",
"test:unit": "export BCH_DEX=test && mocha --exit --timeout 15000 --recursive test/unit/", "test:unit": "export BCH_DEX=test && c8 --reporter=text mocha --exit --timeout 15000 --recursive test/unit/",
"test:e2e:auto": "export BCH_DEX=test && mocha --exit --timeout 30000 test/e2e/automated/", "test:e2e:auto": "export BCH_DEX=test && mocha --exit --timeout 30000 test/e2e/automated/",
"start:e2e:server": "export BCH_DEX=test && node index.js",
"test:integration": "export BCH_DEX=test && mocha --exit --timeout 45000 --recursive test/integration", "test:integration": "export BCH_DEX=test && mocha --exit --timeout 45000 --recursive test/integration",
"test:temp": "export BCH_DEX=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/", "test:temp": "export BCH_DEX=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/",
"lint": "standard --env mocha --fix", "lint": "standard --env mocha --fix",
@@ -28,32 +29,33 @@
}, },
"repository": "Permissionless-Software-Foundation/bch-dex", "repository": "Permissionless-Software-Foundation/bch-dex",
"dependencies": { "dependencies": {
"@chainsafe/libp2p-gossipsub": "13.1.1", "@chainsafe/libp2p-gossipsub": "14.1.0",
"@chainsafe/libp2p-noise": "15.1.1", "@chainsafe/libp2p-noise": "16.0.1",
"@chainsafe/libp2p-yamux": "6.0.2", "@chainsafe/libp2p-yamux": "7.0.1",
"@chris.troutner/retry-queue": "1.0.8", "@chris.troutner/retry-queue": "1.0.8",
"@helia/unixfs": "3.0.7", "@helia/unixfs": "4.0.2",
"@libp2p/bootstrap": "10.1.5", "@libp2p/bootstrap": "11.0.26",
"@libp2p/circuit-relay-v2": "1.1.5", "@libp2p/circuit-relay-v2": "3.2.2",
"@libp2p/identify": "2.1.5", "@libp2p/config": "1.1.0",
"@libp2p/keychain": "4.1.5", "@libp2p/identify": "3.0.22",
"@libp2p/logger": "4.0.20", "@libp2p/keychain": "5.0.14",
"@libp2p/tcp": "9.1.5", "@libp2p/logger": "5.1.8",
"@libp2p/webrtc": "4.1.7", "@libp2p/ping": "2.0.22",
"@libp2p/websockets": "8.2.0", "@libp2p/tcp": "10.1.2",
"@multiformats/multiaddr": "12.3.0", "@libp2p/webrtc": "5.2.2",
"@psf/bch-js": "6.7.3", "@libp2p/websockets": "9.2.2",
"@multiformats/multiaddr": "12.3.5",
"axios": "0.27.2", "axios": "0.27.2",
"bch-message-lib": "2.2.1", "bch-message-lib": "2.2.1",
"bch-nostr": "1.3.1", "bch-nostr": "1.3.3",
"bch-token-sweep": "2.2.1", "bch-token-sweep": "2.2.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bitcoincashjs-lib": "3.3.3", "bitcoincashjs-lib": "3.3.3",
"blockstore-fs": "2.0.0", "blockstore-fs": "2.0.2",
"datastore-fs": "9.1.7", "datastore-fs": "10.0.2",
"glob": "7.1.6", "glob": "7.1.6",
"helia": "4.2.5", "helia": "5.2.1",
"helia-coord": "1.5.15", "helia-coord": "1.7.2",
"ipfs-coord-esm": "9.1.7", "ipfs-coord-esm": "9.1.7",
"ipfs-http-client": "58.0.0", "ipfs-http-client": "58.0.0",
"jsonrpc-lite": "2.2.0", "jsonrpc-lite": "2.2.0",
@@ -70,12 +72,13 @@
"koa-router": "10.0.0", "koa-router": "10.0.0",
"koa-static": "5.0.0", "koa-static": "5.0.0",
"koa2-ratelimit": "0.9.1", "koa2-ratelimit": "0.9.1",
"libp2p": "1.9.1", "libp2p": "2.7.2",
"line-reader": "0.4.0", "line-reader": "0.4.0",
"minimal-slp-wallet": "5.12.0", "minimal-slp-wallet": "5.13.2",
"mongoose": "5.13.14", "mongoose": "5.13.14",
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7", "node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
"nodemailer": "6.7.5", "nodemailer": "6.7.5",
"nostr-tools": "2.10.4",
"p2wdb-esm": "2.2.9", "p2wdb-esm": "2.2.9",
"passport-local": "1.0.0", "passport-local": "1.0.0",
"public-ip": "6.0.1", "public-ip": "6.0.1",
+1 -1
View File
@@ -1 +1 @@
COMPOSE_PROJECT_NAME=ipfs-service-provider COMPOSE_PROJECT_NAME=bch-dex
+10 -7
View File
@@ -12,7 +12,7 @@ RUN apt-get update
RUN apt-get install -y sudo git curl nano gnupg wget RUN apt-get install -y sudo git curl nano gnupg wget
#Install Node and NPM #Install Node and NPM
RUN curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh RUN curl -sL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
RUN bash nodesource_setup.sh RUN bash nodesource_setup.sh
RUN apt-get install -y nodejs build-essential RUN apt-get install -y nodejs build-essential
@@ -34,7 +34,7 @@ WORKDIR /home/safeuser
# Update to the latest version of npm. # Update to the latest version of npm.
#RUN npm install -g npm@7.23.0 #RUN npm install -g npm@7.23.0
RUN npm install -g npm #RUN npm install -g npm
# npm mirror to prevent direct dependency on npm. # npm mirror to prevent direct dependency on npm.
#RUN npm set registry http://94.130.170.209:4873/ #RUN npm set registry http://94.130.170.209:4873/
@@ -46,14 +46,14 @@ RUN npm install -g npm
# Clone the rest.bitcoin.com repository # Clone the rest.bitcoin.com repository
WORKDIR /home/safeuser WORKDIR /home/safeuser
RUN git clone https://github.com/Permissionless-Software-Foundation/bch-dex-ui-v2 RUN git clone https://github.com/Permissionless-Software-Foundation/bch-dex-ui-v3
# Switch to the desired branch. `master` is usually stable, # Switch to the desired branch. `master` is usually stable,
# and `stage` has the most up-to-date changes. # and `stage` has the most up-to-date changes.
WORKDIR /home/safeuser/bch-dex-ui-v2 WORKDIR /home/safeuser/bch-dex-ui-v3
# For development: switch to unstable branch # For development: switch to unstable branch
RUN git checkout ct-unstable RUN git checkout psfoundation
# Install dependencies # Install dependencies
#RUN mkdir .ipfsdata #RUN mkdir .ipfsdata
@@ -61,13 +61,16 @@ RUN git checkout ct-unstable
RUN npm install RUN npm install
# Build the site # Build the site
RUN CI=true npm run build RUN npm run build
# Load the NGINX image. # Load the NGINX image.
FROM nginx FROM nginx
EXPOSE 80 EXPOSE 80
# Copy the files built in the first container to the new NGINX container. # Copy the files built in the first container to the new NGINX container.
COPY --from=builder /home/safeuser/bch-dex-ui-v2/build /usr/share/nginx/html COPY --from=builder /home/safeuser/bch-dex-ui-v3/build /usr/share/nginx/html
# Copy the NGINX configuration file.
COPY default.conf /etc/nginx/conf.d/default.conf
#USER safeuser #USER safeuser
+23
View File
@@ -0,0 +1,23 @@
server {
listen 80;
listen [::]:80;
server_name localhost;
#access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
+2 -2
View File
@@ -12,7 +12,7 @@ RUN apt-get update
RUN apt-get install -y sudo git curl nano gnupg wget RUN apt-get install -y sudo git curl nano gnupg wget
#Install Node and NPM #Install Node and NPM
RUN curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh RUN curl -sL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
RUN bash nodesource_setup.sh RUN bash nodesource_setup.sh
RUN apt-get install -y nodejs build-essential RUN apt-get install -y nodejs build-essential
@@ -34,7 +34,7 @@ WORKDIR /home/safeuser
# Update to the latest version of npm. # Update to the latest version of npm.
#RUN npm install -g npm@7.23.0 #RUN npm install -g npm@7.23.0
RUN npm install -g npm #RUN npm install -g npm
# npm mirror to prevent direct dependency on npm. # npm mirror to prevent direct dependency on npm.
RUN npm set registry http://94.130.170.209:4873/ RUN npm set registry http://94.130.170.209:4873/
@@ -31,4 +31,7 @@ export APP_ID=bch-dex-001
#export P2WDB_URL=http://172.17.0.1:5667 #export P2WDB_URL=http://172.17.0.1:5667
export DISABLE_NEW_ACCOUNTS=true
export ADMIN_PASSWORD=decatur
npm start npm start
+19 -72
View File
@@ -3,9 +3,9 @@
version: '3.9' version: '3.9'
services: services:
mongo-p2wdb-service: mongo-dex:
image: mongo:4.2.0 image: mongo:4.2.0
container_name: mongo-p2wdb-service container_name: mongo-dex
ports: ports:
- '5666:27017' # <host port>:<container port> - '5666:27017' # <host port>:<container port>
volumes: volumes:
@@ -13,69 +13,11 @@ services:
command: mongod --logpath=/dev/null # -- quiet command: mongod --logpath=/dev/null # -- quiet
restart: always restart: always
ipfs:
# https://github.com/christroutner/trickle-ipfs
image: christroutner/trickle-ipfs:v1.0.1
#build:
# context: ./
# dockerfile: Dockerfile
container_name: ipfs
environment:
IPFS_DAEMON_ARGUMENTS: '--enable-pubsub-experiment --migrate=true --agent-version-suffix=docker --routing=dhtclient'
UPLOAD_KBPS: '1000'
DOWNLOAD_KBPS: '1000'
logging:
driver: 'json-file'
options:
max-size: '10m'
max-file: '10'
mem_limit: 2000mb
ports:
- 4001:4001
- 4003:4003
- 172.17.0.1:5001:5001
- 172.17.0.1:8080:8080
command: [
'./start-ipfs.sh'
]
volumes:
- ../data/go-ipfs/data:/root/.ipfs
restart: always
p2wdb:
#build:
# context: ./p2wdb/
# dockerfile: Dockerfile
image: christroutner/p2wdb:v4.1.3
container_name: p2wdb
environment:
CONSUMER_URL: 'https://free-bch.fullstack.cash'
#ENABLE_BCH_PAYMENT: 1
DEBUG_LEVEL: 1
logging:
driver: 'json-file'
options:
max-size: '10m'
max-file: '10'
mem_limit: 1gb
links:
- mongo-p2wdb-service
ports:
# <host port>:<container port>
- 5667:5667 # REST API
#- 5668:5668 # IPFS TCP port
#- 5669:5669 # IPFS WS Port
volumes:
- ../data/ipfsdata/p2wdb:/home/safeuser/ipfs-p2wdb-service/.ipfsdata/p2wdb
- ./p2wdb/start-production.sh:/home/safeuser/ipfs-p2wdb-service/start-production.sh
#- ../scripts/wallet.json:/home/safeuser/ipfs-p2wdb-service/wallet.json
restart: always
bch-dex: bch-dex:
#build: build:
# context: ./bch-dex/ context: ./bch-dex/
# dockerfile: Dockerfile dockerfile: Dockerfile
image: christroutner/bch-dex:v2.2.4 #image: christroutner/bch-dex:v2.2.4
container_name: bch-dex container_name: bch-dex
environment: environment:
CONSUMER_URL: 'https://free-bch.fullstack.cash' CONSUMER_URL: 'https://free-bch.fullstack.cash'
@@ -86,23 +28,28 @@ services:
max-file: '10' max-file: '10'
mem_limit: 1000mb mem_limit: 1000mb
links: links:
- mongo-p2wdb-service - mongo-dex
- p2wdb #- p2wdb
ports: ports:
- '5700:5700' # <host port>:<container port> # <host port>:<container port>
- '5700:5700' # REST API
- '4001:4001' # TCP
- '4003:4003' # Websockets
- '4005:4005' # WebRTC
volumes: volumes:
- ../scripts/wallet.json:/home/safeuser/bch-dex/wallet.json - ../scripts/wallet.json:/home/safeuser/bch-dex/wallet.json
- ./bch-dex/start-production.sh:/home/safeuser/bch-dex/start-production.sh - ./bch-dex/start-production.sh:/home/safeuser/bch-dex/start-production.sh
restart: always restart: always
dex-ui: dex-ui:
#build: build:
# context: ./bch-dex-ui/ context: ./bch-dex-ui/
# dockerfile: Dockerfile dockerfile: Dockerfile
image: christroutner/bch-dex-ui:v1.2.4 #image: christroutner/bch-dex-ui:v1.2.4
container_name: dex-ui container_name: dex-ui
environment: environment:
SERVER: 'http://192.168.2.3' SERVER: 'http://172.17.0.1'
logging: logging:
driver: 'json-file' driver: 'json-file'
options: options:
-71
View File
@@ -1,71 +0,0 @@
# Create a Docker container for the P2WDB
#
#IMAGE BUILD COMMANDS
FROM ubuntu:20.04
MAINTAINER Chris Troutner <chris.troutner@gmail.com>
#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_16.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.
#RUN npm install -g npm@7.23.0
RUN npm install -g npm
# npm mirror to prevent direct dependency on npm.
RUN npm set registry http://94.130.170.209:4873/
# Switch to user account.
#USER safeuser
# Prep 'sudo' commands.
#RUN echo 'abcd8765' | sudo -S pwd
# Clone the rest.bitcoin.com repository
RUN git clone https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service
WORKDIR /home/safeuser/ipfs-p2wdb-service
RUN mkdir .ipfsdata
RUN git checkout ct-unstable
#RUN npm install fsevents
# Install dependencies
#RUN mkdir .ipfsdata
RUN npm install
# Generate the API docs
RUN npm run docs
# Expose the port the API will be served on.
#EXPOSE 5010
# Start the application.
#COPY start-production.sh start-production.sh
CMD ["./start-production.sh"]
#CMD ["npm", "start"]
# Used for debugging
#COPY dummyapp.js dummyapp.js
#CMD ["node", "dummyapp.js"]
-4
View File
@@ -1,4 +0,0 @@
setInterval(function () {
const now = new Date()
console.log(`ping ${now.toLocaleString()}`)
}, 10000)
@@ -1,47 +0,0 @@
#!/bin/bash
# BEGIN: Optional configuration settings
# This mnemonic is used to set up persistent public key for e2ee
# Replace this with your own 12-word mnemonic.
export MNEMONIC="mass response fiscal world message exact series swallow forward confirm canoe festival"
# The human readable name this IPFS node identifies as.
export COORD_NAME=generic-p2wdb-production
# Allow this node to function as a circuit relay. It must not be behind a firewall.
#export ENABLE_CIRCUIT_RELAY=true
# For browsers to use your circuit realy, you must set up a domain, SSL certificate,
# and you must forward that subdomain to the IPFS_WS_PORT.
#export CR_DOMAIN=subdomain.yourdomain.com
# Debug level. 0 = minimal info. 2 = max info.
# This value set by docker-compose.yml
#export DEBUG_LEVEL=1
# Log-in information for retrieving a JWT token from FullStack.cash.
export FULLSTACKLOGIN=demo@demo.com
export FULLSTACKPASS=demo
# END: Optional configuration settings
# Production database connection string.
export DBURL=mongodb://172.17.0.1:5666/p2wdb-service-dev
# Configure IPFS ports
#export IPFS_TCP_PORT=5668
#export IPFS_WS_PORT=5669
# Configure REST API port
export PORT=5667
export P2W_ENV=production
export IPFS_HOST=172.17.0.1
export IPFS_API_PORT=5001
export IPFS_TCP_PORT=4001
# P2WDB specific env vars
#export ORBITDB_NAME=/orbitdb/zdpuAm4fPafpjs5mgbBUJV8Az7kd8Xj1ttD6M1JaAxF8RMRrw/psf-bch-p2wdb-keyvalue-v1.0.0-0004
npm start
+1 -9
View File
@@ -56,7 +56,7 @@ RUN git clone https://github.com/Permissionless-Software-Foundation/bch-dex
WORKDIR /home/safeuser/bch-dex WORKDIR /home/safeuser/bch-dex
# For development: switch to unstable branch # For development: switch to unstable branch
RUN git checkout ct-unstable RUN git checkout master
# Install dependencies # Install dependencies
#RUN mkdir .ipfsdata #RUN mkdir .ipfsdata
@@ -66,14 +66,6 @@ RUN npm install
# Generate the API docs # Generate the API docs
RUN npm run docs RUN npm run docs
# Copy the wallet files
#COPY wallet.json wallet.json
#VOLUME /home/safeuser/keys
# Expose the port the API will be served on.
#EXPOSE 5010
# Start the application. # Start the application.
#COPY start-production.sh start-production.sh #COPY start-production.sh start-production.sh
CMD ["./start-production.sh"] CMD ["./start-production.sh"]
@@ -41,8 +41,14 @@ export IPFS_HOST=172.17.0.1
export IPFS_API_PORT=5001 export IPFS_API_PORT=5001
export IPFS_TCP_PORT=4001 export IPFS_TCP_PORT=4001
export IPFS_WS_PORT=4003 export IPFS_WS_PORT=4003
export IPFS_WEB_RTC_PORT=4005
# Set the debug level for helia-coord. 0-3.
# 0 = no debug logs. 3 = maximum debug logs.
export DEBUG_LEVEL=0
# P2WDB specific env vars # Use this if the IPFS node has a publically accessible IP address.
#export ORBITDB_NAME=/orbitdb/zdpuAqNiwLiJBfbRK7uihV2hAbNSXj78ufzv5VyQb8GuvRwDh/psf-bch-p2wdb-keyvalue-v3.0.0-0001 #export CONNECT_PREF=direct
# Use this if the IPFS node is behind a NAT or Firewall and cannot be accessed directly.
export CONNECT_PREF=cr
npm start npm start
+11
View File
@@ -0,0 +1,11 @@
{
"privateKey": "L4VyBDiWYYeeZhkM3xE3nsZigXJYmvRM29YBxoC7da4FzvubjxFo",
"publicKey": "028d6c77d865e6140db4b879a9bdf69ee4846593c0b6c48a82c448d5792806aab1",
"mnemonic": "split title trigger soda execute frost brush labor vague protect bicycle fiscal",
"cashAddress": "bitcoincash:qprrdk7en7m774gsafkycxcymc9ry209jus5v8wx5z",
"address": "bitcoincash:qprrdk7en7m774gsafkycxcymc9ry209jus5v8wx5z",
"legacyAddress": "17QG25rNUfnfj2NtdaFiyiqKSJatvEnMYd",
"hdPath": "m/44'/245'/0'/0/0",
"slpAddress": "simpleledger:qprrdk7en7m774gsafkycxcymc9ry209juu08umx2u",
"nextAddress": 1
}
+27 -58
View File
@@ -51,70 +51,38 @@ class Admin {
// used by the Listing Manager and test scripts, in order access private API // used by the Listing Manager and test scripts, in order access private API
// functions. // functions.
async createSystemUser () { async createSystemUser () {
// Create the system user.
try { try {
context.password = _this._randomString(20) const context = {
email: 'system@system.com',
const options = { name: 'admin',
method: 'POST', password: _this.config.adminPassword || _this._randomString(20),
url: `${LOCALHOST}/users`, type: 'admin',
data: { mnemonic: 'spread ivory oval pioneer flash gap merit nerve simple image entire trouble'
user: {
email: 'system@system.com',
password: context.password,
name: 'admin'
}
}
} }
const result = await _this.axios.request(options) // Check if the user already exists
// console.log('admin.data: ', result.data) let adminUser = await _this.User.findOne({ email: context.email })
context.email = result.data.user.email if (adminUser) {
context.id = result.data.user._id // Update the password
context.token = result.data.token adminUser.password = context.password
} else {
// Create a new admin user
adminUser = new _this.User(context)
}
// Update context with the new user id and token
context.id = adminUser._id
context.token = await adminUser.generateToken()
// Get the mongoDB entry // Save the user
const user = await _this.User.findById(context.id) await adminUser.save()
// Change the user type to admin // Write the user data to the JSON file
user.type = 'admin' await _this.jsonFiles.writeJSON(context, JSON_PATH)
// console.log(`user created: ${JSON.stringify(user, null, 2)}`)
// Save the user model.
await user.save()
// console.log(`admin user created: ${JSON.stringify(result.body, null, 2)}`)
// console.log(`with password: ${context.password}`)
// Write out the system user information to a JSON file that external
// applications like the Task Manager and the test scripts can access.
await jsonFiles.writeJSON(context, JSON_PATH)
// console.log('context: ', context)
// console.log('JSON_PATH: ', JSON_PATH)
return context return context
} catch (err) { } catch (error) {
// Handle existing system user. console.log('Error in admin.js/createSystemUser()')
if (err.response.status === 422) { throw error
try {
// Delete the existing user
await _this.deleteExistingSystemUser()
// Call this function again.
return _this.createSystemUser()
} catch (err2) {
console.error(
'Error in admin.js/createSystemUser() while trying generate new system user.'
)
// process.end(1)
throw err2
}
} else {
console.log('Error in admin.js/createSystemUser: ')
// process.end(1)
throw err
}
} }
} }
@@ -129,6 +97,7 @@ class Admin {
}) })
await _this.User.deleteOne({ email: 'system@system.com' }) await _this.User.deleteOne({ email: 'system@system.com' })
return true
} catch (err) { } catch (err) {
console.log('Error in admin.js/deleteExistingSystemUser()') console.log('Error in admin.js/deleteExistingSystemUser()')
throw err throw err
@@ -152,7 +121,7 @@ class Admin {
Accept: 'application/json' Accept: 'application/json'
}, },
data: { data: {
email: 'system@system.com', email: existingUser.email,
password: existingUser.password password: existingUser.password
} }
} }
+3
View File
@@ -66,6 +66,9 @@ class Bch {
// } // }
// } // }
/** DEV NOTE: This function are deprecated.?
* https://github.com/Permissionless-Software-Foundation/bch-message-lib/pull/41
*/
async getMerit (slpAddr) { async getMerit (slpAddr) {
try { try {
if (!slpAddr || typeof slpAddr !== 'string') { if (!slpAddr || typeof slpAddr !== 'string') {
+81 -50
View File
@@ -19,17 +19,20 @@ import { noise } from '@chainsafe/libp2p-noise'
import { yamux } from '@chainsafe/libp2p-yamux' import { yamux } from '@chainsafe/libp2p-yamux'
// import { bootstrap } from '@libp2p/bootstrap' // import { bootstrap } from '@libp2p/bootstrap'
// import { identifyService } from 'libp2p/identify' // import { identifyService } from 'libp2p/identify'
import { identify } from '@libp2p/identify' import { identify, identifyPush } from '@libp2p/identify'
// import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2' import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2'
import { circuitRelayServer } from '@libp2p/circuit-relay-v2' // import { circuitRelayServer } from '@libp2p/circuit-relay-v2'
import { gossipsub } from '@chainsafe/libp2p-gossipsub' import { gossipsub } from '@chainsafe/libp2p-gossipsub'
import { webSockets } from '@libp2p/websockets' import { webSockets } from '@libp2p/websockets'
import { publicIpv4 } from 'public-ip' import { publicIpv4 } from 'public-ip'
import { multiaddr } from '@multiformats/multiaddr' import { multiaddr } from '@multiformats/multiaddr'
// import { webRTC } from '@libp2p/webrtc' import { webRTC, webRTCDirect } from '@libp2p/webrtc'
import { keychain } from '@libp2p/keychain' import { keychain } from '@libp2p/keychain'
import { defaultLogger } from '@libp2p/logger'
import { unixfs } from '@helia/unixfs' import { unixfs } from '@helia/unixfs'
// import { generateKeyPairFromSeed } from '@libp2p/crypto/keys'
// import crypto from 'crypto'
import { ping } from '@libp2p/ping'
import { loadOrCreateSelfKey } from '@libp2p/config'
// Local libraries // Local libraries
import config from '../../../config/index.js' import config from '../../../config/index.js'
@@ -64,8 +67,8 @@ class IpfsAdapter {
this.createNode = this.createNode.bind(this) this.createNode = this.createNode.bind(this)
this.stop = this.stop.bind(this) this.stop = this.stop.bind(this)
this.ensureBlocksDir = this.ensureBlocksDir.bind(this) this.ensureBlocksDir = this.ensureBlocksDir.bind(this)
// this.getPrivateKey = this.getPrivateKey.bind(this)
this.getSeed = this.getSeed.bind(this) this.getSeed = this.getSeed.bind(this)
this.getKeychain = this.getKeychain.bind(this)
} }
// Start an IPFS node. // Start an IPFS node.
@@ -111,19 +114,6 @@ class IpfsAdapter {
} }
} }
async getKeychain (datastore) {
const keychainInit = {
pass: await this.getSeed()
}
const chain = this.keychain(keychainInit)({
datastore,
logger: defaultLogger()
})
return chain
}
// This function creates an IPFS node using Helia. // This function creates an IPFS node using Helia.
// It returns the node as an object. // It returns the node as an object.
async createNode () { async createNode () {
@@ -132,44 +122,38 @@ class IpfsAdapter {
const blockstore = new FsBlockstore(`${IPFS_DIR}/blockstore`) const blockstore = new FsBlockstore(`${IPFS_DIR}/blockstore`)
const datastore = new FsDatastore(`${IPFS_DIR}/datastore`) const datastore = new FsDatastore(`${IPFS_DIR}/datastore`)
// const keychainInit = { // Get the private key.
// pass: await this.getSeed() // const privateKey = await this.getPrivateKey()
// } // console.log('privateKey: ', privateKey)
// Create an identity // Create an identity
let peerId const keychainInit = {
// console.log('this.keychain: ', this.keychain) selfKey: 'myKey',
// const chain = this.keychain(keychainInit)({ pass: await this.getSeed()
// datastore,
// logger: defaultLogger()
// })
const chain = await this.getKeychain(datastore)
try {
peerId = await chain.exportPeerId('myKey')
} catch (err) {
await chain.createKey('myKey', 'Ed25519', 4096)
peerId = await chain.exportPeerId('myKey')
} }
const privateKey = await loadOrCreateSelfKey(datastore, keychainInit)
// Configure services // Configure services
const services = { const services = {
identify: identify(), identify: identify(),
pubsub: gossipsub({ allowPublishToZeroTopicPeers: true }) identifyPush: identifyPush(),
pubsub: gossipsub({ allowPublishToZeroTopicPeers: true }),
ping: ping(),
keychain: keychain(keychainInit)
} }
if (this.config.isCircuitRelay) { if (this.config.isCircuitRelay) {
console.log('Helia (IPFS) node IS configured as Circuit Relay') console.log('Helia (IPFS) node IS configured as Circuit Relay')
services.relay = circuitRelayServer({ // makes the node function as a relay server services.relay = circuitRelayServer({ // makes the node function as a relay server
hopTimeout: 30 * 1000, // incoming relay requests must be resolved within this time limit hopTimeout: 30 * 1000, // incoming relay requests must be resolved within this time limit
advertise: true,
reservations: { reservations: {
maxReservations: 15, // how many peers are allowed to reserve relay slots on this server maxReservations: 15, // how many peers are allowed to reserve relay slots on this server
reservationClearInterval: 300 * 1000, // how often to reclaim stale reservations reservationClearInterval: 300 * 1000, // how often to reclaim stale reservations
applyDefaultLimit: true, // whether to apply default data/duration limits to each relayed connection applyDefaultLimit: true, // whether to apply default data/duration limits to each relayed connection
defaultDurationLimit: 2 * 60 * 1000, // the default maximum amount of time a relayed connection can be open for defaultDurationLimit: 2 * 60 * 1000, // the default maximum amount of time a relayed connection can be open for
defaultDataLimit: BigInt(2 << 7), // the default maximum number of bytes that can be transferred over a relayed connection defaultDataLimit: BigInt(2 << 7) // the default maximum number of bytes that can be transferred over a relayed connection
maxInboundHopStreams: 32, // how many inbound HOP streams are allow simultaneously },
maxOutboundHopStreams: 64 // how many outbound HOP streams are allow simultaneously maxInboundHopStreams: 32, // how many inbound HOP streams are allow simultaneously
} maxOutboundHopStreams: 64 // how many outbound HOP streams are allow simultaneously
}) })
} else { } else {
console.log('Helia (IPFS) node IS NOT configured as Circuit Relay') console.log('Helia (IPFS) node IS NOT configured as Circuit Relay')
@@ -177,28 +161,31 @@ class IpfsAdapter {
const transports = [ const transports = [
tcp(), tcp(),
webSockets() webSockets(),
// circuitRelayTransport({ circuitRelayTransport({
// discoverRelays: 3, discoverRelays: 3,
// reservationConcurrency: 3 reservationConcurrency: 3
// }), }),
// webRTC() webRTC(),
webRTCDirect()
] ]
// libp2p is the networking layer that underpins Helia // libp2p is the networking layer that underpins Helia
const libp2p = await this.createLibp2p({ const libp2p = await this.createLibp2p({
peerId, // peerId,
privateKey,
datastore, datastore,
addresses: { addresses: {
listen: [ listen: [
'/ip4/127.0.0.1/tcp/0', '/ip4/127.0.0.1/tcp/0',
`/ip4/0.0.0.0/tcp/${this.config.ipfsTcpPort}`, `/ip4/0.0.0.0/tcp/${this.config.ipfsTcpPort}`,
`/ip4/0.0.0.0/tcp/${this.config.ipfsWsPort}/ws` `/ip4/0.0.0.0/tcp/${this.config.ipfsWsPort}/ws`,
// '/webrtc' '/webrtc',
'/p2p-circuit'
] ]
}, },
transports, transports,
connectionEncryption: [ connectionEncrypters: [
noise() noise()
], ],
streamMuxers: [ streamMuxers: [
@@ -244,6 +231,8 @@ class IpfsAdapter {
!this.fs.existsSync(`${IPFS_DIR}/datastore`) && this.fs.mkdirSync(`${IPFS_DIR}/datastore`) !this.fs.existsSync(`${IPFS_DIR}/datastore`) && this.fs.mkdirSync(`${IPFS_DIR}/datastore`)
!this.fs.existsSync(`${IPFS_DIR}/datastore/pkcs8`) && this.fs.mkdirSync(`${IPFS_DIR}/datastore/pkcs8`)
// !fs.existsSync(`${IPFS_DIR}/datastore/peers`) && fs.mkdirSync(`${IPFS_DIR}/datastore/peers`) // !fs.existsSync(`${IPFS_DIR}/datastore/peers`) && fs.mkdirSync(`${IPFS_DIR}/datastore/peers`)
return true return true
@@ -280,6 +269,48 @@ class IpfsAdapter {
throw err throw err
} }
} }
// Get the private key from disk, or generate a new one and save it,
// if it doesn't exist.
// async getPrivateKey () {
// try {
// const filename = `${IPFS_DIR}/privkey.json`
// let privKeyHex
// try {
// // Try to read the JSON file containing the seed.
// privKeyHex = await this.jsonFiles.readJSON(filename)
// // console.log('saved privKeyHex: ', privKeyHex)
// } catch (err) {
// // Generate a new private key and save it to disk.
// // Generate a new private key and save it to disk.
// const randomBuffer = crypto.randomBytes(32)
// // console.log('randomBuffer: ', randomBuffer)
// privKeyHex = randomBuffer.toString('hex')
// // console.log('new privKeyHex: ', privKeyHex)
// // Save the newly generated seed
// await this.jsonFiles.writeJSON(privKeyHex, filename)
// }
// // Convert the saved hex string to a buffer.
// const privKeyBuf = Buffer.from(privKeyHex, 'hex')
// // Convert the buffer to a Uint8Array 'seed'
// const seed = new Uint8Array(privKeyBuf.buffer, privKeyBuf.byteOffset, privKeyBuf.byteLength)
// // console.log('seed: ', seed)
// // Generate a ED25519 key pair.
// const privKey = await generateKeyPairFromSeed('Ed25519', seed)
// return privKey
// } catch (err) {
// console.error('Error in adapters/ipfs/ipfs.js/getPrivateKey(): ', err)
// throw err
// }
// }
} }
export default IpfsAdapter export default IpfsAdapter
+10
View File
@@ -7,6 +7,11 @@ import Users from './models/users.js'
import Entries from './models/entry.js' import Entries from './models/entry.js'
import Order from './models/order.js' import Order from './models/order.js'
import Offer from './models/offer.js' import Offer from './models/offer.js'
import Usage from './models/usage.js'
import SmAccount from './models/smAccount.js'
import DeletedChat from './models/deletedChat.js'
import DeletedPost from './models/deletedPost.js'
import CounterOffer from './models/counter-offer.js'
class LocalDB { class LocalDB {
constructor () { constructor () {
@@ -15,6 +20,11 @@ class LocalDB {
this.Entry = Entries this.Entry = Entries
this.Order = Order this.Order = Order
this.Offer = Offer this.Offer = Offer
this.Usage = Usage
this.SmAccount = SmAccount
this.DeletedChat = DeletedChat
this.DeletedPost = DeletedPost
this.CounterOffer = CounterOffer
} }
} }
@@ -0,0 +1,14 @@
import mongoose from 'mongoose'
const CounterOffer = new mongoose.Schema({
nostrEventId: { type: String, required: true }, // Nostr Event Id.
takerAddr: { type: String, default: null },
takerNpub: { type: String, default: null },
makerNpub: { type: String, default: null },
counterOfferAddr: { type: String, default: null },
counterOfferUtxo: { type: String, default: null },
takerOfferUtxo: { type: String, default: null },
tokenId: { type: String, default: null }
})
export default mongoose.model('counterOffer', CounterOffer)
@@ -0,0 +1,16 @@
/*
Database model for tracking deleted Nostr chat messages.
*/
import mongoose from 'mongoose'
const DeletedChat = new mongoose.Schema({
eventId: { type: String, required: true, default: null }, // Event ID of the deleted message.
npub: { type: String, default: null }, // Nostr public key of the user who created the message.
bchAddr: { type: String, default: null }, // bch address of the user who created the message.
pubkey: { type: String, default: null }, // Nostr public key of the user who created the message.
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
})
export default mongoose.model('deletedChat', DeletedChat)
@@ -0,0 +1,16 @@
/*
Database model for tracking deleted Nostr posts.
*/
import mongoose from 'mongoose'
const DeletedPost = new mongoose.Schema({
eventId: { type: String, required: true, default: null }, // Event ID of the deleted post.
npub: { type: String, default: null }, // Nostr public key of the user who created the post.
bchAddr: { type: String, default: null }, // bch address of the user who created the post.
pubkey: { type: String, default: null }, // Nostr public key of the user who created the post.
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
})
export default mongoose.model('deletedPost', DeletedPost)
+16 -1
View File
@@ -18,6 +18,7 @@ const Offer = new mongoose.Schema({
p2wdbHash: { type: String }, p2wdbHash: { type: String },
offerStatus: { type: String }, offerStatus: { type: String },
makerAddr: { type: String }, makerAddr: { type: String },
makerNpub: { type: String, default: null },
// Authentication data // Authentication data
signature: { type: String }, signature: { type: String },
@@ -34,8 +35,22 @@ const Offer = new mongoose.Schema({
// SWaP Protocol Properties // SWaP Protocol Properties
lokadId: { type: String }, lokadId: { type: String },
messageType: { type: Number }, messageType: { type: Number },
messageClass: { type: Number } messageClass: { type: Number },
nostrEventId: { type: String }, // Nostr Event Id.
// Operator fee and address
operatorAddress: { type: String },
operatorPercentage: { type: Number },
// Token Data
immutableDataCid: { type: String, default: null },
mutableDataCid: { type: String, default: null },
tokenIconUrl: { type: String, default: null },
tokenData: { type: Object },
tokenCategories: { type: Array, default: [] },
tokenTags: { type: Array, default: [] },
userDataStr: { type: String }, // Token user data
lastUpdatedTokenData: { type: String, default: null } // ISO timestamp of the last time token data was updated.
}) })
export default mongoose.model('offer', Offer) export default mongoose.model('offer', Offer)
+22 -11
View File
@@ -10,20 +10,21 @@ import mongoose from 'mongoose'
const Order = new mongoose.Schema({ const Order = new mongoose.Schema({
// Token data // Token data
tokenId: { type: String }, tokenId: { type: String, required: true },
utxoTxid: { type: String }, utxoTxid: { type: String, required: true },
utxoVout: { type: Number }, utxoVout: { type: Number, required: true },
ticker: { type: String }, ticker: { type: String, required: true },
tokenType: { type: Number }, tokenType: { type: Number, required: true },
// Trade data // Trade data
buyOrSell: { type: String }, buyOrSell: { type: String, required: true },
numTokens: { type: Number }, numTokens: { type: Number, required: true },
rateInBaseUnit: { type: String }, rateInBaseUnit: { type: String, required: true },
minUnitsToExchange: { type: String }, minUnitsToExchange: { type: String },
p2wdbTxid: { type: String }, p2wdbTxid: { type: String },
p2wdbHash: { type: String }, p2wdbHash: { type: String },
makerAddr: { type: String }, makerAddr: { type: String, required: true },
makerNpub: { type: String, required: true },
// Authentication data // Authentication data
signature: { type: String }, signature: { type: String },
@@ -32,12 +33,22 @@ const Order = new mongoose.Schema({
offerPubKey: { type: String }, offerPubKey: { type: String },
// Wallet Data // Wallet Data
hdIndex: { type: Number }, // HD index address holding the UTXO for this offer. hdIndex: { type: Number, required: true }, // HD index address holding the UTXO for this offer.
// SWaP Protocol Properties // SWaP Protocol Properties
lokadId: { type: String }, lokadId: { type: String },
messageType: { type: Number }, messageType: { type: Number },
messageClass: { type: Number } messageClass: { type: Number },
nostrEventId: { type: String, required: true }, // Nostr Event Id.
// Additional properties found in createOrder
dataType: { type: String, required: true },
userId: { type: String, required: true },
// Operator fee and address
operatorAddress: { type: String },
operatorPercentage: { type: Number }
}) })
export default mongoose.model('order', Order) export default mongoose.model('order', Order)
+25
View File
@@ -0,0 +1,25 @@
/*
Social Media Account Model.
This model is used to track content creators using the DEX
Nostr social media channel.
*/
import mongoose from 'mongoose'
const SmAccount = new mongoose.Schema({
npub: { type: String, required: true },
bchAddr: { type: String, required: true, default: '' },
pubkey: { type: String, required: true },
pfp: { type: String, default: '' },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
followerCnt: { type: Number, default: 0 },
followingCnt: { type: Number, default: 0 },
posts: { type: Number, default: 0 },
likes: { type: Number, default: 0 },
comments: { type: Number, default: 0 },
reposts: { type: Number, default: 0 }
})
export default mongoose.model('smAccount', SmAccount)
+15
View File
@@ -0,0 +1,15 @@
/*
Usage data model
*/
// Global npm libraries
import mongoose from 'mongoose'
const Usage = new mongoose.Schema({
ip: { type: String },
url: { type: String },
method: { type: String },
timestamp: { type: Date }
})
export default mongoose.model('usage', Usage)
+1
View File
@@ -11,6 +11,7 @@ const User = new mongoose.Schema({
name: { type: String }, name: { type: String },
username: { type: String }, username: { type: String },
password: { type: String, required: true }, password: { type: String, required: true },
mnemonic: { type: String, required: true },
email: { email: {
type: String, type: String,
required: true, required: true,
+162 -2
View File
@@ -2,13 +2,19 @@
Adapter library for working with Nostr. Adapter library for working with Nostr.
*/ */
// Global npm libraries
import BchNostr from 'bch-nostr' import BchNostr from 'bch-nostr'
import { RelayPool } from 'nostr' import { RelayPool } from 'nostr'
import RetryQueue from '@chris.troutner/retry-queue'
import * as nip19 from 'nostr-tools/nip19'
import { base58_to_binary as base58Tobinary } from 'base58-js'
import { getPublicKey } from 'nostr-tools/pure'
class NostrAdapter { class NostrAdapter {
constructor (localConfig = { nostrRelay: '', nostrTopic: '' }) { constructor (localConfig = { nostrRelay: '', nostrTopic: '' }) {
this.relayWs = localConfig.nostrRelay // this.relayWs = localConfig.nostrRelay //
this.topic = localConfig.nostrTopic this.topic = localConfig.nostrTopic
this.globalFeed = localConfig.nostrGlobalFeed
if (!this.relayWs) { if (!this.relayWs) {
throw new Error( throw new Error(
@@ -25,11 +31,22 @@ class NostrAdapter {
// Encapsulate dependencies // Encapsulate dependencies
this.bchNostr = new BchNostr() this.bchNostr = new BchNostr()
this.RelayPool = RelayPool this.RelayPool = RelayPool
this.retryQueue = new RetryQueue({
concurrency: 1,
attempts: 5,
retryPeriod: 1000
})
// Bind the 'this' object // Bind the 'this' object
this.start = this.start.bind(this) this.start = this.start.bind(this)
this.post = this.post.bind(this) this.post = this.post.bind(this)
this.read = this.read.bind(this) this.read = this.read.bind(this)
this.eventId2note = this.eventId2note.bind(this)
this.pubkey2npub = this.pubkey2npub.bind(this)
this.npub2pubkey = this.npub2pubkey.bind(this)
this.readGlobalFeed = this.readGlobalFeed.bind(this)
this.getFollowers = this.getFollowers.bind(this)
this.privKeyToNpub = this.privKeyToNpub.bind(this)
} }
// Create nostr keys. // Create nostr keys.
@@ -71,7 +88,7 @@ class NostrAdapter {
msg, msg,
tags: [['t', this.topic]] tags: [['t', this.topic]]
} }
const eventId = await this.bchNostr.post.uploadToNostr(inObj) const eventId = await this.retryQueue.addToQueue(this.bchNostr.post.uploadToNostr, inObj)
return eventId return eventId
} catch (error) { } catch (error) {
console.log(`Error in nostr.js/post() ${error.message} `) console.log(`Error in nostr.js/post() ${error.message} `)
@@ -103,7 +120,8 @@ class NostrAdapter {
}) })
pool.on('event', (relay, subId, ev) => { pool.on('event', (relay, subId, ev) => {
messages.push(ev.content) // console.log('ev: ', ev)
messages.push({ content: ev.content, eventId: ev.id })
}) })
}) })
@@ -115,6 +133,148 @@ class NostrAdapter {
throw error throw error
} }
} }
// Convert an Event ID into a `noteabc..` syntax that Astral expects.
// This can be used to generate a link to Astral to display the post.
eventId2note (eventId) {
return nip19.noteEncode(eventId)
}
// Convert a pubkey into a `npubabc..` syntax that Astral expects.
pubkey2npub (pubkey) {
return nip19.npubEncode(pubkey)
}
npub2pubkey (npub) {
return nip19.decode(npub)
}
// Read the global feed.
async readGlobalFeed (inObj = {}) {
try {
const { limit = 10 } = inObj
if (typeof limit !== 'number' || limit < 1) {
throw new Error('Limit must be greater than 0')
}
const relays = [this.relayWs]
const pool = this.RelayPool(relays)
const nostrData = new Promise((resolve, reject) => {
const messages = []
pool.on('open', (relay) => {
// relay.subscribe('REQ', { ids: [eventId] })
relay.subscribe('REQ', { limit, kinds: [1], '#t': [this.globalFeed] })
})
pool.on('eose', (relay) => {
relay.close()
resolve(messages)
})
pool.on('event', (relay, subId, ev) => {
// console.log('ev: ', ev)
const { content, id, tags, pubkey } = ev
const msg = {
content,
id,
tags,
npub: this.pubkey2npub(pubkey),
pubkey
}
messages.push(msg)
})
})
const messages = await nostrData
return messages
} catch (error) {
console.log(`Error in nostr.js/readGlobalFeed() ${error.message} `)
throw error
}
}
async getFollowers (inObj = {}) {
try {
const { pubkey } = inObj
if (!pubkey || typeof pubkey !== 'string') {
throw new Error('pubkey must be a string!')
}
const relays = [this.relayWs]
const pool = this.RelayPool(relays)
const nostrData = new Promise((resolve, reject) => {
const followers = []
pool.on('open', (relay) => {
// Query for kind 3 events where the target pubkey appears in tags
// This finds all contact lists that include the target pubkey
relay.subscribe('subid', {
limit: 100,
kinds: [3],
'#p': [pubkey] // Filter by tag 'p' containing the target pubkey
})
})
pool.on('eose', relay => {
relay.close()
resolve(followers)
})
pool.on('event', (relay, subId, ev) => {
// Each event represents a contact list from someone who follows the target
// The author of this event is a follower
const follower = {
pubkey: ev.pubkey,
npub: this.pubkey2npub(ev.pubkey),
contactList: ev.tags,
createdAt: ev.created_at
}
followers.push(follower)
})
})
const followers = await nostrData
return followers
} catch (error) {
console.log(`Error in nostr.js/getFollowers() ${error.message} `)
throw error
}
}
// Convert a BCH private key (WIF) to a Nostr npub.
privKeyToNpub (privKey) {
try {
if (!privKey || typeof privKey !== 'string') {
throw new Error('privKey must be a string!')
}
// Extract the privaty key from the WIF, using this guide:
// https://learnmeabitcoin.com/technical/keys/private-key/wif/
const wifBuf = base58Tobinary(privKey)
const privBuf = wifBuf.slice(1, 33)
// console.log('privBuf: ', privBuf)
const nostrPubKey = getPublicKey(privBuf)
// console.log('nostrPubKey: ', nostrPubKey)
// Convert pubkey to npub
const nostrNpub = nip19.npubEncode(nostrPubKey)
// console.log('nostrNpub: ', nostrNpub)
return nostrNpub
} catch (error) {
console.log(`Error in nostr.js/privKeyToNpub() ${error.message} `)
throw error
}
}
} }
export default NostrAdapter export default NostrAdapter
+7
View File
@@ -20,6 +20,13 @@ class Wallet {
// Bind 'this' object to all subfunctions // Bind 'this' object to all subfunctions
this.instanceWalletWithoutInitialization = this.instanceWalletWithoutInitialization.bind(this) this.instanceWalletWithoutInitialization = this.instanceWalletWithoutInitialization.bind(this)
this._instanceWallet = this._instanceWallet.bind(this)
this.openWallet = this.openWallet.bind(this)
this.instanceWallet = this.instanceWallet.bind(this)
this.incrementNextAddress = this.incrementNextAddress.bind(this)
this.getKeyPair = this.getKeyPair.bind(this)
this.optimize = this.optimize.bind(this)
this.getBalance = this.getBalance.bind(this)
} }
// This is used for initializing the wallet, without waiting to update the wallet // This is used for initializing the wallet, without waiting to update the wallet
+153 -13
View File
@@ -1,16 +1,17 @@
/* /*
Adapter library for working with a wallet. Adapter library for working with a wallet.
This library is specific to bch-dex. The other wallet adapter library is
inherited from ipfs-service-provider.
*/ */
// Public npm libraries // Public npm libraries
import BchWallet from 'minimal-slp-wallet' import BchWallet from 'minimal-slp-wallet'
import BchTokenSweep from 'bch-token-sweep/index.js' import BchTokenSweep from 'bch-token-sweep/index.js'
import bitcoinJs from 'bitcoincashjs-lib' import bitcoinJs from 'bitcoincashjs-lib'
import RetryQueue from '@chris.troutner/retry-queue'
// Local libraries // Local libraries
import JsonFiles from './json-files.js' import JsonFiles from './json-files.js'
import config from '../../config/index.js' import config from '../../config/index.js'
// Hack to get __dirname back. // Hack to get __dirname back.
@@ -33,11 +34,29 @@ class WalletAdapter {
this.config = config this.config = config
this.bitcoinJs = bitcoinJs this.bitcoinJs = bitcoinJs
this.BchWallet = BchWallet this.BchWallet = BchWallet
this.BchTokenSweep = BchTokenSweep
this.bchWallet = {} // Will be replaced when initialized. this.bchWallet = {} // Will be replaced when initialized.
// this.advancedConfig = localConfig.advancedConfig // this.advancedConfig = localConfig.advancedConfig
this.retryQueue = new RetryQueue({
concurrency: 1,
attempts: 5,
retryPeriod: 5000
})
// Bind the 'this' object // Bind the 'this' object
this.openWallet = this.openWallet.bind(this)
this.instanceWallet = this.instanceWallet.bind(this)
this.incrementNextAddress = this.incrementNextAddress.bind(this)
this.getKeyPair = this.getKeyPair.bind(this)
this.generateSignature = this.generateSignature.bind(this)
this.generatePartialTx = this.generatePartialTx.bind(this)
this.moveTokens = this.moveTokens.bind(this) this.moveTokens = this.moveTokens.bind(this)
this.moveBch = this.moveBch.bind(this)
this.deseralizeTx = this.deseralizeTx.bind(this)
this.completeTx = this.completeTx.bind(this)
this.reclaimTokens = this.reclaimTokens.bind(this)
this.moveTokensFromCustomWallet = this.moveTokensFromCustomWallet.bind(this)
this.cid2json = this.cid2json.bind(this)
} }
// Open the wallet file, or create one if the file doesn't exist. // Open the wallet file, or create one if the file doesn't exist.
@@ -144,10 +163,9 @@ class WalletAdapter {
} }
} }
// Note: This method uses the system wallet, not the user or admin wallet.
// This method returns an object that contains a private key WIF, public address, // This method returns an object that contains a private key WIF, public address,
// and the index of the HD wallet that the key pair was generated from. // and the index of the HD wallet that the key pair was generated from.
// TODO: Allow input integer. If input is used, use that as the index. If no
// input is provided, then call incrementNextAddress().
async getKeyPair (hdIndex = 0) { async getKeyPair (hdIndex = 0) {
try { try {
if (!hdIndex) { if (!hdIndex) {
@@ -156,6 +174,7 @@ class WalletAdapter {
} }
const mnemonic = this.bchWallet.walletInfo.mnemonic const mnemonic = this.bchWallet.walletInfo.mnemonic
console.log(`getKeyPair() for HD index: ${hdIndex} called on mnemonic: ${mnemonic}`)
// root seed buffer // root seed buffer
const rootSeed = await this.bchWallet.bchjs.Mnemonic.toSeed(mnemonic) const rootSeed = await this.bchWallet.bchjs.Mnemonic.toSeed(mnemonic)
@@ -188,7 +207,9 @@ class WalletAdapter {
// Generate a cryptographic signature, required to write to the P2WDB. // Generate a cryptographic signature, required to write to the P2WDB.
async generateSignature (message) { async generateSignature (message) {
try { try {
// TODO: Add input validation for message. if (!message || typeof message !== 'string') {
throw new Error('message is required!')
}
const privKey = this.bchWallet.walletInfo.privateKey const privKey = this.bchWallet.walletInfo.privateKey
@@ -345,7 +366,7 @@ class WalletAdapter {
throw err throw err
} }
// return true // return true
} }
// Move tokens to an address controlled by the HD wallet, to generate a // Move tokens to an address controlled by the HD wallet, to generate a
@@ -353,6 +374,12 @@ class WalletAdapter {
async moveTokens (inObj = {}) { async moveTokens (inObj = {}) {
try { try {
const { tokenId, qty } = inObj const { tokenId, qty } = inObj
if (!tokenId || typeof tokenId !== 'string') {
throw new Error('tokenId must be a string!')
}
if (!qty || typeof qty !== 'number') {
throw new Error('qty must be a number!')
}
const keyPair = await this.getKeyPair() const keyPair = await this.getKeyPair()
console.log('keyPair: ', keyPair) console.log('keyPair: ', keyPair)
@@ -375,6 +402,10 @@ class WalletAdapter {
x => x.tokenId === tokenId x => x.tokenId === tokenId
) )
if (tokenUtxos.length === 0) {
throw new Error('tokenId not found!')
}
const txid = await this.bchWallet.sendTokens(receiver, 3) const txid = await this.bchWallet.sendTokens(receiver, 3)
const utxoInfo = { const utxoInfo = {
@@ -395,6 +426,10 @@ class WalletAdapter {
// generate a segregated UTXO. // generate a segregated UTXO.
async moveBch (amountSat) { async moveBch (amountSat) {
try { try {
if (!amountSat) {
throw new Error('amountSat is required!')
}
const keyPair = await this.getKeyPair() const keyPair = await this.getKeyPair()
console.log('keyPair: ', keyPair) console.log('keyPair: ', keyPair)
@@ -427,6 +462,10 @@ class WalletAdapter {
// representing the transaction. // representing the transaction.
async deseralizeTx (txHex) { async deseralizeTx (txHex) {
try { try {
// Input validation
if (!txHex || typeof txHex !== 'string') {
throw new Error('hex must be a string!')
}
// Ensure the URL points at FullStack.cash, since the web 3 infra does not // Ensure the URL points at FullStack.cash, since the web 3 infra does not
// yet support this call. // yet support this call.
const oldUrl = this.bchWallet.bchjs.RawTransactions.restURL const oldUrl = this.bchWallet.bchjs.RawTransactions.restURL
@@ -441,18 +480,33 @@ class WalletAdapter {
return txObj2 return txObj2
} catch (err) { } catch (err) {
console.error('Error in wallet.js/deserializePartialTx()') console.error('Error in wallet.js/deserializeTx()')
throw err throw err
} }
} }
// Complete the partially signed transaction by signing the first input, // Complete the partially signed transaction by signing the first input,
// then broadcasting the transaction to the network. // then broadcasting the transaction to the network.
async completeTx (hex, hdIndex) { async completeTx (hex, hdIndex, mnemonic) {
try { try {
// Input validation
if (!hex || typeof hex !== 'string') {
throw new Error('hex must be a string!')
}
if (typeof hdIndex !== 'number' || hdIndex < 0) {
throw new Error('hdIndex must be a non-negative number!')
}
if (!mnemonic || typeof mnemonic !== 'string') {
throw new Error('mnemonic must be a string!')
}
// console.log('hex: ', hex) // console.log('hex: ', hex)
console.log('completeTx() hdIndex: ', hdIndex)
console.log('completeTx() mnemonic: ', mnemonic)
const bchjs = this.bchWallet.bchjs // Instantiate the user wallet
const userWallet = await this.instanceWallet({ mnemonic })
const bchjs = userWallet.bchjs
// instance of transaction builder // instance of transaction builder
// const transactionBuilder = new bchjs.TransactionBuilder() // const transactionBuilder = new bchjs.TransactionBuilder()
@@ -475,8 +529,10 @@ class WalletAdapter {
// 'mainnet' // 'mainnet'
// ) // )
console.log('completeTx() userWallet.walletInfo: ', userWallet.walletInfo)
// Get the keypair for the address used in the Order // Get the keypair for the address used in the Order
const keyPair = await this.getKeyPair(hdIndex) const keyPair = await userWallet.getKeyPair(hdIndex)
console.log(`maker keyPair: ${JSON.stringify(keyPair, null, 2)}`) console.log(`maker keyPair: ${JSON.stringify(keyPair, null, 2)}`)
const makerECPair = bchjs.ECPair.fromWIF(keyPair.wif) const makerECPair = bchjs.ECPair.fromWIF(keyPair.wif)
@@ -508,7 +564,9 @@ class WalletAdapter {
// return csTxHex // return csTxHex
// Broadcast transaction to the network // Broadcast transaction to the network
const txid = await this.bchWallet.ar.sendTx(csTxHex) // const txid = await this.bchWallet.ar.sendTx(csTxHex)
const txid = await this.retryQueue.addToQueue(this.bchWallet.broadcast, { hex: csTxHex })
console.log('completeTx() txid: ', txid)
return txid return txid
} catch (err) { } catch (err) {
@@ -519,8 +577,12 @@ class WalletAdapter {
// Given an Order database object, this function sends the tokens from the // Given an Order database object, this function sends the tokens from the
// HD index where they are stored, back to the root address of the wallet. // HD index where they are stored, back to the root address of the wallet.
async reclaimTokens (orderData) { async reclaimTokens (orderData = {}) {
try { try {
if (!orderData.hdIndex) {
throw new Error('orderData.hdIndex is required!')
}
// Get the key pair from the Order. // Get the key pair from the Order.
const keyPair = await this.getKeyPair(orderData.hdIndex) const keyPair = await this.getKeyPair(orderData.hdIndex)
console.log('keyPair: ', keyPair) console.log('keyPair: ', keyPair)
@@ -540,7 +602,7 @@ class WalletAdapter {
// const txid = await tempWallet.sendTokens(receiver) // const txid = await tempWallet.sendTokens(receiver)
// Sweep the tokens to the root address. // Sweep the tokens to the root address.
const sweeper = new BchTokenSweep( const sweeper = new this.BchTokenSweep(
keyPair.wif, keyPair.wif,
this.bchWallet.walletInfo.privateKey, this.bchWallet.walletInfo.privateKey,
this.bchWallet, this.bchWallet,
@@ -557,6 +619,84 @@ class WalletAdapter {
throw err throw err
} }
} }
// This function is consumed by the use-cases/order.js/createOrder() function.
// Tokens are moved from the primary address to a holding address to protect the Order UTXO.
async moveTokensFromCustomWallet (inObj = {}) {
try {
const { tokenId, qty, wallet } = inObj
// Input validation
if (!tokenId || typeof tokenId !== 'string') {
throw new Error('tokenId must be a string!')
}
if (!qty) {
throw new Error('qty must be a number!')
}
if (!wallet) {
throw new Error('wallet is required!')
}
// Replace the system wallet with the user wallet.
// this.bchWallet = wallet
// DEV Note: Storing all token Offers in index 2 of the HD wallet.
// This makes them easy to sweep, and can be uniquely identified by their UTXO.
// Note: use walle.getKeyPair() to use the user wallet. this.getKeyPair uses the system wallet.
const keyPair = await wallet.getKeyPair(2)
console.log('keyPair: ', keyPair)
const receiver = {
address: keyPair.cashAddress,
tokenId,
qty
}
console.log('receiver: ', receiver)
// Update the UTXO store of the wallet.
await wallet.initialize()
// Get the token type of the token being moved.
// Combine Fungible and NFT token UTXOs.
let tokenUtxos = wallet.utxos.utxoStore.slpUtxos.type1.tokens.concat(
wallet.utxos.utxoStore.slpUtxos.nft.tokens)
// Get token UTXOs that match the token in the order.
tokenUtxos = tokenUtxos.filter(
x => x.tokenId === tokenId
)
const txid = await wallet.sendTokens(receiver, 3)
const utxoInfo = {
txid,
vout: 1,
hdIndex: keyPair.hdIndex,
tokenType: tokenUtxos[0].tokenType
}
return utxoInfo
} catch (err) {
console.error('Error in wallet.js/moveTokensFromCustomWallet()')
throw err
}
}
async cid2json (urlOrCid) {
try {
// Input validation
if (!urlOrCid || typeof urlOrCid !== 'string') {
throw new Error('urlOrCid must be a string!')
}
// Extract the cid from the url or cid.
const cid = urlOrCid.split('/').pop()
console.log('cid to json: ', cid)
const jsonRes = await this.bchWallet.cid2json({ cid })
const json = jsonRes.json
// console.log('json: ', json)
return json
} catch (err) {
console.error('Error in wallet.js/cid2json()', err.message)
throw err
}
}
} }
export default WalletAdapter export default WalletAdapter
+15 -13
View File
@@ -6,13 +6,15 @@ import config from '../../config/index.js'
import axios from 'axios' import axios from 'axios'
let _this
class WebHook { class WebHook {
constructor () { constructor () {
_this = this this.config = config
_this.config = config this.axios = axios
_this.axios = axios this.sleepTime = 2000
this.createWebhook = this.createWebhook.bind(this)
this.deleteWebhook = this.deleteWebhook.bind(this)
this.waitUntilSuccess = this.waitUntilSuccess.bind(this)
this.sleep = this.sleep.bind(this)
} }
// REST petition to create a webhook in p2wdb-service // REST petition to create a webhook in p2wdb-service
@@ -22,14 +24,14 @@ class WebHook {
throw new Error('url must be a string') throw new Error('url must be a string')
} }
const endpoint = _this.config.webhookService const endpoint = this.config.webhookService
const obj = { const obj = {
appId: this.config.p2wdbAppId, appId: this.config.p2wdbAppId,
url url
} }
const result = await axios.post(endpoint, obj) const result = await this.axios.post(endpoint, obj)
return result.data return result.data
} catch (err) { } catch (err) {
@@ -45,14 +47,14 @@ class WebHook {
throw new Error('url must be a string') throw new Error('url must be a string')
} }
const endpoint = _this.config.webhookService const endpoint = this.config.webhookService
const obj = { const obj = {
appId: this.config.p2wdbAppId, appId: this.config.p2wdbAppId,
url url
} }
const result = await axios.delete(endpoint, { data: obj }) const result = await this.axios.delete(endpoint, { data: obj })
return result.data return result.data
} catch (err) { } catch (err) {
@@ -86,7 +88,7 @@ class WebHook {
} catch (err) { } catch (err) {
const now = new Date() const now = new Date()
console.log(`${now.toLocaleString()}: Error trying to create webhook with P2WDB. Trying again...`) console.log(`${now.toLocaleString()}: Error trying to create webhook with P2WDB. Trying again...`)
await sleep(2000) await this.sleep(this.sleepTime)
} }
} while (!success) } while (!success)
@@ -96,10 +98,10 @@ class WebHook {
throw err throw err
} }
} }
}
function sleep (ms) { async sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms)) return new Promise(resolve => setTimeout(resolve, ms))
}
} }
export default WebHook export default WebHook
+7
View File
@@ -21,6 +21,13 @@ class Controllers {
this.useCases = new UseCases({ adapters: this.adapters }) this.useCases = new UseCases({ adapters: this.adapters })
this.timerControllers = new TimerControllers({ adapters: this.adapters, useCases: this.useCases }) this.timerControllers = new TimerControllers({ adapters: this.adapters, useCases: this.useCases })
this.config = config this.config = config
// Bind 'this' object to all subfunction
this.initAdapters = this.initAdapters.bind(this)
this.initUseCases = this.initUseCases.bind(this)
this.attachRESTControllers = this.attachRESTControllers.bind(this)
this.attachControllers = this.attachControllers.bind(this)
this.attachRPCControllers = this.attachRPCControllers.bind(this)
} }
// Spin up any adapter libraries that have async startup needs. // Spin up any adapter libraries that have async startup needs.
@@ -29,7 +29,36 @@ class EntryRESTControllerLib {
_this = this _this = this
} }
/**
* @api {post} /entry Create an entry
* @apiPermission public
* @apiName CreateEntry
* @apiGroup REST Entry
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{"entry": "1234567890", "description": "1234567890", "slpAddress": "1234567890", "signature": "1234567890", "category": "1234567890"}' localhost:5001/entry
*
* @apiSuccess {Object} entry Entry object
* @apiSuccess {String} entry.id Entry ID
* @apiSuccess {String} entry.description Entry description
* @apiSuccess {String} entry.slpAddress Entry slp address
* @apiSuccess {String} entry.signature Entry signature
* @apiSuccess {String} entry.category Entry category
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "entry": {
* "id": "1234567890",
* "description": "1234567890",
* "slpAddress": "1234567890",
* "signature": "1234567890",
* "category": "1234567890"
* }
* }
*/
// No api-doc documentation because this wont be a public endpoint // No api-doc documentation because this wont be a public endpoint
// Note: this is a private endpoint, needs to be authenticated
async createEntry (ctx) { async createEntry (ctx) {
try { try {
console.log('body: ', ctx.request.body) console.log('body: ', ctx.request.body)
+17
View File
@@ -17,6 +17,9 @@ import EntryRouter from './entry/index.js'
import OfferRouter from './offer/index.js' import OfferRouter from './offer/index.js'
import OrderRouter from './order/index.js' import OrderRouter from './order/index.js'
import P2WDBRouter from './p2wdb/index.js' import P2WDBRouter from './p2wdb/index.js'
import UsageRESTController from './usage/index.js'
import SmAccountRouter from './smAccount/index.js'
import NostrRouter from './nostr/index.js'
class RESTControllers { class RESTControllers {
constructor (localConfig = {}) { constructor (localConfig = {}) {
@@ -34,6 +37,9 @@ class RESTControllers {
) )
} }
// Bind 'this' object to all subfunctions.
this.attachRESTControllers = this.attachRESTControllers.bind(this)
// Encapsulate dependencies // Encapsulate dependencies
this.config = config this.config = config
} }
@@ -78,6 +84,17 @@ class RESTControllers {
const p2wdbRouter = new P2WDBRouter(dependencies) const p2wdbRouter = new P2WDBRouter(dependencies)
p2wdbRouter.attach(app) p2wdbRouter.attach(app)
// Attach the REST API Controllers associated with the /usage route
const usageRESTController = new UsageRESTController(dependencies)
usageRESTController.attach(app)
const smAccountRouter = new SmAccountRouter(dependencies)
smAccountRouter.attach(app)
// Attach the REST API Controllers associated with the /nostr route
const nostrRouter = new NostrRouter(dependencies)
nostrRouter.attach(app)
} }
} }
+70 -3
View File
@@ -40,11 +40,23 @@ class IpfsRESTControllerLib {
* @api {get} /ipfs Get status on IPFS infrastructure * @api {get} /ipfs Get status on IPFS infrastructure
* @apiPermission public * @apiPermission public
* @apiName GetIpfsStatus * @apiName GetIpfsStatus
* @apiGroup REST BCH * @apiGroup REST IPFS
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs * curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs
* *
* @apiSuccess {Object} status IPFS status object
* @apiSuccess {String} status.ipfsId IPFS ID
* @apiSuccess {String} status.multiaddrs IPFS multiaddrs
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "status": {
* "ipfsId": "1234567890",
* "multiaddrs": ["/ip4/123.45.67.89/tcp/4001/p2p/1234567890"]
* }
* }
*/ */
async getStatus (ctx) { async getStatus (ctx) {
try { try {
@@ -58,7 +70,26 @@ class IpfsRESTControllerLib {
} }
} }
// Return information on IPFS peers this node is connected to. /**
* @api {post} /ipfs/peers Get information on IPFS peers this node is connected to
* @apiPermission public
* @apiName GetIpfsPeers
* @apiGroup REST IPFS
*
* @apiParam {Boolean} [showAll=false] Whether to include detailed peer data
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST localhost:5001/ipfs/peers \
* -d '{"showAll": false}'
*
* @apiSuccess {Object[]} peers Array of peer objects
* @apiSuccess {String} peers[].peer Peer ID
* @apiSuccess {String} peers[].name Peer name
* @apiSuccess {String} peers[].protocol Protocol used by the peer
* @apiSuccess {String} peers[].version Peer version
* @apiSuccess {String} peers[].connectionAddr Connection address
* @apiSuccess {Object} [peers[].peerData] Detailed peer data (when showAll=true)
*/
async getPeers (ctx) { async getPeers (ctx) {
try { try {
const showAll = ctx.request.body.showAll const showAll = ctx.request.body.showAll
@@ -73,6 +104,24 @@ class IpfsRESTControllerLib {
} }
} }
/**
* @api {post} /ipfs/relays Get data about the known Circuit Relays
* @apiPermission public
* @apiName GetIpfsRelays
* @apiGroup REST IPFS
*
* @apiDescription Returns information about Circuit Relays, both v1 and v2, that this node knows about. V2 relays are hydrated with peer data from the connected peers list.
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST localhost:5001/ipfs/relays
*
* @apiSuccess {Object} relays Object containing relay information
* @apiSuccess {Object[]} relays.v2Relays Array of v2 Circuit Relay objects
* @apiSuccess {String} relays.v2Relays[].ipfsId IPFS ID of the relay
* @apiSuccess {String} relays.v2Relays[].name Name of the relay (hydrated from peer data)
* @apiSuccess {String} relays.v2Relays[].description Description of the relay (hydrated from peer data)
* @apiSuccess {Object[]} relays.v1Relays Array of v1 Circuit Relay configurations
*/
// Get data about the known Circuit Relays. Hydrate with data from peers list. // Get data about the known Circuit Relays. Hydrate with data from peers list.
async getRelays (ctx) { async getRelays (ctx) {
try { try {
@@ -86,6 +135,24 @@ class IpfsRESTControllerLib {
} }
} }
/**
* @api {post} /ipfs/connect Connect to a specific IPFS peer
* @apiPermission public
* @apiName ConnectToIpfsPeer
* @apiGroup REST IPFS
*
* @apiDescription Attempts to establish a connection to a specific IPFS peer using the provided multiaddr. Optionally returns detailed information about the connection.
*
* @apiParam {String} multiaddr Multiaddress of the peer to connect to (required)
* @apiParam {Boolean} [getDetails=false] Whether to return detailed connection information
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST localhost:5001/ipfs/connect \
* -d '{"multiaddr": "/ip4/161.35.99.207/tcp/4001/p2p/12D3KooWDtj9cfj1SKuLbDNKvKRKSsGN8qivq9M8CYpLPDpcD5pu", "getDetails": false}'
*
* @apiSuccess {Boolean} success Indicates whether the connection attempt was successful
* @apiSuccess {Object} [details] Additional connection details (when getDetails=true)
*/
async connect (ctx) { async connect (ctx) {
try { try {
const multiaddr = ctx.request.body.multiaddr const multiaddr = ctx.request.body.multiaddr
@@ -107,7 +174,7 @@ class IpfsRESTControllerLib {
* @api {get} /ipfs/node Get a copy of the thisNode object from helia-coord * @api {get} /ipfs/node Get a copy of the thisNode object from helia-coord
* @apiPermission public * @apiPermission public
* @apiName GetThisNode * @apiName GetThisNode
* @apiGroup REST BCH * @apiGroup REST IPFS
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/node * curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/node
@@ -0,0 +1,219 @@
/*
REST API Controller library for the /nostr route
*/
import wlogger from '../../../adapters/wlogger.js'
class NostrRESTControllerLib {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating /nostr REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating /nostr REST Controller.'
)
}
this.createDeletedChat = this.createDeletedChat.bind(this)
this.getDeletedChats = this.getDeletedChats.bind(this)
this.createDeletedPost = this.createDeletedPost.bind(this)
this.getDeletedPosts = this.getDeletedPosts.bind(this)
this.handleError = this.handleError.bind(this)
}
/**
* @api {post} /nostr/deletedChat Create Deleted Chat.
* @apiPermission admin
* @apiName CreateDeletedChat
* @apiGroup REST Nostr
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -H "Authorization: Bearer <token>" -X POST -d '{"eventId": "note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q" , "npub" : "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8" , "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a", "pubkey":"6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1"}' localhost:5700/nostr/deletedChat
*
* @apiSuccess {String} _id DB Model id
* @apiSuccess {String} eventId Event id
* @apiSuccess {String} bchAddr BCH Address
* @apiSuccess {String} npub Nostr Npub
* @apiSuccess {String} pubkey Nostr pubkey
* @apiSuccess {Date} createdAt Created at date.
* @apiSuccess {Date} updatedAt Updated at date.
*
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "_id": "6900f87f9cafee38028ea27e",
* "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8",
* "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a",
* "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1",
* "createdAt":"2025-10-28T17:08:15.409Z",
* "updatedAt": "2025-10-28T17:08:15.409Z"
* }
*
* @apiUse TokenError
*/
async createDeletedChat (ctx) {
try {
const chat = ctx.request.body
const result = await this.useCases.nostr.createDeletedChat(chat)
ctx.body = {
deletedChat: result
}
} catch (err) {
wlogger.error('Error in createDeletedChat() REST API handler.')
this.handleError(ctx, err)
}
}
/**
* @api {get} /nostr/deletedChat Get Deleted Chats.
* @apiPermission public
* @apiName GetDeletedChats
* @apiGroup REST Nostr
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5700/nostr/deletedChat
*
* @apiSuccess {String} _id DB Model id
* @apiSuccess {String} eventId Event id
* @apiSuccess {String} bchAddr BCH Address
* @apiSuccess {String} npub Nostr Npub
* @apiSuccess {String} pubkey Nostr pubkey
* @apiSuccess {Date} createdAt Created at date.
* @apiSuccess {Date} updatedAt Updated at date.
*
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* [{
* "_id": "6900f87f9cafee38028ea27e",
* "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8",
* "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a",
* "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1",
* "createdAt":"2025-10-28T17:08:15.409Z",
* "updatedAt": "2025-10-28T17:08:15.409Z"
* }]
*
* */
async getDeletedChats (ctx) {
try {
const deletedChats = await this.useCases.nostr.getDeletedChats()
ctx.body = { deletedChats }
} catch (err) {
wlogger.error('Error in getDeletedChats() REST API handler.')
this.handleError(ctx, err)
}
}
/**
* @api {post} /nostr/deletedPost Create Deleted Post.
* @apiPermission admin
* @apiName CreateDeletedPost
* @apiGroup REST Nostr
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -H "Authorization: Bearer <token>" -X POST -d '{"eventId": "note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q" , "npub" : "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8" , "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a", "pubkey":"6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1"}' localhost:5700/nostr/deletedPost
*
* @apiSuccess {String} _id DB Model id
* @apiSuccess {String} eventId Event id
* @apiSuccess {String} bchAddr BCH Address
* @apiSuccess {String} npub Nostr Npub
* @apiSuccess {String} pubkey Nostr pubkey
* @apiSuccess {Date} createdAt Created at date.
* @apiSuccess {Date} updatedAt Updated at date.
*
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "_id": "6900f87f9cafee38028ea27e",
* "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8",
* "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a",
* "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1",
* "createdAt":"2025-10-28T17:08:15.409Z",
* "updatedAt": "2025-10-28T17:08:15.409Z"
* }
*
* @apiUse TokenError
*/
async createDeletedPost (ctx) {
try {
const post = ctx.request.body
const result = await this.useCases.nostr.createDeletedPost(post)
ctx.body = {
deletedPost: result
}
} catch (err) {
wlogger.error('Error in createDeletedPost() REST API handler.')
this.handleError(ctx, err)
}
}
/**
* @api {get} /nostr/deletedPost Get Deleted Posts.
* @apiPermission public
* @apiName GetDeletedPosts
* @apiGroup REST Nostr
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5700/nostr/deletedPost
*
* @apiSuccess {String} _id DB Model id
* @apiSuccess {String} eventId Event id
* @apiSuccess {String} bchAddr BCH Address
* @apiSuccess {String} npub Nostr Npub
* @apiSuccess {String} pubkey Nostr pubkey
* @apiSuccess {Date} createdAt Created at date.
* @apiSuccess {Date} updatedAt Updated at date.
*
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* [{
* "_id": "6900f87f9cafee38028ea27e",
* "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8",
* "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a",
* "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1",
* "createdAt":"2025-10-28T17:08:15.409Z",
* "updatedAt": "2025-10-28T17:08:15.409Z"
* }]
*
* */
async getDeletedPosts (ctx) {
try {
const deletedPosts = await this.useCases.nostr.getDeletedPosts()
ctx.body = { deletedPosts }
} catch (err) {
wlogger.error('Error in getDeletedPosts() REST API handler.')
this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
// If an HTTP status is specified by the buisiness logic, use that.
if (err.status) {
if (err.message) {
ctx.throw(err.status, err.message)
} else {
ctx.throw(err.status)
}
} else {
// By default use a 422 error if the HTTP status is not specified.
ctx.throw(422, err.message)
}
}
}
export default NostrRESTControllerLib
+81
View File
@@ -0,0 +1,81 @@
/*
REST API library for /nostr route.
*/
// Public npm libraries.
import Router from 'koa-router'
// Local libraries.
import NostrRESTControllerLib from './controller.js'
import Validators from '../middleware/validators.js'
import config from '../../../../config/index.js'
class NostrRouter {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating NostrRouter REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating NostrRouter REST Controller.'
)
}
const dependencies = {
adapters: this.adapters,
useCases: this.useCases
}
// Encapsulate dependencies.
this.config = config
this.nostrRESTController = new NostrRESTControllerLib(dependencies)
this.validators = new Validators()
// Instantiate the router and set the base route.
const baseUrl = '/nostr'
this.router = new Router({ prefix: baseUrl })
this.attach = this.attach.bind(this)
this.createDeletedChat = this.createDeletedChat.bind(this)
this.createDeletedPost = this.createDeletedPost.bind(this)
}
attach (app) {
if (!app) {
throw new Error(
'Must pass app object when attaching REST API controllers.'
)
}
// Define the routes and attach the controller.
this.router.post('/deletedChat', this.createDeletedChat)
this.router.get('/deletedChat', this.nostrRESTController.getDeletedChats)
this.router.post('/deletedPost', this.createDeletedPost)
this.router.get('/deletedPost', this.nostrRESTController.getDeletedPosts)
// Attach the Controller routes to the Koa app.
app.use(this.router.routes())
app.use(this.router.allowedMethods())
}
async createDeletedChat (ctx, next) {
await this.validators.ensureAdmin(ctx, next)
await this.nostrRESTController.createDeletedChat(ctx, next)
return true
}
async createDeletedPost (ctx, next) {
await this.validators.ensureAdmin(ctx, next)
await this.nostrRESTController.createDeletedPost(ctx, next)
return true
}
}
export default NostrRouter
+359 -15
View File
@@ -1,11 +1,12 @@
/* /*
REST API Controller library for the /offer route REST API Controller library for the /offer route
TODO:
- Add api-doc documentation for all endpoints in this file.
*/ */
import wlogger from '../../../adapters/wlogger.js' import wlogger from '../../../adapters/wlogger.js'
let _this
class OfferRESTControllerLib { class OfferRESTControllerLib {
constructor (localConfig = {}) { constructor (localConfig = {}) {
// Dependency Injection. // Dependency Injection.
@@ -26,17 +27,27 @@ class OfferRESTControllerLib {
this.OfferModel = this.adapters.localdb.Offer this.OfferModel = this.adapters.localdb.Offer
// this.userUseCases = this.useCases.user // this.userUseCases = this.useCases.user
_this = this // Bind 'this' object to all subfunctions.
this.createOffer = this.createOffer.bind(this)
this.listOffers = this.listOffers.bind(this)
this.listNftOffers = this.listNftOffers.bind(this)
this.listFungibleOffers = this.listFungibleOffers.bind(this)
this.takeOffer = this.takeOffer.bind(this)
this.listOffersByAddress = this.listOffersByAddress.bind(this)
this.syncOfferMutableData = this.syncOfferMutableData.bind(this)
this.listCounterOffersByAddress = this.listCounterOffersByAddress.bind(this)
this.handleError = this.handleError.bind(this)
} }
// No api-doc documentation because this wont be a public endpoint // No api-doc documentation because this wont be a public endpoint
// This function has no endpoint
async createOffer (ctx) { async createOffer (ctx) {
try { try {
console.log('body: ', ctx.request.body) console.log('body: ', ctx.request.body)
const offerObj = ctx.request.body const offerObj = ctx.request.body
await _this.useCases.offer.createOffer(offerObj) await this.useCases.offer.createOffer(offerObj)
ctx.body = { ctx.body = {
success: true success: true
@@ -45,9 +56,68 @@ class OfferRESTControllerLib {
// console.log(`err.message: ${err.message}`) // console.log(`err.message: ${err.message}`)
// console.log('err: ', err) // console.log('err: ', err)
// ctx.throw(422, err.message) // ctx.throw(422, err.message)
_this.handleError(ctx, err) this.handleError(ctx, err)
} }
} }
/**
* @api {get} /offer/list/all/:page Get all offers
* @apiPermission public
* @apiName ListOffers
* @apiGroup REST Offer
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5000/offer/list/all/0
*
* @apiSuccess {Object[]} offers Array of offer objects
* @apiSuccess {ObjectId} offers._id Offer id
* @apiSuccess {String} offers.ticker Ticker
* @apiSuccess {String} offers.tokenId Token id
* @apiSuccess {String} offers.utxoTxid Utxo transaction id
* @apiSuccess {Number} offers.utxoVout Utxo output index
* @apiSuccess {String} offers.buyOrSell Buy or sell
* @apiSuccess {Number} offers.numTokens Number of tokens
* @apiSuccess {String} offers.rateInBaseUnit Rate in base unit
* @apiSuccess {String} offers.minUnitsToExchange Minimum units to exchange
* @apiSuccess {String} offers.offerStatus Offer status
* @apiSuccess {String} offers.makerAddr Maker address
* @apiSuccess {Number} offers.timestamp Timestamp
* @apiSuccess {String} offers.displayCategory Display category
* @apiSuccess {Boolean} offers.nsfw Nsfw
* @apiSuccess {Array} offers.flags Flags
* @apiSuccess {Number} offers.messageType Message type
* @apiSuccess {Number} offers.messageClass Message class
* @apiSuccess {String} offers.nostrEventId Nostr event id
* @apiSuccess {String} offers.operatorAddress Operator address
* @apiSuccess {Number} offers.operatorPercentage Operator percentage
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "offers": [{
* "_id": "56bd1da600a526986cf65c80",
* "tokenId": "1234567890",
* "utxoTxid": "1234567890",
* "utxoVout": 0,
* "buyOrSell": "buy",
* "numTokens": 1,
* "rateInBaseUnit": "100000000",
* "minUnitsToExchange": "100000000",
* "offerStatus": "active",
* "makerAddr": "1234567890",
* "timestamp": 1716883200,
* "displayCategory": "nft",
* "nsfw": false,
* "flags": [],
* "messageType": 1,
* "messageClass": 1,
* "nostrEventId": "1234567890",
* "operatorAddress": "1234567890",
* "operatorPercentage": 100
* }]
* }
*
* @apiUse TokenError
*/
// curl -X GET http://localhost:5700/offer/list/all/0 // curl -X GET http://localhost:5700/offer/list/all/0
async listOffers (ctx) { async listOffers (ctx) {
@@ -55,59 +125,333 @@ class OfferRESTControllerLib {
let page = ctx.params.page let page = ctx.params.page
if (!page) page = 0 if (!page) page = 0
const offers = await _this.useCases.offer.listOffers(page) const offers = await this.useCases.offer.listOffers(page)
ctx.body = offers ctx.body = offers
} catch (err) { } catch (err) {
console.log('Error in listOffers REST API handler: ', err) console.log('Error in listOffers REST API handler: ', err)
_this.handleError(ctx, err) this.handleError(ctx, err)
} }
} }
/**
* @api {get} /offer/list/nft/:page Get all nft offers
* @apiPermission public
* @apiName ListNftOffers
* @apiGroup REST Offer
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5000/offer/list/nft/0
*
* @apiSuccess {Object[]} offers Array of offer objects
* @apiSuccess {ObjectId} offers._id Offer id
* @apiSuccess {String} offers.ticker Ticker
* @apiSuccess {String} offers.tokenId Token id
* @apiSuccess {String} offers.utxoTxid Utxo transaction id
* @apiSuccess {Number} offers.utxoVout Utxo output index
* @apiSuccess {String} offers.buyOrSell Buy or sell
* @apiSuccess {Number} offers.numTokens Number of tokens
* @apiSuccess {String} offers.rateInBaseUnit Rate in base unit
* @apiSuccess {String} offers.minUnitsToExchange Minimum units to exchange
* @apiSuccess {String} offers.offerStatus Offer status
* @apiSuccess {String} offers.makerAddr Maker address
* @apiSuccess {Number} offers.timestamp Timestamp
* @apiSuccess {String} offers.displayCategory Display category
* @apiSuccess {Boolean} offers.nsfw Nsfw
* @apiSuccess {Array} offers.flags Flags
* @apiSuccess {Number} offers.messageType Message type
* @apiSuccess {Number} offers.messageClass Message class
* @apiSuccess {String} offers.nostrEventId Nostr event id
* @apiSuccess {String} offers.operatorAddress Operator address
* @apiSuccess {Number} offers.operatorPercentage Operator percentage
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "offers": [{
* "_id": "56bd1da600a526986cf65c80",
* "tokenId": "1234567890",
* "utxoTxid": "1234567890",
* "utxoVout": 0,
* "buyOrSell": "buy",
* "numTokens": 1,
* "rateInBaseUnit": "100000000",
* "minUnitsToExchange": "100000000",
* "offerStatus": "active",
* "makerAddr": "1234567890",
* "timestamp": 1716883200,
* "displayCategory": "nft",
* "nsfw": false,
* "flags": [],
* "messageType": 1,
* "messageClass": 1,
* "nostrEventId": "1234567890",
* "operatorAddress": "1234567890",
* "operatorPercentage": 100
* }]
* }
*
* @apiUse TokenError
*/
// curl -X GET http://localhost:5700/offer/list/nft/0 // curl -X GET http://localhost:5700/offer/list/nft/0
async listNftOffers (ctx) { async listNftOffers (ctx) {
try { try {
let page = ctx.params.page let page = ctx.params.page
if (!page) page = 0 if (!page) page = 0
const offers = await _this.useCases.offer.listNftOffers(page) const offers = await this.useCases.offer.listNftOffers(page)
ctx.body = offers ctx.body = offers
} catch (err) { } catch (err) {
console.log('Error in listNftOffers REST API handler: ', err) console.log('Error in listNftOffers REST API handler: ', err)
_this.handleError(ctx, err) this.handleError(ctx, err)
} }
} }
/**
* @api {get} /offer/list/fungible/:page Get all fungible offers
* @apiPermission public
* @apiName ListFungibleOffers
* @apiGroup REST Offer
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5000/offer/list/fungible/0
*
* @apiSuccess {Object[]} offers Array of offer objects
* @apiSuccess {ObjectId} offers._id Offer id
* @apiSuccess {String} offers.ticker Ticker
* @apiSuccess {String} offers.tokenId Token id
* @apiSuccess {String} offers.utxoTxid Utxo transaction id
* @apiSuccess {Number} offers.utxoVout Utxo output index
* @apiSuccess {String} offers.buyOrSell Buy or sell
* @apiSuccess {Number} offers.numTokens Number of tokens
* @apiSuccess {String} offers.rateInBaseUnit Rate in base unit
* @apiSuccess {String} offers.minUnitsToExchange Minimum units to exchange
* @apiSuccess {String} offers.offerStatus Offer status
* @apiSuccess {String} offers.makerAddr Maker address
* @apiSuccess {Number} offers.timestamp Timestamp
* @apiSuccess {String} offers.displayCategory Display category
* @apiSuccess {Boolean} offers.nsfw Nsfw
* @apiSuccess {Array} offers.flags Flags
* @apiSuccess {Number} offers.messageType Message type
* @apiSuccess {Number} offers.messageClass Message class
* @apiSuccess {String} offers.nostrEventId Nostr event id
* @apiSuccess {String} offers.operatorAddress Operator address
* @apiSuccess {Number} offers.operatorPercentage Operator percentage
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "offers": [{
* "_id": "56bd1da600a526986cf65c80",
* "tokenId": "1234567890",
* "utxoTxid": "1234567890",
* "utxoVout": 0,
* "buyOrSell": "buy",
* "numTokens": 1,
* "rateInBaseUnit": "100000000",
* "minUnitsToExchange": "100000000",
* "offerStatus": "active",
* "makerAddr": "1234567890",
* "timestamp": 1716883200,
* "displayCategory": "fungible",
* "nsfw": false,
* "flags": [],
* "messageType": 1,
* "messageClass": 1,
* "nostrEventId": "1234567890",
* "operatorAddress": "1234567890",
* "operatorPercentage": 100
* }]
* }
*
* @apiUse TokenError
*/
// curl -X GET http://localhost:5700/offer/list/fungible/0 // curl -X GET http://localhost:5700/offer/list/fungible/0
async listFungibleOffers (ctx) { async listFungibleOffers (ctx) {
try { try {
let page = ctx.params.page let page = ctx.params.page
if (!page) page = 0 if (!page) page = 0
const offers = await _this.useCases.offer.listFungibleOffers(page) const offers = await this.useCases.offer.listFungibleOffers(page)
ctx.body = offers ctx.body = offers
} catch (err) { } catch (err) {
console.log('Error in Fungible REST API handler: ', err) console.log('Error in Fungible REST API handler: ', err)
_this.handleError(ctx, err) this.handleError(ctx, err)
} }
} }
/**
* @api {post} /offer/take Take an offer
* @apiPermission public
* @apiName TakeOffer
* @apiGroup REST Offer
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "nostrEventId": "1234567890" }' localhost:5010/offer/take
*
* @apiSuccess {String} eventId Event id
* @apiSuccess {String} noteId Note id
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "eventId": "1234567890",
* "noteId": "1234567890"
* }
*
* @apiUse TokenError
*/
// Currently only supports 'sell' offers, and will only buy the 'numTokens' // Currently only supports 'sell' offers, and will only buy the 'numTokens'
// listed in the offer. // listed in the offer.
async takeOffer (ctx) { async takeOffer (ctx) {
try { try {
console.log('REST API controller, body: ', ctx.request.body) console.log('REST API controller, body: ', ctx.request.body)
const offerCid = ctx.request.body.offerCid const nostrEventId = ctx.request.body.nostrEventId
const hash = await _this.useCases.offer.takeOffer(offerCid) const { eventId, noteId } = await this.useCases.offer.takeOffer(nostrEventId)
ctx.body = { hash } ctx.body = { eventId, noteId }
} catch (err) { } catch (err) {
wlogger.error('Error in takeOffer() REST API handler.') wlogger.error('Error in takeOffer() REST API handler.')
_this.handleError(ctx, err) this.handleError(ctx, err)
}
}
/**
* @api {get} /offer/list/addr/:addr List all offers being made by a given address
* @apiPermission public
* @apiName ListOffersByAddress
* @apiGroup REST Offer
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5000/offer/list/addr/bitcoincash:qrpxtnrlhfz9wsuuse7z5k2mxmvw0r3pu5qepyhmq2
*
*
* @apiSuccess {Object[]} offers Array of offer objects
* @apiSuccess {ObjectId} offers._id Offer id
* @apiSuccess {String} offers.ticker Ticker
* @apiSuccess {String} offers.tokenId Token id
* @apiSuccess {String} offers.utxoTxid Utxo transaction id
* @apiSuccess {Number} offers.utxoVout Utxo output index
* @apiSuccess {String} offers.buyOrSell Buy or sell
* @apiSuccess {Number} offers.numTokens Number of tokens
* @apiSuccess {String} offers.rateInBaseUnit Rate in base unit
* @apiSuccess {String} offers.minUnitsToExchange Minimum units to exchange
* @apiSuccess {String} offers.offerStatus Offer status
* @apiSuccess {String} offers.makerAddr Maker address
* @apiSuccess {Number} offers.timestamp Timestamp
* @apiSuccess {String} offers.displayCategory Display category
* @apiSuccess {Boolean} offers.nsfw Nsfw
* @apiSuccess {Array} offers.flags Flags
* @apiSuccess {Number} offers.messageType Message type
* @apiSuccess {Number} offers.messageClass Message class
* @apiSuccess {String} offers.nostrEventId Nostr event id
* @apiSuccess {String} offers.operatorAddress Operator address
* @apiSuccess {Number} offers.operatorPercentage Operator percentage
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "offers": [{
* "_id": "56bd1da600a526986cf65c80",
* "tokenId": "1234567890",
* "utxoTxid": "1234567890",
* "utxoVout": 0,
* "buyOrSell": "buy",
* "numTokens": 1,
* "rateInBaseUnit": "100000000",
* "minUnitsToExchange": "100000000",
* "offerStatus": "active",
* "makerAddr": "bitcoincash:qrpxtnrlhfz9wsuuse7z5k2mxmvw0r3pu5qepyhmq2",
* "timestamp": 1716883200,
* "displayCategory": "fungible",
* "nsfw": false,
* "flags": [],
* "messageType": 1,
* "messageClass": 1,
* "nostrEventId": "1234567890",
* "operatorAddress": "1234567890",
* "operatorPercentage": 100
* }]
* }
*
* @apiUse TokenError
*/
// List all offers being made by a given address.
// curl -X GET http://localhost:5700/offer/list/addr/bitcoincash:qrpxtnrlhfz9wsuuse7z5k2mxmvw0r3pu5qepyhmq2
async listOffersByAddress (ctx) {
try {
const addr = ctx.params.addr
const offers = await this.useCases.offer.listOffersByAddress(addr)
ctx.body = offers
} catch (err) {
console.log('Error in listOffersByAddress REST API handler: ', err)
this.handleError(ctx, err)
}
}
async syncOfferMutableData (ctx) {
try {
const tokenId = ctx.request.body.tokenId
const offer = await this.useCases.offer.syncOfferMutableData(tokenId)
ctx.body = offer
} catch (err) {
console.log('Error in syncOfferMutableData REST API handler: ', err)
this.handleError(ctx, err)
}
}
/**
* @api {get} /offer/list/counter-offer/:addr List all counter offers being made by a given address
* @apiPermission public
* @apiName listCounterOffersByAddress
* @apiGroup REST Offer
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5000/offer/list/counter-offer/bitcoincash:qq54a3xyyptty63ztlcavzzh64gy4d247qs4ueaupe
*
*
* @apiSuccess {String} offers.nostrEventId Nostr event id
* @apiSuccess {String} offers.takerNpub Nostr Npub.
* @apiSuccess {String} offers.takerAddr BCH Address.
* @apiSuccess {String} offers.counterOfferAddr BCH Address
* @apiSuccess {String} offers.counterOfferUtxo Utxo txid.
*
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "counterOffers": [{
* "_id": "56bd1da600a526986cf65c80",
* "nostrEventId": "1234567890",
* "takerAddr": "bitcoincash:qq54a3xyyptty63ztlcavzzh64gy4d247qs4ueaupe",
* "takerNpub": "npub1nhc47z2tf42a4ps7plzcyax2j76gdausve763tqw3xuw7jjd7ceqppquly",
* "counterOfferAddr": "bitcoincash:qzltx40ldxr53edchprppj5lpg4q7vl20usx25ga44",
* "counterOfferUtxo": "71dab5b81cf06f3495aeebcc70fb351109e5e88bc85da67d0d144c0e5ca0bd6a",
* }]
* }
*
* @apiUse TokenError
*/
async listCounterOffersByAddress (ctx) {
try {
const addr = ctx.params.addr
const counterOffers = await this.useCases.offer.listCounterOffersByAddress(addr)
ctx.body = { counterOffers }
} catch (err) {
console.log('Error in listCounterOffersByAddress REST API handler: ', err)
this.handleError(ctx, err)
} }
} }
+17 -10
View File
@@ -8,8 +8,6 @@ import Router from 'koa-router'
// Local libraries. // Local libraries.
import OfferRESTControllerLib from './controller.js' import OfferRESTControllerLib from './controller.js'
let _this
class OfferRouter { class OfferRouter {
constructor (localConfig = {}) { constructor (localConfig = {}) {
// Dependency Injection. // Dependency Injection.
@@ -38,7 +36,8 @@ class OfferRouter {
const baseUrl = '/offer' const baseUrl = '/offer'
this.router = new Router({ prefix: baseUrl }) this.router = new Router({ prefix: baseUrl })
_this = this // Bind 'this' object to all subfunctions.
this.attach = this.attach.bind(this)
} }
attach (app) { attach (app) {
@@ -48,16 +47,24 @@ class OfferRouter {
) )
} }
// 12/3/24 CT:
// Note: The createOffer() path was used by a P2WDB webhook to generate an
// Offer from an Order. This has been deprecated and Offers are now created
// by a Timer Controller monitoring a Nostr topic.
// Define the routes and attach the controller. // Define the routes and attach the controller.
this.router.post('/', _this.offerRESTController.createOffer) // this.router.post('/', _this.offerRESTController.createOffer) // Deprecated.
this.router.post('/take', _this.offerRESTController.takeOffer) this.router.post('/take', this.offerRESTController.takeOffer)
this.router.get('/list/all/:page', _this.offerRESTController.listOffers) this.router.post('/mutable/sync/', this.offerRESTController.syncOfferMutableData)
this.router.get('/list/nft/:page', _this.offerRESTController.listNftOffers) this.router.get('/list/all/:page', this.offerRESTController.listOffers)
this.router.get('/list/fungible/:page', _this.offerRESTController.listFungibleOffers) this.router.get('/list/nft/:page', this.offerRESTController.listNftOffers)
this.router.get('/list/fungible/:page', this.offerRESTController.listFungibleOffers)
this.router.get('/list/addr/:addr', this.offerRESTController.listOffersByAddress)
this.router.get('/list/counter-offer/:addr', this.offerRESTController.listCounterOffersByAddress)
// Attach the Controller routes to the Koa app. // Attach the Controller routes to the Koa app.
app.use(_this.router.routes()) app.use(this.router.routes())
app.use(_this.router.allowedMethods()) app.use(this.router.allowedMethods())
} }
} }
+112 -5
View File
@@ -29,17 +29,40 @@ class OrderRESTControllerLib {
_this = this _this = this
} }
// No api-doc documentation because this wont be a public endpoint /**
* @api {post} /order Create an order
* @apiPermission User
* @apiName CreateOrder
* @apiGroup REST Order
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{"messageType": "1", "messageClass": "1", "tokenId": "1234567890", "buyOrSell": "buy", "rateInBaseUnit": "100000000", "minUnitsToExchange": "100000000", "numTokens": "1", "makerAddr": "1234567890"}' localhost:5001/order
*
* @apiSuccess {String} eventId Event id
* @apiSuccess {String} noteId Note id
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "eventId": "1234567890",
* "noteId": "1234567890"
* }
*
* @apiUse TokenError
*/
async createOrder (ctx) { async createOrder (ctx) {
try { try {
// console.log('body: ', ctx.request.body) // console.log('body: ', ctx.request.body)
const orderObj = ctx.request.body.order const orderObj = ctx.request.body.order
const user = ctx.state.user
orderObj.userId = user._id
console.log('orderObj: ', orderObj) console.log('orderObj: ', orderObj)
const hash = await _this.useCases.order.createOrder(orderObj) const { eventId, noteId } = await _this.useCases.order.createOrder(orderObj)
ctx.body = { hash } ctx.body = { eventId, noteId }
} catch (err) { } catch (err) {
// console.log(`err.message: ${err.message}`) // console.log(`err.message: ${err.message}`)
// console.log('err: ', err) // console.log('err: ', err)
@@ -47,6 +70,68 @@ class OrderRESTControllerLib {
_this.handleError(ctx, err) _this.handleError(ctx, err)
} }
} }
/**
* @api {get} /order/list/all/:page List all orders
* @apiPermission user
* @apiName ListOrders
* @apiGroup REST Order
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5000/order/list/all/0
*
* @apiSuccess {Object[]} orders Array of order objects
* @apiSuccess {ObjectId} orders._id Order id
* @apiSuccess {String} orders.tokenId Token id
* @apiSuccess {String} orders.ticker Ticker
* @apiSuccess {String} orders.dataType Data type
* @apiSuccess {String} orders.utxoTxid Utxo transaction id
* @apiSuccess {Number} orders.utxoVout Utxo output index
* @apiSuccess {String} orders.buyOrSell Buy or sell
* @apiSuccess {Number} orders.numTokens Number of tokens
* @apiSuccess {String} orders.rateInBaseUnit Rate in base unit
* @apiSuccess {String} orders.minUnitsToExchange Minimum units to exchange
* @apiSuccess {String} orders.offerStatus Offer status
* @apiSuccess {String} orders.makerAddr Maker address
* @apiSuccess {Number} orders.timestamp Timestamp
* @apiSuccess {String} orders.displayCategory Display category
* @apiSuccess {Boolean} orders.nsfw Nsfw
* @apiSuccess {Array} orders.flags Flags
* @apiSuccess {Number} orders.messageType Message type
* @apiSuccess {Number} orders.messageClass Message class
* @apiSuccess {String} orders.nostrEventId Nostr event id
* @apiSuccess {String} orders.operatorAddress Operator address
* @apiSuccess {Number} orders.operatorPercentage Operator percentage
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "orders": [{
* "_id": "56bd1da600a526986cf65c80",
* "tokenId": "1234567890",
* "ticker": "BTC",
* "dataType": "offer",
* "utxoTxid": "1234567890",
* "utxoVout": 0,
* "buyOrSell": "buy",
* "numTokens": 1,
* "rateInBaseUnit": "100000000",
* "minUnitsToExchange": "100000000",
* "offerStatus": "active",
* "makerAddr": "1234567890",
* "timestamp": 1716883200,
* "displayCategory": "nft",
* "nsfw": false,
* "flags": [],
* "messageType": 1,
* "messageClass": 1,
* "nostrEventId": "1234567890",
* "operatorAddress": "1234567890",
* "operatorPercentage": 100
* }]
* }
*
* @apiUse TokenError
*/
// curl -X GET http://localhost:5700/order/list/all/0 // curl -X GET http://localhost:5700/order/list/all/0
async listOrders (ctx) { async listOrders (ctx) {
@@ -65,14 +150,36 @@ class OrderRESTControllerLib {
// Delete an existing order by returning the token to the root address of // Delete an existing order by returning the token to the root address of
// the DEX wallet. // the DEX wallet.
/**
* @api {post} /order/delete Delete an order
* @apiPermission user
* @apiName DeleteOrder
* @apiGroup REST Order
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "nostrEventId": "1234567890" }' localhost:5000/order/delete
*
* @apiSuccess {String} txid Transaction id
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "txid": "1234567890"
* }
*
* @apiUse TokenError
*/
// curl -X POST http://localhost:5700/order/delete -d '{ "nostrEventId": "1234567890" }'
async deleteOrder (ctx) { async deleteOrder (ctx) {
try { try {
// console.log('body: ', ctx.request.body) // console.log('body: ', ctx.request.body)
const p2wdbHash = ctx.request.body.p2wdbHash const nostrEventId = ctx.request.body.nostrEventId
// console.log('p2wdbHash: ', p2wdbHash) // console.log('p2wdbHash: ', p2wdbHash)
const txid = await _this.useCases.order.deleteOrder(p2wdbHash) const txid = await _this.useCases.order.deleteOrder(nostrEventId)
ctx.body = { txid } ctx.body = { txid }
} catch (err) { } catch (err) {
+15 -2
View File
@@ -7,6 +7,7 @@ import Router from 'koa-router'
// Local libraries. // Local libraries.
import OrderRESTControllerLib from './controller.js' import OrderRESTControllerLib from './controller.js'
import Validators from '../middleware/validators.js'
let _this let _this
@@ -32,12 +33,14 @@ class OrderRouter {
} }
// Encapsulate dependencies. // Encapsulate dependencies.
this.validators = new Validators()
this.orderRESTController = new OrderRESTControllerLib(dependencies) this.orderRESTController = new OrderRESTControllerLib(dependencies)
// Instantiate the router and set the base route. // Instantiate the router and set the base route.
const baseUrl = '/order' const baseUrl = '/order'
this.router = new Router({ prefix: baseUrl }) this.router = new Router({ prefix: baseUrl })
this.createOrder = this.createOrder.bind(this)
_this = this _this = this
} }
@@ -49,14 +52,24 @@ class OrderRouter {
} }
// Define the routes and attach the controller. // Define the routes and attach the controller.
this.router.post('/', _this.orderRESTController.createOrder) this.router.post('/', this.createOrder)
this.router.get('/list/all/:page', _this.orderRESTController.listOrders) this.router.get('/list/all/:page', _this.orderRESTController.listOrders)
this.router.post('/delete', _this.orderRESTController.deleteOrder) this.router.post('/delete', this.deleteOrder)
// Attach the Controller routes to the Koa app. // Attach the Controller routes to the Koa app.
app.use(_this.router.routes()) app.use(_this.router.routes())
app.use(_this.router.allowedMethods()) app.use(_this.router.allowedMethods())
} }
async createOrder (ctx, next) {
await _this.validators.ensureUser(ctx, next)
await _this.orderRESTController.createOrder(ctx, next)
}
async deleteOrder (ctx, next) {
await _this.validators.ensureUser(ctx, next)
await _this.orderRESTController.deleteOrder(ctx, next)
}
} }
export default OrderRouter export default OrderRouter
+20 -1
View File
@@ -31,7 +31,25 @@ class P2WDBRESTControllerLib {
_this = this _this = this
} }
// No api-doc documentation because this wont be a public endpoint /**
* @api {post} /p2wdb Route a P2WDB webhook
* @apiPermission public
* @apiName RouteWebhook
* @apiGroup REST P2WDB
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{"data": {"dataType": "offer"} } ' localhost:5001/p2wdb
*
* @apiSuccess {String} success Success message
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "success": true
* }
*/
// Note: verify if this is a private endpoint, needs to be authenticated
async routeWebhook (ctx) { async routeWebhook (ctx) {
try { try {
console.log('p2wdb REST API handler: body: ', ctx.request.body) console.log('p2wdb REST API handler: body: ', ctx.request.body)
@@ -64,6 +82,7 @@ class P2WDBRESTControllerLib {
await _this.useCases.offer.flagOffer(flagData) await _this.useCases.offer.flagOffer(flagData)
} else { } else {
console.log('Could not route P2WDB webhook data.') console.log('Could not route P2WDB webhook data.')
// NOTE: Should we throw an error here?
} }
// const orderObj = ctx.request.body.order // const orderObj = ctx.request.body.order
@@ -0,0 +1,137 @@
/*
REST API Controller library for the /order route
*/
// const { wlogger } = require('../../../adapters/wlogger')
let _this
class SmAccountRESTControllerLib {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating /sm REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating /sm REST Controller.'
)
}
// Encapsulate dependencies
this.SmAccountModel = this.adapters.localdb.SmAccount
// this.userUseCases = this.useCases.user
_this = this
}
/**
* @api {get} /sm/list/all/{page} List all Social Media Accounts
* @apiPermission public
* @apiName ListAccounts
* @apiGroup REST Social Media Accounts
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5700/sm/list/all/0
*
*/
// curl -X GET http://localhost:5700/sm/list/all/0
async listAccounts (ctx) {
try {
let page = ctx.params.page
if (!page) page = 0
const offers = await _this.useCases.smAccount.listAccounts(page)
ctx.body = offers
} catch (err) {
console.log('Error in listAccounts REST API handler: ', err)
_this.handleError(ctx, err)
}
}
/**
* @api {get} /sm/npub/{npub} Get Social Media Account by Npub
* @apiPermission public
* @apiName GetAccountByNpub
* @apiGroup REST Social Media Accounts
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5700/sm/npub/npub1y3xq402pu9aqms3khetnt5gzm54t63pzcla56wwfmwshde0ws77qkff5gc
*
*/
// curl -X GET http://localhost:5700/sm/npub/npub1y3xq402pu9aqms3khetnt5gzm54t63pzcla56wwfmwshde0ws77qkff5gc
async getAccountByNpub (ctx) {
try {
const npub = ctx.params.npub
if (!npub) {
ctx.throw(400, 'Npub is required')
}
const account = await _this.useCases.smAccount.getAccountByNpub(npub)
if (!account) {
ctx.throw(404, 'Account not found')
}
ctx.body = account
} catch (err) {
// console.log('Error in getAccountByNpub REST API handler: ', err)
_this.handleError(ctx, err)
}
}
/**
* @api {get} /sm/bchAddr/{bchAddr} Get Social Media Account by BCH address
* @apiPermission public
* @apiName GetAccountByBchAddr
* @apiGroup REST Social Media Accounts
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5700/sm/bchAddr/bitcoincash:qz6lf6gpmn3secx73g7ucfcgy8mrh3sz2y2ylk643x
*
*/
// curl -X GET http://localhost:5700/sm/bchAddr/bitcoincash:qz6lf6gpmn3secx73g7ucfcgy8mrh3sz2y2ylk643x
async getAccountByBchAddr (ctx) {
try {
const bchAddr = ctx.params.bchAddr
if (!bchAddr) {
ctx.throw(400, 'BCH address is required')
}
const account = await _this.useCases.smAccount.getAccountByBchAddr(bchAddr)
if (!account) {
ctx.throw(404, 'Account not found')
}
ctx.body = account
} catch (err) {
// console.log('Error in getAccountByBchAddr REST API handler: ', err)
_this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
console.log('err', err)
// If an HTTP status is specified by the buisiness logic, use that.
if (err.status) {
if (err.message) {
ctx.throw(err.status, err.message)
} else {
ctx.throw(err.status)
}
} else {
// By default use a 422 error if the HTTP status is not specified.
ctx.throw(422, err.message)
}
}
}
export default SmAccountRESTControllerLib
@@ -0,0 +1,64 @@
/*
REST API library for /order route.
*/
// Public npm libraries.
import Router from 'koa-router'
// Local libraries.
import SmAccountRESTControllerLib from './controller.js'
import Validators from '../middleware/validators.js'
let _this
class SmAccountRouter {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating /sm REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating /sm REST Controller.'
)
}
const dependencies = {
adapters: this.adapters,
useCases: this.useCases
}
// Encapsulate dependencies.
this.validators = new Validators()
this.smAccountRESTController = new SmAccountRESTControllerLib(dependencies)
// Instantiate the router and set the base route.
const baseUrl = '/sm'
this.router = new Router({ prefix: baseUrl })
_this = this
}
attach (app) {
if (!app) {
throw new Error(
'Must pass app object when attached REST API controllers.'
)
}
// Define the routes and attach the controller.
this.router.get('/list/all/:page', _this.smAccountRESTController.listAccounts)
this.router.get('/npub/:npub', _this.smAccountRESTController.getAccountByNpub)
this.router.get('/bchAddr/:bchAddr', _this.smAccountRESTController.getAccountByBchAddr)
// Attach the Controller routes to the Koa app.
app.use(_this.router.routes())
app.use(_this.router.allowedMethods())
}
}
export default SmAccountRouter
@@ -0,0 +1,119 @@
/*
REST API Controller library for the /usage route
*/
// Global npm libraries
// Local libraries
import wlogger from '../../../adapters/wlogger.js'
class UsageRESTControllerLib {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating /usage REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating /usage REST Controller.'
)
}
// Encapsulate dependencies
// Bind 'this' object to all subfunctions
this.getStatus = this.getStatus.bind(this)
this.getTopIps = this.getTopIps.bind(this)
this.getTopEndpoints = this.getTopEndpoints.bind(this)
this.handleError = this.handleError.bind(this)
}
/**
* @api {get} /usage Get status on IPFS infrastructure
* @apiPermission public
* @apiName GetUsageStatus
* @apiGroup REST Usage
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage
*
*/
getStatus (ctx) {
try {
// const status = await this.adapters.ipfs.getStatus()
const status = this.useCases.usage.getRestSummary()
ctx.body = { status }
} catch (err) {
wlogger.error('Error in usage/controller.js/getStatus(): ')
// ctx.throw(422, err.message)
this.handleError(ctx, err)
}
}
/**
* @api {get} /usage/ips Get top IP addresses consuming the REST API
* @apiPermission public
* @apiName GetUsageIPs
* @apiGroup REST Usage
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage/ips
*
*/
getTopIps (ctx) {
try {
const ips = this.useCases.usage.getTopIps()
ctx.body = { ips }
} catch (err) {
wlogger.error('Error in usage/controller.js/getTopIps(): ')
// ctx.throw(422, err.message)
this.handleError(ctx, err)
}
}
/**
* @api {get} /usage/endpoints Get top endpoints consumed from the REST API
* @apiPermission public
* @apiName GetUsageEndpoints
* @apiGroup REST Usage
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage/endpoints
*
*/
getTopEndpoints (ctx) {
try {
const endpoints = this.useCases.usage.getTopEndpoints()
ctx.body = { endpoints }
} catch (err) {
wlogger.error('Error in usage/controller.js/getTopEndpoints(): ')
// ctx.throw(422, err.message)
this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
// If an HTTP status is specified by the buisiness logic, use that.
if (err.status) {
if (err.message) {
ctx.throw(err.status, err.message)
} else {
ctx.throw(err.status)
}
} else {
// By default use a 422 error if the HTTP status is not specified.
ctx.throw(422, err.message)
}
}
}
// module.exports = IpfsRESTControllerLib
export default UsageRESTControllerLib
+65
View File
@@ -0,0 +1,65 @@
/*
REST API library for the /usage route.
*/
// Public npm libraries.
import Router from 'koa-router'
// Local libraries.
import UsageRESTControllerLib from './controller.js'
import Validators from '../middleware/validators.js'
// let _this
class UsageRouter {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating IPFS REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating IPFS REST Controller.'
)
}
const dependencies = {
adapters: this.adapters,
useCases: this.useCases
}
// Encapsulate dependencies.
this.usageRESTController = new UsageRESTControllerLib(dependencies)
this.validators = new Validators()
// Instantiate the router and set the base route.
const baseUrl = '/usage'
this.router = new Router({ prefix: baseUrl })
// _this = this
}
attach (app) {
if (!app) {
throw new Error(
'Must pass app object when attaching REST API controllers.'
)
}
// Define the routes and attach the controller.
this.router.get('/', this.usageRESTController.getStatus)
this.router.get('/ips', this.usageRESTController.getTopIps)
this.router.get('/endpoints', this.usageRESTController.getTopEndpoints)
// Attach the Controller routes to the Koa app.
app.use(this.router.routes())
app.use(this.router.allowedMethods())
}
}
// module.exports = BchRouter
export default UsageRouter
+14 -3
View File
@@ -10,6 +10,8 @@ import UserRESTControllerLib from './controller.js'
import Validators from '../middleware/validators.js' import Validators from '../middleware/validators.js'
import config from '../../../../config/index.js'
let _this let _this
class UserRouter { class UserRouter {
@@ -34,6 +36,7 @@ class UserRouter {
} }
// Encapsulate dependencies. // Encapsulate dependencies.
this.config = config
this.userRESTController = new UserRESTControllerLib(dependencies) this.userRESTController = new UserRESTControllerLib(dependencies)
this.validators = new Validators() this.validators = new Validators()
@@ -52,7 +55,7 @@ class UserRouter {
} }
// Define the routes and attach the controller. // Define the routes and attach the controller.
this.router.post('/', this.userRESTController.createUser) this.router.post('/', this.createUser)
this.router.get('/', this.getAll) this.router.get('/', this.getAll)
this.router.get('/:id', this.getById) this.router.get('/:id', this.getById)
this.router.put('/:id', this.updateUser) this.router.put('/:id', this.updateUser)
@@ -63,13 +66,21 @@ class UserRouter {
app.use(this.router.allowedMethods()) app.use(this.router.allowedMethods())
} }
async createUser (ctx, next) {
if (process.env.DISABLE_NEW_ACCOUNTS) {
await _this.validators.ensureAdmin(ctx, next)
}
await _this.userRESTController.createUser(ctx, next)
return true
}
async getAll (ctx, next) { async getAll (ctx, next) {
await _this.validators.ensureUser(ctx, next) await _this.validators.ensureAdmin(ctx, next)
await _this.userRESTController.getUsers(ctx, next) await _this.userRESTController.getUsers(ctx, next)
} }
async getById (ctx, next) { async getById (ctx, next) {
await _this.validators.ensureUser(ctx, next) await _this.validators.ensureTargetUserOrAdmin(ctx, next)
await _this.userRESTController.getUser(ctx, next) await _this.userRESTController.getUser(ctx, next)
} }
+140 -6
View File
@@ -1,6 +1,6 @@
/* /*
This Controller library is concerned with timer-based functions that are This Controller library is concerned with timer-based functions that are
kicked off periodicially. kicked off periodically.
*/ */
import config from '../../config/index.js' import config from '../../config/index.js'
@@ -22,6 +22,15 @@ class TimerControllers {
} }
this.debugLevel = localConfig.debugLevel this.debugLevel = localConfig.debugLevel
// Constants
this.cleanUsageInterval = 60000 * 60 // 1 hour
this.backupUsageInterval = 60000 * 10 // 10 minutes
this.newSmAccountsInterval = 60000 * 60 // 60 minutes
this.updateSmAccountsInterval = 60000 * 10 // 10 minutes
this.removeOlderDeletedChatsAndPostsInterval = 60000 * 60 * 1 // 1 hour
// Encapsulate dependencies
this.config = config this.config = config
// Bind 'this' object to all subfunctions // Bind 'this' object to all subfunctions
@@ -29,35 +38,78 @@ class TimerControllers {
this.gcOrders = this.gcOrders.bind(this) this.gcOrders = this.gcOrders.bind(this)
this.gcOffers = this.gcOffers.bind(this) this.gcOffers = this.gcOffers.bind(this)
this.checkDupOffers = this.checkDupOffers.bind(this) this.checkDupOffers = this.checkDupOffers.bind(this)
this.loadOffers = this.loadOffers.bind(this)
this.cleanUsage = this.cleanUsage.bind(this)
this.backupUsage = this.backupUsage.bind(this)
this.newSmAccounts = this.newSmAccounts.bind(this)
this.updateSmAccounts = this.updateSmAccounts.bind(this)
this.removeOlderDeletedChatsAndPosts = this.removeOlderDeletedChatsAndPosts.bind(this)
// State // State
this.gcOrdersInt = null this.gcOrdersInt = null
this.gcOffersInt = null this.gcOffersInt = null
this.checkDupOffersInt = null this.checkDupOffersInt = null
this.loadOffersInt = null
this.startTimers() this.newSmAccountsInt = null
this.updateSmAccountsInt = null
this.removeOlderDeletedChatsAndPostsInt = null
} }
// Start all the time-based controllers. // Start all the time-based controllers.
startTimers () { startTimers () {
this.gcOrdersInt = setInterval(this.gcOrders, 60000 * 5) this.gcOrdersInt = setInterval(this.gcOrders, 60000 * 5)
this.gcOffersInt = setInterval(this.gcOffers, 60000 * 5) this.gcOffersInt = setInterval(this.gcOffers, 60000 * 5)
this.checkDupOffersInt = setInterval(this.checkDupOffers, 60000 * 4.5) // this.checkDupOffersInt = setInterval(this.checkDupOffers, 60000 * 4.5)
this.loadOffersInt = setInterval(this.loadOffers, 60000 * 2)
this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval)
this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval)
this.newSmAccountsInt = setInterval(this.newSmAccounts, this.newSmAccountsInterval)
this.updateSmAccountsInt = setInterval(this.updateSmAccounts, this.updateSmAccountsInterval)
this.removeOlderDeletedChatsAndPostsInt = setInterval(this.removeOlderDeletedChatsAndPosts, this.removeOlderDeletedChatsAndPostsInterval)
return true
} }
stopTimers () { stopTimers () {
clearInterval(this.gcOrdersInt) clearInterval(this.gcOrdersInt)
clearInterval(this.gcOffersInt) clearInterval(this.gcOffersInt)
clearInterval(this.checkDupOffers) // clearInterval(this.checkDupOffers)
clearInterval(this.optimizeWalletHandle)
clearInterval(this.cleanUsageHandle)
clearInterval(this.backupUsageHandle)
clearInterval(this.newSmAccountsInt)
clearInterval(this.updateSmAccountsInt)
clearInterval(this.removeOlderDeletedChatsAndPostsInt)
} }
// Garbage Collect the Orders. // Garbage Collect the Orders.
gcOrders () { gcOrders () {
try { try {
this.useCases.order.removeStaleOrders() this.useCases.order.removeStaleOrders()
return true
} catch (err) { } catch (err) {
// Do not throw an error. This is a top-level function. // Do not throw an error. This is a top-level function.
console.log('Error in timer-controllers.js/gcOrders(): ', err) console.log('Error in timer-controllers.js/gcOrders(): ', err)
return false
}
}
// Clean the usage state so that stats reflect the last 24 hours.
cleanUsage () {
try {
clearInterval(this.cleanUsageHandle)
const now = new Date()
console.log(`cleanUsage() Timer Controller executing at ${now.toLocaleString()}`)
this.useCases.usage.cleanUsage()
this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval)
return true
} catch (err) {
console.error('Error in time-controller.js/cleanUsage(): ', err)
this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval)
return false
} }
} }
@@ -65,9 +117,11 @@ class TimerControllers {
gcOffers () { gcOffers () {
try { try {
this.useCases.offer.removeStaleOffers() this.useCases.offer.removeStaleOffers()
return true
} catch (err) { } catch (err) {
// Do not throw an error. This is a top-level function. // Do not throw an error. This is a top-level function.
console.log('Error in timer-controllers.js/gcOffers(): ', err) console.log('Error in timer-controllers.js/gcOffers(): ', err)
return false
} }
} }
@@ -75,9 +129,89 @@ class TimerControllers {
checkDupOffers () { checkDupOffers () {
try { try {
this.useCases.offer.removeDuplicateOffers() this.useCases.offer.removeDuplicateOffers()
return true
} catch (err) { } catch (err) {
// Do not throw an error. This is a top-level function. // Do not throw an error. This is a top-level function.
console.log('Error in timer-controllers.js/checkDupOffers(): ', err) console.log('Error in timer-controllers.js/checkDupOffers(): ', err)
return false
}
}
// Load offers From nostr .
async loadOffers () {
try {
await this.useCases.offer.loadOffers()
return true
} catch (err) {
// Do not throw an error. This is a top-level function.
console.log('Error in timer-controllers.js/loadOffers(): ', err)
return false
}
}
// Backup the usage stats to the database
async backupUsage () {
try {
clearInterval(this.backupUsageHandle)
console.log('backupUsage() Timer Controller executing at ', new Date().toLocaleString())
// Clear the database of old usage data.
await this.useCases.usage.clearUsage()
// Save the current usage snapshot to the database.
await this.useCases.usage.saveUsage()
this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval)
return true
} catch (err) {
console.error('Error in time-controller.js/backupUsage(): ', err)
this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval)
// Note: Do not throw an error. This is a top-level function.
return false
}
}
// Check for new Social Media Accounts.
async newSmAccounts () {
try {
await this.useCases.smAccount.checkForNewSmAccounts()
return true
} catch (err) {
console.error('Error in time-controller.js/newSmAccounts(): ', err)
// Note: Do not throw an error. This is a top-level function.
return false
}
}
// Update the Social Media Accounts.
async updateSmAccounts () {
try {
await this.useCases.smAccount.updateSmAccounts()
return true
} catch (err) {
console.error('Error in time-controller.js/updateSmAccounts(): ', err)
return false
}
}
// Remove older deleted chats and posts.
async removeOlderDeletedChatsAndPosts () {
try {
// console.log('removeOlderDeletedChatsAndPosts() Timer Controller executing at ', new Date().toLocaleString())
clearInterval(this.removeOlderDeletedChatsAndPostsInt)
await this.useCases.nostr.removeOlderDeletedChats()
await this.useCases.nostr.removeOlderDeletedPosts()
this.removeOlderDeletedChatsAndPostsInt = setInterval(this.removeOlderDeletedChatsAndPosts, this.removeOlderDeletedChatsAndPostsInterval)
return true
} catch (err) {
console.error('Error in time-controller.js/removeOlderDeletedChatsAndPosts(): ', err)
this.removeOlderDeletedChatsAndPostsInt = setInterval(this.removeOlderDeletedChatsAndPosts, this.removeOlderDeletedChatsAndPostsInterval)
return false
} }
} }
} }
+68
View File
@@ -0,0 +1,68 @@
/*
Counter Offer Entity
*/
class CounterOfferEntity {
validate (offerData = {}) {
console.log('counter offer data input validation ', offerData)
// Throw an error if input object does not have a data property
if (!offerData.data) {
throw new Error(
'Input to counterOffer.validate() must be an object with a data property.'
)
}
const {
nostrEventId,
takerAddr,
takerNpub,
makerNpub,
counterOfferAddr,
counterOfferUtxo,
takerOfferUtxo,
tokenId
} = offerData.data
// Input Validation
if (!nostrEventId || typeof nostrEventId !== 'string') {
throw new Error("Property 'nostrEventId' must be a string.")
}
// Next validations are optional, to allow old counter offers without the data.
if (takerAddr && typeof takerAddr !== 'string') {
throw new Error("Property 'takerAddr' must be a string.")
}
if (takerNpub && typeof takerNpub !== 'string') {
throw new Error("Property 'takerNpub' must be a string.")
}
if (counterOfferAddr && typeof counterOfferAddr !== 'string') {
throw new Error("Property 'counterOfferAddr' must be a string.")
}
if (counterOfferUtxo && typeof counterOfferUtxo !== 'string') {
throw new Error("Property 'counterOfferUtxo' must be a string.")
}
if (takerOfferUtxo && typeof takerOfferUtxo !== 'string') {
throw new Error("Property 'takerOfferUtxo' must be a string.")
}
if (tokenId && typeof tokenId !== 'string') {
throw new Error("Property 'tokenId' must be a string.")
}
if (makerNpub && typeof makerNpub !== 'string') {
throw new Error("Property 'makerNpub' must be a string.")
}
const validatedCounterOfferData = {
nostrEventId,
takerAddr,
takerNpub,
counterOfferAddr,
counterOfferUtxo,
takerOfferUtxo,
tokenId,
makerNpub
}
return validatedCounterOfferData
}
}
export default CounterOfferEntity
+17
View File
@@ -0,0 +1,17 @@
/*
DeletedChat Entity
*/
class DeletedChat {
validate (data = {}) {
const { eventId } = data
// Input Validation
if (!eventId || typeof eventId !== 'string') {
throw new Error("Property 'eventId' must be a string!")
}
return data
}
}
export default DeletedChat
+17
View File
@@ -0,0 +1,17 @@
/*
DeletedPost Entity
*/
class DeletedPost {
validate (data = {}) {
const { eventId } = data
// Input Validation
if (!eventId || typeof eventId !== 'string') {
throw new Error("Property 'eventId' must be a string!")
}
return data
}
}
export default DeletedPost
+3
View File
@@ -1,6 +1,9 @@
/* /*
Entry Entity Entry Entity
TODO: Explain what an entry is and how it works.
*/ */
class Entry { class Entry {
validate ({ validate ({
entry, entry,
+27 -3
View File
@@ -1,6 +1,6 @@
/* /*
Offer Entity Offer Entity
An ffer is created when a new Signal is detected via the P2WDB webhook. An offer is created when a new Signal is detected via the P2WDB webhook.
It's destroyed when the UTXO described in the Signal has been detected as spent. It's destroyed when the UTXO described in the Signal has been detected as spent.
*/ */
@@ -17,6 +17,8 @@ class OfferEntity {
) )
} }
// console.log('offer entity validate() offerData: ', offerData)
const { const {
messageType, messageType,
messageClass, messageClass,
@@ -29,8 +31,12 @@ class OfferEntity {
utxoVout, utxoVout,
offerStatus, offerStatus,
makerAddr, makerAddr,
makerNpub,
ticker, ticker,
tokenType tokenType,
nostrEventId,
operatorAddress,
operatorPercentage
} = offerData.data } = offerData.data
// Input Validation // Input Validation
@@ -73,6 +79,21 @@ class OfferEntity {
if (!tokenType || typeof tokenType !== 'number') { if (!tokenType || typeof tokenType !== 'number') {
throw new Error("Property 'tokenType' must be a number.") throw new Error("Property 'tokenType' must be a number.")
} }
if (!nostrEventId || typeof nostrEventId !== 'string') {
throw new Error("Property 'nostrEventId' must be a string.")
}
// Add the operator address and percentage to the offer.
if (!operatorAddress || typeof operatorAddress !== 'string') {
throw new Error("Property 'operatorAddress' must be a string.")
}
if (!operatorPercentage || typeof operatorPercentage !== 'number') {
throw new Error("Property 'operatorPercentage' must be a number.")
}
// Validate npub type if it exist
if (makerNpub && typeof makerNpub !== 'string') {
throw new Error("Property 'makerNpub' must be a string.")
}
// Convert the timestamp to a number. // Convert the timestamp to a number.
let timestamp = new Date(offerData.timestamp) let timestamp = new Date(offerData.timestamp)
@@ -96,8 +117,11 @@ class OfferEntity {
offerStatus: offerStatus || this.offerStatus[0], offerStatus: offerStatus || this.offerStatus[0],
makerAddr, makerAddr,
ticker, ticker,
tokenType tokenType,
nostrEventId,
makerNpub
} }
// console.log('offer entity validatedOfferData: ', validatedOfferData)
return validatedOfferData return validatedOfferData
} }
+7 -1
View File
@@ -1,3 +1,8 @@
/*
Entry use-case library.
TODO: Explain what an entry is and how it works.
*/
// local libraries // local libraries
import wlogger from '../adapters/wlogger.js' import wlogger from '../adapters/wlogger.js'
@@ -30,11 +35,12 @@ class EntryLib {
if (!isValidSignature) { if (!isValidSignature) {
throw new Error('Invalid signature') throw new Error('Invalid signature')
} }
// Verify psf tokens balance // Verify psf tokens balance
// const psfBalance = await this.bch.getPSFTokenBalance( // const psfBalance = await this.bch.getPSFTokenBalance(
// entryEntity.slpAddress // entryEntity.slpAddress
// ) // )
// NOTE: this.bch.getPSFTokenBalance , does not exist , should be created?
const psfBalance = 1 const psfBalance = 1
if (psfBalance < 10) { if (psfBalance < 10) {
+10
View File
@@ -4,10 +4,14 @@
https://troutsblog.com/blog/clean-architecture https://troutsblog.com/blog/clean-architecture
*/ */
// Local libraries
import UserUseCases from './user.js' import UserUseCases from './user.js'
import EntryUseCases from './entry.js' import EntryUseCases from './entry.js'
import OfferUseCases from './offer/index.js' import OfferUseCases from './offer/index.js'
import OrderUseCases from './order.js' import OrderUseCases from './order.js'
import { UsageUseCases } from './usage-use-cases.js'
import SmAccountUseCases from './smAccount-use-cases.js'
import NostrUseCases from './nostr-use-cases.js'
class UseCases { class UseCases {
constructor (localConfig = {}) { constructor (localConfig = {}) {
@@ -24,10 +28,16 @@ class UseCases {
this.order = new OrderUseCases(localConfig) this.order = new OrderUseCases(localConfig)
localConfig.order = this.order localConfig.order = this.order
this.offer = new OfferUseCases(localConfig) this.offer = new OfferUseCases(localConfig)
this.usage = new UsageUseCases(localConfig)
this.smAccount = new SmAccountUseCases(localConfig)
this.nostr = new NostrUseCases(localConfig)
} }
// Run any startup Use Cases at the start of the app. // Run any startup Use Cases at the start of the app.
async start () { async start () {
// Load the usage stats from the database
await this.usage.loadUsage()
console.log('Async Use Cases have been started.') console.log('Async Use Cases have been started.')
return true return true
+130
View File
@@ -0,0 +1,130 @@
/*
This library contains business-logic for dealing with deleted chats. Most of these
functions are called by the /nostr REST API endpoints.
*/
import wlogger from '../adapters/wlogger.js'
import DeletedChatEntity from '../entities/deletedChat.js'
import DeletedPostEntity from '../entities/deletedPost.js'
class NostrUseCases {
constructor (localConfig = {}) {
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of adapters must be passed in when instantiating Nostr Use Cases library.'
)
}
// Encapsulate dependencies
this.DeletedChatEntity = new DeletedChatEntity()
this.DeletedPostEntity = new DeletedPostEntity()
this.DeletedChatModel = this.adapters.localdb.DeletedChat
this.DeletedPostModel = this.adapters.localdb.DeletedPost
this.createDeletedChat = this.createDeletedChat.bind(this)
this.getDeletedChats = this.getDeletedChats.bind(this)
this.createDeletedPost = this.createDeletedPost.bind(this)
this.getDeletedPosts = this.getDeletedPosts.bind(this)
this.removeOlderDeletedChats = this.removeOlderDeletedChats.bind(this)
this.removeOlderDeletedPosts = this.removeOlderDeletedPosts.bind(this)
}
// Create a new deleted chat model and add it to the Mongo database.
async createDeletedChat (deletedChatObj) {
try {
// Input Validation
const deletedChatEntity = this.DeletedChatEntity.validate(deletedChatObj)
const deletedChat = new this.DeletedChatModel(deletedChatEntity)
// Save the new model to the database.
await deletedChat.save()
return deletedChat
} catch (err) {
// console.log('createDeletedChat() error: ', err)
wlogger.error('Error in lib/nostr.js/createDeletedChat()')
throw err
}
}
// Returns an array of all deleted chats models in the Mongo database.
async getDeletedChats () {
try {
// Get deleted chats models.
const deletedChats = await this.DeletedChatModel.find({})
return deletedChats
} catch (err) {
wlogger.error('Error in lib/users.js/getAllDeletedChat()')
throw err
}
}
// Create a new deleted post model and add it to the Mongo database.
async createDeletedPost (deletedPostObj) {
try {
// Input Validation
const deletedPostEntity = this.DeletedPostEntity.validate(deletedPostObj)
const deletedPost = new this.DeletedPostModel(deletedPostEntity)
// Save the new model to the database.
await deletedPost.save()
return deletedPost
} catch (err) {
wlogger.error('Error in lib/users.js/createDeletedPost()')
throw err
}
}
// Returns an array of all deleted posts models in the Mongo database.
async getDeletedPosts () {
try {
// Get deleted posts models.
const deletedPosts = await this.DeletedPostModel.find({})
return deletedPosts
} catch (err) {
wlogger.error('Error in lib/users.js/getDeletedPosts()')
throw err
}
}
// Remove deleted chats that are older than 3 months.
async removeOlderDeletedChats () {
try {
const deletedChats = await this.DeletedChatModel.find({})
const threeMonthsAgo = new Date()
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3)
for (let i = 0; i < deletedChats.length; i++) {
const deletedChat = deletedChats[i]
if (deletedChat.createdAt.getTime() < threeMonthsAgo.getTime()) {
await deletedChat.remove()
}
}
} catch (err) {
wlogger.error('Error in lib/users.js/removeOlderDeletedChats()')
throw err
}
}
// Remove deleted posts that are older than 3 months.
async removeOlderDeletedPosts () {
try {
const deletedPosts = await this.DeletedPostModel.find({})
const threeMonthsAgo = new Date()
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3)
for (let i = 0; i < deletedPosts.length; i++) {
const deletedPost = deletedPosts[i]
if (deletedPost.createdAt.getTime() < threeMonthsAgo.getTime()) {
await deletedPost.remove()
}
}
} catch (err) {
wlogger.error('Error in lib/users.js/removeOlderDeletedPosts()')
throw err
}
}
}
export default NostrUseCases
+527 -101
View File
@@ -19,6 +19,7 @@ import RetryQueue from '@chris.troutner/retry-queue'
// Local libraries // Local libraries
import OfferEntity from '../../entities/offer.js' import OfferEntity from '../../entities/offer.js'
import CounterOfferEntity from '../../entities/counterOffer.js'
import config from '../../../config/index.js' import config from '../../../config/index.js'
const DEFAULT_ENTRIES_PER_PAGE = 20 const DEFAULT_ENTRIES_PER_PAGE = 20
@@ -37,7 +38,7 @@ class OfferUseCases {
this.orderUseCase = localConfig.order this.orderUseCase = localConfig.order
if (!this.orderUseCase) { if (!this.orderUseCase) {
throw new Error( throw new Error(
'Instance of Offer Use Cases must be passed in when instantiating Offer Use Cases library.' 'Instance of Order Use Cases must be passed in when instantiating Offer Use Cases library.'
) )
} }
@@ -45,24 +46,46 @@ class OfferUseCases {
this.config = config this.config = config
this.axios = axios this.axios = axios
this.offerEntity = new OfferEntity() this.offerEntity = new OfferEntity()
this.counterOfferEntity = new CounterOfferEntity()
this.OfferModel = this.adapters.localdb.Offer this.OfferModel = this.adapters.localdb.Offer
this.CounterOfferModel = this.adapters.localdb.CounterOffer
this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 }) this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 })
// Bind 'this' object to functions // Bind 'this' object to functions
this.createOffer = this.createOffer.bind(this)
this.detectNsfw = this.detectNsfw.bind(this) this.detectNsfw = this.detectNsfw.bind(this)
this.categorizeToken = this.categorizeToken.bind(this)
this.listOffers = this.listOffers.bind(this)
this.listNftOffers = this.listNftOffers.bind(this)
this.listFungibleOffers = this.listFungibleOffers.bind(this)
this.takeOffer = this.takeOffer.bind(this)
this.ensureFunds = this.ensureFunds.bind(this)
this.findOfferByEvent = this.findOfferByEvent.bind(this)
this.findOfferByTxid = this.findOfferByTxid.bind(this)
this.acceptCounterOffer = this.acceptCounterOffer.bind(this)
this.removeDuplicateOffers = this.removeDuplicateOffers.bind(this)
this.removeStaleOffers = this.removeStaleOffers.bind(this)
this.flagOffer = this.flagOffer.bind(this)
this.loadOffers = this.loadOffers.bind(this)
this.listOffersByAddress = this.listOffersByAddress.bind(this)
this.syncOfferMutableData = this.syncOfferMutableData.bind(this)
this.listCounterOffersByAddress = this.listCounterOffersByAddress.bind(this)
// State
this.seenOffers = []
this.seenCounterOffers = []
} }
// This method is called by the POST /offer REST API controller, which is // This method is called by timer controller to load offers from a Nostr topic.
// triggered by a P2WDB webhook.
async createOffer (offerObj) { async createOffer (offerObj) {
try { try {
console.log('Use Case createOffer(offerObj): ', offerObj) // console.log('Use Case createOffer(offerObj): ', offerObj)
// Return if Offer already exists in database with the same P2WDB CID. // Return if Offer already exists in database with the same utxo transaction id.
try { try {
await this.findOfferByHash(offerObj.hash) await this.findOfferByTxid(offerObj.data.utxoTxid)
console.log('Offer already found in local database.') // console.log('Offer already found in local database.')
return false return false
} catch (err) { /* exit quietly */ } } catch (err) { /* exit quietly */ }
@@ -71,9 +94,17 @@ class OfferUseCases {
// Input Validation // Input Validation
// TODO: This is a hack. Find a better way to protect against the corner- // TODO: This is a hack. Find a better way to protect against the corner-
// case of counter-offers getting routed here. // case of counter-offers getting routed here.
if (offerObj.data.dataType === 'counter-offer') { // if (offerObj.data.dataType === 'counter-offer') {
console.log('WARN: Counter Offer innappropriately routed to createOffer()') // console.log('WARN: Counter Offer innappropriately routed to createOffer()')
// }
// Quickly skip over offers that have already been processed.
const eventId = offerObj.data.nostrEventId
if (this.seenOffers.includes(eventId)) {
// console.log(`Offer with event ID ${eventId} already processed. Skipping.`)
return false
} }
this.seenOffers.push(eventId)
// Verify that UTXO in offer is unspent. If it is spent, then ignore the // Verify that UTXO in offer is unspent. If it is spent, then ignore the
// offer. // offer.
@@ -83,33 +114,78 @@ class OfferUseCases {
} }
// const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo) // const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
const utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo) const utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
console.log('utxoStatus: ', utxoStatus) // console.log('utxoStatus: ', utxoStatus)
// if (utxoStatus === null) return false // if (utxoStatus === null) return false
if (!utxoStatus) return false if (!utxoStatus) {
console.log(`UTXO txid: ${offerObj.data.utxoTxid}, vout: ${offerObj.data.utxoVout} has been spent. Skipping.`)
return false
}
// A new offer gets a status of 'posted' // A new offer gets a status of 'posted'
offerObj.data.offerStatus = 'posted' offerObj.data.offerStatus = 'posted'
// Set timestamp
offerObj.timestamp = new Date().getTime()
const offerEntity = this.offerEntity.validate(offerObj) const offerEntity = this.offerEntity.validate(offerObj)
console.log('offerEntity: ', offerEntity) // console.log('offerEntity: ', offerEntity)
console.log(`New Offer for token ID ${offerEntity.tokenId} detected from Nostr post ${eventId}`)
// Get data about the token. // Get data about the token.
const tokenId = offerEntity.tokenId const tokenId = offerEntity.tokenId
// const tokenData = await this.adapters.wallet.bchWallet.getTokenData(tokenId) let tokenData = null
const tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTokenData, tokenId) try {
console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`) tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTokenData, tokenId)
} catch (err) {
console.error('Error in OfferUseCases/createOffer() getting token data: ', err.message)
}
// Generate a 'display category' for the token. This will allow the // Do additional data analysis if the token data was successfully retrieved.
// front end UI to figure out how to display the token. if (tokenData) {
const displayCategory = this.categorizeToken(offerEntity, tokenData) // Store the mutable and immutable data cids.
console.log('displayCategory: ', displayCategory) const mutableDataCid = tokenData.mutableData
offerEntity.displayCategory = displayCategory const immutableDataCid = tokenData.immutableData
offerEntity.mutableDataCid = mutableDataCid
offerEntity.immutableDataCid = immutableDataCid
offerEntity.tokenData = tokenData
// Detect if user set the NSFW flag. // Get the mutable data from the cid if it exists.
let nsfw = false if (mutableDataCid && typeof mutableDataCid === 'string') {
// nsfw = await this.detectNsfw(tokenData) let mutableData = null
nsfw = await this.retryQueue.addToQueue(this.detectNsfw, tokenData) try {
offerEntity.nsfw = nsfw mutableData = await this.retryQueue.addToQueue(this.adapters.wallet.cid2json, mutableDataCid)
} catch (err) {
console.error('Error in OfferUseCases/createOffer() getting mutable data: ', err.message)
}
console.log('mutableData: ', mutableData)
if (mutableData) {
try {
offerEntity.tokenIconUrl = mutableData.tokenIcon
offerEntity.tokenCategories = mutableData.category
offerEntity.tokenTags = mutableData.tags
offerEntity.lastUpdatedTokenData = new Date().getTime()
offerEntity.userDataStr = JSON.stringify(mutableData.userData)
} catch (error) {
// skip error
}
}
}
console.log('offerEntity: ', offerEntity)
// Generate a 'display category' for the token. This will allow the
// front end UI to figure out how to display the token.
const displayCategory = this.categorizeToken(offerEntity, tokenData)
console.log('displayCategory: ', displayCategory)
offerEntity.displayCategory = displayCategory
// Detect if user set the NSFW flag.
// const nsfw = false
// nsfw = await this.retryQueue.addToQueue(this.detectNsfw, tokenData)
// offerEntity.nsfw = nsfw
}
// Add offer to the local database. // Add offer to the local database.
const offerModel = new this.OfferModel(offerEntity) const offerModel = new this.OfferModel(offerEntity)
@@ -117,7 +193,7 @@ class OfferUseCases {
return true return true
} catch (err) { } catch (err) {
console.error('Error in createOffer()') console.error('\n\nError in createOffer()', err.message, '\n\n', err)
throw err throw err
} }
} }
@@ -139,16 +215,29 @@ class OfferUseCases {
// Retrieve the mutable data from Filecoin/IPFS. // Retrieve the mutable data from Filecoin/IPFS.
// const url = `https://${cid}.ipfs.w3s.link/data.json` // const url = `https://${cid}.ipfs.w3s.link/data.json`
const url = `${this.config.ipfsGateway}${cid}/data.json` let mutableData = {}
const result = await axios.get(url)
const mutableData = result.data try {
console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`) // Try the conventional data URL.
const url = `${this.config.ipfsGateway}${cid}/data.json`
const result = await this.axios.get(url)
mutableData = result.data
// console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`)
} catch (err) {
// Try the newer data URL.
const url = `${this.config.ipfsGateway}${cid}`
const result = await this.axios.get(url)
mutableData = result.data
// console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`)
}
console.log('mutableData: ', mutableData)
// Logical tests // Logical tests
const hasNsfw = !!mutableData.nsfw const hasNsfw = !!mutableData.nsfw
const nsfwSetTrue = mutableData.nsfw === true const nsfwSetTrue = mutableData.nsfw === true
const nsfwStringTrue = mutableData.nsfw === 'true' const nsfwStringTrue = mutableData.nsfw === 'true'
const nsfwDetected = hasNsfw && (nsfwSetTrue || nsfwStringTrue) const nsfwDetected = hasNsfw && (nsfwSetTrue || nsfwStringTrue)
// console.log(`hasNsfw: ${hasNsfw}, nsfwSetTrue: ${nsfwSetTrue}, nsfwStringTrue: ${nsfwStringTrue}, nsfwDetected: ${nsfwDetected}`)
if (nsfwDetected) { if (nsfwDetected) {
console.log('NSFW flag set as true') console.log('NSFW flag set as true')
@@ -207,11 +296,11 @@ class OfferUseCases {
async listOffers (page = 0) { async listOffers (page = 0) {
try { try {
const data = await this.OfferModel.find({}) const data = await this.OfferModel.find({})
// Sort entries so newest entries show first. // Sort entries so newest entries show first.
.sort('-timestamp') .sort('-timestamp')
// Skip to the start of the selected page. // Skip to the start of the selected page.
.skip(page * DEFAULT_ENTRIES_PER_PAGE) .skip(page * DEFAULT_ENTRIES_PER_PAGE)
// Only return 20 results. // Only return 20 results.
.limit(DEFAULT_ENTRIES_PER_PAGE) .limit(DEFAULT_ENTRIES_PER_PAGE)
return data return data
@@ -221,17 +310,17 @@ class OfferUseCases {
} }
} }
async listNftOffers (page = 0, nsfw = false) { /* async listNftOffers (page = 0, nsfw = false) {
try { try {
const data = await this.OfferModel.find({ const data = await this.OfferModel.find({
displayCategory: { $ne: 'fungible' }, displayCategory: { $ne: 'fungible' },
nsfw nsfw
}) })
// Sort entries so newest entries show first. // Sort entries so newest entries show first.
.sort('-timestamp') .sort('-timestamp')
// Skip to the start of the selected page. // Skip to the start of the selected page.
.skip(page * NFT_ENTRIES_PER_PAGE) .skip(page * NFT_ENTRIES_PER_PAGE)
// Only return 20 results. // Only return 20 results.
.limit(NFT_ENTRIES_PER_PAGE) .limit(NFT_ENTRIES_PER_PAGE)
// console.log('listNftOffers() returning this data: ', data) // console.log('listNftOffers() returning this data: ', data)
@@ -241,16 +330,52 @@ class OfferUseCases {
console.error('Error in use-cases/offer/listNftOffers()') console.error('Error in use-cases/offer/listNftOffers()')
throw error throw error
} }
} */
async listNftOffers (page = 0, nsfw = false) {
try {
const query = {
displayCategory: { $ne: 'fungible' },
nsfw
}
// Total query offers
const totalOffers = await this.OfferModel.countDocuments(query)
// Total pages
const totalPages = Math.ceil(totalOffers / NFT_ENTRIES_PER_PAGE)
const data = await this.OfferModel.find(query)
// Sort entries so newest entries show first.
.sort('-timestamp')
// Skip to the start of the selected page.
.skip(page * NFT_ENTRIES_PER_PAGE)
// Only return 20 results.
.limit(NFT_ENTRIES_PER_PAGE)
// console.log('listNftOffers() returning this data: ', data)
return {
data,
pagination: {
currentPage: page,
totalPages,
totalOffers,
pageSize: NFT_ENTRIES_PER_PAGE
}
}
} catch (error) {
console.error('Error in use-cases/offer/listNftOffers()')
throw error
}
} }
async listFungibleOffers (page = 0) { async listFungibleOffers (page = 0) {
try { try {
const data = await this.OfferModel.find({ displayCategory: 'fungible' }) const data = await this.OfferModel.find({ displayCategory: 'fungible' })
// Sort entries so newest entries show first. // Sort entries so newest entries show first.
.sort('-timestamp') .sort('-timestamp')
// Skip to the start of the selected page. // Skip to the start of the selected page.
.skip(page * FUNGIBLE_ENTRIES_PER_PAGE) .skip(page * FUNGIBLE_ENTRIES_PER_PAGE)
// Only return 20 results. // Only return 20 results.
.limit(FUNGIBLE_ENTRIES_PER_PAGE) .limit(FUNGIBLE_ENTRIES_PER_PAGE)
// console.log('listFungibleOffers() returning this data: ', data) // console.log('listFungibleOffers() returning this data: ', data)
@@ -270,12 +395,12 @@ class OfferUseCases {
// provide functionality to take less than the total amount of tokens offered // provide functionality to take less than the total amount of tokens offered
// (offerInfo.numTokens). Taking less than the offered amount will be added // (offerInfo.numTokens). Taking less than the offered amount will be added
// in the future. // in the future.
async takeOffer (offerCid) { async takeOffer (eventId) {
try { try {
console.log('offerCid: ', offerCid) if (!eventId || typeof eventId !== 'string') throw new Error('eventId must be a string')
// Get the Offer information // Get the Offer information
const offerInfo = await this.findOfferByHash(offerCid) const offerInfo = await this.findOfferByEvent(eventId)
console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`) console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`)
// Ensure the offer is in a 'posted' state and not already 'taken' // Ensure the offer is in a 'posted' state and not already 'taken'
@@ -288,8 +413,10 @@ class OfferUseCases {
tx_hash: offerInfo.utxoTxid, tx_hash: offerInfo.utxoTxid,
tx_pos: offerInfo.utxoVout tx_pos: offerInfo.utxoVout
} }
// Note : should be added to retry-queue?
const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo) const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
console.log('utxoStatus: ', utxoStatus) // console.log('utxoStatus: ', utxoStatus)
if (!utxoStatus) { if (!utxoStatus) {
console.log(`utxo txid: ${offerInfo.utxoTxid}, vout: ${offerInfo.utxoVout}`) console.log(`utxo txid: ${offerInfo.utxoTxid}, vout: ${offerInfo.utxoVout}`)
@@ -302,11 +429,12 @@ class OfferUseCases {
await this.ensureFunds(offerInfo) await this.ensureFunds(offerInfo)
// Get UTXOs. // Get UTXOs.
const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore // const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore
console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
// Calculate amount of sats to generate a counter offer. // Calculate amount of sats to generate a counter offer.
let satsToMove = Math.ceil(offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit)) let satsToMove = Math.ceil(offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit))
console.log('satsToMove', satsToMove, offerInfo)
if (isNaN(satsToMove)) { if (isNaN(satsToMove)) {
throw new Error('Could not calculate the amount of BCH to generate counter offer') throw new Error('Could not calculate the amount of BCH to generate counter offer')
} }
@@ -332,9 +460,9 @@ class OfferUseCases {
// Create valid Offer object // Create valid Offer object
const takenOfferInfo = Object.assign({}, offerInfo) const takenOfferInfo = Object.assign({}, offerInfo)
takenOfferInfo.partialTxHex = partialTxHex takenOfferInfo.partialTxHex = partialTxHex
delete takenOfferInfo.p2wdbHash delete takenOfferInfo.nostrEventId
delete takenOfferInfo._id delete takenOfferInfo._id
takenOfferInfo.offerHash = offerInfo.p2wdbHash takenOfferInfo.offerHash = offerInfo.nostrEventId
// Add P2WDB specific flag for signaling that this is a new offer. // Add P2WDB specific flag for signaling that this is a new offer.
takenOfferInfo.dataType = 'counter-offer' takenOfferInfo.dataType = 'counter-offer'
@@ -342,19 +470,21 @@ class OfferUseCases {
// Write offer info to the P2WDB // Write offer info to the P2WDB
// TODO: This will trigger the webhook. Find some way of triggering the // TODO: This will trigger the webhook. Find some way of triggering the
// webhook on new offers, but not on counteroffers // webhook on new offers, but not on counteroffers
const p2wdbObj = { const nostrData = {
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey, // wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
data: takenOfferInfo, data: takenOfferInfo
appId: this.config.p2wdbAppId // appId: this.config.p2wdbAppId
} }
const hash = await this.adapters.p2wdb.write(p2wdbObj) const resultEventId = await this.adapters.nostr.post(JSON.stringify(nostrData))
const noteId = this.adapters.nostr.eventId2note(resultEventId)
// Delete the Offer from the database, so that the user doesn't attempt // Delete the Offer from the database, so that the user doesn't attempt
// to take the offer more than once. // to take the offer more than once.
offerInfo.remove() // offerInfo.remove()
// Return the P2WDB CID // Return the P2WDB CID
return hash return { eventId: resultEventId, noteId }
// return 'fake-hash' // return 'fake-hash'
} catch (err) { } catch (err) {
@@ -377,9 +507,10 @@ class OfferUseCases {
await this.adapters.wallet.bchWallet.initialize() await this.adapters.wallet.bchWallet.initialize()
// Ensure the app wallet has enough funds to write to the P2WDB. // Ensure the app wallet has enough funds to write to the P2WDB.
const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey // Note : this validation should be deprecated for nostr functionality?
const canWriteToP2WDB = await this.adapters.p2wdb.checkForSufficientFunds(wif) // const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey
if (!canWriteToP2WDB) throw new Error('App wallet does not have funds for writing to the P2WDB.') // const canWriteToP2WDB = await this.adapters.p2wdb.checkForSufficientFunds(wif)
// if (!canWriteToP2WDB) throw new Error('App wallet does not have funds for writing to the P2WDB.')
if (offerEntity.buyOrSell.includes('sell')) { if (offerEntity.buyOrSell.includes('sell')) {
// Sell Offer // Sell Offer
@@ -398,7 +529,7 @@ class OfferUseCases {
throw new Error('App wallet does not control enough BCH to purchase the tokens.') throw new Error('App wallet does not control enough BCH to purchase the tokens.')
} }
// //
} else { } else {
// Buy Offer // Buy Offer
throw new Error('Buy offers are not supported yet.') throw new Error('Buy offers are not supported yet.')
@@ -417,82 +548,217 @@ class OfferUseCases {
} }
} }
async findOfferByHash (p2wdbHash) { // Retrieve an Order model from the database. Find it by its event Id.
async findOfferByEvent (nostrEventId) {
try {
if (typeof nostrEventId !== 'string' || !nostrEventId) {
throw new Error('nostrEventId must be a string')
}
const order = await this.OfferModel.findOne({ nostrEventId })
if (!order) {
throw new Error('offer not found')
}
const orderObject = order.toObject()
// return this.offerEntity.validateFromModel(offerObject)
return orderObject
} catch (err) {
console.error('Error in findOfferByEvent()')
throw err
}
}
async findOfferByTxid (utxoTxid) {
// try { // try {
if (typeof p2wdbHash !== 'string' || !p2wdbHash) { // try {
throw new Error('p2wdbHash must be a string') if (typeof utxoTxid !== 'string' || !utxoTxid) {
throw new Error('utxoTxid must be a string')
} }
const offer = await this.OfferModel.findOne({ p2wdbHash }) const offer = await this.OfferModel.findOne({ utxoTxid })
// TODO: Offer should be found by TXID, then if there is more than one
// result, they should be filtered by the vout property. That will leave
// one remaining UTXO.
if (!offer) { if (!offer) {
throw new Error('offer not found') throw new Error('offer not found')
} }
return offer return offer
// } catch (error) {
// const offerObject = offer.toObject() // // console.error('Error in use-cases/offer/findOfferByTxid(): ', error.message)
// return this.offerEntity.validateFromModel(offerObject) // throw error
// return offerObject
// } catch (err) {
// // console.error('Error in findOffer(): ', err)
// throw err
// } // }
} }
// This function is called by the P2WDB webhook REST API handler. When a // This function is called by loadOffers().
// Counter Offer is passed to bch-dex by the P2WDB, the data is then passed // It does due diligence on the Counter Offer, then signs
// to this function. It does due dilligence on the Counter Offer, then signs
// and broadcasts the transaction to accept the Counter Offer. // and broadcasts the transaction to accept the Counter Offer.
async acceptCounterOffer (p2wdbData) { async acceptCounterOffer (offerData) {
try { try {
console.log(`acceptCounterOffer() p2wdbData: ${JSON.stringify(p2wdbData, null, 2)}`) console.log('offerData', offerData)
// console.log(`acceptCounterOffer() offerData: ${JSON.stringify(offerData, null, 2)}`)
// Quickly skip over offers that have already been processed.
const eventId = offerData.data.nostrEventId
// Create counter offer entity for nostr event id if it not exist
const existingCounterOffer = await this.CounterOfferModel.findOne(({ nostrEventId: eventId }))
if (!existingCounterOffer) {
// Add taker offer utxo to counter offer data.
offerData.data.takerOfferUtxo = offerData.data.utxoTxid
const counterOEntity = this.counterOfferEntity.validate(offerData)
// Create new counter offfer model and save it.
const counterOffer = new this.CounterOfferModel(counterOEntity)
await counterOffer.save()
}
if (this.seenOffers.includes(eventId)) {
// console.log(`Offer with event ID ${eventId} already processed. Skipping.`)
return false
}
console.log(`New counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerData.data.nostrEventId)}`)
// See if this instance of bch-dex is managing the Order associated with // See if this instance of bch-dex is managing the Order associated with
// the incoming Counter Offer. // the incoming Counter Offer.
const orderHash = p2wdbData.data.offerHash
// Note : this should be handled by nostrEvent id or UtxoId?
// const orderHash = offerData.data.nostrEventId
let orderData = {} let orderData = {}
try { try {
orderData = await this.orderUseCase.findOrderByHash(orderHash) orderData = await this.orderUseCase.findOrderByUtxo(offerData)
console.log(`orderData: ${JSON.stringify(orderData, null, 2)}`) console.log(`orderData: ${JSON.stringify(orderData, null, 2)}`)
} catch (err) { } catch (err) {
console.log('Order matching this Counter Offer is not managed by this instance of bch-dex. Exiting.') console.log('Order matching this Counter Offer is not managed by this instance of bch-dex. Skipping.\n')
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
this.seenOffers.push(eventId)
return 'N/A'
}
// Verify that the UTXO for sale is valid.
let utxoStatus = null
try {
// Get the status of the UTXO associate with this Offer.
const utxo = {
tx_hash: offerData.data.utxoTxid,
tx_pos: offerData.data.utxoVout
}
// console.log(`Checking this UTXO: ${JSON.stringify(utxo, null, 2)}`)
// utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
console.log('utxoStatus: ', utxoStatus)
} catch (err) {
console.log('acceptCounterOffer() err validating UTXO: ', err)
// Handle corner case of bad-data in the Offer model.
if (err.isAxiosError) {
console.log('Error trying to contact wallet service: ', err)
return 'N/A'
} else {
return 'N/A'
}
}
// If the Offer UTXO is spent, exit.
if (utxoStatus === false) {
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
this.seenOffers.push(eventId)
console.log(`Aborting Counter Offer from Event ID ${eventId}`)
console.log(`https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(eventId)}`)
return 'N/A' return 'N/A'
} }
// Deserialize the partially signed transaction. // Deserialize the partially signed transaction.
const txHex = p2wdbData.data.partialTxHex const txHex = offerData.data.partialTxHex
const txObj = await this.adapters.wallet.deseralizeTx(txHex) const txObj = await this.adapters.wallet.deseralizeTx(txHex)
console.log(`txObj: ${JSON.stringify(txObj, null, 2)}`) console.log(`txObj: ${JSON.stringify(txObj, null, 2)}`)
// Ensure the 3rd output (vout=2) contains the required amount of BCH. // Ensure the 3rd output (vout=2) contains the required amount of BCH.
const satsToReceive = Math.ceil(orderData.numTokens * parseInt(orderData.rateInBaseUnit)) const satsToReceive = Math.ceil(orderData.numTokens * parseInt(orderData.rateInBaseUnit))
console.log('Ceil', satsToReceive)
if (isNaN(satsToReceive)) { if (isNaN(satsToReceive)) {
throw new Error('Could not calculate the amount of BCH offered in the Counter Offer') throw new Error('Could not calculate the amount of BCH offered in the Counter Offer')
} }
const satsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[2].value) const satsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[2].value)
const hasRequiredAmount = satsOut === satsToReceive const hasRequiredAmount = satsOut === satsToReceive
if (!hasRequiredAmount) { if (!hasRequiredAmount) {
throw new Error(`The Counter Offer has an output of ${satsOut}, which does not match the required ${satsToReceive} in the Offer.`) throw new Error(`The Counter Offer has an output of ${satsOut}, which does not match the required ${satsToReceive} in the Offer.`)
} }
// Ensure the Counter Offer has an output for the Operator of bch-dex.
if (!txObj.vout[3]) {
console.log('The Counter Offer does not have an output for the Operator.')
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
this.seenOffers.push(eventId)
return 'N/A'
}
// Ensure the 3rd output (vout=2) is going to the maker address specified // Ensure the 3rd output (vout=2) is going to the maker address specified
// in the Offer. // in the Offer.
const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0] const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0]
const makerAddr = orderData.makerAddr const makerAddr = orderData.makerAddr
const hasCorrectAddr = makerAddr === addrInCounterOffer const hasCorrectAddr = makerAddr === addrInCounterOffer
if (!hasCorrectAddr) { if (!hasCorrectAddr) {
throw new Error(`The Counter Offer has an output address of ${addrInCounterOffer}, which does not match the Maker address of ${makerAddr} in the Offer.`) throw new Error(`The Counter Offer has an output address of ${addrInCounterOffer}, which does not match the Maker address of ${makerAddr} in the Offer.`)
} }
// Ensure the 4th output (vout=3) is going to the operator address specified in the Offer.
// NOTE: This is the operator of the bch-dex intance, NOT the seller of the token.
const operatorAddr = orderData.operatorAddress
const hasCorrectOperatorAddr = operatorAddr === txObj.vout[3].scriptPubKey.addresses[0]
if (!hasCorrectOperatorAddr) {
throw new Error(`The Counter Offer has an output address of ${txObj.vout[3].scriptPubKey.addresses[0]}, which does not match the Operator address of ${operatorAddr} in the Offer.`)
}
// Ensure the 4th output (vout=3) contains the required amount of BCH.
const operatorSatsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[3].value)
let estimatedOperatorFee = Math.floor(txObj.vout[3].value * orderData.operatorPercentage / 100)
if (estimatedOperatorFee < 546) estimatedOperatorFee = 546
console.log('operatorSatsOut: ', operatorSatsOut, 'estimatedOperatorFee: ', estimatedOperatorFee)
if (operatorSatsOut < estimatedOperatorFee) {
console.log(`Skipping: The Counter Offer has an output of ${operatorSatsOut}, which is less than the estimated operator fee of ${estimatedOperatorFee}.`)
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
this.seenOffers.push(eventId)
return 'N/A'
}
// Get the User ID from the Order model.
const userId = orderData.userId
// Get the User model from the database.
this.UserModel = this.adapters.localdb.Users
const user = await this.UserModel.findById(userId)
console.log('user: ', user)
// Get the HD index from the Order model.
const hdIndex = orderData.hdIndex
// Sign and broadcast the transaction. // Sign and broadcast the transaction.
const txid = await this.adapters.wallet.completeTx(txHex, orderData.hdIndex) const txid = await this.adapters.wallet.completeTx(txHex, hdIndex, user.mnemonic)
console.log('txid: ', txid) console.log('txid: ', txid)
return txid return txid
} catch (err) { } catch (err) {
console.error('Error in acceptCounterOffer()') console.error('Error in acceptCounterOffer(): ', err)
throw err throw err
} }
} }
@@ -580,12 +846,13 @@ class OfferUseCases {
// If the Offer UTXO is spent, delete the Offer model. // If the Offer UTXO is spent, delete the Offer model.
if (utxoStatus === false) { if (utxoStatus === false) {
console.log('utxoStatus: ', utxoStatus) // console.log('utxoStatus: ', utxoStatus)
console.log(`Spent UTXO detected. Deleting this Offer: ${JSON.stringify(thisOffer, null, 2)}`) console.log(`Spent UTXO detected. Deleting this Offer: ${JSON.stringify(thisOffer, null, 2)}`)
await thisOffer.remove() await thisOffer.remove()
} }
// If the Offer is older than 7 days, delete it. // TODO: Instead of deleting the offer, send the token back to the Makers wallet.
// If the Offer is older than a threshold, delete it.
const nowMs = now.getTime() const nowMs = now.getTime()
const eightWeeks = 1000 * 60 * 60 * 24 * 7 * 8 const eightWeeks = 1000 * 60 * 60 * 24 * 7 * 8
const eightWeeksAgo = nowMs - eightWeeks const eightWeeksAgo = nowMs - eightWeeks
@@ -600,32 +867,191 @@ class OfferUseCases {
} }
} }
async flagOffer (flagData) { // Flag offers as NSFW.
console.log(`flagData: ${JSON.stringify(flagData, null, 2)}`) async flagOffer (flagData = {}) {
try {
if (!flagData.data) throw new Error('"data" property is required')
const p2wdbHash = flagData.data.p2wdbHash console.log(`flagData: ${JSON.stringify(flagData, null, 2)}`)
// Get the offer from the database. const eventId = flagData.data.nostrEventId
const offer = await this.findOfferByHash(p2wdbHash)
console.log(`Flagging this offer: ${JSON.stringify(offer, null, 2)}`)
if (!offer) { // Get the offer from the database.
throw new Error(`Offer ${p2wdbHash} not found in the database.`) const offer = await this.findOfferByEvent(eventId)
console.log(`Flagging this offer: ${JSON.stringify(offer, null, 2)}`)
if (!offer) {
throw new Error(`Offer ${eventId} not found in the database.`)
}
// Add the raw flag data to the database model.
offer.flags.push(flagData)
// If flag count is 3 or more, mark the Offer as NSFW
const flagCnt = offer.flags.length
if (flagCnt >= 3) {
offer.nsfw = true
}
// Save the updated offer data to the database.
await offer.save()
return true
} catch (error) {
console.error('Error in flagOffer(): ', error)
throw error
} }
}
// Add the raw flag data to the database model. // Get offers data from nostr.
offer.flags.push(flagData) async loadOffers () {
try {
// Retrieve offers array.
const offers = await this.adapters.nostr.read()
// console.log('offers: ', offers)
// If flag count is 3 or more, mark the Offer as NSFW for (let i = 0; i < offers.length; i++) {
const flagCnt = offer.flags.length try {
if (flagCnt >= 3) { const offer = offers[i]
offer.nsfw = true
// offer data
const offerObj = JSON.parse(offer.content)
// Append the Nostr Event ID to the offer object
offerObj.data.nostrEventId = offer.eventId
// console.log('loadOffers() offerObj: ', offerObj)
if (offerObj.data.dataType === 'offer') {
// Try to create new offer
await this.createOffer(offerObj)
}
if (offerObj.data.dataType === 'counter-offer') {
// console.log('Counter offer detected: ', offerObj)
// console.log('Counter offer detected: ', offerObj.data.nostrEventId)
// console.log(`Counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerObj.data.nostrEventId)}`)
await this.acceptCounterOffer(offerObj)
}
// Ignore the post if it doesn't fit the above filters.
} catch (error) {
/* exit quietly */
}
}
} catch (error) {
console.error('Error in loadOffers(): ', error)
throw error
} }
}
// Save the updated offer data to the database. // List all offers being made by a given address.
await offer.save() async listOffersByAddress (addr) {
try {
const offers = await this.OfferModel.find({ makerAddr: addr })
return offers
} catch (err) {
console.error('Error in listOffersByAddress(): ', err)
throw err
}
}
return true // update offer model mutable data
async syncOfferMutableData (tokenId) {
try {
// validate input
if (!tokenId || typeof tokenId !== 'string') {
throw new Error('tokenId must be a string!')
}
// validate existing offer with the associated tokenId
const offer = await this.OfferModel.findOne({ tokenId })
if (!offer) throw new Error('Associated offer not found!')
// Verify last update timestamp. This prevents users from spamming the API with requests.
// It only updates the mutable data if it has been more than 5 minutes since the last update.
const lastUpdateTs = Number(offer.lastUpdatedTokenData)
const now = new Date().getTime()
const period = 5
if (lastUpdateTs) {
// add 5 minutes to the last update
const lastUpdate = new Date(lastUpdateTs)
console.log('lastUpdate', lastUpdate)
lastUpdate.setMinutes(lastUpdate.getMinutes() + period)
console.log(new Date().getMinutes() + ' ' + lastUpdate.getMinutes())
// if now is less than the lastUpdate + 5 minutos , them skip.
if (now < lastUpdate.getTime()) {
console.log('Skipping , token lastUpdate is less than 5 minutes.')
return offer
}
}
let tokenData = null
try {
tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTokenData, tokenId)
} catch (err) {
// Dev Note: If getTokenData() fails, the code below will
console.error('Error in OfferUseCases/createOffer() getting token data: ', err.message)
}
// Do additional data analysis if the token data was successfully retrieved.
if (tokenData) {
// Store the mutable and immutable data cids.
const mutableDataCid = tokenData.mutableData
const immutableDataCid = tokenData.immutableData
offer.mutableDataCid = mutableDataCid
offer.immutableDataCid = immutableDataCid
offer.tokenData = tokenData
// Get the mutable data from the cid if it exists.
if (mutableDataCid && typeof mutableDataCid === 'string') {
let mutableData = null
try {
mutableData = await this.retryQueue.addToQueue(this.adapters.wallet.cid2json, mutableDataCid)
} catch (err) {
console.error('Error in OfferUseCases/createOffer() getting mutable data: ', err.message)
}
console.log('mutableData: ', mutableData)
if (mutableData) {
try {
offer.tokenIconUrl = mutableData.tokenIcon
offer.tokenCategories = mutableData.category
offer.tokenTags = mutableData.tags
offer.userDataStr = JSON.stringify(mutableData.userData)
} catch (error) {
// skip error
}
}
}
// Save update time stamp
offer.lastUpdatedTokenData = new Date().getTime()
}
// Save the updated offer data to the database.
await offer.save()
// Return the updated offer data.
return offer
} catch (err) {
console.error('Error in syncOfferMutableData(): ', err)
throw err
}
}
async listCounterOffersByAddress (takerAddr) {
try {
if (!takerAddr || typeof takerAddr !== 'string') {
throw new Error('takerAddr must be a string')
}
const counterOffers = await this.CounterOfferModel.find({ takerAddr })
return counterOffers
} catch (err) {
console.error('Error in listCounterOffersByAddress(): ', err)
throw err
}
} }
} }
+106 -43
View File
@@ -25,88 +25,123 @@ class OrderLib {
// Encapsulate dependencies // Encapsulate dependencies
this.orderEntity = new OrderEntity() this.orderEntity = new OrderEntity()
this.OrderModel = this.adapters.localdb.Order this.OrderModel = this.adapters.localdb.Order
this.UserModel = this.adapters.localdb.Users
this.bch = this.adapters.bch this.bch = this.adapters.bch
this.config = config this.config = config
this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 }) this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 })
// Bind subfunctions to the 'this' object. // Bind subfunctions to the 'this' object.
this.ensureFunds = this.ensureFunds.bind(this) this.ensureFunds = this.ensureFunds.bind(this)
this.createOrder = this.createOrder.bind(this)
this.findOrderByUtxo = this.findOrderByUtxo.bind(this)
} }
// Create a new order model and add it to the Mongo database. // Create a new order model and add it to the Mongo database.
async createOrder (entryObj) { async createOrder (entryObj) {
try { try {
console.log('createOrder(entryObj): ', entryObj) // console.log('createOrder(entryObj): ', entryObj)
if (!entryObj) return false if (!entryObj) return false
if (!entryObj.tokenId) throw new Error('entry does not contain required properties') if (!entryObj.tokenId) throw new Error('entry does not contain required properties')
// Get the user data from the database, for the user creating the new Order.
const user = await this.UserModel.findById(entryObj.userId)
if (!user) throw new Error('user not found')
console.log(`Using FullStack.cash: ${this.config.useFullStackCash}`)
const advancedConfig = {}
if (this.config.useFullStackCash) {
advancedConfig.interface = 'rest-api'
advancedConfig.restURL = this.config.apiServer
advancedConfig.apiToken = this.config.apiToken
} else {
advancedConfig.interface = 'consumer-api'
advancedConfig.restURL = this.config.consumerUrl
}
// Instantiate minimal-slp-wallet with the user's mnemonic.
const BchWallet = this.adapters.wallet.BchWallet
const userWallet = new BchWallet(user.mnemonic, advancedConfig)
// Wait for wallet to initialize.
await userWallet.walletInfoPromise
await userWallet.initialize()
// Specify the address to send payment. // Specify the address to send payment.
entryObj.makerAddr = this.adapters.wallet.bchWallet.walletInfo.cashAddress console.log('userWallet.walletInfo: ', userWallet.walletInfo)
entryObj.makerAddr = userWallet.walletInfo.cashAddress
console.log('entryObj.makerAddr: ', entryObj.makerAddr) console.log('entryObj.makerAddr: ', entryObj.makerAddr)
// console.log('entryObj.makerAddr: ', entryObj.makerAddr)
// Input Validation // Input Validation
const orderEntity = this.orderEntity.inputValidate(entryObj) const orderEntity = this.orderEntity.inputValidate(entryObj)
console.log('orderEntity: ', orderEntity) // console.log('orderEntity: ', orderEntity)
// Optimize the wallet to speed up working with it. // Optimize the wallet to speed up working with it.
console.log('Optimizing wallet before creating new order.') console.log('Optimizing wallet before creating new order.')
await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {}) // await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {})
await userWallet.optimize()
// Ensure sufficient tokens exist to create the order.
// await this.ensureFunds(orderEntity)
await this.retryQueue.addToQueue(this.ensureFunds, orderEntity)
// Get Ticker for token ID. // Get Ticker for token ID.
// const tokenData = await this.adapters.wallet.bchWallet.getTxData([entryObj.tokenId]) const tokenData = await userWallet.getTxData([entryObj.tokenId])
const tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTxData, [entryObj.tokenId])
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`) // console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
orderEntity.ticker = tokenData[0].tokenTicker orderEntity.ticker = tokenData[0].tokenTicker
// Move the tokens to holding address. // Move the tokens to holding address.
const moveObj = { const moveObj = {
tokenId: orderEntity.tokenId, tokenId: orderEntity.tokenId,
qty: orderEntity.numTokens qty: orderEntity.numTokens,
wallet: userWallet
} }
// const utxoInfo = await this.adapters.wallet.moveTokens(moveObj) // const utxoInfo = await this.adapters.wallet.moveTokens(moveObj)
const utxoInfo = await this.retryQueue.addToQueue(this.adapters.wallet.moveTokens, moveObj) const utxoInfo = await this.retryQueue.addToQueue(this.adapters.wallet.moveTokensFromCustomWallet, moveObj)
console.log('utxoInfo: ', utxoInfo) // console.log('utxoInfo: ', utxoInfo)
// Update the UTXO store for the wallet. // Update the UTXO store for the wallet.
await this.adapters.wallet.bchWallet.bchjs.Util.sleep(3000) await userWallet.bchjs.Util.sleep(3000)
// await this.adapters.wallet.bchWallet.getUtxos() // await this.adapters.wallet.bchWallet.getUtxos()
await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {}) // await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
await userWallet.initialize()
// Update the order with the new UTXO information. // Update the order with the new UTXO information.
orderEntity.utxoTxid = utxoInfo.txid orderEntity.utxoTxid = utxoInfo.txid
orderEntity.utxoVout = utxoInfo.vout orderEntity.utxoVout = utxoInfo.vout
orderEntity.tokenType = utxoInfo.tokenType orderEntity.tokenType = utxoInfo.tokenType
// Add P2WDB specific flag for signaling that this is a new offer. // Add flag to signal that this is a new offer.
orderEntity.dataType = 'offer' orderEntity.dataType = 'offer'
orderEntity.userId = user._id
// Add order to P2WDB. // Add the operator address and percentage to the order.
const p2wdbObj = { orderEntity.operatorAddress = this.config.operatorAddress
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey, orderEntity.operatorPercentage = this.config.operatorPercentage
data: orderEntity,
appId: this.config.p2wdbAppId // Convert the Seller's BCH private key to a Nostr npub. This is used to identify the seller in Nostr.
orderEntity.makerNpub = this.adapters.nostr.privKeyToNpub(userWallet.walletInfo.privateKey)
// Post the new Order information to Nostr under the topic set in the
// config file.
const postObj = {
data: orderEntity
} }
// const hash = await this.adapters.p2wdb.write(p2wdbObj) const postMsg = JSON.stringify(postObj)
const hash = await this.retryQueue.addToQueue(this.adapters.p2wdb.write, p2wdbObj) const eventId = await this.adapters.nostr.post(postMsg)
// console.log('hash: ', hash) // console.log('hash: ', hash)
// Create a MongoDB model to hold the Order // Create a MongoDB model to hold the Order
orderEntity.hdIndex = utxoInfo.hdIndex orderEntity.hdIndex = utxoInfo.hdIndex
orderEntity.p2wdbHash = hash orderEntity.nostrEventId = eventId
console.log(`creating new order model: ${JSON.stringify(orderEntity, null, 2)}`) console.log(`creating new order model: ${JSON.stringify(orderEntity, null, 2)}`)
const order = new this.OrderModel(orderEntity) const order = new this.OrderModel(orderEntity)
await order.save() await order.save()
return hash const noteId = this.adapters.nostr.eventId2note(eventId)
return { eventId, noteId }
} catch (err) { } catch (err) {
// console.log("Error in use-cases/entry.js/createEntry()", err.message) // console.log("Error in use-cases/entry.js/createEntry()", err.message)
wlogger.error('Error in use-cases/order.js/createOrder())') wlogger.error('Error in use-cases/order.js/createOrder(): ', err)
console.log('error entryObj: ', entryObj) console.log('error entryObj: ', entryObj)
throw err throw err
} }
@@ -137,12 +172,10 @@ class OrderLib {
// Combine Fungible and NFT token UTXOs. // Combine Fungible and NFT token UTXOs.
let tokenUtxos = utxos.slpUtxos.type1.tokens.concat(utxos.slpUtxos.nft.tokens) let tokenUtxos = utxos.slpUtxos.type1.tokens.concat(utxos.slpUtxos.nft.tokens)
// Get token UTXOs that match the token in the order. // Get token UTXOs that match the token in the order.
tokenUtxos = tokenUtxos.filter( tokenUtxos = tokenUtxos.filter(
x => x.tokenId === orderEntity.tokenId x => x.tokenId === orderEntity.tokenId
) )
console.log('tokenUtxos: ', tokenUtxos)
// Get the total amount of tokens in the wallet that match the token // Get the total amount of tokens in the wallet that match the token
// in the order. // in the order.
@@ -158,7 +191,7 @@ class OrderLib {
) )
} }
// //
} else { } else {
// Buy Order // Buy Order
throw new Error('Buy orders are not supported yet.') throw new Error('Buy orders are not supported yet.')
@@ -171,14 +204,14 @@ class OrderLib {
} }
} }
// Retrieve an Order model from the database. Find it by its P2WDB CID. // Retrieve an Order model from the database. Find it by its event Id.
async findOrderByHash (p2wdbHash) { async findOrderByEvent (nostrEventId) {
try { try {
if (typeof p2wdbHash !== 'string' || !p2wdbHash) { if (typeof nostrEventId !== 'string' || !nostrEventId) {
throw new Error('p2wdbHash must be a string') throw new Error('nostrEventId must be a string')
} }
const order = await this.OrderModel.findOne({ p2wdbHash }) const order = await this.OrderModel.findOne({ nostrEventId })
if (!order) { if (!order) {
throw new Error('order not found') throw new Error('order not found')
@@ -189,7 +222,38 @@ class OrderLib {
return orderObject return orderObject
} catch (err) { } catch (err) {
console.error('Error in findOrder()') console.error('Error in findOrderByEvent()')
throw err
}
}
// Retrieve an Order model from the database. Find it by its UTXO (TXID & Vout)
async findOrderByUtxo (offerData = {}) {
try {
// console.log('findOrderByUtxo() offerData: ', offerData)
// const order = await this.OrderModel.findOne({ nostrEventId })
const order = await this.OrderModel.findOne({ utxoTxid: offerData.data.utxoTxid, utxoVout: offerData.data.utxoVout })
// const order = await this.OrderModel.findOne({ utxoTxid: offerData.data.utxoTxid })
// const order = await this.OrderModel.findOne({ tokenId: offerData.data.tokenId })
console.log('\nfindOrderByUtxo() order: ', order)
if (!order) {
throw new Error('order not found')
}
if (!order.nostrEventId) {
throw new Error('order not found')
}
const orderObject = order.toObject()
// return this.offerEntity.validateFromModel(offerObject)
return orderObject
} catch (err) {
if (!err.message.toString().includes('order not found')) {
console.error('Error in findOrderByUtxo(): ', err)
}
throw err throw err
} }
} }
@@ -242,7 +306,7 @@ class OrderLib {
// If the Order UTXO is spent, delete the Order model. // If the Order UTXO is spent, delete the Order model.
if (utxoStatus === false) { if (utxoStatus === false) {
console.log('utxoStatus: ', utxoStatus) // console.log('utxoStatus: ', utxoStatus)
console.log(`Spent UTXO detected. Deleting this Order: ${JSON.stringify(thisOrder, null, 2)}`) console.log(`Spent UTXO detected. Deleting this Order: ${JSON.stringify(thisOrder, null, 2)}`)
await thisOrder.remove() await thisOrder.remove()
} }
@@ -256,11 +320,12 @@ class OrderLib {
async listOrders (page = 0) { async listOrders (page = 0) {
try { try {
const data = await this.OrderModel.find({}) const data = await this.OrderModel.find({})
// Sort entries so newest entries show first.
// Sort entries so newest entries show first.
.sort('-timestamp') .sort('-timestamp')
// Skip to the start of the selected page. // Skip to the start of the selected page.
.skip(page * DEFAULT_ENTRIES_PER_PAGE) .skip(page * DEFAULT_ENTRIES_PER_PAGE)
// Only return 20 results. // Only return 20 results.
.limit(DEFAULT_ENTRIES_PER_PAGE) .limit(DEFAULT_ENTRIES_PER_PAGE)
return data return data
@@ -272,12 +337,10 @@ class OrderLib {
// Delete an Order model by sending the token back to the root address. This // Delete an Order model by sending the token back to the root address. This
// will allow the garbage collectors to delete the Order and the Offer. // will allow the garbage collectors to delete the Order and the Offer.
async deleteOrder (p2wdbHash) { async deleteOrder (nostrEventId) {
try { try {
console.log('p2wdbHash: ', p2wdbHash)
// Find the order by the given hash. // Find the order by the given hash.
const order = await this.findOrderByHash(p2wdbHash) const order = await this.findOrderByEvent(nostrEventId)
console.log('order: ', order) console.log('order: ', order)
// Reclaim the tokens // Reclaim the tokens
+134
View File
@@ -0,0 +1,134 @@
/*
Social Media Account Use Cases.
*/
class smAccountUseCases {
constructor (localConfig = {}) {
// console.log('User localConfig: ', localConfig)
// Dependency injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of adapters must be passed in when instantiating SM Account Use Cases library.'
)
}
// Bind 'this' object to all methods
this.checkForNewSmAccounts = this.checkForNewSmAccounts.bind(this)
this.listAccounts = this.listAccounts.bind(this)
this.getAccountByNpub = this.getAccountByNpub.bind(this)
this.getAccountByBchAddr = this.getAccountByBchAddr.bind(this)
this.updateSmAccounts = this.updateSmAccounts.bind(this)
}
// Check for new Social Media Accounts.
async checkForNewSmAccounts () {
try {
console.log('Checking for new Social Media Accounts...')
const messages = await this.adapters.nostr.readGlobalFeed()
console.log('messages: ', messages)
for (let i = 0; i < messages.length; i++) {
const msg = messages[i]
// const { content, id, tags, npub, pubkey } = msg
const { tags, npub, pubkey } = msg
let bchAddr = null
// console.log('msg: ', JSON.stringify(msg, null, 2))
// Find the 'u' tag if it exists
const uTag = tags.find(tag => tag[0] === 'u')
// If 'u' tag exists and contains a BCH address
if (uTag && uTag[1].includes('bitcoincash:')) {
bchAddr = uTag[1]
// console.log('BCH address found:', bchAddr)
}
const SmAccount = await this.adapters.localdb.SmAccount
if (bchAddr) {
const smAccount = await SmAccount.findOne({ npub })
if (!smAccount) {
console.log(`Creating new Social Media Account with npub: ${npub} and bchAddr: ${bchAddr}`)
const newSmAccount = new SmAccount({ npub, bchAddr, pubkey })
await newSmAccount.save()
} else {
console.log(`Social Media Account with npub: ${npub} already exists`)
}
}
}
return true
} catch (err) {
console.error('Error in smAccount-use-cases.js/checkForNewSmAccounts(): ', err)
throw err
}
}
// List all Social Media Accounts.
async listAccounts (page = 0) {
try {
const SmAccount = await this.adapters.localdb.SmAccount
const accounts = await SmAccount.find({}).skip(page * 10).limit(10)
return accounts
} catch (err) {
console.error('Error in smAccount-use-cases.js/listAccounts(): ', err)
throw err
}
}
// Get a Social Media Account by Npub.
async getAccountByNpub (npub) {
try {
const SmAccount = await this.adapters.localdb.SmAccount
const account = await SmAccount.findOne({ npub })
return account
} catch (err) {
console.error('Error in smAccount-use-cases.js/getAccountByNpub(): ', err)
throw err
}
}
// Get a Social Media Account by BCH address.
async getAccountByBchAddr (bchAddr) {
try {
const SmAccount = await this.adapters.localdb.SmAccount
const account = await SmAccount.findOne({ bchAddr })
console.log('Account found: ', account)
return account
} catch (err) {
console.error('Error in smAccount-use-cases.js/getAccountByBchAddr(): ', err)
throw err
}
}
// Update metadata about social media accounts being tracked.
async updateSmAccounts () {
try {
console.log('Updating Social Media Accounts...')
const SmAccount = await this.adapters.localdb.SmAccount
const accounts = await SmAccount.find({})
for (let i = 0; i < accounts.length; i++) {
const account = accounts[i]
// console.log(`Account ${i}: `, account)
// Get the number of accounts following this account
const pubkey = account.pubkey
const followList = await this.adapters.nostr.getFollowers({ pubkey })
// console.log(`Follow list ${i}: `, JSON.stringify(followList, null, 2))
// Update the follower count for this account.
const followerCnt = followList.length
await SmAccount.updateOne({ _id: account._id }, { $set: { followerCnt } })
}
return true
} catch (err) {
console.error('Error in smAccount-use-cases.js/updateSmAccounts(): ', err)
throw err
}
}
}
export default smAccountUseCases
+214
View File
@@ -0,0 +1,214 @@
/*
Use Case library for tracking usage. This library contains business logic
for tracking the usage of REST API and JSON RPC calls. This library is used
by admins to keep an eye on how many API calls were made in a 24-hour and
1-hour time period.
Usage stats are held in memory. But they are periodically backed up to the
Mongo database. On startup, the usage stats are loaded from the database.
This allows the usage stats to be persisted across restarts.
*/
// This global variable is used to share data between the REST middleware and
// the Usage Use Case class instance.
let restCalls = []
class UsageUseCases {
constructor (localConfig = {}) {
// console.log('User localConfig: ', localConfig)
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of adapters must be passed in when instantiating Usage Use Cases library.'
)
}
// Encapsulate dependencies
this.UsageModel = this.adapters.localdb.Usage
// Bind 'this' object to all subfunctions
this.cleanUsage = this.cleanUsage.bind(this)
this.getRestSummary = this.getRestSummary.bind(this)
this.getTopIps = this.getTopIps.bind(this)
this.getTopEndpoints = this.getTopEndpoints.bind(this)
this.clearUsage = this.clearUsage.bind(this)
this.saveUsage = this.saveUsage.bind(this)
this.loadUsage = this.loadUsage.bind(this)
// State
}
// Clean up the state by removing entries that are older than 24 hours. This
// ensures stats reflect only the last 24 hours.
// This function is called by a Timer Controller.
cleanUsage () {
try {
const now = new Date()
const twentyFourHoursAgo = now.getTime() - (60000 * 60 * 24)
console.log('cleanUsage() now: ', now)
console.log('cleanUsage() restCalls.length before filtering: ', restCalls.length)
restCalls = restCalls.filter(x => x.timestamp > twentyFourHoursAgo)
console.log('cleanUsage() restCalls.length after filtering: ', restCalls.length)
return restCalls
} catch (err) {
console.error('Error in usage-use-cases.js/cleanUsage()')
throw err
}
}
// Track the calls to a REST API
getRestSummary (inObj = {}) {
try {
console.log(`getRestSummary(): There have been ${restCalls.length} REST calls`)
return restCalls.length
} catch (err) {
console.error('Error in usage-use-cases.js/getRestSummary()')
throw err
}
}
// Get the top 20 IP addresses from the stats.
getTopIps () {
try {
const ips = restCalls.map(x => x.ip)
// Create a Map to count occurrences of each IP address string
const countMap = new Map()
ips.forEach(ip => {
countMap.set(ip, (countMap.get(ip) || 0) + 1)
})
// Convert the Map into an array of objects with `str` and `cnt` properties
const result = Array.from(countMap, ([ip, cnt]) => ({ ip, cnt }))
// Sort the results by the `cnt` property in descending order
result.sort((a, b) => b.cnt - a.cnt)
// Ensure the result has at most 20 elements
return result.slice(0, 20)
} catch (err) {
console.error('Error in usage-use-cases.js/getTopIps()')
throw err
}
}
// Get the top 20 most consumed endpoints.
getTopEndpoints () {
try {
const endpoints = restCalls.map(x => `${x.method} ${x.url}`)
// Create a Map to count occurrences of each IP address string
const countMap = new Map()
endpoints.forEach(endpoint => {
countMap.set(endpoint, (countMap.get(endpoint) || 0) + 1)
})
// Convert the Map into an array of objects with `str` and `cnt` properties
const result = Array.from(countMap, ([endpoint, cnt]) => ({ endpoint, cnt }))
// Sort the results by the `cnt` property in descending order
result.sort((a, b) => b.cnt - a.cnt)
// Ensure the result has at most 20 elements
return result.slice(0, 20)
} catch (err) {
console.error('Error in usage-use-cases.js/getTopEndpoints()')
throw err
}
}
// Clear the usage database data
async clearUsage () {
try {
await this.UsageModel.deleteMany({})
// Debugging: verify the database is empty
// Delete this code after debugging
const usage = await this.UsageModel.find({})
console.log('clearUsage() usage: ', usage)
return true
} catch (err) {
console.error('Error in usage-use-cases.js/clearUsage()')
throw err
}
}
// Save the usage data to the database
async saveUsage (inObj = {}) {
try {
for (let i = 0; i < restCalls.length; i++) {
const thisRestCall = restCalls[i]
// Debugging: delete this code after debugging
// if (i === 5) {
// console.log('saveUsage() thisRestCall: ', thisRestCall)
// }
const usageData = {
ip: thisRestCall.ip,
url: thisRestCall.url,
method: thisRestCall.method,
timestamp: thisRestCall.timestamp
}
const usage = new this.UsageModel(usageData)
await usage.save()
}
return true
} catch (err) {
console.error('Error in usage-use-cases.js/saveUsage()')
throw err
}
}
// Load usage data from the database
async loadUsage () {
try {
const usage = await this.UsageModel.find({})
// console.log('usage: ', usage)
// Debugging: delete this code after debugging
if (usage[5]) {
console.log('loadUsage() usage[5]: ', usage[5])
}
restCalls = usage
return usage
} catch (err) {
console.error('Error in usage-use-cases.js/loadUsage(): ', err)
return false
// throw err
}
}
}
// This Koa middleware is called any time there is a REST API. It logs the
// details from the request object.
function usageMiddleware () {
return async (ctx, next) => {
try {
await next()
// console.log('ctx.request: ', ctx.request)
const now = new Date()
const reqObj = {
ip: ctx.request.ip,
url: ctx.request.url,
method: ctx.request.method,
timestamp: now.getTime()
}
// console.log('reqObj: ', reqObj)
restCalls.push(reqObj)
} catch (err) {
ctx.status = err.status || 500
ctx.body = err.message
ctx.app.emit('error', err, ctx)
}
}
};
export { UsageUseCases, usageMiddleware, restCalls }
+10 -1
View File
@@ -16,7 +16,7 @@ class UserLib {
'Instance of adapters must be passed in when instantiating User Use Cases library.' 'Instance of adapters must be passed in when instantiating User Use Cases library.'
) )
} }
this.BchWallet = this.adapters.wallet.BchWallet
// Encapsulate dependencies // Encapsulate dependencies
this.UserEntity = new UserEntity() this.UserEntity = new UserEntity()
this.UserModel = this.adapters.localdb.Users this.UserModel = this.adapters.localdb.Users
@@ -30,6 +30,11 @@ class UserLib {
const userEntity = this.UserEntity.validate(userObj) const userEntity = this.UserEntity.validate(userObj)
const user = new this.UserModel(userEntity) const user = new this.UserModel(userEntity)
const wallet = new this.BchWallet()
const walletInfo = await wallet.walletInfoPromise
const mnemonic = walletInfo.mnemonic
user.mnemonic = mnemonic
// Enforce default value of 'user' // Enforce default value of 'user'
user.type = 'user' user.type = 'user'
// console.log('user: ', user) // console.log('user: ', user)
@@ -102,6 +107,7 @@ class UserLib {
// Input Validation // Input Validation
// Optional inputs, but they must be strings if included. // Optional inputs, but they must be strings if included.
if (newData.email && typeof newData.email !== 'string') { if (newData.email && typeof newData.email !== 'string') {
throw new Error("Property 'email' must be a string!") throw new Error("Property 'email' must be a string!")
} }
@@ -111,6 +117,9 @@ class UserLib {
if (newData.password && typeof newData.password !== 'string') { if (newData.password && typeof newData.password !== 'string') {
throw new Error("Property 'password' must be a string!") throw new Error("Property 'password' must be a string!")
} }
if (newData.mnemonic) {
throw new Error("Property 'mnemonic' cannot be updated!")
}
// Save a copy of the original user type. // Save a copy of the original user type.
const userType = existingUser.type const userType = existingUser.type
-3
View File
@@ -1,3 +0,0 @@
/key/swarm/psk/1.0.0/
/base16/
bbd935b70105b03ebd0c6a3c2d2730cd22fc0d18c490ccf689b6c8a22e1bed2a
@@ -0,0 +1,45 @@
/*
Liveness test runs before all other tests to ensure the server is running.
*/
// Public npm libraries
import { assert } from 'chai'
import axios from 'axios'
import testUtils from '../../utils/test-utils.js'
// const sinon = require('sinon')
// Local support libraries
import config from '../../../config/index.js'
// import Server from '../../../bin/server.js'
// import testUtils from '../../utils/test-utils.js'
// const adminLib = new AdminLib()
const LOCALHOST = `http://localhost:${config.port}`
describe('#Check Server Liveness', () => {
// before(async () => {
it('should confirm the server is running', async () => {
try {
const response = await axios.get(`${LOCALHOST}/`)
assert(response.status === 200, 'Server is running, continuing with E2E tests.')
} catch (err) {
console.log('\nServer is not running, exiting tests.')
console.log('Start the server with `npm run start:e2e:server` before running E2E tests.\n')
console.log('Ensure running npm run docs before running the test server')
process.exit(1)
}
})
it('should confirm the server is running over test enviroment', async () => {
try {
const res = await testUtils.loginAdminUser()
assert.property(res, 'user')
assert.property(res, 'token')
assert.property(res, 'id')
} catch (err) {
console.log('\nServer is not running over test enviroment, exiting tests.')
console.log('Start the server with `npm run start:e2e:server` before running E2E tests.\n')
process.exit(1)
}
})
})
+5 -8
View File
@@ -13,10 +13,8 @@ import axios from 'axios'
// Local support libraries // Local support libraries
import config from '../../../config/index.js' import config from '../../../config/index.js'
import Server from '../../../bin/server.js' // import Server from '../../../bin/server.js'
import testUtils from '../../utils/test-utils.js' import testUtils from '../../utils/test-utils.js'
import AdminLib from '../../../src/adapters/admin.js'
const adminLib = new AdminLib()
// const request = supertest.agent(app.listen()) // const request = supertest.agent(app.listen())
const context = {} const context = {}
@@ -26,10 +24,12 @@ const LOCALHOST = `http://localhost:${config.port}`
if (!config.noMongo) { if (!config.noMongo) {
describe('Auth', () => { describe('Auth', () => {
before(async () => { before(async () => {
const app = new Server() // const app = new Server()
// This should be the first instruction. It starts the REST API server. // This should be the first instruction. It starts the REST API server.
await app.startServer() // await app.startServer()
// TODO:
// Stop the IPFS node for the rest of the e2e tests. // Stop the IPFS node for the rest of the e2e tests.
// await app.controllers.adapters.ipfs.stop() // await app.controllers.adapters.ipfs.stop()
@@ -37,9 +37,6 @@ if (!config.noMongo) {
// Delete all previous users in the database. // Delete all previous users in the database.
await testUtils.deleteAllUsers() await testUtils.deleteAllUsers()
// Create a new admin user.
await adminLib.createSystemUser()
const userObj = { const userObj = {
email: 'test@test.com', email: 'test@test.com',
password: 'pass', password: 'pass',
+85 -80
View File
@@ -5,16 +5,16 @@ import axios from 'axios'
import sinon from 'sinon' import sinon from 'sinon'
import util from 'util' import util from 'util'
import UserController from '../../../src/controllers/rest-api/users/controller.js' // import UserController from '../../../src/controllers/rest-api/users/controller.js'
import Adapters from '../../../src/adapters/index.js' // import Adapters from '../../../src/adapters/index.js'
import UseCases from '../../../src/use-cases/index.js' // import UseCases from '../../../src/use-cases/index.js'
util.inspect.defaultOptions = { depth: 1 } util.inspect.defaultOptions = { depth: 1 }
const LOCALHOST = `http://localhost:${config.port}` const LOCALHOST = `http://localhost:${config.port}`
const context = {} const context = {}
const adapters = new Adapters() // const adapters = new Adapters()
let uut // let uut
let sandbox let sandbox
// const mockContext = require('../../unit/mocks/ctx-mock').context // const mockContext = require('../../unit/mocks/ctx-mock').context
@@ -50,8 +50,8 @@ if (!config.noMongo) {
}) })
beforeEach(() => { beforeEach(() => {
const useCases = new UseCases({ adapters }) // const useCases = new UseCases({ adapters })
uut = new UserController({ adapters, useCases }) // uut = new UserController({ adapters, useCases })
sandbox = sinon.createSandbox() sandbox = sinon.createSandbox()
}) })
@@ -173,6 +173,7 @@ if (!config.noMongo) {
) )
assert.property(result.data, 'token', 'Token property exists.') assert.property(result.data, 'token', 'Token property exists.')
assert.equal(result.data.user.type, 'user') assert.equal(result.data.user.type, 'user')
assert.property(result.data.user, 'mnemonic')
}) })
}) })
@@ -249,52 +250,55 @@ if (!config.noMongo) {
} }
}) })
it('should fetch all users', async () => { // it('should fetch all users', async () => {
const { token } = context // const { token } = context
const options = { // const options = {
method: 'GET', // method: 'GET',
url: `${LOCALHOST}/users`, // url: `${LOCALHOST}/users`,
headers: { // headers: {
Accept: 'application/json', // Accept: 'application/json',
Authorization: `Bearer ${token}` // Authorization: `Bearer ${token}`
} // }
} // }
const result = await axios(options) // const result = await axios(options)
const users = result.data.users // const users = result.data.users
// console.log(`users: ${util.inspect(users)}`) // // console.log(`users: ${util.inspect(users)}`)
assert.hasAnyKeys(users[0], ['type', '_id', 'email']) // assert.hasAnyKeys(users[0], ['type', '_id', 'email', 'mnemonic'])
assert.isNumber(users.length) // assert.isNumber(users.length)
}) // })
it('should return a 422 http status if biz-logic throws an error', async () => { // it('should return a 422 http status if biz-logic throws an error', async () => {
try { // try {
const { token } = context // const { token } = context
// Force an error // // Force an error
sandbox // sandbox
.stub(uut.useCases.user, 'getAllUsers') // .stub(uut.useCases.user, 'getAllUsers')
.rejects(new Error('test error')) // .rejects(new Error('test error'))
const options = { // const options = {
method: 'GET', // method: 'GET',
url: `${LOCALHOST}/users`, // url: `${LOCALHOST}/users`,
headers: { // headers: {
Accept: 'application/json', // Accept: 'application/json',
Authorization: `Bearer ${token}` // Authorization: `Bearer ${token}`
} // }
} // }
await axios(options) // await axios(options)
assert.fail('Unexpected code path!') // assert.fail('Unexpected code path!')
} catch (err) { // } catch (err) {
// console.log(err) // // console.log(err)
assert.equal(err.response.status, 422) // assert.equal(err.response.status, 422)
assert.equal(err.response.data, 'test error') // assert.equal(err.response.data, 'test error')
} // }
}) // })
// assert.hasAnyKeys(users[0], ['type', '_id', 'email'])
// assert.isNumber(users.length)
// })
}) })
describe('GET /users/:id', () => { describe('GET /users/:id', () => {
@@ -316,45 +320,45 @@ if (!config.noMongo) {
} }
}) })
it("should throw 404 if user doesn't exist", async () => { // it("should throw 404 if user doesn't exist", async () => {
const { token } = context // const { token } = context
try { // try {
const options = { // const options = {
method: 'GET', // method: 'GET',
url: `${LOCALHOST}/users/5fa4bd7ee1828f5f4d8ed004`, // url: `${LOCALHOST}/users/5fa4bd7ee1828f5f4d8ed004`,
headers: { // headers: {
Accept: 'application/json', // Accept: 'application/json',
Authorization: `Bearer ${token}` // Authorization: `Bearer ${token}`
} // }
} // }
await axios(options) // await axios(options)
assert.equal(true, false, 'Unexpected behavior') // assert.equal(true, false, 'Unexpected behavior')
} catch (err) { // } catch (err) {
assert.equal(err.response.status, 404) // assert.equal(err.response.status, 404)
} // }
}) // })
it('should throw 422 for invalid input', async () => { // it('should throw 422 for invalid input', async () => {
const { token } = context // const { token } = context
try { // try {
const options = { // const options = {
method: 'GET', // method: 'GET',
url: `${LOCALHOST}/users/1`, // url: `${LOCALHOST}/users/1`,
headers: { // headers: {
Accept: 'application/json', // Accept: 'application/json',
Authorization: `Bearer ${token}` // Authorization: `Bearer ${token}`
} // }
} // }
await axios(options) // await axios(options)
assert.equal(true, false, 'Unexpected behavior') // assert.equal(true, false, 'Unexpected behavior')
} catch (err) { // } catch (err) {
assert.equal(err.response.status, 422) // assert.equal(err.response.status, 422)
} // }
}) // })
it('should fetch own user', async () => { it('should fetch own user', async () => {
const _id = context.user._id const _id = context.user._id
@@ -375,6 +379,7 @@ if (!config.noMongo) {
assert.property(user, 'type') assert.property(user, 'type')
assert.property(user, 'email') assert.property(user, 'email')
assert.property(user, 'mnemonic')
assert.property(user, '_id') assert.property(user, '_id')
assert.equal(user._id, _id) assert.equal(user._id, _id)
+74
View File
@@ -0,0 +1,74 @@
/*
End-to-end tests for /usage endpoints.
*/
import config from '../../../config/index.js'
import { assert } from 'chai'
import axios from 'axios'
import sinon from 'sinon'
import util from 'util'
util.inspect.defaultOptions = { depth: 1 }
const LOCALHOST = `http://localhost:${config.port}`
let sandbox
describe('Usage', () => {
beforeEach(() => {
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
describe('GET /usage', () => {
it('should return usage status', async () => {
try {
const options = {
method: 'get',
url: `${LOCALHOST}/usage`
}
const result = await axios(options)
assert.property(result.data, 'status')
} catch (err) {
assert(false, 'Unexpected result')
}
})
})
describe('GET /usage/ips', () => {
it('should return ips', async () => {
try {
const options = {
method: 'get',
url: `${LOCALHOST}/usage/ips`
}
const result = await axios(options)
assert.property(result.data, 'ips')
} catch (err) {
assert(false, 'Unexpected result')
}
})
})
describe('GET /usage/endpoints', () => {
it('should return ips', async () => {
try {
const options = {
method: 'get',
url: `${LOCALHOST}/usage/endpoints`
}
const result = await axios(options)
assert.property(result.data, 'endpoints')
} catch (err) {
assert(false, 'Unexpected result')
}
})
})
})
+188
View File
@@ -0,0 +1,188 @@
import testUtils from '../../utils/test-utils.js'
import { assert } from 'chai'
import config from '../../../config/index.js'
import axios from 'axios'
import sinon from 'sinon'
import util from 'util'
util.inspect.defaultOptions = { depth: 1 }
const LOCALHOST = `http://localhost:${config.port}`
const context = {}
let sandbox
// const mockContext = require('../../unit/mocks/ctx-mock').context
if (!config.noMongo) {
describe('Nostr', () => {
before(async () => {
// console.log(`config: ${JSON.stringify(config, null, 2)}`)
// Create a second test user.
const userObj = {
email: 'test2@test.com',
password: 'pass2',
name: 'test2'
}
const testUser = await testUtils.createUser(userObj)
// console.log(`testUser2: ${JSON.stringify(testUser, null, 2)}`)
context.user2 = testUser.user
context.token2 = testUser.token
context.id2 = testUser.user._id
// Get the JWT used to log in as the admin 'system' user.
const adminJWT = await testUtils.getAdminJWT()
console.log(`adminJWT: ${adminJWT}`)
context.adminJWT = adminJWT
// const admin = await testUtils.loginAdminUser()
// context.adminJWT = admin.token
// const admin = await adminLib.loginAdmin()
// console.log(`admin: ${JSON.stringify(admin, null, 2)}`)
})
beforeEach(() => {
// const useCases = new UseCases({ adapters })
// uut = new UserController({ adapters, useCases })
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
describe('POST /nostr/deletedChat - Create Deleted Chat', () => {
it('should not create deleted chat if the authorization header is missing', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedChat`
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
it('should not create deleted chat if the authorization header is invalid scheme', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedChat`,
headers: {
Authorization: 'Bearer invalid'
}
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
it('should not create deleted chat if the authorization header is invalid token', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedChat`,
headers: {
Authorization: `Bearer ${context.token}`
}
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
it('should not create deleted chat if token is not admin', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedChat`,
headers: {
Authorization: `Bearer ${context.token}`
}
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
})
describe('POST /nostr/deletedPost - Create Deleted Post', () => {
it('should not create deleted post if the authorization header is missing', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedPost`
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
it('should not create deleted post if the authorization header is invalid scheme', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedPost`,
headers: {
Authorization: 'Bearer invalid'
}
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
it('should not create deleted post if the authorization header is invalid token', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedPost`,
headers: {
Authorization: `Bearer ${context.token}`
}
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
it('should not create deleted post if token is not admin', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedPost`,
headers: {
Authorization: `Bearer ${context.token}`
}
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
})
})
}
+17 -2
View File
@@ -29,8 +29,7 @@ describe('#adapters', () => {
uut.config.getJwtAtStartup = true uut.config.getJwtAtStartup = true
uut.config.useIpfs = true uut.config.useIpfs = true
uut.config.env = 'not-a-test' uut.config.env = 'not-a-test'
sandbox.stub(uut.fullStackJwt, 'getJWT').resolves() sandbox.stub(uut.wallet, 'instanceWallet').resolves()
sandbox.stub(uut.fullStackJwt, 'instanceBchjs').resolves()
sandbox.stub(uut.ipfs, 'start').resolves() sandbox.stub(uut.ipfs, 'start').resolves()
const result = await uut.start() const result = await uut.start()
@@ -38,6 +37,22 @@ describe('#adapters', () => {
assert.equal(result, true) assert.equal(result, true)
}) })
it('should not start ipfs on test enviroment', async () => {
// Mock dependencies
uut.config.getJwtAtStartup = true
uut.config.useIpfs = true
uut.config.env = 'test'
sandbox.stub(uut.fullStackJwt, 'getJWT').resolves()
sandbox.stub(uut.fullStackJwt, 'instanceBchjs').resolves()
const ipfsSpy = sandbox.stub(uut.ipfs, 'start').resolves(null)
const result = await uut.start()
assert.isTrue(ipfsSpy.notCalled)
assert.equal(result, true)
})
it('should catch and throw an error', async () => { it('should catch and throw an error', async () => {
try { try {
// Force an error // Force an error
@@ -19,26 +19,13 @@ describe('Admin', () => {
if (!config.noMongo) { if (!config.noMongo) {
describe('loginAdmin()', () => { describe('loginAdmin()', () => {
it('should logind admin', async () => { it('should login admin', async () => {
try { try {
const error = new Error('test error') sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'pass' })
error.response = { sandbox.stub(uut.axios, 'request').resolves(true)
status: 422
}
// sandbox.stub(uut.axios, 'request').onFirstCall().throws(error)
const result = await uut.loginAdmin() const result = await uut.loginAdmin()
const user = result.data.user assert.isTrue(result)
assert.property(user, '_id')
assert.property(user, 'email')
assert.property(user, 'type')
assert.isString(user._id)
assert.isString(user.email)
assert.isString(user.type)
assert.equal(user.type, 'admin')
} catch (err) { } catch (err) {
assert(false, 'Unexpected result') assert(false, 'Unexpected result')
} }
@@ -48,13 +35,13 @@ describe('Admin', () => {
try { try {
// Returns an erroneous password to force // Returns an erroneous password to force
// an auth error // an auth error
sandbox.stub(uut.axios, 'request').throws(new Error('test error'))
sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'wrong' }) sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'wrong' })
await uut.loginAdmin() await uut.loginAdmin()
assert(false, 'Unexpected result') assert(false, 'Unexpected result')
} catch (err) { } catch (err) {
assert.equal(err.response.status, 401) assert.include(err.message, 'test error')
assert.include(err.response.data, 'Unauthorized')
} }
}) })
}) })
@@ -62,6 +49,7 @@ describe('Admin', () => {
describe('createSystemUser()', () => { describe('createSystemUser()', () => {
it('should create admin', async () => { it('should create admin', async () => {
try { try {
await uut.deleteExistingSystemUser()
const result = await uut.createSystemUser() const result = await uut.createSystemUser()
assert.property(result, 'email') assert.property(result, 'email')
@@ -72,44 +60,59 @@ describe('Admin', () => {
assert(false, 'Unexpected result') assert(false, 'Unexpected result')
} }
}) })
it('should update admin password', async () => {
it('should handle axios error', async () => {
try { try {
const error1 = new Error('test error') uut.config.adminPassword = 'newpassword'
error1.response = {
status: 422
}
const error2 = new Error('test error')
error1.response = {
status: 500
}
// The loginAdmin() function in some use cases is recursive
// after handling the 422 error, it gets called again
sandbox
.stub(uut.axios, 'request')
.onFirstCall()
.throws(error1)
.onSecondCall()
.throws(error2)
await uut.createSystemUser() const fakeUser = {
password: 'oldpassword',
save: () => { return 'token' },
generateToken: () => { return 'token' }
}
sandbox.stub(uut.User, 'findOne').resolves(fakeUser)
sandbox.stub(uut.jsonFiles, 'writeJSON').resolves(true)
const result = await uut.createSystemUser()
assert.property(result, 'email')
assert.property(result, 'password')
assert.property(result, 'id')
assert.property(result, 'token')
assert.equal(fakeUser.password, 'newpassword', 'password should be updated')
} catch (err) {
console.log(err)
assert(false, 'Unexpected result') assert(false, 'Unexpected result')
}
})
it('should handle error', async () => {
try {
sandbox.stub(uut.User, 'findOne').throws(new Error('test error'))
await uut.createSystemUser()
assert.fail('Unexpected result')
} catch (err) { } catch (err) {
assert.include(err.message, 'test error') assert.include(err.message, 'test error')
} }
}) })
})
it('should handle errors when remove user', async () => { describe('deleteExistingSystemUser()', () => {
it('should delete admin', async () => {
try { try {
const error1 = new Error('test error') sandbox.stub(uut.User, 'deleteOne').resolves(true)
error1.response = { const result = await uut.deleteExistingSystemUser()
status: 422 assert.isTrue(result)
} } catch (err) {
sandbox.stub(uut.axios, 'request').throws(error1)
sandbox.stub(uut.User, 'deleteOne').throws(new Error('test error'))
await uut.createSystemUser()
assert(false, 'Unexpected result') assert(false, 'Unexpected result')
}
})
it('should handle error when deleting admin', async () => {
try {
sandbox.stub(uut.User, 'deleteOne').throws(new Error('test error'))
await uut.deleteExistingSystemUser()
assert.fail('Unexpected result')
} catch (err) { } catch (err) {
assert.include(err.message, 'test error') assert.include(err.message, 'test error')
} }
+14 -13
View File
@@ -135,18 +135,19 @@ describe('bch', () => {
} }
}) })
// it('should return the merit ', async () => { /* it('should return the merit ', async () => {
// try { try {
// // Mock live network calls. // Mock live network calls.
// sandbox.stub(uut.msgLib.merit, 'agMerit').resolves(100) sandbox.stub(uut.msgLib.merit, 'agMerit').resolves(100)
//
// const slpAddr = const slpAddr =
// 'simpleledger:qqgnksc6zr4nzxrye69fq625wu2myxey6uh9kzjy96' 'simpleledger:qqgnksc6zr4nzxrye69fq625wu2myxey6uh9kzjy96'
// const merit = await uut.getMerit(slpAddr) const merit = await uut.getMerit(slpAddr)
// assert.isNumber(merit) assert.isNumber(merit)
// } catch (err) { } catch (err) {
// assert.fail('Unexpected result') console.log('err: ', err)
// } assert.fail('Unexpected result')
// }) }
}) */
}) })
}) })
+12 -12
View File
@@ -6,7 +6,7 @@
import { assert } from 'chai' import { assert } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
import cloneDeep from 'lodash.clonedeep' import cloneDeep from 'lodash.clonedeep'
import { peerIdFromString } from '@libp2p/peer-id' // import { peerIdFromString } from '@libp2p/peer-id'
// Local libraries // Local libraries
import IPFSLib from '../../../src/adapters/ipfs/ipfs.js' import IPFSLib from '../../../src/adapters/ipfs/ipfs.js'
@@ -165,17 +165,17 @@ describe('#IPFS-adapter', () => {
uut.config.isCircuitRelay = false uut.config.isCircuitRelay = false
// Mock dependencies and force desired code path. // Mock dependencies and force desired code path.
let beenCalled = false // const beenCalled = false
sandbox.stub(uut, 'getKeychain').resolves({ // sandbox.stub(uut, 'getKeychain').resolves({
exportPeerId: async () => { // exportPeerId: async () => {
if (!beenCalled) { // if (!beenCalled) {
beenCalled = true // beenCalled = true
throw new Error('test error') // throw new Error('test error')
} // }
return peerIdFromString('12D3KooWSXF1PnEfiA8bCG8SJduCvzdwHtvhVPK4WC6zzDoto2XP') // return peerIdFromString('12D3KooWSXF1PnEfiA8bCG8SJduCvzdwHtvhVPK4WC6zzDoto2XP')
}, // },
createKey: async () => {} // createKey: async () => {}
}) // })
sandbox.stub(uut, 'createLibp2p').resolves() sandbox.stub(uut, 'createLibp2p').resolves()
sandbox.stub(uut, 'createHelia').resolves({}) sandbox.stub(uut, 'createHelia').resolves({})
+125
View File
@@ -128,4 +128,129 @@ describe('#nostr.js', () => {
assert.equal(result.length, 1) assert.equal(result.length, 1)
}) })
}) })
describe('#eventId2note', () => {
it('should handle encode error', async () => {
try {
await uut.eventId2note('invalid format')
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Invalid byte sequence')
}
})
it('should handle valid eventId', async () => {
const result = await uut.eventId2note('b1e4a3d5e40b6d0b3f1e2c234b6c9dbf4d6a80e9b3e8a2d765c7f9ed84c9a5ef')
assert.isString(result)
assert.include(result, 'note')
})
})
describe('#pubkey2npub', () => {
it('should handle encode error', async () => {
try {
await uut.pubkey2npub('invalid format')
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Invalid byte sequence')
}
})
it('should handle valid pubkey', async () => {
const result = await uut.pubkey2npub('f5a1b39d2cfae8e10e93b9b4d7ec8248eaf45fbe4a73409d8d4c67015e9729a3')
assert.isString(result)
assert.include(result, 'npub')
})
})
describe('#npub2pubkey', () => {
it('should handle decode error', async () => {
try {
await uut.npub2pubkey('npub17ksm88fvlt5wzr5nhx6d0myzfr40g')
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Invalid checksum')
}
})
it('should handle valid pubkey', async () => {
const result = await uut.npub2pubkey('npub17ksm88fvlt5wzr5nhx6d0myzfr40gha7ffe5p8vdf3nszh5h9x3sdwl7u2')
assert.isObject(result)
assert.property(result, 'type')
assert.property(result, 'data')
assert.equal(result.type, 'npub')
assert.equal(result.data.length, 64)
})
})
describe('#readGlobalFeed', () => {
it('should throw an error if provided limit is wrong', async () => {
try {
await uut.readGlobalFeed({ limit: -5 })
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Limit must be greater than 0')
}
})
it('should read message', async () => {
const result = await uut.readGlobalFeed()
assert.isArray(result)
})
it('should read message with custom limit', async () => {
const result = await uut.readGlobalFeed({ limit: 1 })
assert.isArray(result)
})
})
describe('#getFollowers', () => {
it('should throw an error if no pubkey is provided', async () => {
try {
await uut.getFollowers()
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'pubkey must be a string!')
}
})
it('should get followers', async () => {
const result = await uut.getFollowers({ pubkey: 'f5a1b39d2cfae8e10e93b9b4d7ec8248eaf45fbe4a73409d8d4c67015e9729a3' })
assert.isArray(result)
})
})
describe('#privKeyToNpub', () => {
it('should handle error if providedprivKey is not a string', async () => {
try {
uut.privKeyToNpub()
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'privKey must be a string!')
}
})
it('should return npub if privKey is valid', async () => {
const result = uut.privKeyToNpub('L2kh34VinQNRq1ad8nnXnGoFrb9pT7j8GJXiT6Scsg18KMxsUVDr')
// console.log('result: ', result)
assert.isString(result)
assert.include(result, 'npub')
})
it('should handle unexpected error', async () => {
try {
uut.privKeyToNpub('invalid format')
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Invalid base58 character')
}
})
})
}) })
+11 -1
View File
@@ -47,7 +47,17 @@ describe('#OrderPagination', () => {
timestamp: 'timestamp', timestamp: 'timestamp',
localTimestamp: 'localTimestamp', localTimestamp: 'localTimestamp',
txid: 'txid', txid: 'txid',
p2wdbHash: 'p2wdbHash' p2wdbHash: 'p2wdbHash',
hdIndex: 0,
nostrEventId: 'nostrEventId',
dataType: 'dataType',
makerAddr: 'makerAddr',
makerNpub: 'makerNpub',
rateInBaseUnit: 'rateInBaseUnit',
minUnitsToExchange: 'minUnitsToExchange',
ticker: 'ticker',
tokenType: 1,
userId: 'userId'
}) })
await order.save() await order.save()
}) })
@@ -41,5 +41,17 @@ describe('#passport.js', () => {
assert.include(err.message, 'cant auth user') assert.include(err.message, 'cant auth user')
} }
}) })
it('should authenticate user', async () => {
const ctx = {}
const errMock = null
const userMock = {
_id: '123',
email: 'test@test.com'
}
sandbox.stub(uut.passport, 'authenticate').yields(errMock, userMock)
const user = await uut.authUser(ctx)
assert.equal(user._id, userMock._id)
assert.equal(user.email, userMock.email)
})
}) })
}) })
+9 -1
View File
@@ -31,7 +31,8 @@ describe('#User-Adapter', () => {
testuser = new User({ testuser = new User({
email: 'test983@test.com', email: 'test983@test.com',
name: 'test983', name: 'test983',
password: 'password' password: 'password',
mnemonic: '12 words mnemonic'
}) })
}) })
@@ -54,6 +55,13 @@ describe('#User-Adapter', () => {
assert.notEqual(testuser.password, 'password') assert.notEqual(testuser.password, 'password')
}) })
it('should ignore password encrption if password property is not provided', async () => {
const lastPassword = testuser.password
await testuser.save()
// console.log('testuser: ', testuser)
assert.equal(testuser.password, lastPassword)
})
}) })
describe('#validatePassword', () => { describe('#validatePassword', () => {
+527
View File
@@ -0,0 +1,527 @@
/*
Unit tests for the adapter/wallet.js library.
*/
// Global npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
import WalletAdapter from '../../../src/adapters/wallet.js'
import { MockBchWallet } from '../mocks/adapters/wallet.js'
import { BchTokenSweepMock } from '../mocks/bch-token-sweep-mock.js'
import offerMockData from '../mocks/use-cases/offer-mock-data.js'
import fs from 'fs'
import * as url from 'url'
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
const testWalletFile = `${__dirname.toString()}/../../../test-wallet.json`
describe('#wallet', () => {
let uut
let sandbox
let testWalletData
before(() => {
// try to remove the test wallet file
try {
fs.unlinkSync(testWalletFile)
} catch (error) {
console.log('error fsUnlinkSync: ', error)
// silently fail
}
})
after(() => {
// try to remove the test wallet file
try {
fs.unlinkSync(testWalletFile)
} catch (error) {
console.log('error fsUnlinkSync: ', error)
// silently fail
}
})
beforeEach(() => {
uut = new WalletAdapter()
uut.BchWallet = MockBchWallet
uut.bchWallet = new MockBchWallet()
uut.BchTokenSweep = BchTokenSweepMock
uut.WALLET_FILE = testWalletFile
uut.bitcoinJs = {
Transaction: {
fromHex: () => { return {} },
SIGHASH_ALL: () => { return {} }
},
TransactionBuilder: {
fromTransaction: () => { return { sign: () => { return {} }, build: () => { return { toHex: () => { return 'hex' } } } } }
},
ECPair: {
fromWIF: () => { return {} }
}
}
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
describe('#openWallet', () => {
it('should create a new wallet when wallet file does not exist', async () => {
// Ensure we open the test file, not the production wallet file.
const result = await uut.openWallet()
testWalletData = result
// console.log('result: ', result)
assert.property(result, 'mnemonic')
assert.property(result, 'privateKey')
assert.property(result, 'publicKey')
assert.property(result, 'cashAddress')
assert.property(result, 'address')
assert.property(result, 'slpAddress')
assert.property(result, 'legacyAddress')
assert.property(result, 'hdPath')
})
it('should open existing wallet file', async () => {
// This test case uses the file created in the previous test case.
// Ensure we open the test file, not the production wallet file.
const result = await uut.openWallet()
// console.log('result: ', result)
assert.property(result, 'mnemonic')
assert.property(result, 'privateKey')
assert.property(result, 'publicKey')
assert.property(result, 'cashAddress')
assert.property(result, 'address')
assert.property(result, 'slpAddress')
assert.property(result, 'legacyAddress')
assert.property(result, 'hdPath')
})
it('should catch and throw an error', async () => {
try {
uut.WALLET_FILE = ''
// Force an error
uut.BchWallet = () => {
}
await uut.openWallet()
// console.log('result: ', result)
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'this.BchWallet is not a constructor')
}
})
})
describe('#instanceWallet', () => {
it('should create an instance of BchWallet with rest-api', async () => {
uut.config.useFullStackCash = true
const result = await uut.instanceWallet(testWalletData)
assert.equal(result.advancedOptions.interface, 'rest-api')
})
it('should create an instance of BchWallet with consumer-api', async () => {
uut.config.useFullStackCash = false
const result = await uut.instanceWallet(testWalletData)
assert.equal(result.advancedOptions.interface, 'consumer-api')
})
it('should catch and throw an error if no mnemonic is provided', async () => {
try {
// Force an error
await uut.instanceWallet({})
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Wallet data is not formatted correctly. Can not read mnemonic in wallet file!')
}
})
})
describe('#incrementNextAddress', () => {
it('should increment the nextAddress property', async () => {
const result = await uut.incrementNextAddress()
assert.equal(result, 2)
})
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox.stub(uut, 'openWallet').rejects(new Error('test error'))
await uut.incrementNextAddress()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#getKeyPair', () => {
it('should return an object with a key pair', async () => {
// Ensure we open the test file, not the production wallet file.
// uut.WALLET_FILE = testWalletFile
uut.config.walletFile = testWalletFile
const result = await uut.getKeyPair()
// console.log('result: ', result)
assert.property(result, 'cashAddress')
assert.property(result, 'wif')
assert.property(result, 'hdIndex')
})
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox
.stub(uut, 'incrementNextAddress')
.rejects(new Error('test error'))
await uut.getKeyPair()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#generateSignature', () => {
it('should sign message', async () => {
const result = await uut.generateSignature('unit test message')
// console.log('result: ', result)
assert.isString(result)
})
it('should catch and throw an error', async () => {
try {
await uut.generateSignature()
} catch (err) {
assert.include(err.message, 'message is required!')
}
})
})
describe('#generatePartialTx', () => {
it('should generate tx for fungible token', async () => {
const utxoInfo = {
txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
vout: 0,
sats: 1000,
wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3'
}
sandbox.stub(uut.bchWallet, 'getTxData').resolves([offerMockData.fungibleTokenData01.genesisData])
const result = await uut.generatePartialTx(offerMockData.fungibleOffer01, utxoInfo)
assert.isString(result)
})
it('should generate tx for nft', async () => {
const utxoInfo = {
txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
vout: 0,
sats: 1000,
wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3'
}
sandbox.stub(uut.bchWallet, 'getTxData').resolves([offerMockData.nftTokenData01.genesisData])
const result = await uut.generatePartialTx(offerMockData.nftOffer01, utxoInfo)
// console.log('result: ', result)
assert.isString(result)
})
it('should handle unknown token type', async () => {
try {
const utxoInfo = {
txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
vout: 0,
sats: 1000,
wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3'
}
const offerMock = Object.assign({}, offerMockData.nftOffer01)
offerMock.tokenType = 100
await uut.generatePartialTx(offerMock, utxoInfo)
} catch (err) {
assert.include(err.message, 'Unknown token type')
}
})
it('should handle multiple outputs', async () => {
const utxoInfo = {
txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
vout: 0,
sats: 1000,
wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3'
}
sandbox.stub(uut.bchWallet, 'getTxData').resolves([offerMockData.nftTokenData01.genesisData])
sandbox.stub(uut.bchWallet.bchjs.SLP.NFT1, 'generateNFTChildSendOpReturn').returns({ outputs: 5 })
const res = await uut.generatePartialTx(offerMockData.nftOffer01, utxoInfo)
assert.isUndefined(res)
})
})
describe('#moveTokens', () => {
it('should move tokens', async () => {
sandbox.stub(uut, 'getKeyPair').resolves({
cashAddress: 'cashAddress',
wif: 'wif',
hdIndex: 11
})
const input = {
tokenId: uut.bchWallet.utxos.utxoStore.slpUtxos.type1.tokens[0].tokenId,
qty: 1
}
const res = await uut.moveTokens(input)
assert.isObject(res)
})
it('should throw an error if the tokenId is not found', async () => {
try {
sandbox.stub(uut, 'getKeyPair').resolves({
cashAddress: 'cashAddress',
wif: 'wif',
hdIndex: 11
})
const input = {
tokenId: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
qty: 1
}
await uut.moveTokens(input)
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'tokenId not found!')
}
})
it('should throw an error if the tokenId is not provided', async () => {
try {
await uut.moveTokens({
qty: 1
})
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'tokenId must be a string!')
}
})
it('should throw an error if the qty is not provided', async () => {
try {
await uut.moveTokens({
tokenId: 'tokenId'
})
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'qty must be a number!')
}
})
})
describe('#moveBch', () => {
it('should move bch', async () => {
sandbox.stub(uut, 'getKeyPair').resolves({
cashAddress: 'cashAddress',
wif: 'wif',
hdIndex: 11
})
const res = await uut.moveBch(100)
assert.isObject(res)
})
it('should throw an error if the amountSat is not provided', async () => {
try {
sandbox.stub(uut, 'getKeyPair').resolves({
cashAddress: 'cashAddress',
wif: 'wif',
hdIndex: 11
})
await uut.moveBch()
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'amountSat is required!')
}
})
})
describe('#moveTokensFromCustomWallet', () => {
it('should move tokens from a custom wallet', async () => {
sandbox.stub(uut, 'getKeyPair').resolves({
cashAddress: 'cashAddress',
wif: 'wif',
hdIndex: 11
})
const customWallet = new MockBchWallet()
customWallet.getKeyPair = () => { return { cashAddress: 'cashAddress', wif: 'wif', hdIndex: 11 } }
customWallet.sendTokens = () => { return 'move token tx id result' }
customWallet.utxos.utxoStore.slpUtxos.type1.tokens = [
{
tokenId: 'tokenId',
tokenType: 1,
qty: 1
}
]
const inObj = {
qty: 1,
wallet: customWallet,
tokenId: 'tokenId'
}
const result = await uut.moveTokensFromCustomWallet(inObj)
assert.equal(result.txid, 'move token tx id result')
assert.equal(result.hdIndex, 11)
assert.equal(result.tokenType, 1)
assert.equal(result.vout, 1)
})
it('should throw an error if the wallet is not provided', async () => {
try {
const inObj = {
qty: 1,
tokenId: 'tokenId'
}
await uut.moveTokensFromCustomWallet(inObj)
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'wallet is required!')
}
})
it('should throw an error if the tokenId is not provided', async () => {
try {
const inObj = {
qty: 1,
wallet: new MockBchWallet()
}
await uut.moveTokensFromCustomWallet(inObj)
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'tokenId must be a string!')
}
})
it('should throw an error if the qty is not provided', async () => {
try {
const inObj = {
tokenId: 'tokenId',
wallet: new MockBchWallet()
}
await uut.moveTokensFromCustomWallet(inObj)
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'qty must be a number!')
}
})
})
describe('#deseralizeTx', () => {
it('should throw an error if the hex is not provided', async () => {
try {
await uut.deseralizeTx()
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'hex must be a string!')
}
})
it('should decode hex', async () => {
try {
sandbox.stub(uut.bchWallet.bchjs.RawTransactions, 'decodeRawTransaction').resolves({})
const hex = '020000000286b2959cccdb80163b68bb1360abac8e0228ba7b70c779443168cc65c7c42d820100000000ffffffff879b066729eda979f1f062c83b7eccdb9e77f49b417e4723864b3861bf061c24000000006b483045022100b99c6161e18e4e34c1e438b6b03e808703695f217b30da42c3f787798a9f40cc02206d26b4f6799735779d4fac823bc60d276fbb3cea91c66dbfa136cf5b1bd0d8324121028dcd38a58c8295dbd105ef524014f77af4cddfa19b26c50926b4aaeebd95c7ceffffffff030000000000000000376a04534c500001010453454e4420a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b208000000000000006422020000000000001976a914bef6c5180648165704f22e71a1b011ba56336aee88acf81d0000000000001976a914c1f16a3876f8fbe3701a66d3cb3b9c7abb07a06c88ac00000000'
const result = await uut.deseralizeTx(hex)
assert.isObject(result)
} catch (error) {
assert.fail('Unexpected code path')
}
})
})
describe('#completeTx', () => {
it('should throw an error if the hex is not provided', async () => {
try {
const hdIndex = 11
await uut.completeTx(null, hdIndex)
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'hex must be a string!')
}
})
it('should throw an error if the hdIndex is not provided', async () => {
try {
const hex = 'hex'
await uut.completeTx(hex)
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'hdIndex must be a non-negative number!')
}
})
it('should throw an error if the mnemonic is not provided', async () => {
try {
const hex = 'hex'
const hdIndex = 11
await uut.completeTx(hex, hdIndex)
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'mnemonic must be a string!')
}
})
it('should complete a transaction', async () => {
sandbox.stub(uut, 'getKeyPair').resolves({
cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
wif: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ',
hdIndex: 11
})
sandbox.stub(uut, 'deseralizeTx').resolves({
txid: 'complete tx id result'
})
sandbox.stub(uut.retryQueue, 'addToQueue').resolves('complete tx id result')
const hex = 'hex'
const hdIndex = 11
const mnemonic = 'course abstract aerobic deer try switch turtle diet fence affair butter top'
const txid = await uut.completeTx(hex, hdIndex, mnemonic)
assert.equal(txid, 'complete tx id result')
})
})
describe('#reClaimTokens', () => {
it('should re-claim tokens', async () => {
const orderData = {
hdIndex: 11,
tokenId: 'tokenId',
numTokens: 1
}
const res = await uut.reclaimTokens(orderData)
assert.equal(res, 'fakeTxid')
})
it('should handle error if order hd index is not provided', async () => {
try {
const orderData = {}
await uut.reclaimTokens(orderData)
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'orderData.hdIndex is required!')
}
})
})
describe('#cid2json', () => {
it('should throw error if cid is not provided', async () => {
try {
await uut.cid2json()
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'urlOrCid must be a string!')
}
})
it('should convert cid to json', async () => {
try {
sandbox.stub(uut.bchWallet, 'cid2json').resolves({ json: offerMockData.mutableDataMock })
const cid = 'bafkreidr6wfd6mcmwpea7abm5uk5rrprc2wfbcvo5wdcmtlolrpjab5oqm'
const result = await uut.cid2json(cid)
assert.isObject(result)
} catch (error) {
assert.fail('Unexpected code path')
}
})
it('should convert ipfs url to json', async () => {
try {
sandbox.stub(uut.bchWallet, 'cid2json').resolves({ json: offerMockData.mutableDataMock })
const url = 'https://ipfs.io/ipfs/bafkreidr6wfd6mcmwpea7abm5uk5rrprc2wfbcvo5wdcmtlolrpjab5oqm'
const result = await uut.cid2json(url)
assert.isObject(result)
} catch (error) {
assert.fail('Unexpected code path')
}
})
})
})
@@ -65,4 +65,51 @@ describe('#Webhook-Adapter', () => {
assert.isString(result.id) assert.isString(result.id)
}) })
}) })
describe('#deleteWebhook', () => {
it('should throw error if input is not provided', async () => {
try {
await uut.deleteWebhook()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'url must be a string')
}
})
it('should delete webhook', async () => {
sandbox.stub(uut.axios, 'delete').resolves({ data: { success: true } })
const url = 'https://test.com/my-webhook'
const result = await uut.deleteWebhook(url)
assert.isObject(result)
assert.property(result, 'success')
})
})
describe('#waitUntilSuccess', () => {
it('should handle error', async () => {
try {
sandbox.stub(uut, 'sleep').throws(new Error('test error'))
sandbox.stub(uut, 'deleteWebhook')
.onCall(0).throws(new Error('test error'))
sandbox.stub(uut, 'createWebhook')
.onCall(0).throws(new Error('test error'))
.onCall(1).resolves({ data: { success: true } })
await uut.waitUntilSuccess()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should wait until success', async () => {
uut.sleepTime = 100
sandbox.stub(uut, 'deleteWebhook')
.onCall(0).throws(new Error('test error'))
sandbox.stub(uut, 'createWebhook')
.onCall(0).throws(new Error('test error'))
.onCall(1).resolves({ data: { success: true } })
const result = await uut.waitUntilSuccess()
assert.isTrue(result)
})
})
}) })
+21
View File
@@ -38,4 +38,25 @@ describe('#Controllers', () => {
await uut.attachControllers(app) await uut.attachControllers(app)
}) })
}) })
describe('#attachRESTControllers', () => {
it('should attach the controllers', async () => {
const app = {
use: () => {}
}
await uut.attachRESTControllers(app)
})
})
describe('#initAdapters', () => {
it('should attach the controllers', async () => {
sandbox.stub(uut.adapters, 'start').resolves({})
await uut.initAdapters()
})
})
describe('#initUseCases', () => {
it('should attach the controllers', async () => {
sandbox.stub(uut.useCases, 'start').resolves({})
await uut.initUseCases()
})
})
}) })
@@ -79,11 +79,22 @@ describe('#Auth-REST-Router', () => {
await uut.authUser(ctx) await uut.authUser(ctx)
}) })
it('should catch and throw an error', async () => { it('should catch and throw passport error', async () => {
try { try {
// Force an error // Force an error
sandbox.stub(uut.passport, 'authUser').rejects('test error') sandbox.stub(uut.passport, 'authUser').rejects('test error')
await uut.authUser(ctx)
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Unauthorized')
}
})
it('should handle error if user is not found!', async () => {
try {
// Force an error
sandbox.stub(uut.passport, 'authUser').resolves(null)
await uut.authUser(ctx) await uut.authUser(ctx)
} catch (err) { } catch (err) {
// console.log('err: ', err) // console.log('err: ', err)
@@ -0,0 +1,66 @@
/*
Unit tests for the REST API middleware that handle response errors.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local libraries
import errorMiddleware from '../../../../../src/controllers/rest-api/middleware/error.js'
import { context as mockContext } from '../../../../unit/mocks/ctx-mock.js'
describe('#Validators', () => {
let ctx
let sandbox
beforeEach(() => {
// Mock the context object.
ctx = mockContext()
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
describe('#errorMiddleware', () => {
it('should run next function', async () => {
// Spy on next
const next = sinon.spy(() => { })
errorMiddleware()(ctx, next)
assert.isTrue(next.calledOnce)
})
it('should handle unknown status error', async () => {
try {
const next = async () => {
const e = new Error('test error')
e.status = null
throw e
}
await errorMiddleware()(ctx, next)
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(ctx.status, 500)
assert.equal(ctx.body, 'test error')
}
})
it('should handle known status error', async () => {
try {
const next = async () => {
const e = new Error('test error')
e.status = 422
throw e
}
await errorMiddleware()(ctx, next)
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(ctx.status, 422)
assert.equal(ctx.body, 'test error')
}
})
})
})
@@ -0,0 +1,201 @@
/*
Unit tests for the REST API handler for the /nostr endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
import NostrController from '../../../../../src/controllers/rest-api/nostr/controller.js'
import { context as mockContext } from '../../../../unit/mocks/ctx-mock.js'
let uut
let sandbox
let ctx
describe('#Nostr-REST-Controller', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new NostrController({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
ctx = mockContext()
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new NostrController()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating /nostr REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new NostrController({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating /nostr REST Controller.'
)
}
})
})
describe('#POST /nostr/deletedChat', () => {
it('should return 422 status on biz logic error', async () => {
try {
sandbox.stub(uut.useCases.nostr, 'createDeletedChat').rejects(new Error('test error'))
await uut.createDeletedChat(ctx)
assert.fail('Unexpected result')
} catch (err) {
// console.log(err)
assert.equal(err.status, 422)
assert.include(err.message, 'test error')
}
})
it('should return 200 status on success', async () => {
ctx.request.body = {
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q',
npub: 'npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8',
bchAddr: 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a',
pubkey: '6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1'
}
await uut.createDeletedChat(ctx)
// Assert the expected HTTP response
assert.equal(ctx.status, 200)
assert.isObject(ctx.response.body)
})
})
describe('#GET /nostr/deletedChat', () => {
it('should return 422 status on arbitrary biz logic error', async () => {
try {
// Force an error
sandbox
.stub(uut.useCases.nostr, 'getDeletedChats')
.rejects(new Error('test error'))
await uut.getDeletedChats(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(err.status, 422)
assert.include(err.message, 'test error')
}
})
it('should return 200 status on success', async () => {
await uut.getDeletedChats(ctx)
// Assert the expected HTTP response
assert.equal(ctx.status, 200)
// Assert that expected properties exist in the returned data.
assert.isObject(ctx.response.body)
assert.property(ctx.response.body, 'deletedChats')
assert.isArray(ctx.response.body.deletedChats)
})
})
describe('#POST /nostr/deletedPost', () => {
it('should return 422 status on biz logic error', async () => {
try {
sandbox.stub(uut.useCases.nostr, 'createDeletedPost').rejects(new Error('test error'))
await uut.createDeletedPost(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(err.status, 422)
assert.include(err.message, 'test error')
}
})
it('should return 200 status on success', async () => {
ctx.request.body = {
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q',
npub: 'npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8',
bchAddr: 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a',
pubkey: '6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1'
}
await uut.createDeletedPost(ctx)
assert.equal(ctx.status, 200)
assert.isObject(ctx.response.body)
assert.property(ctx.response.body, 'deletedPost')
assert.isObject(ctx.response.body.deletedPost)
})
})
describe('#GET /nostr/deletedPost', () => {
it('should return 422 status on biz logic error', async () => {
try {
sandbox.stub(uut.useCases.nostr, 'getDeletedPosts').rejects(new Error('test error'))
await uut.getDeletedPosts(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(err.status, 422)
assert.include(err.message, 'test error')
}
})
it('should return 200 status on success', async () => {
await uut.getDeletedPosts(ctx)
// Assert the expected HTTP response
assert.equal(ctx.status, 200)
assert.isObject(ctx.response.body)
assert.property(ctx.response.body, 'deletedPosts')
assert.isArray(ctx.response.body.deletedPosts)
})
})
describe('#handleError', () => {
it('should still throw error if there is no message', () => {
try {
const err = {
status: 404
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'Not Found')
}
})
it('should throw error with message', () => {
try {
const err = {
status: 422,
message: 'test error'
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
})
@@ -0,0 +1,108 @@
/*
Unit tests for the REST API handler for the /nostr endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
// const app = require('../../../mocks/app-mock')
import NostrRouter from '../../../../../src/controllers/rest-api/nostr/index.js'
let uut
let sandbox
// let ctx
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
describe('#Nostr-REST-Router', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new NostrRouter({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
// ctx = mockContext()
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new NostrRouter()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating NostrRouter REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new NostrRouter({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating NostrRouter REST Controller.'
)
}
})
})
describe('#attach', () => {
it('should throw an error if app is not passed in.', () => {
try {
uut.attach()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Must pass app object when attaching REST API controllers.'
)
}
})
})
describe('#createDeletedChat', () => {
it('should route to the createDeletedChat function', async () => {
// Stub functions
const validationSpy = sandbox.stub(uut.validators, 'ensureAdmin').resolves(true)
sandbox.stub(uut.nostrRESTController, 'createDeletedChat').resolves(true)
// Call function
const res = await uut.createDeletedChat()
assert.isTrue(validationSpy.calledOnce, 'Admin validator should be called')
assert.isTrue(res)
})
})
describe('#createDeletedPost', () => {
it('should route to the createDeletedPost function', async () => {
// Stub functions
const validationSpy = sandbox.stub(uut.validators, 'ensureAdmin').resolves(true)
sandbox.stub(uut.nostrRESTController, 'createDeletedPost').resolves(true)
// Call function
const res = await uut.createDeletedPost()
assert.isTrue(validationSpy.calledOnce, 'Admin validator should be called')
assert.isTrue(res)
})
})
})
@@ -91,25 +91,186 @@ describe('#Offer-REST-Router', () => {
// assert.equal(ctx.body.hash, 'testHash') // assert.equal(ctx.body.hash, 'testHash')
}) })
// it('should catch and throw an error', async () => { it('should catch and throw an error', async () => {
// try { try {
// ctx.request.body = { ctx.request.body = {
// offer: {} offer: {}
// } }
//
// // Force an error // Force an error
// sandbox sandbox
// .stub(uut.useCases.offer, 'createOffer') .stub(uut.useCases.offer, 'createOffer')
// .rejects(new Error('test error')) .rejects(new Error('test error'))
//
// await uut.createOffer(ctx) await uut.createOffer(ctx)
//
// assert.fail('Unexpected code path') assert.fail('Unexpected code path')
// } catch (err) { } catch (err) {
// // console.log('err: ', err) // console.log('err: ', err)
// assert.include(err.message, 'test error') assert.include(err.message, 'test error')
// } }
// }) })
})
describe('#listOffers', () => {
it('should list offers', async () => {
ctx.params = { page: 0 }
sandbox.stub(uut.useCases.offer, 'listOffers').resolves([])
await uut.listOffers(ctx)
assert.isArray(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { }
sandbox
.stub(uut.useCases.offer, 'listOffers')
.throws(new Error('test error'))
await uut.listOffers(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#listNftOffers', () => {
it('should list nft offers', async () => {
ctx.params = { page: 0 }
sandbox.stub(uut.useCases.offer, 'listNftOffers').resolves([])
await uut.listNftOffers(ctx)
assert.isArray(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { page: 0 }
sandbox
.stub(uut.useCases.offer, 'listNftOffers')
.throws(new Error('test error'))
await uut.listNftOffers(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#listFungibleOffers', () => {
it('should list fungible offers', async () => {
ctx.params = { page: 0 }
sandbox.stub(uut.useCases.offer, 'listFungibleOffers').resolves([])
await uut.listFungibleOffers(ctx)
assert.isArray(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { page: 0 }
sandbox
.stub(uut.useCases.offer, 'listFungibleOffers')
.throws(new Error('test error'))
await uut.listFungibleOffers(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#takeOffer', () => {
it('should take an offer', async () => {
ctx.request.body = {
nostrEventId: 'testEventId'
}
sandbox.stub(uut.useCases.offer, 'takeOffer').resolves({ eventId: 'testEventId', noteId: 'testNoteId' })
await uut.takeOffer(ctx)
assert.isObject(ctx.body)
assert.property(ctx.body, 'eventId')
assert.property(ctx.body, 'noteId')
})
it('should catch and throw an error', async () => {
try {
ctx.request.body = {
nostrEventId: 'testEventId'
}
sandbox
.stub(uut.useCases.offer, 'takeOffer')
.throws(new Error('test error'))
await uut.takeOffer(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#listOffersByAddress', () => {
it('should list offers by address', async () => {
ctx.params = { addr: 'testAddress' }
sandbox.stub(uut.useCases.offer, 'listOffersByAddress').resolves([])
await uut.listOffersByAddress(ctx)
assert.isArray(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { addr: 'testAddress' }
sandbox
.stub(uut.useCases.offer, 'listOffersByAddress')
.throws(new Error('test error'))
await uut.listOffersByAddress(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#syncOfferMutableData', () => {
it('should sync mutable data', async () => {
ctx.request.body = { tokenId: 'tokenId' }
sandbox.stub(uut.useCases.offer, 'syncOfferMutableData').resolves({})
await uut.syncOfferMutableData(ctx)
assert.isObject(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.request.body = { tokenId: 'tokenId' }
sandbox
.stub(uut.useCases.offer, 'syncOfferMutableData')
.throws(new Error('test error'))
await uut.syncOfferMutableData(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#listCounterOffersByAddress', () => {
it('should list counter offers by address', async () => {
ctx.params = { addr: 'testAddress' }
sandbox.stub(uut.useCases.offer, 'listCounterOffersByAddress').resolves([])
await uut.listCounterOffersByAddress(ctx)
assert.isArray(ctx.body.counterOffers)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { addr: 'testAddress' }
sandbox
.stub(uut.useCases.offer, 'listCounterOffersByAddress')
.throws(new Error('test error'))
await uut.listCounterOffersByAddress(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
}) })
describe('#handleError', () => { describe('#handleError', () => {
@@ -65,13 +65,17 @@ describe('#Order-REST-Router', () => {
ctx.request.body = { ctx.request.body = {
order: {} order: {}
} }
ctx.state.user = {
id: 'testUserId'
}
// Mock dependencies // Mock dependencies
sandbox.stub(uut.useCases.order, 'createOrder').resolves('testHash') sandbox.stub(uut.useCases.order, 'createOrder').resolves({ eventId: 'testEventId', noteId: 'testNoteId' })
await uut.createOrder(ctx) await uut.createOrder(ctx)
assert.equal(ctx.body.hash, 'testHash') assert.equal(ctx.body.eventId, 'testEventId')
assert.equal(ctx.body.noteId, 'testNoteId')
}) })
it('should catch and throw an error', async () => { it('should catch and throw an error', async () => {
@@ -79,6 +83,9 @@ describe('#Order-REST-Router', () => {
ctx.request.body = { ctx.request.body = {
order: {} order: {}
} }
ctx.state.user = {
id: 'testUserId'
}
// Force an error // Force an error
sandbox sandbox
@@ -94,7 +101,47 @@ describe('#Order-REST-Router', () => {
} }
}) })
}) })
describe('#listOrders', () => {
it('should list orders', async () => {
ctx.params = { page: 0 }
sandbox.stub(uut.useCases.order, 'listOrders').resolves([])
await uut.listOrders(ctx)
assert.isArray(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { page: 0 }
sandbox.stub(uut.useCases.order, 'listOrders').throws(new Error('test error'))
await uut.listOrders(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#deleteOrder', () => {
it('should delete an order', async () => {
ctx.request.body = {
nostrEventId: 'testEventId'
}
sandbox.stub(uut.useCases.order, 'deleteOrder').resolves('testTxid')
await uut.deleteOrder(ctx)
assert.equal(ctx.body.txid, 'testTxid')
})
it('should catch and throw an error', async () => {
try {
ctx.request.body = {
nostrEventId: 'testEventId'
}
sandbox.stub(uut.useCases.order, 'deleteOrder').throws(new Error('test error'))
await uut.deleteOrder(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#handleError', () => { describe('#handleError', () => {
it('should still throw error if there is no message', () => { it('should still throw error if there is no message', () => {
try { try {
@@ -74,4 +74,22 @@ describe('#Order-REST-Router', () => {
} }
}) })
}) })
describe('#createOrder', () => {
it('should route to controller', async () => {
sandbox.stub(uut.validators, 'ensureUser').resolves(true)
const spy = sandbox.stub(uut.orderRESTController, 'createOrder').resolves(true)
await uut.createOrder()
assert.isTrue(spy.calledOnce)
})
})
describe('#deleteOrder', () => {
it('should route to controller', async () => {
sandbox.stub(uut.validators, 'ensureUser').resolves(true)
const spy = sandbox.stub(uut.orderRESTController, 'deleteOrder').resolves(true)
await uut.deleteOrder()
assert.isTrue(spy.calledOnce)
})
})
}) })
@@ -0,0 +1,160 @@
/*
Unit tests for the REST API handler for the /sm endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
import P2WDBRESTController from '../../../../../src/controllers/rest-api/p2wdb/controller.js'
import { context as mockContext } from '../../../../unit/mocks/ctx-mock.js'
let uut
let sandbox
let ctx
describe('#P2WDB-REST-Router', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new P2WDBRESTController({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
ctx = mockContext()
ctx.params = { }
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new P2WDBRESTController()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating /p2wdb REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new P2WDBRESTController({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating /p2wdb REST Controller.'
)
}
})
})
describe('#routeWebhook', () => {
it('should route the webhook for a new counter offer', async () => {
ctx.request = {
body: {
data: {
dataType: 'counter-offer'
}
}
}
sandbox.stub(uut.useCases.offer, 'acceptCounterOffer').resolves()
await uut.routeWebhook(ctx)
assert.isTrue(true)
})
it('should route the webhook for a new offer', async () => {
ctx.request = {
body: {
data: {
dataType: 'offer'
}
}
}
sandbox.stub(uut.useCases.offer, 'createOffer').resolves()
await uut.routeWebhook(ctx)
assert.isTrue(true)
})
it('should route the webhook flag', async () => {
ctx.request = {
body: {
data: {
dataType: 'flag'
}
}
}
sandbox.stub(uut.useCases.offer, 'flagOffer').resolves()
await uut.routeWebhook(ctx)
assert.isTrue(true)
})
it('should handle unknown data type', async () => {
ctx.request = {
body: {
data: {
dataType: 'unknown'
}
}
}
sandbox.stub(uut.useCases.offer, 'flagOffer').resolves()
await uut.routeWebhook(ctx)
assert.isTrue(true)
})
it('should catch and throw an error', async () => {
try {
ctx.request.body = {}
await uut.routeWebhook(ctx)
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Cannot read properties of undefined')
}
})
describe('#handleError', () => {
it('should still throw error if there is no message', () => {
try {
const err = {
status: 404
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'Not Found')
}
})
it('should catch error if message is provided', () => {
try {
const err = {
status: 422,
message: 'Unprocessable Entity'
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'Unprocessable Entity')
}
})
})
})
})
@@ -0,0 +1,77 @@
/*
Unit tests for the REST API handler for the /p2wdb endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
import P2WDBRouter from '../../../../../src/controllers/rest-api/p2wdb/index.js'
let uut
let sandbox
// let ctx
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
describe('#P2WDB-REST-Router', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new P2WDBRouter({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
// ctx = mockContext()
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new P2WDBRouter()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating /p2wdb REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new P2WDBRouter({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating /p2wdb REST Controller.'
)
}
})
})
describe('#attach', () => {
it('should throw an error if app is not passed in.', () => {
try {
uut.attach()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Must pass app object when attached REST API controllers.'
)
}
})
})
})
@@ -20,7 +20,7 @@ describe('#RESTControllers', () => {
let sandbox let sandbox
// let ctx // let ctx
before(async () => {}) before(async () => { })
beforeEach(() => { beforeEach(() => {
const useCases = new UseCasesMock() const useCases = new UseCasesMock()
@@ -64,4 +64,19 @@ describe('#RESTControllers', () => {
} }
}) })
}) })
describe('#attachRESTControllers', () => {
it('should attach controllers without mongo service', () => {
uut.config.noMongo = true
const app = { use: () => {} }
uut.attachRESTControllers(app)
})
it('should attach controllers with mongo service', () => {
uut.config.noMongo = false
const app = { use: () => {} }
uut.attachRESTControllers(app)
})
})
}) })
@@ -0,0 +1,203 @@
/*
Unit tests for the REST API handler for the /sm endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
import SmAccountRESTController from '../../../../../src/controllers/rest-api/smAccount/controller.js'
import { context as mockContext } from '../../../../unit/mocks/ctx-mock.js'
let uut
let sandbox
let ctx
describe('#SmAccount-REST-Router', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new SmAccountRESTController({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
ctx = mockContext()
ctx.params = { }
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new SmAccountRESTController()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating /sm REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new SmAccountRESTController({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating /sm REST Controller.'
)
}
})
})
describe('#listAccounts', () => {
it('should list all social media accounts', async () => {
ctx.params.page = 0
// Mock dependencies
sandbox.stub(uut.useCases.smAccount, 'listAccounts').resolves([])
await uut.listAccounts(ctx)
assert.isArray(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.params.page = 0
// Force an error
sandbox.stub(uut.useCases.smAccount, 'listAccounts')
.throws(new Error('test error'))
await uut.listAccounts(ctx)
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'test error')
}
})
})
describe('#getAccountByNpub', () => {
it('should get a social media account by npub', async () => {
sandbox.stub(uut.useCases.smAccount, 'getAccountByNpub').resolves({ npub: 'testNpub' })
ctx.params.npub = 'testNpub'
await uut.getAccountByNpub(ctx)
})
it('should throw 400 if npub is not provided', async () => {
try {
await uut.getAccountByNpub(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Npub is required')
}
})
it('should throw 400 if npub is not found', async () => {
try {
sandbox.stub(uut.useCases.smAccount, 'getAccountByNpub').resolves(null)
ctx.params.npub = 'testNpub'
await uut.getAccountByNpub(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Account not found')
}
})
it('should catch and throw an error', async () => {
try {
sandbox.stub(uut.useCases.smAccount, 'getAccountByNpub').throws(new Error('test error'))
ctx.params.npub = 'testNpub'
await uut.getAccountByNpub(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#getAccountByBchAddr', () => {
it('should get a social media account by npub', async () => {
sandbox.stub(uut.useCases.smAccount, 'getAccountByBchAddr').resolves({ bchAddr: 'testBchAddr' })
ctx.params.bchAddr = 'testBchAddr'
await uut.getAccountByBchAddr(ctx)
})
it('should throw 400 if bchAddr is not provided', async () => {
try {
await uut.getAccountByBchAddr(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'BCH address is required')
}
})
it('should throw 400 if bchAddr is not found', async () => {
try {
sandbox.stub(uut.useCases.smAccount, 'getAccountByBchAddr').resolves(null)
ctx.params.bchAddr = 'testBchAddr'
await uut.getAccountByBchAddr(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Account not found')
}
})
it('should catch and throw an error', async () => {
try {
sandbox.stub(uut.useCases.smAccount, 'getAccountByBchAddr').throws(new Error('test error'))
ctx.params.bchAddr = 'testBchAddr'
await uut.getAccountByBchAddr(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#handleError', () => {
it('should still throw error if there is no message', () => {
try {
const err = {
status: 404
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'Not Found')
}
})
it('should catch error if message is provided', () => {
try {
const err = {
status: 422,
message: 'Unprocessable Entity'
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'Unprocessable Entity')
}
})
})
})
@@ -0,0 +1,77 @@
/*
Unit tests for the REST API handler for the /sm endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
import SmAccountRouter from '../../../../../src/controllers/rest-api/smAccount/index.js'
let uut
let sandbox
// let ctx
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
describe('#SmAccount-REST-Router', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new SmAccountRouter({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
// ctx = mockContext()
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new SmAccountRouter()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating /sm REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new SmAccountRouter({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating /sm REST Controller.'
)
}
})
})
describe('#attach', () => {
it('should throw an error if app is not passed in.', () => {
try {
uut.attach()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Must pass app object when attached REST API controllers.'
)
}
})
})
})
@@ -0,0 +1,163 @@
/*
Unit tests for the REST API handler for the /usage endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
import UsageController from '../../../../../src/controllers/rest-api/usage/controller.js'
import { context as mockContext } from '../../../mocks/ctx-mock.js'
let uut
let sandbox
let ctx
describe('#Usage-REST-Controller', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new UsageController({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
ctx = mockContext()
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new UsageController()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating /usage REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new UsageController({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating /usage REST Controller.'
)
}
})
})
describe('#Get /usage', () => {
it('should return 422 status on biz logic error', async () => {
try {
sandbox.stub(uut.useCases.usage, 'getRestSummary').throws(new Error('test error'))
await uut.getStatus(ctx)
assert.fail('Unexpected result')
} catch (err) {
// console.log(err)
assert.equal(err.status, 422)
assert.include(err.message, 'test error')
}
})
it('should return 200 status on success', async () => {
await uut.getStatus(ctx)
// Assert the expected HTTP response
assert.equal(ctx.status, 200)
// Assert that expected properties exist in the returned data.
assert.property(ctx.response.body, 'status')
})
})
describe('#Get /ips', () => {
it('should return 422 status on biz logic error', async () => {
try {
sandbox.stub(uut.useCases.usage, 'getTopIps').throws(new Error('test error'))
await uut.getTopIps(ctx)
assert.fail('Unexpected result')
} catch (err) {
// console.log(err)
assert.equal(err.status, 422)
assert.include(err.message, 'test error')
}
})
it('should return 200 status on success', async () => {
await uut.getTopIps(ctx)
// Assert the expected HTTP response
assert.equal(ctx.status, 200)
// Assert that expected properties exist in the returned data.
assert.property(ctx.response.body, 'ips')
})
})
describe('#Get /endpoints', () => {
it('should return 422 status on biz logic error', async () => {
try {
sandbox.stub(uut.useCases.usage, 'getTopEndpoints').throws(new Error('test error'))
await uut.getTopEndpoints(ctx)
assert.fail('Unexpected result')
} catch (err) {
// console.log(err)
assert.equal(err.status, 422)
assert.include(err.message, 'test error')
}
})
it('should return 200 status on success', async () => {
await uut.getTopEndpoints(ctx)
// Assert the expected HTTP response
assert.equal(ctx.status, 200)
// Assert that expected properties exist in the returned data.
assert.property(ctx.response.body, 'endpoints')
})
})
describe('#handleError', () => {
it('should pass an error message', () => {
try {
const err = {
status: 422,
message: 'Unprocessable Entity'
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'Unprocessable Entity')
}
})
it('should still throw error if there is no message', () => {
try {
const err = {
status: 404
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'Not Found')
}
})
})
})
@@ -0,0 +1,75 @@
/*
Unit tests for the REST API handler for the /usage endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
import UsageRouter from '../../../../../src/controllers/rest-api/usage/index.js'
let uut
let sandbox
// let ctx
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
describe('#Usage-REST-Router', () => {
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new UsageRouter({ adapters, useCases })
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new UsageRouter()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating IPFS REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new UsageRouter({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating IPFS REST Controller.'
)
}
})
})
describe('#attach', () => {
it('should throw an error if app is not passed in.', () => {
try {
uut.attach()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Must pass app object when attaching REST API controllers.'
)
}
})
})
})
@@ -153,6 +153,20 @@ describe('#Users-REST-Controller', () => {
// Assert that expected properties exist in the returned data. // Assert that expected properties exist in the returned data.
assert.property(ctx.response.body, 'user') assert.property(ctx.response.body, 'user')
}) })
it('should run next function if it exists', async () => {
// Mock dependencies
const nextSpy = sandbox.spy()
sandbox.stub(uut.useCases.user, 'getUser').resolves({ _id: '123' })
await uut.getUser(ctx, nextSpy)
// Assert the expected HTTP response
assert.equal(ctx.status, 200)
// Assert that expected properties exist in the returned data.
assert.property(ctx.response.body, 'user')
assert.isTrue(nextSpy.calledOnce)
})
it('should return other error status passed by biz logic', async () => { it('should return other error status passed by biz logic', async () => {
try { try {
@@ -79,4 +79,73 @@ describe('#Users-REST-Router', () => {
} }
}) })
}) })
describe('#createUser', () => {
it('should ignore admin validator when DISABLE_NEW_ACCOUNTS is not defined', async () => {
// Stub functions
const validationSpy = sandbox.stub(uut.validators, 'ensureAdmin').resolves(true)
sandbox.stub(uut.userRESTController, 'createUser').resolves(true)
// Call function
await uut.createUser()
// Assertions
assert.isTrue(validationSpy.notCalled, 'Admin validator should not be called')
})
it('should ensure admin when DISABLE_NEW_ACCOUNTS is defined', async () => {
// Set environment variable
process.env.DISABLE_NEW_ACCOUNTS = true
// Stub functions
const validationSpy = sandbox.stub(uut.validators, 'ensureAdmin').resolves(true)
sandbox.stub(uut.userRESTController, 'createUser').resolves(true)
// Call function
await uut.createUser()
// Assertions
assert.isTrue(validationSpy.calledOnce, 'Admin validator should be called')
})
})
describe('#getAll', () => {
it('should route to controller', async () => {
sandbox.stub(uut.validators, 'ensureAdmin').resolves(true)
const spy = sandbox.stub(uut.userRESTController, 'getUsers').resolves(true)
await uut.getAll()
assert.isTrue(spy.calledOnce)
})
})
describe('#getById', () => {
it('should route to controller', async () => {
sandbox.stub(uut.validators, 'ensureTargetUserOrAdmin').resolves(true)
const spy = sandbox.stub(uut.userRESTController, 'getUser').resolves(true)
await uut.getById()
assert.isTrue(spy.calledOnce)
})
})
describe('#updateUser', () => {
it('should route to controller', async () => {
sandbox.stub(uut.validators, 'ensureTargetUserOrAdmin').resolves(true)
sandbox.stub(uut.userRESTController, 'getUser').resolves(true)
const spy = sandbox.stub(uut.userRESTController, 'updateUser').resolves(true)
await uut.updateUser()
assert.isTrue(spy.calledOnce)
})
})
describe('#deleteUser', () => {
it('should route to controller', async () => {
sandbox.stub(uut.validators, 'ensureTargetUserOrAdmin').resolves(true)
sandbox.stub(uut.userRESTController, 'getUser').resolves(true)
const spy = sandbox.stub(uut.userRESTController, 'deleteUser').resolves(true)
await uut.deleteUser()
assert.isTrue(spy.calledOnce)
})
})
}) })
+146 -22
View File
@@ -56,27 +56,151 @@ describe('#Timer-Controllers', () => {
}) })
}) })
// describe('#startTimers', () => { describe('#startTimers', () => {
// it('should start the timers', () => { it('should start the timers', () => {
// const result = uut.startTimers() const result = uut.startTimers()
//
// // uut.stopTimers()
//
// assert.equal(result, true)
// })
// })
// describe('#exampleTimerFunc', () => { // uut.stopTimers()
// it('should kick off the Use Case', async () => {
// const result = await uut.exampleTimerFunc() assert.equal(result, true)
// })
// assert.equal(result, true) })
// })
// describe('#gcOrders', () => {
// it('should return false on error', async () => { it('should kick off the Use Case', async () => {
// const result = await uut.exampleTimerFunc(true) const result = await uut.gcOrders()
//
// assert.equal(result, false) assert.equal(result, true)
// }) })
// })
it('should return false on error', async () => {
sandbox.stub(uut.useCases.order, 'removeStaleOrders').throws(new Error('test error'))
const result = await uut.gcOrders()
assert.equal(result, false)
})
})
describe('#gcOffers', () => {
it('should kick off the Use Case', async () => {
const result = await uut.gcOffers()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.offer, 'removeStaleOffers').throws(new Error('test error'))
const result = await uut.gcOffers()
assert.equal(result, false)
})
})
describe('#checkDupOffers', () => {
it('should kick off the Use Case', async () => {
const result = await uut.checkDupOffers()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.offer, 'removeDuplicateOffers').throws(new Error('test error'))
const result = await uut.checkDupOffers()
assert.equal(result, false)
})
})
describe('#loadOffers', () => {
it('should kick off the Use Case', async () => {
const result = await uut.loadOffers()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.offer, 'loadOffers').throws(new Error('test error'))
const result = await uut.loadOffers()
assert.equal(result, false)
})
})
describe('#cleanUsage', () => {
it('should kick off the Use Case', async () => {
const result = await uut.cleanUsage()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.usage, 'cleanUsage').throws(new Error('test error'))
const result = await uut.cleanUsage()
assert.equal(result, false)
})
})
describe('#backupUsage', () => {
it('should kick off the Use Case', async () => {
const result = await uut.backupUsage()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.usage, 'clearUsage').throws(new Error('test error'))
// sandbox.stub(uut.useCases.usage, 'saveUsage').throws(new Error('test error'))
const result = await uut.backupUsage()
assert.equal(result, false)
})
})
describe('#newSmAccounts', () => {
it('should kick off the Use Case', async () => {
const result = await uut.newSmAccounts()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.smAccount, 'checkForNewSmAccounts').throws(new Error('test error'))
// sandbox.stub(uut.useCases.usage, 'saveUsage').throws(new Error('test error'))
const result = await uut.newSmAccounts()
assert.equal(result, false)
})
})
describe('#updateSmAccounts', () => {
it('should kick off the Use Case', async () => {
const result = await uut.updateSmAccounts()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.smAccount, 'updateSmAccounts').throws(new Error('test error'))
// sandbox.stub(uut.useCases.usage, 'saveUsage').throws(new Error('test error'))
const result = await uut.updateSmAccounts()
assert.equal(result, false)
})
})
describe('#removeOlderDeletedChatsAndPosts', () => {
it('should kick off the Use Cases and return true', async () => {
sandbox.stub(uut.useCases.nostr, 'removeOlderDeletedChats').resolves()
sandbox.stub(uut.useCases.nostr, 'removeOlderDeletedPosts').resolves()
const result = await uut.removeOlderDeletedChatsAndPosts()
assert.equal(result, true)
})
})
it('should kick off the Use Cases and return false on error', async () => {
sandbox.stub(uut.useCases.nostr, 'removeOlderDeletedChats').throws(new Error('test error'))
sandbox.stub(uut.useCases.nostr, 'removeOlderDeletedPosts').throws(new Error('test error'))
const result = await uut.removeOlderDeletedChatsAndPosts()
assert.equal(result, false)
})
}) })

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