Compare commits

..
20 Commits
Author SHA1 Message Date
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 2af50ecf02 Merge pull request #105 from Permissionless-Software-Foundation/operator-fee
Adding required Operator fee to SWaP TX
2025-08-10 11:06:55 -07:00
Chris Troutner f88a24c1b5 Updating unit tests 2025-08-10 10:44:45 -07:00
Chris Troutner a4b4ff13d2 Handling corner cases when validating counter offer against operator fee 2025-08-09 17:54:30 -07:00
Chris Troutner 2bf4f9df28 fix(acceptCounterOffer()): Skipping Counter Offers that are missing an output for the operator 2025-08-09 16:28:56 -07:00
Chris Troutner 19ba38e8ce Adding operator fee to Offer use case, entity, and database model 2025-08-09 15:08:31 -07:00
Chris Troutner 373a52ebf1 feat(operator fee): Adding config values to ensure bch-dex operator recieves a fee 2025-08-09 15:02:29 -07:00
Chris Troutner fe82478dc4 linting 2025-08-09 14:49:41 -07:00
Chris Troutner bed281c8c3 minor edit 2025-08-09 14:49:32 -07:00
Chris Troutner 01484b4e03 fixing typo in comments 2025-08-07 07:00:58 -07:00
Chris Troutner c86512254d Merge pull request #104 from Permissionless-Software-Foundation/dh-test-coverage
feat(tests): Improved adapters test coverage
2025-08-06 09:42:17 -07:00
Daniel Gonzalez 7df9b9b8a4 feat(tests): Improved adapters test coverage 2025-08-05 16:45:37 -04:00
Chris Troutner 5f91395766 Merge pull request #103 from Permissionless-Software-Foundation/ct-unstable
fix(GET /offer/list/addr/:addr): Get offers by an address
2025-07-28 11:14:25 -07:00
Chris Troutner dda6cb2aa4 fix(GET /offer/list/addr/:addr): Get offers by an address 2025-07-28 11:12:04 -07:00
Chris Troutner 578aade39b Merge pull request #102 from Permissionless-Software-Foundation/ct-unstable
fix(metrics): Adding timer-controller that updates metrics of SM Acco…
2025-07-14 19:30:21 -07:00
Chris Troutner b09890a527 fix(metrics): Adding timer-controller that updates metrics of SM Accounts 2025-07-14 19:16:00 -07:00
Chris Troutner a32bd840b9 Merge pull request #101 from Permissionless-Software-Foundation/ct-unstable
fix(smAccount): Adding GET endpoints
2025-07-08 09:24:11 -07:00
Chris Troutner 0bdc232938 fix(smAccount): Adding GET endpoints 2025-07-08 08:58:31 -07:00
45 changed files with 2570 additions and 165 deletions
+5 -1
View File
@@ -191,5 +191,9 @@ export default {
disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false,
// Admin password
adminPassword: process.env.ADMIN_PASSWORD
adminPassword: process.env.ADMIN_PASSWORD,
// The Operator of BCH DEX can elect to receive a percentage of each sale.
operatorAddress: process.env.OPERATOR_ADDRESS ? process.env.OPERATOR_ADDRESS : 'bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr',
operatorPercentage: process.env.OPERATOR_PERCENTAGE ? parseFloat(process.env.OPERATOR_PERCENTAGE) : 2.0
}
+3
View File
@@ -66,6 +66,9 @@ class Bch {
// }
// }
/** DEV NOTE: This function are deprecated.?
* https://github.com/Permissionless-Software-Foundation/bch-message-lib/pull/41
*/
async getMerit (slpAddr) {
try {
if (!slpAddr || typeof slpAddr !== 'string') {
+4 -1
View File
@@ -35,8 +35,11 @@ const Offer = new mongoose.Schema({
lokadId: { type: String },
messageType: { type: Number },
messageClass: { type: Number },
nostrEventId: { type: String } // Nostr Event Id.
nostrEventId: { type: String }, // Nostr Event Id.
// Operator fee and address
operatorAddress: { type: String },
operatorPercentage: { type: Number }
})
export default mongoose.model('offer', Offer)
+5 -1
View File
@@ -43,7 +43,11 @@ const Order = new mongoose.Schema({
// Additional properties found in createOrder
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)
+2 -2
View File
@@ -14,8 +14,8 @@ const SmAccount = new mongoose.Schema({
pfp: { type: String, default: '' },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
followers: { type: Number, default: 0 },
following: { type: Number, default: 0 },
followerCnt: { type: Number, default: 0 },
followingCnt: { type: Number, default: 0 },
posts: { type: Number, default: 0 },
likes: { type: Number, default: 0 },
comments: { type: Number, default: 0 },
+57
View File
@@ -41,7 +41,9 @@ class NostrAdapter {
this.read = this.read.bind(this)
this.eventId2note = this.eventId2note.bind(this)
this.pubkey2npub = this.pubkey2npub.bind(this)
this.npub2pubkey = this.npub2pubkey.bind(this)
this.readGlobalFeed = this.readGlobalFeed.bind(this)
this.getFollowers = this.getFollowers.bind(this)
}
// Create nostr keys.
@@ -140,6 +142,10 @@ class NostrAdapter {
return nip19.npubEncode(pubkey)
}
npub2pubkey (npub) {
return nip19.decode(npub)
}
// Read the global feed.
async readGlobalFeed (inObj = {}) {
try {
@@ -190,6 +196,57 @@ class NostrAdapter {
throw error
}
}
async getFollowers (inObj = {}) {
try {
const { pubkey } = inObj
if (!pubkey || typeof pubkey !== 'string') {
throw new Error('pubkey must be a string!')
}
const relays = [this.relayWs]
const pool = this.RelayPool(relays)
const nostrData = new Promise((resolve, reject) => {
const followers = []
pool.on('open', (relay) => {
// Query for kind 3 events where the target pubkey appears in tags
// This finds all contact lists that include the target pubkey
relay.subscribe('subid', {
limit: 100,
kinds: [3],
'#p': [pubkey] // Filter by tag 'p' containing the target pubkey
})
})
pool.on('eose', relay => {
relay.close()
resolve(followers)
})
pool.on('event', (relay, subId, ev) => {
// Each event represents a contact list from someone who follows the target
// The author of this event is a follower
const follower = {
pubkey: ev.pubkey,
npub: this.pubkey2npub(ev.pubkey),
contactList: ev.tags,
createdAt: ev.created_at
}
followers.push(follower)
})
})
const followers = await nostrData
return followers
} catch (error) {
console.log(`Error in nostr.js/getFollowers() ${error.message} `)
throw error
}
}
}
export default NostrAdapter
+28 -3
View File
@@ -34,6 +34,7 @@ class WalletAdapter {
this.config = config
this.bitcoinJs = bitcoinJs
this.BchWallet = BchWallet
this.BchTokenSweep = BchTokenSweep
this.bchWallet = {} // Will be replaced when initialized.
// this.advancedConfig = localConfig.advancedConfig
this.retryQueue = new RetryQueue({
@@ -205,7 +206,9 @@ class WalletAdapter {
// Generate a cryptographic signature, required to write to the P2WDB.
async generateSignature (message) {
try {
// TODO: Add input validation for message.
if (!message || typeof message !== 'string') {
throw new Error('message is required!')
}
const privKey = this.bchWallet.walletInfo.privateKey
@@ -370,6 +373,12 @@ class WalletAdapter {
async moveTokens (inObj = {}) {
try {
const { tokenId, qty } = inObj
if (!tokenId || typeof tokenId !== 'string') {
throw new Error('tokenId must be a string!')
}
if (!qty || typeof qty !== 'number') {
throw new Error('qty must be a number!')
}
const keyPair = await this.getKeyPair()
console.log('keyPair: ', keyPair)
@@ -392,6 +401,10 @@ class WalletAdapter {
x => x.tokenId === tokenId
)
if (tokenUtxos.length === 0) {
throw new Error('tokenId not found!')
}
const txid = await this.bchWallet.sendTokens(receiver, 3)
const utxoInfo = {
@@ -412,6 +425,10 @@ class WalletAdapter {
// generate a segregated UTXO.
async moveBch (amountSat) {
try {
if (!amountSat) {
throw new Error('amountSat is required!')
}
const keyPair = await this.getKeyPair()
console.log('keyPair: ', keyPair)
@@ -444,6 +461,10 @@ class WalletAdapter {
// representing the transaction.
async deseralizeTx (txHex) {
try {
// Input validation
if (!txHex || typeof txHex !== 'string') {
throw new Error('hex must be a string!')
}
// Ensure the URL points at FullStack.cash, since the web 3 infra does not
// yet support this call.
const oldUrl = this.bchWallet.bchjs.RawTransactions.restURL
@@ -555,8 +576,12 @@ class WalletAdapter {
// Given an Order database object, this function sends the tokens from the
// HD index where they are stored, back to the root address of the wallet.
async reclaimTokens (orderData) {
async reclaimTokens (orderData = {}) {
try {
if (!orderData.hdIndex) {
throw new Error('orderData.hdIndex is required!')
}
// Get the key pair from the Order.
const keyPair = await this.getKeyPair(orderData.hdIndex)
console.log('keyPair: ', keyPair)
@@ -576,7 +601,7 @@ class WalletAdapter {
// const txid = await tempWallet.sendTokens(receiver)
// Sweep the tokens to the root address.
const sweeper = new BchTokenSweep(
const sweeper = new this.BchTokenSweep(
keyPair.wif,
this.bchWallet.walletInfo.privateKey,
this.bchWallet,
+15 -13
View File
@@ -6,13 +6,15 @@ import config from '../../config/index.js'
import axios from 'axios'
let _this
class WebHook {
constructor () {
_this = this
_this.config = config
_this.axios = axios
this.config = config
this.axios = axios
this.sleepTime = 2000
this.createWebhook = this.createWebhook.bind(this)
this.deleteWebhook = this.deleteWebhook.bind(this)
this.waitUntilSuccess = this.waitUntilSuccess.bind(this)
this.sleep = this.sleep.bind(this)
}
// REST petition to create a webhook in p2wdb-service
@@ -22,14 +24,14 @@ class WebHook {
throw new Error('url must be a string')
}
const endpoint = _this.config.webhookService
const endpoint = this.config.webhookService
const obj = {
appId: this.config.p2wdbAppId,
url
}
const result = await axios.post(endpoint, obj)
const result = await this.axios.post(endpoint, obj)
return result.data
} catch (err) {
@@ -45,14 +47,14 @@ class WebHook {
throw new Error('url must be a string')
}
const endpoint = _this.config.webhookService
const endpoint = this.config.webhookService
const obj = {
appId: this.config.p2wdbAppId,
url
}
const result = await axios.delete(endpoint, { data: obj })
const result = await this.axios.delete(endpoint, { data: obj })
return result.data
} catch (err) {
@@ -86,7 +88,7 @@ class WebHook {
} catch (err) {
const now = new Date()
console.log(`${now.toLocaleString()}: Error trying to create webhook with P2WDB. Trying again...`)
await sleep(2000)
await this.sleep(this.sleepTime)
}
} while (!success)
@@ -96,10 +98,10 @@ class WebHook {
throw err
}
}
}
function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
async sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
}
export default WebHook
@@ -1,5 +1,8 @@
/*
REST API Controller library for the /offer route
TODO:
- Add api-doc documentation for all endpoints in this file.
*/
import wlogger from '../../../adapters/wlogger.js'
@@ -30,6 +33,7 @@ class OfferRESTControllerLib {
this.listNftOffers = this.listNftOffers.bind(this)
this.listFungibleOffers = this.listFungibleOffers.bind(this)
this.takeOffer = this.takeOffer.bind(this)
this.listOffersByAddress = this.listOffersByAddress.bind(this)
this.handleError = this.handleError.bind(this)
}
@@ -115,6 +119,21 @@ class OfferRESTControllerLib {
}
}
// List all offers being made by a given address.
// curl -X GET http://localhost:5700/offer/list/addr/bitcoincash:qrpxtnrlhfz9wsuuse7z5k2mxmvw0r3pu5qepyhmq2
async listOffersByAddress (ctx) {
try {
const addr = ctx.params.addr
const offers = await this.useCases.offer.listOffersByAddress(addr)
ctx.body = offers
} catch (err) {
console.log('Error in listOffersByAddress REST API handler: ', err)
this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
console.log('err', err.message)
+10 -10
View File
@@ -8,8 +8,6 @@ import Router from 'koa-router'
// Local libraries.
import OfferRESTControllerLib from './controller.js'
let _this
class OfferRouter {
constructor (localConfig = {}) {
// Dependency Injection.
@@ -38,7 +36,8 @@ class OfferRouter {
const baseUrl = '/offer'
this.router = new Router({ prefix: baseUrl })
_this = this
// Bind 'this' object to all subfunctions.
this.attach = this.attach.bind(this)
}
attach (app) {
@@ -50,19 +49,20 @@ class OfferRouter {
// 12/3/24 CT:
// Note: The createOffer() path was used by a P2WDB webhook to generate an
// Offer from and Order. This has been deprecated and Offers are now created
// Offer from an Order. This has been deprecated and Offers are now created
// by a Timer Controller monitoring a Nostr topic.
// Define the routes and attach the controller.
// this.router.post('/', _this.offerRESTController.createOffer) // Deprecated.
this.router.post('/take', _this.offerRESTController.takeOffer)
this.router.get('/list/all/:page', _this.offerRESTController.listOffers)
this.router.get('/list/nft/:page', _this.offerRESTController.listNftOffers)
this.router.get('/list/fungible/:page', _this.offerRESTController.listFungibleOffers)
this.router.post('/take', this.offerRESTController.takeOffer)
this.router.get('/list/all/:page', this.offerRESTController.listOffers)
this.router.get('/list/nft/:page', this.offerRESTController.listNftOffers)
this.router.get('/list/fungible/:page', this.offerRESTController.listFungibleOffers)
this.router.get('/list/addr/:addr', this.offerRESTController.listOffersByAddress)
// Attach the Controller routes to the Koa app.
app.use(_this.router.routes())
app.use(_this.router.allowedMethods())
app.use(this.router.routes())
app.use(this.router.allowedMethods())
}
}
@@ -64,6 +64,7 @@ class P2WDBRESTControllerLib {
await _this.useCases.offer.flagOffer(flagData)
} else {
console.log('Could not route P2WDB webhook data.')
// NOTE: Should we throw an error here?
}
// const orderObj = ctx.request.body.order
@@ -12,13 +12,13 @@ class SmAccountRESTControllerLib {
this.adapters = localConfig.adapters
if (!this.adapters) {
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
if (!this.useCases) {
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.'
)
}
@@ -29,6 +29,16 @@ class SmAccountRESTControllerLib {
_this = this
}
/**
* @api {get} /sm/list/all/{page} List all Social Media Accounts
* @apiPermission public
* @apiName ListAccounts
* @apiGroup REST Social Media Accounts
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5700/sm/list/all/0
*
*/
// curl -X GET http://localhost:5700/sm/list/all/0
async listAccounts (ctx) {
try {
@@ -44,6 +54,68 @@ class SmAccountRESTControllerLib {
}
}
/**
* @api {get} /sm/npub/{npub} Get Social Media Account by Npub
* @apiPermission public
* @apiName GetAccountByNpub
* @apiGroup REST Social Media Accounts
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5700/sm/npub/npub1y3xq402pu9aqms3khetnt5gzm54t63pzcla56wwfmwshde0ws77qkff5gc
*
*/
// curl -X GET http://localhost:5700/sm/npub/npub1y3xq402pu9aqms3khetnt5gzm54t63pzcla56wwfmwshde0ws77qkff5gc
async getAccountByNpub (ctx) {
try {
const npub = ctx.params.npub
if (!npub) {
ctx.throw(400, 'Npub is required')
}
const account = await _this.useCases.smAccount.getAccountByNpub(npub)
if (!account) {
ctx.throw(404, 'Account not found')
}
ctx.body = account
} catch (err) {
// console.log('Error in getAccountByNpub REST API handler: ', err)
_this.handleError(ctx, err)
}
}
/**
* @api {get} /sm/bchAddr/{bchAddr} Get Social Media Account by BCH address
* @apiPermission public
* @apiName GetAccountByBchAddr
* @apiGroup REST Social Media Accounts
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5700/sm/bchAddr/bitcoincash:qz6lf6gpmn3secx73g7ucfcgy8mrh3sz2y2ylk643x
*
*/
// curl -X GET http://localhost:5700/sm/bchAddr/bitcoincash:qz6lf6gpmn3secx73g7ucfcgy8mrh3sz2y2ylk643x
async getAccountByBchAddr (ctx) {
try {
const bchAddr = ctx.params.bchAddr
if (!bchAddr) {
ctx.throw(400, 'BCH address is required')
}
const account = await _this.useCases.smAccount.getAccountByBchAddr(bchAddr)
if (!account) {
ctx.throw(404, 'Account not found')
}
ctx.body = account
} catch (err) {
// console.log('Error in getAccountByBchAddr REST API handler: ', err)
_this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
console.log('err', err)
+2 -6
View File
@@ -40,7 +40,6 @@ class SmAccountRouter {
const baseUrl = '/sm'
this.router = new Router({ prefix: baseUrl })
this.listAccounts = this.listAccounts.bind(this)
_this = this
}
@@ -53,16 +52,13 @@ class SmAccountRouter {
// Define the routes and attach the controller.
this.router.get('/list/all/:page', _this.smAccountRESTController.listAccounts)
this.router.get('/npub/:npub', _this.smAccountRESTController.getAccountByNpub)
this.router.get('/bchAddr/:bchAddr', _this.smAccountRESTController.getAccountByBchAddr)
// Attach the Controller routes to the Koa app.
app.use(_this.router.routes())
app.use(_this.router.allowedMethods())
}
async listAccounts (ctx, next) {
// await _this.validators.ensureUser(ctx, next)
await _this.smAccountRESTController.listAccounts(ctx, next)
}
}
export default SmAccountRouter
+17 -1
View File
@@ -27,6 +27,7 @@ class TimerControllers {
this.cleanUsageInterval = 60000 * 60 // 1 hour
this.backupUsageInterval = 60000 * 10 // 10 minutes
this.newSmAccountsInterval = 60000 * 60 // 60 minutes
this.updateSmAccountsInterval = 60000 * 10 // 10 minutes
// Encapsulate dependencies
this.config = config
@@ -40,6 +41,7 @@ class TimerControllers {
this.cleanUsage = this.cleanUsage.bind(this)
this.backupUsage = this.backupUsage.bind(this)
this.newSmAccounts = this.newSmAccounts.bind(this)
this.updateSmAccounts = this.updateSmAccounts.bind(this)
// State
this.gcOrdersInt = null
@@ -47,6 +49,7 @@ class TimerControllers {
this.checkDupOffersInt = null
this.loadOffersInt = null
this.newSmAccountsInt = null
this.updateSmAccountsInt = null
}
// Start all the time-based controllers.
@@ -58,7 +61,7 @@ class TimerControllers {
this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval)
this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval)
this.newSmAccountsInt = setInterval(this.newSmAccounts, this.newSmAccountsInterval)
this.updateSmAccountsInt = setInterval(this.updateSmAccounts, this.updateSmAccountsInterval)
return true
}
@@ -70,6 +73,7 @@ class TimerControllers {
clearInterval(this.cleanUsageHandle)
clearInterval(this.backupUsageHandle)
clearInterval(this.newSmAccountsInt)
clearInterval(this.updateSmAccountsInt)
}
// Garbage Collect the Orders.
@@ -101,6 +105,7 @@ class TimerControllers {
console.error('Error in time-controller.js/cleanUsage(): ', err)
this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval)
return false
}
}
@@ -178,6 +183,17 @@ class TimerControllers {
return false
}
}
// Update the Social Media Accounts.
async updateSmAccounts () {
try {
await this.useCases.smAccount.updateSmAccounts()
return true
} catch (err) {
console.error('Error in time-controller.js/updateSmAccounts(): ', err)
return false
}
}
}
export default TimerControllers
+3
View File
@@ -1,6 +1,9 @@
/*
Entry Entity
TODO: Explain what an entry is and how it works.
*/
class Entry {
validate ({
entry,
+12 -2
View File
@@ -1,6 +1,6 @@
/*
Offer Entity
An ffer is created when a new Signal is detected via the P2WDB webhook.
An offer is created when a new Signal is detected via the P2WDB webhook.
It's destroyed when the UTXO described in the Signal has been detected as spent.
*/
@@ -33,7 +33,9 @@ class OfferEntity {
makerAddr,
ticker,
tokenType,
nostrEventId
nostrEventId,
operatorAddress,
operatorPercentage
} = offerData.data
// Input Validation
@@ -80,6 +82,14 @@ class OfferEntity {
throw new Error("Property 'nostrEventId' must be a string.")
}
// Add the operator address and percentage to the offer.
if (!operatorAddress || typeof operatorAddress !== 'string') {
throw new Error("Property 'operatorAddress' must be a string.")
}
if (!operatorPercentage || typeof operatorPercentage !== 'number') {
throw new Error("Property 'operatorPercentage' must be a number.")
}
// Convert the timestamp to a number.
let timestamp = new Date(offerData.timestamp)
timestamp = timestamp.getTime()
+7 -1
View File
@@ -1,3 +1,8 @@
/*
Entry use-case library.
TODO: Explain what an entry is and how it works.
*/
// local libraries
import wlogger from '../adapters/wlogger.js'
@@ -30,11 +35,12 @@ class EntryLib {
if (!isValidSignature) {
throw new Error('Invalid signature')
}
// Verify psf tokens balance
// const psfBalance = await this.bch.getPSFTokenBalance(
// entryEntity.slpAddress
// )
// NOTE: this.bch.getPSFTokenBalance , does not exist , should be created?
const psfBalance = 1
if (psfBalance < 10) {
+62 -8
View File
@@ -64,6 +64,7 @@ class OfferUseCases {
this.removeStaleOffers = this.removeStaleOffers.bind(this)
this.flagOffer = this.flagOffer.bind(this)
this.loadOffers = this.loadOffers.bind(this)
this.listOffersByAddress = this.listOffersByAddress.bind(this)
// State
this.seenOffers = []
@@ -110,7 +111,10 @@ class OfferUseCases {
const utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
// console.log('utxoStatus: ', utxoStatus)
// if (utxoStatus === null) return false
if (!utxoStatus) return false
if (!utxoStatus) {
console.log(`UTXO txid: ${offerObj.data.utxoTxid}, vout: ${offerObj.data.utxoVout} has been spent. Skipping.`)
return false
}
// A new offer gets a status of 'posted'
offerObj.data.offerStatus = 'posted'
@@ -512,9 +516,8 @@ class OfferUseCases {
// }
}
// This function is called by the P2WDB webhook REST API handler. When a
// Counter Offer is passed to bch-dex by the P2WDB, the data is then passed
// to this function. It does due diligence on the Counter Offer, then signs
// This function is called by loadOffers().
// It does due diligence on the Counter Offer, then signs
// and broadcasts the transaction to accept the Counter Offer.
async acceptCounterOffer (offerData) {
try {
@@ -527,6 +530,8 @@ class OfferUseCases {
return false
}
console.log(`New counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerData.data.nostrEventId)}`)
// See if this instance of bch-dex is managing the Order associated with
// the incoming Counter Offer.
@@ -593,20 +598,57 @@ class OfferUseCases {
throw new Error('Could not calculate the amount of BCH offered in the Counter Offer')
}
const satsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[2].value)
const hasRequiredAmount = satsOut === satsToReceive
if (!hasRequiredAmount) {
throw new Error(`The Counter Offer has an output of ${satsOut}, which does not match the required ${satsToReceive} in the Offer.`)
}
// Ensure the Counter Offer has an output for the Operator of bch-dex.
if (!txObj.vout[3]) {
console.log('The Counter Offer does not have an output for the Operator.')
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
this.seenOffers.push(eventId)
return 'N/A'
}
// Ensure the 3rd output (vout=2) is going to the maker address specified
// in the Offer.
const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0]
const makerAddr = orderData.makerAddr
const hasCorrectAddr = makerAddr === addrInCounterOffer
if (!hasCorrectAddr) {
throw new Error(`The Counter Offer has an output address of ${addrInCounterOffer}, which does not match the Maker address of ${makerAddr} in the Offer.`)
}
// Ensure the 4th output (vout=3) is going to the operator address specified in the Offer.
// NOTE: This is the operator of the bch-dex intance, NOT the seller of the token.
const operatorAddr = orderData.operatorAddress
const hasCorrectOperatorAddr = operatorAddr === txObj.vout[3].scriptPubKey.addresses[0]
if (!hasCorrectOperatorAddr) {
throw new Error(`The Counter Offer has an output address of ${txObj.vout[3].scriptPubKey.addresses[0]}, which does not match the Operator address of ${operatorAddr} in the Offer.`)
}
// Ensure the 4th output (vout=3) contains the required amount of BCH.
const operatorSatsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[3].value)
let estimatedOperatorFee = Math.floor(txObj.vout[3].value * orderData.operatorPercentage / 100)
if (estimatedOperatorFee < 546) estimatedOperatorFee = 546
console.log('operatorSatsOut: ', operatorSatsOut, 'estimatedOperatorFee: ', estimatedOperatorFee)
if (operatorSatsOut < estimatedOperatorFee) {
console.log(`Skipping: The Counter Offer has an output of ${operatorSatsOut}, which is less than the estimated operator fee of ${estimatedOperatorFee}.`)
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
this.seenOffers.push(eventId)
return 'N/A'
}
// Get the User ID from the Order model.
const userId = orderData.userId
@@ -717,7 +759,8 @@ class OfferUseCases {
await thisOffer.remove()
}
// If the Offer is older than 7 days, delete it.
// TODO: Instead of deleting the offer, send the token back to the Makers wallet.
// If the Offer is older than a threshold, delete it.
const nowMs = now.getTime()
const eightWeeks = 1000 * 60 * 60 * 24 * 7 * 8
const eightWeeksAgo = nowMs - eightWeeks
@@ -773,7 +816,7 @@ class OfferUseCases {
try {
// Retrieve offers array.
const offers = await this.adapters.nostr.read()
// console.log('offers: ', offers)
console.log('offers: ', offers)
for (let i = 0; i < offers.length; i++) {
try {
@@ -784,7 +827,7 @@ class OfferUseCases {
// Append the Nostr Event ID to the offer object
offerObj.data.nostrEventId = offer.eventId
// console.log('loadOffers() offerObj: ', offerObj)
console.log('loadOffers() offerObj: ', offerObj)
if (offerObj.data.dataType === 'offer') {
// Try to create new offer
@@ -794,7 +837,7 @@ class OfferUseCases {
if (offerObj.data.dataType === 'counter-offer') {
// console.log('Counter offer detected: ', offerObj)
// console.log('Counter offer detected: ', offerObj.data.nostrEventId)
console.log(`Counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerObj.data.nostrEventId)}`)
// console.log(`Counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerObj.data.nostrEventId)}`)
await this.acceptCounterOffer(offerObj)
}
@@ -808,6 +851,17 @@ class OfferUseCases {
throw error
}
}
// List all offers being made by a given address.
async listOffersByAddress (addr) {
try {
const offers = await this.OfferModel.find({ makerAddr: addr })
return offers
} catch (err) {
console.error('Error in listOffersByAddress(): ', err)
throw err
}
}
}
export default OfferUseCases
+7 -7
View File
@@ -44,6 +44,7 @@ class OrderLib {
if (!entryObj.tokenId) throw new Error('entry does not contain required properties')
// Get the user data from the database, for the user creating the new Order.
const user = await this.UserModel.findById(entryObj.userId)
if (!user) throw new Error('user not found')
@@ -81,13 +82,7 @@ class OrderLib {
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {})
await userWallet.optimize()
// Ensure sufficient tokens exist to create the order.
// await this.ensureFunds(orderEntity)
// await this.retryQueue.addToQueue(this.ensureFunds, orderEntity)
// Get Ticker for token ID.
// const tokenData = await this.adapters.wallet.bchWallet.getTxData([entryObj.tokenId])
// const tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTxData, [entryObj.tokenId])
const tokenData = await userWallet.getTxData([entryObj.tokenId])
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
orderEntity.ticker = tokenData[0].tokenTicker
@@ -107,15 +102,20 @@ class OrderLib {
// await this.adapters.wallet.bchWallet.getUtxos()
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
await userWallet.initialize()
// Update the order with the new UTXO information.
orderEntity.utxoTxid = utxoInfo.txid
orderEntity.utxoVout = utxoInfo.vout
orderEntity.tokenType = utxoInfo.tokenType
// Add P2WDB specific flag for signaling that this is a new offer.
// Add flag to signal that this is a new offer.
orderEntity.dataType = 'offer'
orderEntity.userId = user._id
// Add the operator address and percentage to the order.
orderEntity.operatorAddress = this.config.operatorAddress
orderEntity.operatorPercentage = this.config.operatorPercentage
// Post the new Order information to Nostr under the topic set in the
// config file.
const postObj = {
+57 -1
View File
@@ -10,13 +10,16 @@ class smAccountUseCases {
this.adapters = localConfig.adapters
if (!this.adapters) {
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.'
)
}
// Bind 'this' object to all methods
this.checkForNewSmAccounts = this.checkForNewSmAccounts.bind(this)
this.listAccounts = this.listAccounts.bind(this)
this.getAccountByNpub = this.getAccountByNpub.bind(this)
this.getAccountByBchAddr = this.getAccountByBchAddr.bind(this)
this.updateSmAccounts = this.updateSmAccounts.bind(this)
}
// Check for new Social Media Accounts.
@@ -25,6 +28,7 @@ class smAccountUseCases {
console.log('Checking for new Social Media Accounts...')
const messages = await this.adapters.nostr.readGlobalFeed()
console.log('messages: ', messages)
for (let i = 0; i < messages.length; i++) {
const msg = messages[i]
// const { content, id, tags, npub, pubkey } = msg
@@ -73,6 +77,58 @@ class smAccountUseCases {
throw err
}
}
// Get a Social Media Account by Npub.
async getAccountByNpub (npub) {
try {
const SmAccount = await this.adapters.localdb.SmAccount
const account = await SmAccount.findOne({ npub })
return account
} catch (err) {
console.error('Error in smAccount-use-cases.js/getAccountByNpub(): ', err)
throw err
}
}
// Get a Social Media Account by BCH address.
async getAccountByBchAddr (bchAddr) {
try {
const SmAccount = await this.adapters.localdb.SmAccount
const account = await SmAccount.findOne({ bchAddr })
console.log('Account found: ', account)
return account
} catch (err) {
console.error('Error in smAccount-use-cases.js/getAccountByBchAddr(): ', err)
throw err
}
}
// Update metadata about social media accounts being tracked.
async updateSmAccounts () {
try {
console.log('Updating Social Media Accounts...')
const SmAccount = await this.adapters.localdb.SmAccount
const accounts = await SmAccount.find({})
for (let i = 0; i < accounts.length; i++) {
const account = accounts[i]
// console.log(`Account ${i}: `, account)
// Get the number of accounts following this account
const pubkey = account.pubkey
const followList = await this.adapters.nostr.getFollowers({ pubkey })
// console.log(`Follow list ${i}: `, JSON.stringify(followList, null, 2))
// Update the follower count for this account.
const followerCnt = followList.length
await SmAccount.updateOne({ _id: account._id }, { $set: { followerCnt } })
}
return true
} catch (err) {
console.error('Error in smAccount-use-cases.js/updateSmAccounts(): ', err)
throw err
}
}
}
export default smAccountUseCases
+10 -11
View File
@@ -24,19 +24,18 @@ describe('#adapters', () => {
})
describe('#start', () => {
// it('should start the async adapters', async () => {
// // Mock dependencies
// uut.config.getJwtAtStartup = true
// uut.config.useIpfs = true
// uut.config.env = 'not-a-test'
// sandbox.stub(uut.fullStackJwt, 'getJWT').resolves()
// sandbox.stub(uut.fullStackJwt, 'instanceBchjs').resolves()
// sandbox.stub(uut.ipfs, 'start').resolves()
it('should start the async adapters', async () => {
// Mock dependencies
uut.config.getJwtAtStartup = true
uut.config.useIpfs = true
uut.config.env = 'not-a-test'
sandbox.stub(uut.wallet, 'instanceWallet').resolves()
sandbox.stub(uut.ipfs, 'start').resolves()
// const result = await uut.start()
const result = await uut.start()
// assert.equal(result, true)
// })
assert.equal(result, true)
})
it('should not start ipfs on test enviroment', async () => {
// Mock dependencies
+14 -13
View File
@@ -135,18 +135,19 @@ describe('bch', () => {
}
})
// it('should return the merit ', async () => {
// try {
// // Mock live network calls.
// sandbox.stub(uut.msgLib.merit, 'agMerit').resolves(100)
//
// const slpAddr =
// 'simpleledger:qqgnksc6zr4nzxrye69fq625wu2myxey6uh9kzjy96'
// const merit = await uut.getMerit(slpAddr)
// assert.isNumber(merit)
// } catch (err) {
// assert.fail('Unexpected result')
// }
// })
/* it('should return the merit ', async () => {
try {
// Mock live network calls.
sandbox.stub(uut.msgLib.merit, 'agMerit').resolves(100)
const slpAddr =
'simpleledger:qqgnksc6zr4nzxrye69fq625wu2myxey6uh9kzjy96'
const merit = await uut.getMerit(slpAddr)
assert.isNumber(merit)
} catch (err) {
console.log('err: ', err)
assert.fail('Unexpected result')
}
}) */
})
})
+96
View File
@@ -128,4 +128,100 @@ describe('#nostr.js', () => {
assert.equal(result.length, 1)
})
})
describe('#eventId2note', () => {
it('should handle encode error', async () => {
try {
await uut.eventId2note('invalid format')
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Invalid byte sequence')
}
})
it('should handle valid eventId', async () => {
const result = await uut.eventId2note('b1e4a3d5e40b6d0b3f1e2c234b6c9dbf4d6a80e9b3e8a2d765c7f9ed84c9a5ef')
assert.isString(result)
assert.include(result, 'note')
})
})
describe('#pubkey2npub', () => {
it('should handle encode error', async () => {
try {
await uut.pubkey2npub('invalid format')
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Invalid byte sequence')
}
})
it('should handle valid pubkey', async () => {
const result = await uut.pubkey2npub('f5a1b39d2cfae8e10e93b9b4d7ec8248eaf45fbe4a73409d8d4c67015e9729a3')
assert.isString(result)
assert.include(result, 'npub')
})
})
describe('#npub2pubkey', () => {
it('should handle decode error', async () => {
try {
await uut.npub2pubkey('npub17ksm88fvlt5wzr5nhx6d0myzfr40g')
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Invalid checksum')
}
})
it('should handle valid pubkey', async () => {
const result = await uut.npub2pubkey('npub17ksm88fvlt5wzr5nhx6d0myzfr40gha7ffe5p8vdf3nszh5h9x3sdwl7u2')
assert.isObject(result)
assert.property(result, 'type')
assert.property(result, 'data')
assert.equal(result.type, 'npub')
assert.equal(result.data.length, 64)
})
})
describe('#readGlobalFeed', () => {
it('should throw an error if provided limit is wrong', async () => {
try {
await uut.readGlobalFeed({ limit: -5 })
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Limit must be greater than 0')
}
})
it('should read message', async () => {
const result = await uut.readGlobalFeed()
assert.isArray(result)
})
it('should read message with custom limit', async () => {
const result = await uut.readGlobalFeed({ limit: 1 })
assert.isArray(result)
})
})
describe('#getFollowers', () => {
it('should throw an error if no pubkey is provided', async () => {
try {
await uut.getFollowers()
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'pubkey must be a string!')
}
})
it('should get followers', async () => {
const result = await uut.getFollowers({ pubkey: 'f5a1b39d2cfae8e10e93b9b4d7ec8248eaf45fbe4a73409d8d4c67015e9729a3' })
assert.isArray(result)
})
})
})
+378 -26
View File
@@ -9,24 +9,56 @@ import sinon from 'sinon'
import WalletAdapter from '../../../src/adapters/wallet.js'
import { MockBchWallet } from '../mocks/adapters/wallet.js'
import { BchTokenSweepMock } from '../mocks/bch-token-sweep-mock.js'
import offerMockData from '../mocks/use-cases/offer-mock-data.js'
import fs from 'fs'
import * as url from 'url'
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
const testWalletFile = `${__dirname.toString()}/../../../test-wallet.json`
describe('#wallet', () => {
let uut
let sandbox
let testWalletData
before(() => {
// try to remove the test wallet file
try {
fs.unlinkSync(testWalletFile)
} catch (error) {
console.log('error fsUnlinkSync: ', error)
// silently fail
}
})
after(() => {
// try to remove the test wallet file
try {
fs.unlinkSync(testWalletFile)
} catch (error) {
console.log('error fsUnlinkSync: ', error)
// silently fail
}
})
beforeEach(() => {
uut = new WalletAdapter()
uut.BchWallet = MockBchWallet
uut.bchWallet = new MockBchWallet()
uut.BchTokenSweep = BchTokenSweepMock
uut.WALLET_FILE = testWalletFile
uut.bitcoinJs = {
Transaction: {
fromHex: () => { return { } },
SIGHASH_ALL: () => { return { } }
fromHex: () => { return {} },
SIGHASH_ALL: () => { return {} }
},
TransactionBuilder: {
fromTransaction: () => { return { sign: () => { return { } }, build: () => { return { toHex: () => { return 'hex' } } } } }
fromTransaction: () => { return { sign: () => { return {} }, build: () => { return { toHex: () => { return 'hex' } } } } }
},
ECPair: {
fromWIF: () => { return { } }
fromWIF: () => { return {} }
}
}
sandbox = sinon.createSandbox()
@@ -34,6 +66,278 @@ describe('#wallet', () => {
afterEach(() => sandbox.restore())
describe('#openWallet', () => {
it('should create a new wallet when wallet file does not exist', async () => {
// Ensure we open the test file, not the production wallet file.
const result = await uut.openWallet()
testWalletData = result
// console.log('result: ', result)
assert.property(result, 'mnemonic')
assert.property(result, 'privateKey')
assert.property(result, 'publicKey')
assert.property(result, 'cashAddress')
assert.property(result, 'address')
assert.property(result, 'slpAddress')
assert.property(result, 'legacyAddress')
assert.property(result, 'hdPath')
})
it('should open existing wallet file', async () => {
// This test case uses the file created in the previous test case.
// Ensure we open the test file, not the production wallet file.
const result = await uut.openWallet()
// console.log('result: ', result)
assert.property(result, 'mnemonic')
assert.property(result, 'privateKey')
assert.property(result, 'publicKey')
assert.property(result, 'cashAddress')
assert.property(result, 'address')
assert.property(result, 'slpAddress')
assert.property(result, 'legacyAddress')
assert.property(result, 'hdPath')
})
it('should catch and throw an error', async () => {
try {
uut.WALLET_FILE = ''
// Force an error
uut.BchWallet = () => {
}
await uut.openWallet()
// console.log('result: ', result)
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'this.BchWallet is not a constructor')
}
})
})
describe('#instanceWallet', () => {
it('should create an instance of BchWallet with rest-api', async () => {
uut.config.useFullStackCash = true
const result = await uut.instanceWallet(testWalletData)
assert.equal(result.advancedOptions.interface, 'rest-api')
})
it('should create an instance of BchWallet with consumer-api', async () => {
uut.config.useFullStackCash = false
const result = await uut.instanceWallet(testWalletData)
assert.equal(result.advancedOptions.interface, 'consumer-api')
})
it('should catch and throw an error if no mnemonic is provided', async () => {
try {
// Force an error
await uut.instanceWallet({})
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Wallet data is not formatted correctly. Can not read mnemonic in wallet file!')
}
})
})
describe('#incrementNextAddress', () => {
it('should increment the nextAddress property', async () => {
const result = await uut.incrementNextAddress()
assert.equal(result, 2)
})
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox.stub(uut, 'openWallet').rejects(new Error('test error'))
await uut.incrementNextAddress()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#getKeyPair', () => {
it('should return an object with a key pair', async () => {
// Ensure we open the test file, not the production wallet file.
// uut.WALLET_FILE = testWalletFile
uut.config.walletFile = testWalletFile
const result = await uut.getKeyPair()
// console.log('result: ', result)
assert.property(result, 'cashAddress')
assert.property(result, 'wif')
assert.property(result, 'hdIndex')
})
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox
.stub(uut, 'incrementNextAddress')
.rejects(new Error('test error'))
await uut.getKeyPair()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#generateSignature', () => {
it('should sign message', async () => {
const result = await uut.generateSignature('unit test message')
// console.log('result: ', result)
assert.isString(result)
})
it('should catch and throw an error', async () => {
try {
await uut.generateSignature()
} catch (err) {
assert.include(err.message, 'message is required!')
}
})
})
describe('#generatePartialTx', () => {
it('should generate tx for fungible token', async () => {
const utxoInfo = {
txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
vout: 0,
sats: 1000,
wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3'
}
sandbox.stub(uut.bchWallet, 'getTxData').resolves([offerMockData.fungibleTokenData01.genesisData])
const result = await uut.generatePartialTx(offerMockData.fungibleOffer01, utxoInfo)
assert.isString(result)
})
it('should generate tx for nft', async () => {
const utxoInfo = {
txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
vout: 0,
sats: 1000,
wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3'
}
sandbox.stub(uut.bchWallet, 'getTxData').resolves([offerMockData.nftTokenData01.genesisData])
const result = await uut.generatePartialTx(offerMockData.nftOffer01, utxoInfo)
// console.log('result: ', result)
assert.isString(result)
})
it('should handle unknown token type', async () => {
try {
const utxoInfo = {
txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
vout: 0,
sats: 1000,
wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3'
}
const offerMock = Object.assign({}, offerMockData.nftOffer01)
offerMock.tokenType = 100
await uut.generatePartialTx(offerMock, utxoInfo)
} catch (err) {
assert.include(err.message, 'Unknown token type')
}
})
it('should handle multiple outputs', async () => {
const utxoInfo = {
txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
vout: 0,
sats: 1000,
wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3'
}
sandbox.stub(uut.bchWallet, 'getTxData').resolves([offerMockData.nftTokenData01.genesisData])
sandbox.stub(uut.bchWallet.bchjs.SLP.NFT1, 'generateNFTChildSendOpReturn').returns({ outputs: 5 })
const res = await uut.generatePartialTx(offerMockData.nftOffer01, utxoInfo)
assert.isUndefined(res)
})
})
describe('#moveTokens', () => {
it('should move tokens', async () => {
sandbox.stub(uut, 'getKeyPair').resolves({
cashAddress: 'cashAddress',
wif: 'wif',
hdIndex: 11
})
const input = {
tokenId: uut.bchWallet.utxos.utxoStore.slpUtxos.type1.tokens[0].tokenId,
qty: 1
}
const res = await uut.moveTokens(input)
assert.isObject(res)
})
it('should throw an error if the tokenId is not found', async () => {
try {
sandbox.stub(uut, 'getKeyPair').resolves({
cashAddress: 'cashAddress',
wif: 'wif',
hdIndex: 11
})
const input = {
tokenId: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
qty: 1
}
await uut.moveTokens(input)
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'tokenId not found!')
}
})
it('should throw an error if the tokenId is not provided', async () => {
try {
await uut.moveTokens({
qty: 1
})
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'tokenId must be a string!')
}
})
it('should throw an error if the qty is not provided', async () => {
try {
await uut.moveTokens({
tokenId: 'tokenId'
})
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'qty must be a number!')
}
})
})
describe('#moveBch', () => {
it('should move bch', async () => {
sandbox.stub(uut, 'getKeyPair').resolves({
cashAddress: 'cashAddress',
wif: 'wif',
hdIndex: 11
})
const res = await uut.moveBch(100)
assert.isObject(res)
})
it('should throw an error if the amountSat is not provided', async () => {
try {
sandbox.stub(uut, 'getKeyPair').resolves({
cashAddress: 'cashAddress',
wif: 'wif',
hdIndex: 11
})
await uut.moveBch()
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'amountSat is required!')
}
})
})
describe('#moveTokensFromCustomWallet', () => {
it('should move tokens from a custom wallet', async () => {
sandbox.stub(uut, 'getKeyPair').resolves({
@@ -100,28 +404,27 @@ describe('#wallet', () => {
}
})
})
describe('#deseralizeTx', () => {
it('should throw an error if the hex is not provided', async () => {
try {
await uut.deseralizeTx()
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'hex must be a string!')
}
})
it('should decode hex', async () => {
try {
sandbox.stub(uut.bchWallet.bchjs.RawTransactions, 'decodeRawTransaction').resolves({})
const hex = '020000000286b2959cccdb80163b68bb1360abac8e0228ba7b70c779443168cc65c7c42d820100000000ffffffff879b066729eda979f1f062c83b7eccdb9e77f49b417e4723864b3861bf061c24000000006b483045022100b99c6161e18e4e34c1e438b6b03e808703695f217b30da42c3f787798a9f40cc02206d26b4f6799735779d4fac823bc60d276fbb3cea91c66dbfa136cf5b1bd0d8324121028dcd38a58c8295dbd105ef524014f77af4cddfa19b26c50926b4aaeebd95c7ceffffffff030000000000000000376a04534c500001010453454e4420a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b208000000000000006422020000000000001976a914bef6c5180648165704f22e71a1b011ba56336aee88acf81d0000000000001976a914c1f16a3876f8fbe3701a66d3cb3b9c7abb07a06c88ac00000000'
const result = await uut.deseralizeTx(hex)
assert.isObject(result)
} catch (error) {
assert.fail('Unexpected code path')
}
})
})
describe('#completeTx', () => {
// TODO: This test needs to be refactored to use the new instanceWallet() method for loading the user wallet.
// it('should complete a transaction', async () => {
// sandbox.stub(uut, 'getKeyPair').resolves({
// cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
// wif: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ',
// hdIndex: 11
// })
// sandbox.stub(uut, 'deseralizeTx').resolves({
// txid: 'complete tx id result'
// })
// sandbox.stub(uut.retryQueue, 'addToQueue').resolves('complete tx id result')
// const hex = 'hex'
// const hdIndex = 11
// const mnemonic = 'course abstract aerobic deer try switch turtle diet fence affair butter top'
// const txid = await uut.completeTx(hex, hdIndex, mnemonic)
// assert.equal(txid, 'complete tx id result')
// })
it('should throw an error if the hex is not provided', async () => {
try {
const hdIndex = 11
@@ -134,11 +437,60 @@ describe('#wallet', () => {
it('should throw an error if the hdIndex is not provided', async () => {
try {
const hex = 'hex'
await uut.completeTx(hex, null)
await uut.completeTx(hex)
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'hdIndex must be a non-negative number!')
}
})
it('should throw an error if the mnemonic is not provided', async () => {
try {
const hex = 'hex'
const hdIndex = 11
await uut.completeTx(hex, hdIndex)
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'mnemonic must be a string!')
}
})
it('should complete a transaction', async () => {
sandbox.stub(uut, 'getKeyPair').resolves({
cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
wif: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ',
hdIndex: 11
})
sandbox.stub(uut, 'deseralizeTx').resolves({
txid: 'complete tx id result'
})
sandbox.stub(uut.retryQueue, 'addToQueue').resolves('complete tx id result')
const hex = 'hex'
const hdIndex = 11
const mnemonic = 'course abstract aerobic deer try switch turtle diet fence affair butter top'
const txid = await uut.completeTx(hex, hdIndex, mnemonic)
assert.equal(txid, 'complete tx id result')
})
})
describe('#reClaimTokens', () => {
it('should re-claim tokens', async () => {
const orderData = {
hdIndex: 11,
tokenId: 'tokenId',
numTokens: 1
}
const res = await uut.reclaimTokens(orderData)
assert.equal(res, 'fakeTxid')
})
it('should handle error if order hd index is not provided', async () => {
try {
const orderData = {}
await uut.reclaimTokens(orderData)
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'orderData.hdIndex is required!')
}
})
})
})
@@ -65,4 +65,51 @@ describe('#Webhook-Adapter', () => {
assert.isString(result.id)
})
})
describe('#deleteWebhook', () => {
it('should throw error if input is not provided', async () => {
try {
await uut.deleteWebhook()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'url must be a string')
}
})
it('should delete webhook', async () => {
sandbox.stub(uut.axios, 'delete').resolves({ data: { success: true } })
const url = 'https://test.com/my-webhook'
const result = await uut.deleteWebhook(url)
assert.isObject(result)
assert.property(result, 'success')
})
})
describe('#waitUntilSuccess', () => {
it('should handle error', async () => {
try {
sandbox.stub(uut, 'sleep').throws(new Error('test error'))
sandbox.stub(uut, 'deleteWebhook')
.onCall(0).throws(new Error('test error'))
sandbox.stub(uut, 'createWebhook')
.onCall(0).throws(new Error('test error'))
.onCall(1).resolves({ data: { success: true } })
await uut.waitUntilSuccess()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should wait until success', async () => {
uut.sleepTime = 100
sandbox.stub(uut, 'deleteWebhook')
.onCall(0).throws(new Error('test error'))
sandbox.stub(uut, 'createWebhook')
.onCall(0).throws(new Error('test error'))
.onCall(1).resolves({ data: { success: true } })
const result = await uut.waitUntilSuccess()
assert.isTrue(result)
})
})
})
@@ -91,25 +91,143 @@ describe('#Offer-REST-Router', () => {
// assert.equal(ctx.body.hash, 'testHash')
})
// it('should catch and throw an error', async () => {
// try {
// ctx.request.body = {
// offer: {}
// }
//
// // Force an error
// sandbox
// .stub(uut.useCases.offer, 'createOffer')
// .rejects(new Error('test error'))
//
// await uut.createOffer(ctx)
//
// assert.fail('Unexpected code path')
// } catch (err) {
// // console.log('err: ', err)
// assert.include(err.message, 'test error')
// }
// })
it('should catch and throw an error', async () => {
try {
ctx.request.body = {
offer: {}
}
// Force an error
sandbox
.stub(uut.useCases.offer, 'createOffer')
.rejects(new Error('test error'))
await uut.createOffer(ctx)
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'test error')
}
})
})
describe('#listOffers', () => {
it('should list offers', async () => {
ctx.params = { page: 0 }
sandbox.stub(uut.useCases.offer, 'listOffers').resolves([])
await uut.listOffers(ctx)
assert.isArray(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { }
sandbox
.stub(uut.useCases.offer, 'listOffers')
.throws(new Error('test error'))
await uut.listOffers(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#listNftOffers', () => {
it('should list nft offers', async () => {
ctx.params = { page: 0 }
sandbox.stub(uut.useCases.offer, 'listNftOffers').resolves([])
await uut.listNftOffers(ctx)
assert.isArray(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { page: 0 }
sandbox
.stub(uut.useCases.offer, 'listNftOffers')
.throws(new Error('test error'))
await uut.listNftOffers(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#listFungibleOffers', () => {
it('should list fungible offers', async () => {
ctx.params = { page: 0 }
sandbox.stub(uut.useCases.offer, 'listFungibleOffers').resolves([])
await uut.listFungibleOffers(ctx)
assert.isArray(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { page: 0 }
sandbox
.stub(uut.useCases.offer, 'listFungibleOffers')
.throws(new Error('test error'))
await uut.listFungibleOffers(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#takeOffer', () => {
it('should take an offer', async () => {
ctx.request.body = {
nostrEventId: 'testEventId'
}
sandbox.stub(uut.useCases.offer, 'takeOffer').resolves({ eventId: 'testEventId', noteId: 'testNoteId' })
await uut.takeOffer(ctx)
assert.isObject(ctx.body)
assert.property(ctx.body, 'eventId')
assert.property(ctx.body, 'noteId')
})
it('should catch and throw an error', async () => {
try {
ctx.request.body = {
nostrEventId: 'testEventId'
}
sandbox
.stub(uut.useCases.offer, 'takeOffer')
.throws(new Error('test error'))
await uut.takeOffer(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#listOffersByAddress', () => {
it('should list offers by address', async () => {
ctx.params = { addr: 'testAddress' }
sandbox.stub(uut.useCases.offer, 'listOffersByAddress').resolves([])
await uut.listOffersByAddress(ctx)
assert.isArray(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { addr: 'testAddress' }
sandbox
.stub(uut.useCases.offer, 'listOffersByAddress')
.throws(new Error('test error'))
await uut.listOffersByAddress(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#handleError', () => {
@@ -101,7 +101,47 @@ describe('#Order-REST-Router', () => {
}
})
})
describe('#listOrders', () => {
it('should list orders', async () => {
ctx.params = { page: 0 }
sandbox.stub(uut.useCases.order, 'listOrders').resolves([])
await uut.listOrders(ctx)
assert.isArray(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { page: 0 }
sandbox.stub(uut.useCases.order, 'listOrders').throws(new Error('test error'))
await uut.listOrders(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#deleteOrder', () => {
it('should delete an order', async () => {
ctx.request.body = {
nostrEventId: 'testEventId'
}
sandbox.stub(uut.useCases.order, 'deleteOrder').resolves('testTxid')
await uut.deleteOrder(ctx)
assert.equal(ctx.body.txid, 'testTxid')
})
it('should catch and throw an error', async () => {
try {
ctx.request.body = {
nostrEventId: 'testEventId'
}
sandbox.stub(uut.useCases.order, 'deleteOrder').throws(new Error('test error'))
await uut.deleteOrder(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#handleError', () => {
it('should still throw error if there is no message', () => {
try {
@@ -74,4 +74,13 @@ 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)
})
})
})
@@ -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', () => {
// it('should route to controller', async () => {
// sandbox.stub(uut.validators, 'ensureUser').resolves(true)
// const spy = sandbox.stub(uut.userRESTController, 'getUsers').resolves(true)
describe('#getAll', () => {
it('should route to controller', async () => {
sandbox.stub(uut.validators, 'ensureAdmin').resolves(true)
const spy = sandbox.stub(uut.userRESTController, 'getUsers').resolves(true)
// await uut.getAll()
// assert.isTrue(spy.calledOnce)
// })
// })
await uut.getAll()
assert.isTrue(spy.calledOnce)
})
})
// describe('#getById', () => {
// it('should route to controller', async () => {
// sandbox.stub(uut.validators, 'ensureUser').resolves(true)
// const spy = sandbox.stub(uut.userRESTController, 'getUser').resolves(true)
describe('#getById', () => {
it('should route to controller', async () => {
sandbox.stub(uut.validators, 'ensureTargetUserOrAdmin').resolves(true)
const spy = sandbox.stub(uut.userRESTController, 'getUser').resolves(true)
// await uut.getById()
// assert.isTrue(spy.calledOnce)
// })
// })
await uut.getById()
assert.isTrue(spy.calledOnce)
})
})
describe('#updateUser', () => {
it('should route to controller', async () => {
@@ -130,12 +130,12 @@ describe('#Timer-Controllers', () => {
assert.equal(result, true)
})
// it('should return false on error', async () => {
// sandbox.stub(uut.useCases.usage, 'cleanUsage').throws(new Error('test error'))
// const result = await uut.cleanUsage()
it('should return false on error', async () => {
sandbox.stub(uut.useCases.usage, 'cleanUsage').throws(new Error('test error'))
const result = await uut.cleanUsage()
// assert.equal(result, false)
// })
assert.equal(result, false)
})
})
describe('#backupUsage', () => {
@@ -154,4 +154,37 @@ describe('#Timer-Controllers', () => {
assert.equal(result, false)
})
})
describe('#newSmAccounts', () => {
it('should kick off the Use Case', async () => {
const result = await uut.newSmAccounts()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.smAccount, 'checkForNewSmAccounts').throws(new Error('test error'))
// sandbox.stub(uut.useCases.usage, 'saveUsage').throws(new Error('test error'))
const result = await uut.newSmAccounts()
assert.equal(result, false)
})
})
describe('#updateSmAccounts', () => {
it('should kick off the Use Case', async () => {
const result = await uut.updateSmAccounts()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.smAccount, 'updateSmAccounts').throws(new Error('test error'))
// sandbox.stub(uut.useCases.usage, 'saveUsage').throws(new Error('test error'))
const result = await uut.updateSmAccounts()
assert.equal(result, false)
})
})
})
+228 -1
View File
@@ -237,6 +237,113 @@ describe('#Offer-Entity', () => {
}
})
it('should throw an error if makerAddr is not included', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorPercentage: 10
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'makerAddr' must be a string.")
}
})
it('should throw an error if makerAddr is not a string', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 1234567890
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'makerAddr' must be a string.")
}
})
it('should throw an error if tokenType is not included', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorPercentage: 10
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'tokenType' must be a number.")
}
})
it('should throw an error if tokenType is not a number', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
tokenType: 'notANumber'
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'tokenType' must be a number.")
}
})
it('should throw an error if nostrEventId is not included', () => {
try {
const offerData = {
@@ -267,6 +374,124 @@ describe('#Offer-Entity', () => {
}
})
it('should throw an error if operatorAddress is not included', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
tokenType: 1,
nostrEventId: 'test',
operatorPercentage: 10
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'operatorAddress' must be a string.")
}
})
it('should throw an error if operatorAddress is not a string', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
tokenType: 1,
nostrEventId: 'test',
operatorAddress: 1234567890,
operatorPercentage: 10
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'operatorAddress' must be a string.")
}
})
it('should throw an error if operatorPercentage is not included', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
tokenType: 1,
nostrEventId: 'test',
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00'
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'operatorPercentage' must be a number.")
}
})
it('should throw an error if operatorPercentage is not a number', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
tokenType: 1,
nostrEventId: 'test',
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorPercentage: 'notANumber'
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'operatorPercentage' must be a number.")
}
})
it('should validate a new offer', () => {
const offerObj = {
appId: 'swapTest555',
@@ -286,7 +511,9 @@ describe('#Offer-Entity', () => {
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
ticker: 'TROUT',
tokenType: 1,
nostrEventId: 'test'
nostrEventId: 'test',
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorPercentage: 10
},
timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM',
+66
View File
@@ -130,5 +130,71 @@ describe('#Order-Entity', () => {
assert.include(err.message, "Property 'numTokens' must be a number.")
}
})
it('should throw an error if numTokens is not a number', () => {
try {
const data = {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 'notANumber'
}
uut.inputValidate(data)
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'numTokens' must be a number.")
}
})
it('should throw an error if makerAddress is not included', () => {
try {
const data = {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1000
}
uut.inputValidate(data)
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'makerAddr' must be a string.")
}
})
it('should throw an error if makerAddress is not a string', () => {
try {
const data = {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1000,
makerAddr: 1234567890
}
uut.inputValidate(data)
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'makerAddr' must be a string.")
}
})
it('should return the correct data', () => {
const data = {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1000,
makerAddr: 'fakeAddr'
}
const result = uut.inputValidate(data)
assert.deepEqual(result, data)
})
})
})
+15 -1
View File
@@ -150,6 +150,18 @@ const localdb = {
static find () {}
static findOne () {}
async save () {
return {}
}
},
SmAccount: class SmAccount {
constructor (obj) {}
static findById () {}
static find () {}
static findOne () {}
static updateOne () {}
async save () {
return {}
}
@@ -202,7 +214,9 @@ const p2wdb = {
const nostr = {
post: async () => {return true },
read: async () => { return true },
eventId2note: () => { return 'testNoteId' }
eventId2note: () => { return 'testNoteId' },
readGlobalFeed: async () => { return [] },
getFollowers: async () => { return [] }
}
export default { ipfs, localdb, bch, wallet, p2wdb, bchjs, nostr}
+22 -1
View File
@@ -12,8 +12,18 @@ const mockWallet = {
nextAddress: 1
};
class MockBchWallet {
class AdapterRoute {
constructor() {
this.sendTx = async () => {
return 'fakeTxid';
}
}
}
class MockBchWallet {
constructor(mnemonic, advancedOptions) {
this.advancedOptions = advancedOptions
this.walletInfoPromise = true;
this.walletInfo = mockWallet;
this.initialize = async () => {}
@@ -24,6 +34,9 @@ class MockBchWallet {
this.sendTokens = async () => {
return 'fakeTxid';
};
this.send = async () => {
return 'fakeTxid';
};
this.utxoIsValid =async ()=>{}
this.getUtxos = async () => { };
this.getBalance = async () => { };
@@ -33,7 +46,15 @@ class MockBchWallet {
tokenTicker: 'TROUT'
}];
};
this.getKeyPair = async () => {
return {
cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
wif: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ',
hdIndex: 11
}
};
this.optimize = async () => { };
this.ar = new AdapterRoute()
// Environment variable is used by wallet-balance.unit.js to force an error.
if (process.env.NO_UTXO) {
this.utxos = {};
+27
View File
@@ -0,0 +1,27 @@
class BchTokenSweepMock {
constructor(wif, privateKey, bchWallet, fee, cashAddress) {
this.wif = wif
this.privateKey = privateKey
this.bchWallet = bchWallet
this.fee = fee
this.cashAddress = cashAddress
}
populateObjectFromNetwork() {
return {
success: true,
txid: 'txid'
}
}
sweepTo() {
return {
success: true,
txid: 'txid'
}
}
}
export { BchTokenSweepMock }
export default {
BchTokenSweepMock
}
+42
View File
@@ -0,0 +1,42 @@
const messagesMock = [
{
content: JSON.stringify("I'm testing so hard!!!"),
id: '1',
tags: [['u', 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477']],
npub: 'npub1234567890',
pubkey: '1234567890',
}
]
const smAccountsMock = [
{
npub: 'npub1234567890',
bchAddr: 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477',
pubkey: '1234567890',
pfp: '',
createdAt: new Date(),
updatedAt: new Date(),
followerCnt: 0,
followingCnt: 0,
posts: 0,
likes: 0,
comments: 0,
reposts: 0
},
{
npub: 'npub1234567891',
bchAddr: 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478',
pubkey: '1234567891',
pfp: '',
createdAt: new Date(),
updatedAt: new Date(),
followerCnt: 0,
followingCnt: 0,
posts: 0,
likes: 0,
comments: 0,
reposts: 0
}
]
export default { messagesMock, smAccountsMock }
+46
View File
@@ -50,6 +50,27 @@ class Offer {
async loadOffers(){
}
async listOffers() {
return []
}
async listNftOffers() {
return []
}
async listFungibleOffers() {
return []
}
async takeOffer() {
return []
}
async listOffersByAddress() {
return []
}
async flagOffer() {
return {}
}
async acceptCounterOffer() {
return {}
}
}
class Order {
@@ -59,6 +80,12 @@ class Order {
async removeStaleOrders(){
return {}
}
async listOrders() {
return []
}
async deleteOrder() {
return 'testTxid'
}
}
class UsageUseCaseMock {
@@ -87,6 +114,24 @@ class UsageUseCaseMock {
}
}
class SmAccountUseCaseMock {
async listAccounts() {
return []
}
async getAccountByNpub() {
return {}
}
async getAccountByBchAddr() {
return {}
}
async checkForNewSmAccounts() {
return true
}
async updateSmAccounts() {
return true
}
}
class UseCasesMock {
constuctor(localConfig = {}) {
// this.user = new UserUseCaseMock(localConfig)
@@ -97,6 +142,7 @@ class UseCasesMock {
offer = new Offer()
order = new Order()
usage = new UsageUseCaseMock()
smAccount = new SmAccountUseCaseMock()
}
export default UseCasesMock;
+37 -1
View File
@@ -115,6 +115,7 @@ const fungibleTokenData01 = {
"documentUri": "troutsblog.com",
"documentHash": "",
"decimals": 2,
"tokenDecimals": 2,
"mintBatonIsActive": true,
"tokensInCirculationBN": "100097954686",
"tokensInCirculationStr": "100097954686",
@@ -141,10 +142,38 @@ const offerMockData = {
utxoVout: 0,
makerAddr: 'address',
tokenType: 1,
nostrEventId: 'test'
nostrEventId: 'test',
operatorAddress: 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478',
operatorPercentage: 10
}
}
const deserealizeTxMockNoOperatorOut = {
//...
vout: [
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
//...
]
}
const deserealizeTxMock = {
//...
vout: [
@@ -166,6 +195,12 @@ const deserealizeTxMock = {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
//...
]
}
@@ -178,5 +213,6 @@ export default {
fungibleOffer01,
fungibleTokenData01,
offerMockData,
deserealizeTxMockNoOperatorOut,
deserealizeTxMock
};
+175 -12
View File
@@ -5,6 +5,7 @@
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
import RetryQueue from '@chris.troutner/retry-queue'
// Local support libraries
// const testUtils = require('../../utils/test-utils')
@@ -30,6 +31,7 @@ describe('#offer-use-case', () => {
const order = new OrderUseCase({ adapters })
uut = new OfferLib({ adapters, order })
uut.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 1 })
})
afterEach(() => sandbox.restore())
@@ -85,6 +87,15 @@ describe('#offer-use-case', () => {
assert.isFalse(result)
})
it('should return false if offer already processed', async () => {
const offerObj = mockData.offerMockData
uut.seenOffers.push(offerObj.data.nostrEventId)
// Mock dependencies
const result = await uut.createOffer(offerObj)
assert.isFalse(result)
})
it('should return false for invalid utxo', async () => {
const offerObj = mockData.offerMockData
@@ -334,6 +345,14 @@ describe('#offer-use-case', () => {
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
assert.isFalse(result)
})
it('should fetch alternative url', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.axios, 'get').onCall(0).throws(new Error())
.onCall(1).resolves({ data: { nsfw: true } })
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
assert.isTrue(result)
})
})
describe('#listOffers', () => {
@@ -597,7 +616,7 @@ describe('#offer-use-case', () => {
})
})
describe('#findOrderByEvent', () => {
describe('#findOfferByEvent', () => {
it('should throw an error if hash is not provided', async () => {
try {
await uut.findOfferByEvent()
@@ -629,6 +648,29 @@ describe('#offer-use-case', () => {
})
})
describe('#listOffersByAddress', () => {
it('should throw an error if order is not found!', async () => {
try {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error'))
await uut.listOffersByAddress('eventId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should return offer by eventId', async () => {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').resolves([])
const result = await uut.listOffersByAddress()
assert.isArray(result)
})
})
describe('#flagOffer', () => {
it('should throw an error if input is not provided', async () => {
try {
@@ -687,23 +729,84 @@ describe('#offer-use-case', () => {
it('should skip internal function errors ', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.nostr, 'read').resolves([mockData.offerMockData])
const nostrReadMock = [{ content: JSON.stringify(mockData.offerMockData) }]
sandbox.stub(uut.adapters.nostr, 'read').resolves(nostrReadMock)
await uut.loadOffers()
})
it('should review and load offers', async () => {
it('should createOffer ', async () => {
mockData.offerMockData.data.dataType = 'offer'
// Mock dependencies
sandbox.stub(uut.adapters.nostr, 'read').resolves([JSON.stringify(mockData.offerMockData)])
const nostrReadMock = [{ content: JSON.stringify(mockData.offerMockData) }]
sandbox.stub(uut.adapters.nostr, 'read').resolves(nostrReadMock)
const spy = sandbox.stub(uut, 'createOffer').resolves(true)
await uut.loadOffers()
assert.isTrue(spy.calledOnce)
})
it('should accept counter Offer ', async () => {
mockData.offerMockData.data.dataType = 'counter-offer'
// Mock dependencies
const nostrReadMock = [{ content: JSON.stringify(mockData.offerMockData) }]
sandbox.stub(uut.adapters.nostr, 'read').resolves(nostrReadMock)
const spy = sandbox.stub(uut, 'acceptCounterOffer').resolves(true)
await uut.loadOffers()
assert.isTrue(spy.calledOnce)
})
it('should skip error inside loop', async () => {
// Mock dependencies
const nostrReadMock = [{ content: '' }]
sandbox.stub(uut.adapters.nostr, 'read').resolves(nostrReadMock)
await uut.loadOffers()
})
})
describe('#acceptCounterOffer', () => {
it('should return false if order already processed', async () => {
const offerObj = mockData.offerMockData
uut.seenOffers.push(offerObj.data.nostrEventId)
// Mock dependencies
const result = await uut.acceptCounterOffer(offerObj)
assert.isFalse(result)
})
it('should return if order is not found!', async () => {
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').throws(new Error('test error'))
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').throws(new Error('test error'))
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.equal(result, 'N/A')
})
it('should return if utxo cant be validated', async () => {
// Mock dependencies
const mock = Object.assign({}, mockData.offerMockData.data)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').throws(new Error('test error'))
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.equal(result, 'N/A')
})
it('should handle axios error', async () => {
// Mock dependencies
const mock = Object.assign({}, mockData.offerMockData.data)
const stubErr = new Error()
stubErr.isAxiosError = true
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').throws(stubErr)
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.equal(result, 'N/A')
})
it('should return if utxo is invalid', async () => {
// Mock dependencies
const mock = Object.assign({}, mockData.offerMockData.data)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.equal(result, 'N/A')
@@ -719,7 +822,7 @@ describe('#offer-use-case', () => {
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
const result = await uut.acceptCounterOffer({ data: {} })
console.log('result: ', result)
assert.fail('unexpected code path')
} catch (err) {
@@ -734,7 +837,6 @@ describe('#offer-use-case', () => {
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mockData.offerMockData.data)
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
@@ -746,6 +848,44 @@ describe('#offer-use-case', () => {
}
})
it('should skip transactions that do not have an output for the operator', async () => {
// Mock data
const mock = Object.assign({}, mockData.offerMockData.data)
mock.makerAddr = 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477' // Unknow Adress
mock.rateInBaseUnit = 0
mock.numTokens = 0
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMockNoOperatorOut)
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.equal(result, 'N/A')
})
it('should skip transactions if operator address does not match', async () => {
try {
// Mock data
const mock = Object.assign({}, mockData.offerMockData.data)
mock.makerAddr = mockData.deserealizeTxMockNoOperatorOut.vout[2].scriptPubKey.addresses[0]
mock.operatorAddress = 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477'
mock.rateInBaseUnit = 0
mock.numTokens = 0
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
await uut.acceptCounterOffer({ data: {} })
assert.fail('unexpected code path')
} catch (error) {
assert.include(error.message, 'The Counter Offer has an output address of')
}
})
it('should handle error for wrong transaction output address', async () => {
try {
// Mock data
@@ -761,7 +901,7 @@ describe('#offer-use-case', () => {
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
await uut.acceptCounterOffer({ data: { /** .... */ } })
await uut.acceptCounterOffer({ data: {} })
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'The Counter Offer has an output address of')
@@ -769,21 +909,44 @@ describe('#offer-use-case', () => {
}
})
it('should return tx id', async () => {
it('should handle error if operator sats to receive cant be calculated', async () => {
// Mock data
const mock = Object.assign({}, mockData.offerMockData.data)
mock.makerAddr = mockData.deserealizeTxMock.vout[2].scriptPubKey.addresses[0] // Maker Address
mock.makerAddr = mockData.deserealizeTxMock.vout[2].scriptPubKey.addresses[0]
mock.operatorAddress = mockData.deserealizeTxMock.vout[3].scriptPubKey.addresses[0]
mock.rateInBaseUnit = 0
mock.numTokens = 0
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
const result = await uut.acceptCounterOffer({ data: {} })
assert.equal(result, 'N/A')
})
it('should return tx id', async () => {
// Mock data
const mock = Object.assign({}, mockData.offerMockData.data)
mock.makerAddr = mockData.deserealizeTxMock.vout[2].scriptPubKey.addresses[0]
mock.operatorAddress = mockData.deserealizeTxMock.vout[3].scriptPubKey.addresses[0]
mock.rateInBaseUnit = 0
mock.numTokens = 0
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').onCall(0).returns(0).onCall(1).returns(1000)
sandbox.stub(uut.adapters.localdb.Users, 'findById').resolves({ mnemonic: 'test' })
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
//
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
const result = await uut.acceptCounterOffer({ data: {} })
assert.isString(result)
assert.notEqual('N/A')
})
})
})
@@ -158,6 +158,7 @@ describe('#order-use-case', () => {
describe('#createOrder', () => {
it('should create an order and return the hash', async () => {
uut.config.useFullStackCash = true
const entryObj = {
lokadId: 'SWP',
messageType: 1,
@@ -185,6 +186,7 @@ describe('#order-use-case', () => {
assert.property(result, 'eventId')
assert.property(result, 'noteId')
})
it('should create an order with consumer-api', async () => {
uut.config.useFullStackCash = false
const entryObj = {
@@ -214,6 +216,7 @@ describe('#order-use-case', () => {
assert.property(result, 'eventId')
assert.property(result, 'noteId')
})
it('should throw error if user is not found', async () => {
try {
const entryObj = {
@@ -384,4 +387,55 @@ describe('#order-use-case', () => {
assert.isString(result)
})
})
describe('#findOrderByUtxo', () => {
it('should throw an error if data is not provided', async () => {
try {
await uut.findOrderByUtxo({})
} catch (err) {
assert.include(err.message, 'Cannot read properties of undefined')
}
})
it('should throw an error if order is not found', async () => {
try {
sandbox.stub(uut.OrderModel, 'findOne').resolves(null)
const offerData = {
data: {
utxoTxid: 'utxoTxid',
utxoVout: 'utxoVout'
}
}
await uut.findOrderByUtxo(offerData)
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'order not found')
}
})
it('should throw an error if order event id is not found', async () => {
try {
sandbox.stub(uut.OrderModel, 'findOne').resolves({ nostrEventId: null })
const offerData = {
data: {
utxoTxid: 'utxoTxid',
utxoVout: 'utxoVout'
}
}
await uut.findOrderByUtxo(offerData)
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'order not found')
}
})
it('should return order by utxo', async () => {
sandbox.stub(uut.OrderModel, 'findOne').resolves({ toObject: () => { return {} }, nostrEventId: 'nostrEventId' })
const offerData = {
data: {
utxoTxid: 'utxoTxid',
utxoVout: 'utxoVout'
}
}
const result = await uut.findOrderByUtxo(offerData)
assert.isObject(result)
})
})
})
+166
View File
@@ -0,0 +1,166 @@
/*
Unit tests for the SM Account Use Case library.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Unit under test (uut)
import SMAccountLib from '../../../src/use-cases/smAccount-use-cases.js'
import SMMock from '../mocks/social-media-mocks.js'
// Local support libraries
import adapters from '../mocks/adapters/index.js'
describe('#sm-account-use-case', () => {
let uut
let sandbox
before(async () => {
// Delete all previous users in the database.
// await testUtils.deleteAllUsers()
})
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new SMAccountLib({ adapters })
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new SMAccountLib()
assert.fail('Unexpected code path')
console.log(uut) // linter
} catch (err) {
assert.include(
err.message,
'Instance of adapters must be passed in when instantiating SM Account Use Cases library.'
)
}
})
})
describe('#checkForNewSmAccounts', () => {
it('check for new SM accounts', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.nostr, 'readGlobalFeed').resolves(SMMock.messagesMock)
const result = await uut.checkForNewSmAccounts()
assert.isTrue(result)
})
it('should skip existing SM accounts', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'findOne').resolves({ npub: SMMock.messagesMock[0].npub })
sandbox.stub(uut.adapters.nostr, 'readGlobalFeed').resolves(SMMock.messagesMock)
const result = await uut.checkForNewSmAccounts()
assert.isTrue(result)
})
it('should throw an error if the readGlobalFeed fails', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.nostr, 'readGlobalFeed').rejects(new Error('Failed to read global feed'))
try {
await uut.checkForNewSmAccounts()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Failed to read global feed')
}
})
})
describe('#listAccounts', () => {
it('should return a list of SM accounts', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'find').callsFake(() => {
return {
skip: () => {
return {
limit: () => {
return SMMock.messagesMock
}
}
}
}
})
const result = await uut.listAccounts()
assert.isArray(result)
})
it('should throw an error if the find fails', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'find').throws(new Error('Failed to find SM accounts'))
try {
await uut.listAccounts()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Failed to find SM accounts')
}
})
})
describe('#getAccountByNpub', () => {
it('should return a SM account by npub', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'findOne').resolves(SMMock.messagesMock[0])
const result = await uut.getAccountByNpub(SMMock.messagesMock[0].npub)
assert.isObject(result)
})
it('should catch error', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'findOne').throws(new Error('Failed to find SM account by npub'))
try {
await uut.getAccountByNpub(SMMock.messagesMock[0].npub)
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Failed to find SM account by npub')
}
})
})
describe('#getAccountByBchAddr', () => {
it('should return a SM account by bch addr', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'findOne').resolves(SMMock.messagesMock[0])
const result = await uut.getAccountByBchAddr('bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477')
assert.isObject(result)
})
it('should catch error', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'findOne').throws(new Error('Failed to find SM account by bch addr'))
try {
await uut.getAccountByBchAddr('bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Failed to find SM account by bch addr')
}
})
})
describe('#updateSmAccounts', () => {
it('should update the SM accounts', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'find').resolves(SMMock.smAccountsMock)
const result = await uut.updateSmAccounts()
assert.isTrue(result)
})
it('should catch error', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.localdb.SmAccount, 'find').throws(new Error('Failed to find SM accounts'))
try {
await uut.updateSmAccounts()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Failed to find SM accounts')
}
})
})
})