From eadf1922822775c968c05c7ffd0e0eb740eec30d Mon Sep 17 00:00:00 2001 From: jouzo Date: Tue, 3 Aug 2021 21:28:12 +0200 Subject: [PATCH] Add SPV claimhtc RPC --- .idea/dictionaries/fuxing.xml | 4 +- .../category/account/listBurnHistory.test.ts | 2 +- .../category/governance/vote.test.ts | 230 ++++++++++++++++++ .../icxorderbook/claimDFCHTLC.test.ts | 95 +++++--- .../category/icxorderbook/closeOffer.test.ts | 118 +++++---- .../category/icxorderbook/closeOrder.test.ts | 24 +- .../icxorderbook/complexTests.test.ts | 38 ++- .../category/icxorderbook/getOrder.test.ts | 12 +- .../category/icxorderbook/listHTLCs.test.ts | 49 ++-- .../category/icxorderbook/listOrders.test.ts | 16 +- .../category/icxorderbook/makeOffer.test.ts | 24 +- .../icxorderbook/submitDFCHTLC.test.ts | 36 +-- .../icxorderbook/submitExtHTLC.test.ts | 43 ++-- .../category/mining/getMiningInfo.test.ts | 9 +- .../category/oracle/appointOracle.test.ts | 52 ++++ .../category/oracle/getPrice.test.ts | 17 -- .../oracle/listLatestRawPrices.test.ts | 28 --- .../category/oracle/listPrices.test.ts | 26 -- .../category/oracle/setOracleData.test.ts | 28 ++- .../category/oracle/updateOracle.test.ts | 108 ++++++++ .../__tests__/category/spv/claimHtlc.test.ts | 135 ++++++++++ .../__tests__/category/spv/createHtlc.test.ts | 91 +++++++ .../category/spv/decodeHtlcScript.test.ts | 121 +++++++++ .../spv/listReceivedByAddress.test.ts | 4 +- .../category/spv/sendToAddress.test.ts | 117 +++++++++ .../src/category/governance.ts | 28 +++ .../jellyfish-api-core/src/category/mining.ts | 13 +- .../jellyfish-api-core/src/category/oracle.ts | 2 +- .../jellyfish-api-core/src/category/spv.ts | 105 +++++++- .../src/chains/defid_container.ts | 2 +- .../src/chains/reg_test_container/index.ts | 3 +- website/docs/jellyfish/api/governance.md | 27 ++ website/docs/jellyfish/api/spv.md | 80 +++++- 33 files changed, 1413 insertions(+), 274 deletions(-) create mode 100644 packages/jellyfish-api-core/__tests__/category/governance/vote.test.ts create mode 100644 packages/jellyfish-api-core/__tests__/category/spv/claimHtlc.test.ts create mode 100644 packages/jellyfish-api-core/__tests__/category/spv/createHtlc.test.ts create mode 100644 packages/jellyfish-api-core/__tests__/category/spv/decodeHtlcScript.test.ts create mode 100644 packages/jellyfish-api-core/__tests__/category/spv/sendToAddress.test.ts diff --git a/.idea/dictionaries/fuxing.xml b/.idea/dictionaries/fuxing.xml index d4429d3efc..d4eeec705e 100644 --- a/.idea/dictionaries/fuxing.xml +++ b/.idea/dictionaries/fuxing.xml @@ -55,6 +55,8 @@ dummypos dumpprivkey equalverify + eunospayaheight + fortcanningheight fromaltstack fullstackninja fuxingloh @@ -212,4 +214,4 @@ zynesis - \ No newline at end of file + diff --git a/packages/jellyfish-api-core/__tests__/category/account/listBurnHistory.test.ts b/packages/jellyfish-api-core/__tests__/category/account/listBurnHistory.test.ts index 53d655c4ae..79ba83a713 100644 --- a/packages/jellyfish-api-core/__tests__/category/account/listBurnHistory.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/account/listBurnHistory.test.ts @@ -86,7 +86,7 @@ describe('Account', () => { it('first transaction should be masternode creation fee burn', async () => { const history = await client.account.listBurnHistory() - expect(history[6].owner.slice(0, 16)).toStrictEqual('6a1a446654784301') + expect(history[6].owner.slice(0, 16)).toStrictEqual('6a1c446654784301') expect(history[6].type).toStrictEqual('CreateMasternode') expect(history[6].txn).toStrictEqual(2) expect(history[6].amounts.length).toStrictEqual(1) diff --git a/packages/jellyfish-api-core/__tests__/category/governance/vote.test.ts b/packages/jellyfish-api-core/__tests__/category/governance/vote.test.ts new file mode 100644 index 0000000000..e7fc018d22 --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/governance/vote.test.ts @@ -0,0 +1,230 @@ +import { ContainerAdapterClient } from '../../container_adapter_client' +import { RpcApiError } from '../../../src' +import { VoteDecision } from '../../../src/category/governance' +import { GovernanceMasterNodeRegTestContainer } from './governance_container' + +describe('Governance', () => { + const container = new GovernanceMasterNodeRegTestContainer() + const client = new ContainerAdapterClient(container) + + beforeAll(async () => { + await container.start() + await container.waitForReady() + await container.waitForWalletCoinbaseMaturity() + }) + + afterAll(async () => { + await container.stop() + }) + + it('should vote on a proposal', async () => { + const proposalId = await container.call('createcfp', [{ + title: 'A community fund proposal', + amount: 100, + payoutAddress: await container.call('getnewaddress') + }]) + await container.generate(1) + + const masternodes = await client.masternode.listMasternodes() + let masternodeId = '' + for (const id in masternodes) { + const masternode = masternodes[id] + if (masternode.mintedBlocks > 0) { // Find masternode that mined at least one block to vote on proposal + masternodeId = id + } + } + + const txid = await client.governance.vote({ proposalId, masternodeId, decision: VoteDecision.YES }) + await container.generate(1) + expect(typeof txid).toStrictEqual('string') + expect(txid.length).toStrictEqual(64) + }) + + it('should not vote on a proposal with a masternode that does not exist', async () => { + const proposalId = await container.call('createcfp', [{ + title: 'A community fund proposal', + amount: 100, + payoutAddress: await container.call('getnewaddress') + }]) + await container.generate(1) + + const masternodeId = '2b830a4c5673402fca8066847344a189844f5446cf2b5dfb0a6a4bb537f4a4b1' + + const promise = client.governance.vote({ proposalId, masternodeId, decision: VoteDecision.YES }) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow(`RpcApiError: 'The masternode ${masternodeId} does not exist', code: -8, method: vote`) + }) + + it('should not vote on a proposal with an inactive masternode', async () => { + const proposalId = await container.call('createcfp', [{ + title: 'A community fund proposal', + amount: 100, + payoutAddress: await container.call('getnewaddress') + }]) + await container.generate(1) + + const ownerAddress = await container.getNewAddress() + const masternodeId = await client.masternode.createMasternode(ownerAddress) + await container.generate(1) + + const promise = client.governance.vote({ proposalId, masternodeId, decision: VoteDecision.YES }) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow(`RpcApiError: 'Test VoteTx execution failed: +masternode <${masternodeId}> is not active', code: -32600, method: vote`) + }) + + it('should not vote on a proposal with a masternode that did not mine at least one block', async () => { + const proposalId = await container.call('createcfp', [{ + title: 'A community fund proposal', + amount: 100, + payoutAddress: await container.call('getnewaddress') + }]) + + const ownerAddress = await container.getNewAddress() + const masternodeId = await client.masternode.createMasternode(ownerAddress) + await container.generate(20) // Enables masternode + + const promise = client.governance.vote({ proposalId, masternodeId, decision: VoteDecision.YES }) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow(`RpcApiError: 'Test VoteTx execution failed: +masternode <${masternodeId}> does not mine at least one block', code: -32600, method: vote`) + }) + + it('should not vote on a proposal not in voting period', async () => { + const proposalId: string = await container.call('createcfp', [{ + title: 'A community fund proposal', + amount: 100, + payoutAddress: await container.call('getnewaddress') + }]) + await container.generate(200) // Expires proposal + + const ownerAddress = await container.getNewAddress() + const masternodeId = await client.masternode.createMasternode(ownerAddress) + await container.generate(20) // Enables masternode + + const promise = client.governance.vote({ proposalId, masternodeId, decision: VoteDecision.YES }) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow(`RpcApiError: 'Proposal <${proposalId}> is not in voting period', code: -8, method: vote`) + }) + + it('should not vote on a proposal that does not exists', async () => { + const proposalId: string = await container.call('createcfp', [{ + title: 'A community fund proposal', + amount: 100, + payoutAddress: await container.call('getnewaddress') + }]) + + const masternodes = await client.masternode.listMasternodes() + let masternodeId = '' + for (const id in masternodes) { + const masternode = masternodes[id] + if (masternode.mintedBlocks > 0) { // Find masternode that mined at least one block to vote on proposal + masternodeId = id + } + } + + const promise = client.governance.vote({ proposalId, masternodeId, decision: VoteDecision.YES }) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow(`RpcApiError: 'Proposal <${proposalId}> does not exists', code: -8, method: vote`) + }) + + it('should vote with utxos', async () => { + const proposalId: string = await container.call('createcfp', [{ + title: 'A community fund proposal', + amount: 100, + payoutAddress: await container.call('getnewaddress') + }]) + await container.generate(1) + + const masternodes = await client.masternode.listMasternodes() + let masternodeId = '' + for (const id in masternodes) { + const masternode = masternodes[id] + if (masternode.mintedBlocks > 0) { // Find masternode that mined at least one block to vote on proposal + masternodeId = id + } + } + + const utxos = await container.call('listunspent', [1, 9999999, [masternodes[masternodeId].ownerAuthAddress]]) + const utxo = utxos[0] + const voteTx = await client.governance.vote({ proposalId, masternodeId, decision: VoteDecision.YES }, [utxo]) + await container.generate(1) + + expect(typeof voteTx).toStrictEqual('string') + expect(voteTx.length).toStrictEqual(64) + + const rawtx = await container.call('getrawtransaction', [voteTx, true]) + expect(rawtx.vin[0].txid).toStrictEqual(utxo.txid) + expect(rawtx.vin[0].vout).toStrictEqual(utxo.vout) + }) + + it('should not vote with utxos not from the owner', async () => { + const proposalId: string = await container.call('createcfp', [{ + title: 'A community fund proposal', + amount: 100, + payoutAddress: await container.call('getnewaddress') + }]) + await container.generate(1) + + const masternodes = await client.masternode.listMasternodes() + let masternodeId = '' + for (const id in masternodes) { + const masternode = masternodes[id] + if (masternode.mintedBlocks > 0) { // Find masternode that mined at least one block to vote on proposal + masternodeId = id + } + } + + const utxo = await container.fundAddress(await container.call('getnewaddress'), 10) + const promise = client.governance.vote({ proposalId, masternodeId, decision: VoteDecision.YES }, [utxo]) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow(`RpcApiError: 'Test VoteTx execution failed: +tx must have at least one input from the owner', code: -32600, method: vote`) + }) + + it('should not vote with wrongly formatted utxos\' txid', async () => { + const proposalId: string = await container.call('createcfp', [{ + title: 'A community fund proposal', + amount: 100, + payoutAddress: await container.call('getnewaddress') + }]) + await container.generate(1) + + const masternodes = await client.masternode.listMasternodes() + let masternodeId = '' + for (const id in masternodes) { + const masternode = masternodes[id] + if (masternode.mintedBlocks > 0) { // Find masternode that mined at least one block to vote on proposal + masternodeId = id + } + } + + const promise = client.governance.vote({ proposalId, masternodeId, decision: VoteDecision.YES }, [{ txid: 'XXXX', vout: 1 }]) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow('RpcApiError: \'txid must be of length 64 (not 4, for \'XXXX\')\', code: -8, method: vote') + }) + + it('should not vote with invalid utxos\' txid', async () => { + const proposalId: string = await container.call('createcfp', [{ + title: 'A community fund proposal', + amount: 100, + payoutAddress: await container.call('getnewaddress') + }]) + await container.generate(1) + + const masternodes = await client.masternode.listMasternodes() + let masternodeId = '' + for (const id in masternodes) { + const masternode = masternodes[id] + if (masternode.mintedBlocks > 0) { // Find masternode that mined at least one block to vote on proposal + masternodeId = id + } + } + + const txid = '817f1d1aa80bd908e845f747912bbc1bd29fc87f6e2bb762ead7330e1801c3cd' // random hex string of 64 char + + const promise = client.governance.vote({ proposalId, masternodeId, decision: VoteDecision.YES }, [{ txid, vout: 1 }]) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow('RpcApiError: \'Insufficient funds\', code: -4, method: vote') + }) +}) diff --git a/packages/jellyfish-api-core/__tests__/category/icxorderbook/claimDFCHTLC.test.ts b/packages/jellyfish-api-core/__tests__/category/icxorderbook/claimDFCHTLC.test.ts index 40395bfcb7..ba88ff8e6c 100644 --- a/packages/jellyfish-api-core/__tests__/category/icxorderbook/claimDFCHTLC.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/icxorderbook/claimDFCHTLC.test.ts @@ -36,16 +36,15 @@ describe('ICXOrderBook.claimDFCHTLC', () => { }) afterEach(async () => { - // enable this after #ain/583 - // await icxSetup.closeAllOpenOffers() + await icxSetup.closeAllOpenOffers() }) it('should claim DFC HTLC for DFI sell order', async () => { const { createOrderTxId } = await icxSetup.createDFISellOrder('BTC', accountDFI, '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', new BigNumber(15), new BigNumber(0.01)) const { makeOfferTxId } = await icxSetup.createDFIBuyOffer(createOrderTxId, new BigNumber(0.10), accountBTC) - const { DFCHTLCTxId } = await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 500) + const { DFCHTLCTxId } = await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 1440) await icxSetup.submitExtHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(0.10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', 15) + '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', 24) const accountDFIBeforeClaim: Record = await client.call('getaccount', [accountDFI, {}, true], 'bignumber') const accountBTCBeforeClaim: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') @@ -86,9 +85,9 @@ describe('ICXOrderBook.claimDFCHTLC', () => { it('should claim DFC HTLC with input utxos', async () => { const { createOrderTxId } = await icxSetup.createDFISellOrder('BTC', accountDFI, '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', new BigNumber(15), new BigNumber(0.01)) const { makeOfferTxId } = await icxSetup.createDFIBuyOffer(createOrderTxId, new BigNumber(0.10), accountBTC) - const { DFCHTLCTxId } = await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 500) + const { DFCHTLCTxId } = await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 1440) await icxSetup.submitExtHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(0.10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', 15) + '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', 24) const accountDFIBeforeClaim: Record = await client.call('getaccount', [accountDFI, {}, true], 'bignumber') const accountBTCBeforeClaim: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') @@ -136,9 +135,9 @@ describe('ICXOrderBook.claimDFCHTLC', () => { it('should return an error when try to claim DFC HTLC with invalid transaction id', async () => { const { createOrderTxId } = await icxSetup.createDFISellOrder('BTC', accountDFI, '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', new BigNumber(15), new BigNumber(0.01)) const { makeOfferTxId } = await icxSetup.createDFIBuyOffer(createOrderTxId, new BigNumber(0.10), accountBTC) - await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 500) + await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 1440) await icxSetup.submitExtHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(0.10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', 15) + '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', 24) // claim with invalid DFC HTLC tx id "123" const promise = client.icxorderbook.claimDFCHTLC('123', 'f75a61ad8f7a6e0ab701d5be1f5d4523a9b534571e4e92e0c4610c6a6784ccef') @@ -154,9 +153,9 @@ describe('ICXOrderBook.claimDFCHTLC', () => { it('should return an error when try to claim DFC HTLC with invalid seed', async () => { const { createOrderTxId } = await icxSetup.createDFISellOrder('BTC', accountDFI, '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', new BigNumber(15), new BigNumber(0.01)) const { makeOfferTxId } = await icxSetup.createDFIBuyOffer(createOrderTxId, new BigNumber(0.10), accountBTC) - const { DFCHTLCTxId } = await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 500) + const { DFCHTLCTxId } = await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 1440) await icxSetup.submitExtHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(0.10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', 15) + '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', 24) // claim with invalid seed "INVALID_SEED" const promise = client.icxorderbook.claimDFCHTLC(DFCHTLCTxId, 'INVALID_SEED') @@ -164,32 +163,52 @@ describe('ICXOrderBook.claimDFCHTLC', () => { await expect(promise).rejects.toThrow('RpcApiError: \'Test ICXClaimDFCHTLCTx execution failed:\nhash generated from given seed is different than in dfc htlc: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 - 957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220!\', code: -32600, method: icx_claimdfchtlc') }) - // NOTE(surangap): Why this test is failing. should not be able to claim with arbitary utxos? - // it('should return an error when try to claim DFC HTLC with arbitary input utxos', async () => { - // const { createOrderTxId } = await icxSetup.createDFISellOrder('BTC', accountDFI, '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', new BigNumber(15), new BigNumber(0.01)) - // const { makeOfferTxId } = await icxSetup.createDFIBuyOffer(createOrderTxId, new BigNumber(0.10), accountBTC) - // const { DFCHTLCTxId } = await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 500) - // await icxSetup.submitExtHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(0.10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - // '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', 15) - - // // input utxos - // const inputUTXOs = await container.fundAddress(await container.getNewAddress(), 10) - // // claim - // const promise = client.icxorderbook.claimDFCHTLC(DFCHTLCTxId, 'f75a61ad8f7a6e0ab701d5be1f5d4523a9b534571e4e92e0c4610c6a6784ccef', [inputUTXOs]) - // await expect(promise).rejects.toThrow(RpcApiError) - // await expect(promise).rejects.toThrow('RpcApiError: \'Test ICXSubmitEXTHTLCTx execution failed:\ntx must have at least one input from offer owner\', code: -32600, method: icx_submitexthtlc') - - // // List htlc and check - // const listHTLCOptions: ICXListHTLCOptions = { - // offerTx: makeOfferTxId, - // } - // const HTLCs: Record = await client.call('icx_listhtlcs', [listHTLCOptions], 'bignumber') - // expect(Object.keys(HTLCs).length).toBe(3) // extra entry for the warning text returned by the RPC atm. - // // check HTLC DFCHTLCTxId is still in OPEN status - // if (HTLCs[DFCHTLCTxId].type === ICXHTLCType.DFC) { - // // ICXDFCHTLCInfo cast - // const DFCHTLCInfo: ICXDFCHTLCInfo = HTLCs[DFCHTLCTxId] as ICXDFCHTLCInfo - // expect(DFCHTLCInfo.status).toStrictEqual(ICXHTLCStatus.OPEN) - // } - // }) + it('should be able to claim DFC HTLC with arbitary input utxos', async () => { + const { createOrderTxId } = await icxSetup.createDFISellOrder('BTC', accountDFI, '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', new BigNumber(15), new BigNumber(0.01)) + const { makeOfferTxId } = await icxSetup.createDFIBuyOffer(createOrderTxId, new BigNumber(0.10), accountBTC) + const { DFCHTLCTxId } = await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 1440) + await icxSetup.submitExtHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(0.10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', + '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', 24) + + const accountDFIBeforeClaim: Record = await client.call('getaccount', [accountDFI, {}, true], 'bignumber') + const accountBTCBeforeClaim: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') + // input utxos + const inputUTXOs = await container.fundAddress(await container.getNewAddress(), 10) + // claim + const claimTxId = (await client.icxorderbook.claimDFCHTLC(DFCHTLCTxId, 'f75a61ad8f7a6e0ab701d5be1f5d4523a9b534571e4e92e0c4610c6a6784ccef', [inputUTXOs])).txid + await container.generate(1) + + const rawtx = await container.call('getrawtransaction', [claimTxId, true]) + expect(rawtx.vin[0].txid).toStrictEqual(inputUTXOs.txid) + expect(rawtx.vin[0].vout).toStrictEqual(inputUTXOs.vout) + + // List htlc and check + const listHTLCOptions: ICXListHTLCOptions = { + offerTx: makeOfferTxId, + closed: true + } + const HTLCs: Record = await client.call('icx_listhtlcs', [listHTLCOptions], 'bignumber') + expect(Object.keys(HTLCs).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 (HTLCs[claimTxId].type === ICXHTLCType.CLAIM_DFC) { + // ICXClaimDFCHTLCInfo cast + const ClaimHTLCInfo: ICXClaimDFCHTLCInfo = HTLCs[claimTxId] as ICXClaimDFCHTLCInfo + expect(ClaimHTLCInfo.dfchtlcTx).toStrictEqual(DFCHTLCTxId) + expect(ClaimHTLCInfo.seed).toStrictEqual('f75a61ad8f7a6e0ab701d5be1f5d4523a9b534571e4e92e0c4610c6a6784ccef') + } + + // check HTLC DFCHTLCTxId is in claimed status + if (HTLCs[DFCHTLCTxId].type === ICXHTLCType.DFC) { + // ICXDFCHTLCInfo cast + const DFCHTLCInfo: ICXDFCHTLCInfo = HTLCs[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.010).plus(0.00250)) + expect(accountBTCAfterClaim[idDFI]).toStrictEqual(accountBTCBeforeClaim[idDFI].plus(10)) + }) }) diff --git a/packages/jellyfish-api-core/__tests__/category/icxorderbook/closeOffer.test.ts b/packages/jellyfish-api-core/__tests__/category/icxorderbook/closeOffer.test.ts index 28a92f3447..ef7b8c31d4 100644 --- a/packages/jellyfish-api-core/__tests__/category/icxorderbook/closeOffer.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/icxorderbook/closeOffer.test.ts @@ -1,8 +1,15 @@ import { ContainerAdapterClient } from '../../container_adapter_client' import { MasterNodeRegTestContainer } from '@defichain/testcontainers' -import { ICXGenericResult, ICXOfferInfo, ICXOrderInfo, ICXOffer, ICXOrder, ICXOrderStatus } from '../../../src/category/icxorderbook' +import { + ICXGenericResult, + ICXOffer, + ICXOfferInfo, + ICXOrder, + ICXOrderInfo, + ICXOrderStatus +} from '../../../src/category/icxorderbook' import BigNumber from 'bignumber.js' -import { accountDFI, idDFI, accountBTC, ICXSetup, symbolDFI } from './icx_setup' +import { accountBTC, accountDFI, ICXSetup, idDFI, symbolDFI } from './icx_setup' import { RpcApiError } from '../../../src' describe('ICXOrderBook.closeOffer', () => { @@ -29,6 +36,10 @@ describe('ICXOrderBook.closeOffer', () => { await container.stop() }) + afterEach(async () => { + await icxSetup.closeAllOpenOffers() + }) + it('should close an offer', async () => { // create order - maker const order: ICXOrder = { @@ -81,58 +92,57 @@ describe('ICXOrderBook.closeOffer', () => { expect(accountBTCAfterOfferClose).toStrictEqual(accountBTCBeforeOffer) }) - // NOTE(surangap): This test is failing. - // it('should close an offer for sell BTC order from chain:BTC to chain:DFI', async () => { - // // create order - maker - // const order: ICXOrder = { - // chainFrom: 'BTC', - // tokenTo: idDFI, - // ownerAddress: accountDFI, - // amountFrom: new BigNumber(2), - // orderPrice: new BigNumber(100) - // } - // const createOrderResult: ICXGenericResult = await client.icxorderbook.createOrder(order, []) - // const createOrderTxId = createOrderResult.txid - // await container.generate(1) - - // // list ICX orders and check status - // const ordersAfterCreateOrder: Record = await client.call('icx_listorders', [], 'bignumber') - // 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(10), // - // ownerAddress: accountBTC, - // receivePubkey: '0348790cb93b203a8ea5ce07279cb209d807b535b2ca8b0988a6f7a6578e41f7a5' - // } - // 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 accountBTCBefore[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.call('icx_listorders', [{ orderTx: createOrderTxId }], 'bignumber') - // 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) - - // // close offer makeOfferTxId - taker - // await client.icxorderbook.closeOffer(makeOfferTxId) - // await container.generate(1) - - // // List the ICX offers for orderTx = createOrderTxId and check no more offers - // const ordersAfterCloseOffer: Record = await await container.call('icx_listorders', [{ orderTx: createOrderTxId }]) - // expect(Object.keys(ordersAfterCloseOffer).length).toBe(1) // extra entry for the warning text returned by the RPC atm. - - // // check accountBTC balance, should be the same as accountBTCBeforeOffer - // const accountBTCAfterOfferClose: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') - // expect(accountBTCAfterOfferClose).toStrictEqual(accountBTCBeforeOffer) - // }) + it('should close an offer for sell BTC order from chain:BTC to chain:DFI', async () => { + // create order - maker + const order: ICXOrder = { + chainFrom: 'BTC', + tokenTo: idDFI, + ownerAddress: accountDFI, + amountFrom: new BigNumber(2), + orderPrice: new BigNumber(100) + } + const createOrderResult: ICXGenericResult = await client.icxorderbook.createOrder(order, []) + const createOrderTxId = createOrderResult.txid + await container.generate(1) + + // list ICX orders and check status + const ordersAfterCreateOrder: Record = await client.call('icx_listorders', [], 'bignumber') + 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(10), // + ownerAddress: accountBTC, + receivePubkey: '0348790cb93b203a8ea5ce07279cb209d807b535b2ca8b0988a6f7a6578e41f7a5' + } + 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 accountBTCBefore[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.call('icx_listorders', [{ orderTx: createOrderTxId }], 'bignumber') + 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) + + // close offer makeOfferTxId - taker + await client.icxorderbook.closeOffer(makeOfferTxId) + await container.generate(1) + + // List the ICX offers for orderTx = createOrderTxId and check no more offers + const ordersAfterCloseOffer: Record = await await container.call('icx_listorders', [{ orderTx: createOrderTxId }]) + expect(Object.keys(ordersAfterCloseOffer).length).toBe(1) // extra entry for the warning text returned by the RPC atm. + + // check accountBTC balance, should be the same as accountBTCBeforeOffer + const accountBTCAfterOfferClose: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') + expect(accountBTCAfterOfferClose).toStrictEqual(accountBTCBeforeOffer) + }) it('should close an offer with input utxos', async () => { // create order - maker diff --git a/packages/jellyfish-api-core/__tests__/category/icxorderbook/closeOrder.test.ts b/packages/jellyfish-api-core/__tests__/category/icxorderbook/closeOrder.test.ts index dd4f1d1df2..b75af67273 100644 --- a/packages/jellyfish-api-core/__tests__/category/icxorderbook/closeOrder.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/icxorderbook/closeOrder.test.ts @@ -1,11 +1,24 @@ 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 + ExtHTLC, + HTLC, + ICXClaimDFCHTLCInfo, + ICXDFCHTLCInfo, + ICXEXTHTLCInfo, + ICXGenericResult, + ICXHTLCStatus, + ICXHTLCType, + ICXListHTLCOptions, + ICXOffer, + ICXOfferInfo, + ICXOrder, + ICXOrderInfo, + ICXOrderStatus, + ICXOrderType } from '../../../src/category/icxorderbook' import BigNumber from 'bignumber.js' -import { accountDFI, idDFI, accountBTC, symbolDFI, ICXSetup, symbolBTC } from './icx_setup' +import { accountBTC, accountDFI, ICXSetup, idDFI, symbolBTC, symbolDFI } from './icx_setup' import { RpcApiError } from '../../../src' describe('ICXOrderBook.closeOrder', () => { @@ -109,7 +122,6 @@ describe('ICXOrderBook.closeOrder', () => { 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 = { @@ -191,7 +203,7 @@ describe('ICXOrderBook.closeOrder', () => { offerTx: makeOfferTxId, amount: new BigNumber(10), // in DFC hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - timeout: 500 + timeout: 1440 } const DFCHTLCTxId = (await client.icxorderbook.submitDFCHTLC(DFCHTLC)).txid await container.generate(1) @@ -216,7 +228,7 @@ describe('ICXOrderBook.closeOrder', () => { hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', ownerPubkey: '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - timeout: 15 + timeout: 24 } const ExtHTLCTxId = (await client.icxorderbook.submitExtHTLC(ExtHTLC)).txid await container.generate(1) diff --git a/packages/jellyfish-api-core/__tests__/category/icxorderbook/complexTests.test.ts b/packages/jellyfish-api-core/__tests__/category/icxorderbook/complexTests.test.ts index cb0d041d62..e44ceb459f 100644 --- a/packages/jellyfish-api-core/__tests__/category/icxorderbook/complexTests.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/icxorderbook/complexTests.test.ts @@ -1,11 +1,31 @@ import { ContainerAdapterClient } from '../../container_adapter_client' import { MasterNodeRegTestContainer } from '@defichain/testcontainers' import { - HTLC, ICXGenericResult, ICXOfferInfo, ICXOrderInfo, ICXOffer, ICXOrder, ICXOrderStatus, ICXClaimDFCHTLCInfo, - ICXDFCHTLCInfo, ICXEXTHTLCInfo, ICXHTLCStatus, ICXHTLCType, ICXListHTLCOptions + HTLC, + ICXClaimDFCHTLCInfo, + ICXDFCHTLCInfo, + ICXEXTHTLCInfo, + ICXGenericResult, + ICXHTLCStatus, + ICXHTLCType, + ICXListHTLCOptions, + ICXOffer, + ICXOfferInfo, + ICXOrder, + ICXOrderInfo, + ICXOrderStatus } from '../../../src/category/icxorderbook' import BigNumber from 'bignumber.js' -import { accountDFI, idDFI, accountBTC, ICXSetup, DEX_DFI_PER_BTC_RATE, ICX_TAKERFEE_PER_BTC, symbolDFI, symbolBTC } from './icx_setup' +import { + accountBTC, + accountDFI, + DEX_DFI_PER_BTC_RATE, + ICX_TAKERFEE_PER_BTC, + ICXSetup, + idDFI, + symbolBTC, + symbolDFI +} from './icx_setup' import { accountToAccount } from '@defichain/testing' describe('ICX Complex test scenarios', () => { @@ -33,8 +53,7 @@ describe('ICX Complex test scenarios', () => { }) afterEach(async () => { - // NOTE(surangap): enable this after #ain/583 - // await icxSetup.closeAllOpenOffers() + await icxSetup.closeAllOpenOffers() }) it('make a higher offer to an sell DFI order, then the extra amount should be returned when DFC HTLC is submitted', async () => { @@ -92,7 +111,7 @@ describe('ICX Complex test scenarios', () => { offerTx: makeOfferTxId, amount: new BigNumber(15), // in DFC hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - timeout: 500 + timeout: 1440 } await client.icxorderbook.submitDFCHTLC(DFCHTLC) await container.generate(1) @@ -106,8 +125,7 @@ describe('ICX Complex test scenarios', () => { expect(accountBTCAfterDFCHTLC[idDFI]).toStrictEqual(accountBTCAfterOffer[idDFI].plus(0.005)) }) - // NOTE(surangap): enable this after 1.8.x - it.skip('should claim DFC HTLC for DFI sell order when DEX rate is changed in between', async () => { + it('should claim DFC HTLC for DFI sell order when DEX rate is changed in between', async () => { const { createOrderTxId } = await icxSetup.createDFISellOrder('BTC', accountDFI, '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', new BigNumber(15), new BigNumber(0.01)) const { makeOfferTxId } = await icxSetup.createDFIBuyOffer(createOrderTxId, new BigNumber(0.10), accountBTC) @@ -115,9 +133,9 @@ describe('ICX Complex test scenarios', () => { await accountToAccount(container, symbolBTC, 1, { from: accountBTC, to: accountDFI }) await icxSetup.addLiquidityToBTCDFIPool(1, 150) - const { DFCHTLCTxId } = await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 500) + const { DFCHTLCTxId } = await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 1440) await icxSetup.submitExtHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(0.10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', 15) + '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', 24) const accountDFIBeforeClaim: Record = await client.call('getaccount', [accountDFI, {}, true], 'bignumber') const accountBTCBeforeClaim: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') diff --git a/packages/jellyfish-api-core/__tests__/category/icxorderbook/getOrder.test.ts b/packages/jellyfish-api-core/__tests__/category/icxorderbook/getOrder.test.ts index f48fb56ffb..b8c73fe231 100644 --- a/packages/jellyfish-api-core/__tests__/category/icxorderbook/getOrder.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/icxorderbook/getOrder.test.ts @@ -45,6 +45,10 @@ describe('ICXOrderBook.getOrder', () => { await container.stop() }) + afterEach(async () => { + await icxSetup.closeAllOpenOffers() + }) + it('should get the correct order', async () => { // create first order - maker const order: ICXOrder = { @@ -63,7 +67,7 @@ describe('ICXOrderBook.getOrder', () => { const retrievedOrder: Record = await client.icxorderbook.getOrder(createOrderTxId) expect((retrievedOrder as Record)[createOrderTxId]).toStrictEqual( { - // status: ICXOrderStatus.OPEN, //NOTE(surangap): status is not returned? + status: ICXOrderStatus.OPEN, type: ICXOrderType.INTERNAL, tokenFrom: symbolDFI, chainTo: order.chainTo, @@ -95,7 +99,7 @@ describe('ICXOrderBook.getOrder', () => { // check details for createOrder2TxId expect((retrievedOrder2 as Record)[createOrder2TxId]).toStrictEqual( { - // status: ICXOrderStatus.OPEN, //NOTE(surangap): status is not returned? + status: ICXOrderStatus.OPEN, type: ICXOrderType.EXTERNAL, tokenTo: symbolDFI, chainFrom: order2.chainFrom, @@ -162,7 +166,7 @@ describe('ICXOrderBook.getOrder', () => { expect((retrievedOrder as Record)[makeOffer2TxId]).toStrictEqual( { orderTx: createOrder2TxId, - status: ICXOrderStatus.EXPIRED, // NOTE(surangap): why this is EXPIRED ? should be OPEN? + status: ICXOrderStatus.OPEN, amount: offer2.amount, amountInFromAsset: offer2.amount.dividedBy(order2.orderPrice), ownerAddress: offer2.ownerAddress, @@ -177,7 +181,7 @@ describe('ICXOrderBook.getOrder', () => { expect((retrievedOrder2 as Record)[makeOfferTxId]).toStrictEqual( { orderTx: createOrderTxId, - status: ICXOrderStatus.EXPIRED, // NOTE(surangap): why this is EXPIRED ? should be OPEN? + status: ICXOrderStatus.OPEN, amount: offer.amount, amountInFromAsset: offer.amount.dividedBy(order.orderPrice), ownerAddress: offer.ownerAddress, 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 5f54393bcf..1b6cbfc095 100644 --- a/packages/jellyfish-api-core/__tests__/category/icxorderbook/listHTLCs.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/icxorderbook/listHTLCs.test.ts @@ -1,8 +1,20 @@ import { ContainerAdapterClient } from '../../container_adapter_client' import { MasterNodeRegTestContainer } from '@defichain/testcontainers' import { - ExtHTLC, HTLC, ICXClaimDFCHTLCInfo, ICXDFCHTLCInfo, ICXEXTHTLCInfo, ICXGenericResult, ICXListHTLCOptions, - ICXOfferInfo, ICXOrderInfo, ICXOffer, ICXOrder, ICXHTLCType, ICXHTLCStatus, ICXOrderStatus + ExtHTLC, + HTLC, + ICXClaimDFCHTLCInfo, + ICXDFCHTLCInfo, + ICXEXTHTLCInfo, + ICXGenericResult, + ICXHTLCStatus, + ICXHTLCType, + ICXListHTLCOptions, + ICXOffer, + ICXOfferInfo, + ICXOrder, + ICXOrderInfo, + ICXOrderStatus } from '../../../src/category/icxorderbook' import BigNumber from 'bignumber.js' import { accountBTC, accountDFI, ICXSetup, idDFI, symbolDFI } from './icx_setup' @@ -80,7 +92,7 @@ describe('ICXOrderBook.listHTLCs', () => { offerTx: makeOfferTxId, amount: new BigNumber(10), // in DFC hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - timeout: 500 + timeout: 1440 } const DFCHTLCTxId = (await client.icxorderbook.submitDFCHTLC(DFCHTLC)).txid await container.generate(1) @@ -115,7 +127,7 @@ describe('ICXOrderBook.listHTLCs', () => { hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', ownerPubkey: '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - timeout: 15 + timeout: 24 } const ExtHTLCTxId = (await client.icxorderbook.submitExtHTLC(ExtHTLC)).txid await container.generate(1) @@ -165,7 +177,7 @@ describe('ICXOrderBook.listHTLCs', () => { offerTx: makeOfferTxId, amount: new BigNumber(10), // in DFC hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - timeout: 500 + timeout: 1440 } const DFCHTLCTxId = (await client.icxorderbook.submitDFCHTLC(DFCHTLC)).txid await container.generate(1) @@ -200,7 +212,7 @@ describe('ICXOrderBook.listHTLCs', () => { hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', ownerPubkey: '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - timeout: 15 + timeout: 24 } const ExtHTLCTxId = (await client.icxorderbook.submitExtHTLC(ExtHTLC)).txid await container.generate(1) @@ -239,14 +251,13 @@ describe('ICXOrderBook.listHTLCs', () => { } ) - // NOTE(surangap): Enable this after AIN#599 // List htlc with limit of 1 and check - // const listHTLCOptionsWithLimit1 = { - // offerTx: makeOfferTxId, - // limit: 1 - // } - // const HTLCsWithLimit1: Record = await client.icxorderbook.listHTLCs(listHTLCOptionsWithLimit1) - // expect(Object.keys(HTLCsWithLimit1).length).toBe(2) + const listHTLCOptionsWithLimit1 = { + offerTx: makeOfferTxId, + limit: 1 + } + const HTLCsWithLimit1: Record = await client.icxorderbook.listHTLCs(listHTLCOptionsWithLimit1) + expect(Object.keys(HTLCsWithLimit1).length).toBe(2) }) it('should list closed HTLCs with ICXListHTLCOptions.closed parameter after HTLCs are expired', async () => { @@ -260,7 +271,7 @@ describe('ICXOrderBook.listHTLCs', () => { offerTx: makeOfferTxId, amount: new BigNumber(10), // in DFC hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - timeout: 500 + timeout: 1440 } const DFCHTLCTxId = (await client.icxorderbook.submitDFCHTLC(DFCHTLC)).txid await container.generate(1) @@ -275,7 +286,7 @@ describe('ICXOrderBook.listHTLCs', () => { hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', ownerPubkey: '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - timeout: 15 + timeout: 24 } const ExtHTLCTxId = (await client.icxorderbook.submitExtHTLC(ExtHTLC)).txid await container.generate(1) @@ -315,7 +326,7 @@ describe('ICXOrderBook.listHTLCs', () => { ) // expire HTLCs - await container.generate(550) + await container.generate(2000) const endBlockHeight = (await container.call('getblockchaininfo', [])).blocks expect(endBlockHeight).toBeGreaterThan(Number(startBlockHeight) + Number(550)) @@ -345,7 +356,7 @@ describe('ICXOrderBook.listHTLCs', () => { offerTx: makeOfferTxId, amount: new BigNumber(10), // in DFC hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - timeout: 500 + timeout: 1440 } const DFCHTLCTxId = (await client.icxorderbook.submitDFCHTLC(DFCHTLC)).txid await container.generate(1) @@ -380,7 +391,7 @@ describe('ICXOrderBook.listHTLCs', () => { hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', ownerPubkey: '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - timeout: 15 + timeout: 24 } const ExtHTLCTxId = (await client.icxorderbook.submitExtHTLC(ExtHTLC)).txid await container.generate(1) @@ -477,7 +488,7 @@ describe('ICXOrderBook.listHTLCs', () => { offerTx: makeOfferTxId, amount: new BigNumber(10), // in DFC hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - timeout: 500 + timeout: 1440 } const DFCHTLCTxId = (await client.icxorderbook.submitDFCHTLC(DFCHTLC)).txid await container.generate(1) diff --git a/packages/jellyfish-api-core/__tests__/category/icxorderbook/listOrders.test.ts b/packages/jellyfish-api-core/__tests__/category/icxorderbook/listOrders.test.ts index 4fcb386faf..6e5fda4a03 100644 --- a/packages/jellyfish-api-core/__tests__/category/icxorderbook/listOrders.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/icxorderbook/listOrders.test.ts @@ -67,7 +67,7 @@ describe('ICXOrderBook.listOrders', () => { const retrivedOrder: Record = await client.icxorderbook.getOrder(createOrderTxId) expect((retrivedOrder as Record)[createOrderTxId]).toStrictEqual( { - // status: ICXOrderStatus.OPEN, //NOTE(surangap): uncomment after ain/#571 + status: ICXOrderStatus.OPEN, type: ICXOrderType.INTERNAL, tokenFrom: symbolDFI, chainTo: order.chainTo, @@ -549,14 +549,12 @@ describe('ICXOrderBook.listOrders', () => { } ) - // NOTE(surangap): This list all the orders in the system which is incorrect - // Uncomment once C++ side is fixed. // list offers for Tx Id "INVALID_ORDER_TX_ID" and check - // const ordersForInvalidOrderTX: Record = await client.icxorderbook.listOrders({ orderTx: 'INVALID_ORDER_TX_ID' }) - // expect(ordersForInvalidOrderTX).toStrictEqual( - // { - // WARNING: 'ICX and Atomic Swap are experimental features. You might end up losing your funds. USE IT AT YOUR OWN RISK.' - // } - // ) + const ordersForInvalidOrderTX: Record = await client.icxorderbook.listOrders({ orderTx: 'INVALID_ORDER_TX_ID' }) + expect(ordersForInvalidOrderTX).toStrictEqual( + { + WARNING: 'ICX and Atomic Swap are experimental features. You might end up losing your funds. USE IT AT YOUR OWN RISK.' + } + ) }) }) diff --git a/packages/jellyfish-api-core/__tests__/category/icxorderbook/makeOffer.test.ts b/packages/jellyfish-api-core/__tests__/category/icxorderbook/makeOffer.test.ts index 9674212e5b..46d57f7f2f 100644 --- a/packages/jellyfish-api-core/__tests__/category/icxorderbook/makeOffer.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/icxorderbook/makeOffer.test.ts @@ -1,8 +1,24 @@ import { ContainerAdapterClient } from '../../container_adapter_client' import { MasterNodeRegTestContainer } from '@defichain/testcontainers' -import { ICXGenericResult, ICXOfferInfo, ICXOrderInfo, ICXOffer, ICXOrder, ICXOrderStatus, ICXOrderType } from '../../../src/category/icxorderbook' +import { + ICXGenericResult, + ICXOffer, + ICXOfferInfo, + ICXOrder, + ICXOrderInfo, + ICXOrderStatus, + ICXOrderType +} from '../../../src/category/icxorderbook' import BigNumber from 'bignumber.js' -import { ICXSetup, symbolDFI, accountDFI, idDFI, accountBTC, ICX_TAKERFEE_PER_BTC, DEX_DFI_PER_BTC_RATE } from './icx_setup' +import { + accountBTC, + accountDFI, + DEX_DFI_PER_BTC_RATE, + ICX_TAKERFEE_PER_BTC, + ICXSetup, + idDFI, + symbolDFI +} from './icx_setup' import { RpcApiError } from '@defichain/jellyfish-api-core' describe('ICXOrderBook.makeOffer', () => { @@ -29,6 +45,10 @@ describe('ICXOrderBook.makeOffer', () => { await container.stop() }) + afterEach(async () => { + await icxSetup.closeAllOpenOffers() + }) + it('should make an partial offer to an sell DFI order', async () => { const accountDFIBeforeOrder: Record = await client.call('getaccount', [accountDFI, {}, true], 'bignumber') // create order - maker diff --git a/packages/jellyfish-api-core/__tests__/category/icxorderbook/submitDFCHTLC.test.ts b/packages/jellyfish-api-core/__tests__/category/icxorderbook/submitDFCHTLC.test.ts index 02d4de701c..7bc417e0fc 100644 --- a/packages/jellyfish-api-core/__tests__/category/icxorderbook/submitDFCHTLC.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/icxorderbook/submitDFCHTLC.test.ts @@ -1,13 +1,22 @@ import { ContainerAdapterClient } from '../../container_adapter_client' import { MasterNodeRegTestContainer } from '@defichain/testcontainers' import { - HTLC, ICXClaimDFCHTLCInfo, ICXDFCHTLCInfo, ICXEXTHTLCInfo, ICXGenericResult, ExtHTLC, - ICXListHTLCOptions, ICXOfferInfo, ICXOrderInfo, ICXOffer, ICXOrder, ICXOrderStatus, ICXHTLCType + ExtHTLC, + HTLC, + ICXClaimDFCHTLCInfo, + ICXDFCHTLCInfo, + ICXEXTHTLCInfo, + ICXGenericResult, + ICXHTLCType, + ICXListHTLCOptions, + ICXOffer, + ICXOfferInfo, + ICXOrder, + ICXOrderInfo, + ICXOrderStatus } from '../../../src/category/icxorderbook' import BigNumber from 'bignumber.js' -import { - accountDFI, idDFI, accountBTC, ICXSetup, symbolDFI -} from './icx_setup' +import { accountBTC, accountDFI, ICXSetup, idDFI, symbolDFI } from './icx_setup' import { RpcApiError } from '@defichain/jellyfish-api-core' describe('ICXOrderBook.submitDFCHTLC', () => { @@ -35,8 +44,7 @@ describe('ICXOrderBook.submitDFCHTLC', () => { }) afterEach(async () => { - // enable this after #ain/583 - // await icxSetup.closeAllOpenOffers() + await icxSetup.closeAllOpenOffers() }) it('should submit DFC HTLC for a DFC buy offer', async () => { @@ -88,7 +96,7 @@ describe('ICXOrderBook.submitDFCHTLC', () => { offerTx: makeOfferTxId, amount: new BigNumber(10), // in DFC hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - timeout: 500 + timeout: 1440 } const DFCHTLCTxId = (await client.icxorderbook.submitDFCHTLC(DFCHTLC)).txid await container.generate(1) @@ -167,7 +175,7 @@ describe('ICXOrderBook.submitDFCHTLC', () => { offerTx: makeOfferTxId, amount: new BigNumber(5), // lower than the amout in offer hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - timeout: 500 + timeout: 1440 } const DFCHTLCTxId = (await client.icxorderbook.submitDFCHTLC(DFCHTLC)).txid await container.generate(1) @@ -301,7 +309,7 @@ describe('ICXOrderBook.submitDFCHTLC', () => { amount: DFCHTLC.amount, amountInEXTAsset: DFCHTLC.amount.dividedBy(order.orderPrice), hash: DFCHTLC.hash, - timeout: new BigNumber(500), + timeout: new BigNumber(480), height: expect.any(BigNumber), refundHeight: expect.any(BigNumber) } @@ -318,7 +326,7 @@ describe('ICXOrderBook.submitDFCHTLC', () => { offerTx: makeOfferTxId, amount: new BigNumber(10), // in DFC hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - timeout: 500 + timeout: 1440 } // input utxos @@ -365,7 +373,7 @@ describe('ICXOrderBook.submitDFCHTLC', () => { offerTx: 'INVALID_OFFER_TX_ID', amount: new BigNumber(10), // in DFC hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - timeout: 500 + timeout: 1440 } const promise = client.icxorderbook.submitDFCHTLC(DFCHTLC) @@ -377,7 +385,7 @@ describe('ICXOrderBook.submitDFCHTLC', () => { offerTx: '123', amount: new BigNumber(10), // in DFC hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - timeout: 500 + timeout: 1440 } const promise2 = client.icxorderbook.submitDFCHTLC(DFCHTLC2) @@ -406,7 +414,7 @@ describe('ICXOrderBook.submitDFCHTLC', () => { offerTx: makeOfferTxId, amount: new BigNumber(15), // here the amount is 15 DFI, but in the offer the amount is 10 DFI hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - timeout: 500 + timeout: 1440 } const promise = client.icxorderbook.submitDFCHTLC(DFCHTLC) diff --git a/packages/jellyfish-api-core/__tests__/category/icxorderbook/submitExtHTLC.test.ts b/packages/jellyfish-api-core/__tests__/category/icxorderbook/submitExtHTLC.test.ts index f5ddb9575e..3e46ddc1fd 100644 --- a/packages/jellyfish-api-core/__tests__/category/icxorderbook/submitExtHTLC.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/icxorderbook/submitExtHTLC.test.ts @@ -44,8 +44,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { }) afterEach(async () => { - // enable this after #ain/583 - // await icxSetup.closeAllOpenOffers() + await icxSetup.closeAllOpenOffers() }) it('should submit ExtHTLC for a DFC buy offer', async () => { @@ -96,7 +95,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { offerTx: makeOfferTxId, amount: new BigNumber(10), // in DFC hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - timeout: 500 + timeout: 1440 } const DFCHTLCTxId = (await client.icxorderbook.submitDFCHTLC(DFCHTLC)).txid await container.generate(1) @@ -123,7 +122,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', ownerPubkey: '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - timeout: 15 + timeout: 24 } const ExtHTLCTxId = (await client.icxorderbook.submitExtHTLC(ExtHTLC)).txid await container.generate(1) @@ -235,7 +234,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { it('should submit ExtHTLC for a DFC buy offer with input utxos', async () => { const { order, createOrderTxId } = await icxSetup.createDFISellOrder('BTC', accountDFI, '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', new BigNumber(15), new BigNumber(0.01)) const { makeOfferTxId } = await icxSetup.createDFIBuyOffer(createOrderTxId, new BigNumber(0.10), accountBTC) - await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 500) + await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 1440) const accountBTCBeforeEXTHTLC: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') // submit EXT HTLC - taker @@ -245,7 +244,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', ownerPubkey: '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - timeout: 15 + timeout: 24 } // input utxos @@ -286,7 +285,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { it('should return an error when submitting ExtHTLC with incorrect ExtHTLC.offerTx', async () => { const { createOrderTxId } = await icxSetup.createDFISellOrder('BTC', accountDFI, '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', new BigNumber(15), new BigNumber(0.01)) const { makeOfferTxId } = await icxSetup.createDFIBuyOffer(createOrderTxId, new BigNumber(0.10), accountBTC) - await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 500) + await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 1440) const accountBTCBeforeEXTHTLC: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') // submit EXT HTLC with offer tx "123"- taker @@ -296,7 +295,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', ownerPubkey: '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - timeout: 15 + timeout: 24 } const promise = client.icxorderbook.submitExtHTLC(ExtHTLC) await expect(promise).rejects.toThrow(RpcApiError) @@ -309,7 +308,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', ownerPubkey: '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - timeout: 15 + timeout: 24 } const promise2 = client.icxorderbook.submitExtHTLC(ExtHTLC2) await expect(promise2).rejects.toThrow(RpcApiError) @@ -330,7 +329,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { it('should return an error when submitting ExtHTLC with incorrect ExtHTLC.amount than the amount in DFC HTLC', async () => { const { createOrderTxId } = await icxSetup.createDFISellOrder('BTC', accountDFI, '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', new BigNumber(15), new BigNumber(0.01)) const { makeOfferTxId } = await icxSetup.createDFIBuyOffer(createOrderTxId, new BigNumber(0.10), accountBTC) - await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 500) + await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 1440) const accountBTCBeforeEXTHTLC: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') // submit EXT HTLC with amount 0.20 BTC- taker @@ -340,7 +339,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', ownerPubkey: '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - timeout: 15 + timeout: 24 } const promise = client.icxorderbook.submitExtHTLC(ExtHTLC) await expect(promise).rejects.toThrow(RpcApiError) @@ -353,7 +352,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', ownerPubkey: '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - timeout: 15 + timeout: 24 } const promise2 = client.icxorderbook.submitExtHTLC(ExtHTLC2) await expect(promise2).rejects.toThrow(RpcApiError) @@ -374,7 +373,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { it('should return an error when submitting ExtHTLC with incorrect hash from the hash in DFC HTLC', async () => { const { createOrderTxId } = await icxSetup.createDFISellOrder('BTC', accountDFI, '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', new BigNumber(15), new BigNumber(0.01)) const { makeOfferTxId } = await icxSetup.createDFIBuyOffer(createOrderTxId, new BigNumber(0.10), accountBTC) - await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 500) + await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 1440) const accountBTCBeforeEXTHTLC: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') // submit EXT HTLC with incorrect hash "INCORRECT_HASH" from DFCHTLC hash - taker @@ -384,7 +383,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { hash: 'INCORRECT_HASH', htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', ownerPubkey: '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - timeout: 15 + timeout: 24 } const promise = client.icxorderbook.submitExtHTLC(ExtHTLC) await expect(promise).rejects.toThrow(RpcApiError) @@ -405,7 +404,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { it('should return an error when submitting ExtHTLC with invalid ExtHTLC.ownerPubkey', async () => { const { createOrderTxId } = await icxSetup.createDFISellOrder('BTC', accountDFI, '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', new BigNumber(15), new BigNumber(0.01)) const { makeOfferTxId } = await icxSetup.createDFIBuyOffer(createOrderTxId, new BigNumber(0.10), accountBTC) - await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 500) + await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 1440) const accountBTCBeforeEXTHTLC: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') // submit EXT HTLC with incorrect ownerPubkey "INVALID_OWNER_PUB_KEY" - taker @@ -415,7 +414,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', ownerPubkey: 'INVALID_OWNER_PUB_KEY', - timeout: 15 + timeout: 24 } const promise = client.icxorderbook.submitExtHTLC(ExtHTLC) await expect(promise).rejects.toThrow(RpcApiError) @@ -436,10 +435,10 @@ describe('ICXOrderBook.submitExtHTLC', () => { it('should test submitting ExtHTLC with different values for ExtHTLC.timeout', async () => { const { createOrderTxId } = await icxSetup.createDFISellOrder('BTC', accountDFI, '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', new BigNumber(15), new BigNumber(0.01)) const { makeOfferTxId } = await icxSetup.createDFIBuyOffer(createOrderTxId, new BigNumber(0.10), accountBTC) - await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 500) + await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 1440) const accountBTCBeforeEXTHTLC: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') - // submit EXT HTLC with timeout < 14 - taker + // submit EXT HTLC with timeout < 24 - taker const ExtHTLC: ExtHTLC = { offerTx: makeOfferTxId, amount: new BigNumber(0.10), @@ -450,7 +449,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { } const promise = client.icxorderbook.submitExtHTLC(ExtHTLC) await expect(promise).rejects.toThrow(RpcApiError) - await expect(promise).rejects.toThrow('RpcApiError: \'Test ICXSubmitEXTHTLCTx execution failed:\ntimeout must be greater than 14\', code: -32600, method: icx_submitexthtlc') + await expect(promise).rejects.toThrow('RpcApiError: \'Test ICXSubmitEXTHTLCTx execution failed:\ntimeout must be greater than 23\', code: -32600, method: icx_submitexthtlc') // submit EXT HTLC with a ExtHTLC.timeout such that order->creationHeight + order->expiry < current height + (ExtHTLC.timeout * 16) - taker ExtHTLC.timeout = 400 @@ -519,7 +518,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', ownerPubkey: '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - timeout: 15 + timeout: 24 } const promise = client.icxorderbook.submitExtHTLC(ExtHTLC) await expect(promise).rejects.toThrow(RpcApiError) @@ -540,7 +539,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { it('should not submit ExtHTLC for a DFC buy offer with arbitary input utxos', async () => { const { createOrderTxId } = await icxSetup.createDFISellOrder('BTC', accountDFI, '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', new BigNumber(15), new BigNumber(0.01)) const { makeOfferTxId } = await icxSetup.createDFIBuyOffer(createOrderTxId, new BigNumber(0.10), accountBTC) - await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 500) + await icxSetup.createDFCHTLCForDFIBuyOffer(makeOfferTxId, new BigNumber(10), '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 1440) const accountBTCBeforeEXTHTLC: Record = await client.call('getaccount', [accountBTC, {}, true], 'bignumber') // submit EXT HTLC with incorrect ownerPubkey "INVALID_OWNER_PUB_KEY" - taker @@ -550,7 +549,7 @@ describe('ICXOrderBook.submitExtHTLC', () => { hash: '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', htlcScriptAddress: '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', ownerPubkey: '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - timeout: 15 + timeout: 24 } // input utxos const inputUTXOs = await container.fundAddress(await container.getNewAddress(), 10) diff --git a/packages/jellyfish-api-core/__tests__/category/mining/getMiningInfo.test.ts b/packages/jellyfish-api-core/__tests__/category/mining/getMiningInfo.test.ts index d94e7bcdfa..e5bb7bbbc1 100644 --- a/packages/jellyfish-api-core/__tests__/category/mining/getMiningInfo.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/mining/getMiningInfo.test.ts @@ -28,12 +28,13 @@ describe('Mining', () => { expect(info.difficulty).toBeDefined() expect(info.isoperator).toStrictEqual(true) - expect(mn1.masternodeid).toBeDefined() - expect(mn1.masternodeoperator).toBeDefined() - expect(mn1.masternodestate).toStrictEqual('ENABLED') + expect(mn1.id).toBeDefined() + expect(mn1.operator).toBeDefined() + expect(mn1.state).toStrictEqual('ENABLED') expect(mn1.generate).toStrictEqual(false) expect(mn1.mintedblocks).toStrictEqual(0) - expect(mn1.lastblockcreationattempt).toStrictEqual('0') + expect(typeof mn1.lastblockcreationattempt).toStrictEqual('string') + expect(typeof mn1.targetMultiplier).toStrictEqual('number') expect(info.networkhashps).toBeGreaterThan(0) expect(info.pooledtx).toStrictEqual(0) diff --git a/packages/jellyfish-api-core/__tests__/category/oracle/appointOracle.test.ts b/packages/jellyfish-api-core/__tests__/category/oracle/appointOracle.test.ts index be1060f809..d7eb133b76 100644 --- a/packages/jellyfish-api-core/__tests__/category/oracle/appointOracle.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/oracle/appointOracle.test.ts @@ -91,6 +91,58 @@ describe('Oracle', () => { ]) }) + it('should appointOracle if weightage is 0', async () => { + const priceFeeds = [ + { token: 'APPLE', currency: 'EUR' }, + { token: 'TESLA', currency: 'USD' } + ] + + const oracleid = await client.oracle.appointOracle(await container.getNewAddress(), priceFeeds, { weightage: 0 }) + + expect(typeof oracleid).toStrictEqual('string') + expect(oracleid.length).toStrictEqual(64) + + await container.generate(1) + }) + + it('should appointOracle if weightage is 255', async () => { + const priceFeeds = [ + { token: 'APPLE', currency: 'EUR' }, + { token: 'TESLA', currency: 'USD' } + ] + + const oracleid = await client.oracle.appointOracle(await container.getNewAddress(), priceFeeds, { weightage: 255 }) + + expect(typeof oracleid).toStrictEqual('string') + expect(oracleid.length).toStrictEqual(64) + + await container.generate(1) + }) + + it('should not appointOracle if weightage is -1', async () => { + const priceFeeds = [ + { token: 'APPLE', currency: 'EUR' }, + { token: 'TESLA', currency: 'USD' } + ] + + const promise = client.oracle.appointOracle(await container.getNewAddress(), priceFeeds, { weightage: -1 }) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow('RpcApiError: \'the weightage value is out of bounds\', code: -25, method: appointoracle') + }) + + it('should not appointOracle if weightage is 256', async () => { + const priceFeeds = [ + { token: 'APPLE', currency: 'EUR' }, + { token: 'TESLA', currency: 'USD' } + ] + + const promise = client.oracle.appointOracle(await container.getNewAddress(), priceFeeds, { weightage: 256 }) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow('RpcApiError: \'the weightage value is out of bounds\', code: -25, method: appointoracle') + }) + it('should appointOracle with utxos', async () => { const address = await container.getNewAddress() diff --git a/packages/jellyfish-api-core/__tests__/category/oracle/getPrice.test.ts b/packages/jellyfish-api-core/__tests__/category/oracle/getPrice.test.ts index 5bb44032f2..b25adb786e 100644 --- a/packages/jellyfish-api-core/__tests__/category/oracle/getPrice.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/oracle/getPrice.test.ts @@ -39,23 +39,6 @@ describe('Oracle', () => { expect(data.toString()).toStrictEqual(new BigNumber('0.83333333').toString()) }) - it('should not getPrice for price timestamps 4200 seconds after the current time', async () => { - const oracleid = await container.call('appointoracle', [await container.getNewAddress(), [{ token: 'TESLA', currency: 'USD' }], 1]) - - await container.generate(1) - - const timestamp = Math.floor(new Date().getTime() / 1000) + 4200 - const prices = [{ tokenAmount: '0.5@TESLA', currency: 'USD' }] - await container.call('setoracledata', [oracleid, timestamp, prices]) - - await container.generate(1) - - const promise = client.oracle.getPrice({ token: 'TESLA', currency: 'USD' }) - - await expect(promise).rejects.toThrow(RpcApiError) - await expect(promise).rejects.toThrow('RpcApiError: \'no live oracles for specified request\', code: -1, method: getprice') - }) - it('should not getPrice for price timestamps 4200 seconds before the current time', async () => { const oracleid = await container.call('appointoracle', [await container.getNewAddress(), [{ token: 'FB', currency: 'CNY' }], 1]) diff --git a/packages/jellyfish-api-core/__tests__/category/oracle/listLatestRawPrices.test.ts b/packages/jellyfish-api-core/__tests__/category/oracle/listLatestRawPrices.test.ts index e262c6a672..c58dcd94b5 100644 --- a/packages/jellyfish-api-core/__tests__/category/oracle/listLatestRawPrices.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/oracle/listLatestRawPrices.test.ts @@ -109,34 +109,6 @@ describe('Oracle', () => { ) }) - it('should listLatestRawPrices for timestamps 4200 seconds after the current time', async () => { - const oracleid = await container.call('appointoracle', [await container.getNewAddress(), [{ token: 'APPLE', currency: 'EUR' }], 1]) - - await container.generate(1) - - const timestamp = Math.floor(new Date().getTime() / 1000) + 4200 - const prices = [{ tokenAmount: '0.5@APPLE', currency: 'EUR' }] - await container.call('setoracledata', [oracleid, timestamp, prices]) - - await container.generate(1) - - // NOTE(jingyi2811): Pagination is not supported. - const data = await client.oracle.listLatestRawPrices() - - expect(data).toStrictEqual( - [ - { - priceFeeds: { token: 'APPLE', currency: 'EUR' }, - oracleid: oracleid, - weightage: new BigNumber(1), - timestamp: new BigNumber(timestamp), - rawprice: new BigNumber(0.5), - state: OracleRawPriceState.EXPIRED - } - ] - ) - }) - it('should listLatestRawPrices for timestamps 4200 seconds before the current time', async () => { const oracleid = await container.call('appointoracle', [await container.getNewAddress(), [{ token: 'APPLE', currency: 'EUR' }], 1]) diff --git a/packages/jellyfish-api-core/__tests__/category/oracle/listPrices.test.ts b/packages/jellyfish-api-core/__tests__/category/oracle/listPrices.test.ts index 5f49438a46..a2fbccfef7 100644 --- a/packages/jellyfish-api-core/__tests__/category/oracle/listPrices.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/oracle/listPrices.test.ts @@ -70,32 +70,6 @@ describe('Oracle', () => { expect(data.length).toStrictEqual(0) }) - it('should listPrices with error msg for price timestamps 4200 seconds after the current time', async () => { - const oracleid = await container.call('appointoracle', [await container.getNewAddress(), [{ - token: 'APPLE', - currency: 'EUR' - }], 1]) - - await container.generate(1) - - const timestamp = Math.floor(new Date().getTime() / 1000) + 4200 - const prices = [{ tokenAmount: '0.5@APPLE', currency: 'EUR' }] - await container.call('setoracledata', [oracleid, timestamp, prices]) - - await container.generate(1) - - const data = await client.oracle.listPrices() - expect(data).toStrictEqual( - [ - { - token: 'APPLE', - currency: 'EUR', - ok: 'no live oracles for specified request' - } - ] - ) - }) - it('should listPrices with error msg for price timestamps 4200 seconds before the current time', async () => { const oracleid = await container.call('appointoracle', [await container.getNewAddress(), [{ token: 'APPLE', diff --git a/packages/jellyfish-api-core/__tests__/category/oracle/setOracleData.test.ts b/packages/jellyfish-api-core/__tests__/category/oracle/setOracleData.test.ts index 8746b54d7b..e8800715aa 100644 --- a/packages/jellyfish-api-core/__tests__/category/oracle/setOracleData.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/oracle/setOracleData.test.ts @@ -25,7 +25,7 @@ describe('Oracle', () => { await container.generate(1) - const timestamp = new Date().getTime() + const timestamp = Math.floor(new Date().getTime() / 1000) const prices = [{ tokenAmount: '0.5@APPLE', currency: 'EUR' }] await client.oracle.setOracleData(oracleid, timestamp, { prices }) @@ -56,7 +56,7 @@ describe('Oracle', () => { const oracleid = 'e40775f8bb396cd3d94429843453e66e68b1c7625d99b0b4c505ab004506697b' const prices = [{ tokenAmount: '0.5@APPLE', currency: 'EUR' }] - const promise = client.oracle.setOracleData(oracleid, new Date().getTime(), { prices }) + const promise = client.oracle.setOracleData(oracleid, Math.floor(new Date().getTime() / 1000), { prices }) await expect(promise).rejects.toThrow(RpcApiError) await expect(promise).rejects.toThrow(`RpcApiError: 'oracle <${oracleid as string}> not found', code: -32600, method: setoracledata`) @@ -73,7 +73,7 @@ describe('Oracle', () => { const prices = [{ tokenAmount: '0.5@TESLA', currency: 'USD' }] - const promise = client.oracle.setOracleData(oracleid, new Date().getTime(), { prices }) + const promise = client.oracle.setOracleData(oracleid, Math.floor(new Date().getTime() / 1000), { prices }) await expect(promise).rejects.toThrow(RpcApiError) await expect(promise).rejects.toThrow('Test SetOracleDataTx execution failed:\ntoken - currency is not allowed\', code: -32600, method: setoracledata') @@ -90,7 +90,7 @@ describe('Oracle', () => { const prices = [{ tokenAmount: '1000000000000@APPLE', currency: 'EUR' }] - const promise = client.oracle.setOracleData(oracleid, new Date().getTime(), { prices }) + const promise = client.oracle.setOracleData(oracleid, Math.floor(new Date().getTime() / 1000), { prices }) await expect(promise).rejects.toThrow(RpcApiError) await expect(promise).rejects.toThrow('RpcApiError: \'Invalid amount\', code: -22, method: setoracledata') @@ -107,7 +107,7 @@ describe('Oracle', () => { await container.generate(1) - const timestamp = new Date().getTime() + const timestamp = Math.floor(new Date().getTime() / 1000) const prices = [{ tokenAmount: '0.5@APPLE', currency: 'EUR' }] const input = await container.fundAddress(address, 10) @@ -145,7 +145,7 @@ describe('Oracle', () => { await container.generate(1) - const timestamp = new Date().getTime() + const timestamp = Math.floor(new Date().getTime() / 1000) const prices = [{ tokenAmount: '0.5@APPLE', currency: 'EUR' }] const { txid, vout } = await container.fundAddress(await container.getNewAddress(), 10) @@ -154,4 +154,20 @@ describe('Oracle', () => { await expect(promise).rejects.toThrow(RpcApiError) await expect(promise).rejects.toThrow('RpcApiError: \'Test SetOracleDataTx execution failed:\ntx must have at least one input from account owner\', code: -32600, method: setoracledata') }) + + it('should return an error when setoracledata timestamp is greater than 5 minutes into the future', async () => { + const priceFeeds = [ + { token: 'APPLE', currency: 'EUR' } + ] + + const oracleid = await container.call('appointoracle', [await container.getNewAddress(), priceFeeds, 1]) + await container.generate(1) + + const timestamp = Math.floor(new Date().getTime() / 1000) + 4200 + const prices = [{ tokenAmount: '0.5@APPLE', currency: 'EUR' }] + + const promise = client.oracle.setOracleData(oracleid, timestamp, { prices }) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow('RpcApiError: \'timestamp cannot be negative, zero or over 5 minutes in the future\', code: -8') + }) }) diff --git a/packages/jellyfish-api-core/__tests__/category/oracle/updateOracle.test.ts b/packages/jellyfish-api-core/__tests__/category/oracle/updateOracle.test.ts index 543aeb8980..1c203d760c 100644 --- a/packages/jellyfish-api-core/__tests__/category/oracle/updateOracle.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/oracle/updateOracle.test.ts @@ -155,6 +155,114 @@ describe('Oracle', () => { ]) }) + it('should updateOracle if weightage is 0', async () => { + // Appoint oracle + const appointOraclePriceFeeds = [ + { token: 'APPLE', currency: 'EUR' }, + { token: 'TESLA', currency: 'USD' } + ] + + let oracleid = await container.call('appointoracle', [await container.getNewAddress(), appointOraclePriceFeeds, 1]) + + await container.generate(1) + + // Update oracle + const updateOraclePriceFeeds = [ + { token: 'FB', currency: 'CNY' }, + { token: 'MSFT', currency: 'SGD' } + ] + + oracleid = await client.oracle.updateOracle(oracleid, await container.getNewAddress(), { + priceFeeds: updateOraclePriceFeeds, + weightage: 0 + }) + + await container.generate(1) + + expect(typeof oracleid).toStrictEqual('string') + expect(oracleid.length).toStrictEqual(64) + }) + + it('should updateOracle if weightage is 255', async () => { + // Appoint oracle + const appointOraclePriceFeeds = [ + { token: 'APPLE', currency: 'EUR' }, + { token: 'TESLA', currency: 'USD' } + ] + + let oracleid = await container.call('appointoracle', [await container.getNewAddress(), appointOraclePriceFeeds, 1]) + + await container.generate(1) + + // Update oracle + const updateOraclePriceFeeds = [ + { token: 'FB', currency: 'CNY' }, + { token: 'MSFT', currency: 'SGD' } + ] + + oracleid = await client.oracle.updateOracle(oracleid, await container.getNewAddress(), { + priceFeeds: updateOraclePriceFeeds, + weightage: 255 + }) + + await container.generate(1) + + expect(typeof oracleid).toStrictEqual('string') + expect(oracleid.length).toStrictEqual(64) + }) + + it('should not updateOracle if weightage is -1', async () => { + // Appoint oracle + const appointOraclePriceFeeds = [ + { token: 'APPLE', currency: 'EUR' }, + { token: 'TESLA', currency: 'USD' } + ] + + const oracleid = await container.call('appointoracle', [await container.getNewAddress(), appointOraclePriceFeeds, 1]) + + await container.generate(1) + + // Update oracle + const updateOraclePriceFeeds = [ + { token: 'FB', currency: 'CNY' }, + { token: 'MSFT', currency: 'SGD' } + ] + + const promise = client.oracle.updateOracle(oracleid, await container.getNewAddress(), { + priceFeeds: updateOraclePriceFeeds, + weightage: -1 + }) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow('RpcApiError: \'the weightage value is out of bounds\', code: -25, method: updateoracle') + }) + + it('should not updateOracle if weightage is 256', async () => { + // Appoint oracle + const appointOraclePriceFeeds = [ + { token: 'APPLE', currency: 'EUR' }, + { token: 'TESLA', currency: 'USD' } + ] + + const oracleid = await container.call('appointoracle', [await container.getNewAddress(), appointOraclePriceFeeds, 1]) + + await container.generate(1) + + // Update oracle + const updateOraclePriceFeeds = [ + { token: 'FB', currency: 'CNY' }, + { token: 'MSFT', currency: 'SGD' } + ] + + const promise = client.oracle.updateOracle(oracleid, await container.getNewAddress(), { + priceFeeds: updateOraclePriceFeeds, + weightage: 256 + }) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow('RpcApiError: \'the weightage value is out of bounds\', code: -25, method: updateoracle') + }) + it('should not updateOracle with arbitrary utxos', async () => { // Appoint oracle const appointOraclePriceFeeds = [ diff --git a/packages/jellyfish-api-core/__tests__/category/spv/claimHtlc.test.ts b/packages/jellyfish-api-core/__tests__/category/spv/claimHtlc.test.ts new file mode 100644 index 0000000000..7cd210c14e --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/spv/claimHtlc.test.ts @@ -0,0 +1,135 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { RpcApiError } from '@defichain/jellyfish-api-core' +import { ContainerAdapterClient } from '../../container_adapter_client' + +describe('Spv', () => { + const container = new MasterNodeRegTestContainer() + const client = new ContainerAdapterClient(container) + + beforeAll(async () => { + await container.start() + await container.waitForReady() + + await container.call('spv_fundaddress', [await container.call('spv_getnewaddress')]) // Funds 1 BTC + }) + + afterAll(async () => { + await container.stop() + }) + + it('should claimHtlc', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const htlc = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10']) + + await container.call('spv_sendtoaddress', [htlc.address, 0.1]) // Funds HTLC address + const claimedHtlc = await client.spv.claimHtlc( + htlc.address, + await container.call('spv_getnewaddress'), + { seed: htlc.seed } + ) + expect(typeof claimedHtlc.txid).toStrictEqual('string') + expect(claimedHtlc.sendmessage).toStrictEqual('Success') + }) + + it('should not claimHtlc when no unspent HTLC outputs found', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const htlc = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10']) + + const promise = client.spv.claimHtlc( + htlc.address, + await container.call('spv_getnewaddress'), + { seed: htlc.seed } + ) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'No unspent HTLC outputs found', code: -4, method: spv_claimhtlc") + }) + + it('should not claimHtlc when provided seed is not in hex form', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const htlc = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10']) + + const promise = client.spv.claimHtlc( + htlc.address, + await container.call('spv_getnewaddress'), + { seed: 'XXXX' } + ) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Provided seed is not in hex form', code: -5, method: spv_claimhtlc") + }) + + it('should not claimHtlc when seed provided does not match seed hash in contract', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const htlc = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10']) + + const promise = client.spv.claimHtlc( + htlc.address, + await container.call('spv_getnewaddress'), + { seed: '00' } + ) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Seed provided does not match seed hash in contract', code: -5, method: spv_claimhtlc") + }) + + it('should not claimHtlc with invalid address', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const htlc = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10']) + + const promise = client.spv.claimHtlc( + 'XXXX', + await container.call('spv_getnewaddress'), + { seed: htlc.seed } + ) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Invalid address', code: -5, method: spv_claimhtlc") + }) + + it('should not claimHtlc when redeem script not found in wallet', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const htlc = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10']) + const randomAddress = '2Mu4edSkC5gKVwYayfDq2fTFwT6YD4mujSX' + + const promise = client.spv.claimHtlc( + randomAddress, + await container.call('spv_getnewaddress'), + { seed: htlc.seed } + ) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Redeem script not found in wallet', code: -4, method: spv_claimhtlc") + }) + + it('should not claimHtlc with invalid destination address', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const htlc = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10']) + + const promise = client.spv.claimHtlc( + htlc.address, + 'XXXX', + { seed: htlc.seed } + ) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Invalid destination address', code: -5, method: spv_claimhtlc") + }) + + it('should not claimHtlc with not enough funds to cover fee', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const htlc = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10']) + + await container.call('spv_sendtoaddress', [htlc.address, 0.00000546]) // Funds HTLC address with dust + + const promise = client.spv.claimHtlc( + htlc.address, + await container.call('spv_getnewaddress'), + { seed: htlc.seed } + ) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Not enough funds to cover fee', code: -1, method: spv_claimhtlc") + }) +}) diff --git a/packages/jellyfish-api-core/__tests__/category/spv/createHtlc.test.ts b/packages/jellyfish-api-core/__tests__/category/spv/createHtlc.test.ts new file mode 100644 index 0000000000..237dcb4196 --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/spv/createHtlc.test.ts @@ -0,0 +1,91 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { RpcApiError } from '@defichain/jellyfish-api-core' +import { ContainerAdapterClient } from '../../container_adapter_client' +import { HTLC_MINIMUM_BLOCK_COUNT } from '../../../src/category/spv' + +describe('Spv', () => { + const container = new MasterNodeRegTestContainer() + const client = new ContainerAdapterClient(container) + + beforeAll(async () => { + await container.start() + await container.waitForReady() + }) + + afterAll(async () => { + await container.stop() + }) + + it('should createHtlc', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const timeout = 10 + + const htlc = await client.spv.createHtlc(pubKeyA, pubKeyB, { timeout: `${timeout}` }) + expect(typeof htlc.address).toStrictEqual('string') + expect(htlc.address.length).toStrictEqual(35) + expect(typeof htlc.redeemScript).toStrictEqual('string') + expect(typeof htlc.seed).toStrictEqual('string') + expect(htlc.seed.length).toStrictEqual(64) + expect(typeof htlc.seedhash).toStrictEqual('string') + expect(htlc.seedhash.length).toStrictEqual(64) + + const decodedScript = await container.call('spv_decodehtlcscript', [htlc.redeemScript]) + expect(decodedScript.sellerkey).toStrictEqual(pubKeyA) + expect(decodedScript.buyerkey).toStrictEqual(pubKeyB) + expect(decodedScript.blocks).toStrictEqual(timeout) + expect(decodedScript.hash).toStrictEqual(htlc.seedhash) + }) + + it('should not createHtlc with invalid public key as receiverPubKey', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + + const promise = client.spv.createHtlc('XXXX', pubKeyA, { timeout: '10' }) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Invalid public key: XXXX', code: -5, method: spv_createhtlc") + }) + + it('should not createHtlc with with invalid public key as ownerPubKey', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + + const promise = client.spv.createHtlc(pubKeyA, 'XXXX', { timeout: '10' }) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Invalid public key: XXXX', code: -5, method: spv_createhtlc") + }) + + it('should not createHtlc with timeout < HTLC_MINIMUM_BLOCK_COUNT', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + + const promise = client.spv.createHtlc(pubKeyA, pubKeyB, { timeout: `${HTLC_MINIMUM_BLOCK_COUNT - 1}` }) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Timeout below minimum of 9', code: -3, method: spv_createhtlc") + }) + + it('should not createHtlc with denominated relative timeout', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + + const promise = client.spv.createHtlc(pubKeyA, pubKeyB, { timeout: '4194304' }) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Invalid block denominated relative timeout', code: -3, method: spv_createhtlc") + }) + + it('should not createHtlc with seed length != 32', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + + const promise = client.spv.createHtlc(pubKeyA, pubKeyB, { timeout: '10', seed: '00' }) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Invalid hash image length, 32 (SHA256) accepted', code: -3, method: spv_createhtlc") + }) + + it('should not createHtlc with invalid seed', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + + const promise = client.spv.createHtlc(pubKeyA, pubKeyB, { timeout: '8', seed: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' }) // 32 char seed with non-hex char + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Invalid hash image', code: -3, method: spv_createhtlc") + }) +}) diff --git a/packages/jellyfish-api-core/__tests__/category/spv/decodeHtlcScript.test.ts b/packages/jellyfish-api-core/__tests__/category/spv/decodeHtlcScript.test.ts new file mode 100644 index 0000000000..99d7dc0559 --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/spv/decodeHtlcScript.test.ts @@ -0,0 +1,121 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { RpcApiError } from '@defichain/jellyfish-api-core' +import { ContainerAdapterClient } from '../../container_adapter_client' + +describe('Spv', () => { + const container = new MasterNodeRegTestContainer() + const client = new ContainerAdapterClient(container) + + beforeAll(async () => { + await container.start() + await container.waitForReady() + }) + + afterAll(async () => { + await container.stop() + }) + + /** Util function to replace a char at index in string */ + const replaceAt = (s: string, index: number, replacement: string): string => s.substr(0, index) + replacement + s.substr(index + replacement.length) + + it('should createHtlc', async () => { + const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')]) + const blocks = 10 + const htlc = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, `${blocks}`]) + + const decodedScript = await client.spv.decodeHtlcScript(htlc.redeemScript) + expect(decodedScript.sellerkey).toStrictEqual(pubKeyA) + expect(decodedScript.buyerkey).toStrictEqual(pubKeyB) + expect(decodedScript.blocks).toStrictEqual(blocks) + expect(decodedScript.hash).toStrictEqual(htlc.seedhash) + }) + + it('should not decodeHtlcScript with redeemscript not as hex format', async () => { + const promise = client.spv.decodeHtlcScript('XXXX') + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Redeemscript expected in hex format', code: -3, method: spv_decodehtlcscript") + }) + + it('should not decodeHtlcScript with redeemscript with incorrect length', async () => { + const promise = client.spv.decodeHtlcScript('00') + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Incorrect redeemscript length', code: -5, method: spv_decodehtlcscript") + }) + + it('should not decodeHtlcScript with redeemscript with invalid seller public key', async () => { + const redeemscript = '63a82001e7c5853b6d96346eb917445f49132f56bfb2dec56aafbf6a2a310b0d0b6e9f8821024adf8f420049083e5a28d88ea40d8441cf55d1a5aafce3ae5812fe1668548c92675ab2752103088a9944e39df3196e40d3edd4ea1f291eb3103d81ddf98c16bf060bc121ad8368ac' + const sellerPublicKey = '024adf8f420049083e5a28d88ea40d8441cf55d1a5aafce3ae5812fe1668548c92' + const tamperedRedeemScript = redeemscript.replace(sellerPublicKey, '000000000000000000000000000000000000000000000000000000000000000000') // '0' * 66 (sellerPublicKey length) + const promise = client.spv.decodeHtlcScript(tamperedRedeemScript) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Invalid seller pubkey', code: -5, method: spv_decodehtlcscript") + }) + + it('should not decodeHtlcScript with redeemscript with invalid buyer public key', async () => { + const redeemscript = '63a82001e7c5853b6d96346eb917445f49132f56bfb2dec56aafbf6a2a310b0d0b6e9f8821024adf8f420049083e5a28d88ea40d8441cf55d1a5aafce3ae5812fe1668548c92675ab2752103088a9944e39df3196e40d3edd4ea1f291eb3103d81ddf98c16bf060bc121ad8368ac' + const buyerPublicKey = '03088a9944e39df3196e40d3edd4ea1f291eb3103d81ddf98c16bf060bc121ad83' + const tamperedRedeemScript = redeemscript.replace(buyerPublicKey, '000000000000000000000000000000000000000000000000000000000000000000') // '0' * 66 (buyerPublicKey length) + const promise = client.spv.decodeHtlcScript(tamperedRedeemScript) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Invalid buyer pubkey', code: -5, method: spv_decodehtlcscript") + }) + + it('should not decodeHtlcScript with redeemscript with invalid seller public key length', async () => { + const redeemscript = '63a82001e7c5853b6d96346eb917445f49132f56bfb2dec56aafbf6a2a310b0d0b6e9f8821024adf8f420049083e5a28d88ea40d8441cf55d1a5aafce3ae5812fe1668548c92675ab2752103088a9944e39df3196e40d3edd4ea1f291eb3103d81ddf98c16bf060bc121ad8368ac' + /** + * Tamper seller public key length. + * Should be 0x41 or 0x21 as seen in `ain` repository src/pubkey.h + * PUBLIC_KEY_SIZE=65 + * COMPRESSED_PUBLIC_KEY_SIZE=33 + */ + const tamperedRedeemScript = replaceAt(redeemscript, 72, '9') // Temper seller public key length to 0x91 + const promise = client.spv.decodeHtlcScript(tamperedRedeemScript) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Seller pubkey incorrect pubkey length', code: -5, method: spv_decodehtlcscript") + }) + + it('should not decodeHtlcScript with redeemscript with invalid buyer public key length', async () => { + const redeemscript = '63a82001e7c5853b6d96346eb917445f49132f56bfb2dec56aafbf6a2a310b0d0b6e9f8821024adf8f420049083e5a28d88ea40d8441cf55d1a5aafce3ae5812fe1668548c92675ab2752103088a9944e39df3196e40d3edd4ea1f291eb3103d81ddf98c16bf060bc121ad8368ac' + /** + * Tamper buyer public key length. + * Should be 0x41 or 0x21 as seen in `ain` repository src/pubkey.h + * PUBLIC_KEY_SIZE=65 + * COMPRESSED_PUBLIC_KEY_SIZE=33 + */ + const tamperedRedeemScript = replaceAt(redeemscript, 148, '9') // Temper buyer public key length to 0x91 + const promise = client.spv.decodeHtlcScript(tamperedRedeemScript) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Buyer pubkey incorrect pubkey length', code: -5, method: spv_decodehtlcscript") + }) + + it('should not decodeHtlcScript with redeemscript with incorrect timeout length', async () => { + const redeemscript = '63a82001e7c5853b6d96346eb917445f49132f56bfb2dec56aafbf6a2a310b0d0b6e9f8821024adf8f420049083e5a28d88ea40d8441cf55d1a5aafce3ae5812fe1668548c92675ab2752103088a9944e39df3196e40d3edd4ea1f291eb3103d81ddf98c16bf060bc121ad8368ac' + /** + * Tamper timeout length. + * Should >= OP_1 as seen in `ain` repository src/spv/spv_wrapper.cpp:977 + */ + const tamperedRedeemScript = replaceAt(redeemscript, 142, '1') // Temper timeout length to 0x1a + const promise = client.spv.decodeHtlcScript(tamperedRedeemScript) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Incorrect timeout length', code: -5, method: spv_decodehtlcscript") + }) + + it('should not decodeHtlcScript with redeemscript with incorrect seed hash length', async () => { + const redeemscript = '63a82001e7c5853b6d96346eb917445f49132f56bfb2dec56aafbf6a2a310b0d0b6e9f8821024adf8f420049083e5a28d88ea40d8441cf55d1a5aafce3ae5812fe1668548c92675ab2752103088a9944e39df3196e40d3edd4ea1f291eb3103d81ddf98c16bf060bc121ad8368ac' + /** + * Tamper seed hash length. + * Should be 0x20 as seen in `ain` repository src/spv/spv_wrapper.cpp:949 + */ + const tamperedRedeemScript = replaceAt(redeemscript, 4, '9') // Tamper seed hash length to 0x90 + const promise = client.spv.decodeHtlcScript(tamperedRedeemScript) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Incorrect seed hash length', code: -5, method: spv_decodehtlcscript") + }) +}) diff --git a/packages/jellyfish-api-core/__tests__/category/spv/listReceivedByAddress.test.ts b/packages/jellyfish-api-core/__tests__/category/spv/listReceivedByAddress.test.ts index 2383ff9085..4c50856600 100644 --- a/packages/jellyfish-api-core/__tests__/category/spv/listReceivedByAddress.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/spv/listReceivedByAddress.test.ts @@ -1,6 +1,7 @@ import { MasterNodeRegTestContainer } from '@defichain/testcontainers' import { RpcApiError } from '@defichain/jellyfish-api-core' import { ContainerAdapterClient } from '../../container_adapter_client' +import BigNumber from 'bignumber.js' describe('Spv', () => { const container = new MasterNodeRegTestContainer() @@ -23,7 +24,8 @@ describe('Spv', () => { expect(listReceivedByAddress.length).toStrictEqual(1) expect(listReceivedByAddress[0].address).toStrictEqual(address) expect(listReceivedByAddress[0].type).toStrictEqual('Bech32') - expect(listReceivedByAddress[0].amount).toStrictEqual(1) + expect(listReceivedByAddress[0].amount instanceof BigNumber).toStrictEqual(true) + expect(listReceivedByAddress[0].amount).toStrictEqual(new BigNumber(1)) expect(listReceivedByAddress[0].confirmations).toStrictEqual(1) expect(listReceivedByAddress[0].txids[0]).toStrictEqual(txid) }) diff --git a/packages/jellyfish-api-core/__tests__/category/spv/sendToAddress.test.ts b/packages/jellyfish-api-core/__tests__/category/spv/sendToAddress.test.ts new file mode 100644 index 0000000000..f82d6906fc --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/spv/sendToAddress.test.ts @@ -0,0 +1,117 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { RpcApiError } from '@defichain/jellyfish-api-core' +import { ContainerAdapterClient } from '../../container_adapter_client' +import BigNumber from 'bignumber.js' + +describe('Spv', () => { + const container = new MasterNodeRegTestContainer() + const client = new ContainerAdapterClient(container) + + beforeAll(async () => { + await container.start() + await container.waitForReady() + + const address = await container.call('spv_getnewaddress') + await container.call('spv_fundaddress', [address]) // Funds 1 BTC + }) + + afterAll(async () => { + await container.stop() + }) + + it('should sendToAddress', async () => { + const otherAddress = 'bcrt1qfpnmx6jrn30yvscrw9spudj5aphyrc8es6epva' + const amount = new BigNumber(0.1) + + const result = await client.spv.sendToAddress(otherAddress, amount) + expect(typeof result.txid).toStrictEqual('string') + expect(result.sendmessage).toStrictEqual('Success') + + const balance = new BigNumber(await container.call('spv_getbalance')) + const expectedBalance = new BigNumber(1).minus(amount) + expect(balance.lte(expectedBalance)).toStrictEqual(true) + }) + + it('should not sendToAddress for invalid address', async () => { + const promise = client.spv.sendToAddress('XXXX', new BigNumber(1)) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Invalid address', code: -5, method: spv_sendtoaddress") + }) + + it('should not sendToAddress with amount < 0', async () => { + const promise = client.spv.sendToAddress(await container.call('spv_getnewaddress'), new BigNumber(-1)) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Amount out of range', code: -3, method: spv_sendtoaddress") + }) + + it('should not sendToAddress with amount > 1200000000', async () => { + const promise = client.spv.sendToAddress(await container.call('spv_getnewaddress'), new BigNumber(1_200_000_000.1)) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Amount out of range', code: -3, method: spv_sendtoaddress") + }) +}) + +describe('Spv with custom feeRate', () => { + const container = new MasterNodeRegTestContainer() + const client = new ContainerAdapterClient(container) + + beforeAll(async () => { + await container.start() + await container.waitForReady() + + const address = await container.call('spv_getnewaddress') + await container.call('spv_fundaddress', [address]) // Funds 1 BTC + }) + + afterAll(async () => { + await container.stop() + }) + + it('should sendToAddress with custom feeRate', async () => { + const otherAddress = 'bcrt1qfpnmx6jrn30yvscrw9spudj5aphyrc8es6epva' + const amount = new BigNumber(0.1) + const feeRate = new BigNumber(1000) + + const result = await client.spv.sendToAddress(otherAddress, amount, { feeRate }) + expect(typeof result.txid).toStrictEqual('string') + expect(result.sendmessage).toStrictEqual('Success') + + const balance = new BigNumber(await container.call('spv_getbalance')) + const expectedBalance = new BigNumber(1).minus(amount) + expect(balance.lte(expectedBalance)).toStrictEqual(true) + }) + + it('should not sendToAddress with feeRate below minimum acceptable amount', async () => { + const promise = client.spv.sendToAddress(await container.call('spv_getnewaddress'), new BigNumber(0.1), { feeRate: new BigNumber(999) }) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Fee size below minimum acceptable amount', code: -8, method: spv_sendtoaddress") + }) +}) + +describe('Spv with insufficient funds', () => { + const container = new MasterNodeRegTestContainer() + const client = new ContainerAdapterClient(container) + + beforeAll(async () => { + await container.start() + await container.waitForReady() + }) + + afterAll(async () => { + await container.stop() + }) + + it('should not sendToAddress with insufficient funds', async () => { + const promise = client.spv.sendToAddress(await container.call('spv_getnewaddress'), new BigNumber(1)) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow('RpcApiError: \'Insufficient funds\', code: -6, method: spv_sendtoaddress') + }) + + it('should not sendToAddress with insufficient funds to cover fee', async () => { + await container.call('spv_fundaddress', [await container.call('spv_getnewaddress')]) // Funds 1 BTC + + const promise = client.spv.sendToAddress(await container.call('spv_getnewaddress'), new BigNumber(1)) // Sends 1 BTC + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow('RpcApiError: \'Insufficient funds to cover fee\', code: -6, method: spv_sendtoaddress') + }) +}) diff --git a/packages/jellyfish-api-core/src/category/governance.ts b/packages/jellyfish-api-core/src/category/governance.ts index 7897a29eec..f71bd093de 100644 --- a/packages/jellyfish-api-core/src/category/governance.ts +++ b/packages/jellyfish-api-core/src/category/governance.ts @@ -27,6 +27,12 @@ export enum ListProposalsStatus { ALL = 'all' } +export enum VoteDecision { + YES = 'yes', + NO = 'no', + NEUTRAL = 'neutral' +} + /** * Governance RPCs for DeFi Blockchain */ @@ -94,6 +100,22 @@ export class Governance { } = {}): Promise { return await this.client.call('listproposals', [type, status], 'number') } + + /** + * Vote on a community proposal. + * + * @param {VoteData} data Vote data + * @param {string} data.proposalId Proposal id + * @param {number} data.masternodeId Masternode id + * @param {VoteDecision} data.decision Vote decision. See VoteDecision. + * @param {UTXO[]} [utxos = []] Specific utxos to spend + * @param {string} [utxos.txid] The transaction id + * @param {string} [utxos.vout] The output number + * @return {Promise} txid + */ + async vote (data: VoteData, utxos: UTXO[] = []): Promise { + return await this.client.call('vote', [data.proposalId, data.masternodeId, data.decision, utxos], 'number') + } } export interface CFPData { @@ -122,3 +144,9 @@ export interface ProposalInfo { finalizeAfter: number payoutAddress: string } + +export interface VoteData { + proposalId: string + masternodeId: string + decision: VoteDecision +} diff --git a/packages/jellyfish-api-core/src/category/mining.ts b/packages/jellyfish-api-core/src/category/mining.ts index 0adcd66ba5..bfc1d79ce8 100644 --- a/packages/jellyfish-api-core/src/category/mining.ts +++ b/packages/jellyfish-api-core/src/category/mining.ts @@ -86,12 +86,13 @@ export interface MiningInfo { * Masternode related information */ export interface MasternodeInfo { - masternodeid?: string - masternodeoperator?: string - masternodestate?: 'PRE_ENABLED' | 'ENABLED' | 'PRE_RESIGNED' | 'RESIGNED' | 'PRE_BANNED' | 'BANNED' - generate?: boolean - mintedblocks?: number - lastblockcreationattempt?: string + id: string + operator: string + state: 'PRE_ENABLED' | 'ENABLED' | 'PRE_RESIGNED' | 'RESIGNED' | 'PRE_BANNED' | 'BANNED' + generate: boolean + mintedblocks: number + lastblockcreationattempt: string + targetMultiplier: number } export interface SmartFeeEstimation { diff --git a/packages/jellyfish-api-core/src/category/oracle.ts b/packages/jellyfish-api-core/src/category/oracle.ts index 885266a688..4ca3aeb52a 100644 --- a/packages/jellyfish-api-core/src/category/oracle.ts +++ b/packages/jellyfish-api-core/src/category/oracle.ts @@ -68,7 +68,7 @@ export class Oracle { * Set oracle data transaction. * * @param {string} oracleid - * @param {number} timestamp + * @param {number} timestamp timestamp in seconds * @param {SetOracleDataOptions} [options] * @param {OraclePrice[]} options.prices * @param {UTXO[]} [options.utxos = []] diff --git a/packages/jellyfish-api-core/src/category/spv.ts b/packages/jellyfish-api-core/src/category/spv.ts index cd2d547ae7..df3128d49c 100644 --- a/packages/jellyfish-api-core/src/category/spv.ts +++ b/packages/jellyfish-api-core/src/category/spv.ts @@ -1,4 +1,7 @@ import { ApiClient } from '../.' +import BigNumber from 'bignumber.js' + +export const HTLC_MINIMUM_BLOCK_COUNT = 9 /** * SPV RPCs for DeFi Blockchain @@ -37,7 +40,60 @@ export class Spv { * @return {Promise} */ async listReceivedByAddress (minConfirmation: number = 1, address?: string): Promise { - return await this.client.call('spv_listreceivedbyaddress', [minConfirmation, address], 'number') + return await this.client.call('spv_listreceivedbyaddress', [minConfirmation, address], { + amount: 'bignumber' + }) + } + + /** + * Send a Bitcoin amount to a given address. + * + * @param {string} address Bitcoin address + * @param {BigNumber} amount Bitcoin amount + * @param {SendToAddressOptions} [options] + * @param {BigNumber} [options.feeRate=10000] Fee rate in satoshis per KB. Minimum is 1000. + * @return {Promise} + */ + async sendToAddress (address: string, amount: BigNumber, options = { feeRate: new BigNumber('10000') }): Promise { + return await this.client.call('spv_sendtoaddress', [address, amount, options.feeRate], 'bignumber') + } + + /** + * Creates a Bitcoin address whose funds can be unlocked with a seed or as a refund. + * + * @param {string} receiverPubKey The public key of the possessor of the seed + * @param {number} ownerPubKey The public key of the recipient of the refund + * @param {CreateHtlcOptions} options + * @param {string} options.timeout Timeout of the contract (denominated in blocks) relative to its placement in the blockchain. Minimum 9. See HTLC_MINIMUM_BLOCK_COUNT + * @param {string} [options.seed] SHA256 hash of the seed. If none provided one will be generated + * @return {Promise} + */ + async createHtlc (receiverPubKey: string, ownerPubKey: string, options: CreateHtlcOptions): Promise { + return await this.client.call('spv_createhtlc', [receiverPubKey, ownerPubKey, options.timeout, options.seed], 'number') + } + + /** + * Decode and return value in a HTLC redeemscript. + * + * @param {string} redeemScript HTLC redeem script + * @return {Promise} + */ + async decodeHtlcScript (redeemScript: string): Promise { + return await this.client.call('spv_decodehtlcscript', [redeemScript], 'number') + } + + /** + * Claims all coins in HTLC address. + * + * @param {string} scriptAddress HTLC address + * @param {string} destinationAddress Destination address to send HTLC funds to + * @param {ClaimHtlcOptions} options + * @param {string} options.seed HTLC seed + * @param {BigNumber} [options.feeRate=10000] Fee rate in satoshis per KB. Minimum is 1000. + * @return {Promise} + */ + async claimHtlc (scriptAddress: string, destinationAddress: string, options: ClaimHtlcOptions): Promise { + return await this.client.call('spv_claimhtlc', [scriptAddress, destinationAddress, options.seed, options.feeRate], 'bignumber') } } @@ -47,9 +103,54 @@ export interface ReceivedByAddressInfo { /** Address type */ type: string /** Total amount of BTC recieved by the address */ - amount: number + amount: BigNumber /** The number of confirmations */ confirmations: number /** The ids of transactions received by the address */ txids: string[] } + +export interface SendToAddressOptions { + feeRate?: BigNumber +} + +export interface SendMessageResult { + txid: string + sendmessage: string +} + +export interface CreateHtlcOptions { + /** Timeout of the contract (denominated in blocks) relative to its placement in the blockchain. Minimum 9. See HTLC_MINIMUM_BLOCK_COUNT */ + timeout: string + /** SHA256 hash of the seed. If none provided one will be generated */ + seed?: string +} + +export interface CreateHtlcResult { + /** The value of the new Bitcoin address */ + address: string + /** Hex-encoded redemption script */ + redeemScript: string + /** Hex-encoded seed */ + seed: string + /** Hex-encoded seed hash */ + seedhash: string +} + +export interface DecodeHtlcResult { + /** seller's public key */ + sellerkey: string + /** buyer's public key */ + buyerkey: string + /** Timeout of the contract (denominated in blocks) relative to its placement in the blockchain at creation time */ + blocks: number + /** Hex-encoded seed hash */ + hash: string +} + +export interface ClaimHtlcOptions { + /** HTLC seed */ + seed: string + /** Fee rate in satoshis per KB */ + feeRate?: BigNumber +} diff --git a/packages/testcontainers/src/chains/defid_container.ts b/packages/testcontainers/src/chains/defid_container.ts index b080d764fd..367fc56ac2 100644 --- a/packages/testcontainers/src/chains/defid_container.ts +++ b/packages/testcontainers/src/chains/defid_container.ts @@ -27,7 +27,7 @@ export abstract class DeFiDContainer extends DockerContainer { if (process?.env?.DEFICHAIN_DOCKER_IMAGE !== undefined) { return process.env.DEFICHAIN_DOCKER_IMAGE } - return 'defi/defichain:1.7.11' + return 'defi/defichain:1.8.0' } public static readonly DefaultStartOptions = { diff --git a/packages/testcontainers/src/chains/reg_test_container/index.ts b/packages/testcontainers/src/chains/reg_test_container/index.ts index a29147b291..71c75893b9 100644 --- a/packages/testcontainers/src/chains/reg_test_container/index.ts +++ b/packages/testcontainers/src/chains/reg_test_container/index.ts @@ -30,7 +30,8 @@ export class RegTestContainer extends DeFiDContainer { '-clarkequayheight=3', '-dakotaheight=4', '-dakotacrescentheight=5', - '-eunosheight=6' + '-eunosheight=6', + '-eunospayaheight=7' ] } diff --git a/website/docs/jellyfish/api/governance.md b/website/docs/jellyfish/api/governance.md index 7fa3a459c2..0452f10c66 100644 --- a/website/docs/jellyfish/api/governance.md +++ b/website/docs/jellyfish/api/governance.md @@ -134,3 +134,30 @@ interface ProposalInfo { payoutAddress: string } ``` + +## vote + +Vote on a community proposal. + +```ts title="client.governance.vote()" +interface governance { + async vote (data: VoteData, utxos: UTXO[] = []): Promise +} + +enum VoteDecision { + YES = 'yes', + NO = 'no', + NEUTRAL = 'neutral' +} + +interface VoteData { + proposalId: string + masternodeId: string + decision: VoteDecision +} + +interface UTXO { + txid: string + vout: number +} +``` diff --git a/website/docs/jellyfish/api/spv.md b/website/docs/jellyfish/api/spv.md index 351358e267..96a3ed267c 100644 --- a/website/docs/jellyfish/api/spv.md +++ b/website/docs/jellyfish/api/spv.md @@ -45,8 +45,86 @@ interface spv { interface ReceivedByAddressInfo { address: string type: string - amount: number + amount: BigNumber confirmations: number txids: string[] } ``` + +## sendToAddress + +Send a Bitcoin amount to a given address. + +```ts title="client.spv.sendToAddress()" +interface spv { + sendToAddress (address: string, amount: BigNumber, options: SendToAddressOptions = { feerate: 10000 }): Promise +} + +interface SendToAddressOptions { + feerate?: BigNumber +} + +interface SendMessageResult { + txid: string + sendmessage: string +} +``` + +## createHtlc + +Creates a Bitcoin address whose funds can be unlocked with a seed or as a refund. + +```ts title="client.spv.createHtlc()" +interface spv { + createHtlc (receiverPubKey: string, ownerPubKey: string, options: CreateHtlcOptions): Promise +} + +interface CreateHtlcOptions { + timeout: string + seed?: string +} + +interface CreateHtlcResult { + address: string + redeemScript: string + seed: number + seedhash: string +} +``` + +## decodeHtlcScript + +Decode and return value in a HTLC redeemscript. + +```ts title="client.spv.decodeHtlcScript()" +interface spv { + decodeHtlcScript (redeemScript: string): Promise +} + +interface DecodeHtlcResult { + sellerkey: string + buyerkey: string + blocks: number + hash: string +} +``` + +## claimHtlc + +Claims all coins in HTLC address. + +```ts title="client.spv.claimHtlc()" +interface spv { + claimHtlc (scriptAddress: string, destinationAddress: string, options: ClaimHtlcOptions): Promise +} + +interface ClaimHtlcOptions { + seed: string + feeRate?: BigNumber +} + +interface SendMessageResult { + txid: string + sendmessage: string +} +```