Overview
Starkzap supports bidirectional bridging between Starknet and supported external chains:
- Ethereum (Canonical, CCTP, OFT, OFT-migrated routes)
- Solana (Hyperlane routes)
Deposit flow (external chain → Starknet):
- Configure the SDK (including optional bridging config)
- Fetch bridgeable tokens with
sdk.getBridgingTokens(...)
- Connect an external wallet (
ConnectedEthereumWallet or ConnectedSolanaWallet)
- Inspect balance, allowance, and estimated fees
- Call
wallet.deposit(...) to submit the source-chain transaction
Withdraw flow (Starknet → external chain):
- Inspect L2 balance and estimated fees with
wallet.getWithdrawBalance(...) and wallet.getInitiateWithdrawFeeEstimate(...)
- Call
wallet.initiateWithdraw(...) to burn/lock tokens on Starknet
- Monitor status with
wallet.getWithdrawalState(...) or wallet.monitorWithdrawal(...)
- For Canonical and CCTP: call
wallet.completeWithdraw(...) when state is READY_TO_CLAIM
Install Optional Dependencies
Install only what you use.
For Ethereum routes:
For Solana routes:
npm install @solana/web3.js @hyperlane-xyz/sdk @hyperlane-xyz/registry @hyperlane-xyz/utils
SDK Configuration
Use bridging config when you need custom external RPCs or OFT support.
The SDK uses external RPCs to read source-chain state (balances/allowances), estimate bridge fees, and submit source-chain transactions reliably. Without explicit RPC URLs, these operations can be rate-limited or unavailable depending on your environment:
import { StarkZap } from "starkzap";
const sdk = new StarkZap({
network: "mainnet",
bridging: {
ethereumRpcUrl: "https://eth-mainnet.g.alchemy.com/v2/<key>",
solanaRpcUrl: "https://solana-mainnet.g.alchemy.com/v2/<key>",
layerZeroApiKey: "<layerzero-key>", // required for OFT/OFT-migrated routes
},
});
OFT bridging requires bridging.layerZeroApiKey and is supported on Starknet Mainnet routes only.
Fetch Bridgeable Tokens
import { ExternalChain } from "starkzap";
// All bridgeable tokens for current Starknet environment
const allTokens = await sdk.getBridgingTokens();
// Filter by source chain
const ethereumTokens = await sdk.getBridgingTokens(ExternalChain.ETHEREUM);
const solanaTokens = await sdk.getBridgingTokens(ExternalChain.SOLANA);
Connect External Wallets
Take a look at the Examples. For WalletConnect setup details, see WalletConnect Docs. In practice, you establish the external wallet session first (for example with WalletConnect), then pass its provider/account/chain into ConnectedEthereumWallet.from(...) or ConnectedSolanaWallet.from(...) for bridge calls.
Ethereum (EIP-1193)
import { ConnectedEthereumWallet, ExternalChain } from "starkzap";
const evmProvider = window.ethereum;
const [evmAddress] = await evmProvider.request({ method: "eth_requestAccounts" });
const evmChainId = await evmProvider.request({ method: "eth_chainId" }); // "0x1" or "0xaa36a7"
const ethWallet = await ConnectedEthereumWallet.from(
{
chain: ExternalChain.ETHEREUM,
provider: evmProvider,
address: evmAddress,
chainId: evmChainId, // evm wallet's chain id
},
wallet.getChainId() // starknet wallet's chain id
);
Solana
import { ConnectedSolanaWallet, ExternalChain } from "starkzap";
const solWallet = await ConnectedSolanaWallet.from(
{
chain: ExternalChain.SOLANA,
provider: solanaProvider, // must implement signAndSendTransaction()
address: solanaAddress,
chainId: solanaChainId, // e.g. mainnet/testnet genesis hash from wallet adapter
},
wallet.getChainId()
);
External wallet network and Starknet network must match by environment:
Ethereum Mainnet with Starknet Mainnet, Ethereum Sepolia with Starknet Sepolia, Solana Mainnet with Starknet Mainnet, and Solana Testnet with Starknet Sepolia.| External Network | Identifier | Starknet Network |
|---|
| Ethereum Mainnet | 1 | Starknet Mainnet |
| Ethereum Sepolia | 11155111 | Starknet Sepolia |
| Solana Mainnet | 5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp | Starknet Mainnet |
| Solana Testnet | 4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z | Starknet Sepolia |
Estimate and Deposit
import { Amount, fromAddress } from "starkzap";
const token = ethereumTokens[0];
if (!token) throw new Error("No bridge token available");
// 1) Source-chain available balance
const available = await wallet.getDepositBalance(token, ethWallet);
// 2) ERC20 allowance (null for native/non-allowance routes)
const allowance = await wallet.getAllowance(token, ethWallet);
// 3) Fee estimation (fastTransfer only applies to CCTP)
const fees = await wallet.getDepositFeeEstimate(token, ethWallet, {
fastTransfer: true,
});
// 4) Submit deposit tx on source chain
const recipient = fromAddress(
"0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
); // Starknet recipient
const tx = await wallet.deposit(
recipient,
Amount.parse("25", token.decimals, token.symbol),
token,
ethWallet,
{ fastTransfer: true }
);
console.log(tx.hash);
Withdraw from Starknet
Initiate (all protocols)
import { Amount, fromAddress } from "starkzap";
const token = ethereumTokens[0];
if (!token) throw new Error("No bridge token available");
const recipient = "0xYourEthereumAddress"; // L1 recipient
// 1) L2 balance available to withdraw
const balance = await wallet.getWithdrawBalance(token, ethWallet);
// 2) Estimate L2 fee for initiating the withdrawal
const fees = await wallet.getInitiateWithdrawFeeEstimate(token, ethWallet, {
protocol: "cctp",
fastTransfer: true,
});
// 3) Submit the Starknet burn/initiate transaction
const tx = await wallet.initiateWithdraw(
recipient,
Amount.parse("25", token.decimals, token.symbol),
token,
ethWallet,
{ protocol: "cctp", fastTransfer: true }
);
console.log(tx.hash);
Complete (Canonical & CCTP only)
OFT and Hyperlane are single-step — a relayer handles L1 delivery automatically. For Canonical and CCTP, a second L1 transaction is required once the state becomes READY_TO_CLAIM.
// Poll until the withdrawal is ready to claim
const result = await wallet.monitorWithdrawal(token, tx.hash);
if (result.protocol === "cctp" && result.attestation && result.message) {
// Estimate the L1 gas cost before submitting
const l1Fee = await wallet.getCompleteWithdrawFeeEstimate(
Amount.parse("25", token.decimals, token.symbol),
recipient,
token,
ethWallet,
{
protocol: "cctp",
attestation: result.attestation,
message: result.message,
nonce: result.nonce,
expirationBlock: result.expirationBlock,
}
);
// Submit the L1 completion transaction
const l1Tx = await wallet.completeWithdraw(
recipient,
Amount.parse("25", token.decimals, token.symbol),
token,
ethWallet,
{
protocol: "cctp",
attestation: result.attestation,
message: result.message,
nonce: result.nonce,
expirationBlock: result.expirationBlock,
}
);
console.log(l1Tx.hash);
}
Auto-withdraw (Canonical only)
Canonical supports an autoWithdraw option where a relayer handles L1 completion — no completeWithdraw call needed.
const fees = await wallet.getInitiateWithdrawFeeEstimate(token, ethWallet, {
protocol: "canonical",
autoWithdraw: true,
});
await wallet.initiateWithdraw(
recipient,
Amount.parse("25", token.decimals, token.symbol),
token,
ethWallet,
{
protocol: "canonical",
autoWithdraw: true,
preferredFeeToken: fees.autoWithdrawFee?.token,
}
);
Monitor Bridge Transfers
Simplified state (recommended for UI)
// Withdrawal state: "PENDING" | "READY_TO_CLAIM" | "COMPLETED" | "ERROR"
const state = await wallet.getWithdrawalState(token, { starknetTxHash: tx.hash });
// Deposit state: "PENDING" | "COMPLETED" | "ERROR"
const depositState = await wallet.getDepositState(token, { externalTxHash: ethTxHash });
WithdrawalState values:
PENDING — bridging in progress, no user action needed
READY_TO_CLAIM — ready to finalize on L1; call completeWithdraw (CCTP/Canonical)
COMPLETED — bridge flow fully complete
ERROR — unrecoverable error
Detailed status (advanced use)
Use monitorWithdrawal to get the full status snapshot including CCTP attestation data needed for completeWithdraw:
const result = await wallet.monitorWithdrawal(token, tx.hash);
// result.status: BridgeTransferStatus
// For CCTP when READY_TO_CLAIM: result.attestation, result.message, result.nonce
// You can pass a previously-fetched result back to avoid redundant network calls
const state = await wallet.getWithdrawalState(token, result);
Similarly for deposits:
const result = await wallet.monitorDeposit(token, ethTxHash);
const depositState = await wallet.getDepositState(token, result);
Protocol Notes
| Protocol | Chain | Deposit | Withdrawal |
|---|
canonical | Ethereum | Standard flow; approval may be required for ERC20 tokens. | Two-step: initiate on L2, complete on L1. Supports autoWithdraw to skip the L1 step via relayer. |
cctp | Ethereum | Supports fastTransfer; fee estimate includes CCTP fast transfer bp fee. | Two-step: wait for Circle attestation, then call completeWithdraw. Expired attestations are re-requested automatically. |
oft / oft-migrated | Ethereum | Requires bridging.layerZeroApiKey; mainnet-only route availability. | Single-step: LayerZero relayer handles L1 delivery automatically. No completeWithdraw needed. |
hyperlane | Solana | Requires Solana + Hyperlane optional dependencies. | Single-step: Hyperlane relayer handles delivery automatically. |
Common Errors
- Chain mismatch: token source chain and connected external wallet chain must match.
- Missing LayerZero key: OFT routes require
bridging.layerZeroApiKey.
- Unsupported chain pair: Ethereum mainnet must pair with Starknet mainnet; testnet pairings must match.
- CCTP options missing:
completeWithdraw requires options with attestation data for CCTP routes — the parameter is non-optional in practice.
- Attestation expired: CCTP attestations have an expiration block; re-attestation is requested automatically during
completeWithdraw.
For additional issues, see Troubleshooting.
Next Steps