diff --git a/package.json b/package.json index 07965e5..8ed259c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "dotenv": "^16.3.1", "ethers": "^6.9.0", "express": "^4.18.2", + "log4js": "^6.9.1", "node-cron": "^3.0.3", "redux": "^5.0.0" } diff --git a/src/constants/coingecko.ts b/src/constants/coingecko.ts index a410d36..e7c0df5 100644 --- a/src/constants/coingecko.ts +++ b/src/constants/coingecko.ts @@ -7,7 +7,7 @@ const chainlinkTickerToCoingeckoMap: {[symbol: string]: string} = { "ETH": "ethereum", "LINK": "chainlink", "SNX": "havven" -} +}; export function getCoingeckoIdByChainlinkTicker(ticker: string) { return chainlinkTickerToCoingeckoMap[ticker]; diff --git a/src/index.ts b/src/index.ts index 6ffa4ab..3069ff1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import router from "./app"; import { createServer, Server } from "http"; +import logger from "./utils/logger.util"; const server: Server = createServer(router); @@ -8,8 +9,8 @@ server.listen(router.get("port"), router.get("host"), async () => { process.exit(); }); - console.log(`Server started on ${router.get("host")}:${router.get("port")}`); - console.log("Press CTRL-C to stop\n"); + logger.log(`Server started on ${router.get("host")}:${router.get("port")}`); + logger.log("Press CTRL-C to stop\n"); }); export default server; \ No newline at end of file diff --git a/src/jobs/app.jobs.ts b/src/jobs/app.jobs.ts index 0a59a2d..2d26178 100644 --- a/src/jobs/app.jobs.ts +++ b/src/jobs/app.jobs.ts @@ -1,7 +1,9 @@ import { initTickerState, setupTickerFetchingJob } from "./ticker.jobs"; -import {setupOffchainPriceFetchingJob} from "./price.jobs"; +import { setupOffchainPriceFetchingJob } from "./price.jobs"; +import logger from "../utils/logger.util"; const initAppState = async () => { + logger.log("Initialising the app state"); await initTickerState(); }; @@ -9,5 +11,5 @@ export const initApp = async () => { await initAppState(); await setupTickerFetchingJob(); - await setupOffchainPriceFetchingJob() + await setupOffchainPriceFetchingJob(); }; \ No newline at end of file diff --git a/src/jobs/price.jobs.ts b/src/jobs/price.jobs.ts index 41b2278..2c5baf9 100644 --- a/src/jobs/price.jobs.ts +++ b/src/jobs/price.jobs.ts @@ -1,24 +1,30 @@ 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} from "../redux/prices/prices.redux.actions"; +import { getCoingeckoIdByChainlinkTicker } from "../constants/coingecko"; +import { setCurrentOffchainPrice } from "../redux/prices/prices.redux.actions"; +import OracleService from "../services/oracle/oracle.service"; export const setupOffchainPriceFetchingJob = async () => { const coinGecko = new CoinGeckoService(); + const oracleService = new OracleService(); CronService.scheduleRecurringJob(async () => { const tickerSymbols = store.getState().tickers.symbols; const tickerCoinGeckoIds = tickerSymbols.map(getCoingeckoIdByChainlinkTicker); const offchainPrices = await coinGecko.getPrices(tickerCoinGeckoIds); - tickerCoinGeckoIds.forEach((id, index) => { + for (let index = 0; index < tickerSymbols.length; index++) { + const id = tickerCoinGeckoIds[index]; const symbol = tickerSymbols[index]; const currentPrice = offchainPrices[id]?.usd; if (currentPrice) { store.dispatch(setCurrentOffchainPrice(symbol, currentPrice)); } - }) - }) -} \ No newline at end of file + try { + await oracleService.updateOnchainPrice(symbol, currentPrice); + } catch (err) { console.log(err);} + } + }); +}; \ No newline at end of file diff --git a/src/jobs/ticker.jobs.ts b/src/jobs/ticker.jobs.ts index 0983d23..793a89a 100644 --- a/src/jobs/ticker.jobs.ts +++ b/src/jobs/ticker.jobs.ts @@ -2,6 +2,7 @@ import TickersService from "../services/tickers/tickers.service"; import store from "../redux/store"; import { addTicker } from "../redux/tickers/tickers.redux.actions"; import { TickerRegistryData } from "../services/tickers/tickers.service.types"; +import logger from "../utils/logger.util"; const tickersService = new TickersService(); @@ -10,10 +11,17 @@ const updateReduxTickerState = (tickerData: TickerRegistryData) => { }; export const initTickerState = async () => { + logger.log("Fetching all currently available tickers"); const tickers = await tickersService.getAllTickers(); - tickers.forEach(updateReduxTickerState); + const symbols: string[] = []; + tickers.forEach(tickerData => { + updateReduxTickerState(tickerData); + symbols.push(tickerData.tickerSymbol); + }); + logger.log("Tickers found: ", symbols); }; export const setupTickerFetchingJob = async () => { + logger.log("Setting up the Ticker Fetching observer"); tickersService.listenForTickerUpdates(updateReduxTickerState).then(); }; \ 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 a9bf849..0149f0a 100644 --- a/src/redux/prices/prices.redux.actions.ts +++ b/src/redux/prices/prices.redux.actions.ts @@ -1,4 +1,4 @@ -import {EPricesReduxActions, SetCurrentOffchainPriceAction, SetCurrentOnchainPriceAction} from "./prices.redux.types"; +import { EPricesReduxActions, SetCurrentOffchainPriceAction, SetCurrentOnchainPriceAction } from "./prices.redux.types"; export function setCurrentOnchainPrice(tickerSymbol: string, onchainPrice: number): SetCurrentOnchainPriceAction { return { @@ -7,7 +7,7 @@ export function setCurrentOnchainPrice(tickerSymbol: string, onchainPrice: numbe tickerSymbol, onchainPrice } - } + }; } export function setCurrentOffchainPrice(tickerSymbol: string, offchainPrice: number): SetCurrentOffchainPriceAction { @@ -17,5 +17,5 @@ export function setCurrentOffchainPrice(tickerSymbol: string, offchainPrice: num tickerSymbol, offchainPrice } - } + }; } \ 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 d88a8fe..41384e5 100644 --- a/src/redux/prices/prices.redux.reducer.ts +++ b/src/redux/prices/prices.redux.reducer.ts @@ -1,9 +1,9 @@ -import {EPricesReduxActions, PricesReduxActions, PricesReduxReducerState} from "./prices.redux.types"; -import {Reducer} from "redux"; +import { EPricesReduxActions, PricesReduxActions, PricesReduxReducerState } from "./prices.redux.types"; +import { Reducer } from "redux"; const initialState: PricesReduxReducerState = { current: {} -} +}; const pricesReduxReducer: Reducer = (state = initialState, action) => { switch (action.type) { @@ -19,7 +19,7 @@ const pricesReduxReducer: Reducer = onchain: onchainPrice } } - } + }; } case EPricesReduxActions.SET_CURRENT_OFFCHAIN_PRICE: { const { tickerSymbol, offchainPrice } = action.payload; @@ -33,12 +33,12 @@ const pricesReduxReducer: Reducer = offchain: offchainPrice } } - } + }; } default: { return initialState; } } -} +}; export default pricesReduxReducer; \ No newline at end of file diff --git a/src/redux/prices/prices.redux.types.ts b/src/redux/prices/prices.redux.types.ts index 90729a1..e21a682 100644 --- a/src/redux/prices/prices.redux.types.ts +++ b/src/redux/prices/prices.redux.types.ts @@ -1,4 +1,4 @@ -import {ReduxAction} from "../redux.types"; +import { ReduxAction } from "../redux.types"; export enum EPricesReduxActions { SET_CURRENT_ONCHAIN_PRICE = "SET_CURRENT_ONCHAIN_PRICE", diff --git a/src/services/oracle/oracle.service.ts b/src/services/oracle/oracle.service.ts new file mode 100644 index 0000000..654c272 --- /dev/null +++ b/src/services/oracle/oracle.service.ts @@ -0,0 +1,41 @@ +import Web3Service from "../web3.service"; +import { CONFIG } from "../../config"; +import logger from "../../utils/logger.util"; + +class OracleService { + private _tickerPriceContract; + + constructor() { + this._tickerPriceContract = Web3Service.getTickerPriceStorageContract(CONFIG.TICKER_PRICE_STORAGE, true); + } + + public async updateOnchainPrice(ticker: string, newPrice: number) { + const numberOfChainlinkDecimals = 8; + // @TODO Update to use BigInt + const parsedPrice = Math.trunc(newPrice * 10**numberOfChainlinkDecimals); + logger.log("Trying to update onchain price of ", ticker, " to ", newPrice); + + const willRevert = await this.isPriceUpdateGoingToRevert(ticker, parsedPrice); + + if(!willRevert) { + const tx = await this._tickerPriceContract.set(ticker, parsedPrice); + logger.info(`Transaction for updating ${ticker} to ${newPrice} sent, tx hash: ${tx.hash}`); + await tx.wait(1); + logger.info(`Transaction with tx hash: ${tx.hash} successful`); + } else { + logger.info(`Updating ticker ${ticker} to price ${newPrice} canceled`); + } + } + + public async isPriceUpdateGoingToRevert(ticker: string, newPrice: number): Promise { + try { + await this._tickerPriceContract.set.staticCall(ticker, newPrice); + + return false; + } catch (err) { + return true; + } + } +} + +export default OracleService; \ No newline at end of file diff --git a/src/services/prices/prices.service.ts b/src/services/prices/prices.service.ts deleted file mode 100644 index 0ec61cb..0000000 --- a/src/services/prices/prices.service.ts +++ /dev/null @@ -1,6 +0,0 @@ -class PricesService { - - public static updateOffchainPrices() { - - } -} \ No newline at end of file diff --git a/src/services/tickers/tickers.service.ts b/src/services/tickers/tickers.service.ts index c498d6e..b590127 100644 --- a/src/services/tickers/tickers.service.ts +++ b/src/services/tickers/tickers.service.ts @@ -2,6 +2,7 @@ import Web3Service from "../web3.service"; import { CONFIG } from "../../config"; import { TypedEventLog } from "../../contracts/common"; import { TickerRegistryData } from "./tickers.service.types"; +import logger from "../../utils/logger.util"; class TickersService { private _registryContract; @@ -21,6 +22,8 @@ class TickersService { } const parsedEvent = this.parseTickerFeedUpdatedEvent(data); + logger.log("New ticker found: ", parsedEvent); + onTickerUpdate(parsedEvent); }); } diff --git a/src/services/web3.service.ts b/src/services/web3.service.ts index 4606a65..2fca3bc 100644 --- a/src/services/web3.service.ts +++ b/src/services/web3.service.ts @@ -1,5 +1,5 @@ import { ethers, JsonRpcProvider, Wallet } from "ethers"; -import { TickerUSDFeedRegistryAbi__factory } from "../contracts"; +import { TickerPriceStorageAbi__factory, TickerUSDFeedRegistryAbi__factory } from "../contracts"; import { CONFIG } from "../config"; @@ -12,14 +12,16 @@ class Web3Service { this.signer = new ethers.Wallet(signerPrivateKey, this.provider); } + private _getRunner(isMutatingState: boolean) { + return isMutatingState ? this.signer : this.provider; + } + public getTickerUSDFeedRegistryContract(address: string, isMutatingState?: boolean) { - const contract = TickerUSDFeedRegistryAbi__factory.connect(address); + return TickerUSDFeedRegistryAbi__factory.connect(address, this._getRunner(!!isMutatingState)); + } - if (isMutatingState) { - return contract.connect(this.signer); - } else { - return contract.connect(this.provider); - } + public getTickerPriceStorageContract(address: string, isMutatingState?: boolean) { + return TickerPriceStorageAbi__factory.connect(address, this._getRunner(!!isMutatingState)); } } diff --git a/src/utils/logger.util.ts b/src/utils/logger.util.ts new file mode 100644 index 0000000..7cbf7df --- /dev/null +++ b/src/utils/logger.util.ts @@ -0,0 +1,13 @@ +import * as log4js from "log4js"; +log4js.configure({ + appenders: { + out: { type: "stdout" } + }, + categories: { + default: { appenders: ["out"], level: "all" } + } +}); + +const logger = log4js.getLogger(); + +export default logger; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 31ce278..8b55430 100644 --- a/yarn.lock +++ b/yarn.lock @@ -681,6 +681,11 @@ cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -1007,7 +1012,7 @@ flat-cache@^3.0.4: keyv "^4.5.3" rimraf "^3.0.2" -flatted@^3.2.9: +flatted@^3.2.7, flatted@^3.2.9: version "3.2.9" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== @@ -1045,6 +1050,15 @@ fs-extra@^7.0.0: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1134,7 +1148,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2, graceful-fs@^4.1.6: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -1342,6 +1356,17 @@ lodash@^4.17.15: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log4js@^6.9.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" + integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + flatted "^3.2.7" + rfdc "^1.3.0" + streamroller "^3.1.5" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -1660,6 +1685,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -1773,6 +1803,15 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +streamroller@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" + integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + fs-extra "^8.1.0" + string-format@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b"