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

Migrate from ethers v5 to ethers v6 #178

Merged
merged 35 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0b5ca2f
Update dependencies for ethers@6, remove unused
Siegrift Jan 26, 2024
c9eb86b
Compile Typechain artifacts
Siegrift Jan 26, 2024
458d6a0
Migrate some functions
Siegrift Jan 26, 2024
b7dde71
Migrate defaultAbiCoder
Siegrift Jan 26, 2024
97ac292
Fix randomBytes
Siegrift Jan 26, 2024
e6ad089
Replate BigNumber.from with BigInt constructor
Siegrift Jan 26, 2024
6d8ce1d
Change generate bytes API
Siegrift Jan 26, 2024
10974dd
Fix some test TS errors
Siegrift Jan 26, 2024
8f3287a
More renamings
Siegrift Jan 26, 2024
81818be
Fix contract setup issues
Siegrift Jan 26, 2024
552d32b
Fix ethers@6 providers rename
Siegrift Jan 26, 2024
554c20e
More rename
Siegrift Jan 26, 2024
6ad8a5d
Wallet path algo
Siegrift Jan 26, 2024
d47e818
Rename fromMnemonic
Siegrift Jan 26, 2024
5a273c5
Fix more update test issues
Siegrift Jan 26, 2024
b916652
More fixes
Siegrift Jan 26, 2024
be6557b
More fixes
Siegrift Jan 26, 2024
67e41dd
Compile protocol-v1 contracts manually
Siegrift Jan 26, 2024
3c16fdd
Rename all ethers.utils.xyz to ethers.xyz
Siegrift Jan 26, 2024
5af8388
Fix gas price and initialize chain script
Siegrift Jan 26, 2024
d87a234
Do not import ethers@5 factories
Siegrift Jan 26, 2024
2642140
Replace ethers.BigNumber with bigint
Siegrift Jan 26, 2024
d3d2e12
Resolve more issues
Siegrift Jan 26, 2024
9b2c5c9
Resolve remaining TS issues
Siegrift Jan 26, 2024
242210e
Fix derivation algorithm
Siegrift Jan 29, 2024
ff6bd18
Fix BigInt serialization
Siegrift Jan 29, 2024
0a6b21b
Fix deviation tests
Siegrift Jan 29, 2024
655c366
Fix more tests
Siegrift Jan 29, 2024
c4c46b3
Fix update dAPI test
Siegrift Jan 29, 2024
dfe6d14
Move deps section over devDeps
Siegrift Jan 30, 2024
ddf6ffb
Self review and fix TODOs
Siegrift Jan 30, 2024
379e0d0
Fix e2e test
Siegrift Jan 30, 2024
3669a07
Fix local test configuration
Siegrift Jan 30, 2024
16be144
Self review
Siegrift Jan 30, 2024
6e0b884
Use BigInt literal when possible
Siegrift Feb 7, 2024
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
5 changes: 5 additions & 0 deletions contracts/airnode-protocol-v1-contracts.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;

import "@api3/airnode-protocol-v1/contracts/access-control-registry/AccessControlRegistry.sol";
import "@api3/airnode-protocol-v1/contracts/api3-server-v1/Api3ServerV1.sol";
4 changes: 0 additions & 4 deletions contracts/contract-imports.sol

This file was deleted.

4 changes: 4 additions & 0 deletions contracts/dapi-management-contracts.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;

import "api3-contracts/contracts/AirseekerRegistry.sol";
4 changes: 2 additions & 2 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { HardhatUserConfig } from 'hardhat/types';
import '@nomicfoundation/hardhat-toolbox';

const config: HardhatUserConfig = {
solidity: { version: '0.8.18' },
solidity: { compilers: [{ version: '0.8.17' }] },
networks: {
localhost: {
url: 'http://127.0.0.1:8545/',
Expand All @@ -14,7 +14,7 @@ const config: HardhatUserConfig = {
// flattened version (only the contents of the "src" folder). This is also in anticipation of importing the
// Typechain types instead of generating them at build time.
outDir: 'src/typechain-types',
target: 'ethers-v5',
target: 'ethers-v6',
},
defaultNetwork: 'localhost',
};
Expand Down
4 changes: 4 additions & 0 deletions jest-unit.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ module.exports = {
testMatch: ['**/?(*.)+(spec|test).[t]s?(x)'],
testPathIgnorePatterns: ['<rootDir>/.build', '<rootDir>/dist/', '<rootDir>/build/'],
verbose: true,

// See: https://github.com/jestjs/jest/issues/11617#issuecomment-1028651059. We can't use "workerThreads" mentioned
// later, because it complains that some of the node internal modules used in commons processing are unavailable.
maxWorkers: 1,
};
2 changes: 1 addition & 1 deletion local-test-configuration/airnode-feed-1/airnode-feed.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
}
],
"nodeSettings": {
"nodeVersion": "0.2.0",
"nodeVersion": "0.4.0",
"airnodeWalletMnemonic": "${AIRNODE_WALLET_MNEMONIC}",
"stage": "local-example"
}
Expand Down
2 changes: 1 addition & 1 deletion local-test-configuration/airnode-feed-2/airnode-feed.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
}
],
"nodeSettings": {
"nodeVersion": "0.2.0",
"nodeVersion": "0.4.0",
"airnodeWalletMnemonic": "${AIRNODE_WALLET_MNEMONIC}",
"stage": "local-example"
}
Expand Down
141 changes: 95 additions & 46 deletions local-test-configuration/monitoring/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ <h2>Active data feeds</h2>
<pre id="activeDataFeeds"></pre>
</body>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.umd.min.js"
integrity="sha512-FDcVY+g7vc5CXANbrTSg1K5qLyriCsGDYCE02Li1tXEYdNQPvLPHNE+rT2Mjei8N7fZbe0WLhw27j2SrGRpdMg=="
src="https://cdnjs.cloudflare.com/ajax/libs/ethers/6.10.0/ethers.umd.min.js"
integrity="sha512-O+pv4/QL+b3vRcPZ64zjoh+t6yhvo8L/OgQQuQIUI9GbMC6VwsujvLiUV+aIxlPLSo+SLVgf8orHcb15S5ieiQ=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
Expand Down Expand Up @@ -188,6 +188,45 @@ <h2>Active data feeds</h2>
name: 'UpdatedSignedApiUrl',
type: 'event',
},
{
inputs: [],
name: 'MAXIMUM_BEACON_COUNT_IN_SET',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'MAXIMUM_SIGNED_API_URL_LENGTH',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'MAXIMUM_UPDATE_PARAMETERS_LENGTH',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'activeDapiNameCount',
Expand Down Expand Up @@ -236,6 +275,16 @@ <h2>Active data feeds</h2>
name: 'dataFeedTimestamp',
type: 'uint32',
},
{
internalType: 'int224[]',
name: 'beaconValues',
type: 'int224[]',
},
{
internalType: 'uint32[]',
name: 'beaconTimestamps',
type: 'uint32[]',
},
{
internalType: 'bytes',
name: 'updateParameters',
Expand Down Expand Up @@ -529,7 +578,7 @@ <h2>Active data feeds</h2>
inputs: [],
name: 'renounceOwnership',
outputs: [],
stateMutability: 'nonpayable',
stateMutability: 'pure',
type: 'function',
},
{
Expand Down Expand Up @@ -648,7 +697,7 @@ <h2>Active data feeds</h2>
],
name: 'transferOwnership',
outputs: [],
stateMutability: 'nonpayable',
stateMutability: 'pure',
type: 'function',
},
{
Expand Down Expand Up @@ -676,7 +725,6 @@ <h2>Active data feeds</h2>
type: 'function',
},
];

// Configuration
const urlParams = new URLSearchParams(window.location.search);
const rpcUrl = urlParams.get('rpcUrl'),
Expand All @@ -686,26 +734,34 @@ <h2>Active data feeds</h2>
if (!airseekerRegistryAddress) throw new Error('airseekerRegistryAddress must be provided as URL parameter');
if (!airseekerMnemonic) throw new Error('airseekerMnemonic must be provided as URL parameter');

// See: https://github.com/GoogleChromeLabs/jsbi/issues/30#issuecomment-953187833
BigInt.prototype.toJSON = function () {
return this.toString();
};

function deriveBeaconId(airnodeAddress, templateId) {
return ethers.utils.solidityKeccak256(['address', 'bytes32'], [airnodeAddress, templateId]);
return ethers.solidityPackedKeccak256(['address', 'bytes32'], [airnodeAddress, templateId]);
}

function deriveBeaconSetId(beaconIds) {
return ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['bytes32[]'], [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
const [airnodeAddress, templateId] = ethers.utils.defaultAbiCoder.decode(['address', 'bytes32'], dataFeed);
const [airnodeAddress, templateId] = ethers.AbiCoder.defaultAbiCoder().decode(['address', 'bytes32'], dataFeed);

const dataFeedId = deriveBeaconId(airnodeAddress, templateId);

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

const [airnodeAddresses, templateIds] = ethers.utils.defaultAbiCoder.decode(['address[]', 'bytes32[]'], dataFeed);
const [airnodeAddresses, templateIds] = ethers.AbiCoder.defaultAbiCoder().decode(
['address[]', 'bytes32[]'],
dataFeed
);

const beacons = airnodeAddresses.map((airnodeAddress, idx) => {
const templateId = templateIds[idx];
Expand All @@ -722,7 +778,7 @@ <h2>Active data feeds</h2>
const decodeUpdateParameters = (updateParameters) => {
// https://github.com/api3dao/airnode-protocol-v1/blob/5f861715749e182e334c273d6a52c4f2560c7994/contracts/api3-server-v1/extensions/BeaconSetUpdatesWithPsp.sol#L122
const [deviationThresholdInPercentage, deviationReference, heartbeatInterval] =
ethers.utils.defaultAbiCoder.decode(['uint256', 'int224', 'uint256'], updateParameters);
ethers.AbiCoder.defaultAbiCoder().decode(['uint256', 'int224', 'uint256'], updateParameters);

// 2 characters for the '0x' preamble + 3 parameters, 32 * 2 hexadecimals for 32 bytes each
if (updateParameters.length !== 2 + 3 * (32 * 2)) {
Expand All @@ -741,52 +797,46 @@ <h2>Active data feeds</h2>
const mid = Math.floor(arr.length / 2);

const nums = [...arr].sort((a, b) => {
if (a.lt(b)) return -1;
else if (a.gt(b)) return 1;
if (a < b) return -1;
else if (a > b) return 1;
else return 0;
});

return arr.length % 2 === 0 ? nums[mid - 1].add(nums[mid]).div(2) : nums[mid];
return arr.length % 2 === 0 ? (nums[mid - 1] + nums[mid]) / 2n : nums[mid];
};

const decodeBeaconValue = (encodedBeaconValue) => {
// Solidity type(int224).min
const INT224_MIN = ethers.BigNumber.from(2).pow(ethers.BigNumber.from(223)).mul(ethers.BigNumber.from(-1));
const INT224_MIN = 2n ** 223n * -1n;
// Solidity type(int224).max
const INT224_MAX = ethers.BigNumber.from(2).pow(ethers.BigNumber.from(223)).sub(ethers.BigNumber.from(1));
const INT224_MAX = 2n ** 223n - 1n;
aquarat marked this conversation as resolved.
Show resolved Hide resolved

const decodedBeaconValue = ethers.BigNumber.from(
ethers.utils.defaultAbiCoder.decode(['int256'], encodedBeaconValue)[0]
);
if (decodedBeaconValue.gt(INT224_MAX) || decodedBeaconValue.lt(INT224_MIN)) {
const decodedBeaconValue = BigInt(ethers.AbiCoder.defaultAbiCoder().decode(['int256'], encodedBeaconValue)[0]);
if (decodedBeaconValue > INT224_MAX || decodedBeaconValue < INT224_MIN) {
return null;
}

return decodedBeaconValue;
};

const abs = (n) => (n < 0n ? -n : n);

const calculateUpdateInPercentage = (initialValue, updatedValue) => {
const delta = updatedValue.sub(initialValue);
const absoluteDelta = delta.abs();
const delta = updatedValue - initialValue;
const absoluteDelta = abs(delta);

// Avoid division by 0
const absoluteInitialValue = initialValue.isZero() ? ethers.BigNumber.from(1) : initialValue.abs();

return absoluteDelta.mul(ethers.BigNumber.from(1e8)).div(absoluteInitialValue);
};

const checkDeviationThresholdExceeded = (onChainValue, deviationThreshold, apiValue) => {
const updateInPercentage = calculateUpdateInPercentage(onChainValue, apiValue);
const absoluteInitialValue = initialValue === 0n ? 1n : abs(initialValue);

return updateInPercentage.gt(deviationThreshold);
return (absoluteDelta * BigInt(1e8)) / absoluteInitialValue;
};

function deriveWalletPathFromSponsorAddress(sponsorAddress) {
const sponsorAddressBN = ethers.BigNumber.from(sponsorAddress);
const sponsorAddressBN = BigInt(sponsorAddress);
const paths = [];
for (let i = 0; i < 6; i++) {
const shiftedSponsorAddressBN = sponsorAddressBN.shr(31 * i);
paths.push(shiftedSponsorAddressBN.mask(31).toString());
const shiftedSponsorAddressBN = sponsorAddressBN >> BigInt(31 * i);
paths.push((shiftedSponsorAddressBN % 2n ** 31n).toString());
}
const AIRSEEKER_PROTOCOL_ID = '5'; // From: https://github.com/api3dao/airnode/blob/ef16c54f33d455a1794e7886242567fc47ee14ef/packages/airnode-protocol/src/index.ts#L46
return `${AIRSEEKER_PROTOCOL_ID}/${paths.join('/')}`;
Expand All @@ -795,20 +845,23 @@ <h2>Active data feeds</h2>
const deriveSponsorWallet = (sponsorWalletMnemonic, dapiNameOrDataFeedId) => {
// Hash the dAPI name or data feed ID because we need to take the first 20 bytes of it which could result in
// collisions for dAPIs with the same prefix.
const hashedDapiNameOrDataFeedId = ethers.utils.keccak256(dapiNameOrDataFeedId);
const hashedDapiNameOrDataFeedId = ethers.keccak256(dapiNameOrDataFeedId);

// Take first 20 bytes of the hashed dapiName or data feed ID as sponsor address together with the "0x" prefix.
const sponsorAddress = ethers.utils.getAddress(hashedDapiNameOrDataFeedId.slice(0, 42));
const sponsorWallet = ethers.Wallet.fromMnemonic(
const sponsorAddress = ethers.getAddress(hashedDapiNameOrDataFeedId.slice(0, 42));
// NOTE: Be sure not to use "ethers.Wallet.fromPhrase(sponsorWalletMnemonic).derivePath" because that produces a
// different result.
const sponsorWallet = ethers.HDNodeWallet.fromPhrase(
sponsorWalletMnemonic,
undefined,
`m/44'/60'/0'/${deriveWalletPathFromSponsorAddress(sponsorAddress)}`
);

return sponsorWallet;
};

setInterval(async () => {
const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
const provider = new ethers.JsonRpcProvider(rpcUrl);
const airseekerRegistry = new ethers.Contract(airseekerRegistryAddress, airseekerRegistryAbi, provider);
const activeDataFeedCount = await airseekerRegistry.activeDataFeedCount();

Expand Down Expand Up @@ -851,20 +904,16 @@ <h2>Active data feeds</h2>
}
console.info('Signed datas', signedDatas); // For debugging purposes.

const newBeaconSetValue = calculateMedian(
signedDatas.map((signedData) => ethers.BigNumber.from(signedData.value))
);
const newBeaconSetTimestamp = calculateMedian(
signedDatas.map((signedData) => ethers.BigNumber.from(signedData.timestamp))
).toNumber();
const newBeaconSetValue = calculateMedian(signedDatas.map((signedData) => BigInt(signedData.value)));
const newBeaconSetTimestamp = calculateMedian(signedDatas.map((signedData) => BigInt(signedData.timestamp)));

const deviationPercentage = calculateUpdateInPercentage(dataFeedValue, newBeaconSetValue).toNumber() / 1e6;
const deviationThresholdPercentage = deviationThresholdInPercentage.toNumber() / 1e6;
const deviationPercentage = Number(calculateUpdateInPercentage(dataFeedValue, newBeaconSetValue)) / 1e6;
const deviationThresholdPercentage = Number(deviationThresholdInPercentage) / 1e6;
const sponsorWallet = deriveSponsorWallet(airseekerMnemonic, dapiName ?? dataFeed.decodedDataFeed.dataFeedId);
const dataFeedInfo = {
dapiName: dapiName,
dataFeedId: dataFeed.decodedDataFeed.dataFeedId,
decodedDapiName: ethers.utils.parseBytes32String(dapiName),
decodedDapiName: ethers.decodeBytes32String(dapiName),
dataFeedValue: dataFeed.dataFeedValue,
offChainValue: {
value: newBeaconSetValue.toString(),
Expand All @@ -874,7 +923,7 @@ <h2>Active data feeds</h2>
deviationPercentage > deviationThresholdPercentage ? `<b>${deviationPercentage}</b>` : deviationPercentage,
deviationThresholdPercentage: deviationThresholdPercentage,
sponsorWalletAddress: sponsorWallet.address,
sponsorWalletBalance: ethers.utils.formatEther(await provider.getBalance(sponsorWallet.address)),
sponsorWalletBalance: ethers.formatEther(await provider.getBalance(sponsorWallet.address)),
};

newActiveDataFeedsHtml += JSON.stringify(dataFeedInfo, null, 2) + '\n\n';
Expand Down
Loading
Loading