From 263d2d1fba0c9555b1835f29a317a7d14bee8538 Mon Sep 17 00:00:00 2001 From: Franklin Waller Date: Wed, 4 May 2022 15:05:15 +0200 Subject: [PATCH] feat(pairCheckerDeviation): Add support for checking price updates with deviation pushing enabled --- src/modules.ts | 3 +- .../pairDeviationChecker/FluxPriceFeed.json | 569 ++++++++++++++++++ .../PairDeviationCheckerModule.ts | 154 +++++ .../PairDeviationCheckerModuleConfig.ts | 41 ++ .../models/PairDeviationDataRequest.ts | 57 ++ .../models/PairDeviationReport.ts | 8 + .../services/FetchLastUpdateService.ts | 81 +++ .../services/PairDeviationService.ts | 48 ++ .../services/TelegramNotificationService.ts | 79 +++ src/modules/pairDeviationChecker/utils.ts | 18 + src/modules/pushPair/PushPairModule.ts | 4 +- src/modules/pushPair/models/PushPairConfig.ts | 2 - src/services/AppConfigService.ts | 4 +- 13 files changed, 1063 insertions(+), 5 deletions(-) create mode 100644 src/modules/pairDeviationChecker/FluxPriceFeed.json create mode 100644 src/modules/pairDeviationChecker/PairDeviationCheckerModule.ts create mode 100644 src/modules/pairDeviationChecker/models/PairDeviationCheckerModuleConfig.ts create mode 100644 src/modules/pairDeviationChecker/models/PairDeviationDataRequest.ts create mode 100644 src/modules/pairDeviationChecker/models/PairDeviationReport.ts create mode 100644 src/modules/pairDeviationChecker/services/FetchLastUpdateService.ts create mode 100644 src/modules/pairDeviationChecker/services/PairDeviationService.ts create mode 100644 src/modules/pairDeviationChecker/services/TelegramNotificationService.ts create mode 100644 src/modules/pairDeviationChecker/utils.ts diff --git a/src/modules.ts b/src/modules.ts index d919d4f..a81652f 100644 --- a/src/modules.ts +++ b/src/modules.ts @@ -2,10 +2,11 @@ import { FetchJob } from "./jobs/fetch/FetchJob"; import { BalanceCheckerModule } from "./modules/balanceChecker/BalanceCheckerModule"; import { LayerZeroModule } from "./modules/layerZero/LayerZeroModule"; import { PairCheckerModule } from "./modules/pairChecker/PairCheckerModule"; +import { PairDeviationCheckerModule } from "./modules/pairDeviationChecker/PairDeviationCheckerModule"; import { PushPairModule } from "./modules/pushPair/PushPairModule"; import EvmNetwork from "./networks/evm/EvmNetwork"; import { NearNetwork } from "./networks/near/NearNetwork"; export const AVAILABLE_NETWORKS = [EvmNetwork, NearNetwork]; -export const AVAILABLE_MODULES = [LayerZeroModule, PushPairModule, BalanceCheckerModule, PairCheckerModule]; +export const AVAILABLE_MODULES = [LayerZeroModule, PushPairModule, BalanceCheckerModule, PairCheckerModule, PairDeviationCheckerModule]; export const AVAILABLE_JOBS = [FetchJob]; diff --git a/src/modules/pairDeviationChecker/FluxPriceFeed.json b/src/modules/pairDeviationChecker/FluxPriceFeed.json new file mode 100644 index 0000000..840a9a4 --- /dev/null +++ b/src/modules/pairDeviationChecker/FluxPriceFeed.json @@ -0,0 +1,569 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "FluxPriceFeed", + "sourceName": "contracts/FluxPriceFeed.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_validator", + "type": "address" + }, + { + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + }, + { + "internalType": "string", + "name": "_description", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "int256", + "name": "current", + "type": "int256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "roundId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "updatedAt", + "type": "uint256" + } + ], + "name": "AnswerUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "roundId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "startedBy", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "startedAt", + "type": "uint256" + } + ], + "name": "NewRound", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint32", + "name": "aggregatorRoundId", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "int192", + "name": "answer", + "type": "int192" + }, + { + "indexed": false, + "internalType": "address", + "name": "transmitter", + "type": "address" + } + ], + "name": "NewTransmission", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "VALIDATOR_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "description", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_roundId", + "type": "uint256" + } + ], + "name": "getAnswer", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint80", + "name": "_roundId", + "type": "uint80" + } + ], + "name": "getRoundData", + "outputs": [ + { + "internalType": "uint80", + "name": "roundId", + "type": "uint80" + }, + { + "internalType": "int256", + "name": "answer", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "startedAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "updatedAt", + "type": "uint256" + }, + { + "internalType": "uint80", + "name": "answeredInRound", + "type": "uint80" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_roundId", + "type": "uint256" + } + ], + "name": "getTimestamp", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "latestAggregatorRoundId", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "latestAnswer", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "latestRound", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "latestRoundData", + "outputs": [ + { + "internalType": "uint80", + "name": "roundId", + "type": "uint80" + }, + { + "internalType": "int256", + "name": "answer", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "startedAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "updatedAt", + "type": "uint256" + }, + { + "internalType": "uint80", + "name": "answeredInRound", + "type": "uint80" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "latestTimestamp", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "latestTransmissionDetails", + "outputs": [ + { + "internalType": "int192", + "name": "_latestAnswer", + "type": "int192" + }, + { + "internalType": "uint64", + "name": "_latestTimestamp", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int192", + "name": "_answer", + "type": "int192" + } + ], + "name": "transmit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "typeAndVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x60a06040523480156200001157600080fd5b50604051620012ab380380620012ab8339810160408190526200003491620001f2565b620000607f21702c8af46127c7fa207f89d0b0a8441bb32959a0ac7df790e9ab1a25c989268462000086565b60ff821660805280516200007c90600390602084019062000136565b5050505062000349565b62000092828262000096565b5050565b6000828152602081815260408083206001600160a01b038516845290915290205460ff1662000092576000828152602081815260408083206001600160a01b03851684529091529020805460ff19166001179055620000f23390565b6001600160a01b0316816001600160a01b0316837f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a45050565b82805462000144906200030c565b90600052602060002090601f016020900481019282620001685760008555620001b3565b82601f106200018357805160ff1916838001178555620001b3565b82800160010185558215620001b3579182015b82811115620001b357825182559160200191906001019062000196565b50620001c1929150620001c5565b5090565b5b80821115620001c15760008155600101620001c6565b634e487b7160e01b600052604160045260246000fd5b6000806000606084860312156200020857600080fd5b83516001600160a01b03811681146200022057600080fd5b8093505060208085015160ff811681146200023a57600080fd5b60408601519093506001600160401b03808211156200025857600080fd5b818701915087601f8301126200026d57600080fd5b815181811115620002825762000282620001dc565b604051601f8201601f19908116603f01168101908382118183101715620002ad57620002ad620001dc565b816040528281528a86848701011115620002c657600080fd5b600093505b82841015620002ea5784840186015181850187015292850192620002cb565b82841115620002fc5760008684830101525b8096505050505050509250925092565b600181811c908216806200032157607f821691505b602082108114156200034357634e487b7160e01b600052602260045260246000fd5b50919050565b608051610f4662000365600039600061023c0152610f466000f3fe608060405234801561001057600080fd5b50600436106101825760003560e01c80638205bf6a116100d8578063b5ab58dc1161008c578063d547741f11610066578063d547741f146103fd578063e5fe457714610410578063feaf968c1461043b57600080fd5b8063b5ab58dc146103b0578063b633620c146103c3578063c49baebe146103d657600080fd5b806391d14854116100bd57806391d14854146103275780639a6fc8f51461035e578063a217fddf146103a857600080fd5b80638205bf6a146102e657806382b8ebc71461031457600080fd5b806336568abe1161013a5780635ed63b40116101145780635ed63b40146102ab578063668a0f02146102d05780637284e416146102de57600080fd5b806336568abe1461027057806350d25bcd1461028357806354fd4d50146102a357600080fd5b8063248a9ca31161016b578063248a9ca3146101f15780632f2ff15d14610222578063313ce5671461023757600080fd5b806301ffc9a714610187578063181f5a77146101af575b600080fd5b61019a610195366004610c98565b61048b565b60405190151581526020015b60405180910390f35b60408051808201909152601381527f466c757850726963654665656420312e302e300000000000000000000000000060208201525b6040516101a69190610cf2565b6102146101ff366004610d25565b60009081526020819052604090206001015490565b6040519081526020016101a6565b610235610230366004610d3e565b6104c2565b005b61025e7f000000000000000000000000000000000000000000000000000000000000000081565b60405160ff90911681526020016101a6565b61023561027e366004610d3e565b6104ed565b60015463ffffffff1660009081526002602052604090205460170b610214565b610214600181565b6001546102bb9063ffffffff1681565b60405163ffffffff90911681526020016101a6565b60015463ffffffff16610214565b6101e461057e565b60015463ffffffff16600090815260026020526040902054600160c01b900467ffffffffffffffff16610214565b610235610322366004610d7a565b610610565b61019a610335366004610d3e565b6000918252602082815260408084206001600160a01b0393909316845291905290205460ff1690565b61037161036c366004610d9d565b61076b565b6040805169ffffffffffffffffffff968716815260208101959095528401929092526060830152909116608082015260a0016101a6565b610214600081565b6102146103be366004610d25565b61082e565b6102146103d1366004610d25565b610860565b6102147f21702c8af46127c7fa207f89d0b0a8441bb32959a0ac7df790e9ab1a25c9892681565b61023561040b366004610d3e565b6108a0565b6104186108c6565b6040805160179390930b835267ffffffffffffffff9091166020830152016101a6565b61037160015463ffffffff16600081815260026020908152604091829020825180840190935254601781900b808452600160c01b90910467ffffffffffffffff1692909101829052919281908490565b60006001600160e01b03198216637965db0b60e01b14806104bc57506301ffc9a760e01b6001600160e01b03198316145b92915050565b6000828152602081905260409020600101546104de813361094d565b6104e883836109cb565b505050565b6001600160a01b03811633146105705760405162461bcd60e51b815260206004820152602f60248201527f416363657373436f6e74726f6c3a2063616e206f6e6c792072656e6f756e636560448201527f20726f6c657320666f722073656c66000000000000000000000000000000000060648201526084015b60405180910390fd5b61057a8282610a69565b5050565b60606003805461058d90610dc9565b80601f01602080910402602001604051908101604052809291908181526020018280546105b990610dc9565b80156106065780601f106105db57610100808354040283529160200191610606565b820191906000526020600020905b8154815290600101906020018083116105e957829003601f168201915b5050505050905090565b3360009081527f5111aeae4aa79889928e72f88b5872109754de9d419ea9a4e3df5fba21d4d46f602052604090205460ff1661068e5760405162461bcd60e51b815260206004820152601960248201527f43616c6c6572206973206e6f7420612076616c696461746f72000000000000006044820152606401610567565b6001805463ffffffff169060006106a483610e1a565b825463ffffffff9182166101009390930a928302928202191691909117909155604080518082018252601785900b80825267ffffffffffffffff428116602080850191825260018054881660009081526002835287902095519251909316600160c01b0277ffffffffffffffffffffffffffffffffffffffffffffffff909216919091179093555483519182523392820192909252921692507f17eabd0a66fa631f7537cefdd5df6aa25d5ac904cf7596e958d43a75a00d0d68910160405180910390a250565b600080600080600063ffffffff8669ffffffffffffffffffff1611156040518060400160405280600f81526020017f4e6f20646174612070726573656e740000000000000000000000000000000000815250906107db5760405162461bcd60e51b81526004016105679190610cf2565b5050505063ffffffff8316600090815260026020908152604091829020825180840190935254601781900b808452600160c01b90910467ffffffffffffffff169290910182905293949092508291508490565b600063ffffffff82111561084457506000919050565b5063ffffffff1660009081526002602052604090205460170b90565b600063ffffffff82111561087657506000919050565b5063ffffffff16600090815260026020526040902054600160c01b900467ffffffffffffffff1690565b6000828152602081905260409020600101546108bc813361094d565b6104e88383610a69565b6000803332146109185760405162461bcd60e51b815260206004820152601460248201527f4f6e6c792063616c6c61626c6520627920454f410000000000000000000000006044820152606401610567565b505060015463ffffffff16600090815260026020526040902054601781900b91600160c01b90910467ffffffffffffffff1690565b6000828152602081815260408083206001600160a01b038516845290915290205460ff1661057a57610989816001600160a01b03166014610ae8565b610994836020610ae8565b6040516020016109a5929190610e3e565b60408051601f198184030181529082905262461bcd60e51b825261056791600401610cf2565b6000828152602081815260408083206001600160a01b038516845290915290205460ff1661057a576000828152602081815260408083206001600160a01b03851684529091529020805460ff19166001179055610a253390565b6001600160a01b0316816001600160a01b0316837f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a45050565b6000828152602081815260408083206001600160a01b038516845290915290205460ff161561057a576000828152602081815260408083206001600160a01b0385168085529252808320805460ff1916905551339285917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a45050565b60606000610af7836002610ebf565b610b02906002610ede565b67ffffffffffffffff811115610b1a57610b1a610ef6565b6040519080825280601f01601f191660200182016040528015610b44576020820181803683370190505b509050600360fc1b81600081518110610b5f57610b5f610f0c565b60200101906001600160f81b031916908160001a905350600f60fb1b81600181518110610b8e57610b8e610f0c565b60200101906001600160f81b031916908160001a9053506000610bb2846002610ebf565b610bbd906001610ede565b90505b6001811115610c42577f303132333435363738396162636465660000000000000000000000000000000085600f1660108110610bfe57610bfe610f0c565b1a60f81b828281518110610c1457610c14610f0c565b60200101906001600160f81b031916908160001a90535060049490941c93610c3b81610f22565b9050610bc0565b508315610c915760405162461bcd60e51b815260206004820181905260248201527f537472696e67733a20686578206c656e67746820696e73756666696369656e746044820152606401610567565b9392505050565b600060208284031215610caa57600080fd5b81356001600160e01b031981168114610c9157600080fd5b60005b83811015610cdd578181015183820152602001610cc5565b83811115610cec576000848401525b50505050565b6020815260008251806020840152610d11816040850160208701610cc2565b601f01601f19169190910160400192915050565b600060208284031215610d3757600080fd5b5035919050565b60008060408385031215610d5157600080fd5b8235915060208301356001600160a01b0381168114610d6f57600080fd5b809150509250929050565b600060208284031215610d8c57600080fd5b81358060170b8114610c9157600080fd5b600060208284031215610daf57600080fd5b813569ffffffffffffffffffff81168114610c9157600080fd5b600181811c90821680610ddd57607f821691505b60208210811415610dfe57634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052601160045260246000fd5b600063ffffffff80831681811415610e3457610e34610e04565b6001019392505050565b7f416363657373436f6e74726f6c3a206163636f756e7420000000000000000000815260008351610e76816017850160208801610cc2565b7f206973206d697373696e6720726f6c65200000000000000000000000000000006017918401918201528351610eb3816028840160208801610cc2565b01602801949350505050565b6000816000190483118215151615610ed957610ed9610e04565b500290565b60008219821115610ef157610ef1610e04565b500190565b634e487b7160e01b600052604160045260246000fd5b634e487b7160e01b600052603260045260246000fd5b600081610f3157610f31610e04565b50600019019056fea164736f6c6343000809000a", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106101825760003560e01c80638205bf6a116100d8578063b5ab58dc1161008c578063d547741f11610066578063d547741f146103fd578063e5fe457714610410578063feaf968c1461043b57600080fd5b8063b5ab58dc146103b0578063b633620c146103c3578063c49baebe146103d657600080fd5b806391d14854116100bd57806391d14854146103275780639a6fc8f51461035e578063a217fddf146103a857600080fd5b80638205bf6a146102e657806382b8ebc71461031457600080fd5b806336568abe1161013a5780635ed63b40116101145780635ed63b40146102ab578063668a0f02146102d05780637284e416146102de57600080fd5b806336568abe1461027057806350d25bcd1461028357806354fd4d50146102a357600080fd5b8063248a9ca31161016b578063248a9ca3146101f15780632f2ff15d14610222578063313ce5671461023757600080fd5b806301ffc9a714610187578063181f5a77146101af575b600080fd5b61019a610195366004610c98565b61048b565b60405190151581526020015b60405180910390f35b60408051808201909152601381527f466c757850726963654665656420312e302e300000000000000000000000000060208201525b6040516101a69190610cf2565b6102146101ff366004610d25565b60009081526020819052604090206001015490565b6040519081526020016101a6565b610235610230366004610d3e565b6104c2565b005b61025e7f000000000000000000000000000000000000000000000000000000000000000081565b60405160ff90911681526020016101a6565b61023561027e366004610d3e565b6104ed565b60015463ffffffff1660009081526002602052604090205460170b610214565b610214600181565b6001546102bb9063ffffffff1681565b60405163ffffffff90911681526020016101a6565b60015463ffffffff16610214565b6101e461057e565b60015463ffffffff16600090815260026020526040902054600160c01b900467ffffffffffffffff16610214565b610235610322366004610d7a565b610610565b61019a610335366004610d3e565b6000918252602082815260408084206001600160a01b0393909316845291905290205460ff1690565b61037161036c366004610d9d565b61076b565b6040805169ffffffffffffffffffff968716815260208101959095528401929092526060830152909116608082015260a0016101a6565b610214600081565b6102146103be366004610d25565b61082e565b6102146103d1366004610d25565b610860565b6102147f21702c8af46127c7fa207f89d0b0a8441bb32959a0ac7df790e9ab1a25c9892681565b61023561040b366004610d3e565b6108a0565b6104186108c6565b6040805160179390930b835267ffffffffffffffff9091166020830152016101a6565b61037160015463ffffffff16600081815260026020908152604091829020825180840190935254601781900b808452600160c01b90910467ffffffffffffffff1692909101829052919281908490565b60006001600160e01b03198216637965db0b60e01b14806104bc57506301ffc9a760e01b6001600160e01b03198316145b92915050565b6000828152602081905260409020600101546104de813361094d565b6104e883836109cb565b505050565b6001600160a01b03811633146105705760405162461bcd60e51b815260206004820152602f60248201527f416363657373436f6e74726f6c3a2063616e206f6e6c792072656e6f756e636560448201527f20726f6c657320666f722073656c66000000000000000000000000000000000060648201526084015b60405180910390fd5b61057a8282610a69565b5050565b60606003805461058d90610dc9565b80601f01602080910402602001604051908101604052809291908181526020018280546105b990610dc9565b80156106065780601f106105db57610100808354040283529160200191610606565b820191906000526020600020905b8154815290600101906020018083116105e957829003601f168201915b5050505050905090565b3360009081527f5111aeae4aa79889928e72f88b5872109754de9d419ea9a4e3df5fba21d4d46f602052604090205460ff1661068e5760405162461bcd60e51b815260206004820152601960248201527f43616c6c6572206973206e6f7420612076616c696461746f72000000000000006044820152606401610567565b6001805463ffffffff169060006106a483610e1a565b825463ffffffff9182166101009390930a928302928202191691909117909155604080518082018252601785900b80825267ffffffffffffffff428116602080850191825260018054881660009081526002835287902095519251909316600160c01b0277ffffffffffffffffffffffffffffffffffffffffffffffff909216919091179093555483519182523392820192909252921692507f17eabd0a66fa631f7537cefdd5df6aa25d5ac904cf7596e958d43a75a00d0d68910160405180910390a250565b600080600080600063ffffffff8669ffffffffffffffffffff1611156040518060400160405280600f81526020017f4e6f20646174612070726573656e740000000000000000000000000000000000815250906107db5760405162461bcd60e51b81526004016105679190610cf2565b5050505063ffffffff8316600090815260026020908152604091829020825180840190935254601781900b808452600160c01b90910467ffffffffffffffff169290910182905293949092508291508490565b600063ffffffff82111561084457506000919050565b5063ffffffff1660009081526002602052604090205460170b90565b600063ffffffff82111561087657506000919050565b5063ffffffff16600090815260026020526040902054600160c01b900467ffffffffffffffff1690565b6000828152602081905260409020600101546108bc813361094d565b6104e88383610a69565b6000803332146109185760405162461bcd60e51b815260206004820152601460248201527f4f6e6c792063616c6c61626c6520627920454f410000000000000000000000006044820152606401610567565b505060015463ffffffff16600090815260026020526040902054601781900b91600160c01b90910467ffffffffffffffff1690565b6000828152602081815260408083206001600160a01b038516845290915290205460ff1661057a57610989816001600160a01b03166014610ae8565b610994836020610ae8565b6040516020016109a5929190610e3e565b60408051601f198184030181529082905262461bcd60e51b825261056791600401610cf2565b6000828152602081815260408083206001600160a01b038516845290915290205460ff1661057a576000828152602081815260408083206001600160a01b03851684529091529020805460ff19166001179055610a253390565b6001600160a01b0316816001600160a01b0316837f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a45050565b6000828152602081815260408083206001600160a01b038516845290915290205460ff161561057a576000828152602081815260408083206001600160a01b0385168085529252808320805460ff1916905551339285917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a45050565b60606000610af7836002610ebf565b610b02906002610ede565b67ffffffffffffffff811115610b1a57610b1a610ef6565b6040519080825280601f01601f191660200182016040528015610b44576020820181803683370190505b509050600360fc1b81600081518110610b5f57610b5f610f0c565b60200101906001600160f81b031916908160001a905350600f60fb1b81600181518110610b8e57610b8e610f0c565b60200101906001600160f81b031916908160001a9053506000610bb2846002610ebf565b610bbd906001610ede565b90505b6001811115610c42577f303132333435363738396162636465660000000000000000000000000000000085600f1660108110610bfe57610bfe610f0c565b1a60f81b828281518110610c1457610c14610f0c565b60200101906001600160f81b031916908160001a90535060049490941c93610c3b81610f22565b9050610bc0565b508315610c915760405162461bcd60e51b815260206004820181905260248201527f537472696e67733a20686578206c656e67746820696e73756666696369656e746044820152606401610567565b9392505050565b600060208284031215610caa57600080fd5b81356001600160e01b031981168114610c9157600080fd5b60005b83811015610cdd578181015183820152602001610cc5565b83811115610cec576000848401525b50505050565b6020815260008251806020840152610d11816040850160208701610cc2565b601f01601f19169190910160400192915050565b600060208284031215610d3757600080fd5b5035919050565b60008060408385031215610d5157600080fd5b8235915060208301356001600160a01b0381168114610d6f57600080fd5b809150509250929050565b600060208284031215610d8c57600080fd5b81358060170b8114610c9157600080fd5b600060208284031215610daf57600080fd5b813569ffffffffffffffffffff81168114610c9157600080fd5b600181811c90821680610ddd57607f821691505b60208210811415610dfe57634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052601160045260246000fd5b600063ffffffff80831681811415610e3457610e34610e04565b6001019392505050565b7f416363657373436f6e74726f6c3a206163636f756e7420000000000000000000815260008351610e76816017850160208801610cc2565b7f206973206d697373696e6720726f6c65200000000000000000000000000000006017918401918201528351610eb3816028840160208801610cc2565b01602801949350505050565b6000816000190483118215151615610ed957610ed9610e04565b500290565b60008219821115610ef157610ef1610e04565b500190565b634e487b7160e01b600052604160045260246000fd5b634e487b7160e01b600052603260045260246000fd5b600081610f3157610f31610e04565b50600019019056fea164736f6c6343000809000a", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/src/modules/pairDeviationChecker/PairDeviationCheckerModule.ts b/src/modules/pairDeviationChecker/PairDeviationCheckerModule.ts new file mode 100644 index 0000000..f8eb673 --- /dev/null +++ b/src/modules/pairDeviationChecker/PairDeviationCheckerModule.ts @@ -0,0 +1,154 @@ +import Big from "big.js"; +import { ENABLE_TELEGRAM_NOTIFICATIONS, TELEGRAM_ALERTS_CHAT_ID, TELEGRAM_BOT_API, TELEGRAM_STATS_CHAT_ID } from "../../config"; +import { FetchJob } from "../../jobs/fetch/FetchJob"; +import { AppConfig } from "../../models/AppConfig"; +import { IJob } from "../../models/IJob"; +import { Module } from "../../models/Module"; +import { OutcomeType } from "../../models/Outcome"; +import { createSafeAppConfigString } from "../../services/AppConfigUtils"; +import logger from "../../services/LoggerService"; +import { debouncedInterval } from "../../services/TimerUtils"; +import { InternalPairDeviationCheckerModuleConfig, PairDeviationCheckerModuleConfig, parsePairDeviationCheckerModuleConfig } from "./models/PairDeviationCheckerModuleConfig"; +import { createRequestsFromPairs, PairDeviationDataRequest } from "./models/PairDeviationDataRequest"; +import { PairDeviationReport } from "./models/PairDeviationReport"; +import { fetchLatestPrice, fetchLatestTimestamp } from "./services/FetchLastUpdateService"; +import { shouldPricePairUpdate } from "./services/PairDeviationService"; +import { notifyTelegram } from "./services/TelegramNotificationService"; + +export class PairDeviationCheckerModule extends Module { + static type = "PairDeviationCheckerModule"; + + private internalConfig: InternalPairDeviationCheckerModuleConfig; + private prices: Map = new Map(); + private dataRequests: PairDeviationDataRequest[]; + private fetchJob: IJob; + private lastCheckAnyFailed: boolean = false; + + + constructor(moduleConfig: PairDeviationCheckerModuleConfig, appConfig: AppConfig) { + super(PairDeviationCheckerModule.type, moduleConfig, appConfig); + this.internalConfig = parsePairDeviationCheckerModuleConfig(moduleConfig); + this.dataRequests = createRequestsFromPairs(this.internalConfig, this.network); + + const fetchJob = appConfig.jobs.find(job => job.type === FetchJob.type); + if (!fetchJob) throw new Error(`No job found with id ${FetchJob.type}`); + this.fetchJob = fetchJob; + } + + async checkPairs() { + try { + logger.info(`[${this.id}] Checking ${this.dataRequests.length} pairs`); + const forceNotification = this.lastCheckAnyFailed; + this.lastCheckAnyFailed = false; + + // Generating all reports for every single pair in this set + const reports: PairDeviationReport[] = await Promise.all(this.dataRequests.map>(async (dataRequest) => { + const latestTimestamp = await fetchLatestTimestamp(dataRequest, this.network); + + if (!latestTimestamp) { + return { + pair: dataRequest, + updated: false, + message: 'FAIL_TIMESTAMP_FETCH', + diff: -1, + }; + } + + const timestampDiff = Date.now() - latestTimestamp; + const latestPrice = await fetchLatestPrice(dataRequest, this.network); + + if (!latestPrice) { + return { + pair: dataRequest, + updated: false, + message: 'FAIL_PRICE_FETCH', + diff: -1, + } + } + + const executeOutcome = await this.fetchJob.executeRequest(dataRequest); + + if (executeOutcome.type === OutcomeType.Invalid) { + logger.error(`[${this.id}] Could not resolve ${dataRequest.internalId}`, { + config: createSafeAppConfigString(this.appConfig), + logs: executeOutcome.logs, + fingerprint: `${this.id}-could-not-resolve`, + }); + + return { + pair: dataRequest, + updated: false, + message: 'INVALID_FETCH_OUTCOME', + diff: -1, + }; + } + + const shouldUpdateReport = shouldPricePairUpdate(dataRequest, latestTimestamp, new Big(executeOutcome.answer), this.prices.get(dataRequest.internalId)); + + if (shouldUpdateReport.shouldUpdate) { + return { + pair: dataRequest, + updated: false, + message: shouldUpdateReport.reason, + diff: timestampDiff / 1000, + }; + } + + this.prices.set(dataRequest.internalId, new Big(executeOutcome.answer)); + + return { + pair: dataRequest, + updated: true, + diff: timestampDiff / 1000, + }; + })); + + this.lastCheckAnyFailed = reports.some(r => !r.updated); + + if (ENABLE_TELEGRAM_NOTIFICATIONS) { + await notifyTelegram(reports, this.internalConfig.provider, forceNotification); + } + } catch (error) { + logger.error(`[${this.id}] Unknown error`, { + error, + fingerprint: `${this.id}-checkPairs-unknown`, + }); + } + } + + async start(): Promise { + // Check environment variables + if (ENABLE_TELEGRAM_NOTIFICATIONS) { + if (!TELEGRAM_BOT_API) { + logger.error(`[${this.id}] Could not start \`PairCheckerModule\`: \`TELEGRAM_BOT_TOKEN\` is undefined`, { + config: createSafeAppConfigString(this.appConfig), + fingerprint: `${this.type}-${this.internalConfig.provider}-start-failure`, + }); + + return false; + } else if (!TELEGRAM_ALERTS_CHAT_ID && !TELEGRAM_STATS_CHAT_ID) { + logger.error(`[${this.id}] Could not start \`PairCheckerModule\`: \`TELEGRAM_ALERTS_CHAT_ID\` and \`TELEGRAM_STATS_CHAT_ID\` are undefined`, { + config: createSafeAppConfigString(this.appConfig), + fingerprint: `${this.type}-${this.internalConfig.provider}-start-failure`, + }); + + return false; + } + } + + + logger.info(`[${this.id}] Fetching latest prices`); + await Promise.all(this.dataRequests.map(async (request) => { + const latestPrice = await fetchLatestPrice(request, this.network); + this.prices.set(request.internalId, latestPrice); + })); + logger.info(`[${this.id}] Done fetching latest prices`); + + await this.checkPairs(); + debouncedInterval(async () => { + await this.checkPairs(); + }, this.internalConfig.interval); + + return true; + } +} diff --git a/src/modules/pairDeviationChecker/models/PairDeviationCheckerModuleConfig.ts b/src/modules/pairDeviationChecker/models/PairDeviationCheckerModuleConfig.ts new file mode 100644 index 0000000..342e6a0 --- /dev/null +++ b/src/modules/pairDeviationChecker/models/PairDeviationCheckerModuleConfig.ts @@ -0,0 +1,41 @@ +import { ModuleConfig } from "../../../models/IModule"; +import { Pair } from "../../pushPair/models/PushPairConfig"; + +export interface CheckingPair extends Pair { + address: string; + deviationPercentage: number; + minimumUpdateInterval: number; + provider?: string; +} + +export interface PairDeviationCheckerModuleConfig extends ModuleConfig { + interval?: number; + pairs?: CheckingPair[]; + provider?: string; +} + +export interface InternalPairDeviationCheckerModuleConfig extends ModuleConfig { + interval: number; + provider: string; + pairs: CheckingPair[]; +} + +export function parsePairDeviationCheckerModuleConfig(config: PairDeviationCheckerModuleConfig): InternalPairDeviationCheckerModuleConfig { + if (typeof config.interval === 'undefined' || typeof config.interval !== "number") throw new Error(`[PairDeviationCheckerModule] "interval" is required and must be a number`); + if (typeof config.provider === 'undefined' || typeof config.provider !== "string") throw new Error(`[PairDeviationCheckerModule] "provider" is required and must be a string`); + // if (typeof config.threshold === 'undefined' || typeof config.threshold !== "number") throw new Error(`[PairDeviationCheckerModule] "threshold" is required and must be a number`); + + if (!Array.isArray(config.pairs)) throw new Error(`[PairDeviationCheckerModule] "pairs" is required and must be an array`); + config.pairs.forEach((pair: Partial) => { + if (typeof pair.address === 'undefined' || typeof pair.address !== 'string') throw new Error(`[PairDeviationCheckerModule] "address" is required for each item in "pairs"`); + if (typeof pair.pair === 'undefined' || typeof pair.pair !== 'string') throw new Error(`[PairDeviationCheckerModule] "pair" is required for each item in "pairs"`); + }); + + return { + ...config, + interval: config.interval, + pairs: config.pairs, + provider: config.provider, + // threshold: config.threshold, + }; +} diff --git a/src/modules/pairDeviationChecker/models/PairDeviationDataRequest.ts b/src/modules/pairDeviationChecker/models/PairDeviationDataRequest.ts new file mode 100644 index 0000000..54dac50 --- /dev/null +++ b/src/modules/pairDeviationChecker/models/PairDeviationDataRequest.ts @@ -0,0 +1,57 @@ +import Big from "big.js"; +import { FetchJob } from "../../../jobs/fetch/FetchJob"; +import { DataRequest } from "../../../models/DataRequest"; +import { INetwork } from "../../../models/INetwork"; +import { CheckingPair, InternalPairDeviationCheckerModuleConfig } from "../models/PairDeviationCheckerModuleConfig"; + +export interface PairDeviationDataRequest extends DataRequest { + extraInfo: { + address: string; + pair: string; + decimals: number; + deviationPercentage: number; + minimumUpdateInterval: number; + provider: string; + }, +} + +/** + * Creates a data request that can be used to execute jobs + * + * @export + * @param {CheckingPair[]} pairs + * @param {INetwork} network Only used for completeness of the data request interface. Can be a mock + * @return {PairDeviationDataRequest[]} + */ +export function createRequestsFromPairs(config: InternalPairDeviationCheckerModuleConfig, network: INetwork): PairDeviationDataRequest[] { + return config.pairs.map((pair, index) => { + return { + args: [ + FetchJob.type, + JSON.stringify(pair.sources), + 'number', + (10 ** pair.decimals).toString(), + ], + confirmationsRequired: new Big(0), + createdInfo: { + // Block info is not important for this request + block: { + hash: '0x000000', + number: new Big(0), + receiptRoot: '0x000000', + }, + }, + extraInfo: { + address: pair.address, + decimals: pair.decimals, + deviationPercentage: pair.deviationPercentage, + minimumUpdateInterval: pair.minimumUpdateInterval, + pair: pair.pair, + provider: pair.provider ?? config.provider, + }, + internalId: `${network.id}/p${pair.pair}-i${index}-a${pair.address}`, + originNetwork: network, + targetNetwork: network, + }; + }); +} diff --git a/src/modules/pairDeviationChecker/models/PairDeviationReport.ts b/src/modules/pairDeviationChecker/models/PairDeviationReport.ts new file mode 100644 index 0000000..4e548fb --- /dev/null +++ b/src/modules/pairDeviationChecker/models/PairDeviationReport.ts @@ -0,0 +1,8 @@ +import { PairDeviationDataRequest } from "./PairDeviationDataRequest"; + +export interface PairDeviationReport { + pair: PairDeviationDataRequest; + diff: number; + updated: boolean; + message?: string; +} diff --git a/src/modules/pairDeviationChecker/services/FetchLastUpdateService.ts b/src/modules/pairDeviationChecker/services/FetchLastUpdateService.ts new file mode 100644 index 0000000..5bb1c4f --- /dev/null +++ b/src/modules/pairDeviationChecker/services/FetchLastUpdateService.ts @@ -0,0 +1,81 @@ +import Big from "big.js"; +import { Network } from "../../../models/Network"; +import { NearNetwork } from "../../../networks/near/NearNetwork"; +import logger from "../../../services/LoggerService"; + +import FluxPriceFeed from '../FluxPriceFeed.json'; +import { PairDeviationDataRequest } from "../models/PairDeviationDataRequest"; + +export async function fetchLatestTimestamp(pair: PairDeviationDataRequest, network: Network): Promise { + try { + if (network.type === 'evm') { + const latestTimestamp = await network.view({ + method: 'latestTimestamp', + address: pair.extraInfo.address, + amount: '0', + params: {}, + abi: FluxPriceFeed.abi, + }); + + // Convert contract timestamp to milliseconds + return latestTimestamp.toNumber() * 1000; + } else if (network.type === 'near') { + const entry = await network.view({ + method: 'get_entry', + address: pair.extraInfo.address, + amount: '0', + params: { + provider: pair.extraInfo.provider, + pair: pair.extraInfo.pair, + }, + }); + + // Convert contract timestamp to milliseconds + return Math.floor(entry.last_update / 1000000); + } + + throw new Error(`Network ${network.type} is not supported by this module`); + } catch (error) { + logger.error(`[PairDeviationChecker] Fetch latest timestamp error`, { + error, + fingerprint: `PairDeviationChecker-fetchLatestTimestamp-failure`, + }); + } +} + +export async function fetchLatestPrice(pair: PairDeviationDataRequest, network: Network): Promise { + try { + if (network.type === 'evm') { + const result = await network.view({ + method: 'latestAnswer', + address: pair.extraInfo.address, + amount: '0', + params: {}, + abi: FluxPriceFeed.abi, + }); + + return new Big(result.toString()); + } else if (network.type === 'near') { + const result = await network.view({ + method: 'get_entry', + address: pair.extraInfo.address, + amount: '0', + params: { + provider: (network as NearNetwork).internalConfig?.account.accountId, + pair: pair.extraInfo.pair, + }, + }); + + return new Big(result.price); + } + + throw new Error(`Network ${network.type} is not supported by this module`); + } catch (error) { + logger.error(`[PairDeviationChecker] failed to fetchLatestPrice`, { + error, + fingerprint: `PairDeviationChecker-fetchLatestPrice-unknown`, + }); + + return undefined; + } +} diff --git a/src/modules/pairDeviationChecker/services/PairDeviationService.ts b/src/modules/pairDeviationChecker/services/PairDeviationService.ts new file mode 100644 index 0000000..5ecb556 --- /dev/null +++ b/src/modules/pairDeviationChecker/services/PairDeviationService.ts @@ -0,0 +1,48 @@ +import Big from "big.js"; +import { PairDeviationDataRequest } from "../models/PairDeviationDataRequest"; + +export enum PricePairUpdateReason { + NO_PRICE = 'NO_PRICE', + PRICE_DEVIATION = 'PRICE_DEVIATION', + EXCEEDED_TIMESTAMP = 'EXCEEDED_TIMESTAMP', +} + +export interface PricePairUpdateReasonReport { + shouldUpdate: boolean; + reason: PricePairUpdateReason; +} + +export function shouldPricePairUpdate(pair: PairDeviationDataRequest, lastUpdate: number, newPrice: Big, oldPrice?: Big): PricePairUpdateReasonReport { + // This is probably the first time we are pushing + if (!oldPrice) { + return { + shouldUpdate: true, + reason: PricePairUpdateReason.NO_PRICE, + }; + } + + const timeSinceUpdate = Date.now() - lastUpdate; + + // There hasn't been an update in a while, we should just update + if (timeSinceUpdate >= pair.extraInfo.minimumUpdateInterval) { + return { + shouldUpdate: true, + reason: PricePairUpdateReason.EXCEEDED_TIMESTAMP, + }; + } + + const valueChange = newPrice.minus(oldPrice); + const percentageChange = valueChange.div(oldPrice).times(100); + + if (percentageChange.lt(0)) { + return { + shouldUpdate: percentageChange.lte(-pair.extraInfo.deviationPercentage), + reason: PricePairUpdateReason.PRICE_DEVIATION, + }; + } + + return { + shouldUpdate: percentageChange.gte(pair.extraInfo.deviationPercentage), + reason: PricePairUpdateReason.PRICE_DEVIATION, + } +} diff --git a/src/modules/pairDeviationChecker/services/TelegramNotificationService.ts b/src/modules/pairDeviationChecker/services/TelegramNotificationService.ts new file mode 100644 index 0000000..b425b40 --- /dev/null +++ b/src/modules/pairDeviationChecker/services/TelegramNotificationService.ts @@ -0,0 +1,79 @@ +import { TELEGRAM_BOT_API, TELEGRAM_ALERTS_CHAT_ID, TELEGRAM_STATS_CHAT_ID } from "../../../config"; +import { prettySeconds } from "../utils"; +import { PairDeviationReport } from "../models/PairDeviationReport"; +import logger from "../../../services/LoggerService"; + +export async function notifyTelegram(reports: PairDeviationReport[], groupName: string, forceNotification: boolean) { + // Sort reports by last update + reports = reports.sort(function (a, b) { return a.diff - b.diff; }); + + // Not updated reports + const notUpdatedReports = reports.filter(report => !report.updated); + const sendAlert = notUpdatedReports.length > 0; + + // Stats message (only sent there are alerts of if `TELEGRAM_STATS_CHAT_ID` is set) + let messages: string[] = []; + if (sendAlert || forceNotification || TELEGRAM_STATS_CHAT_ID) { + const updates = `[${groupName}] ${reports.length - notUpdatedReports.length}/${reports.length} pairs recently updated`; + let stats; + if (!sendAlert) { + stats = `✅ *${updates}* \n\n`; + } else if (notUpdatedReports.length != reports.length) { + stats = `⚠️ *${updates}* \n\n`; + } else { + stats = `🆘 *${updates}* \n\n`; + } + // Last update per pair + for (var i = 0; i < reports.length; i++) { + const statMessage = `${reports[i].diff != -1 ? `updated ${prettySeconds(reports[i].diff, true)} ago` : `check failed`}`; + stats += `\t ${reports[i].updated ? '✓' : '⨯'} [[${reports[i].pair.extraInfo.pair}]] ${statMessage} ${reports[i].message ?? ''}\n`; + } + + messages.push(stats); + } + + // Alert message (including addresses of not updated pairs) + if (sendAlert) { + let alerts = `🔍 *[${groupName}] Not updated addresses:* \n\n`; + for (var i = 0; i < notUpdatedReports.length; i++) { + alerts += `\t*[${notUpdatedReports[i].pair.extraInfo.pair}]* ${notUpdatedReports[i].pair.extraInfo.address}\n`; + } + + messages.push(alerts); + } + + // Send to Statistics telegram group + if (TELEGRAM_STATS_CHAT_ID) { + for await (let message of messages) { + await sendTelegramMessage(TELEGRAM_BOT_API, TELEGRAM_STATS_CHAT_ID, message, !sendAlert); + } + } + + // Send to Alerts telegram group + if ((sendAlert || forceNotification) && TELEGRAM_ALERTS_CHAT_ID) { + for await (let message of messages) { + await sendTelegramMessage(TELEGRAM_BOT_API, TELEGRAM_ALERTS_CHAT_ID, message); + } + } +} + +export async function sendTelegramMessage(url: string, chatId: string, text: string, disable_notification?: boolean) { + try { + await fetch(`${url}/sendMessage`, { + method: "POST", + body: JSON.stringify({ + chat_id: chatId, + text, + disable_notification + }), + headers: { + 'Content-Type': 'application/json' + }, + }); + } catch (error) { + logger.error(`[TelegramNotification] Unknown error`, { + error, + fingerprint: `PairDeviationChecker-telegram-unknown`, + }); + } +} diff --git a/src/modules/pairDeviationChecker/utils.ts b/src/modules/pairDeviationChecker/utils.ts new file mode 100644 index 0000000..8700c51 --- /dev/null +++ b/src/modules/pairDeviationChecker/utils.ts @@ -0,0 +1,18 @@ +export function prettySeconds(seconds: number, short?: boolean): string { + // Seconds + if (seconds < 60) { + return Math.floor(seconds) + `${short ? "s" : " seconds"}`; + } + // Minutes + else if (seconds < 3600) { + return Math.floor(seconds / 60) + `${short ? "m" : " minutes"}`; + } + // Hours + else if (seconds < 86400) { + return Math.floor(seconds / 3600) + `${short ? "h" : " hours"}`; + } + // Days + else { + return Math.floor(seconds / 86400) + `${short ? "d" : " days"}`; + } +} diff --git a/src/modules/pushPair/PushPairModule.ts b/src/modules/pushPair/PushPairModule.ts index 23f1f63..96ca1b2 100644 --- a/src/modules/pushPair/PushPairModule.ts +++ b/src/modules/pushPair/PushPairModule.ts @@ -78,12 +78,14 @@ export class PushPairModule extends Module { } // When the prices don't deviate too much we don't need to update the price pair - if(!shouldPricePairUpdate(unresolvedRequest, lastUpdate, new Big(outcome.answer), this.prices.get(unresolvedRequest.internalId))) { + if (!shouldPricePairUpdate(unresolvedRequest, lastUpdate, new Big(outcome.answer), this.prices.get(unresolvedRequest.internalId))) { logger.debug(`[${this.id}] ${unresolvedRequest.internalId} Price ${outcome.answer} doesn't deviate ${unresolvedRequest.extraInfo.deviationPercentage}% from ${this.prices.get(unresolvedRequest.internalId)}`); remainingInterval = this.internalConfig.interval; return null; } + // NOTICE: Limitation here is that we assume that the price update transaction may fail + // we do not know whether or not the transaction failed this.prices.set(unresolvedRequest.internalId, new Big(outcome.answer)); return createResolvePairRequest(outcome, unresolvedRequest, this.internalConfig); })); diff --git a/src/modules/pushPair/models/PushPairConfig.ts b/src/modules/pushPair/models/PushPairConfig.ts index 2417752..6e118df 100644 --- a/src/modules/pushPair/models/PushPairConfig.ts +++ b/src/modules/pushPair/models/PushPairConfig.ts @@ -42,8 +42,6 @@ export function parsePushPairConfig(config: PushPairConfig): PushPairInternalCon if (typeof config.interval === 'undefined' || typeof config.interval !== 'number') throw new Error(`[PushPairModule] "interval" is required and must be a number`); if (typeof config.deviationPercentage !== 'undefined' && typeof config.deviationPercentage !== 'number') throw new Error(`[PushPairModule] "deviationPercentage" should be a number`); if (typeof config.minimumUpdateInterval !== 'undefined' && typeof config.minimumUpdateInterval !== 'number') throw new Error(`[PushPairModule] "minimumUpdateInterval" should be a number`); - - if (!Array.isArray(config.pairs)) throw new Error(`[PushPairModule] "pairs" is required and must be an array`); config.pairs.forEach((pair: Partial) => { diff --git a/src/services/AppConfigService.ts b/src/services/AppConfigService.ts index 3f92ff4..f045397 100644 --- a/src/services/AppConfigService.ts +++ b/src/services/AppConfigService.ts @@ -35,6 +35,8 @@ export async function parseAppConfig(): Promise { return new network(parsedNetworkConfig); }); + appConfig.jobs = AVAILABLE_JOBS.map(job => new job(appConfig)); + if (!config.modules || !Array.isArray(config.modules)) throw new Error(`at least 1 item in "modules" is required and it must be an array`); appConfig.modules = config.modules.map((moduleConfig) => { @@ -46,7 +48,7 @@ export async function parseAppConfig(): Promise { return new module(parsedModuleConfig, appConfig); }); - appConfig.jobs = AVAILABLE_JOBS.map(job => new job(appConfig)); + return appConfig; }