Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Read beacon values directly from AirseekerRegistry contract #180

Merged
merged 5 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions local-test-configuration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,8 @@ also required for the monitoring page.
The `AIRSEEKER_MNEMONIC` needs to be URI encoded via `encodeURIComponent` in JS. For example,
`test%20test%20test%20test%20test%20test%20test%20test%20test%20test%20test%20junk`.

Initially, you should see errors because the beacons are not initialized. After you run Airseeker, it will do the
updates and the errors should be gone. The page constantly polls the chain and respective signed APIs and compares the
on-chain and off-chain values. If the deviation exceeds the treshold, the value is marked bold and should be updated by
Airseeker shortly.
The page constantly polls the chain and respective signed APIs and compares the on-chain and off-chain values. If the
deviation exceeds the treshold, the value is marked bold and should be updated by Airseeker shortly.

- Run the Airseeker:

Expand Down
31 changes: 15 additions & 16 deletions local-test-configuration/monitoring/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -743,19 +743,19 @@ <h2>Active data feeds</h2>
return ethers.solidityPackedKeccak256(['address', 'bytes32'], [airnodeAddress, templateId]);
}

function deriveBeaconSetId(beaconIds) {
return ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(['bytes32[]'], [beaconIds]));
}

const decodeDataFeedDetails = (dataFeed) => {
if (dataFeed.length === 130) {
// (64 [actual bytes] * 2[hex encoding] ) + 2 [for the '0x' preamble]
// This is a hex encoded string, the contract works with bytes directly
// The contract returns empty bytes if the data feed is not registered. See:
// https://github.com/bbenligiray/api3-contracts/blob/d394581549e4d2f343e9910bc330b21266808851/contracts/AirseekerRegistry.sol#L346
if (dataFeed === '0x') return null;

// This is a hex encoded string, the contract works with bytes directly
// 2 characters for the '0x' preamble + 32 * 2 hexadecimals for 32 bytes + 32 * 2 hexadecimals for 32 bytes
if (dataFeed.length === 2 + 32 * 2 + 32 * 2) {
const [airnodeAddress, templateId] = ethers.AbiCoder.defaultAbiCoder().decode(['address', 'bytes32'], dataFeed);

const dataFeedId = deriveBeaconId(airnodeAddress, templateId);

return { dataFeedId, beacons: [{ beaconId: dataFeedId, airnodeAddress, templateId }] };
return [{ beaconId: dataFeedId, airnodeAddress, templateId }];
}

const [airnodeAddresses, templateIds] = ethers.AbiCoder.defaultAbiCoder().decode(
Expand All @@ -770,9 +770,7 @@ <h2>Active data feeds</h2>
return { beaconId, airnodeAddress, templateId };
});

const dataFeedId = deriveBeaconSetId(beacons.map((b) => b.beaconId));

return { dataFeedId, beacons };
return beacons;
};

const decodeUpdateParameters = (updateParameters) => {
Expand Down Expand Up @@ -889,17 +887,18 @@ <h2>Active data feeds</h2>
},
dataFeedValue: dataFeedValue.toString(),
dataFeedTimestamp: dataFeedTimestamp.toString(),
decodedDataFeed: decodeDataFeedDetails(dataFeedDetails),
// This slightly differs from the main logic in Airseeker, but we only care about beacon IDs here.
beacons: decodeDataFeedDetails(dataFeedDetails),
signedApiUrls,
};
console.info('Data feed', dataFeed); // For debugging purposes.

let signedDatas = [];
for (let i = 0; i < signedApiUrls.length; i++) {
const url = signedApiUrls[i].replace('host.docker.internal', 'localhost');
const airnode = dataFeed.decodedDataFeed.beacons[i].airnodeAddress;
const airnode = dataFeed.beacons[i].airnodeAddress;
const signedApiResponse = await fetch(`${url}/${airnode}`).then((res) => res.json());
const signedData = signedApiResponse.data[dataFeed.decodedDataFeed.beacons[i].beaconId];
const signedData = signedApiResponse.data[dataFeed.beacons[i].beaconId];
signedDatas.push({ ...signedData, value: decodeBeaconValue(signedData.encodedValue).toString() });
}
console.info('Signed datas', signedDatas); // For debugging purposes.
Expand All @@ -909,10 +908,10 @@ <h2>Active data feeds</h2>

const deviationPercentage = Number(calculateUpdateInPercentage(dataFeedValue, newBeaconSetValue)) / 1e6;
const deviationThresholdPercentage = Number(deviationThresholdInPercentage) / 1e6;
const sponsorWallet = deriveSponsorWallet(airseekerMnemonic, dapiName ?? dataFeed.decodedDataFeed.dataFeedId);
const sponsorWallet = deriveSponsorWallet(airseekerMnemonic, dapiName ?? dataFeed.dataFeedId);
const dataFeedInfo = {
dapiName: dapiName,
dataFeedId: dataFeed.decodedDataFeed.dataFeedId,
dataFeedId: dataFeed.dataFeedId,
decodedDapiName: ethers.decodeBytes32String(dapiName),
dataFeedValue: dataFeed.dataFeedValue,
offChainValue: {
Expand Down
4 changes: 2 additions & 2 deletions renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
{
"matchDepTypes": ["devDependencies"],
"matchUpdateTypes": ["patch", "minor"],
"schedule": ["before 1am on the first day of the month"],
"schedule": ["before 4am on Monday"],
"groupName": "non-major-dev-dependencies"
},
{
"matchDepTypes": ["dependencies"],
"matchUpdateTypes": ["patch", "minor"],
"schedule": ["before 1am on the first day of the month"],
"schedule": ["before 4am on Monday"],
"groupName": "non-major-dependencies"
}
],
Expand Down
4 changes: 2 additions & 2 deletions src/deployment/cloudformation-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"CloudWatchLogsGroup": {
"Type": "AWS::Logs::LogGroup",
"Properties": {
"LogGroupName": "AirseekerLogGroup",
"LogGroupName": "AirseekerLogGroup-<SOME_ID>",
"RetentionInDays": 7
}
},
Expand All @@ -32,7 +32,7 @@
},
{
"Name": "LOG_LEVEL",
"Value": "debug"
"Value": "info"
}
],
"EntryPoint": [
Expand Down
11 changes: 0 additions & 11 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,3 @@ export const signedApiResponseSchema = z.object({
count: z.number().positive(),
data: z.record(signedDataSchema),
});

export interface Beacon {
airnodeAddress: AirnodeAddress;
templateId: TemplateId;
beaconId: string;
}

export interface DecodedDataFeed {
dataFeedId: string;
beacons: Beacon[];
}
44 changes: 19 additions & 25 deletions src/update-feeds-loops/contracts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,24 @@ describe('helper functions', () => {
const decodedResultSingle = decodeDataFeedDetails(single);
const decodedResultMultiple = decodeDataFeedDetails(multiple);

expect(decodedResultSingle).toStrictEqual({
dataFeedId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc6',
beacons: [
{
airnodeAddress: '0xc52EeA00154B4fF1EbbF8Ba39FDe37F1AC3B9Fd4',
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc6',
templateId: '0x457a3b3da67e394a895ea49e534a4d91b2d009477bef15eab8cbed313925b010',
},
],
});
expect(decodedResultMultiple).toStrictEqual({
dataFeedId: '0xfcb594f05d31036e4eb0884f2dd1130eced8f1aa09e00bda642fee3668ffd170',
beacons: [
{
airnodeAddress: '0xc52EeA00154B4fF1EbbF8Ba39FDe37F1AC3B9Fd4',
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc6',
templateId: '0x457a3b3da67e394a895ea49e534a4d91b2d009477bef15eab8cbed313925b010',
},
{
airnodeAddress: '0xc52EeA00154B4fF1EbbF8Ba39FDe37F1AC3B9Fd4',
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc6',
templateId: '0x457a3b3da67e394a895ea49e534a4d91b2d009477bef15eab8cbed313925b010',
},
],
});
expect(decodedResultSingle).toStrictEqual([
{
airnodeAddress: '0xc52EeA00154B4fF1EbbF8Ba39FDe37F1AC3B9Fd4',
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc6',
templateId: '0x457a3b3da67e394a895ea49e534a4d91b2d009477bef15eab8cbed313925b010',
},
]);
expect(decodedResultMultiple).toStrictEqual([
{
airnodeAddress: '0xc52EeA00154B4fF1EbbF8Ba39FDe37F1AC3B9Fd4',
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc6',
templateId: '0x457a3b3da67e394a895ea49e534a4d91b2d009477bef15eab8cbed313925b010',
},
{
airnodeAddress: '0xc52EeA00154B4fF1EbbF8Ba39FDe37F1AC3B9Fd4',
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc6',
templateId: '0x457a3b3da67e394a895ea49e534a4d91b2d009477bef15eab8cbed313925b010',
},
]);
});
});
69 changes: 50 additions & 19 deletions src/update-feeds-loops/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ethers } from 'ethers';
import { zip } from 'lodash';

import {
Api3ServerV1__factory as Api3ServerV1Factory,
type AirseekerRegistry,
AirseekerRegistry__factory as AirseekerRegistryFactory,
Api3ServerV1__factory as Api3ServerV1Factory,
} from '../typechain-types';
import type { DecodedDataFeed } from '../types';
import { decodeDapiName, deriveBeaconId, deriveBeaconSetId } from '../utils';
import type { AirnodeAddress, TemplateId } from '../types';
import { decodeDapiName, deriveBeaconId } from '../utils';

export const getApi3ServerV1 = (address: string, provider: ethers.JsonRpcProvider) =>
Api3ServerV1Factory.connect(address, provider);
Expand All @@ -29,9 +30,15 @@ export const decodeGetBlockNumberResponse = Number;

export const decodeGetChainIdResponse = Number;

export const decodeDataFeedDetails = (dataFeed: string): DecodedDataFeed | null => {
export interface Beacon {
airnodeAddress: AirnodeAddress;
templateId: TemplateId;
beaconId: string;
}

export const decodeDataFeedDetails = (dataFeed: string): Beacon[] | null => {
// The contract returns empty bytes if the data feed is not registered. See:
// https://github.com/api3dao/dapi-management/blob/f3d39e4707c33c075a8f07aa8f8369f8dc07736f/contracts/AirseekerRegistry.sol#L209
// https://github.com/bbenligiray/api3-contracts/blob/d394581549e4d2f343e9910bc330b21266808851/contracts/AirseekerRegistry.sol#L346
if (dataFeed === '0x') return null;

// This is a hex encoded string, the contract works with bytes directly
Expand All @@ -41,7 +48,7 @@ export const decodeDataFeedDetails = (dataFeed: string): DecodedDataFeed | null

const dataFeedId = deriveBeaconId(airnodeAddress, templateId)!;

return { dataFeedId, beacons: [{ beaconId: dataFeedId, airnodeAddress, templateId }] };
return [{ beaconId: dataFeedId, airnodeAddress, templateId }];
}

const [airnodeAddresses, templateIds] = ethers.AbiCoder.defaultAbiCoder().decode(
Expand All @@ -56,9 +63,7 @@ export const decodeDataFeedDetails = (dataFeed: string): DecodedDataFeed | null
return { beaconId, airnodeAddress, templateId };
});

const dataFeedId = deriveBeaconSetId(beacons.map((b) => b.beaconId))!;

return { dataFeedId, beacons };
return beacons;
};

export interface DecodedUpdateParameters {
Expand All @@ -83,40 +88,66 @@ export const decodeUpdateParameters = (updateParameters: string): DecodedUpdateP
};
};

export interface BeaconWithData extends Beacon {
value: bigint;
timestamp: bigint;
}

export interface DecodedActiveDataFeedResponse {
dapiName: string | null;
dataFeedId: string;
decodedDapiName: string | null;
decodedUpdateParameters: DecodedUpdateParameters;
dataFeedValue: bigint;
dataFeedTimestamp: bigint;
decodedDataFeed: DecodedDataFeed;
beaconsWithData: BeaconWithData[];
signedApiUrls: string[];
}

export const createBeaconsWithData = (beacons: Beacon[], beaconValues: bigint[], beaconTimestamps: bigint[]) => {
return zip(beacons, beaconValues, beaconTimestamps).map(([beacon, value, timestamp]) => ({
...beacon!,
value: value!,
timestamp: BigInt(timestamp!),
}));
};

export const decodeActiveDataFeedResponse = (
airseekerRegistry: AirseekerRegistry,
activeDataFeedReturndata: string
): DecodedActiveDataFeedResponse | null => {
const { dapiName, updateParameters, dataFeedValue, dataFeedTimestamp, dataFeedDetails, signedApiUrls } =
airseekerRegistry.interface.decodeFunctionResult('activeDataFeed', activeDataFeedReturndata) as unknown as Awaited<
ReturnType<AirseekerRegistry['activeDataFeed']['staticCall']>
>;

// https://github.com/api3dao/dapi-management/blob/f3d39e4707c33c075a8f07aa8f8369f8dc07736f/contracts/AirseekerRegistry.sol#L162
const decodedDataFeed = decodeDataFeedDetails(dataFeedDetails);
if (!decodedDataFeed) return null;
const {
dataFeedId,
dapiName,
updateParameters,
dataFeedValue,
dataFeedTimestamp,
dataFeedDetails,
signedApiUrls,
beaconValues,
beaconTimestamps,
} = airseekerRegistry.interface.decodeFunctionResult(
'activeDataFeed',
activeDataFeedReturndata
) as unknown as Awaited<ReturnType<AirseekerRegistry['activeDataFeed']['staticCall']>>;

// https://github.com/bbenligiray/api3-contracts/blob/d394581549e4d2f343e9910bc330b21266808851/contracts/AirseekerRegistry.sol#L295
const beacons = decodeDataFeedDetails(dataFeedDetails);
if (!beacons) return null;
const beaconsWithData = createBeaconsWithData(beacons, beaconValues, beaconTimestamps);

// The dAPI name will be set to zero (in bytes32) in case the data feed is not a dAPI and is identified by a data feed
// ID.
const decodedDapiName = decodeDapiName(dapiName);

return {
dapiName: decodedDapiName === '' ? null : dapiName, // NOTE: Anywhere in the codebase the "dapiName" is the encoded version of the dAPI name.
dataFeedId,
decodedDapiName: decodedDapiName === '' ? null : decodedDapiName,
decodedUpdateParameters: decodeUpdateParameters(updateParameters),
dataFeedValue,
dataFeedTimestamp,
decodedDataFeed,
beaconsWithData,
signedApiUrls,
};
};
Loading
Loading