Lineth SDK
Bridge ETH and tokens, and send and claim messages between Ethereum (L1) and Linea (L2), using a set of composable TypeScript functions. The Lineth SDK is built on viem: you create standard viem clients and call SDK actions on them, or extend a client with SDK decorators.
The SDK is published as two packages:
@lfdt-lineth/sdk-viem: the actions and decorators most applications use.@lfdt-lineth/sdk-core: framework-agnostic utilities (Merkle tree, message types, chain and contract helpers). Most users get it automatically through@lfdt-lineth/sdk-viemand do not install it directly.
It supports Ethereum Mainnet, Linea Mainnet, Sepolia, and Linea Sepolia. You select the network by passing the matching chain from viem/chains, and the SDK resolves the correct contract addresses for you.
Installation​
Install the SDK alongside viem, which is a required peer dependency (version 2.22.0 or later):
npm install @lfdt-lineth/sdk-viem viem
Set up your clients​
Most actions take a viem client. Read actions use a public client, and state-changing actions (deposits, withdrawals, claims) use a wallet client. Use mainnet and linea for production, or sepolia and lineaSepolia for testing:
import { createPublicClient, http } from 'viem';
import { sepolia, lineaSepolia } from 'viem/chains';
const l1Client = createPublicClient({ chain: sepolia, transport: http() });
const l2Client = createPublicClient({ chain: lineaSepolia, transport: http() });
Bridge ETH and tokens​
Use deposit to bridge from L1 to L2, and withdraw to bridge from L2 to L1. Set token to zeroAddress to bridge native ETH. Both actions take a wallet client and return a transaction hash.
import { createWalletClient, createPublicClient, http, zeroAddress } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { sepolia, lineaSepolia } from 'viem/chains';
import { deposit } from '@lfdt-lineth/sdk-viem';
const client = createWalletClient({ chain: sepolia, transport: http() });
const l2Client = createPublicClient({ chain: lineaSepolia, transport: http() });
const hash = await deposit(client, {
l2Client,
account: privateKeyToAccount('0x...'),
amount: 1_000_000_000_000n,
token: zeroAddress, // Use zeroAddress for ETH
to: '0xRecipientAddress',
data: '0x', // Optional data
fee: 100_000_000n, // Optional fee
});
import { createWalletClient, http, zeroAddress } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { lineaSepolia } from 'viem/chains';
import { withdraw } from '@lfdt-lineth/sdk-viem';
const client = createWalletClient({ chain: lineaSepolia, transport: http() });
const hash = await withdraw(client, {
account: privateKeyToAccount('0x...'),
amount: 1_000_000_000_000n,
token: zeroAddress, // Use zeroAddress for ETH
to: '0xRecipientAddress',
data: '0x', // Optional data
});
Claim messages​
A message sent across the bridge is claimed on the destination layer. Messages to L2 are usually delivered automatically by the Postman service, so manual claiming is mainly needed on L1. Use claimOnL1 to finalize an L2-to-L1 message, and claimOnL2 to finalize an L1-to-L2 message.
Claiming on L1 requires a Merkle proof. You can fetch it with getMessageProof and pass it in, as shown below.
- Claim on L1
- Claim on L2
import { createWalletClient, http, zeroAddress } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { sepolia } from 'viem/chains';
import { claimOnL1 } from '@lfdt-lineth/sdk-viem';
const client = createWalletClient({ chain: sepolia, transport: http() });
const hash = await claimOnL1(client, {
account: privateKeyToAccount('0x...'),
from: '0xSenderAddress',
to: '0xRecipientAddress',
fee: 100_000_000n,
value: 1_000_000_000_000n,
messageNonce: 1n,
calldata: '0x',
feeRecipient: zeroAddress,
messageProof: {
root: '0x...',
proof: ['0x...'],
leafIndex: 0,
},
});
import { createWalletClient, http, zeroAddress } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { lineaSepolia } from 'viem/chains';
import { claimOnL2 } from '@lfdt-lineth/sdk-viem';
const client = createWalletClient({ chain: lineaSepolia, transport: http() });
const hash = await claimOnL2(client, {
account: privateKeyToAccount('0x...'),
from: '0xSenderAddress',
to: '0xRecipientAddress',
fee: 100_000_000n,
value: 1_000_000_000_000n,
messageNonce: 1n,
calldata: '0x',
feeRecipient: zeroAddress,
// Optional transaction parameters
gas: 21000n,
maxFeePerGas: 100_000_000n,
maxPriorityFeePerGas: 1_000_000n,
});
Read message data​
These actions run on a public client and read data from the chain. To check whether a message is ready to claim, query its status, and to claim on L1, fetch its proof:
import { createPublicClient, http } from 'viem';
import { sepolia, lineaSepolia } from 'viem/chains';
import { getL2ToL1MessageStatus, getMessageProof } from '@lfdt-lineth/sdk-viem';
const l1Client = createPublicClient({ chain: sepolia, transport: http() });
const l2Client = createPublicClient({ chain: lineaSepolia, transport: http() });
const status = await getL2ToL1MessageStatus(l1Client, {
l2Client,
messageHash: '0x...',
});
const messageProof = await getMessageProof(l1Client, {
l2Client,
messageHash: '0x...',
});
The full set of read actions:
| Action | Returns |
|---|---|
getL1ToL2MessageStatus | Whether an L1-to-L2 message is unknown, claimable, or claimed. |
getL2ToL1MessageStatus | Whether an L2-to-L1 message is unknown, claimable, or claimed, including finalization checks. |
getMessageProof | The Merkle proof needed to claim an L2-to-L1 message on L1. |
getMessageByMessageHash | Full message details from onchain MessageSent events. |
getMessagesByTransactionHash | All messages emitted in a given transaction. |
getTransactionReceiptByMessageHash | The transaction receipt that contains a specific message. |
getMessageSentEvents | MessageSent logs for a given block range. |
getBlockExtraData | Decoded Linea block extraData (version and fee parameters). |
computeMessageHash | Deterministically hashes message parameters (utility). |
Decorators​
Instead of calling actions as standalone functions, you can extend a viem client so the actions become methods on the client. Use publicActionsL1 and publicActionsL2 for read actions, and walletActionsL1 and walletActionsL2 for write actions:
import { createPublicClient, http } from 'viem';
import { sepolia, lineaSepolia } from 'viem/chains';
import { publicActionsL1, publicActionsL2 } from '@lfdt-lineth/sdk-viem';
const l1Client = createPublicClient({ chain: sepolia, transport: http() }).extend(publicActionsL1());
const l2Client = createPublicClient({ chain: lineaSepolia, transport: http() }).extend(publicActionsL2());
Each decorator accepts optional contract addresses (lineaRollupAddress, l2MessageServiceAddress, and for wallet decorators the token bridge addresses) for non-standard deployments. On the supported networks you can omit them, and the SDK resolves the correct addresses automatically.
Core utilities​
For advanced or framework-agnostic use, @lfdt-lineth/sdk-core exposes the building blocks the viem package is built on: SparseMerkleTree for proof generation and verification, parseBlockExtraData, formatMessageStatus, getContractsAddressesByChainId, the OnChainMessageStatus enum, chain helpers, and the message types. Install it directly only if you need these without viem:
npm install @lfdt-lineth/sdk-core
Resources​
@lfdt-lineth/sdk-viemREADME: the full reference for every action, decorator, and error type.- viem documentation: client setup, transports, and accounts.
This SDK was previously published as @consensys/linea-sdk. The package was renamed to @lfdt-lineth/sdk-viem and rebuilt on viem. The previous @consensys/linea-sdk API (the LineaSDK class and L1ClaimingService) is no longer the documented interface.