diff --git a/packages/jellyfish-api-core/__tests__/category/icxorderbook/closeOrder.test.ts b/packages/jellyfish-api-core/__tests__/category/icxorderbook/closeOrder.test.ts new file mode 100644 index 0000000000..dd4f1d1df2 --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/icxorderbook/closeOrder.test.ts @@ -0,0 +1,350 @@ +import { ContainerAdapterClient } from '../../container_adapter_client' +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { + ExtHTLC, HTLC, ICXClaimDFCHTLCInfo, ICXDFCHTLCInfo, ICXEXTHTLCInfo, ICXGenericResult, ICXHTLCStatus, + ICXHTLCType, ICXListHTLCOptions, ICXOfferInfo, ICXOrderInfo, ICXOffer, ICXOrder, ICXOrderStatus, ICXOrderType +} from '../../../src/category/icxorderbook' +import BigNumber from 'bignumber.js' +import { accountDFI, idDFI, accountBTC, symbolDFI, ICXSetup, symbolBTC } from './icx_setup' +import { RpcApiError } from '../../../src' + +describe('ICXOrderBook.closeOrder', () => { + const container = new MasterNodeRegTestContainer() + const client = new ContainerAdapterClient(container) + const icxSetup = new ICXSetup(container, client) + + beforeAll(async () => { + await container.start() + await container.waitForReady() + await container.waitForWalletCoinbaseMaturity() + await icxSetup.createAccounts() + await icxSetup.createBTCToken() + await icxSetup.initializeTokensIds() + await icxSetup.mintBTCtoken(100) + await icxSetup.fundAccount(accountDFI, symbolDFI, 500) + await icxSetup.fundAccount(accountBTC, symbolDFI, 10) // for fee + await icxSetup.createBTCDFIPool() + await icxSetup.addLiquidityToBTCDFIPool(1, 100) + await icxSetup.setTakerFee(0.001) + }) + + afterAll(async () => { + await container.stop() + }) + + afterEach(async () => { + await icxSetup.closeAllOpenOffers() + }) + + it('should close the correct order', async () => { + const accountDFIBeforeOrder: Record = await client.call('getaccount', [accountDFI, {}, true], 'bignumber') + // create first order - maker + const order: ICXOrder = { + tokenFrom: idDFI, + chainTo: 'BTC', + ownerAddress: accountDFI, + receivePubkey: '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', + amountFrom: new BigNumber(15), + orderPrice: new BigNumber(0.01) + } + const createOrderResult: ICXGenericResult = await client.icxorderbook.createOrder(order, []) + const createOrderTxId = createOrderResult.txid + await container.generate(1) + + // list ICX orders and check + const ordersAfterCreateOrder: Record = await client.icxorderbook.listOrders() + expect((ordersAfterCreateOrder as Record)[createOrderTxId].status).toStrictEqual(ICXOrderStatus.OPEN) + + // create second order - maker + const order2: ICXOrder = { + chainFrom: 'BTC', + tokenTo: idDFI, + ownerAddress: accountDFI, + amountFrom: new BigNumber(2), + orderPrice: new BigNumber(100) + } + const createOrder2Result = await client.icxorderbook.createOrder(order2, []) + const createOrder2TxId = createOrder2Result.txid + await container.generate(1) + + // list ICX orders and check + const ordersAfterCreateOrder2: Record = await client.icxorderbook.listOrders() + // check details for createOrder2TxId + expect((ordersAfterCreateOrder2 as Record)[createOrder2TxId].status).toStrictEqual(ICXOrderStatus.OPEN) + + // close order createOrderTxId - maker + await client.icxorderbook.closeOrder(createOrderTxId) + await container.generate(1) + + // check createOrderTxId is no more and createOrder2TxId order still exists + const ordersAfterCloseOrder1: Record = await client.icxorderbook.listOrders() + expect((ordersAfterCloseOrder1 as Record)[createOrderTxId]).toBeUndefined() + expect((ordersAfterCloseOrder1 as Record)[createOrder2TxId]).toStrictEqual( + { + status: ICXOrderStatus.OPEN, + type: ICXOrderType.EXTERNAL, + tokenTo: order2.tokenTo === '0' ? symbolDFI : symbolBTC, + chainFrom: order2.chainFrom, + ownerAddress: order2.ownerAddress, + amountFrom: order2.amountFrom, + amountToFill: order2.amountFrom, + orderPrice: order2.orderPrice, + amountToFillInToAsset: order2.amountFrom.multipliedBy(order2.orderPrice), + height: expect.any(BigNumber), + expireHeight: expect.any(BigNumber) + } + ) + + // close createOrder2TxId order also - maker + await client.icxorderbook.closeOrder(createOrder2TxId) + await container.generate(1) + + // check both createOrder2TxId and createOrder2TxId is gone + const ordersAfterCloseOrder2: Record = await client.icxorderbook.listOrders() + expect((ordersAfterCloseOrder2 as Record)[createOrderTxId]).toBeUndefined() + expect((ordersAfterCloseOrder2 as Record)[createOrder2TxId]).toBeUndefined() + + // check accountDFI[idDFI] balance + const accountDFIBalance: Record = await client.call('getaccount', [accountDFI, {}, true], 'bignumber') + expect(accountDFIBalance).toStrictEqual(accountDFIBeforeOrder) + }) + + // NOTE(surangap): check why this is failing + it('should close order with input utxos', async () => { + // create an order - maker + const order: ICXOrder = { + tokenFrom: idDFI, + chainTo: 'BTC', + ownerAddress: accountDFI, + receivePubkey: '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', + amountFrom: new BigNumber(15), + orderPrice: new BigNumber(0.01) + } + const createOrderResult: ICXGenericResult = await client.icxorderbook.createOrder(order, []) + const createOrderTxId = createOrderResult.txid + await container.generate(1) + + // list ICX orders and check + const ordersAfterCreateOrder: Record = await client.icxorderbook.listOrders() + expect((ordersAfterCreateOrder as Record)[createOrderTxId].status).toStrictEqual(ICXOrderStatus.OPEN) + + // input utxos + const inputUTXOs = await container.fundAddress(accountDFI, 10) + // close order createOrderTxId - maker + const closeTxId = (await client.icxorderbook.closeOrder(createOrderTxId, [inputUTXOs])).txid + await container.generate(1) + + const rawtx = await container.call('getrawtransaction', [closeTxId, true]) + expect(rawtx.vin[0].txid).toStrictEqual(inputUTXOs.txid) + expect(rawtx.vin[0].vout).toStrictEqual(inputUTXOs.vout) + + // list closed orders and check order createOrderTxId + const ordersAfterCloseOrder: Record = await client.icxorderbook.listOrders({ closed: true }) + const orderInfoRetrived = ordersAfterCloseOrder as Record + expect(orderInfoRetrived[createOrderTxId].status).toStrictEqual(ICXOrderStatus.CLOSED) + expect(orderInfoRetrived[createOrderTxId].ownerAddress).toStrictEqual(order.ownerAddress) + }) + + it('should close a partially settled DFI sell order', async () => { + // create order - maker + const order: ICXOrder = { + tokenFrom: idDFI, + chainTo: 'BTC', + ownerAddress: accountDFI, + receivePubkey: '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', + amountFrom: new BigNumber(15), + orderPrice: new BigNumber(0.01) + } + const createOrderResult: ICXGenericResult = await client.icxorderbook.createOrder(order, []) + const createOrderTxId = createOrderResult.txid + await container.generate(1) + + // list ICX orders anc check + const ordersAfterCreateOrder: Record = await client.icxorderbook.listOrders() + expect((ordersAfterCreateOrder as Record)[createOrderTxId].status).toStrictEqual(ICXOrderStatus.OPEN) + + const accountBTCBeforeOffer: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') + // make offer to partial amount 10 DFI - taker + const offer: ICXOffer = { + orderTx: createOrderTxId, + amount: new BigNumber(0.10), // 0.10 BTC = 10 DFI + ownerAddress: accountBTC + } + const makeOfferResult = await client.icxorderbook.makeOffer(offer, []) + const makeOfferTxId = makeOfferResult.txid + await container.generate(1) + + const accountBTCAfterOffer: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') + + // check fee of 0.01 DFI has been reduced from the accountBTCBeforeOffer[idDFI] + // Fee = takerFeePerBTC(inBTC) * amount(inBTC) * DEX DFI per BTC rate + expect(accountBTCAfterOffer[idDFI]).toStrictEqual(accountBTCBeforeOffer[idDFI].minus(0.01)) + + // List the ICX offers for orderTx = createOrderTxId and check + const ordersAfterMakeOffer: Record = await client.icxorderbook.listOrders({ orderTx: createOrderTxId }) + expect(Object.keys(ordersAfterMakeOffer).length).toBe(2) // extra entry for the warning text returned by the RPC atm. + expect((ordersAfterMakeOffer as Record)[makeOfferTxId].status).toStrictEqual(ICXOrderStatus.OPEN) + + const accountDFIBeforeDFCHTLC: Record = await client.call('getaccount', [accountDFI, {}, true], 'bignumber') + // create DFCHTLC - maker + const DFCHTLC: HTLC = { + offerTx: makeOfferTxId, + amount: new BigNumber(10), // in DFC + hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', + timeout: 500 + } + const DFCHTLCTxId = (await client.icxorderbook.submitDFCHTLC(DFCHTLC)).txid + await container.generate(1) + + const accountDFIAfterDFCHTLC: Record = await client.call('getaccount', [accountDFI, {}, true], 'bignumber') + expect(accountDFIAfterDFCHTLC[idDFI]).toStrictEqual(accountDFIBeforeDFCHTLC[idDFI].minus(0.01)) + + // List htlc anc check + const listHTLCOptions: ICXListHTLCOptions = { + offerTx: makeOfferTxId + } + const HTLCs: Record = await client.icxorderbook.listHTLCs(listHTLCOptions) + expect(Object.keys(HTLCs).length).toBe(2) // extra entry for the warning text returned by the RPC atm. + expect((HTLCs[DFCHTLCTxId] as ICXDFCHTLCInfo).type).toStrictEqual(ICXHTLCType.DFC) + expect((HTLCs[DFCHTLCTxId] as ICXDFCHTLCInfo).status).toStrictEqual(ICXOrderStatus.OPEN) + expect((HTLCs[DFCHTLCTxId] as ICXDFCHTLCInfo).offerTx).toStrictEqual(makeOfferTxId) + + // submit EXT HTLC - taker + const ExtHTLC: ExtHTLC = { + offerTx: makeOfferTxId, + amount: new BigNumber(0.10), + hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', + htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', + ownerPubkey: '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', + timeout: 15 + } + const ExtHTLCTxId = (await client.icxorderbook.submitExtHTLC(ExtHTLC)).txid + await container.generate(1) + + // List htlc and check + const listHTLCOptionsAfterExtHTLC = { + offerTx: makeOfferTxId + } + const HTLCsAfterExtHTLC: Record = await client.icxorderbook.listHTLCs(listHTLCOptionsAfterExtHTLC) + expect(Object.keys(HTLCsAfterExtHTLC).length).toBe(3) // extra entry for the warning text returned by the RPC atm. + expect((HTLCsAfterExtHTLC[ExtHTLCTxId] as ICXEXTHTLCInfo).type).toStrictEqual(ICXHTLCType.EXTERNAL) + expect((HTLCsAfterExtHTLC[ExtHTLCTxId] as ICXEXTHTLCInfo).status).toStrictEqual(ICXOrderStatus.OPEN) + expect((HTLCsAfterExtHTLC[ExtHTLCTxId] as ICXEXTHTLCInfo).offerTx).toStrictEqual(makeOfferTxId) + + const accountBTCAfterEXTHTLC: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') + // should have the same balance as accountBTCAfter + expect(accountBTCAfterEXTHTLC).toStrictEqual(accountBTCAfterEXTHTLC) + + const accountDFIBeforeClaim: Record = await client.call('getaccount', [accountDFI, {}, true], 'bignumber') + const accountBTCBeforeClaim: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') + + // claim - taker + const claimTxId = (await client.icxorderbook.claimDFCHTLC(DFCHTLCTxId, 'f75a61ad8f7a6e0ab701d5be1f5d4523a9b534571e4e92e0c4610c6a6784ccef')).txid + await container.generate(1) + + // List htlc and check + const listHTLCOptionsAfterClaim = { + offerTx: makeOfferTxId, + closed: true + } + const HTLCsAfterClaim: Record = await client.icxorderbook.listHTLCs(listHTLCOptionsAfterClaim) + expect(Object.keys(HTLCsAfterClaim).length).toBe(4) // extra entry for the warning text returned by the RPC atm. + // we have a common field "type", use that to narrow down the record + if (HTLCsAfterClaim[claimTxId].type === ICXHTLCType.CLAIM_DFC) { + // ICXClaimDFCHTLCInfo cast + const ClaimHTLCInfo: ICXClaimDFCHTLCInfo = HTLCsAfterClaim[claimTxId] as ICXClaimDFCHTLCInfo + expect(ClaimHTLCInfo.dfchtlcTx).toStrictEqual(DFCHTLCTxId) + expect(ClaimHTLCInfo.seed).toStrictEqual('f75a61ad8f7a6e0ab701d5be1f5d4523a9b534571e4e92e0c4610c6a6784ccef') + } + + // check HTLC DFCHTLCTxId is in claimed status + if (HTLCsAfterClaim[DFCHTLCTxId].type === ICXHTLCType.DFC) { + // ICXDFCHTLCInfo cast + const DFCHTLCInfo: ICXDFCHTLCInfo = HTLCsAfterClaim[DFCHTLCTxId] as ICXDFCHTLCInfo + expect(DFCHTLCInfo.status).toStrictEqual(ICXHTLCStatus.CLAIMED) + } + + const accountDFIAfterClaim: Record = await client.call('getaccount', [accountDFI, {}, true], 'bignumber') + const accountBTCAfterClaim: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') + + // maker should get incentive + maker deposit and taker should get amount in DFCHTLCTxId HTLC - takerfee + expect(accountDFIAfterClaim[idDFI]).toStrictEqual(accountDFIBeforeClaim[idDFI].plus(0.0125)) + expect(accountBTCAfterClaim[idDFI]).toStrictEqual(accountBTCBeforeClaim[idDFI].plus(10)) + + // offer should be close by now + const offersForOrder1: Record = await client.icxorderbook.listOrders({ orderTx: createOrderTxId }) + expect(Object.keys(offersForOrder1).length).toBe(1) // extra entry for the warning text returned by the RPC atm. + + // check partial order remaining + const ordersAfterClaim: Record = await client.icxorderbook.listOrders() + const orderInfo: Record = ordersAfterClaim as Record + expect(orderInfo[createOrderTxId].tokenFrom).toStrictEqual(symbolDFI) + expect(orderInfo[createOrderTxId].chainTo).toStrictEqual('BTC') + expect(orderInfo[createOrderTxId].ownerAddress).toStrictEqual(accountDFI) + expect(orderInfo[createOrderTxId].receivePubkey).toStrictEqual('037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941') + expect(orderInfo[createOrderTxId].amountFrom).toStrictEqual(new BigNumber(15)) + expect(orderInfo[createOrderTxId].amountToFill).toStrictEqual(new BigNumber(5)) + expect(orderInfo[createOrderTxId].orderPrice).toStrictEqual(new BigNumber(0.01)) + expect(orderInfo[createOrderTxId].amountToFillInToAsset).toStrictEqual(new BigNumber(0.0500000)) + + // try to close the createOrderTxId order - maker + await client.icxorderbook.closeOrder(createOrderTxId) + await container.generate(1) + + // check createOrderTxId order is no more + const ordersAfterCloseOrder: Record = await client.icxorderbook.listOrders() + expect((ordersAfterCloseOrder as Record)[createOrderTxId]).toBeUndefined() + + // check accountDFI[idDFI] balance, remaining 5 DFI should be returned + const accountDFIFinalBalance: Record = await client.call('getaccount', [accountDFI, {}, true], 'bignumber') + expect(accountDFIFinalBalance[idDFI]).toStrictEqual(accountDFIAfterClaim[idDFI].plus(5)) + }) + + it('should return an error when try to close an order with invalid transaction id', async () => { + // create an order - maker + const order: ICXOrder = { + tokenFrom: idDFI, + chainTo: 'BTC', + ownerAddress: accountDFI, + receivePubkey: '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', + amountFrom: new BigNumber(15), + orderPrice: new BigNumber(0.01) + } + const createOrderResult: ICXGenericResult = await client.icxorderbook.createOrder(order, []) + const createOrderTxId = createOrderResult.txid + await container.generate(1) + + // list ICX orders and check + const ordersAfterCreateOrder: Record = await client.icxorderbook.listOrders() + expect((ordersAfterCreateOrder as Record)[createOrderTxId].status).toStrictEqual(ICXOrderStatus.OPEN) + + // close order "123" - maker + const promise = client.icxorderbook.closeOrder('123') + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow('RpcApiError: \'orderTx (0000000000000000000000000000000000000000000000000000000000000123) does not exist\', code: -8, method: icx_closeorder') + + // close order "INVALID_TX_ID" - maker + const promise2 = client.icxorderbook.closeOrder('INVALID_TX_ID') + await expect(promise2).rejects.toThrow(RpcApiError) + await expect(promise2).rejects.toThrow('RpcApiError: \'orderTx (0000000000000000000000000000000000000000000000000000000000000000) does not exist\', code: -8, method: icx_closeorder') + + // check createOrderTxId order still exists + const ordersAfterCloseOrderAttempts: Record = await client.icxorderbook.listOrders() + expect((ordersAfterCloseOrderAttempts as Record)[createOrderTxId]).toStrictEqual( + { + status: ICXOrderStatus.OPEN, + type: ICXOrderType.INTERNAL, + tokenFrom: order.tokenFrom === '0' ? symbolDFI : symbolBTC, + chainTo: order.chainTo, + receivePubkey: order.receivePubkey, + ownerAddress: order.ownerAddress, + amountFrom: order.amountFrom, + amountToFill: order.amountFrom, + orderPrice: order.orderPrice, + amountToFillInToAsset: order.amountFrom.multipliedBy(order.orderPrice), + height: expect.any(BigNumber), + expireHeight: expect.any(BigNumber) + } + ) + }) +}) diff --git a/packages/jellyfish-api-core/__tests__/category/icxorderbook/listHTLCs.test.ts b/packages/jellyfish-api-core/__tests__/category/icxorderbook/listHTLCs.test.ts index 541ab14c4e..5f54393bcf 100644 --- a/packages/jellyfish-api-core/__tests__/category/icxorderbook/listHTLCs.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/icxorderbook/listHTLCs.test.ts @@ -32,8 +32,7 @@ describe('ICXOrderBook.listHTLCs', () => { }) afterEach(async () => { - // NOTE(surangap): enable this after #ain/583 - // await icxSetup.closeAllOpenOffers() + await icxSetup.closeAllOpenOffers() }) it('should list HTLCs for particular offer', async () => { diff --git a/packages/jellyfish-api-core/src/category/icxorderbook.ts b/packages/jellyfish-api-core/src/category/icxorderbook.ts index e66abef5c4..1c877e8883 100644 --- a/packages/jellyfish-api-core/src/category/icxorderbook.ts +++ b/packages/jellyfish-api-core/src/category/icxorderbook.ts @@ -153,6 +153,25 @@ export class ICXOrderBook { ) } + /** + * Closes ICX order + * + * @param {string} orderTx Transaction id of maker order + * @param {UTXO[]} [utxos = []] Specific utxos to spend + * @param {string} utxos.txid transaction Id + * @param {number} utxos.vout The output number + * @return {Promise} Object indluding transaction id of the the transaction + */ + async closeOrder (orderTx: string, utxos: UTXO[] = []): Promise { + return await this.client.call( + 'icx_closeorder', + [ + orderTx, utxos + ], + 'bignumber' + ) + } + /** * Returns information about order or fillorder * diff --git a/website/docs/jellyfish/api/icxorderbook.md b/website/docs/jellyfish/api/icxorderbook.md index 092f36c067..2269b4d963 100644 --- a/website/docs/jellyfish/api/icxorderbook.md +++ b/website/docs/jellyfish/api/icxorderbook.md @@ -169,6 +169,26 @@ interface ICXGenericResult { } ``` +## closeOrder + +Closes ICX order + +```ts title="client.icxorderbook.closeOrder()" +interface icxorderbook { + closeOrder (orderTx: string, utxos: UTXO[] = []): Promise +} + +interface UTXO { + txid: string + vout: number +} + +interface ICXGenericResult { + WARNING: string + txid: string +} +``` + ## getOrder Returns information about order or fillorder