# Mechanics of cross chain composability using Shared Publisher and Account Abstraction
## Introduction: Shared Publisher, Inboxes, and 2PC
Cross-chain transactions have long been plagued by uncertainty: messages sent from one chain may or may not arrive on another, and developers often have to build complex retry or rollback logic. **Shared Publisher + 2PC** changes the game: sequencers simulate all cross-chain calls (mailbox writes), exchange XCallRequests/XCallResponses, and run a **two-phase commit** so that, at execution time, every `Inbox` slot is already pre-populated with the correct payload. This ensures **atomicity**—reading from an inbox and writing to an outbox across chains must match exactly, and the Shared Publisher verifies this via a succinct zk proof under the hood.
## Example: wETH ↔ SSV Swap via SP+xCall and AA
In this example, we swap **1 wETH → SSV → wETH** across Rollup A and Rollup B—without modifying any bridge or DEX contracts. All orchestration is done off-chain in a web app that builds two AA transactions (UserOps), one per chain, each batching multiple calls.
* **Chains:** A (chainId 1001) and B (chainId 1002)
* **Tokens:** wETH (`0xC02aaa39b223fe8D0A0e5C4F27eAD9083C756Cc2`) and SSV (`0x9D65fF81a3C488d585bBfb0Bfe3c7707C7917F54`)
* **DEX (on B):** Uniswap V2 Router02 (`0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D`)
* **Mailbox Contracts:** `0xMailboxA…`, `0xMailboxB…`
* **AA Wallet:** Pre-deployed ERC‑4337 smart account (`0xAAwallet…`) on both chains
### Step 1: Initiator UserOp on Chain A
```ts
// Config
const chainIdA = 1001;
const chainIdB = 1002;
const sessionId = "sessionXYZ";
const user = "0xUser…";
const AAwalletA = "0xAAwallet…";
const bridgeA = "0xC02aaa39b223fe8D0A0e5C4F27eAD9083C756Cc2"; // wETH
const amountIn = ethers.utils.parseUnits("1.0", 18);
const minOut = ethers.utils.parseUnits("0.9", 18);
const deadline = Math.floor(Date.now()/1000) + 20*60;
// ABIs
const bridgeAbi = ["function burn(address,uint256)"];
const mailboxAbi = ["function Outbox(uint256,bytes)", "function Inbox(uint256,bytes32) view returns(bytes)"];
const aaAbi = ["function execBatch(address[] calldata,bytes[] calldata) external"];
const bridgeIF = new ethers.utils.Interface(bridgeAbi);
const mailboxIF = new ethers.utils.Interface(mailboxAbi);
const aaIF = new ethers.utils.Interface(aaAbi);
// Helper for msgId
function msgId(chain: number, session: string, step: string): string {
return ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(["uint256","string","string"], [chain, session, step])
);
}
// 1) Burn on A triggers bridge to write a "mint" outbox message to B
const burnCalldata = bridgeIF.encodeFunctionData("burn", [user, amountIn]);
// 2) (Optional) Read return from B and mint final SSV
const inboxReturn = mailboxIF.encodeFunctionData("Inbox", [chainIdB, msgId(chainIdB, sessionId, "return")]);
const finalMintData = new ethers.utils.Interface(["function mint(address,uint256)"])
.encodeFunctionData("mint", [user, minOut]);
// 3) Batch into execBatch
const targetsA = [ bridgeA, "0xMailboxA…", bridgeA ];
const callsA = [ burnCalldata, inboxReturn, finalMintData ];
const batchA = aaIF.encodeFunctionData("execBatch", [targetsA, callsA]);
const userOpA = {
sender: AAwalletA,
callData: batchA,
// gas, signature...
};
```
> **Note:** Calling `bridgeA.burn(...)` automatically emits the outbox mint instruction (via the bridge contract’s internal hook). The sequencer pre-populates B’s inbox before you submit.
### Step 2: Executor UserOp on Chain B
```ts
// Config
const chainIdA = 1001;
const AAwalletB = "0xAAwallet…";
const bridgeB = "0xC02aaa39b223fe8D0A0e5C4F27eAD9083C756Cc2"; // wETH
const dexRouter = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D";
const usdcB = "0x9D65fF81a3C488d585bBfb0Bfe3c7707C7917F54"; // SSV
const amountIn = ethers.utils.parseUnits("1.0", 18);
const minOut = ethers.utils.parseUnits("0.9", 18);
const path = [ bridgeB, usdcB ];
const deadline = Math.floor(Date.now()/1000) + 20*60;
// ABIs
const mailboxAbi = ["function Inbox(uint256,bytes32) view returns(bytes)", "function Outbox(uint256,bytes) "];
const bridgeAbi = ["function mint(address,uint256)"];
const erc20Abi = ["function approve(address,uint256)"];
const dexAbi = ["function swapExactTokensForTokens(uint256,uint256,address[],address,uint256) returns(uint256[])"];
const aaAbi = ["function execBatch(address[] calldata,bytes[] calldata) external"];
const mailboxIF = new ethers.utils.Interface(mailboxAbi);
const bridgeIF = new ethers.utils.Interface(bridgeAbi);
const erc20IF = new ethers.utils.Interface(erc20Abi);
const dexIF = new ethers.utils.Interface(dexAbi);
const aaIF = new ethers.utils.Interface(aaAbi);
// Helper for msgId
function msgId(chain: number, session: string, step: string): string {
return ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(["uint256","string","string"], [chain, session, step])
);
}
// 1) Inbox mint from A
const inboxMint = mailboxIF.encodeFunctionData("Inbox", [chainIdA, msgId(chainIdA, sessionId, "mint")]);
// 2) Mint bridged wETH on B
const mintB = bridgeIF.encodeFunctionData("mint", [user, amountIn]);
// 3) Approve & swap on Uniswap V2
const approve = erc20IF.encodeFunctionData("approve", [dexRouter, amountIn]);
const swap = dexIF.encodeFunctionData("swapExactTokensForTokens", [amountIn, minOut, path, user, deadline]);
// 4) Outbox return to A
const returnPayload = ethers.utils.defaultAbiCoder.encode(
["string","address","address","uint256"],
["return", user, usdcB, minOut]
);
const outboxReturn = mailboxIF.encodeFunctionData("Outbox", [chainIdA, returnPayload]);
// 5) Batch into execBatch
const targetsB = [ "0xMailboxB…", bridgeB, bridgeB, dexRouter, "0xMailboxB…" ];
const callsB = [ inboxMint, mintB, approve, swap, outboxReturn ];
const batchB = aaIF.encodeFunctionData("execBatch", [targetsB, callsB]);
const userOpB = {
sender: AAwalletB,
callData: batchB,
// gas, signature by sequencer...
};
```
**Result:** Two AA transactions (one on A, one on B) drive the full cross-chain swap, with no on-chain waiting and **no changes** to any bridge or DEX contracts. The **sequencers** handle SP+xCall simulation and 2PC to guarantee each `Inbox` call returns the right data, achieving atomic composability across chains.
## Integration Guide for dApp Developers
1. **Detect User’s Chain**
The web application reads the connected wallet’s chain ID via `ethereum.request({ method: 'eth_chainId' })`.
2. **Determine Cross-Transaction**
If the connected chain ID does not match the dApp’s deployed chain, the app treats the action as a cross-chain transaction and triggers the AA workflow.
3. **Build & Submit UserOps**
Following the patterns above, the webapp assembles the appropriate `execBatch` UserOperations for each chain (burn/mint and swap/return), and submits them to the AA bundler or EntryPoint.
### Leading AA SDKs & Libraries
* Ethers.js AA Module: First-class support for ERC-4337 with built-in UserOperation builders and EntryPoint interaction.
* Account Abstraction Kit: Modular framework offering wallet templates, plugins, and helper libraries for AA across multiple EVMs.
* ZeroDev SDK: High-level JavaScript/TypeScript library for sponsored AA transactions, gasless onboarding, and bundling.
* Biconomy AA SDK: Intent-driven AA tooling, bundler integration, and customizable paymaster flows.
* Safe SDK: Official Gnosis Safe library extended with AA batch execution and multi-signature support.
* hardhat-deploy-4337: Hardhat plugin for local testing, deploying EntryPoints, and simulating AA UserOps in development.
## AA + MetaMask example
Below is a minimal end-to-end example of constructing and sending an ERC-20 transfer via an ERC-4337 “UserOperation” (AA) using the Rari/Rocket Pool Account-Abstraction SDK and MetaMask as your signer/provider. It:
1. Grabs the MetaMask provider
2. Wraps it in ethers.js
3. Instantiates the Account-Abstraction helper
4. Builds and signs a UserOperation to call an ERC-20’s transfer(...)
5. Sends the UserOp off to a bundler
```ts
// 1) Install:
// npm install ethers @account-abstraction/sdk
import { ethers } from "ethers";
import {
SimpleAccountAPI,
HttpRpcClient,
UserOperationStruct
} from "@account-abstraction/sdk";
async function sendERC20ViaAA() {
// ——————————————————————————————————————————————
// 1. Connect to MetaMask
// ——————————————————————————————————————————————
if (!window.ethereum) {
throw new Error("MetaMask not detected");
}
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const ownerSigner = provider.getSigner();
// ——————————————————————————————————————————————
// 2. Setup EntryPoint + Bundler RPC
// ——————————————————————————————————————————————
// Replace these with your deployed addresses
const ENTRY_POINT_ADDRESS = "0xEntryPointContract…";
const ACCOUNT_FACTORY_ADDRESS = "0xAccountFactory…";
const BUNDLER_URL = "https://your-bundler.rpc.endpoint";
const rpcClient = new HttpRpcClient(
BUNDLER_URL,
ENTRY_POINT_ADDRESS,
provider
);
// ——————————————————————————————————————————————
// 3. Instantiate Account API
// ——————————————————————————————————————————————
const accountAPI = new SimpleAccountAPI({
provider,
entryPointAddress: ENTRY_POINT_ADDRESS,
factoryAddress: ACCOUNT_FACTORY_ADDRESS,
owner: ownerSigner
});
// Compute your counterfactual AA address (for UI, optional)
const aaAddress = await accountAPI.getAccountAddress();
console.log("Your AA smart-account address:", aaAddress);
// ——————————————————————————————————————————————
// 4. Prepare call data: ERC-20 transfer
// ——————————————————————————————————————————————
const ERC20 = new ethers.Contract(
"0xTokenAddress…",
[
"function transfer(address to, uint256 amount) public returns (bool)"
],
ownerSigner
);
const to = "0xRecipientAddress…";
const amount = ethers.utils.parseUnits("1.0", 18);
const callData = ERC20.interface.encodeFunctionData("transfer", [
to,
amount
]);
// ——————————————————————————————————————————————
// 5. Build, sign & send the UserOperation
// ——————————————————————————————————————————————
// a) createUnsignedUserOp automatically fills in nonce, gas limits, etc.
let userOp: UserOperationStruct = await accountAPI.createUnsignedUserOp({
target: ERC20.address,
data: callData
});
// b) sign via your ownerSigner (MetaMask)
userOp = await accountAPI.signUserOp(userOp);
// c) dispatch to bundler
const userOpHash = await rpcClient.sendUserOpToBundler(userOp);
console.log("👀 userOpHash:", userOpHash);
// Optionally: wait for inclusion
const receipt = await rpcClient.waitForUserOpReceipt(userOpHash);
console.log("✅ AA tx executed in tx:", receipt.transactionHash);
}
sendERC20ViaAA().catch(console.error);
```