Skip to content

Commit

Permalink
core: Price Updating - revert history observation (#6)
Browse files Browse the repository at this point in the history
* created the etherscan service

* chainlink service created

* added tx input parser for oracle service

* added reverts to prices redux

* created reverts api route

* reverts history cron job created

* lint fixes
  • Loading branch information
pajicf authored Dec 15, 2023
1 parent fdbdd0a commit e3d397c
Show file tree
Hide file tree
Showing 17 changed files with 336 additions and 10 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=

Expand Down
31 changes: 31 additions & 0 deletions src/api/v1/definitions/reverts.route.d.ts
Original file line number Diff line number Diff line change
@@ -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> =
T extends ERevertsRoute.GetRevertList ? RevertEntity[] :
EmptyObject

type RequestBody<T extends ERevertsRoute> = // eslint-disable-line @typescript-eslint/no-unused-vars
EmptyObject;

type RequestQueries<T extends ERevertsRoute> = // eslint-disable-line @typescript-eslint/no-unused-vars
EmptyObject

type RequestParams<T extends ERevertsRoute> = // eslint-disable-line @typescript-eslint/no-unused-vars
EmptyObject

type Response<T extends ERevertsRoute> = ExpressResponse<IResponseSuccess<ResponseBody<T>>>

type Request<T extends ERevertsRoute> = ExpressRequest<RequestParams<T> & ParamsDictionary, IResponseSuccess<ResponseBody<T>>, RequestBody<T>, RequestQueries<T>>

type RouteMethod<T extends ERevertsRoute> = (request: Request<T>, response: Response<T>, next: NextFunction) => Promise<any>;
}

export default RevertsRouteDefinitions;
5 changes: 4 additions & 1 deletion src/api/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
24 changes: 24 additions & 0 deletions src/api/v1/routes/reverts.route.ts
Original file line number Diff line number Diff line change
@@ -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<ERevertsRoute.GetRevertList> = 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;
10 changes: 8 additions & 2 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
};
24 changes: 24 additions & 0 deletions src/entities/revert.entity.ts
Original file line number Diff line number Diff line change
@@ -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<RevertEntity> {
const reverts = state.prices.reverts[txHash];

if (reverts) {
return {
txHash: txHash,
tickerSymbol: reverts.tickerSymbol,
chainlinkPrice: reverts.chainlinkPrice,
sentPrice: reverts.sentPrice
};
} else {
return undefined;
}
}
4 changes: 3 additions & 1 deletion src/jobs/app.jobs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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";

Expand All @@ -13,6 +13,8 @@ const initAppState = async () => {

export const initApp = async () => {
await initAppState();
await setupPriceUpdateRevertObserverJob();


await setupTickerFetchingJob();
await setupOffchainPriceFetchingJob();
Expand Down
47 changes: 46 additions & 1 deletion src/jobs/price.jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@ 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 { 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();

export const setupOffchainPriceFetchingJob = async () => {
Expand Down Expand Up @@ -56,4 +66,39 @@ export const setupOnchainPriceFetchingJobFor = async (tickerSymbol: string) => {
updateReduxTickerOnchainPrice(tickerSymbol, tickerPriceData.newPrice);
RootSocket.emitEvent("TickerPriceUpdated", [tickerSymbol, tickerPriceData.newPrice]);
});
};

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);
};
19 changes: 18 additions & 1 deletion src/redux/prices/prices.redux.actions.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -18,4 +23,16 @@ export function setCurrentOffchainPrice(tickerSymbol: string, offchainPrice: num
offchainPrice
}
};
}

export function addPriceRevertedTx(txHash: string, tickerSymbol: string, sentPrice: number, chainlinkPrice: number): AddPriceRevertedTx {
return {
type: EPricesReduxActions.ADD_PRICE_REVERTED_TX,
payload: {
txHash,
tickerSymbol,
sentPrice,
chainlinkPrice
}
};
}
23 changes: 22 additions & 1 deletion src/redux/prices/prices.redux.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { EPricesReduxActions, PricesReduxActions, PricesReduxReducerState } from
import { Reducer } from "redux";

const initialState: PricesReduxReducerState = {
current: {}
current: {},
reverts: {}
};

const pricesReduxReducer: Reducer<PricesReduxReducerState, PricesReduxActions> = (state = initialState, action) => {
Expand Down Expand Up @@ -35,6 +36,26 @@ const pricesReduxReducer: Reducer<PricesReduxReducerState, PricesReduxActions> =
}
};
}
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;
}
Expand Down
18 changes: 17 additions & 1 deletion src/redux/prices/prices.redux.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EPricesReduxActions.SET_CURRENT_ONCHAIN_PRICE, {
Expand All @@ -15,9 +16,17 @@ export type SetCurrentOffchainPriceAction = ReduxAction<EPricesReduxActions.SET_
offchainPrice: number;
}>

export type AddPriceRevertedTx = ReduxAction<EPricesReduxActions.ADD_PRICE_REVERTED_TX, {
txHash: string,
tickerSymbol: string,
sentPrice: number,
chainlinkPrice: number
}>

export type PricesReduxActions =
SetCurrentOnchainPriceAction |
SetCurrentOffchainPriceAction;
SetCurrentOffchainPriceAction |
AddPriceRevertedTx;

export type PricesReduxReducerState = {
current: {
Expand All @@ -26,4 +35,11 @@ export type PricesReduxReducerState = {
offchain?: number;
}
}
reverts: {
[txHash: string]: {
tickerSymbol: string,
sentPrice: number,
chainlinkPrice: number
}
}
}
20 changes: 20 additions & 0 deletions src/services/chainlink.service.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit e3d397c

Please sign in to comment.