Skip to content

Commit 69ad347

Browse files
authored
feat: add support for any collateral token to fundkey script (#7253)
1 parent a03b02e commit 69ad347

File tree

13 files changed

+682
-77
lines changed

13 files changed

+682
-77
lines changed

.changeset/four-paws-whisper.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperlane-xyz/sdk": minor
3+
---
4+
5+
Implemented the getMetadata method on native token adapters and fixed the populateTransferTx method for SVM token adapters when the receiver does not have a created associated token account

typescript/infra/scripts/funding/fund-wallet-from-deployer-key.ts

Lines changed: 191 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,32 @@
1-
import { ethers } from 'ethers';
2-
import { formatUnits, parseUnits } from 'ethers/lib/utils.js';
1+
import { formatUnits } from 'ethers/lib/utils.js';
32
import { format } from 'util';
43

54
import {
5+
ChainMap,
66
ChainName,
77
CoinGeckoTokenPriceGetter,
8+
ITokenAdapter,
89
MultiProtocolSignerSignerAccountInfo,
10+
PROTOCOL_TO_DEFAULT_PROVIDER_TYPE,
911
ProtocolTypedTransaction,
10-
TOKEN_STANDARD_TO_PROVIDER_TYPE,
1112
Token,
1213
TransferParams,
14+
getCollateralTokenAdapter,
1315
getSignerForChain,
1416
} from '@hyperlane-xyz/sdk';
1517
import {
1618
Address,
1719
ProtocolType,
1820
assert,
1921
rootLogger,
20-
strip0x,
2122
toWei,
2223
} from '@hyperlane-xyz/utils';
2324

2425
import { Contexts } from '../../config/contexts.js';
2526
import { getDeployerKey } from '../../src/agents/key-utils.js';
2627
import { getCoinGeckoApiKey } from '../../src/coingecko/utils.js';
2728
import { EnvironmentConfig } from '../../src/config/environment.js';
29+
import { tokens as knownInfraTokens } from '../../src/config/warp.js';
2830
import { assertChain } from '../../src/utils/utils.js';
2931
import { getAgentConfig, getArgs } from '../agent-utils.js';
3032
import { getEnvironmentConfig } from '../core-utils.js';
@@ -43,6 +45,26 @@ const logger = rootLogger.child({
4345
*/
4446
const MAX_FUNDING_AMOUNT_IN_USD = 1000;
4547

48+
const enum TokenFundingType {
49+
native = 'native',
50+
non_native = 'non_native',
51+
}
52+
53+
type TokenToFundInfo =
54+
| {
55+
type: TokenFundingType.native;
56+
amount: number;
57+
recipientAddress: Address;
58+
tokenDecimals?: number;
59+
}
60+
| {
61+
type: TokenFundingType.non_native;
62+
tokenAddress: Address;
63+
amount: number;
64+
recipientAddress: Address;
65+
tokenDecimals?: number;
66+
};
67+
4668
async function main() {
4769
const argv = await getArgs()
4870
.string('recipient')
@@ -64,30 +86,112 @@ async function main() {
6486
.demandOption('chain')
6587
.coerce('chain', assertChain)
6688

89+
.string('symbol')
90+
.alias('s', 'symbol')
91+
.describe(
92+
'symbol',
93+
'Token symbol for the token to send in this transfer. If the token is not known provide the token address with the --token flag instead',
94+
)
95+
.conflicts('symbol', 'token')
96+
97+
.string('token')
98+
.alias('t', 'token')
99+
.describe(
100+
'token',
101+
'Optional token address for the token that should be funded. The native token will be used if no address is provided',
102+
)
103+
.conflicts('token', 'symbol')
104+
105+
.string('decimals')
106+
.alias('d', 'decimals')
107+
.describe(
108+
'decimals',
109+
'Optional token decimals used to format the amount into its native denomination if the token metadata cannot be derived on chain',
110+
)
111+
67112
.boolean('dry-run')
68113
.describe('dry-run', 'Simulate the transaction without sending')
69114
.default('dry-run', false).argv;
70115

71116
const config = getEnvironmentConfig(argv.environment);
72-
const { recipient, amount, chain, dryRun } = argv;
117+
const { recipient, amount, chain, dryRun, token, decimals, symbol } = argv;
73118

74119
logger.info(
75120
{
76121
recipient,
77122
amount,
78123
chain,
79124
dryRun,
125+
token: token ?? 'native token',
80126
},
81127
'Starting funding operation',
82128
);
83129

130+
assert(chain, 'Chain is required');
131+
132+
let tokenToFundInfo: TokenToFundInfo;
133+
if (symbol) {
134+
const registry = await config.getRegistry();
135+
136+
const warpRoutes = await registry.getWarpRoutes();
137+
const knownTokenAddresses: ChainMap<Record<string, string>> = {};
138+
Object.values(warpRoutes).forEach(({ tokens }) =>
139+
tokens.forEach((tokenConfig) => {
140+
if (!tokenConfig.collateralAddressOrDenom) {
141+
return;
142+
}
143+
144+
const knownTokensForCurrentChain =
145+
(knownInfraTokens as Record<ChainName, Record<string, string>>)[
146+
tokenConfig.chainName
147+
] ?? {};
148+
149+
knownTokenAddresses[tokenConfig.chainName] ??= {};
150+
knownTokenAddresses[tokenConfig.chainName][
151+
tokenConfig.symbol.toLowerCase()
152+
] =
153+
// Default to the address in the infra mapping if one exists
154+
knownTokensForCurrentChain[tokenConfig.symbol.toLowerCase()] ??
155+
tokenConfig.collateralAddressOrDenom;
156+
}),
157+
);
158+
159+
const tokenAddress = knownTokenAddresses[chain]?.[symbol.toLowerCase()];
160+
assert(
161+
tokenAddress,
162+
`An address was not found for token with symbol "${symbol}" on chain "${chain}". Please provide the token address instead`,
163+
);
164+
165+
tokenToFundInfo = {
166+
amount: parseFloat(amount),
167+
recipientAddress: recipient,
168+
tokenAddress,
169+
type: TokenFundingType.non_native,
170+
tokenDecimals: decimals ? parseInt(decimals) : undefined,
171+
};
172+
} else if (token) {
173+
tokenToFundInfo = {
174+
type: TokenFundingType.non_native,
175+
amount: parseFloat(amount),
176+
recipientAddress: recipient,
177+
tokenAddress: token,
178+
tokenDecimals: decimals ? parseInt(decimals) : undefined,
179+
};
180+
} else {
181+
tokenToFundInfo = {
182+
type: TokenFundingType.native,
183+
amount: parseFloat(amount),
184+
recipientAddress: recipient,
185+
tokenDecimals: decimals ? parseInt(decimals) : undefined,
186+
};
187+
}
188+
84189
try {
85190
await fundAccount({
86191
config,
87-
chainName: chain!,
88-
recipientAddress: recipient,
89-
amount,
192+
chainName: chain,
90193
dryRun,
194+
fundInfo: tokenToFundInfo,
91195
});
92196

93197
logger.info('Funding operation completed successfully');
@@ -108,18 +212,18 @@ async function main() {
108212
interface FundingParams {
109213
config: EnvironmentConfig;
110214
chainName: ChainName;
111-
recipientAddress: Address;
112-
amount: string;
215+
fundInfo: TokenToFundInfo;
113216
dryRun: boolean;
114217
}
115218

116219
async function fundAccount({
117220
config,
118221
chainName,
119-
recipientAddress,
120-
amount,
121222
dryRun,
223+
fundInfo,
122224
}: FundingParams): Promise<void> {
225+
const { amount, recipientAddress, tokenDecimals } = fundInfo;
226+
123227
const multiProtocolProvider = await config.getMultiProtocolProvider();
124228

125229
const chainMetadata = multiProtocolProvider.getChainMetadata(chainName);
@@ -128,6 +232,7 @@ async function fundAccount({
128232
const fundingLogger = logger.child({
129233
chainName,
130234
protocol,
235+
type: fundInfo.type,
131236
});
132237

133238
const tokenPriceGetter = new CoinGeckoTokenPriceGetter({
@@ -137,29 +242,78 @@ async function fundAccount({
137242

138243
let tokenPrice;
139244
try {
140-
tokenPrice = await tokenPriceGetter.getTokenPrice(chainName);
245+
if (fundInfo.type === TokenFundingType.non_native) {
246+
tokenPrice = await tokenPriceGetter.fetchPriceDataByContractAddress(
247+
chainName,
248+
fundInfo.tokenAddress,
249+
);
250+
} else {
251+
tokenPrice = await tokenPriceGetter.getTokenPrice(chainName);
252+
}
141253
} catch (err) {
142254
fundingLogger.error(
143-
{ chainName, err },
144-
`Failed to get native token price for ${chainName}, falling back to 1usd`,
255+
{ err },
256+
`Failed to get token price for ${chainName}, falling back to 1usd`,
145257
);
146258
tokenPrice = 1;
147259
}
148-
const fundingAmountInUsd = parseFloat(amount) * tokenPrice;
260+
const fundingAmountInUsd = amount * tokenPrice;
149261

150262
if (fundingAmountInUsd > MAX_FUNDING_AMOUNT_IN_USD) {
151263
throw new Error(
152264
`Funding amount in USD exceeds max funding amount. Max: ${MAX_FUNDING_AMOUNT_IN_USD}. Got: ${fundingAmountInUsd}`,
153265
);
154266
}
155267

156-
// Create token instance
157-
const token = Token.FromChainMetadataNativeToken(chainMetadata);
158-
const adapter = token.getAdapter(multiProtocolProvider);
268+
// Create adapter instance
269+
let adapter: ITokenAdapter<unknown>;
270+
if (fundInfo.type === TokenFundingType.non_native) {
271+
adapter = getCollateralTokenAdapter({
272+
chainName,
273+
multiProvider: multiProtocolProvider,
274+
tokenAddress: fundInfo.tokenAddress,
275+
});
276+
} else {
277+
const tokenInstance = Token.FromChainMetadataNativeToken(chainMetadata);
278+
adapter = tokenInstance.getAdapter(multiProtocolProvider);
279+
}
159280

160-
// Get signer
161-
fundingLogger.info('Retrieved signer info');
281+
let tokenMetadata: {
282+
name: string;
283+
symbol: string;
284+
decimals: number;
285+
};
286+
try {
287+
const { name, symbol, decimals } = await adapter.getMetadata();
288+
assert(
289+
decimals,
290+
`Expected decimals for ${fundInfo.type} token funding of ${fundInfo.type === TokenFundingType.non_native ? fundInfo.tokenAddress : ''} on chain "${chainName}" to be defined`,
291+
);
162292

293+
tokenMetadata = {
294+
name,
295+
symbol,
296+
decimals,
297+
};
298+
} catch (err) {
299+
fundingLogger.error(
300+
{ err },
301+
`Failed to get token metadata for ${chainName}`,
302+
);
303+
304+
assert(
305+
tokenDecimals,
306+
`tokenDecimals is required as the token metadata can't be derived on chain`,
307+
);
308+
309+
tokenMetadata = {
310+
name: 'NAME NOT SPECIFIED',
311+
symbol: 'SYMBOL NOT SPECIFIED',
312+
decimals: tokenDecimals,
313+
};
314+
}
315+
316+
// Get signer
163317
const agentConfig = getAgentConfig(Contexts.Hyperlane, config.environment);
164318
const privateKeyAgent = getDeployerKey(agentConfig, chainName);
165319

@@ -193,11 +347,6 @@ async function fundAccount({
193347
multiProtocolProvider,
194348
);
195349

196-
fundingLogger.info(
197-
{ chainName, protocol },
198-
'Performing pre transaction checks',
199-
);
200-
201350
// Check balance before transfer
202351
const fromAddress = await signer.address();
203352
const currentBalance = await adapter.getBalance(fromAddress);
@@ -206,13 +355,13 @@ async function fundAccount({
206355
{
207356
fromAddress,
208357
currentBalance: currentBalance.toString(),
209-
symbol: token.symbol,
358+
symbol: tokenMetadata.symbol,
210359
},
211-
'Current sender balance',
360+
'Retrieved signer balance info',
212361
);
213362

214363
// Convert amount to wei/smallest unit
215-
const decimals = token.decimals;
364+
const decimals = tokenMetadata.decimals;
216365
const weiAmount = BigInt(toWei(amount, decimals));
217366

218367
fundingLogger.info(
@@ -227,7 +376,7 @@ async function fundAccount({
227376
// Check if we have sufficient balance
228377
if (currentBalance < weiAmount) {
229378
throw new Error(
230-
`Insufficient balance. Have: ${formatUnits(currentBalance, decimals)} ${token.symbol}, Need: ${amount} ${token.symbol}`,
379+
`Insufficient balance. Have: ${formatUnits(currentBalance.toString(), decimals)} ${tokenMetadata.symbol}, Need: ${amount} ${tokenMetadata.symbol}`,
231380
);
232381
}
233382

@@ -238,22 +387,22 @@ async function fundAccount({
238387
fromAccountOwner: fromAddress,
239388
};
240389

241-
fundingLogger.info(
242-
{
243-
transferParams,
244-
dryRun,
245-
},
246-
'Preparing transfer transaction',
247-
);
248-
249390
// Execute the transfer
250391
const transferTx = await adapter.populateTransferTx(transferParams);
251392

252393
const protocolTypedTx = {
253394
transaction: transferTx,
254-
type: TOKEN_STANDARD_TO_PROVIDER_TYPE[token.standard],
395+
type: PROTOCOL_TO_DEFAULT_PROVIDER_TYPE[protocol],
255396
} as ProtocolTypedTransaction<typeof protocol>;
256397

398+
fundingLogger.info(
399+
{
400+
transferParams,
401+
dryRun,
402+
},
403+
'Prepared transfer transaction data',
404+
);
405+
257406
if (dryRun) {
258407
fundingLogger.info('DRY RUN: Would execute transfer with above parameters');
259408
return;
@@ -272,9 +421,9 @@ async function fundAccount({
272421
fundingLogger.info(
273422
{
274423
transactionHash,
275-
senderNewBalance: formatUnits(newBalance, decimals),
276-
recipientBalance: formatUnits(recipientBalance, decimals),
277-
symbol: token.symbol,
424+
senderNewBalance: formatUnits(newBalance.toString(), decimals),
425+
recipientBalance: formatUnits(recipientBalance.toString(), decimals),
426+
symbol: tokenMetadata.symbol,
278427
},
279428
'Transfer completed successfully',
280429
);

0 commit comments

Comments
 (0)