mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex.git
synced 2026-09-21 16:52:00 -07:00
Merge pull request #105 from Permissionless-Software-Foundation/operator-fee
Adding required Operator fee to SWaP TX
This commit is contained in:
Vendored
+5
-1
@@ -191,5 +191,9 @@ export default {
|
|||||||
disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false,
|
disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false,
|
||||||
|
|
||||||
// Admin password
|
// Admin password
|
||||||
adminPassword: process.env.ADMIN_PASSWORD
|
adminPassword: process.env.ADMIN_PASSWORD,
|
||||||
|
|
||||||
|
// The Operator of BCH DEX can elect to receive a percentage of each sale.
|
||||||
|
operatorAddress: process.env.OPERATOR_ADDRESS ? process.env.OPERATOR_ADDRESS : 'bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr',
|
||||||
|
operatorPercentage: process.env.OPERATOR_PERCENTAGE ? parseFloat(process.env.OPERATOR_PERCENTAGE) : 2.0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,11 @@ const Offer = new mongoose.Schema({
|
|||||||
lokadId: { type: String },
|
lokadId: { type: String },
|
||||||
messageType: { type: Number },
|
messageType: { type: Number },
|
||||||
messageClass: { type: Number },
|
messageClass: { type: Number },
|
||||||
nostrEventId: { type: String } // Nostr Event Id.
|
nostrEventId: { type: String }, // Nostr Event Id.
|
||||||
|
|
||||||
|
// Operator fee and address
|
||||||
|
operatorAddress: { type: String },
|
||||||
|
operatorPercentage: { type: Number }
|
||||||
})
|
})
|
||||||
|
|
||||||
export default mongoose.model('offer', Offer)
|
export default mongoose.model('offer', Offer)
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ const Order = new mongoose.Schema({
|
|||||||
// Additional properties found in createOrder
|
// Additional properties found in createOrder
|
||||||
dataType: { type: String, required: true },
|
dataType: { type: String, required: true },
|
||||||
|
|
||||||
userId: { type: String, required: true }
|
userId: { type: String, required: true },
|
||||||
|
|
||||||
|
// Operator fee and address
|
||||||
|
operatorAddress: { type: String },
|
||||||
|
operatorPercentage: { type: Number }
|
||||||
})
|
})
|
||||||
|
|
||||||
export default mongoose.model('order', Order)
|
export default mongoose.model('order', Order)
|
||||||
|
|||||||
+12
-2
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Offer Entity
|
Offer Entity
|
||||||
An ffer is created when a new Signal is detected via the P2WDB webhook.
|
An offer is created when a new Signal is detected via the P2WDB webhook.
|
||||||
It's destroyed when the UTXO described in the Signal has been detected as spent.
|
It's destroyed when the UTXO described in the Signal has been detected as spent.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -33,7 +33,9 @@ class OfferEntity {
|
|||||||
makerAddr,
|
makerAddr,
|
||||||
ticker,
|
ticker,
|
||||||
tokenType,
|
tokenType,
|
||||||
nostrEventId
|
nostrEventId,
|
||||||
|
operatorAddress,
|
||||||
|
operatorPercentage
|
||||||
} = offerData.data
|
} = offerData.data
|
||||||
|
|
||||||
// Input Validation
|
// Input Validation
|
||||||
@@ -80,6 +82,14 @@ class OfferEntity {
|
|||||||
throw new Error("Property 'nostrEventId' must be a string.")
|
throw new Error("Property 'nostrEventId' must be a string.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the operator address and percentage to the offer.
|
||||||
|
if (!operatorAddress || typeof operatorAddress !== 'string') {
|
||||||
|
throw new Error("Property 'operatorAddress' must be a string.")
|
||||||
|
}
|
||||||
|
if (!operatorPercentage || typeof operatorPercentage !== 'number') {
|
||||||
|
throw new Error("Property 'operatorPercentage' must be a number.")
|
||||||
|
}
|
||||||
|
|
||||||
// Convert the timestamp to a number.
|
// Convert the timestamp to a number.
|
||||||
let timestamp = new Date(offerData.timestamp)
|
let timestamp = new Date(offerData.timestamp)
|
||||||
timestamp = timestamp.getTime()
|
timestamp = timestamp.getTime()
|
||||||
|
|||||||
@@ -111,7 +111,10 @@ class OfferUseCases {
|
|||||||
const utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
|
const utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
|
||||||
// console.log('utxoStatus: ', utxoStatus)
|
// console.log('utxoStatus: ', utxoStatus)
|
||||||
// if (utxoStatus === null) return false
|
// if (utxoStatus === null) return false
|
||||||
if (!utxoStatus) 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'
|
// A new offer gets a status of 'posted'
|
||||||
offerObj.data.offerStatus = 'posted'
|
offerObj.data.offerStatus = 'posted'
|
||||||
@@ -513,9 +516,8 @@ class OfferUseCases {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is called by the P2WDB webhook REST API handler. When a
|
// This function is called by loadOffers().
|
||||||
// Counter Offer is passed to bch-dex by the P2WDB, the data is then passed
|
// It does due diligence on the Counter Offer, then signs
|
||||||
// to this function. It does due diligence 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 (offerData) {
|
async acceptCounterOffer (offerData) {
|
||||||
try {
|
try {
|
||||||
@@ -528,6 +530,8 @@ class OfferUseCases {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`New counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerData.data.nostrEventId)}`)
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
||||||
@@ -599,6 +603,16 @@ class OfferUseCases {
|
|||||||
throw new Error(`The Counter Offer has an output of ${satsOut}, which does not match the required ${satsToReceive} in the Offer.`)
|
throw new Error(`The Counter Offer has an output of ${satsOut}, which does not match the required ${satsToReceive} in the Offer.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the Counter Offer has an output for the Operator of bch-dex.
|
||||||
|
if (!txObj.vout[3]) {
|
||||||
|
console.log('The Counter Offer does not have an output for the Operator.')
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure the 3rd output (vout=2) is going to the maker address specified
|
// Ensure the 3rd output (vout=2) is going to the maker address specified
|
||||||
// in the Offer.
|
// in the Offer.
|
||||||
const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0]
|
const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0]
|
||||||
@@ -608,6 +622,31 @@ class OfferUseCases {
|
|||||||
throw new Error(`The Counter Offer has an output address of ${addrInCounterOffer}, which does not match the Maker address of ${makerAddr} in the Offer.`)
|
throw new Error(`The Counter Offer has an output address of ${addrInCounterOffer}, which does not match the Maker address of ${makerAddr} in the Offer.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the 4th output (vout=3) is going to the operator address specified in the Offer.
|
||||||
|
const operatorAddr = orderData.operatorAddress
|
||||||
|
const hasCorrectOperatorAddr = operatorAddr === txObj.vout[3].scriptPubKey.addresses[0]
|
||||||
|
if (!hasCorrectOperatorAddr) {
|
||||||
|
throw new Error(`The Counter Offer has an output address of ${txObj.vout[3].scriptPubKey.addresses[0]}, which does not match the Operator address of ${operatorAddr} in the Offer.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the 4th output (vout=3) contains the required amount of BCH.
|
||||||
|
const operatorSatsToReceive = Math.ceil(orderData.numTokens * parseInt(orderData.rateInBaseUnit))
|
||||||
|
if (isNaN(operatorSatsToReceive)) {
|
||||||
|
throw new Error('Could not calculate the amount of BCH offered in the Counter Offer')
|
||||||
|
}
|
||||||
|
const operatorSatsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[3].value)
|
||||||
|
let estimatedOperatorFee = Math.floor(txObj.vout[3].value * orderData.operatorPercentage / 100)
|
||||||
|
if (estimatedOperatorFee < 546) estimatedOperatorFee = 546
|
||||||
|
console.log('operatorSatsOut: ', operatorSatsOut, 'estimatedOperatorFee: ', estimatedOperatorFee)
|
||||||
|
if (operatorSatsOut < estimatedOperatorFee) {
|
||||||
|
console.log(`Skipping: The Counter Offer has an output of ${operatorSatsOut}, which is less than the estimated operator fee of ${estimatedOperatorFee}.`)
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
}
|
||||||
|
|
||||||
// Get the User ID from the Order model.
|
// Get the User ID from the Order model.
|
||||||
const userId = orderData.userId
|
const userId = orderData.userId
|
||||||
|
|
||||||
@@ -718,7 +757,8 @@ class OfferUseCases {
|
|||||||
await thisOffer.remove()
|
await thisOffer.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the Offer is older than 7 days, delete it.
|
// TODO: Instead of deleting the offer, send the token back to the Makers wallet.
|
||||||
|
// If the Offer is older than a threshold, delete it.
|
||||||
const nowMs = now.getTime()
|
const nowMs = now.getTime()
|
||||||
const eightWeeks = 1000 * 60 * 60 * 24 * 7 * 8
|
const eightWeeks = 1000 * 60 * 60 * 24 * 7 * 8
|
||||||
const eightWeeksAgo = nowMs - eightWeeks
|
const eightWeeksAgo = nowMs - eightWeeks
|
||||||
@@ -795,7 +835,7 @@ class OfferUseCases {
|
|||||||
if (offerObj.data.dataType === 'counter-offer') {
|
if (offerObj.data.dataType === 'counter-offer') {
|
||||||
// console.log('Counter offer detected: ', offerObj)
|
// console.log('Counter offer detected: ', offerObj)
|
||||||
// console.log('Counter offer detected: ', offerObj.data.nostrEventId)
|
// console.log('Counter offer detected: ', offerObj.data.nostrEventId)
|
||||||
console.log(`Counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerObj.data.nostrEventId)}`)
|
// console.log(`Counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerObj.data.nostrEventId)}`)
|
||||||
await this.acceptCounterOffer(offerObj)
|
await this.acceptCounterOffer(offerObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class OrderLib {
|
|||||||
|
|
||||||
if (!entryObj.tokenId) throw new Error('entry does not contain required properties')
|
if (!entryObj.tokenId) throw new Error('entry does not contain required properties')
|
||||||
|
|
||||||
|
// Get the user data from the database, for the user creating the new Order.
|
||||||
const user = await this.UserModel.findById(entryObj.userId)
|
const user = await this.UserModel.findById(entryObj.userId)
|
||||||
if (!user) throw new Error('user not found')
|
if (!user) throw new Error('user not found')
|
||||||
|
|
||||||
@@ -81,13 +82,7 @@ class OrderLib {
|
|||||||
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {})
|
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {})
|
||||||
await userWallet.optimize()
|
await userWallet.optimize()
|
||||||
|
|
||||||
// Ensure sufficient tokens exist to create the order.
|
|
||||||
// await 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.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTxData, [entryObj.tokenId])
|
|
||||||
const tokenData = await userWallet.getTxData([entryObj.tokenId])
|
const tokenData = await userWallet.getTxData([entryObj.tokenId])
|
||||||
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
|
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
|
||||||
orderEntity.ticker = tokenData[0].tokenTicker
|
orderEntity.ticker = tokenData[0].tokenTicker
|
||||||
@@ -107,15 +102,20 @@ class OrderLib {
|
|||||||
// await this.adapters.wallet.bchWallet.getUtxos()
|
// await this.adapters.wallet.bchWallet.getUtxos()
|
||||||
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
|
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
|
||||||
await userWallet.initialize()
|
await userWallet.initialize()
|
||||||
|
|
||||||
// Update the order with the new UTXO information.
|
// Update the order with the new UTXO information.
|
||||||
orderEntity.utxoTxid = utxoInfo.txid
|
orderEntity.utxoTxid = utxoInfo.txid
|
||||||
orderEntity.utxoVout = utxoInfo.vout
|
orderEntity.utxoVout = utxoInfo.vout
|
||||||
orderEntity.tokenType = utxoInfo.tokenType
|
orderEntity.tokenType = utxoInfo.tokenType
|
||||||
|
|
||||||
// Add P2WDB specific flag for signaling that this is a new offer.
|
// Add flag to signal that this is a new offer.
|
||||||
orderEntity.dataType = 'offer'
|
orderEntity.dataType = 'offer'
|
||||||
orderEntity.userId = user._id
|
orderEntity.userId = user._id
|
||||||
|
|
||||||
|
// Add the operator address and percentage to the order.
|
||||||
|
orderEntity.operatorAddress = this.config.operatorAddress
|
||||||
|
orderEntity.operatorPercentage = this.config.operatorPercentage
|
||||||
|
|
||||||
// Post the new Order information to Nostr under the topic set in the
|
// Post the new Order information to Nostr under the topic set in the
|
||||||
// config file.
|
// config file.
|
||||||
const postObj = {
|
const postObj = {
|
||||||
|
|||||||
@@ -286,7 +286,9 @@ describe('#Offer-Entity', () => {
|
|||||||
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||||
ticker: 'TROUT',
|
ticker: 'TROUT',
|
||||||
tokenType: 1,
|
tokenType: 1,
|
||||||
nostrEventId: 'test'
|
nostrEventId: 'test',
|
||||||
|
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||||
|
operatorPercentage: 10
|
||||||
},
|
},
|
||||||
timestamp: '2021-09-20T17:54:26.395Z',
|
timestamp: '2021-09-20T17:54:26.395Z',
|
||||||
localTimeStamp: '9/20/2021, 10:54:26 AM',
|
localTimeStamp: '9/20/2021, 10:54:26 AM',
|
||||||
|
|||||||
@@ -142,10 +142,37 @@ const offerMockData = {
|
|||||||
utxoVout: 0,
|
utxoVout: 0,
|
||||||
makerAddr: 'address',
|
makerAddr: 'address',
|
||||||
tokenType: 1,
|
tokenType: 1,
|
||||||
nostrEventId: 'test'
|
nostrEventId: 'test',
|
||||||
|
operatorAddress: 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478',
|
||||||
|
operatorPercentage: 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deserealizeTxMockNoOperatorOut = {
|
||||||
|
//...
|
||||||
|
vout: [
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
scriptPubKey: {
|
||||||
|
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
scriptPubKey: {
|
||||||
|
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
scriptPubKey: {
|
||||||
|
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const deserealizeTxMock = {
|
const deserealizeTxMock = {
|
||||||
//...
|
//...
|
||||||
vout: [
|
vout: [
|
||||||
@@ -167,6 +194,12 @@ const deserealizeTxMock = {
|
|||||||
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
scriptPubKey: {
|
||||||
|
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
||||||
|
}
|
||||||
|
},
|
||||||
//...
|
//...
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -179,5 +212,6 @@ export default {
|
|||||||
fungibleOffer01,
|
fungibleOffer01,
|
||||||
fungibleTokenData01,
|
fungibleTokenData01,
|
||||||
offerMockData,
|
offerMockData,
|
||||||
|
deserealizeTxMockNoOperatorOut,
|
||||||
deserealizeTxMock
|
deserealizeTxMock
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -746,6 +746,24 @@ describe('#offer-use-case', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should skip transactions that do not have an output for the operator', async () => {
|
||||||
|
// 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.orderUseCase, 'findOrderByUtxo').resolves(mock)
|
||||||
|
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
|
||||||
|
|
||||||
|
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMockNoOperatorOut)
|
||||||
|
|
||||||
|
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
|
||||||
|
assert.equal(result, 'N/A')
|
||||||
|
})
|
||||||
|
|
||||||
it('should handle error for wrong transaction output address', async () => {
|
it('should handle error for wrong transaction output address', async () => {
|
||||||
try {
|
try {
|
||||||
// Mock data
|
// Mock data
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ describe('#order-use-case', () => {
|
|||||||
assert.property(result, 'eventId')
|
assert.property(result, 'eventId')
|
||||||
assert.property(result, 'noteId')
|
assert.property(result, 'noteId')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should create an order with consumer-api', async () => {
|
it('should create an order with consumer-api', async () => {
|
||||||
uut.config.useFullStackCash = false
|
uut.config.useFullStackCash = false
|
||||||
const entryObj = {
|
const entryObj = {
|
||||||
@@ -214,6 +215,7 @@ describe('#order-use-case', () => {
|
|||||||
assert.property(result, 'eventId')
|
assert.property(result, 'eventId')
|
||||||
assert.property(result, 'noteId')
|
assert.property(result, 'noteId')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error if user is not found', async () => {
|
it('should throw error if user is not found', async () => {
|
||||||
try {
|
try {
|
||||||
const entryObj = {
|
const entryObj = {
|
||||||
|
|||||||
Reference in New Issue
Block a user