From 19a9c5cee7c79b6f457e4afdc56e5430ffc77f48 Mon Sep 17 00:00:00 2001 From: Filip Pajic Date: Fri, 15 Dec 2023 14:16:35 +0100 Subject: [PATCH 1/7] created the etherscan service --- .env.example | 4 ++ src/config/index.ts | 10 +++- src/jobs/app.jobs.ts | 5 +- src/jobs/price.jobs.ts | 6 +- src/services/etherscan/etherscan.service.ts | 56 +++++++++++++++++++ .../etherscan/etherscan.service.types.ts | 33 +++++++++++ src/services/oracle/oracle.service.ts | 1 + 7 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 src/services/etherscan/etherscan.service.ts create mode 100644 src/services/etherscan/etherscan.service.types.ts diff --git a/.env.example b/.env.example index 9ac2c04..18c7299 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,10 @@ COINGECKO_BASE_URL=https://api.coingecko.com/api/v3/ COINGECKO_API_KEY= COINGECKO_API_KEY_QUERY_NAME=x_cg_demo_api_key +ETHERSCAN_BASE_URL= +ETHERSCAN_API_KEY= +ETHERSCAN_API_KEY_QUERY_NAME= + ALCHEMY_URL= PRIVATE_KEY= diff --git a/src/config/index.ts b/src/config/index.ts index e654aad..21b3a72 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -26,7 +26,10 @@ const { PRIVATE_KEY, TICKER_USD_FEED_REGISTRY, TICKER_PRICE_STORAGE, - CONTRACTS_DEPLOYMENT_BLOCK + CONTRACTS_DEPLOYMENT_BLOCK, + ETHERSCAN_BASE_URL, + ETHERSCAN_API_KEY, + ETHERSCAN_API_KEY_QUERY_NAME } = process.env; const ENV: ApplicationEnv = NODE_ENV as ApplicationEnv || ApplicationEnv.DEVELOPMENT; @@ -43,5 +46,8 @@ export const CONFIG = { PRIVATE_KEY, TICKER_USD_FEED_REGISTRY, TICKER_PRICE_STORAGE, - CONTRACTS_DEPLOYMENT_BLOCK: Number(CONTRACTS_DEPLOYMENT_BLOCK) + CONTRACTS_DEPLOYMENT_BLOCK: Number(CONTRACTS_DEPLOYMENT_BLOCK), + ETHERSCAN_BASE_URL, + ETHERSCAN_API_KEY, + ETHERSCAN_API_KEY_QUERY_NAME }; diff --git a/src/jobs/app.jobs.ts b/src/jobs/app.jobs.ts index e7b3be7..1949915 100644 --- a/src/jobs/app.jobs.ts +++ b/src/jobs/app.jobs.ts @@ -1,7 +1,9 @@ import { initTickerState, setupTickerFetchingJob } from "./ticker.jobs"; -import { initOnchainPriceState, setupOffchainPriceFetchingJob } from "./price.jobs"; +import {initOnchainPriceState, setupOffchainPriceFetchingJob, setupPriceUpdateRevertObserverJob} from "./price.jobs"; import logger from "../utils/logger.util"; import store from "../redux/store"; +import EtherscanService from "../services/etherscan/etherscan.service"; +import {CONFIG} from "../config"; const initAppState = async () => { logger.log("Initialising the app state"); @@ -16,4 +18,5 @@ export const initApp = async () => { await setupTickerFetchingJob(); await setupOffchainPriceFetchingJob(); + await setupPriceUpdateRevertObserverJob(); }; \ No newline at end of file diff --git a/src/jobs/price.jobs.ts b/src/jobs/price.jobs.ts index e382847..39f0148 100644 --- a/src/jobs/price.jobs.ts +++ b/src/jobs/price.jobs.ts @@ -56,4 +56,8 @@ export const setupOnchainPriceFetchingJobFor = async (tickerSymbol: string) => { updateReduxTickerOnchainPrice(tickerSymbol, tickerPriceData.newPrice); RootSocket.emitEvent("TickerPriceUpdated", [tickerSymbol, tickerPriceData.newPrice]); }); -}; \ No newline at end of file +}; + +export const setupPriceUpdateRevertObserverJob = async () => { + +} \ No newline at end of file diff --git a/src/services/etherscan/etherscan.service.ts b/src/services/etherscan/etherscan.service.ts new file mode 100644 index 0000000..6bffcea --- /dev/null +++ b/src/services/etherscan/etherscan.service.ts @@ -0,0 +1,56 @@ +import RestService from "../rest.service"; +import {CONFIG} from "../../config"; +import {EAuthenticationType} from "../../types/auth.types"; +import {EtherscanContractTxHistoryResponse, EtherscanRevertedTxHistory} from "./etherscan.service.types"; + +class EtherscanService extends RestService { + constructor() { + super({ + baseUrl: CONFIG.ETHERSCAN_BASE_URL, + authConfig: { + type: EAuthenticationType.QUERY_PARAM, + paramName: CONFIG.ETHERSCAN_API_KEY_QUERY_NAME, + paramValue: CONFIG.ETHERSCAN_API_KEY + } + }) + } + + public async getContractTxHistory(contractAddress: string, startBlock: number): Promise { + const response = await this.get({ + url: "api", + config: { + params: { + module: "account", + action: "txlist", + address: contractAddress, + startBlock: startBlock, + sort: "asc" + } + } + }) + + return response.data; + } + + public async getRevertedTxHistory(contractAddress: string, startBlock: number): Promise { + const apiResponse = await this.getContractTxHistory(contractAddress, startBlock); + const results: EtherscanRevertedTxHistory = []; + + if (apiResponse.status == "1" && apiResponse.result.length > 0) { + apiResponse.result.forEach(etherscanResponse => { + if (etherscanResponse.isError == "1") { + results.push({ + blockNumber: etherscanResponse.blockNumber, + hash: etherscanResponse.hash + }) + } + }) + } else { + return []; + } + + return results; + } +} + +export default EtherscanService \ No newline at end of file diff --git a/src/services/etherscan/etherscan.service.types.ts b/src/services/etherscan/etherscan.service.types.ts new file mode 100644 index 0000000..05b2a0c --- /dev/null +++ b/src/services/etherscan/etherscan.service.types.ts @@ -0,0 +1,33 @@ +export type EtherscanResponse = { + status: string, + message: string, + result: T +} + +export type EtherscanContractTxHistoryResponse = EtherscanResponse<{ + blockNumber: string, + timeStamp: string, + hash: string, + nonce: string, + blockHash: string, + transactionIndex: string, + from: string, + to: string, + value: string, + gas: string, + gasPrice: string, + isError: string, + txreceipt_status: string, + input: string, + contractAddress: string, + cumulativeGasUsed: string, + gasUsed: string, + confirmations: string, + methodId: string, + functionName: string +}[]> + +export type EtherscanRevertedTxHistory = { + blockNumber: string, + hash: string, +}[] \ No newline at end of file diff --git a/src/services/oracle/oracle.service.ts b/src/services/oracle/oracle.service.ts index 171e33a..4d45930 100644 --- a/src/services/oracle/oracle.service.ts +++ b/src/services/oracle/oracle.service.ts @@ -3,6 +3,7 @@ import { CONFIG } from "../../config"; import logger from "../../utils/logger.util"; import { TypedEventLog } from "../../contracts/common"; import { TickerPriceData } from "./oracle.service.types"; +import {EtherscanProvider} from "ethers"; class OracleService { private _tickerPriceContract; From f89392ac9d825f12b0fe719ceb7a7d17df3d49bb Mon Sep 17 00:00:00 2001 From: Filip Pajic Date: Fri, 15 Dec 2023 14:33:12 +0100 Subject: [PATCH 2/7] chainlink service created --- src/jobs/app.jobs.ts | 7 +++---- src/jobs/price.jobs.ts | 6 ++++-- src/services/chainlink.service.ts | 20 ++++++++++++++++++++ src/services/etherscan/etherscan.service.ts | 16 ++++++++-------- src/services/oracle/oracle.service.ts | 1 - src/services/web3.service.ts | 10 +++++++++- 6 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 src/services/chainlink.service.ts diff --git a/src/jobs/app.jobs.ts b/src/jobs/app.jobs.ts index 1949915..9f58cfc 100644 --- a/src/jobs/app.jobs.ts +++ b/src/jobs/app.jobs.ts @@ -1,9 +1,7 @@ import { initTickerState, setupTickerFetchingJob } from "./ticker.jobs"; -import {initOnchainPriceState, setupOffchainPriceFetchingJob, setupPriceUpdateRevertObserverJob} from "./price.jobs"; +import { initOnchainPriceState, setupOffchainPriceFetchingJob, setupPriceUpdateRevertObserverJob } from "./price.jobs"; import logger from "../utils/logger.util"; import store from "../redux/store"; -import EtherscanService from "../services/etherscan/etherscan.service"; -import {CONFIG} from "../config"; const initAppState = async () => { logger.log("Initialising the app state"); @@ -15,8 +13,9 @@ const initAppState = async () => { export const initApp = async () => { await initAppState(); + await setupPriceUpdateRevertObserverJob(); + await setupTickerFetchingJob(); await setupOffchainPriceFetchingJob(); - await setupPriceUpdateRevertObserverJob(); }; \ No newline at end of file diff --git a/src/jobs/price.jobs.ts b/src/jobs/price.jobs.ts index 39f0148..d7565d1 100644 --- a/src/jobs/price.jobs.ts +++ b/src/jobs/price.jobs.ts @@ -6,9 +6,12 @@ import { setCurrentOffchainPrice, setCurrentOnchainPrice } from "../redux/prices import OracleService from "../services/oracle/oracle.service"; import logger from "../utils/logger.util"; import { RootSocket } from "../index"; +import Web3Service from "../services/web3.service"; +import { CONFIG } from "../config"; const coinGecko = new CoinGeckoService(); const oracleService = new OracleService(); +const registryContract = Web3Service.getTickerUSDFeedRegistryContract(CONFIG.TICKER_USD_FEED_REGISTRY); export const setupOffchainPriceFetchingJob = async () => { CronService.scheduleRecurringJob(async () => { @@ -59,5 +62,4 @@ export const setupOnchainPriceFetchingJobFor = async (tickerSymbol: string) => { }; export const setupPriceUpdateRevertObserverJob = async () => { - -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/services/chainlink.service.ts b/src/services/chainlink.service.ts new file mode 100644 index 0000000..a40b41e --- /dev/null +++ b/src/services/chainlink.service.ts @@ -0,0 +1,20 @@ +import { ChainlinkAggregatorV3Abi } from "../contracts"; +import Web3Service from "./web3.service"; + +class ChainlinkService { + private _aggregatorContract: ChainlinkAggregatorV3Abi; + + constructor(aggregatorAddress: string) { + this._aggregatorContract = Web3Service.getChainlinkPriceFeedContract(aggregatorAddress); + } + + public async getPriceForBlock(blockNumber: number) { + const result = await this._aggregatorContract.latestRoundData({ + blockTag: blockNumber + }); + + return result.answer; + } +} + +export default ChainlinkService; \ No newline at end of file diff --git a/src/services/etherscan/etherscan.service.ts b/src/services/etherscan/etherscan.service.ts index 6bffcea..24f82c6 100644 --- a/src/services/etherscan/etherscan.service.ts +++ b/src/services/etherscan/etherscan.service.ts @@ -1,7 +1,7 @@ import RestService from "../rest.service"; -import {CONFIG} from "../../config"; -import {EAuthenticationType} from "../../types/auth.types"; -import {EtherscanContractTxHistoryResponse, EtherscanRevertedTxHistory} from "./etherscan.service.types"; +import { CONFIG } from "../../config"; +import { EAuthenticationType } from "../../types/auth.types"; +import { EtherscanContractTxHistoryResponse, EtherscanRevertedTxHistory } from "./etherscan.service.types"; class EtherscanService extends RestService { constructor() { @@ -12,7 +12,7 @@ class EtherscanService extends RestService { paramName: CONFIG.ETHERSCAN_API_KEY_QUERY_NAME, paramValue: CONFIG.ETHERSCAN_API_KEY } - }) + }); } public async getContractTxHistory(contractAddress: string, startBlock: number): Promise { @@ -27,7 +27,7 @@ class EtherscanService extends RestService { sort: "asc" } } - }) + }); return response.data; } @@ -42,9 +42,9 @@ class EtherscanService extends RestService { results.push({ blockNumber: etherscanResponse.blockNumber, hash: etherscanResponse.hash - }) + }); } - }) + }); } else { return []; } @@ -53,4 +53,4 @@ class EtherscanService extends RestService { } } -export default EtherscanService \ No newline at end of file +export default EtherscanService; \ No newline at end of file diff --git a/src/services/oracle/oracle.service.ts b/src/services/oracle/oracle.service.ts index 4d45930..171e33a 100644 --- a/src/services/oracle/oracle.service.ts +++ b/src/services/oracle/oracle.service.ts @@ -3,7 +3,6 @@ import { CONFIG } from "../../config"; import logger from "../../utils/logger.util"; import { TypedEventLog } from "../../contracts/common"; import { TickerPriceData } from "./oracle.service.types"; -import {EtherscanProvider} from "ethers"; class OracleService { private _tickerPriceContract; diff --git a/src/services/web3.service.ts b/src/services/web3.service.ts index 2fca3bc..eff7f5d 100644 --- a/src/services/web3.service.ts +++ b/src/services/web3.service.ts @@ -1,5 +1,9 @@ import { ethers, JsonRpcProvider, Wallet } from "ethers"; -import { TickerPriceStorageAbi__factory, TickerUSDFeedRegistryAbi__factory } from "../contracts"; +import { + ChainlinkAggregatorV3Abi__factory, + TickerPriceStorageAbi__factory, + TickerUSDFeedRegistryAbi__factory +} from "../contracts"; import { CONFIG } from "../config"; @@ -23,6 +27,10 @@ class Web3Service { public getTickerPriceStorageContract(address: string, isMutatingState?: boolean) { return TickerPriceStorageAbi__factory.connect(address, this._getRunner(!!isMutatingState)); } + + public getChainlinkPriceFeedContract(address: string) { + return ChainlinkAggregatorV3Abi__factory.connect(address, this._getRunner(false)); + } } // We'll create a singleton for now while it's single network client From d4480cbd9d868d1d335951b70ba1517d36e7366e Mon Sep 17 00:00:00 2001 From: Filip Pajic Date: Fri, 15 Dec 2023 14:45:23 +0100 Subject: [PATCH 3/7] added tx input parser for oracle service --- src/jobs/price.jobs.ts | 1 + src/services/etherscan/etherscan.service.ts | 4 +++- src/services/etherscan/etherscan.service.types.ts | 1 + src/services/oracle/oracle.service.ts | 11 ++++++++++- src/services/oracle/oracle.service.types.ts | 5 +++++ 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/jobs/price.jobs.ts b/src/jobs/price.jobs.ts index d7565d1..2409578 100644 --- a/src/jobs/price.jobs.ts +++ b/src/jobs/price.jobs.ts @@ -8,6 +8,7 @@ import logger from "../utils/logger.util"; import { RootSocket } from "../index"; import Web3Service from "../services/web3.service"; import { CONFIG } from "../config"; +import ChainlinkService from "../services/chainlink.service"; const coinGecko = new CoinGeckoService(); const oracleService = new OracleService(); diff --git a/src/services/etherscan/etherscan.service.ts b/src/services/etherscan/etherscan.service.ts index 24f82c6..e029575 100644 --- a/src/services/etherscan/etherscan.service.ts +++ b/src/services/etherscan/etherscan.service.ts @@ -2,6 +2,7 @@ import RestService from "../rest.service"; import { CONFIG } from "../../config"; import { EAuthenticationType } from "../../types/auth.types"; import { EtherscanContractTxHistoryResponse, EtherscanRevertedTxHistory } from "./etherscan.service.types"; +import {ethers} from "ethers"; class EtherscanService extends RestService { constructor() { @@ -41,7 +42,8 @@ class EtherscanService extends RestService { if (etherscanResponse.isError == "1") { results.push({ blockNumber: etherscanResponse.blockNumber, - hash: etherscanResponse.hash + hash: etherscanResponse.hash, + input: etherscanResponse.input }); } }); diff --git a/src/services/etherscan/etherscan.service.types.ts b/src/services/etherscan/etherscan.service.types.ts index 05b2a0c..31c1f08 100644 --- a/src/services/etherscan/etherscan.service.types.ts +++ b/src/services/etherscan/etherscan.service.types.ts @@ -30,4 +30,5 @@ export type EtherscanContractTxHistoryResponse = EtherscanResponse<{ export type EtherscanRevertedTxHistory = { blockNumber: string, hash: string, + input: string }[] \ No newline at end of file diff --git a/src/services/oracle/oracle.service.ts b/src/services/oracle/oracle.service.ts index 171e33a..db341a0 100644 --- a/src/services/oracle/oracle.service.ts +++ b/src/services/oracle/oracle.service.ts @@ -2,7 +2,7 @@ import Web3Service from "../web3.service"; import { CONFIG } from "../../config"; import logger from "../../utils/logger.util"; import { TypedEventLog } from "../../contracts/common"; -import { TickerPriceData } from "./oracle.service.types"; +import {SetFunctionArguments, TickerPriceData} from "./oracle.service.types"; class OracleService { private _tickerPriceContract; @@ -67,6 +67,15 @@ class OracleService { }); } + public parseInputData(input: string): SetFunctionArguments { + const txDescription = this._tickerPriceContract.interface.parseTransaction({ data: input }); + + return { + ticker: txDescription.args[0], + price: txDescription.args[1] + } + } + private parseTickerPriceUpdatedEvent(event: TypedEventLog): TickerPriceData { const args = event.args as [string, number]; const newPrice = args[1]; diff --git a/src/services/oracle/oracle.service.types.ts b/src/services/oracle/oracle.service.types.ts index e182eda..d8a1289 100644 --- a/src/services/oracle/oracle.service.types.ts +++ b/src/services/oracle/oracle.service.types.ts @@ -1,3 +1,8 @@ export type TickerPriceData = { newPrice: number; +} + +export type SetFunctionArguments = { + ticker: string, + price: BigInt } \ No newline at end of file From c0baaa7947e407f800cf3b28f4f32cd9b8285dda Mon Sep 17 00:00:00 2001 From: Filip Pajic Date: Fri, 15 Dec 2023 14:58:31 +0100 Subject: [PATCH 4/7] added reverts to prices redux --- src/redux/prices/prices.redux.actions.ts | 12 ++++++++++++ src/redux/prices/prices.redux.reducer.ts | 23 ++++++++++++++++++++++- src/redux/prices/prices.redux.types.ts | 18 +++++++++++++++++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/redux/prices/prices.redux.actions.ts b/src/redux/prices/prices.redux.actions.ts index 0149f0a..7b3f447 100644 --- a/src/redux/prices/prices.redux.actions.ts +++ b/src/redux/prices/prices.redux.actions.ts @@ -18,4 +18,16 @@ export function setCurrentOffchainPrice(tickerSymbol: string, offchainPrice: num offchainPrice } }; +} + +export function addPriceRevertedTx(txHash: string, tickerSymbol: string, sentPrice: number, chainlinkPrice: number) { + return { + type: EPricesReduxActions.ADD_PRICE_REVERTED_TX, + payload: { + txHash, + tickerSymbol, + sentPrice, + chainlinkPrice + } + } } \ No newline at end of file diff --git a/src/redux/prices/prices.redux.reducer.ts b/src/redux/prices/prices.redux.reducer.ts index 41384e5..eb8b113 100644 --- a/src/redux/prices/prices.redux.reducer.ts +++ b/src/redux/prices/prices.redux.reducer.ts @@ -2,7 +2,8 @@ import { EPricesReduxActions, PricesReduxActions, PricesReduxReducerState } from import { Reducer } from "redux"; const initialState: PricesReduxReducerState = { - current: {} + current: {}, + reverts: {} }; const pricesReduxReducer: Reducer = (state = initialState, action) => { @@ -35,6 +36,26 @@ const pricesReduxReducer: Reducer = } }; } + case EPricesReduxActions.ADD_PRICE_REVERTED_TX: { + const { + txHash, + tickerSymbol, + sentPrice, + chainlinkPrice + } = action.payload; + + return { + ...state, + reverts: { + ...state.reverts, + [txHash]: { + tickerSymbol, + sentPrice, + chainlinkPrice + } + } + } + } default: { return initialState; } diff --git a/src/redux/prices/prices.redux.types.ts b/src/redux/prices/prices.redux.types.ts index e21a682..0d37bbf 100644 --- a/src/redux/prices/prices.redux.types.ts +++ b/src/redux/prices/prices.redux.types.ts @@ -3,6 +3,7 @@ import { ReduxAction } from "../redux.types"; export enum EPricesReduxActions { SET_CURRENT_ONCHAIN_PRICE = "SET_CURRENT_ONCHAIN_PRICE", SET_CURRENT_OFFCHAIN_PRICE = "SET_CURRENT_OFFCHAIN_PRICE", + ADD_PRICE_REVERTED_TX = "ADD_PRICE_REVERTED_TX" } export type SetCurrentOnchainPriceAction = ReduxAction +export type AddPriceRevertedTx = ReduxAction + export type PricesReduxActions = SetCurrentOnchainPriceAction | - SetCurrentOffchainPriceAction; + SetCurrentOffchainPriceAction | + AddPriceRevertedTx; export type PricesReduxReducerState = { current: { @@ -26,4 +35,11 @@ export type PricesReduxReducerState = { offchain?: number; } } + reverts: { + [txHash: string]: { + tickerSymbol: string, + sentPrice: number, + chainlinkPrice: number + } + } } \ No newline at end of file From 1635209159253e4c5219cca761258428360ee212 Mon Sep 17 00:00:00 2001 From: Filip Pajic Date: Fri, 15 Dec 2023 15:13:35 +0100 Subject: [PATCH 5/7] created reverts api route --- src/api/v1/definitions/reverts.route.d.ts | 31 +++++++++++++++++++++ src/api/v1/index.ts | 5 +++- src/api/v1/routes/reverts.route.ts | 24 ++++++++++++++++ src/entities/revert.entity.ts | 24 ++++++++++++++++ src/redux/prices/prices.redux.actions.ts | 2 +- src/redux/prices/prices.redux.reducer.ts | 2 +- src/services/etherscan/etherscan.service.ts | 1 - src/services/oracle/oracle.service.ts | 4 +-- src/services/oracle/oracle.service.types.ts | 2 +- 9 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 src/api/v1/definitions/reverts.route.d.ts create mode 100644 src/api/v1/routes/reverts.route.ts create mode 100644 src/entities/revert.entity.ts diff --git a/src/api/v1/definitions/reverts.route.d.ts b/src/api/v1/definitions/reverts.route.d.ts new file mode 100644 index 0000000..ba8bcc3 --- /dev/null +++ b/src/api/v1/definitions/reverts.route.d.ts @@ -0,0 +1,31 @@ +import { NextFunction, Request as ExpressRequest, Response as ExpressResponse } from "express"; +import { IResponseSuccess } from "../../../utils/response.util"; +import { EmptyObject, ParamsDictionary } from "../../../types/util.types"; +import { RevertEntity } from "../../../entities/revert.entity"; + +export enum ERevertsRoute { + GetRevertList = "GetRevertList" +} + +declare namespace RevertsRouteDefinitions { + type ResponseBody = + T extends ERevertsRoute.GetRevertList ? RevertEntity[] : + EmptyObject + + type RequestBody = // eslint-disable-line @typescript-eslint/no-unused-vars + EmptyObject; + + type RequestQueries = // eslint-disable-line @typescript-eslint/no-unused-vars + EmptyObject + + type RequestParams = // eslint-disable-line @typescript-eslint/no-unused-vars + EmptyObject + + type Response = ExpressResponse>> + + type Request = ExpressRequest & ParamsDictionary, IResponseSuccess>, RequestBody, RequestQueries> + + type RouteMethod = (request: Request, response: Response, next: NextFunction) => Promise; +} + +export default RevertsRouteDefinitions; \ No newline at end of file diff --git a/src/api/v1/index.ts b/src/api/v1/index.ts index b59c252..5c7b098 100644 --- a/src/api/v1/index.ts +++ b/src/api/v1/index.ts @@ -3,14 +3,17 @@ import { error } from "./middlewares/error.middleware"; import StatusRoute from "./routes/status.route"; import TickersRoute from "./routes/tickers.route"; import TickersValidator from "./validators/tickers.validator"; +import RevertsRoute from "./routes/reverts.route"; const v1 = Router(); +v1.get("/status", StatusRoute.getStatus); + v1.get("/tickers/", TickersRoute.getTickerList); v1.get("/tickers/:tickerSymbol", TickersValidator.validateGetTicker, TickersRoute.getTicker); v1.get("/tickers/:tickerSymbol/history", TickersValidator.validateGetTickerPriceHistory, TickersRoute.getTickerPriceHistory); -v1.get("/status", StatusRoute.getStatus); +v1.get("/reverts/", RevertsRoute.getRevertList); v1.use(error); diff --git a/src/api/v1/routes/reverts.route.ts b/src/api/v1/routes/reverts.route.ts new file mode 100644 index 0000000..a650185 --- /dev/null +++ b/src/api/v1/routes/reverts.route.ts @@ -0,0 +1,24 @@ +import RevertsRouteDefinitions, { ERevertsRoute } from "../definitions/reverts.route"; +import store from "../../../redux/store"; +import { RevertEntity, revertEntityFromReduxState } from "../../../entities/revert.entity"; +import { APIResponse } from "../../../utils/response.util"; + +class RevertsRoute { + public static getRevertList: RevertsRouteDefinitions.RouteMethod = async (request, response, next) => { + try { + const rootState = store.getState(); + const txHashes = Object.keys(rootState.prices.reverts); + + const revertList: RevertEntity[] = []; + txHashes.forEach(txHash => { + revertList.push(revertEntityFromReduxState(txHash, rootState)); + }); + + return response.status(200).json(APIResponse.success(revertList)); + } catch (error) { + next(error); + } + }; +} + +export default RevertsRoute; \ No newline at end of file diff --git a/src/entities/revert.entity.ts b/src/entities/revert.entity.ts new file mode 100644 index 0000000..89ba6a2 --- /dev/null +++ b/src/entities/revert.entity.ts @@ -0,0 +1,24 @@ +import { RootState } from "../redux/redux.types"; +import { Nullable } from "../types/util.types"; + +export type RevertEntity = { + txHash: string, + tickerSymbol: string, + sentPrice: number, + chainlinkPrice: number +} + +export function revertEntityFromReduxState(txHash: string, state: RootState): Nullable { + const reverts = state.prices.reverts[txHash]; + + if (reverts) { + return { + txHash: txHash, + tickerSymbol: reverts.tickerSymbol, + chainlinkPrice: reverts.chainlinkPrice, + sentPrice: reverts.sentPrice + }; + } else { + return undefined; + } +} \ No newline at end of file diff --git a/src/redux/prices/prices.redux.actions.ts b/src/redux/prices/prices.redux.actions.ts index 7b3f447..70ec41e 100644 --- a/src/redux/prices/prices.redux.actions.ts +++ b/src/redux/prices/prices.redux.actions.ts @@ -29,5 +29,5 @@ export function addPriceRevertedTx(txHash: string, tickerSymbol: string, sentPri sentPrice, chainlinkPrice } - } + }; } \ No newline at end of file diff --git a/src/redux/prices/prices.redux.reducer.ts b/src/redux/prices/prices.redux.reducer.ts index eb8b113..68b7d32 100644 --- a/src/redux/prices/prices.redux.reducer.ts +++ b/src/redux/prices/prices.redux.reducer.ts @@ -54,7 +54,7 @@ const pricesReduxReducer: Reducer = chainlinkPrice } } - } + }; } default: { return initialState; diff --git a/src/services/etherscan/etherscan.service.ts b/src/services/etherscan/etherscan.service.ts index e029575..bf0feac 100644 --- a/src/services/etherscan/etherscan.service.ts +++ b/src/services/etherscan/etherscan.service.ts @@ -2,7 +2,6 @@ import RestService from "../rest.service"; import { CONFIG } from "../../config"; import { EAuthenticationType } from "../../types/auth.types"; import { EtherscanContractTxHistoryResponse, EtherscanRevertedTxHistory } from "./etherscan.service.types"; -import {ethers} from "ethers"; class EtherscanService extends RestService { constructor() { diff --git a/src/services/oracle/oracle.service.ts b/src/services/oracle/oracle.service.ts index db341a0..6a3e880 100644 --- a/src/services/oracle/oracle.service.ts +++ b/src/services/oracle/oracle.service.ts @@ -2,7 +2,7 @@ import Web3Service from "../web3.service"; import { CONFIG } from "../../config"; import logger from "../../utils/logger.util"; import { TypedEventLog } from "../../contracts/common"; -import {SetFunctionArguments, TickerPriceData} from "./oracle.service.types"; +import { SetFunctionArguments, TickerPriceData } from "./oracle.service.types"; class OracleService { private _tickerPriceContract; @@ -73,7 +73,7 @@ class OracleService { return { ticker: txDescription.args[0], price: txDescription.args[1] - } + }; } private parseTickerPriceUpdatedEvent(event: TypedEventLog): TickerPriceData { diff --git a/src/services/oracle/oracle.service.types.ts b/src/services/oracle/oracle.service.types.ts index d8a1289..5f22a3b 100644 --- a/src/services/oracle/oracle.service.types.ts +++ b/src/services/oracle/oracle.service.types.ts @@ -4,5 +4,5 @@ export type TickerPriceData = { export type SetFunctionArguments = { ticker: string, - price: BigInt + price: bigint } \ No newline at end of file From a298f7631ea673042930827c19ccf542876d0a92 Mon Sep 17 00:00:00 2001 From: Filip Pajic Date: Fri, 15 Dec 2023 15:50:06 +0100 Subject: [PATCH 6/7] reverts history cron job created --- src/jobs/price.jobs.ts | 44 ++++++++++++++++++++++-- src/redux/prices/prices.redux.actions.ts | 9 +++-- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/jobs/price.jobs.ts b/src/jobs/price.jobs.ts index 2409578..15e3f47 100644 --- a/src/jobs/price.jobs.ts +++ b/src/jobs/price.jobs.ts @@ -2,17 +2,23 @@ import CoinGeckoService from "../services/coingecko/coingecko.service"; import CronService from "../services/cron.service"; import store from "../redux/store"; import { getCoingeckoIdByChainlinkTicker } from "../constants/coingecko"; -import { setCurrentOffchainPrice, setCurrentOnchainPrice } from "../redux/prices/prices.redux.actions"; +import { + addPriceRevertedTx, + setCurrentOffchainPrice, + setCurrentOnchainPrice +} from "../redux/prices/prices.redux.actions"; import OracleService from "../services/oracle/oracle.service"; import logger from "../utils/logger.util"; import { RootSocket } from "../index"; -import Web3Service from "../services/web3.service"; import { CONFIG } from "../config"; +import EtherscanService from "../services/etherscan/etherscan.service"; import ChainlinkService from "../services/chainlink.service"; +import Web3Service from "../services/web3.service"; const coinGecko = new CoinGeckoService(); +const etherscan = new EtherscanService(); + const oracleService = new OracleService(); -const registryContract = Web3Service.getTickerUSDFeedRegistryContract(CONFIG.TICKER_USD_FEED_REGISTRY); export const setupOffchainPriceFetchingJob = async () => { CronService.scheduleRecurringJob(async () => { @@ -62,5 +68,37 @@ export const setupOnchainPriceFetchingJobFor = async (tickerSymbol: string) => { }); }; +let lastBlockCache: number = CONFIG.CONTRACTS_DEPLOYMENT_BLOCK; +const fetchPriceUpdatingRevertHistory = async () => { + logger.log(`Trying to find reverts since block number: ${lastBlockCache}`) + + const currentBlockNumber = await Web3Service.provider.getBlockNumber(); + const revertedTransactions = await etherscan.getRevertedTxHistory(CONFIG.TICKER_PRICE_STORAGE, lastBlockCache); + lastBlockCache = currentBlockNumber; + + for (let i = 0; i < revertedTransactions.length; i++) { + const tx = revertedTransactions[i]; + logger.log(`Revert found with tx hash ${tx.hash}`) + + const parsedTxInputs = oracleService.parseInputData(tx.input); + const chainlinkFeedAddress = store.getState().tickers.chainlinkFeed[parsedTxInputs.ticker]; + + // @TODO Handle invalid ticker param sent + if (!chainlinkFeedAddress) { + continue; + } + + const chainlinkFeed = new ChainlinkService(chainlinkFeedAddress); + const chainlinkPrice = await chainlinkFeed.getPriceForBlock(Number(tx.blockNumber)); + + store.dispatch(addPriceRevertedTx(tx.hash, parsedTxInputs.ticker, Number(parsedTxInputs.price), Number(chainlinkPrice))); + } +} + export const setupPriceUpdateRevertObserverJob = async () => { + const minutesRefreshRate = 30; + const runOnInit = true; + logger.log("Setting up revert history job") + + CronService.scheduleRecurringJob(fetchPriceUpdatingRevertHistory, minutesRefreshRate, runOnInit); }; \ No newline at end of file diff --git a/src/redux/prices/prices.redux.actions.ts b/src/redux/prices/prices.redux.actions.ts index 70ec41e..fc7e218 100644 --- a/src/redux/prices/prices.redux.actions.ts +++ b/src/redux/prices/prices.redux.actions.ts @@ -1,4 +1,9 @@ -import { EPricesReduxActions, SetCurrentOffchainPriceAction, SetCurrentOnchainPriceAction } from "./prices.redux.types"; +import { + AddPriceRevertedTx, + EPricesReduxActions, + SetCurrentOffchainPriceAction, + SetCurrentOnchainPriceAction +} from "./prices.redux.types"; export function setCurrentOnchainPrice(tickerSymbol: string, onchainPrice: number): SetCurrentOnchainPriceAction { return { @@ -20,7 +25,7 @@ export function setCurrentOffchainPrice(tickerSymbol: string, offchainPrice: num }; } -export function addPriceRevertedTx(txHash: string, tickerSymbol: string, sentPrice: number, chainlinkPrice: number) { +export function addPriceRevertedTx(txHash: string, tickerSymbol: string, sentPrice: number, chainlinkPrice: number): AddPriceRevertedTx { return { type: EPricesReduxActions.ADD_PRICE_REVERTED_TX, payload: { From b786ce7e32f87e2cfd9902c07a0ed712ca5f91b8 Mon Sep 17 00:00:00 2001 From: Filip Pajic Date: Fri, 15 Dec 2023 15:50:20 +0100 Subject: [PATCH 7/7] lint fixes --- src/jobs/price.jobs.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/jobs/price.jobs.ts b/src/jobs/price.jobs.ts index 15e3f47..3f37899 100644 --- a/src/jobs/price.jobs.ts +++ b/src/jobs/price.jobs.ts @@ -70,7 +70,7 @@ export const setupOnchainPriceFetchingJobFor = async (tickerSymbol: string) => { let lastBlockCache: number = CONFIG.CONTRACTS_DEPLOYMENT_BLOCK; const fetchPriceUpdatingRevertHistory = async () => { - logger.log(`Trying to find reverts since block number: ${lastBlockCache}`) + logger.log(`Trying to find reverts since block number: ${lastBlockCache}`); const currentBlockNumber = await Web3Service.provider.getBlockNumber(); const revertedTransactions = await etherscan.getRevertedTxHistory(CONFIG.TICKER_PRICE_STORAGE, lastBlockCache); @@ -78,7 +78,7 @@ const fetchPriceUpdatingRevertHistory = async () => { for (let i = 0; i < revertedTransactions.length; i++) { const tx = revertedTransactions[i]; - logger.log(`Revert found with tx hash ${tx.hash}`) + logger.log(`Revert found with tx hash ${tx.hash}`); const parsedTxInputs = oracleService.parseInputData(tx.input); const chainlinkFeedAddress = store.getState().tickers.chainlinkFeed[parsedTxInputs.ticker]; @@ -93,12 +93,12 @@ const fetchPriceUpdatingRevertHistory = async () => { store.dispatch(addPriceRevertedTx(tx.hash, parsedTxInputs.ticker, Number(parsedTxInputs.price), Number(chainlinkPrice))); } -} +}; export const setupPriceUpdateRevertObserverJob = async () => { const minutesRefreshRate = 30; const runOnInit = true; - logger.log("Setting up revert history job") + logger.log("Setting up revert history job"); CronService.scheduleRecurringJob(fetchPriceUpdatingRevertHistory, minutesRefreshRate, runOnInit); }; \ No newline at end of file