Background

This document covers the Typescript Package Skip provides to wrap the functionality of the Skip Go API and make it even easier for developers to build applications with it.

Set up the Library

Install the library:

Shell
npm install @skip-go/client

Initialize Client

All integrations start by initializing a SkipClient client (@skip-go/client.SkipClient).

This helper objects wraps interactions with the API and provides useful helper methods for a variety of common actions that depend on inputs or outputs of the API (e.g. querying for assets and chains, constructing a route, constructing + signing a transaction from a message returned by the API)

TypeScript
import { SkipClient, SKIP_API_URL } from "@skip-go/client";

const client = new SkipClient();

The client constructor takes several optional inputs provided together by the SkipClientOptions object

  • apiURL?: string;: A URL for interacting with the Skip Go API (Defaults to the production URL)
  • getEVMSigner?: (chainID: string) => Promise<WalletClient>;: Method that SkipClient calls internally whenever it needs a client for constructing and signing an EVM transaction. (WalletClient is defined in viem and other popular EVM client libraries)
  • getCosmosSigner?: (chainID: string) => Promise<OfflineSigner>;: Method that SkipClient calls internally whenever it needs a client for constructing and signing a Cosmos transaction. (OfflineSigner is defined in @cosmjs/proto-signing)
  • getSVMSigner?: () => Promise<Adapter>;: Method that SkipClient calls internally whenever it needs a client for constructing and signing a Solana transaction. (Adapter is defined in @solana/wallet-adapter-base)
  • endpointOptions:
    • endpoints?: Record<string, clientTypes.EndpointOptions>;: A map of chainIDs to their respective endpoints. _ clientTypes.EndpointOptions has rpc and rest fields _ If you implement this, you do not need to implement the two functions below
    • getRpcEndpointForChain?: (chainID: string) => Promise<string>;: A function that returns the RPC endpoint for a given chainID
    • getRestEndpointForChain?: (chainID: string) => Promise<string>;: A function that returns the REST endpoint for a given chainID
  • aminoTypes?: AminoConverters;: additional amino types to be used in the router.
  • registryTypes?: Iterable<[string, GeneratedType]>;: additional registry types to be used in the router

Setup Signers

Optionally, you can set up a signer for each EVM, SVM, or Cosmos SDK ecosystems.

For EVM transactions, you need to install the viem package:

shell
npm i viem

For SVM transactions, you can pick which wallets you want to use in @solana/wallet-adapter-wallets. For this example, we are going to use Phantom wallet:

shell
npm i @solana/wallet-adapter-phantom

Now, let’s create the SkipClient instance with the signers for each ecosystem!

We are going to use Keplr for Cosmos transactions, MetaMask for EVM transactions, and Phantom for Solana transactions.

import { SkipClient } from '@skip-router/client';
import { PhantomWalletAdapter } from '@solana/wallet-adapter-phantom';
import { createWalletClient } from 'viem';

const skipClient = new SkipClient({
  getCosmosSigner: async (chainID) => {
    const offlineSigner = window.keplr?.getOfflineSigner(chainID);
    if (!offlineSigner) throw new Error('Keplr not installed');
    return offlineSigner;
  },
  getEVMSigner: async () => {
    const ethereum = window.ethereum;
    if (!ethereum) throw new Error('MetaMask not installed');
    const client = createWalletClient({
      chain: mainnet,
      transport: custom(window.ethereum),
    });
    return client;
  },
  getSVMSigner: async () => {
    const phantom = new PhantomWalletAdapter();
    return phantom;
  },
});

Query Basic Info

Now that your client is initialized, you can use it to query basic information about chains & assets to populate your user interface.

For example:

Get list of chains

// get a Chain[] of all supported cosmos chains
const chains = await client.chains();
// include EVM, SVM and testnets chains
const chains = await client.chains({
  includeEVM: true,
  includeSVM: true,
  includeTestnets: true,
});

Get a map of assets by chain ID

Return type will be Record<string, Asset[]>

// get all chains assets
const assets = await client.assets({
  includeEvmAssets: true,
  includeCW20Assets: true,
  includeSvmAssets: true,
});

// get assets filtered by chain ID
const assets = await client.assets({
  chainID: 'cosmoshub-4'
  includeEvmAssets: true,
  includeCW20Assets: true,
  includeSvmAssets: true,
})

For more functions documentation please check

After querying basic information about chains and assets, you can utilize the recommendAssets method to find the best routes for moving assets between chains. This feature is particularly useful for obtaining recommendations on which unique asset should be received on the destination chain for an optimal user experience. These recommendations are based on factors like liquidity pools, token denominations, and the unique characteristics of each IBC transfer route.

Usage example:

const request = [
  {
    source_asset_denom: 'uusdc',
    source_asset_chain_id: 'axelar-dojo-1',
    dest_chain_id: 'cosmoshub-4',
  },
  {
    source_asset_denom: 'uusdc',
    source_asset_chain_id: 'axelar-dojo-1',
    dest_chain_id: 'osmosis-1',
  },
];

const recommendations = await client.recommendAssets(request);

Response example:

{
  "recommendations": [],
  "recommendation_entries": [
    {
      "recommendations": [
        {
          "asset": {
            "denom": "ibc/932D6003DA334ECBC5B23A071B4287D0A5CC97331197FE9F1C0689BA002A8421",
            "chain_id": "cosmoshub-4",
            "origin_denom": "uusdc",
            "origin_chain_id": "axelar-dojo-1",
            "trace": "transfer/channel-293",
            "is_cw20": false,
            "is_evm": false,
            "symbol": "USDC",
            "name": "USDC",
            "logo_uri": "https://raw.githubusercontent.com/cosmos/chain-registry/master/axelar/images/usdc.png",
            "decimals": 6,
            "description": "Circle's stablecoin on Axelar",
            "coingecko_id": "axlusdc",
            "recommended_symbol": "USDC.axl"
          },
          "reason": "MOST_LIQUID"
        }
      ]
    },
    {
      "recommendations": [
        {
          "asset": {
            "denom": "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858",
            "chain_id": "osmosis-1",
            "origin_denom": "uusdc",
            "origin_chain_id": "axelar-dojo-1",
            "trace": "transfer/channel-208",
            "is_cw20": false,
            "is_evm": false,
            "symbol": "USDC",
            "name": "USDC",
            "logo_uri": "https://raw.githubusercontent.com/cosmos/chain-registry/master/axelar/images/usdc.png",
            "decimals": 6,
            "description": "Circle's stablecoin on Axelar",
            "coingecko_id": "axlusdc",
            "recommended_symbol": "USDC.axl"
          },
          "reason": "MOST_LIQUID"
        }
      ]
    }
  ]
}

Get a Route

Once your user has selected source and destination tokens and chains, you can use the client to generate a route and quote:

const route = await client.route({
  amountIn: AMOUNT_IN,
  sourceAssetDenom: SOURCE_DENOM,
  sourceAssetChainID: SOURCE_CHAIN_ID,
  destAssetDenom: DEST_DENOM,
  destAssetChainID: DEST_CHAIN_ID,
  cumulativeAffiliateFeeBPS: '0',
});

Get Required Addresses

Now that we have the route response, we can use our signer to get the list of chains where the user or reeceiver addresses are required for executing the route.

We’ll use these addresses in the next step to populate userAddresses when executing the route with the SkipClient.executeRoute method.

The requiredChainAddresses field in the output of route gives the chainIDs we need to use.

Use addresses your user can sign with

Funds could end up in any of the addresses you provide — including intermediate chains in certain failure conditions (e.g. an IBC tranfer timesout after a swap). So you must be sure your user can sign for each address you provide.

See Cross-chain Failure Cases for more details on when funds might end up in an intermediate address.

(Rest assured that SkipClient.transactionStatus will tell you where your users funds end up even if they don’t make it to their final destination.)

It’s recommended you keep the address somewhere and have function to get the address from the chainID. In this example we have a function called getAddress that takes the chainID and returns the address.

Add the following snippet to your script after getting the route to retrieve the address associated with each requiredChainAddresses entry:

// get user addresses for each requiredChainAddress
    const userAddresses = await Promise.all(route.requiredChainAddresses.map(async (chainID) => {
        return {
            chainID: chainID,
            address: await getAddress(chainID)
        }
    }))

Execute a Route

Once you have a route, you can execute it in a single function call (passing in the route, the user addresses for at least the chains the route includes, and an optional callback function.

For example:

await client.executeRoute({
  route,
  userAddresses: USER_ADDRESSES,
  onTransactionSuccess: async (chainID, txHash, success) => {
    console.log(txHash);
  },
});

For routes that consist of multiple transactions, this will monitor each transaction until it completes, then generate the transaction for the next step and prompt the user to sign it.

Alternatively, you can use the route to generate, sign, and submit the messages separately — or use your own solutions for any/all of these steps if you like. See SkipClient.messages, SkipClient.signMultiChainMessageDirect, and SkipClient.submitTransaction for details on these lower-level functions

Cross-chain Transaction Tracking

Once the user has signed a transaction to execute a cross-chain action, you can track it using the SkipClient functions:

  • SkipClient.trackTransaction: Requests tracking for a that’s already been submitted to the network through an RPC, using the transaction hash
  • SkipClient.submitTransaction: Publishes the signed transaction to the network & begins tracking the cross-chain actions this transaction produces

After you’ve used one of these two functions to kick-off realtime tracking for a cross-chain action, you can request the current status of the transaction using SkipClient.transactionStatus.

We also provide a waitForTransaction helper function that hangs until the entire cross-chain action flow has completed, which you can use instead of configuring your own polling via transactionStatus

Want to help us get better? Have questions or feedback?

You can reach us easily by joining our Discord and grabbing the “Skip Go Developer” role.