Merge pull request #80 from Permissionless-Software-Foundation/dh-create-offr-nostr

feat(offer): Trigger createOffer() from Nostr topic
This commit is contained in:
Chris Troutner
2024-12-06 09:43:58 -08:00
committed by GitHub
14 changed files with 982 additions and 232 deletions
+1 -1
View File
@@ -166,7 +166,7 @@ export default {
chatPubSubChan: 'psf-ipfs-chat-001', chatPubSubChan: 'psf-ipfs-chat-001',
// IPFS gateway // IPFS gateway
ipfsGateway: process.env.IPFS_GATEWAY ? process.env.IPFS_GATEWAY : 'https://p2wdb-gateway-678.fullstack.cash/ipfs/', ipfsGateway: process.env.IPFS_GATEWAY ? process.env.IPFS_GATEWAY : 'https://pin.fullstack.cash/ipfs/download/',
// This can add specific Circuit Relay v2 servers to connect to. // This can add specific Circuit Relay v2 servers to connect to.
bootstrapRelays: [ bootstrapRelays: [
+2 -1
View File
@@ -34,7 +34,8 @@ const Offer = new mongoose.Schema({
// SWaP Protocol Properties // SWaP Protocol Properties
lokadId: { type: String }, lokadId: { type: String },
messageType: { type: Number }, messageType: { type: Number },
messageClass: { type: Number } messageClass: { type: Number },
nostrEventId: { type: String } // Nostr Event Id.
}) })
+19 -15
View File
@@ -4,8 +4,6 @@
import wlogger from '../../../adapters/wlogger.js' import wlogger from '../../../adapters/wlogger.js'
let _this
class OfferRESTControllerLib { class OfferRESTControllerLib {
constructor (localConfig = {}) { constructor (localConfig = {}) {
// Dependency Injection. // Dependency Injection.
@@ -26,7 +24,13 @@ class OfferRESTControllerLib {
this.OfferModel = this.adapters.localdb.Offer this.OfferModel = this.adapters.localdb.Offer
// this.userUseCases = this.useCases.user // this.userUseCases = this.useCases.user
_this = this // Bind 'this' object to all subfunctions.
this.createOffer = this.createOffer.bind(this)
this.listOffers = this.listOffers.bind(this)
this.listNftOffers = this.listNftOffers.bind(this)
this.listFungibleOffers = this.listFungibleOffers.bind(this)
this.takeOffer = this.takeOffer.bind(this)
this.handleError = this.handleError.bind(this)
} }
// No api-doc documentation because this wont be a public endpoint // No api-doc documentation because this wont be a public endpoint
@@ -36,7 +40,7 @@ class OfferRESTControllerLib {
const offerObj = ctx.request.body const offerObj = ctx.request.body
await _this.useCases.offer.createOffer(offerObj) await this.useCases.offer.createOffer(offerObj)
ctx.body = { ctx.body = {
success: true success: true
@@ -45,7 +49,7 @@ class OfferRESTControllerLib {
// console.log(`err.message: ${err.message}`) // console.log(`err.message: ${err.message}`)
// console.log('err: ', err) // console.log('err: ', err)
// ctx.throw(422, err.message) // ctx.throw(422, err.message)
_this.handleError(ctx, err) this.handleError(ctx, err)
} }
} }
@@ -55,12 +59,12 @@ class OfferRESTControllerLib {
let page = ctx.params.page let page = ctx.params.page
if (!page) page = 0 if (!page) page = 0
const offers = await _this.useCases.offer.listOffers(page) const offers = await this.useCases.offer.listOffers(page)
ctx.body = offers ctx.body = offers
} catch (err) { } catch (err) {
console.log('Error in listOffers REST API handler: ', err) console.log('Error in listOffers REST API handler: ', err)
_this.handleError(ctx, err) this.handleError(ctx, err)
} }
} }
@@ -70,12 +74,12 @@ class OfferRESTControllerLib {
let page = ctx.params.page let page = ctx.params.page
if (!page) page = 0 if (!page) page = 0
const offers = await _this.useCases.offer.listNftOffers(page) const offers = await this.useCases.offer.listNftOffers(page)
ctx.body = offers ctx.body = offers
} catch (err) { } catch (err) {
console.log('Error in listNftOffers REST API handler: ', err) console.log('Error in listNftOffers REST API handler: ', err)
_this.handleError(ctx, err) this.handleError(ctx, err)
} }
} }
@@ -85,12 +89,12 @@ class OfferRESTControllerLib {
let page = ctx.params.page let page = ctx.params.page
if (!page) page = 0 if (!page) page = 0
const offers = await _this.useCases.offer.listFungibleOffers(page) const offers = await this.useCases.offer.listFungibleOffers(page)
ctx.body = offers ctx.body = offers
} catch (err) { } catch (err) {
console.log('Error in Fungible REST API handler: ', err) console.log('Error in Fungible REST API handler: ', err)
_this.handleError(ctx, err) this.handleError(ctx, err)
} }
} }
@@ -100,14 +104,14 @@ class OfferRESTControllerLib {
try { try {
console.log('REST API controller, body: ', ctx.request.body) console.log('REST API controller, body: ', ctx.request.body)
const offerCid = ctx.request.body.offerCid const nostrEventId = ctx.request.body.nostrEventId
const hash = await _this.useCases.offer.takeOffer(offerCid) const eventId = await this.useCases.offer.takeOffer(nostrEventId)
ctx.body = { hash } ctx.body = { eventId }
} catch (err) { } catch (err) {
wlogger.error('Error in takeOffer() REST API handler.') wlogger.error('Error in takeOffer() REST API handler.')
_this.handleError(ctx, err) this.handleError(ctx, err)
} }
} }
+6 -1
View File
@@ -48,8 +48,13 @@ 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
// by a Timer Controller monitoring a Nostr topic.
// Define the routes and attach the controller. // Define the routes and attach the controller.
this.router.post('/', _this.offerRESTController.createOffer) // this.router.post('/', _this.offerRESTController.createOffer) // Deprecated.
this.router.post('/take', _this.offerRESTController.takeOffer) this.router.post('/take', _this.offerRESTController.takeOffer)
this.router.get('/list/all/:page', _this.offerRESTController.listOffers) this.router.get('/list/all/:page', _this.offerRESTController.listOffers)
this.router.get('/list/nft/:page', _this.offerRESTController.listNftOffers) this.router.get('/list/nft/:page', _this.offerRESTController.listNftOffers)
+22 -3
View File
@@ -29,13 +29,12 @@ class TimerControllers {
this.gcOrders = this.gcOrders.bind(this) this.gcOrders = this.gcOrders.bind(this)
this.gcOffers = this.gcOffers.bind(this) this.gcOffers = this.gcOffers.bind(this)
this.checkDupOffers = this.checkDupOffers.bind(this) this.checkDupOffers = this.checkDupOffers.bind(this)
this.loadOffers = this.loadOffers.bind(this)
// State // State
this.gcOrdersInt = null this.gcOrdersInt = null
this.gcOffersInt = null this.gcOffersInt = null
this.checkDupOffersInt = null this.checkDupOffersInt = null
this.loadOffersInt = null
this.startTimers()
} }
// Start all the time-based controllers. // Start all the time-based controllers.
@@ -43,6 +42,8 @@ class TimerControllers {
this.gcOrdersInt = setInterval(this.gcOrders, 60000 * 5) this.gcOrdersInt = setInterval(this.gcOrders, 60000 * 5)
this.gcOffersInt = setInterval(this.gcOffers, 60000 * 5) this.gcOffersInt = setInterval(this.gcOffers, 60000 * 5)
this.checkDupOffersInt = setInterval(this.checkDupOffers, 60000 * 4.5) this.checkDupOffersInt = setInterval(this.checkDupOffers, 60000 * 4.5)
this.loadOffersInt = setInterval(this.loadOffers, 60000 * 2)
return true
} }
stopTimers () { stopTimers () {
@@ -55,9 +56,11 @@ class TimerControllers {
gcOrders () { gcOrders () {
try { try {
this.useCases.order.removeStaleOrders() this.useCases.order.removeStaleOrders()
return true
} catch (err) { } catch (err) {
// Do not throw an error. This is a top-level function. // Do not throw an error. This is a top-level function.
console.log('Error in timer-controllers.js/gcOrders(): ', err) console.log('Error in timer-controllers.js/gcOrders(): ', err)
return false
} }
} }
@@ -65,9 +68,11 @@ class TimerControllers {
gcOffers () { gcOffers () {
try { try {
this.useCases.offer.removeStaleOffers() this.useCases.offer.removeStaleOffers()
return true
} catch (err) { } catch (err) {
// Do not throw an error. This is a top-level function. // Do not throw an error. This is a top-level function.
console.log('Error in timer-controllers.js/gcOffers(): ', err) console.log('Error in timer-controllers.js/gcOffers(): ', err)
return false
} }
} }
@@ -75,9 +80,23 @@ class TimerControllers {
checkDupOffers () { checkDupOffers () {
try { try {
this.useCases.offer.removeDuplicateOffers() this.useCases.offer.removeDuplicateOffers()
return true
} catch (err) { } catch (err) {
// Do not throw an error. This is a top-level function. // Do not throw an error. This is a top-level function.
console.log('Error in timer-controllers.js/checkDupOffers(): ', err) console.log('Error in timer-controllers.js/checkDupOffers(): ', err)
return false
}
}
// Load offers From nostr .
async loadOffers () {
try {
await this.useCases.offer.loadOffers()
return true
} catch (err) {
// Do not throw an error. This is a top-level function.
console.log('Error in timer-controllers.js/loadOffers(): ', err)
return false
} }
} }
} }
+137 -72
View File
@@ -37,7 +37,7 @@ class OfferUseCases {
this.orderUseCase = localConfig.order this.orderUseCase = localConfig.order
if (!this.orderUseCase) { if (!this.orderUseCase) {
throw new Error( throw new Error(
'Instance of Offer Use Cases must be passed in when instantiating Offer Use Cases library.' 'Instance of Order Use Cases must be passed in when instantiating Offer Use Cases library.'
) )
} }
@@ -50,17 +50,17 @@ class OfferUseCases {
// Bind 'this' object to functions // Bind 'this' object to functions
this.detectNsfw = this.detectNsfw.bind(this) this.detectNsfw = this.detectNsfw.bind(this)
this.loadOffers = this.loadOffers.bind(this)
} }
// This method is called by the POST /offer REST API controller, which is // This method is called by timer controller to load offers from a Nostr topic.
// triggered by a P2WDB webhook.
async createOffer (offerObj) { async createOffer (offerObj) {
try { try {
console.log('Use Case createOffer(offerObj): ', offerObj) console.log('Use Case createOffer(offerObj): ', offerObj)
// Return if Offer already exists in database with the same P2WDB CID. // Return if Offer already exists in database with the same utxo transaction id.
try { try {
await this.findOfferByHash(offerObj.hash) await this.findOfferByTxid(offerObj.data.utxoTxid)
console.log('Offer already found in local database.') console.log('Offer already found in local database.')
return false return false
@@ -71,9 +71,9 @@ class OfferUseCases {
// Input Validation // Input Validation
// TODO: This is a hack. Find a better way to protect against the corner- // TODO: This is a hack. Find a better way to protect against the corner-
// case of counter-offers getting routed here. // case of counter-offers getting routed here.
if (offerObj.data.dataType === 'counter-offer') { // if (offerObj.data.dataType === 'counter-offer') {
console.log('WARN: Counter Offer innappropriately routed to createOffer()') // console.log('WARN: Counter Offer innappropriately routed to createOffer()')
} // }
// Verify that UTXO in offer is unspent. If it is spent, then ignore the // Verify that UTXO in offer is unspent. If it is spent, then ignore the
// offer. // offer.
@@ -90,6 +90,9 @@ class OfferUseCases {
// A new offer gets a status of 'posted' // A new offer gets a status of 'posted'
offerObj.data.offerStatus = 'posted' offerObj.data.offerStatus = 'posted'
// Set timestamp
offerObj.timestamp = new Date().getTime()
const offerEntity = this.offerEntity.validate(offerObj) const offerEntity = this.offerEntity.validate(offerObj)
console.log('offerEntity: ', offerEntity) console.log('offerEntity: ', offerEntity)
@@ -117,7 +120,7 @@ class OfferUseCases {
return true return true
} catch (err) { } catch (err) {
console.error('Error in createOffer()') console.error('Error in createOffer()', err.message)
throw err throw err
} }
} }
@@ -140,7 +143,7 @@ class OfferUseCases {
// Retrieve the mutable data from Filecoin/IPFS. // Retrieve the mutable data from Filecoin/IPFS.
// const url = `https://${cid}.ipfs.w3s.link/data.json` // const url = `https://${cid}.ipfs.w3s.link/data.json`
const url = `${this.config.ipfsGateway}${cid}/data.json` const url = `${this.config.ipfsGateway}${cid}/data.json`
const result = await axios.get(url) const result = await this.axios.get(url)
const mutableData = result.data const mutableData = result.data
console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`) console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`)
@@ -207,11 +210,11 @@ class OfferUseCases {
async listOffers (page = 0) { async listOffers (page = 0) {
try { try {
const data = await this.OfferModel.find({}) const data = await this.OfferModel.find({})
// Sort entries so newest entries show first. // Sort entries so newest entries show first.
.sort('-timestamp') .sort('-timestamp')
// Skip to the start of the selected page. // Skip to the start of the selected page.
.skip(page * DEFAULT_ENTRIES_PER_PAGE) .skip(page * DEFAULT_ENTRIES_PER_PAGE)
// Only return 20 results. // Only return 20 results.
.limit(DEFAULT_ENTRIES_PER_PAGE) .limit(DEFAULT_ENTRIES_PER_PAGE)
return data return data
@@ -227,11 +230,11 @@ class OfferUseCases {
displayCategory: { $ne: 'fungible' }, displayCategory: { $ne: 'fungible' },
nsfw nsfw
}) })
// Sort entries so newest entries show first. // Sort entries so newest entries show first.
.sort('-timestamp') .sort('-timestamp')
// Skip to the start of the selected page. // Skip to the start of the selected page.
.skip(page * NFT_ENTRIES_PER_PAGE) .skip(page * NFT_ENTRIES_PER_PAGE)
// Only return 20 results. // Only return 20 results.
.limit(NFT_ENTRIES_PER_PAGE) .limit(NFT_ENTRIES_PER_PAGE)
// console.log('listNftOffers() returning this data: ', data) // console.log('listNftOffers() returning this data: ', data)
@@ -246,11 +249,11 @@ class OfferUseCases {
async listFungibleOffers (page = 0) { async listFungibleOffers (page = 0) {
try { try {
const data = await this.OfferModel.find({ displayCategory: 'fungible' }) const data = await this.OfferModel.find({ displayCategory: 'fungible' })
// Sort entries so newest entries show first. // Sort entries so newest entries show first.
.sort('-timestamp') .sort('-timestamp')
// Skip to the start of the selected page. // Skip to the start of the selected page.
.skip(page * FUNGIBLE_ENTRIES_PER_PAGE) .skip(page * FUNGIBLE_ENTRIES_PER_PAGE)
// Only return 20 results. // Only return 20 results.
.limit(FUNGIBLE_ENTRIES_PER_PAGE) .limit(FUNGIBLE_ENTRIES_PER_PAGE)
// console.log('listFungibleOffers() returning this data: ', data) // console.log('listFungibleOffers() returning this data: ', data)
@@ -270,12 +273,12 @@ class OfferUseCases {
// provide functionality to take less than the total amount of tokens offered // provide functionality to take less than the total amount of tokens offered
// (offerInfo.numTokens). Taking less than the offered amount will be added // (offerInfo.numTokens). Taking less than the offered amount will be added
// in the future. // in the future.
async takeOffer (offerCid) { async takeOffer (eventId) {
try { try {
console.log('offerCid: ', offerCid) if (!eventId || typeof eventId !== 'string') throw new Error('eventId must be a string')
// Get the Offer information // Get the Offer information
const offerInfo = await this.findOfferByHash(offerCid) const offerInfo = await this.findOfferByEvent(eventId)
console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`) console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`)
// Ensure the offer is in a 'posted' state and not already 'taken' // Ensure the offer is in a 'posted' state and not already 'taken'
@@ -288,6 +291,8 @@ class OfferUseCases {
tx_hash: offerInfo.utxoTxid, tx_hash: offerInfo.utxoTxid,
tx_pos: offerInfo.utxoVout tx_pos: offerInfo.utxoVout
} }
// Note : should be added to retry-queue?
const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo) const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
console.log('utxoStatus: ', utxoStatus) console.log('utxoStatus: ', utxoStatus)
if (!utxoStatus) { if (!utxoStatus) {
@@ -307,6 +312,7 @@ class OfferUseCases {
// Calculate amount of sats to generate a counter offer. // Calculate amount of sats to generate a counter offer.
let satsToMove = Math.ceil(offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit)) let satsToMove = Math.ceil(offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit))
console.log('satsToMove', satsToMove, offerInfo)
if (isNaN(satsToMove)) { if (isNaN(satsToMove)) {
throw new Error('Could not calculate the amount of BCH to generate counter offer') throw new Error('Could not calculate the amount of BCH to generate counter offer')
} }
@@ -332,9 +338,9 @@ class OfferUseCases {
// Create valid Offer object // Create valid Offer object
const takenOfferInfo = Object.assign({}, offerInfo) const takenOfferInfo = Object.assign({}, offerInfo)
takenOfferInfo.partialTxHex = partialTxHex takenOfferInfo.partialTxHex = partialTxHex
delete takenOfferInfo.p2wdbHash delete takenOfferInfo.nostrEventId
delete takenOfferInfo._id delete takenOfferInfo._id
takenOfferInfo.offerHash = offerInfo.p2wdbHash takenOfferInfo.offerHash = offerInfo.nostrEventId
// Add P2WDB specific flag for signaling that this is a new offer. // Add P2WDB specific flag for signaling that this is a new offer.
takenOfferInfo.dataType = 'counter-offer' takenOfferInfo.dataType = 'counter-offer'
@@ -342,19 +348,19 @@ class OfferUseCases {
// Write offer info to the P2WDB // Write offer info to the P2WDB
// TODO: This will trigger the webhook. Find some way of triggering the // TODO: This will trigger the webhook. Find some way of triggering the
// webhook on new offers, but not on counteroffers // webhook on new offers, but not on counteroffers
const p2wdbObj = { const nostrData = {
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey, wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
data: takenOfferInfo, data: takenOfferInfo,
appId: this.config.p2wdbAppId appId: this.config.p2wdbAppId
} }
const hash = await this.adapters.p2wdb.write(p2wdbObj) const resultEventId = await this.adapters.nostr.post(nostrData)
// Delete the Offer from the database, so that the user doesn't attempt // Delete the Offer from the database, so that the user doesn't attempt
// to take the offer more than once. // to take the offer more than once.
offerInfo.remove() offerInfo.remove()
// Return the P2WDB CID // Return the P2WDB CID
return hash return resultEventId
// return 'fake-hash' // return 'fake-hash'
} catch (err) { } catch (err) {
@@ -377,9 +383,10 @@ class OfferUseCases {
await this.adapters.wallet.bchWallet.initialize() await this.adapters.wallet.bchWallet.initialize()
// Ensure the app wallet has enough funds to write to the P2WDB. // Ensure the app wallet has enough funds to write to the P2WDB.
const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey // Note : this validation should be deprecated for nostr functionality?
const canWriteToP2WDB = await this.adapters.p2wdb.checkForSufficientFunds(wif) // const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey
if (!canWriteToP2WDB) throw new Error('App wallet does not have funds for writing to the P2WDB.') // const canWriteToP2WDB = await this.adapters.p2wdb.checkForSufficientFunds(wif)
// if (!canWriteToP2WDB) throw new Error('App wallet does not have funds for writing to the P2WDB.')
if (offerEntity.buyOrSell.includes('sell')) { if (offerEntity.buyOrSell.includes('sell')) {
// Sell Offer // Sell Offer
@@ -398,7 +405,7 @@ class OfferUseCases {
throw new Error('App wallet does not control enough BCH to purchase the tokens.') throw new Error('App wallet does not control enough BCH to purchase the tokens.')
} }
// //
} else { } else {
// Buy Offer // Buy Offer
throw new Error('Buy offers are not supported yet.') throw new Error('Buy offers are not supported yet.')
@@ -417,41 +424,66 @@ class OfferUseCases {
} }
} }
async findOfferByHash (p2wdbHash) { // Retrieve an Order model from the database. Find it by its event Id.
// try { async findOfferByEvent (nostrEventId) {
if (typeof p2wdbHash !== 'string' || !p2wdbHash) { try {
throw new Error('p2wdbHash must be a string') if (typeof nostrEventId !== 'string' || !nostrEventId) {
throw new Error('nostrEventId must be a string')
}
const order = await this.OfferModel.findOne({ nostrEventId })
if (!order) {
throw new Error('offer not found')
}
const orderObject = order.toObject()
// return this.offerEntity.validateFromModel(offerObject)
return orderObject
} catch (err) {
console.error('Error in findOfferByEvent()')
throw err
} }
}
const offer = await this.OfferModel.findOne({ p2wdbHash }) async findOfferByTxid (utxoTxid) {
try {
// try {
if (typeof utxoTxid !== 'string' || !utxoTxid) {
throw new Error('utxoTxid must be a string')
}
if (!offer) { const offer = await this.OfferModel.findOne({ utxoTxid })
throw new Error('offer not found')
// TODO: Offer should be found by TXID, then if there is more than one
// result, they should be filtered by the vout property. That will leave
// one remaining UTXO.
if (!offer) {
throw new Error('offer not found')
}
return offer
} catch (error) {
console.error('Error in use-cases/offer/findOfferByTxid(): ', error.message)
throw error
} }
return offer
// const offerObject = offer.toObject()
// return this.offerEntity.validateFromModel(offerObject)
// return offerObject
// } catch (err) {
// // console.error('Error in findOffer(): ', err)
// throw err
// }
} }
// This function is called by the P2WDB webhook REST API handler. When a // This function is called by the P2WDB webhook REST API handler. When a
// Counter Offer is passed to bch-dex by the P2WDB, the data is then passed // Counter Offer is passed to bch-dex by the P2WDB, the data is then passed
// to this function. It does due dilligence on the Counter Offer, then signs // to this function. It does due dilligence on the Counter Offer, then signs
// and broadcasts the transaction to accept the Counter Offer. // and broadcasts the transaction to accept the Counter Offer.
async acceptCounterOffer (p2wdbData) { async acceptCounterOffer (offerData) {
try { try {
console.log(`acceptCounterOffer() p2wdbData: ${JSON.stringify(p2wdbData, null, 2)}`) console.log(`acceptCounterOffer() offerData: ${JSON.stringify(offerData, null, 2)}`)
// See if this instance of bch-dex is managing the Order associated with // See if this instance of bch-dex is managing the Order associated with
// the incoming Counter Offer. // the incoming Counter Offer.
const orderHash = p2wdbData.data.offerHash
// Note : this should be handle by nostrEvent id or UtxoId?
const orderHash = offerData.data.nostrEventId
let orderData = {} let orderData = {}
try { try {
orderData = await this.orderUseCase.findOrderByEvent(orderHash) orderData = await this.orderUseCase.findOrderByEvent(orderHash)
@@ -462,12 +494,13 @@ class OfferUseCases {
} }
// Deserialize the partially signed transaction. // Deserialize the partially signed transaction.
const txHex = p2wdbData.data.partialTxHex const txHex = offerData.data.partialTxHex
const txObj = await this.adapters.wallet.deseralizeTx(txHex) const txObj = await this.adapters.wallet.deseralizeTx(txHex)
console.log(`txObj: ${JSON.stringify(txObj, null, 2)}`) console.log(`txObj: ${JSON.stringify(txObj, null, 2)}`)
// Ensure the 3rd output (vout=2) contains the required amount of BCH. // Ensure the 3rd output (vout=2) contains the required amount of BCH.
const satsToReceive = Math.ceil(orderData.numTokens * parseInt(orderData.rateInBaseUnit)) const satsToReceive = Math.ceil(orderData.numTokens * parseInt(orderData.rateInBaseUnit))
console.log('Ceil', satsToReceive)
if (isNaN(satsToReceive)) { if (isNaN(satsToReceive)) {
throw new Error('Could not calculate the amount of BCH offered in the Counter Offer') throw new Error('Could not calculate the amount of BCH offered in the Counter Offer')
} }
@@ -600,32 +633,64 @@ class OfferUseCases {
} }
} }
async flagOffer (flagData) { // Flag offers as NSFW.
console.log(`flagData: ${JSON.stringify(flagData, null, 2)}`) async flagOffer (flagData = {}) {
try {
if (!flagData.data) throw new Error('"data" property is required')
const p2wdbHash = flagData.data.p2wdbHash console.log(`flagData: ${JSON.stringify(flagData, null, 2)}`)
// Get the offer from the database. const eventId = flagData.data.nostrEventId
const offer = await this.findOfferByHash(p2wdbHash)
console.log(`Flagging this offer: ${JSON.stringify(offer, null, 2)}`)
if (!offer) { // Get the offer from the database.
throw new Error(`Offer ${p2wdbHash} not found in the database.`) const offer = await this.findOfferByEvent(eventId)
console.log(`Flagging this offer: ${JSON.stringify(offer, null, 2)}`)
if (!offer) {
throw new Error(`Offer ${eventId} not found in the database.`)
}
// Add the raw flag data to the database model.
offer.flags.push(flagData)
// If flag count is 3 or more, mark the Offer as NSFW
const flagCnt = offer.flags.length
if (flagCnt >= 3) {
offer.nsfw = true
}
// Save the updated offer data to the database.
await offer.save()
return true
} catch (error) {
console.error('Error in flagOffer(): ', error)
throw error
} }
}
// Add the raw flag data to the database model. // Get offers data from nostr.
offer.flags.push(flagData) async loadOffers () {
try {
// Retrieve offers array.
const offers = await this.adapters.nostr.read()
for (let i = 0; i < offers.length; i++) {
try {
const offer = offers[i]
// If flag count is 3 or more, mark the Offer as NSFW // offer data
const flagCnt = offer.flags.length const offerObj = JSON.parse(offer)
if (flagCnt >= 3) {
offer.nsfw = true // Try to create new offer
await this.createOffer(offerObj)
} catch (error) {
/* exit quietly */
}
}
} catch (error) {
console.error('Error in loadOffers(): ', error)
throw error
} }
// Save the updated offer data to the database.
await offer.save()
return true
} }
} }
+1 -1
View File
@@ -55,7 +55,7 @@ class OrderLib {
// Ensure sufficient tokens exist to create the order. // Ensure sufficient tokens exist to create the order.
// await this.ensureFunds(orderEntity) // await this.ensureFunds(orderEntity)
await this.retryQueue.addToQueue(this.ensureFunds, orderEntity) // await this.retryQueue.addToQueue(this.ensureFunds, orderEntity)
// Get Ticker for token ID. // Get Ticker for token ID.
// const tokenData = await this.adapters.wallet.bchWallet.getTxData([entryObj.tokenId]) // const tokenData = await this.adapters.wallet.bchWallet.getTxData([entryObj.tokenId])
+65 -22
View File
@@ -56,27 +56,70 @@ describe('#Timer-Controllers', () => {
}) })
}) })
// describe('#startTimers', () => { describe('#startTimers', () => {
// it('should start the timers', () => { it('should start the timers', () => {
// const result = uut.startTimers() const result = uut.startTimers()
//
// // uut.stopTimers()
//
// assert.equal(result, true)
// })
// })
// describe('#exampleTimerFunc', () => { // uut.stopTimers()
// it('should kick off the Use Case', async () => {
// const result = await uut.exampleTimerFunc() assert.equal(result, true)
// })
// assert.equal(result, true) })
// })
// describe('#gcOrders', () => {
// it('should return false on error', async () => { it('should kick off the Use Case', async () => {
// const result = await uut.exampleTimerFunc(true) const result = await uut.gcOrders()
//
// assert.equal(result, false) assert.equal(result, true)
// }) })
// })
it('should return false on error', async () => {
sandbox.stub(uut.useCases.order, 'removeStaleOrders').throws(new Error('test error'))
const result = await uut.gcOrders()
assert.equal(result, false)
})
})
describe('#gcOffers', () => {
it('should kick off the Use Case', async () => {
const result = await uut.gcOffers()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.offer, 'removeStaleOffers').throws(new Error('test error'))
const result = await uut.gcOffers()
assert.equal(result, false)
})
})
describe('#checkDupOffers', () => {
it('should kick off the Use Case', async () => {
const result = await uut.checkDupOffers()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.offer, 'removeDuplicateOffers').throws(new Error('test error'))
const result = await uut.checkDupOffers()
assert.equal(result, false)
})
})
describe('#loadOffers', () => {
it('should kick off the Use Case', async () => {
const result = await uut.loadOffers()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.offer, 'loadOffers').throws(new Error('test error'))
const result = await uut.loadOffers()
assert.equal(result, false)
})
})
}) })
+5 -1
View File
@@ -151,7 +151,11 @@ const wallet = {
}, },
bchWallet: new MockBchWallet(), bchWallet: new MockBchWallet(),
moveTokens: async () => {}, moveTokens: async () => {},
reclaimTokens: async ()=>{} moveBch: async () => {},
reclaimTokens: async ()=>{},
generatePartialTx: async ()=>{},
deseralizeTx:async ()=>{},
completeTx:async ()=>{ return ''},
} }
const p2wdb = { const p2wdb = {
+1
View File
@@ -24,6 +24,7 @@ class MockBchWallet {
this.sendTokens = async () => { this.sendTokens = async () => {
return 'fakeTxid'; return 'fakeTxid';
}; };
this.utxoIsValid =async ()=>{}
this.getUtxos = async () => { }; this.getUtxos = async () => { };
this.getBalance = async () => { }; this.getBalance = async () => { };
this.listTokens = async () => { }; this.listTokens = async () => { };
+12
View File
@@ -41,12 +41,24 @@ class Offer {
async createOffer() { async createOffer() {
return {} return {}
} }
async removeStaleOffers(){
}
async removeDuplicateOffers(){
}
async loadOffers(){
}
} }
class Order { class Order {
async createOrder() { async createOrder() {
return {} return {}
} }
async removeStaleOrders(){
}
} }
class UseCasesMock { class UseCasesMock {
+82 -37
View File
@@ -3,45 +3,45 @@
*/ */
const nftOffer01 = { const nftOffer01 = {
"messageType": 1, "messageType": 1,
"messageClass": 1, "messageClass": 1,
"tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b", "tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b",
"buyOrSell": "sell", "buyOrSell": "sell",
"rateInBaseUnit": 7672536, "rateInBaseUnit": 7672536,
"minUnitsToExchange": 7672536, "minUnitsToExchange": 7672536,
"numTokens": 1, "numTokens": 1,
"utxoTxid": "c736d73e274df20e9b069b4990d1a264fb80aa98d67cc6c7a39e42bff48e7c04", "utxoTxid": "c736d73e274df20e9b069b4990d1a264fb80aa98d67cc6c7a39e42bff48e7c04",
"utxoVout": 1, "utxoVout": 1,
"timestamp": 1662930309998, "timestamp": 1662930309998,
"globaltimestamp": "2022-09-11T21:05:09.998Z", "globaltimestamp": "2022-09-11T21:05:09.998Z",
"localTimestamp": "9/11/2022, 9:05:09 PM", "localTimestamp": "9/11/2022, 9:05:09 PM",
"txid": "4266862b8358664996038d8c29d49dc0f3d3058fd1ef3d567b084e6b16ceb5b2", "txid": "4266862b8358664996038d8c29d49dc0f3d3058fd1ef3d567b084e6b16ceb5b2",
"p2wdbHash": "zdpuArq7rCuVCGPyTnWpLjy9AYsh8yrbwjGdKwg2GDDALkM8t", "p2wdbHash": "zdpuArq7rCuVCGPyTnWpLjy9AYsh8yrbwjGdKwg2GDDALkM8t",
"offerStatus": "posted", "offerStatus": "posted",
"makerAddr": "bitcoincash:qrqlz63cwmu0hcmsrfnd8jemn3atkpaqds6tf4ksrr", "makerAddr": "bitcoincash:qrqlz63cwmu0hcmsrfnd8jemn3atkpaqds6tf4ksrr",
"ticker": "TV001", "ticker": "TV001",
"tokenType": 65 "tokenType": 65
} }
const nftTokenData01 = { const nftTokenData01 = {
"genesisData": { "genesisData": {
"type": 65, "type": 65,
"ticker": "TV001", "ticker": "TV001",
"name": "Introduction to NFTs on BCH", "name": "Introduction to NFTs on BCH",
"tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b", "tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b",
"documentUri": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4", "documentUri": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4",
"documentHash": "c1731268f4873f1928438abdaf6ffc546d86a1817dc6f3c6bc73fbdfb4664f10", "documentHash": "c1731268f4873f1928438abdaf6ffc546d86a1817dc6f3c6bc73fbdfb4664f10",
"decimals": 0, "decimals": 0,
"mintBatonIsActive": false, "mintBatonIsActive": false,
"tokensInCirculationBN": "1", "tokensInCirculationBN": "1",
"tokensInCirculationStr": "1", "tokensInCirculationStr": "1",
"blockCreated": 740395, "blockCreated": 740395,
"totalBurned": "0", "totalBurned": "0",
"totalMinted": "1", "totalMinted": "1",
"parentGroupId": "030563ddd65772d8e9b79b825529ed53c7d27037507b57c528788612b4911107" "parentGroupId": "030563ddd65772d8e9b79b825529ed53c7d27037507b57c528788612b4911107"
}, },
"immutableData": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4", "immutableData": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4",
"mutableData": "ipfs://bafybeifzhunfpodsztsj5x4ypopngkxuapbxwaxaxkahaybdktk6joqtlq" "mutableData": "ipfs://bafybeifzhunfpodsztsj5x4ypopngkxuapbxwaxaxkahaybdktk6joqtlq"
} }
const simpleNftOffer01 = { const simpleNftOffer01 = {
@@ -126,11 +126,56 @@ const fungibleTokenData01 = {
"mutableData": "" "mutableData": ""
} }
const offerMockData = {
data: {
messageType: 1,
messageClass: 1,
tokenId:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
buyOrSell: 'sell',
rateInBaseUnit: 1000,
minUnitsToExchange: 10,
numTokens: 0.02,
utxoTxid:
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
utxoVout: 0,
makerAddr: 'address',
tokenType: 1
}
}
const deserealizeTxMock = {
//...
vout: [
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
//...
]
}
export default { export default {
nftOffer01, nftOffer01,
nftTokenData01, nftTokenData01,
simpleNftOffer01, simpleNftOffer01,
simpleNftTokenData01, simpleNftTokenData01,
fungibleOffer01, fungibleOffer01,
fungibleTokenData01 fungibleTokenData01,
offerMockData,
deserealizeTxMock
}; };
+624 -74
View File
@@ -48,79 +48,67 @@ describe('#offer-use-case', () => {
) )
} }
}) })
it('should throw an error if order use cases are not passed in', () => {
try {
uut = new OfferLib({ adapters })
assert.fail('Unexpected code path')
console.log(uut) // linter
} catch (err) {
assert.include(
err.message,
'Instance of Order Use Cases must be passed in when instantiating Offer Use Cases library.'
)
}
})
}) })
describe('#createOffer', () => { describe('#createOffer', () => {
// it('should ignore an offer if utxo has been spent', async () => { it('should handle error', async () => {
// const offerObj = { try {
// appId: 'swapTest555', await uut.createOffer()
// data: { assert.fail('unexpected code path')
// messageType: 1, } catch (error) {
// messageClass: 1, assert.include(error.message, 'Cannot read properties of undefined')
// tokenId: }
// '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0', })
// buyOrSell: 'sell', it('should return false if offer already exist', async () => {
// rateInSats: 1000, const offerObj = mockData.offerMockData
// minSatsToExchange: 10,
// numTokens: 0.02,
// utxoTxid:
// '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
// utxoVout: 0
// },
// timestamp: '2021-09-20T17:54:26.395Z',
// localTimeStamp: '9/20/2021, 10:54:26 AM',
// txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7',
// hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
// }
//
// // Mock dependencies
// // sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
// sandbox.stub(uut.adapters.wallet.bchWallet, 'getTokenData').resolves({})
// sandbox.stub(uut, 'categorizeToken').resolves('nft')
// sandbox.stub(uut, 'detectNsfw').resolves(false)
//
// const result = await uut.createOffer(offerObj)
// // console.log('result: ', result)
//
// assert.equal(result, false)
// })
// it('should create an offer and return the hash', async () => { // Mock dependencies
// const offerObj = { // sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
// appId: 'swapTest555', sandbox.stub(uut, 'findOfferByTxid').resolves({})
// data: {
// messageType: 1, const result = await uut.createOffer(offerObj)
// messageClass: 1, assert.isFalse(result)
// tokenId: })
// '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0', it('should return false for invalid utxo', async () => {
// buyOrSell: 'sell', const offerObj = mockData.offerMockData
// rateInBaseUnit: 1000,
// minUnitsToExchange: 10, // Mock dependencies
// numTokens: 0.02, // sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
// utxoTxid: sandbox.stub(uut, 'findOfferByTxid').throws(new Error('offer not found'))
// '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87', sandbox.stub(uut.retryQueue, 'addToQueue').resolves(null)
// utxoVout: 0,
// makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', const result = await uut.createOffer(offerObj)
// ticker: 'TROUT', assert.isFalse(result)
// tokenType: 1 })
// },
// timestamp: '2021-09-20T17:54:26.395Z', it('should create offer', async () => {
// localTimeStamp: '9/20/2021, 10:54:26 AM', const tokenDataMock = mockData.simpleNftTokenData01
// txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7', const offerObj = mockData.offerMockData
// hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
// } // Mock dependencies
// // sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
// // Mock dependencies sandbox.stub(uut, 'findOfferByTxid').throws(new Error('offer not found'))
// // sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(true) sandbox.stub(uut.retryQueue, 'addToQueue')
// sandbox.stub(uut, 'categorizeToken').resolves('fungible') .onCall(0).resolves({}) // Utxo Status call
// sandbox.stub(uut.adapters.wallet.bchWallet, 'getTokenData').resolves({}) .onCall(1).resolves(tokenDataMock) // Token Data call
// sandbox.stub(uut, 'detectNsfw').resolves(false) .onCall(2).resolves(false) // detectNsfw call
//
// const result = await uut.createOffer(offerObj) const result = await uut.createOffer(offerObj)
// // console.log('result: ', result) assert.isTrue(result)
// })
// assert.equal(result, true)
// })
}) })
describe('#categorizeToken', () => { describe('#categorizeToken', () => {
@@ -159,15 +147,28 @@ describe('#offer-use-case', () => {
assert.equal(result, 'fungible') assert.equal(result, 'fungible')
}) })
it('should unknow type', async () => {
try {
// Mock dependencies
const offerData = mockData.fungibleOffer01
const tokenData = { genesisData: {} }
await uut.categorizeToken(offerData, tokenData)
assert.fail('unexpected code path')
} catch (error) {
assert.include(error.message, 'Unknown token type:')
}
})
}) })
describe('#removeDuplicateOffers', () => { describe('#removeDuplicateOffers', () => {
it('should remove duplicate entries and return true', async () => { it('should remove duplicate entries and return true', async () => {
// Mock dependencies and force desired code path. // Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').resolves([ sandbox.stub(uut.OfferModel, 'find').resolves([
{ p2wdbHash: 'a', remove: async () => {} }, { p2wdbHash: 'a', remove: async () => { } },
{ p2wdbHash: 'a', remove: async () => {} }, { p2wdbHash: 'a', remove: async () => { } },
{ p2wdbHash: 'b', remove: async () => {} } { p2wdbHash: 'b', remove: async () => { } }
]) ])
// sandbox.stub(uut.OfferModel, 'remove').resolves() // sandbox.stub(uut.OfferModel, 'remove').resolves()
@@ -180,8 +181,8 @@ describe('#offer-use-case', () => {
it('should return false if there are no duplicate entries', async () => { it('should return false if there are no duplicate entries', async () => {
// Mock dependencies and force desired code path. // Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').resolves([ sandbox.stub(uut.OfferModel, 'find').resolves([
{ p2wdbHash: 'a', remove: async () => {} }, { p2wdbHash: 'a', remove: async () => { } },
{ p2wdbHash: 'b', remove: async () => {} } { p2wdbHash: 'b', remove: async () => { } }
]) ])
// sandbox.stub(uut.OfferModel, 'remove').resolves() // sandbox.stub(uut.OfferModel, 'remove').resolves()
@@ -190,5 +191,554 @@ describe('#offer-use-case', () => {
assert.equal(result, false) assert.equal(result, false)
}) })
it('should handle error', async () => {
try {
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error'))
// sandbox.stub(uut.OfferModel, 'remove').resolves()
await uut.removeDuplicateOffers()
assert.fail('unexpected code path')
} catch (error) {
assert.equal(error.message, 'test error')
}
})
})
describe('#removeStaleOffers', () => {
it('remove offer with wrong utxoState', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }])
sandbox.stub(uut.retryQueue, 'addToQueue').resolves(false)
await uut.removeStaleOffers()
})
it('remove offer with wrong txid', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }])
sandbox.stub(uut.retryQueue, 'addToQueue').throws(new Error('txid needs to be a proper transaction ID'))
await uut.removeStaleOffers()
})
it('remove expired offer ', async () => {
const tsMock = new Date()
tsMock.setMonth(tsMock.getMonth() - 3)
const timestamp = tsMock.getTime()
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').resolves([{ timestamp, remove: async () => { } }])
sandbox.stub(uut.retryQueue, 'addToQueue').resolves(true)
await uut.removeStaleOffers()
})
it('should handle axios error ', async () => {
const testErr = new Error()
testErr.isAxiosError = true
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }])
sandbox.stub(uut.retryQueue, 'addToQueue').throws(testErr)
await uut.removeStaleOffers()
})
it('should handle error ', async () => {
try {
const testErr = new Error('unknow error')
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }])
sandbox.stub(uut.retryQueue, 'addToQueue').throws(testErr)
await uut.removeStaleOffers()
assert.fail('unexpected code path')
} catch (error) {
assert.equal(error.message, 'unknow error')
}
})
})
describe('#findOfferByTxid', () => {
it('should throw an error if input is not provided', async () => {
try {
await uut.findOfferByTxid()
assert.fail('unexpected code path')
} catch (error) {
assert.equal(error.message, 'utxoTxid must be a string')
}
})
it('should throw an error if offer is not found', async () => {
try {
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'findOne').resolves(null)
await uut.findOfferByTxid('241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87')
assert.fail('unexpected code path')
} catch (error) {
assert.equal(error.message, 'offer not found')
}
})
it('should return offer', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'findOne').resolves(mockData.nftOffer01)
const result = await uut.findOfferByTxid('241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87')
assert.isObject(result)
})
})
describe('#detectNsfw', () => {
it('should return false for wrong cid format', async () => {
const result = await uut.detectNsfw({ mutableData: '' })
assert.isFalse(result)
})
it('should return true if nft boolean detected', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.axios, 'get').resolves({ data: { nsfw: true } })
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
assert.isTrue(result)
})
it('should return true if nft string detected', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.axios, 'get').resolves({ data: { nsfw: 'true' } })
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
assert.isTrue(result)
})
it('should return false if nfsw property does not exist', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.axios, 'get').resolves({ data: {} })
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
assert.isFalse(result)
})
it('should return false on error', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.axios, 'get').throws(new Error('test error'))
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
assert.isFalse(result)
})
})
describe('#listOffers', () => {
it('should handle error', async () => {
try {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error'))
await uut.listOffers()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should list orders', async () => {
const queryMock = {
sort () {
return this
},
skip () {
return this
},
limit () { return [] }
}
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').returns(queryMock)
const result = await uut.listOffers()
assert.isArray(result)
})
})
describe('#listNftOffers', () => {
it('should handle error', async () => {
try {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error'))
await uut.listNftOffers()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should list orders', async () => {
const queryMock = {
sort () {
return this
},
skip () {
return this
},
limit () { return [] }
}
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').returns(queryMock)
const result = await uut.listNftOffers(1, true)
assert.isArray(result)
})
})
describe('#listFungibleOffers', () => {
it('should handle error', async () => {
try {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error'))
await uut.listFungibleOffers()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should list orders', async () => {
const queryMock = {
sort () {
return this
},
skip () {
return this
},
limit () { return [] }
}
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').returns(queryMock)
const result = await uut.listFungibleOffers()
assert.isArray(result)
})
})
describe('#takeOffer', () => {
it('should handle error if input is not provided', async () => {
try {
await uut.takeOffer()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'eventId must be a string')
}
})
it('should handle error for wrong offer status', async () => {
try {
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'completed' })
await uut.takeOffer('eventId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'offer status is not "posted", so offer is dead and can not be countered.')
}
})
it('should handle error for invalid utxo', async () => {
try {
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' })
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(false)
await uut.takeOffer('eventId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'UTXO does not exist. Aborting.')
}
})
it('should handle insufficient funds', async () => {
try {
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' })
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true)
sandbox.stub(uut, 'ensureFunds').throws(new Error('test error'))
await uut.takeOffer('eventId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should handle error if counter offer cant be calculated', async () => {
try {
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' })
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true)
sandbox.stub(uut, 'ensureFunds').resolves(true)
await uut.takeOffer('eventId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Could not calculate the amount of BCH to generate counter offer')
}
})
it('should handle error if counter offer cant be calculated', async () => {
try {
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' })
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true)
sandbox.stub(uut, 'ensureFunds').resolves(true)
await uut.takeOffer('eventId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Could not calculate the amount of BCH to generate counter offer')
}
})
it('should take offer', async () => {
// Mock data
const offerMock = Object.assign({}, mockData.offerMockData.data)
offerMock.remove = async () => { }
offerMock.offerStatus = 'posted'
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').returns(offerMock)
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true)
sandbox.stub(uut, 'ensureFunds').resolves(true)
sandbox.stub(uut.adapters.wallet, 'moveBch').resolves({ sats: 1 })
await uut.takeOffer('eventId')
})
})
describe('#ensureFunds', () => {
// it('should handle insufficient funds to use p2wdb', async () => {
// try {
// // Mock dependencies
// sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(false)
//
// await uut.ensureFunds(mockData.offerMockData.data)
//
// assert.fail('Unexpected code path')
// } catch (err) {
// assert.include(err.message, 'App wallet does not have funds for writing to the P2WDB')
// }
// })
it('should throw error if sats needed could no be able to calculated', async () => {
try {
// Mock dependencies
sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(true)
// Mock Input
const mock = Object.assign({}, mockData.offerMockData.data)
mock.rateInBaseUnit = null
await uut.ensureFunds(mock)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Could not calculate sats needed!')
}
})
it('should throw error if app wallet does not have enough bch', async () => {
try {
// Mock dependencies
sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(true)
sandbox.stub(uut.adapters.wallet.bchWallet, 'getBalance').resolves(0)
await uut.ensureFunds(mockData.offerMockData.data)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'App wallet does not control enough BCH to purchase the tokens.')
}
})
it('should handle BUY offer', async () => {
try {
const mock = Object.assign({}, mockData.offerMockData.data)
mock.buyOrSell = 'buy'
await uut.ensureFunds(mock)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Buy offers are not supported yet.')
}
})
it('should return true', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(true)
sandbox.stub(uut.adapters.wallet.bchWallet, 'getBalance').resolves(10 * 10 ** 6)
const result = await uut.ensureFunds(mockData.offerMockData.data)
assert.isTrue(result)
})
})
describe('#findOrderByEvent', () => {
it('should throw an error if hash is not provided', async () => {
try {
await uut.findOfferByEvent()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'nostrEventId must be a string')
}
})
it('should throw an error if order is not found!', async () => {
try {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'findOne').resolves(null)
await uut.findOfferByEvent('eventId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'offer not found')
}
})
it('should return offer by eventId', async () => {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'findOne').resolves({ toObject: () => { return { hash: 'hash' } } })
const result = await uut.findOfferByEvent('eventId')
assert.isObject(result)
})
})
describe('#flagOffer', () => {
it('should throw an error if input is not provided', async () => {
try {
await uut.flagOffer()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, '"data" property is required')
}
})
it('should throw an error if offer is not found!', async () => {
try {
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').resolves(null)
const input = {
data:
{
nostrEventId: 'eventId'
}
}
await uut.flagOffer(input)
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'not found in the database')
}
})
it('should flag offer', async () => {
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').resolves({ flags: ['a', 'b', 'c'], save: () => { } })
const input = {
data:
{
nostrEventId: 'eventId'
}
}
const result = await uut.flagOffer(input)
assert.isTrue(result)
})
})
describe('#loadOffers', () => {
it('should handle nostr error', async () => {
try {
// Mock dependencies
sandbox.stub(uut.adapters.nostr, 'read').throws(new Error('test error'))
await uut.loadOffers()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should skip internal function errors ', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.nostr, 'read').resolves([mockData.offerMockData])
await uut.loadOffers()
})
it('should review and load offers', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.nostr, 'read').resolves([JSON.stringify(mockData.offerMockData)])
await uut.loadOffers()
})
})
describe('#acceptCounterOffer', () => {
it('should return if order is not found!', async () => {
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').throws(new Error('test error'))
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.equal(result, 'N/A')
})
it('should handle error if counter offer cant be calculated', async () => {
try {
// Mock Data
const mock = Object.assign({}, mockData.offerMockData.data)
mock.rateInBaseUnit = null
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock)
await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Could not calculate the amount of BCH offered in the Counter Offer')
}
})
it('should handle error for wrong transaction output', async () => {
try {
// Mock dependencies
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)
await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'The Counter Offer has an output of ')
assert.include(err.message, 'which does not match the required')
}
})
it('should handle error for wrong transaction output address', async () => {
try {
// 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, 'findOrderByEvent').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 (err) {
assert.include(err.message, 'The Counter Offer has an output address of')
assert.include(err.message, 'which does not match the Maker address')
}
})
it('should return tx id', async () => {
// Mock data
const mock = Object.assign({}, mockData.offerMockData.data)
mock.makerAddr = mockData.deserealizeTxMock.vout[2].scriptPubKey.addresses[0] // Maker Address
mock.rateInBaseUnit = 0
mock.numTokens = 0
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').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.isString(result)
})
}) })
}) })
+5 -4
View File
@@ -9,10 +9,10 @@
// Public npm libraries // Public npm libraries
import BCHJS from '@psf/bch-js' import BCHJS from '@psf/bch-js'
import BchTokenSweep from 'bch-token-sweep/index' import BchTokenSweep from 'bch-token-sweep'
// Local libraries // Local libraries
import WalletAdapter from '../../src/adapters/wallet' import WalletAdapter from '../../src/adapters/wallet.js'
// Constants // Constants
const EMTPY_ADDR_CUTOFF = 15 const EMTPY_ADDR_CUTOFF = 15
@@ -22,6 +22,7 @@ async function sweepFunds () {
// Open the wallet files. // Open the wallet files.
const wallet = new WalletAdapter() const wallet = new WalletAdapter()
const walletInfo = await wallet.openWallet() const walletInfo = await wallet.openWallet()
const bchWallet = await wallet.instanceWallet(walletInfo)
console.log('walletInfo: ', walletInfo) console.log('walletInfo: ', walletInfo)
const rootAddr = walletInfo.cashAddress const rootAddr = walletInfo.cashAddress
@@ -49,7 +50,7 @@ async function sweepFunds () {
const sweeper = new BchTokenSweep( const sweeper = new BchTokenSweep(
wifToSweep, wifToSweep,
rootWif, rootWif,
bchjs, bchWallet,
550, 550,
rootAddr rootAddr
) )
@@ -68,7 +69,7 @@ async function sweepFunds () {
// Wait between loop iterations. // Wait between loop iterations.
await bchjs.Util.sleep(3000) await bchjs.Util.sleep(3000)
} catch (err) { } catch (err) {
console.log(`error message with index ${hdIndex}: ${err.message}`) console.log(`error message with index ${hdIndex}: ${err}`)
emptyAddrCnt++ emptyAddrCnt++
} }