From 97aeb471b5b518793f25866e7b4a78c62795cdfc Mon Sep 17 00:00:00 2001 From: Kven Ho Date: Mon, 12 Dec 2022 17:45:02 +0800 Subject: [PATCH] feat(apps/whale-api): add updatemasternode indexer (#1869) #### What this PR does / why we need it: Add `UpdateMasternodeIndexer extends DfTxIndexer` to update the masternode index when the DfTx UpdaterMasternode transaction is created. #### Which issue(s) does this PR fixes?: Fixes part of #1842 Signed-off-by: Isaac Yong Signed-off-by: Kven Ho Signed-off-by: Fuxing Loh <4266087+fuxingloh@users.noreply.github.com> Co-authored-by: Mark Tan Co-authored-by: Mark Tan <35588098+marktanrj@users.noreply.github.com> Co-authored-by: Shoham Chakraborty Co-authored-by: Dilshan Madushanka Co-authored-by: Isaac Yong Co-authored-by: Fuxing Loh <4266087+fuxingloh@users.noreply.github.com> --- .../src/module.indexer/model/dftx.indexer.ts | 3 + .../src/module.indexer/model/dftx/_module.ts | 2 + .../model/dftx/create.masternode.spec.ts | 1 + .../model/dftx/create.masternode.ts | 17 +- .../model/dftx/update.masternode.spec.ts | 322 ++++++++++++++++++ .../model/dftx/update.masternode.ts | 86 +++++ apps/whale-api/src/module.model/masternode.ts | 6 + 7 files changed, 432 insertions(+), 5 deletions(-) create mode 100644 apps/whale-api/src/module.indexer/model/dftx/update.masternode.spec.ts create mode 100644 apps/whale-api/src/module.indexer/model/dftx/update.masternode.ts diff --git a/apps/whale-api/src/module.indexer/model/dftx.indexer.ts b/apps/whale-api/src/module.indexer/model/dftx.indexer.ts index 330fe64378..6048e0bc61 100644 --- a/apps/whale-api/src/module.indexer/model/dftx.indexer.ts +++ b/apps/whale-api/src/module.indexer/model/dftx.indexer.ts @@ -9,6 +9,7 @@ import { SetOracleDataIndexer } from './dftx/set.oracle.data' import { SetOracleDataIntervalIndexer } from './dftx/set.oracle.data.interval' import { CreateMasternodeIndexer } from './dftx/create.masternode' import { ResignMasternodeIndexer } from './dftx/resign.masternode' +import { UpdateMasternodeIndexer } from './dftx/update.masternode' import { Injectable, Logger } from '@nestjs/common' import { DfTxIndexer, DfTxTransaction } from './dftx/_abstract' import { PoolSwapIndexer } from './dftx/pool.swap' @@ -30,6 +31,7 @@ export class MainDfTxIndexer extends Indexer { setOracleDataInterval: SetOracleDataIntervalIndexer, createMasternode: CreateMasternodeIndexer, resignMasternode: ResignMasternodeIndexer, + updateMasternode: UpdateMasternodeIndexer, poolSwapIndexer: PoolSwapIndexer, compositeSwapIndexer: CompositeSwapIndexer, poolSwapIntervalIndexer: PoolSwapAggregatedIndexer, @@ -45,6 +47,7 @@ export class MainDfTxIndexer extends Indexer { setOracleDataInterval, createMasternode, resignMasternode, + updateMasternode, poolSwapIndexer, compositeSwapIndexer, poolSwapIntervalIndexer, diff --git a/apps/whale-api/src/module.indexer/model/dftx/_module.ts b/apps/whale-api/src/module.indexer/model/dftx/_module.ts index d31db7eb62..9c2e8f702b 100644 --- a/apps/whale-api/src/module.indexer/model/dftx/_module.ts +++ b/apps/whale-api/src/module.indexer/model/dftx/_module.ts @@ -6,6 +6,7 @@ import { SetOracleDataIndexer } from './set.oracle.data' import { SetOracleDataIntervalIndexer } from './set.oracle.data.interval' import { CreateMasternodeIndexer } from './create.masternode' import { ResignMasternodeIndexer } from './resign.masternode' +import { UpdateMasternodeIndexer } from './update.masternode' import { NetworkName } from '@defichain/jellyfish-network' import { ConfigService } from '@nestjs/config' import { PoolSwapIndexer } from './pool.swap' @@ -23,6 +24,7 @@ const indexers = [ UpdateOracleIndexer, CreateMasternodeIndexer, ResignMasternodeIndexer, + UpdateMasternodeIndexer, PoolSwapIndexer, PoolSwapAggregatedIndexer, CompositeSwapIndexer, diff --git a/apps/whale-api/src/module.indexer/model/dftx/create.masternode.spec.ts b/apps/whale-api/src/module.indexer/model/dftx/create.masternode.spec.ts index f982334db2..fd926428d2 100644 --- a/apps/whale-api/src/module.indexer/model/dftx/create.masternode.spec.ts +++ b/apps/whale-api/src/module.indexer/model/dftx/create.masternode.spec.ts @@ -114,6 +114,7 @@ describe('create masternode (pre eunos paya)', () => { expect(masternode?.creationHeight).toStrictEqual(masternodeRPCInfo.creationHeight) expect(masternode?.resignHeight).toStrictEqual(masternodeRPCInfo.resignHeight) expect(masternode?.mintedBlocks).toStrictEqual(masternodeRPCInfo.mintedBlocks) + expect(masternode?.history).toStrictEqual(undefined) }) }) diff --git a/apps/whale-api/src/module.indexer/model/dftx/create.masternode.ts b/apps/whale-api/src/module.indexer/model/dftx/create.masternode.ts index 5aaca617b1..6c5d6591e6 100644 --- a/apps/whale-api/src/module.indexer/model/dftx/create.masternode.ts +++ b/apps/whale-api/src/module.indexer/model/dftx/create.masternode.ts @@ -30,11 +30,7 @@ export class CreateMasternodeIndexer extends DfTxIndexer { // This is actually the operatorPubKeyHash but jellyfish deserializes like so if (data.operatorPubKeyHash !== undefined) { - if (data.operatorType === MasternodeKeyType.PKHashType) { - operatorAddress = P2PKH.to(this.network, data.operatorPubKeyHash).utf8String - } else { // WitV0KeyHashType - operatorAddress = P2WPKH.to(this.network, data.operatorPubKeyHash).utf8String - } + operatorAddress = CreateMasternodeIndexer.getAddress(this.network, data.operatorType, data.operatorPubKeyHash) ?? '' } await this.masternodeMapper.put({ @@ -92,6 +88,17 @@ export class CreateMasternodeIndexer extends DfTxIndexer { async invalidateBlockStart (block: RawBlock): Promise { await this.masternodeStatsMapper.delete(block.height) } + + public static getAddress (network: NetworkName, type: MasternodeKeyType, addressPubKeyHash: string): string | undefined { + if (type === MasternodeKeyType.PKHashType) { + return P2PKH.to(network, addressPubKeyHash).utf8String + } + + if (type === MasternodeKeyType.WitV0KeyHashType) { + return P2WPKH.to(network, addressPubKeyHash).utf8String + } + return undefined + } } enum MasternodeKeyType { diff --git a/apps/whale-api/src/module.indexer/model/dftx/update.masternode.spec.ts b/apps/whale-api/src/module.indexer/model/dftx/update.masternode.spec.ts new file mode 100644 index 0000000000..4a441bcb4e --- /dev/null +++ b/apps/whale-api/src/module.indexer/model/dftx/update.masternode.spec.ts @@ -0,0 +1,322 @@ +import { + createTestingApp, + DelayedEunosPayaTestContainer, + invalidateFromHeight, + stopTestingApp, + waitForIndexedHeight +} from '../../../e2e.module' +import { NestFastifyApplication } from '@nestjs/platform-fastify' +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' +import { RegTest } from '@defichain/jellyfish-network' +import { P2WPKH } from '@defichain/jellyfish-address' +import { MasternodeMapper } from '../../../module.model/masternode' +import { AddressType } from '@defichain/jellyfish-api-core/dist/category/wallet' + +describe('Update masternode', () => { + const container = new DelayedEunosPayaTestContainer() + let app: NestFastifyApplication + let client: JsonRpcClient + let masternodeId: string + let ownerAddress: string + + beforeAll(async () => { + await container.start() + await container.waitForWalletCoinbaseMaturity() + + app = await createTestingApp(container) + client = new JsonRpcClient(await container.getCachedRpcUrl()) + + await container.generate(1) + + ownerAddress = await client.wallet.getNewAddress() + masternodeId = await client.masternode.createMasternode(ownerAddress) + await container.generate(20) + + const height = await client.blockchain.getBlockCount() + // enable updating + await client.masternode.setGov({ + ATTRIBUTES: { + 'v0/params/feature/mn-setowneraddress': 'true', + 'v0/params/feature/mn-setoperatoraddress': 'true', + 'v0/params/feature/mn-setrewardaddress': 'true' + } + }) + await container.generate(70) + await waitForIndexedHeight(app, height) + }) + + afterAll(async () => { + await stopTestingApp(container, app) + }) + + it('should index update operatorAddress, ownerAddress, and rewardAddress in masternode', async () => { + const masternodeMapper = app.get(MasternodeMapper) + const masternode = await masternodeMapper.get(masternodeId) + expect(masternode).not.toStrictEqual(undefined) + + const address = await container.getNewAddress('', 'bech32') + const addressDest: P2WPKH = P2WPKH.fromAddress(RegTest, address, P2WPKH) + + await client.masternode.updateMasternode(masternodeId, { + operatorAddress: addressDest.utf8String, + rewardAddress: addressDest.utf8String, + ownerAddress: addressDest.utf8String + }) + + await container.generate(20) + const updateHeight1 = await client.blockchain.getBlockCount() + await container.generate(1) + await waitForIndexedHeight(app, updateHeight1) + + const mnResultExpectTransferring = await client.masternode.getMasternode(masternodeId) + expect(mnResultExpectTransferring[masternodeId].state).toStrictEqual('TRANSFERRING') + + await container.generate(30) + const updateHeight2 = await client.blockchain.getBlockCount() + await container.generate(1) + await waitForIndexedHeight(app, updateHeight2) + + const mnResultExpectPreEnable = await client.masternode.getMasternode(masternodeId) + expect(mnResultExpectPreEnable[masternodeId].state).toStrictEqual('PRE_ENABLED') + + await container.generate(20) + const updateHeight3 = await client.blockchain.getBlockCount() + await container.generate(1) + await waitForIndexedHeight(app, updateHeight3) + + const mnResult = await client.masternode.getMasternode(masternodeId) + expect(mnResult[masternodeId]).toStrictEqual({ + ownerAuthAddress: addressDest.utf8String, + operatorAuthAddress: addressDest.utf8String, + rewardAddress: addressDest.utf8String, + creationHeight: 103, + resignHeight: -1, + resignTx: expect.stringMatching(/[0-9a-f]{64}/), + collateralTx: expect.stringMatching(/[0-9a-f]{64}/), + state: 'ENABLED', + mintedBlocks: 0, + ownerIsMine: true, + operatorIsMine: true, + localMasternode: false, + targetMultipliers: [1, 1] + }) + + const updatedMasternode = await masternodeMapper.get(masternodeId) + expect(updatedMasternode).toStrictEqual({ + id: expect.stringMatching(/[0-9a-f]{64}/), + sort: expect.stringMatching(/[0-9a-f]{72}/), + ownerAddress: addressDest.utf8String, + operatorAddress: addressDest.utf8String, + creationHeight: 103, + resignHeight: -1, + mintedBlocks: 0, + timelock: 0, + block: { + hash: expect.stringMatching(/[0-9a-f]{64}/), + height: 103, + medianTime: expect.any(Number), + time: expect.any(Number) + }, + collateral: '2.00000000', + history: [ + { + txid: expect.stringMatching(/[0-9a-f]{64}/), + ownerAddress: addressDest.utf8String, + operatorAddress: addressDest.utf8String + }, + { + txid: expect.stringMatching(/[0-9a-f]{64}/), + ownerAddress: ownerAddress, + operatorAddress: ownerAddress + } + ] + }) + }) + + it('should index update remove rewardAddress in masternode', async () => { + const gotBlockCount = await client.blockchain.getBlockCount() + await client.masternode.setGov({ + ATTRIBUTES: { + 'v0/params/feature/mn-setowneraddress': 'true', + 'v0/params/feature/mn-setoperatoraddress': 'true', + 'v0/params/feature/mn-setrewardaddress': 'true' + } + }) + await container.generate(70) + await waitForIndexedHeight(app, gotBlockCount) + + await client.masternode.updateMasternode(masternodeId, { + rewardAddress: '' + }) + + await container.generate(1) + const updateAgainHeight = await client.blockchain.getBlockCount() + await container.generate(1) + await waitForIndexedHeight(app, updateAgainHeight) + + const gotMasternodeForRewardAgain = await client.masternode.getMasternode(masternodeId) + expect(gotMasternodeForRewardAgain[masternodeId]?.rewardAddress).toStrictEqual('') + }) +}) + +describe('invalidate', () => { + const container = new DelayedEunosPayaTestContainer() + let app: NestFastifyApplication + let client: JsonRpcClient + let masternodeMapper: MasternodeMapper + + beforeAll(async () => { + await container.start() + await container.waitForWalletCoinbaseMaturity() + + app = await createTestingApp(container) + masternodeMapper = app.get(MasternodeMapper) + client = new JsonRpcClient(await container.getCachedRpcUrl()) + + await client.masternode.setGov({ + ATTRIBUTES: { + 'v0/params/feature/mn-setowneraddress': 'true', + 'v0/params/feature/mn-setoperatoraddress': 'true', + 'v0/params/feature/mn-setrewardaddress': 'true' + } + }) + + await container.generate(1) + }) + + afterAll(async () => { + await stopTestingApp(container, app) + }) + + it('should update masternode and invalidate', async () => { + const ownerAddress = await client.wallet.getNewAddress() + const masternodeId = await client.masternode.createMasternode(ownerAddress) + await container.generate(20) + + const height = await client.blockchain.getBlockCount() + await container.generate(1) + await waitForIndexedHeight(app, height) + + const masternode = await masternodeMapper.get(masternodeId) + + expect(masternode).not.toStrictEqual(undefined) + expect(masternode?.history?.length).toStrictEqual(undefined) + + const address = await container.getNewAddress('', 'bech32') + const addressDest: P2WPKH = P2WPKH.fromAddress(RegTest, address, P2WPKH) + const addressDestHex = addressDest.utf8String + + await client.masternode.updateMasternode(masternodeId, { operatorAddress: addressDestHex }) + + await container.generate(1) + const updateHeight = await client.blockchain.getBlockCount() + await container.generate(1) + await waitForIndexedHeight(app, updateHeight) + + const updateMasternode = await masternodeMapper.get(masternodeId) + expect(updateMasternode?.operatorAddress).toStrictEqual(addressDestHex) + expect(updateMasternode?.history?.length).toStrictEqual(2) + + await invalidateFromHeight(app, container, updateHeight) + await container.generate(2) + await waitForIndexedHeight(app, updateHeight) + + { + const invalidatedMasternode = await masternodeMapper.get(masternodeId) + const initialAddressDest: P2WPKH = P2WPKH.fromAddress(RegTest, ownerAddress, P2WPKH) + const initialAddressDestHex = initialAddressDest.utf8String + + expect(invalidatedMasternode?.ownerAddress).toStrictEqual(initialAddressDestHex) + expect(invalidatedMasternode?.operatorAddress).toStrictEqual(initialAddressDestHex) + expect(invalidatedMasternode?.history?.length).toStrictEqual(1) + } + }) + + it('should update masternode and invalidate and update again', async () => { + const initialOwnerAddress = await client.wallet.getNewAddress('', AddressType.BECH32) + const masternodeId = await client.masternode.createMasternode(initialOwnerAddress) + await container.generate(20) + + const height = await client.blockchain.getBlockCount() + await container.generate(1) + await waitForIndexedHeight(app, height) + + const newAddress = await container.getNewAddress('', 'bech32') + + const updatedTxId = await client.masternode.updateMasternode(masternodeId, { + operatorAddress: P2WPKH.fromAddress(RegTest, newAddress, P2WPKH)?.utf8String + }) + + await container.generate(1) + const updateHeight = await client.blockchain.getBlockCount() + await container.generate(1) + await waitForIndexedHeight(app, updateHeight) + + { // Validate Updated + const updateMasternode = await masternodeMapper.get(masternodeId) + expect(updateMasternode).toStrictEqual(expect.objectContaining({ + operatorAddress: newAddress, + history: [ + { + txid: updatedTxId, + ownerAddress: initialOwnerAddress, + operatorAddress: newAddress + }, + { + txid: masternodeId, + ownerAddress: initialOwnerAddress, + operatorAddress: initialOwnerAddress + } + ] + })) + } + + await invalidateFromHeight(app, container, updateHeight) + await container.generate(2) + await waitForIndexedHeight(app, updateHeight) + + { // Validate Invalided + const invalidatedMasternode = await masternodeMapper.get(masternodeId) + expect(invalidatedMasternode).toStrictEqual(expect.objectContaining({ + operatorAddress: initialOwnerAddress, + ownerAddress: initialOwnerAddress, + history: [ + { + txid: masternodeId, + ownerAddress: initialOwnerAddress, + operatorAddress: initialOwnerAddress + } + ] + })) + } + + const updated2TxId = await client.masternode.updateMasternode(masternodeId, { + ownerAddress: P2WPKH.fromAddress(RegTest, newAddress, P2WPKH)?.utf8String + }) + + await container.generate(1) + const updateHeight2 = await client.blockchain.getBlockCount() + await container.generate(1) + await waitForIndexedHeight(app, updateHeight2) + + { // Validate Updated + const updateMasternode = await masternodeMapper.get(masternodeId) + expect(updateMasternode).toStrictEqual(expect.objectContaining({ + ownerAddress: newAddress, + operatorAddress: initialOwnerAddress, + history: [ + { + txid: updated2TxId, + ownerAddress: newAddress, + operatorAddress: initialOwnerAddress + }, + { + txid: masternodeId, + ownerAddress: initialOwnerAddress, + operatorAddress: initialOwnerAddress + } + ] + })) + } + }) +}) diff --git a/apps/whale-api/src/module.indexer/model/dftx/update.masternode.ts b/apps/whale-api/src/module.indexer/model/dftx/update.masternode.ts new file mode 100644 index 0000000000..415cf411e1 --- /dev/null +++ b/apps/whale-api/src/module.indexer/model/dftx/update.masternode.ts @@ -0,0 +1,86 @@ +import { DfTxIndexer, DfTxTransaction } from './_abstract' +import { CUpdateMasternode, UpdateMasternode } from '@defichain/jellyfish-transaction' +import { NetworkName } from '@defichain/jellyfish-network' +import { RawBlock } from '../_abstract' +import { Inject, Injectable } from '@nestjs/common' +import { MasternodeMapper } from '../../../module.model/masternode' +import { CreateMasternodeIndexer } from './create.masternode' + +@Injectable() +export class UpdateMasternodeIndexer extends DfTxIndexer { + OP_CODE: number = CUpdateMasternode.OP_CODE + + constructor ( + private readonly masternodeMapper: MasternodeMapper, + @Inject('NETWORK') protected readonly network: NetworkName + ) { + super() + } + + async indexTransaction (block: RawBlock, transaction: DfTxTransaction): Promise { + const masternodeId = transaction.dftx.data.nodeId + const masternode = await this.masternodeMapper.get(masternodeId) + const updates = this.getUpdates(transaction) + + if (masternode !== undefined) { + await this.masternodeMapper.put({ + ...masternode, + ownerAddress: updates.owner ?? masternode.ownerAddress, + operatorAddress: updates.operator ?? masternode.operatorAddress, + history: [ + { + txid: transaction.txn.txid, + ownerAddress: updates.owner ?? masternode.ownerAddress, + operatorAddress: updates.operator ?? masternode.operatorAddress + }, + ...masternode.history ?? [ + { + txid: masternode.id, + ownerAddress: masternode.ownerAddress, + operatorAddress: masternode.operatorAddress + } + ] + ] + }) + } + } + + async invalidateTransaction (block: RawBlock, transaction: DfTxTransaction): Promise { + const masternodeId = transaction.dftx.data.nodeId + const masternode = await this.masternodeMapper.get(masternodeId) + + if (masternode !== undefined) { + const history = masternode.history ?? [] + + await this.masternodeMapper.put({ + ...masternode, + ownerAddress: history[1].ownerAddress ?? masternode.ownerAddress, + operatorAddress: history[1].operatorAddress ?? masternode.operatorAddress, + history: history.slice(1) + }) + } + } + + /** + * 0x01 = OwnerAddress + * 0x02 = OperatorAddress + * 0x03 = SetRewardAddress + * 0x04 = RemRewardAddress + * + * @see UpdateMasternodeData + */ + private getUpdates (transaction: DfTxTransaction): { owner?: string, operator?: string } { + const updates: { owner?: string, operator?: string } = {} + + for (const update of transaction.dftx.data.updates) { + if (update.address !== undefined && update.updateType === 0x01) { + updates.owner = transaction.txn.vout[1].scriptPubKey.addresses[0] + } + if (update.address !== undefined && update.updateType === 0x02 && update.address.addressPubKeyHash !== undefined) { + updates.operator = CreateMasternodeIndexer.getAddress(this.network, update.address.addressType, update.address.addressPubKeyHash) + } + } + + return updates + } +} diff --git a/apps/whale-api/src/module.model/masternode.ts b/apps/whale-api/src/module.model/masternode.ts index 6999ecf902..75fcf4bdbd 100644 --- a/apps/whale-api/src/module.model/masternode.ts +++ b/apps/whale-api/src/module.model/masternode.ts @@ -60,4 +60,10 @@ export interface Masternode extends Model { time: number medianTime: number } + + history?: Array<{ + txid: string + ownerAddress: string + operatorAddress: string + }> }