-
Notifications
You must be signed in to change notification settings - Fork 3
/
wallets.ts
152 lines (129 loc) · 5.38 KB
/
wallets.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import { ethers } from 'ethers';
import { uniq } from 'lodash';
import { go, goSync } from '@api3/promise-utils';
import * as node from '@api3/airnode-node';
import * as protocol from '@api3/airnode-protocol';
import { getState, updateState, SponsorWalletsPrivateKey, Provider } from './state';
import { shortenAddress } from './utils';
import { logger } from './logging';
import { DataFeedUpdates } from './validation';
import { RateLimitedProvider } from './providers';
export type ChainSponsorGroup = {
chainId: string;
sponsorAddress: string;
providers: Provider[];
};
export type SponsorBalanceStatus = {
chainId: string;
sponsorAddress: string;
isEmpty: boolean;
};
export const initializeAirseekerWallet = () => {
const { config } = getState();
// Derive airseeker wallet
const airseekerWalletPrivateKey = ethers.Wallet.fromMnemonic(config.airseekerWalletMnemonic).privateKey;
updateState((state) => ({ ...state, airseekerWalletPrivateKey }));
};
export const initializeSponsorWallets = () => {
const { config } = getState();
// Derive sponsor wallets
const groupSponsorsByChain = Object.values(config.triggers.dataFeedUpdates);
const uniqueSponsors = uniq(groupSponsorsByChain.flatMap(Object.keys));
const sponsorWalletsPrivateKey: SponsorWalletsPrivateKey = Object.fromEntries(
uniqueSponsors.map((sponsorAddress) => [
sponsorAddress,
node.evm.deriveSponsorWalletFromMnemonic(
config.airseekerWalletMnemonic,
sponsorAddress,
protocol.PROTOCOL_IDS.AIRSEEKER
).privateKey,
])
);
updateState((state) => ({ ...state, sponsorWalletsPrivateKey }));
};
export const retrieveSponsorWalletAddress = (sponsorAddress: string): string => {
const { sponsorWalletsPrivateKey } = getState();
if (!sponsorWalletsPrivateKey || !sponsorWalletsPrivateKey[sponsorAddress])
throw new Error(`Pre-generated private key not found for sponsor ${sponsorAddress}`);
return new ethers.Wallet(sponsorWalletsPrivateKey[sponsorAddress]).address;
};
export const isBalanceZero = async (
rpcProvider: RateLimitedProvider,
sponsorWalletAddress: string
): Promise<boolean> => {
const goResult = await go(() => rpcProvider.getBalance(sponsorWalletAddress), { retries: 1 });
if (!goResult.success) {
throw new Error(goResult.error.message);
}
return goResult.data.isZero();
};
export const getSponsorBalanceStatus = async (
chainSponsorGroup: ChainSponsorGroup
): Promise<SponsorBalanceStatus | null> => {
const { chainId, sponsorAddress, providers } = chainSponsorGroup;
const logOptions = { meta: { 'Chain-ID': chainId, Sponsor: shortenAddress(sponsorAddress) } };
const goResult = goSync(() => retrieveSponsorWalletAddress(sponsorAddress));
if (!goResult.success) {
const message = `Failed to retrieve wallet address for sponsor ${sponsorAddress}. Skipping. Error: ${goResult.error.message}`;
logger.warn(message, logOptions);
return null;
}
const sponsorWalletAddress = goResult.data;
const balanceProviders = providers.map(async ({ rpcProvider }) => isBalanceZero(rpcProvider, sponsorWalletAddress));
const goAnyResult = await go(() => Promise.any(balanceProviders));
if (!goAnyResult.success) {
const message = `Failed to get balance for ${sponsorWalletAddress}. No provider was resolved. Error: ${goAnyResult.error.message}`;
logger.warn(message, logOptions);
return null;
}
const isEmpty = goAnyResult.data;
return { sponsorAddress, chainId, isEmpty };
};
export const filterEmptySponsors = async () => {
const { config, providers: stateProviders, sponsorWalletsPrivateKey } = getState();
const chainSponsorGroups = Object.entries(config.triggers.dataFeedUpdates).reduce(
(acc: ChainSponsorGroup[], [chainId, dataFeedUpdatesPerSponsor]) => {
const providers = stateProviders[chainId];
const providersSponsorGroups = Object.keys(dataFeedUpdatesPerSponsor).map((sponsorAddress) => {
return {
chainId,
providers,
sponsorAddress,
};
});
return [...acc, ...providersSponsorGroups];
},
[]
);
const balanceGroupsOrNull = await Promise.all(chainSponsorGroups.map(getSponsorBalanceStatus));
const balanceGroups = balanceGroupsOrNull.filter((group): group is SponsorBalanceStatus => group !== null);
// Update dataFeedUpdates with non-empty sponsor wallets
const fundedBalanceGroups = balanceGroups.filter(({ isEmpty }) => isEmpty === false);
const fundedDataFeedUpdates = fundedBalanceGroups.reduce((acc: DataFeedUpdates, { chainId, sponsorAddress }) => {
return {
...acc,
[chainId]: { ...acc[chainId], [sponsorAddress]: config.triggers.dataFeedUpdates[chainId][sponsorAddress] },
};
}, {});
const fundedSponsorWalletsPrivateKey = fundedBalanceGroups.reduce(
(acc: SponsorWalletsPrivateKey, { sponsorAddress }) => {
return {
...acc,
[sponsorAddress]: sponsorWalletsPrivateKey[sponsorAddress],
};
},
{}
);
updateState((state) => ({
...state,
config: { ...config, triggers: { ['dataFeedUpdates']: fundedDataFeedUpdates } },
sponsorWalletsPrivateKey: fundedSponsorWalletsPrivateKey,
}));
logger.info(
`Fetched balances for ${balanceGroups.length}/${balanceGroupsOrNull.length} sponsor wallets. Continuing with ${fundedBalanceGroups.length} funded sponsors.`
);
};
export const initializeWallets = () => {
initializeAirseekerWallet();
initializeSponsorWallets();
};