Compare commits

...
89 Commits
Author SHA1 Message Date
Chris Troutner ac83827fca Merge branch 'master' into fullstack 2025-12-04 15:29:13 +00:00
Chris Troutner 0811024030 Merge pull request #123 from Permissionless-Software-Foundation/dh-token-data
feat(offer): Added tokenData to offer model
2025-12-04 08:08:45 -07:00
Daniel Gonzalez 737d35463d feat(offer): Added tokenData to offer model 2025-12-03 19:21:40 -04:00
Chris Troutner 2ed1eaba9c Merge branch 'master' into fullstack 2025-11-26 14:16:56 +00:00
Chris Troutner 6dddfbdcb6 Merge pull request #122 from Permissionless-Software-Foundation/dh-ctr-offr-page
feat(counterOffer): Added new properties to counter offer model
2025-11-25 18:59:41 -08:00
Daniel Gonzalez fc2d7b4b9b feat(counterOffer): Added new properties to counter offer model 2025-11-25 20:57:44 -04:00
Chris Troutner c877898ae7 Merge pull request #121 from Permissionless-Software-Foundation/dh-track-counter-offrs
feat(offers): Track Counter Offers
2025-11-21 15:54:45 -08:00
Daniel Gonzalez 88e27f47f5 feat(offers): Track Counter Offers 2025-11-21 17:34:05 -04:00
Chris Troutner 9681def3e0 Merge branch 'master' into fullstack 2025-11-14 19:24:38 +00:00
Chris Troutner 0057007b4c Merge pull request #120 from Permissionless-Software-Foundation/dh-npub-offer
feat(nostr): Added npub to Offer model
2025-11-13 06:28:42 -08:00
Daniel Gonzalez 8aad93e572 feat(nostr): Added npub to Offer model 2025-11-11 14:48:09 -04:00
Chris Troutner fa0ec3ece2 Merge pull request #119 from Permissionless-Software-Foundation/dh-npub-order
feat(nostr): Added npub to order
2025-11-11 06:56:18 -08:00
Daniel Gonzalez 4f9f2f8370 feat(nostr): Added npub to order 2025-11-10 18:45:37 -04:00
Chris Troutner 0fb93be2cd Merge pull request #118 from Permissionless-Software-Foundation/dh-timer-deleted
feat(chats): Added timer to delete old deleted chat and post database models
2025-11-06 10:58:25 -08:00
Daniel Gonzalez 26dcd6072e feat(chats): Added timer to delete old deleted chat and post database models 2025-11-06 11:42:46 -04:00
Chris Troutner 42f228af0e Merge branch 'master' into fullstack 2025-11-05 18:50:34 +00:00
Chris Troutner 8be70baff2 Merge pull request #117 from Permissionless-Software-Foundation/dh-nostr-route
feat(nostr): Added new route for deleteChat and deletePost models
2025-10-29 09:02:51 -07:00
Daniel Gonzalez 7e5c314000 feat(nostr): Added new route for deleteChat and deletePost models 2025-10-28 19:18:18 -04:00
Chris Troutner 9669112d69 Merge pull request #116 from Permissionless-Software-Foundation/ct-unstable
fix(offer): Adding makerNpub property to db model
2025-10-26 11:14:07 -07:00
Chris Troutner 9a71763f79 fix(offer): Adding makerNpub property to db model 2025-10-26 11:12:49 -07:00
Chris Troutner 1a96eac8c9 Merge pull request #115 from Permissionless-Software-Foundation/ct-unstable
Adding deletedChat and deletedPost DB models
2025-10-25 16:03:55 -07:00
Chris Troutner a2acdef539 fix(database): Adding deletedChat and deletedPost DB models 2025-10-25 16:02:35 -07:00
Chris Troutner 58ddbbcc4e fix(updateUser): Adding util scripts for dev & prod 2025-10-25 14:35:22 -07:00
Chris Troutner 5ff33ff19f Merge remote-tracking branch 'upstream/master' into ct-unstable 2025-10-25 14:34:05 -07:00
Chris Troutner 682f599261 fixing updateUser script 2025-10-25 14:33:51 -07:00
Chris Troutner 7c26e33a8c Merge pull request #181 from Permissionless-Software-Foundation/ct-unstable
fix(util): Fixing problem with updateUser.js script
2025-10-25 14:33:12 -07:00
Chris Troutner 2595ee8295 fix(util): Fixing problem with updateUser.js script 2025-10-25 14:31:33 -07:00
Chris Troutner 531685c0e4 Fixing merge conflicts with upstream 2025-10-25 14:26:16 -07:00
Chris Troutner 476fdcd0aa Merge pull request #180 from Permissionless-Software-Foundation/ct-unstable
fix(utils): Adding script to update users password or convert to admin
2025-10-25 14:24:43 -07:00
Chris Troutner b6762b215e fix(utils): Adding script to update users password or convert to admin 2025-10-25 14:23:28 -07:00
Chris Troutner 4ac7f59378 Merge branch 'master' into fullstack 2025-10-25 18:30:01 +00:00
Chris Troutner 3cdf49bb56 Merge pull request #114 from Permissionless-Software-Foundation/dh-update-endpoint
feat(update): Added Update button REST endpoint
2025-10-25 11:25:02 -07:00
Chris Troutner 28615403b8 Formatting and adding dev comments 2025-10-25 11:22:13 -07:00
Daniel Gonzalez bb10bd819a feat(update): Added Update button REST endpoint 2025-10-24 19:58:56 -04:00
Chris Troutner 3882217e6a Merge pull request #113 from Permissionless-Software-Foundation/dh-user-data
feat(userdata): Store user data on the database
2025-10-21 12:57:45 -07:00
Chris Troutner 8cd2896115 linting 2025-10-21 12:56:23 -07:00
Chris Troutner e570519ec8 Switching default back end to free-bch 2025-10-21 12:56:07 -07:00
Chris Troutner 2f74b74110 fix(createOffer()): Handling API errors when token data can not be retrieved 2025-10-21 12:44:48 -07:00
Chris Troutner fddb5499f0 Making offer use-case unit tests less flaky 2025-10-21 12:29:23 -07:00
Daniel Gonzalez fed98d51be feat(userdata): Store user data on the database 2025-10-20 18:37:54 -04:00
Chris Troutner 8e3ee3a0fb Merge pull request #112 from Permissionless-Software-Foundation/dh-offer-model
feat(offer): Store mutable data in bch-dex Offer db model
2025-10-17 22:30:36 -07:00
Daniel Gonzalez e57faf8f26 feat(offer): Store mutable data in bch-dex Offer db model 2025-10-16 18:38:06 -04:00
Chris Troutner 725c1c39a5 Merge branch 'master' into fullstack 2025-10-09 15:09:25 +00:00
Chris Troutner 61fa6b0ed6 Merge pull request #111 from Permissionless-Software-Foundation/dh-pagination
feat(NFTs): Added pagination controls for NFTs
2025-10-09 07:21:07 -07:00
Daniel Gonzalez 99843061ab feat(NFTs): Added pagination controls for NFTs 2025-10-08 20:20:11 -04:00
Chris Troutner 3a23763800 Adding an extra property to offer db model 2025-10-04 09:44:15 -07:00
Chris Troutner 3f4d49a228 Merge pull request #110 from Permissionless-Software-Foundation/ct-unstable
fix(offer): Adding new properties to offer db model
2025-10-04 09:41:39 -07:00
Chris Troutner 88cfadbc17 fix(offer): Adding new properties to offer db model 2025-10-04 09:40:07 -07:00
Chris Troutner 6c1da41cb3 Syncing with upstream. Fixing merge conflicts 2025-10-04 09:00:38 -07:00
Chris Troutner 0e6a411012 Merge pull request #179 from Permissionless-Software-Foundation/ct-unstable
fix(IPFS): Adding API docs for the IPFS endpoints that need it
2025-10-04 08:32:55 -07:00
Chris Troutner 55e9e35c1f fix(IPFS): Adding API docs for the IPFS endpoints that need it 2025-10-04 08:31:17 -07:00
Chris Troutner 84603356ef Merge pull request #109 from Permissionless-Software-Foundation/dh-access-validation
feat(test): Added user/admin access validation to endpoint
2025-10-03 06:17:40 -07:00
Daniel Gonzalez 2f6b4dfc09 feat(test): Added user/admin access validation to endpoint 2025-10-01 17:24:29 -04:00
Chris Troutner b19af253ae Merge pull request #108 from Permissionless-Software-Foundation/dh-api-docs
fix(docs): Added api-docs to bch-dex endpoints
2025-09-30 07:03:10 -07:00
Daniel Gonzalez b6c6c186a8 fix(docs): Added api-docs to bch-dex endpoints 2025-09-29 18:58:20 -04:00
Chris Troutner ae6e708f51 Merge pull request #107 from Permissionless-Software-Foundation/ct-unstable
Reducing debug console logs
2025-09-16 10:22:09 -07:00
Chris Troutner 442f4ed752 Reducing more console logs 2025-09-16 10:04:00 -07:00
Chris Troutner 8617cf5fd0 fix(offers): Reducing console logs 2025-09-14 09:52:03 -07:00
Chris Troutner 2e00230f79 Merge branch 'master' into ct-unstable 2025-09-14 08:41:21 -07:00
Chris Troutner 44dcce6c45 Merge pull request #106 from Permissionless-Software-Foundation/dh-unit-test-coverage
feat(tests): Added more unit test coverage
2025-09-14 08:32:52 -07:00
Chris Troutner 8d43192a1c minor edits 2025-09-14 08:31:29 -07:00
Daniel Gonzalez 59e2bb473b feat(tests): Added more unit test coverage 2025-09-13 14:36:27 -04:00
Chris Troutner b4d2506e81 Tried AI generated architectural diagram 2025-09-10 06:50:35 -07:00
Chris Troutner 7290c8db30 Merge branch 'master' into fullstack 2025-08-10 18:11:09 +00:00
Chris Troutner 2af50ecf02 Merge pull request #105 from Permissionless-Software-Foundation/operator-fee
Adding required Operator fee to SWaP TX
2025-08-10 11:06:55 -07:00
Chris Troutner f88a24c1b5 Updating unit tests 2025-08-10 10:44:45 -07:00
Chris Troutner a4b4ff13d2 Handling corner cases when validating counter offer against operator fee 2025-08-09 17:54:30 -07:00
Chris Troutner 2bf4f9df28 fix(acceptCounterOffer()): Skipping Counter Offers that are missing an output for the operator 2025-08-09 16:28:56 -07:00
Chris Troutner 19ba38e8ce Adding operator fee to Offer use case, entity, and database model 2025-08-09 15:08:31 -07:00
Chris Troutner 373a52ebf1 feat(operator fee): Adding config values to ensure bch-dex operator recieves a fee 2025-08-09 15:02:29 -07:00
Chris Troutner fe82478dc4 linting 2025-08-09 14:49:41 -07:00
Chris Troutner bed281c8c3 minor edit 2025-08-09 14:49:32 -07:00
Chris Troutner 01484b4e03 fixing typo in comments 2025-08-07 07:00:58 -07:00
Chris Troutner 36e1ec1f8e Merge branch 'master' into fullstack 2025-07-28 18:19:52 +00:00
Chris Troutner baa574a93e Merge branch 'master' into fullstack 2025-07-08 23:56:41 +00:00
Chris Troutner 8333563d98 Merge branch 'master' into fullstack 2025-07-06 21:44:33 +00:00
Chris Troutner 99fd116b6d Merge branch 'master' into fullstack 2025-06-27 16:28:28 +00:00
Chris Troutner ccae89ad01 Merge branch 'master' into fullstack 2025-06-06 18:53:44 +00:00
Chris Troutner 0ecae58be2 Merge branch 'master' into fullstack 2025-06-02 01:52:27 +00:00
Chris Troutner 5f442835e5 Customizing scrips 2025-05-20 19:38:16 +00:00
Chris Troutner aa8fd89227 Merge branch 'master' into fullstack 2025-05-19 22:45:42 +00:00
Chris Troutner 3da7432273 Building UI from psfoundation branch 2025-05-16 17:10:43 +00:00
Chris Troutner 61c1dd5878 Merge branch 'master' into fullstack 2025-05-16 14:56:09 +00:00
Chris Troutner c095e7af39 noop 2025-05-16 14:55:57 +00:00
Chris Troutner fb9612bb65 Using ct-unstable branch 2025-05-16 02:22:19 +00:00
Chris Troutner e4a37b1c10 Merge branch 'ct-unstable' into fullstack 2025-05-16 00:58:49 +00:00
Chris Troutner ba4f847e57 Merge branch 'master' into fullstack 2025-05-16 00:39:57 +00:00
Chris Troutner 81e623e35f Merge branch 'ct-unstable' into fullstack 2025-03-09 01:10:05 +00:00
Chris Troutner db1da9172c Using fullstack branch of UI 2024-12-29 09:37:28 -08:00
70 changed files with 4792 additions and 118 deletions
+7 -3
View File
@@ -47,8 +47,8 @@ export default {
useFullStackCash: process.env.USE_FULLSTACKCASH ? true : false, useFullStackCash: process.env.USE_FULLSTACKCASH ? true : false,
consumerUrl: process.env.CONSUMER_URL consumerUrl: process.env.CONSUMER_URL
? process.env.CONSUMER_URL ? process.env.CONSUMER_URL
// : 'https://free-bch.fullstack.cash', : 'https://free-bch.fullstack.cash',
: 'https://dev-consumer.psfoundation.info', // : 'https://dev-consumer.psfoundation.info',
// : 'https://wa-usa-bch-consumer.fullstackcash.nl', // : 'https://wa-usa-bch-consumer.fullstackcash.nl',
// P2WDB URL that will accept API calls from the p2wdb npm library. // P2WDB URL that will accept API calls from the p2wdb npm library.
@@ -191,5 +191,9 @@ export default {
disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false, disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false,
// Admin password // 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

+1
View File
@@ -10,6 +10,7 @@
"test:all": "export BCH_DEX=test && c8 --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/", "test:all": "export BCH_DEX=test && c8 --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/",
"test:unit": "export BCH_DEX=test && c8 --reporter=text mocha --exit --timeout 15000 --recursive test/unit/", "test:unit": "export BCH_DEX=test && c8 --reporter=text mocha --exit --timeout 15000 --recursive test/unit/",
"test:e2e:auto": "export BCH_DEX=test && mocha --exit --timeout 30000 test/e2e/automated/", "test:e2e:auto": "export BCH_DEX=test && mocha --exit --timeout 30000 test/e2e/automated/",
"start:e2e:server": "export BCH_DEX=test && node index.js",
"test:integration": "export BCH_DEX=test && mocha --exit --timeout 45000 --recursive test/integration", "test:integration": "export BCH_DEX=test && mocha --exit --timeout 45000 --recursive test/integration",
"test:temp": "export BCH_DEX=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/", "test:temp": "export BCH_DEX=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/",
"lint": "standard --env mocha --fix", "lint": "standard --env mocha --fix",
+1 -1
View File
@@ -53,7 +53,7 @@ RUN git clone https://github.com/Permissionless-Software-Foundation/bch-dex-ui-v
WORKDIR /home/safeuser/bch-dex-ui-v3 WORKDIR /home/safeuser/bch-dex-ui-v3
# For development: switch to unstable branch # For development: switch to unstable branch
RUN git checkout ct-unstable RUN git checkout psfoundation
# Install dependencies # Install dependencies
#RUN mkdir .ipfsdata #RUN mkdir .ipfsdata
@@ -31,4 +31,7 @@ export APP_ID=bch-dex-001
#export P2WDB_URL=http://172.17.0.1:5667 #export P2WDB_URL=http://172.17.0.1:5667
export DISABLE_NEW_ACCOUNTS=true
export ADMIN_PASSWORD=decatur
npm start npm start
+11
View File
@@ -0,0 +1,11 @@
{
"privateKey": "L4VyBDiWYYeeZhkM3xE3nsZigXJYmvRM29YBxoC7da4FzvubjxFo",
"publicKey": "028d6c77d865e6140db4b879a9bdf69ee4846593c0b6c48a82c448d5792806aab1",
"mnemonic": "split title trigger soda execute frost brush labor vague protect bicycle fiscal",
"cashAddress": "bitcoincash:qprrdk7en7m774gsafkycxcymc9ry209jus5v8wx5z",
"address": "bitcoincash:qprrdk7en7m774gsafkycxcymc9ry209jus5v8wx5z",
"legacyAddress": "17QG25rNUfnfj2NtdaFiyiqKSJatvEnMYd",
"hdPath": "m/44'/245'/0'/0/0",
"slpAddress": "simpleledger:qprrdk7en7m774gsafkycxcymc9ry209juu08umx2u",
"nextAddress": 1
}
+6
View File
@@ -9,6 +9,9 @@ import Order from './models/order.js'
import Offer from './models/offer.js' import Offer from './models/offer.js'
import Usage from './models/usage.js' import Usage from './models/usage.js'
import SmAccount from './models/smAccount.js' import SmAccount from './models/smAccount.js'
import DeletedChat from './models/deletedChat.js'
import DeletedPost from './models/deletedPost.js'
import CounterOffer from './models/counter-offer.js'
class LocalDB { class LocalDB {
constructor () { constructor () {
@@ -19,6 +22,9 @@ class LocalDB {
this.Offer = Offer this.Offer = Offer
this.Usage = Usage this.Usage = Usage
this.SmAccount = SmAccount this.SmAccount = SmAccount
this.DeletedChat = DeletedChat
this.DeletedPost = DeletedPost
this.CounterOffer = CounterOffer
} }
} }
@@ -0,0 +1,14 @@
import mongoose from 'mongoose'
const CounterOffer = new mongoose.Schema({
nostrEventId: { type: String, required: true }, // Nostr Event Id.
takerAddr: { type: String, default: null },
takerNpub: { type: String, default: null },
makerNpub: { type: String, default: null },
counterOfferAddr: { type: String, default: null },
counterOfferUtxo: { type: String, default: null },
takerOfferUtxo: { type: String, default: null },
tokenId: { type: String, default: null }
})
export default mongoose.model('counterOffer', CounterOffer)
@@ -0,0 +1,16 @@
/*
Database model for tracking deleted Nostr chat messages.
*/
import mongoose from 'mongoose'
const DeletedChat = new mongoose.Schema({
eventId: { type: String, required: true, default: null }, // Event ID of the deleted message.
npub: { type: String, default: null }, // Nostr public key of the user who created the message.
bchAddr: { type: String, default: null }, // bch address of the user who created the message.
pubkey: { type: String, default: null }, // Nostr public key of the user who created the message.
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
})
export default mongoose.model('deletedChat', DeletedChat)
@@ -0,0 +1,16 @@
/*
Database model for tracking deleted Nostr posts.
*/
import mongoose from 'mongoose'
const DeletedPost = new mongoose.Schema({
eventId: { type: String, required: true, default: null }, // Event ID of the deleted post.
npub: { type: String, default: null }, // Nostr public key of the user who created the post.
bchAddr: { type: String, default: null }, // bch address of the user who created the post.
pubkey: { type: String, default: null }, // Nostr public key of the user who created the post.
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
})
export default mongoose.model('deletedPost', DeletedPost)
+15 -1
View File
@@ -18,6 +18,7 @@ const Offer = new mongoose.Schema({
p2wdbHash: { type: String }, p2wdbHash: { type: String },
offerStatus: { type: String }, offerStatus: { type: String },
makerAddr: { type: String }, makerAddr: { type: String },
makerNpub: { type: String, default: null },
// Authentication data // Authentication data
signature: { type: String }, signature: { type: String },
@@ -35,8 +36,21 @@ const Offer = new mongoose.Schema({
lokadId: { type: String }, lokadId: { type: String },
messageType: { type: Number }, messageType: { type: Number },
messageClass: { 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 },
tokenData: { type: Object },
tokenCategories: { type: Array, default: [] },
tokenTags: { type: Array, default: [] },
userDataStr: { type: String }, // Token user data
lastUpdatedTokenData: { type: String, default: null } // ISO timestamp of the last time token data was updated.
}) })
export default mongoose.model('offer', Offer) export default mongoose.model('offer', Offer)
+6 -1
View File
@@ -24,6 +24,7 @@ const Order = new mongoose.Schema({
p2wdbTxid: { type: String }, p2wdbTxid: { type: String },
p2wdbHash: { type: String }, p2wdbHash: { type: String },
makerAddr: { type: String, required: true }, makerAddr: { type: String, required: true },
makerNpub: { type: String, required: true },
// Authentication data // Authentication data
signature: { type: String }, signature: { type: String },
@@ -43,7 +44,11 @@ const Order = new mongoose.Schema({
// Additional properties found in createOrder // Additional properties found in createOrder
dataType: { type: String, required: true }, dataType: { type: String, required: true },
userId: { type: String, required: true } userId: { type: String, required: true },
// Operator fee and address
operatorAddress: { type: String },
operatorPercentage: { type: Number }
}) })
export default mongoose.model('order', Order) export default mongoose.model('order', Order)
+28
View File
@@ -7,6 +7,8 @@ import BchNostr from 'bch-nostr'
import { RelayPool } from 'nostr' import { RelayPool } from 'nostr'
import RetryQueue from '@chris.troutner/retry-queue' import RetryQueue from '@chris.troutner/retry-queue'
import * as nip19 from 'nostr-tools/nip19' import * as nip19 from 'nostr-tools/nip19'
import { base58_to_binary as base58Tobinary } from 'base58-js'
import { getPublicKey } from 'nostr-tools/pure'
class NostrAdapter { class NostrAdapter {
constructor (localConfig = { nostrRelay: '', nostrTopic: '' }) { constructor (localConfig = { nostrRelay: '', nostrTopic: '' }) {
@@ -44,6 +46,7 @@ class NostrAdapter {
this.npub2pubkey = this.npub2pubkey.bind(this) this.npub2pubkey = this.npub2pubkey.bind(this)
this.readGlobalFeed = this.readGlobalFeed.bind(this) this.readGlobalFeed = this.readGlobalFeed.bind(this)
this.getFollowers = this.getFollowers.bind(this) this.getFollowers = this.getFollowers.bind(this)
this.privKeyToNpub = this.privKeyToNpub.bind(this)
} }
// Create nostr keys. // Create nostr keys.
@@ -247,6 +250,31 @@ class NostrAdapter {
throw error throw error
} }
} }
// Convert a BCH private key (WIF) to a Nostr npub.
privKeyToNpub (privKey) {
try {
if (!privKey || typeof privKey !== 'string') {
throw new Error('privKey must be a string!')
}
// Extract the privaty key from the WIF, using this guide:
// https://learnmeabitcoin.com/technical/keys/private-key/wif/
const wifBuf = base58Tobinary(privKey)
const privBuf = wifBuf.slice(1, 33)
// console.log('privBuf: ', privBuf)
const nostrPubKey = getPublicKey(privBuf)
// console.log('nostrPubKey: ', nostrPubKey)
// Convert pubkey to npub
const nostrNpub = nip19.npubEncode(nostrPubKey)
// console.log('nostrNpub: ', nostrNpub)
return nostrNpub
} catch (error) {
console.log(`Error in nostr.js/privKeyToNpub() ${error.message} `)
throw error
}
}
} }
export default NostrAdapter export default NostrAdapter
+20
View File
@@ -56,6 +56,7 @@ class WalletAdapter {
this.completeTx = this.completeTx.bind(this) this.completeTx = this.completeTx.bind(this)
this.reclaimTokens = this.reclaimTokens.bind(this) this.reclaimTokens = this.reclaimTokens.bind(this)
this.moveTokensFromCustomWallet = this.moveTokensFromCustomWallet.bind(this) this.moveTokensFromCustomWallet = this.moveTokensFromCustomWallet.bind(this)
this.cid2json = this.cid2json.bind(this)
} }
// Open the wallet file, or create one if the file doesn't exist. // Open the wallet file, or create one if the file doesn't exist.
@@ -677,6 +678,25 @@ class WalletAdapter {
throw err throw err
} }
} }
async cid2json (urlOrCid) {
try {
// Input validation
if (!urlOrCid || typeof urlOrCid !== 'string') {
throw new Error('urlOrCid must be a string!')
}
// Extract the cid from the url or cid.
const cid = urlOrCid.split('/').pop()
console.log('cid to json: ', cid)
const jsonRes = await this.bchWallet.cid2json({ cid })
const json = jsonRes.json
// console.log('json: ', json)
return json
} catch (err) {
console.error('Error in wallet.js/cid2json()', err.message)
throw err
}
}
} }
export default WalletAdapter export default WalletAdapter
@@ -29,7 +29,36 @@ class EntryRESTControllerLib {
_this = this _this = this
} }
/**
* @api {post} /entry Create an entry
* @apiPermission public
* @apiName CreateEntry
* @apiGroup REST Entry
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{"entry": "1234567890", "description": "1234567890", "slpAddress": "1234567890", "signature": "1234567890", "category": "1234567890"}' localhost:5001/entry
*
* @apiSuccess {Object} entry Entry object
* @apiSuccess {String} entry.id Entry ID
* @apiSuccess {String} entry.description Entry description
* @apiSuccess {String} entry.slpAddress Entry slp address
* @apiSuccess {String} entry.signature Entry signature
* @apiSuccess {String} entry.category Entry category
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "entry": {
* "id": "1234567890",
* "description": "1234567890",
* "slpAddress": "1234567890",
* "signature": "1234567890",
* "category": "1234567890"
* }
* }
*/
// No api-doc documentation because this wont be a public endpoint // No api-doc documentation because this wont be a public endpoint
// Note: this is a private endpoint, needs to be authenticated
async createEntry (ctx) { async createEntry (ctx) {
try { try {
console.log('body: ', ctx.request.body) console.log('body: ', ctx.request.body)
+5
View File
@@ -19,6 +19,7 @@ import OrderRouter from './order/index.js'
import P2WDBRouter from './p2wdb/index.js' import P2WDBRouter from './p2wdb/index.js'
import UsageRESTController from './usage/index.js' import UsageRESTController from './usage/index.js'
import SmAccountRouter from './smAccount/index.js' import SmAccountRouter from './smAccount/index.js'
import NostrRouter from './nostr/index.js'
class RESTControllers { class RESTControllers {
constructor (localConfig = {}) { constructor (localConfig = {}) {
@@ -90,6 +91,10 @@ class RESTControllers {
const smAccountRouter = new SmAccountRouter(dependencies) const smAccountRouter = new SmAccountRouter(dependencies)
smAccountRouter.attach(app) smAccountRouter.attach(app)
// Attach the REST API Controllers associated with the /nostr route
const nostrRouter = new NostrRouter(dependencies)
nostrRouter.attach(app)
} }
} }
+70 -3
View File
@@ -40,11 +40,23 @@ class IpfsRESTControllerLib {
* @api {get} /ipfs Get status on IPFS infrastructure * @api {get} /ipfs Get status on IPFS infrastructure
* @apiPermission public * @apiPermission public
* @apiName GetIpfsStatus * @apiName GetIpfsStatus
* @apiGroup REST BCH * @apiGroup REST IPFS
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs * curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs
* *
* @apiSuccess {Object} status IPFS status object
* @apiSuccess {String} status.ipfsId IPFS ID
* @apiSuccess {String} status.multiaddrs IPFS multiaddrs
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "status": {
* "ipfsId": "1234567890",
* "multiaddrs": ["/ip4/123.45.67.89/tcp/4001/p2p/1234567890"]
* }
* }
*/ */
async getStatus (ctx) { async getStatus (ctx) {
try { try {
@@ -58,7 +70,26 @@ class IpfsRESTControllerLib {
} }
} }
// Return information on IPFS peers this node is connected to. /**
* @api {post} /ipfs/peers Get information on IPFS peers this node is connected to
* @apiPermission public
* @apiName GetIpfsPeers
* @apiGroup REST IPFS
*
* @apiParam {Boolean} [showAll=false] Whether to include detailed peer data
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST localhost:5001/ipfs/peers \
* -d '{"showAll": false}'
*
* @apiSuccess {Object[]} peers Array of peer objects
* @apiSuccess {String} peers[].peer Peer ID
* @apiSuccess {String} peers[].name Peer name
* @apiSuccess {String} peers[].protocol Protocol used by the peer
* @apiSuccess {String} peers[].version Peer version
* @apiSuccess {String} peers[].connectionAddr Connection address
* @apiSuccess {Object} [peers[].peerData] Detailed peer data (when showAll=true)
*/
async getPeers (ctx) { async getPeers (ctx) {
try { try {
const showAll = ctx.request.body.showAll const showAll = ctx.request.body.showAll
@@ -73,6 +104,24 @@ class IpfsRESTControllerLib {
} }
} }
/**
* @api {post} /ipfs/relays Get data about the known Circuit Relays
* @apiPermission public
* @apiName GetIpfsRelays
* @apiGroup REST IPFS
*
* @apiDescription Returns information about Circuit Relays, both v1 and v2, that this node knows about. V2 relays are hydrated with peer data from the connected peers list.
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST localhost:5001/ipfs/relays
*
* @apiSuccess {Object} relays Object containing relay information
* @apiSuccess {Object[]} relays.v2Relays Array of v2 Circuit Relay objects
* @apiSuccess {String} relays.v2Relays[].ipfsId IPFS ID of the relay
* @apiSuccess {String} relays.v2Relays[].name Name of the relay (hydrated from peer data)
* @apiSuccess {String} relays.v2Relays[].description Description of the relay (hydrated from peer data)
* @apiSuccess {Object[]} relays.v1Relays Array of v1 Circuit Relay configurations
*/
// Get data about the known Circuit Relays. Hydrate with data from peers list. // Get data about the known Circuit Relays. Hydrate with data from peers list.
async getRelays (ctx) { async getRelays (ctx) {
try { try {
@@ -86,6 +135,24 @@ class IpfsRESTControllerLib {
} }
} }
/**
* @api {post} /ipfs/connect Connect to a specific IPFS peer
* @apiPermission public
* @apiName ConnectToIpfsPeer
* @apiGroup REST IPFS
*
* @apiDescription Attempts to establish a connection to a specific IPFS peer using the provided multiaddr. Optionally returns detailed information about the connection.
*
* @apiParam {String} multiaddr Multiaddress of the peer to connect to (required)
* @apiParam {Boolean} [getDetails=false] Whether to return detailed connection information
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST localhost:5001/ipfs/connect \
* -d '{"multiaddr": "/ip4/161.35.99.207/tcp/4001/p2p/12D3KooWDtj9cfj1SKuLbDNKvKRKSsGN8qivq9M8CYpLPDpcD5pu", "getDetails": false}'
*
* @apiSuccess {Boolean} success Indicates whether the connection attempt was successful
* @apiSuccess {Object} [details] Additional connection details (when getDetails=true)
*/
async connect (ctx) { async connect (ctx) {
try { try {
const multiaddr = ctx.request.body.multiaddr const multiaddr = ctx.request.body.multiaddr
@@ -107,7 +174,7 @@ class IpfsRESTControllerLib {
* @api {get} /ipfs/node Get a copy of the thisNode object from helia-coord * @api {get} /ipfs/node Get a copy of the thisNode object from helia-coord
* @apiPermission public * @apiPermission public
* @apiName GetThisNode * @apiName GetThisNode
* @apiGroup REST BCH * @apiGroup REST IPFS
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/node * curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/node
@@ -0,0 +1,219 @@
/*
REST API Controller library for the /nostr route
*/
import wlogger from '../../../adapters/wlogger.js'
class NostrRESTControllerLib {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating /nostr REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating /nostr REST Controller.'
)
}
this.createDeletedChat = this.createDeletedChat.bind(this)
this.getDeletedChats = this.getDeletedChats.bind(this)
this.createDeletedPost = this.createDeletedPost.bind(this)
this.getDeletedPosts = this.getDeletedPosts.bind(this)
this.handleError = this.handleError.bind(this)
}
/**
* @api {post} /nostr/deletedChat Create Deleted Chat.
* @apiPermission admin
* @apiName CreateDeletedChat
* @apiGroup REST Nostr
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -H "Authorization: Bearer <token>" -X POST -d '{"eventId": "note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q" , "npub" : "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8" , "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a", "pubkey":"6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1"}' localhost:5700/nostr/deletedChat
*
* @apiSuccess {String} _id DB Model id
* @apiSuccess {String} eventId Event id
* @apiSuccess {String} bchAddr BCH Address
* @apiSuccess {String} npub Nostr Npub
* @apiSuccess {String} pubkey Nostr pubkey
* @apiSuccess {Date} createdAt Created at date.
* @apiSuccess {Date} updatedAt Updated at date.
*
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "_id": "6900f87f9cafee38028ea27e",
* "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8",
* "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a",
* "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1",
* "createdAt":"2025-10-28T17:08:15.409Z",
* "updatedAt": "2025-10-28T17:08:15.409Z"
* }
*
* @apiUse TokenError
*/
async createDeletedChat (ctx) {
try {
const chat = ctx.request.body
const result = await this.useCases.nostr.createDeletedChat(chat)
ctx.body = {
deletedChat: result
}
} catch (err) {
wlogger.error('Error in createDeletedChat() REST API handler.')
this.handleError(ctx, err)
}
}
/**
* @api {get} /nostr/deletedChat Get Deleted Chats.
* @apiPermission public
* @apiName GetDeletedChats
* @apiGroup REST Nostr
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5700/nostr/deletedChat
*
* @apiSuccess {String} _id DB Model id
* @apiSuccess {String} eventId Event id
* @apiSuccess {String} bchAddr BCH Address
* @apiSuccess {String} npub Nostr Npub
* @apiSuccess {String} pubkey Nostr pubkey
* @apiSuccess {Date} createdAt Created at date.
* @apiSuccess {Date} updatedAt Updated at date.
*
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* [{
* "_id": "6900f87f9cafee38028ea27e",
* "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8",
* "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a",
* "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1",
* "createdAt":"2025-10-28T17:08:15.409Z",
* "updatedAt": "2025-10-28T17:08:15.409Z"
* }]
*
* */
async getDeletedChats (ctx) {
try {
const deletedChats = await this.useCases.nostr.getDeletedChats()
ctx.body = { deletedChats }
} catch (err) {
wlogger.error('Error in getDeletedChats() REST API handler.')
this.handleError(ctx, err)
}
}
/**
* @api {post} /nostr/deletedPost Create Deleted Post.
* @apiPermission admin
* @apiName CreateDeletedPost
* @apiGroup REST Nostr
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -H "Authorization: Bearer <token>" -X POST -d '{"eventId": "note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q" , "npub" : "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8" , "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a", "pubkey":"6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1"}' localhost:5700/nostr/deletedPost
*
* @apiSuccess {String} _id DB Model id
* @apiSuccess {String} eventId Event id
* @apiSuccess {String} bchAddr BCH Address
* @apiSuccess {String} npub Nostr Npub
* @apiSuccess {String} pubkey Nostr pubkey
* @apiSuccess {Date} createdAt Created at date.
* @apiSuccess {Date} updatedAt Updated at date.
*
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "_id": "6900f87f9cafee38028ea27e",
* "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8",
* "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a",
* "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1",
* "createdAt":"2025-10-28T17:08:15.409Z",
* "updatedAt": "2025-10-28T17:08:15.409Z"
* }
*
* @apiUse TokenError
*/
async createDeletedPost (ctx) {
try {
const post = ctx.request.body
const result = await this.useCases.nostr.createDeletedPost(post)
ctx.body = {
deletedPost: result
}
} catch (err) {
wlogger.error('Error in createDeletedPost() REST API handler.')
this.handleError(ctx, err)
}
}
/**
* @api {get} /nostr/deletedPost Get Deleted Posts.
* @apiPermission public
* @apiName GetDeletedPosts
* @apiGroup REST Nostr
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5700/nostr/deletedPost
*
* @apiSuccess {String} _id DB Model id
* @apiSuccess {String} eventId Event id
* @apiSuccess {String} bchAddr BCH Address
* @apiSuccess {String} npub Nostr Npub
* @apiSuccess {String} pubkey Nostr pubkey
* @apiSuccess {Date} createdAt Created at date.
* @apiSuccess {Date} updatedAt Updated at date.
*
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* [{
* "_id": "6900f87f9cafee38028ea27e",
* "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8",
* "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a",
* "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1",
* "createdAt":"2025-10-28T17:08:15.409Z",
* "updatedAt": "2025-10-28T17:08:15.409Z"
* }]
*
* */
async getDeletedPosts (ctx) {
try {
const deletedPosts = await this.useCases.nostr.getDeletedPosts()
ctx.body = { deletedPosts }
} catch (err) {
wlogger.error('Error in getDeletedPosts() REST API handler.')
this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
// If an HTTP status is specified by the buisiness logic, use that.
if (err.status) {
if (err.message) {
ctx.throw(err.status, err.message)
} else {
ctx.throw(err.status)
}
} else {
// By default use a 422 error if the HTTP status is not specified.
ctx.throw(422, err.message)
}
}
}
export default NostrRESTControllerLib
+81
View File
@@ -0,0 +1,81 @@
/*
REST API library for /nostr route.
*/
// Public npm libraries.
import Router from 'koa-router'
// Local libraries.
import NostrRESTControllerLib from './controller.js'
import Validators from '../middleware/validators.js'
import config from '../../../../config/index.js'
class NostrRouter {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating NostrRouter REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating NostrRouter REST Controller.'
)
}
const dependencies = {
adapters: this.adapters,
useCases: this.useCases
}
// Encapsulate dependencies.
this.config = config
this.nostrRESTController = new NostrRESTControllerLib(dependencies)
this.validators = new Validators()
// Instantiate the router and set the base route.
const baseUrl = '/nostr'
this.router = new Router({ prefix: baseUrl })
this.attach = this.attach.bind(this)
this.createDeletedChat = this.createDeletedChat.bind(this)
this.createDeletedPost = this.createDeletedPost.bind(this)
}
attach (app) {
if (!app) {
throw new Error(
'Must pass app object when attaching REST API controllers.'
)
}
// Define the routes and attach the controller.
this.router.post('/deletedChat', this.createDeletedChat)
this.router.get('/deletedChat', this.nostrRESTController.getDeletedChats)
this.router.post('/deletedPost', this.createDeletedPost)
this.router.get('/deletedPost', this.nostrRESTController.getDeletedPosts)
// Attach the Controller routes to the Koa app.
app.use(this.router.routes())
app.use(this.router.allowedMethods())
}
async createDeletedChat (ctx, next) {
await this.validators.ensureAdmin(ctx, next)
await this.nostrRESTController.createDeletedChat(ctx, next)
return true
}
async createDeletedPost (ctx, next) {
await this.validators.ensureAdmin(ctx, next)
await this.nostrRESTController.createDeletedPost(ctx, next)
return true
}
}
export default NostrRouter
@@ -34,10 +34,13 @@ class OfferRESTControllerLib {
this.listFungibleOffers = this.listFungibleOffers.bind(this) this.listFungibleOffers = this.listFungibleOffers.bind(this)
this.takeOffer = this.takeOffer.bind(this) this.takeOffer = this.takeOffer.bind(this)
this.listOffersByAddress = this.listOffersByAddress.bind(this) this.listOffersByAddress = this.listOffersByAddress.bind(this)
this.syncOfferMutableData = this.syncOfferMutableData.bind(this)
this.listCounterOffersByAddress = this.listCounterOffersByAddress.bind(this)
this.handleError = this.handleError.bind(this) this.handleError = this.handleError.bind(this)
} }
// No api-doc documentation because this wont be a public endpoint // No api-doc documentation because this wont be a public endpoint
// This function has no endpoint
async createOffer (ctx) { async createOffer (ctx) {
try { try {
console.log('body: ', ctx.request.body) console.log('body: ', ctx.request.body)
@@ -56,6 +59,65 @@ class OfferRESTControllerLib {
this.handleError(ctx, err) this.handleError(ctx, err)
} }
} }
/**
* @api {get} /offer/list/all/:page Get all offers
* @apiPermission public
* @apiName ListOffers
* @apiGroup REST Offer
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5000/offer/list/all/0
*
* @apiSuccess {Object[]} offers Array of offer objects
* @apiSuccess {ObjectId} offers._id Offer id
* @apiSuccess {String} offers.ticker Ticker
* @apiSuccess {String} offers.tokenId Token id
* @apiSuccess {String} offers.utxoTxid Utxo transaction id
* @apiSuccess {Number} offers.utxoVout Utxo output index
* @apiSuccess {String} offers.buyOrSell Buy or sell
* @apiSuccess {Number} offers.numTokens Number of tokens
* @apiSuccess {String} offers.rateInBaseUnit Rate in base unit
* @apiSuccess {String} offers.minUnitsToExchange Minimum units to exchange
* @apiSuccess {String} offers.offerStatus Offer status
* @apiSuccess {String} offers.makerAddr Maker address
* @apiSuccess {Number} offers.timestamp Timestamp
* @apiSuccess {String} offers.displayCategory Display category
* @apiSuccess {Boolean} offers.nsfw Nsfw
* @apiSuccess {Array} offers.flags Flags
* @apiSuccess {Number} offers.messageType Message type
* @apiSuccess {Number} offers.messageClass Message class
* @apiSuccess {String} offers.nostrEventId Nostr event id
* @apiSuccess {String} offers.operatorAddress Operator address
* @apiSuccess {Number} offers.operatorPercentage Operator percentage
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "offers": [{
* "_id": "56bd1da600a526986cf65c80",
* "tokenId": "1234567890",
* "utxoTxid": "1234567890",
* "utxoVout": 0,
* "buyOrSell": "buy",
* "numTokens": 1,
* "rateInBaseUnit": "100000000",
* "minUnitsToExchange": "100000000",
* "offerStatus": "active",
* "makerAddr": "1234567890",
* "timestamp": 1716883200,
* "displayCategory": "nft",
* "nsfw": false,
* "flags": [],
* "messageType": 1,
* "messageClass": 1,
* "nostrEventId": "1234567890",
* "operatorAddress": "1234567890",
* "operatorPercentage": 100
* }]
* }
*
* @apiUse TokenError
*/
// curl -X GET http://localhost:5700/offer/list/all/0 // curl -X GET http://localhost:5700/offer/list/all/0
async listOffers (ctx) { async listOffers (ctx) {
@@ -72,6 +134,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 // curl -X GET http://localhost:5700/offer/list/nft/0
async listNftOffers (ctx) { async listNftOffers (ctx) {
try { try {
@@ -87,6 +209,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 // curl -X GET http://localhost:5700/offer/list/fungible/0
async listFungibleOffers (ctx) { async listFungibleOffers (ctx) {
try { try {
@@ -102,6 +283,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' // Currently only supports 'sell' offers, and will only buy the 'numTokens'
// listed in the offer. // listed in the offer.
async takeOffer (ctx) { async takeOffer (ctx) {
@@ -119,6 +321,67 @@ 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. // List all offers being made by a given address.
// curl -X GET http://localhost:5700/offer/list/addr/bitcoincash:qrpxtnrlhfz9wsuuse7z5k2mxmvw0r3pu5qepyhmq2 // curl -X GET http://localhost:5700/offer/list/addr/bitcoincash:qrpxtnrlhfz9wsuuse7z5k2mxmvw0r3pu5qepyhmq2
async listOffersByAddress (ctx) { async listOffersByAddress (ctx) {
@@ -134,6 +397,64 @@ class OfferRESTControllerLib {
} }
} }
async syncOfferMutableData (ctx) {
try {
const tokenId = ctx.request.body.tokenId
const offer = await this.useCases.offer.syncOfferMutableData(tokenId)
ctx.body = offer
} catch (err) {
console.log('Error in syncOfferMutableData REST API handler: ', err)
this.handleError(ctx, err)
}
}
/**
* @api {get} /offer/list/counter-offer/:addr List all counter offers being made by a given address
* @apiPermission public
* @apiName listCounterOffersByAddress
* @apiGroup REST Offer
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5000/offer/list/counter-offer/bitcoincash:qq54a3xyyptty63ztlcavzzh64gy4d247qs4ueaupe
*
*
* @apiSuccess {String} offers.nostrEventId Nostr event id
* @apiSuccess {String} offers.takerNpub Nostr Npub.
* @apiSuccess {String} offers.takerAddr BCH Address.
* @apiSuccess {String} offers.counterOfferAddr BCH Address
* @apiSuccess {String} offers.counterOfferUtxo Utxo txid.
*
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "counterOffers": [{
* "_id": "56bd1da600a526986cf65c80",
* "nostrEventId": "1234567890",
* "takerAddr": "bitcoincash:qq54a3xyyptty63ztlcavzzh64gy4d247qs4ueaupe",
* "takerNpub": "npub1nhc47z2tf42a4ps7plzcyax2j76gdausve763tqw3xuw7jjd7ceqppquly",
* "counterOfferAddr": "bitcoincash:qzltx40ldxr53edchprppj5lpg4q7vl20usx25ga44",
* "counterOfferUtxo": "71dab5b81cf06f3495aeebcc70fb351109e5e88bc85da67d0d144c0e5ca0bd6a",
* }]
* }
*
* @apiUse TokenError
*/
async listCounterOffersByAddress (ctx) {
try {
const addr = ctx.params.addr
const counterOffers = await this.useCases.offer.listCounterOffersByAddress(addr)
ctx.body = { counterOffers }
} catch (err) {
console.log('Error in listCounterOffersByAddress REST API handler: ', err)
this.handleError(ctx, err)
}
}
// DRY error handler // DRY error handler
handleError (ctx, err) { handleError (ctx, err) {
console.log('err', err.message) console.log('err', err.message)
+2
View File
@@ -55,10 +55,12 @@ class OfferRouter {
// Define the routes and attach the controller. // Define the routes and attach the controller.
// this.router.post('/', _this.offerRESTController.createOffer) // Deprecated. // this.router.post('/', _this.offerRESTController.createOffer) // Deprecated.
this.router.post('/take', this.offerRESTController.takeOffer) this.router.post('/take', this.offerRESTController.takeOffer)
this.router.post('/mutable/sync/', this.offerRESTController.syncOfferMutableData)
this.router.get('/list/all/:page', this.offerRESTController.listOffers) this.router.get('/list/all/:page', this.offerRESTController.listOffers)
this.router.get('/list/nft/:page', this.offerRESTController.listNftOffers) this.router.get('/list/nft/:page', this.offerRESTController.listNftOffers)
this.router.get('/list/fungible/:page', this.offerRESTController.listFungibleOffers) this.router.get('/list/fungible/:page', this.offerRESTController.listFungibleOffers)
this.router.get('/list/addr/:addr', this.offerRESTController.listOffersByAddress) this.router.get('/list/addr/:addr', this.offerRESTController.listOffersByAddress)
this.router.get('/list/counter-offer/:addr', this.offerRESTController.listCounterOffersByAddress)
// Attach the Controller routes to the Koa app. // Attach the Controller routes to the Koa app.
app.use(this.router.routes()) app.use(this.router.routes())
+106 -1
View File
@@ -29,7 +29,28 @@ class OrderRESTControllerLib {
_this = this _this = this
} }
// No api-doc documentation because this wont be a public endpoint /**
* @api {post} /order Create an order
* @apiPermission User
* @apiName CreateOrder
* @apiGroup REST Order
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{"messageType": "1", "messageClass": "1", "tokenId": "1234567890", "buyOrSell": "buy", "rateInBaseUnit": "100000000", "minUnitsToExchange": "100000000", "numTokens": "1", "makerAddr": "1234567890"}' localhost:5001/order
*
* @apiSuccess {String} eventId Event id
* @apiSuccess {String} noteId Note id
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "eventId": "1234567890",
* "noteId": "1234567890"
* }
*
* @apiUse TokenError
*/
async createOrder (ctx) { async createOrder (ctx) {
try { try {
// console.log('body: ', ctx.request.body) // console.log('body: ', ctx.request.body)
@@ -49,6 +70,68 @@ class OrderRESTControllerLib {
_this.handleError(ctx, err) _this.handleError(ctx, err)
} }
} }
/**
* @api {get} /order/list/all/:page List all orders
* @apiPermission user
* @apiName ListOrders
* @apiGroup REST Order
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5000/order/list/all/0
*
* @apiSuccess {Object[]} orders Array of order objects
* @apiSuccess {ObjectId} orders._id Order id
* @apiSuccess {String} orders.tokenId Token id
* @apiSuccess {String} orders.ticker Ticker
* @apiSuccess {String} orders.dataType Data type
* @apiSuccess {String} orders.utxoTxid Utxo transaction id
* @apiSuccess {Number} orders.utxoVout Utxo output index
* @apiSuccess {String} orders.buyOrSell Buy or sell
* @apiSuccess {Number} orders.numTokens Number of tokens
* @apiSuccess {String} orders.rateInBaseUnit Rate in base unit
* @apiSuccess {String} orders.minUnitsToExchange Minimum units to exchange
* @apiSuccess {String} orders.offerStatus Offer status
* @apiSuccess {String} orders.makerAddr Maker address
* @apiSuccess {Number} orders.timestamp Timestamp
* @apiSuccess {String} orders.displayCategory Display category
* @apiSuccess {Boolean} orders.nsfw Nsfw
* @apiSuccess {Array} orders.flags Flags
* @apiSuccess {Number} orders.messageType Message type
* @apiSuccess {Number} orders.messageClass Message class
* @apiSuccess {String} orders.nostrEventId Nostr event id
* @apiSuccess {String} orders.operatorAddress Operator address
* @apiSuccess {Number} orders.operatorPercentage Operator percentage
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "orders": [{
* "_id": "56bd1da600a526986cf65c80",
* "tokenId": "1234567890",
* "ticker": "BTC",
* "dataType": "offer",
* "utxoTxid": "1234567890",
* "utxoVout": 0,
* "buyOrSell": "buy",
* "numTokens": 1,
* "rateInBaseUnit": "100000000",
* "minUnitsToExchange": "100000000",
* "offerStatus": "active",
* "makerAddr": "1234567890",
* "timestamp": 1716883200,
* "displayCategory": "nft",
* "nsfw": false,
* "flags": [],
* "messageType": 1,
* "messageClass": 1,
* "nostrEventId": "1234567890",
* "operatorAddress": "1234567890",
* "operatorPercentage": 100
* }]
* }
*
* @apiUse TokenError
*/
// curl -X GET http://localhost:5700/order/list/all/0 // curl -X GET http://localhost:5700/order/list/all/0
async listOrders (ctx) { async listOrders (ctx) {
@@ -67,6 +150,28 @@ class OrderRESTControllerLib {
// Delete an existing order by returning the token to the root address of // Delete an existing order by returning the token to the root address of
// the DEX wallet. // the DEX wallet.
/**
* @api {post} /order/delete Delete an order
* @apiPermission user
* @apiName DeleteOrder
* @apiGroup REST Order
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "nostrEventId": "1234567890" }' localhost:5000/order/delete
*
* @apiSuccess {String} txid Transaction id
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "txid": "1234567890"
* }
*
* @apiUse TokenError
*/
// curl -X POST http://localhost:5700/order/delete -d '{ "nostrEventId": "1234567890" }'
async deleteOrder (ctx) { async deleteOrder (ctx) {
try { try {
// console.log('body: ', ctx.request.body) // console.log('body: ', ctx.request.body)
+6 -1
View File
@@ -54,7 +54,7 @@ class OrderRouter {
// Define the routes and attach the controller. // Define the routes and attach the controller.
this.router.post('/', this.createOrder) this.router.post('/', this.createOrder)
this.router.get('/list/all/:page', _this.orderRESTController.listOrders) this.router.get('/list/all/:page', _this.orderRESTController.listOrders)
this.router.post('/delete', _this.orderRESTController.deleteOrder) this.router.post('/delete', this.deleteOrder)
// Attach the Controller routes to the Koa app. // Attach the Controller routes to the Koa app.
app.use(_this.router.routes()) app.use(_this.router.routes())
@@ -65,6 +65,11 @@ class OrderRouter {
await _this.validators.ensureUser(ctx, next) await _this.validators.ensureUser(ctx, next)
await _this.orderRESTController.createOrder(ctx, next) await _this.orderRESTController.createOrder(ctx, next)
} }
async deleteOrder (ctx, next) {
await _this.validators.ensureUser(ctx, next)
await _this.orderRESTController.deleteOrder(ctx, next)
}
} }
export default OrderRouter export default OrderRouter
+20 -1
View File
@@ -31,7 +31,25 @@ class P2WDBRESTControllerLib {
_this = this _this = this
} }
// No api-doc documentation because this wont be a public endpoint /**
* @api {post} /p2wdb Route a P2WDB webhook
* @apiPermission public
* @apiName RouteWebhook
* @apiGroup REST P2WDB
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{"data": {"dataType": "offer"} } ' localhost:5001/p2wdb
*
* @apiSuccess {String} success Success message
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "success": true
* }
*/
// Note: verify if this is a private endpoint, needs to be authenticated
async routeWebhook (ctx) { async routeWebhook (ctx) {
try { try {
console.log('p2wdb REST API handler: body: ', ctx.request.body) console.log('p2wdb REST API handler: body: ', ctx.request.body)
@@ -64,6 +82,7 @@ class P2WDBRESTControllerLib {
await _this.useCases.offer.flagOffer(flagData) await _this.useCases.offer.flagOffer(flagData)
} else { } else {
console.log('Could not route P2WDB webhook data.') console.log('Could not route P2WDB webhook data.')
// NOTE: Should we throw an error here?
} }
// const orderObj = ctx.request.body.order // const orderObj = ctx.request.body.order
@@ -12,13 +12,13 @@ class SmAccountRESTControllerLib {
this.adapters = localConfig.adapters this.adapters = localConfig.adapters
if (!this.adapters) { if (!this.adapters) {
throw new Error( throw new Error(
'Instance of Adapters library required when instantiating /order REST Controller.' 'Instance of Adapters library required when instantiating /sm REST Controller.'
) )
} }
this.useCases = localConfig.useCases this.useCases = localConfig.useCases
if (!this.useCases) { if (!this.useCases) {
throw new Error( throw new Error(
'Instance of Use Cases library required when instantiating /order REST Controller.' 'Instance of Use Cases library required when instantiating /sm REST Controller.'
) )
} }
@@ -40,7 +40,6 @@ class SmAccountRouter {
const baseUrl = '/sm' const baseUrl = '/sm'
this.router = new Router({ prefix: baseUrl }) this.router = new Router({ prefix: baseUrl })
this.listAccounts = this.listAccounts.bind(this)
_this = this _this = this
} }
@@ -60,11 +59,6 @@ class SmAccountRouter {
app.use(_this.router.routes()) app.use(_this.router.routes())
app.use(_this.router.allowedMethods()) app.use(_this.router.allowedMethods())
} }
async listAccounts (ctx, next) {
// await _this.validators.ensureUser(ctx, next)
await _this.smAccountRESTController.listAccounts(ctx, next)
}
} }
export default SmAccountRouter export default SmAccountRouter
+22 -1
View File
@@ -28,6 +28,7 @@ class TimerControllers {
this.backupUsageInterval = 60000 * 10 // 10 minutes this.backupUsageInterval = 60000 * 10 // 10 minutes
this.newSmAccountsInterval = 60000 * 60 // 60 minutes this.newSmAccountsInterval = 60000 * 60 // 60 minutes
this.updateSmAccountsInterval = 60000 * 10 // 10 minutes this.updateSmAccountsInterval = 60000 * 10 // 10 minutes
this.removeOlderDeletedChatsAndPostsInterval = 60000 * 60 * 1 // 1 hour
// Encapsulate dependencies // Encapsulate dependencies
this.config = config this.config = config
@@ -42,7 +43,7 @@ class TimerControllers {
this.backupUsage = this.backupUsage.bind(this) this.backupUsage = this.backupUsage.bind(this)
this.newSmAccounts = this.newSmAccounts.bind(this) this.newSmAccounts = this.newSmAccounts.bind(this)
this.updateSmAccounts = this.updateSmAccounts.bind(this) this.updateSmAccounts = this.updateSmAccounts.bind(this)
this.removeOlderDeletedChatsAndPosts = this.removeOlderDeletedChatsAndPosts.bind(this)
// State // State
this.gcOrdersInt = null this.gcOrdersInt = null
this.gcOffersInt = null this.gcOffersInt = null
@@ -50,6 +51,7 @@ class TimerControllers {
this.loadOffersInt = null this.loadOffersInt = null
this.newSmAccountsInt = null this.newSmAccountsInt = null
this.updateSmAccountsInt = null this.updateSmAccountsInt = null
this.removeOlderDeletedChatsAndPostsInt = null
} }
// Start all the time-based controllers. // Start all the time-based controllers.
@@ -62,6 +64,7 @@ class TimerControllers {
this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval) this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval)
this.newSmAccountsInt = setInterval(this.newSmAccounts, this.newSmAccountsInterval) this.newSmAccountsInt = setInterval(this.newSmAccounts, this.newSmAccountsInterval)
this.updateSmAccountsInt = setInterval(this.updateSmAccounts, this.updateSmAccountsInterval) this.updateSmAccountsInt = setInterval(this.updateSmAccounts, this.updateSmAccountsInterval)
this.removeOlderDeletedChatsAndPostsInt = setInterval(this.removeOlderDeletedChatsAndPosts, this.removeOlderDeletedChatsAndPostsInterval)
return true return true
} }
@@ -74,6 +77,7 @@ class TimerControllers {
clearInterval(this.backupUsageHandle) clearInterval(this.backupUsageHandle)
clearInterval(this.newSmAccountsInt) clearInterval(this.newSmAccountsInt)
clearInterval(this.updateSmAccountsInt) clearInterval(this.updateSmAccountsInt)
clearInterval(this.removeOlderDeletedChatsAndPostsInt)
} }
// Garbage Collect the Orders. // Garbage Collect the Orders.
@@ -105,6 +109,7 @@ class TimerControllers {
console.error('Error in time-controller.js/cleanUsage(): ', err) console.error('Error in time-controller.js/cleanUsage(): ', err)
this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval) this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval)
return false
} }
} }
@@ -193,6 +198,22 @@ class TimerControllers {
return false return false
} }
} }
// Remove older deleted chats and posts.
async removeOlderDeletedChatsAndPosts () {
try {
// console.log('removeOlderDeletedChatsAndPosts() Timer Controller executing at ', new Date().toLocaleString())
clearInterval(this.removeOlderDeletedChatsAndPostsInt)
await this.useCases.nostr.removeOlderDeletedChats()
await this.useCases.nostr.removeOlderDeletedPosts()
this.removeOlderDeletedChatsAndPostsInt = setInterval(this.removeOlderDeletedChatsAndPosts, this.removeOlderDeletedChatsAndPostsInterval)
return true
} catch (err) {
console.error('Error in time-controller.js/removeOlderDeletedChatsAndPosts(): ', err)
this.removeOlderDeletedChatsAndPostsInt = setInterval(this.removeOlderDeletedChatsAndPosts, this.removeOlderDeletedChatsAndPostsInterval)
return false
}
}
} }
export default TimerControllers export default TimerControllers
+68
View File
@@ -0,0 +1,68 @@
/*
Counter Offer Entity
*/
class CounterOfferEntity {
validate (offerData = {}) {
console.log('counter offer data input validation ', offerData)
// Throw an error if input object does not have a data property
if (!offerData.data) {
throw new Error(
'Input to counterOffer.validate() must be an object with a data property.'
)
}
const {
nostrEventId,
takerAddr,
takerNpub,
makerNpub,
counterOfferAddr,
counterOfferUtxo,
takerOfferUtxo,
tokenId
} = offerData.data
// Input Validation
if (!nostrEventId || typeof nostrEventId !== 'string') {
throw new Error("Property 'nostrEventId' must be a string.")
}
// Next validations are optional, to allow old counter offers without the data.
if (takerAddr && typeof takerAddr !== 'string') {
throw new Error("Property 'takerAddr' must be a string.")
}
if (takerNpub && typeof takerNpub !== 'string') {
throw new Error("Property 'takerNpub' must be a string.")
}
if (counterOfferAddr && typeof counterOfferAddr !== 'string') {
throw new Error("Property 'counterOfferAddr' must be a string.")
}
if (counterOfferUtxo && typeof counterOfferUtxo !== 'string') {
throw new Error("Property 'counterOfferUtxo' must be a string.")
}
if (takerOfferUtxo && typeof takerOfferUtxo !== 'string') {
throw new Error("Property 'takerOfferUtxo' must be a string.")
}
if (tokenId && typeof tokenId !== 'string') {
throw new Error("Property 'tokenId' must be a string.")
}
if (makerNpub && typeof makerNpub !== 'string') {
throw new Error("Property 'makerNpub' must be a string.")
}
const validatedCounterOfferData = {
nostrEventId,
takerAddr,
takerNpub,
counterOfferAddr,
counterOfferUtxo,
takerOfferUtxo,
tokenId,
makerNpub
}
return validatedCounterOfferData
}
}
export default CounterOfferEntity
+17
View File
@@ -0,0 +1,17 @@
/*
DeletedChat Entity
*/
class DeletedChat {
validate (data = {}) {
const { eventId } = data
// Input Validation
if (!eventId || typeof eventId !== 'string') {
throw new Error("Property 'eventId' must be a string!")
}
return data
}
}
export default DeletedChat
+17
View File
@@ -0,0 +1,17 @@
/*
DeletedPost Entity
*/
class DeletedPost {
validate (data = {}) {
const { eventId } = data
// Input Validation
if (!eventId || typeof eventId !== 'string') {
throw new Error("Property 'eventId' must be a string!")
}
return data
}
}
export default DeletedPost
+3
View File
@@ -1,6 +1,9 @@
/* /*
Entry Entity Entry Entity
TODO: Explain what an entry is and how it works.
*/ */
class Entry { class Entry {
validate ({ validate ({
entry, entry,
+19 -3
View File
@@ -1,6 +1,6 @@
/* /*
Offer Entity Offer Entity
An ffer is created when a new Signal is detected via the P2WDB webhook. An offer is created when a new Signal is detected via the P2WDB webhook.
It's destroyed when the UTXO described in the Signal has been detected as spent. It's destroyed when the UTXO described in the Signal has been detected as spent.
*/ */
@@ -31,9 +31,12 @@ class OfferEntity {
utxoVout, utxoVout,
offerStatus, offerStatus,
makerAddr, makerAddr,
makerNpub,
ticker, ticker,
tokenType, tokenType,
nostrEventId nostrEventId,
operatorAddress,
operatorPercentage
} = offerData.data } = offerData.data
// Input Validation // Input Validation
@@ -80,6 +83,18 @@ class OfferEntity {
throw new Error("Property 'nostrEventId' must be a string.") throw new Error("Property 'nostrEventId' must be a string.")
} }
// Add the operator address and percentage to the offer.
if (!operatorAddress || typeof operatorAddress !== 'string') {
throw new Error("Property 'operatorAddress' must be a string.")
}
if (!operatorPercentage || typeof operatorPercentage !== 'number') {
throw new Error("Property 'operatorPercentage' must be a number.")
}
// Validate npub type if it exist
if (makerNpub && typeof makerNpub !== 'string') {
throw new Error("Property 'makerNpub' must be a string.")
}
// Convert the timestamp to a number. // Convert the timestamp to a number.
let timestamp = new Date(offerData.timestamp) let timestamp = new Date(offerData.timestamp)
timestamp = timestamp.getTime() timestamp = timestamp.getTime()
@@ -103,7 +118,8 @@ class OfferEntity {
makerAddr, makerAddr,
ticker, ticker,
tokenType, tokenType,
nostrEventId nostrEventId,
makerNpub
} }
// console.log('offer entity validatedOfferData: ', validatedOfferData) // console.log('offer entity validatedOfferData: ', validatedOfferData)
+7 -1
View File
@@ -1,3 +1,8 @@
/*
Entry use-case library.
TODO: Explain what an entry is and how it works.
*/
// local libraries // local libraries
import wlogger from '../adapters/wlogger.js' import wlogger from '../adapters/wlogger.js'
@@ -30,11 +35,12 @@ class EntryLib {
if (!isValidSignature) { if (!isValidSignature) {
throw new Error('Invalid signature') throw new Error('Invalid signature')
} }
// Verify psf tokens balance // Verify psf tokens balance
// const psfBalance = await this.bch.getPSFTokenBalance( // const psfBalance = await this.bch.getPSFTokenBalance(
// entryEntity.slpAddress // entryEntity.slpAddress
// ) // )
// NOTE: this.bch.getPSFTokenBalance , does not exist , should be created?
const psfBalance = 1 const psfBalance = 1
if (psfBalance < 10) { if (psfBalance < 10) {
+2
View File
@@ -11,6 +11,7 @@ import OfferUseCases from './offer/index.js'
import OrderUseCases from './order.js' import OrderUseCases from './order.js'
import { UsageUseCases } from './usage-use-cases.js' import { UsageUseCases } from './usage-use-cases.js'
import SmAccountUseCases from './smAccount-use-cases.js' import SmAccountUseCases from './smAccount-use-cases.js'
import NostrUseCases from './nostr-use-cases.js'
class UseCases { class UseCases {
constructor (localConfig = {}) { constructor (localConfig = {}) {
@@ -29,6 +30,7 @@ class UseCases {
this.offer = new OfferUseCases(localConfig) this.offer = new OfferUseCases(localConfig)
this.usage = new UsageUseCases(localConfig) this.usage = new UsageUseCases(localConfig)
this.smAccount = new SmAccountUseCases(localConfig) this.smAccount = new SmAccountUseCases(localConfig)
this.nostr = new NostrUseCases(localConfig)
} }
// Run any startup Use Cases at the start of the app. // Run any startup Use Cases at the start of the app.
+130
View File
@@ -0,0 +1,130 @@
/*
This library contains business-logic for dealing with deleted chats. Most of these
functions are called by the /nostr REST API endpoints.
*/
import wlogger from '../adapters/wlogger.js'
import DeletedChatEntity from '../entities/deletedChat.js'
import DeletedPostEntity from '../entities/deletedPost.js'
class NostrUseCases {
constructor (localConfig = {}) {
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of adapters must be passed in when instantiating Nostr Use Cases library.'
)
}
// Encapsulate dependencies
this.DeletedChatEntity = new DeletedChatEntity()
this.DeletedPostEntity = new DeletedPostEntity()
this.DeletedChatModel = this.adapters.localdb.DeletedChat
this.DeletedPostModel = this.adapters.localdb.DeletedPost
this.createDeletedChat = this.createDeletedChat.bind(this)
this.getDeletedChats = this.getDeletedChats.bind(this)
this.createDeletedPost = this.createDeletedPost.bind(this)
this.getDeletedPosts = this.getDeletedPosts.bind(this)
this.removeOlderDeletedChats = this.removeOlderDeletedChats.bind(this)
this.removeOlderDeletedPosts = this.removeOlderDeletedPosts.bind(this)
}
// Create a new deleted chat model and add it to the Mongo database.
async createDeletedChat (deletedChatObj) {
try {
// Input Validation
const deletedChatEntity = this.DeletedChatEntity.validate(deletedChatObj)
const deletedChat = new this.DeletedChatModel(deletedChatEntity)
// Save the new model to the database.
await deletedChat.save()
return deletedChat
} catch (err) {
// console.log('createDeletedChat() error: ', err)
wlogger.error('Error in lib/nostr.js/createDeletedChat()')
throw err
}
}
// Returns an array of all deleted chats models in the Mongo database.
async getDeletedChats () {
try {
// Get deleted chats models.
const deletedChats = await this.DeletedChatModel.find({})
return deletedChats
} catch (err) {
wlogger.error('Error in lib/users.js/getAllDeletedChat()')
throw err
}
}
// Create a new deleted post model and add it to the Mongo database.
async createDeletedPost (deletedPostObj) {
try {
// Input Validation
const deletedPostEntity = this.DeletedPostEntity.validate(deletedPostObj)
const deletedPost = new this.DeletedPostModel(deletedPostEntity)
// Save the new model to the database.
await deletedPost.save()
return deletedPost
} catch (err) {
wlogger.error('Error in lib/users.js/createDeletedPost()')
throw err
}
}
// Returns an array of all deleted posts models in the Mongo database.
async getDeletedPosts () {
try {
// Get deleted posts models.
const deletedPosts = await this.DeletedPostModel.find({})
return deletedPosts
} catch (err) {
wlogger.error('Error in lib/users.js/getDeletedPosts()')
throw err
}
}
// Remove deleted chats that are older than 3 months.
async removeOlderDeletedChats () {
try {
const deletedChats = await this.DeletedChatModel.find({})
const threeMonthsAgo = new Date()
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3)
for (let i = 0; i < deletedChats.length; i++) {
const deletedChat = deletedChats[i]
if (deletedChat.createdAt.getTime() < threeMonthsAgo.getTime()) {
await deletedChat.remove()
}
}
} catch (err) {
wlogger.error('Error in lib/users.js/removeOlderDeletedChats()')
throw err
}
}
// Remove deleted posts that are older than 3 months.
async removeOlderDeletedPosts () {
try {
const deletedPosts = await this.DeletedPostModel.find({})
const threeMonthsAgo = new Date()
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3)
for (let i = 0; i < deletedPosts.length; i++) {
const deletedPost = deletedPosts[i]
if (deletedPost.createdAt.getTime() < threeMonthsAgo.getTime()) {
await deletedPost.remove()
}
}
} catch (err) {
wlogger.error('Error in lib/users.js/removeOlderDeletedPosts()')
throw err
}
}
}
export default NostrUseCases
+253 -20
View File
@@ -19,6 +19,7 @@ import RetryQueue from '@chris.troutner/retry-queue'
// Local libraries // Local libraries
import OfferEntity from '../../entities/offer.js' import OfferEntity from '../../entities/offer.js'
import CounterOfferEntity from '../../entities/counterOffer.js'
import config from '../../../config/index.js' import config from '../../../config/index.js'
const DEFAULT_ENTRIES_PER_PAGE = 20 const DEFAULT_ENTRIES_PER_PAGE = 20
@@ -45,7 +46,9 @@ class OfferUseCases {
this.config = config this.config = config
this.axios = axios this.axios = axios
this.offerEntity = new OfferEntity() this.offerEntity = new OfferEntity()
this.counterOfferEntity = new CounterOfferEntity()
this.OfferModel = this.adapters.localdb.Offer this.OfferModel = this.adapters.localdb.Offer
this.CounterOfferModel = this.adapters.localdb.CounterOffer
this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 }) this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 })
// Bind 'this' object to functions // Bind 'this' object to functions
@@ -65,6 +68,8 @@ class OfferUseCases {
this.flagOffer = this.flagOffer.bind(this) this.flagOffer = this.flagOffer.bind(this)
this.loadOffers = this.loadOffers.bind(this) this.loadOffers = this.loadOffers.bind(this)
this.listOffersByAddress = this.listOffersByAddress.bind(this) this.listOffersByAddress = this.listOffersByAddress.bind(this)
this.syncOfferMutableData = this.syncOfferMutableData.bind(this)
this.listCounterOffersByAddress = this.listCounterOffersByAddress.bind(this)
// State // State
this.seenOffers = [] this.seenOffers = []
@@ -111,7 +116,10 @@ class OfferUseCases {
const utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo) const utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
// console.log('utxoStatus: ', utxoStatus) // console.log('utxoStatus: ', utxoStatus)
// if (utxoStatus === null) return false // if (utxoStatus === null) return false
if (!utxoStatus) return false if (!utxoStatus) {
console.log(`UTXO txid: ${offerObj.data.utxoTxid}, vout: ${offerObj.data.utxoVout} has been spent. Skipping.`)
return false
}
// A new offer gets a status of 'posted' // A new offer gets a status of 'posted'
offerObj.data.offerStatus = 'posted' offerObj.data.offerStatus = 'posted'
@@ -126,20 +134,58 @@ class OfferUseCases {
// Get data about the token. // Get data about the token.
const tokenId = offerEntity.tokenId const tokenId = offerEntity.tokenId
// const tokenData = await this.adapters.wallet.bchWallet.getTokenData(tokenId) let tokenData = null
const tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTokenData, tokenId) try {
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`) tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTokenData, tokenId)
} catch (err) {
console.error('Error in OfferUseCases/createOffer() getting token data: ', err.message)
}
// Generate a 'display category' for the token. This will allow the // Do additional data analysis if the token data was successfully retrieved.
// front end UI to figure out how to display the token. if (tokenData) {
const displayCategory = this.categorizeToken(offerEntity, tokenData) // Store the mutable and immutable data cids.
console.log('displayCategory: ', displayCategory) const mutableDataCid = tokenData.mutableData
offerEntity.displayCategory = displayCategory const immutableDataCid = tokenData.immutableData
offerEntity.mutableDataCid = mutableDataCid
offerEntity.immutableDataCid = immutableDataCid
offerEntity.tokenData = tokenData
// Detect if user set the NSFW flag. // Get the mutable data from the cid if it exists.
// const nsfw = false if (mutableDataCid && typeof mutableDataCid === 'string') {
// nsfw = await this.retryQueue.addToQueue(this.detectNsfw, tokenData) let mutableData = null
// offerEntity.nsfw = nsfw 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. // Add offer to the local database.
const offerModel = new this.OfferModel(offerEntity) const offerModel = new this.OfferModel(offerEntity)
@@ -147,7 +193,7 @@ class OfferUseCases {
return true return true
} catch (err) { } catch (err) {
console.error('Error in createOffer()', err.message) console.error('\n\nError in createOffer()', err.message, '\n\n', err)
throw err throw err
} }
} }
@@ -264,7 +310,7 @@ class OfferUseCases {
} }
} }
async listNftOffers (page = 0, nsfw = false) { /* async listNftOffers (page = 0, nsfw = false) {
try { try {
const data = await this.OfferModel.find({ const data = await this.OfferModel.find({
displayCategory: { $ne: 'fungible' }, displayCategory: { $ne: 'fungible' },
@@ -284,6 +330,42 @@ class OfferUseCases {
console.error('Error in use-cases/offer/listNftOffers()') console.error('Error in use-cases/offer/listNftOffers()')
throw error throw error
} }
} */
async listNftOffers (page = 0, nsfw = false) {
try {
const query = {
displayCategory: { $ne: 'fungible' },
nsfw
}
// Total query offers
const totalOffers = await this.OfferModel.countDocuments(query)
// Total pages
const totalPages = Math.ceil(totalOffers / NFT_ENTRIES_PER_PAGE)
const data = await this.OfferModel.find(query)
// Sort entries so newest entries show first.
.sort('-timestamp')
// Skip to the start of the selected page.
.skip(page * NFT_ENTRIES_PER_PAGE)
// Only return 20 results.
.limit(NFT_ENTRIES_PER_PAGE)
// console.log('listNftOffers() returning this data: ', data)
return {
data,
pagination: {
currentPage: page,
totalPages,
totalOffers,
pageSize: NFT_ENTRIES_PER_PAGE
}
}
} catch (error) {
console.error('Error in use-cases/offer/listNftOffers()')
throw error
}
} }
async listFungibleOffers (page = 0) { async listFungibleOffers (page = 0) {
@@ -513,21 +595,35 @@ class OfferUseCases {
// } // }
} }
// This function is called by the P2WDB webhook REST API handler. When a // This function is called by loadOffers().
// Counter Offer is passed to bch-dex by the P2WDB, the data is then passed // It does due diligence on the Counter Offer, then signs
// to this function. It does due diligence on the Counter Offer, then signs
// and broadcasts the transaction to accept the Counter Offer. // and broadcasts the transaction to accept the Counter Offer.
async acceptCounterOffer (offerData) { async acceptCounterOffer (offerData) {
try { try {
console.log('offerData', offerData)
// console.log(`acceptCounterOffer() offerData: ${JSON.stringify(offerData, null, 2)}`) // console.log(`acceptCounterOffer() offerData: ${JSON.stringify(offerData, null, 2)}`)
// Quickly skip over offers that have already been processed. // Quickly skip over offers that have already been processed.
const eventId = offerData.data.nostrEventId const eventId = offerData.data.nostrEventId
// Create counter offer entity for nostr event id if it not exist
const existingCounterOffer = await this.CounterOfferModel.findOne(({ nostrEventId: eventId }))
if (!existingCounterOffer) {
// Add taker offer utxo to counter offer data.
offerData.data.takerOfferUtxo = offerData.data.utxoTxid
const counterOEntity = this.counterOfferEntity.validate(offerData)
// Create new counter offfer model and save it.
const counterOffer = new this.CounterOfferModel(counterOEntity)
await counterOffer.save()
}
if (this.seenOffers.includes(eventId)) { if (this.seenOffers.includes(eventId)) {
// console.log(`Offer with event ID ${eventId} already processed. Skipping.`) // console.log(`Offer with event ID ${eventId} already processed. Skipping.`)
return false return false
} }
console.log(`New counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerData.data.nostrEventId)}`)
// See if this instance of bch-dex is managing the Order associated with // See if this instance of bch-dex is managing the Order associated with
// the incoming Counter Offer. // the incoming Counter Offer.
@@ -594,20 +690,57 @@ class OfferUseCases {
throw new Error('Could not calculate the amount of BCH offered in the Counter Offer') throw new Error('Could not calculate the amount of BCH offered in the Counter Offer')
} }
const satsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[2].value) const satsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[2].value)
const hasRequiredAmount = satsOut === satsToReceive const hasRequiredAmount = satsOut === satsToReceive
if (!hasRequiredAmount) { if (!hasRequiredAmount) {
throw new Error(`The Counter Offer has an output of ${satsOut}, which does not match the required ${satsToReceive} in the Offer.`) throw new Error(`The Counter Offer has an output of ${satsOut}, which does not match the required ${satsToReceive} in the Offer.`)
} }
// Ensure the Counter Offer has an output for the Operator of bch-dex.
if (!txObj.vout[3]) {
console.log('The Counter Offer does not have an output for the Operator.')
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
this.seenOffers.push(eventId)
return 'N/A'
}
// Ensure the 3rd output (vout=2) is going to the maker address specified // Ensure the 3rd output (vout=2) is going to the maker address specified
// in the Offer. // in the Offer.
const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0] const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0]
const makerAddr = orderData.makerAddr const makerAddr = orderData.makerAddr
const hasCorrectAddr = makerAddr === addrInCounterOffer const hasCorrectAddr = makerAddr === addrInCounterOffer
if (!hasCorrectAddr) { if (!hasCorrectAddr) {
throw new Error(`The Counter Offer has an output address of ${addrInCounterOffer}, which does not match the Maker address of ${makerAddr} in the Offer.`) throw new Error(`The Counter Offer has an output address of ${addrInCounterOffer}, which does not match the Maker address of ${makerAddr} in the Offer.`)
} }
// Ensure the 4th output (vout=3) is going to the operator address specified in the Offer.
// NOTE: This is the operator of the bch-dex intance, NOT the seller of the token.
const operatorAddr = orderData.operatorAddress
const hasCorrectOperatorAddr = operatorAddr === txObj.vout[3].scriptPubKey.addresses[0]
if (!hasCorrectOperatorAddr) {
throw new Error(`The Counter Offer has an output address of ${txObj.vout[3].scriptPubKey.addresses[0]}, which does not match the Operator address of ${operatorAddr} in the Offer.`)
}
// Ensure the 4th output (vout=3) contains the required amount of BCH.
const operatorSatsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[3].value)
let estimatedOperatorFee = Math.floor(txObj.vout[3].value * orderData.operatorPercentage / 100)
if (estimatedOperatorFee < 546) estimatedOperatorFee = 546
console.log('operatorSatsOut: ', operatorSatsOut, 'estimatedOperatorFee: ', estimatedOperatorFee)
if (operatorSatsOut < estimatedOperatorFee) {
console.log(`Skipping: The Counter Offer has an output of ${operatorSatsOut}, which is less than the estimated operator fee of ${estimatedOperatorFee}.`)
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
this.seenOffers.push(eventId)
return 'N/A'
}
// Get the User ID from the Order model. // Get the User ID from the Order model.
const userId = orderData.userId const userId = orderData.userId
@@ -718,7 +851,8 @@ class OfferUseCases {
await thisOffer.remove() await thisOffer.remove()
} }
// If the Offer is older than 7 days, delete it. // TODO: Instead of deleting the offer, send the token back to the Makers wallet.
// If the Offer is older than a threshold, delete it.
const nowMs = now.getTime() const nowMs = now.getTime()
const eightWeeks = 1000 * 60 * 60 * 24 * 7 * 8 const eightWeeks = 1000 * 60 * 60 * 24 * 7 * 8
const eightWeeksAgo = nowMs - eightWeeks const eightWeeksAgo = nowMs - eightWeeks
@@ -795,7 +929,7 @@ class OfferUseCases {
if (offerObj.data.dataType === 'counter-offer') { if (offerObj.data.dataType === 'counter-offer') {
// console.log('Counter offer detected: ', offerObj) // console.log('Counter offer detected: ', offerObj)
// console.log('Counter offer detected: ', offerObj.data.nostrEventId) // 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) await this.acceptCounterOffer(offerObj)
} }
@@ -820,6 +954,105 @@ class OfferUseCases {
throw err throw err
} }
} }
// update offer model mutable data
async syncOfferMutableData (tokenId) {
try {
// validate input
if (!tokenId || typeof tokenId !== 'string') {
throw new Error('tokenId must be a string!')
}
// validate existing offer with the associated tokenId
const offer = await this.OfferModel.findOne({ tokenId })
if (!offer) throw new Error('Associated offer not found!')
// Verify last update timestamp. This prevents users from spamming the API with requests.
// It only updates the mutable data if it has been more than 5 minutes since the last update.
const lastUpdateTs = Number(offer.lastUpdatedTokenData)
const now = new Date().getTime()
const period = 5
if (lastUpdateTs) {
// add 5 minutes to the last update
const lastUpdate = new Date(lastUpdateTs)
console.log('lastUpdate', lastUpdate)
lastUpdate.setMinutes(lastUpdate.getMinutes() + period)
console.log(new Date().getMinutes() + ' ' + lastUpdate.getMinutes())
// if now is less than the lastUpdate + 5 minutos , them skip.
if (now < lastUpdate.getTime()) {
console.log('Skipping , token lastUpdate is less than 5 minutes.')
return offer
}
}
let tokenData = null
try {
tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTokenData, tokenId)
} catch (err) {
// Dev Note: If getTokenData() fails, the code below will
console.error('Error in OfferUseCases/createOffer() getting token data: ', err.message)
}
// Do additional data analysis if the token data was successfully retrieved.
if (tokenData) {
// Store the mutable and immutable data cids.
const mutableDataCid = tokenData.mutableData
const immutableDataCid = tokenData.immutableData
offer.mutableDataCid = mutableDataCid
offer.immutableDataCid = immutableDataCid
offer.tokenData = tokenData
// Get the mutable data from the cid if it exists.
if (mutableDataCid && typeof mutableDataCid === 'string') {
let mutableData = null
try {
mutableData = await this.retryQueue.addToQueue(this.adapters.wallet.cid2json, mutableDataCid)
} catch (err) {
console.error('Error in OfferUseCases/createOffer() getting mutable data: ', err.message)
}
console.log('mutableData: ', mutableData)
if (mutableData) {
try {
offer.tokenIconUrl = mutableData.tokenIcon
offer.tokenCategories = mutableData.category
offer.tokenTags = mutableData.tags
offer.userDataStr = JSON.stringify(mutableData.userData)
} catch (error) {
// skip error
}
}
}
// Save update time stamp
offer.lastUpdatedTokenData = new Date().getTime()
}
// Save the updated offer data to the database.
await offer.save()
// Return the updated offer data.
return offer
} catch (err) {
console.error('Error in syncOfferMutableData(): ', err)
throw err
}
}
async listCounterOffersByAddress (takerAddr) {
try {
if (!takerAddr || typeof takerAddr !== 'string') {
throw new Error('takerAddr must be a string')
}
const counterOffers = await this.CounterOfferModel.find({ takerAddr })
return counterOffers
} catch (err) {
console.error('Error in listCounterOffersByAddress(): ', err)
throw err
}
}
} }
export default OfferUseCases export default OfferUseCases
+10 -7
View File
@@ -44,6 +44,7 @@ class OrderLib {
if (!entryObj.tokenId) throw new Error('entry does not contain required properties') if (!entryObj.tokenId) throw new Error('entry does not contain required properties')
// Get the user data from the database, for the user creating the new Order.
const user = await this.UserModel.findById(entryObj.userId) const user = await this.UserModel.findById(entryObj.userId)
if (!user) throw new Error('user not found') if (!user) throw new Error('user not found')
@@ -81,13 +82,7 @@ class OrderLib {
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {}) // await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {})
await userWallet.optimize() await userWallet.optimize()
// Ensure sufficient tokens exist to create the order.
// await this.ensureFunds(orderEntity)
// await this.retryQueue.addToQueue(this.ensureFunds, orderEntity)
// Get Ticker for token ID. // Get Ticker for token ID.
// const tokenData = await this.adapters.wallet.bchWallet.getTxData([entryObj.tokenId])
// const tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTxData, [entryObj.tokenId])
const tokenData = await userWallet.getTxData([entryObj.tokenId]) const tokenData = await userWallet.getTxData([entryObj.tokenId])
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`) // console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
orderEntity.ticker = tokenData[0].tokenTicker orderEntity.ticker = tokenData[0].tokenTicker
@@ -107,15 +102,23 @@ class OrderLib {
// await this.adapters.wallet.bchWallet.getUtxos() // await this.adapters.wallet.bchWallet.getUtxos()
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {}) // await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
await userWallet.initialize() await userWallet.initialize()
// Update the order with the new UTXO information. // Update the order with the new UTXO information.
orderEntity.utxoTxid = utxoInfo.txid orderEntity.utxoTxid = utxoInfo.txid
orderEntity.utxoVout = utxoInfo.vout orderEntity.utxoVout = utxoInfo.vout
orderEntity.tokenType = utxoInfo.tokenType orderEntity.tokenType = utxoInfo.tokenType
// Add P2WDB specific flag for signaling that this is a new offer. // Add flag to signal that this is a new offer.
orderEntity.dataType = 'offer' orderEntity.dataType = 'offer'
orderEntity.userId = user._id orderEntity.userId = user._id
// Add the operator address and percentage to the order.
orderEntity.operatorAddress = this.config.operatorAddress
orderEntity.operatorPercentage = this.config.operatorPercentage
// Convert the Seller's BCH private key to a Nostr npub. This is used to identify the seller in Nostr.
orderEntity.makerNpub = this.adapters.nostr.privKeyToNpub(userWallet.walletInfo.privateKey)
// Post the new Order information to Nostr under the topic set in the // Post the new Order information to Nostr under the topic set in the
// config file. // config file.
const postObj = { const postObj = {
+3 -1
View File
@@ -10,7 +10,7 @@ class smAccountUseCases {
this.adapters = localConfig.adapters this.adapters = localConfig.adapters
if (!this.adapters) { if (!this.adapters) {
throw new Error( throw new Error(
'Instance of adapters must be passed in when instantiating Order Use Cases library.' 'Instance of adapters must be passed in when instantiating SM Account Use Cases library.'
) )
} }
@@ -28,6 +28,7 @@ class smAccountUseCases {
console.log('Checking for new Social Media Accounts...') console.log('Checking for new Social Media Accounts...')
const messages = await this.adapters.nostr.readGlobalFeed() const messages = await this.adapters.nostr.readGlobalFeed()
console.log('messages: ', messages)
for (let i = 0; i < messages.length; i++) { for (let i = 0; i < messages.length; i++) {
const msg = messages[i] const msg = messages[i]
// const { content, id, tags, npub, pubkey } = msg // const { content, id, tags, npub, pubkey } = msg
@@ -122,6 +123,7 @@ class smAccountUseCases {
const followerCnt = followList.length const followerCnt = followList.length
await SmAccount.updateOne({ _id: account._id }, { $set: { followerCnt } }) await SmAccount.updateOne({ _id: account._id }, { $set: { followerCnt } })
} }
return true
} catch (err) { } catch (err) {
console.error('Error in smAccount-use-cases.js/updateSmAccounts(): ', err) console.error('Error in smAccount-use-cases.js/updateSmAccounts(): ', err)
throw err throw err
+188
View File
@@ -0,0 +1,188 @@
import testUtils from '../../utils/test-utils.js'
import { assert } from 'chai'
import config from '../../../config/index.js'
import axios from 'axios'
import sinon from 'sinon'
import util from 'util'
util.inspect.defaultOptions = { depth: 1 }
const LOCALHOST = `http://localhost:${config.port}`
const context = {}
let sandbox
// const mockContext = require('../../unit/mocks/ctx-mock').context
if (!config.noMongo) {
describe('Nostr', () => {
before(async () => {
// console.log(`config: ${JSON.stringify(config, null, 2)}`)
// Create a second test user.
const userObj = {
email: 'test2@test.com',
password: 'pass2',
name: 'test2'
}
const testUser = await testUtils.createUser(userObj)
// console.log(`testUser2: ${JSON.stringify(testUser, null, 2)}`)
context.user2 = testUser.user
context.token2 = testUser.token
context.id2 = testUser.user._id
// Get the JWT used to log in as the admin 'system' user.
const adminJWT = await testUtils.getAdminJWT()
console.log(`adminJWT: ${adminJWT}`)
context.adminJWT = adminJWT
// const admin = await testUtils.loginAdminUser()
// context.adminJWT = admin.token
// const admin = await adminLib.loginAdmin()
// console.log(`admin: ${JSON.stringify(admin, null, 2)}`)
})
beforeEach(() => {
// const useCases = new UseCases({ adapters })
// uut = new UserController({ adapters, useCases })
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
describe('POST /nostr/deletedChat - Create Deleted Chat', () => {
it('should not create deleted chat if the authorization header is missing', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedChat`
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
it('should not create deleted chat if the authorization header is invalid scheme', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedChat`,
headers: {
Authorization: 'Bearer invalid'
}
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
it('should not create deleted chat if the authorization header is invalid token', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedChat`,
headers: {
Authorization: `Bearer ${context.token}`
}
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
it('should not create deleted chat if token is not admin', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedChat`,
headers: {
Authorization: `Bearer ${context.token}`
}
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
})
describe('POST /nostr/deletedPost - Create Deleted Post', () => {
it('should not create deleted post if the authorization header is missing', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedPost`
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
it('should not create deleted post if the authorization header is invalid scheme', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedPost`,
headers: {
Authorization: 'Bearer invalid'
}
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
it('should not create deleted post if the authorization header is invalid token', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedPost`,
headers: {
Authorization: `Bearer ${context.token}`
}
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
it('should not create deleted post if token is not admin', async () => {
try {
const options = {
method: 'POST',
url: `${LOCALHOST}/nostr/deletedPost`,
headers: {
Authorization: `Bearer ${context.token}`
}
}
await axios(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.response.status, 401)
}
})
})
})
}
+29
View File
@@ -224,4 +224,33 @@ describe('#nostr.js', () => {
assert.isArray(result) assert.isArray(result)
}) })
}) })
describe('#privKeyToNpub', () => {
it('should handle error if providedprivKey is not a string', async () => {
try {
uut.privKeyToNpub()
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'privKey must be a string!')
}
})
it('should return npub if privKey is valid', async () => {
const result = uut.privKeyToNpub('L2kh34VinQNRq1ad8nnXnGoFrb9pT7j8GJXiT6Scsg18KMxsUVDr')
// console.log('result: ', result)
assert.isString(result)
assert.include(result, 'npub')
})
it('should handle unexpected error', async () => {
try {
uut.privKeyToNpub('invalid format')
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Invalid base58 character')
}
})
})
}) })
@@ -52,6 +52,7 @@ describe('#OrderPagination', () => {
nostrEventId: 'nostrEventId', nostrEventId: 'nostrEventId',
dataType: 'dataType', dataType: 'dataType',
makerAddr: 'makerAddr', makerAddr: 'makerAddr',
makerNpub: 'makerNpub',
rateInBaseUnit: 'rateInBaseUnit', rateInBaseUnit: 'rateInBaseUnit',
minUnitsToExchange: 'minUnitsToExchange', minUnitsToExchange: 'minUnitsToExchange',
ticker: 'ticker', ticker: 'ticker',
+31
View File
@@ -493,4 +493,35 @@ describe('#wallet', () => {
} }
}) })
}) })
describe('#cid2json', () => {
it('should throw error if cid is not provided', async () => {
try {
await uut.cid2json()
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'urlOrCid must be a string!')
}
})
it('should convert cid to json', async () => {
try {
sandbox.stub(uut.bchWallet, 'cid2json').resolves({ json: offerMockData.mutableDataMock })
const cid = 'bafkreidr6wfd6mcmwpea7abm5uk5rrprc2wfbcvo5wdcmtlolrpjab5oqm'
const result = await uut.cid2json(cid)
assert.isObject(result)
} catch (error) {
assert.fail('Unexpected code path')
}
})
it('should convert ipfs url to json', async () => {
try {
sandbox.stub(uut.bchWallet, 'cid2json').resolves({ json: offerMockData.mutableDataMock })
const url = 'https://ipfs.io/ipfs/bafkreidr6wfd6mcmwpea7abm5uk5rrprc2wfbcvo5wdcmtlolrpjab5oqm'
const result = await uut.cid2json(url)
assert.isObject(result)
} catch (error) {
assert.fail('Unexpected code path')
}
})
})
}) })
@@ -0,0 +1,201 @@
/*
Unit tests for the REST API handler for the /nostr endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
import NostrController from '../../../../../src/controllers/rest-api/nostr/controller.js'
import { context as mockContext } from '../../../../unit/mocks/ctx-mock.js'
let uut
let sandbox
let ctx
describe('#Nostr-REST-Controller', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new NostrController({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
ctx = mockContext()
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new NostrController()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating /nostr REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new NostrController({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating /nostr REST Controller.'
)
}
})
})
describe('#POST /nostr/deletedChat', () => {
it('should return 422 status on biz logic error', async () => {
try {
sandbox.stub(uut.useCases.nostr, 'createDeletedChat').rejects(new Error('test error'))
await uut.createDeletedChat(ctx)
assert.fail('Unexpected result')
} catch (err) {
// console.log(err)
assert.equal(err.status, 422)
assert.include(err.message, 'test error')
}
})
it('should return 200 status on success', async () => {
ctx.request.body = {
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q',
npub: 'npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8',
bchAddr: 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a',
pubkey: '6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1'
}
await uut.createDeletedChat(ctx)
// Assert the expected HTTP response
assert.equal(ctx.status, 200)
assert.isObject(ctx.response.body)
})
})
describe('#GET /nostr/deletedChat', () => {
it('should return 422 status on arbitrary biz logic error', async () => {
try {
// Force an error
sandbox
.stub(uut.useCases.nostr, 'getDeletedChats')
.rejects(new Error('test error'))
await uut.getDeletedChats(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(err.status, 422)
assert.include(err.message, 'test error')
}
})
it('should return 200 status on success', async () => {
await uut.getDeletedChats(ctx)
// Assert the expected HTTP response
assert.equal(ctx.status, 200)
// Assert that expected properties exist in the returned data.
assert.isObject(ctx.response.body)
assert.property(ctx.response.body, 'deletedChats')
assert.isArray(ctx.response.body.deletedChats)
})
})
describe('#POST /nostr/deletedPost', () => {
it('should return 422 status on biz logic error', async () => {
try {
sandbox.stub(uut.useCases.nostr, 'createDeletedPost').rejects(new Error('test error'))
await uut.createDeletedPost(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(err.status, 422)
assert.include(err.message, 'test error')
}
})
it('should return 200 status on success', async () => {
ctx.request.body = {
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q',
npub: 'npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8',
bchAddr: 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a',
pubkey: '6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1'
}
await uut.createDeletedPost(ctx)
assert.equal(ctx.status, 200)
assert.isObject(ctx.response.body)
assert.property(ctx.response.body, 'deletedPost')
assert.isObject(ctx.response.body.deletedPost)
})
})
describe('#GET /nostr/deletedPost', () => {
it('should return 422 status on biz logic error', async () => {
try {
sandbox.stub(uut.useCases.nostr, 'getDeletedPosts').rejects(new Error('test error'))
await uut.getDeletedPosts(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(err.status, 422)
assert.include(err.message, 'test error')
}
})
it('should return 200 status on success', async () => {
await uut.getDeletedPosts(ctx)
// Assert the expected HTTP response
assert.equal(ctx.status, 200)
assert.isObject(ctx.response.body)
assert.property(ctx.response.body, 'deletedPosts')
assert.isArray(ctx.response.body.deletedPosts)
})
})
describe('#handleError', () => {
it('should still throw error if there is no message', () => {
try {
const err = {
status: 404
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'Not Found')
}
})
it('should throw error with message', () => {
try {
const err = {
status: 422,
message: 'test error'
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
})
@@ -0,0 +1,108 @@
/*
Unit tests for the REST API handler for the /nostr endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
// const app = require('../../../mocks/app-mock')
import NostrRouter from '../../../../../src/controllers/rest-api/nostr/index.js'
let uut
let sandbox
// let ctx
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
describe('#Nostr-REST-Router', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new NostrRouter({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
// ctx = mockContext()
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new NostrRouter()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating NostrRouter REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new NostrRouter({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating NostrRouter REST Controller.'
)
}
})
})
describe('#attach', () => {
it('should throw an error if app is not passed in.', () => {
try {
uut.attach()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Must pass app object when attaching REST API controllers.'
)
}
})
})
describe('#createDeletedChat', () => {
it('should route to the createDeletedChat function', async () => {
// Stub functions
const validationSpy = sandbox.stub(uut.validators, 'ensureAdmin').resolves(true)
sandbox.stub(uut.nostrRESTController, 'createDeletedChat').resolves(true)
// Call function
const res = await uut.createDeletedChat()
assert.isTrue(validationSpy.calledOnce, 'Admin validator should be called')
assert.isTrue(res)
})
})
describe('#createDeletedPost', () => {
it('should route to the createDeletedPost function', async () => {
// Stub functions
const validationSpy = sandbox.stub(uut.validators, 'ensureAdmin').resolves(true)
sandbox.stub(uut.nostrRESTController, 'createDeletedPost').resolves(true)
// Call function
const res = await uut.createDeletedPost()
assert.isTrue(validationSpy.calledOnce, 'Admin validator should be called')
assert.isTrue(res)
})
})
})
@@ -91,25 +91,186 @@ describe('#Offer-REST-Router', () => {
// assert.equal(ctx.body.hash, 'testHash') // assert.equal(ctx.body.hash, 'testHash')
}) })
// it('should catch and throw an error', async () => { it('should catch and throw an error', async () => {
// try { try {
// ctx.request.body = { ctx.request.body = {
// offer: {} offer: {}
// } }
//
// // Force an error // Force an error
// sandbox sandbox
// .stub(uut.useCases.offer, 'createOffer') .stub(uut.useCases.offer, 'createOffer')
// .rejects(new Error('test error')) .rejects(new Error('test error'))
//
// await uut.createOffer(ctx) await uut.createOffer(ctx)
//
// assert.fail('Unexpected code path') assert.fail('Unexpected code path')
// } catch (err) { } catch (err) {
// // console.log('err: ', err) // console.log('err: ', err)
// assert.include(err.message, 'test error') assert.include(err.message, 'test error')
// } }
// }) })
})
describe('#listOffers', () => {
it('should list offers', async () => {
ctx.params = { page: 0 }
sandbox.stub(uut.useCases.offer, 'listOffers').resolves([])
await uut.listOffers(ctx)
assert.isArray(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { }
sandbox
.stub(uut.useCases.offer, 'listOffers')
.throws(new Error('test error'))
await uut.listOffers(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#listNftOffers', () => {
it('should list nft offers', async () => {
ctx.params = { page: 0 }
sandbox.stub(uut.useCases.offer, 'listNftOffers').resolves([])
await uut.listNftOffers(ctx)
assert.isArray(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { page: 0 }
sandbox
.stub(uut.useCases.offer, 'listNftOffers')
.throws(new Error('test error'))
await uut.listNftOffers(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#listFungibleOffers', () => {
it('should list fungible offers', async () => {
ctx.params = { page: 0 }
sandbox.stub(uut.useCases.offer, 'listFungibleOffers').resolves([])
await uut.listFungibleOffers(ctx)
assert.isArray(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { page: 0 }
sandbox
.stub(uut.useCases.offer, 'listFungibleOffers')
.throws(new Error('test error'))
await uut.listFungibleOffers(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#takeOffer', () => {
it('should take an offer', async () => {
ctx.request.body = {
nostrEventId: 'testEventId'
}
sandbox.stub(uut.useCases.offer, 'takeOffer').resolves({ eventId: 'testEventId', noteId: 'testNoteId' })
await uut.takeOffer(ctx)
assert.isObject(ctx.body)
assert.property(ctx.body, 'eventId')
assert.property(ctx.body, 'noteId')
})
it('should catch and throw an error', async () => {
try {
ctx.request.body = {
nostrEventId: 'testEventId'
}
sandbox
.stub(uut.useCases.offer, 'takeOffer')
.throws(new Error('test error'))
await uut.takeOffer(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#listOffersByAddress', () => {
it('should list offers by address', async () => {
ctx.params = { addr: 'testAddress' }
sandbox.stub(uut.useCases.offer, 'listOffersByAddress').resolves([])
await uut.listOffersByAddress(ctx)
assert.isArray(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { addr: 'testAddress' }
sandbox
.stub(uut.useCases.offer, 'listOffersByAddress')
.throws(new Error('test error'))
await uut.listOffersByAddress(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#syncOfferMutableData', () => {
it('should sync mutable data', async () => {
ctx.request.body = { tokenId: 'tokenId' }
sandbox.stub(uut.useCases.offer, 'syncOfferMutableData').resolves({})
await uut.syncOfferMutableData(ctx)
assert.isObject(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.request.body = { tokenId: 'tokenId' }
sandbox
.stub(uut.useCases.offer, 'syncOfferMutableData')
.throws(new Error('test error'))
await uut.syncOfferMutableData(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#listCounterOffersByAddress', () => {
it('should list counter offers by address', async () => {
ctx.params = { addr: 'testAddress' }
sandbox.stub(uut.useCases.offer, 'listCounterOffersByAddress').resolves([])
await uut.listCounterOffersByAddress(ctx)
assert.isArray(ctx.body.counterOffers)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { addr: 'testAddress' }
sandbox
.stub(uut.useCases.offer, 'listCounterOffersByAddress')
.throws(new Error('test error'))
await uut.listCounterOffersByAddress(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
}) })
describe('#handleError', () => { describe('#handleError', () => {
@@ -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', () => { describe('#handleError', () => {
it('should still throw error if there is no message', () => { it('should still throw error if there is no message', () => {
try { try {
@@ -74,4 +74,22 @@ describe('#Order-REST-Router', () => {
} }
}) })
}) })
describe('#createOrder', () => {
it('should route to controller', async () => {
sandbox.stub(uut.validators, 'ensureUser').resolves(true)
const spy = sandbox.stub(uut.orderRESTController, 'createOrder').resolves(true)
await uut.createOrder()
assert.isTrue(spy.calledOnce)
})
})
describe('#deleteOrder', () => {
it('should route to controller', async () => {
sandbox.stub(uut.validators, 'ensureUser').resolves(true)
const spy = sandbox.stub(uut.orderRESTController, 'deleteOrder').resolves(true)
await uut.deleteOrder()
assert.isTrue(spy.calledOnce)
})
})
}) })
@@ -0,0 +1,160 @@
/*
Unit tests for the REST API handler for the /sm endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
import P2WDBRESTController from '../../../../../src/controllers/rest-api/p2wdb/controller.js'
import { context as mockContext } from '../../../../unit/mocks/ctx-mock.js'
let uut
let sandbox
let ctx
describe('#P2WDB-REST-Router', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new P2WDBRESTController({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
ctx = mockContext()
ctx.params = { }
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new P2WDBRESTController()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating /p2wdb REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new P2WDBRESTController({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating /p2wdb REST Controller.'
)
}
})
})
describe('#routeWebhook', () => {
it('should route the webhook for a new counter offer', async () => {
ctx.request = {
body: {
data: {
dataType: 'counter-offer'
}
}
}
sandbox.stub(uut.useCases.offer, 'acceptCounterOffer').resolves()
await uut.routeWebhook(ctx)
assert.isTrue(true)
})
it('should route the webhook for a new offer', async () => {
ctx.request = {
body: {
data: {
dataType: 'offer'
}
}
}
sandbox.stub(uut.useCases.offer, 'createOffer').resolves()
await uut.routeWebhook(ctx)
assert.isTrue(true)
})
it('should route the webhook flag', async () => {
ctx.request = {
body: {
data: {
dataType: 'flag'
}
}
}
sandbox.stub(uut.useCases.offer, 'flagOffer').resolves()
await uut.routeWebhook(ctx)
assert.isTrue(true)
})
it('should handle unknown data type', async () => {
ctx.request = {
body: {
data: {
dataType: 'unknown'
}
}
}
sandbox.stub(uut.useCases.offer, 'flagOffer').resolves()
await uut.routeWebhook(ctx)
assert.isTrue(true)
})
it('should catch and throw an error', async () => {
try {
ctx.request.body = {}
await uut.routeWebhook(ctx)
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Cannot read properties of undefined')
}
})
describe('#handleError', () => {
it('should still throw error if there is no message', () => {
try {
const err = {
status: 404
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'Not Found')
}
})
it('should catch error if message is provided', () => {
try {
const err = {
status: 422,
message: 'Unprocessable Entity'
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'Unprocessable Entity')
}
})
})
})
})
@@ -0,0 +1,77 @@
/*
Unit tests for the REST API handler for the /p2wdb endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
import P2WDBRouter from '../../../../../src/controllers/rest-api/p2wdb/index.js'
let uut
let sandbox
// let ctx
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
describe('#P2WDB-REST-Router', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new P2WDBRouter({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
// ctx = mockContext()
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new P2WDBRouter()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating /p2wdb REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new P2WDBRouter({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating /p2wdb REST Controller.'
)
}
})
})
describe('#attach', () => {
it('should throw an error if app is not passed in.', () => {
try {
uut.attach()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Must pass app object when attached REST API controllers.'
)
}
})
})
})
@@ -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.'
)
}
})
})
})
@@ -108,25 +108,25 @@ describe('#Users-REST-Router', () => {
}) })
}) })
// describe('#getAll', () => { describe('#getAll', () => {
// it('should route to controller', async () => { it('should route to controller', async () => {
// sandbox.stub(uut.validators, 'ensureUser').resolves(true) sandbox.stub(uut.validators, 'ensureAdmin').resolves(true)
// const spy = sandbox.stub(uut.userRESTController, 'getUsers').resolves(true) const spy = sandbox.stub(uut.userRESTController, 'getUsers').resolves(true)
// await uut.getAll() await uut.getAll()
// assert.isTrue(spy.calledOnce) assert.isTrue(spy.calledOnce)
// }) })
// }) })
// describe('#getById', () => { describe('#getById', () => {
// it('should route to controller', async () => { it('should route to controller', async () => {
// sandbox.stub(uut.validators, 'ensureUser').resolves(true) sandbox.stub(uut.validators, 'ensureTargetUserOrAdmin').resolves(true)
// const spy = sandbox.stub(uut.userRESTController, 'getUser').resolves(true) const spy = sandbox.stub(uut.userRESTController, 'getUser').resolves(true)
// await uut.getById() await uut.getById()
// assert.isTrue(spy.calledOnce) assert.isTrue(spy.calledOnce)
// }) })
// }) })
describe('#updateUser', () => { describe('#updateUser', () => {
it('should route to controller', async () => { it('should route to controller', async () => {
@@ -130,12 +130,12 @@ describe('#Timer-Controllers', () => {
assert.equal(result, true) assert.equal(result, true)
}) })
// it('should return false on error', async () => { it('should return false on error', async () => {
// sandbox.stub(uut.useCases.usage, 'cleanUsage').throws(new Error('test error')) sandbox.stub(uut.useCases.usage, 'cleanUsage').throws(new Error('test error'))
// const result = await uut.cleanUsage() const result = await uut.cleanUsage()
// assert.equal(result, false) assert.equal(result, false)
// }) })
}) })
describe('#backupUsage', () => { describe('#backupUsage', () => {
@@ -154,4 +154,53 @@ describe('#Timer-Controllers', () => {
assert.equal(result, false) assert.equal(result, false)
}) })
}) })
describe('#newSmAccounts', () => {
it('should kick off the Use Case', async () => {
const result = await uut.newSmAccounts()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.smAccount, 'checkForNewSmAccounts').throws(new Error('test error'))
// sandbox.stub(uut.useCases.usage, 'saveUsage').throws(new Error('test error'))
const result = await uut.newSmAccounts()
assert.equal(result, false)
})
})
describe('#updateSmAccounts', () => {
it('should kick off the Use Case', async () => {
const result = await uut.updateSmAccounts()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.smAccount, 'updateSmAccounts').throws(new Error('test error'))
// sandbox.stub(uut.useCases.usage, 'saveUsage').throws(new Error('test error'))
const result = await uut.updateSmAccounts()
assert.equal(result, false)
})
})
describe('#removeOlderDeletedChatsAndPosts', () => {
it('should kick off the Use Cases and return true', async () => {
sandbox.stub(uut.useCases.nostr, 'removeOlderDeletedChats').resolves()
sandbox.stub(uut.useCases.nostr, 'removeOlderDeletedPosts').resolves()
const result = await uut.removeOlderDeletedChatsAndPosts()
assert.equal(result, true)
})
})
it('should kick off the Use Cases and return false on error', async () => {
sandbox.stub(uut.useCases.nostr, 'removeOlderDeletedChats').throws(new Error('test error'))
sandbox.stub(uut.useCases.nostr, 'removeOlderDeletedPosts').throws(new Error('test error'))
const result = await uut.removeOlderDeletedChatsAndPosts()
assert.equal(result, false)
})
}) })
@@ -0,0 +1,177 @@
/*
Unit tests for the CounterOffer entity library.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import CounterOfferEntity from '../../../src/entities/counterOffer.js'
let sandbox
let uut
describe('#CounterOffer-Entity', () => {
before(async () => { })
beforeEach(() => {
uut = new CounterOfferEntity()
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
describe('#validate', () => {
it('should throw an error if data is not provided', () => {
try {
uut.validate({})
} catch (err) {
assert.include(err.message, 'Input to counterOffer.validate() must be an object with a data property.')
}
})
it('should throw an error if nostrEventId is not provided', () => {
try {
uut.validate({ data: {} })
} catch (err) {
assert.include(err.message, "Property 'nostrEventId' must be a string")
}
})
it('should throw an error if provided takerAddr is not string', () => {
try {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 1234
}
uut.validate({ data: inObj })
} catch (err) {
assert.include(err.message, "Property 'takerAddr' must be a string")
}
})
it('should throw an error if provided takerNpub is not string', () => {
try {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 'taker address',
takerNpub: 1234
}
uut.validate({ data: inObj })
} catch (err) {
assert.include(err.message, "Property 'takerNpub' must be a string")
}
})
it('should throw an error if provided makerNpub is not string', () => {
try {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 'taker address',
takerNpub: 'npub',
makerNpub: 1234
}
uut.validate({ data: inObj })
} catch (err) {
assert.include(err.message, "Property 'makerNpub' must be a string")
}
})
it('should throw an error if provided counterOfferAddr is not string', () => {
try {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 'taker address',
takerNpub: 'npub',
makerNpub: 'npub',
counterOfferAddr: 1234
}
uut.validate({ data: inObj })
} catch (err) {
assert.include(err.message, "Property 'counterOfferAddr' must be a string")
}
})
it('should throw an error if provided counterOfferUtxo is not string', () => {
try {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 'taker address',
takerNpub: 'npub',
makerNpub: 'npub',
counterOfferAddr: 'counter offer address',
counterOfferUtxo: 1234
}
uut.validate({ data: inObj })
} catch (err) {
assert.include(err.message, "Property 'counterOfferUtxo' must be a string")
}
})
it('should throw an error if provided takerOfferUtxo is not string', () => {
try {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 'taker address',
takerNpub: 'npub',
makerNpub: 'npub',
counterOfferAddr: 'counter offer address',
counterOfferUtxo: 'utxo txid',
takerOfferUtxo: 1234
}
uut.validate({ data: inObj })
} catch (err) {
assert.include(err.message, "Property 'takerOfferUtxo' must be a string")
}
})
it('should throw an error if provided tokenId is not string', () => {
try {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 'taker address',
takerNpub: 'npub',
makerNpub: 'npub',
counterOfferAddr: 'counter offer address',
counterOfferUtxo: 'utxo txid',
takerOfferUtxo: 'utxo txid',
tokenId: 1234
}
uut.validate({ data: inObj })
} catch (err) {
assert.include(err.message, "Property 'tokenId' must be a string")
}
})
it('should return a counter offer object', () => {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 'taker address',
takerNpub: 'npub',
makerNpub: 'npub',
counterOfferAddr: 'counter offer address',
counterOfferUtxo: 'utxo txid',
takerOfferUtxo: 'utxo txid',
tokenId: 'token id'
}
const counterOfferEntity = uut.validate({ data: inObj })
assert.property(counterOfferEntity, 'nostrEventId')
assert.equal(counterOfferEntity.nostrEventId, inObj.nostrEventId)
assert.property(counterOfferEntity, 'takerAddr')
assert.equal(counterOfferEntity.takerAddr, inObj.takerAddr)
assert.property(counterOfferEntity, 'takerNpub')
assert.equal(counterOfferEntity.takerNpub, inObj.takerNpub)
assert.property(counterOfferEntity, 'makerNpub')
assert.equal(counterOfferEntity.makerNpub, inObj.makerNpub)
assert.property(counterOfferEntity, 'counterOfferAddr')
assert.equal(counterOfferEntity.counterOfferAddr, inObj.counterOfferAddr)
assert.property(counterOfferEntity, 'counterOfferUtxo')
assert.equal(counterOfferEntity.counterOfferUtxo, inObj.counterOfferUtxo)
assert.property(counterOfferEntity, 'takerOfferUtxo')
assert.equal(counterOfferEntity.takerOfferUtxo, inObj.takerOfferUtxo)
assert.property(counterOfferEntity, 'tokenId')
assert.equal(counterOfferEntity.tokenId, inObj.tokenId)
})
})
})
@@ -0,0 +1,57 @@
/*
Unit tests for the DeletedChat entity library.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import DeletedChat from '../../../src/entities/deletedChat.js'
let sandbox
let uut
describe('#DeletedChat-Entity', () => {
before(async () => {})
beforeEach(() => {
uut = new DeletedChat()
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
describe('#validate', () => {
it('should throw an error if eventId is not provided', () => {
try {
uut.validate()
} catch (err) {
assert.include(err.message, "Property 'eventId' must be a string!")
}
})
it('should return a DeletedChat object', () => {
const inputData = {
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q',
npub: 'npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8',
bchAddr: 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a',
pubkey: '6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1'
}
const deletedChat = uut.validate(inputData)
// console.log('entry: ', entry)
assert.property(deletedChat, 'eventId')
assert.equal(deletedChat.eventId, inputData.eventId)
assert.property(deletedChat, 'npub')
assert.equal(deletedChat.npub, inputData.npub)
assert.property(deletedChat, 'bchAddr')
assert.equal(deletedChat.bchAddr, inputData.bchAddr)
assert.property(deletedChat, 'pubkey')
assert.equal(deletedChat.pubkey, inputData.pubkey)
})
})
})
@@ -0,0 +1,57 @@
/*
Unit tests for the DeletedPost entity library.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import DeletedPost from '../../../src/entities/deletedPost.js'
let sandbox
let uut
describe('#DeletedPost-Entity', () => {
before(async () => { })
beforeEach(() => {
uut = new DeletedPost()
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
describe('#validate', () => {
it('should throw an error if eventId is not provided', () => {
try {
uut.validate()
} catch (err) {
assert.include(err.message, "Property 'eventId' must be a string!")
}
})
it('should return a DeletedPost object', () => {
const inputData = {
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q',
npub: 'npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8',
bchAddr: 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a',
pubkey: '6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1'
}
const deletedPost = uut.validate(inputData)
// console.log('entry: ', entry)
assert.property(deletedPost, 'eventId')
assert.equal(deletedPost.eventId, inputData.eventId)
assert.property(deletedPost, 'npub')
assert.equal(deletedPost.npub, inputData.npub)
assert.property(deletedPost, 'bchAddr')
assert.equal(deletedPost.bchAddr, inputData.bchAddr)
assert.property(deletedPost, 'pubkey')
assert.equal(deletedPost.pubkey, inputData.pubkey)
})
})
})
+261 -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', () => { it('should throw an error if nostrEventId is not included', () => {
try { try {
const offerData = { const offerData = {
@@ -267,6 +374,154 @@ 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 throw an error if makerNpub 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: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorPercentage: 10,
makerNpub: 1234
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'makerNpub' must be a string.")
}
})
it('should validate a new offer', () => { it('should validate a new offer', () => {
const offerObj = { const offerObj = {
appId: 'swapTest555', appId: 'swapTest555',
@@ -286,7 +541,10 @@ describe('#Offer-Entity', () => {
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
ticker: 'TROUT', ticker: 'TROUT',
tokenType: 1, tokenType: 1,
nostrEventId: 'test' nostrEventId: 'test',
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorPercentage: 10,
makerNpub: 'npub1dtkf954h5k2yuv04xp44g5hnmk5k0ppv8mekxafm2gc0vveaxh6s0297qg'
}, },
timestamp: '2021-09-20T17:54:26.395Z', timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM', localTimeStamp: '9/20/2021, 10:54:26 AM',
@@ -312,6 +570,8 @@ describe('#Offer-Entity', () => {
assert.property(result, 'txid') assert.property(result, 'txid')
assert.property(result, 'p2wdbHash') assert.property(result, 'p2wdbHash')
assert.property(result, 'tokenType') assert.property(result, 'tokenType')
assert.property(result, 'makerAddr')
assert.property(result, 'makerNpub')
}) })
}) })
}) })
+66
View File
@@ -130,5 +130,71 @@ describe('#Order-Entity', () => {
assert.include(err.message, "Property 'numTokens' must be a number.") 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)
})
}) })
}) })
+51 -1
View File
@@ -149,6 +149,53 @@ const localdb = {
static findById () {} static findById () {}
static find () {} static find () {}
static findOne () {} static findOne () {}
static countDocuments(){}
async save () {
return {}
}
},
CounterOffer: class CounterOffer {
constructor (obj) {}
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 {}
}
},
DeletedChat: class DeletedChat {
constructor (obj) {}
static findById () {}
static find () {}
static findOne () {}
static updateOne () {}
async save () {
return {}
}
},
DeletedPost: class DeletedPost {
constructor (obj) {}
static findById () {}
static find () {}
static findOne () {}
static updateOne () {}
async save () { async save () {
return {} return {}
@@ -202,7 +249,10 @@ const p2wdb = {
const nostr = { const nostr = {
post: async () => {return true }, post: async () => {return true },
read: async () => { return true }, read: async () => { return true },
eventId2note: () => { return 'testNoteId' } eventId2note: () => { return 'testNoteId' },
readGlobalFeed: async () => { return [] },
getFollowers: async () => { return [] },
privKeyToNpub: async () => { return 'fakeNpub' }
} }
export default { ipfs, localdb, bch, wallet, p2wdb, bchjs, nostr} export default { ipfs, localdb, bch, wallet, p2wdb, bchjs, nostr}
+1
View File
@@ -54,6 +54,7 @@ class MockBchWallet {
} }
}; };
this.optimize = async () => { }; this.optimize = async () => { };
this.cid2json = async () => {};
this.ar = new AdapterRoute() this.ar = new AdapterRoute()
// Environment variable is used by wallet-balance.unit.js to force an error. // Environment variable is used by wallet-balance.unit.js to force an error.
if (process.env.NO_UTXO) { if (process.env.NO_UTXO) {
+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 }
+74
View File
@@ -50,6 +50,33 @@ class Offer {
async loadOffers(){ async loadOffers(){
} }
async listOffers() {
return []
}
async listNftOffers() {
return []
}
async listFungibleOffers() {
return []
}
async takeOffer() {
return []
}
async listOffersByAddress() {
return []
}
async flagOffer() {
return {}
}
async acceptCounterOffer() {
return {}
}
async syncOfferMutableData(){
return {}
}
async listCounterOffersByAddress() {
return []
}
} }
class Order { class Order {
@@ -59,6 +86,12 @@ class Order {
async removeStaleOrders(){ async removeStaleOrders(){
return {} return {}
} }
async listOrders() {
return []
}
async deleteOrder() {
return 'testTxid'
}
} }
class UsageUseCaseMock { class UsageUseCaseMock {
@@ -87,6 +120,45 @@ class UsageUseCaseMock {
} }
} }
class SmAccountUseCaseMock {
async listAccounts() {
return []
}
async getAccountByNpub() {
return {}
}
async getAccountByBchAddr() {
return {}
}
async checkForNewSmAccounts() {
return true
}
async updateSmAccounts() {
return true
}
}
class NostrUseCasesMock {
async createDeletedChat() {
return {}
}
async getDeletedChats() {
return []
}
async createDeletedPost() {
return {}
}
async getDeletedPosts() {
return []
}
async removeOlderDeletedChats() {
return true
}
async removeOlderDeletedPosts() {
return true
}
}
class UseCasesMock { class UseCasesMock {
constuctor(localConfig = {}) { constuctor(localConfig = {}) {
// this.user = new UserUseCaseMock(localConfig) // this.user = new UserUseCaseMock(localConfig)
@@ -97,6 +169,8 @@ class UseCasesMock {
offer = new Offer() offer = new Offer()
order = new Order() order = new Order()
usage = new UsageUseCaseMock() usage = new UsageUseCaseMock()
smAccount = new SmAccountUseCaseMock()
nostr = new NostrUseCasesMock()
} }
export default UseCasesMock; export default UseCasesMock;
+52 -2
View File
@@ -126,6 +126,20 @@ const fungibleTokenData01 = {
"immutableData": "troutsblog.com", "immutableData": "troutsblog.com",
"mutableData": "" "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 = { const offerMockData = {
data: { data: {
@@ -142,10 +156,38 @@ const offerMockData = {
utxoVout: 0, utxoVout: 0,
makerAddr: 'address', makerAddr: 'address',
tokenType: 1, 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 = { const deserealizeTxMock = {
//... //...
vout: [ vout: [
@@ -167,6 +209,12 @@ const deserealizeTxMock = {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478'] addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
} }
}, },
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
//... //...
] ]
} }
@@ -179,5 +227,7 @@ export default {
fungibleOffer01, fungibleOffer01,
fungibleTokenData01, fungibleTokenData01,
offerMockData, offerMockData,
deserealizeTxMock deserealizeTxMockNoOperatorOut,
deserealizeTxMock,
mutableDataMock
}; };
+287
View File
@@ -0,0 +1,287 @@
/*
Unit tests for the Nostr Use Case library.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Unit under test (uut)
import NostrUseCases from '../../../src/use-cases/nostr-use-cases.js'
// Local support libraries
import adapters from '../mocks/adapters/index.js'
describe('#nostr-use-cases', () => {
let uut
let sandbox
before(async () => {
// Delete all previous users in the database.
// await testUtils.deleteAllUsers()
})
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new NostrUseCases({ adapters })
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new NostrUseCases()
assert.fail('Unexpected code path')
console.log(uut) // linter
} catch (err) {
assert.include(
err.message,
'Instance of adapters must be passed in when instantiating Nostr Use Cases library.'
)
}
})
})
describe('#createDeletedChat', () => {
it('should throw an error if no input is given', async () => {
try {
await uut.createDeletedChat()
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
// assert.equal(err.status, 422)
assert.include(err.message, "Property 'eventId' must be a string!")
}
})
it('should throw an error if eventId is not provided', async () => {
try {
await uut.createDeletedChat({})
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, "Property 'eventId' must be a string!")
}
})
it('should catch and throw DB errors', async () => {
try {
// Force an error with the database.
sandbox.stub(uut, 'DeletedChatModel').throws(new Error('test error'))
const inObj = {
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q'
}
await uut.createDeletedChat(inObj)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should create a new deleted chat model with minimum inputs', async () => {
const inObj = {
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q'
}
const res = await uut.createDeletedChat(inObj)
assert.isObject(res)
})
it('should create a new deleted chat model with maximum inputs', async () => {
const inObj = {
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q',
npub: 'npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8',
bchAddr: 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a',
pubkey: '6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1'
}
const res = await uut.createDeletedChat(inObj)
assert.isObject(res)
})
})
describe('#getDeletedChats', () => {
it('should return all deleted chats from the database', async () => {
sandbox.stub(uut.DeletedChatModel, 'find').resolves([])
const res = await uut.getDeletedChats()
assert.isArray(res)
})
it('should catch and throw an error', async () => {
try {
// Force an error.
sandbox.stub(uut.DeletedChatModel, 'find').rejects(new Error('test error'))
await uut.getDeletedChats()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#createDeletedPost', () => {
it('should throw an error if no input is given', async () => {
try {
await uut.createDeletedPost()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, "Property 'eventId' must be a string!")
}
})
it('should throw an error if eventId is not provided', async () => {
try {
await uut.createDeletedPost({})
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, "Property 'eventId' must be a string!")
}
})
it('should catch and throw DB errors', async () => {
try {
// Force an error with the database.
sandbox.stub(uut, 'DeletedPostModel').throws(new Error('test error'))
const inObj = {
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q'
}
await uut.createDeletedPost(inObj)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should create a new deleted post model with minimum inputs', async () => {
const inObj = {
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q'
}
const res = await uut.createDeletedPost(inObj)
assert.isObject(res)
})
it('should create a new deleted post model with maximum inputs', async () => {
const inObj = {
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q',
npub: 'npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8',
bchAddr: 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a',
pubkey: '6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1'
}
const res = await uut.createDeletedPost(inObj)
assert.isObject(res)
})
})
describe('#getDeletedPosts', () => {
it('should return all deleted posts from the database', async () => {
sandbox.stub(uut.DeletedPostModel, 'find').resolves([])
const res = await uut.getDeletedPosts()
assert.isArray(res)
})
})
it('should catch and throw an error', async () => {
try {
// Force an error.
sandbox.stub(uut.DeletedPostModel, 'find').rejects(new Error('test error'))
await uut.getDeletedPosts()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
describe('#removeOlderDeletedChats', () => {
it('should remove older deleted chats from the database', async () => {
const threeMonthsAgo = new Date()
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3) // 3 months ago
threeMonthsAgo.setDate(threeMonthsAgo.getDate() - 1) // 1 day ago
const spy = sinon.spy()
const deletedChatsMock = [
// Newer deleted chat
{
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q',
createdAt: new Date(),
remove: spy
},
// Older deleted chat
{
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q',
createdAt: threeMonthsAgo,
remove: spy
}
]
sandbox.stub(uut.DeletedChatModel, 'find').resolves(deletedChatsMock)
await uut.removeOlderDeletedChats()
assert.equal(spy.callCount, 1, 'Expected to be called once.')
})
it('should handle error', async () => {
try {
// Force an error.
sandbox.stub(uut.DeletedChatModel, 'find').throws(new Error('test error'))
await uut.removeOlderDeletedChats()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#removeOlderDeletedPosts', () => {
it('should remove older deleted posts from the database', async () => {
const threeMonthsAgo = new Date()
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3) // 3 months ago
threeMonthsAgo.setDate(threeMonthsAgo.getDate() - 1) // 1 day ago
const spy = sinon.spy()
const deletedPostsMock = [
// Newer deleted post
{
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q',
createdAt: new Date(),
remove: spy
},
// Older deleted post
{
eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q',
createdAt: threeMonthsAgo,
remove: spy
}
]
sandbox.stub(uut.DeletedPostModel, 'find').resolves(deletedPostsMock)
await uut.removeOlderDeletedPosts()
assert.equal(spy.callCount, 1, 'Expected to be called once.')
})
it('should handle error', async () => {
try {
// Force an error.
sandbox.stub(uut.DeletedPostModel, 'find').throws(new Error('test error'))
await uut.removeOlderDeletedPosts()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
})
+440 -16
View File
@@ -5,6 +5,7 @@
// Public npm libraries // Public npm libraries
import { assert } from 'chai' import { assert } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
import RetryQueue from '@chris.troutner/retry-queue'
// Local support libraries // Local support libraries
// const testUtils = require('../../utils/test-utils') // const testUtils = require('../../utils/test-utils')
@@ -30,6 +31,7 @@ describe('#offer-use-case', () => {
const order = new OrderUseCase({ adapters }) const order = new OrderUseCase({ adapters })
uut = new OfferLib({ adapters, order }) uut = new OfferLib({ adapters, order })
uut.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 1 })
}) })
afterEach(() => sandbox.restore()) afterEach(() => sandbox.restore())
@@ -85,6 +87,15 @@ describe('#offer-use-case', () => {
assert.isFalse(result) 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 () => { it('should return false for invalid utxo', async () => {
const offerObj = mockData.offerMockData const offerObj = mockData.offerMockData
@@ -96,6 +107,98 @@ describe('#offer-use-case', () => {
const result = await uut.createOffer(offerObj) const result = await uut.createOffer(offerObj)
assert.isFalse(result) assert.isFalse(result)
}) })
it('should create offer with token data', async () => {
const tokenDataMock = mockData.nftTokenData01
const offerObj = mockData.offerMockData
const mutableDataMock = mockData.mutableDataMock
const offerEntityMock = Object.assign({}, mockData.offerMockData.data)
// Mock dependencies
sandbox.stub(uut.offerEntity, 'validate').returns(offerEntityMock)
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)
console.log('offerEntityMock', offerEntityMock)
assert.property(offerEntityMock, 'tokenData')
})
it('should create offer with mutable data', async () => {
const tokenDataMock = mockData.nftTokenData01
const offerObj = mockData.offerMockData
const mutableDataMock = mockData.mutableDataMock
const offerEntityMock = Object.assign({}, mockData.offerMockData.data)
// Mock dependencies
sandbox.stub(uut.offerEntity, 'validate').returns(offerEntityMock)
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)
console.log('offerEntityMock', offerEntityMock)
assert.property(offerEntityMock, 'tokenIconUrl')
assert.property(offerEntityMock, 'tokenCategories')
assert.property(offerEntityMock, 'tokenTags')
assert.property(offerEntityMock, 'lastUpdatedTokenData')
})
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 () => { it('should create offer', async () => {
const tokenDataMock = mockData.simpleNftTokenData01 const tokenDataMock = mockData.simpleNftTokenData01
@@ -334,6 +437,14 @@ describe('#offer-use-case', () => {
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' }) const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
assert.isFalse(result) 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', () => { describe('#listOffers', () => {
@@ -381,7 +492,7 @@ describe('#offer-use-case', () => {
} }
}) })
it('should list orders', async () => { it('should list offers', async () => {
const queryMock = { const queryMock = {
sort () { sort () {
return this return this
@@ -395,7 +506,11 @@ describe('#offer-use-case', () => {
sandbox.stub(uut.OfferModel, 'find').returns(queryMock) sandbox.stub(uut.OfferModel, 'find').returns(queryMock)
const result = await uut.listNftOffers(1, true) 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 +712,7 @@ describe('#offer-use-case', () => {
}) })
}) })
describe('#findOrderByEvent', () => { describe('#findOfferByEvent', () => {
it('should throw an error if hash is not provided', async () => { it('should throw an error if hash is not provided', async () => {
try { try {
await uut.findOfferByEvent() await uut.findOfferByEvent()
@@ -629,6 +744,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', () => { describe('#flagOffer', () => {
it('should throw an error if input is not provided', async () => { it('should throw an error if input is not provided', async () => {
try { try {
@@ -687,30 +825,122 @@ describe('#offer-use-case', () => {
it('should skip internal function errors ', async () => { it('should skip internal function errors ', async () => {
// Mock dependencies // 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() await uut.loadOffers()
}) })
it('should createOffer ', async () => {
it('should review and load offers', async () => { mockData.offerMockData.data.dataType = 'offer'
// Mock dependencies // 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() 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', () => { describe('#acceptCounterOffer', () => {
it('should capture counter offer if does not exist', async () => {
const offerObj = mockData.offerMockData
sandbox.stub(uut.CounterOfferModel, 'findOne').resolves(null)
const spy = sandbox.stub(uut.CounterOfferModel.prototype, 'save')
// Mock dependencies
await uut.acceptCounterOffer(offerObj)
assert.isTrue(spy.calledOnce)
})
it('should not save counter offer if already exist', async () => {
const offerObj = mockData.offerMockData
sandbox.stub(uut.CounterOfferModel, 'findOne').resolves(offerObj.data)
const spy = sandbox.stub(uut.CounterOfferModel.prototype, 'save')
// Mock dependencies
await uut.acceptCounterOffer(offerObj)
assert.isFalse(spy.calledOnce)
})
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 () => { it('should return if order is not found!', async () => {
// Mock dependencies // Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').throws(new Error('test error')) const offerObj = mockData.offerMockData
const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').throws(new Error('test error'))
const result = await uut.acceptCounterOffer(offerObj)
assert.equal(result, 'N/A')
})
it('should return N/A on axios error', async () => {
// Mock dependencies
const offerObj = mockData.offerMockData
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'))
const axiosErr = new Error('axios err')
axiosErr.isAxiosError = true
sandbox.stub(uut.retryQueue, 'addToQueue').throws(axiosErr)
const result = await uut.acceptCounterOffer(offerObj)
assert.equal(result, 'N/A')
})
it('should return if utxo can not be validated', async () => {
// Mock dependencies
const offerObj = mockData.offerMockData
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(offerObj)
assert.equal(result, 'N/A')
})
it('should return if utxo is invalid', async () => {
// Mock dependencies
const offerObj = mockData.offerMockData
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(offerObj)
assert.equal(result, 'N/A') assert.equal(result, 'N/A')
}) })
it('should handle error if counter offer cant be calculated', async () => { it('should handle error if counter offer cant be calculated', async () => {
try { try {
const offerObj = mockData.offerMockData
// Mock Data // Mock Data
const mock = Object.assign({}, mockData.offerMockData.data) const mock = Object.assign({}, mockData.offerMockData.data)
mock.rateInBaseUnit = null mock.rateInBaseUnit = null
@@ -719,8 +949,7 @@ describe('#offer-use-case', () => {
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock) sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) await uut.acceptCounterOffer(offerObj)
console.log('result: ', result)
assert.fail('unexpected code path') assert.fail('unexpected code path')
} catch (err) { } catch (err) {
assert.include(err.message, 'Could not calculate the amount of BCH offered in the Counter Offer') assert.include(err.message, 'Could not calculate the amount of BCH offered in the Counter Offer')
@@ -730,11 +959,11 @@ describe('#offer-use-case', () => {
it('should handle error for wrong transaction output', async () => { it('should handle error for wrong transaction output', async () => {
try { try {
// Mock Data // Mock Data
const mock = Object.assign({}, mockData.offerMockData.data) const mock = Object.assign({}, mockData.offerMockData.data)
// Mock dependencies // Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) 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.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock) sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
@@ -746,6 +975,45 @@ 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: mock })
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: mock })
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 () => { it('should handle error for wrong transaction output address', async () => {
try { try {
// Mock data // Mock data
@@ -761,7 +1029,7 @@ describe('#offer-use-case', () => {
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock) sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
await uut.acceptCounterOffer({ data: { /** .... */ } }) await uut.acceptCounterOffer({ data: mock })
assert.fail('unexpected code path') assert.fail('unexpected code path')
} catch (err) { } catch (err) {
assert.include(err.message, 'The Counter Offer has an output address of') assert.include(err.message, 'The Counter Offer has an output address of')
@@ -769,21 +1037,177 @@ 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 // Mock data
const mock = Object.assign({}, mockData.offerMockData.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.rateInBaseUnit = 0
mock.numTokens = 0 mock.numTokens = 0
// Mock dependencies // Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock) 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.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
const result = await uut.acceptCounterOffer({ data: mock })
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) sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
// //
const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) const result = await uut.acceptCounterOffer({ data: mock })
assert.isString(result) assert.isString(result)
assert.notEqual('N/A')
})
})
describe('#syncOfferMutableData', () => {
it('should throw error if tokenId is not provided!', async () => {
try {
await uut.syncOfferMutableData()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'tokenId must be a string!')
}
})
it('should throw error if tokenId is not found!', async () => {
try {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'findOne').resolves(null)
await uut.syncOfferMutableData('tokenId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Associated offer not found!')
}
})
it('should return current data if lastUpdatedTokenData is less than 5 minutes', async () => {
// Mock dependencies
const lastUpdatedTokenData = new Date().getTime()
const offerMock = Object.assign({}, mockData.offerMockData)
offerMock.lastUpdatedTokenData = lastUpdatedTokenData
// stub
sandbox.stub(uut.OfferModel, 'findOne').returns(offerMock)
const spy = sandbox.stub(uut.retryQueue, 'addToQueue').resolves(true)
const offer = await uut.syncOfferMutableData('tokenId')
assert.isObject(offer)
assert.isTrue(spy.notCalled, 'it should not call retryQueue functions.')
})
it('should sync offer', async () => {
// create a timestamp 6 minutes in the past
// Create offer mock
const offerMock = Object.assign({}, mockData.offerMockData)
offerMock.save = () => { }
// Stub functions
sandbox.stub(uut.OfferModel, 'findOne').returns(offerMock)
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves(mockData.simpleNftTokenData01) // Token Data call
.onCall(1).resolves(mockData.mutableDataMock)
const offer = await uut.syncOfferMutableData('tokenId')
assert.isObject(offer)
assert.isNumber(offer.lastUpdatedTokenData)
assert.property(offer, 'tokenData')
assert.property(offer, 'tokenIconUrl')
assert.property(offer, 'tokenCategories')
assert.property(offer, 'tokenTags')
assert.property(offer, 'lastUpdatedTokenData')
})
it('should skip userData stringify error', async () => {
// create mutable data mock
const mutableDataMock = mockData.mutableDataMock
mutableDataMock.userData = { n: 10n }
// create offer mock
const offerMock = Object.assign({}, mockData.offerMockData)
offerMock.save = () => { }
// Stub functions
sandbox.stub(uut.OfferModel, 'findOne').returns(offerMock)
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves(mockData.simpleNftTokenData01) // Token Data call
.onCall(1).resolves(mutableDataMock)
const offer = await uut.syncOfferMutableData('tokenId')
assert.isObject(offer)
assert.isNumber(offer.lastUpdatedTokenData)
})
it('should handle error getting token data', async () => {
// create offer mock
const offerMock = Object.assign({}, mockData.offerMockData)
offerMock.save = () => { }
// Stub functions
sandbox.stub(uut.OfferModel, 'findOne').returns(offerMock)
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).throws(new Error('tokendata error')) // Token Data call
.onCall(1).resolves(mockData.mutableDataMock)
const offer = await uut.syncOfferMutableData('tokenId')
assert.isObject(offer)
})
it('should handle error getting mutable data', async () => {
// create offer mock
const offerMock = Object.assign({}, mockData.offerMockData)
offerMock.save = () => { }
// Stub functions
sandbox.stub(uut.OfferModel, 'findOne').returns(offerMock)
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves(mockData.simpleNftTokenData01) // Token Data call
.onCall(1).throws(new Error('mutable data error'))
const offer = await uut.syncOfferMutableData('tokenId')
assert.isObject(offer)
assert.isNumber(offer.lastUpdatedTokenData)
})
})
describe('#listCounterOffersByAddress', () => {
it('should throw an error if takerAddr is not provided!', async () => {
try {
await uut.listCounterOffersByAddress()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'takerAddr must be a string')
}
})
it('should return counter offers by address', async () => {
// Mock dependencies
sandbox.stub(uut.CounterOfferModel, 'find').resolves([])
const result = await uut.listCounterOffersByAddress('bitcoincash:qzltx40ldxr53edchprppj5lpg4q7vl20usx25ga44')
assert.isArray(result)
}) })
}) })
}) })
@@ -158,6 +158,7 @@ describe('#order-use-case', () => {
describe('#createOrder', () => { describe('#createOrder', () => {
it('should create an order and return the hash', async () => { it('should create an order and return the hash', async () => {
uut.config.useFullStackCash = true
const entryObj = { const entryObj = {
lokadId: 'SWP', lokadId: 'SWP',
messageType: 1, messageType: 1,
@@ -185,6 +186,7 @@ describe('#order-use-case', () => {
assert.property(result, 'eventId') assert.property(result, 'eventId')
assert.property(result, 'noteId') assert.property(result, 'noteId')
}) })
it('should create an order with consumer-api', async () => { it('should create an order with consumer-api', async () => {
uut.config.useFullStackCash = false uut.config.useFullStackCash = false
const entryObj = { const entryObj = {
@@ -214,6 +216,7 @@ describe('#order-use-case', () => {
assert.property(result, 'eventId') assert.property(result, 'eventId')
assert.property(result, 'noteId') assert.property(result, 'noteId')
}) })
it('should throw error if user is not found', async () => { it('should throw error if user is not found', async () => {
try { try {
const entryObj = { const entryObj = {
@@ -384,4 +387,55 @@ describe('#order-use-case', () => {
assert.isString(result) 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')
}
})
})
})
+31
View File
@@ -0,0 +1,31 @@
import mongoose from 'mongoose'
import config from '../../../config/index.js'
import User from '../../../src/adapters/localdb/models/users.js'
async function getUsers () {
// Connect to the Mongo Database.
mongoose.Promise = global.Promise
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
await mongoose.connect(
config.database,
{ useNewUrlParser: true, useUnifiedTopology: true }
)
// Find the user by email.
const user = await User.findOne({
email: 'test@test.com'
}, '-password')
// console.log('user: ', user)
// Update the users password
user.password = 'test'
// Change the user to an admin
// user.type = 'admin'
// Save the changes to the database.
await user.save()
mongoose.connection.close()
}
getUsers()
+4 -4
View File
@@ -11,12 +11,12 @@ import axios from 'axios'
// import User from '../../../src/adapters/localdb/models/users.js' // import User from '../../../src/adapters/localdb/models/users.js'
// Customize these variables // Customize these variables
const EMAIL = 'someone@somewhere.com' const EMAIL = 'chris.troutner@gmail.com'
const NAME = 'Someone' const NAME = 'Chris'
const PASSWORD = 'test' const PASSWORD = '0mega16'
const ADMIN_EMAIL = 'system@system.com' const ADMIN_EMAIL = 'system@system.com'
const ADMIN_PASSWORD = 'admin' const ADMIN_PASSWORD = 'decatur'
async function createUser () { async function createUser () {
try { try {
+32
View File
@@ -0,0 +1,32 @@
import mongoose from 'mongoose'
// import config from '../../config/index.js'
import User from '../../src/adapters/localdb/models/users.js'
const mongooseConnectStr = 'mongodb://172.17.0.1:5666/bch-swap-service-prod'
async function getUsers () {
// Connect to the Mongo Database.
mongoose.Promise = global.Promise
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
await mongoose.connect(
mongooseConnectStr,
{ useNewUrlParser: true, useUnifiedTopology: true }
)
// Find the user by email.
const user = await User.findOne({
email: 'test@test.com'
}, '-password')
// Update the users password
// user.password = 'newpassword'
// Change the user to an admin
// user.type = 'admin'
// Save the changes to the database.
await user.save()
mongoose.connection.close()
}
getUsers()