diff --git a/src/adapters/localdb/models/offer.js b/src/adapters/localdb/models/offer.js index 51a4855..b5eed7b 100644 --- a/src/adapters/localdb/models/offer.js +++ b/src/adapters/localdb/models/offer.js @@ -6,6 +6,7 @@ const Offer = new mongoose.Schema({ tokenId: { type: String }, utxoTxid: { type: String }, utxoVout: { type: Number }, + ticker: { type: String }, // Trade data buyOrSell: { type: String }, diff --git a/src/adapters/localdb/models/order.js b/src/adapters/localdb/models/order.js index f89e604..e489f4d 100644 --- a/src/adapters/localdb/models/order.js +++ b/src/adapters/localdb/models/order.js @@ -13,6 +13,7 @@ const Order = new mongoose.Schema({ tokenId: { type: String }, utxoTxid: { type: String }, utxoVout: { type: Number }, + ticker: { type: String }, // Trade data buyOrSell: { type: String }, diff --git a/src/controllers/index.js b/src/controllers/index.js index 90bde54..d59d0de 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -6,18 +6,12 @@ // Public npm libraries. -// Load the Clean Architecture Adapters library +// Local libraries const Adapters = require('../adapters') - -// Load the JSON RPC Controller. const JSONRPC = require('./json-rpc') - -// Load the Clean Architecture Use Case libraries. const UseCases = require('../use-cases') -// const useCases = new UseCases({ adapters }) - -// Load the REST API Controllers. const RESTControllers = require('./rest-api') +const TimerControllers = require('./timer-controllers') class Controllers { constructor (localConfig = {}) { @@ -33,6 +27,7 @@ class Controllers { // this.attachRESTControllers(app) // this.attachRPCControllers() + this.timerControllers = new TimerControllers({ adapters: this.adapters, useCases: this.useCases }) } // Top-level function for this library. diff --git a/src/controllers/timer-controllers.js b/src/controllers/timer-controllers.js new file mode 100644 index 0000000..6a08067 --- /dev/null +++ b/src/controllers/timer-controllers.js @@ -0,0 +1,59 @@ +/* + This Controller library is concerned with timer-based functions that are + kicked off periodicially. +*/ + +// Used to retain scope of 'this', when the scope is lost. +let _this + +class TimerControllers { + constructor (localConfig = {}) { + // Dependency Injection. + this.adapters = localConfig.adapters + if (!this.adapters) { + throw new Error( + 'Instance of Adapters library required when instantiating Timer Controller libraries.' + ) + } + this.useCases = localConfig.useCases + if (!this.useCases) { + throw new Error( + 'Instance of Use Cases library required when instantiating Timer Controller libraries.' + ) + } + + this.debugLevel = localConfig.debugLevel + + _this = this + + this.startTimers() + } + + // Start all the time-based controllers. + startTimers () { + setInterval(this.gcOrders, 60000 * 5) + setInterval(this.gcOffers, 60000 * 5) + } + + // Garbage Collect the Orders. + gcOrders () { + try { + _this.useCases.order.removeStaleOrders() + } catch (err) { + // Do not throw an error. This is a top-level function. + console.log('Error in timer-controllers.js/gcOrders(): ', err) + } + } + + // Garbage Collect the Offers. + gcOffers () { + try { + _this.useCases.offer.removeStaleOffers() + } catch (err) { + // Do not throw an error. This is a top-level function. + console.log('Error in timer-controllers.js/gcOffers(): ', err) + } + } +} + +module.exports = TimerControllers diff --git a/src/entities/offer.js b/src/entities/offer.js index c82f079..2fc502c 100644 --- a/src/entities/offer.js +++ b/src/entities/offer.js @@ -28,7 +28,8 @@ class OfferEntity { utxoTxid, utxoVout, offerStatus, - makerAddr + makerAddr, + ticker } = offerData.data // Input Validation @@ -65,6 +66,9 @@ class OfferEntity { if (!makerAddr || typeof makerAddr !== 'string') { throw new Error("Property 'makerAddr' must be a string.") } + if (!ticker || typeof ticker !== 'string') { + throw new Error("Property 'ticker' must be a string.") + } const validatedOfferData = { messageType, @@ -81,7 +85,8 @@ class OfferEntity { txid: offerData.txid, p2wdbHash: offerData.hash, offerStatus: offerStatus || this.offerStatus[0], - makerAddr + makerAddr, + ticker } return validatedOfferData diff --git a/src/entities/order.js b/src/entities/order.js index 4e2e789..5137114 100644 --- a/src/entities/order.js +++ b/src/entities/order.js @@ -16,7 +16,8 @@ class Order { rateInBaseUnit, minUnitsToExchange, numTokens, - makerAddr + makerAddr, + ticker } = data // Input Validation @@ -44,6 +45,9 @@ class Order { if (!makerAddr || typeof makerAddr !== 'string') { throw new Error("Property 'makerAddr' must be a string.") } + if (!ticker || typeof ticker !== 'string') { + throw new Error("Property 'ticker' must be a string.") + } const offerData = { messageType, @@ -53,7 +57,8 @@ class Order { rateInBaseUnit, minUnitsToExchange, numTokens, - makerAddr + makerAddr, + ticker } return offerData diff --git a/src/use-cases/offer/index.js b/src/use-cases/offer/index.js index bb4f72a..035a78f 100644 --- a/src/use-cases/offer/index.js +++ b/src/use-cases/offer/index.js @@ -29,7 +29,7 @@ class OfferUseCases { 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.' + 'Instance of Offer Use Cases must be passed in when instantiating Offer Use Cases library.' ) } @@ -299,6 +299,53 @@ class OfferUseCases { 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. + utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut( + thisOffer.utxoTxid, + thisOffer.utxoVout + ) + // console.log('utxoStatus: ', utxoStatus) + } catch (err) { + // Handle corner case of bad-data in the Offer model. + if (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 { + throw err + } + } + + // If the Offer UTXO is spent, delete the Offer model. + if (utxoStatus === null) { + console.log(`Spent UTXO detected. Deleting this Offer: ${JSON.stringify(thisOffer, null, 2)}`) + await thisOffer.remove() + } + } + } catch (err) { + console.error('Error in removeStaleOffers()') + throw err + } + } } module.exports = OfferUseCases diff --git a/src/use-cases/order.js b/src/use-cases/order.js index 1a3e346..29eef8e 100644 --- a/src/use-cases/order.js +++ b/src/use-cases/order.js @@ -33,6 +33,11 @@ class OrderLib { entryObj.makerAddr = this.adapters.wallet.bchWallet.walletInfo.cashAddress console.log('entryObj.makerAddr: ', entryObj.makerAddr) + // Get Ticker for token ID. + const tokenData = await this.adapters.wallet.bchWallet.getTxData([entryObj.tokenId]) + // console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`) + entryObj.ticker = tokenData[0].tokenTicker + // Input Validation const orderEntity = this.orderEntity.validate(entryObj) console.log('orderEntity: ', orderEntity) @@ -158,6 +163,53 @@ class OrderLib { throw err } } + + // This function is called by the garbage collection timer controller. It + // checks the UTXO associated with each Order in the database. If the UTXO + // has been spent, the Order is deleted from the database. + async removeStaleOrders () { + try { + const now = new Date() + console.log(`Starting garbage collection for Orders at ${now.toLocaleString()}`) + + // Get all Orders in the database. + const orders = await this.OrderModel.find({}) + // console.log('orders: ', orders) + + // Loop through each Order and ensure the UTXO is still valid. + for (let i = 0; i < orders.length; i++) { + const thisOrder = orders[i] + + let utxoStatus = null + try { + // Get the status of the UTXO associate with this Order. + utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut( + thisOrder.utxoTxid, + thisOrder.utxoVout + ) + // console.log('utxoStatus: ', utxoStatus) + } catch (err) { + // Handle corner case of bad-data in the Order model. + if (err.message.includes('txid needs to be a proper transaction ID')) { + console.log(`Deleting Order with bad data: ${JSON.stringify(thisOrder, null, 2)}`) + await thisOrder.remove() + continue + } else { + throw err + } + } + + // If the Order UTXO is spent, delete the Order model. + if (utxoStatus === null) { + console.log(`Spent UTXO detected. Deleting this Order: ${JSON.stringify(thisOrder, null, 2)}`) + await thisOrder.remove() + } + } + } catch (err) { + console.error('Error in removeStaleOrders()') + throw err + } + } } module.exports = OrderLib diff --git a/test/unit/entities/offer.entity.unit.js b/test/unit/entities/offer.entity.unit.js index 520605f..480d4eb 100644 --- a/test/unit/entities/offer.entity.unit.js +++ b/test/unit/entities/offer.entity.unit.js @@ -253,12 +253,14 @@ describe('#Offer-Entity', () => { '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87', utxoVout: 0, offerStatus: 'posted', - makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00' + makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', + ticker: 'TROUT' }, timestamp: '2021-09-20T17:54:26.395Z', localTimeStamp: '9/20/2021, 10:54:26 AM', txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7', hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD' + } const result = uut.validate(offerObj) diff --git a/test/unit/mocks/adapters/wallet.js b/test/unit/mocks/adapters/wallet.js index e7a70ae..b62b385 100644 --- a/test/unit/mocks/adapters/wallet.js +++ b/test/unit/mocks/adapters/wallet.js @@ -29,6 +29,11 @@ class MockBchWallet { } this.getUtxos = async () => { } + this.getTxData = async () => { + return [{ + tokenTicker: 'TROUT' + }] + } // Environment variable is used by wallet-balance.unit.js to force an error. if (process.env.NO_UTXO) { diff --git a/test/unit/use-cases/offer.use-case.unit.js b/test/unit/use-cases/offer.use-case.unit.js index 7c0b210..97036b0 100644 --- a/test/unit/use-cases/offer.use-case.unit.js +++ b/test/unit/use-cases/offer.use-case.unit.js @@ -95,7 +95,8 @@ describe('#offer-use-case', () => { utxoTxid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87', utxoVout: 0, - makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00' + makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', + ticker: 'TROUT' }, timestamp: '2021-09-20T17:54:26.395Z', localTimeStamp: '9/20/2021, 10:54:26 AM', diff --git a/test/unit/use-cases/order.use-case.unit.js b/test/unit/use-cases/order.use-case.unit.js index ef40da1..f710845 100644 --- a/test/unit/use-cases/order.use-case.unit.js +++ b/test/unit/use-cases/order.use-case.unit.js @@ -109,13 +109,12 @@ describe('#order-use-case', () => { // Mock dependencies // sandbox.stub(uut.adapters.wallet, 'burnPsf').resolves('fakeTxid') + // sandbox.stub(uut.adapters.wallet.bchWallet, 'getTxData').resolves({ tokenTicker: 'TROUT' }) sandbox.stub(uut.orderEntity, 'validate').returns(entryObj) sandbox.stub(uut, 'ensureFunds').resolves() + sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.Util, 'sleep').resolves() sandbox.stub(uut.adapters.wallet, 'moveTokens').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 }) sandbox.stub(uut.adapters.wallet.bchWallet, 'getUtxos').resolves() - // sandbox - // .stub(uut.adapters.wallet, 'generateSignature') - // .resolves('fakeSignature') sandbox.stub(uut.adapters.p2wdb, 'write').resolves('fakeHash') const result = await uut.createOrder(entryObj)