Compare commits

...
82 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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 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 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
79 changed files with 30997 additions and 374 deletions
+19 -11
View File
@@ -31,24 +31,32 @@ The above software is orchestrated using [Docker](https://www.docker.com/) and D
Setup instructions:
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:
- 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`.
- Install dependencies by running `npm install`
- Create a wallet:
- `cd production/scripts`
- `node create-wallet.js`
1. Change directory to the `production/docker` folder.
1. Pull the Docker images down from Docker Hub: `docker-compose pull`
1. Build the core software: `docker-compose build --no-cache`
1. Start the Docker containers with `docker-compose up -d`
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.
1. To take the other side of the trade, click the `Take` button in the UI.
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).
- Change directory to the `production/docker` folder.
- Pull the Docker images down from Docker Hub: `docker-compose pull`
- Build the back end: `docker-compose build --no-cache bch-dex`
- Build the front end: `docker-compose build --no-cache dex-ui`
- Start the Docker containers with `docker-compose up -d`
- It will take about 10 minutes for the bch-dex to sync with the network and display tokens for sale.
- 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
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.
## Environment Variables
- `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.
- `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.
## License
[MIT](./LICENSE.md)
+8 -2
View File
@@ -48,6 +48,7 @@ export default {
consumerUrl: process.env.CONSUMER_URL
? process.env.CONSUMER_URL
: 'https://free-bch.fullstack.cash',
// : 'https://dev-consumer.psfoundation.info',
// : 'https://wa-usa-bch-consumer.fullstackcash.nl',
// P2WDB URL that will accept API calls from the p2wdb npm library.
@@ -183,11 +184,16 @@ export default {
// Nostr
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
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

+26701
View File
File diff suppressed because it is too large Load Diff
-14
View File
@@ -9861,20 +9861,6 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "http://94.130.170.209:4873/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "http://94.130.170.209:4873/function-bind/-/function-bind-1.1.2.tgz",
+3 -2
View File
@@ -6,10 +6,11 @@
"type": "module",
"scripts": {
"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: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/",
"start:e2e:server": "export BCH_DEX=test && node index.js",
"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/",
"lint": "standard --env mocha --fix",
+3
View File
@@ -70,4 +70,7 @@ EXPOSE 80
# Copy the files built in the first container to the new NGINX container.
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
+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;
}
}
+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) {
try {
if (!slpAddr || typeof slpAddr !== 'string') {
+4
View File
@@ -7,6 +7,8 @@ import Users from './models/users.js'
import Entries from './models/entry.js'
import Order from './models/order.js'
import Offer from './models/offer.js'
import Usage from './models/usage.js'
import SmAccount from './models/smAccount.js'
class LocalDB {
constructor () {
@@ -15,6 +17,8 @@ class LocalDB {
this.Entry = Entries
this.Order = Order
this.Offer = Offer
this.Usage = Usage
this.SmAccount = SmAccount
}
}
+13 -1
View File
@@ -35,8 +35,20 @@ const Offer = new mongoose.Schema({
lokadId: { type: String },
messageType: { type: Number },
messageClass: { type: Number },
nostrEventId: { type: String } // Nostr Event Id.
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 },
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)
+7 -1
View File
@@ -41,7 +41,13 @@ const Order = new mongoose.Schema({
nostrEventId: { type: String, required: true }, // Nostr Event Id.
// Additional properties found in createOrder
dataType: { type: String, required: true }
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)
+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)
+116
View File
@@ -12,6 +12,7 @@ class NostrAdapter {
constructor (localConfig = { nostrRelay: '', nostrTopic: '' }) {
this.relayWs = localConfig.nostrRelay //
this.topic = localConfig.nostrTopic
this.globalFeed = localConfig.nostrGlobalFeed
if (!this.relayWs) {
throw new Error(
@@ -39,6 +40,10 @@ class NostrAdapter {
this.post = this.post.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)
}
// Create nostr keys.
@@ -131,6 +136,117 @@ class NostrAdapter {
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
}
}
}
export default NostrAdapter
+70 -10
View File
@@ -34,6 +34,7 @@ class WalletAdapter {
this.config = config
this.bitcoinJs = bitcoinJs
this.BchWallet = BchWallet
this.BchTokenSweep = BchTokenSweep
this.bchWallet = {} // Will be replaced when initialized.
// this.advancedConfig = localConfig.advancedConfig
this.retryQueue = new RetryQueue({
@@ -55,6 +56,7 @@ class WalletAdapter {
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.
@@ -161,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,
// 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) {
try {
if (!hdIndex) {
@@ -173,6 +174,7 @@ class WalletAdapter {
}
const mnemonic = this.bchWallet.walletInfo.mnemonic
console.log(`getKeyPair() for HD index: ${hdIndex} called on mnemonic: ${mnemonic}`)
// root seed buffer
const rootSeed = await this.bchWallet.bchjs.Mnemonic.toSeed(mnemonic)
@@ -205,7 +207,9 @@ class WalletAdapter {
// Generate a cryptographic signature, required to write to the P2WDB.
async generateSignature (message) {
try {
// TODO: Add input validation for message.
if (!message || typeof message !== 'string') {
throw new Error('message is required!')
}
const privKey = this.bchWallet.walletInfo.privateKey
@@ -370,6 +374,12 @@ class WalletAdapter {
async moveTokens (inObj = {}) {
try {
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()
console.log('keyPair: ', keyPair)
@@ -392,6 +402,10 @@ class WalletAdapter {
x => x.tokenId === tokenId
)
if (tokenUtxos.length === 0) {
throw new Error('tokenId not found!')
}
const txid = await this.bchWallet.sendTokens(receiver, 3)
const utxoInfo = {
@@ -412,6 +426,10 @@ class WalletAdapter {
// generate a segregated UTXO.
async moveBch (amountSat) {
try {
if (!amountSat) {
throw new Error('amountSat is required!')
}
const keyPair = await this.getKeyPair()
console.log('keyPair: ', keyPair)
@@ -444,6 +462,10 @@ class WalletAdapter {
// representing the transaction.
async deseralizeTx (txHex) {
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
// yet support this call.
const oldUrl = this.bchWallet.bchjs.RawTransactions.restURL
@@ -465,7 +487,7 @@ class WalletAdapter {
// Complete the partially signed transaction by signing the first input,
// then broadcasting the transaction to the network.
async completeTx (hex, hdIndex) {
async completeTx (hex, hdIndex, mnemonic) {
try {
// Input validation
if (!hex || typeof hex !== 'string') {
@@ -474,10 +496,17 @@ class WalletAdapter {
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('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
// const transactionBuilder = new bchjs.TransactionBuilder()
@@ -500,10 +529,10 @@ class WalletAdapter {
// 'mainnet'
// )
console.log('completeTx() this.walletInfo: ', this.walletInfo)
console.log('completeTx() userWallet.walletInfo: ', userWallet.walletInfo)
// 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)}`)
const makerECPair = bchjs.ECPair.fromWIF(keyPair.wif)
@@ -548,8 +577,12 @@ class WalletAdapter {
// 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.
async reclaimTokens (orderData) {
async reclaimTokens (orderData = {}) {
try {
if (!orderData.hdIndex) {
throw new Error('orderData.hdIndex is required!')
}
// Get the key pair from the Order.
const keyPair = await this.getKeyPair(orderData.hdIndex)
console.log('keyPair: ', keyPair)
@@ -569,7 +602,7 @@ class WalletAdapter {
// const txid = await tempWallet.sendTokens(receiver)
// Sweep the tokens to the root address.
const sweeper = new BchTokenSweep(
const sweeper = new this.BchTokenSweep(
keyPair.wif,
this.bchWallet.walletInfo.privateKey,
this.bchWallet,
@@ -587,6 +620,8 @@ class WalletAdapter {
}
}
// 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
@@ -601,7 +636,13 @@ class WalletAdapter {
throw new Error('wallet is required!')
}
const keyPair = await this.getKeyPair()
// 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 = {
@@ -637,6 +678,25 @@ class WalletAdapter {
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
+15 -13
View File
@@ -6,13 +6,15 @@ import config from '../../config/index.js'
import axios from 'axios'
let _this
class WebHook {
constructor () {
_this = this
_this.config = config
_this.axios = axios
this.config = config
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
@@ -22,14 +24,14 @@ class WebHook {
throw new Error('url must be a string')
}
const endpoint = _this.config.webhookService
const endpoint = this.config.webhookService
const obj = {
appId: this.config.p2wdbAppId,
url
}
const result = await axios.post(endpoint, obj)
const result = await this.axios.post(endpoint, obj)
return result.data
} catch (err) {
@@ -45,14 +47,14 @@ class WebHook {
throw new Error('url must be a string')
}
const endpoint = _this.config.webhookService
const endpoint = this.config.webhookService
const obj = {
appId: this.config.p2wdbAppId,
url
}
const result = await axios.delete(endpoint, { data: obj })
const result = await this.axios.delete(endpoint, { data: obj })
return result.data
} catch (err) {
@@ -86,7 +88,7 @@ class WebHook {
} catch (err) {
const now = new Date()
console.log(`${now.toLocaleString()}: Error trying to create webhook with P2WDB. Trying again...`)
await sleep(2000)
await this.sleep(this.sleepTime)
}
} while (!success)
@@ -96,10 +98,10 @@ class WebHook {
throw err
}
}
}
function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
async sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
}
export default WebHook
@@ -29,7 +29,36 @@ class EntryRESTControllerLib {
_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
// Note: this is a private endpoint, needs to be authenticated
async createEntry (ctx) {
try {
console.log('body: ', ctx.request.body)
+4
View File
@@ -18,6 +18,7 @@ import OfferRouter from './offer/index.js'
import OrderRouter from './order/index.js'
import P2WDBRouter from './p2wdb/index.js'
import UsageRESTController from './usage/index.js'
import SmAccountRouter from './smAccount/index.js'
class RESTControllers {
constructor (localConfig = {}) {
@@ -86,6 +87,9 @@ class RESTControllers {
// 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)
}
}
+70 -3
View File
@@ -40,11 +40,23 @@ class IpfsRESTControllerLib {
* @api {get} /ipfs Get status on IPFS infrastructure
* @apiPermission public
* @apiName GetIpfsStatus
* @apiGroup REST BCH
* @apiGroup REST IPFS
*
* @apiExample Example usage:
* 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) {
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) {
try {
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.
async getRelays (ctx) {
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) {
try {
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
* @apiPermission public
* @apiName GetThisNode
* @apiGroup REST BCH
* @apiGroup REST IPFS
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/node
@@ -1,5 +1,8 @@
/*
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'
@@ -30,10 +33,12 @@ class OfferRESTControllerLib {
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.handleError = this.handleError.bind(this)
}
// No api-doc documentation because this wont be a public endpoint
// This function has no endpoint
async createOffer (ctx) {
try {
console.log('body: ', ctx.request.body)
@@ -52,6 +57,65 @@ class OfferRESTControllerLib {
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
async listOffers (ctx) {
@@ -68,6 +132,66 @@ class OfferRESTControllerLib {
}
}
/**
* @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
async listNftOffers (ctx) {
try {
@@ -83,6 +207,65 @@ class OfferRESTControllerLib {
}
}
/**
* @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
async listFungibleOffers (ctx) {
try {
@@ -98,6 +281,27 @@ class OfferRESTControllerLib {
}
}
/**
* @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'
// listed in the offer.
async takeOffer (ctx) {
@@ -115,6 +319,82 @@ class OfferRESTControllerLib {
}
}
/**
* @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)
}
}
// DRY error handler
handleError (ctx, err) {
console.log('err', err.message)
+10 -10
View File
@@ -8,8 +8,6 @@ import Router from 'koa-router'
// Local libraries.
import OfferRESTControllerLib from './controller.js'
let _this
class OfferRouter {
constructor (localConfig = {}) {
// Dependency Injection.
@@ -38,7 +36,8 @@ class OfferRouter {
const baseUrl = '/offer'
this.router = new Router({ prefix: baseUrl })
_this = this
// Bind 'this' object to all subfunctions.
this.attach = this.attach.bind(this)
}
attach (app) {
@@ -50,19 +49,20 @@ class OfferRouter {
// 12/3/24 CT:
// Note: The createOffer() path was used by a P2WDB webhook to generate an
// Offer from and Order. This has been deprecated and Offers are now created
// 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.
// this.router.post('/', _this.offerRESTController.createOffer) // Deprecated.
this.router.post('/take', _this.offerRESTController.takeOffer)
this.router.get('/list/all/:page', _this.offerRESTController.listOffers)
this.router.get('/list/nft/:page', _this.offerRESTController.listNftOffers)
this.router.get('/list/fungible/:page', _this.offerRESTController.listFungibleOffers)
this.router.post('/take', this.offerRESTController.takeOffer)
this.router.get('/list/all/:page', this.offerRESTController.listOffers)
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)
// Attach the Controller routes to the Koa app.
app.use(_this.router.routes())
app.use(_this.router.allowedMethods())
app.use(this.router.routes())
app.use(this.router.allowedMethods())
}
}
+106 -1
View File
@@ -29,7 +29,28 @@ class OrderRESTControllerLib {
_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) {
try {
// console.log('body: ', ctx.request.body)
@@ -49,6 +70,68 @@ class OrderRESTControllerLib {
_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
async listOrders (ctx) {
@@ -67,6 +150,28 @@ class OrderRESTControllerLib {
// Delete an existing order by returning the token to the root address of
// 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) {
try {
// console.log('body: ', ctx.request.body)
+6 -1
View File
@@ -54,7 +54,7 @@ class OrderRouter {
// Define the routes and attach the controller.
this.router.post('/', this.createOrder)
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.
app.use(_this.router.routes())
@@ -65,6 +65,11 @@ class OrderRouter {
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
+20 -1
View File
@@ -31,7 +31,25 @@ class P2WDBRESTControllerLib {
_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) {
try {
console.log('p2wdb REST API handler: body: ', ctx.request.body)
@@ -64,6 +82,7 @@ class P2WDBRESTControllerLib {
await _this.useCases.offer.flagOffer(flagData)
} else {
console.log('Could not route P2WDB webhook data.')
// NOTE: Should we throw an error here?
}
// 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
+2 -2
View File
@@ -75,12 +75,12 @@ class UserRouter {
}
async getAll (ctx, next) {
await _this.validators.ensureUser(ctx, next)
await _this.validators.ensureAdmin(ctx, next)
await _this.userRESTController.getUsers(ctx, next)
}
async getById (ctx, next) {
await _this.validators.ensureUser(ctx, next)
await _this.validators.ensureTargetUserOrAdmin(ctx, next)
await _this.userRESTController.getUser(ctx, next)
}
+80 -13
View File
@@ -22,6 +22,14 @@ class TimerControllers {
}
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
// Encapsulate dependencies
this.config = config
// Bind 'this' object to all subfunctions
@@ -30,15 +38,18 @@ class TimerControllers {
this.gcOffers = this.gcOffers.bind(this)
this.checkDupOffers = this.checkDupOffers.bind(this)
this.loadOffers = this.loadOffers.bind(this)
// Bind 'this' object to all subfunctions.
// this.exampleTimerFunc = this.exampleTimerFunc.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)
// State
this.gcOrdersInt = null
this.gcOffersInt = null
this.checkDupOffersInt = null
this.loadOffersInt = null
this.newSmAccountsInt = null
this.updateSmAccountsInt = null
}
// Start all the time-based controllers.
@@ -47,11 +58,10 @@ class TimerControllers {
this.gcOffersInt = setInterval(this.gcOffers, 60000 * 5)
// this.checkDupOffersInt = setInterval(this.checkDupOffers, 60000 * 4.5)
this.loadOffersInt = setInterval(this.loadOffers, 60000 * 2)
// Any new timer control functions can be added here. They will be started
// when the server starts.
// this.optimizeWalletHandle = setInterval(this.exampleTimerFunc, 60000 * 60)
this.cleanUsageHandle = setInterval(this.cleanUsage, 60000 * 60) // 1 hour
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)
return true
}
@@ -61,6 +71,9 @@ class TimerControllers {
// clearInterval(this.checkDupOffers)
clearInterval(this.optimizeWalletHandle)
clearInterval(this.cleanUsageHandle)
clearInterval(this.backupUsageHandle)
clearInterval(this.newSmAccountsInt)
clearInterval(this.updateSmAccountsInt)
}
// Garbage Collect the Orders.
@@ -75,6 +88,27 @@ class TimerControllers {
}
}
// 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
}
}
// Garbage Collect the Offers.
gcOffers () {
try {
@@ -111,22 +145,55 @@ class TimerControllers {
}
}
// Clean the usage state so that stats reflect the last 24 hours.
cleanUsage () {
// Backup the usage stats to the database
async backupUsage () {
try {
const now = new Date()
console.log(`cleanUsage() Timer Controller executing at ${now.toLocaleString()}`)
clearInterval(this.backupUsageHandle)
this.useCases.usage.cleanUsage()
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/cleanUsage(): ', 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
}
}
}
export default TimerControllers
+3
View File
@@ -1,6 +1,9 @@
/*
Entry Entity
TODO: Explain what an entry is and how it works.
*/
class Entry {
validate ({
entry,
+12 -2
View File
@@ -1,6 +1,6 @@
/*
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.
*/
@@ -33,7 +33,9 @@ class OfferEntity {
makerAddr,
ticker,
tokenType,
nostrEventId
nostrEventId,
operatorAddress,
operatorPercentage
} = offerData.data
// Input Validation
@@ -80,6 +82,14 @@ class OfferEntity {
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.")
}
// Convert the timestamp to a number.
let timestamp = new Date(offerData.timestamp)
timestamp = timestamp.getTime()
+7 -1
View File
@@ -1,3 +1,8 @@
/*
Entry use-case library.
TODO: Explain what an entry is and how it works.
*/
// local libraries
import wlogger from '../adapters/wlogger.js'
@@ -30,11 +35,12 @@ class EntryLib {
if (!isValidSignature) {
throw new Error('Invalid signature')
}
// Verify psf tokens balance
// const psfBalance = await this.bch.getPSFTokenBalance(
// entryEntity.slpAddress
// )
// NOTE: this.bch.getPSFTokenBalance , does not exist , should be created?
const psfBalance = 1
if (psfBalance < 10) {
+5
View File
@@ -10,6 +10,7 @@ import EntryUseCases from './entry.js'
import OfferUseCases from './offer/index.js'
import OrderUseCases from './order.js'
import { UsageUseCases } from './usage-use-cases.js'
import SmAccountUseCases from './smAccount-use-cases.js'
class UseCases {
constructor (localConfig = {}) {
@@ -27,10 +28,14 @@ class UseCases {
localConfig.order = this.order
this.offer = new OfferUseCases(localConfig)
this.usage = new UsageUseCases(localConfig)
this.smAccount = new SmAccountUseCases(localConfig)
}
// Run any startup Use Cases at the start of the app.
async start () {
// Load the usage stats from the database
await this.usage.loadUsage()
console.log('Async Use Cases have been started.')
return true
+160 -22
View File
@@ -64,6 +64,7 @@ class OfferUseCases {
this.removeStaleOffers = this.removeStaleOffers.bind(this)
this.flagOffer = this.flagOffer.bind(this)
this.loadOffers = this.loadOffers.bind(this)
this.listOffersByAddress = this.listOffersByAddress.bind(this)
// State
this.seenOffers = []
@@ -110,7 +111,10 @@ class OfferUseCases {
const utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
// console.log('utxoStatus: ', utxoStatus)
// 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'
offerObj.data.offerStatus = 'posted'
@@ -125,20 +129,57 @@ class OfferUseCases {
// Get data about the token.
const tokenId = offerEntity.tokenId
// const tokenData = await this.adapters.wallet.bchWallet.getTokenData(tokenId)
const tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTokenData, tokenId)
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
let tokenData = null
try {
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
// front end UI to figure out how to display the token.
const displayCategory = this.categorizeToken(offerEntity, tokenData)
console.log('displayCategory: ', displayCategory)
offerEntity.displayCategory = displayCategory
// 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
offerEntity.mutableDataCid = mutableDataCid
offerEntity.immutableDataCid = immutableDataCid
// Detect if user set the NSFW flag.
// const nsfw = false
// nsfw = await this.retryQueue.addToQueue(this.detectNsfw, tokenData)
// offerEntity.nsfw = nsfw
// 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 {
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.
const offerModel = new this.OfferModel(offerEntity)
@@ -146,7 +187,7 @@ class OfferUseCases {
return true
} catch (err) {
console.error('Error in createOffer()', err.message)
console.error('\n\nError in createOffer()', err.message, '\n\n', err)
throw err
}
}
@@ -263,7 +304,7 @@ class OfferUseCases {
}
}
async listNftOffers (page = 0, nsfw = false) {
/* async listNftOffers (page = 0, nsfw = false) {
try {
const data = await this.OfferModel.find({
displayCategory: { $ne: 'fungible' },
@@ -283,6 +324,42 @@ class OfferUseCases {
console.error('Error in use-cases/offer/listNftOffers()')
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) {
@@ -512,9 +589,8 @@ class OfferUseCases {
// }
}
// This function is called by the P2WDB webhook REST API handler. When a
// Counter Offer is passed to bch-dex by the P2WDB, the data is then passed
// to this function. It does due diligence on the Counter Offer, then signs
// This function is called by loadOffers().
// It does due diligence on the Counter Offer, then signs
// and broadcasts the transaction to accept the Counter Offer.
async acceptCounterOffer (offerData) {
try {
@@ -527,6 +603,8 @@ class OfferUseCases {
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
// the incoming Counter Offer.
@@ -537,7 +615,7 @@ class OfferUseCases {
orderData = await this.orderUseCase.findOrderByUtxo(offerData)
console.log(`orderData: ${JSON.stringify(orderData, null, 2)}`)
} catch (err) {
console.log('Order matching this Counter Offer is not managed by this instance of bch-dex. Skipping.')
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)
@@ -593,22 +671,70 @@ class OfferUseCases {
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 hasRequiredAmount = satsOut === satsToReceive
if (!hasRequiredAmount) {
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
// in the Offer.
const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0]
const makerAddr = orderData.makerAddr
const hasCorrectAddr = makerAddr === addrInCounterOffer
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.`)
}
// 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.
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)
return txid
@@ -706,7 +832,8 @@ class OfferUseCases {
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 eightWeeks = 1000 * 60 * 60 * 24 * 7 * 8
const eightWeeksAgo = nowMs - eightWeeks
@@ -783,7 +910,7 @@ class OfferUseCases {
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)}`)
// console.log(`Counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerObj.data.nostrEventId)}`)
await this.acceptCounterOffer(offerObj)
}
@@ -797,6 +924,17 @@ class OfferUseCases {
throw error
}
}
// List all offers being made by a given address.
async listOffersByAddress (addr) {
try {
const offers = await this.OfferModel.find({ makerAddr: addr })
return offers
} catch (err) {
console.error('Error in listOffersByAddress(): ', err)
throw err
}
}
}
export default OfferUseCases
+13 -9
View File
@@ -44,6 +44,7 @@ class OrderLib {
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')
@@ -81,13 +82,7 @@ class OrderLib {
// 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.
// const tokenData = await this.adapters.wallet.bchWallet.getTxData([entryObj.tokenId])
// const tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTxData, [entryObj.tokenId])
const tokenData = await userWallet.getTxData([entryObj.tokenId])
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
orderEntity.ticker = tokenData[0].tokenTicker
@@ -107,13 +102,19 @@ class OrderLib {
// await this.adapters.wallet.bchWallet.getUtxos()
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
await userWallet.initialize()
// Update the order with the new UTXO information.
orderEntity.utxoTxid = utxoInfo.txid
orderEntity.utxoVout = utxoInfo.vout
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.userId = user._id
// Add the operator address and percentage to the order.
orderEntity.operatorAddress = this.config.operatorAddress
orderEntity.operatorPercentage = this.config.operatorPercentage
// Post the new Order information to Nostr under the topic set in the
// config file.
@@ -232,7 +233,7 @@ class OrderLib {
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('findOrderByUtxo() order: ', order)
console.log('\nfindOrderByUtxo() order: ', order)
if (!order) {
throw new Error('order not found')
@@ -246,7 +247,10 @@ class OrderLib {
return orderObject
} catch (err) {
console.error('Error in findOrderByUtxo(): ', err)
if (!err.message.toString().includes('order not found')) {
console.error('Error in findOrderByUtxo(): ', err)
}
throw err
}
}
+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
+74
View File
@@ -3,6 +3,10 @@
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
@@ -19,11 +23,17 @@ class UsageUseCases {
)
}
// 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
}
@@ -108,6 +118,70 @@ class UsageUseCases {
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
-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
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 AdminLib from '../../../src/adapters/admin.js'
const adminLib = new AdminLib()
// const request = supertest.agent(app.listen())
const context = {}
@@ -26,10 +24,12 @@ const LOCALHOST = `http://localhost:${config.port}`
if (!config.noMongo) {
describe('Auth', () => {
before(async () => {
const app = new Server()
// const app = new 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.
// await app.controllers.adapters.ipfs.stop()
@@ -37,9 +37,6 @@ if (!config.noMongo) {
// Delete all previous users in the database.
await testUtils.deleteAllUsers()
// Create a new admin user.
await adminLib.createSystemUser()
const userObj = {
email: 'test@test.com',
password: 'pass',
+83 -133
View File
@@ -5,16 +5,16 @@ import axios from 'axios'
import sinon from 'sinon'
import util from 'util'
import UserController from '../../../src/controllers/rest-api/users/controller.js'
import Adapters from '../../../src/adapters/index.js'
import UseCases from '../../../src/use-cases/index.js'
// import UserController from '../../../src/controllers/rest-api/users/controller.js'
// import Adapters from '../../../src/adapters/index.js'
// import UseCases from '../../../src/use-cases/index.js'
util.inspect.defaultOptions = { depth: 1 }
const LOCALHOST = `http://localhost:${config.port}`
const context = {}
const adapters = new Adapters()
let uut
// const adapters = new Adapters()
// let uut
let sandbox
// const mockContext = require('../../unit/mocks/ctx-mock').context
@@ -50,8 +50,8 @@ if (!config.noMongo) {
})
beforeEach(() => {
const useCases = new UseCases({ adapters })
uut = new UserController({ adapters, useCases })
// const useCases = new UseCases({ adapters })
// uut = new UserController({ adapters, useCases })
sandbox = sinon.createSandbox()
})
@@ -175,59 +175,6 @@ if (!config.noMongo) {
assert.equal(result.data.user.type, 'user')
assert.property(result.data.user, 'mnemonic')
})
it('should reject signup when DISABLE_NEW_ACCOUNTS is true', async () => {
try {
process.env.DISABLE_NEW_ACCOUNTS = true
const options = {
method: 'POST',
url: `${LOCALHOST}/users`,
data: {
email: 'test2@test.com',
password: 'supersecretpassword',
name: 'test3'
}
}
await axios(options)
assert(false, 'Unexpected result')
} catch (err) {
assert(err.response.status === 401, 'Error code 401 expected.')
}
})
it('admin can create a user when DISABLE_NEW_ACCOUNTS is true', async () => {
process.env.DISABLE_NEW_ACCOUNTS = true
const options = {
method: 'post',
url: `${LOCALHOST}/users`,
headers: {
Authorization: `Bearer ${context.adminJWT}`
},
data: {
user: {
email: 'fromAdmin@test.com',
password: 'supersecretpassword',
name: 'test3'
}
}
}
const result = await axios(options)
context.user = result.data.user
context.token = result.data.token
assert(result.status === 200, 'Status Code 200 expected.')
assert(
result.data.user.email === 'fromAdmin@test.com',
'Email of test expected'
)
assert(
result.data.user.password === undefined,
'Password expected to be omited'
)
assert.property(result.data, 'token', 'Token property exists.')
assert.equal(result.data.user.type, 'user')
})
})
describe('GET /users', () => {
@@ -303,52 +250,55 @@ if (!config.noMongo) {
}
})
it('should fetch all users', async () => {
const { token } = context
// it('should fetch all users', async () => {
// const { token } = context
const options = {
method: 'GET',
url: `${LOCALHOST}/users`,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
const result = await axios(options)
// const options = {
// method: 'GET',
// url: `${LOCALHOST}/users`,
// headers: {
// Accept: 'application/json',
// Authorization: `Bearer ${token}`
// }
// }
// const result = await axios(options)
const users = result.data.users
// console.log(`users: ${util.inspect(users)}`)
// const users = result.data.users
// // console.log(`users: ${util.inspect(users)}`)
assert.hasAnyKeys(users[0], ['type', '_id', 'email', 'mnemonic'])
assert.isNumber(users.length)
})
// assert.hasAnyKeys(users[0], ['type', '_id', 'email', 'mnemonic'])
// assert.isNumber(users.length)
// })
it('should return a 422 http status if biz-logic throws an error', async () => {
try {
const { token } = context
// it('should return a 422 http status if biz-logic throws an error', async () => {
// try {
// const { token } = context
// Force an error
sandbox
.stub(uut.useCases.user, 'getAllUsers')
.rejects(new Error('test error'))
// // Force an error
// sandbox
// .stub(uut.useCases.user, 'getAllUsers')
// .rejects(new Error('test error'))
const options = {
method: 'GET',
url: `${LOCALHOST}/users`,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
await axios(options)
// const options = {
// method: 'GET',
// url: `${LOCALHOST}/users`,
// headers: {
// Accept: 'application/json',
// Authorization: `Bearer ${token}`
// }
// }
// await axios(options)
assert.fail('Unexpected code path!')
} catch (err) {
// console.log(err)
assert.equal(err.response.status, 422)
assert.equal(err.response.data, 'test error')
}
})
// assert.fail('Unexpected code path!')
// } catch (err) {
// // console.log(err)
// assert.equal(err.response.status, 422)
// assert.equal(err.response.data, 'test error')
// }
// })
// assert.hasAnyKeys(users[0], ['type', '_id', 'email'])
// assert.isNumber(users.length)
// })
})
describe('GET /users/:id', () => {
@@ -370,45 +320,45 @@ if (!config.noMongo) {
}
})
it("should throw 404 if user doesn't exist", async () => {
const { token } = context
// it("should throw 404 if user doesn't exist", async () => {
// const { token } = context
try {
const options = {
method: 'GET',
url: `${LOCALHOST}/users/5fa4bd7ee1828f5f4d8ed004`,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
await axios(options)
// try {
// const options = {
// method: 'GET',
// url: `${LOCALHOST}/users/5fa4bd7ee1828f5f4d8ed004`,
// headers: {
// Accept: 'application/json',
// Authorization: `Bearer ${token}`
// }
// }
// await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 404)
}
})
// assert.equal(true, false, 'Unexpected behavior')
// } catch (err) {
// assert.equal(err.response.status, 404)
// }
// })
it('should throw 422 for invalid input', async () => {
const { token } = context
// it('should throw 422 for invalid input', async () => {
// const { token } = context
try {
const options = {
method: 'GET',
url: `${LOCALHOST}/users/1`,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
await axios(options)
// try {
// const options = {
// method: 'GET',
// url: `${LOCALHOST}/users/1`,
// headers: {
// Accept: 'application/json',
// Authorization: `Bearer ${token}`
// }
// }
// await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 422)
}
})
// assert.equal(true, false, 'Unexpected behavior')
// } catch (err) {
// assert.equal(err.response.status, 422)
// }
// })
it('should fetch own user', async () => {
const _id = context.user._id
+17 -2
View File
@@ -29,8 +29,7 @@ describe('#adapters', () => {
uut.config.getJwtAtStartup = true
uut.config.useIpfs = true
uut.config.env = 'not-a-test'
sandbox.stub(uut.fullStackJwt, 'getJWT').resolves()
sandbox.stub(uut.fullStackJwt, 'instanceBchjs').resolves()
sandbox.stub(uut.wallet, 'instanceWallet').resolves()
sandbox.stub(uut.ipfs, 'start').resolves()
const result = await uut.start()
@@ -38,6 +37,22 @@ describe('#adapters', () => {
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 () => {
try {
// Force an error
@@ -19,26 +19,13 @@ describe('Admin', () => {
if (!config.noMongo) {
describe('loginAdmin()', () => {
it('should logind admin', async () => {
it('should login admin', async () => {
try {
const error = new Error('test error')
error.response = {
status: 422
}
// sandbox.stub(uut.axios, 'request').onFirstCall().throws(error)
sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'pass' })
sandbox.stub(uut.axios, 'request').resolves(true)
const result = await uut.loginAdmin()
const user = result.data.user
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')
assert.isTrue(result)
} catch (err) {
assert(false, 'Unexpected result')
}
@@ -48,13 +35,13 @@ describe('Admin', () => {
try {
// Returns an erroneous password to force
// an auth error
sandbox.stub(uut.axios, 'request').throws(new Error('test error'))
sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'wrong' })
await uut.loginAdmin()
assert(false, 'Unexpected result')
} catch (err) {
assert.equal(err.response.status, 401)
assert.include(err.response.data, 'Unauthorized')
assert.include(err.message, 'test error')
}
})
})
@@ -84,6 +71,7 @@ describe('Admin', () => {
}
sandbox.stub(uut.User, 'findOne').resolves(fakeUser)
sandbox.stub(uut.jsonFiles, 'writeJSON').resolves(true)
const result = await uut.createSystemUser()
assert.property(result, 'email')
+14 -13
View File
@@ -135,18 +135,19 @@ describe('bch', () => {
}
})
// it('should return the merit ', async () => {
// try {
// // Mock live network calls.
// sandbox.stub(uut.msgLib.merit, 'agMerit').resolves(100)
//
// const slpAddr =
// 'simpleledger:qqgnksc6zr4nzxrye69fq625wu2myxey6uh9kzjy96'
// const merit = await uut.getMerit(slpAddr)
// assert.isNumber(merit)
// } catch (err) {
// assert.fail('Unexpected result')
// }
// })
/* it('should return the merit ', async () => {
try {
// Mock live network calls.
sandbox.stub(uut.msgLib.merit, 'agMerit').resolves(100)
const slpAddr =
'simpleledger:qqgnksc6zr4nzxrye69fq625wu2myxey6uh9kzjy96'
const merit = await uut.getMerit(slpAddr)
assert.isNumber(merit)
} catch (err) {
console.log('err: ', err)
assert.fail('Unexpected result')
}
}) */
})
})
+96
View File
@@ -128,4 +128,100 @@ describe('#nostr.js', () => {
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)
})
})
})
+2 -1
View File
@@ -55,7 +55,8 @@ describe('#OrderPagination', () => {
rateInBaseUnit: 'rateInBaseUnit',
minUnitsToExchange: 'minUnitsToExchange',
ticker: 'ticker',
tokenType: 1
tokenType: 1,
userId: 'userId'
})
await order.save()
})
@@ -41,5 +41,17 @@ describe('#passport.js', () => {
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)
})
})
})
+7
View File
@@ -55,6 +55,13 @@ describe('#User-Adapter', () => {
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', () => {
+402 -13
View File
@@ -9,24 +9,56 @@ 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 { } }
fromHex: () => { return {} },
SIGHASH_ALL: () => { return {} }
},
TransactionBuilder: {
fromTransaction: () => { return { sign: () => { return { } }, build: () => { return { toHex: () => { return 'hex' } } } } }
fromTransaction: () => { return { sign: () => { return {} }, build: () => { return { toHex: () => { return 'hex' } } } } }
},
ECPair: {
fromWIF: () => { return { } }
fromWIF: () => { return {} }
}
}
sandbox = sinon.createSandbox()
@@ -34,6 +66,278 @@ describe('#wallet', () => {
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({
@@ -43,6 +347,7 @@ describe('#wallet', () => {
})
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 = [
{
@@ -99,7 +404,55 @@ describe('#wallet', () => {
}
})
})
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',
@@ -113,25 +466,61 @@ describe('#wallet', () => {
const hex = 'hex'
const hdIndex = 11
const txid = await uut.completeTx(hex, hdIndex)
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')
})
it('should throw an error if the hex is not provided', async () => {
})
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 hdIndex = 11
await uut.completeTx(null, hdIndex)
const orderData = {}
await uut.reclaimTokens(orderData)
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'hex must be a string!')
assert.equal(error.message, 'orderData.hdIndex is required!')
}
})
it('should throw an error if the hdIndex is not provided', async () => {
})
describe('#cid2json', () => {
it('should throw error if cid is not provided', async () => {
try {
const hex = 'hex'
await uut.completeTx(hex, null)
await uut.cid2json()
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'hdIndex must be a non-negative number!')
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)
})
})
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)
})
})
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)
})
it('should catch and throw an error', async () => {
it('should catch and throw passport error', async () => {
try {
// Force an 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)
} catch (err) {
// console.log('err: ', err)
@@ -91,25 +91,143 @@ describe('#Offer-REST-Router', () => {
// assert.equal(ctx.body.hash, 'testHash')
})
// it('should catch and throw an error', async () => {
// try {
// ctx.request.body = {
// offer: {}
// }
//
// // Force an error
// sandbox
// .stub(uut.useCases.offer, 'createOffer')
// .rejects(new Error('test error'))
//
// await uut.createOffer(ctx)
//
// assert.fail('Unexpected code path')
// } catch (err) {
// // console.log('err: ', err)
// assert.include(err.message, 'test error')
// }
// })
it('should catch and throw an error', async () => {
try {
ctx.request.body = {
offer: {}
}
// Force an error
sandbox
.stub(uut.useCases.offer, 'createOffer')
.rejects(new Error('test error'))
await uut.createOffer(ctx)
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
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('#handleError', () => {
@@ -101,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', () => {
it('should still throw error if there is no message', () => {
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 ctx
before(async () => {})
before(async () => { })
beforeEach(() => {
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.'
)
}
})
})
})
@@ -153,6 +153,20 @@ describe('#Users-REST-Controller', () => {
// Assert that expected properties exist in the returned data.
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 () => {
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)
})
})
})
@@ -137,4 +137,54 @@ describe('#Timer-Controllers', () => {
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)
})
})
})
+228 -1
View File
@@ -237,6 +237,113 @@ describe('#Offer-Entity', () => {
}
})
it('should throw an error if makerAddr is not included', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorPercentage: 10
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'makerAddr' must be a string.")
}
})
it('should throw an error if makerAddr is not a string', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 1234567890
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'makerAddr' must be a string.")
}
})
it('should throw an error if tokenType is not included', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorPercentage: 10
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'tokenType' must be a number.")
}
})
it('should throw an error if tokenType is not a number', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
tokenType: 'notANumber'
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'tokenType' must be a number.")
}
})
it('should throw an error if nostrEventId is not included', () => {
try {
const offerData = {
@@ -267,6 +374,124 @@ describe('#Offer-Entity', () => {
}
})
it('should throw an error if operatorAddress is not included', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
tokenType: 1,
nostrEventId: 'test',
operatorPercentage: 10
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'operatorAddress' must be a string.")
}
})
it('should throw an error if operatorAddress is not a string', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
tokenType: 1,
nostrEventId: 'test',
operatorAddress: 1234567890,
operatorPercentage: 10
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'operatorAddress' must be a string.")
}
})
it('should throw an error if operatorPercentage is not included', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
tokenType: 1,
nostrEventId: 'test',
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00'
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'operatorPercentage' must be a number.")
}
})
it('should throw an error if operatorPercentage is not a number', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
tokenType: 1,
nostrEventId: 'test',
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorPercentage: 'notANumber'
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'operatorPercentage' must be a number.")
}
})
it('should validate a new offer', () => {
const offerObj = {
appId: 'swapTest555',
@@ -286,7 +511,9 @@ describe('#Offer-Entity', () => {
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
ticker: 'TROUT',
tokenType: 1,
nostrEventId: 'test'
nostrEventId: 'test',
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorPercentage: 10
},
timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM',
+66
View File
@@ -130,5 +130,71 @@ describe('#Order-Entity', () => {
assert.include(err.message, "Property 'numTokens' must be a number.")
}
})
it('should throw an error if numTokens is not a number', () => {
try {
const data = {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 'notANumber'
}
uut.inputValidate(data)
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'numTokens' must be a number.")
}
})
it('should throw an error if makerAddress is not included', () => {
try {
const data = {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1000
}
uut.inputValidate(data)
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'makerAddr' must be a string.")
}
})
it('should throw an error if makerAddress is not a string', () => {
try {
const data = {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1000,
makerAddr: 1234567890
}
uut.inputValidate(data)
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'makerAddr' must be a string.")
}
})
it('should return the correct data', () => {
const data = {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1000,
makerAddr: 'fakeAddr'
}
const result = uut.inputValidate(data)
assert.deepEqual(result, data)
})
})
})
+15
View File
@@ -34,6 +34,12 @@ describe('#passport', () => {
passportCallback(id, 'password', done)
})
it('should handle not found user', () => {
// Mock Users model.
sandbox.stub(User, 'findOne').resolves(null)
passportCallback(id, 'password', done)
})
it('should return if password is validated', () => {
// Mock Users model.
@@ -41,6 +47,15 @@ describe('#passport', () => {
passportCallback(id, 'password', done)
})
it('should handle error on password validation', () => {
// Mock Users model.
const userMock = {
validatePassword: () => false
}
sandbox.stub(User, 'findOne').resolves(userMock)
passportCallback(id, 'password', done)
})
it('should catch a high-level error', () => {
// Force an error
+1 -1
View File
@@ -31,7 +31,7 @@ describe('#server', () => {
sandbox.stub(uut.controllers, 'attachControllers').resolves()
sandbox.stub(uut, 'sleep').resolves()
uut.config.env = 'dev'
uut.config.port = 5040
const result = await uut.startServer()
// console.log('result: ', result)
+48 -1
View File
@@ -74,6 +74,39 @@ const localdb = {
}
},
Usage: class Usage {
static findById () {}
static find () {}
static findOne () {
return {
validatePassword: localdb.validatePassword
}
}
async save () {
return {}
}
generateToken () {
return '123'
}
toJSON () {
return {}
}
async remove () {
return true
}
async validatePassword () {
return true
}
static async deleteMany(){
return true
}
},
validatePassword: () => {
return true
},
@@ -116,6 +149,18 @@ const localdb = {
static findById () {}
static find () {}
static findOne () {}
static countDocuments(){}
async save () {
return {}
}
},
SmAccount: class SmAccount {
constructor (obj) {}
static findById () {}
static find () {}
static findOne () {}
static updateOne () {}
async save () {
return {}
@@ -169,7 +214,9 @@ const p2wdb = {
const nostr = {
post: async () => {return true },
read: async () => { return true },
eventId2note: () => { return 'testNoteId' }
eventId2note: () => { return 'testNoteId' },
readGlobalFeed: async () => { return [] },
getFollowers: async () => { return [] }
}
export default { ipfs, localdb, bch, wallet, p2wdb, bchjs, nostr}
+23 -1
View File
@@ -12,8 +12,18 @@ const mockWallet = {
nextAddress: 1
};
class MockBchWallet {
class AdapterRoute {
constructor() {
this.sendTx = async () => {
return 'fakeTxid';
}
}
}
class MockBchWallet {
constructor(mnemonic, advancedOptions) {
this.advancedOptions = advancedOptions
this.walletInfoPromise = true;
this.walletInfo = mockWallet;
this.initialize = async () => {}
@@ -24,6 +34,9 @@ class MockBchWallet {
this.sendTokens = async () => {
return 'fakeTxid';
};
this.send = async () => {
return 'fakeTxid';
};
this.utxoIsValid =async ()=>{}
this.getUtxos = async () => { };
this.getBalance = async () => { };
@@ -33,7 +46,16 @@ class MockBchWallet {
tokenTicker: 'TROUT'
}];
};
this.getKeyPair = async () => {
return {
cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
wif: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ',
hdIndex: 11
}
};
this.optimize = async () => { };
this.cid2json = async () => {};
this.ar = new AdapterRoute()
// Environment variable is used by wallet-balance.unit.js to force an error.
if (process.env.NO_UTXO) {
this.utxos = {};
+27
View File
@@ -0,0 +1,27 @@
class BchTokenSweepMock {
constructor(wif, privateKey, bchWallet, fee, cashAddress) {
this.wif = wif
this.privateKey = privateKey
this.bchWallet = bchWallet
this.fee = fee
this.cashAddress = cashAddress
}
populateObjectFromNetwork() {
return {
success: true,
txid: 'txid'
}
}
sweepTo() {
return {
success: true,
txid: 'txid'
}
}
}
export { BchTokenSweepMock }
export default {
BchTokenSweepMock
}
+42
View File
@@ -0,0 +1,42 @@
const messagesMock = [
{
content: JSON.stringify("I'm testing so hard!!!"),
id: '1',
tags: [['u', 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477']],
npub: 'npub1234567890',
pubkey: '1234567890',
}
]
const smAccountsMock = [
{
npub: 'npub1234567890',
bchAddr: 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477',
pubkey: '1234567890',
pfp: '',
createdAt: new Date(),
updatedAt: new Date(),
followerCnt: 0,
followingCnt: 0,
posts: 0,
likes: 0,
comments: 0,
reposts: 0
},
{
npub: 'npub1234567891',
bchAddr: 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478',
pubkey: '1234567891',
pfp: '',
createdAt: new Date(),
updatedAt: new Date(),
followerCnt: 0,
followingCnt: 0,
posts: 0,
likes: 0,
comments: 0,
reposts: 0
}
]
export default { messagesMock, smAccountsMock }
+54
View File
@@ -50,6 +50,27 @@ class Offer {
async loadOffers(){
}
async listOffers() {
return []
}
async listNftOffers() {
return []
}
async listFungibleOffers() {
return []
}
async takeOffer() {
return []
}
async listOffersByAddress() {
return []
}
async flagOffer() {
return {}
}
async acceptCounterOffer() {
return {}
}
}
class Order {
@@ -59,6 +80,12 @@ class Order {
async removeStaleOrders(){
return {}
}
async listOrders() {
return []
}
async deleteOrder() {
return 'testTxid'
}
}
class UsageUseCaseMock {
@@ -77,6 +104,32 @@ class UsageUseCaseMock {
async getTopEndpoints(existingUser, newData) {
return true
}
async clearUsage() {
return true
}
async saveUsage() {
return true
}
}
class SmAccountUseCaseMock {
async listAccounts() {
return []
}
async getAccountByNpub() {
return {}
}
async getAccountByBchAddr() {
return {}
}
async checkForNewSmAccounts() {
return true
}
async updateSmAccounts() {
return true
}
}
class UseCasesMock {
@@ -89,6 +142,7 @@ class UseCasesMock {
offer = new Offer()
order = new Order()
usage = new UsageUseCaseMock()
smAccount = new SmAccountUseCaseMock()
}
export default UseCasesMock;
+53 -2
View File
@@ -115,6 +115,7 @@ const fungibleTokenData01 = {
"documentUri": "troutsblog.com",
"documentHash": "",
"decimals": 2,
"tokenDecimals": 2,
"mintBatonIsActive": true,
"tokensInCirculationBN": "100097954686",
"tokensInCirculationStr": "100097954686",
@@ -125,6 +126,20 @@ const fungibleTokenData01 = {
"immutableData": "troutsblog.com",
"mutableData": ""
}
const mutableDataMock = {
schema: 'ps007-v1.0.3',
schemaDocs: 'https://github.com/Permissionless-Software-Foundation/specifications/blob/master/ps007-token-data-schema.md',
tokenIcon: 'https://files.tokentiger.com/ipfs/view/bafkreidr6wfd6mcmwpea7abm5uk5rrprc2wfbcvo5wdcmtlolrpjab5oqm',
fullSizedUrl: '',
nsfw: false,
userData: { currentUrl: 'https://tokentiger.com' },
jsonLd: {},
about: 'This is AI generated art.',
category: '',
tags: [],
mediaType: 'image',
currentOwner: {},
}
const offerMockData = {
data: {
@@ -141,10 +156,38 @@ const offerMockData = {
utxoVout: 0,
makerAddr: 'address',
tokenType: 1,
nostrEventId: 'test'
nostrEventId: 'test',
operatorAddress: 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478',
operatorPercentage: 10
}
}
const deserealizeTxMockNoOperatorOut = {
//...
vout: [
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
//...
]
}
const deserealizeTxMock = {
//...
vout: [
@@ -166,6 +209,12 @@ const deserealizeTxMock = {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
//...
]
}
@@ -178,5 +227,7 @@ export default {
fungibleOffer01,
fungibleTokenData01,
offerMockData,
deserealizeTxMock
deserealizeTxMockNoOperatorOut,
deserealizeTxMock,
mutableDataMock
};
+238 -14
View File
@@ -5,6 +5,7 @@
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
import RetryQueue from '@chris.troutner/retry-queue'
// Local support libraries
// const testUtils = require('../../utils/test-utils')
@@ -30,6 +31,7 @@ describe('#offer-use-case', () => {
const order = new OrderUseCase({ adapters })
uut = new OfferLib({ adapters, order })
uut.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 1 })
})
afterEach(() => sandbox.restore())
@@ -85,6 +87,15 @@ describe('#offer-use-case', () => {
assert.isFalse(result)
})
it('should return false if offer already processed', async () => {
const offerObj = mockData.offerMockData
uut.seenOffers.push(offerObj.data.nostrEventId)
// Mock dependencies
const result = await uut.createOffer(offerObj)
assert.isFalse(result)
})
it('should return false for invalid utxo', async () => {
const offerObj = mockData.offerMockData
@@ -97,6 +108,73 @@ describe('#offer-use-case', () => {
assert.isFalse(result)
})
it('should create offer with mutable data', async () => {
const tokenDataMock = mockData.nftTokenData01
const offerObj = mockData.offerMockData
const mutableDataMock = mockData.mutableDataMock
// Mock dependencies
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
sandbox.stub(uut, 'findOfferByTxid').throws(new Error('offer not found'))
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves({}) // Utxo Status call
.onCall(1).resolves(tokenDataMock) // Token Data call
.onCall(2).resolves(mutableDataMock)
const result = await uut.createOffer(offerObj)
assert.isTrue(result)
})
it('should skip userData stringify error', async () => {
const tokenDataMock = mockData.nftTokenData01
const offerObj = mockData.offerMockData
const mutableDataMock = mockData.mutableDataMock
mutableDataMock.userData = { n: 10n }
// Mock dependencies
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
sandbox.stub(uut, 'findOfferByTxid').throws(new Error('offer not found'))
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves({}) // Utxo Status call
.onCall(1).resolves(tokenDataMock) // Token Data call
.onCall(2).resolves(mutableDataMock)
const result = await uut.createOffer(offerObj)
assert.isTrue(result)
})
it('should handle error getting token data', async () => {
// const tokenDataMock = mockData.nftTokenData01
const offerObj = mockData.offerMockData
const mutableDataMock = mockData.mutableDataMock
mutableDataMock.userData = { n: 10n }
// Mock dependencies
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
sandbox.stub(uut, 'findOfferByTxid').throws(new Error('offer not found'))
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves({}) // Utxo Status call
.onCall(1).throws(new Error('test error'))
const result = await uut.createOffer(offerObj)
assert.isTrue(result)
})
it('should handle error getting mutable data', async () => {
const tokenDataMock = mockData.nftTokenData01
const offerObj = mockData.offerMockData
const mutableDataMock = mockData.mutableDataMock
mutableDataMock.userData = { n: 10n }
// Mock dependencies
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
sandbox.stub(uut, 'findOfferByTxid').throws(new Error('offer not found'))
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves({}) // Utxo Status call
.onCall(1).resolves(tokenDataMock) // Token Data call
.onCall(2).throws(new Error('test error')) // cid2json call
const result = await uut.createOffer(offerObj)
assert.isTrue(result)
})
it('should create offer', async () => {
const tokenDataMock = mockData.simpleNftTokenData01
const offerObj = mockData.offerMockData
@@ -334,6 +412,14 @@ describe('#offer-use-case', () => {
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
assert.isFalse(result)
})
it('should fetch alternative url', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.axios, 'get').onCall(0).throws(new Error())
.onCall(1).resolves({ data: { nsfw: true } })
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
assert.isTrue(result)
})
})
describe('#listOffers', () => {
@@ -381,7 +467,7 @@ describe('#offer-use-case', () => {
}
})
it('should list orders', async () => {
it('should list offers', async () => {
const queryMock = {
sort () {
return this
@@ -395,7 +481,11 @@ describe('#offer-use-case', () => {
sandbox.stub(uut.OfferModel, 'find').returns(queryMock)
const result = await uut.listNftOffers(1, true)
assert.isArray(result)
assert.isObject(result)
assert.property(result, 'data')
assert.property(result, 'pagination')
assert.isArray(result.data)
assert.isObject(result.pagination)
})
})
@@ -597,7 +687,7 @@ describe('#offer-use-case', () => {
})
})
describe('#findOrderByEvent', () => {
describe('#findOfferByEvent', () => {
it('should throw an error if hash is not provided', async () => {
try {
await uut.findOfferByEvent()
@@ -629,6 +719,29 @@ describe('#offer-use-case', () => {
})
})
describe('#listOffersByAddress', () => {
it('should throw an error if order is not found!', async () => {
try {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error'))
await uut.listOffersByAddress('eventId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should return offer by eventId', async () => {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').resolves([])
const result = await uut.listOffersByAddress()
assert.isArray(result)
})
})
describe('#flagOffer', () => {
it('should throw an error if input is not provided', async () => {
try {
@@ -687,23 +800,74 @@ describe('#offer-use-case', () => {
it('should skip internal function errors ', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.nostr, 'read').resolves([mockData.offerMockData])
const nostrReadMock = [{ content: JSON.stringify(mockData.offerMockData) }]
sandbox.stub(uut.adapters.nostr, 'read').resolves(nostrReadMock)
await uut.loadOffers()
})
it('should review and load offers', async () => {
it('should createOffer ', async () => {
mockData.offerMockData.data.dataType = 'offer'
// Mock dependencies
sandbox.stub(uut.adapters.nostr, 'read').resolves([JSON.stringify(mockData.offerMockData)])
const nostrReadMock = [{ content: JSON.stringify(mockData.offerMockData) }]
sandbox.stub(uut.adapters.nostr, 'read').resolves(nostrReadMock)
const spy = sandbox.stub(uut, 'createOffer').resolves(true)
await uut.loadOffers()
assert.isTrue(spy.calledOnce)
})
it('should accept counter Offer ', async () => {
mockData.offerMockData.data.dataType = 'counter-offer'
// Mock dependencies
const nostrReadMock = [{ content: JSON.stringify(mockData.offerMockData) }]
sandbox.stub(uut.adapters.nostr, 'read').resolves(nostrReadMock)
const spy = sandbox.stub(uut, 'acceptCounterOffer').resolves(true)
await uut.loadOffers()
assert.isTrue(spy.calledOnce)
})
it('should skip error inside loop', async () => {
// Mock dependencies
const nostrReadMock = [{ content: '' }]
sandbox.stub(uut.adapters.nostr, 'read').resolves(nostrReadMock)
await uut.loadOffers()
})
})
describe('#acceptCounterOffer', () => {
it('should return false if order already processed', async () => {
const offerObj = mockData.offerMockData
uut.seenOffers.push(offerObj.data.nostrEventId)
// Mock dependencies
const result = await uut.acceptCounterOffer(offerObj)
assert.isFalse(result)
})
it('should return if order is not found!', async () => {
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').throws(new Error('test error'))
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').throws(new Error('test error'))
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.equal(result, 'N/A')
})
it('should return if utxo can not be validated', async () => {
// Mock dependencies
const mock = Object.assign({}, mockData.offerMockData.data)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').throws(new Error('test error'))
sandbox.stub(uut.retryQueue, 'addToQueue').throws(new Error('test error'))
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.equal(result, 'N/A')
})
it('should return if utxo is invalid', async () => {
// Mock dependencies
const mock = Object.assign({}, mockData.offerMockData.data)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.equal(result, 'N/A')
@@ -719,7 +883,7 @@ describe('#offer-use-case', () => {
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
const result = await uut.acceptCounterOffer({ data: {} })
console.log('result: ', result)
assert.fail('unexpected code path')
} catch (err) {
@@ -734,7 +898,6 @@ describe('#offer-use-case', () => {
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mockData.offerMockData.data)
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
@@ -746,6 +909,44 @@ describe('#offer-use-case', () => {
}
})
it('should skip transactions that do not have an output for the operator', async () => {
// Mock data
const mock = Object.assign({}, mockData.offerMockData.data)
mock.makerAddr = 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477' // Unknow Adress
mock.rateInBaseUnit = 0
mock.numTokens = 0
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMockNoOperatorOut)
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.equal(result, 'N/A')
})
it('should skip transactions if operator address does not match', async () => {
try {
// Mock data
const mock = Object.assign({}, mockData.offerMockData.data)
mock.makerAddr = mockData.deserealizeTxMockNoOperatorOut.vout[2].scriptPubKey.addresses[0]
mock.operatorAddress = 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477'
mock.rateInBaseUnit = 0
mock.numTokens = 0
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
await uut.acceptCounterOffer({ data: {} })
assert.fail('unexpected code path')
} catch (error) {
assert.include(error.message, 'The Counter Offer has an output address of')
}
})
it('should handle error for wrong transaction output address', async () => {
try {
// Mock data
@@ -761,7 +962,7 @@ describe('#offer-use-case', () => {
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
await uut.acceptCounterOffer({ data: { /** .... */ } })
await uut.acceptCounterOffer({ data: {} })
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'The Counter Offer has an output address of')
@@ -769,21 +970,44 @@ describe('#offer-use-case', () => {
}
})
it('should return tx id', async () => {
it('should handle error if operator sats to receive cant be calculated', async () => {
// Mock data
const mock = Object.assign({}, mockData.offerMockData.data)
mock.makerAddr = mockData.deserealizeTxMock.vout[2].scriptPubKey.addresses[0] // Maker Address
mock.makerAddr = mockData.deserealizeTxMock.vout[2].scriptPubKey.addresses[0]
mock.operatorAddress = mockData.deserealizeTxMock.vout[3].scriptPubKey.addresses[0]
mock.rateInBaseUnit = 0
mock.numTokens = 0
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
const result = await uut.acceptCounterOffer({ data: {} })
assert.equal(result, 'N/A')
})
it('should return tx id', async () => {
// Mock data
const mock = Object.assign({}, mockData.offerMockData.data)
mock.makerAddr = mockData.deserealizeTxMock.vout[2].scriptPubKey.addresses[0]
mock.operatorAddress = mockData.deserealizeTxMock.vout[3].scriptPubKey.addresses[0]
mock.rateInBaseUnit = 0
mock.numTokens = 0
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').onCall(0).returns(0).onCall(1).returns(1000)
sandbox.stub(uut.adapters.localdb.Users, 'findById').resolves({ mnemonic: 'test' })
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
//
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
const result = await uut.acceptCounterOffer({ data: {} })
assert.isString(result)
assert.notEqual('N/A')
})
})
})
@@ -158,6 +158,7 @@ describe('#order-use-case', () => {
describe('#createOrder', () => {
it('should create an order and return the hash', async () => {
uut.config.useFullStackCash = true
const entryObj = {
lokadId: 'SWP',
messageType: 1,
@@ -185,6 +186,7 @@ describe('#order-use-case', () => {
assert.property(result, 'eventId')
assert.property(result, 'noteId')
})
it('should create an order with consumer-api', async () => {
uut.config.useFullStackCash = false
const entryObj = {
@@ -214,6 +216,7 @@ describe('#order-use-case', () => {
assert.property(result, 'eventId')
assert.property(result, 'noteId')
})
it('should throw error if user is not found', async () => {
try {
const entryObj = {
@@ -384,4 +387,55 @@ describe('#order-use-case', () => {
assert.isString(result)
})
})
describe('#findOrderByUtxo', () => {
it('should throw an error if data is not provided', async () => {
try {
await uut.findOrderByUtxo({})
} catch (err) {
assert.include(err.message, 'Cannot read properties of undefined')
}
})
it('should throw an error if order is not found', async () => {
try {
sandbox.stub(uut.OrderModel, 'findOne').resolves(null)
const offerData = {
data: {
utxoTxid: 'utxoTxid',
utxoVout: 'utxoVout'
}
}
await uut.findOrderByUtxo(offerData)
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'order not found')
}
})
it('should throw an error if order event id is not found', async () => {
try {
sandbox.stub(uut.OrderModel, 'findOne').resolves({ nostrEventId: null })
const offerData = {
data: {
utxoTxid: 'utxoTxid',
utxoVout: 'utxoVout'
}
}
await uut.findOrderByUtxo(offerData)
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'order not found')
}
})
it('should return order by utxo', async () => {
sandbox.stub(uut.OrderModel, 'findOne').resolves({ toObject: () => { return {} }, nostrEventId: 'nostrEventId' })
const offerData = {
data: {
utxoTxid: 'utxoTxid',
utxoVout: 'utxoVout'
}
}
const result = await uut.findOrderByUtxo(offerData)
assert.isObject(result)
})
})
})
+166
View File
@@ -0,0 +1,166 @@
/*
Unit tests for the SM Account Use Case library.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Unit under test (uut)
import SMAccountLib from '../../../src/use-cases/smAccount-use-cases.js'
import SMMock from '../mocks/social-media-mocks.js'
// Local support libraries
import adapters from '../mocks/adapters/index.js'
describe('#sm-account-use-case', () => {
let uut
let sandbox
before(async () => {
// Delete all previous users in the database.
// await testUtils.deleteAllUsers()
})
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new SMAccountLib({ adapters })
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new SMAccountLib()
assert.fail('Unexpected code path')
console.log(uut) // linter
} catch (err) {
assert.include(
err.message,
'Instance of adapters must be passed in when instantiating SM Account Use Cases library.'
)
}
})
})
describe('#checkForNewSmAccounts', () => {
it('check for new SM accounts', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.nostr, 'readGlobalFeed').resolves(SMMock.messagesMock)
const result = await uut.checkForNewSmAccounts()
assert.isTrue(result)
})
it('should skip existing SM accounts', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'findOne').resolves({ npub: SMMock.messagesMock[0].npub })
sandbox.stub(uut.adapters.nostr, 'readGlobalFeed').resolves(SMMock.messagesMock)
const result = await uut.checkForNewSmAccounts()
assert.isTrue(result)
})
it('should throw an error if the readGlobalFeed fails', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.nostr, 'readGlobalFeed').rejects(new Error('Failed to read global feed'))
try {
await uut.checkForNewSmAccounts()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Failed to read global feed')
}
})
})
describe('#listAccounts', () => {
it('should return a list of SM accounts', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'find').callsFake(() => {
return {
skip: () => {
return {
limit: () => {
return SMMock.messagesMock
}
}
}
}
})
const result = await uut.listAccounts()
assert.isArray(result)
})
it('should throw an error if the find fails', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'find').throws(new Error('Failed to find SM accounts'))
try {
await uut.listAccounts()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Failed to find SM accounts')
}
})
})
describe('#getAccountByNpub', () => {
it('should return a SM account by npub', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'findOne').resolves(SMMock.messagesMock[0])
const result = await uut.getAccountByNpub(SMMock.messagesMock[0].npub)
assert.isObject(result)
})
it('should catch error', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'findOne').throws(new Error('Failed to find SM account by npub'))
try {
await uut.getAccountByNpub(SMMock.messagesMock[0].npub)
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Failed to find SM account by npub')
}
})
})
describe('#getAccountByBchAddr', () => {
it('should return a SM account by bch addr', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'findOne').resolves(SMMock.messagesMock[0])
const result = await uut.getAccountByBchAddr('bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477')
assert.isObject(result)
})
it('should catch error', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'findOne').throws(new Error('Failed to find SM account by bch addr'))
try {
await uut.getAccountByBchAddr('bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Failed to find SM account by bch addr')
}
})
})
describe('#updateSmAccounts', () => {
it('should update the SM accounts', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'find').resolves(SMMock.smAccountsMock)
const result = await uut.updateSmAccounts()
assert.isTrue(result)
})
it('should catch error', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'find').throws(new Error('Failed to find SM accounts'))
try {
await uut.updateSmAccounts()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Failed to find SM accounts')
}
})
})
})
@@ -255,4 +255,67 @@ describe('#usage-use-case', () => {
}
})
})
describe('#clearUsage', () => {
it('should clear the usage database data', async () => {
const res = await uut.clearUsage()
assert.isTrue(res)
})
it('should handle error', async () => {
try {
sandbox.stub(uut.UsageModel, 'deleteMany').throws(new Error('uut error'))
await uut.clearUsage()
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'uut error')
}
})
})
describe('#saveUsage', () => {
it('should save usage', async () => {
// Set mock data
restCalls.push({
timestamp: new Date().getTime(),
ip: 'localhost',
url: 'fakeUrl',
method: 'unit test'
})
const res = await uut.saveUsage()
assert.isTrue(res)
})
it('should handle error', async () => {
try {
restCalls.push(null)
await uut.saveUsage()
assert.fail('Unexpected code path')
} catch (error) {
console.log(error)
assert.include(error.message, 'Cannot read properties')
}
})
})
describe('#loadUsage', () => {
it('should load usage', async () => {
// Set mock data
const mockObj = {
timestamp: new Date().getTime(),
ip: 'localhost',
url: 'fakeUrl',
method: 'unit test'
}
sandbox.stub(uut.UsageModel, 'find').returns(new Array(10).fill(null).map((_, i) => (mockObj)))
const res = await uut.loadUsage()
assert.equal(restCalls.length, 10)
assert.equal(res.length, 10)
})
it('should skip error', async () => {
sandbox.stub(uut.UsageModel, 'find').throws(new Error('uut error'))
const res = await uut.loadUsage()
assert.isFalse(res)
})
})
})
+45 -20
View File
@@ -8,7 +8,6 @@ import axios from 'axios'
// Local libraries
import config from '../../config/index.js'
import User from '../../src/adapters/localdb/models/users.js'
import JsonFiles from '../../src/adapters/json-files.js'
// Hack to get __dirname back.
@@ -33,24 +32,6 @@ async function cleanDb () {
}
}
// Delete all users in the database. This ensures there is no previous state
// to confuse tests.
async function deleteAllUsers () {
try {
// Get all the users in the DB.
const users = await User.find({}, '-password')
// console.log(`users: ${JSON.stringify(users, null, 2)}`)
// Delete each user.
for (let i = 0; i < users.length; i++) {
const thisUser = users[i]
await thisUser.remove()
}
} catch (err) {
console.error('Error in test-utils.js/deleteAllUsers()')
}
}
// This function is used to create new users.
// userObj = {
// username,
@@ -170,12 +151,56 @@ async function getAdminJWT () {
throw err
}
}
// Fetches all users from the database.
async function getAllUsers () {
try {
const adminJWT = await getAdminJWT()
const options = {
method: 'GET',
url: `${LOCALHOST}/users`,
headers: {
Authorization: `Bearer ${adminJWT}`
}
}
const result = await axios(options)
return result.data.users
} catch (err) {
console.error('Error in test/utils.js/getAllUsers()', err)
throw err
}
}
// Deletes all users from the database.
async function deleteAllUsers () {
try {
const allUsers = await getAllUsers()
const adminJWT = await getAdminJWT()
for (let i = 0; i < allUsers.length; i++) {
const user = allUsers[i]
// Skip the admin user.
if (user.type === 'admin') {
continue
}
const options = {
method: 'DELETE',
url: `${LOCALHOST}/users/${user._id}`,
headers: {
Authorization: `Bearer ${adminJWT}`
}
}
await axios(options)
}
} catch (err) {
console.error('Error in test/utils.js/deleteAllUsers()', err)
throw err
}
}
export default {
cleanDb,
createUser,
loginTestUser,
loginAdminUser,
getAdminJWT,
deleteAllUsers
deleteAllUsers,
getAllUsers
}
+2 -2
View File
@@ -12,8 +12,8 @@ import fs from 'fs'
// import User from '../../../src/adapters/localdb/models/users.js'
// Customize these variables
const EMAIL = 'someone@somewhere.com'
const NAME = 'Someone'
const EMAIL = 'test@test.com'
const NAME = 'tester'
const PASSWORD = 'test'
async function createUser () {