From ee7767ccfb37128c4e4a9a52ac5a9a37e87762be Mon Sep 17 00:00:00 2001 From: canonbrother Date: Thu, 9 Sep 2021 15:11:56 +0800 Subject: [PATCH] Add SPV `listAnchors`, `listAnchorsPending`, `listAnchorAuths` and `setLastHeight` rpc (#618) * added listanchors update desc * added listanchorspending * added listanchorauths typo * added setlastheight * pr changes fix * jellyfish-testing supports multinodes (#621) * jellyfish supports multinode added jellyfish-testing misc * missing param on doc * refactor testing in group apply new TestingGroup to test fix syntax err * pr changes * refine the optional input mnkeys logic * added configurable container * refactor testing.ts with default and used exec Co-authored-by: Fuxing Loh * refactor to use tgroup * fix path * added generateanchorauth * refine setlastheight test * refactor: testinganchor to testinggroupanchor * add desc for generateanchorauths * fix fix * temp ignore test * remove redundent default params * remove extra br * fix Co-authored-by: Fuxing Loh --- .../category/spv/createAnchor.test.ts | 130 +++++++-------- .../category/spv/listAnchorAuths.test.ts | 79 +++++++++ .../category/spv/listAnchors.test.ts | 150 ++++++++++++++++++ .../category/spv/listAnchorsPending.test.ts | 102 ++++++++++++ .../category/spv/setLastHeight.test.ts | 109 +++++++++++++ .../jellyfish-api-core/src/category/spv.ts | 104 +++++++++++- packages/jellyfish-testing/src/anchor.ts | 29 ++++ packages/jellyfish-testing/src/index.ts | 81 +--------- packages/jellyfish-testing/src/misc.ts | 20 +++ packages/jellyfish-testing/src/testing.ts | 143 +++++++++++++++++ .../chains/reg_test_container/masternode.ts | 2 + website/docs/jellyfish/api/spv.md | 86 ++++++++++ 12 files changed, 878 insertions(+), 157 deletions(-) create mode 100644 packages/jellyfish-api-core/__tests__/category/spv/listAnchorAuths.test.ts create mode 100644 packages/jellyfish-api-core/__tests__/category/spv/listAnchors.test.ts create mode 100644 packages/jellyfish-api-core/__tests__/category/spv/listAnchorsPending.test.ts create mode 100644 packages/jellyfish-api-core/__tests__/category/spv/setLastHeight.test.ts create mode 100644 packages/jellyfish-testing/src/anchor.ts create mode 100644 packages/jellyfish-testing/src/misc.ts create mode 100644 packages/jellyfish-testing/src/testing.ts diff --git a/packages/jellyfish-api-core/__tests__/category/spv/createAnchor.test.ts b/packages/jellyfish-api-core/__tests__/category/spv/createAnchor.test.ts index d9d692ccaa..d93312926e 100644 --- a/packages/jellyfish-api-core/__tests__/category/spv/createAnchor.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/spv/createAnchor.test.ts @@ -1,69 +1,55 @@ import { RpcApiError } from '@defichain/jellyfish-api-core' -import { MasterNodeRegTestContainer, ContainerGroup, GenesisKeys } from '@defichain/testcontainers' -import { ContainerAdapterClient } from '../../container_adapter_client' +import { TestingGroup, Testing } from '@defichain/jellyfish-testing' +import { GenesisKeys, MasterNodeRegTestContainer } from '@defichain/testcontainers' import BigNumber from 'bignumber.js' -import { Testing } from '@defichain/jellyfish-testing' describe('Spv', () => { - const group = new ContainerGroup([ - new MasterNodeRegTestContainer(GenesisKeys[0]), - new MasterNodeRegTestContainer(GenesisKeys[1]), - new MasterNodeRegTestContainer(GenesisKeys[2]) - ]) - - const clients = [ - new ContainerAdapterClient(group.get(0)), - new ContainerAdapterClient(group.get(1)), - new ContainerAdapterClient(group.get(2)) - ] + const tGroup = TestingGroup.create(3) beforeAll(async () => { - await group.start() - + await tGroup.start() await setup() }) afterAll(async () => { - await group.stop() + await tGroup.stop() }) - async function setMockTime ( - clients: ContainerAdapterClient[], pastHour: number, futureHour = 0 - ): Promise { - const offset = Date.now() - (pastHour * 60 * 60 * 1000) + (futureHour * 60 * 60 * 1000) - for (let i = 0; i < clients.length; i += 1) { - await clients[i].misc.setMockTime(offset) - } + async function setMockTime (offsetHour: number): Promise { + await tGroup.exec(async testing => { + await testing.misc.offsetTimeHourly(offsetHour) + }) } async function setup (): Promise { - const anchorAuths = await group.get(0).call('spv_listanchorauths') - expect(anchorAuths.length).toStrictEqual(0) + const auths = await tGroup.get(0).container.call('spv_listanchorauths') + expect(auths.length).toStrictEqual(0) - // time travel back 13 hours ago - await setMockTime(clients, 13) + // time travel back 12 hours ago + const initOffsetHour = -12 + await setMockTime(initOffsetHour) - const anchorFreq = 15 - for (let i = 0; i < anchorFreq; i += 1) { - const container = group.get(i % clients.length) + // 15 as anchor frequency + for (let i = 0; i < 15; i += 1) { + const { container } = tGroup.get(i % tGroup.length()) await container.generate(1) - await group.waitForSync() + await tGroup.waitForSync() } { - const count = await group.get(0).getBlockCount() + const count = await tGroup.get(0).container.getBlockCount() expect(count).toStrictEqual(15) } // check the auth and confirm anchor mn teams at current height 15 - await group.get(0).waitForAnchorTeams(clients.length) + await tGroup.get(0).container.waitForAnchorTeams(tGroup.length()) // assertion for team - for (let i = 0; i < clients.length; i += 1) { - const container = group.get(i % clients.length) + for (let i = 0; i < tGroup.length(); i += 1) { + const { container } = tGroup.get(i % tGroup.length()) const team = await container.call('getanchorteams') - expect(team.auth.length).toStrictEqual(clients.length) - expect(team.confirm.length).toStrictEqual(clients.length) + expect(team.auth.length).toStrictEqual(tGroup.length()) + expect(team.confirm.length).toStrictEqual(tGroup.length()) expect(team.auth.includes(GenesisKeys[0].operator.address)) expect(team.auth.includes(GenesisKeys[1].operator.address)) expect(team.auth.includes(GenesisKeys[2].operator.address)) @@ -74,41 +60,31 @@ describe('Spv', () => { { // anchor auths length should be still zero - const auths = await group.get(0).call('spv_listanchorauths') + const auths = await tGroup.get(0).container.call('spv_listanchorauths') expect(auths.length).toStrictEqual(0) } // forward 3 hours from 12 hours ago and generate 15 blocks per hour // set 3 hours because block height and hash chosen is then 3 hours // then every 15 blocks will be matched again - for (let i = 1; i < 3 + 1; i += 1) { - await setMockTime(clients, 12, i) - await group.get(0).generate(15) - await group.waitForSync() - } - - { - const count = await group.get(0).getBlockCount() - expect(count).toStrictEqual(60) - } + // generate 2 anchor auths + await tGroup.anchor.generateAnchorAuths(2, initOffsetHour) - await group.get(0).waitForAnchorAuths(3) + await tGroup.get(0).container.waitForAnchorAuths(tGroup.length()) // check each container should be quorum ready - for (let i = 0; i < clients.length; i += 1) { - const container = group.get(i % clients.length) + for (let i = 0; i < tGroup.length(); i += 1) { + const { container } = tGroup.get(i % tGroup.length()) const auths = await container.call('spv_listanchorauths') - expect(auths.length).toStrictEqual(1) - expect(auths[0].signers).toStrictEqual(clients.length) - expect(auths[0].blockHeight).toStrictEqual(15) - expect(auths[0].creationHeight).toStrictEqual(60) + expect(auths.length).toStrictEqual(2) + expect(auths[0].signers).toStrictEqual(tGroup.length()) } } it('should be failed as invalid txid', async () => { - const rewardAddress = await clients[0].spv.getNewAddress() + const rewardAddress = await tGroup.get(0).rpc.spv.getNewAddress() - const promise = clients[0].spv.createAnchor([{ + const promise = tGroup.get(0).rpc.spv.createAnchor([{ txid: 'INVALID', vout: 2, amount: 15800, @@ -120,9 +96,9 @@ describe('Spv', () => { }) it('should be failed as not enough money to create anchor', async () => { - const rewardAddress = await clients[0].spv.getNewAddress() + const rewardAddress = await tGroup.get(0).rpc.spv.getNewAddress() - const promise = clients[0].spv.createAnchor([{ + const promise = tGroup.get(0).rpc.spv.createAnchor([{ txid: '11a276bb25585f6973a4dd68373cffff41dbcaddf12bbc1c2b489d1dc84564ee', vout: 2, amount: 1, @@ -134,9 +110,9 @@ describe('Spv', () => { }) it('should be failed as invalid privkey', async () => { - const rewardAddress = await clients[0].spv.getNewAddress() + const rewardAddress = await tGroup.get(0).rpc.spv.getNewAddress() - const promise = clients[0].spv.createAnchor([{ + const promise = tGroup.get(0).rpc.spv.createAnchor([{ txid: '11a276bb25585f6973a4dd68373cffff41dbcaddf12bbc1c2b489d1dc84564ee', vout: 2, amount: 15800, @@ -148,7 +124,7 @@ describe('Spv', () => { }) it('should be failed as invalid address', async () => { - const promise = clients[0].spv.createAnchor([{ + const promise = tGroup.get(0).rpc.spv.createAnchor([{ txid: '11a276bb25585f6973a4dd68373cffff41dbcaddf12bbc1c2b489d1dc84564ee', vout: 2, amount: 15800, @@ -160,9 +136,9 @@ describe('Spv', () => { }) it('should createAnchor', async () => { - const rewardAddress = await clients[0].spv.getNewAddress() + const rewardAddress = await tGroup.get(0).rpc.spv.getNewAddress() - const anchor = await clients[0].spv.createAnchor([{ + const anchor = await tGroup.get(0).rpc.spv.createAnchor([{ txid: '11a276bb25585f6973a4dd68373cffff41dbcaddf12bbc1c2b489d1dc84564ee', vout: 2, amount: 15800, @@ -171,7 +147,7 @@ describe('Spv', () => { expect(typeof anchor.txHex).toStrictEqual('string') expect(typeof anchor.txHash).toStrictEqual('string') expect(typeof anchor.defiHash).toStrictEqual('string') - expect(anchor.defiHeight).toStrictEqual(15) + expect(typeof anchor.defiHeight).toStrictEqual('number') expect(anchor.estimatedReward).toStrictEqual(new BigNumber(0)) expect(anchor.cost).toStrictEqual(new BigNumber(3556)) expect(anchor.sendResult).toStrictEqual(0) @@ -179,48 +155,48 @@ describe('Spv', () => { // pending anchor list is updated { - const pending = await group.get(0).call('spv_listanchorspending') + const pending = await tGroup.get(0).container.call('spv_listanchorspending') expect(pending.length).toStrictEqual(1) } // generate the anchor block - await group.get(0).generate(1) + await tGroup.get(0).generate(1) // pending is cleared { - const pending = await group.get(0).call('spv_listanchorspending') + const pending = await tGroup.get(0).container.call('spv_listanchorspending') expect(pending.length).toStrictEqual(0) } // should be not active yet - const anchors = await group.get(0).call('spv_listanchors') + const anchors = await tGroup.get(0).container.call('spv_listanchors') expect(anchors.length).toStrictEqual(1) expect(anchors[0].btcBlockHeight).toStrictEqual(0) expect(typeof anchors[0].btcBlockHash).toStrictEqual('string') expect(typeof anchors[0].btcTxHash).toStrictEqual('string') expect(typeof anchors[0].previousAnchor).toStrictEqual('string') - expect(anchors[0].defiBlockHeight).toStrictEqual(15) + expect(typeof anchors[0].defiBlockHeight).toStrictEqual('number') expect(typeof anchors[0].defiBlockHash).toStrictEqual('string') expect(anchors[0].rewardAddress).toStrictEqual(rewardAddress) expect(anchors[0].confirmations).toBeLessThan(6) expect(anchors[0].signatures).toStrictEqual(2) - expect(anchors[0].anchorCreationHeight).toStrictEqual(60) + expect(typeof anchors[0].anchorCreationHeight).toStrictEqual('number') expect(anchors[0].active).toStrictEqual(false) // to activate anchor at min 6 conf - await group.get(0).call('spv_setlastheight', [6]) + await tGroup.get(0).container.call('spv_setlastheight', [6]) { // should be active now - const anchors = await group.get(0).call('spv_listanchors') + const anchors = await tGroup.get(0).container.call('spv_listanchors') expect(anchors[0].confirmations).toBeGreaterThanOrEqual(6) expect(anchors[0].active).toStrictEqual(true) } { - // auths back to zero after the anchor active - const auths = await group.get(0).call('spv_listanchorauths') - expect(auths.length).toStrictEqual(0) + // auths reduce from 2 to 1 after the anchor active + const auths = await tGroup.get(0).container.call('spv_listanchorauths') + expect(auths.length).toStrictEqual(1) } }) }) diff --git a/packages/jellyfish-api-core/__tests__/category/spv/listAnchorAuths.test.ts b/packages/jellyfish-api-core/__tests__/category/spv/listAnchorAuths.test.ts new file mode 100644 index 0000000000..eb980ad737 --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/spv/listAnchorAuths.test.ts @@ -0,0 +1,79 @@ +import { TestingGroup } from '@defichain/jellyfish-testing' +import { GenesisKeys } from '@defichain/testcontainers' + +describe('Spv', () => { + const tGroup = TestingGroup.create(3) + + beforeAll(async () => { + await tGroup.start() + await setup() + }) + + afterAll(async () => { + await tGroup.stop() + }) + + async function setMockTime (offsetHour: number): Promise { + await tGroup.exec(async testing => { + await testing.misc.offsetTimeHourly(offsetHour) + }) + } + + async function setup (): Promise { + const auths = await tGroup.get(0).container.call('spv_listanchorauths') + expect(auths.length).toStrictEqual(0) + + // time travel back 12 hours ago + const initOffsetHour = -12 + await setMockTime(initOffsetHour) + + // 15 as anchor frequency + for (let i = 0; i < 15; i += 1) { + const { container } = tGroup.get(i % tGroup.length()) + await container.generate(1) + await tGroup.waitForSync() + } + + const blockCount = await tGroup.get(0).container.getBlockCount() + expect(blockCount).toStrictEqual(15) + + // check the auth and confirm anchor mn teams + await tGroup.get(0).container.waitForAnchorTeams(tGroup.length()) + + // assertion for team + for (let i = 0; i < tGroup.length(); i += 1) { + const { container } = tGroup.get(i % tGroup.length()) + const team = await container.call('getanchorteams') + expect(team.auth.length).toStrictEqual(tGroup.length()) + expect(team.confirm.length).toStrictEqual(tGroup.length()) + expect(team.auth.includes(GenesisKeys[0].operator.address)) + expect(team.auth.includes(GenesisKeys[1].operator.address)) + expect(team.auth.includes(GenesisKeys[2].operator.address)) + expect(team.confirm.includes(GenesisKeys[0].operator.address)) + expect(team.confirm.includes(GenesisKeys[1].operator.address)) + expect(team.confirm.includes(GenesisKeys[2].operator.address)) + } + + // generate 2 anchor auths + await tGroup.anchor.generateAnchorAuths(2, initOffsetHour) + + await tGroup.get(0).container.waitForAnchorAuths(tGroup.length()) + } + + it('should listAnchorAuths', async () => { + for (let i = 0; i < 2; i += 1) { + const auths = await tGroup.get(0).rpc.spv.listAnchorAuths() + expect(auths.length).toStrictEqual(2) + expect(typeof auths[i].previousAnchor).toStrictEqual('string') + expect(typeof auths[i].blockHeight).toStrictEqual('number') + expect(typeof auths[i].blockHash).toStrictEqual('string') + expect(typeof auths[i].creationHeight).toStrictEqual('number') + expect(typeof auths[i].signers).toStrictEqual('number') + expect(auths[i].signers).toStrictEqual(tGroup.length()) + expect(auths[i].signees?.length).toStrictEqual(tGroup.length()) + expect(auths[i].signees?.includes(GenesisKeys[0].operator.address)) + expect(auths[i].signees?.includes(GenesisKeys[1].operator.address)) + expect(auths[i].signees?.includes(GenesisKeys[2].operator.address)) + } + }) +}) diff --git a/packages/jellyfish-api-core/__tests__/category/spv/listAnchors.test.ts b/packages/jellyfish-api-core/__tests__/category/spv/listAnchors.test.ts new file mode 100644 index 0000000000..15aefa1279 --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/spv/listAnchors.test.ts @@ -0,0 +1,150 @@ +import { CreateAnchorResult } from '@defichain/jellyfish-api-core/category/spv' +import { TestingGroup } from '@defichain/jellyfish-testing' +import { GenesisKeys } from '@defichain/testcontainers' + +describe('Spv', () => { + const tGroup = TestingGroup.create(3) + + beforeAll(async () => { + await tGroup.start() + await setup() + }) + + afterAll(async () => { + await tGroup.stop() + }) + + async function setMockTime (offsetHour: number): Promise { + await tGroup.exec(async testing => { + await testing.misc.offsetTimeHourly(offsetHour) + }) + } + + async function setup (): Promise { + { + const auths = await tGroup.get(0).container.call('spv_listanchorauths') + expect(auths.length).toStrictEqual(0) + } + + // time travel back 12 hours ago + const initOffsetHour = -12 + await setMockTime(initOffsetHour) + + // 15 as anchor frequency + for (let i = 0; i < 15; i += 1) { + const { container } = tGroup.get(i % tGroup.length()) + await container.generate(1) + await tGroup.waitForSync() + } + + // check the auth and confirm anchor mn teams + await tGroup.get(0).container.waitForAnchorTeams(tGroup.length()) + + // assertion for team + for (let i = 0; i < tGroup.length(); i += 1) { + const { container } = tGroup.get(i % tGroup.length()) + const team = await container.call('getanchorteams') + expect(team.auth.length).toStrictEqual(tGroup.length()) + expect(team.confirm.length).toStrictEqual(tGroup.length()) + expect(team.auth.includes(GenesisKeys[0].operator.address)) + expect(team.auth.includes(GenesisKeys[1].operator.address)) + expect(team.auth.includes(GenesisKeys[2].operator.address)) + expect(team.confirm.includes(GenesisKeys[0].operator.address)) + expect(team.confirm.includes(GenesisKeys[1].operator.address)) + expect(team.confirm.includes(GenesisKeys[2].operator.address)) + } + + await tGroup.anchor.generateAnchorAuths(2, initOffsetHour) + + await tGroup.get(0).container.waitForAnchorAuths(tGroup.length()) + + // check each container should be quorum ready + for (let i = 0; i < tGroup.length(); i += 1) { + const { container } = tGroup.get(i % tGroup.length()) + const auths = await container.call('spv_listanchorauths') + expect(auths.length).toStrictEqual(2) + expect(auths[0].signers).toStrictEqual(tGroup.length()) + } + + await tGroup.get(0).container.call('spv_setlastheight', [1]) + const anchor1 = await createAnchor() + await tGroup.get(0).generate(1) + await tGroup.waitForSync() + + await tGroup.get(0).container.call('spv_setlastheight', [2]) + const anchor2 = await createAnchor() + await tGroup.get(0).generate(1) + await tGroup.waitForSync() + + await tGroup.get(0).container.call('spv_setlastheight', [3]) + const anchor3 = await createAnchor() + await tGroup.get(0).generate(1) + await tGroup.waitForSync() + + await tGroup.get(0).container.call('spv_setlastheight', [4]) + const anchor4 = await createAnchor() + await tGroup.get(0).generate(1) + await tGroup.waitForSync() + + await tGroup.get(1).container.call('spv_sendrawtx', [anchor1.txHex]) + await tGroup.get(1).container.call('spv_sendrawtx', [anchor2.txHex]) + await tGroup.get(1).container.call('spv_sendrawtx', [anchor3.txHex]) + await tGroup.get(1).container.call('spv_sendrawtx', [anchor4.txHex]) + await tGroup.get(1).generate(1) + await tGroup.waitForSync() + + await tGroup.get(0).container.call('spv_setlastheight', [6]) + } + + async function createAnchor (): Promise { + const rewardAddress = await tGroup.get(0).rpc.spv.getNewAddress() + return await tGroup.get(0).rpc.spv.createAnchor([{ + txid: '11a276bb25585f6973a4dd68373cffff41dbcaddf12bbc1c2b489d1dc84564ee', + vout: 2, + amount: 15800, + privkey: 'b0528d87cfdb09f72c9d10b7b3cc00727062d93537a3e8abcf1fde821d08b59d' + }], rewardAddress) + } + + it('should listAnchors', async () => { + const anchors = await tGroup.get(0).rpc.spv.listAnchors() + expect(anchors.length).toStrictEqual(4) + for (const anchor of anchors) { + expect(typeof anchor.btcBlockHeight).toStrictEqual('number') + expect(typeof anchor.btcBlockHash).toStrictEqual('string') + expect(typeof anchor.btcTxHash).toStrictEqual('string') + expect(typeof anchor.previousAnchor).toStrictEqual('string') + expect(typeof anchor.defiBlockHeight).toStrictEqual('number') + expect(typeof anchor.defiBlockHash).toStrictEqual('string') + expect(typeof anchor.rewardAddress).toStrictEqual('string') + expect(typeof anchor.confirmations).toStrictEqual('number') + expect(typeof anchor.signatures).toStrictEqual('number') + expect(typeof anchor.anchorCreationHeight).toStrictEqual('number') + expect(typeof anchor.active).toStrictEqual('boolean') + } + }) + + it('should listAnchors with minBtcHeight', async () => { + const anchors = await tGroup.get(0).rpc.spv.listAnchors({ minBtcHeight: 4 }) + expect(anchors.length).toStrictEqual(1) + expect(anchors.every(anchor => anchor.btcBlockHeight <= 4)).toStrictEqual(true) + }) + + it('should listAnchors with maxBtcHeight', async () => { + const anchors = await tGroup.get(0).rpc.spv.listAnchors({ maxBtcHeight: 3 }) + expect(anchors.length).toStrictEqual(3) + expect(anchors.every(anchor => anchor.btcBlockHeight <= 3)).toStrictEqual(true) + }) + + it('should listAnchors with minConfs', async () => { + const anchors = await tGroup.get(0).rpc.spv.listAnchors({ minConfs: 5 }) + expect(anchors.length).toStrictEqual(2) + expect(anchors.every(anchor => anchor.confirmations >= 5)).toStrictEqual(true) + }) + + it('should listAnchors with maxConfs', async () => { + const anchors = await tGroup.get(0).rpc.spv.listAnchors({ maxConfs: 3 }) + expect(anchors.length).toStrictEqual(1) + expect(anchors.every(anchor => anchor.confirmations <= 3)).toStrictEqual(true) + }) +}) diff --git a/packages/jellyfish-api-core/__tests__/category/spv/listAnchorsPending.test.ts b/packages/jellyfish-api-core/__tests__/category/spv/listAnchorsPending.test.ts new file mode 100644 index 0000000000..673d325267 --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/spv/listAnchorsPending.test.ts @@ -0,0 +1,102 @@ +import { TestingGroup } from '@defichain/jellyfish-testing' +import { GenesisKeys } from '@defichain/testcontainers' + +describe('Spv', () => { + const tGroup = TestingGroup.create(3) + + beforeAll(async () => { + await tGroup.start() + await setup() + }) + + afterAll(async () => { + await tGroup.stop() + }) + + async function setMockTime (offsetHour: number): Promise { + await tGroup.exec(async testing => { + await testing.misc.offsetTimeHourly(offsetHour) + }) + } + + async function setup (): Promise { + const auths = await tGroup.get(0).container.call('spv_listanchorauths') + expect(auths.length).toStrictEqual(0) + + // time travel back 12 hours ago + const initOffsetHour = -12 + await setMockTime(initOffsetHour) + + // 15 as anchor frequency + for (let i = 0; i < 15; i += 1) { + const { container } = tGroup.get(i % tGroup.length()) + await container.generate(1) + await tGroup.waitForSync() + } + + const blockCount = await tGroup.get(0).container.getBlockCount() + expect(blockCount).toStrictEqual(15) + + // check the auth and confirm anchor mn teams + await tGroup.get(0).container.waitForAnchorTeams(tGroup.length()) + + // assertion for team + for (let i = 0; i < tGroup.length(); i += 1) { + const { container } = tGroup.get(i % tGroup.length()) + const team = await container.call('getanchorteams') + expect(team.auth.length).toStrictEqual(tGroup.length()) + expect(team.confirm.length).toStrictEqual(tGroup.length()) + expect(team.auth.includes(GenesisKeys[0].operator.address)) + expect(team.auth.includes(GenesisKeys[1].operator.address)) + expect(team.auth.includes(GenesisKeys[2].operator.address)) + expect(team.confirm.includes(GenesisKeys[0].operator.address)) + expect(team.confirm.includes(GenesisKeys[1].operator.address)) + expect(team.confirm.includes(GenesisKeys[2].operator.address)) + } + + // generate 2 anchor auths + await tGroup.anchor.generateAnchorAuths(2, initOffsetHour) + + await tGroup.get(0).container.waitForAnchorAuths(tGroup.length()) + + // check each container should be quorum ready + for (let i = 0; i < tGroup.length(); i += 1) { + const { container } = tGroup.get(i % tGroup.length()) + const auths = await container.call('spv_listanchorauths') + expect(auths.length).toStrictEqual(2) + expect(auths[0].signers).toStrictEqual(tGroup.length()) + } + + await createAnchor() + await createAnchor() + await createAnchor() + await createAnchor() + } + + async function createAnchor (): Promise { + const rewardAddress = await tGroup.get(0).rpc.spv.getNewAddress() + await tGroup.get(0).rpc.spv.createAnchor([{ + txid: '11a276bb25585f6973a4dd68373cffff41dbcaddf12bbc1c2b489d1dc84564ee', + vout: 2, + amount: 15800, + privkey: 'b0528d87cfdb09f72c9d10b7b3cc00727062d93537a3e8abcf1fde821d08b59d' + }], rewardAddress) + } + + it('should listAnchorsPending', async () => { + const anchors = await tGroup.get(0).rpc.spv.listAnchorsPending() + expect(anchors.length).toStrictEqual(4) + for (const anchor of anchors) { + expect(typeof anchor.btcBlockHeight).toStrictEqual('number') + expect(typeof anchor.btcBlockHash).toStrictEqual('string') + expect(typeof anchor.btcTxHash).toStrictEqual('string') + expect(typeof anchor.previousAnchor).toStrictEqual('string') + expect(typeof anchor.defiBlockHeight).toStrictEqual('number') + expect(typeof anchor.defiBlockHash).toStrictEqual('string') + expect(typeof anchor.rewardAddress).toStrictEqual('string') + expect(typeof anchor.confirmations).toStrictEqual('number') + expect(typeof anchor.signatures).toStrictEqual('number') + expect(typeof anchor.anchorCreationHeight).toStrictEqual('number') + } + }) +}) diff --git a/packages/jellyfish-api-core/__tests__/category/spv/setLastHeight.test.ts b/packages/jellyfish-api-core/__tests__/category/spv/setLastHeight.test.ts new file mode 100644 index 0000000000..d07f56e38b --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/spv/setLastHeight.test.ts @@ -0,0 +1,109 @@ +import { TestingGroup } from '@defichain/jellyfish-testing' +import { GenesisKeys } from '@defichain/testcontainers' + +describe('Spv', () => { + const tGroup = TestingGroup.create(3) + + beforeAll(async () => { + await tGroup.start() + await setup() + }) + + afterAll(async () => { + await tGroup.stop() + }) + + async function setMockTime (offsetHour: number): Promise { + await tGroup.exec(async testing => { + await testing.misc.offsetTimeHourly(offsetHour) + }) + } + + async function setup (): Promise { + const auths = await tGroup.get(0).container.call('spv_listanchorauths') + expect(auths.length).toStrictEqual(0) + + // time travel back 12 hours ago + const initOffsetHour = -12 + await setMockTime(initOffsetHour) + + // 15 as anchor frequency + for (let i = 0; i < 15; i += 1) { + const { container } = tGroup.get(i % tGroup.length()) + await container.generate(1) + await tGroup.waitForSync() + } + + const blockCount = await tGroup.get(0).container.getBlockCount() + expect(blockCount).toStrictEqual(15) + + // check the auth and confirm anchor mn teams + await tGroup.get(0).container.waitForAnchorTeams(tGroup.length()) + + // assertion for team + for (let i = 0; i < tGroup.length(); i += 1) { + const { container } = tGroup.get(i % tGroup.length()) + const team = await container.call('getanchorteams') + expect(team.auth.length).toStrictEqual(tGroup.length()) + expect(team.confirm.length).toStrictEqual(tGroup.length()) + expect(team.auth.includes(GenesisKeys[0].operator.address)) + expect(team.auth.includes(GenesisKeys[1].operator.address)) + expect(team.auth.includes(GenesisKeys[2].operator.address)) + expect(team.confirm.includes(GenesisKeys[0].operator.address)) + expect(team.confirm.includes(GenesisKeys[1].operator.address)) + expect(team.confirm.includes(GenesisKeys[2].operator.address)) + } + + // generate 2 anchor auths + await tGroup.anchor.generateAnchorAuths(2, initOffsetHour) + + await tGroup.get(0).container.waitForAnchorAuths(tGroup.length()) + + // check each container should be quorum ready + for (let i = 0; i < tGroup.length(); i += 1) { + const { container } = tGroup.get(i % tGroup.length()) + const auths = await container.call('spv_listanchorauths') + expect(auths.length).toStrictEqual(2) + expect(auths[0].signers).toStrictEqual(tGroup.length()) + } + + await createAnchor() + await tGroup.get(0).generate(1) + await tGroup.waitForSync() + } + + async function createAnchor (): Promise { + const rewardAddress = await tGroup.get(0).rpc.spv.getNewAddress() + await tGroup.get(0).rpc.spv.createAnchor([{ + txid: '11a276bb25585f6973a4dd68373cffff41dbcaddf12bbc1c2b489d1dc84564ee', + vout: 2, + amount: 15800, + privkey: 'b0528d87cfdb09f72c9d10b7b3cc00727062d93537a3e8abcf1fde821d08b59d' + }], rewardAddress) + } + + it('should setLastHeight', async () => { + { + const anchors = await tGroup.get(0).rpc.spv.listAnchors() + expect(anchors.length).toStrictEqual(1) + expect(anchors[0].confirmations).toStrictEqual(1) + expect(anchors[0].active).toStrictEqual(false) + } + + { + await tGroup.get(0).rpc.spv.setLastHeight(3) + const anchors = await tGroup.get(0).rpc.spv.listAnchors() + expect(anchors.length).toStrictEqual(1) + expect(anchors[0].confirmations).toStrictEqual(1 + 3) + expect(anchors[0].active).toStrictEqual(false) + } + + { + await tGroup.get(0).rpc.spv.setLastHeight(15) + const anchors = await tGroup.get(0).rpc.spv.listAnchors() + expect(anchors.length).toStrictEqual(1) + expect(anchors[0].confirmations).toStrictEqual(1 + 15) + expect(anchors[0].active).toStrictEqual(true) + } + }) +}) diff --git a/packages/jellyfish-api-core/src/category/spv.ts b/packages/jellyfish-api-core/src/category/spv.ts index 65da4dfd23..e6f7087caf 100644 --- a/packages/jellyfish-api-core/src/category/spv.ts +++ b/packages/jellyfish-api-core/src/category/spv.ts @@ -156,6 +156,55 @@ export class Spv { { cost: 'bignumber', estimatedReward: 'bignumber' } ) } + + /** + * List anchors + * + * @param {ListAnchorsOptions} [options] + * @param {number} [options.minBtcHeight=-1] + * @param {number} [options.maxBtcHeight=-1] + * @param {number} [options.minConfs=-1] + * @param {number} [options.maxConfs=-1] + * @return {Promise} + */ + async listAnchors ( + options: ListAnchorsOptions = {} + ): Promise { + const opts = { minBtcHeight: -1, maxBtcHeight: -1, minConfs: -1, maxConfs: -1, ...options } + return await this.client.call( + 'spv_listanchors', + [opts.minBtcHeight, opts.maxBtcHeight, opts.minConfs, opts.maxConfs], + 'number' + ) + } + + /** + * List pending anchors in mempool + * + * @return {Promise} + */ + async listAnchorsPending (): Promise { + return await this.client.call('spv_listanchorspending', [], 'number') + } + + /** + * List anchor auths + * + * @return {Promise} + */ + async listAnchorAuths (): Promise { + return await this.client.call('spv_listanchorauths', [], 'number') + } + + /** + * Set last height on BTC chain, use for testing purpose + * + * @param {number} height + * @return {Promise} + */ + async setLastHeight (height: number): Promise { + return await this.client.call('spv_setlastheight', [height], 'number') + } } export interface ReceivedByAddressInfo { @@ -262,9 +311,9 @@ export interface CreateAnchorResult { txHex: string /** the transaction hash */ txHash: string - /** the anchor block hash */ + /** the defi block hash */ defiHash: string - /** the anchor block height */ + /** the defi block height */ defiHeight: number /** estimated anchor reward */ estimatedReward: BigNumber @@ -275,3 +324,54 @@ export interface CreateAnchorResult { /** decoded sendResult */ sendMessage: string } + +export interface ListAnchorsOptions { + /** min BTC height */ + minBtcHeight?: number + /** max BTC height */ + maxBtcHeight?: number + /** min confirmations */ + minConfs?: number + /** max confirmations */ + maxConfs?: number +} + +export interface ListAnchorsResult { + /** BTC block height */ + btcBlockHeight: number + /** BTC block hash */ + btcBlockHash: string + /** BTC transaction hash */ + btcTxHash: string + /** previous anchor */ + previousAnchor: string + /** defi block height */ + defiBlockHeight: number + /** defi block hash */ + defiBlockHash: string + /** anchor reward address */ + rewardAddress: string + /** BTC confirmations */ + confirmations: number + /** number of signatures */ + signatures: number + /** anchor status */ + active?: boolean + /** anchor creation height */ + anchorCreationHeight?: number +} + +export interface ListAnchorAuthsResult { + /** previous anchor */ + previousAnchor: string + /** defi block height */ + blockHeight: number + /** defi block hash */ + blockHash: string + /** anchor creation height */ + creationHeight: number + /** number of anchor signers */ + signers: number + /** anchor signees address */ + signees?: string[] +} diff --git a/packages/jellyfish-testing/src/anchor.ts b/packages/jellyfish-testing/src/anchor.ts new file mode 100644 index 0000000000..5ab28cd994 --- /dev/null +++ b/packages/jellyfish-testing/src/anchor.ts @@ -0,0 +1,29 @@ +import { TestingGroup } from './testing' + +export class TestingGroupAnchor { + constructor ( + private readonly testingGroup: TestingGroup + ) { + } + + /** + * After every new block the anchor data creation team members will create + * and sign new anchor data. + * Anchor data is created every fifthteen blocks, + * the block height and hash chosen is then three hours further back into the chain + * and then more blocks until the every the fifthteen block frequency is matched again. + * https://github.com/DeFiCh/ain/wiki/What-is-an-anchor%3F#anchor-data-creation + * @param {number} numOfAuths + * @param {number} initOffsetHour + * @return {Promise} + */ + async generateAnchorAuths (numOfAuths: number, initOffsetHour: number): Promise { + for (let i = 1; i < 3 + numOfAuths + 1; i += 1) { + await this.testingGroup.exec(async testing => { + await testing.misc.offsetTimeHourly(initOffsetHour + i) + }) + await this.testingGroup.get(0).generate(15) + await this.testingGroup.waitForSync() + } + } +} diff --git a/packages/jellyfish-testing/src/index.ts b/packages/jellyfish-testing/src/index.ts index 94552ba736..a967cd8146 100644 --- a/packages/jellyfish-testing/src/index.ts +++ b/packages/jellyfish-testing/src/index.ts @@ -1,83 +1,8 @@ -import fetch from 'cross-fetch' -import { MasterNodeRegTestContainer } from '@defichain/testcontainers' -import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' -import { TestingPoolPair } from './poolpair' -import { TestingRawTx } from './rawtx' -import { TestingToken } from './token' -import { TestingFixture } from './fixture' -import { TestingICX } from './icxorderbook' - export * from './fixture' export * from './poolpair' export * from './rawtx' export * from './token' export * from './icxorderbook' - -export class Testing { - public readonly fixture = new TestingFixture(this) - public readonly token = new TestingToken(this.container, this.rpc) - public readonly poolpair = new TestingPoolPair(this.container, this.rpc) - public readonly rawtx = new TestingRawTx(this.container, this.rpc) - public readonly icxorderbook = new TestingICX(this) - - private readonly addresses: Record = {} - - private constructor ( - public readonly container: MasterNodeRegTestContainer, - public readonly rpc: TestingJsonRpcClient - ) { - } - - async generate (n: number): Promise { - await this.container.generate(n) - } - - async address (key: number | string): Promise { - key = key.toString() - if (this.addresses[key] === undefined) { - this.addresses[key] = await this.generateAddress() - } - return this.addresses[key] - } - - generateAddress (): Promise - generateAddress (n: 1): Promise - generateAddress (n: number): Promise - - async generateAddress (n?: number): Promise { - if (n === undefined || n === 1) { - return await this.container.getNewAddress() - } - - const addresses: string[] = [] - for (let i = 0; i < n; i++) { - addresses[i] = await this.container.getNewAddress() - } - return addresses - } - - static create (container: MasterNodeRegTestContainer): Testing { - const rpc = new TestingJsonRpcClient(container) - return new Testing(container, rpc) - } -} - -/** - * JsonRpcClient with dynamic url resolved from MasterNodeRegTestContainer. - */ -class TestingJsonRpcClient extends JsonRpcClient { - constructor (public readonly container: MasterNodeRegTestContainer) { - super('resolved in fetch') - } - - protected async fetch (body: string, controller: any): Promise { - const url = await this.container.getCachedRpcUrl() - return await fetch(url, { - method: 'POST', - body: body, - cache: 'no-cache', - headers: this.options.headers, - signal: controller.signal - }) - } -} +export * from './misc' +export * from './anchor' +export * from './testing' diff --git a/packages/jellyfish-testing/src/misc.ts b/packages/jellyfish-testing/src/misc.ts new file mode 100644 index 0000000000..4aa38b7272 --- /dev/null +++ b/packages/jellyfish-testing/src/misc.ts @@ -0,0 +1,20 @@ +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' + +export class TestingMisc { + constructor ( + private readonly container: MasterNodeRegTestContainer, + private readonly rpc: JsonRpcClient + ) { + } + + /** + * Offset time hourly + * @param {number} offsetBy can be positive/negative value to determine offset into future or past + * @return {Promise} + */ + async offsetTimeHourly (offsetBy: number): Promise { + const offset = Date.now() + (offsetBy * 60 * 60 * 1000) + await this.rpc.misc.setMockTime(offset) + } +} diff --git a/packages/jellyfish-testing/src/testing.ts b/packages/jellyfish-testing/src/testing.ts new file mode 100644 index 0000000000..7e309cb2d0 --- /dev/null +++ b/packages/jellyfish-testing/src/testing.ts @@ -0,0 +1,143 @@ +import fetch from 'cross-fetch' +import { TestingFixture } from './fixture' +import { TestingToken } from './token' +import { TestingPoolPair } from './poolpair' +import { TestingRawTx } from './rawtx' +import { TestingICX } from './icxorderbook' +import { TestingMisc } from './misc' +import { TestingGroupAnchor } from './anchor' +import { ContainerGroup, GenesisKeys, MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' + +export class Testing { + public readonly fixture = new TestingFixture(this) + public readonly token = new TestingToken(this.container, this.rpc) + public readonly poolpair = new TestingPoolPair(this.container, this.rpc) + public readonly rawtx = new TestingRawTx(this.container, this.rpc) + public readonly icxorderbook = new TestingICX(this) + public readonly misc = new TestingMisc(this.container, this.rpc) + + private readonly addresses: Record = {} + + private constructor ( + public readonly container: MasterNodeRegTestContainer, + public readonly rpc: TestingJsonRpcClient + ) { + } + + async generate (n: number): Promise { + await this.container.generate(n) + } + + async address (key: number | string): Promise { + key = key.toString() + if (this.addresses[key] === undefined) { + this.addresses[key] = await this.generateAddress() + } + return this.addresses[key] + } + + generateAddress (): Promise + generateAddress (n: 1): Promise + generateAddress (n: number): Promise + + async generateAddress (n?: number): Promise { + if (n === undefined || n === 1) { + return await this.container.getNewAddress() + } + + const addresses: string[] = [] + for (let i = 0; i < n; i++) { + addresses[i] = await this.container.getNewAddress() + } + return addresses + } + + static create (container: MasterNodeRegTestContainer): Testing { + const rpc = new TestingJsonRpcClient(container) + return new Testing(container, rpc) + } +} + +export class TestingGroup { + public readonly anchor = new TestingGroupAnchor(this) + + private constructor ( + public readonly group: ContainerGroup, + public readonly testings: Testing[] + ) { + } + + /** + * @param {number} n of testing container to create + * @param {(index: number) => MasterNodeRegTestContainer} [init=MasterNodeRegTestContainer] + */ + static create ( + n: number, + init = (index: number) => new MasterNodeRegTestContainer(GenesisKeys[index]) + ): TestingGroup { + const containers: MasterNodeRegTestContainer[] = [] + const testings: Testing[] = [] + for (let i = 0; i < n; i += 1) { + const container = init(i) + containers.push(container) + + const testing = Testing.create(container) + testings.push(testing) + } + + const group = new ContainerGroup(containers) + return new TestingGroup(group, testings) + } + + get (index: number): Testing { + return this.testings[index] + } + + length (): number { + return this.testings.length + } + + async start (): Promise { + return await this.group.start() + } + + async stop (): Promise { + return await this.group.stop() + } + + async exec (runner: (testing: Testing) => Promise): Promise { + for (let i = 0; i < this.testings.length; i += 1) { + await runner(this.testings[i]) + } + } + + async waitForSync (): Promise { + return await this.group.waitForSync() + } + + /* istanbul ignore next, TODO(canonbrother) */ + async waitForMempoolSync (txid: string, timeout = 15000): Promise { + return await this.group.waitForMempoolSync(txid, timeout) + } +} + +/** + * JsonRpcClient with dynamic url resolved from MasterNodeRegTestContainer. + */ +class TestingJsonRpcClient extends JsonRpcClient { + constructor (public readonly container: MasterNodeRegTestContainer) { + super('resolved in fetch') + } + + protected async fetch (body: string, controller: any): Promise { + const url = await this.container.getCachedRpcUrl() + return await fetch(url, { + method: 'POST', + body: body, + cache: 'no-cache', + headers: this.options.headers, + signal: controller.signal + }) + } +} diff --git a/packages/testcontainers/src/chains/reg_test_container/masternode.ts b/packages/testcontainers/src/chains/reg_test_container/masternode.ts index 610145ac85..204ee1e862 100644 --- a/packages/testcontainers/src/chains/reg_test_container/masternode.ts +++ b/packages/testcontainers/src/chains/reg_test_container/masternode.ts @@ -133,6 +133,7 @@ export class MasterNodeRegTestContainer extends RegTestContainer { * @param {number} [timeout=30000] in ms * @return {Promise} */ + /* istanbul ignore next, TODO(canonbrother) */ async waitForAnchorTeams (nodesLength: number, timeout = 30000): Promise { return await waitForCondition(async () => { const anchorTeams = await this.call('getanchorteams') @@ -150,6 +151,7 @@ export class MasterNodeRegTestContainer extends RegTestContainer { * @param {number} [timeout=30000] in ms * @return {Promise} */ + /* istanbul ignore next, TODO(canonbrother) */ async waitForAnchorAuths (nodesLength: number, timeout = 30000): Promise { return await waitForCondition(async () => { const auths = await this.call('spv_listanchorauths') diff --git a/website/docs/jellyfish/api/spv.md b/website/docs/jellyfish/api/spv.md index ea977bbbd4..329a66f1d2 100644 --- a/website/docs/jellyfish/api/spv.md +++ b/website/docs/jellyfish/api/spv.md @@ -216,3 +216,89 @@ interface CreateAnchorResult { sendMessage: string } ``` + +## listAnchors + +List anchors. + +```ts title=client.spv.listAnchors()" +interface spv { + listAnchors ( + options: ListAnchorsOptions = { minBtcHeight: -1, maxBtcHeight: -1, minConfs: -1, maxConfs: -1 } + ): Promise +} + +interface ListAnchorsOptions { + minBtcHeight?: number + maxBtcHeight?: number + minConfs?: number + maxConfs?: number +} + +interface ListAnchorsResult { + btcBlockHeight: number + btcBlockHash: string + btcTxHash: string + previousAnchor: string + defiBlockHeight: number + defiBlockHash: string + rewardAddress: string + confirmations: number + signatures: number + active?: boolean + anchorCreationHeight?: number +} +``` + +## listAnchorsPending + +List pending anchors in mempool. + +```ts title=client.spv.listAnchorsPending()" +interface spv { + listAnchorsPending (): Promise +} + +interface ListAnchorsResult { + btcBlockHeight: number + btcBlockHash: string + btcTxHash: string + previousAnchor: string + defiBlockHeight: number + defiBlockHash: string + rewardAddress: string + confirmations: number + signatures: number + active?: boolean + anchorCreationHeight?: number +} +``` + +## listAnchorAuths + +List anchor auths. + +```ts title=client.spv.listAnchorAuths()" +interface spv { + listAnchorAuths (): Promise +} + +interface ListAnchorAuthsResult { + previousAnchor: string + blockHeight: number + blockHash: string + creationHeight: number + signers: number + signees?: string[] +} +``` + +## setLastHeight + +Set last height on BTC chain, use for testing purpose. + +```ts title=client.spv.setLastHeight()" +interface spv { + setLastHeight (height: number): Promise +} +``` \ No newline at end of file