Skip to main content

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):
  1. Configure the SDK (including optional bridging config)
  2. Fetch bridgeable tokens with sdk.getBridgingTokens(...)
  3. Connect an external wallet (ConnectedEthereumWallet or ConnectedSolanaWallet)
  4. Inspect balance, allowance, and estimated fees
  5. Call wallet.deposit(...) to submit the source-chain transaction
Withdraw flow (Starknet → external chain):
  1. Inspect L2 balance and estimated fees with wallet.getWithdrawBalance(...) and wallet.getInitiateWithdrawFeeEstimate(...)
  2. Call wallet.initiateWithdraw(...) to burn/lock tokens on Starknet
  3. Monitor status with wallet.getWithdrawalState(...) or wallet.monitorWithdrawal(...)
  4. For Canonical and CCTP: call wallet.completeWithdraw(...) when state is READY_TO_CLAIM

Install Optional Dependencies

Install only what you use. For Ethereum routes:
npm install ethers
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 NetworkIdentifierStarknet Network
Ethereum Mainnet1Starknet Mainnet
Ethereum Sepolia11155111Starknet Sepolia
Solana Mainnet5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpStarknet Mainnet
Solana Testnet4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zStarknet 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

// 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

ProtocolChainDepositWithdrawal
canonicalEthereumStandard 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.
cctpEthereumSupports 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-migratedEthereumRequires bridging.layerZeroApiKey; mainnet-only route availability.Single-step: LayerZero relayer handles L1 delivery automatically. No completeWithdraw needed.
hyperlaneSolanaRequires 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