/* Use Case library for Offers. Offers are created by a webhook trigger from the P2WDB. Offers are a result of new data in P2WDB. They differ from Offers, which are generated by a local user. An Offer is created to match a local Offer, but it's created indirectly, as a response to the webhook from the P2WDB. In this way, Offers generated from local Offers are no different than Offers generated by other peers. A Counter Offer is created by calling the /take/:cid endpoint. It creates a partially signed transaction. */ // Global npm libraries import axios from 'axios' import RetryQueue from '@chris.troutner/retry-queue' // Local libraries import OfferEntity from '../../entities/offer.js' import config from '../../../config/index.js' const DEFAULT_ENTRIES_PER_PAGE = 20 const NFT_ENTRIES_PER_PAGE = 6 const FUNGIBLE_ENTRIES_PER_PAGE = 20 class OfferUseCases { constructor (localConfig = {}) { // console.log('User localConfig: ', localConfig) this.adapters = localConfig.adapters if (!this.adapters) { throw new Error( 'Instance of adapters must be passed in when instantiating Offer Use Cases library.' ) } this.orderUseCase = localConfig.order if (!this.orderUseCase) { throw new Error( 'Instance of Order Use Cases must be passed in when instantiating Offer Use Cases library.' ) } // Encapsulate dependencies this.config = config this.axios = axios this.offerEntity = new OfferEntity() this.OfferModel = this.adapters.localdb.Offer this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 }) // Bind 'this' object to functions this.createOffer = this.createOffer.bind(this) this.detectNsfw = this.detectNsfw.bind(this) this.categorizeToken = this.categorizeToken.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.ensureFunds = this.ensureFunds.bind(this) this.findOfferByEvent = this.findOfferByEvent.bind(this) this.findOfferByTxid = this.findOfferByTxid.bind(this) this.acceptCounterOffer = this.acceptCounterOffer.bind(this) this.removeDuplicateOffers = this.removeDuplicateOffers.bind(this) this.removeStaleOffers = this.removeStaleOffers.bind(this) this.flagOffer = this.flagOffer.bind(this) this.loadOffers = this.loadOffers.bind(this) this.listOffersByAddress = this.listOffersByAddress.bind(this) // State this.seenOffers = [] this.seenCounterOffers = [] } // This method is called by timer controller to load offers from a Nostr topic. async createOffer (offerObj) { try { // console.log('Use Case createOffer(offerObj): ', offerObj) // Return if Offer already exists in database with the same utxo transaction id. try { await this.findOfferByTxid(offerObj.data.utxoTxid) // console.log('Offer already found in local database.') return false } catch (err) { /* exit quietly */ } // console.log('this.adapters.bchjs: ', this.adapters.bchjs) // Input Validation // TODO: This is a hack. Find a better way to protect against the corner- // case of counter-offers getting routed here. // if (offerObj.data.dataType === 'counter-offer') { // console.log('WARN: Counter Offer innappropriately routed to createOffer()') // } // Quickly skip over offers that have already been processed. const eventId = offerObj.data.nostrEventId if (this.seenOffers.includes(eventId)) { // console.log(`Offer with event ID ${eventId} already processed. Skipping.`) return false } this.seenOffers.push(eventId) // Verify that UTXO in offer is unspent. If it is spent, then ignore the // offer. const utxo = { tx_hash: offerObj.data.utxoTxid, tx_pos: offerObj.data.utxoVout } // const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo) const utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo) // console.log('utxoStatus: ', utxoStatus) // if (utxoStatus === null) return false if (!utxoStatus) { console.log(`UTXO txid: ${offerObj.data.utxoTxid}, vout: ${offerObj.data.utxoVout} has been spent. Skipping.`) return false } // A new offer gets a status of 'posted' offerObj.data.offerStatus = 'posted' // Set timestamp offerObj.timestamp = new Date().getTime() const offerEntity = this.offerEntity.validate(offerObj) // console.log('offerEntity: ', offerEntity) console.log(`New Offer for token ID ${offerEntity.tokenId} detected from Nostr post ${eventId}`) // Get data about the token. const tokenId = offerEntity.tokenId // const tokenData = await this.adapters.wallet.bchWallet.getTokenData(tokenId) const tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTokenData, tokenId) // console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`) // Generate a 'display category' for the token. This will allow the // front end UI to figure out how to display the token. const displayCategory = this.categorizeToken(offerEntity, tokenData) console.log('displayCategory: ', displayCategory) offerEntity.displayCategory = displayCategory // Detect if user set the NSFW flag. // const nsfw = false // nsfw = await this.retryQueue.addToQueue(this.detectNsfw, tokenData) // offerEntity.nsfw = nsfw // Add offer to the local database. const offerModel = new this.OfferModel(offerEntity) await offerModel.save() return true } catch (err) { console.error('Error in createOffer()', err.message) throw err } } // By default this function returns false, to indicate the NFT is safe for work. // If the user who created the token sets the nsfw property in the mutable data, // this function will return true. async detectNsfw (tokenData) { try { let nsfw = false const mutableCid = tokenData.mutableData // If there is no mutable IPFS CID, then skip this token. if (!mutableCid.includes('ipfs://')) return nsfw // Revove the ipfs:// prefix. const cid = mutableCid.substring(7) // Retrieve the mutable data from Filecoin/IPFS. // const url = `https://${cid}.ipfs.w3s.link/data.json` let mutableData = {} try { // Try the conventional data URL. const url = `${this.config.ipfsGateway}${cid}/data.json` const result = await this.axios.get(url) mutableData = result.data // console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`) } catch (err) { // Try the newer data URL. const url = `${this.config.ipfsGateway}${cid}` const result = await this.axios.get(url) mutableData = result.data // console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`) } console.log('mutableData: ', mutableData) // Logical tests const hasNsfw = !!mutableData.nsfw const nsfwSetTrue = mutableData.nsfw === true const nsfwStringTrue = mutableData.nsfw === 'true' const nsfwDetected = hasNsfw && (nsfwSetTrue || nsfwStringTrue) // console.log(`hasNsfw: ${hasNsfw}, nsfwSetTrue: ${nsfwSetTrue}, nsfwStringTrue: ${nsfwStringTrue}, nsfwDetected: ${nsfwDetected}`) if (nsfwDetected) { console.log('NSFW flag set as true') nsfw = true } return nsfw } catch (err) { console.error('Error in detectNsfw(): ', err) return false } } // Categorize the token for display purposes. This will categorize a token // into one of these categories: // - nft // - group // - fungible // - simple-nft // // The first three are easy to categorize. The simple-nft is a fungible token // with a quantity of 1, decimals of 0, and no minting baton. Categorizing this // type of token is the main reason why this function exists. categorizeToken (offerData, tokenData) { try { // console.log(`categorizeToken(): ${JSON.stringify(offerData, null, 2)}`) // const tokenId = offerData.tokenId // // const tokenData = await this.adapters.wallet.bchWallet.getTokenData(tokenId) // console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`) if (tokenData.genesisData.type === 65) { return 'nft' } // Create a set of checks to detect a simple NFT const isType1 = tokenData.genesisData.type === 1 const hasNoMintingBaton = !tokenData.genesisData.mintBatonIsActive const hasNoDecimals = !tokenData.genesisData.decimals const hasQtyOfOne = parseInt(tokenData.genesisData.tokensInCirculationStr) === 1 if (isType1 && hasNoMintingBaton && hasNoDecimals && hasQtyOfOne) { return 'simple-nft' } if (isType1) return 'fungible' throw new Error(`Unknown token type: ${tokenData.genesisData.type}`) } catch (err) { console.error('Error in use-cases/offer/index.js categorizeToken(): ', err) throw err } } async listOffers (page = 0) { try { const data = await this.OfferModel.find({}) // Sort entries so newest entries show first. .sort('-timestamp') // Skip to the start of the selected page. .skip(page * DEFAULT_ENTRIES_PER_PAGE) // Only return 20 results. .limit(DEFAULT_ENTRIES_PER_PAGE) return data } catch (error) { console.error('Error in use-cases/offer/listOffers()') throw error } } async listNftOffers (page = 0, nsfw = false) { try { const data = await this.OfferModel.find({ displayCategory: { $ne: 'fungible' }, nsfw }) // Sort entries so newest entries show first. .sort('-timestamp') // Skip to the start of the selected page. .skip(page * NFT_ENTRIES_PER_PAGE) // Only return 20 results. .limit(NFT_ENTRIES_PER_PAGE) // console.log('listNftOffers() returning this data: ', data) return data } catch (error) { console.error('Error in use-cases/offer/listNftOffers()') throw error } } async listFungibleOffers (page = 0) { try { const data = await this.OfferModel.find({ displayCategory: 'fungible' }) // Sort entries so newest entries show first. .sort('-timestamp') // Skip to the start of the selected page. .skip(page * FUNGIBLE_ENTRIES_PER_PAGE) // Only return 20 results. .limit(FUNGIBLE_ENTRIES_PER_PAGE) // console.log('listFungibleOffers() returning this data: ', data) return data } catch (error) { console.error('Error in use-cases/offer/listFungibleOffers()') throw error } } // Generate phase 2 of 3 - take the other side of an Offer. // Based on this example: // https://github.com/Permission=less-Software-Foundation/bch-js-examples/blob/master/bch/applications/collaborate/sell-slp/e2e-exchange/step2-purchase-tx.js // // Dev Note: Right now this use cases takes all the tokens offered. It does not // provide functionality to take less than the total amount of tokens offered // (offerInfo.numTokens). Taking less than the offered amount will be added // in the future. async takeOffer (eventId) { try { if (!eventId || typeof eventId !== 'string') throw new Error('eventId must be a string') // Get the Offer information const offerInfo = await this.findOfferByEvent(eventId) console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`) // Ensure the offer is in a 'posted' state and not already 'taken' if (!offerInfo.offerStatus || offerInfo.offerStatus !== 'posted') { throw new Error('offer status is not "posted", so offer is dead and can not be countered.') } // Verify that UTXO for sale is unspent. Abort if it's been spent. const utxo = { tx_hash: offerInfo.utxoTxid, tx_pos: offerInfo.utxoVout } // Note : should be added to retry-queue? const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo) // console.log('utxoStatus: ', utxoStatus) if (!utxoStatus) { console.log(`utxo txid: ${offerInfo.utxoTxid}, vout: ${offerInfo.utxoVout}`) // TODO: Mark this Offer as 'dead' throw new Error('UTXO does not exist. Aborting.') } // Ensure the app has enough funds to complete the trade. await this.ensureFunds(offerInfo) // Get UTXOs. // const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) // Calculate amount of sats to generate a counter offer. let satsToMove = Math.ceil(offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit)) console.log('satsToMove', satsToMove, offerInfo) if (isNaN(satsToMove)) { throw new Error('Could not calculate the amount of BCH to generate counter offer') } // Add sats to cover mining fees and dust for token UTXO satsToMove += 1000 // Move funds to create a segrated UTXO for taking the offer const utxoInfo = await this.adapters.wallet.moveBch(satsToMove) utxoInfo.sats = satsToMove console.log('utxoInfo: ', utxoInfo) // Create a partially signed transaction. // https://github.com/Permissionless-Software-Foundation/bch-js-examples/blob/master/bch/applications/collaborate/sell-slp/e2e-exchange/step2-purchase-tx.js#L59 const partialTxHex = await this.adapters.wallet.generatePartialTx(offerInfo, utxoInfo) console.log('partialTxHex: ', partialTxHex) // return partialTxHex // Debug: Decode the transaction for manual QA const txObj = await this.adapters.wallet.deseralizeTx(partialTxHex) console.log(`partially-signed transaction: ${JSON.stringify(txObj, null, 2)}`) // Create valid Offer object const takenOfferInfo = Object.assign({}, offerInfo) takenOfferInfo.partialTxHex = partialTxHex delete takenOfferInfo.nostrEventId delete takenOfferInfo._id takenOfferInfo.offerHash = offerInfo.nostrEventId // Add P2WDB specific flag for signaling that this is a new offer. takenOfferInfo.dataType = 'counter-offer' // Write offer info to the P2WDB // TODO: This will trigger the webhook. Find some way of triggering the // webhook on new offers, but not on counteroffers const nostrData = { // wif: this.adapters.wallet.bchWallet.walletInfo.privateKey, data: takenOfferInfo // appId: this.config.p2wdbAppId } const resultEventId = await this.adapters.nostr.post(JSON.stringify(nostrData)) const noteId = this.adapters.nostr.eventId2note(resultEventId) // Delete the Offer from the database, so that the user doesn't attempt // to take the offer more than once. // offerInfo.remove() // Return the P2WDB CID return { eventId: resultEventId, noteId } // return 'fake-hash' } catch (err) { console.error('Error in use-cases/offer/takeOffer(): ', err) throw err } } // Ensure that the wallet has enough BCH and tokens to complete the requested // trade. Will return true if it does. Will throw an error if it doesn't. async ensureFunds (offerEntity) { try { // console.log('this.adapters.wallet: ', this.adapters.wallet.bchWallet) // console.log(`walletInfo: ${JSON.stringify(this.adapters.wallet.bchWallet.walletInfo, null, 2)}`) await this.adapters.wallet.bchWallet.walletInfoPromise // console.log(`utxos: ${JSON.stringify(this.adapters.wallet.bchWallet.utxos.utxoStore, null, 2)}`) // Refresh the wallet UTXOs await this.adapters.wallet.bchWallet.initialize() // Ensure the app wallet has enough funds to write to the P2WDB. // Note : this validation should be deprecated for nostr functionality? // const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey // 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')) { // Sell Offer // Calculate the sats needed const satsNeeded = Math.ceil(offerEntity.numTokens * parseInt(offerEntity.rateInBaseUnit)) if (isNaN(satsNeeded)) { throw new Error('Could not calculate sats needed!') } // Ensure the app wallet controlls enough BCH to pay for the tokens. const balance = await this.adapters.wallet.bchWallet.getBalance() console.log(`wallet balance: ${balance}, sats needed: ${satsNeeded}`) const SATS_MARGIN = 5000 if (satsNeeded + SATS_MARGIN > balance) { throw new Error('App wallet does not control enough BCH to purchase the tokens.') } // } else { // Buy Offer throw new Error('Buy offers are not supported yet.') } return true } catch (err) { console.error('Error in offer/index.js/ensureFunds()') // Debugging try { console.error(`Error with this address: ${this.adapters.wallet.bchWallet.walletInfo.cashAddress}`) } catch (err) { /* exit quietly */ } throw err } } // Retrieve an Order model from the database. Find it by its event Id. async findOfferByEvent (nostrEventId) { try { 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 } } async findOfferByTxid (utxoTxid) { // try { // try { if (typeof utxoTxid !== 'string' || !utxoTxid) { throw new Error('utxoTxid must be a string') } const offer = await this.OfferModel.findOne({ utxoTxid }) // 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 // } } // This function is called by the P2WDB webhook REST API handler. When a // Counter Offer is passed to bch-dex by the P2WDB, the data is then passed // to this function. It does due diligence on the Counter Offer, then signs // and broadcasts the transaction to accept the Counter Offer. async acceptCounterOffer (offerData) { try { // console.log(`acceptCounterOffer() offerData: ${JSON.stringify(offerData, null, 2)}`) // Quickly skip over offers that have already been processed. const eventId = offerData.data.nostrEventId if (this.seenOffers.includes(eventId)) { // console.log(`Offer with event ID ${eventId} already processed. Skipping.`) return false } // See if this instance of bch-dex is managing the Order associated with // the incoming Counter Offer. // Note : this should be handled by nostrEvent id or UtxoId? // const orderHash = offerData.data.nostrEventId let orderData = {} try { orderData = await this.orderUseCase.findOrderByUtxo(offerData) console.log(`orderData: ${JSON.stringify(orderData, null, 2)}`) } catch (err) { console.log('Order matching this Counter Offer is not managed by this instance of bch-dex. Skipping.\n') // Add order to list of seen orders, so that we don't spent time trying to validate it again. this.seenOffers.push(eventId) return 'N/A' } // Verify that the UTXO for sale is valid. let utxoStatus = null try { // Get the status of the UTXO associate with this Offer. const utxo = { tx_hash: offerData.data.utxoTxid, tx_pos: offerData.data.utxoVout } // console.log(`Checking this UTXO: ${JSON.stringify(utxo, null, 2)}`) // utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo) utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo) console.log('utxoStatus: ', utxoStatus) } catch (err) { console.log('acceptCounterOffer() err validating UTXO: ', err) // Handle corner case of bad-data in the Offer model. if (err.isAxiosError) { console.log('Error trying to contact wallet service: ', err) return 'N/A' } else { return 'N/A' } } // If the Offer UTXO is spent, exit. if (utxoStatus === false) { // Add order to list of seen orders, so that we don't spent time trying to validate it again. this.seenOffers.push(eventId) console.log(`Aborting Counter Offer from Event ID ${eventId}`) console.log(`https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(eventId)}`) return 'N/A' } // Deserialize the partially signed transaction. const txHex = offerData.data.partialTxHex const txObj = await this.adapters.wallet.deseralizeTx(txHex) console.log(`txObj: ${JSON.stringify(txObj, null, 2)}`) // Ensure the 3rd output (vout=2) contains the required amount of BCH. const satsToReceive = Math.ceil(orderData.numTokens * parseInt(orderData.rateInBaseUnit)) console.log('Ceil', satsToReceive) if (isNaN(satsToReceive)) { throw new Error('Could not calculate the amount of BCH offered in the Counter Offer') } const satsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[2].value) const hasRequiredAmount = satsOut === satsToReceive if (!hasRequiredAmount) { throw new Error(`The Counter Offer has an output of ${satsOut}, which does not match the required ${satsToReceive} in the Offer.`) } // Ensure the 3rd output (vout=2) is going to the maker address specified // in the Offer. const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0] const makerAddr = orderData.makerAddr const hasCorrectAddr = makerAddr === addrInCounterOffer if (!hasCorrectAddr) { throw new Error(`The Counter Offer has an output address of ${addrInCounterOffer}, which does not match the Maker address of ${makerAddr} in the Offer.`) } // Get the User ID from the Order model. const userId = orderData.userId // Get the User model from the database. this.UserModel = this.adapters.localdb.Users const user = await this.UserModel.findById(userId) console.log('user: ', user) // Get the HD index from the Order model. const hdIndex = orderData.hdIndex // Sign and broadcast the transaction. const txid = await this.adapters.wallet.completeTx(txHex, hdIndex, user.mnemonic) console.log('txid: ', txid) return txid } catch (err) { console.error('Error in acceptCounterOffer(): ', err) throw err } } // TODO: Write unit tests for this function, then add it to the Timer Controllers // Looks for duplicate offers and removes the duplicate. // Duplicates are checked for when an offer is created, and so should not // exist. But emperical testing shows that they do. This function is called // periodically by a timer, to clean up any duplicates that slipped through // the cracks. async removeDuplicateOffers () { try { const now = new Date() console.log(`Starting removeDuplicateOffers() at ${now.toLocaleString()}`) let duplicateFound = false // Get all Offers in the database. const offers = await this.OfferModel.find({}) // console.log('offers: ', offers) const offerZcids = [] for (let i = 0; i < offers.length; i++) { const thisOffer = offers[i] if (offerZcids.includes(thisOffer.p2wdbHash)) { await thisOffer.remove() duplicateFound = true continue } // Add the zcid to the array. offerZcids.push(thisOffer.p2wdbHash) } return duplicateFound } catch (err) { console.error('Error in removeDuplicateOffers()') throw err } } // This function is called by the garbage collection timer controller. It // checks the UTXO associated with each Offer in the database. If the UTXO // has been spent, the Offer is deleted from the database. async removeStaleOffers () { try { const now = new Date() console.log(`Starting garbage collection for Offers at ${now.toLocaleString()}`) // Get all Offers in the database. const offers = await this.OfferModel.find({}) // console.log('offers: ', offers) // Loop through each Offer and ensure the UTXO is still valid. for (let i = 0; i < offers.length; i++) { const thisOffer = offers[i] let utxoStatus = null try { // Get the status of the UTXO associate with this Offer. const utxo = { tx_hash: thisOffer.utxoTxid, tx_pos: thisOffer.utxoVout } // console.log(`Checking this UTXO: ${JSON.stringify(utxo, null, 2)}`) // utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo) utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo) // console.log('utxoStatus: ', utxoStatus) } catch (err) { // Handle corner case of bad-data in the Offer model. if (err.message && err.message.includes('txid needs to be a proper transaction ID')) { console.log(`Deleting Offer with bad data: ${JSON.stringify(thisOffer, null, 2)}`) await thisOffer.remove() continue } else if (err.isAxiosError) { console.log('Error trying to contact wallet service: ', err) continue } else { throw err } } // If the Offer UTXO is spent, delete the Offer model. if (utxoStatus === false) { // console.log('utxoStatus: ', utxoStatus) console.log(`Spent UTXO detected. Deleting this Offer: ${JSON.stringify(thisOffer, null, 2)}`) await thisOffer.remove() } // If the Offer is older than 7 days, delete it. const nowMs = now.getTime() const eightWeeks = 1000 * 60 * 60 * 24 * 7 * 8 const eightWeeksAgo = nowMs - eightWeeks if (thisOffer.timestamp < eightWeeksAgo) { console.log('Offer older than 8 weeks. Deleting.') await thisOffer.remove() } } } catch (err) { console.error('Error in removeStaleOffers(): ', err) throw err } } // Flag offers as NSFW. async flagOffer (flagData = {}) { try { if (!flagData.data) throw new Error('"data" property is required') console.log(`flagData: ${JSON.stringify(flagData, null, 2)}`) const eventId = flagData.data.nostrEventId // Get the offer from 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 } } // Get offers data from nostr. async loadOffers () { try { // Retrieve offers array. const offers = await this.adapters.nostr.read() // console.log('offers: ', offers) for (let i = 0; i < offers.length; i++) { try { const offer = offers[i] // offer data const offerObj = JSON.parse(offer.content) // Append the Nostr Event ID to the offer object offerObj.data.nostrEventId = offer.eventId // console.log('loadOffers() offerObj: ', offerObj) if (offerObj.data.dataType === 'offer') { // Try to create new offer await this.createOffer(offerObj) } if (offerObj.data.dataType === 'counter-offer') { // console.log('Counter offer detected: ', offerObj) // console.log('Counter offer detected: ', offerObj.data.nostrEventId) console.log(`Counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerObj.data.nostrEventId)}`) await this.acceptCounterOffer(offerObj) } // Ignore the post if it doesn't fit the above filters. } catch (error) { /* exit quietly */ } } } catch (error) { console.error('Error in loadOffers(): ', error) throw error } } // List all offers being made by a given address. async listOffersByAddress (addr) { try { const offers = await this.OfferModel.find({ makerAddr: addr }) return offers } catch (err) { console.error('Error in listOffersByAddress(): ', err) throw err } } } export default OfferUseCases