Skip to content

Zero to Hero Guide: Building a Chain Abstracted Application

Ca

This will be a zero-to-hero introduction to the Klaster SDK. Here, we will explore all of the main concepts pertaining to the development of chain abstracted apps and flows.

In this tutorial, you will get all the required steps to build a chain abstracted application. If you're interested in what a chain abstracted application looks like, check out our video on AbstraktAAVE - a chain abstracted implementation of AAVE.

You can also use the app yourself at:

https://demo-aave.klaster.io

Install Klaster and Viem

npm i klaster-sdk viem

Create a signer object

Usign viem, create a signer object - which will be used to sign transactions.

import {
  buildMultichainReadonlyClient,
  buildRpcInfo,
  initKlaster,
  klasterNodeHost,
  loadBicoV2Account,
} from "klaster-sdk";
import { createWalletClient, custom, http } from "viem";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import { arbitrum, base, optimism, polygon, scroll } from "viem/chains";
 
// When in browser environment create a signer
// which uses the injected wallet (e.g. MetaMask, Rabby, ...)
const signer = createWalletClient({
  transport: custom((window as any).ethereum),
});
 
// When in non-browser environment you can use private key
// to sign messages.
const privateKey = generatePrivateKey();
const signerAccount = privateKeyToAccount(privateKey);
const signer = createWalletClient({
  transport: http(),
});

Initialize Klaster SDK

To initialize the Klaster SDK, you must provide it with the following information:

  • Klaster Node URL
  • ERC4337 Account Provider

You can use multiple account providers (check documentation reference for supported account providers). In this tutorial, we will use the Biconomy account provider:

// Fetch the address from viem. You might need to call signer.requestAddresses()
// depending on your injected Web3 provider.
const [address] = await signer.getAddresses();
 
// Initializing the Klaster SDK with Biconomy as the smart contract account
// provider.
const klaster = await initKlaster({
  accountInitData: loadBicoV2Account({
    owner: address, // Fetch
  }),
  nodeUrl: klasterNodeHost.default,
});

Initialize a multichain readonly RPC client

In order to achieve certain functionality for chain abstraction, such as getting a unified token balance across chains or encoding bridging actions - you will need to have a public RPC for every chain you wish to interact with. Do note that this is only a public RPC, you don't need to have the abilty to write to chain - as this is handled by the Klaster nodes.

// Build a multichain client by calling the buildMultichainReadonlyClient
// and passing the buildRpcInfo objects into the array
const mcClient = buildMultichainReadonlyClient([
  buildRpcInfo(optimism.id, "<op-rpc-url>"),
  buildRpcInfo(base.id, "<base-rpc-url>"),
]);

Since this is a tutorial project, we can use the viem default RPCs.

// This is a hacky way of doing this, just to get you quickly started
// with the tutorial. Please don't initialize the client like this in
// production projects.
const mcClient = buildMultichainReadonlyClient(
  [optimism, base, polygon, arbitrum, scroll].map((x) => {
    return {
      chainId: x.id,
      rpcUrl: x.rpcUrls.default.http[0],
    };
  })
);

Map token deployments across multiple blockchains

As well as working with the Multichain RPC provider, the Klaster SDK works best when provided with MultichainTokenMapping objects. These objects represent mappings of tokens you consider equivalent across various blockchains. One example would be USDC - which is widely deployed by Circle across multiple blockchains.

You can use the Circle Token Deployments to see which address the canonical version of which token is deployed to.

However, nothing limits you to using only offical token deployments. If you consider the axlUSDC to be equivalent to cannonical USDC - you can simply map them together and the Klaster SDK will treat them as if they're the same token. You can provide one token per chain.

// Build a token mapping by calling the buildTokenMapping function and passing instances of
// deployment object into the array. These represent the chainId β†’ address mappings.
const mcUSDC = buildTokenMapping([
  deployment(optimism.id, "0x<USDC-ON-OPTIMISM-ADDRESS>"),
  deployment(base.id, "0x<USDC-ON-OPTIMISM-ADDRESS>"),
]);

The Klaster SDK exposes some multichain tokens by default. These are mcUSDC and mcUSDT. They are available to you as exported constants. They expose deployments on a lot of blockchains so it's important that you truncate them before usage only to blockchains which you use.

// A lambda which intersects the chains available in the
// token mapping with the ones available in the multichain client.
// In general, you should not be using this in a production project.
// Instead, you should create your own tokenmappings by the method shown
// above.
const intersectTokenAndClients = (
  token: MultichainTokenMapping,
  mcClient: MultichainClient
) => {
  return token.filter((deployment) =>
    mcClient.chainsRpcInfo
      .map((info) => info.chainId)
      .includes(deployment.chainId)
  );
};
 
// Store the intersection of the Klaster provided token and the chains your project is using.
const mUSDC = intersectTokenAndClients(mcUSDC, mcClient);

Create a Bridging Plugin

Klaster introduces a concept of Bridging Plugigs. Yoa can think of them as templates, which the Klaster SDK uses to encode and execute a bridging action whenever it needs to. Since Klaster is an agnostic protocol and SDK, every function that might require bridging can accept the function of type BridgePlugin as input.

For this tutorial we have prepared a LiFi bridging plugin example.

But in general, what is a Klaster Bridging Plugin?

It's a simple function which provides a data object containing things needed to encode a brdiging transaction such as sourceChainId, destinationChainId, sourceToken, destinationToken, amount, etc... and expects an object in return which tells the Klaster SDK i) which transactions need to be executed ii) how much will be received on destination.

For this tutorial, we can create a LiFi bridging plugin, which will leverage LiFi Aggregation to get us the fastest route available for every transfer. Since Klaster is primarily a user experience oriented framework, the speed of execution is paramout!

First, install the LiFi SDK

npm i @lifi/sdk

Then write the plugin:

import { getRoutes, RoutesRequest } from "@lifi/sdk";
import { Address, batchTx, BridgePlugin, rawTx } from "klaster-sdk";
import { Hex } from "viem";
 
export const liFiBrigePlugin: BridgePlugin = async (data) => {
 
  const routesRequest: RoutesRequest = {
    fromChainId: data.sourceChainId,
    toChainId: data.destinationChainId,
    fromTokenAddress: data.sourceToken,
    toTokenAddress: data.destinationToken,
    fromAmount: data.amount.toString(),
    options: {
      order: "FASTEST",
    },
  };
 
  const result = await getRoutes(routesRequest);
  const route = result.routes.at(0)
 
  if (!route) {
    throw Error('...');
  }
 
  const routeSteps = route.steps.map(step => {
    if(!step.transactionRequest) { throw Error('...') }
    const { to, gasLimit, data, value} = step.transactionRequest
    if(!to || !gasLimit || !data || !value) { throw Error('...') } 
    return rawTx({
      to: to as Address,
      gasLimit: BigInt(gasLimit),
      data: data as Hex,
      value: BigInt(value)
    })
  })
 
  return {
    receivedOnDestination: BigInt(route.toAmountMin),
    txBatch: batchTx(data.sourceChainId, routeSteps)
  }
};

It's as simple as that! Now - you Klaster SDK has LiFi support πŸ₯³

Fetching a Unified Balance

In order to achieve true chain abstraction, the user should be presented with a unified ERC20 balance across all of the chains that the app is using.

For example, in the Klaster AbstraktAAVE Demo app, you can see the unified balance: Unified

and then the breakdown per chain:

In order to achieve this, you can use the Multichain token and Multichain RPC objects from earlier:

const uBalance = await mcClient.getUnifiedErc20Balance({
  tokenMapping: mUSDC,
  account: klaster.account,
});
 
// Total balance across all used chains, expressed in base units
uBalance.balance;
 
// Breakdown of balances across each separate blockchain
uBalance.breakdown;
 
// The decimals of the token. In order for tokenMapping to be created,
// all instances must have the same number of decimals.
uBalance.decimals;

Encoding the bridging + execution actions

Okay! Finally our entire setup is complete. This is most definitely one of the more complex cases which Klaster covers - a fully chain abstracted app, so the amount of setup is a bit higher. For simpler cases, such as a unified deposit address of one-click checkout flows, a lot of these steps can be omitted.

So, for this demo, we'll say that we want to send the amount we receive from bridging on the destination chain to some other address. This in an intentionally simplistic case, and it could have been solved by just using LiFi bridge function, but for the purposes of this tutorial it just demonstrates how Klaster works.

Encoding the bridging operations

In your own project, you'll be able to encode arbitrarily complex actions across any number of blockchains. For example, in the AAVE case, after a token lands on the destination chain, the supply function is called on the AAVE Smart Contract

Anyways let's continue. The SDK is intelligent enough to calculate on its own the required steps to get some amount of tokens on some desired destination chain.

Let's assume a following situation. You have:

  • 10 USDC on Polygon
  • 20 USDC on Optimism
  • 12 USDC on Scroll

and you're trying to call some function on Base with 35 USDC while having zero USDC on Base. What the Klaster SDK will do is calculate a route for bridging your tokens:

e.g. take 20 USDC (total balance) from OP, 12 USDC (total balance) from Scroll and 3 USDC (+ additional for what was lost to bridge fees when bridging from chains where you took the total balance) from Polygon and bridge it all to Base. Once the funds arrive to Base - call the desired function there!

For most cases, the SDK will be able to calculate te exact routes for bridging to get your desired outcome. If this is not possible, you can always encode them yourself.

The function that does the calculations is called encodeBridgingOps. Simply pass all of the required parameters and as an output you'll get a series of steps (EVM Transactions) to be executed on one or more blockchains to get your desired amount onto your desired destinationChain

const bridgingOps = await encodeBridgingOps({
  tokenMapping: mUSDC,
  account: klaster.account,
  amount: uBalance.balance - parseUnits("1", uBalance.decimals), // Don't send entire balance
  bridgePlugin: liFiBrigePlugin,
  client: mcClient,
  destinationChainId: base.id,
  unifiedBalance: uBalance,
});

Encoding the desired action

Now we can encode the transfer call on the destination chain. This is done with the call to the Klaster SDK rawTx helper function:

const sendERC20Op = rawTx({
  gasLimit: 100000n,
  to: destChainTokenAddress,
  data: encodeFunctionData({
    abi: erc20Abi,
    functionName: "transfer",
    args: [recipient, bridgingOps.totalReceivedOnDestination],
  }),
});

Note one thing here, the usage of bridgingOps.totalReceivedOnDestination - this is a variable which has calculated how much assets will be available on the destination chain after bridging has completed. If you had zero on the destination then this will be the total amount available. If you had > 0 available then the total available will be

totalReceivedOnDestination + balanceOnDestination

Creating an Interchain Transaction

Now, we can use all of the transactions we have created to encode an interchain transaction or an iTx. An iTx is an object containing one or more transactions across one or more blockchains. In our case, the iTx will contain

  • All necessary approve + bridging transactions calculated by the encodeBridgingOps function - on the source chain(s)
  • The send transaction on the destination chain

To create an iTx, we use the helper function buildItx.

const iTx = buildItx({
  // BridgingOPs + Execution on the destination chain
  // added as steps to the iTx
  steps: bridgingOps.steps.concat(singleTx(destinationChainId, sendERC20Op)),
  // Klaster works with cross-chain gas abstraction. This instructs the Klaster
  // nodes to take USDC on Optimism as tx fee payment.
  feeTx: klaster.encodePaymentFee(optimism.id, "USDC"),
});

Executing the Interchain Transaction

Now that we have everything we need encoded in the iTx object, we can send it to the Klaster network for execution.

Getting a quote

Executing an Interchain transaction is a two-step process. The first part of the process is getting a quote from the Klaster Network. The quote will tell you how much the node wants to charge for the execution of the transaction in the token which you set in the feeTx field when building an iTx.

To get the quote, call:

const quote = await klaster.getQuote(iTx);
 
// The amount of USD that the node is willing to execute the iTx for
quote.tokenValue;

Signing the quote

If you're satisfied with the quoted execution price, prompt the user to sign the iTx hash. You can get the hash from the quote object. By signing the iTx hash you're giving the Klaster Nodes approval to execute the transactions within the iTx. This is cryptographically verified by smart contracts on every chain where the data is being executed - so the nodes are unable to execute anything without the explicit approval from the user.

const signed = await signer.signMessage({
  message: {
    raw: quote.itxHash,
  },
  account: signerAccount,
});

Sending the iTx for execution

After the user has signed the iTx hash, pass the QuoteResponse together with the signed hash to the execute function.

const result = await klaster.execute(quote, signed)

Tracking the execution

After the iTx has been received by the Klaster Network, you will receive a result from the execute function. In that result, you will have the iTx hash. You can go to the Klaster Explorer to check the status of the hash:

https://explorer.klaster.io/details/{iTxHash}

Conclusion

Congrats - you have sucessfully executed your first Interchain Transaction and prepared your project for chain abstraction.