Compare commits

...
5 Commits
12 changed files with 190 additions and 18 deletions
+1
View File
@@ -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 },
+1
View File
@@ -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 },
+3 -8
View File
@@ -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.
+59
View File
@@ -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
+7 -2
View File
@@ -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
+7 -2
View File
@@ -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
+48 -1
View File
@@ -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
+52
View File
@@ -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
+3 -1
View File
@@ -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)
+5
View File
@@ -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) {
+2 -1
View File
@@ -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',
+2 -3
View File
@@ -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)