Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.rhinestone.dev/llms.txt

Use this file to discover all available pages before exploring further.

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 prepared = await rhinestoneAccount.prepareTransaction({
  sourceChains: [base],
  targetChain: arbitrum,
  calls: [
    {
      to: 'USDC',
      data: encodeFunctionData({
        abi: erc20Abi,
        functionName: 'transfer',
        args: [recipient, amount],
      }),
    },
  ],
  tokenRequests: [
    {
      address: 'USDC',
      amount,
    },
  ],
})
const signed = await rhinestoneAccount.signTransaction(prepared)
const transferTransaction = await rhinestoneAccount.submitTransaction(signed)

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.prepareTransaction({
  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.prepareTransaction({
  sourceChains: [base],
  targetChain: arbitrum,
  calls: [
    // …
  ],
  tokenRequests: [
    // …
  ],
  sourceAssets: ['USDC', 'ETH']
})
You can also specify source assets on a per-chain basis:
const transaction = await rhinestoneAccount.prepareTransaction({
  targetChain: arbitrum,
  calls: [
    // …
  ],
  tokenRequests: [
    // …
  ],
  sourceAssets: {
    [base.id]: ['USDC'],
    [optimism.id]: ['0x4200000000000000000000000000000000000006'], // WETH
  },
})
Chain-specific sourceAssets take precendence over sourceChain parameter.

Settlement Layers

Rhinestone routes intents through a settlement layer. By default, the orchestrator picks the best one. Pass settlementLayers to constrain the choice — either include a specific set, or exclude one you want to avoid. Pin to a specific layer (or set of layers):
const transaction = await rhinestoneAccount.prepareTransaction({
  sourceChains: [base],
  targetChain: arbitrum,
  calls: [
    // …
  ],
  tokenRequests: [
    // …
  ],
  settlementLayers: { include: ['ACROSS'] },
})
Exclude a layer (e.g. while one is degraded) and let the orchestrator pick from the rest:
const transaction = await rhinestoneAccount.prepareTransaction({
  sourceChains: [base],
  targetChain: arbitrum,
  calls: [
    // …
  ],
  tokenRequests: [
    // …
  ],
  settlementLayers: { exclude: ['RELAY'] },
})
The same filter works on splitIntents (see Liquidity Splitting).

Choosing a different quote

prepareTransaction returns multiple candidate routes via prepared.quotes. By default, signTransaction picks prepared.quotes.best — the orchestrator’s recommended route. To pick a different one, iterate prepared.quotes.all and pass the chosen quote’s intentId to signTransaction:
const prepared = await rhinestoneAccount.prepareTransaction({ /* ... */ })

// e.g. pick the fastest route under a $1 fee cap
const fastest = prepared.quotes.all
  .filter((q) => q.cost.fees.total.usd <= 1)
  .reduce((a, b) =>
    a.estimatedFillTime.seconds <= b.estimatedFillTime.seconds ? a : b
  )

const signed = await rhinestoneAccount.signTransaction(prepared, {
  intentId: fastest.intentId,
})
const transaction = await rhinestoneAccount.submitTransaction(signed)
Each quote has its own signData — signatures from one quote won’t verify against another. Passing { intentId } tells the SDK to sign the matching quote.

Auxiliary Funds

auxiliaryFunds declares balances that aren’t visible to the orchestrator yet but will be available by the time the intent settles. Use it to quote ahead of an inflow — a pending CEX deposit, a vault withdrawal, an unstake, or any other balance that will arrive before fill.
const transaction = await rhinestoneAccount.prepareTransaction({
  targetChain: arbitrum,
  calls: [
    // …
  ],
  tokenRequests: [
    // …
  ],
  auxiliaryFunds: {
    [base.id]: {
      [USDC_ADDRESS]: parseUnits('100', 6),
      [WETH_ADDRESS]: parseUnits('1', 18)
    }
  }
})
Don’t list funds the account already holds — the orchestrator picks those up automatically, and adding them via auxiliaryFunds double-counts the balance and inflates the input amount in the quote.
For tokens produced by a source call (a vault exit, unwrap, unstake), declare them via provides on the call itself — not here.

Wait for Execution

submitTransaction returns once the intent has been accepted by the orchestrator. To wait until it actually settles onchain, use waitForExecution:
const transaction = await rhinestoneAccount.submitTransaction(signed)
const transactionResult = await rhinestoneAccount.waitForExecution(transaction)

Get Intent Status

You can also fetch the intent status directly to implement a custom polling logic:
const transaction = await rhinestoneAccount.submitTransaction(signed)
const transactionResult = await rhinestone.getIntentStatus(transaction.id)

Liquidity Splitting

When transferring large token amounts, a single settlement layer may not have enough liquidity to fill the intent efficiently. Use splitIntents to break the amount into smaller portions that can be routed through different settlement layers in parallel, improving execution speed and pricing. First, compute the splits:
const splits = await rhinestone.splitIntents({
  chain: base,
  tokens: {
    [USDC_ADDRESS]: parseUnits('100000', 6), // 100k USDC
  },
})
Then, send each split as a separate transaction:
async function sendIntent(split) {
  const prepared = await rhinestoneAccount.prepareTransaction({
    sourceChains: [optimism],
    targetChain: base,
    tokenRequests: Object.entries(split.tokens).map(([address, amount]) => ({
      address,
      amount,
    })),
    calls: [
      // …
    ],
  })
  const signed = await rhinestoneAccount.signTransaction(prepared)
  return rhinestoneAccount.submitTransaction(signed)
}

const transactions = await Promise.all(splits.map(sendIntent))
You can optionally filter which settlement layers to use:
const splits = await rhinestone.splitIntents({
  chain: base,
  tokens: {
    [USDC_ADDRESS]: parseUnits('100000', 6),
  },
  settlementLayers: { include: ['ACROSS', 'ECO'] },
})