Bridge Service is a backend API server built on Express framework that allows a user to request unsigned deposit and withdraw transaction payloads for bridging ETH and ERC20 tokens between Ethereum Sepolia (L1) and Arbitrum Sepolia (L2). The API validates payloads, checks balances/allowance, and returns unsigned transaction payloads to initiate deposit and withdrawals to and from Arbitrum Sepolia.
The service also offers status endpoints: /v1/status/deposit and /v1/status/withdraw to track the status of published transactions on the Child (Arbitrum Sepolia) and Parent Chain (Ethereum Sepolia) respectively.
- Node.js 18+ (for global
fetch) - A funded test wallet on Ethereum Sepolia for live calls
- RPC URLs (Infura or equivalent)
-
Install dependencies
yarn install
-
Create
.env(copy.env.example) and set values:
-
Set the
ERC_20_DEPLOYED_TOKEN_ADDRESSto a sample ERC-20 token contract on Ethereum Sepolia that you have sufficient funds of, this ERC-20 token will be used to deposit and withdraw to and from Arbitrum Sepolia chain. -
In case you have no such ERC-20 Token contract on Ethereum Sepola Network, set
TOKEN_DEPLOYER_PRIVATE_KEYto the private key of the account through which you can deploy one on Ethereum Sepolia. To deploy runnpm run deploy:erc20, it will deploy the Fake Token Contract. -
The
PARENT_CHAIN_WALLET_KEYshould be set to a wallet with sufficient Sepolia ETH, as it is used in End-to-End Testing Script to sign and publish the deposit and withdrawal transactions. -
Set the
L1_RPC_URLandL2_RPC_URLto any RPC provider URL of your choice. -
The
INFURA_L1_RPC_URLandINFURA_L2_RPC_URLvariables should only hold Infura RPC URLs, these are used to make/v1/status/withdrawcalls to query Outgoing message status for ETH and ERC-20 withdrawals, since these areeth_getLogsheavy calls, that's why Infura is used.
-
Run the server
npm run dev
Base URL: http://localhost:3000
GET /health
Response:
{ "status": "ok" }POST /v1/payloads/deposit
Body:
{
"assetType": "ETH" | "ERC20",
"amount": "0.1",
"chainId": 11155111,
"from": "0x...",
"tokenAddress": "0x..." // required if assetType = "ERC20"
}✅ Success (ETH or ERC20 with sufficient allowance):
// ETH Deposit
// txRequest in the response below is the ETH deposit payload
{
"kind": "deposit",
"txRequest": {
"to": "0xaAe29B0366299461418F5324a79Afc425BE5ae21",
"value": {
"type": "BigNumber",
"hex": "0x........."
},
"data": "0x...",
"from": "0x..."
}
}
// ERC-20 Deposit
// txRequest in the response below is the ERC-20 token deposit payload
{
"kind": "deposit",
"txRequest": {
"to": "0x...",
"data": "0x...",
"value": {
"type": "BigNumber",
"hex": "0x08a742799900"
},
"from": "0x..."
}
}✅ Success (ERC20 approve required):
// txRequest in the response below is the approve transaction payload
{
"kind": "approve",
"message": "Approve the gateway to spend your token before depositing.",
"txRequest": {
"to": "0x...",
"data": "0x...",
"value": "0x0"
}
}❌ Errors:
// For ETH
{
"error": "RequestError",
"message": "Insufficient ETH balance (0.5).",
"details": null
}
// For ERC-20
{
"error": "RequestError",
"message": "Insufficient Token balance (51.5).",
"details": null
}
// OR
// Unknown Error
{
"error": "RequestError",
"message": "Something Went Wrong."
}POST /v1/payloads/withdraw
Body:
{
"assetType": "ETH" | "ERC20",
"amount": "0.1",
"chainId": 421614,
"from": "0x...",
"destinationAddress": "0x...",
"tokenAddress": "0x..." // required if assetType = "ERC20"
}✅ Success:
// For both ETH & ERC-20
// txRequest in the response below is the withdraw transaction payload
{
"txRequest": {
"data": "0x...",
"to": "0x...",
"value": {
"type": "BigNumber",
"hex": "0x00"
},
"from": "0x..."
}
}❌ Errors:
// For ETH
{
"error": "RequestError",
"message": "Insufficient ETH balance (0.5).",
"details": null
}
// For ERC-20
{
"error": "RequestError",
"message": "Insufficient Token balance (11.5).",
"details": null
}
// OR
// Unknown Error
{
"error": "RequestError",
"message": "Something Went Wrong."
}GET /v1/status/deposit/:parentTxHash
Response:
// ETH Deposit Status
{
"status": "PENDING" | "DEPOSITED",
"message": "ETH-Deposit yet to be confirmed on Child Chain...⌛️" | "ETH-Deposit Confirmed on Child Chain ✅"
}
// ERC-20 Deposit Status
{
"status": "NOT_YET_CREATED" | "CREATION_FAILED" | "FUNDS_DEPOSITED_ON_CHILD" | "REDEEMED" | "EXPIRED",
"message": "The message has not been created yet." | "Deposit Transaction was not created or creation failed ❌" | "Deposit Transaction has been created on Child Chain, but not redeemed yet." | "Transaction has been redeemed on the Child chain ✅" | "Transaction has Expired on the Child chain ❌"
}GET /v1/status/withdraw/:childTxHash
Response:
{
"status": "UNCONFIRMED" | "CONFIRMED" | "EXECUTED",
"message": "Outgoing Message yet to be Confirmed on Parent chain... ⌛️" | "Outgoing Message Confirmed ✅, yet to be Executed." | "Message already executed on Parent chain 👍🏻"
}// If request body is more than 1Kb.
{
"error": "Internal Server Error",
"message": "request entity too large"
}
// If v1/payloads/* routes take more than 20s to return
// If v1/status/* routes take more than 45s to return
{
"message": "Request timed out"
}npm run dev: start server with hot reloadnpm run build: compile TypeScriptnpm run start: run built servernpm run lint: lint codenpm run deploy:erc20: deploy test ERC20 on parent chainnpm run call:deposit -- --assetType <ETH|ERC20> --amount 0.1 --from 0x...npm run call:withdraw -- --assetType <ETH|ERC20> --amount 0.1 --from 0x... --to 0x...
npm run test:integration: local HTTP tests (no RPC usage)npm run test:e2e: real network tests (requiresRUN_E2E=1and RPC keys)
Example E2E:
RUN_E2E=1 npm run test:e2e