đź’ˇ Chain Abstraction is in early access.

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.

How It Works

Apps need to pass gas as null, while sending a transaction to allow proper gas estimation by the wallet. Refer to this guide for more details.

When sending a transaction, you need to:

  1. Check if the required chain has enough funds to complete the transaction
  2. If not, use the prepare method to generate necessary bridging transactions
  3. Sign routing and initial transaction hashes, prepared by the prepare method
  4. Use execute method to broadcast routing and initial transactions and wait for it to be completed

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

The following methods from WalletKit are used in implementing chain abstraction.

đź’ˇ Chain abstraction is currently in the early access phase

Prepare

This method is used to check if chain abstraction is needed. If it is, it will return a PrepareDetailedResponseSuccessCompat object with the necessary transactions and funding information. If it is not, it will return a PrepareResponseNotRequiredCompat object with the original transaction.

Future<PrepareDetailedResponseCompat> prepare({
  required String chainId,
  required String from,
  required CallCompat call,
  Currency? localCurrency,
});

Execute

This method is used to execute the chain abstraction operation. The method will handle broadcasting all transactions in the correct order and monitor the cross-chain transfer process. It returns an ExecuteDetails object with the transaction status and results.

Future<ExecuteDetailsCompat> execute({
  required UiFieldsCompat uiFields,
  required List<String> routeTxnSigs,
  required String initialTxnSig,
})

Usage

When sending a transaction, first check if chain abstraction is needed using the prepare method. Call the execute method to broadcast the routing and initial transactions and wait for it to be completed.

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

final response = await _walletKit.prepare(
  chainId: chainId, // selected chain id
  from: from, // sender address
  call: CallCompat(
    to: to, // contract address
    input: input, // calldata
  ),
);
response.when(
  success: (PrepareDetailedResponseSuccessCompat deatailResponse) {
    deatailResponse.when(
      available: (UiFieldsCompat uiFieldsCompat) {
        // If the route is available, present a CA transaction UX flow and sign hashes when approved
        final TxnDetailsCompat initial = uiFieldsCompat.initial;
        final List<TxnDetailsCompat> route = uiFieldsCompat.route;
        
        final String initialSignature = signHashMethod(initial.transactionHashToSign);
        final List<String> routeSignatures = route.map((route) {
          final String rSignature = signHashMethod(route.transactionHashToSign);
          return rSignature;
        }).toList();

        await _walletKit.execute(
          uiFields: uiFields,
          initialTxnSig: initialSignature,
          routeTxnSigs: routeSignatures,
        );
      },
      notRequired: (PrepareResponseNotRequiredCompat notRequired) {
        // user does not need to move funds from other chains
        // proceeds as normal transaction with notRequired.initialTransaction
      },
    );
  },
  error: (PrepareResponseError prepareError) {
    // Show an error
    // contains prepareError.error as BridgingError and could be either:
    // noRoutesAvailable, insufficientFunds, insufficientGasFunds
  },
);

Implementation during Session Request

If you are looking to trigger Chain Abstraction during a eth_sendTransaction Session Request you should do it inside the session request handler as explained in Responding to Session requests section.

Future<void> _ethSendTransactionHandler(String topic, dynamic params) async {
  final SessionRequest pendingRequest = _walletKit.pendingRequests.getAll().last;
  final int requestId = pendingRequest.id;
  final String chainId = pendingRequest.chainId;

  final transaction = (params as List<dynamic>).first as Map<String, dynamic>;

  // Intercept to check if Chain Abstraction is required
  if (transaction.containsKey('input') || transaction.containsKey('data')) {
    final inputData = transaction.containsKey('input') ?? transaction.containsKey('data');
    final response = await _walletKit.prepare(
      chainId: chainId,
      from: transaction['from'],
      call: CallCompat(
        to: transaction['to'],
        input: inputData,
      ),
    );
    response.when(
      success: (PrepareDetailedResponseSuccessCompat deatailResponse) {
        deatailResponse.when(
          available: (UiFieldsCompat uiFieldsCompat) {
            // Only if the route is available, present a Chain Abstraction approval modal 
            // and proceed with execute() method
            if (approved) {
              final TxnDetailsCompat initial = uiFieldsCompat.initial;
              final List<TxnDetailsCompat> route = uiFieldsCompat.route;
              
              final String initialSignature = signHashMethod(initial.transactionHashToSign);
              final List<String> routeSignatures = route.map((route) {
                final String rSignature = signHashMethod(route.transactionHashToSign);
                return rSignature;
              }).toList();

              final executeResponse = await _walletKit.execute(
                uiFields: uiFields,
                initialTxnSig: initialSignature,
                routeTxnSigs: routeSignatures,
              );

              // Respond to the session request. Flow shouldn't end here as the transaction was processed
              return await _walletKit.respondSessionRequest(
                topic: topic,
                response: JsonRpcResponse(
                  id: requestId, 
                  jsonrpc: '2.0', 
                  result: executeResponse.initialTxnReceipt,
                ),
              );
            }
          },
          // If deatailResponse is not `available` type
          // then let the flow to continue to regular send transacrion
        );
      },
    );
  }

  // display a prompt for the user to approve or reject the request
  // if approved
  if (approved) {
    final signedTx = await sendTransaction(transaction, int.parse(chainId));
    // respond to requester
    await _walletKit.respondSessionRequest(
      topic: topic,
      response: JsonRpcResponse(
        id: requestId, 
        jsonrpc: '2.0', 
        result: signedTx,
      ),
    );
  }

  // if rejected
  return _walletKit.respondSessionRequest(
    topic: topic,
    response: JsonRpcResponse(
      id: id,
      jsonrpc: '2.0',
      error: const JsonRpcError(code: 5001, message: 'User rejected method'),
    ),
  );
}

For example, check out implementation of chain abstraction in sample wallet with Flutter.

Token Balance

You can use this method to query the token balance of the given address

Future<String> erc20TokenBalance({
  required String chainId, // chain id
  required String token, // token address
  required String owner, // user address
})

Android

In your android (project’s) build.gradle file add support for Jitpack:

allprojects {
    repositories {
        google()
        mavenCentral()
        maven { url 'https://jitpack.io' } // <- add jipack url
    }
}

It shouldn’t happen but if you encounter issues with minification, add the below rules to your application:

-keepattributes *Annotation*
-keep class com.sun.jna.** { *; }
-keepclassmembers class com.sun.jna.** {
    native <methods>;
    *;
}
-keep class uniffi.** { *; }
# Preserve all public and protected fields and methods
-keepclassmembers class ** {
    public *;
    protected *;
}
-dontwarn uniffi.**
-dontwarn com.sun.jna.**

Error Handling

When implementing Chain Abstraction, you may encounter different types of errors. Here’s how to handle them effectively:

Application-Level Errors

These errors (PrepareError) indicate specific issues that need to be addressed and typically require user action:

  • Insufficient Gas Fees: User needs to add more gas tokens to their wallet
  • Malformed Transaction Requests: Transaction parameters are invalid or incomplete
  • Minimum Bridging Amount Not Met: Currently set at $0.60
  • Invalid Token or Network Selection: Selected token or network is not supported

When handling these errors, you should display clear, user-friendly error messages that provide specific guidance on how to resolve the issue. Allow users to modify their transaction parameters and consider implementing validation checks before initiating transactions.

Retryable Errors

These errors (Result::Err) indicate temporary issues that may be resolved by retrying the operation. Examples of these types of issues include network connection timeouts, TLS negotiation issues, service outages, or other transient errors.

For retryable errors, show a generic “oops” message to users and provide a retry button. Log detailed error information to your error tracking service, but avoid displaying technical details to end users.

For errors in the execute() method, a retry may not resolve the issue. In such cases, allow users to cancel the transaction, return them to the application, and let the application initiate a new transaction.

Critical Errors

Critical errors indicate bugs or implementation issues that should be treated as high-priority incidents: incorrect usage of WalletKit API, wrong data encoding or wrong fields passed to WalletKit, or WalletKit internal bugs.

Testing

Best way to test Chain Abstraction is to use our Sample wallet.

You can also use the AppKit laboratory and try sending USDC/USDT with any chain abstraction-supported wallet.