本指南说明了在使用 Skip Go Client Library 构建自定义前端时,如何实现 Gas on Receive 功能。Gas on Receive 可帮助用户在跨链交换期间,自动在目标链上获得原生 gas 代币。

概览

Gas on Receive 会自动在目标链上提供少量原生 gas 代币,从而防止用户拿到无法使用的资产后“卡住”。 客户端库提供了 getRouteWithGasOnReceive 函数来替你处理所有复杂性;如果你需要特定行为,也可以自行实现自定义逻辑。

前置条件

  • Skip Go Client Library v1.5.0 或更高版本
  • 理解基础的 route 和 executeRoute 函数
  • 能够访问用户在多条链上的钱包 signer

支持的链

  • 目标链:Cosmos 链和 EVM L2(不支持 Solana)
  • 源链:除 Ethereum 主网和 Sepolia 测试网外的大多数链

快速开始:使用 getRouteWithGasOnReceive

实现 Gas on Receive 最简单的方式,是使用内置的 getRouteWithGasOnReceive 函数:
import { 
  route, 
  getRouteWithGasOnReceive, 
  executeMultipleRoutes,
  executeRoute,
  type RouteRequest,
  type RouteResponse,
  type UserAddress
} from "@skip-go/client";

async function swapWithGasOnReceive() {
  try {
    // Step 1: Define your route request
    const routeRequest = {
      amountIn: "1000000", // 1 OSMO
      sourceAssetChainId: "osmosis-1",
      sourceAssetDenom: "uosmo",
      destAssetChainId: "42161", // Arbitrum
      destAssetDenom: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", // WETH
      smartRelay: true
    };

    // Step 2: Get your initial route
    const originalRoute = await route(routeRequest);

    // Step 3: Automatically split into main and gas routes
    const { mainRoute, gasRoute } = await getRouteWithGasOnReceive({
      routeResponse: originalRoute,
      routeRequest
    });

    // Step 4: Get user addresses based on required chains
    // Note: mainRoute and gasRoute may require different chains
    const mainRouteAddresses = mainRoute.requiredChainAddresses.map(chainId => ({
      chainId,
      address: getUserAddressForChain(chainId) // Your function to get user's address
    }));
    
    const gasRouteAddresses = gasRoute?.requiredChainAddresses.map(chainId => ({
      chainId, 
      address: getUserAddressForChain(chainId)
    }));
    
    // Example helper function:
    // function getUserAddressForChain(chainId: string): string {
    //   const addresses = {
    //     "osmosis-1": "osmo1...",
    //     "42161": "0x..."
    //   };
    //   return addresses[chainId] || throw new Error(`No address for chain ${chainId}`);
    // }

  // Step 5: Execute routes
  if (gasRoute && gasRouteAddresses) {
    // Execute both routes together
    await executeMultipleRoutes({
      route: { mainRoute, feeRoute: gasRoute },
      userAddresses: {
        mainRoute: mainRouteAddresses,
        feeRoute: gasRouteAddresses // May be different chains than mainRoute
      },
      slippageTolerancePercent: {
        mainRoute: "1",
        feeRoute: "10" // Higher tolerance for gas route
      },
      getCosmosSigningClient: async (chainId) => {
        return yourCosmosWallet.getSigningClient(chainId);
      },
      getEVMSigningClient: async (chainId) => {
        return yourEvmWallet.getSigningClient(chainId);
      },
      onRouteStatusUpdated: (status) => {
        console.log("Route status:", status);
        
        // Check if gas route failed
        const gasRouteFailed = status.relatedRoutes?.find(
          r => r.routeKey === "feeRoute" && r.status === "failed"
        );
        
        if (gasRouteFailed) {
          console.warn("Gas route failed, but main swap continues");
        }
      }
    });
  } else {
    // No gas route needed, execute original route
    await executeRoute({
      route: originalRoute,
      userAddresses: mainRouteAddresses,
      slippageTolerancePercent: "1",
      getCosmosSigningClient: async (chainId) => {
        return yourCosmosWallet.getSigningClient(chainId);
      },
      getEVMSigningClient: async (chainId) => {
        return yourEvmWallet.getSigningClient(chainId);
      }
    });
  } catch (error) {
    console.error("Failed to execute swap:", error);
    // Handle error appropriately
  }
}

getRouteWithGasOnReceive 的工作方式

该函数会自动:
  1. 检查目标链是否受支持(排除 Solana 链)
  2. 检查源链是否受支持(排除 Ethereum 主网和 Sepolia 测试网)
  3. 验证目标资产本身是否已经是手续费资产
  4. 计算合适的 gas 金额:
    • Cosmos 链:平均 gas price × 3(如果无法获取 gas price,则回退为 0.10 美元)
    • EVM L2 链:价值 2.00 美元的原生代币
  5. 创建一条用于获取原生代币的 gas 路由
  6. 相应调整主路由金额
  7. 返回两条路由;如果 gas 路由创建失败,则返回原始路由作为 mainRoute
注意:如果 gas 路由因任何原因创建失败,该函数会返回原始路由作为 mainRoute,并将 gasRoute 设为 undefined,这样你的交换仍可在没有 gas-on-receive 的情况下继续执行。

自定义实现指南

如果你需要在 getRouteWithGasOnReceive 提供的能力之外,进一步自定义 Gas on Receive 的行为:

第 1 步:检查目标链 gas 余额

在发起交换前,先检查用户在目标链上是否有足够的 gas:
import { balances } from "@skip-go/client";

async function checkDestinationGasBalance(
  destinationChainId: string,
  userAddress: string,
  requiredGasAmount?: string
) {
  // Fetch user's balance on destination chain
  const userBalances = await balances({
    chains: {
      [destinationChainId]: { address: userAddress }
    }
  });

  const chainBalances = userBalances.chains?.[destinationChainId]?.denoms;
  
  // For EVM chains, check native token (address 0x0000...)
  const nativeTokenDenom = getNativeTokenDenom(destinationChainId);
  const nativeBalance = chainBalances?.[nativeTokenDenom];

  // Simple check: does user have any native token?
  if (!nativeBalance?.amount || nativeBalance.amount === "0") {
    return false;
  }

  // Optional: Check against a minimum threshold
  if (requiredGasAmount) {
    return Number(nativeBalance.amount) >= Number(requiredGasAmount);
  }

  return true;
}

function getNativeTokenDenom(chainId: string): string {
  // For EVM chains
  if (isEvmChain(chainId)) {
    return "0x0000000000000000000000000000000000000000";
  }
  
  // For Cosmos chains, you'll need to fetch the fee assets
  // This varies by chain (e.g., "uosmo" for Osmosis, "uatom" for Cosmos Hub)
  return getCosmosNativeDenom(chainId);
}

第 2 步:计算所需 gas 数量

const GAS_AMOUNTS_USD = {
  cosmos: 0.10,    // $0.10 for Cosmos chains
  evm_l2: 2.00,    // $2.00 for EVM L2 chains
  evm_mainnet: 0   // Disabled for Ethereum mainnet
};

async function calculateGasAmount(
  sourceAsset: { chainId: string; denom: string },
  destinationChainId: string,
  sourceAssetPriceUsd: number
): Promise<string> {
  const chainType = await getChainType(destinationChainId);
  
  // Determine USD amount based on chain type
  let usdAmount = 0;
  if (chainType === 'cosmos') {
    usdAmount = GAS_AMOUNTS_USD.cosmos;
  } else if (chainType === 'evm' && destinationChainId !== "1") {
    usdAmount = GAS_AMOUNTS_USD.evm_l2;
  }
  
  if (usdAmount === 0) {
    throw new Error("Gas on Receive not supported for this chain");
  }
  
  // Convert USD amount to source asset amount
  const sourceAmount = usdAmount / sourceAssetPriceUsd;
  
  // Convert to crypto amount (considering decimals)
  const sourceAssetDecimals = await getAssetDecimals(sourceAsset);
  return convertToCryptoAmount(sourceAmount, sourceAssetDecimals);
}

第 3 步:用自定义逻辑创建路由

import { route } from "@skip-go/client";

async function createRoutesManually(
  amountIn: string,
  sourceAsset: { chainId: string; denom: string },
  destAsset: { chainId: string; denom: string },
  gasAmount: string,
  enableGasOnReceive: boolean
) {
  // Calculate adjusted amounts
  const gasRouteAmount = enableGasOnReceive ? gasAmount : "0";
  const mainRouteAmount = (Number(amountIn) - Number(gasRouteAmount)).toString();
  
  // Create main route with reduced amount
  const mainRoute = await route({
    amountIn: mainRouteAmount,
    sourceAssetChainId: sourceAsset.chainId,
    sourceAssetDenom: sourceAsset.denom,
    destAssetChainId: destAsset.chainId,
    destAssetDenom: destAsset.denom,
    smartRelay: true
  });
  
  // Create gas route only if enabled
  let gasRoute = null;
  if (enableGasOnReceive) {
    const nativeTokenDenom = getNativeTokenDenom(destAsset.chainId);
    
    gasRoute = await route({
      amountIn: gasRouteAmount,
      sourceAssetChainId: sourceAsset.chainId,
      sourceAssetDenom: sourceAsset.denom,
      destAssetChainId: destAsset.chainId,
      destAssetDenom: nativeTokenDenom,
      smartRelay: true
    });
  }
  
  return { mainRoute, gasRoute };
}

第 4 步:执行路由

import { executeMultipleRoutes, type RouteResponse, type UserAddress } from "@skip-go/client";

async function executeSwapWithGasOnReceive(
  mainRoute: RouteResponse,
  gasRoute: RouteResponse | null,
  userAddresses: UserAddress[],
  signers: {
    getCosmosSigningClient: (chainId: string) => Promise<any>;
    getEVMSigningClient: (chainId: string) => Promise<any>;
  }
) {
  // Build routes object with consistent naming
  const routes = gasRoute 
    ? { mainRoute, feeRoute: gasRoute }
    : { mainRoute };
  
  // Build user addresses object
  const addresses = gasRoute
    ? { mainRoute: userAddresses, feeRoute: userAddresses }
    : { mainRoute: userAddresses };
  
  // Build slippage settings
  const slippage = gasRoute
    ? { mainRoute: "1", feeRoute: "10" } // Higher tolerance for gas route
    : { mainRoute: "1" };
  
  // Execute both routes
  await executeMultipleRoutes({
    route: routes,
    userAddresses: addresses,
    slippageTolerancePercent: slippage,
    getCosmosSigningClient: signers.getCosmosSigningClient,
    getEVMSigningClient: signers.getEVMSigningClient,
    onRouteStatusUpdated: (status) => {
      // Handle route status updates
      console.log("Route status:", status);
      
      // Check if gas route failed
      const gasRouteFailed = status.relatedRoutes?.find(
        r => r.routeKey === "feeRoute" && r.status === "failed"
      );
      
      if (gasRouteFailed) {
        console.warn("Gas route failed, but main swap continues");
      }
    }
  });
}

完整实现示例

下面是一个结合所有概念的完整示例:
import { 
  route, 
  executeMultipleRoutes, 
  balances,
  assets,
  getRouteWithGasOnReceive
} from "@skip-go/client";

class GasOnReceiveManager {
  private readonly GAS_AMOUNTS_USD = {
    cosmos: 0.10,
    evm_l2: 2.00
  };

  async shouldEnableGasOnReceive(
    destinationChainId: string,
    destinationAddress: string,
    destinationAssetDenom: string
  ): Promise<boolean> {
    // 检查链是否受支持
    if (!this.isChainSupported(destinationChainId)) {
      return false;
    }
    
    // 如果目标资产本身已经是 gas 代币,则不要启用
    if (await this.isGasToken(destinationChainId, destinationAssetDenom)) {
      return false;
    }
    
    // 检查用户的 gas 余额
    const hasGas = await this.checkGasBalance(destinationChainId, destinationAddress);
    return !hasGas;
  }
  
  private isChainSupported(chainId: string): boolean {
    // 目标链不支持 Solana 链
    const unsupportedDestChains = ["solana", "solana-devnet"];
    // 源链不支持 Ethereum 主网和 Sepolia
    const unsupportedSourceChains = ["1", "11155111"];
    // 本示例中检查的是目标链支持情况
    return !unsupportedDestChains.includes(chainId);
  }
  
  private async isGasToken(chainId: string, denom: string): boolean {
    const chainAssets = await assets({ chainId });
    const gasTokens = chainAssets.chain?.feeAssets || [];
    return gasTokens.some(token => token.denom === denom);
  }
  
  private async checkGasBalance(
    chainId: string, 
    address: string
  ): Promise<boolean> {
    const balanceResponse = await balances({
      chains: { [chainId]: { address } }
    });
    
    const nativeDenom = await this.getNativeDenom(chainId);
    const balance = balanceResponse?.chains?.[chainId]?.denoms?.[nativeDenom];
    
    // 检查用户是否有任何余额
    return balance?.amount && balance.amount !== "0";
  }
  
  async executeSwapWithGasOnReceive(
    params: {
      amountIn: string;
      sourceAsset: { chainId: string; denom: string };
      destAsset: { chainId: string; denom: string };
      userAddresses: Array<{ chainId: string; address: string }>;
      enableGasOnReceive: boolean;
      signers: any;
    }
  ) {
    const { 
      amountIn, 
      sourceAsset, 
      destAsset, 
      userAddresses, 
      enableGasOnReceive,
      signers 
    } = params;
    
    // 获取初始路由
    const originalRoute = await route({
      amountIn,
      sourceAssetChainId: sourceAsset.chainId,
      sourceAssetDenom: sourceAsset.denom,
      destAssetChainId: destAsset.chainId,
      destAssetDenom: destAsset.denom
    });
    
    if (enableGasOnReceive) {
      // 使用自动拆分
      const { mainRoute, gasRoute } = await getRouteWithGasOnReceive({
        routeResponse: originalRoute,
        routeRequest: {
          amountIn,
          sourceAssetChainId: sourceAsset.chainId,
          sourceAssetDenom: sourceAsset.denom,
          destAssetChainId: destAsset.chainId,
          destAssetDenom: destAsset.denom
        }
      });
      
      if (gasRoute) {
        // 执行两条路由
        await executeMultipleRoutes({
          route: { mainRoute, feeRoute: gasRoute },
          userAddresses: { 
            mainRoute: userAddresses,
            feeRoute: userAddresses 
          },
          slippageTolerancePercent: {
            mainRoute: "1",
            feeRoute: "10"  // gas 路由使用更高的滑点容忍度
          },
          ...signers,
          onRouteStatusUpdated: this.handleRouteStatus
        });
      } else {
        // 仅执行主路由
        await executeRoute({
          route: mainRoute,
          userAddresses,
          slippageTolerancePercent: "1",
          ...signers
        });
      }
    } else {
      // 不带 gas 执行原始路由
      await executeRoute({
        route: originalRoute,
        userAddresses,
        slippageTolerancePercent: "1",
        ...signers
      });
    }
  }
  
  private handleRouteStatus(status: RouteStatus) {
    if (status.status === "completed") {
      console.log("兑换已成功完成");
    }
    
    // 检查 gas 路由状态
    const gasRoute = status.relatedRoutes?.find(r => r.routeKey === "feeRoute");
    if (gasRoute?.status === "failed") {
      console.warn("Gas 补充失败,但主兑换会继续");
    } else if (gasRoute?.status === "completed") {
      console.log("Gas 代币已成功到账");
    }
  }
}

UI 注意事项

在你的 UI 中实现 Gas on Receive 时:

展示 Gas 信息

function GasOnReceiveToggle({ 
  enabled, 
  gasAmount, 
  gasAssetSymbol,
  onToggle 
}: GasOnReceiveProps) {
  return (
    <div className="gas-on-receive">
      <div className="gas-info">
        <GasIcon />
        <span>启用 gas 补充功能 - 你将收到 {gasAmount} {gasAssetSymbol}</span>
        <Tooltip content="在目标链上接收原生代币,用于支付 gas 费用" />
      </div>
      <Switch checked={enabled} onChange={onToggle} />
    </div>
  );
}

在执行期间显示状态

function GasStatus({ status, amount, symbol }: GasStatusProps) {
  switch (status) {
    case 'pending':
      return <span>正在接收 {amount} {symbol}...</span>;
    case 'completed':
      return <span>✓ 已收到 {amount} {symbol} 作为 gas 补充</span>;
    case 'failed':
      return <span>⚠ 接收 gas 代币失败</span>;
    default:
      return null;
  }
}

错误处理

优雅地处理各种失败场景:
async function handleGasRouteErrors(error: Error, mainRouteStatus: string) {
  // gas 路由失败不会影响主兑换
  if (mainRouteStatus === 'completed') {
    console.log("尽管 gas 路由失败,主兑换仍然成功");
    // 向用户显示缺少 gas 的警告
    showWarning("兑换已完成,但未收到 gas 代币");
  }
  
  // 记录日志以便调试
  console.error("Gas 路由错误:", error);
  
  // 在分析中跟踪
  trackEvent("gas_route_failed", {
    error: error.message,
    mainRouteStatus
  });
}

最佳实践

  1. 使用 getRouteWithGasOnReceive:自动函数会处理边界情况和优化
  2. 自动检测:检查 gas 余额,并在需要时建议启用 Gas on Receive
  3. 用户控制:始终允许用户开启或关闭该功能
  4. 清晰沟通:透明展示准确金额和成本
  5. 优雅降级:即使 gas 路由失败,主兑换也应继续进行
  6. 更高滑点:gas 路由使用 10% 滑点(主路由为 1%)
  7. 链支持:对 Ethereum 主网和 Solana 禁用
  8. 金额限制:使用推荐金额(Cosmos 为 0.10,EVML2为0.10,EVM L2 为 2.00)

高级配置

自定义 Gas 金额

// 覆盖默认 gas 金额
const customGasAmounts = {
  "osmosis-1": "100000", // 0.1 OSMO
  "42161": "0.001",      // Arbitrum 上 0.001 ETH
  "137": "2"             // Polygon 上 2 MATIC
};

async function getCustomGasAmount(chainId: string): Promise<string> {
  return customGasAmounts[chainId] || getDefaultGasAmount(chainId);
}

动态定价

// 根据当前 gas 价格调整 gas 金额
async function calculateDynamicGasAmount(chainId: string) {
  const gasPrice = await getGasPrice(chainId);
  const estimatedTxCount = 5; // 假设用户需要 5 笔交易的 gas
  const gasPerTx = 21000; // 基础转账 gas 上限
  
  const totalGasNeeded = gasPrice * gasPerTx * estimatedTxCount;
  return totalGasNeeded.toString();
}

与 Widget 实现的对比

功能Widget(自动)客户端库(手动)
Gas 余额检测自动手动或使用 getRouteWithGasOnReceive
路由创建自动使用 getRouteWithGasOnReceive 或手动实现
金额计算内置默认值通过 getRouteWithGasOnReceive 内置
UI 组件已提供自行构建
错误处理自动手动实现
状态跟踪内置通过回调

总结

Skip Go Client Library 为实现 Gas on Receive 提供了灵活的选项:
  1. 使用 getRouteWithGasOnReceive 实现快速集成,自动完成路由拆分
  2. 通过手动余额检查和路由创建实现完全控制
  3. 通过 executeMultipleRoutes 中的回调实现状态跟踪
  4. 提供优雅的错误处理,即 gas 路由失败也不会影响主兑换
请选择最适合你应用需求的方案。对于大多数使用场景,getRouteWithGasOnReceive 在简洁性和功能性之间提供了理想的平衡。
This guide explains how to implement Gas on Receive functionality when building custom frontends with the Skip Go Client Library. Gas on Receive helps users automatically obtain native gas tokens on destination chains during cross-chain swaps.

Overview

Gas on Receive prevents users from getting “stuck” with assets they can’t use by automatically providing a small amount of native gas tokens on the destination chain. The client library provides the getRouteWithGasOnReceive function that handles all the complexity for you, or you can implement custom logic if you need specific behavior.

Prerequisites

  • Skip Go Client Library v1.5.0 or higher
  • Understanding of the basic route and executeRoute functions
  • Access to user wallet signers for multiple chains

Supported Chains

  • Destination: Cosmos chains and EVM L2s (Solana not supported)
  • Source: Most chains except Ethereum mainnet and Sepolia testnet

Quick Start: Using getRouteWithGasOnReceive

The simplest way to implement Gas on Receive is using the built-in getRouteWithGasOnReceive function:
import { 
  route, 
  getRouteWithGasOnReceive, 
  executeMultipleRoutes,
  executeRoute,
  type RouteRequest,
  type RouteResponse,
  type UserAddress
} from "@skip-go/client";

async function swapWithGasOnReceive() {
  try {
    // Step 1: Define your route request
    const routeRequest = {
      amountIn: "1000000", // 1 OSMO
      sourceAssetChainId: "osmosis-1",
      sourceAssetDenom: "uosmo",
      destAssetChainId: "42161", // Arbitrum
      destAssetDenom: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", // WETH
      smartRelay: true
    };

    // Step 2: Get your initial route
    const originalRoute = await route(routeRequest);

    // Step 3: Automatically split into main and gas routes
    const { mainRoute, gasRoute } = await getRouteWithGasOnReceive({
      routeResponse: originalRoute,
      routeRequest
    });

    // Step 4: Get user addresses based on required chains
    // Note: mainRoute and gasRoute may require different chains
    const mainRouteAddresses = mainRoute.requiredChainAddresses.map(chainId => ({
      chainId,
      address: getUserAddressForChain(chainId) // Your function to get user's address
    }));
    
    const gasRouteAddresses = gasRoute?.requiredChainAddresses.map(chainId => ({
      chainId, 
      address: getUserAddressForChain(chainId)
    }));
    
    // Example helper function:
    // function getUserAddressForChain(chainId: string): string {
    //   const addresses = {
    //     "osmosis-1": "osmo1...",
    //     "42161": "0x..."
    //   };
    //   return addresses[chainId] || throw new Error(`No address for chain ${chainId}`);
    // }

  // Step 5: Execute routes
  if (gasRoute && gasRouteAddresses) {
    // Execute both routes together
    await executeMultipleRoutes({
      route: { mainRoute, feeRoute: gasRoute },
      userAddresses: {
        mainRoute: mainRouteAddresses,
        feeRoute: gasRouteAddresses // May be different chains than mainRoute
      },
      slippageTolerancePercent: {
        mainRoute: "1",
        feeRoute: "10" // Higher tolerance for gas route
      },
      getCosmosSigningClient: async (chainId) => {
        return yourCosmosWallet.getSigningClient(chainId);
      },
      getEVMSigningClient: async (chainId) => {
        return yourEvmWallet.getSigningClient(chainId);
      },
      onRouteStatusUpdated: (status) => {
        console.log("Route status:", status);
        
        // Check if gas route failed
        const gasRouteFailed = status.relatedRoutes?.find(
          r => r.routeKey === "feeRoute" && r.status === "failed"
        );
        
        if (gasRouteFailed) {
          console.warn("Gas route failed, but main swap continues");
        }
      }
    });
  } else {
    // No gas route needed, execute original route
    await executeRoute({
      route: originalRoute,
      userAddresses: mainRouteAddresses,
      slippageTolerancePercent: "1",
      getCosmosSigningClient: async (chainId) => {
        return yourCosmosWallet.getSigningClient(chainId);
      },
      getEVMSigningClient: async (chainId) => {
        return yourEvmWallet.getSigningClient(chainId);
      }
    });
  } catch (error) {
    console.error("Failed to execute swap:", error);
    // Handle error appropriately
  }
}

How getRouteWithGasOnReceive Works

The function automatically:
  1. Checks if the destination chain is supported (excludes Solana chains)
  2. Checks source chain support (excludes Ethereum mainnet and Sepolia testnet)
  3. Verifies the destination asset isn’t already a fee asset
  4. Calculates appropriate gas amounts:
    • Cosmos chains: Average gas price × 3 (or $0.10 USD fallback if gas price unavailable)
    • EVM L2 chains: $2.00 USD worth of native tokens
  5. Creates a gas route to obtain native tokens
  6. Adjusts the main route amount accordingly
  7. Returns both routes, or the original route as mainRoute if gas route creation fails
Note: If gas route creation fails for any reason, the function returns the original route as mainRoute with gasRoute as undefined, allowing your swap to proceed without gas-on-receive.

Custom Implementation Guide

If you need to customize the Gas on Receive behavior beyond what getRouteWithGasOnReceive provides:

Step 1: Check Destination Gas Balance

Before initiating a swap, check if the user has sufficient gas on the destination chain:
import { balances } from "@skip-go/client";

async function checkDestinationGasBalance(
  destinationChainId: string,
  userAddress: string,
  requiredGasAmount?: string
) {
  // Fetch user's balance on destination chain
  const userBalances = await balances({
    chains: {
      [destinationChainId]: { address: userAddress }
    }
  });

  const chainBalances = userBalances.chains?.[destinationChainId]?.denoms;
  
  // For EVM chains, check native token (address 0x0000...)
  const nativeTokenDenom = getNativeTokenDenom(destinationChainId);
  const nativeBalance = chainBalances?.[nativeTokenDenom];

  // Simple check: does user have any native token?
  if (!nativeBalance?.amount || nativeBalance.amount === "0") {
    return false;
  }

  // Optional: Check against a minimum threshold
  if (requiredGasAmount) {
    return Number(nativeBalance.amount) >= Number(requiredGasAmount);
  }

  return true;
}

function getNativeTokenDenom(chainId: string): string {
  // For EVM chains
  if (isEvmChain(chainId)) {
    return "0x0000000000000000000000000000000000000000";
  }
  
  // For Cosmos chains, you'll need to fetch the fee assets
  // This varies by chain (e.g., "uosmo" for Osmosis, "uatom" for Cosmos Hub)
  return getCosmosNativeDenom(chainId);
}

Step 2: Calculate Gas Amount Needed

const GAS_AMOUNTS_USD = {
  cosmos: 0.10,    // $0.10 for Cosmos chains
  evm_l2: 2.00,    // $2.00 for EVM L2 chains
  evm_mainnet: 0   // Disabled for Ethereum mainnet
};

async function calculateGasAmount(
  sourceAsset: { chainId: string; denom: string },
  destinationChainId: string,
  sourceAssetPriceUsd: number
): Promise<string> {
  const chainType = await getChainType(destinationChainId);
  
  // Determine USD amount based on chain type
  let usdAmount = 0;
  if (chainType === 'cosmos') {
    usdAmount = GAS_AMOUNTS_USD.cosmos;
  } else if (chainType === 'evm' && destinationChainId !== "1") {
    usdAmount = GAS_AMOUNTS_USD.evm_l2;
  }
  
  if (usdAmount === 0) {
    throw new Error("Gas on Receive not supported for this chain");
  }
  
  // Convert USD amount to source asset amount
  const sourceAmount = usdAmount / sourceAssetPriceUsd;
  
  // Convert to crypto amount (considering decimals)
  const sourceAssetDecimals = await getAssetDecimals(sourceAsset);
  return convertToCryptoAmount(sourceAmount, sourceAssetDecimals);
}

Step 3: Create Routes with Custom Logic

import { route } from "@skip-go/client";

async function createRoutesManually(
  amountIn: string,
  sourceAsset: { chainId: string; denom: string },
  destAsset: { chainId: string; denom: string },
  gasAmount: string,
  enableGasOnReceive: boolean
) {
  // Calculate adjusted amounts
  const gasRouteAmount = enableGasOnReceive ? gasAmount : "0";
  const mainRouteAmount = (Number(amountIn) - Number(gasRouteAmount)).toString();
  
  // Create main route with reduced amount
  const mainRoute = await route({
    amountIn: mainRouteAmount,
    sourceAssetChainId: sourceAsset.chainId,
    sourceAssetDenom: sourceAsset.denom,
    destAssetChainId: destAsset.chainId,
    destAssetDenom: destAsset.denom,
    smartRelay: true
  });
  
  // Create gas route only if enabled
  let gasRoute = null;
  if (enableGasOnReceive) {
    const nativeTokenDenom = getNativeTokenDenom(destAsset.chainId);
    
    gasRoute = await route({
      amountIn: gasRouteAmount,
      sourceAssetChainId: sourceAsset.chainId,
      sourceAssetDenom: sourceAsset.denom,
      destAssetChainId: destAsset.chainId,
      destAssetDenom: nativeTokenDenom,
      smartRelay: true
    });
  }
  
  return { mainRoute, gasRoute };
}

Step 4: Execute Routes

import { executeMultipleRoutes, type RouteResponse, type UserAddress } from "@skip-go/client";

async function executeSwapWithGasOnReceive(
  mainRoute: RouteResponse,
  gasRoute: RouteResponse | null,
  userAddresses: UserAddress[],
  signers: {
    getCosmosSigningClient: (chainId: string) => Promise<any>;
    getEVMSigningClient: (chainId: string) => Promise<any>;
  }
) {
  // Build routes object with consistent naming
  const routes = gasRoute 
    ? { mainRoute, feeRoute: gasRoute }
    : { mainRoute };
  
  // Build user addresses object
  const addresses = gasRoute
    ? { mainRoute: userAddresses, feeRoute: userAddresses }
    : { mainRoute: userAddresses };
  
  // Build slippage settings
  const slippage = gasRoute
    ? { mainRoute: "1", feeRoute: "10" } // Higher tolerance for gas route
    : { mainRoute: "1" };
  
  // Execute both routes
  await executeMultipleRoutes({
    route: routes,
    userAddresses: addresses,
    slippageTolerancePercent: slippage,
    getCosmosSigningClient: signers.getCosmosSigningClient,
    getEVMSigningClient: signers.getEVMSigningClient,
    onRouteStatusUpdated: (status) => {
      // Handle route status updates
      console.log("Route status:", status);
      
      // Check if gas route failed
      const gasRouteFailed = status.relatedRoutes?.find(
        r => r.routeKey === "feeRoute" && r.status === "failed"
      );
      
      if (gasRouteFailed) {
        console.warn("Gas route failed, but main swap continues");
      }
    }
  });
}

Complete Implementation Example

Here’s a full example combining all the concepts:
import { 
  route, 
  executeMultipleRoutes, 
  balances,
  assets,
  getRouteWithGasOnReceive
} from "@skip-go/client";

class GasOnReceiveManager {
  private readonly GAS_AMOUNTS_USD = {
    cosmos: 0.10,
    evm_l2: 2.00
  };

  async shouldEnableGasOnReceive(
    destinationChainId: string,
    destinationAddress: string,
    destinationAssetDenom: string
  ): Promise<boolean> {
    // Check if chain is supported
    if (!this.isChainSupported(destinationChainId)) {
      return false;
    }
    
    // Don't enable if destination asset is already a gas token
    if (await this.isGasToken(destinationChainId, destinationAssetDenom)) {
      return false;
    }
    
    // Check user's gas balance
    const hasGas = await this.checkGasBalance(destinationChainId, destinationAddress);
    return !hasGas;
  }
  
  private isChainSupported(chainId: string): boolean {
    // Solana chains not supported for destination
    const unsupportedDestChains = ["solana", "solana-devnet"];
    // Ethereum mainnet and Sepolia not supported as source
    const unsupportedSourceChains = ["1", "11155111"];
    // For this example, checking destination support
    return !unsupportedDestChains.includes(chainId);
  }
  
  private async isGasToken(chainId: string, denom: string): boolean {
    const chainAssets = await assets({ chainId });
    const gasTokens = chainAssets.chain?.feeAssets || [];
    return gasTokens.some(token => token.denom === denom);
  }
  
  private async checkGasBalance(
    chainId: string, 
    address: string
  ): Promise<boolean> {
    const balanceResponse = await balances({
      chains: { [chainId]: { address } }
    });
    
    const nativeDenom = await this.getNativeDenom(chainId);
    const balance = balanceResponse?.chains?.[chainId]?.denoms?.[nativeDenom];
    
    // Check if user has any balance
    return balance?.amount && balance.amount !== "0";
  }
  
  async executeSwapWithGasOnReceive(
    params: {
      amountIn: string;
      sourceAsset: { chainId: string; denom: string };
      destAsset: { chainId: string; denom: string };
      userAddresses: Array<{ chainId: string; address: string }>;
      enableGasOnReceive: boolean;
      signers: any;
    }
  ) {
    const { 
      amountIn, 
      sourceAsset, 
      destAsset, 
      userAddresses, 
      enableGasOnReceive,
      signers 
    } = params;
    
    // Get initial route
    const originalRoute = await route({
      amountIn,
      sourceAssetChainId: sourceAsset.chainId,
      sourceAssetDenom: sourceAsset.denom,
      destAssetChainId: destAsset.chainId,
      destAssetDenom: destAsset.denom
    });
    
    if (enableGasOnReceive) {
      // Use automatic splitting
      const { mainRoute, gasRoute } = await getRouteWithGasOnReceive({
        routeResponse: originalRoute,
        routeRequest: {
          amountIn,
          sourceAssetChainId: sourceAsset.chainId,
          sourceAssetDenom: sourceAsset.denom,
          destAssetChainId: destAsset.chainId,
          destAssetDenom: destAsset.denom
        }
      });
      
      if (gasRoute) {
        // Execute both routes
        await executeMultipleRoutes({
          route: { mainRoute, feeRoute: gasRoute },
          userAddresses: { 
            mainRoute: userAddresses,
            feeRoute: userAddresses 
          },
          slippageTolerancePercent: {
            mainRoute: "1",
            feeRoute: "10"  // Higher tolerance for gas route
          },
          ...signers,
          onRouteStatusUpdated: this.handleRouteStatus
        });
      } else {
        // Execute just the main route
        await executeRoute({
          route: mainRoute,
          userAddresses,
          slippageTolerancePercent: "1",
          ...signers
        });
      }
    } else {
      // Execute original route without gas
      await executeRoute({
        route: originalRoute,
        userAddresses,
        slippageTolerancePercent: "1",
        ...signers
      });
    }
  }
  
  private handleRouteStatus(status: RouteStatus) {
    if (status.status === "completed") {
      console.log("Swap completed successfully");
    }
    
    // Check gas route status
    const gasRoute = status.relatedRoutes?.find(r => r.routeKey === "feeRoute");
    if (gasRoute?.status === "failed") {
      console.warn("Gas provision failed, but main swap continues");
    } else if (gasRoute?.status === "completed") {
      console.log("Gas tokens received successfully");
    }
  }
}

UI Considerations

When implementing Gas on Receive in your UI:

Display Gas Information

function GasOnReceiveToggle({ 
  enabled, 
  gasAmount, 
  gasAssetSymbol,
  onToggle 
}: GasOnReceiveProps) {
  return (
    <div className="gas-on-receive">
      <div className="gas-info">
        <GasIcon />
        <span>Enable gas top up - You'll get {gasAmount} in {gasAssetSymbol}</span>
        <Tooltip content="Receive native tokens for gas fees on destination chain" />
      </div>
      <Switch checked={enabled} onChange={onToggle} />
    </div>
  );
}

Show Status During Execution

function GasStatus({ status, amount, symbol }: GasStatusProps) {
  switch (status) {
    case 'pending':
      return <span>Receiving {amount} in {symbol}...</span>;
    case 'completed':
      return <span>✓ Received {amount} in {symbol} as gas top-up</span>;
    case 'failed':
      return <span>⚠ Failed to receive gas tokens</span>;
    default:
      return null;
  }
}

Error Handling

Handle various failure scenarios gracefully:
async function handleGasRouteErrors(error: Error, mainRouteStatus: string) {
  // Gas route failures don't affect main swap
  if (mainRouteStatus === 'completed') {
    console.log("Main swap succeeded despite gas route failure");
    // Show warning to user about missing gas
    showWarning("Swap completed but gas tokens were not received");
  }
  
  // Log for debugging
  console.error("Gas route error:", error);
  
  // Track in analytics
  trackEvent("gas_route_failed", {
    error: error.message,
    mainRouteStatus
  });
}

Best Practices

  1. Use getRouteWithGasOnReceive: The automatic function handles edge cases and optimizations
  2. Auto-detection: Check gas balances and suggest Gas on Receive when needed
  3. User Control: Always allow users to toggle the feature on/off
  4. Clear Communication: Show exact amounts and costs transparently
  5. Graceful Degradation: Main swap should continue even if gas route fails
  6. Higher Slippage: Use 10% slippage for gas routes (vs 1% for main routes)
  7. Chain Support: Disable for Ethereum mainnet and Solana
  8. Amount Limits: Use recommended amounts (0.10forCosmos,0.10 for Cosmos, 2.00 for EVM L2s)

Advanced Configuration

Custom Gas Amounts

// Override default gas amounts
const customGasAmounts = {
  "osmosis-1": "100000", // 0.1 OSMO
  "42161": "0.001",      // 0.001 ETH on Arbitrum
  "137": "2"             // 2 MATIC on Polygon
};

async function getCustomGasAmount(chainId: string): Promise<string> {
  return customGasAmounts[chainId] || getDefaultGasAmount(chainId);
}

Dynamic Pricing

// Adjust gas amount based on current gas prices
async function calculateDynamicGasAmount(chainId: string) {
  const gasPrice = await getGasPrice(chainId);
  const estimatedTxCount = 5; // Assume user needs gas for 5 transactions
  const gasPerTx = 21000; // Basic transfer gas limit
  
  const totalGasNeeded = gasPrice * gasPerTx * estimatedTxCount;
  return totalGasNeeded.toString();
}

Comparison with Widget Implementation

FeatureWidget (Automatic)Client Library (Manual)
Gas balance detectionAutomaticManual or use getRouteWithGasOnReceive
Route creationAutomaticUse getRouteWithGasOnReceive or manual
Amount calculationBuilt-in defaultsBuilt-in with getRouteWithGasOnReceive
UI componentsProvidedBuild your own
Error handlingAutomaticManual implementation
Status trackingBuilt-inVia callbacks

Summary

The Skip Go Client Library provides flexible options for implementing Gas on Receive:
  1. Quick implementation with getRouteWithGasOnReceive for automatic route splitting
  2. Full control with manual balance checking and route creation
  3. Status tracking via callbacks in executeMultipleRoutes
  4. Graceful error handling where gas route failures don’t affect main swaps
Choose the approach that best fits your application’s needs. For most use cases, getRouteWithGasOnReceive provides the ideal balance of simplicity and functionality.