diff --git a/packages/api/src/abi/WitnetPriceFeeds.json b/packages/api/src/abi/WitnetPriceFeeds.json new file mode 100644 index 00000000..b0db0f3e --- /dev/null +++ b/packages/api/src/abi/WitnetPriceFeeds.json @@ -0,0 +1,1300 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_operator", + "type": "address" + }, + { + "internalType": "contract WitnetRequestBoard", + "name": "_wrb", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "EmptyBuffer", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "range", + "type": "uint256" + } + ], + "name": "IndexOutOfBounds", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "InvalidLengthEncoding", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "read", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expected", + "type": "uint256" + } + ], + "name": "UnexpectedMajorType", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "unexpected", + "type": "uint256" + } + ], + "name": "UnsupportedMajorType", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + }, + { + "indexed": false, + "internalType": "string", + "name": "caption", + "type": "string" + } + ], + "name": "DeletedFeed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + }, + { + "indexed": false, + "internalType": "string", + "name": "caption", + "type": "string" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "radHash", + "type": "bytes32" + } + ], + "name": "SettledFeed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + }, + { + "indexed": false, + "internalType": "string", + "name": "caption", + "type": "string" + }, + { + "indexed": false, + "internalType": "address", + "name": "solver", + "type": "address" + } + ], + "name": "SettledFeedSolver", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "slaHash", + "type": "bytes32" + } + ], + "name": "SettledRadonSLA", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "slaHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "UpdatingFeed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "UpdatingFeedReward", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "solver", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "codehash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "constructorParams", + "type": "bytes" + } + ], + "name": "WitnetPriceSolverDeployed", + "type": "event" + }, + { + "stateMutability": "nonpayable", + "type": "fallback" + }, + { + "inputs": [], + "name": "dataType", + "outputs": [ + { + "internalType": "enum Witnet.RadonDataTypes", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "prefix", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "specs", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "witnet", + "outputs": [ + { + "internalType": "contract WitnetRequestBoard", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "caption", + "type": "string" + } + ], + "name": "hash", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + } + ], + "name": "lookupCaption", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "supportedFeeds", + "outputs": [ + { + "internalType": "bytes4[]", + "name": "_ids", + "type": "bytes4[]" + }, + { + "internalType": "string[]", + "name": "_captions", + "type": "string[]" + }, + { + "internalType": "bytes32[]", + "name": "_solvers", + "type": "bytes32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "caption", + "type": "string" + } + ], + "name": "supportsCaption", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalFeeds", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "defaultRadonSLA", + "outputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "witnessingCommitteeSize", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "witnessingWitTotalReward", + "type": "uint64" + } + ], + "internalType": "struct WitnetV2.RadonSLA", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_evmGasPrice", + "type": "uint256" + } + ], + "name": "estimateUpdateBaseFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + } + ], + "name": "latestResponse", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "fromFinality", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "tallyHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "cborBytes", + "type": "bytes" + } + ], + "internalType": "struct WitnetV2.Response", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + } + ], + "name": "latestResult", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "components": [ + { + "components": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "cursor", + "type": "uint256" + } + ], + "internalType": "struct WitnetBuffer.Buffer", + "name": "buffer", + "type": "tuple" + }, + { + "internalType": "uint8", + "name": "initialByte", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "majorType", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "additionalInformation", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "len", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "tag", + "type": "uint64" + } + ], + "internalType": "struct WitnetCBOR.CBOR", + "name": "value", + "type": "tuple" + } + ], + "internalType": "struct Witnet.Result", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + } + ], + "name": "latestUpdateQueryId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + } + ], + "name": "latestUpdateRequest", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "witnessingCommitteeSize", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "witnessingWitTotalReward", + "type": "uint64" + } + ], + "internalType": "struct WitnetV2.RadonSLA", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + } + ], + "name": "latestUpdateResponse", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "fromFinality", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "tallyHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "cborBytes", + "type": "bytes" + } + ], + "internalType": "struct WitnetV2.Response", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + } + ], + "name": "latestUpdateResultError", + "outputs": [ + { + "components": [ + { + "internalType": "enum Witnet.ResultErrorCodes", + "name": "code", + "type": "uint8" + }, + { + "internalType": "string", + "name": "reason", + "type": "string" + } + ], + "internalType": "struct Witnet.ResultError", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + } + ], + "name": "latestUpdateResultStatus", + "outputs": [ + { + "internalType": "enum WitnetV2.ResultStatus", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + } + ], + "name": "lookupBytecode", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + } + ], + "name": "lookupRadHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + } + ], + "name": "lookupRetrievals", + "outputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "argsCount", + "type": "uint8" + }, + { + "internalType": "enum Witnet.RadonDataRequestMethods", + "name": "method", + "type": "uint8" + }, + { + "internalType": "enum Witnet.RadonDataTypes", + "name": "resultDataType", + "type": "uint8" + }, + { + "internalType": "string", + "name": "url", + "type": "string" + }, + { + "internalType": "string", + "name": "body", + "type": "string" + }, + { + "internalType": "string[2][]", + "name": "headers", + "type": "string[2][]" + }, + { + "internalType": "bytes", + "name": "script", + "type": "bytes" + } + ], + "internalType": "struct Witnet.RadonRetrieval[]", + "name": "_retrievals", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "registry", + "outputs": [ + { + "internalType": "contract WitnetBytecodes", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + } + ], + "name": "requestUpdate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "witnessingCommitteeSize", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "witnessingWitTotalReward", + "type": "uint64" + } + ], + "internalType": "struct WitnetV2.RadonSLA", + "name": "updateSLA", + "type": "tuple" + } + ], + "name": "requestUpdate", + "outputs": [ + { + "internalType": "uint256", + "name": "_usedFunds", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "caption", + "type": "string" + } + ], + "name": "deleteFeed", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "witnessingCommitteeSize", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "witnessingWitTotalReward", + "type": "uint64" + } + ], + "internalType": "struct WitnetV2.RadonSLA", + "name": "defaultSLA", + "type": "tuple" + } + ], + "name": "settleDefaultRadonSLA", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "caption", + "type": "string" + }, + { + "internalType": "bytes32", + "name": "radHash", + "type": "bytes32" + } + ], + "name": "settleFeedRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "caption", + "type": "string" + }, + { + "internalType": "contract WitnetRequest", + "name": "request", + "type": "address" + } + ], + "name": "settleFeedRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "caption", + "type": "string" + }, + { + "internalType": "contract WitnetRequestTemplate", + "name": "template", + "type": "address" + }, + { + "internalType": "string[][]", + "name": "args", + "type": "string[][]" + } + ], + "name": "settleFeedRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "caption", + "type": "string" + }, + { + "internalType": "address", + "name": "solver", + "type": "address" + }, + { + "internalType": "string[]", + "name": "deps", + "type": "string[]" + } + ], + "name": "settleFeedSolver", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + } + ], + "name": "lookupDecimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + } + ], + "name": "lookupPriceSolver", + "outputs": [ + { + "internalType": "contract IWitnetPriceSolver", + "name": "_solverAddress", + "type": "address" + }, + { + "internalType": "string[]", + "name": "_solverDeps", + "type": "string[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "feedId", + "type": "bytes4" + } + ], + "name": "latestPrice", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "tallyHash", + "type": "bytes32" + }, + { + "internalType": "enum WitnetV2.ResultStatus", + "name": "status", + "type": "uint8" + } + ], + "internalType": "struct IWitnetPriceSolver.Price", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4[]", + "name": "feedIds", + "type": "bytes4[]" + } + ], + "name": "latestPrices", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "tallyHash", + "type": "bytes32" + }, + { + "internalType": "enum WitnetV2.ResultStatus", + "name": "status", + "type": "uint8" + } + ], + "internalType": "struct IWitnetPriceSolver.Price[]", + "name": "_prices", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "initcode", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "constructorParams", + "type": "bytes" + } + ], + "name": "deployPriceSolver", + "outputs": [ + { + "internalType": "address", + "name": "_solver", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "initcode", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "constructorParams", + "type": "bytes" + } + ], + "name": "determinePriceSolverAddress", + "outputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "feedId", + "type": "bytes32" + } + ], + "name": "valueFor", + "outputs": [ + { + "internalType": "int256", + "name": "_value", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "_timestamp", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_status", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index f4da491e..05dfa362 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -49,7 +49,7 @@ async function main () { { repositories, Web3: Web3 }, dataFeeds ) - web3Middleware.listen() + web3Middleware.listen2() await createServer(repositories, svgCache, { dataFeedsConfig: dataFeeds, diff --git a/packages/api/src/repository/ResultRequest.ts b/packages/api/src/repository/ResultRequest.ts index 062a5254..97eda876 100644 --- a/packages/api/src/repository/ResultRequest.ts +++ b/packages/api/src/repository/ResultRequest.ts @@ -14,6 +14,8 @@ export class ResultRequestRepository { ResultRequestDbObject | WithoutId > + latestResults: Record> + constructor (db: Db, _dataFeeds: Array) { this.collection = db.collection('result_request') } @@ -89,6 +91,9 @@ export class ResultRequestRepository { if (this.isValidResultRequest(resultRequest)) { const response = await this.collection.insertOne(resultRequest) + // store in cache + this.latestResults[resultRequest.feedFullName] = resultRequest + return this.normalizeId(response[0]) } else { console.error( @@ -99,6 +104,24 @@ export class ResultRequestRepository { } } + async insertIfLatest ( + resultRequest: WithoutId + ): Promise { + const storedResult = this.latestResults[resultRequest.feedFullName] || (await this.getLastResult(resultRequest.feedFullName)) + const timestampChanged = storedResult?.timestamp !== resultRequest.timestamp + + if (timestampChanged) { + return await this.insert(resultRequest) + } else { + console.error( + 'Error inserting result request: Validation Error', + resultRequest + ) + return null + } + } + + private normalizeId ( resultRequest: ResultRequestDbObject ): ResultRequestDbObjectNormalized | null { diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index b4216a92..9b72aa14 100644 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -1,7 +1,7 @@ import { ApolloServer } from '@apollo/server' -import typeDefs from './typeDefs' +import typeDefs from './typeDefs.js' import { DIRECTIVES } from '@graphql-codegen/typescript-mongodb' -import resolvers from './resolvers' +import resolvers from './resolvers.js' import { ConfigByFullName, Context, @@ -9,10 +9,10 @@ import { Loaders, NetworksConfig, Repositories -} from './types' +} from './types.js' import { startStandaloneServer } from '@apollo/server/standalone' -import { LoadersFactory } from './loaders' -import SvgCache from './svgCache' +import { LoadersFactory } from './loaders/index.js' +import SvgCache from './svgCache.js' export async function createServer ( repositories: Repositories, diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index e77c91b6..3a91b0f4 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -86,6 +86,10 @@ export type FeedInfoGeneric = { finality: string } +export type configurationFile = { + +} + export type NetworksConfig = { chain: string label: string @@ -180,6 +184,7 @@ export type ExtendedFeedConfig = { export type Chain = { name: string hide?: boolean + legacy?: boolean, networks: Record } diff --git a/packages/api/src/web3Middleware/NetworkRouter.ts b/packages/api/src/web3Middleware/NetworkRouter.ts new file mode 100644 index 00000000..bc032dea --- /dev/null +++ b/packages/api/src/web3Middleware/NetworkRouter.ts @@ -0,0 +1,117 @@ +import Web3 from "web3" +import WitnetPriceFeedsABI from './../abi/WitnetPriceFeeds.json' +import { AbiItem, Repositories } from "./../types" +import { toHex } from "web3-utils" +import { createFeedFullName } from "../utils" + + +enum ResultStatus { + Void = 0, + Awaiting = 1, + Ready = 2, + Error = 3, + AwaitingReady = 4, + AwaitingError = 5 +} + +type SupportedFeed = { + id: string, + caption: string + solver: string +} + +type LatestPrice = { + value: string, + timestamp: string, + tallyHash: string, + status: ResultStatus +} + +export type NetworkInfo = { + name: string, + provider: string, + address: string, + pollingPeriod: number, + maxSecsBetweenUpdates: number, + feeds?: Array<{ name: string, maxSecsBetweenUpdates: number}> +} +export type NetworkSnapshot = { + network: string, + feeds: Array +} + +export class NetworkRouter { + public contract: any + public network: string + public pollingPeriod: number + public maxSecsBetweenUpdates: number + public feeds?: Array<{ name: string; maxSecsBetweenUpdates: number }> + + public repositories: Repositories + + constructor(repositories: Repositories, networkInfo: NetworkInfo) { + const { name, provider, address, pollingPeriod, maxSecsBetweenUpdates, feeds } = networkInfo + + const web3 = new Web3(new Web3.providers.HttpProvider(provider, { timeout: 30000 })) + this.contract = new web3.eth.Contract( WitnetPriceFeedsABI as unknown as AbiItem, address) + this.network = name + this.pollingPeriod = pollingPeriod, + this.maxSecsBetweenUpdates = maxSecsBetweenUpdates + this.feeds = feeds + this.repositories = repositories + } + + // Periodically fetch the price feed router contract and store it in mongodb + public listen() { + setInterval(async () => { + const snapshot = await this.getSnapshot() + console.log("Last contract snapshot", snapshot) + const insertPromises = snapshot.feeds.filter(feed => feed.status !== ResultStatus.Ready).map((feed) => ({ + feedFullName: createFeedFullName(this.network, feed.caption, feed.caption.split("-").reverse()[0]), + drTxHash: toHex(feed.tallyHash).slice(2), + requestId: feed.id, + result: feed.value, + timestamp: feed.timestamp + })) + .map(this.repositories.resultRequestRepository.insertIfLatest) + + Promise.all(insertPromises) + }, this.pollingPeriod) + } + + async getSnapshot(): Promise { + const supportedFeeds = await this.getSupportedFeeds() + const feedIds = supportedFeeds.map(feed => feed.id) + const latestPrices = await this.latestPrices(feedIds) + + return { + network: this.network, + feeds: supportedFeeds.map((supportedFeed, index) => ({ + ...supportedFeed, + ...latestPrices[index] + })) + } + } + + // Wrap supportedFeeds contract method + private async getSupportedFeeds (): Promise> { + const supportedFeeds = await this.contract.methods.supportedFeeds().call() + + return supportedFeeds._ids.map((_, index) => ({ + id: supportedFeeds._ids[index], + caption: supportedFeeds._captions[index], + solver: supportedFeeds._solvers[index], + })) + } + + // Wrap latestPrices contract method + private async latestPrices (ids: Array): Promise> { + const latestPrices = await this.contract.methods.latestPrices(ids).call() + return latestPrices.map(latestPrice => ({ + value: latestPrice.value.toString(), + timestamp: latestPrice.timestamp.toString(), + tallyHash: latestPrice.tallyHash, + status: Number(latestPrice.status) + })) + } +} diff --git a/packages/api/src/web3Middleware/index.ts b/packages/api/src/web3Middleware/index.ts index b27d91db..76ef1ee6 100644 --- a/packages/api/src/web3Middleware/index.ts +++ b/packages/api/src/web3Middleware/index.ts @@ -6,22 +6,22 @@ import { ContractsState, FeedInfo, Repositories, - ResultRequestDbObject, ContractInfo, Contract } from '../types' import { isZeroAddress } from '../utils/index' import { getProvider } from './provider' +import { NetworkInfo, NetworkRouter } from './NetworkRouter' export class Web3Middleware { public repositories: Repositories private Web3: typeof Web3 public dataFeeds: Array - public lastStoredResult: Record = {} public routerContractByNetwork: Record = {} public contractIdByFeedId: Record = {} // feedFullname -> address public currentFeedAddresses: Record = {} + public networkRouters: Array private intervals = [] @@ -34,7 +34,24 @@ export class Web3Middleware { this.Web3 = dependencies.Web3 } - public async initializeAddresses (): Promise> { + public async listen2 () { + // TODO: move this to dataFeeds information requested from github + const networks: Array = [{ + name:'ethereum.sepholia', + address: "0x9999999d139bdBFbF25923ba39F63bBFc7593400", + provider:"https://sepolia.infura.io/v3/3199c9172a13459caa4e25fd30827194", + pollingPeriod: 12000, + maxSecsBetweenUpdates: 86400, + // optional + feeds: [ + { name: '', maxSecsBetweenUpdates: 86400} + ] + }] + + networks.forEach(networkInfo => new NetworkRouter(this.repositories, networkInfo).listen()) + } + + private async initializeAddresses (): Promise> { const promises = this.dataFeeds.map(feed => this.recheckFeedAddress(feed)) const feeds = await Promise.all(promises) @@ -253,26 +270,16 @@ export class Web3Middleware { contracts, feedFullName ) - const decodedDrTxHash = toHex(lastDrTxHash) - const lastStoredResult = - this.lastStoredResult[feedFullName] || - (await this.repositories.resultRequestRepository.getLastResult( + await this.repositories.resultRequestRepository.insertIfLatest( + { + result: lastPrice, + timestamp: lastTimestamp, + requestId: requestId, + drTxHash: toHex(lastDrTxHash).slice(2), feedFullName - )) - const timestampChanged = lastStoredResult?.timestamp !== lastTimestamp - if (timestampChanged) { - const result = await this.repositories.resultRequestRepository.insert( - { - result: lastPrice, - timestamp: lastTimestamp, - requestId: requestId, - drTxHash: decodedDrTxHash.slice(2), - feedFullName - } - ) - this.lastStoredResult[feedFullName] = result - resolve(true) - } + } + ) + resolve(true) } catch (error) { console.error( `Error reading contracts state for ${feedFullName}:`, diff --git a/packages/api/test/web3Middleware/NetworkRouter.spec.ts b/packages/api/test/web3Middleware/NetworkRouter.spec.ts new file mode 100644 index 00000000..471045ed --- /dev/null +++ b/packages/api/test/web3Middleware/NetworkRouter.spec.ts @@ -0,0 +1,30 @@ +import { Repositories } from '../../src/types.js' +import { NetworkRouter } from '../../src/web3Middleware/NetworkRouter' + + +describe('NetworkRouter', () => { + it('should fetch network contract', async () => { + // FIXME: create a proper mock + const repositories = { feedRepository: { }, resultRequestRepository: { } } as unknown as Repositories + const networkInfo = { + address:'0x9999999d139bdBFbF25923ba39F63bBFc7593400', + provider: 'https://rpc2.sepolia.org', + name: 'ethereum.sepholia', + pollingPeriod: 1, + maxSecsBetweenUpdates: 1 + } + const router = new NetworkRouter(repositories, networkInfo) + + const snapshot = await router.getSnapshot() + + expect(snapshot.feeds[0].caption).toBeTruthy() + expect(snapshot.feeds[0].id).toBeTruthy() + expect(snapshot.feeds[0].solver).toBeTruthy() + expect(snapshot.feeds[0].status).toBeTruthy() + expect(snapshot.feeds[0].tallyHash).toBeTruthy() + expect(snapshot.feeds[0].timestamp).toBeTruthy() + expect(snapshot.feeds[0].value).toBeTruthy() + + expect(snapshot.network).toBe('ethereum.sepholia') + }) +})