diff --git a/src/adapters/wallet.js b/src/adapters/wallet.js index da77130..12b76e9 100644 --- a/src/adapters/wallet.js +++ b/src/adapters/wallet.js @@ -54,6 +54,7 @@ class WalletAdapter { this.deseralizeTx = this.deseralizeTx.bind(this) this.completeTx = this.completeTx.bind(this) this.reclaimTokens = this.reclaimTokens.bind(this) + this.moveTokensFromCustomWallet = this.moveTokensFromCustomWallet.bind(this) } // Open the wallet file, or create one if the file doesn't exist. @@ -575,6 +576,48 @@ class WalletAdapter { throw err } } + + async moveTokensFromCustomWallet (inObj = {}) { + try { + const { tokenId, qty, wallet } = inObj + + const keyPair = await this.getKeyPair() + console.log('keyPair: ', keyPair) + + const receiver = { + address: keyPair.cashAddress, + tokenId, + qty + } + console.log('receiver: ', receiver) + + // Update the UTXO store of the wallet. + await wallet.initialize() + + // Get the token type of the token being moved. + // Combine Fungible and NFT token UTXOs. + let tokenUtxos = wallet.utxos.utxoStore.slpUtxos.type1.tokens.concat( + wallet.utxos.utxoStore.slpUtxos.nft.tokens) + // Get token UTXOs that match the token in the order. + tokenUtxos = tokenUtxos.filter( + x => x.tokenId === tokenId + ) + + const txid = await wallet.sendTokens(receiver, 3) + console.log('txid: ', txid) + const utxoInfo = { + txid, + vout: 1, + hdIndex: wallet.walletInfo.hdIndex, + tokenType: tokenUtxos[0].tokenType + } + + return utxoInfo + } catch (err) { + console.error('Error in wallet.js/moveTokens()') + throw err + } + } } export default WalletAdapter diff --git a/src/controllers/rest-api/order/controller.js b/src/controllers/rest-api/order/controller.js index 6c49209..8bb338e 100644 --- a/src/controllers/rest-api/order/controller.js +++ b/src/controllers/rest-api/order/controller.js @@ -35,6 +35,8 @@ class OrderRESTControllerLib { // console.log('body: ', ctx.request.body) const orderObj = ctx.request.body.order + const user = ctx.state.user + orderObj.userId = user._id console.log('orderObj: ', orderObj) const { eventId, noteId } = await _this.useCases.order.createOrder(orderObj) diff --git a/src/controllers/rest-api/order/index.js b/src/controllers/rest-api/order/index.js index ab3dc37..d00487c 100644 --- a/src/controllers/rest-api/order/index.js +++ b/src/controllers/rest-api/order/index.js @@ -7,6 +7,7 @@ import Router from 'koa-router' // Local libraries. import OrderRESTControllerLib from './controller.js' +import Validators from '../middleware/validators.js' let _this @@ -32,12 +33,14 @@ class OrderRouter { } // Encapsulate dependencies. + this.validators = new Validators() this.orderRESTController = new OrderRESTControllerLib(dependencies) // Instantiate the router and set the base route. const baseUrl = '/order' this.router = new Router({ prefix: baseUrl }) + this.createOrder = this.createOrder.bind(this) _this = this } @@ -49,7 +52,7 @@ class OrderRouter { } // Define the routes and attach the controller. - this.router.post('/', _this.orderRESTController.createOrder) + this.router.post('/', this.createOrder) this.router.get('/list/all/:page', _this.orderRESTController.listOrders) this.router.post('/delete', _this.orderRESTController.deleteOrder) @@ -57,6 +60,11 @@ class OrderRouter { app.use(_this.router.routes()) app.use(_this.router.allowedMethods()) } + + async createOrder (ctx, next) { + await _this.validators.ensureUser(ctx, next) + await _this.orderRESTController.createOrder(ctx, next) + } } export default OrderRouter diff --git a/src/use-cases/order.js b/src/use-cases/order.js index 68aa2e1..4606dc9 100644 --- a/src/use-cases/order.js +++ b/src/use-cases/order.js @@ -25,6 +25,7 @@ class OrderLib { // Encapsulate dependencies this.orderEntity = new OrderEntity() this.OrderModel = this.adapters.localdb.Order + this.UserModel = this.adapters.localdb.Users this.bch = this.adapters.bch this.config = config this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 }) @@ -43,8 +44,31 @@ class OrderLib { if (!entryObj.tokenId) throw new Error('entry does not contain required properties') + const user = await this.UserModel.findById(entryObj.userId) + if (!user) throw new Error('user not found') + + console.log(`Using FullStack.cash: ${this.config.useFullStackCash}`) + const advancedConfig = {} + if (this.config.useFullStackCash) { + advancedConfig.interface = 'rest-api' + advancedConfig.restURL = this.config.apiServer + advancedConfig.apiToken = this.config.apiToken + } else { + advancedConfig.interface = 'consumer-api' + advancedConfig.restURL = this.config.consumerUrl + } + + // Instantiate minimal-slp-wallet with the user's mnemonic. + const BchWallet = this.adapters.wallet.BchWallet + const userWallet = new BchWallet(user.mnemonic, advancedConfig) + + // Wait for wallet to initialize. + await userWallet.walletInfoPromise + await userWallet.initialize() + // Specify the address to send payment. - entryObj.makerAddr = this.adapters.wallet.bchWallet.walletInfo.cashAddress + entryObj.makerAddr = userWallet.walletInfo.cashAddress + console.log('entryObj.makerAddr: ', entryObj.makerAddr) // console.log('entryObj.makerAddr: ', entryObj.makerAddr) // Input Validation @@ -53,7 +77,8 @@ class OrderLib { // Optimize the wallet to speed up working with it. console.log('Optimizing wallet before creating new order.') - await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {}) + // await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {}) + await userWallet.optimize() // Ensure sufficient tokens exist to create the order. // await this.ensureFunds(orderEntity) @@ -61,24 +86,26 @@ class OrderLib { // 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 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 // Move the tokens to holding address. const moveObj = { tokenId: orderEntity.tokenId, - qty: orderEntity.numTokens + qty: orderEntity.numTokens, + wallet: userWallet } // const utxoInfo = await this.adapters.wallet.moveTokens(moveObj) - const utxoInfo = await this.retryQueue.addToQueue(this.adapters.wallet.moveTokens, moveObj) + const utxoInfo = await this.retryQueue.addToQueue(this.adapters.wallet.moveTokensFromCustomWallet, moveObj) // console.log('utxoInfo: ', utxoInfo) // Update the UTXO store for the wallet. - await this.adapters.wallet.bchWallet.bchjs.Util.sleep(3000) + await userWallet.bchjs.Util.sleep(3000) // 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() // Update the order with the new UTXO information. orderEntity.utxoTxid = utxoInfo.txid orderEntity.utxoVout = utxoInfo.vout @@ -159,7 +186,7 @@ class OrderLib { ) } - // + // } else { // Buy Order throw new Error('Buy orders are not supported yet.') @@ -286,11 +313,11 @@ class OrderLib { try { const data = await this.OrderModel.find({}) - // Sort entries so newest entries show first. + // Sort entries so newest entries show first. .sort('-timestamp') - // Skip to the start of the selected page. + // Skip to the start of the selected page. .skip(page * DEFAULT_ENTRIES_PER_PAGE) - // Only return 20 results. + // Only return 20 results. .limit(DEFAULT_ENTRIES_PER_PAGE) return data diff --git a/test/unit/controllers/rest-api/order/order.rest.controller.unit.js b/test/unit/controllers/rest-api/order/order.rest.controller.unit.js index 43e354b..5ec50ab 100644 --- a/test/unit/controllers/rest-api/order/order.rest.controller.unit.js +++ b/test/unit/controllers/rest-api/order/order.rest.controller.unit.js @@ -65,6 +65,9 @@ describe('#Order-REST-Router', () => { ctx.request.body = { order: {} } + ctx.state.user = { + id: 'testUserId' + } // Mock dependencies sandbox.stub(uut.useCases.order, 'createOrder').resolves({ eventId: 'testEventId', noteId: 'testNoteId' }) @@ -80,6 +83,9 @@ describe('#Order-REST-Router', () => { ctx.request.body = { order: {} } + ctx.state.user = { + id: 'testUserId' + } // Force an error sandbox diff --git a/test/unit/mocks/adapters/index.js b/test/unit/mocks/adapters/index.js index edfd691..c6bfb25 100644 --- a/test/unit/mocks/adapters/index.js +++ b/test/unit/mocks/adapters/index.js @@ -152,6 +152,7 @@ const wallet = { bchWallet: new MockBchWallet(), BchWallet: MockBchWallet, moveTokens: async () => {}, + moveTokensFromCustomWallet: async () => {}, moveBch: async () => {}, reclaimTokens: async ()=>{}, generatePartialTx: async ()=>{}, diff --git a/test/unit/use-cases/order.use-case.unit.js b/test/unit/use-cases/order.use-case.unit.js index d619802..c147bca 100644 --- a/test/unit/use-cases/order.use-case.unit.js +++ b/test/unit/use-cases/order.use-case.unit.js @@ -173,8 +173,9 @@ describe('#order-use-case', () => { // Mock dependencies and force expected code path sandbox.stub(uut.orderEntity, 'inputValidate').returns(entryObj) sandbox.stub(uut, 'ensureFunds').resolves() + sandbox.stub(uut.UserModel, 'findById').resolves({ mnemonic: 'testMnemonic' }) 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, 'moveTokensFromCustomWallet').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 }) sandbox.stub(uut.adapters.wallet.bchWallet, 'initialize').resolves() sandbox.stub(uut.adapters.nostr, 'post').resolves('fakeEvenetId') @@ -184,6 +185,55 @@ 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 = { + lokadId: 'SWP', + messageType: 1, + messageClass: 1, + tokenId: 'token-id', + buyOrSell: 'sell', + rateInBaseUnit: 1000, + minUnitsToExchange: 1250, + numTokens: 1, + ticker: 'TEST' + } + + // Mock dependencies and force expected code path + sandbox.stub(uut.orderEntity, 'inputValidate').returns(entryObj) + sandbox.stub(uut, 'ensureFunds').resolves() + sandbox.stub(uut.UserModel, 'findById').resolves({ mnemonic: 'testMnemonic' }) + sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.Util, 'sleep').resolves() + sandbox.stub(uut.adapters.wallet, 'moveTokensFromCustomWallet').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 }) + sandbox.stub(uut.adapters.wallet.bchWallet, 'initialize').resolves() + sandbox.stub(uut.adapters.nostr, 'post').resolves('fakeEvenetId') + + const result = await uut.createOrder(entryObj) + console.log('result: ', result) + + assert.property(result, 'eventId') + assert.property(result, 'noteId') + }) + it('should throw error if user is not found', async () => { + try { + const entryObj = { + lokadId: 'SWP', + messageType: 1, + messageClass: 1, + tokenId: 'token-id', + buyOrSell: 'sell', + rateInBaseUnit: 1000, + minUnitsToExchange: 1250, + numTokens: 1, + ticker: 'TEST' + } + await uut.createOrder(entryObj) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'user not found') + } + }) it('should catch and throw an error', async () => { try {