Skip to content

Commit 6c7985d

Browse files
committed
Refactor to allow for single sponsorWallet updates that enables deploying airseeker-v2 as fallback
1 parent 55a9c8a commit 6c7985d

File tree

15 files changed

+329
-159
lines changed

15 files changed

+329
-159
lines changed

local-test-configuration/airnode-feed-1/airnode-feed.json

+2-4
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@
2222
"triggers": {
2323
"signedApiUpdates": [
2424
{
25-
"signedApiName": "localhost",
2625
"templateIds": [
2726
"0xa5419706d8edb3bbafad83fe2b4e7dc851de5e4cd9529f9f27bb393016c81ae5",
2827
"0x8f255387c5fdb03117d82372b8fa5c7813881fd9a8202b7cc373f1a5868496b2",
2928
"0x89bbdb4e2d7510abf1ca0ed53295e31c00a6939fc12e77d67bf3a4cb3c31f61c"
3029
],
31-
"fetchInterval": 10,
32-
"updateDelay": 0
30+
"fetchInterval": 10
3331
}
3432
]
3533
},
@@ -94,7 +92,7 @@
9492
}
9593
],
9694
"nodeSettings": {
97-
"nodeVersion": "0.4.0",
95+
"nodeVersion": "1.0.0",
9896
"airnodeWalletMnemonic": "${AIRNODE_WALLET_MNEMONIC}",
9997
"stage": "local-example"
10098
}

local-test-configuration/airnode-feed-2/airnode-feed.json

+2-4
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@
2222
"triggers": {
2323
"signedApiUpdates": [
2424
{
25-
"signedApiName": "localhost",
2625
"templateIds": [
2726
"0xa5419706d8edb3bbafad83fe2b4e7dc851de5e4cd9529f9f27bb393016c81ae5",
2827
"0x8f255387c5fdb03117d82372b8fa5c7813881fd9a8202b7cc373f1a5868496b2",
2928
"0x89bbdb4e2d7510abf1ca0ed53295e31c00a6939fc12e77d67bf3a4cb3c31f61c"
3029
],
31-
"fetchInterval": 20,
32-
"updateDelay": 60
30+
"fetchInterval": 20
3331
}
3432
]
3533
},
@@ -94,7 +92,7 @@
9492
}
9593
],
9694
"nodeSettings": {
97-
"nodeVersion": "0.4.0",
95+
"nodeVersion": "1.0.0",
9896
"airnodeWalletMnemonic": "${AIRNODE_WALLET_MNEMONIC}",
9997
"stage": "local-example"
10098
}

local-test-configuration/monitoring/index.html

+20-6
Original file line numberDiff line numberDiff line change
@@ -730,10 +730,13 @@ <h2>Active data feeds</h2>
730730
const rpcUrl = urlParams.get('rpcUrl'),
731731
airseekerRegistryAddress = urlParams.get('airseekerRegistryAddress'),
732732
airseekerMnemonic = decodeURIComponent(urlParams.get('airseekerMnemonic')),
733-
walletDerivationScheme = decodeURIComponent(urlParams.get('walletDerivationScheme'));
733+
walletDerivationScheme = decodeURIComponent(urlParams.get('walletDerivationScheme')),
734+
sponsorAddress = decodeURIComponent(urlParams.get('sponsorAddress'));
734735

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

738741
// See: https://github.com/GoogleChromeLabs/jsbi/issues/30#issuecomment-953187833
739742
BigInt.prototype.toJSON = function () {
@@ -876,10 +879,18 @@ <h2>Active data feeds</h2>
876879
//
877880
// For self-funded feeds it's more suitable to derive the hash also from update parameters. This does not apply to
878881
// mananaged feeds which want to be funded by the same wallet independently of the update parameters.
879-
const sponsorAddressHash =
880-
walletDerivationScheme === 'self-funded'
881-
? deriveSponsorAddressHashForSelfFundedFeed(dapiNameOrDataFeedId, updateParameters)
882-
: deriveSponsorAddressHashForManagedFeed(dapiNameOrDataFeedId);
882+
let sponsorAddressHash;
883+
switch (walletDerivationScheme.type) {
884+
case 'self-funded':
885+
sponsorAddressHash = deriveSponsorAddressHashForSelfFundedFeed(dapiNameOrDataFeedId, updateParameters);
886+
break;
887+
case 'managed':
888+
sponsorAddressHash = deriveSponsorAddressHashForManagedFeed(dapiNameOrDataFeedId);
889+
break;
890+
case 'fallback':
891+
sponsorAddressHash = walletDerivationScheme.sponsorAddress;
892+
break;
893+
}
883894

884895
return deriveSponsorWalletFromSponsorAddressHash(sponsorWalletMnemonic, sponsorAddressHash);
885896
};
@@ -938,7 +949,10 @@ <h2>Active data feeds</h2>
938949
airseekerMnemonic,
939950
dapiName ?? dataFeed.dataFeedId,
940951
updateParameters,
941-
walletDerivationScheme
952+
{
953+
type: walletDerivationScheme,
954+
...(sponsorAddress && { sponsorAddress }),
955+
}
942956
);
943957
const dataFeedInfo = {
944958
dapiName: dapiName,

local-test-configuration/scripts/initialize-chain.ts

+75-52
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,20 @@ import { join } from 'node:path';
33

44
import { encode } from '@api3/airnode-abi';
55
import {
6-
type Address,
7-
type Hex,
86
deriveBeaconId,
97
interpolateSecretsIntoConfig,
108
loadConfig,
119
loadSecrets,
10+
type Address,
11+
type Hex,
1212
} from '@api3/commons';
1313
import {
14-
AirseekerRegistry__factory as AirseekerRegistryFactory,
1514
AccessControlRegistry__factory as AccessControlRegistryFactory,
15+
AirseekerRegistry__factory as AirseekerRegistryFactory,
1616
Api3ServerV1__factory as Api3ServerV1Factory,
1717
} from '@api3/contracts';
1818
import dotenv from 'dotenv';
19-
import type { ContractTransactionResponse, Signer } from 'ethers';
20-
import { ethers } from 'ethers';
19+
import { NonceManager, ethers } from 'ethers';
2120
import { zip } from 'lodash';
2221

2322
import {
@@ -59,22 +58,29 @@ export const deriveRole = (adminRole: string, roleDescription: string) => {
5958

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

6768
// Initialize sponsor wallets
68-
for (const beaconSetName of getBeaconSetNames()) {
69+
const beaconSetNames = await getBeaconSetNames();
70+
for (const beaconSetName of beaconSetNames) {
6971
const dapiName = encodeDapiName(beaconSetName);
7072

71-
const sponsorAddressHash = deriveSponsorAddressHashForManagedFeed(dapiName);
73+
const sponsorAddressHash =
74+
rawConfig.walletDerivationScheme.type === 'fallback'
75+
? rawConfig.walletDerivationScheme.sponsorAddress
76+
: deriveSponsorAddressHashForManagedFeed(dapiName);
7277
const sponsorWallet = deriveSponsorWalletFromSponsorAddressHash(
7378
airseekerWalletMnemonic,
7479
sponsorAddressHash
7580
).connect(funderWallet.provider);
76-
const sponsorWalletBalance = await funderWallet.provider!.getBalance(sponsorWallet.address);
77-
console.info('Sponsor wallet balance:', ethers.formatEther(sponsorWalletBalance.toString()));
81+
const sponsorWalletAddress = await sponsorWallet.getAddress();
82+
const sponsorWalletBalance = await funderWallet.provider!.getBalance(sponsorWalletAddress);
83+
console.info('Sponsor wallet balance:', sponsorWalletAddress, ethers.formatEther(sponsorWalletBalance.toString()));
7884

7985
const feeData = await sponsorWallet.provider!.getFeeData();
8086
const { gasPrice } = feeData;
@@ -86,7 +92,7 @@ export const refundFunder = async (funderWallet: ethers.HDNodeWallet) => {
8692
continue;
8793
}
8894
const tx = await sponsorWallet.sendTransaction({
89-
to: funderWallet.address,
95+
to: await funderWallet.getAddress(),
9096
gasPrice,
9197
gasLimit: BigInt(21_000),
9298
value: sponsorWalletBalance - gasFee,
@@ -95,7 +101,7 @@ export const refundFunder = async (funderWallet: ethers.HDNodeWallet) => {
95101

96102
console.info(`Refunding funder wallet from sponsor wallet`, {
97103
dapiName,
98-
sponsorWalletAddress: sponsorWallet.address,
104+
sponsorWalletAddress,
99105
});
100106
}
101107
};
@@ -112,64 +118,80 @@ const loadAirnodeFeedConfig = (airnodeFeedDir: 'airnode-feed-1' | 'airnode-feed-
112118
return interpolateSecretsIntoConfig(rawConfig, rawSecrets);
113119
};
114120

115-
const getBeaconSetNames = () => {
121+
const getBeaconSetNames = async () => {
116122
const airnodeFeed = loadAirnodeFeedConfig('airnode-feed-1');
117123
const airnodeFeedWallet = ethers.Wallet.fromPhrase(airnodeFeed.nodeSettings.airnodeWalletMnemonic);
124+
const airnodeFeedWalletAddress = await airnodeFeedWallet.getAddress();
118125
const airnodeFeedBeacons = Object.values(airnodeFeed.templates).map((template: any) => {
119-
return deriveBeaconData({ ...template, airnodeAddress: airnodeFeedWallet.address });
126+
return deriveBeaconData({ ...template, airnodeAddress: airnodeFeedWalletAddress });
120127
});
121128

122129
return airnodeFeedBeacons.map((beacon) => beacon.parameters[0]!.value);
123130
};
124131

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

130139
// Initialize sponsor wallets
131-
for (const beaconSetName of getBeaconSetNames()) {
140+
const beaconSetNames = await getBeaconSetNames();
141+
for (const beaconSetName of beaconSetNames) {
132142
const dapiName = encodeDapiName(beaconSetName);
133143

134-
const sponsorAddressHash = deriveSponsorAddressHashForManagedFeed(dapiName);
144+
const sponsorAddressHash =
145+
rawConfig.walletDerivationScheme.type === 'fallback'
146+
? rawConfig.walletDerivationScheme.sponsorAddress
147+
: deriveSponsorAddressHashForManagedFeed(dapiName);
135148
const sponsorWallet = deriveSponsorWalletFromSponsorAddressHash(airseekerWalletMnemonic, sponsorAddressHash);
136-
const sponsorWalletBalance = await funderWallet.provider!.getBalance(sponsorWallet.address);
149+
const sponsorWalletAddress = await sponsorWallet.getAddress();
150+
const sponsorWalletBalance = await funderWallet.provider!.getBalance(sponsorWalletAddress);
137151
console.info('Sponsor wallet balance:', ethers.formatEther(sponsorWalletBalance.toString()));
138152

139153
const tx = await funderWallet.sendTransaction({
140-
to: sponsorWallet.address,
141-
value: ethers.parseEther('1'),
154+
to: sponsorWalletAddress,
155+
value: ethers.parseEther('0.1'),
142156
});
143157
await tx.wait();
144158

145159
console.info(`Funding sponsor wallets`, {
146160
dapiName,
147-
sponsorWalletAddress: sponsorWallet.address,
161+
decodedDapiName: ethers.decodeBytes32String(dapiName),
162+
sponsorWalletAddress,
148163
});
149164
}
150165
};
151166

152-
export const deploy = async (funderWallet: ethers.HDNodeWallet, provider: ethers.JsonRpcProvider) => {
167+
export const deploy = async (funderWallet: ethers.NonceManager, provider: ethers.JsonRpcProvider) => {
153168
// NOTE: It is OK if all of these roles are done via the funder wallet.
154-
const deployerAndManager = funderWallet,
155-
randomPerson = funderWallet;
169+
const deployerAndManager = funderWallet;
170+
const deployerAndManagerAddress = await deployerAndManager.getAddress();
171+
172+
const randomPerson = ethers.Wallet.createRandom().connect(deployerAndManager.provider);
173+
const fundRandomPersonTx = await deployerAndManager.sendTransaction({
174+
to: await randomPerson.getAddress(),
175+
value: ethers.parseEther('1'),
176+
});
177+
await fundRandomPersonTx.wait();
156178

157179
// Deploy contracts
158-
const accessControlRegistryFactory = new AccessControlRegistryFactory(deployerAndManager as Signer);
180+
const accessControlRegistryFactory = new AccessControlRegistryFactory(deployerAndManager);
159181
const accessControlRegistry = await accessControlRegistryFactory.deploy();
160182
await accessControlRegistry.waitForDeployment();
161-
const api3ServerV1Factory = new Api3ServerV1Factory(deployerAndManager as Signer);
183+
const api3ServerV1Factory = new Api3ServerV1Factory(deployerAndManager);
162184
const api3ServerV1AdminRoleDescription = 'Api3ServerV1 admin';
163185
const api3ServerV1 = await api3ServerV1Factory.deploy(
164-
accessControlRegistry.getAddress(),
186+
await accessControlRegistry.getAddress(),
165187
api3ServerV1AdminRoleDescription,
166-
deployerAndManager.address
188+
deployerAndManagerAddress
167189
);
168190
await api3ServerV1.waitForDeployment();
169-
const airseekerRegistryFactory = new AirseekerRegistryFactory(deployerAndManager as Signer);
191+
const airseekerRegistryFactory = new AirseekerRegistryFactory(deployerAndManager);
170192
const airseekerRegistry = await airseekerRegistryFactory.deploy(
171-
await (deployerAndManager as Signer).getAddress(),
172-
api3ServerV1.getAddress()
193+
deployerAndManagerAddress,
194+
await api3ServerV1.getAddress()
173195
);
174196
await airseekerRegistry.waitForDeployment();
175197

@@ -179,25 +201,24 @@ export const deploy = async (funderWallet: ethers.HDNodeWallet, provider: ethers
179201
const airnodeFeed1Wallet = ethers.Wallet.fromPhrase(airnodeFeed1.nodeSettings.airnodeWalletMnemonic).connect(
180202
provider
181203
);
182-
const airnodeFeed2Wallet = ethers.Wallet.fromPhrase(airnodeFeed2.nodeSettings.airnodeWalletMnemonic).connect(
183-
provider
184-
);
204+
const airnodeFeed1WalletAddress = await airnodeFeed1Wallet.getAddress();
205+
const airnodeFeed2Wallet = ethers.Wallet.fromPhrase(airnodeFeed2.nodeSettings.airnodeWalletMnemonic, provider);
206+
const airnodeFeed2WalletAddress = await airnodeFeed2Wallet.getAddress();
185207
const airnodeFeed1Beacons = Object.values(airnodeFeed1.templates).map((template: any) => {
186-
return deriveBeaconData({ ...template, airnodeAddress: airnodeFeed1Wallet.address });
208+
return deriveBeaconData({ ...template, airnodeAddress: airnodeFeed1WalletAddress });
187209
});
188210
const airnodeFeed2Beacons = Object.values(airnodeFeed2.templates).map((template: any) => {
189-
return deriveBeaconData({ ...template, airnodeAddress: airnodeFeed2Wallet.address });
211+
return deriveBeaconData({ ...template, airnodeAddress: airnodeFeed2WalletAddress });
190212
});
191213

192214
// Set active dAPIs
193215
const apiTreeValues = [
194-
[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.
195-
[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.
216+
[airnodeFeed1WalletAddress, joinUrl(airnodeFeed1.signedApis[0].url, 'default')], // NOTE: Airnode feed pushes to the "/" of the signed API, but we need to query it additional path.
217+
[airnodeFeed2WalletAddress, joinUrl(airnodeFeed2.signedApis[0].url, 'default')], // NOTE: Airnode feed pushes to the "/" of the signed API, but we need to query it additional path.
196218
] as const;
197-
let tx: ContractTransactionResponse;
198219
for (const [airnode, url] of apiTreeValues) {
199-
tx = await airseekerRegistry.connect(deployerAndManager).setSignedApiUrl(airnode, url);
200-
await tx.wait();
220+
const setSignedApiUrlTx = await airseekerRegistry.connect(deployerAndManager).setSignedApiUrl(airnode, url);
221+
await setSignedApiUrlTx.wait();
201222
}
202223
const dapiInfos = zip(airnodeFeed1Beacons, airnodeFeed2Beacons).map(([airnodeFeed1Beacon, airnodeFeed2Beacon]) => {
203224
return {
@@ -219,17 +240,19 @@ export const deploy = async (funderWallet: ethers.HDNodeWallet, provider: ethers
219240
['address[]', 'bytes32[]'],
220241
[airnodes, templateIds]
221242
);
222-
tx = await airseekerRegistry.connect(randomPerson).registerDataFeed(encodedBeaconSetData);
223-
await tx.wait();
243+
const registerDataFeedTx = await airseekerRegistry.connect(randomPerson).registerDataFeed(encodedBeaconSetData);
244+
await registerDataFeedTx.wait();
224245
const HUNDRED_PERCENT = 1e8;
225246
const deviationThresholdInPercentage = BigInt(HUNDRED_PERCENT / 100); // 1%
226247
const deviationReference = 0n;
227248
const heartbeatInterval = BigInt(86_400); // 24 hrs
228-
tx = await api3ServerV1.connect(deployerAndManager).setDapiName(dapiName, beaconSetId);
229-
await tx.wait();
230-
tx = await airseekerRegistry.connect(deployerAndManager).setDapiNameToBeActivated(dapiName);
231-
await tx.wait();
232-
tx = await airseekerRegistry
249+
const setDapiNameTx = await api3ServerV1.connect(deployerAndManager).setDapiName(dapiName, beaconSetId);
250+
await setDapiNameTx.wait();
251+
const setDapiNameToBeActivatedTx = await airseekerRegistry
252+
.connect(deployerAndManager)
253+
.setDapiNameToBeActivated(dapiName);
254+
await setDapiNameToBeActivatedTx.wait();
255+
const setDapiNameUpdateParametersTx = await airseekerRegistry
233256
.connect(deployerAndManager)
234257
.setDapiNameUpdateParameters(
235258
dapiName,
@@ -238,7 +261,7 @@ export const deploy = async (funderWallet: ethers.HDNodeWallet, provider: ethers
238261
[deviationThresholdInPercentage, deviationReference, heartbeatInterval]
239262
)
240263
);
241-
await tx.wait();
264+
await setDapiNameUpdateParametersTx.wait();
242265
}
243266

244267
return {
@@ -265,10 +288,10 @@ async function main() {
265288
polling: true,
266289
pollingInterval: 100,
267290
});
268-
const funderWallet = ethers.Wallet.fromPhrase(process.env.FUNDER_MNEMONIC).connect(provider);
291+
const funderWallet = new NonceManager(ethers.Wallet.fromPhrase(process.env.FUNDER_MNEMONIC, provider));
269292

270293
await refundFunder(funderWallet);
271-
const balance = await provider.getBalance(funderWallet.address);
294+
const balance = await provider.getBalance(await funderWallet.getAddress());
272295
console.info('Funder balance:', ethers.formatEther(balance.toString()));
273296
console.info();
274297

local-test-configuration/signed-api-1/signed-api.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
],
99
"allowedAirnodes": [{ "address": "0xaC0653E412acAE526Da3a33af0135205A34E21AF", "authTokens": null }],
1010
"stage": "signed-api-1",
11-
"version": "0.4.0"
11+
"version": "1.0.0"
1212
}

local-test-configuration/signed-api-2/signed-api.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
],
99
"allowedAirnodes": [{ "address": "0x2Bf0dddA8Daa1C3C0Fae9e85866807A1C2D601B7", "authTokens": null }],
1010
"stage": "signed-api-2",
11-
"version": "0.4.0"
11+
"version": "1.0.0"
1212
}

0 commit comments

Comments
 (0)