buildComposable
The buildComposable
method creates advanced transactions with runtime parameter injection, allowing values to be determined at execution time rather than when building the transaction.
What is Composability?
Composability in blockchain transactions refers to the ability to create complex operations by combining simpler ones in ways that allow them to interact with each other. In the context of the AbstractJS SDK, composability takes this concept further by enabling:
-
Runtime Value Resolution: Instead of hardcoding values that might change between transaction creation and execution, composable transactions can use values determined at the exact moment of execution.
-
Cross-Chain Data Flow: Values and results from one chain can be used as inputs on another chain, enabling true cross-chain applications.
-
Transaction Dependencies: Later transactions can use the outputs from earlier ones, creating chains of dependent operations that would be impossible with traditional transactions.
-
Conditional Execution Logic: Transactions can adapt to the current state of the blockchain at execution time, rather than using potentially outdated information.
Why is Composability Important?
Composability solves several critical problems in blockchain development:
-
Reduced Transaction Failures: By using real-time values instead of hardcoded ones, transactions are less likely to fail due to changed conditions.
-
Improved Capital Efficiency: Exact amounts can be used in subsequent transactions, eliminating the need for over-allocation of funds as a safety buffer.
-
Enhanced UX: Users don't need to sign multiple transactions or manually coordinate between chains, as composable transactions can handle complex sequences automatically.
-
DeFi Optimization: Creates opportunity for zero-slippage cross-chain operations by precisely using outputs from one transaction as inputs to another.
-
Developer Productivity: Complex multi-step operations can be created as a single logical unit, reducing code complexity and error potential.
When to Use buildComposable
vs build
Use build
When:
- You need simple, static transactions with known values
- Your transactions don't depend on outputs from other operations
- You're performing basic operations within a single chain
- You're using pre-determined amounts that won't change
Example:
// Simple static transaction - good for build()
const simpleTx = await mcNexus.build({
type: "default",
data: {
chainId: optimism.id,
calls: [{
to: "0xContract",
value: parseEther("0.1"), // Static, known value
data: "0xCalldata"
}]
}
});
Use buildComposable
When:
- You need runtime values that can't be known in advance
- Your transaction depends on the results of previous transactions
- You're working with token balances that may change
- You need to reference values across chains
- You need to encode complex data that includes runtime components
Example:
// Dynamic transaction with runtime values - needs buildComposable()
const dynamicTx = await mcNexus.buildComposable({
type: "default",
data: {
to: mcUSDC.addressOn(chainId),
abi: erc20Abi,
functionName: "transfer",
args: [
recipient,
runtimeERC20BalanceOf({ // Use current balance at execution time
targetAddress: mcNexus.addressOn(chainId),
tokenAddress: mcUSDC.addressOn(chainId)
})
],
chainId: chainId
}
});
Real-World Use Cases
Composable transactions shine in these scenarios:
-
Bridge & Use Pattern: Bridge tokens to a destination chain and immediately use them without knowing the exact received amount in advance.
-
Swap & Stake Workflows: Swap tokens on a DEX and stake the exact result without needing a safety buffer.
-
Asset Rebalancing: Move funds between chains based on current balances to maintain desired ratios.
-
Cross-Chain Arbitrage: Execute multi-step trading strategies across different chains based on real-time amounts.
-
Gas Optimization: Pay for gas with tokens on one chain while executing transactions on another.
Overview
Composable transactions enable:
- Using real-time token balances instead of hardcoded amounts
- Referencing outputs from previous transactions
- Adding validation constraints to ensure safe execution
- Creating complex cross-chain workflows
Parameters
Parameter | Type | Required | Description |
---|---|---|---|
type | string | Yes | The type of composable action ("default" , "transfer" , "approval" , "batch" , "rawCalldata" , "transferFrom" , "approve" ) |
data | object | Yes | Type-specific configuration parameters |
Type-Specific Data Parameters
Depending on the type
parameter, the data
object requires different fields:
"default"
to
:Address
- The target contract addressabi
:Abi
- The ABI of the contractfunctionName
:string
- The function to callargs
:Array<any>
- Function arguments, can include runtime valueschainId
:number
- The chain ID for executiongasLimit?
:bigint
- Optional gas limitvalue?
:bigint
- Optional native token value
"transfer"
recipient
:Address
- The recipient addresstokenAddress
:Address
- The token contract addressamount
:bigint
or runtime value - Amount to transferchainId
:number
- The chain ID for execution
"batch"
instructions
:Instruction[]
- Array of instructions to execute in batch
"rawCalldata"
to
:Address
- The target contract addresscalldata
:string
- The encoded calldatachainId
:number
- The chain ID for execution
Runtime Functions
Function | Purpose |
---|---|
runtimeERC20BalanceOf | Gets token balance at execution time |
runtimeEncodeAbiParameters | Encodes complex data with runtime values |
Examples
Basic Token Transfer
This example shows a simple ERC20 transfer using the current balance at execution time:
const transferInstruction = await mcNexus.buildComposable({
type: "transfer",
data: {
recipient: recipientAddress,
tokenAddress: mcUSDC.addressOn(chainId),
amount: runtimeERC20BalanceOf({
targetAddress: mcNexus.addressOn(chainId),
tokenAddress: mcUSDC.addressOn(chainId)
}),
chainId: chainId
}
});
Contract Interaction with Runtime Values
This example shows calling a contract function with runtime parameters:
const approveInstruction = await mcNexus.buildComposable({
type: "default",
data: {
to: mcUSDC.addressOn(chainId),
abi: erc20Abi,
functionName: "approve",
args: [
uniswapRouterAddress,
runtimeERC20BalanceOf({
targetAddress: mcNexus.addressOn(chainId),
tokenAddress: mcUSDC.addressOn(chainId),
constraints: [greaterThanOrEqualTo(parseUnits("0.01", 6))]
})
],
chainId: chainId
}
});
Passing Runtime Values in Arrays
This example shows how to use runtime values inside complex data structures:
const complexInstruction = await mcNexus.buildComposable({
type: "default",
data: {
to: contractAddress,
abi: CONTRACT_ABI,
functionName: "transferFundsWithRuntimeParamInsideArray",
args: [
[address1, address2], // First array argument (static)
[
runtimeERC20BalanceOf({ // Second array argument (dynamic)
targetAddress: address1,
tokenAddress: mcUSDC.addressOn(chainId),
constraints: [greaterThanOrEqualTo(parseUnits("0.01", 6))]
})
]
],
chainId: chainId
}
});
Encoding Complex Parameters
This example shows encoding complex types with runtime values:
const encodedParamsInstruction = await mcNexus.buildComposable({
type: "default",
data: {
to: contractAddress,
abi: CONTRACT_ABI,
functionName: "foo",
args: [
param1,
param2,
runtimeEncodeAbiParameters(
[
{ name: "x", type: "uint256" },
{ name: "y", type: "uint256" },
{ name: "z", type: "bool" }
],
[
420n, // Static value
runtimeERC20BalanceOf({ // Dynamic value
targetAddress: accountAddress,
tokenAddress: tokenAddress,
constraints: []
}),
true // Static value
]
),
param4,
param5
],
chainId: chainId
}
});
Using Raw Calldata
This example shows how to use pre-encoded calldata:
const rawCalldata = encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: [spenderAddress, amount]
});
const rawCalldataInstruction = await mcNexus.buildComposable({
type: "rawCalldata",
data: {
to: tokenAddress,
calldata: rawCalldata,
chainId: chainId
}
});
Batching Multiple Instructions
This example shows how to batch multiple instructions together:
// Create multiple instructions
const transferInstruction = await mcNexus.buildComposable({
type: "transfer",
data: {
recipient: recipientAddress,
tokenAddress: mcUSDC.addressOn(chainId),
amount: amount,
chainId: chainId
}
});
const contractInstruction = await mcNexus.buildComposable({
type: "default",
data: {
to: contractAddress,
abi: CONTRACT_ABI,
functionName: "foo",
args: [param1, param2],
chainId: chainId
}
});
// Batch them together
const batchedInstructions = await mcNexus.buildComposable({
type: "batch",
data: {
instructions: [...transferInstruction, ...contractInstruction]
}
});
Complete DeFi Example: Approve and Swap
This example shows a typical DeFi flow of approving and swapping tokens using runtime values:
// 1. Approve tokens for Uniswap
const approveInstruction = await mcNexus.buildComposable({
type: "default",
data: {
to: inToken.addressOn(chainId),
abi: erc20Abi,
functionName: "approve",
args: [
uniswapRouterAddress,
runtimeERC20BalanceOf({
targetAddress: mcNexus.addressOn(chainId),
tokenAddress: inToken.addressOn(chainId)
})
],
chainId: chainId
}
});
// 2. Swap tokens on Uniswap
const swapInstruction = await mcNexus.buildComposable({
type: "default",
data: {
to: uniswapRouterAddress,
abi: UniswapSwapRouterAbi,
functionName: "exactInputSingle",
args: [{
tokenIn: inToken.addressOn(chainId),
tokenOut: outToken.addressOn(chainId),
fee: 3000,
recipient: mcNexus.addressOn(chainId),
deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
amountIn: runtimeERC20BalanceOf({
targetAddress: mcNexus.addressOn(chainId),
tokenAddress: inToken.addressOn(chainId)
}),
amountOutMinimum: parseUnits("0.0001", 18),
sqrtPriceLimitX96: 0n
}],
chainId: chainId
}
});
// Execute both instructions
const { hash } = await meeClient.executeQuote({
quote: await meeClient.getQuote({
instructions: [approveInstruction, swapInstruction],
feeToken: {
address: feeToken.addressOn(chainId),
chainId: chainId
}
})
});
Using Constraints
You can add constraints to runtime values to ensure they meet specific conditions:
runtimeERC20BalanceOf({
targetAddress: accountAddress,
tokenAddress: tokenAddress,
constraints: [
greaterThanOrEqualTo(parseUnits("0.01", 6)) // Ensure balance is at least 0.01 USDC
]
})
Available constraints include:
greaterThanOrEqualTo(value)
lessThanOrEqualTo(value)
equalTo(value)
notEqualTo(value)
When to Use Composable Transactions
Composable transactions are ideal for:
- Working with actual token balances at execution time
- Creating multi-step DeFi workflows that depend on previous steps
- Ensuring transactions have appropriate failsafes through constraints
- Building cross-chain operations that require dynamic parameters