From 1b38168af1a214d6141ce0777a3ef4d6386d187f Mon Sep 17 00:00:00 2001 From: quyvo Date: Fri, 27 Nov 2020 17:41:52 +0700 Subject: [PATCH] feat(demux): :sparkles: support get blockchain data from hyperion api --- packages/demux/src/HyperionActionReader.ts | 268 ++++++++ packages/demux/src/HyperionBlock.ts | 76 ++ packages/demux/src/index.ts | 1 + packages/demux/src/utils.ts | 32 + .../demux/test/HyperionActionReader.test.ts | 648 +++++++++++++++++ packages/demux/test/HyperionBlock.test.ts | 41 ++ packages/demux/test/hyperionRawData.ts | 649 ++++++++++++++++++ packages/demux/test/util.test.ts | 36 + packages/example-api-gateway/.env.defaults | 4 +- packages/example-api-gateway/README.md | 27 +- .../example-api-gateway/src/.env.defaults | 24 + .../example-api-gateway/src/readBlockchain.ts | 9 +- 12 files changed, 1808 insertions(+), 7 deletions(-) create mode 100644 packages/demux/src/HyperionActionReader.ts create mode 100644 packages/demux/src/HyperionBlock.ts create mode 100644 packages/demux/src/utils.ts create mode 100644 packages/demux/test/HyperionActionReader.test.ts create mode 100644 packages/demux/test/HyperionBlock.test.ts create mode 100644 packages/demux/test/hyperionRawData.ts create mode 100644 packages/demux/test/util.test.ts create mode 100644 packages/example-api-gateway/src/.env.defaults diff --git a/packages/demux/src/HyperionActionReader.ts b/packages/demux/src/HyperionActionReader.ts new file mode 100644 index 0000000..f69fb32 --- /dev/null +++ b/packages/demux/src/HyperionActionReader.ts @@ -0,0 +1,268 @@ +import * as http from 'http'; +import * as https from 'https'; +import { AbstractActionReader, NotInitializedError, ActionReaderOptions } from 'demux'; +import fetch from 'node-fetch'; +import { retry } from './utils'; +import { HyperionBlock } from './HyperionBlock'; + +export interface HyperionActionReaderOptions extends ActionReaderOptions { + hyperionEndpoint?: string; +} + +/** + * Reads from an hyperion node to get blocks of actions. + * deferred transactions and inline actions will be included, + */ +export class HyperionActionReader extends AbstractActionReader { + protected hyperionEndpoint: string; + protected httpAgent: http.Agent; + protected httpsAgent: https.Agent; + + constructor(options: HyperionActionReaderOptions = {}) { + super(options); + const hyperionEndpoint = options.hyperionEndpoint + ? options.hyperionEndpoint + : 'http://localhost:8888'; + this.hyperionEndpoint = hyperionEndpoint.replace(/\/+$/g, ''); // Removes trailing slashes + } + + /** + * Returns a http/https agent use to request. + * create new agent if not existed + */ + private getConnectionAgent(_parsedURL) { + if (_parsedURL.protocol === 'http:') { + if (!this.httpAgent) { + this.httpAgent = new http.Agent({ + keepAlive: true, + keepAliveMsecs: 3000, + }); + } + return this.httpAgent; + } else { + if (!this.httpsAgent) { + this.httpsAgent = new https.Agent({ + keepAlive: true, + keepAliveMsecs: 3000, + }); + } + return this.httpsAgent; + } + } + + private async processBlockMeta( + block: any, + numRetries: number = 120, + waitTimeMs: number = 250, + ){ + const processedTransactions = []; + for (const transaction of block.transactions) { + if (this.includeMetaAction(transaction)) { + processedTransactions.push({ + id: transaction.id, + actions: await this.getActionMeta(transaction.id, numRetries, waitTimeMs), + }) + } else { + processedTransactions.push(transaction); + } + } + + block.transactions = processedTransactions; + return block; + } + + /** + * get meta data of action. + */ + private async getActionMeta( + transactionId: string, + numRetries: number = 120, + waitTimeMs: number = 250, + ): Promise { + try { + const rawTransaction = await retry( + async () => { + return await fetch( + `${this.hyperionEndpoint}/v2/history/get_transaction?id=${transactionId}`, + { + method: 'get', + headers: { 'Content-Type': 'application/json' }, + agent: this.getConnectionAgent.bind(this), + }, + ).then(res => { + if (res.ok) { + return res.json(); + } else { + throw new Error(res.statusText); + } + }); + }, + numRetries, + waitTimeMs, + ); + + if (!rawTransaction.actions) { + return []; + } + + const actionMeta = []; + + for (const action of rawTransaction.actions) { + for (const receipt of action.receipts) { + actionMeta.push({ + receiver: receipt.receiver, + account: action.act.account, + action: action.act.name, + authorization: action.act.authorization.map(auth => { + return { account: auth.actor, permission: auth.permission }; + }), + data: action.act.data, + }); + } + } + + return actionMeta; + + } catch (err) { + this.log.error(err); + throw new Error('Error get action meta, max retries failed'); + } + } + + private includeMetaAction(transaction: any) { + for (const action of transaction.actions) { + if (action.receiver === 'eosio') { + if ( + action.action === 'newaccount' || + action.action === 'updateauth' || + action.action === 'unstaketorex' || + action.action === 'buyrex' || + action.action === 'buyram' || + action.action === 'buyrambytes' || + action.action === 'undelegatebw' || + action.action === 'delegatebw' + ) { + return true; + } + } + + if (action.action === 'transfer') { + return true; + } + } + + return false; + } + + /** + * Returns a promise for the head block number. + */ + public async getHeadBlockNumber( + numRetries: number = 120, + waitTimeMs: number = 250, + ): Promise { + try { + return await retry( + async () => { + const blockInfo = await fetch(`${this.hyperionEndpoint}/v1/chain/get_info`, { + agent: this.getConnectionAgent.bind(this), + }).then(res => { + if (res.ok) { + return res.json(); + } else { + throw new Error(res.statusText); + } + }); + return blockInfo.head_block_num; + }, + numRetries, + waitTimeMs, + ); + } catch (err) { + throw new Error('Error retrieving head block, max retries failed'); + } + } + + public async getLastIrreversibleBlockNumber( + numRetries: number = 120, + waitTimeMs: number = 250, + ): Promise { + try { + return await retry( + async () => { + const blockInfo = await fetch(`${this.hyperionEndpoint}/v1/chain/get_info`, { + agent: this.getConnectionAgent.bind(this), + }).then(res => { + if (res.ok) { + return res.json(); + } else { + throw new Error(res.statusText); + } + }); + return blockInfo.last_irreversible_block_num; + }, + numRetries, + waitTimeMs, + ); + } catch (err) { + this.log.error(err); + throw new Error('Error retrieving last irreversible block, max retries failed'); + } + } + + /** + * Returns a promise for a `NodeosBlock`. + */ + public async getBlock( + blockNumber: number, + numRetries: number = 120, + waitTimeMs: number = 250, + ): Promise { + try { + const block = await retry( + async () => { + return await fetch(`${this.hyperionEndpoint}/v1/trace_api/get_block`, { + method: 'post', + body: JSON.stringify({ block_num: blockNumber }), + headers: { 'Content-Type': 'application/json' }, + agent: this.getConnectionAgent.bind(this), + }).then(res => { + if (res.ok) { + return res.json(); + } else { + throw new Error(res.statusText); + } + }); + }, + numRetries, + waitTimeMs, + ); + + const processedBlock = await this.processBlockMeta(block, numRetries, waitTimeMs); + return new HyperionBlock(processedBlock, this.log); + } catch (err) { + this.log.error(err); + throw new Error('Error block, max retries failed'); + } + } + + protected async setup(): Promise { + if (this.initialized) { + return; + } + + try { + await fetch(`${this.hyperionEndpoint}/v1/chain/get_info`, { + agent: this.getConnectionAgent.bind(this), + }).then(res => { + if (res.ok) { + return res.json(); + } else { + throw new Error(res.statusText); + } + }); + } catch (err) { + throw new NotInitializedError('Cannot reach supplied nodeos endpoint.', err); + } + } +} diff --git a/packages/demux/src/HyperionBlock.ts b/packages/demux/src/HyperionBlock.ts new file mode 100644 index 0000000..e941ade --- /dev/null +++ b/packages/demux/src/HyperionBlock.ts @@ -0,0 +1,76 @@ +import * as Logger from 'bunyan'; +import { Block, BlockInfo } from 'demux'; +import { Action } from 'demux'; + +export interface EosAuthorization { + actor: string + permission: string +} + +export interface EosPayload { + account: string + authorization: EosAuthorization[] + data: ActionStruct + name: string + transactionId: string + actionIndex?: number + actionOrdinal?: number + producer?: string + notifiedAccounts?: string[] + receiver?: string + isContextFree?: boolean + isInline?: boolean + isNotification?: boolean + contextFreeData?: Buffer[] + transactionActions?: TransactionActions +} + +export interface EosAction extends Action { + payload: EosPayload +} + +export interface TransactionActions { + contextFreeActions: EosAction[] + actions: EosAction[] + inlineActions: EosAction[] +} + +export class HyperionBlock implements Block { + public actions: EosAction[]; + public blockInfo: BlockInfo; + constructor( + rawBlock: any, + private log: Logger, + ) { + this.blockInfo = { + blockNumber: rawBlock.number, + blockHash: rawBlock.id, + previousBlockHash: rawBlock.previous_id, + timestamp: new Date(rawBlock.timestamp), + }; + this.actions = this.collectActionsFromBlock(rawBlock); + } + + protected collectActionsFromBlock(rawBlock: any): EosAction[] { + const producer = rawBlock.producer; + return this.flattenArray(rawBlock.transactions.map((transaction: any) => { + return transaction.actions.map((action: any, actionIndex: number) => { + const block = { + type: `${action.receiver}::${action.action}`, + payload: { + producer, + transactionId: transaction.id, + actionIndex, + ...action, + }, + }; + return block + }) + })) + } + + private flattenArray(arr: any[]): any[] { + return arr.reduce((flat, toFlatten) => + flat.concat(Array.isArray(toFlatten) ? this.flattenArray(toFlatten) : toFlatten), []) + } +} diff --git a/packages/demux/src/index.ts b/packages/demux/src/index.ts index 2f32f1c..e6dcb1f 100644 --- a/packages/demux/src/index.ts +++ b/packages/demux/src/index.ts @@ -12,6 +12,7 @@ export type { DbUpdaterActionPayload } from './DbUpdater'; export { BaseHandlerVersion } from './BaseHandlerVersion'; export { AloxideActionHandler } from './AloxideActionHandler'; +export { HyperionActionReader } from './HyperionActionReader'; export { DbUpdater } from './DbUpdater'; export { AloxideDataManager } from './AloxideDataManager'; export { indexStateSchema } from './indexStateSchema'; diff --git a/packages/demux/src/utils.ts b/packages/demux/src/utils.ts new file mode 100644 index 0000000..603fcfd --- /dev/null +++ b/packages/demux/src/utils.ts @@ -0,0 +1,32 @@ +import Logger from 'bunyan'; + +function wait(ms: number): Promise { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} + +async function retry( + func: () => Promise, + maxNumAttempts: number, + waitMs: number, + log?: Logger, +): Promise { + let numAttempts = 1; + while (numAttempts <= maxNumAttempts) { + try { + return await func(); + } catch (err) { + if (numAttempts === maxNumAttempts) { + throw err; + } + } + numAttempts += 1; + log?.debug('retry count:', numAttempts); + + await wait(waitMs); + } + throw new Error(`${maxNumAttempts} retries failed`); +} + +export { retry }; diff --git a/packages/demux/test/HyperionActionReader.test.ts b/packages/demux/test/HyperionActionReader.test.ts new file mode 100644 index 0000000..ada1fd0 --- /dev/null +++ b/packages/demux/test/HyperionActionReader.test.ts @@ -0,0 +1,648 @@ +import * as http from 'http'; +import * as https from 'https'; +import { NotInitializedError } from 'demux'; +import { hyperionRawBlock, hyperionRawTransaction, hyperionActionMeta } from './hyperionRawData'; +import { HyperionActionReader } from '../src/HyperionActionReader'; +jest.mock('node-fetch'); +import fetch from 'node-fetch'; + +describe('NodeosActionReader', () => { + let reader: HyperionActionReader; + const hyperionEndpoint = 'https://hyperion.testnet.canfoundation.io'; + + beforeEach(() => { + jest.resetAllMocks(); + reader = new HyperionActionReader({ + hyperionEndpoint, + startAtBlock: 10, + onlyIrreversible: false, + }); + }); + + it('should initilize reader', async () => { + const localReader = new HyperionActionReader({ + startAtBlock: 10, + onlyIrreversible: false, + }); + + const onlineReader = new HyperionActionReader({ + hyperionEndpoint, + startAtBlock: 10, + onlyIrreversible: false, + }); + + const defaultEndpoint = new HyperionActionReader(); + + // @ts-ignore + expect(localReader.hyperionEndpoint).toBe('http://localhost:8888'); + // @ts-ignore + expect(onlineReader.hyperionEndpoint).toBe(hyperionEndpoint); + // @ts-ignore + expect(defaultEndpoint.hyperionEndpoint).toBe('http://localhost:8888'); + }); + + it('should get connection agent', async () => { + const httpURL = { + protocol: 'http:', + }; + + const httpsURL = { + protocol: 'https:', + }; + + // @ts-ignore + const httpConnection = await reader.getConnectionAgent(httpURL); + + // @ts-ignore + const currentHttpConnection = await reader.getConnectionAgent(httpURL); + + // @ts-ignore + const httpsConnection = await reader.getConnectionAgent(httpsURL); + + // @ts-ignore + const currentHttpsConnection = await reader.getConnectionAgent(httpsURL); + + expect(httpConnection).toBeInstanceOf(http.Agent); + expect(httpsConnection).toBeInstanceOf(https.Agent); + expect(currentHttpConnection).toBe(currentHttpConnection); + expect(currentHttpsConnection).toBe(httpsConnection); + }); + + it('should check transaction include meta action', async () => { + const sampleAction = { + receiver: 'eosio', + account: 'daniel111111', + action: 'newaccount', + authorization: [ + { + account: 'daniel111111', + permission: 'active', + }, + ], + }; + + const newAccountTransaction = { + id: '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + actions: [sampleAction], + }; + + const updateAuthTransaction = { + id: '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + actions: [{ ...sampleAction, action: 'updateauth' }], + }; + + const unstaketorexTransaction = { + id: '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + actions: [{ ...sampleAction, action: 'unstaketorex' }], + }; + + const buyrexTransaction = { + id: '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + actions: [{ ...sampleAction, action: 'buyrex' }], + }; + + const buyramTransaction = { + id: '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + actions: [{ ...sampleAction, action: 'buyram' }], + }; + + const buyrambytesTransaction = { + id: '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + actions: [{ ...sampleAction, action: 'buyrambytes' }], + }; + + const undelegatebwTransaction = { + id: '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + actions: [{ ...sampleAction, action: 'undelegatebw' }], + }; + + const delegatebwTransaction = { + id: '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + actions: [{ ...sampleAction, action: 'delegatebw' }], + }; + + const linkauthTransaction = { + id: '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + actions: [{ ...sampleAction, action: 'linkauth' }], + }; + + // @ts-ignore + const result1 = await reader.includeMetaAction(newAccountTransaction); + // @ts-ignore + const result2 = await reader.includeMetaAction(updateAuthTransaction); + // @ts-ignore + const result3 = await reader.includeMetaAction(unstaketorexTransaction); + // @ts-ignore + const result4 = await reader.includeMetaAction(buyrexTransaction); + // @ts-ignore + const result5 = await reader.includeMetaAction(buyramTransaction); + // @ts-ignore + const result6 = await reader.includeMetaAction(buyrambytesTransaction); + // @ts-ignore + const result7 = await reader.includeMetaAction(undelegatebwTransaction); + // @ts-ignore + const result8 = await reader.includeMetaAction(delegatebwTransaction); + // @ts-ignore + const result9 = await reader.includeMetaAction(linkauthTransaction); + + expect( + result1 && result2 && result3 && result4 && result5 && result6 && result7 && result8, + ).toBe(true); + + expect(result9).toBe(false); + }); + + it('should get action meta', async () => { + const transactionId = '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2'; + const response = Promise.resolve({ + ok: true, + status: 200, + json: () => { + return hyperionRawTransaction; + }, + }); + + // @ts-ignore + fetch.mockReturnValueOnce(response); + + // @ts-ignore + const actionMeta = await reader.getActionMeta(transactionId); + expect(actionMeta.length).toBe(10); + expect(actionMeta[0].receiver).toBe(hyperionRawTransaction.actions[0].receipts[0].receiver); + }); + + it('should get action meta return empty if transaction has no action', async () => { + const transactionId = '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2'; + const response = Promise.resolve({ + ok: true, + status: 200, + json: () => { + return { + executed: true, + trx_id: '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + lib: 27962634, + query_time_ms: 31.314, + }; + }, + }); + + // @ts-ignore + fetch.mockReturnValueOnce(response); + + // @ts-ignore + const actionMeta = await reader.getActionMeta(transactionId); + expect(actionMeta.length).toBe(0); + }); + + it('should get action meta retry request', async () => { + const transactionId = '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2'; + const responseError = Promise.resolve({ + ok: false, + status: 500, + statusText: 'Internal server error', + }); + + const responseSuccess = Promise.resolve({ + ok: true, + status: 200, + json: () => { + return hyperionRawTransaction; + }, + }); + + // @ts-ignore + fetch.mockReturnValueOnce(responseError); + // @ts-ignore + fetch.mockReturnValueOnce(responseSuccess); + + // @ts-ignore + const actionMeta = await reader.getActionMeta(transactionId); + expect(actionMeta.length).toBe(10); + expect(actionMeta[0].receiver).toBe(hyperionRawTransaction.actions[0].receipts[0].receiver); + expect(fetch).toBeCalledTimes(2); + // @ts-ignore + expect(fetch.mock.calls[0][0]).toBe( + `${hyperionEndpoint}/v2/history/get_transaction?id=${transactionId}`, + ); + }); + + it('should get action meta throw error if max retry request failed', async () => { + const transactionId = '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2'; + const responseError = Promise.resolve({ + ok: false, + status: 500, + statusText: 'Internal server error', + }); + + // @ts-ignore + fetch.mockReturnValueOnce(responseError); + + // @ts-ignore + await expect(reader.getActionMeta(transactionId, 1, 100)).rejects.toThrowError( + 'Error get action meta, max retries failed', + ); + expect(fetch).toBeCalledTimes(1); + // @ts-ignore + expect(fetch.mock.calls[0][0]).toBe( + `${hyperionEndpoint}/v2/history/get_transaction?id=${transactionId}`, + ); + }); + + it('should process block meta', async () => { + // @ts-ignore + const mockGetActionMeta = jest.spyOn(reader, 'getActionMeta'); + // @ts-ignore + mockGetActionMeta.mockResolvedValueOnce(hyperionActionMeta); + + // @ts-ignore + const processedBlockResult = await reader.processBlockMeta(hyperionRawBlock); + expect(processedBlockResult.transactions[0].actions.length).toBe(10); + expect(processedBlockResult.transactions[0].actions[1].receiver).toBe('eosio.token'); + expect(processedBlockResult.transactions[0].actions[1].data.from).toBe('iyeqsiu44wv4'); + }); + + it('should process block meta without meta action', async () => { + // @ts-ignore + const mockGetActionMeta = jest.spyOn(reader, 'getActionMeta'); + // @ts-ignore + mockGetActionMeta.mockResolvedValueOnce(hyperionActionMeta); + + // @ts-ignore + const processedBlockResult = await reader.processBlockMeta({ + query_time_ms: 12.014, + id: '01a808454f3aa21a1602478e2dbdbc5ebf0b735b226cdc344ea4e4a9d7141276', + number: 27789381, + previous_id: '01a80844388a2285339b279b14cd25cb32e905edfeb2895a7e47b5c8e1da99dd', + status: 'irreversible', + timestamp: '2020-11-26T04:24:13.000', + producer: 'test3.bp', + transactions: [ + { + id: '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + actions: [ + { + receiver: 'cpu', + account: 'cpu', + action: 'freecpunet', + authorization: [ + { + account: 'cpu', + permission: 'active', + }, + ], + data: { + to: 'iyeqsiu44wv4', + memo: 'pay cpu and net', + }, + }, + ], + }, + ], + }); + expect(processedBlockResult.transactions[0].actions.length).toBe(1); + expect(processedBlockResult.transactions[0].actions[0].receiver).toBe('cpu'); + expect(processedBlockResult.transactions[0].actions[0].data.to).toBe('iyeqsiu44wv4'); + + expect(mockGetActionMeta).toBeCalledTimes(0); + }); + + it('returns head block number', async () => { + const blockInfo = { + server_version: '3ce1a372', + chain_id: '353c0a7c6744e58778a2a334d1da2303eb12a111cc636bb494e63a84c9e7ffeb', + head_block_num: 27982695, + last_irreversible_block_num: 27982650, + last_irreversible_block_id: + '01aafb3a81f6607ec7d8812a576c19b5c21429ad0c3aec5bcc3eeb9f2bc67fb5', + head_block_id: '01aafb67640063451af8bae0827c05541ae252253b9a63dde18e3351fe0ebb68', + head_block_time: '2020-11-27T07:15:10.000', + head_block_producer: 'test2.bp', + virtual_block_cpu_limit: 100000000, + virtual_block_net_limit: 1048576000, + block_cpu_limit: 99900, + block_net_limit: 1048576, + server_version_string: 'v2.0.2', + fork_db_head_block_num: 27982695, + fork_db_head_block_id: '01aafb67640063451af8bae0827c05541ae252253b9a63dde18e3351fe0ebb68', + server_full_version_string: 'v2.0.2-3ce1a372159d5e1b65f9031b29f8def631ee5e93', + }; + const responseSuccess = Promise.resolve({ + ok: true, + status: 200, + json: () => { + return blockInfo; + }, + }); + // @ts-ignore + fetch.mockReturnValueOnce(responseSuccess); + const blockNum = await reader.getHeadBlockNumber(); + expect(blockNum).toBe(blockInfo.head_block_num); + }); + + it('returns head block number retry', async () => { + const responseError = Promise.resolve({ + ok: false, + status: 500, + statusText: 'Internal server error', + }); + + const blockInfo = { + server_version: '3ce1a372', + chain_id: '353c0a7c6744e58778a2a334d1da2303eb12a111cc636bb494e63a84c9e7ffeb', + head_block_num: 27982695, + last_irreversible_block_num: 27982650, + last_irreversible_block_id: + '01aafb3a81f6607ec7d8812a576c19b5c21429ad0c3aec5bcc3eeb9f2bc67fb5', + head_block_id: '01aafb67640063451af8bae0827c05541ae252253b9a63dde18e3351fe0ebb68', + head_block_time: '2020-11-27T07:15:10.000', + head_block_producer: 'test2.bp', + virtual_block_cpu_limit: 100000000, + virtual_block_net_limit: 1048576000, + block_cpu_limit: 99900, + block_net_limit: 1048576, + server_version_string: 'v2.0.2', + fork_db_head_block_num: 27982695, + fork_db_head_block_id: '01aafb67640063451af8bae0827c05541ae252253b9a63dde18e3351fe0ebb68', + server_full_version_string: 'v2.0.2-3ce1a372159d5e1b65f9031b29f8def631ee5e93', + }; + + const responseSuccess = Promise.resolve({ + ok: true, + status: 200, + json: () => { + return blockInfo; + }, + }); + + // @ts-ignore + fetch.mockReturnValueOnce(responseError); + + // @ts-ignore + fetch.mockReturnValueOnce(responseSuccess); + + const blockNum = await reader.getHeadBlockNumber(); + expect(blockNum).toBe(blockInfo.head_block_num); + // @ts-ignore + expect(fetch).toBeCalledTimes(2); + // @ts-ignore + expect(fetch.mock.calls[0][0]).toBe(`${hyperionEndpoint}/v1/chain/get_info`); + }); + + it('returns head block number throw error max retries failed', async () => { + const responseError = Promise.resolve({ + ok: false, + status: 500, + statusText: 'Internal server error', + }); + + // @ts-ignore + fetch.mockReturnValueOnce(responseError); + + await expect(reader.getHeadBlockNumber(1, 100)).rejects.toThrowError( + 'Error retrieving head block, max retries failed', + ); + // @ts-ignore + expect(fetch).toBeCalledTimes(1); + // @ts-ignore + expect(fetch.mock.calls[0][0]).toBe(`${hyperionEndpoint}/v1/chain/get_info`); + }); + + it('returns last irreversible block number', async () => { + const blockInfo = { + server_version: '3ce1a372', + chain_id: '353c0a7c6744e58778a2a334d1da2303eb12a111cc636bb494e63a84c9e7ffeb', + head_block_num: 27982695, + last_irreversible_block_num: 27982650, + last_irreversible_block_id: + '01aafb3a81f6607ec7d8812a576c19b5c21429ad0c3aec5bcc3eeb9f2bc67fb5', + head_block_id: '01aafb67640063451af8bae0827c05541ae252253b9a63dde18e3351fe0ebb68', + head_block_time: '2020-11-27T07:15:10.000', + head_block_producer: 'test2.bp', + virtual_block_cpu_limit: 100000000, + virtual_block_net_limit: 1048576000, + block_cpu_limit: 99900, + block_net_limit: 1048576, + server_version_string: 'v2.0.2', + fork_db_head_block_num: 27982695, + fork_db_head_block_id: '01aafb67640063451af8bae0827c05541ae252253b9a63dde18e3351fe0ebb68', + server_full_version_string: 'v2.0.2-3ce1a372159d5e1b65f9031b29f8def631ee5e93', + }; + const responseSuccess = Promise.resolve({ + ok: true, + status: 200, + json: () => { + return blockInfo; + }, + }); + // @ts-ignore + fetch.mockReturnValueOnce(responseSuccess); + const lastIrreversibleBlockNum = await reader.getLastIrreversibleBlockNumber(); + expect(lastIrreversibleBlockNum).toBe(blockInfo.last_irreversible_block_num); + }); + + it('returns last irreversible block number retry', async () => { + const responseError = Promise.resolve({ + ok: false, + status: 500, + statusText: 'Internal server error', + }); + + const blockInfo = { + server_version: '3ce1a372', + chain_id: '353c0a7c6744e58778a2a334d1da2303eb12a111cc636bb494e63a84c9e7ffeb', + head_block_num: 27982695, + last_irreversible_block_num: 27982650, + last_irreversible_block_id: + '01aafb3a81f6607ec7d8812a576c19b5c21429ad0c3aec5bcc3eeb9f2bc67fb5', + head_block_id: '01aafb67640063451af8bae0827c05541ae252253b9a63dde18e3351fe0ebb68', + head_block_time: '2020-11-27T07:15:10.000', + head_block_producer: 'test2.bp', + virtual_block_cpu_limit: 100000000, + virtual_block_net_limit: 1048576000, + block_cpu_limit: 99900, + block_net_limit: 1048576, + server_version_string: 'v2.0.2', + fork_db_head_block_num: 27982695, + fork_db_head_block_id: '01aafb67640063451af8bae0827c05541ae252253b9a63dde18e3351fe0ebb68', + server_full_version_string: 'v2.0.2-3ce1a372159d5e1b65f9031b29f8def631ee5e93', + }; + + const responseSuccess = Promise.resolve({ + ok: true, + status: 200, + json: () => { + return blockInfo; + }, + }); + + // @ts-ignore + fetch.mockReturnValueOnce(responseError); + + // @ts-ignore + fetch.mockReturnValueOnce(responseSuccess); + + const blockNum = await reader.getLastIrreversibleBlockNumber(); + expect(blockNum).toBe(blockInfo.last_irreversible_block_num); + // @ts-ignore + expect(fetch).toBeCalledTimes(2); + // @ts-ignore + expect(fetch.mock.calls[0][0]).toBe(`${hyperionEndpoint}/v1/chain/get_info`); + }); + + it('returns last irreversible block number throw error max retries failed', async () => { + const responseError = Promise.resolve({ + ok: false, + status: 500, + statusText: 'Internal server error', + }); + + // @ts-ignore + fetch.mockReturnValueOnce(responseError); + + await expect(reader.getLastIrreversibleBlockNumber(1, 100)).rejects.toThrowError( + 'Error retrieving last irreversible block, max retries failed', + ); + // @ts-ignore + expect(fetch).toBeCalledTimes(1); + // @ts-ignore + expect(fetch.mock.calls[0][0]).toBe(`${hyperionEndpoint}/v1/chain/get_info`); + }); + + it('gets block with correct block number', async () => { + const responseSuccess = Promise.resolve({ + ok: true, + status: 200, + json: () => { + return hyperionRawBlock; + }, + }); + + // @ts-ignore + fetch.mockReturnValueOnce(responseSuccess); + // @ts-ignore + const mockGetActionMeta = jest.spyOn(reader, 'getActionMeta'); + // @ts-ignore + mockGetActionMeta.mockResolvedValueOnce(hyperionActionMeta); + const block = await reader.getBlock(hyperionRawBlock.number); + expect(block.blockInfo.blockNumber).toEqual(hyperionRawBlock.number); + expect(block.actions.length).toBe(10); + // @ts-ignore + expect(fetch).toBeCalledTimes(1); + // @ts-ignore + expect(fetch.mock.calls[0][0]).toBe(`${hyperionEndpoint}/v1/trace_api/get_block`); + }); + + it('gets block with correct block number retry', async () => { + const responseError = Promise.resolve({ + ok: false, + status: 500, + statusText: 'Internal server error', + }); + + // @ts-ignore + fetch.mockReturnValueOnce(responseError); + + const responseSuccess = Promise.resolve({ + ok: true, + status: 200, + json: () => { + return hyperionRawBlock; + }, + }); + + // @ts-ignore + fetch.mockReturnValueOnce(responseSuccess); + // @ts-ignore + const mockGetActionMeta = jest.spyOn(reader, 'getActionMeta'); + // @ts-ignore + mockGetActionMeta.mockResolvedValueOnce(hyperionActionMeta); + const block = await reader.getBlock(hyperionRawBlock.number); + expect(block.blockInfo.blockNumber).toEqual(hyperionRawBlock.number); + expect(block.actions.length).toBe(10); + // @ts-ignore + expect(fetch).toBeCalledTimes(2); + // @ts-ignore + expect(fetch.mock.calls[0][0]).toBe(`${hyperionEndpoint}/v1/trace_api/get_block`); + }); + + it('gets block with correct block number throw error max retries failed', async () => { + const responseError = Promise.resolve({ + ok: false, + status: 500, + statusText: 'Internal server error', + }); + + // @ts-ignore + fetch.mockReturnValueOnce(responseError); + await expect(reader.getBlock(hyperionRawBlock.number, 1, 100)).rejects.toThrowError( + 'Error block, max retries failed', + ); + // @ts-ignore + expect(fetch).toBeCalledTimes(1); + // @ts-ignore + expect(fetch.mock.calls[0][0]).toBe(`${hyperionEndpoint}/v1/trace_api/get_block`); + }); + + it('throws if not correctly initialized', async () => { + const responseError = Promise.resolve({ + ok: false, + status: 500, + statusText: 'Internal server error', + }); + + // @ts-ignore + fetch.mockReturnValueOnce(responseError); + + const result = reader.getNextBlock(); + await expect(result).rejects.toThrow(NotInitializedError); + }); + + it('do not initialize again', async () => { + // @ts-ignore + reader.initialized = true; + + // @ts-ignore + const result = await reader.setup(); + expect(fetch).toBeCalledTimes(0); + }); + + it('should initialize successfully', async () => { + const blockInfo = { + server_version: '3ce1a372', + chain_id: '353c0a7c6744e58778a2a334d1da2303eb12a111cc636bb494e63a84c9e7ffeb', + head_block_num: 27982695, + last_irreversible_block_num: 27982650, + last_irreversible_block_id: + '01aafb3a81f6607ec7d8812a576c19b5c21429ad0c3aec5bcc3eeb9f2bc67fb5', + head_block_id: '01aafb67640063451af8bae0827c05541ae252253b9a63dde18e3351fe0ebb68', + head_block_time: '2020-11-27T07:15:10.000', + head_block_producer: 'test2.bp', + virtual_block_cpu_limit: 100000000, + virtual_block_net_limit: 1048576000, + block_cpu_limit: 99900, + block_net_limit: 1048576, + server_version_string: 'v2.0.2', + fork_db_head_block_num: 27982695, + fork_db_head_block_id: '01aafb67640063451af8bae0827c05541ae252253b9a63dde18e3351fe0ebb68', + server_full_version_string: 'v2.0.2-3ce1a372159d5e1b65f9031b29f8def631ee5e93', + }; + + const responseSuccess = Promise.resolve({ + ok: true, + status: 200, + json: () => { + return blockInfo; + }, + }); + // @ts-ignore + reader.initialized = false; + + // @ts-ignore + fetch.mockReturnValueOnce(responseSuccess); + // @ts-ignore + await reader.setup(); + expect(fetch).toBeCalledTimes(1); + // @ts-ignore + expect(fetch.mock.calls[0][0]).toBe(`${hyperionEndpoint}/v1/chain/get_info`); + }); +}); diff --git a/packages/demux/test/HyperionBlock.test.ts b/packages/demux/test/HyperionBlock.test.ts new file mode 100644 index 0000000..c730f13 --- /dev/null +++ b/packages/demux/test/HyperionBlock.test.ts @@ -0,0 +1,41 @@ +import * as Logger from 'bunyan'; +import { HyperionBlock } from '../src/HyperionBlock'; +import { processedBlockMeta } from './hyperionRawData'; + +const log = Logger.createLogger({ name: 'HyperionBlockTest', level: 'error' }); + +describe('NodeosBlock', () => { + let eosBlock: HyperionBlock; + + beforeEach(() => { + eosBlock = new HyperionBlock(processedBlockMeta, log); + }); + + it('collects actions from blocks', async () => { + const { actions, blockInfo } = eosBlock; + expect(blockInfo.blockHash).toEqual( + '01a808454f3aa21a1602478e2dbdbc5ebf0b735b226cdc344ea4e4a9d7141276', + ); + expect(actions[0]).toEqual({ + type: 'cpu::freecpunet', + payload: { + producer: 'test3.bp', + transactionId: '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + actionIndex: 0, + receiver: 'cpu', + account: 'cpu', + action: 'freecpunet', + authorization: [ + { + account: 'cpu', + permission: 'active', + }, + ], + data: { + to: 'iyeqsiu44wv4', + memo: 'pay cpu and net', + }, + }, + }); + }); +}); diff --git a/packages/demux/test/hyperionRawData.ts b/packages/demux/test/hyperionRawData.ts new file mode 100644 index 0000000..73bdf1f --- /dev/null +++ b/packages/demux/test/hyperionRawData.ts @@ -0,0 +1,649 @@ +export const hyperionRawBlock = { + query_time_ms: 12.014, + id: '01a808454f3aa21a1602478e2dbdbc5ebf0b735b226cdc344ea4e4a9d7141276', + number: 27789381, + previous_id: '01a80844388a2285339b279b14cd25cb32e905edfeb2895a7e47b5c8e1da99dd', + status: 'irreversible', + timestamp: '2020-11-26T04:24:13.000', + producer: 'test3.bp', + transactions: [ + { + id: '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + actions: [ + { + receiver: 'cpu', + account: 'cpu', + action: 'freecpunet', + authorization: [ + { + account: 'cpu', + permission: 'active' + } + ], + data: { + to: 'iyeqsiu44wv4', + memo: 'pay cpu and net' + } + }, + { + receiver: 'eosio.token', + account: 'eosio.token', + action: 'transfer', + authorization: [ + { + account: 'iyeqsiu44wv4', + permission: 'active' + } + ], + data: { + 'quantity': '0.1123 CAT' + } + }, + { + receiver: 'iyeqsiu44wv4', + account: 'eosio.token', + action: 'transfer', + authorization: [ + { + account: 'iyeqsiu44wv4', + permission: 'active' + } + ], + data: { + quantity: '0.1123 CAT' + } + }, + { + receiver: '511nh3eth2bq', + account: 'eosio.token', + action: 'transfer', + authorization: [ + { + account: 'iyeqsiu44wv4', + permissio: 'active' + } + ], + data: { + quantity: '0.1123 CAT' + } + }, + { + receive: 'eosio.token', + account: 'eosio.token', + action: 'transfer', + authorization: [ + { + account: 'iyeqsiu44wv4', + permission: 'active' + } + ], + data: { + quantity: '0.1123 CAT' + } + }, + { + receiver: 'iyeqsiu44wv4', + account: 'eosio.token', + action: 'transfer', + authorization: [ + { + account: 'iyeqsiu44wv4', + permission: 'active' + } + ], + data: { + quantity: '0.1123 CAT' + } + }, + { + receiver: 'yxgqwv4yrtxx', + account: 'eosio.token', + action: 'transfer', + authorization: [ + { + account: 'iyeqsiu44wv4', + permission: 'active' + } + ], + data: { + quantity: '0.1123 CAT' + } + }, + { + receiver: 'eosio.token', + account: 'eosio.token', + action: 'transfer', + authorization: [ + { + account: 'iyeqsiu44wv4', + permission: 'active' + } + ], + data: { + quantity: '0.1123 CAT' + } + }, + { + receiver: 'iyeqsiu44wv4', + account: 'eosio.token', + action: 'transfer', + authorization: [ + { + account: 'iyeqsiu44wv4', + permission: 'active' + } + ], + data: { + quantity: '0.1123 CAT' + } + }, + { + receiver: 'mzyqfirhysjn', + account: 'eosio.token', + action: 'transfer', + authorization: [ + { + account: 'iyeqsiu44wv4', + permission: 'active', + } + ], + data: { + quantity: '0.1123 CAT', + } + } + ] + } + ] +}; + +export const hyperionRawTransaction = { + 'executed': true, + 'trx_id': '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + 'lib': 27962634, + 'actions': [ + { + 'action_ordinal': 1, + 'creator_action_ordinal': 0, + 'act': { + 'account': 'cpu', + 'name': 'freecpunet', + 'authorization': [ + { + 'actor': 'cpu', + 'permission': 'active' + } + ], + 'data': { + 'to': 'iyeqsiu44wv4', + 'memo': 'pay cpu and net' + } + }, + 'context_free': false, + 'elapsed': '0', + '@timestamp': '2020-11-26T04:24:13.000', + 'block_num': 27789381, + 'producer': 'test3.bp', + 'trx_id': '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + 'global_sequence': 27863444, + 'cpu_usage_us': 558, + 'net_usage_words': 44, + 'signatures': [ + 'SIG_K1_Jugie45hLE2i9ohMQWorfG2CbY6Ksd4psqTmwRCHwWajHEhoyuBpYkZcD79iq52iDtWvmCmgNk1cGVZMvc91bLggsM9CzN', + 'SIG_K1_Jwp8qcrA8zQzng9wgpuRR4ipnydaodUEsMbjkYx4xPPTgGAkFzeeFLtxSxyCjQgzjvCjgQchHccsVkbx88DefbmKGBdyXE' + ], + 'inline_count': 9, + 'inline_filtered': false, + 'receipts': [ + { + 'receiver': 'cpu', + 'global_sequence': '27863444', + 'recv_sequence': '15774', + 'auth_sequence': [ + { + 'account': 'cpu', + 'sequence': '19652' + } + ] + } + ], + 'code_sequence': 1, + 'abi_sequence': 1, + 'notified': [ + 'cpu' + ], + 'timestamp': '2020-11-26T04:24:13.000' + }, + { + 'action_ordinal': 2, + 'creator_action_ordinal': 0, + 'act': { + 'account': 'eosio.token', + 'name': 'transfer', + 'authorization': [ + { + 'actor': 'iyeqsiu44wv4', + 'permission': 'active' + } + ], + 'data': { + 'from': 'iyeqsiu44wv4', + 'to': '511nh3eth2bq', + 'amount': 0.1123, + 'symbol': 'CAT', + 'memo': 'Heloo', + 'quantity': '0.1123 CAT' + } + }, + 'context_free': false, + 'elapsed': '0', + 'account_ram_deltas': [ + { + 'account': '511nh3eth2bq', + 'delta': '-128' + }, + { + 'account': 'iyeqsiu44wv4', + 'delta': '128' + } + ], + '@timestamp': '2020-11-26T04:24:13.000', + 'block_num': 27789381, + 'producer': 'test3.bp', + 'trx_id': '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + 'global_sequence': 27863445, + 'receipts': [ + { + 'receiver': 'eosio.token', + 'global_sequence': '27863445', + 'recv_sequence': '5578', + 'auth_sequence': [ + { + 'account': 'iyeqsiu44wv4', + 'sequence': '19' + } + ] + }, + { + 'receiver': 'iyeqsiu44wv4', + 'global_sequence': '27863446', + 'recv_sequence': '24', + 'auth_sequence': [ + { + 'account': 'iyeqsiu44wv4', + 'sequence': '20' + } + ] + }, + { + 'receiver': '511nh3eth2bq', + 'global_sequence': '27863447', + 'recv_sequence': '579', + 'auth_sequence': [ + { + 'account': 'iyeqsiu44wv4', + 'sequence': '21' + } + ] + } + ], + 'code_sequence': 1, + 'abi_sequence': 1, + 'notified': [ + 'eosio.token', + 'iyeqsiu44wv4', + '511nh3eth2bq' + ], + 'timestamp': '2020-11-26T04:24:13.000' + }, + { + 'action_ordinal': 3, + 'creator_action_ordinal': 0, + 'act': { + 'account': 'eosio.token', + 'name': 'transfer', + 'authorization': [ + { + 'actor': 'iyeqsiu44wv4', + 'permission': 'active' + } + ], + 'data': { + 'from': 'iyeqsiu44wv4', + 'to': 'yxgqwv4yrtxx', + 'amount': 0.1123, + 'symbol': 'CAT', + 'memo': 'Heloo', + 'quantity': '0.1123 CAT' + } + }, + 'context_free': false, + 'elapsed': '0', + '@timestamp': '2020-11-26T04:24:13.000', + 'block_num': 27789381, + 'producer': 'test3.bp', + 'trx_id': '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + 'global_sequence': 27863448, + 'receipts': [ + { + 'receiver': 'eosio.token', + 'global_sequence': '27863448', + 'recv_sequence': '5579', + 'auth_sequence': [ + { + 'account': 'iyeqsiu44wv4', + 'sequence': '22' + } + ] + }, + { + 'receiver': 'iyeqsiu44wv4', + 'global_sequence': '27863449', + 'recv_sequence': '25', + 'auth_sequence': [ + { + 'account': 'iyeqsiu44wv4', + 'sequence': '23' + } + ] + }, + { + 'receiver': 'yxgqwv4yrtxx', + 'global_sequence': '27863450', + 'recv_sequence': '29', + 'auth_sequence': [ + { + 'account': 'iyeqsiu44wv4', + 'sequence': '24' + } + ] + } + ], + 'code_sequence': 1, + 'abi_sequence': 1, + 'notified': [ + 'eosio.token', + 'iyeqsiu44wv4', + 'yxgqwv4yrtxx' + ], + 'timestamp': '2020-11-26T04:24:13.000' + }, + { + 'action_ordinal': 4, + 'creator_action_ordinal': 0, + 'act': { + 'account': 'eosio.token', + 'name': 'transfer', + 'authorization': [ + { + 'actor': 'iyeqsiu44wv4', + 'permission': 'active' + } + ], + 'data': { + 'from': 'iyeqsiu44wv4', + 'to': 'mzyqfirhysjn', + 'amount': 0.1123, + 'symbol': 'CAT', + 'memo': 'Heloo', + 'quantity': '0.1123 CAT' + } + }, + 'context_free': false, + 'elapsed': '0', + '@timestamp': '2020-11-26T04:24:13.000', + 'block_num': 27789381, + 'producer': 'test3.bp', + 'trx_id': '637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2', + 'global_sequence': 27863451, + 'receipts': [ + { + 'receiver': 'eosio.token', + 'global_sequence': '27863451', + 'recv_sequence': '5580', + 'auth_sequence': [ + { + 'account': 'iyeqsiu44wv4', + 'sequence': '25' + } + ] + }, + { + 'receiver': 'iyeqsiu44wv4', + 'global_sequence': '27863452', + 'recv_sequence': '26', + 'auth_sequence': [ + { + 'account': 'iyeqsiu44wv4', + 'sequence': '26' + } + ] + }, + { + 'receiver': 'mzyqfirhysjn', + 'global_sequence': '27863453', + 'recv_sequence': '28', + 'auth_sequence': [ + { + 'account': 'iyeqsiu44wv4', + 'sequence': '27' + } + ] + } + ], + 'code_sequence': 1, + 'abi_sequence': 1, + 'notified': [ + 'eosio.token', + 'iyeqsiu44wv4', + 'mzyqfirhysjn' + ], + 'timestamp': '2020-11-26T04:24:13.000' + } + ], + 'query_time_ms': 31.314 +}; + +export const hyperionActionMeta = [ + { + "receiver": "cpu", + "account": "cpu", + "action": "freecpunet", + "authorization": [ + { + "account": "cpu", + "permission": "active" + } + ], + "data": { + "to": "iyeqsiu44wv4", + "memo": "pay cpu and net" + } + }, + { + "receiver": "eosio.token", + "account": "eosio.token", + "action": "transfer", + "authorization": [ + { + "account": "iyeqsiu44wv4", + "permission": "active" + } + ], + "data": { + "from": "iyeqsiu44wv4", + "to": "511nh3eth2bq", + "amount": 0.1123, + "symbol": "CAT", + "memo": "Heloo", + "quantity": "0.1123 CAT" + } + }, + { + "receiver": "iyeqsiu44wv4", + "account": "eosio.token", + "action": "transfer", + "authorization": [ + { + "account": "iyeqsiu44wv4", + "permission": "active" + } + ], + "data": { + "from": "iyeqsiu44wv4", + "to": "511nh3eth2bq", + "amount": 0.1123, + "symbol": "CAT", + "memo": "Heloo", + "quantity": "0.1123 CAT" + } + }, + { + "receiver": "511nh3eth2bq", + "account": "eosio.token", + "action": "transfer", + "authorization": [ + { + "account": "iyeqsiu44wv4", + "permission": "active" + } + ], + "data": { + "from": "iyeqsiu44wv4", + "to": "511nh3eth2bq", + "amount": 0.1123, + "symbol": "CAT", + "memo": "Heloo", + "quantity": "0.1123 CAT" + } + }, + { + "receiver": "eosio.token", + "account": "eosio.token", + "action": "transfer", + "authorization": [ + { + "account": "iyeqsiu44wv4", + "permission": "active" + } + ], + "data": { + "from": "iyeqsiu44wv4", + "to": "yxgqwv4yrtxx", + "amount": 0.1123, + "symbol": "CAT", + "memo": "Heloo", + "quantity": "0.1123 CAT" + } + }, + { + "receiver": "iyeqsiu44wv4", + "account": "eosio.token", + "action": "transfer", + "authorization": [ + { + "account": "iyeqsiu44wv4", + "permission": "active" + } + ], + "data": { + "from": "iyeqsiu44wv4", + "to": "yxgqwv4yrtxx", + "amount": 0.1123, + "symbol": "CAT", + "memo": "Heloo", + "quantity": "0.1123 CAT" + } + }, + { + "receiver": "yxgqwv4yrtxx", + "account": "eosio.token", + "action": "transfer", + "authorization": [ + { + "account": "iyeqsiu44wv4", + "permission": "active" + } + ], + "data": { + "from": "iyeqsiu44wv4", + "to": "yxgqwv4yrtxx", + "amount": 0.1123, + "symbol": "CAT", + "memo": "Heloo", + "quantity": "0.1123 CAT" + } + }, + { + "receiver": "eosio.token", + "account": "eosio.token", + "action": "transfer", + "authorization": [ + { + "account": "iyeqsiu44wv4", + "permission": "active" + } + ], + "data": { + "from": "iyeqsiu44wv4", + "to": "mzyqfirhysjn", + "amount": 0.1123, + "symbol": "CAT", + "memo": "Heloo", + "quantity": "0.1123 CAT" + } + }, + { + "receiver": "iyeqsiu44wv4", + "account": "eosio.token", + "action": "transfer", + "authorization": [ + { + "account": "iyeqsiu44wv4", + "permission": "active" + } + ], + "data": { + "from": "iyeqsiu44wv4", + "to": "mzyqfirhysjn", + "amount": 0.1123, + "symbol": "CAT", + "memo": "Heloo", + "quantity": "0.1123 CAT" + } + }, + { + "receiver": "mzyqfirhysjn", + "account": "eosio.token", + "action": "transfer", + "authorization": [ + { + "account": "iyeqsiu44wv4", + "permission": "active" + } + ], + "data": { + "from": "iyeqsiu44wv4", + "to": "mzyqfirhysjn", + "amount": 0.1123, + "symbol": "CAT", + "memo": "Heloo", + "quantity": "0.1123 CAT" + } + } +]; + +export const processedBlockMeta = { + "query_time_ms": 12.014, + "id": "01a808454f3aa21a1602478e2dbdbc5ebf0b735b226cdc344ea4e4a9d7141276", + "number": 27789381, + "previous_id": "01a80844388a2285339b279b14cd25cb32e905edfeb2895a7e47b5c8e1da99dd", + "status": "irreversible", + "timestamp": "2020-11-26T04:24:13.000", + "producer": "test3.bp", + "transactions": [ + { + "id": "637fb1f1fb82d903893aedf966ce37880d6ae96031b3acf7466ae8978adbbcc2", + "actions": hyperionActionMeta, + } + ] +} diff --git a/packages/demux/test/util.test.ts b/packages/demux/test/util.test.ts new file mode 100644 index 0000000..37d255b --- /dev/null +++ b/packages/demux/test/util.test.ts @@ -0,0 +1,36 @@ +import { retry } from '../src/utils'; +import Logger from 'bunyan'; + +describe('test util file', () => { + it('test retry function but do not retry', async () => { + const res = await retry(() => new Promise((resolve, reject) => resolve(1)), 5, 500); + expect(res).toEqual(1); + }); + + it('test retry function should retry', async () => { + try { + await retry( + () => new Promise((resolve, reject) => reject('error')), + 5, + 500, + Logger.createLogger({ name: 'info' }), + ); + } catch (error) { + expect(error).toEqual('error'); + } + }); + + it('test retry function should retry without log', async () => { + try { + await retry(() => new Promise((resolve, reject) => reject('error')), 5, 500); + } catch (error) { + expect(error).toEqual('error'); + } + }); + + it('test retry should throw` error', async () => { + await expect( + retry(() => new Promise((resolve, reject) => reject('error')), -1, 500), + ).rejects.toThrowError('-1 retries failed'); + }); +}); diff --git a/packages/example-api-gateway/.env.defaults b/packages/example-api-gateway/.env.defaults index 3e90dd7..aa04a28 100644 --- a/packages/example-api-gateway/.env.defaults +++ b/packages/example-api-gateway/.env.defaults @@ -10,9 +10,9 @@ app_postgres_port=5432 app_port=4000 app_enable_eos=true -app_nodeosEndpoint=https://testnet.canfoundation.io +app_hyperionEndpoint=https://hyperion.testnet.canfoundation.io app_d_eos_account_name=helloworld12 -app_startAtBlock=9130005 +app_startAtBlock=28009291 # this is active private key of account app_d_eos_account_name app_d_private_key= diff --git a/packages/example-api-gateway/README.md b/packages/example-api-gateway/README.md index 50129e4..35f04dc 100644 --- a/packages/example-api-gateway/README.md +++ b/packages/example-api-gateway/README.md @@ -53,9 +53,34 @@ cleost push action helloworld12 delpoll '["helloworld12", 1005]' -p helloworld12 cleost push action helloworld12 crevote '["helloworld12", 1005, 1, 2, 3]' -p helloworld12 cleost push action helloworld12 updvote '["helloworld12", 1005, 10, 200, 5]' -p helloworld12 cleost push action helloworld12 delvote '["helloworld12", 1005]' -p helloworld12 +``` + +Use `testtransact` [contract](https://github.com/danielAlvess/eos-send-inline-and-deferred-transaction) to test inline and deferred action + + +```bash +## push action create POLL as inline action + +# delegate creator permission to testtransact account +cleost set account permission daniel111111 active '{"threshold": 1,"keys": [{"key": "EOS6yfoREUrCWa1MZkjnfhLyG2cBk9spkayth6NKPBCmpLkzEK7NG","weight": 1}],"accounts": [{"permission":{"actor":"testtransact","permission":"eosio.code"},"weight":1}]}' owner -p daniel111111 + +# pack action data to create POLL +cleost convert pack_action_data helloworld12 crepoll '["daniel111111", 64, "poll test", "test test test"]' +1042082144e5a649400000000000000009706f6c6c20746573740e7465737420746573742074657374 + +# send inline action to create POLL +cleos -u https://testnet.canfoundation.io push action testtransact sendinline '["daniel111111", "helloworld12", "crepoll", "1042082144e5a649400000000000000009706f6c6c20746573740e7465737420746573742074657374"]' -p daniel111111 + +# send deferred action to create POLL +cleos -u https://testnet.canfoundation.io convert pack_action_data helloworld12 crepoll '["daniel111111", 65, "poll test", "test test test"]' +1042082144e5a649410000000000000009706f6c6c20746573740e7465737420746573742074657374 + +cleos -u https://testnet.canfoundation.io push action testtransact senddeferred '["daniel111111", "helloworld12", "crepoll", "1042082144e5a649410000000000000009706f6c6c20746573740e7465737420746573742074657374"]' -p daniel111111 ``` + + ### Enable watcher on ICON loop Create `.env` file as below @@ -67,7 +92,7 @@ app_enable_icon=true ## Start demo -We can start demo if with defaul setting. +We can start demo if with default setting. 1. change directory to `.../aloxide` 2. start local posgres database using docker compose: `docker-compose up -d` diff --git a/packages/example-api-gateway/src/.env.defaults b/packages/example-api-gateway/src/.env.defaults new file mode 100644 index 0000000..ae47308 --- /dev/null +++ b/packages/example-api-gateway/src/.env.defaults @@ -0,0 +1,24 @@ +# supported type: memory, postgres, sqlite +app_database_type=postgres + +app_postgres_db=aloxide +app_postgres_user=aloxide +app_postgres_pw=localhost-pw2020 +app_postgres_host=localhost +app_postgres_port=5432 + +app_port=4000 + +app_enable_eos=true +app_hyperionEndpoint=https://hyperion.testnet.canfoundation.io +app_d_eos_account_name=helloworld12 +app_startAtBlock=27949390 + +# this is active private key of account app_d_eos_account_name +app_d_private_key= + +app_enable_icon=false +app_icon_endpoint=https://bicon.net.solidwallet.io/api/v3 +app_icon_nid=3 +app_postgres_db_icon=postgres_icon +app_icon_startAtBlock=6536668 diff --git a/packages/example-api-gateway/src/readBlockchain.ts b/packages/example-api-gateway/src/readBlockchain.ts index 9457869..5efb60e 100644 --- a/packages/example-api-gateway/src/readBlockchain.ts +++ b/packages/example-api-gateway/src/readBlockchain.ts @@ -1,7 +1,6 @@ import { readAloxideConfig } from '@aloxide/abstraction'; -import { AloxideDataManager, createWatcher } from '@aloxide/demux'; +import { AloxideDataManager, HyperionActionReader, createWatcher } from '@aloxide/demux'; import { IconActionReader } from '@aloxide/demux-icon'; -import { NodeosActionReader } from 'demux-eos'; import config, { connectDb } from './config'; import { createDataProvider } from './models'; @@ -32,10 +31,12 @@ if (process.env.app_enable_eos == 'true') { aloxideConfig, dataAdapter, logger: config.logger, - actionReader: new NodeosActionReader({ - nodeosEndpoint: process.env.app_nodeosEndpoint, + actionReader: new HyperionActionReader({ + hyperionEndpoint: process.env.app_hyperionEndpoint, onlyIrreversible: false, startAtBlock: parseInt(process.env.app_startAtBlock, 10), + logLevel: 'info', + logSource: 'reader-EOS', }), actionWatcherOptions: { pollInterval: 5000,