mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex.git
synced 2026-09-21 16:52:00 -07:00
633 lines
22 KiB
JavaScript
633 lines
22 KiB
JavaScript
/*
|
|
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 Offer 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.detectNsfw = this.detectNsfw.bind(this)
|
|
}
|
|
|
|
// This method is called by the POST /offer REST API controller, which is
|
|
// triggered by a P2WDB webhook.
|
|
async createOffer (offerObj) {
|
|
try {
|
|
console.log('Use Case createOffer(offerObj): ', offerObj)
|
|
|
|
// Return if Offer already exists in database with the same P2WDB CID.
|
|
try {
|
|
await this.findOfferByHash(offerObj.hash)
|
|
|
|
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()')
|
|
}
|
|
|
|
// 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) return false
|
|
|
|
// A new offer gets a status of 'posted'
|
|
offerObj.data.offerStatus = 'posted'
|
|
|
|
const offerEntity = this.offerEntity.validate(offerObj)
|
|
console.log('offerEntity: ', offerEntity)
|
|
|
|
// 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.
|
|
let nsfw = false
|
|
// nsfw = await this.detectNsfw(tokenData)
|
|
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()')
|
|
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`
|
|
const url = `${this.config.ipfsGateway}${cid}/data.json`
|
|
const result = await axios.get(url)
|
|
const mutableData = result.data
|
|
console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`)
|
|
|
|
// Logical tests
|
|
const hasNsfw = !!mutableData.nsfw
|
|
const nsfwSetTrue = mutableData.nsfw === true
|
|
const nsfwStringTrue = mutableData.nsfw === 'true'
|
|
const nsfwDetected = hasNsfw && (nsfwSetTrue || nsfwStringTrue)
|
|
|
|
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 (offerCid) {
|
|
try {
|
|
console.log('offerCid: ', offerCid)
|
|
|
|
// Get the Offer information
|
|
const offerInfo = await this.findOfferByHash(offerCid)
|
|
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
|
|
}
|
|
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))
|
|
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.p2wdbHash
|
|
delete takenOfferInfo._id
|
|
takenOfferInfo.offerHash = offerInfo.p2wdbHash
|
|
|
|
// 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 p2wdbObj = {
|
|
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
|
|
data: takenOfferInfo,
|
|
appId: this.config.p2wdbAppId
|
|
}
|
|
const hash = await this.adapters.p2wdb.write(p2wdbObj)
|
|
|
|
// 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 hash
|
|
|
|
// 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.
|
|
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
|
|
}
|
|
}
|
|
|
|
async findOfferByHash (p2wdbHash) {
|
|
// try {
|
|
if (typeof p2wdbHash !== 'string' || !p2wdbHash) {
|
|
throw new Error('p2wdbHash must be a string')
|
|
}
|
|
|
|
const offer = await this.OfferModel.findOne({ p2wdbHash })
|
|
|
|
if (!offer) {
|
|
throw new Error('offer not found')
|
|
}
|
|
|
|
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
|
|
// 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
|
|
// and broadcasts the transaction to accept the Counter Offer.
|
|
async acceptCounterOffer (p2wdbData) {
|
|
try {
|
|
console.log(`acceptCounterOffer() p2wdbData: ${JSON.stringify(p2wdbData, null, 2)}`)
|
|
|
|
// See if this instance of bch-dex is managing the Order associated with
|
|
// the incoming Counter Offer.
|
|
const orderHash = p2wdbData.data.offerHash
|
|
let orderData = {}
|
|
try {
|
|
orderData = await this.orderUseCase.findOrderByEvent(orderHash)
|
|
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. Exiting.')
|
|
return 'N/A'
|
|
}
|
|
|
|
// Deserialize the partially signed transaction.
|
|
const txHex = p2wdbData.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))
|
|
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.`)
|
|
}
|
|
|
|
// Sign and broadcast the transaction.
|
|
const txid = await this.adapters.wallet.completeTx(txHex, orderData.hdIndex)
|
|
console.log('txid: ', txid)
|
|
|
|
return txid
|
|
} catch (err) {
|
|
console.error('Error in acceptCounterOffer()')
|
|
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
|
|
}
|
|
}
|
|
|
|
async flagOffer (flagData) {
|
|
console.log(`flagData: ${JSON.stringify(flagData, null, 2)}`)
|
|
|
|
const p2wdbHash = flagData.data.p2wdbHash
|
|
|
|
// Get the offer from the database.
|
|
const offer = await this.findOfferByHash(p2wdbHash)
|
|
console.log(`Flagging this offer: ${JSON.stringify(offer, null, 2)}`)
|
|
|
|
if (!offer) {
|
|
throw new Error(`Offer ${p2wdbHash} 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
|
|
}
|
|
}
|
|
|
|
export default OfferUseCases
|