1- import { ethers } from 'ethers' ;
2- import { formatUnits , parseUnits } from 'ethers/lib/utils.js' ;
1+ import { formatUnits } from 'ethers/lib/utils.js' ;
32import { format } from 'util' ;
43
54import {
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' ;
1517import {
1618 Address ,
1719 ProtocolType ,
1820 assert ,
1921 rootLogger ,
20- strip0x ,
2122 toWei ,
2223} from '@hyperlane-xyz/utils' ;
2324
2425import { Contexts } from '../../config/contexts.js' ;
2526import { getDeployerKey } from '../../src/agents/key-utils.js' ;
2627import { getCoinGeckoApiKey } from '../../src/coingecko/utils.js' ;
2728import { EnvironmentConfig } from '../../src/config/environment.js' ;
29+ import { tokens as knownInfraTokens } from '../../src/config/warp.js' ;
2830import { assertChain } from '../../src/utils/utils.js' ;
2931import { getAgentConfig , getArgs } from '../agent-utils.js' ;
3032import { getEnvironmentConfig } from '../core-utils.js' ;
@@ -43,6 +45,26 @@ const logger = rootLogger.child({
4345 */
4446const 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+
4668async 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() {
108212interface FundingParams {
109213 config : EnvironmentConfig ;
110214 chainName : ChainName ;
111- recipientAddress : Address ;
112- amount : string ;
215+ fundInfo : TokenToFundInfo ;
113216 dryRun : boolean ;
114217}
115218
116219async 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