Compare commits

...
31 Commits
Author SHA1 Message Date
Chris Troutner ac83827fca Merge branch 'master' into fullstack 2025-12-04 15:29:13 +00:00
Chris Troutner 0811024030 Merge pull request #123 from Permissionless-Software-Foundation/dh-token-data
feat(offer): Added tokenData to offer model
2025-12-04 08:08:45 -07:00
Daniel Gonzalez 737d35463d feat(offer): Added tokenData to offer model 2025-12-03 19:21:40 -04:00
Chris Troutner 2ed1eaba9c Merge branch 'master' into fullstack 2025-11-26 14:16:56 +00:00
Chris Troutner 6dddfbdcb6 Merge pull request #122 from Permissionless-Software-Foundation/dh-ctr-offr-page
feat(counterOffer): Added new properties to counter offer model
2025-11-25 18:59:41 -08:00
Daniel Gonzalez fc2d7b4b9b feat(counterOffer): Added new properties to counter offer model 2025-11-25 20:57:44 -04:00
Chris Troutner c877898ae7 Merge pull request #121 from Permissionless-Software-Foundation/dh-track-counter-offrs
feat(offers): Track Counter Offers
2025-11-21 15:54:45 -08:00
Daniel Gonzalez 88e27f47f5 feat(offers): Track Counter Offers 2025-11-21 17:34:05 -04:00
Chris Troutner 9681def3e0 Merge branch 'master' into fullstack 2025-11-14 19:24:38 +00:00
Chris Troutner 0057007b4c Merge pull request #120 from Permissionless-Software-Foundation/dh-npub-offer
feat(nostr): Added npub to Offer model
2025-11-13 06:28:42 -08:00
Daniel Gonzalez 8aad93e572 feat(nostr): Added npub to Offer model 2025-11-11 14:48:09 -04:00
Chris Troutner 42f228af0e Merge branch 'master' into fullstack 2025-11-05 18:50:34 +00:00
Chris Troutner 4ac7f59378 Merge branch 'master' into fullstack 2025-10-25 18:30:01 +00:00
Chris Troutner 725c1c39a5 Merge branch 'master' into fullstack 2025-10-09 15:09:25 +00:00
Chris Troutner 7290c8db30 Merge branch 'master' into fullstack 2025-08-10 18:11:09 +00:00
Chris Troutner 36e1ec1f8e Merge branch 'master' into fullstack 2025-07-28 18:19:52 +00:00
Chris Troutner baa574a93e Merge branch 'master' into fullstack 2025-07-08 23:56:41 +00:00
Chris Troutner 8333563d98 Merge branch 'master' into fullstack 2025-07-06 21:44:33 +00:00
Chris Troutner 99fd116b6d Merge branch 'master' into fullstack 2025-06-27 16:28:28 +00:00
Chris Troutner ccae89ad01 Merge branch 'master' into fullstack 2025-06-06 18:53:44 +00:00
Chris Troutner 0ecae58be2 Merge branch 'master' into fullstack 2025-06-02 01:52:27 +00:00
Chris Troutner 5f442835e5 Customizing scrips 2025-05-20 19:38:16 +00:00
Chris Troutner aa8fd89227 Merge branch 'master' into fullstack 2025-05-19 22:45:42 +00:00
Chris Troutner 3da7432273 Building UI from psfoundation branch 2025-05-16 17:10:43 +00:00
Chris Troutner 61c1dd5878 Merge branch 'master' into fullstack 2025-05-16 14:56:09 +00:00
Chris Troutner c095e7af39 noop 2025-05-16 14:55:57 +00:00
Chris Troutner fb9612bb65 Using ct-unstable branch 2025-05-16 02:22:19 +00:00
Chris Troutner e4a37b1c10 Merge branch 'ct-unstable' into fullstack 2025-05-16 00:58:49 +00:00
Chris Troutner ba4f847e57 Merge branch 'master' into fullstack 2025-05-16 00:39:57 +00:00
Chris Troutner 81e623e35f Merge branch 'ct-unstable' into fullstack 2025-03-09 01:10:05 +00:00
Chris Troutner db1da9172c Using fullstack branch of UI 2024-12-29 09:37:28 -08:00
18 changed files with 534 additions and 22 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ RUN git clone https://github.com/Permissionless-Software-Foundation/bch-dex-ui-v
WORKDIR /home/safeuser/bch-dex-ui-v3 WORKDIR /home/safeuser/bch-dex-ui-v3
# For development: switch to unstable branch # For development: switch to unstable branch
RUN git checkout ct-unstable RUN git checkout psfoundation
# Install dependencies # Install dependencies
#RUN mkdir .ipfsdata #RUN mkdir .ipfsdata
@@ -31,4 +31,7 @@ export APP_ID=bch-dex-001
#export P2WDB_URL=http://172.17.0.1:5667 #export P2WDB_URL=http://172.17.0.1:5667
export DISABLE_NEW_ACCOUNTS=true
export ADMIN_PASSWORD=decatur
npm start npm start
+11
View File
@@ -0,0 +1,11 @@
{
"privateKey": "L4VyBDiWYYeeZhkM3xE3nsZigXJYmvRM29YBxoC7da4FzvubjxFo",
"publicKey": "028d6c77d865e6140db4b879a9bdf69ee4846593c0b6c48a82c448d5792806aab1",
"mnemonic": "split title trigger soda execute frost brush labor vague protect bicycle fiscal",
"cashAddress": "bitcoincash:qprrdk7en7m774gsafkycxcymc9ry209jus5v8wx5z",
"address": "bitcoincash:qprrdk7en7m774gsafkycxcymc9ry209jus5v8wx5z",
"legacyAddress": "17QG25rNUfnfj2NtdaFiyiqKSJatvEnMYd",
"hdPath": "m/44'/245'/0'/0/0",
"slpAddress": "simpleledger:qprrdk7en7m774gsafkycxcymc9ry209juu08umx2u",
"nextAddress": 1
}
+2
View File
@@ -11,6 +11,7 @@ import Usage from './models/usage.js'
import SmAccount from './models/smAccount.js' import SmAccount from './models/smAccount.js'
import DeletedChat from './models/deletedChat.js' import DeletedChat from './models/deletedChat.js'
import DeletedPost from './models/deletedPost.js' import DeletedPost from './models/deletedPost.js'
import CounterOffer from './models/counter-offer.js'
class LocalDB { class LocalDB {
constructor () { constructor () {
@@ -23,6 +24,7 @@ class LocalDB {
this.SmAccount = SmAccount this.SmAccount = SmAccount
this.DeletedChat = DeletedChat this.DeletedChat = DeletedChat
this.DeletedPost = DeletedPost this.DeletedPost = DeletedPost
this.CounterOffer = CounterOffer
} }
} }
@@ -0,0 +1,14 @@
import mongoose from 'mongoose'
const CounterOffer = new mongoose.Schema({
nostrEventId: { type: String, required: true }, // Nostr Event Id.
takerAddr: { type: String, default: null },
takerNpub: { type: String, default: null },
makerNpub: { type: String, default: null },
counterOfferAddr: { type: String, default: null },
counterOfferUtxo: { type: String, default: null },
takerOfferUtxo: { type: String, default: null },
tokenId: { type: String, default: null }
})
export default mongoose.model('counterOffer', CounterOffer)
+2 -1
View File
@@ -18,7 +18,7 @@ const Offer = new mongoose.Schema({
p2wdbHash: { type: String }, p2wdbHash: { type: String },
offerStatus: { type: String }, offerStatus: { type: String },
makerAddr: { type: String }, makerAddr: { type: String },
makerNpub: { type: String }, makerNpub: { type: String, default: null },
// Authentication data // Authentication data
signature: { type: String }, signature: { type: String },
@@ -46,6 +46,7 @@ const Offer = new mongoose.Schema({
immutableDataCid: { type: String, default: null }, immutableDataCid: { type: String, default: null },
mutableDataCid: { type: String, default: null }, mutableDataCid: { type: String, default: null },
tokenIconUrl: { type: String, default: null }, tokenIconUrl: { type: String, default: null },
tokenData: { type: Object },
tokenCategories: { type: Array, default: [] }, tokenCategories: { type: Array, default: [] },
tokenTags: { type: Array, default: [] }, tokenTags: { type: Array, default: [] },
userDataStr: { type: String }, // Token user data userDataStr: { type: String }, // Token user data
@@ -35,6 +35,7 @@ class OfferRESTControllerLib {
this.takeOffer = this.takeOffer.bind(this) this.takeOffer = this.takeOffer.bind(this)
this.listOffersByAddress = this.listOffersByAddress.bind(this) this.listOffersByAddress = this.listOffersByAddress.bind(this)
this.syncOfferMutableData = this.syncOfferMutableData.bind(this) this.syncOfferMutableData = this.syncOfferMutableData.bind(this)
this.listCounterOffersByAddress = this.listCounterOffersByAddress.bind(this)
this.handleError = this.handleError.bind(this) this.handleError = this.handleError.bind(this)
} }
@@ -408,6 +409,52 @@ class OfferRESTControllerLib {
} }
} }
/**
* @api {get} /offer/list/counter-offer/:addr List all counter offers being made by a given address
* @apiPermission public
* @apiName listCounterOffersByAddress
* @apiGroup REST Offer
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5000/offer/list/counter-offer/bitcoincash:qq54a3xyyptty63ztlcavzzh64gy4d247qs4ueaupe
*
*
* @apiSuccess {String} offers.nostrEventId Nostr event id
* @apiSuccess {String} offers.takerNpub Nostr Npub.
* @apiSuccess {String} offers.takerAddr BCH Address.
* @apiSuccess {String} offers.counterOfferAddr BCH Address
* @apiSuccess {String} offers.counterOfferUtxo Utxo txid.
*
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "counterOffers": [{
* "_id": "56bd1da600a526986cf65c80",
* "nostrEventId": "1234567890",
* "takerAddr": "bitcoincash:qq54a3xyyptty63ztlcavzzh64gy4d247qs4ueaupe",
* "takerNpub": "npub1nhc47z2tf42a4ps7plzcyax2j76gdausve763tqw3xuw7jjd7ceqppquly",
* "counterOfferAddr": "bitcoincash:qzltx40ldxr53edchprppj5lpg4q7vl20usx25ga44",
* "counterOfferUtxo": "71dab5b81cf06f3495aeebcc70fb351109e5e88bc85da67d0d144c0e5ca0bd6a",
* }]
* }
*
* @apiUse TokenError
*/
async listCounterOffersByAddress (ctx) {
try {
const addr = ctx.params.addr
const counterOffers = await this.useCases.offer.listCounterOffersByAddress(addr)
ctx.body = { counterOffers }
} catch (err) {
console.log('Error in listCounterOffersByAddress REST API handler: ', err)
this.handleError(ctx, err)
}
}
// DRY error handler // DRY error handler
handleError (ctx, err) { handleError (ctx, err) {
console.log('err', err.message) console.log('err', err.message)
+1
View File
@@ -60,6 +60,7 @@ class OfferRouter {
this.router.get('/list/nft/:page', this.offerRESTController.listNftOffers) this.router.get('/list/nft/:page', this.offerRESTController.listNftOffers)
this.router.get('/list/fungible/:page', this.offerRESTController.listFungibleOffers) this.router.get('/list/fungible/:page', this.offerRESTController.listFungibleOffers)
this.router.get('/list/addr/:addr', this.offerRESTController.listOffersByAddress) this.router.get('/list/addr/:addr', this.offerRESTController.listOffersByAddress)
this.router.get('/list/counter-offer/:addr', this.offerRESTController.listCounterOffersByAddress)
// Attach the Controller routes to the Koa app. // Attach the Controller routes to the Koa app.
app.use(this.router.routes()) app.use(this.router.routes())
+68
View File
@@ -0,0 +1,68 @@
/*
Counter Offer Entity
*/
class CounterOfferEntity {
validate (offerData = {}) {
console.log('counter offer data input validation ', offerData)
// Throw an error if input object does not have a data property
if (!offerData.data) {
throw new Error(
'Input to counterOffer.validate() must be an object with a data property.'
)
}
const {
nostrEventId,
takerAddr,
takerNpub,
makerNpub,
counterOfferAddr,
counterOfferUtxo,
takerOfferUtxo,
tokenId
} = offerData.data
// Input Validation
if (!nostrEventId || typeof nostrEventId !== 'string') {
throw new Error("Property 'nostrEventId' must be a string.")
}
// Next validations are optional, to allow old counter offers without the data.
if (takerAddr && typeof takerAddr !== 'string') {
throw new Error("Property 'takerAddr' must be a string.")
}
if (takerNpub && typeof takerNpub !== 'string') {
throw new Error("Property 'takerNpub' must be a string.")
}
if (counterOfferAddr && typeof counterOfferAddr !== 'string') {
throw new Error("Property 'counterOfferAddr' must be a string.")
}
if (counterOfferUtxo && typeof counterOfferUtxo !== 'string') {
throw new Error("Property 'counterOfferUtxo' must be a string.")
}
if (takerOfferUtxo && typeof takerOfferUtxo !== 'string') {
throw new Error("Property 'takerOfferUtxo' must be a string.")
}
if (tokenId && typeof tokenId !== 'string') {
throw new Error("Property 'tokenId' must be a string.")
}
if (makerNpub && typeof makerNpub !== 'string') {
throw new Error("Property 'makerNpub' must be a string.")
}
const validatedCounterOfferData = {
nostrEventId,
takerAddr,
takerNpub,
counterOfferAddr,
counterOfferUtxo,
takerOfferUtxo,
tokenId,
makerNpub
}
return validatedCounterOfferData
}
}
export default CounterOfferEntity
+7 -1
View File
@@ -31,6 +31,7 @@ class OfferEntity {
utxoVout, utxoVout,
offerStatus, offerStatus,
makerAddr, makerAddr,
makerNpub,
ticker, ticker,
tokenType, tokenType,
nostrEventId, nostrEventId,
@@ -89,6 +90,10 @@ class OfferEntity {
if (!operatorPercentage || typeof operatorPercentage !== 'number') { if (!operatorPercentage || typeof operatorPercentage !== 'number') {
throw new Error("Property 'operatorPercentage' must be a number.") throw new Error("Property 'operatorPercentage' must be a number.")
} }
// Validate npub type if it exist
if (makerNpub && typeof makerNpub !== 'string') {
throw new Error("Property 'makerNpub' must be a string.")
}
// Convert the timestamp to a number. // Convert the timestamp to a number.
let timestamp = new Date(offerData.timestamp) let timestamp = new Date(offerData.timestamp)
@@ -113,7 +118,8 @@ class OfferEntity {
makerAddr, makerAddr,
ticker, ticker,
tokenType, tokenType,
nostrEventId nostrEventId,
makerNpub
} }
// console.log('offer entity validatedOfferData: ', validatedOfferData) // console.log('offer entity validatedOfferData: ', validatedOfferData)
+33
View File
@@ -19,6 +19,7 @@ import RetryQueue from '@chris.troutner/retry-queue'
// Local libraries // Local libraries
import OfferEntity from '../../entities/offer.js' import OfferEntity from '../../entities/offer.js'
import CounterOfferEntity from '../../entities/counterOffer.js'
import config from '../../../config/index.js' import config from '../../../config/index.js'
const DEFAULT_ENTRIES_PER_PAGE = 20 const DEFAULT_ENTRIES_PER_PAGE = 20
@@ -45,7 +46,9 @@ class OfferUseCases {
this.config = config this.config = config
this.axios = axios this.axios = axios
this.offerEntity = new OfferEntity() this.offerEntity = new OfferEntity()
this.counterOfferEntity = new CounterOfferEntity()
this.OfferModel = this.adapters.localdb.Offer this.OfferModel = this.adapters.localdb.Offer
this.CounterOfferModel = this.adapters.localdb.CounterOffer
this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 }) this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 })
// Bind 'this' object to functions // Bind 'this' object to functions
@@ -66,6 +69,7 @@ class OfferUseCases {
this.loadOffers = this.loadOffers.bind(this) this.loadOffers = this.loadOffers.bind(this)
this.listOffersByAddress = this.listOffersByAddress.bind(this) this.listOffersByAddress = this.listOffersByAddress.bind(this)
this.syncOfferMutableData = this.syncOfferMutableData.bind(this) this.syncOfferMutableData = this.syncOfferMutableData.bind(this)
this.listCounterOffersByAddress = this.listCounterOffersByAddress.bind(this)
// State // State
this.seenOffers = [] this.seenOffers = []
@@ -144,6 +148,7 @@ class OfferUseCases {
const immutableDataCid = tokenData.immutableData const immutableDataCid = tokenData.immutableData
offerEntity.mutableDataCid = mutableDataCid offerEntity.mutableDataCid = mutableDataCid
offerEntity.immutableDataCid = immutableDataCid offerEntity.immutableDataCid = immutableDataCid
offerEntity.tokenData = tokenData
// Get the mutable data from the cid if it exists. // Get the mutable data from the cid if it exists.
if (mutableDataCid && typeof mutableDataCid === 'string') { if (mutableDataCid && typeof mutableDataCid === 'string') {
@@ -595,10 +600,23 @@ class OfferUseCases {
// and broadcasts the transaction to accept the Counter Offer. // and broadcasts the transaction to accept the Counter Offer.
async acceptCounterOffer (offerData) { async acceptCounterOffer (offerData) {
try { try {
console.log('offerData', offerData)
// console.log(`acceptCounterOffer() offerData: ${JSON.stringify(offerData, null, 2)}`) // console.log(`acceptCounterOffer() offerData: ${JSON.stringify(offerData, null, 2)}`)
// Quickly skip over offers that have already been processed. // Quickly skip over offers that have already been processed.
const eventId = offerData.data.nostrEventId const eventId = offerData.data.nostrEventId
// Create counter offer entity for nostr event id if it not exist
const existingCounterOffer = await this.CounterOfferModel.findOne(({ nostrEventId: eventId }))
if (!existingCounterOffer) {
// Add taker offer utxo to counter offer data.
offerData.data.takerOfferUtxo = offerData.data.utxoTxid
const counterOEntity = this.counterOfferEntity.validate(offerData)
// Create new counter offfer model and save it.
const counterOffer = new this.CounterOfferModel(counterOEntity)
await counterOffer.save()
}
if (this.seenOffers.includes(eventId)) { if (this.seenOffers.includes(eventId)) {
// console.log(`Offer with event ID ${eventId} already processed. Skipping.`) // console.log(`Offer with event ID ${eventId} already processed. Skipping.`)
return false return false
@@ -983,6 +1001,7 @@ class OfferUseCases {
const immutableDataCid = tokenData.immutableData const immutableDataCid = tokenData.immutableData
offer.mutableDataCid = mutableDataCid offer.mutableDataCid = mutableDataCid
offer.immutableDataCid = immutableDataCid offer.immutableDataCid = immutableDataCid
offer.tokenData = tokenData
// Get the mutable data from the cid if it exists. // Get the mutable data from the cid if it exists.
if (mutableDataCid && typeof mutableDataCid === 'string') { if (mutableDataCid && typeof mutableDataCid === 'string') {
@@ -1020,6 +1039,20 @@ class OfferUseCases {
throw err throw err
} }
} }
async listCounterOffersByAddress (takerAddr) {
try {
if (!takerAddr || typeof takerAddr !== 'string') {
throw new Error('takerAddr must be a string')
}
const counterOffers = await this.CounterOfferModel.find({ takerAddr })
return counterOffers
} catch (err) {
console.error('Error in listCounterOffersByAddress(): ', err)
throw err
}
}
} }
export default OfferUseCases export default OfferUseCases
@@ -251,6 +251,28 @@ describe('#Offer-REST-Router', () => {
}) })
}) })
describe('#listCounterOffersByAddress', () => {
it('should list counter offers by address', async () => {
ctx.params = { addr: 'testAddress' }
sandbox.stub(uut.useCases.offer, 'listCounterOffersByAddress').resolves([])
await uut.listCounterOffersByAddress(ctx)
assert.isArray(ctx.body.counterOffers)
})
it('should catch and throw an error', async () => {
try {
ctx.params = { addr: 'testAddress' }
sandbox
.stub(uut.useCases.offer, 'listCounterOffersByAddress')
.throws(new Error('test error'))
await uut.listCounterOffersByAddress(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#handleError', () => { describe('#handleError', () => {
it('should still throw error if there is no message', () => { it('should still throw error if there is no message', () => {
try { try {
@@ -0,0 +1,177 @@
/*
Unit tests for the CounterOffer entity library.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import CounterOfferEntity from '../../../src/entities/counterOffer.js'
let sandbox
let uut
describe('#CounterOffer-Entity', () => {
before(async () => { })
beforeEach(() => {
uut = new CounterOfferEntity()
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
describe('#validate', () => {
it('should throw an error if data is not provided', () => {
try {
uut.validate({})
} catch (err) {
assert.include(err.message, 'Input to counterOffer.validate() must be an object with a data property.')
}
})
it('should throw an error if nostrEventId is not provided', () => {
try {
uut.validate({ data: {} })
} catch (err) {
assert.include(err.message, "Property 'nostrEventId' must be a string")
}
})
it('should throw an error if provided takerAddr is not string', () => {
try {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 1234
}
uut.validate({ data: inObj })
} catch (err) {
assert.include(err.message, "Property 'takerAddr' must be a string")
}
})
it('should throw an error if provided takerNpub is not string', () => {
try {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 'taker address',
takerNpub: 1234
}
uut.validate({ data: inObj })
} catch (err) {
assert.include(err.message, "Property 'takerNpub' must be a string")
}
})
it('should throw an error if provided makerNpub is not string', () => {
try {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 'taker address',
takerNpub: 'npub',
makerNpub: 1234
}
uut.validate({ data: inObj })
} catch (err) {
assert.include(err.message, "Property 'makerNpub' must be a string")
}
})
it('should throw an error if provided counterOfferAddr is not string', () => {
try {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 'taker address',
takerNpub: 'npub',
makerNpub: 'npub',
counterOfferAddr: 1234
}
uut.validate({ data: inObj })
} catch (err) {
assert.include(err.message, "Property 'counterOfferAddr' must be a string")
}
})
it('should throw an error if provided counterOfferUtxo is not string', () => {
try {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 'taker address',
takerNpub: 'npub',
makerNpub: 'npub',
counterOfferAddr: 'counter offer address',
counterOfferUtxo: 1234
}
uut.validate({ data: inObj })
} catch (err) {
assert.include(err.message, "Property 'counterOfferUtxo' must be a string")
}
})
it('should throw an error if provided takerOfferUtxo is not string', () => {
try {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 'taker address',
takerNpub: 'npub',
makerNpub: 'npub',
counterOfferAddr: 'counter offer address',
counterOfferUtxo: 'utxo txid',
takerOfferUtxo: 1234
}
uut.validate({ data: inObj })
} catch (err) {
assert.include(err.message, "Property 'takerOfferUtxo' must be a string")
}
})
it('should throw an error if provided tokenId is not string', () => {
try {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 'taker address',
takerNpub: 'npub',
makerNpub: 'npub',
counterOfferAddr: 'counter offer address',
counterOfferUtxo: 'utxo txid',
takerOfferUtxo: 'utxo txid',
tokenId: 1234
}
uut.validate({ data: inObj })
} catch (err) {
assert.include(err.message, "Property 'tokenId' must be a string")
}
})
it('should return a counter offer object', () => {
const inObj = {
nostrEventId: 'nostr event id',
takerAddr: 'taker address',
takerNpub: 'npub',
makerNpub: 'npub',
counterOfferAddr: 'counter offer address',
counterOfferUtxo: 'utxo txid',
takerOfferUtxo: 'utxo txid',
tokenId: 'token id'
}
const counterOfferEntity = uut.validate({ data: inObj })
assert.property(counterOfferEntity, 'nostrEventId')
assert.equal(counterOfferEntity.nostrEventId, inObj.nostrEventId)
assert.property(counterOfferEntity, 'takerAddr')
assert.equal(counterOfferEntity.takerAddr, inObj.takerAddr)
assert.property(counterOfferEntity, 'takerNpub')
assert.equal(counterOfferEntity.takerNpub, inObj.takerNpub)
assert.property(counterOfferEntity, 'makerNpub')
assert.equal(counterOfferEntity.makerNpub, inObj.makerNpub)
assert.property(counterOfferEntity, 'counterOfferAddr')
assert.equal(counterOfferEntity.counterOfferAddr, inObj.counterOfferAddr)
assert.property(counterOfferEntity, 'counterOfferUtxo')
assert.equal(counterOfferEntity.counterOfferUtxo, inObj.counterOfferUtxo)
assert.property(counterOfferEntity, 'takerOfferUtxo')
assert.equal(counterOfferEntity.takerOfferUtxo, inObj.takerOfferUtxo)
assert.property(counterOfferEntity, 'tokenId')
assert.equal(counterOfferEntity.tokenId, inObj.tokenId)
})
})
})
+34 -1
View File
@@ -491,6 +491,36 @@ describe('#Offer-Entity', () => {
assert.include(err.message, "Property 'operatorPercentage' must be a number.") assert.include(err.message, "Property 'operatorPercentage' must be a number.")
} }
}) })
it('should throw an error if makerNpub is not a string', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
tokenType: 1,
nostrEventId: 'test',
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorPercentage: 10,
makerNpub: 1234
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'makerNpub' must be a string.")
}
})
it('should validate a new offer', () => { it('should validate a new offer', () => {
const offerObj = { const offerObj = {
@@ -513,7 +543,8 @@ describe('#Offer-Entity', () => {
tokenType: 1, tokenType: 1,
nostrEventId: 'test', nostrEventId: 'test',
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorPercentage: 10 operatorPercentage: 10,
makerNpub: 'npub1dtkf954h5k2yuv04xp44g5hnmk5k0ppv8mekxafm2gc0vveaxh6s0297qg'
}, },
timestamp: '2021-09-20T17:54:26.395Z', timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM', localTimeStamp: '9/20/2021, 10:54:26 AM',
@@ -539,6 +570,8 @@ describe('#Offer-Entity', () => {
assert.property(result, 'txid') assert.property(result, 'txid')
assert.property(result, 'p2wdbHash') assert.property(result, 'p2wdbHash')
assert.property(result, 'tokenType') assert.property(result, 'tokenType')
assert.property(result, 'makerAddr')
assert.property(result, 'makerNpub')
}) })
}) })
}) })
+11
View File
@@ -154,6 +154,17 @@ const localdb = {
return {} return {}
} }
}, },
CounterOffer: class CounterOffer {
constructor (obj) {}
static findById () {}
static find () {}
static findOne () {}
static countDocuments(){}
async save () {
return {}
}
},
SmAccount: class SmAccount { SmAccount: class SmAccount {
constructor (obj) {} constructor (obj) {}
+3
View File
@@ -74,6 +74,9 @@ class Offer {
async syncOfferMutableData(){ async syncOfferMutableData(){
return {} return {}
} }
async listCounterOffersByAddress() {
return []
}
} }
class Order { class Order {
+94 -14
View File
@@ -107,14 +107,14 @@ describe('#offer-use-case', () => {
const result = await uut.createOffer(offerObj) const result = await uut.createOffer(offerObj)
assert.isFalse(result) assert.isFalse(result)
}) })
it('should create offer with token data', async () => {
it('should create offer with mutable data', async () => {
const tokenDataMock = mockData.nftTokenData01 const tokenDataMock = mockData.nftTokenData01
const offerObj = mockData.offerMockData const offerObj = mockData.offerMockData
const mutableDataMock = mockData.mutableDataMock const mutableDataMock = mockData.mutableDataMock
const offerEntityMock = Object.assign({}, mockData.offerMockData.data)
// Mock dependencies // Mock dependencies
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false) sandbox.stub(uut.offerEntity, 'validate').returns(offerEntityMock)
sandbox.stub(uut, 'findOfferByTxid').throws(new Error('offer not found')) sandbox.stub(uut, 'findOfferByTxid').throws(new Error('offer not found'))
sandbox.stub(uut.retryQueue, 'addToQueue') sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves({}) // Utxo Status call .onCall(0).resolves({}) // Utxo Status call
@@ -123,6 +123,31 @@ describe('#offer-use-case', () => {
const result = await uut.createOffer(offerObj) const result = await uut.createOffer(offerObj)
assert.isTrue(result) assert.isTrue(result)
console.log('offerEntityMock', offerEntityMock)
assert.property(offerEntityMock, 'tokenData')
})
it('should create offer with mutable data', async () => {
const tokenDataMock = mockData.nftTokenData01
const offerObj = mockData.offerMockData
const mutableDataMock = mockData.mutableDataMock
const offerEntityMock = Object.assign({}, mockData.offerMockData.data)
// Mock dependencies
sandbox.stub(uut.offerEntity, 'validate').returns(offerEntityMock)
sandbox.stub(uut, 'findOfferByTxid').throws(new Error('offer not found'))
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves({}) // Utxo Status call
.onCall(1).resolves(tokenDataMock) // Token Data call
.onCall(2).resolves(mutableDataMock)
const result = await uut.createOffer(offerObj)
assert.isTrue(result)
console.log('offerEntityMock', offerEntityMock)
assert.property(offerEntityMock, 'tokenIconUrl')
assert.property(offerEntityMock, 'tokenCategories')
assert.property(offerEntityMock, 'tokenTags')
assert.property(offerEntityMock, 'lastUpdatedTokenData')
}) })
it('should skip userData stringify error', async () => { it('should skip userData stringify error', async () => {
@@ -835,6 +860,25 @@ describe('#offer-use-case', () => {
}) })
describe('#acceptCounterOffer', () => { describe('#acceptCounterOffer', () => {
it('should capture counter offer if does not exist', async () => {
const offerObj = mockData.offerMockData
sandbox.stub(uut.CounterOfferModel, 'findOne').resolves(null)
const spy = sandbox.stub(uut.CounterOfferModel.prototype, 'save')
// Mock dependencies
await uut.acceptCounterOffer(offerObj)
assert.isTrue(spy.calledOnce)
})
it('should not save counter offer if already exist', async () => {
const offerObj = mockData.offerMockData
sandbox.stub(uut.CounterOfferModel, 'findOne').resolves(offerObj.data)
const spy = sandbox.stub(uut.CounterOfferModel.prototype, 'save')
// Mock dependencies
await uut.acceptCounterOffer(offerObj)
assert.isFalse(spy.calledOnce)
})
it('should return false if order already processed', async () => { it('should return false if order already processed', async () => {
const offerObj = mockData.offerMockData const offerObj = mockData.offerMockData
uut.seenOffers.push(offerObj.data.nostrEventId) uut.seenOffers.push(offerObj.data.nostrEventId)
@@ -846,14 +890,18 @@ describe('#offer-use-case', () => {
it('should return if order is not found!', async () => { it('should return if order is not found!', async () => {
// Mock dependencies // Mock dependencies
const offerObj = mockData.offerMockData
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').throws(new Error('test error')) sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').throws(new Error('test error'))
const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) const result = await uut.acceptCounterOffer(offerObj)
assert.equal(result, 'N/A') assert.equal(result, 'N/A')
}) })
it('should return N/A on axios error', async () => { it('should return N/A on axios error', async () => {
// Mock dependencies // Mock dependencies
const offerObj = mockData.offerMockData
const mock = Object.assign({}, mockData.offerMockData.data) const mock = Object.assign({}, mockData.offerMockData.data)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').throws(new Error('test error')) // sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').throws(new Error('test error'))
@@ -861,32 +909,38 @@ describe('#offer-use-case', () => {
axiosErr.isAxiosError = true axiosErr.isAxiosError = true
sandbox.stub(uut.retryQueue, 'addToQueue').throws(axiosErr) sandbox.stub(uut.retryQueue, 'addToQueue').throws(axiosErr)
const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) const result = await uut.acceptCounterOffer(offerObj)
assert.equal(result, 'N/A') assert.equal(result, 'N/A')
}) })
it('should return if utxo can not be validated', async () => { it('should return if utxo can not be validated', async () => {
// Mock dependencies // Mock dependencies
const offerObj = mockData.offerMockData
const mock = Object.assign({}, mockData.offerMockData.data) const mock = Object.assign({}, mockData.offerMockData.data)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').throws(new Error('test error')) // sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').throws(new Error('test error'))
sandbox.stub(uut.retryQueue, 'addToQueue').throws(new Error('test error')) sandbox.stub(uut.retryQueue, 'addToQueue').throws(new Error('test error'))
const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) const result = await uut.acceptCounterOffer(offerObj)
assert.equal(result, 'N/A') assert.equal(result, 'N/A')
}) })
it('should return if utxo is invalid', async () => { it('should return if utxo is invalid', async () => {
// Mock dependencies // Mock dependencies
const offerObj = mockData.offerMockData
const mock = Object.assign({}, mockData.offerMockData.data) const mock = Object.assign({}, mockData.offerMockData.data)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false) sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) const result = await uut.acceptCounterOffer(offerObj)
assert.equal(result, 'N/A') assert.equal(result, 'N/A')
}) })
it('should handle error if counter offer cant be calculated', async () => { it('should handle error if counter offer cant be calculated', async () => {
try { try {
const offerObj = mockData.offerMockData
// Mock Data // Mock Data
const mock = Object.assign({}, mockData.offerMockData.data) const mock = Object.assign({}, mockData.offerMockData.data)
mock.rateInBaseUnit = null mock.rateInBaseUnit = null
@@ -895,8 +949,7 @@ describe('#offer-use-case', () => {
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock) sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
const result = await uut.acceptCounterOffer({ data: {} }) await uut.acceptCounterOffer(offerObj)
console.log('result: ', result)
assert.fail('unexpected code path') assert.fail('unexpected code path')
} catch (err) { } catch (err) {
assert.include(err.message, 'Could not calculate the amount of BCH offered in the Counter Offer') assert.include(err.message, 'Could not calculate the amount of BCH offered in the Counter Offer')
@@ -906,6 +959,7 @@ describe('#offer-use-case', () => {
it('should handle error for wrong transaction output', async () => { it('should handle error for wrong transaction output', async () => {
try { try {
// Mock Data // Mock Data
const mock = Object.assign({}, mockData.offerMockData.data) const mock = Object.assign({}, mockData.offerMockData.data)
// Mock dependencies // Mock dependencies
@@ -923,6 +977,7 @@ describe('#offer-use-case', () => {
it('should skip transactions that do not have an output for the operator', async () => { it('should skip transactions that do not have an output for the operator', async () => {
// Mock data // Mock data
const mock = Object.assign({}, mockData.offerMockData.data) const mock = Object.assign({}, mockData.offerMockData.data)
mock.makerAddr = 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477' // Unknow Adress mock.makerAddr = 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477' // Unknow Adress
mock.rateInBaseUnit = 0 mock.rateInBaseUnit = 0
@@ -934,7 +989,7 @@ describe('#offer-use-case', () => {
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMockNoOperatorOut) sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMockNoOperatorOut)
const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) const result = await uut.acceptCounterOffer({ data: mock })
assert.equal(result, 'N/A') assert.equal(result, 'N/A')
}) })
it('should skip transactions if operator address does not match', async () => { it('should skip transactions if operator address does not match', async () => {
@@ -952,7 +1007,7 @@ describe('#offer-use-case', () => {
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock) sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
await uut.acceptCounterOffer({ data: {} }) await uut.acceptCounterOffer({ data: mock })
assert.fail('unexpected code path') assert.fail('unexpected code path')
} catch (error) { } catch (error) {
assert.include(error.message, 'The Counter Offer has an output address of') assert.include(error.message, 'The Counter Offer has an output address of')
@@ -974,7 +1029,7 @@ describe('#offer-use-case', () => {
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock) sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
await uut.acceptCounterOffer({ data: {} }) await uut.acceptCounterOffer({ data: mock })
assert.fail('unexpected code path') assert.fail('unexpected code path')
} catch (err) { } catch (err) {
assert.include(err.message, 'The Counter Offer has an output address of') assert.include(err.message, 'The Counter Offer has an output address of')
@@ -997,7 +1052,7 @@ describe('#offer-use-case', () => {
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock) sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
const result = await uut.acceptCounterOffer({ data: {} }) const result = await uut.acceptCounterOffer({ data: mock })
assert.equal(result, 'N/A') assert.equal(result, 'N/A')
}) })
@@ -1017,7 +1072,7 @@ describe('#offer-use-case', () => {
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock) sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
// //
const result = await uut.acceptCounterOffer({ data: {} }) const result = await uut.acceptCounterOffer({ data: mock })
assert.isString(result) assert.isString(result)
assert.notEqual('N/A') assert.notEqual('N/A')
}) })
@@ -1076,6 +1131,12 @@ describe('#offer-use-case', () => {
const offer = await uut.syncOfferMutableData('tokenId') const offer = await uut.syncOfferMutableData('tokenId')
assert.isObject(offer) assert.isObject(offer)
assert.isNumber(offer.lastUpdatedTokenData) assert.isNumber(offer.lastUpdatedTokenData)
assert.property(offer, 'tokenData')
assert.property(offer, 'tokenIconUrl')
assert.property(offer, 'tokenCategories')
assert.property(offer, 'tokenTags')
assert.property(offer, 'lastUpdatedTokenData')
}) })
it('should skip userData stringify error', async () => { it('should skip userData stringify error', async () => {
@@ -1130,4 +1191,23 @@ describe('#offer-use-case', () => {
assert.isNumber(offer.lastUpdatedTokenData) assert.isNumber(offer.lastUpdatedTokenData)
}) })
}) })
describe('#listCounterOffersByAddress', () => {
it('should throw an error if takerAddr is not provided!', async () => {
try {
await uut.listCounterOffersByAddress()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'takerAddr must be a string')
}
})
it('should return counter offers by address', async () => {
// Mock dependencies
sandbox.stub(uut.CounterOfferModel, 'find').resolves([])
const result = await uut.listCounterOffersByAddress('bitcoincash:qzltx40ldxr53edchprppj5lpg4q7vl20usx25ga44')
assert.isArray(result)
})
})
}) })
+4 -4
View File
@@ -11,12 +11,12 @@ import axios from 'axios'
// import User from '../../../src/adapters/localdb/models/users.js' // import User from '../../../src/adapters/localdb/models/users.js'
// Customize these variables // Customize these variables
const EMAIL = 'someone@somewhere.com' const EMAIL = 'chris.troutner@gmail.com'
const NAME = 'Someone' const NAME = 'Chris'
const PASSWORD = 'test' const PASSWORD = '0mega16'
const ADMIN_EMAIL = 'system@system.com' const ADMIN_EMAIL = 'system@system.com'
const ADMIN_PASSWORD = 'admin' const ADMIN_PASSWORD = 'decatur'
async function createUser () { async function createUser () {
try { try {