Skip to main content

Overview

This guide covers EVM-specific implementation details for delegating wallets on Linea and Ethereum networks. Both networks follow the same ERC20 approval pattern with slight differences in gas costs and confirmation times.
Use the /v1/delegation/evm/post-approval endpoint for both Linea and Ethereum delegations.

Supported EVM Networks

Network Name: Linea Chain ID: 59144 (0xe708) RPC URL: https://rpc.linea.build Block Explorer: https://lineascan.build Currencies: USDC, USDTAdvantages:
  • Significantly lower gas fees than Ethereum mainnet
  • Faster block times (~2-3 seconds)
  • Primary network recommended for most integrations
  • US routing available with x-us-env: true header
Typical Gas Cost: 0.010.01 - 0.10 per transaction

ERC20 Approval Pattern

EVM delegation uses the standard ERC20 approve() function to grant spending authority.

The approve() Function

function approve(address spender, uint256 amount) public returns (bool)
Parameters:
  • spender: Platform’s smart contract address that will spend tokens
  • amount: Maximum amount the spender can transfer (in token’s smallest unit)
Returns: true if approval succeeded

How It Works

1

User Calls approve()

User’s wallet calls the approve() function on the token contract (USDC or USDT)
2

Allowance Updated

Token contract updates the allowance mapping:
allowances[userAddress][platformSpender] = amount
3

Platform Can Spend

Platform’s smart contract can now call transferFrom() to spend up to the approved amount
4

Allowance Decreases

Each time platform spends, the allowance decreases by the spent amount

Implementation

Contract Addresses

Obtain contract addresses dynamically by calling the chain configuration endpoint before starting the delegation flow:
const response = await fetch('https://api.baanx.com/v1/delegation/chain/config', {
  headers: {
    'x-client-key': process.env.CLIENT_PUBLIC_KEY!,
    'Authorization': `Bearer ${userAccessToken}`
  }
});

const config = await response.json();

const TOKEN_ADDRESSES = {
  linea: {
    chainId: config.linea.chain_id,         // 59144
    usdc: config.linea.usdc_address,        // Linea USDC contract
    usdt: config.linea.usdt_address,        // Linea USDT contract
    spender: config.linea.spender_address   // Platform spender for Linea
  },
  ethereum: {
    chainId: config.ethereum.chain_id,      // 1
    usdc: config.ethereum.usdc_address,     // Ethereum USDC
    usdt: config.ethereum.usdt_address,     // Ethereum USDT
    spender: config.ethereum.spender_address // Platform spender for Ethereum
  }
};
Recommended: Always fetch addresses dynamically using GET /v1/delegation/chain/config. This ensures you have the latest contract addresses, especially after platform upgrades or network migrations.
Always verify contract addresses are correct before approving. Approving the wrong address could result in loss of funds.

Frontend Implementation

import { BrowserProvider, Contract, parseUnits } from 'ethers';

const ERC20_ABI = [
  'function approve(address spender, uint256 amount) returns (bool)',
  'function allowance(address owner, address spender) view returns (uint256)'
];

interface EVMDelegationParams {
  network: 'linea' | 'ethereum';
  currency: 'usdc' | 'usdt';
  amount: string;
  tokenAddress: string;
  spenderAddress: string;
  delegationToken: string;
}

async function delegateEVMWallet(
  params: EVMDelegationParams
): Promise<{
  address: string;
  network: string;
  currency: string;
  amount: string;
  txHash: string;
  sigHash: string;
  sigMessage: string;
  token: string;
}> {

  if (!window.ethereum) {
    throw new Error('No Ethereum wallet detected. Please install MetaMask.');
  }

  const provider = new BrowserProvider(window.ethereum);

  await provider.send('eth_requestAccounts', []);

  const network = await provider.getNetwork();
  const expectedChainId = params.network === 'linea' ? 59144n : 1n;

  if (network.chainId !== expectedChainId) {
    await switchNetwork(provider, params.network);
  }

  const signer = await provider.getSigner();
  const address = await signer.getAddress();

  console.log(`Connected wallet: ${address}`);

  const tokenContract = new Contract(
    params.tokenAddress,
    ERC20_ABI,
    signer
  );

  const decimals = params.currency === 'usdc' || params.currency === 'usdt' ? 6 : 18;
  const amountWei = parseUnits(params.amount, decimals);

  console.log(
    `Requesting approval for ${params.amount} ${params.currency.toUpperCase()} ` +
    `on ${params.network}`
  );

  const approveTx = await tokenContract.approve(
    params.spenderAddress,
    amountWei
  );

  console.log('Transaction submitted:', approveTx.hash);
  console.log('Waiting for confirmation...');

  const receipt = await approveTx.wait();

  if (!receipt || receipt.status !== 1) {
    throw new Error('Approval transaction failed');
  }

  console.log('Transaction confirmed in block:', receipt.blockNumber);

  const siweMessage = generateSIWEMessage(
    address,
    network.chainId.toString()
  );

  console.log('Requesting signature for proof...');
  const sigHash = await signer.signMessage(siweMessage);

  return {
    address,
    network: params.network,
    currency: params.currency,
    amount: params.amount,
    txHash: receipt.hash,
    sigHash,
    sigMessage: siweMessage,
    token: params.delegationToken
  };
}

async function switchNetwork(
  provider: BrowserProvider,
  network: 'linea' | 'ethereum'
) {
  const chainId = network === 'linea' ? '0xe708' : '0x1';

  try {
    await provider.send('wallet_switchEthereumChain', [{ chainId }]);
  } catch (error: any) {
    if (error.code === 4902) {
      if (network === 'linea') {
        await provider.send('wallet_addEthereumChain', [
          {
            chainId: '0xe708',
            chainName: 'Linea',
            nativeCurrency: {
              name: 'Ether',
              symbol: 'ETH',
              decimals: 18
            },
            rpcUrls: ['https://rpc.linea.build'],
            blockExplorerUrls: ['https://lineascan.build']
          }
        ]);
      } else {
        throw error;
      }
    } else {
      throw error;
    }
  }
}

function generateSIWEMessage(address: string, chainId: string): string {
  const domain = window.location.host;
  const nonce = generateNonce();
  const issuedAt = new Date().toISOString();
  const expirationTime = new Date(Date.now() + 30 * 60000).toISOString();

  return `${domain} wants you to sign in with your Ethereum account:
${address}

Prove wallet ownership for delegation

URI: https://${domain}
Version: 1
Chain ID: ${chainId}
Nonce: ${nonce}
Issued At: ${issuedAt}
Expiration Time: ${expirationTime}`;
}

function generateNonce(): string {
  return Math.random().toString(36).substring(2, 15) +
         Math.random().toString(36).substring(2, 15);
}

SIWE (Sign-In with Ethereum)

SIWE is the standard format for proving wallet ownership through message signing.

SIWE Message Format

{domain} wants you to sign in with your Ethereum account:
{address}

{statement}

URI: {uri}
Version: {version}
Chain ID: {chainId}
Nonce: {nonce}
Issued At: {issuedAt}
Expiration Time: {expirationTime}

Field Descriptions

FieldRequiredDescriptionExample
domainYesDomain requesting signatureyourplatform.com
addressYesUser’s Ethereum address0x3a11a86cf...
statementNoHuman-readable statementProve wallet ownership for delegation
uriYesFull URI of requesting applicationhttps://yourplatform.com
versionYesSIWE version (always “1”)1
chainIdYesBlockchain chain ID59144 (Linea) or 1 (Ethereum)
nonceYesRandom string to prevent replay attacksabc123xyz789
issuedAtYesISO 8601 timestamp2024-10-08T12:00:00Z
expirationTimeNoISO 8601 timestamp when signature expires2024-10-08T12:30:00Z
The SIWE message format must be exact. Extra whitespace, missing fields, or incorrect formatting will cause signature verification to fail.

SIWE Implementation

function generateSIWEMessage(
  address: string,
  chainId: string,
  domain: string = window.location.host
): string {
  const nonce = crypto.randomUUID().replace(/-/g, '');
  const issuedAt = new Date().toISOString();
  const expirationTime = new Date(Date.now() + 30 * 60000).toISOString();

  return `${domain} wants you to sign in with your Ethereum account:
${address}

Prove wallet ownership for delegation

URI: https://${domain}
Version: 1
Chain ID: ${chainId}
Nonce: ${nonce}
Issued At: ${issuedAt}
Expiration Time: ${expirationTime}`;
}

Gas Optimization

Check Existing Allowance

Before requesting a new approval, check if sufficient allowance already exists:
async function checkAllowance(
  tokenAddress: string,
  ownerAddress: string,
  spenderAddress: string
): Promise<bigint> {
  const provider = new BrowserProvider(window.ethereum);

  const tokenContract = new Contract(
    tokenAddress,
    ['function allowance(address owner, address spender) view returns (uint256)'],
    provider
  );

  const allowance = await tokenContract.allowance(ownerAddress, spenderAddress);
  return allowance;
}

async function requestApprovalIfNeeded(
  tokenAddress: string,
  spenderAddress: string,
  amount: string
): Promise<string | null> {
  const provider = new BrowserProvider(window.ethereum);
  const signer = await provider.getSigner();
  const address = await signer.getAddress();

  const existingAllowance = await checkAllowance(
    tokenAddress,
    address,
    spenderAddress
  );

  const requiredAmount = parseUnits(amount, 6);

  if (existingAllowance >= requiredAmount) {
    console.log('Sufficient allowance already exists');
    return null; // No new transaction needed
  }

  const tokenContract = new Contract(tokenAddress, ERC20_ABI, signer);
  const tx = await tokenContract.approve(spenderAddress, requiredAmount);
  const receipt = await tx.wait();

  return receipt.hash;
}

Gas Price Strategies

Let wallet handle gas price estimation (recommended):
const tx = await tokenContract.approve(spender, amount);
// Wallet will estimate gas automatically

Submit to API

After collecting all data from the blockchain, submit to the EVM-specific endpoint:

API Request

interface EVMDelegationProof {
  address: string;          // Must start with 0x, 42 characters total
  network: 'linea' | 'ethereum';
  currency: 'usdc' | 'usdt';
  amount: string;
  txHash: string;           // Must start with 0x, 66 characters total
  sigHash: string;          // Must start with 0x, 132 characters total
  sigMessage: string;       // SIWE formatted message
  token: string;            // From Step 1
}

async function submitEVMDelegation(
  proof: EVMDelegationProof,
  userAccessToken: string
): Promise<boolean> {
  const response = await fetch(
    'https://api.yourplatform.com/v1/delegation/evm/post-approval',
    {
      method: 'POST',
      headers: {
        'x-client-key': process.env.CLIENT_PUBLIC_KEY!,
        'Authorization': `Bearer ${userAccessToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(proof)
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Delegation failed');
  }

  const data = await response.json();
  return data.success;
}
API Reference: POST /v1/delegation/evm/post-approval

Validation Rules

The API performs strict EVM-specific validation:
  • Must start with 0x
  • Must be exactly 42 characters long
  • Must contain only hexadecimal characters (0-9, a-f)
  • Case-insensitive, but checksummed addresses are preferred
Valid: 0x3a11a86cf218c448be519728cd3ac5c741fb3424 Invalid: 3a11a86cf218c448be519728cd3ac5c741fb3424 (missing 0x)
  • Must start with 0x
  • Must be exactly 66 characters long
  • Must contain only hexadecimal characters
  • Must be a confirmed transaction on the specified network
Valid: 0xb92de09d893e8162b0861c0f7321f68df02212efbc58f208839ae3f176d89638
  • Must start with 0x
  • Must be exactly 132 characters long
  • Must contain only hexadecimal characters
  • Must be valid ECDSA signature that recovers to the provided address
Valid: 0x2039b9765a4df76e8bae80f3bbc640e8ae6acc81f7a5cc96fe91ccc1844b6f7d4c3e8f1a2b...
  • Must follow SIWE format exactly
  • Must contain the provided address
  • Must contain correct chain ID for network
  • Must not be expired (check expirationTime if present)

Error Handling

Common EVM Errors

async function handleEVMDelegation() {
  try {
    await delegateEVMWallet(params);
  } catch (error: any) {
    if (error.code === 4001) {
      console.error('User rejected transaction');
      alert('Please approve the transaction in your wallet to continue');
    }
    else if (error.code === -32603) {
      console.error('Insufficient funds for gas');
      alert('You need more ETH in your wallet to pay for gas fees');
    }
    else if (error.message?.includes('wrong network')) {
      console.error('Wrong network');
      alert('Please switch to the correct network in your wallet');
    }
    else if (error.message?.includes('insufficient allowance')) {
      console.error('Approval failed or insufficient');
      alert('Please try approving the transaction again');
    }
    else if (error.code === -32002) {
      console.error('Request already pending');
      alert('Please check your wallet for a pending request');
    }
    else {
      console.error('Unknown error:', error);
      alert(`Delegation failed: ${error.message}`);
    }
  }
}

API Error Responses

StatusErrorCauseSolution
400Invalid address formatAddress doesn’t match EVM formatEnsure address starts with 0x and is 42 chars
400Invalid transaction hashTransaction hash not found or wrong formatWait for confirmation, verify network
400Invalid signatureSignature doesn’t match addressRe-sign message, ensure correct wallet
400Expired tokenDelegation token expiredGenerate new token from Step 1
400SIWE validation failedMessage format incorrectUse exact SIWE format
429Rate limitedToo many requestsWait and retry with exponential backoff

Testing

Test on Linea Testnet

Before going to production, test on Linea Goerli testnet:
const TESTNET_CONFIG = {
  chainId: 59140, // Linea Goerli
  chainIdHex: '0xe704',
  rpcUrl: 'https://rpc.goerli.linea.build',
  blockExplorer: 'https://goerli.lineascan.build',
  nativeCurrency: {
    name: 'Ether',
    symbol: 'ETH',
    decimals: 18
  }
};

async function addLineaTestnet() {
  await window.ethereum.request({
    method: 'wallet_addEthereumChain',
    params: [{
      chainId: TESTNET_CONFIG.chainIdHex,
      chainName: 'Linea Goerli',
      nativeCurrency: TESTNET_CONFIG.nativeCurrency,
      rpcUrls: [TESTNET_CONFIG.rpcUrl],
      blockExplorerUrls: [TESTNET_CONFIG.blockExplorer]
    }]
  });
}
Get testnet ETH from:

Next Steps