Skip to content

0takuc0mrade/Swiftrail

Repository files navigation

SwiftRail: Gasless Merchant Checkout (Arbitrum)

SwiftRail is a gasless USDC checkout stack for Arbitrum Sepolia.

Customers sign once with USDC permit and do not pay ETH gas. A relayer sponsors gas, takes a small fee, and settles net USDC to the merchant.

How everything fits together (ELI5)

Think of this as 5 parts working together:

  1. frontend: the shop UI where user clicks buy.
  2. sdk: helps connect wallet and enforce Arbitrum Sepolia network.
  3. relayer: backend service that sends on-chain transactions and pays gas.
  4. payment contract: verifies permit and pulls USDC from customer.
  5. USDC token: where permit nonce/balance and token transfers happen.

Payment sequence:

  1. User clicks buy in frontend.
  2. Frontend asks relayer for fee quote (POST /quote).
  3. Frontend calls sdk.checkout(...).
  4. SDK prompts user to sign USDC permit typed-data in wallet.
  5. SDK submits permit payload to relayer (POST /submit-payment).
  6. Relayer calls contract processPayment(...) (tx #1).
  7. Contract does permit + transferFrom(user -> fee_collector).
  8. Relayer waits for tx #1 confirmation.
  9. Relayer transfers net USDC to merchant (tx #2).
  10. Relayer waits for tx #2 confirmation.
  11. Frontend refreshes dashboard (GET /dashboard/:merchant).

This is exactly why the logs show:

  1. Payment transaction submitted
  2. Payment transaction confirmed
  3. Merchant settlement tx submitted
  4. Merchant settlement tx confirmed

What this project includes

  1. Stylus payment contract (contracts/payment-contract) for permit + transfer flow.
  2. Rust relayer backend (relayer) that submits transactions, computes fees, and tracks merchant settlements.
  3. WASM SDK (sdk) for wallet connect, network guard, and checkout primitives.
  4. Demo storefront + checkout modal + merchant dashboard (frontend).

Platform hardening improvements implemented

  1. Intelligent fee model with quote endpoint (POST /quote) and fee explanation.
  2. Fiat settlement dashboard APIs (GET /dashboard/:merchant) and simulated cashout (POST /cashout).
  3. Persistent relayer state to disk (STATE_FILE_PATH, default relayer/state.json) so dashboard data survives relayer restarts.
  4. Relayer health endpoint (GET /health) for faster troubleshooting.
  5. Startup safety checks for chain ID (EXPECTED_CHAIN_ID, default 421614) and .env placeholder detection.
  6. Payment input validation in relayer (non-zero addresses, positive amount, non-expired deadline, valid v).
  7. SDK network guard: wallet connect now enforces/switches to Arbitrum Sepolia.
  8. Dev UX scripts: scripts/deploy_contract.sh, scripts/start_relayer.sh, scripts/run_frontend.sh.

Repository layout

  1. contracts/payment-contract/src/lib.rs: Stylus contract.
  2. relayer/src/main.rs: relayer API + transaction sender + fee model + state persistence.
  3. sdk/src/lib.rs: WASM SDK.
  4. frontend/index.html: demo storefront and dashboard UI.
  5. frontend/app.js: checkout and dashboard logic.
  6. scripts/deploy_contract.sh: build, verify, deploy, and initialize contract.
  7. scripts/start_relayer.sh: relayer launcher.
  8. scripts/run_frontend.sh: local frontend server.

Prerequisites

  1. Rust (stable) and Cargo.
  2. wasm32-unknown-unknown Rust target.
  3. cargo-stylus for Stylus build/deploy.
  4. python3 to serve frontend.
  5. Optional but recommended: Foundry cast for contract initialize verification.
  6. MetaMask connected to Arbitrum Sepolia.
  7. Arbitrum Sepolia ETH for relayer wallet gas.
  8. Arbitrum Sepolia USDC for customer wallet.
  9. Arbitrum Sepolia USDC for relayer settlement sender when fee-collector/sender config differs.

One-time setup

cd /home/peter/gasless
rustup target add wasm32-unknown-unknown
cargo install cargo-stylus

Generate relayer wallet keystore:

cargo run --manifest-path relayer/Cargo.toml --bin generate_wallet

This creates secrets/keystore.json. Fund that relayer address with Arbitrum Sepolia ETH.

Step 1: Deploy and initialize contract

chmod +x scripts/deploy_contract.sh
./scripts/deploy_contract.sh

The script will:

  1. Build and check the Stylus contract.
  2. Prompt for deployer private key.
  3. Prompt for USDC address.
  4. Deploy contract to Arbitrum Sepolia.
  5. If cast is installed, call initialize(address) automatically.

If cast is missing, initialize manually:

cast send <DEPLOYED_CONTRACT_ADDRESS> "initialize(address)" <USDC_ADDRESS> \
  --private-key <DEPLOYER_PRIVATE_KEY> \
  --rpc-url https://sepolia-rollup.arbitrum.io/rpc

Step 2: Configure relayer

Create .env from template:

cp .env.example .env

Fill values in .env:

  1. RPC_URL: Arbitrum Sepolia RPC.
  2. EXPECTED_CHAIN_ID: 421614.
  3. CONTRACT_ADDRESS: deployed Stylus contract.
  4. USDC_ADDRESS: same USDC token used at deploy/initialize.
  5. KEYSTORE_PATH: usually secrets/keystore.json.
  6. RELAYER_FEE_COLLECTOR optional.
  7. STATE_FILE_PATH optional.

Step 3: Start relayer

./scripts/start_relayer.sh

You will be prompted for keystore password.

Health check:

curl -s http://127.0.0.1:3000/health

Quote check:

curl -s -X POST http://127.0.0.1:3000/quote \
  -H "content-type: application/json" \
  -d '{"amount":"20000000"}'

Step 4: Build SDK wasm package (when SDK changes)

wasm-pack build sdk --target web --out-dir ../frontend/pkg

If you did not modify sdk/src/lib.rs, you can skip this and use the committed frontend/pkg.

Step 5: Run frontend

./scripts/run_frontend.sh

Open http://127.0.0.1:4173.

Set Runtime Config in UI:

  1. Relayer URL: http://127.0.0.1:3000
  2. Payment Contract: deployed contract address.
  3. USDC Address: token address.
  4. Merchant Address: wallet that should receive net settlements.
  5. Bank Account Hint: any text (used for simulated cashout log).

Click Save Config.

Developer run order (short)

  1. Deploy/initialize contract.
  2. Fill .env.
  3. Start relayer.
  4. Build SDK wasm if needed.
  5. Serve frontend.
  6. Connect wallet and run payment flow.

One-command local startup

For hackathon demos, use:

./scripts/dev_up.sh

What it does:

  1. Validates .env is present and has no <...> placeholders.
  2. Starts frontend server in background (http://127.0.0.1:4173).
  3. Starts relayer in foreground (you enter keystore password here).
  4. Cleans up frontend automatically when you stop the script.

User flow (what happens in a payment)

  1. Customer clicks Pay With Arbitrum.
  2. Frontend requests fee preview from relayer POST /quote.
  3. Checkout modal shows gross amount, relayer fee, and merchant net.
  4. Frontend invokes SDK checkout (sdk.checkout(...)).
  5. SDK signs permit typed-data and sends payload to relayer POST /submit-payment.
  6. Relayer preflights permit call and normalizes v (0/1 to 27/28 when needed).
  7. Relayer submits contract call processPayment(...) and waits for receipt.
  8. Contract executes permit then transferFrom to fee collector.
  9. Relayer submits merchant settlement transfer and waits for receipt.
  10. Frontend fetches GET /dashboard/:merchant and updates metrics/history.
  11. Merchant may click Cash Out To Bank for simulated off-ramp record (POST /cashout).

API summary

  1. GET /health: relayer health, chain/config, record counts.
  2. POST /quote: returns fee quote for USDC amount.
  3. POST /submit-payment: executes gasless payment path.
  4. GET /dashboard/:merchant: merchant metrics and recent records.
  5. POST /cashout: simulated cashout of available merchant balance.

Key config values explained

  1. merchant address: receives net settlement and is used for dashboard aggregation.
  2. deployer address: contract owner/admin, used for deployment and initialization.
  3. relayer wallet: signs on-chain relayer transactions and pays gas.
  4. relayer fee collector: receives customer gross funds before merchant net settlement.
  5. USDC_ADDRESS: permit and settlement token contract.
  6. CONTRACT_ADDRESS: gasless payment contract called by relayer.

For hackathon MVP, merchant and deployer can be the same address.

Why relayer may need both ETH and USDC

  1. ETH is always needed by relayer wallet for gas.
  2. USDC movement is two-step: user funds first arrive at fee_collector, then relayer sends net to merchant.
  3. If RELAYER_FEE_COLLECTOR is unset, relayer uses unlocked wallet as fee collector (default happy path).
  4. If RELAYER_FEE_COLLECTOR points to a different address, the relayer sender may have no USDC and settlement can fail.
  5. Error ERC20: transfer amount exceeds balance means the settlement sender did not have enough USDC at that moment.

SDK for builders

Use the SDK when you want your app to handle wallet connect, chain guard, permit signing, and relayer submission through one interface.

Published npm package: @0takuc0mrade/swiftrail-sdk
Package URL: https://www.npmjs.com/package/@0takuc0mrade/swiftrail-sdk

Framework-specific examples (React/Vite/Next.js): docs/sdk-integration.md Publishing to npm/crates/JSR: docs/sdk-publishing.md

Install:

npm i @0takuc0mrade/swiftrail-sdk

1. Build SDK artifacts

wasm-pack build sdk --target web --out-dir ./frontend/pkg

If your app lives elsewhere, point --out-dir to your app package folder and import from there.

2. Initialize and create SDK client

import init, { ArbitrumCheckout } from "@0takuc0mrade/swiftrail-sdk";

await init();

const sdk = new ArbitrumCheckout(
  "http://127.0.0.1:3000", // relayer URL
  "0xYourPaymentContract", // CONTRACT_ADDRESS
  "0xYourUsdcAddress"      // USDC_ADDRESS
);

3. Connect wallet (MetaMask)

const userAddress = await sdk.connect_wallet();

What this does:

  1. Requests account access from MetaMask.
  2. Verifies current chain is Arbitrum Sepolia (421614).
  3. Attempts wallet_switchEthereumChain if user is on the wrong network.

4. Submit checkout

const merchantAddress = "0xMerchantAddress";
const amount = "20000000"; // 20.000000 USDC (6 decimals)

const resultJson = await sdk.checkout(userAddress, merchantAddress, amount);
const result = JSON.parse(resultJson);

What checkout(...) does internally:

  1. Reads USDC permit nonce from chain.
  2. Reads permit domain name/version from token (name() / version(), with fallback).
  3. Builds EIP-712 permit typed data.
  4. Prompts MetaMask signature (eth_signTypedData_v4).
  5. Sends signed payload to relayer POST /submit-payment.
  6. Returns relayer response JSON text (or throws on failure).

5. Expected successful response shape

{
  "payment_tx_hash": "0x...",
  "settlement_tx_hash": "0x...",
  "gross_amount": "20000000",
  "fee_amount": "90000",
  "net_amount": "19910000",
  "fee_bps": 45,
  "fee_model_notes": ["..."]
}

6. Error handling pattern

try {
  const resultJson = await sdk.checkout(userAddress, merchantAddress, amount);
  const result = JSON.parse(resultJson);
  console.log("payment", result.payment_tx_hash);
} catch (err) {
  console.error("checkout failed", String(err));
}

7. SDK API reference

  1. new ArbitrumCheckout(relayerUrl, contractAddress, usdcAddress)
  2. connect_wallet(): Promise<string>
  3. checkout(userAddress, recipientAddress, amountBaseUnits): Promise<string>

Notes:

  1. amountBaseUnits must be a decimal string in token base units (USDC has 6 decimals).
  2. SDK assumes browser wallet provider (window.ethereum) is available.
  3. SDK checkout returns JSON text; parse it with JSON.parse(...).

Troubleshooting

  1. Wrong chain detected at relayer startup: Set correct RPC_URL or update EXPECTED_CHAIN_ID.
  2. Payment failed: permit deadline has already expired: Retry checkout and sign again.
  3. No available balance to cash out: At least one successful payment is required first.
  4. Wallet connected but payment fails immediately: Confirm customer has USDC and MetaMask is on Arbitrum Sepolia.
  5. Dashboard data disappeared after restart: Check STATE_FILE_PATH and ensure relayer can write to it.
  6. Settlement transfer failed ... ERC20: transfer amount exceeds balance: Confirm relayer sender/fee collector alignment and USDC balance on settlement sender. Also ensure relayer process includes receipt-wait sequencing (restart relayer after updates).
  7. :4173/favicon.ico 404 in browser console: Harmless missing icon in local frontend server; unrelated to checkout logic.
  8. USDC nonce call returned empty data or Failed to parse nonce: Verify USDC_ADDRESS is correct for the selected chain and exposes nonces(address).
  9. MetaMask provider unavailable: Install/enable MetaMask and run in a browser context (not server-side render).

Hackathon scope notes

This repo is optimized for MVP demonstration speed.

In production, next upgrades would be:

  1. Persistent database instead of JSON file.
  2. Auth/RBAC for merchant endpoints.
  3. Rate limiting and abuse protection.
  4. Retry queue and transaction reconciliation.
  5. Real fiat off-ramp integration.

About

A gasless USDC checkout stack for Arbitrum Sepolia

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors