Skip to content

feat: Solana devnet support — Anchor escrow program in Rust for USDT/USDC #11

Description

@AgrimTawani

Overview

Add Solana as a supported chain on CryptoBazaar. Unlike BSC/Polygon, Solana is not EVM — the escrow program must be written in Rust using the Anchor framework, and the frontend integration uses a completely different wallet stack (@solana/wallet-adapter-react + Phantom). This is the most significant engineering effort of all chain expansions.


Why Solana

Solana has become a dominant chain for USDT/USDC in India, driven by extremely low fees (~$0.00025/tx) and fast finality (~400ms). Many Indian traders moved from Ethereum/Polygon to Solana after the 2021-22 gas crisis. Supporting Solana opens CryptoBazaar to a large segment of the Indian stablecoin market.


Architecture Differences vs EVM

Concern EVM (Polygon/BSC) Solana
Smart contract language Solidity Rust (Anchor)
Token standard ERC-20 SPL Token
Token custody pattern approve() + transferFrom() PDA-owned Associated Token Account
State storage Contract storage mapping Per-order PDA account
Wallet MetaMask (ThirdWeb) Phantom / Solflare (@solana/wallet-adapter)
Gas token MATIC / BNB SOL
Order identity uint256 id auto-increment PDA derived from [b"order", seller_pubkey, order_nonce]
Frontend SDK ThirdWeb v5 @solana/web3.js + @coral-xyz/anchor

Part 1 — Rust/Anchor Escrow Program

Directory structure

contracts/
  evm/          ← existing Foundry project
  solana/       ← new Anchor workspace
    programs/
      cryptobazaar-escrow/
        src/
          lib.rs
          instructions/
            create_order.rs
            lock_order.rs
            mark_paid.rs
            confirm_payment.rs
            cancel_order.rs
            timeout_cancel.rs
            raise_dispute.rs
            resolve_dispute.rs
          state.rs
          errors.rs
    Anchor.toml
    Cargo.toml
    tests/
      escrow.ts   ← Anchor test suite (TypeScript)

State account (state.rs)

#[account]
pub struct EscrowOrder {
    pub seller: Pubkey,          // 32
    pub buyer: Option<Pubkey>,   // 33
    pub token_mint: Pubkey,      // 32
    pub amount: u64,             // 8
    pub price_inr: u64,          // 8  (paise, e.g. 8900 = ₹89.00)
    pub status: OrderStatus,     // 1
    pub locked_at: i64,          // 8  (Unix timestamp)
    pub paid_at: i64,            // 8
    pub bump: u8,                // 1
    pub nonce: u64,              // 8  (seller's per-order counter)
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)]
pub enum OrderStatus {
    Open,
    Locked,
    Paid,
    Disputed,
    Completed,
    Cancelled,
}

PDA derivation

Each order is a PDA:

seeds = [b"order", seller.key().as_ref(), &nonce.to_le_bytes()]

The escrow's SPL token vault is a separate PDA ATA:

seeds = [b"vault", order_pda.key().as_ref()]

The vault is a token account owned by the program PDA — no approve() needed. Tokens are transferred directly into the vault on create_order using transfer_checked.

Key instructions

create_order — Seller deposits tokens into the PDA vault.

Accounts: seller (signer), token_mint, seller_ata, vault_ata (PDA), order_account (PDA), token_program, system_program

lock_order — Buyer records themselves on the order account. No token movement.

Accounts: buyer (signer), order_account (PDA, mut), system_program

mark_paid — Buyer changes status to Paid.

Accounts: buyer (signer, must == order.buyer), order_account (PDA, mut)

confirm_payment — Seller releases vault → buyer ATA, fee → insurance fund ATA.

Accounts: seller (signer), order_account (PDA, mut), vault_ata, buyer_ata, insurance_fund_ata, token_program

timeout_cancel — Seller reclaims vault after 30 min with no markPaid.

Accounts: seller (signer), order_account (PDA, mut), vault_ata, seller_ata, token_program

cancel_order — Seller reclaims while still Open.

Accounts: seller (signer), order_account (PDA, mut), vault_ata, seller_ata, token_program

raise_dispute — Seller or buyer escalates after markPaid.

Accounts: raiser (signer, must be seller or buyer), order_account (PDA, mut)

resolve_dispute — Admin sends vault to winner.

Accounts: admin (signer), order_account (PDA, mut), vault_ata, winner_ata, token_program

Devnet test tokens

  • USDT devnet: 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU (Circle's devnet USDT)
  • USDC devnet: 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU (same faucet endpoint)
  • Get test SOL + tokens: solana airdrop 2 on devnet, then use Circle's devnet faucet for USDC.

Part 2 — Frontend Integration

New dependencies

npm install @solana/web3.js @solana/wallet-adapter-react \
  @solana/wallet-adapter-phantom @solana/spl-token \
  @coral-xyz/anchor

Wallet context — separate from ThirdWeb

Solana wallets (Phantom, Solflare) are completely separate from MetaMask/ThirdWeb. Add a SolanaWalletProvider component that wraps Solana-specific pages:

// components/SolanaWalletProvider.tsx
"use client";

import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
import { PhantomWalletAdapter } from "@solana/wallet-adapter-phantom";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";

const wallets = [new PhantomWalletAdapter()];

export function SolanaWalletProvider({ children }) {
  return (
    <ConnectionProvider endpoint={process.env.NEXT_PUBLIC_SOLANA_RPC_URL}>
      <WalletProvider wallets={wallets} autoConnect>
        <WalletModalProvider>{children}</WalletModalProvider>
      </WalletProvider>
    </ConnectionProvider>
  );
}

This provider is only mounted on Solana trade pages — it does not replace ThirdWeb for EVM pages.

isEvm guard already exists

The trade page already distinguishes chains:

const isEvm = order.chain === "POLYGON" || order.chain === "BSC";
// Solana: isEvm === false → no ThirdWeb wallet check, no MetaMask popups

The Solana trade page will need a parallel walletOk check using useWallet() from @solana/wallet-adapter-react.

Calling the program

Use the Anchor-generated IDL + @coral-xyz/anchor Program class. The IDL is output by anchor build and checked into contracts/solana/target/idl/cryptobazaar_escrow.json.

import { Program, AnchorProvider } from "@coral-xyz/anchor";
import idl from "@/contracts/cryptobazaar_escrow.json";

const provider = new AnchorProvider(connection, wallet, {});
const program = new Program(idl, provider);

await program.methods
  .lockOrder()
  .accounts({ buyer: wallet.publicKey, orderAccount: orderPda })
  .rpc();

Sell page — Solana chain selection

When BSC/Solana selector is built (see issue #10), choosing Solana will:

  1. Render a Phantom connect button instead of the MetaMask reconnect banner.
  2. Derive a new order PDA from [b"order", sellerPubkey, nonce].
  3. Call create_order via the Anchor program client.
  4. Store the PDA address (base58) as orderId in the DB.

Part 3 — DB / API Changes

The existing DB chain enum already includes SOLANA. Two small additions needed:

  • orderId for Solana orders will be the PDA pubkey (base58 string, 44 chars) — already fits in the existing String field.
  • escrowContractAddress for Solana will be the program ID (not a contract address) — same field, same type.

No schema migration needed.


Test Procedure

  1. solana config set --url devnet, airdrop 2 SOL to test wallets.
  2. Mint devnet USDC to the seller wallet via Circle faucet.
  3. anchor test — all 8 instruction tests should pass against devnet.
  4. Open /marketplace/sell in a browser with Phantom installed, select Solana, post an order — confirm 1 popup (no approve, tokens go direct to vault PDA).
  5. Open the order as a buyer with a second Phantom wallet — lock, markPaid, confirm each triggers exactly 1 Phantom popup.
  6. Confirm payment as seller — verify buyer Phantom wallet receives USDC.
  7. Verify ✓ on-chain ↗ link points to Solana Explorer (explorer.solana.com?cluster=devnet).

Files to Create / Touch

Path Action
contracts/solana/ New Anchor workspace (full)
contracts/solana/programs/cryptobazaar-escrow/src/ Rust program
contracts/solana/tests/escrow.ts Anchor test suite
frontend/src/components/SolanaWalletProvider.tsx New wallet context
frontend/src/app/marketplace/sell/page.tsx Solana chain option, Phantom connect
frontend/src/app/marketplace/[id]/page.tsx Solana branch (useWallet, Anchor calls)
frontend/src/lib/explorer.ts Solana Explorer URL
.env.example NEXT_PUBLIC_SOLANA_RPC_URL, NEXT_PUBLIC_SOLANA_PROGRAM_ID, NEXT_PUBLIC_SOLANA_USDC_MINT

Estimated Effort

Phase Work
Rust/Anchor program 3–5 days
Anchor test suite 1–2 days
Frontend wallet adapter + sell page 1–2 days
Frontend trade page Solana branch 2–3 days
End-to-end devnet testing 1 day
Total ~8–13 days

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions