Skip to main content

Chain Abstraction

Chain Abstraction in WalletKit enables users with stablecoins on any network to spend them on-the-fly on a different network. Our Chain Abstraction solution provides a toolkit for wallet developers to integrate this complex functionality using WalletKit.

For example, when an app requests a 100 USDC payment on Base network but the user only has USDC on Arbitrum, WalletKit offers methods to detect this mismatch, generate necessary transactions, track the cross-chain transfer, and complete the original transaction after bridging finishes.

note

Chain abstraction is currently in experimental phase and requires the @ChainAbstractionExperimentalApi annotation.

How It Works

When a dapp requests a transaction through eth_sendTransaction, you need to:

  1. Detect if chain abstraction is needed
  2. Generate necessary bridging transactions
  3. Handle transaction execution and monitoring
  4. Complete the original transaction once bridging is done

The following sequence diagram illustrates the complete flow of a chain abstraction operation, from the initial dapp request to the final transaction confirmation

Chain Abstraction Flow

Methods

Following are the methods from WalletKit that you will use in implementing chain abstraction.

Prepare

This method checks if a transaction requires additional bridging transactions beforehand.

public prepare(params: {
transaction: ChainAbstractionTypes.Transaction;
}): Promise<ChainAbstractionTypes.CanFulfilResponse>;

Status

Helper method used to check the fulfilment status of a bridging operation.

public abstract status(params: {
fulfilmentId: string;
}): Promise<ChainAbstractionTypes.FulfilmentStatusResponse>;

Usage

When your wallet receives an eth_sendTransaction request, first check if chain abstraction is needed using the prepare method, if it is, you need to sign all the fulfilment transactions and broadcast them in parallel. After that, you need to call the status method to check the status of the fulfillment operation.

If the operation is successful, you need to broadcast the initial transaction and await for the transaction hash and receipt. If the operation is not successful, you need to send the JsonRpcError to the dapp and display the error to the user.

walletkit.on("session_request", async (event) => {
const {
id,
topic,
params: { request, chainId },
} = event;

if (request.method === "eth_sendTransaction") {
const originalTransaction = request.params[0];

// Check if bridging transactions are required
const prepareResponse = await wallet.prepare({
transaction: {
...originalTransaction,
chainId,
},
});

if (prepareResponse.status === "error") {
// Display error to user and respond to dapp
await wallet.respondSessionRequest({
topic,
response: formatJsonRpcResult(id, prepareResponse.reason),
});
return;
}

if (prepareResponse.status === "available") {
const { transactions, funding, fulfilmentId, checkIn } = prepareResponse.data;

// Display bridging information to user
// Once approved, execute bridging transactions
for (const transaction of transactions) {
const signedTransaction = await wallet.signTransaction(transaction);
await wallet.sendTransaction(signedTransaction);
}

// Monitor bridging status
const statusInterval = setInterval(async () => {
const statusReponse = await wallet.status({ fulfilmentId });

if (statusResponse.status === "completed") {
clearInterval(statusInterval);
// Proceed with original transaction
const signedTransaction = await wallet.signTransaction(originalTransaction);
const result = await wallet.sendTransaction(signedTransaction);

await wallet.respondSessionRequest({
topic,
response: formatJsonRpcResult(id, result)
});
}
}, checkIn);
} else {
// No bridging required, process transaction normally
const signedTransaction = await wallet.signTransaction(originalTransaction);
const result = await wallet.sendTransaction(signedTransaction);

await wallet.respondSessionRequest({
topic,
response: formatJsonRpcResult(id, result)
});
}
}
});

Types

Following are the types that are used in the chain abstraction methods.

namespace ChainAbstractionTypes {
type FundingFrom = {
tokenContract: string;
amount: string;
chainId: string;
symbol: string;
};

type Transaction = {
from: string;
to: string;
value: string;
chainId: string;
gas?: string;
gasPrice?: string;
data?: string;
nonce?: string;
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
};

type CanFulfilResponse =
| {
status: "not_required";
}
| {
status: "available";
data: {
fulfilmentId: string;
checkIn: number;
transactions: Transaction[];
funding: FundingFrom[];
};
}
| {
status: "error";
reason: string; // reason can be insufficientFunds | insufficientGasFunds | noRoutesAvailable
};

type FulfilmentStatusResponse = {
createdAt: number;
} & (
| {
status: "completed";
}
| { status: "pending"; checkIn: number }
);
}

For example, check our sample wallet implementation with React Native.