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.

How It Works

note

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. Check the status of the bridging transactions and wait for them to be completed
  4. Once the bridging transactions are completed, execute the initial transaction

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.

note

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

Prepare

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

@ChainAbstractionExperimentalApi
fun prepare(
initialTransaction: Wallet.Model.InitialTransaction,
onSuccess: (Wallet.Model.PrepareSuccess) -> Unit,
onError: (Wallet.Model.PrepareError) -> Unit
)

Status

This method is used to check the status of the fulfillment operation. It will return a Status.Completed object if the operation completed successfully or a Status.Error object if it encountered an error.

@ChainAbstractionExperimentalApi
fun status(
fulfilmentId: String,
checkIn: Long,
onSuccess: (Wallet.Model.Status.Completed) -> Unit,
onError: (Wallet.Model.Status.Error) -> Unit
)

Estimate Fees

This method is used to estimate the fees for the fulfillment operation.

@Throws(Exception::class)
@ChainAbstractionExperimentalApi
fun estimateFees(chainId: String): Wallet.Model.EstimatedFees

Get ERC20 Balance

This method is used to get the balance of an ERC20 token on a specific chain.

@Throws(Exception::class)
@ChainAbstractionExperimentalApi
fun getERC20Balance(chainId: String, tokenAddress: String, ownerAddress: String): String

Get Fulfilment Details

This method is used to get the details of the fulfillment operation.

@ChainAbstractionExperimentalApi
fun getTransactionsDetails(
available: Wallet.Model.PrepareSuccess.Available,
onSuccess: (Wallet.Model.TransactionsDetails) -> Unit,
onError: (Wallet.Model.Error) -> Unit
)

Usage

When sending a transaction, first check if chain abstraction is needed using the prepare method. If it is needed, you must sign all the fulfillment 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, broadcast the initial transaction and await the transaction hash and receipt. If the operation is unsuccessful, send the JsonRpcError to the dapp and display the error to the user.

    val initialTransaction = Wallet.Model.Transaction(...)

WalletKit.prepare(
initialTransaction,
onSuccess = { prepareSuccess ->
when (prepareSuccess) {
is Wallet.Model.PrepareSuccess.Available -> {
//Sign all the fulfilment transactions
//Broadcast the fulfilment transactions in parallel
//Await for the transaction hashes and receipts

//Call the status
WalletKit.status(fulfilmentId, checkIn,
onSuccess = {
//Successfull filfilment operation
//Broadcast the inittial transaction
//Await for the tx hash ande receipt
//Send the response to the Dapp
},
onError = {
//Status error - wallet should send the JsonRpcError to a dapp for given request and display error to the user
}
)
}

is Wallet.Model.PrepareSuccess.NotRequired -> {
//Chain abstraction is not required, handle transaction as usual
}
}
},
onError = { prepareError ->
// One of the possible errors: NoRoutesAvailable, InsufficientFunds, InsufficientGasFunds - wallet should send the JsonRpcError to a dapp for given request and display error to the user
}
)

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

Testing

To test Chain Abstraction, you can use the AppKit laboratory and try sending USDC with any chain abstraction supported wallet. You can also use this sample wallet for testing.

Types

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

 data class Transaction(
var from: String,
var to: String,
var value: String,
var gasLimit: String,
var input: String,
var nonce: String,
var chainId: String
)

data class InitialTransaction(
var from: String,
var to: String,
var value: String,
var input: String,
var chainId: String
)

data class FeeEstimatedTransaction(
var from: String,
var to: String,
var value: String,
var gasLimit: String,
var input: String,
var nonce: String,
var maxFeePerGas: String,
var maxPriorityFeePerGas: String,
var chainId: String
)

sealed class PrepareSuccess {
data class Available(
val fulfilmentId: String,
val checkIn: Long,
val transactions: List<Transaction>,
val initialTransaction: Transaction,
val initialTransactionMetadata: InitialTransactionMetadata,
val funding: List<FundingMetadata>
) : PrepareSuccess()

data class NotRequired(val initialTransaction: Transaction) : PrepareSuccess()
}

data class InitialTransactionMetadata(
var symbol: String,
var amount: String,
var decimals: Int,
var tokenContract: String,
var transferTo: String
)

data class FundingMetadata(
var chainId: String,
var tokenContract: String,
var symbol: String,
var amount: String,
var bridgingFee: String,
var decimals: Int
)

sealed class PrepareError : Model() {
data object NoRoutesAvailable : PrepareError()
data object InsufficientFunds : PrepareError()
data object InsufficientGasFunds : PrepareError()
data class Unknown(val message: String) : PrepareError()
}

sealed class Status : Model() {
data class Completed(val createdAt: Long) : Status()
data class Error(val reason: String) : Status()
}

data class TransactionsDetails(
var fulfilmentDetails: List<TransactionDetails>,
var initialDetails: TransactionDetails,
var bridgeFees: List<TransactionFee>,
var localBridgeTotal: Amount,
var localFulfilmentTotal: Amount,
var localTotal: Amount
)

data class TransactionDetails(
var transaction: FeeEstimatedTransaction,
var transactionFee: TransactionFee
)

data class TransactionFee(
var fee: Amount,
var localFee: Amount
)

data class Amount(
var symbol: String,
var amount: String,
var unit: String,
var formatted: String,
var formattedAlt: String
)

ProGuard rules

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