mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex.git
synced 2026-09-22 01:02:00 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2af50ecf02 | ||
|
|
f88a24c1b5 | ||
|
|
a4b4ff13d2 | ||
|
|
2bf4f9df28 | ||
|
|
19ba38e8ce | ||
|
|
373a52ebf1 | ||
|
|
fe82478dc4 | ||
|
|
bed281c8c3 | ||
|
|
01484b4e03 |
Vendored
+5
-1
@@ -191,5 +191,9 @@ export default {
|
||||
disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false,
|
||||
|
||||
// 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 },
|
||||
messageType: { 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)
|
||||
|
||||
@@ -43,7 +43,11 @@ const Order = new mongoose.Schema({
|
||||
// Additional properties found in createOrder
|
||||
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)
|
||||
|
||||
+12
-2
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -33,7 +33,9 @@ class OfferEntity {
|
||||
makerAddr,
|
||||
ticker,
|
||||
tokenType,
|
||||
nostrEventId
|
||||
nostrEventId,
|
||||
operatorAddress,
|
||||
operatorPercentage
|
||||
} = offerData.data
|
||||
|
||||
// Input Validation
|
||||
@@ -80,6 +82,14 @@ class OfferEntity {
|
||||
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.
|
||||
let timestamp = new Date(offerData.timestamp)
|
||||
timestamp = timestamp.getTime()
|
||||
|
||||
@@ -111,7 +111,10 @@ class OfferUseCases {
|
||||
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
|
||||
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'
|
||||
@@ -513,9 +516,8 @@ class OfferUseCases {
|
||||
// }
|
||||
}
|
||||
|
||||
// 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
|
||||
// This function is called by loadOffers().
|
||||
// It does due diligence on the Counter Offer, then signs
|
||||
// and broadcasts the transaction to accept the Counter Offer.
|
||||
async acceptCounterOffer (offerData) {
|
||||
try {
|
||||
@@ -528,6 +530,8 @@ class OfferUseCases {
|
||||
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
|
||||
// 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.`)
|
||||
}
|
||||
|
||||
// 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
|
||||
// in the Offer.
|
||||
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.`)
|
||||
}
|
||||
|
||||
// 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.
|
||||
const userId = orderData.userId
|
||||
|
||||
@@ -718,7 +757,8 @@ class OfferUseCases {
|
||||
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 eightWeeks = 1000 * 60 * 60 * 24 * 7 * 8
|
||||
const eightWeeksAgo = nowMs - eightWeeks
|
||||
@@ -795,7 +835,7 @@ class OfferUseCases {
|
||||
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)}`)
|
||||
// console.log(`Counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerObj.data.nostrEventId)}`)
|
||||
await this.acceptCounterOffer(offerObj)
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ class OrderLib {
|
||||
|
||||
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)
|
||||
if (!user) throw new Error('user not found')
|
||||
|
||||
@@ -81,13 +82,7 @@ class OrderLib {
|
||||
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.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.
|
||||
// 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])
|
||||
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
|
||||
orderEntity.ticker = tokenData[0].tokenTicker
|
||||
@@ -107,15 +102,20 @@ class OrderLib {
|
||||
// await this.adapters.wallet.bchWallet.getUtxos()
|
||||
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
|
||||
await userWallet.initialize()
|
||||
|
||||
// Update the order with the new UTXO information.
|
||||
orderEntity.utxoTxid = utxoInfo.txid
|
||||
orderEntity.utxoVout = utxoInfo.vout
|
||||
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.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
|
||||
// config file.
|
||||
const postObj = {
|
||||
|
||||
@@ -286,7 +286,9 @@ describe('#Offer-Entity', () => {
|
||||
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
ticker: 'TROUT',
|
||||
tokenType: 1,
|
||||
nostrEventId: 'test'
|
||||
nostrEventId: 'test',
|
||||
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
operatorPercentage: 10
|
||||
},
|
||||
timestamp: '2021-09-20T17:54:26.395Z',
|
||||
localTimeStamp: '9/20/2021, 10:54:26 AM',
|
||||
|
||||
@@ -142,10 +142,37 @@ const offerMockData = {
|
||||
utxoVout: 0,
|
||||
makerAddr: 'address',
|
||||
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 = {
|
||||
//...
|
||||
vout: [
|
||||
@@ -167,6 +194,12 @@ const deserealizeTxMock = {
|
||||
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
||||
}
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
scriptPubKey: {
|
||||
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
||||
}
|
||||
},
|
||||
//...
|
||||
]
|
||||
}
|
||||
@@ -179,5 +212,6 @@ export default {
|
||||
fungibleOffer01,
|
||||
fungibleTokenData01,
|
||||
offerMockData,
|
||||
deserealizeTxMockNoOperatorOut,
|
||||
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 () => {
|
||||
try {
|
||||
// Mock data
|
||||
|
||||
@@ -185,6 +185,7 @@ describe('#order-use-case', () => {
|
||||
assert.property(result, 'eventId')
|
||||
assert.property(result, 'noteId')
|
||||
})
|
||||
|
||||
it('should create an order with consumer-api', async () => {
|
||||
uut.config.useFullStackCash = false
|
||||
const entryObj = {
|
||||
@@ -214,6 +215,7 @@ describe('#order-use-case', () => {
|
||||
assert.property(result, 'eventId')
|
||||
assert.property(result, 'noteId')
|
||||
})
|
||||
|
||||
it('should throw error if user is not found', async () => {
|
||||
try {
|
||||
const entryObj = {
|
||||
|
||||
Reference in New Issue
Block a user