@@ -50,10 +50,16 @@ export interface SwidgeCLIResult {
5050
5151// --- Constants ---
5252
53- const EVM_CHAINS : Record < string , { name : string ; rpc : string } > = {
54- "eip155:1" : { name : "Ethereum" , rpc : "https://eth.drpc.org" } ,
55- "eip155:8453" : { name : "Base" , rpc : "https://mainnet.base.org" } ,
56- "eip155:10" : { name : "Optimism" , rpc : "https://mainnet.optimism.io" } ,
53+ // gasCost: relative gas cost ranking (lower = cheaper, used to prefer cheap chains for refuel)
54+ const EVM_CHAINS : Record < string , { name : string ; rpc : string ; gasToken ?: string ; gasCost : number } > = {
55+ "eip155:42161" : { name : "Arbitrum One" , rpc : "https://arb1.arbitrum.io/rpc" , gasCost : 1 } ,
56+ "eip155:8453" : { name : "Base" , rpc : "https://mainnet.base.org" , gasCost : 1 } ,
57+ "eip155:10" : { name : "Optimism" , rpc : "https://mainnet.optimism.io" , gasCost : 1 } ,
58+ "eip155:137" : { name : "Polygon" , rpc : "https://1rpc.io/matic" , gasToken : "POL" , gasCost : 1 } ,
59+ "eip155:56" : { name : "BNB Chain" , rpc : "https://bsc-dataseed.binance.org" , gasCost : 2 } ,
60+ "eip155:43114" : { name : "Avalanche" , rpc : "https://api.avax.network/ext/bc/C/rpc" , gasCost : 2 } ,
61+ "eip155:999" : { name : "Hyperliquid" , rpc : "https://rpc.hyperliquid.xyz/evm" , gasToken : "HYPE" , gasCost : 1 } ,
62+ "eip155:1" : { name : "Ethereum" , rpc : "https://eth.drpc.org" , gasCost : 10 } ,
5763} ;
5864
5965const LIFI_API = "https://li.quest/v1" ;
@@ -78,7 +84,7 @@ function chainName(caip2: string): string {
7884 return EVM_CHAINS [ caip2 ] ?. name || caip2 ;
7985}
8086
81- function rpcUrl ( caip2 : string ) : string | undefined {
87+ export function rpcUrl ( caip2 : string ) : string | undefined {
8288 return EVM_CHAINS [ caip2 ] ?. rpc ;
8389}
8490
@@ -145,9 +151,9 @@ async function getTokenBalanceRpc(
145151 return BigInt ( result ) ;
146152}
147153
148- interface TxReceipt { status : string ; transactionHash : string ; blockNumber : string }
154+ export interface TxReceipt { status : string ; transactionHash : string ; blockNumber : string ; gasUsed : string }
149155
150- async function waitForReceipt (
156+ export async function waitForReceipt (
151157 chainId : string , txHash : string , timeoutMs = 120_000 ,
152158) : Promise < TxReceipt > {
153159 const url = rpcUrl ( chainId ) ;
@@ -215,6 +221,7 @@ export async function swidgeViaWalletConnect(
215221 sdk : WalletConnectCLI ,
216222 address : string ,
217223 options : SwidgeCLIOptions ,
224+ _isRefuel = false ,
218225) : Promise < SwidgeCLIResult > {
219226 const fromChainId = parseChainId ( options . fromChain ) ;
220227 const toChainId = parseChainId ( options . toChain ) ;
@@ -259,6 +266,12 @@ export async function swidgeViaWalletConnect(
259266 }
260267 }
261268
269+ // Refuel check — ensure destination chain has gas for subsequent transactions
270+ // Skip if this is already a refuel operation (explicit flag prevents recursion)
271+ if ( options . fromChain !== options . toChain && ! _isRefuel ) {
272+ await refuelIfNeeded ( sdk , address , options . fromChain , options . toChain , options . fromToken ) ;
273+ }
274+
262275 // ERC-20 approval if needed
263276 if ( ! isNativeToken ( fromTokenAddr ) && quote . estimate . approvalAddress ) {
264277 const allowance = await getAllowanceRpc (
@@ -344,9 +357,110 @@ export async function swidgeViaWalletConnect(
344357 } ;
345358}
346359
360+ // --- Destination gas refuel ---
361+
362+ /** Minimum gas balance threshold (in wei). Below this, we refuel. */
363+ const MIN_GAS_WEI = 10n ** 15n ; // 0.001 ETH/native token
364+
365+ /** Approximate ~$1 refuel amount by token symbol. Avoids decimal-based guessing. */
366+ const REFUEL_AMOUNTS : Record < string , string > = {
367+ usdc : "1" , usdt : "1" , dai : "1" , busd : "1" , // stablecoins: $1 = 1 token
368+ wbtc : "0.00002" , // ~$1 at $50k/BTC
369+ // Everything else (ETH, POL, AVAX, HYPE, etc.) uses default "0.001" below
370+ } ;
371+
372+ function getRefuelAmount ( token : string ) : string {
373+ return REFUEL_AMOUNTS [ token . toLowerCase ( ) ] ?? "0.001" ;
374+ }
375+
347376/**
348- * Check if a send-transaction has insufficient ETH and offer to bridge.
349- * In TTY mode: prompts user. In pipe mode: auto-bridges.
377+ * Check if the destination chain has enough native gas token.
378+ * If not, bridge a small amount of the user's fromToken (e.g. USDC) → dest gas token.
379+ * This avoids requiring native gas on the source chain.
380+ *
381+ * Note: In pipe mode (non-TTY), auto-refuel proceeds without prompting.
382+ * This is safe for WalletConnect because the user's wallet always prompts
383+ * for manual approval of each transaction.
384+ *
385+ * @param fromToken - the token the user is already bridging (e.g. "USDC")
386+ */
387+ async function refuelIfNeeded (
388+ sdk : WalletConnectCLI ,
389+ address : string ,
390+ fromChain : string ,
391+ toChain : string ,
392+ fromToken ?: string ,
393+ ) : Promise < void > {
394+ const destRpc = rpcUrl ( toChain ) ;
395+ if ( ! destRpc ) return ; // can't check, skip
396+
397+ let destBalance : bigint ;
398+ try {
399+ destBalance = await getBalanceRpc ( toChain , address ) ;
400+ } catch {
401+ return ; // RPC failed, skip
402+ }
403+
404+ if ( destBalance >= MIN_GAS_WEI ) return ; // has enough gas
405+
406+ const destChain = EVM_CHAINS [ toChain ] ;
407+ const destGasToken = destChain ?. gasToken || "ETH" ;
408+
409+ // Use the same token the user is bridging (e.g. USDC) as refuel source,
410+ // so we don't need native gas on the source chain. Fall back to source gas token.
411+ const refuelFromToken = fromToken || EVM_CHAINS [ fromChain ] ?. gasToken || "ETH" ;
412+ const refuelAmount = getRefuelAmount ( refuelFromToken ) ;
413+
414+ process . stderr . write (
415+ `\n ⛽ No ${ destGasToken } on ${ chainName ( toChain ) } for gas.\n` ,
416+ ) ;
417+
418+ // TTY: prompt; pipe: auto-refuel (safe — WalletConnect always requires wallet approval)
419+ if ( process . stdin . isTTY ) {
420+ const readline = await import ( "node:readline/promises" ) ;
421+ const rl = readline . createInterface ( { input : process . stdin , output : process . stderr } ) ;
422+ const answer = await rl . question (
423+ ` Bridge ~${ refuelAmount } ${ refuelFromToken } from ${ chainName ( fromChain ) } → ${ destGasToken } for gas? (y/n) ` ,
424+ ) ;
425+ rl . close ( ) ;
426+ if ( answer . trim ( ) . toLowerCase ( ) !== "y" ) {
427+ process . stderr . write ( ` Skipping refuel — transactions on ${ chainName ( toChain ) } may fail without gas.\n\n` ) ;
428+ return ;
429+ }
430+ } else {
431+ process . stderr . write (
432+ ` Auto-bridging ~${ refuelAmount } ${ refuelFromToken } from ${ chainName ( fromChain ) } → ${ destGasToken } for gas...\n` ,
433+ ) ;
434+ }
435+
436+ try {
437+ await swidgeViaWalletConnect ( sdk , address , {
438+ fromChain,
439+ toChain,
440+ fromToken : refuelFromToken ,
441+ toToken : destGasToken ,
442+ amount : refuelAmount ,
443+ } , true ) ; // _isRefuel=true prevents recursion
444+
445+ // Wait for gas to arrive
446+ process . stderr . write ( ` Waiting for gas to arrive...` ) ;
447+ const arrived = await waitForBalance ( toChain , address , destBalance , 120_000 ) ;
448+ if ( arrived ) {
449+ process . stderr . write ( ` done.\n\n` ) ;
450+ } else {
451+ process . stderr . write ( ` timed out. Proceeding anyway.\n\n` ) ;
452+ }
453+ } catch ( err ) {
454+ process . stderr . write (
455+ `\n Refuel failed: ${ err instanceof Error ? err . message : String ( err ) } \n` +
456+ ` Proceeding — you may need to manually bridge gas to ${ chainName ( toChain ) } .\n\n` ,
457+ ) ;
458+ }
459+ }
460+
461+ /**
462+ * Check if a send-transaction has insufficient gas token and offer to bridge.
463+ * In TTY mode: prompts user. In pipe mode: auto-bridges (safe — wallet always approves).
350464 * Returns the bridge result if bridging occurred, null otherwise.
351465 */
352466export async function trySwidgeBeforeSend (
@@ -357,6 +471,8 @@ export async function trySwidgeBeforeSend(
357471) : Promise < SwidgeCLIResult | null > {
358472 if ( ! rpcUrl ( chainId ) || ! txValue ) return null ;
359473
474+ const gasToken = EVM_CHAINS [ chainId ] ?. gasToken || "ETH" ;
475+
360476 let balance : bigint ;
361477 let value : bigint ;
362478 try {
@@ -370,56 +486,60 @@ export async function trySwidgeBeforeSend(
370486 // Add 10% buffer for gas costs on the destination tx
371487 const deficit = ( value - balance ) * 11n / 10n ;
372488
373- // Find a source chain with funds (collect then reduce to avoid race)
374- const otherChains = Object . keys ( EVM_CHAINS ) . filter ( ( c ) => c !== chainId ) ;
489+ // Find a source chain with funds — prefer cheapest gas chains first
490+ const otherChains = Object . keys ( EVM_CHAINS )
491+ . filter ( ( c ) => c !== chainId )
492+ . sort ( ( a , b ) => ( EVM_CHAINS [ a ] . gasCost - EVM_CHAINS [ b ] . gasCost ) ) ;
375493 const balances = await Promise . all (
376494 otherChains . map ( async ( chain ) => {
377495 try {
378- return { chain, balance : await getBalanceRpc ( chain , address ) } ;
496+ return { chain, balance : await getBalanceRpc ( chain , address ) , gasCost : EVM_CHAINS [ chain ] . gasCost } ;
379497 } catch {
380- return { chain, balance : 0n } ;
498+ return { chain, balance : 0n , gasCost : EVM_CHAINS [ chain ] . gasCost } ;
381499 }
382500 } ) ,
383501 ) ;
384- const best = balances . reduce (
385- ( a , b ) => ( b . balance > a . balance ? b : a ) ,
386- { chain : "" , balance : 0n } ,
387- ) ;
502+ // Pick cheapest chain that has sufficient balance; fall back to richest
503+ const sufficient = balances . filter ( ( b ) => b . balance >= deficit ) ;
504+ const best = sufficient . length > 0
505+ ? sufficient . sort ( ( a , b ) => a . gasCost - b . gasCost ) [ 0 ]
506+ : balances . reduce ( ( a , b ) => ( b . balance > a . balance ? b : a ) , { chain : "" , balance : 0n , gasCost : 99 } ) ;
388507 const sourceChain = best . balance > 0n ? best . chain : null ;
389508
390509 if ( ! sourceChain ) {
391510 process . stderr . write (
392- `\nWarning: Insufficient ETH on ${ chainName ( chainId ) } and no funds found on other chains.\n` +
393- ` Consider: walletconnect swidge --from-chain <chain> --to-chain ${ chainId } --from-token ETH --to-token ETH --amount <needed>\n\n` ,
511+ `\nWarning: Insufficient ${ gasToken } on ${ chainName ( chainId ) } and no funds found on other chains.\n` +
512+ ` Consider: walletconnect swidge --from-chain <chain> --to-chain ${ chainId } --from-token ${ gasToken } --to-token ${ gasToken } --amount <needed>\n\n` ,
394513 ) ;
395514 return null ;
396515 }
397516
398517 const deficitFormatted = formatAmount ( deficit , 18 ) ;
518+ const sourceGasToken = EVM_CHAINS [ sourceChain ] ?. gasToken || "ETH" ;
399519
400- // TTY: prompt; pipe: auto-bridge
520+ // TTY: prompt; pipe: auto-bridge (safe — wallet always approves)
401521 if ( process . stdin . isTTY ) {
402522 const readline = await import ( "node:readline/promises" ) ;
403523 const rl = readline . createInterface ( { input : process . stdin , output : process . stderr } ) ;
404524 process . stderr . write (
405- `\nInsufficient ETH on ${ chainName ( chainId ) } .\n` +
406- ` Bridge ~${ deficitFormatted } ETH from ${ chainName ( sourceChain ) } ?\n` ,
525+ `\nInsufficient ${ gasToken } on ${ chainName ( chainId ) } .\n` +
526+ ` Bridge ~${ deficitFormatted } ${ sourceGasToken } from ${ chainName ( sourceChain ) } ?\n` ,
407527 ) ;
408528 const answer = await rl . question ( " Proceed? (y/n) " ) ;
409529 rl . close ( ) ;
410530 if ( answer . trim ( ) . toLowerCase ( ) !== "y" ) return null ;
411531 } else {
412532 process . stderr . write (
413- `Auto-bridging ~${ deficitFormatted } ETH from ${ chainName ( sourceChain ) } to ${ chainName ( chainId ) } ...\n` ,
533+ `Auto-bridging ~${ deficitFormatted } ${ sourceGasToken } from ${ chainName ( sourceChain ) } to ${ chainName ( chainId ) } ...\n` ,
414534 ) ;
415535 }
416536
417537 try {
418538 const result = await swidgeViaWalletConnect ( sdk , address , {
419539 fromChain : sourceChain ,
420540 toChain : chainId ,
421- fromToken : "ETH" ,
422- toToken : "ETH" ,
541+ fromToken : sourceGasToken ,
542+ toToken : gasToken ,
423543 amount : deficitFormatted ,
424544 } ) ;
425545
0 commit comments