diff --git a/hapi-evm/package.json b/hapi-evm/package.json index e0f1340a..e1348ec9 100644 --- a/hapi-evm/package.json +++ b/hapi-evm/package.json @@ -27,6 +27,7 @@ "graphql": "16", "graphql-request": "^6.0.0", "joi": "^17.9.2", + "moment": "^2.29.4", "node-fetch": "^3.3.1", "web3": "^4.0.3", "websocket": "^1.0.34" diff --git a/hapi-evm/src/config/hyperion.config.ts b/hapi-evm/src/config/hyperion.config.ts new file mode 100644 index 00000000..aa4f6e2d --- /dev/null +++ b/hapi-evm/src/config/hyperion.config.ts @@ -0,0 +1,4 @@ +export const api = + process.env.HAPI_HYPERION_API || 'https://test.telos.eosusa.io' +export const startAt = + process.env.HAPI_HYPERION_START_AT || '2021-06-02T00:00:00.000+00:00' diff --git a/hapi-evm/src/config/index.ts b/hapi-evm/src/config/index.ts index cf916619..0f6d6dc9 100644 --- a/hapi-evm/src/config/index.ts +++ b/hapi-evm/src/config/index.ts @@ -1,3 +1,4 @@ export * as serverConfig from './server.config' export * as hasuraConfig from './hasura.config' export * as networkConfig from './network.config' +export * as hyperionConfig from './hyperion.config' diff --git a/hapi-evm/src/models/default.model.ts b/hapi-evm/src/models/default.model.ts index 26922425..4e2cf927 100644 --- a/hapi-evm/src/models/default.model.ts +++ b/hapi-evm/src/models/default.model.ts @@ -15,6 +15,6 @@ export type TableType = keyof typeof Tables export interface Worker { name: string - intervalSec: number + intervalSec?: number action: () => Promise } diff --git a/hapi-evm/src/models/gas/interfaces.ts b/hapi-evm/src/models/gas/interfaces.ts deleted file mode 100644 index 344c070b..00000000 --- a/hapi-evm/src/models/gas/interfaces.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface I { - -} \ No newline at end of file diff --git a/hapi-evm/src/models/gas/queries.ts b/hapi-evm/src/models/gas/queries.ts deleted file mode 100644 index b1c6ea43..00000000 --- a/hapi-evm/src/models/gas/queries.ts +++ /dev/null @@ -1 +0,0 @@ -export default {} diff --git a/hapi-evm/src/models/gas/index.ts b/hapi-evm/src/models/hyperion-state/index.ts similarity index 100% rename from hapi-evm/src/models/gas/index.ts rename to hapi-evm/src/models/hyperion-state/index.ts diff --git a/hapi-evm/src/models/hyperion-state/interfaces.ts b/hapi-evm/src/models/hyperion-state/interfaces.ts new file mode 100644 index 00000000..63ef36a0 --- /dev/null +++ b/hapi-evm/src/models/hyperion-state/interfaces.ts @@ -0,0 +1,4 @@ +export interface HyperionState { + id: string + last_synced_at: string +} diff --git a/hapi-evm/src/models/hyperion-state/queries.ts b/hapi-evm/src/models/hyperion-state/queries.ts new file mode 100644 index 00000000..9a02199f --- /dev/null +++ b/hapi-evm/src/models/hyperion-state/queries.ts @@ -0,0 +1,94 @@ +import { gql } from 'graphql-request' + +import { coreUtil } from '../../utils' +import { HyperionState } from './interfaces' + +interface HyperionStateResponse { + evm_hyperion_state: HyperionState[] +} + +interface HyperionStateInsertOneResponse { + insert_evm_hyperion_state_one: { + id: string + } +} + +export const save = async (lastSyncedAt: string) => { + const mutation = gql` + mutation ($payload: evm_hyperion_state_insert_input!) { + insert_evm_hyperion_state_one(object: $payload) { + id + } + } + ` + + const data = + await coreUtil.hasura.default.request( + mutation, + { + payload: { + last_synced_at: lastSyncedAt + } + } + ) + + return data.insert_evm_hyperion_state_one +} + +export const update = async (id: string, lastSyncedAt: string) => { + const mutation = gql` + mutation ($id: uuid!, $payload: evm_hyperion_state_set_input) { + update_evm_hyperion_state_by_pk(pk_columns: { id: $id }, _set: $payload) { + id + last_synced_at + } + } + ` + + await coreUtil.hasura.default.request(mutation, { + id, + payload: { + last_synced_at: lastSyncedAt + } + }) +} + +export const getState = async () => { + const query = gql` + query { + evm_hyperion_state( + where: { id: { _neq: "00000000-0000-0000-0000-000000000000" } } + limit: 1 + ) { + id + last_synced_at + } + } + ` + const data = await coreUtil.hasura.default.request( + query + ) + + if (!data.evm_hyperion_state.length) { + return + } + + const state = data.evm_hyperion_state[0] + + return { + id: state.id, + lastSyncedAt: state.last_synced_at + } +} + +export const saveOrUpdate = async (lastSyncedAt: string) => { + const currentState = await getState() + + if (!currentState) { + await save(lastSyncedAt) + + return + } + + await update(currentState.id, lastSyncedAt) +} diff --git a/hapi-evm/src/models/incoming-transfer/index.ts b/hapi-evm/src/models/incoming-transfer/index.ts new file mode 100644 index 00000000..5e08e0f1 --- /dev/null +++ b/hapi-evm/src/models/incoming-transfer/index.ts @@ -0,0 +1,2 @@ +export * as interfaces from './interfaces' +export * as queries from './queries' diff --git a/hapi-evm/src/models/incoming-transfer/interfaces.ts b/hapi-evm/src/models/incoming-transfer/interfaces.ts new file mode 100644 index 00000000..3504923f --- /dev/null +++ b/hapi-evm/src/models/incoming-transfer/interfaces.ts @@ -0,0 +1,12 @@ +export interface IncomingTransfer { + id?: string + block: number + transaction_id: string + from: string + to: string + amount: number + symbol: string + memo: string + quantity: string + timestamp: string +} diff --git a/hapi-evm/src/models/incoming-transfer/queries.ts b/hapi-evm/src/models/incoming-transfer/queries.ts new file mode 100644 index 00000000..ba1eaa3e --- /dev/null +++ b/hapi-evm/src/models/incoming-transfer/queries.ts @@ -0,0 +1,34 @@ +import { gql } from 'graphql-request' + +import { coreUtil } from '../../utils' +import { IncomingTransfer } from './interfaces' + +// interface IncomingTransferResponse { +// evm_incoming_transfer: IncomingTransfer[] +// } + +interface IncomingTransferInsertOneResponse { + insert_evm_incoming_transfer_one: { + id: string + } +} + +export const save = async (payload: IncomingTransfer) => { + const mutation = gql` + mutation ($payload: evm_incoming_transfer_insert_input!) { + insert_evm_incoming_transfer_one(object: $payload) { + id + } + } + ` + + const data = + await coreUtil.hasura.default.request( + mutation, + { + payload + } + ) + + return data.insert_evm_incoming_transfer_one +} diff --git a/hapi-evm/src/models/index.ts b/hapi-evm/src/models/index.ts index 86b6f80f..dc6d58a6 100644 --- a/hapi-evm/src/models/index.ts +++ b/hapi-evm/src/models/index.ts @@ -1,4 +1,5 @@ -export * as gasModel from './gas' +export * as defaultModel from './default.model' export * as blockModel from './block' export * as transactionModel from './transaction' -export * as defaultModel from './default.model' +export * as hyperionStateModel from './hyperion-state' +export * as incomingTransferModel from './incoming-transfer' diff --git a/hapi-evm/src/routes/v1/index.ts b/hapi-evm/src/routes/v1/index.ts index a2123c73..6e2abe3d 100644 --- a/hapi-evm/src/routes/v1/index.ts +++ b/hapi-evm/src/routes/v1/index.ts @@ -25,8 +25,8 @@ const baseRoute = '/v1' // (OK) Average gas usage: Show to average gas usage in the last 100 blocks. // () Average transactions per second (block and eosio.token -> ERC20). -// - () internal TLOS transactions. (tEVM address -> tEVM address) -// - () incoming TLOS transactions. (EOS address -> tEVM address) +// - (NO) internal TLOS transactions. (tEVM address -> tEVM address) +// - (OK) incoming TLOS transactions. (EOS address -> tEVM address) // - () outgoing TLOS transactions. (tEVM address -> EOS address) HOW? // (OK) Daily transactions. // (OK) ATH. diff --git a/hapi-evm/src/services/hyperion/index.ts b/hapi-evm/src/services/hyperion/index.ts new file mode 100644 index 00000000..59c18092 --- /dev/null +++ b/hapi-evm/src/services/hyperion/index.ts @@ -0,0 +1,170 @@ +import moment, { DurationInputArg2 } from 'moment' + +import { hyperionConfig } from '../../config' +import { coreUtil, timeUtil } from '../../utils' +import { hyperionStateModel } from '../../models' + +import updaters from './updaters' + +interface GetActionsParams { + after: string + before: string + skip: number +} + +interface GetActionsResponse { + hasMore: boolean + actions: any[] +} + +const TIME_BEFORE_IRREVERSIBILITY = 164 + +const getLastSyncedAt = async () => { + const state = await hyperionStateModel.queries.getState() + + if (state) { + return state.lastSyncedAt + } + + await hyperionStateModel.queries.saveOrUpdate(hyperionConfig.startAt) + + return hyperionConfig.startAt +} + +const getGap = (lastSyncedAt: string) => { + if (moment().diff(moment(lastSyncedAt), 'days') > 0) { + return { + amount: 1, + unit: 'day' + } + } + + if (moment().diff(moment(lastSyncedAt), 'hours') > 0) { + return { + amount: 1, + unit: 'hour' + } + } + + if ( + moment().diff(moment(lastSyncedAt), 'seconds') >= + TIME_BEFORE_IRREVERSIBILITY * 2 + ) { + return { + amount: TIME_BEFORE_IRREVERSIBILITY, + unit: 'seconds' + } + } + + if ( + moment().diff(moment(lastSyncedAt), 'seconds') >= + TIME_BEFORE_IRREVERSIBILITY + 10 + ) { + return { + amount: 10, + unit: 'seconds' + } + } + + return { + amount: 1, + unit: 'seconds' + } +} + +const getActions = async ( + params: GetActionsParams +): Promise => { + const limit = 100 + const { data } = await coreUtil.axios.default.get( + `${hyperionConfig.api}/v2/history/get_actions`, + { + params: { + ...params, + account: 'eosio.evm', // TODO: get it from updater using the notified_account field + limit, + filter: updaters.map(updater => updater.type).join(','), + sort: 'asc', + simple: true, + checkLib: true + } + } + ) + + const notIrreversible = data.simple_actions.find( + (item: any) => !item.irreversible + ) + + if (notIrreversible) { + await timeUtil.sleep(1) + + return getActions(params) + } + + return { + hasMore: data.total.value > limit + params.skip || false, + actions: data.simple_actions + } +} + +const runUpdaters = async (actions: any[]) => { + for (let index = 0; index < actions.length; index++) { + const action = actions[index] + const updater = updaters.find(item => + item.type.startsWith(`${action.contract}:${action.action}`) + ) + + if (!updater) { + continue + } + + await updater.apply(action) + } +} + +const sync = async (): Promise => { + console.log('SYNCING') + await coreUtil.hasura.hasuraAssembled() + const lastSyncedAt = await getLastSyncedAt() + const gap = getGap(lastSyncedAt) + const after = moment(lastSyncedAt).toISOString() + const before = moment(after) + .add(gap.amount, gap.unit as DurationInputArg2) + .toISOString() + const diff = moment().diff(moment(before), 'seconds') + let skip = 0 + let hasMore = true + let actions = [] + + if (diff < TIME_BEFORE_IRREVERSIBILITY) { + await timeUtil.sleep(TIME_BEFORE_IRREVERSIBILITY - diff) + + return sync() + } + + try { + while (hasMore) { + ;({ hasMore, actions } = await getActions({ after, before, skip })) + skip += actions.length + await runUpdaters(actions) + } + } catch (error: any) { + console.error('hyperion error', error.message) + await timeUtil.sleep(5) + + return sync() + } + + await hyperionStateModel.queries.saveOrUpdate(before) + + return sync() +} + +const syncWorker = () => { + return { + name: 'SYNC ACTIONS', + action: sync + } +} + +export default { syncWorker } diff --git a/hapi-evm/src/services/hyperion/updaters/index.ts b/hapi-evm/src/services/hyperion/updaters/index.ts new file mode 100644 index 00000000..d3a15943 --- /dev/null +++ b/hapi-evm/src/services/hyperion/updaters/index.ts @@ -0,0 +1,3 @@ +import tokenTransferUpdater from './token-transfer.updater' + +export default [tokenTransferUpdater] diff --git a/hapi-evm/src/services/hyperion/updaters/token-transfer.updater.ts b/hapi-evm/src/services/hyperion/updaters/token-transfer.updater.ts new file mode 100644 index 00000000..8191062e --- /dev/null +++ b/hapi-evm/src/services/hyperion/updaters/token-transfer.updater.ts @@ -0,0 +1,29 @@ +import { isAddress } from 'web3-validator' + +import { incomingTransferModel } from '../../../models' + +export default { + type: `eosio.token:transfer,act.data.to=eosio.evm`, + notified_account: `eosio.evm`, + apply: async (action: any) => { + if (!isAddress(action.data.memo)) { + return + } + + try { + await incomingTransferModel.queries.save({ + block: action.block, + transaction_id: action.transaction_id, + timestamp: action.timestamp, + from: action.data.from, + to: action.data.to, + amount: action.data.amount, + symbol: action.data.symbol, + memo: action.data.memo, + quantity: action.data.quantity + }) + } catch (error: any) { + console.error(`error to sync ${action.action}: ${error.message}`) + } + } +} diff --git a/hapi-evm/src/services/worker/index.ts b/hapi-evm/src/services/worker/index.ts index f23460ff..1ad8fcc4 100644 --- a/hapi-evm/src/services/worker/index.ts +++ b/hapi-evm/src/services/worker/index.ts @@ -1,12 +1,13 @@ import { timeUtil, coreUtil } from '../../utils' import { defaultModel } from '../../models' import blockService from '../block.service' +import hyperionService from '../hyperion' const run = async (worker: defaultModel.Worker) => { try { await worker.action() } catch (error: any) { - console.log(`${name} ERROR =>`, error.message) + console.log(`${worker.name} ERROR =>`, error.message) } if (!worker.intervalSec) { @@ -20,7 +21,8 @@ const run = async (worker: defaultModel.Worker) => { const init = async () => { await coreUtil.hasura.hasuraAssembled() - run(blockService.syncBlockWorker()) + // run(blockService.syncBlockWorker()) + run(hyperionService.syncWorker()) } export default { diff --git a/hapi-evm/yarn.lock b/hapi-evm/yarn.lock index 2bff766c..1e9b13c9 100644 --- a/hapi-evm/yarn.lock +++ b/hapi-evm/yarn.lock @@ -2087,6 +2087,11 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +moment@^2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" diff --git a/hasura/metadata/databases/default/tables/evm_hyperion_state.yaml b/hasura/metadata/databases/default/tables/evm_hyperion_state.yaml new file mode 100644 index 00000000..4dd64e45 --- /dev/null +++ b/hasura/metadata/databases/default/tables/evm_hyperion_state.yaml @@ -0,0 +1,3 @@ +table: + name: hyperion_state + schema: evm diff --git a/hasura/metadata/databases/default/tables/evm_incoming_transfer.yaml b/hasura/metadata/databases/default/tables/evm_incoming_transfer.yaml new file mode 100644 index 00000000..8f2369de --- /dev/null +++ b/hasura/metadata/databases/default/tables/evm_incoming_transfer.yaml @@ -0,0 +1,3 @@ +table: + name: incoming_transfer + schema: evm diff --git a/hasura/metadata/databases/default/tables/evm_param.yaml b/hasura/metadata/databases/default/tables/evm_param.yaml new file mode 100644 index 00000000..e5a5d86b --- /dev/null +++ b/hasura/metadata/databases/default/tables/evm_param.yaml @@ -0,0 +1,3 @@ +table: + name: param + schema: evm diff --git a/hasura/metadata/databases/default/tables/evm_params.yaml b/hasura/metadata/databases/default/tables/evm_params.yaml new file mode 100644 index 00000000..51c501a4 --- /dev/null +++ b/hasura/metadata/databases/default/tables/evm_params.yaml @@ -0,0 +1,3 @@ +table: + name: params + schema: evm diff --git a/hasura/metadata/databases/default/tables/tables.yaml b/hasura/metadata/databases/default/tables/tables.yaml index 46489b16..4cf2b197 100644 --- a/hasura/metadata/databases/default/tables/tables.yaml +++ b/hasura/metadata/databases/default/tables/tables.yaml @@ -1,4 +1,7 @@ - "!include evm_block.yaml" +- "!include evm_hyperion_state.yaml" +- "!include evm_incoming_transfer.yaml" +- "!include evm_param.yaml" - "!include evm_transaction.yaml" - "!include public_block_history.yaml" - "!include public_block_history_by_producer_type.yaml" diff --git a/hasura/migrations/default/1689963835559_create_table_evm_params/down.sql b/hasura/migrations/default/1689963835559_create_table_evm_params/down.sql new file mode 100644 index 00000000..746c729e --- /dev/null +++ b/hasura/migrations/default/1689963835559_create_table_evm_params/down.sql @@ -0,0 +1 @@ +DROP TABLE "evm"."params"; diff --git a/hasura/migrations/default/1689963835559_create_table_evm_params/up.sql b/hasura/migrations/default/1689963835559_create_table_evm_params/up.sql new file mode 100644 index 00000000..aaba3008 --- /dev/null +++ b/hasura/migrations/default/1689963835559_create_table_evm_params/up.sql @@ -0,0 +1,2 @@ +CREATE TABLE "evm"."params" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "last_block_synced" numeric NOT NULL, PRIMARY KEY ("id") ); +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/hasura/migrations/default/1689963954957_rename_table_evm_params/down.sql b/hasura/migrations/default/1689963954957_rename_table_evm_params/down.sql new file mode 100644 index 00000000..bfaa32fb --- /dev/null +++ b/hasura/migrations/default/1689963954957_rename_table_evm_params/down.sql @@ -0,0 +1 @@ +alter table "evm"."param" rename to "params"; diff --git a/hasura/migrations/default/1689963954957_rename_table_evm_params/up.sql b/hasura/migrations/default/1689963954957_rename_table_evm_params/up.sql new file mode 100644 index 00000000..6ff3d5cd --- /dev/null +++ b/hasura/migrations/default/1689963954957_rename_table_evm_params/up.sql @@ -0,0 +1 @@ +alter table "evm"."params" rename to "param"; diff --git a/hasura/migrations/default/1689964045956_create_table_evm_hyperion_state/down.sql b/hasura/migrations/default/1689964045956_create_table_evm_hyperion_state/down.sql new file mode 100644 index 00000000..7e7c39fb --- /dev/null +++ b/hasura/migrations/default/1689964045956_create_table_evm_hyperion_state/down.sql @@ -0,0 +1 @@ +DROP TABLE "evm"."hyperion_state"; diff --git a/hasura/migrations/default/1689964045956_create_table_evm_hyperion_state/up.sql b/hasura/migrations/default/1689964045956_create_table_evm_hyperion_state/up.sql new file mode 100644 index 00000000..4f40f17e --- /dev/null +++ b/hasura/migrations/default/1689964045956_create_table_evm_hyperion_state/up.sql @@ -0,0 +1,18 @@ +CREATE TABLE "evm"."hyperion_state" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "last_synced_at" timestamptz NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") ); +CREATE OR REPLACE FUNCTION "evm"."set_current_timestamp_updated_at"() +RETURNS TRIGGER AS $$ +DECLARE + _new record; +BEGIN + _new := NEW; + _new."updated_at" = NOW(); + RETURN _new; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER "set_evm_hyperion_state_updated_at" +BEFORE UPDATE ON "evm"."hyperion_state" +FOR EACH ROW +EXECUTE PROCEDURE "evm"."set_current_timestamp_updated_at"(); +COMMENT ON TRIGGER "set_evm_hyperion_state_updated_at" ON "evm"."hyperion_state" +IS 'trigger to set value of column "updated_at" to current timestamp on row update'; +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/hasura/migrations/default/1689983709652_create_table_evm_incoming_transfer/down.sql b/hasura/migrations/default/1689983709652_create_table_evm_incoming_transfer/down.sql new file mode 100644 index 00000000..5b3a9650 --- /dev/null +++ b/hasura/migrations/default/1689983709652_create_table_evm_incoming_transfer/down.sql @@ -0,0 +1 @@ +DROP TABLE "evm"."incoming_transfer"; diff --git a/hasura/migrations/default/1689983709652_create_table_evm_incoming_transfer/up.sql b/hasura/migrations/default/1689983709652_create_table_evm_incoming_transfer/up.sql new file mode 100644 index 00000000..012716d0 --- /dev/null +++ b/hasura/migrations/default/1689983709652_create_table_evm_incoming_transfer/up.sql @@ -0,0 +1,2 @@ +CREATE TABLE "evm"."incoming_transfer" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "block" numeric NOT NULL, "transaction_id" varchar NOT NULL, "from" varchar NOT NULL, "to" varchar NOT NULL, "amount" numeric NOT NULL, "symbol" varchar NOT NULL, "memo" varchar NOT NULL, "quantity" varchar NOT NULL, PRIMARY KEY ("id") );COMMENT ON TABLE "evm"."incoming_transfer" IS E'Incoming TLOS token transfer. (EOS account -> tEVM address)'; +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/hasura/migrations/default/1689984918750_alter_table_evm_incoming_transfer_add_column_timestamp/down.sql b/hasura/migrations/default/1689984918750_alter_table_evm_incoming_transfer_add_column_timestamp/down.sql new file mode 100644 index 00000000..df06c440 --- /dev/null +++ b/hasura/migrations/default/1689984918750_alter_table_evm_incoming_transfer_add_column_timestamp/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "evm"."incoming_transfer" add column "timestamp" timestamptz +-- not null; diff --git a/hasura/migrations/default/1689984918750_alter_table_evm_incoming_transfer_add_column_timestamp/up.sql b/hasura/migrations/default/1689984918750_alter_table_evm_incoming_transfer_add_column_timestamp/up.sql new file mode 100644 index 00000000..9fbbb6b1 --- /dev/null +++ b/hasura/migrations/default/1689984918750_alter_table_evm_incoming_transfer_add_column_timestamp/up.sql @@ -0,0 +1,2 @@ +alter table "evm"."incoming_transfer" add column "timestamp" timestamptz + not null;