evm-force-inclusion
v0.3.1
Published
TypeScript SDK for building force inclusion transactions on Arbitrum and Base
Maintainers
Readme
evm-force-inclusion
TypeScript SDK for constructing L1 transactions that force messages onto Arbitrum and Optimism stack-based rollups. The client wraps the canonical inbox contracts and handles value calculations, refund addresses, and request validation.
Installation
npm install evm-force-inclusion viemUsage
Choose one of the following:
Option 1 — wagmi (browser wallet connectors)
import { useWalletClient } from "wagmi";
import {
ForceInclusionClient,
DEFAULT_OPTIMISM_PORTAL_ADDRESS,
} from "evm-force-inclusion";
import { parseEther } from "viem";
export function DepositButton() {
const { data: walletClient } = useWalletClient();
async function onClick() {
if (!walletClient) return;
const client = new ForceInclusionClient({ walletClient });
await client.sendTransaction({
l2: { type: "op", l1ContractAddress: DEFAULT_OPTIMISM_PORTAL_ADDRESS },
to: "0xRecipient...",
value: parseEther("0.01"),
gasLimit: 200000n,
data: "0x",
});
}
return <button onClick={onClick}>Deposit to OP</button>;
}For a more complete example (including Sepolia), see the wagmi doc in docs/wagmi.md.
Option 2 — private key (server scripts, bots, CLIs)
import {
ForceInclusionClient,
DEFAULT_ARBITRUM_INBOX_ADDRESS,
DEFAULT_OPTIMISM_PORTAL_ADDRESS,
} from "evm-force-inclusion";
const client = ForceInclusionClient.fromPrivateKey(
process.env.WALLET_PRIVATE_KEY!,
"https://mainnet.rpc"
);
// Arbitrum: create a retryable ticket
await client.sendTransaction({
l2: { type: "arb", l1ContractAddress: DEFAULT_ARBITRUM_INBOX_ADDRESS },
to: "0x...", // L2 recipient
l2CallValue: 0n,
maxSubmissionCost: 100000000000000n,
maxGas: 200000n,
gasPriceBid: 1000000000n,
data: "0x",
});
// OP Stack: deposit transaction
await client.sendTransaction({
l2: { type: "op", l1ContractAddress: DEFAULT_OPTIMISM_PORTAL_ADDRESS },
to: "0x...",
value: 1000000000000000n,
gasLimit: 200000n,
});API
ForceInclusionClient
constructor(config)– create with an existingWalletClientfromviemand optional contract overrides.ForceInclusionClient.fromPrivateKey(privateKey, rpcUrl, overrides)– convenience constructor usingviemunder the hood.sendTransaction(request)– unified entry point; passrequest.l2.typeas"arb"or"op"with an explicitl1ContractAddress.createArbitrumRetryableTicket(request)/depositToOptimismPortal(request)remain available for direct interaction, if preferred.
Requests validate numeric inputs, normalize data payloads, and use the caller address for refund parameters by default.
Custom Contracts
Pass contracts to the constructor when you want to point at alternate Nitro or OP Stack deployments:
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const walletClient = createWalletClient({
account: privateKeyToAccount(process.env.WALLET_PRIVATE_KEY! as `0x${string}`),
transport: http("https://mainnet.rpc"),
});
const client = new ForceInclusionClient({
walletClient,
contracts: {
arbitrumInbox: { address: "0x..." },
optimismPortal: { address: "0x..." },
},
});Override a single transaction by setting l2.l1ContractAddress (defaults are exported for convenience, including Sepolia testnet):
await client.sendTransaction({
l2: { type: "arb", l1ContractAddress: "0x..." },
to: "0x...",
l2CallValue: 1n,
maxSubmissionCost: 2n,
maxGas: 3n,
gasPriceBid: 4n,
});To target Sepolia testnets, import DEFAULT_ARBITRUM_SEPOLIA_INBOX_ADDRESS or DEFAULT_OPTIMISM_SEPOLIA_PORTAL_ADDRESS and pass them via l1ContractAddress.
Working with Testnets
Transactions are always submitted on the L1 that secures the target rollup. To interact with Arbitrum or Optimism Sepolia, point the client at an Ethereum Sepolia RPC URL and use the exported Sepolia contract addresses:
const client = ForceInclusionClient.fromPrivateKey(
process.env.WALLET_PRIVATE_KEY!,
`https://sepolia.infura.io/v3/${process.env.INFURA_KEY}`
);
await client.sendTransaction({
l2: { type: "arb", l1ContractAddress: DEFAULT_ARBITRUM_SEPOLIA_INBOX_ADDRESS },
to: "0x...",
l2CallValue: 0n,
maxSubmissionCost: 100000000000000n,
maxGas: 200000n,
gasPriceBid: 1000000000n,
});
await client.sendTransaction({
l2: { type: "op", l1ContractAddress: DEFAULT_OPTIMISM_SEPOLIA_PORTAL_ADDRESS },
to: "0x...",
value: 1000000000000000n,
gasLimit: 200000n,
});
// Ensure the wallet holds ETH on Sepolia to cover L1 gas.Arbitrum retryables vs OP deposits
- Arbitrum (Nitro):
createRetryableTicketwill attempt an auto-redeem on L2. If auto-redeem fails (e.g. insufficientmaxGas/gasPriceBidor the L2 call reverts, or if you are interacting with a testnet), you must redeem the ticket on L2 within its lifetime. This SDK only submits the L1 transaction; it does not redeem on L2 for you. - OP Stack (Optimism/Base, etc.):
depositTransactionis executed by the derivation pipeline; there is no separate redeem step.
Minimal viem example to redeem an Arbitrum retryable on L2 (if needed):
import { createWalletClient, http } from "viem";
import { parseAbi } from "viem";
// Precompile address on Arbitrum chains
const ARB_RETRYABLE_TX = "0x000000000000000000000000000000000000006e" as const;
const ARB_RETRYABLE_ABI = parseAbi([
"function redeem(bytes32 ticketId) returns (bool)"
]);
// ticketId must be obtained from the retryable workflow (e.g. event/indexer)
async function redeemRetryable(walletClient: any, ticketId: `0x${string}`) {
return walletClient.writeContract({
address: ARB_RETRYABLE_TX,
abi: ARB_RETRYABLE_ABI,
functionName: "redeem",
account: walletClient.account,
chain: walletClient.chain,
args: [ticketId],
});
}Notes:
- The ticket lifetime and semantics are defined by Arbitrum; ensure you monitor the L2 execution status and redeem before expiry if auto-redeem fails.
- OP Stack chains do not require a redeem call; ensure you size
gasLimitappropriately for your deposit call.
Default Addresses
DEFAULT_ARBITRUM_INBOX_ADDRESSDEFAULT_ARBITRUM_SEPOLIA_INBOX_ADDRESSDEFAULT_OPTIMISM_PORTAL_ADDRESSDEFAULT_OPTIMISM_SEPOLIA_PORTAL_ADDRESS
Building & Testing
npm install
npm run build
npm run testPublishing is wired through prepublishOnly which runs the build and test suite automatically.
Note on peer dependency
This package now uses viem instead of ethers. viem is declared as a peer dependency, so ensure it is installed in your application.
