Multi-chain intents let you make cross-chain transactions.
Those intents specify both the target (destination) and the source chain(s). The actual transaction (fill) happens on the target chain, while the funds are taken on the source chains (claims). Specifying source chains is optional — Rhinestone finds the optimal path automatically.
Multi-chain intents work similarly to single-chain intents, but with the introduction of settlement layers. From your perspective, making cross-chain transactions is as simple as making normal Ethereum transactions. The Rhinestone Orchestrator handles path finding, liquidity management, and intent execution for you.
Learn more about how Rhinestone intents work here.
Example
We will make a cross-chain token transfer using multi-chain intents.
const amount = parseUnits('100', 6) // 100 USDC
const transferTransaction = await rhinestoneAccount.sendTransaction({
sourceChains: [base],
targetChain: arbitrum,
calls: [
{
to: 'USDC',
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [recipient, amount],
}),
},
],
tokenRequests: [
{
address: 'USDC',
amount,
},
],
})
Under the hood, this will:
- Get a quote from the Rhinestone backend
- Sign the intent using the account owner(s)
- Submit the intent
- Execute the claim transaction on Arbitrum
- Execute the fill transaction on Base
Source Chain
Providing the source chain deploys the account on that chain, as well as uses the funds on that chain to fulfill the intent.
If you already have an account deployed on one or more source chains, you can omit the sourceChain. In that case, the orchestrator will use the best chain(s) to source funds.
Token Requests
tokenRequests is a list of token assets and their amounts that are required on the target chain to make the transaction. It tells the solvers to ensure those assets are present before executing the transaction calls. If you don’t need any assets on the target chain, you can omit this.
Gas Limit
You can override the default gas limit for the target chain execution with gasLimit. Doing this will make the intent better priced, because we can more accurately calculate the fee that a solver needs to be reimbursed with for paying the gas. If this is not provided, we calculate using a gas limit of 1_000_000.
const transaction = await rhinestoneAccount.sendTransaction({
sourceChains: [base],
targetChain: arbitrum,
calls: [
// …
],
tokenRequests: [
// …
],
gasLimit: 200_000n,
})
Source Assets
You can specify what token (or tokens) to use as an input asset:
const transaction = await rhinestoneAccount.sendTransaction({
sourceChains: [base],
targetChain: arbitrum,
calls: [
// …
],
tokenRequests: [
// …
],
sourceAssets: ['USDC', 'ETH']
})
You can also specify source assets on a per-chain basis:
const transaction = await rhinestoneAccount.sendTransaction({
targetChain: arbitrum,
calls: [
// …
],
tokenRequests: [
// …
],
sourceAssets: {
[base.id]: ['USDC'],
[optimism.id]: ['0x4200000000000000000000000000000000000006'], // WETH
},
})
Chain-specific sourceAssets take precendence over sourceChain parameter.
Auxiliary Funds
In case you have funds not available immediately for an intent (e.g. locked in a vault or sitting in an exchange), you can specify them as auxiliary funds:
const transaction = await rhinestoneAccount.prepareTransaction({
targetChain: arbitrum,
calls: [
// …
],
tokenRequests: [
// …
],
auxiliaryFunds: {
[base.id]: {
[USDC_ADDRESS]: parseUnits('100', 6),
[WETH_ADDRESS]: parseUnits('1', 18)
}
}
})
This lets you get a quote before getting the funds ready on the account.
Wait for Execution
sendTransaction returns a pending intent. To wait until it gets executed, use waitForExecution:
const transaction = await rhinestoneAccount.sendTransaction({
// …
})
const transactionResult = await rhinestoneAccount.waitForExecution(transaction)
By default, intents are pre-confirmed by a relayer to be executed before they actually land onchain. You can opt out of this by waiting for onchain execution:
const acceptPreconfirmations = false;
const transactionResult = await rhinestoneAccount.waitForExecution(transaction, acceptPreconfirmations)
Get Intent Status
You can also fetch the intent status directly to implement a custom polling logic:
const transaction = await rhinestoneAccount.sendTransaction({
// …
})
const transactionResult = await rhinestone.getIntentStatus(transaction.id)