Skip to content

Refactor to allow for single sponsorWallet updates #296

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

Merged
merged 39 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6c7985d
Refactor to allow for single sponsorWallet updates that enables deplo…
acenolaza May 2, 2024
aee6a86
Adds support to self-funded walletDerivationScheme type to local-test…
acenolaza May 3, 2024
96937b5
Uses both sponsorWalletAddress and dataFeedId as keys on the pendingT…
acenolaza May 3, 2024
947bdab
Fixes tests
acenolaza May 6, 2024
780250d
Renames walletDerivationScheme type option from fallback to fixed
acenolaza May 6, 2024
48b42f7
Improves walletDerivationSchemeSchema validation
acenolaza May 6, 2024
20aa6f8
Sorts PendingTransactionInfo objects by consecutivelyUpdatableCount b…
acenolaza May 6, 2024
9c86b64
Linting
acenolaza May 6, 2024
2222231
Test for fixed walletDerivationScheme.type on utils.ts
acenolaza May 6, 2024
35aa3a6
Adds test for fixed wallet derivation type to submit-transactions.tes…
acenolaza May 7, 2024
d5215d0
Revert to use address property on ethers.Wallet objects instead of us…
acenolaza May 8, 2024
0a3b3cc
Updates README on fixed walletDerivationScheme type
acenolaza May 8, 2024
9f656ee
walletDerivationSchemeSchema discrimitantedUnion refactor
acenolaza May 9, 2024
d958d2f
Removes .reduce(...) call to fiter out null or undefined PendingTrans…
acenolaza May 9, 2024
bea2afb
Renames test in gas-price.test
acenolaza May 9, 2024
427c832
Removes inline comment and renames variable in gas-price.ts
acenolaza May 9, 2024
2046954
Fixes typo
acenolaza May 9, 2024
ad7ac3a
Refactor sponsor wallet derivation and private key caching mapping
acenolaza May 10, 2024
f1ddddb
Merge remote-tracking branch 'origin/main' into single-sponsor-wallet
acenolaza May 10, 2024
54c2b91
Lint
acenolaza May 10, 2024
40093f5
Adds back .send to contracts function calls
acenolaza May 10, 2024
f0bd142
Removes else statement in submit-transactions.ts"
acenolaza May 10, 2024
03bde8e
Adds comment to pendingTransactionsInfo state object
acenolaza May 13, 2024
3ce687b
Fixes drawio pipeline
acenolaza May 13, 2024
1fc9385
Change .reduce() with 2 .map() on submitBatchTransaction
acenolaza May 13, 2024
de64763
Make submitBatchTransaction() call submitUpdate()
acenolaza May 14, 2024
eec9d7b
Handles transaction errors inside submitUpdate()
acenolaza May 14, 2024
53214da
Refactor wallet derivation types
acenolaza May 14, 2024
d19a02a
Minor refactor
acenolaza May 14, 2024
e2bc900
Merge remote-tracking branch 'origin/main' into single-sponsor-wallet
acenolaza May 15, 2024
8e876ab
Merge remote-tracking branch 'origin/main' into single-sponsor-wallet
acenolaza May 15, 2024
ee61e56
Prettier
acenolaza May 15, 2024
7303ecd
Fixes e2e
acenolaza May 15, 2024
c4d54e8
Updates airseeker.example.json i local-test-configuration dir
acenolaza May 15, 2024
d17db3c
Removes the sort desc by consecutiveUpdatableCount
acenolaza May 15, 2024
49d3db6
Refactor submitTransactions to return the success count
acenolaza May 16, 2024
9b62a07
Merge remote-tracking branch 'origin/main' into single-sponsor-wallet
acenolaza May 16, 2024
960fca3
Update README on self-funded wallet derivation type
acenolaza May 16, 2024
2254e1e
Refactor pendingTransactionsInfo object updater
acenolaza May 16, 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
426 changes: 213 additions & 213 deletions airseeker_v2_pipeline.drawio

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions config/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ The following options are available:
parameters. This is the scheme that was originally used by Nodary for self-funded data feeds.
- `managed` - Derives the wallet from the hash of the dAPI name (or data feed ID). This means the wallet derivation is
agnostic to update parameters, and the same wallet is used when the dAPI is upgraded/downgraded.
- `fixed` - Derives the wallet from the specified `sponsorAddress`. All data feed updates will be done via this single
wallet.

### `stage`

Expand Down
6 changes: 2 additions & 4 deletions local-test-configuration/airnode-feed-1/airnode-feed.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,12 @@
"triggers": {
"signedApiUpdates": [
{
"signedApiName": "localhost",
"templateIds": [
"0xa5419706d8edb3bbafad83fe2b4e7dc851de5e4cd9529f9f27bb393016c81ae5",
"0x8f255387c5fdb03117d82372b8fa5c7813881fd9a8202b7cc373f1a5868496b2",
"0x89bbdb4e2d7510abf1ca0ed53295e31c00a6939fc12e77d67bf3a4cb3c31f61c"
],
"fetchInterval": 10,
"updateDelay": 0
"fetchInterval": 10
}
]
},
Expand Down Expand Up @@ -94,7 +92,7 @@
}
],
"nodeSettings": {
"nodeVersion": "0.4.0",
"nodeVersion": "1.0.0",
"airnodeWalletMnemonic": "${AIRNODE_WALLET_MNEMONIC}",
"stage": "local-example"
}
Expand Down
6 changes: 2 additions & 4 deletions local-test-configuration/airnode-feed-2/airnode-feed.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,12 @@
"triggers": {
"signedApiUpdates": [
{
"signedApiName": "localhost",
"templateIds": [
"0xa5419706d8edb3bbafad83fe2b4e7dc851de5e4cd9529f9f27bb393016c81ae5",
"0x8f255387c5fdb03117d82372b8fa5c7813881fd9a8202b7cc373f1a5868496b2",
"0x89bbdb4e2d7510abf1ca0ed53295e31c00a6939fc12e77d67bf3a4cb3c31f61c"
],
"fetchInterval": 20,
"updateDelay": 60
"fetchInterval": 20
}
]
},
Expand Down Expand Up @@ -94,7 +92,7 @@
}
],
"nodeSettings": {
"nodeVersion": "0.4.0",
"nodeVersion": "1.0.0",
"airnodeWalletMnemonic": "${AIRNODE_WALLET_MNEMONIC}",
"stage": "local-example"
}
Expand Down
10 changes: 6 additions & 4 deletions local-test-configuration/airseeker/airseeker.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"sponsorWalletMnemonic": "${SPONSOR_WALLET_MNEMONIC}",
"chains": {
"31337": {
"alias": "hardhat",
"contracts": {
"Api3ServerV1": "${API3_SERVER_V1_ADDRESS}",
"AirseekerRegistry": "${AIRSEEKER_REGISTRY_ADDRESS}"
Expand All @@ -19,13 +20,14 @@
"maxScalingMultiplier": 2,
"sanitizationMultiplier": 3
},
"dataFeedUpdateInterval": 30,
"dataFeedBatchSize": 10,
"fallbackGasLimit": 2000000
"dataFeedUpdateInterval": 60,
"dataFeedBatchSize": 10
}
},
"deviationThresholdCoefficient": 1,
"signedDataFetchInterval": 10,
"signedApiUrls": [],
"walletDerivationScheme": { "type": "managed" }
"walletDerivationScheme": { "type": "managed" },
"stage": "dev",
"version": "3.1.0"
}
26 changes: 20 additions & 6 deletions local-test-configuration/monitoring/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -730,10 +730,13 @@ <h2>Active data feeds</h2>
const rpcUrl = urlParams.get('rpcUrl'),
airseekerRegistryAddress = urlParams.get('airseekerRegistryAddress'),
airseekerMnemonic = decodeURIComponent(urlParams.get('airseekerMnemonic')),
walletDerivationScheme = decodeURIComponent(urlParams.get('walletDerivationScheme'));
walletDerivationScheme = decodeURIComponent(urlParams.get('walletDerivationScheme')),
sponsorAddress = decodeURIComponent(urlParams.get('sponsorAddress'));

if (!airseekerRegistryAddress) throw new Error('airseekerRegistryAddress must be provided as URL parameter');
if (!airseekerMnemonic) throw new Error('airseekerMnemonic must be provided as URL parameter');
if (walletDerivationScheme === 'fallback' && !sponsorAddress)
throw new Error('sponsorAddress must be provided as URL parameter when walletDerivationScheme is "fallback"');

// See: https://github.com/GoogleChromeLabs/jsbi/issues/30#issuecomment-953187833
BigInt.prototype.toJSON = function () {
Expand Down Expand Up @@ -876,10 +879,18 @@ <h2>Active data feeds</h2>
//
// For self-funded feeds it's more suitable to derive the hash also from update parameters. This does not apply to
// mananaged feeds which want to be funded by the same wallet independently of the update parameters.
const sponsorAddressHash =
walletDerivationScheme === 'self-funded'
? deriveSponsorAddressHashForSelfFundedFeed(dapiNameOrDataFeedId, updateParameters)
: deriveSponsorAddressHashForManagedFeed(dapiNameOrDataFeedId);
let sponsorAddressHash;
switch (walletDerivationScheme.type) {
case 'self-funded':
sponsorAddressHash = deriveSponsorAddressHashForSelfFundedFeed(dapiNameOrDataFeedId, updateParameters);
break;
case 'managed':
sponsorAddressHash = deriveSponsorAddressHashForManagedFeed(dapiNameOrDataFeedId);
break;
case 'fixed':
sponsorAddressHash = walletDerivationScheme.sponsorAddress;
break;
}

return deriveSponsorWalletFromSponsorAddressHash(sponsorWalletMnemonic, sponsorAddressHash);
};
Expand Down Expand Up @@ -938,7 +949,10 @@ <h2>Active data feeds</h2>
airseekerMnemonic,
dapiName ?? dataFeed.dataFeedId,
updateParameters,
walletDerivationScheme
{
type: walletDerivationScheme,
...(sponsorAddress && { sponsorAddress }),
}
);
const dataFeedInfo = {
dapiName: dapiName,
Expand Down
144 changes: 79 additions & 65 deletions local-test-configuration/scripts/initialize-chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,23 @@ import { join } from 'node:path';

import { encode } from '@api3/airnode-abi';
import {
type Address,
type Hex,
deriveBeaconId,
interpolateSecretsIntoConfig,
loadConfig,
loadSecrets,
type Address,
type Hex,
} from '@api3/commons';
import {
AirseekerRegistry__factory as AirseekerRegistryFactory,
AccessControlRegistry__factory as AccessControlRegistryFactory,
AirseekerRegistry__factory as AirseekerRegistryFactory,
Api3ServerV1__factory as Api3ServerV1Factory,
} from '@api3/contracts';
import dotenv from 'dotenv';
import type { ContractTransactionResponse, Signer } from 'ethers';
import { ethers } from 'ethers';
import { NonceManager, ethers } from 'ethers';
import { zip } from 'lodash';

import {
deriveSponsorAddressHashForManagedFeed,
deriveSponsorWalletFromSponsorAddressHash,
encodeDapiName,
} from '../../src/utils';
import { deriveSponsorWallet, encodeDapiName } from '../../src/utils';

interface RawBeaconData {
airnodeAddress: Address;
Expand Down Expand Up @@ -57,26 +52,43 @@ export const deriveRole = (adminRole: string, roleDescription: string) => {
);
};

function encodeUpdateParameters() {
const HUNDRED_PERCENT = 1e8;
const deviationThresholdInPercentage = HUNDRED_PERCENT / 100; // 1%
const deviationReference = 0;
const heartbeatInterval = 86_400; // 24 hrs
const updateParameters = ethers.AbiCoder.defaultAbiCoder().encode(
['uint256', 'int224', 'uint256'],
[deviationThresholdInPercentage, deviationReference, heartbeatInterval]
);
return updateParameters;
}

// NOTE: This function is not used by the initialization script, but you can use it after finishing Airseeker test on a
// public testnet to refund test ETH from sponsor wallets to the funder wallet.
export const refundFunder = async (funderWallet: ethers.HDNodeWallet) => {
const airseekerSecrets = dotenv.parse(readFileSync(join(__dirname, `/../airseeker`, 'secrets.env'), 'utf8'));
export const refundFunder = async (funderWallet: ethers.NonceManager) => {
const configPath = join(__dirname, `/../airseeker`);
const rawConfig = loadConfig(join(configPath, 'airseeker.json'));
const airseekerSecrets = dotenv.parse(readFileSync(join(configPath, 'secrets.env'), 'utf8'));
const airseekerWalletMnemonic = airseekerSecrets.SPONSOR_WALLET_MNEMONIC;
if (!airseekerWalletMnemonic) throw new Error('SPONSOR_WALLET_MNEMONIC not found in Airseeker secrets');

// Initialize sponsor wallets
for (const beaconSetName of getBeaconSetNames()) {
const dapiName = encodeDapiName(beaconSetName);
const updateParameters = encodeUpdateParameters();

const sponsorAddressHash = deriveSponsorAddressHashForManagedFeed(dapiName);
const sponsorWallet = deriveSponsorWalletFromSponsorAddressHash(
airseekerWalletMnemonic,
sponsorAddressHash
).connect(funderWallet.provider);
const sponsorWalletBalance = await funderWallet.provider!.getBalance(sponsorWallet.address);
console.info('Sponsor wallet balance:', ethers.formatEther(sponsorWalletBalance.toString()));
const provider = funderWallet.provider!;

const sponsorWallet = deriveSponsorWallet(airseekerWalletMnemonic, {
...rawConfig.walletDerivationScheme,
dapiNameOrDataFeedId: dapiName,
updateParameters,
}).connect(provider);
const sponsorWalletBalance = await provider.getBalance(sponsorWallet);
console.info('Sponsor wallet balance:', sponsorWallet.address, ethers.formatEther(sponsorWalletBalance.toString()));

const feeData = await sponsorWallet.provider!.getFeeData();
const feeData = await provider.getFeeData();
const { gasPrice } = feeData;
// We assume the legacy gas price will always exist. See:
// https://api3workspace.slack.com/archives/C05TQPT7PNJ/p1699098552350519
Expand All @@ -86,7 +98,7 @@ export const refundFunder = async (funderWallet: ethers.HDNodeWallet) => {
continue;
}
const tx = await sponsorWallet.sendTransaction({
to: funderWallet.address,
to: funderWallet,
gasPrice,
gasLimit: BigInt(21_000),
value: sponsorWalletBalance - gasFee,
Expand Down Expand Up @@ -122,55 +134,67 @@ const getBeaconSetNames = () => {
return airnodeFeedBeacons.map((beacon) => beacon.parameters[0]!.value);
};

export const fundAirseekerSponsorWallet = async (funderWallet: ethers.HDNodeWallet) => {
export const fundAirseekerSponsorWallet = async (funderWallet: ethers.NonceManager) => {
const configPath = join(__dirname, `/../airseeker`);
const rawConfig = loadConfig(join(configPath, 'airseeker.json'));
const airseekerSecrets = dotenv.parse(readFileSync(join(__dirname, `/../airseeker`, 'secrets.env'), 'utf8'));
const airseekerWalletMnemonic = airseekerSecrets.SPONSOR_WALLET_MNEMONIC;
if (!airseekerWalletMnemonic) throw new Error('SPONSOR_WALLET_MNEMONIC not found in Airseeker secrets');

// Initialize sponsor wallets
for (const beaconSetName of getBeaconSetNames()) {
const dapiName = encodeDapiName(beaconSetName);
const updateParameters = encodeUpdateParameters();

const sponsorAddressHash = deriveSponsorAddressHashForManagedFeed(dapiName);
const sponsorWallet = deriveSponsorWalletFromSponsorAddressHash(airseekerWalletMnemonic, sponsorAddressHash);
const sponsorWalletBalance = await funderWallet.provider!.getBalance(sponsorWallet.address);
const provider = funderWallet.provider!;
const sponsorWallet = deriveSponsorWallet(airseekerWalletMnemonic, {
...rawConfig.walletDerivationScheme,
dapiNameOrDataFeedId: dapiName,
updateParameters,
});
const sponsorWalletBalance = await provider.getBalance(sponsorWallet);
console.info('Sponsor wallet balance:', ethers.formatEther(sponsorWalletBalance.toString()));

const tx = await funderWallet.sendTransaction({
to: sponsorWallet.address,
value: ethers.parseEther('1'),
to: sponsorWallet,
value: ethers.parseEther('0.1'),
});
await tx.wait();

console.info(`Funding sponsor wallets`, {
dapiName,
decodedDapiName: ethers.decodeBytes32String(dapiName),
sponsorWalletAddress: sponsorWallet.address,
balance: ethers.formatEther(await provider.getBalance(sponsorWallet)),
});
}
};

export const deploy = async (funderWallet: ethers.HDNodeWallet, provider: ethers.JsonRpcProvider) => {
export const deploy = async (funderWallet: ethers.NonceManager, provider: ethers.JsonRpcProvider) => {
// NOTE: It is OK if all of these roles are done via the funder wallet.
const deployerAndManager = funderWallet,
randomPerson = funderWallet;
const deployerAndManager = funderWallet;

const randomPerson = ethers.Wallet.createRandom().connect(deployerAndManager.provider);
const fundRandomPersonTx = await deployerAndManager.sendTransaction({
to: randomPerson,
value: ethers.parseEther('1'),
});
await fundRandomPersonTx.wait();

// Deploy contracts
const accessControlRegistryFactory = new AccessControlRegistryFactory(deployerAndManager as Signer);
const accessControlRegistryFactory = new AccessControlRegistryFactory(deployerAndManager);
const accessControlRegistry = await accessControlRegistryFactory.deploy();
await accessControlRegistry.waitForDeployment();
const api3ServerV1Factory = new Api3ServerV1Factory(deployerAndManager as Signer);
const api3ServerV1Factory = new Api3ServerV1Factory(deployerAndManager);
const api3ServerV1AdminRoleDescription = 'Api3ServerV1 admin';
const api3ServerV1 = await api3ServerV1Factory.deploy(
accessControlRegistry.getAddress(),
api3ServerV1AdminRoleDescription,
deployerAndManager.address
deployerAndManager
);
await api3ServerV1.waitForDeployment();
const airseekerRegistryFactory = new AirseekerRegistryFactory(deployerAndManager as Signer);
const airseekerRegistry = await airseekerRegistryFactory.deploy(
await (deployerAndManager as Signer).getAddress(),
api3ServerV1.getAddress()
);
const airseekerRegistryFactory = new AirseekerRegistryFactory(deployerAndManager);
const airseekerRegistry = await airseekerRegistryFactory.deploy(deployerAndManager, api3ServerV1.getAddress());
await airseekerRegistry.waitForDeployment();

// Create templates
Expand All @@ -179,9 +203,7 @@ export const deploy = async (funderWallet: ethers.HDNodeWallet, provider: ethers
const airnodeFeed1Wallet = ethers.Wallet.fromPhrase(airnodeFeed1.nodeSettings.airnodeWalletMnemonic).connect(
provider
);
const airnodeFeed2Wallet = ethers.Wallet.fromPhrase(airnodeFeed2.nodeSettings.airnodeWalletMnemonic).connect(
provider
);
const airnodeFeed2Wallet = ethers.Wallet.fromPhrase(airnodeFeed2.nodeSettings.airnodeWalletMnemonic, provider);
const airnodeFeed1Beacons = Object.values(airnodeFeed1.templates).map((template: any) => {
return deriveBeaconData({ ...template, airnodeAddress: airnodeFeed1Wallet.address });
});
Expand All @@ -194,10 +216,9 @@ export const deploy = async (funderWallet: ethers.HDNodeWallet, provider: ethers
[airnodeFeed1Wallet.address, joinUrl(airnodeFeed1.signedApis[0].url, 'default')], // NOTE: Airnode feed pushes to the "/" of the signed API, but we need to query it additional path.
[airnodeFeed2Wallet.address, joinUrl(airnodeFeed2.signedApis[0].url, 'default')], // NOTE: Airnode feed pushes to the "/" of the signed API, but we need to query it additional path.
] as const;
let tx: ContractTransactionResponse;
for (const [airnode, url] of apiTreeValues) {
tx = await airseekerRegistry.connect(deployerAndManager).setSignedApiUrl(airnode, url);
await tx.wait();
const setSignedApiUrlTx = await airseekerRegistry.connect(deployerAndManager).setSignedApiUrl(airnode, url);
await setSignedApiUrlTx.wait();
}
const dapiInfos = zip(airnodeFeed1Beacons, airnodeFeed2Beacons).map(([airnodeFeed1Beacon, airnodeFeed2Beacon]) => {
return {
Expand All @@ -219,26 +240,19 @@ export const deploy = async (funderWallet: ethers.HDNodeWallet, provider: ethers
['address[]', 'bytes32[]'],
[airnodes, templateIds]
);
tx = await airseekerRegistry.connect(randomPerson).registerDataFeed(encodedBeaconSetData);
await tx.wait();
const HUNDRED_PERCENT = 1e8;
const deviationThresholdInPercentage = BigInt(HUNDRED_PERCENT / 100); // 1%
const deviationReference = 0n;
const heartbeatInterval = BigInt(86_400); // 24 hrs
tx = await api3ServerV1.connect(deployerAndManager).setDapiName(dapiName, beaconSetId);
await tx.wait();
tx = await airseekerRegistry.connect(deployerAndManager).setDapiNameToBeActivated(dapiName);
await tx.wait();
tx = await airseekerRegistry
const registerDataFeedTx = await airseekerRegistry.connect(randomPerson).registerDataFeed(encodedBeaconSetData);
await registerDataFeedTx.wait();
const updateParameters = encodeUpdateParameters();
const setDapiNameTx = await api3ServerV1.connect(deployerAndManager).setDapiName(dapiName, beaconSetId);
await setDapiNameTx.wait();
const setDapiNameToBeActivatedTx = await airseekerRegistry
.connect(deployerAndManager)
.setDapiNameUpdateParameters(
dapiName,
ethers.AbiCoder.defaultAbiCoder().encode(
['uint256', 'uint256', 'uint256'],
[deviationThresholdInPercentage, deviationReference, heartbeatInterval]
)
);
await tx.wait();
.setDapiNameToBeActivated(dapiName);
await setDapiNameToBeActivatedTx.wait();
const setDapiNameUpdateParametersTx = await airseekerRegistry
.connect(deployerAndManager)
.setDapiNameUpdateParameters(dapiName, updateParameters);
await setDapiNameUpdateParametersTx.wait();
}

return {
Expand All @@ -265,10 +279,10 @@ async function main() {
polling: true,
pollingInterval: 100,
});
const funderWallet = ethers.Wallet.fromPhrase(process.env.FUNDER_MNEMONIC).connect(provider);
const funderWallet = new NonceManager(ethers.Wallet.fromPhrase(process.env.FUNDER_MNEMONIC, provider));

await refundFunder(funderWallet);
const balance = await provider.getBalance(funderWallet.address);
const balance = await provider.getBalance(funderWallet);
console.info('Funder balance:', ethers.formatEther(balance.toString()));
console.info();

Expand Down
Loading