Summary

This walkthrough shows you how to perform a swap of USDC on Noble to TIA on Celestia.

Having trouble? Get in touch

Please contact us if you believe the API doesn’t conveniently support your desired user experience, or if you’re confused about how to use it.

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

Reference Docs

For detailed information on Skip Go API endpoints and functionality, please see our reference docs.

Example using @skip-go/client package

This example is for browser environments.

1. Install @skip-go/client

In your command line terminal, install the @skip-go/client package:

shell
npm i @skip-go/client

Additional dependencies and steps will be required for EVM & SVM transactions

This example constructs a cross-chain swap between 2 Cosmos chains, but Skip Go API supports all major EVM chains & Solana (see Supported Ecosystems for more info about which chains, routes, and tokens we support).

To perform cross-chain swaps involving Solana or an EVM chain, you’ll need to import extra dependencies and configure additional transaction signers. Once you’ve completed this tutorial, feel free to red the EVM Transactions and SVM Transactions docs for more details.

2. 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.

SwapPage.tsx
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;
    }
});

3. Request a Quote

Now, we can use the SkipRouter.route function to request a quote & route to swap USDC on Noble to TIA on Celestia:

After initializing your skipClient add the following::

const route = await skipClient.route({
  sourceAssetDenom: 'uusdc',
  sourceAssetChainID: 'noble-1',
  destAssetDenom: 'utia',
  destAssetChainID: 'celestia',
  amountIn: '1000000', // 1 uusdc
  smartSwapOptions: {
    splitRoutes: true,
  },
});

Understanding the Route Response

The route response contains a ton of information about how the user can move from their source chain & token to their destination. For more information, see /v2/fungible/route reference docs.

The most basic outputs you will need are:

  • amountOut: The estimated amount your user will receive after their swap / transfer takes place — net of fees (e.g. bridge fees, swap fees, etc..) and price impact.
  • requiredChainAddresses: These are the chainIDs for which you will need to pass a valid user address when generating the transaction.
  • operations: The set of steps that take place to get the user from their source chain & token to their destination chain & token

Adding a fee for yourself

You can your set your own swap fee into the quote by setting cumulativeAffiliateFeeBPS to the total fee amount you want to collect from the user, measured in hundredths of a percent (aka “bips”).

You can find more info about setting fees in Setting Affiliate Fees

5. Get Required Addresses

With the route response available, we can now use our signer to retrieve the list of chains where user or receiver addresses are required to execute 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 store the address(es) somewhere and have a 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),
    };
  })
);

Two things to note about this code:

  1. The userAddresses array contains objects with {chainID, address}
  2. The userAddresses array needs to have the same order and length as the requiredChainAddresses array in the route response

Never attempt to derive an address on one chain from an address on another chain

Whenever you need a user address, please request it from the corresponding wallet or signer. Do not attempt to use bech32 cross-chain derivation.

If you attempt to derive an address on one chain from an address on another chain, you may derive an address that the user cannot actually sign for if the two chains have different address-derivation processes. For example, if you derive a Cosmos address from an Ethereum address, you will get an address that the user cannot sign for and thus risk lost tokens.

4. Execute the route!

Finally, just use the SkipClient.executeRoute method to create, sign, submit, and track the transaction for you.

Simply add the following snippet below your retrieval code to execute the route:

await skipClient.executeRoute({
  route,
  userAddresses,
  onTransactionCompleted: (chainID, txHash, status) => {
    console.log(
      `Route completed with tx hash: ${txHash} & status: ${status.state}`
    );
  },
});

After the transaction completes, you’ll have additional TIA in your Celestia address!

executeRoute callbacks

executeRoute takes care of all the complexity between getting a quote & executing a transaction: creating the tx, signing the tx, submitting it, and tracking it across all the chains.

You can supply a variety of callbacks to hook into the process and update your UI or retrieve information while it’s executing.

These callbacks include:

  • onTransactionBroadcast(txInfo {txHash, chainID}): Executes after the transaction that the user signs gets broadcast on chain.
  • onTransactionTracked(txInfo, {txHash, chainID}): Executes after the transaction that the user signs is successfully registered for tracking.
  • onTransactionCompleted(chainID, txHash, status): Executes after all of the operations triggered by a user’s signature complete. For example, in this case, it executed after the transfer to Celestia completed. (For multi-tx routes that require multiple user signatures, this will be called once for each tx)

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.