From a6b467757196fcc3628b5ee5c5e3b02da0ef20c7 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 10 Mar 2022 13:21:25 -0800 Subject: [PATCH] Working on take-offer logic --- src/adapters/wallet.js | 70 +++--------- src/controllers/rest-api/order/controller.js | 21 ++++ src/controllers/rest-api/order/index.js | 1 + src/use-cases/offer.js | 2 +- src/use-cases/order/index.js | 106 ++++++++++++++++++ .../{create-offer.js => 01-create-order.js} | 0 test/e2e/manual/02-take-order.js | 25 +++++ 7 files changed, 167 insertions(+), 58 deletions(-) rename test/e2e/manual/{create-offer.js => 01-create-order.js} (100%) create mode 100644 test/e2e/manual/02-take-order.js diff --git a/src/adapters/wallet.js b/src/adapters/wallet.js index 842bacc..ff226b5 100644 --- a/src/adapters/wallet.js +++ b/src/adapters/wallet.js @@ -178,63 +178,19 @@ class WalletAdapter { } } -// Burn enough PSF to generate a valide proof-of-burn for writing to the P2WDB. -// async burnPsf () { -// try { -// // TODO: Throw error if this.bchWallet has not been instantiated. -// -// // console.log('walletData: ', walletData) -// // console.log( -// // `walletData.utxos.utxoStore.slpUtxos: ${JSON.stringify( -// // walletData.utxos.utxoStore.slpUtxos, -// // null, -// // 2, -// // )}`, -// // ) -// -// // Get token UTXOs held by the wallet. -// const tokenUtxos = this.bchWallet.utxos.utxoStore.slpUtxos.type1.tokens -// // console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`) -// -// // Find a token UTXO that contains PSF with a quantity higher than needed -// // to generate a proof-of-burn. -// let tokenUtxo = {} -// for (let i = 0; i < tokenUtxos.length; i++) { -// const thisUtxo = tokenUtxos[i] -// -// // If token ID matches. -// if (thisUtxo.tokenId === P2WDB_TOKEN_ID) { -// if (parseFloat(thisUtxo.qtyStr) >= PROOF_OF_BURN_QTY) { -// tokenUtxo = thisUtxo -// break -// } -// } -// } -// -// if (tokenUtxo.tokenId !== P2WDB_TOKEN_ID) { -// throw new Error( -// `Token UTXO of with ID of ${P2WDB_TOKEN_ID} and quantity greater than ${PROOF_OF_BURN_QTY} could not be found in wallet.` -// ) -// } -// // console.log(`tokenUtxo: ${JSON.stringify(tokenUtxo, null, 2)}`) -// -// const result = await this.bchWallet.burnTokens( -// PROOF_OF_BURN_QTY, -// P2WDB_TOKEN_ID -// ) -// // console.log('walletData.burnTokens() result: ', result) -// -// return result -// -// // return { -// // success: true, -// // txid: 'fakeTxid', -// // } -// } catch (err) { -// console.error('Error in burnPsf(): ', err) -// throw err -// } -// } + // Generate a partial transcation to *take* a 'sell' order. + async generatePartialTx (orderInfo) { + // try { + // // const bchjs = this.bchWallet.bchjs + // + // return true + // } catch (err) { + // console.error('Error in wallet.js/generatePartialTx()') + // throw err + // } + + return true + } } module.exports = WalletAdapter diff --git a/src/controllers/rest-api/order/controller.js b/src/controllers/rest-api/order/controller.js index cd09aee..86f50d7 100644 --- a/src/controllers/rest-api/order/controller.js +++ b/src/controllers/rest-api/order/controller.js @@ -61,6 +61,27 @@ class OrderRESTControllerLib { } } + // Currently only supports 'sell' orders, and will only buy the 'numTokens' + // listed in the order. + async takeOrder (ctx) { + try { + console.log('body: ', ctx.request.body) + + const orderCid = ctx.request.body.orderCid + + // Find the Order. + // const orderEntity = await _this.useCases.order.findOrder(orderId) + + // 'Take' the Order. + const hash = await _this.useCases.order.takeOrder(orderCid) + + ctx.body = { hash } + } catch (err) { + console.log('Error in takeOrder REST API handler.') + _this.handleError(ctx, err) + } + } + // DRY error handler handleError (ctx, err) { console.log('err', err.message) diff --git a/src/controllers/rest-api/order/index.js b/src/controllers/rest-api/order/index.js index 8e6712a..8a80625 100644 --- a/src/controllers/rest-api/order/index.js +++ b/src/controllers/rest-api/order/index.js @@ -51,6 +51,7 @@ class OrderRouter { // Define the routes and attach the controller. this.router.post('/', _this.orderRESTController.createOrder) this.router.get('/list', _this.orderRESTController.listOrders) + this.router.post('/take', _this.orderRESTController.takeOrder) // Attach the Controller routes to the Koa app. app.use(_this.router.routes()) diff --git a/src/use-cases/offer.js b/src/use-cases/offer.js index 41419a6..21a75a9 100644 --- a/src/use-cases/offer.js +++ b/src/use-cases/offer.js @@ -90,7 +90,7 @@ class OfferLib { const utxoInfo = { txid, - vout: 0, + vout: 1, hdIndex: keyPair.hdIndex } diff --git a/src/use-cases/order/index.js b/src/use-cases/order/index.js index cc5331c..d8a1e40 100644 --- a/src/use-cases/order/index.js +++ b/src/use-cases/order/index.js @@ -68,6 +68,112 @@ class OrderUseCases { throw error } } + + // Generate phase 2 of 3 - take the other side of an Order. + // Based on this example: + // https://github.com/Permissionless-Software-Foundation/bch-js-examples/blob/master/bch/applications/collaborate/sell-slp/e2e-exchange/step2-purchase-tx.js + async takeOrder (orderCid) { + try { + console.log('orderCid: ', orderCid) + + // Get the Order information + const orderInfo = await this.findOrderByHash(orderCid) + console.log(`orderInfo: ${JSON.stringify(orderInfo, null, 2)}`) + + // Ensure the order is in a 'posted' state and not already 'taken' + if (orderInfo.orderStatus && orderInfo.orderStatus !== 'posted') { + throw new Error('order already taken') + } + + // Verify that UTXO for sale is unspent. Abort if it's been spent. + const txid = orderInfo.utxoTxid + const vout = orderInfo.utxoVout + const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut( + txid, + vout + ) + console.log('utxoStatus: ', utxoStatus) + if (utxoStatus === null) { + console.log(`utxo txid: ${txid}, vout: ${vout}`) + throw new Error('UTXO does not exist. Aborting.') + } + + // Ensure the app has enough funds to complete the trade. + await this.ensureFunds(orderInfo) + + // Get UTXOs. + const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore + console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) + + // 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.adapaters.wallet.generatePartialTx(orderInfo) + + // return partialTxHex + + return true + } catch (err) { + console.error('Error in use-cases/order/takeOrder()') + 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 (orderEntity) { + try { + // console.log('this.adapters.wallet: ', this.adapters.wallet.bchWallet) + // console.log(`walletInfo: ${JSON.stringify(this.adapters.wallet.bchWallet.walletInfo, null, 2)}`) + + // 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 (orderEntity.buyOrSell.includes('sell')) { + // Sell Offer + + // Ensure the app wallet controlls enough BCH to pay for the tokens. + const satsNeeded = orderEntity.numTokens * parseInt(orderEntity.rateInSats) + 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 orders are not supported yet.') + } + + return true + } catch (err) { + console.error('Error in ensureFunds()') + throw err + } + } + + async findOrderByHash (p2wdbHash) { + try { + if (typeof p2wdbHash !== 'string' || !p2wdbHash) { + throw new Error('p2wdbHash must be a string') + } + + const order = await this.OrderModel.findOne({ p2wdbHash }) + + if (!order) { + throw new Error('order not found') + } + + const orderObject = order.toObject() + // return this.orderEntity.validateFromModel(orderObject) + + return orderObject + } catch (err) { + console.error('Error in findOrder(): ', err) + throw err + } + } } module.exports = OrderUseCases diff --git a/test/e2e/manual/create-offer.js b/test/e2e/manual/01-create-order.js similarity index 100% rename from test/e2e/manual/create-offer.js rename to test/e2e/manual/01-create-order.js diff --git a/test/e2e/manual/02-take-order.js b/test/e2e/manual/02-take-order.js new file mode 100644 index 0000000..7b21377 --- /dev/null +++ b/test/e2e/manual/02-take-order.js @@ -0,0 +1,25 @@ +/* + Part 2 of 3: Take an order +*/ + +const axios = require('axios') + +const LOCALHOST = 'http://localhost:5700' + +async function start () { + try { + const options = { + method: 'post', + url: `${LOCALHOST}/order/take`, + data: { + orderCid: 'zdpuAkp98gTuivaNzGP31jTQi3ADXrFA6uANrceQcrQkTXy2j' + } + } + + const result = await axios(options) + console.log('result.data: ', result.data) + } catch (err) { + console.log(err) + } +} +start()