Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: Price Updating - revert history observation #6

Merged
merged 7 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading