From 4415311bca6e41563b1999c0035515f4bdcace46 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 23 Mar 2026 21:18:16 -0700 Subject: [PATCH 1/2] Add a skill for testing the sandbox usdc integrations e2e. --- .claude/skills/grid-usdc-sandbox/SKILL.md | 539 ++++++++++++++++++ .../skills/grid-usdc-sandbox/solana_helper.py | 254 +++++++++ 2 files changed, 793 insertions(+) create mode 100644 .claude/skills/grid-usdc-sandbox/SKILL.md create mode 100644 .claude/skills/grid-usdc-sandbox/solana_helper.py diff --git a/.claude/skills/grid-usdc-sandbox/SKILL.md b/.claude/skills/grid-usdc-sandbox/SKILL.md new file mode 100644 index 00000000..d9258340 --- /dev/null +++ b/.claude/skills/grid-usdc-sandbox/SKILL.md @@ -0,0 +1,539 @@ +--- +name: grid-usdc-sandbox +description: > + End-to-end USDC sandbox flow tests using real Solana devnet funds. Use when the user asks to + "test USDC flows", "run sandbox tests", "test deposits and withdrawals", "test USDC sandbox", + "run e2e USDC test", "test realtime funding", "test USDC to USD", "test USDC to MXN", + or wants to verify Grid's USDC deposit/withdrawal/quote pipeline on devnet. +allowed-tools: + - Bash + - Read + - Grep + - Glob + - WebFetch +--- + +# Grid USDC Sandbox Flow Test + +End-to-end test of USDC sandbox flows: deposits, withdrawals, and cross-currency quotes using real Solana devnet funds. + +## Prerequisites + +Run these steps before any tests. Stop and report if any step fails. + +### 1. Load Grid API credentials + +```bash +export GRID_API_TOKEN_ID=$(jq -r .apiTokenId ~/.grid-credentials) +export GRID_API_CLIENT_SECRET=$(jq -r .apiClientSecret ~/.grid-credentials) +export GRID_BASE_URL=$(jq -r '.baseUrl // "https://api.lightspark.com/grid/2025-10-13"' ~/.grid-credentials) +``` + +### 2. Verify Solana devnet key exists + +```bash +jq -r '.solanaDevnetPrivateKey // empty' ~/.grid-credentials +``` + +If empty, stop and tell the user to add `solanaDevnetPrivateKey` (base58-encoded 64-byte keypair) to `~/.grid-credentials`. + +### 3. Install Python dependencies + +```bash +pip3 install solders solana base58 2>&1 | tail -5 +``` + +### 4. Set helper alias + +```bash +SOLANA_HELPER="python3 $(pwd)/.claude/skills/grid-usdc-sandbox/solana_helper.py" +``` + +### 5. Check SOL balance and airdrop if needed + +```bash +$SOLANA_HELPER sol-balance +``` + +If `sol` < 0.1, airdrop: + +```bash +$SOLANA_HELPER airdrop-sol --amount 1000000000 +``` + +### 6. Check USDC balance + +```bash +$SOLANA_HELPER usdc-balance +``` + +If `amount` < 1.0 USDC, warn the user that some tests may fail due to insufficient devnet USDC. Print instructions for obtaining devnet USDC (e.g., Solana devnet USDC faucet or manual transfer). + +### 7. Print wallet address + +```bash +$SOLANA_HELPER wallet-address +``` + +Save the address as `$WALLET_ADDRESS` for use in test cases. + +--- + +## Test Cases + +Run tests sequentially. Each test may depend on state created by prior tests. Track results for the final summary table. + +--- + +### Test 1: Customer + USDC Account Creation + +**Goal:** Create a customer and verify USDC internal account with funding instructions. + +**Steps:** + +1. Create a customer with a unique `platformCustomerId`: + +```bash +PLATFORM_CUSTOMER_ID="usdc-test-$(date +%s)" +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"customerType\": \"INDIVIDUAL\", + \"platformCustomerId\": \"$PLATFORM_CUSTOMER_ID\", + \"fullName\": \"USDC Test User\", + \"birthDate\": \"1990-01-15\", + \"nationality\": \"US\" + }" \ + "$GRID_BASE_URL/customers" +``` + +Extract and save `CUSTOMER_ID` from the response `id` field. + +2. List internal accounts: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + "$GRID_BASE_URL/customers/internal-accounts?customerId=$CUSTOMER_ID&limit=100" +``` + +3. From the response, extract: + - `USDC_INTERNAL_ID`: the `id` of the internal account where `balance.currency` is `USDC` + - `USD_INTERNAL_ID`: the `id` of the internal account where `balance.currency` is `USD` + - `DEPOSIT_ADDRESS`: from the USDC account's `fundingPaymentInstructions` array, find the entry where `accountOrWalletInfo.accountType` is `SOLANA_WALLET` and extract `accountOrWalletInfo.address` + +4. **PASS criteria:** + - Customer created successfully + - USDC internal account exists + - `fundingPaymentInstructions` contains a `SOLANA_WALLET` entry with a non-empty `address` + +--- + +### Test 2: Fund Internal Account with Real Devnet USDC + +**Goal:** Send real USDC on devnet and verify Grid detects the deposit. + +**Steps:** + +1. Record initial USDC balance from internal account: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + "$GRID_BASE_URL/customers/internal-accounts?customerId=$CUSTOMER_ID¤cy=USDC" +``` + +Save initial `balance.amount` as `INITIAL_USDC_BALANCE`. + +2. Send 0.50 USDC to the deposit address: + +```bash +$SOLANA_HELPER send-usdc --to $DEPOSIT_ADDRESS --amount 500000 +``` + +Verify the send was confirmed (status = "confirmed"). + +3. Poll for balance update every 5 seconds, up to 120 seconds: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + "$GRID_BASE_URL/customers/internal-accounts?customerId=$CUSTOMER_ID¤cy=USDC" +``` + +4. **PASS criteria:** USDC internal account `balance.amount` increases above `INITIAL_USDC_BALANCE`. + +--- + +### Test 3: Transfer Out (USDC internal → external Solana wallet) + +**Goal:** Withdraw USDC from internal account to an external Solana devnet wallet. + +**Steps:** + +1. Create an external account for our wallet: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"customerId\": \"$CUSTOMER_ID\", + \"currency\": \"USDC\", + \"cryptoNetwork\": \"SOLANA_DEVNET\", + \"accountInfo\": { + \"accountType\": \"SOLANA_WALLET\", + \"address\": \"$WALLET_ADDRESS\" + } + }" \ + "$GRID_BASE_URL/customers/external-accounts" +``` + +Save the `id` as `USDC_EXTERNAL_ID`. + +2. Record initial on-chain USDC balance: + +```bash +$SOLANA_HELPER usdc-balance +``` + +Save `raw` as `INITIAL_ONCHAIN_USDC`. + +3. Transfer out 0.10 USDC: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"source\": {\"accountId\": \"$USDC_INTERNAL_ID\"}, + \"destination\": {\"accountId\": \"$USDC_EXTERNAL_ID\"}, + \"amount\": 100000 + }" \ + "$GRID_BASE_URL/transfer-out" +``` + +4. Poll on-chain USDC balance every 5 seconds, up to 120 seconds: + +```bash +$SOLANA_HELPER usdc-balance +``` + +5. **PASS criteria:** On-chain USDC balance (`raw`) increases by approximately 100000 (0.10 USDC) from `INITIAL_ONCHAIN_USDC`. + +--- + +### Test 4: USDC → USD Quote (Real-Time Funded → internal USD account) + +**Goal:** Use real-time funding to convert USDC to USD via a JIT quote, depositing into the customer's internal USD account. + +**Steps:** + +1. Create a real-time funded quote: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"source\": { + \"sourceType\": \"REALTIME_FUNDING\", + \"customerId\": \"$CUSTOMER_ID\", + \"currency\": \"USDC\", + \"cryptoNetwork\": \"SOLANA_DEVNET\" + }, + \"destination\": { + \"destinationType\": \"ACCOUNT\", + \"accountId\": \"$USD_INTERNAL_ID\" + }, + \"lockedCurrencySide\": \"RECEIVING\", + \"lockedCurrencyAmount\": 10, + \"purposeOfPayment\": \"GOODS_OR_SERVICES\" + }" \ + "$GRID_BASE_URL/quotes" +``` + +2. Extract from the response: + - `QUOTE_ID`: the `id` field + - `TRANSACTION_ID`: the `transactionId` field + - `PAYMENT_ADDRESS`: from `paymentInstructions`, find the `SOLANA_WALLET` entry and extract `accountOrWalletInfo.address` + - `TOTAL_SENDING_AMOUNT`: the `totalSendingAmount` field (this is the micro-USDC amount to send) + +3. Record initial USD internal account balance: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + "$GRID_BASE_URL/customers/internal-accounts?customerId=$CUSTOMER_ID¤cy=USD" +``` + +Save `balance.amount` as `INITIAL_USD_BALANCE`. + +4. Send USDC to the payment instructions address: + +```bash +$SOLANA_HELPER send-usdc --to $PAYMENT_ADDRESS --amount $TOTAL_SENDING_AMOUNT +``` + +5. Poll USD internal account balance every 5 seconds, up to 120 seconds: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + "$GRID_BASE_URL/customers/internal-accounts?customerId=$CUSTOMER_ID¤cy=USD" +``` + +6. **PASS criteria:** USD internal account `balance.amount` increases above `INITIAL_USD_BALANCE`. + +--- + +### Test 5: USDC → USD Quote (Real-Time Funded → external USD bank account) + +**Goal:** Convert USDC to USD and send to an external bank account via ACH. + +**Steps:** + +1. Create an external USD bank account: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"customerId\": \"$CUSTOMER_ID\", + \"currency\": \"USD\", + \"accountInfo\": { + \"accountType\": \"USD_ACCOUNT\", + \"paymentRails\": [\"ACH\"], + \"routingNumber\": \"021000021\", + \"accountNumber\": \"123456789012\", + \"accountCategory\": \"CHECKING\", + \"beneficiary\": { + \"beneficiaryType\": \"INDIVIDUAL\", + \"fullName\": \"USDC Test User\", + \"birthDate\": \"1990-01-15\", + \"nationality\": \"US\" + } + } + }" \ + "$GRID_BASE_URL/customers/external-accounts" +``` + +Save the `id` as `USD_EXTERNAL_ID`. + +2. Create a real-time funded quote: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"source\": { + \"sourceType\": \"REALTIME_FUNDING\", + \"customerId\": \"$CUSTOMER_ID\", + \"currency\": \"USDC\", + \"cryptoNetwork\": \"SOLANA_DEVNET\" + }, + \"destination\": { + \"destinationType\": \"ACCOUNT\", + \"accountId\": \"$USD_EXTERNAL_ID\", + \"paymentRail\": \"ACH\" + }, + \"lockedCurrencySide\": \"RECEIVING\", + \"lockedCurrencyAmount\": 10, + \"purposeOfPayment\": \"GOODS_OR_SERVICES\" + }" \ + "$GRID_BASE_URL/quotes" +``` + +3. Extract `QUOTE_ID`, `TRANSACTION_ID`, `PAYMENT_ADDRESS`, and `TOTAL_SENDING_AMOUNT` as in Test 4. + +4. Send USDC to the payment instructions address: + +```bash +$SOLANA_HELPER send-usdc --to $PAYMENT_ADDRESS --amount $TOTAL_SENDING_AMOUNT +``` + +5. Poll transaction status every 5 seconds, up to 120 seconds: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + "$GRID_BASE_URL/transactions/$TRANSACTION_ID" +``` + +6. **PASS criteria:** Transaction status reaches `PROCESSING` or `COMPLETED`. + +--- + +### Test 6: USDC → MXN Quote (Real-Time Funded → external MXN CLABE account) + +**Goal:** Convert USDC to MXN and send to a Mexican bank account via SPEI. + +**Steps:** + +1. Create an external MXN account: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"customerId\": \"$CUSTOMER_ID\", + \"currency\": \"MXN\", + \"accountInfo\": { + \"accountType\": \"MXN_ACCOUNT\", + \"paymentRails\": [\"SPEI\"], + \"clabeNumber\": \"032180000118359719\", + \"beneficiary\": { + \"beneficiaryType\": \"INDIVIDUAL\", + \"fullName\": \"USDC Test User\", + \"birthDate\": \"1990-01-15\", + \"nationality\": \"MX\" + } + } + }" \ + "$GRID_BASE_URL/customers/external-accounts" +``` + +Save the `id` as `MXN_EXTERNAL_ID`. + +2. Create a real-time funded quote: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"source\": { + \"sourceType\": \"REALTIME_FUNDING\", + \"customerId\": \"$CUSTOMER_ID\", + \"currency\": \"USDC\", + \"cryptoNetwork\": \"SOLANA_DEVNET\" + }, + \"destination\": { + \"destinationType\": \"ACCOUNT\", + \"accountId\": \"$MXN_EXTERNAL_ID\", + \"paymentRail\": \"SPEI\" + }, + \"lockedCurrencySide\": \"RECEIVING\", + \"lockedCurrencyAmount\": 200, + \"purposeOfPayment\": \"GOODS_OR_SERVICES\" + }" \ + "$GRID_BASE_URL/quotes" +``` + +Note: `lockedCurrencyAmount: 200` = 2.00 MXN (smallest unit = centavos), roughly ~$0.10 USD. + +3. Extract `QUOTE_ID`, `TRANSACTION_ID`, `PAYMENT_ADDRESS`, and `TOTAL_SENDING_AMOUNT`. + +4. Send USDC: + +```bash +$SOLANA_HELPER send-usdc --to $PAYMENT_ADDRESS --amount $TOTAL_SENDING_AMOUNT +``` + +5. Poll transaction status every 5 seconds, up to 120 seconds: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + "$GRID_BASE_URL/transactions/$TRANSACTION_ID" +``` + +6. **PASS criteria:** Transaction status reaches `PROCESSING` or `COMPLETED`. + +--- + +### Test 7: USD → USDC Quote (Account-Funded → external Solana wallet) + +**Goal:** Convert USD from internal account to USDC delivered to our external Solana devnet wallet. + +**Steps:** + +1. Fund the USD internal account via sandbox endpoint: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d '{"amount": 100}' \ + "$GRID_BASE_URL/sandbox/internal-accounts/$USD_INTERNAL_ID/fund" +``` + +Verify the balance increased (response contains updated account). + +2. Record initial on-chain USDC balance: + +```bash +$SOLANA_HELPER usdc-balance +``` + +Save `raw` as `INITIAL_ONCHAIN_USDC_T7`. + +3. Ensure USDC external account exists (reuse `$USDC_EXTERNAL_ID` from Test 3). If Test 3 was skipped, create it now. + +4. Create an account-funded quote: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"source\": { + \"sourceType\": \"ACCOUNT\", + \"accountId\": \"$USD_INTERNAL_ID\" + }, + \"destination\": { + \"destinationType\": \"ACCOUNT\", + \"accountId\": \"$USDC_EXTERNAL_ID\" + }, + \"lockedCurrencySide\": \"SENDING\", + \"lockedCurrencyAmount\": 50, + \"purposeOfPayment\": \"GOODS_OR_SERVICES\" + }" \ + "$GRID_BASE_URL/quotes" +``` + +Save `QUOTE_ID` and `TRANSACTION_ID` from the response. + +5. Execute the quote: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST \ + "$GRID_BASE_URL/quotes/$QUOTE_ID/execute" +``` + +6. Poll on-chain USDC balance every 5 seconds, up to 120 seconds: + +```bash +$SOLANA_HELPER usdc-balance +``` + +7. **PASS criteria:** On-chain USDC balance (`raw`) increases above `INITIAL_ONCHAIN_USDC_T7`. + +--- + +## Results Summary + +After all tests complete, print a final results table: + +``` +| # | Test Case | Status | Details | +|---|----------------------------------------|--------|---------| +| 1 | Customer + USDC Account Creation | PASS/FAIL | ... | +| 2 | Fund Internal Account (devnet USDC) | PASS/FAIL | ... | +| 3 | Transfer Out (USDC → Solana wallet) | PASS/FAIL | ... | +| 4 | USDC → USD (RT funded → internal) | PASS/FAIL | ... | +| 5 | USDC → USD (RT funded → external bank) | PASS/FAIL | ... | +| 6 | USDC → MXN (RT funded → CLABE) | PASS/FAIL | ... | +| 7 | USD → USDC (Account funded → wallet) | PASS/FAIL | ... | +``` + +Include in Details: relevant amounts, transaction IDs, error messages, or timing info. + +## Error Handling + +- If a test fails, record the failure and continue to the next test (do not abort the entire suite). +- If a polling loop times out, record FAIL with "timeout after 120s" and the last observed state. +- If the `send-usdc` command fails, check SOL balance (may need airdrop for gas) and USDC balance (may be insufficient). +- If a quote returns an error about `totalSendingAmount` being too small or too large, adjust the `lockedCurrencyAmount` and retry once. +- Common API errors: + - `USER_NOT_FOUND`: sandbox VASP may not have the required user — note in results + - `INSUFFICIENT_BALANCE`: the internal account doesn't have enough funds — note in results + - `QUOTE_EXPIRED`: quote expired before funding — retry with faster execution + +## Amounts Reference + +All tests use small amounts to conserve devnet funds: +- Test 2: 0.50 USDC deposit (500000 micro-USDC) +- Test 3: 0.10 USDC transfer-out (100000 micro-USDC) +- Tests 4-5: ~$0.10 USD locked on receiving side (10 cents) +- Test 6: ~2.00 MXN locked on receiving side (~$0.10 USD) +- Test 7: $0.50 USD → USDC (50 cents) +- **Total USDC needed: ~1.0 USDC + gas (~0.01 SOL)** diff --git a/.claude/skills/grid-usdc-sandbox/solana_helper.py b/.claude/skills/grid-usdc-sandbox/solana_helper.py new file mode 100644 index 00000000..6906076d --- /dev/null +++ b/.claude/skills/grid-usdc-sandbox/solana_helper.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +"""Solana devnet CLI for Grid USDC sandbox testing. + +Subcommands: + wallet-address Print public key of loaded devnet keypair + sol-balance [--address] Print SOL balance + usdc-balance [--address] Print USDC balance (raw + human-readable) + send-usdc --to --amount Send USDC on devnet (amount in micro-USDC) + airdrop-sol [--amount] Request devnet SOL airdrop +""" + +import argparse +import json +import os +import sys +import time +from pathlib import Path + +try: + import base58 + from solders.keypair import Keypair + from solders.pubkey import Pubkey + from solders.system_program import ID as SYS_PROGRAM_ID + from solders.transaction import Transaction + from solders.message import Message + from solders.instruction import Instruction, AccountMeta + from solders.hash import Hash + from solana.rpc.api import Client + from solana.rpc.commitment import Confirmed, Finalized + from solana.rpc.types import TxOpts + import struct +except ImportError as e: + print(json.dumps({"error": "Missing dependencies. Install with: pip3 install solders solana base58", "detail": str(e)})) + sys.exit(1) + +DEVNET_RPC = "https://api.devnet.solana.com" +DEVNET_USDC_MINT = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" +TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") +ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") +USDC_DECIMALS = 6 + + +def load_keypair(creds_path=None): + creds_path = creds_path or os.path.expanduser("~/.grid-credentials") + with open(creds_path) as f: + creds = json.load(f) + secret_key = creds.get("solanaDevnetPrivateKey") + if not secret_key: + print(json.dumps({"error": "solanaDevnetPrivateKey not found in ~/.grid-credentials"})) + sys.exit(1) + raw = base58.b58decode(secret_key) + if len(raw) == 32: + return Keypair.from_seed(raw) + return Keypair.from_bytes(raw) + + +def get_client(): + return Client(DEVNET_RPC) + + +def get_ata(owner, mint): + seeds = [bytes(owner), bytes(TOKEN_PROGRAM_ID), bytes(mint)] + ata, _bump = Pubkey.find_program_address(seeds, ASSOCIATED_TOKEN_PROGRAM_ID) + return ata + + +def get_token_balance(client, address, mint_str): + mint = Pubkey.from_string(mint_str) + ata = get_ata(address, mint) + try: + resp = client.get_token_account_balance(ata) + except Exception: + return 0, "0" + if resp.value is None: + return 0, "0" + return int(resp.value.amount), resp.value.ui_amount_string + + +def cmd_wallet_address(args): + kp = load_keypair() + print(json.dumps({"address": str(kp.pubkey())})) + + +def cmd_sol_balance(args): + client = get_client() + if args.address: + pubkey = Pubkey.from_string(args.address) + else: + kp = load_keypair() + pubkey = kp.pubkey() + resp = client.get_balance(pubkey, commitment=Confirmed) + lamports = resp.value + print(json.dumps({ + "address": str(pubkey), + "lamports": lamports, + "sol": lamports / 1e9 + })) + + +def cmd_usdc_balance(args): + client = get_client() + mint_str = args.mint or DEVNET_USDC_MINT + if args.address: + pubkey = Pubkey.from_string(args.address) + else: + kp = load_keypair() + pubkey = kp.pubkey() + raw_amount, ui_amount = get_token_balance(client, pubkey, mint_str) + print(json.dumps({ + "address": str(pubkey), + "mint": mint_str, + "raw": raw_amount, + "amount": raw_amount / (10 ** USDC_DECIMALS), + "ui_amount": ui_amount + })) + + +def cmd_send_usdc(args): + kp = load_keypair() + client = get_client() + mint_str = args.mint or DEVNET_USDC_MINT + mint = Pubkey.from_string(mint_str) + recipient = Pubkey.from_string(args.to) + amount = int(args.amount) + + sender_ata = get_ata(kp.pubkey(), mint) + recipient_ata = get_ata(recipient, mint) + + instructions = [] + + recipient_ata_info = client.get_account_info(recipient_ata) + if recipient_ata_info.value is None: + create_ata_ix = Instruction( + program_id=ASSOCIATED_TOKEN_PROGRAM_ID, + accounts=[ + AccountMeta(pubkey=kp.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=recipient_ata, is_signer=False, is_writable=True), + AccountMeta(pubkey=recipient, is_signer=False, is_writable=False), + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False), + AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), + ], + data=bytes(), + ) + instructions.append(create_ata_ix) + + transfer_data = bytearray([12]) + transfer_data.extend(struct.pack(" Date: Thu, 26 Mar 2026 09:46:52 -0700 Subject: [PATCH 2/2] Adding base tests --- .../skills/grid-base-usdc-sandbox/SKILL.md | 537 ++++++++++++++++++ .../grid-base-usdc-sandbox/base_helper.py | 185 ++++++ 2 files changed, 722 insertions(+) create mode 100644 .claude/skills/grid-base-usdc-sandbox/SKILL.md create mode 100644 .claude/skills/grid-base-usdc-sandbox/base_helper.py diff --git a/.claude/skills/grid-base-usdc-sandbox/SKILL.md b/.claude/skills/grid-base-usdc-sandbox/SKILL.md new file mode 100644 index 00000000..2629d3c0 --- /dev/null +++ b/.claude/skills/grid-base-usdc-sandbox/SKILL.md @@ -0,0 +1,537 @@ +--- +name: grid-base-usdc-sandbox +description: > + End-to-end Base USDC sandbox flow tests using real Base Sepolia testnet funds. Use when the user asks to + "test Base USDC flows", "run Base sandbox tests", "test Base deposits and withdrawals", "test Base USDC sandbox", + "run e2e Base USDC test", "test Base realtime funding", "test Base USDC to USD", "test Base USDC to MXN", + or wants to verify Grid's USDC deposit/withdrawal/quote pipeline on Base testnet. +allowed-tools: + - Bash + - Read + - Grep + - Glob + - WebFetch +--- + +# Grid Base USDC Sandbox Flow Test + +End-to-end test of USDC sandbox flows on Base Sepolia: deposits, withdrawals, and cross-currency quotes using real Base testnet funds. + +## Prerequisites + +Run these steps before any tests. Stop and report if any step fails. + +### 1. Load Grid API credentials + +```bash +export GRID_API_TOKEN_ID=$(jq -r .apiTokenId ~/.grid-credentials) +export GRID_API_CLIENT_SECRET=$(jq -r .apiClientSecret ~/.grid-credentials) +export GRID_BASE_URL=$(jq -r '.baseUrl // "https://api.lightspark.com/grid/2025-10-13"' ~/.grid-credentials) +``` + +### 2. Verify Base testnet key exists + +```bash +jq -r '.baseTestnetPrivateKey // empty' ~/.grid-credentials +``` + +If empty, stop and tell the user to add `baseTestnetPrivateKey` (hex-encoded Ethereum private key, with or without `0x` prefix) to `~/.grid-credentials`. + +### 3. Install Python dependencies + +```bash +pip3 install web3 2>&1 | tail -5 +``` + +### 4. Set helper alias + +```bash +BASE_HELPER="python3 $(pwd)/.claude/skills/grid-base-usdc-sandbox/base_helper.py" +``` + +### 5. Check ETH balance (gas) + +```bash +$BASE_HELPER eth-balance +``` + +If `eth` < 0.001, warn the user that they need Base Sepolia ETH for gas. They can obtain it from: +- https://www.alchemy.com/faucets/base-sepolia +- https://faucet.quicknode.com/base/sepolia + +### 6. Check USDC balance + +```bash +$BASE_HELPER usdc-balance +``` + +If `amount` < 1.0 USDC, warn the user that some tests may fail due to insufficient testnet USDC. The Base Sepolia USDC contract is `0x036CbD53842c5426634e7929541eC2318f3dCF7e` — they can obtain testnet USDC from Circle's testnet faucet at https://faucet.circle.com/ (select Base Sepolia). + +### 7. Print wallet address + +```bash +$BASE_HELPER wallet-address +``` + +Save the address as `$WALLET_ADDRESS` for use in test cases. + +--- + +## Test Cases + +Run tests sequentially. Each test may depend on state created by prior tests. Track results for the final summary table. + +--- + +### Test 1: Customer + USDC Account Creation + +**Goal:** Create a customer and verify USDC internal account with Base wallet funding instructions. + +**Steps:** + +1. Create a customer with a unique `platformCustomerId`: + +```bash +PLATFORM_CUSTOMER_ID="base-usdc-test-$(date +%s)" +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"customerType\": \"INDIVIDUAL\", + \"platformCustomerId\": \"$PLATFORM_CUSTOMER_ID\", + \"fullName\": \"Base USDC Test User\", + \"birthDate\": \"1990-01-15\", + \"nationality\": \"US\" + }" \ + "$GRID_BASE_URL/customers" +``` + +Extract and save `CUSTOMER_ID` from the response `id` field. + +2. List internal accounts: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + "$GRID_BASE_URL/customers/internal-accounts?customerId=$CUSTOMER_ID&limit=100" +``` + +3. From the response, extract: + - `USDC_INTERNAL_ID`: the `id` of the internal account where `balance.currency` is `USDC` + - `USD_INTERNAL_ID`: the `id` of the internal account where `balance.currency` is `USD` + - `DEPOSIT_ADDRESS`: from the USDC account's `fundingPaymentInstructions` array, find the entry where `accountOrWalletInfo.accountType` is `BASE_WALLET` and extract `accountOrWalletInfo.address` + +4. **PASS criteria:** + - Customer created successfully + - USDC internal account exists + - `fundingPaymentInstructions` contains a `BASE_WALLET` entry with a non-empty `address` + +--- + +### Test 2: Fund Internal Account with Real Testnet USDC + +**Goal:** Send real USDC on Base Sepolia and verify Grid detects the deposit. + +**Steps:** + +1. Record initial USDC balance from internal account: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + "$GRID_BASE_URL/customers/internal-accounts?customerId=$CUSTOMER_ID¤cy=USDC" +``` + +Save initial `balance.amount` as `INITIAL_USDC_BALANCE`. + +2. Send 0.50 USDC to the deposit address: + +```bash +$BASE_HELPER send-usdc --to $DEPOSIT_ADDRESS --amount 500000 +``` + +Verify the send was confirmed (status = "confirmed"). + +3. Poll for balance update every 5 seconds, up to 120 seconds: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + "$GRID_BASE_URL/customers/internal-accounts?customerId=$CUSTOMER_ID¤cy=USDC" +``` + +4. **PASS criteria:** USDC internal account `balance.amount` increases above `INITIAL_USDC_BALANCE`. + +--- + +### Test 3: Transfer Out (USDC internal → external Base wallet) + +**Goal:** Withdraw USDC from internal account to an external Base Sepolia wallet. + +**Steps:** + +1. Create an external account for our wallet: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"customerId\": \"$CUSTOMER_ID\", + \"currency\": \"USDC\", + \"cryptoNetwork\": \"BASE_TESTNET\", + \"accountInfo\": { + \"accountType\": \"BASE_WALLET\", + \"address\": \"$WALLET_ADDRESS\" + } + }" \ + "$GRID_BASE_URL/customers/external-accounts" +``` + +Save the `id` as `USDC_EXTERNAL_ID`. + +2. Record initial on-chain USDC balance: + +```bash +$BASE_HELPER usdc-balance +``` + +Save `raw` as `INITIAL_ONCHAIN_USDC`. + +3. Transfer out 0.10 USDC: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"source\": {\"accountId\": \"$USDC_INTERNAL_ID\"}, + \"destination\": {\"accountId\": \"$USDC_EXTERNAL_ID\"}, + \"amount\": 100000 + }" \ + "$GRID_BASE_URL/transfer-out" +``` + +4. Poll on-chain USDC balance every 5 seconds, up to 120 seconds: + +```bash +$BASE_HELPER usdc-balance +``` + +5. **PASS criteria:** On-chain USDC balance (`raw`) increases by approximately 100000 (0.10 USDC) from `INITIAL_ONCHAIN_USDC`. + +--- + +### Test 4: USDC → USD Quote (Real-Time Funded → internal USD account) + +**Goal:** Use real-time funding to convert USDC to USD via a JIT quote, depositing into the customer's internal USD account. + +**Steps:** + +1. Create a real-time funded quote: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"source\": { + \"sourceType\": \"REALTIME_FUNDING\", + \"customerId\": \"$CUSTOMER_ID\", + \"currency\": \"USDC\", + \"cryptoNetwork\": \"BASE_TESTNET\" + }, + \"destination\": { + \"destinationType\": \"ACCOUNT\", + \"accountId\": \"$USD_INTERNAL_ID\" + }, + \"lockedCurrencySide\": \"RECEIVING\", + \"lockedCurrencyAmount\": 10, + \"purposeOfPayment\": \"GOODS_OR_SERVICES\" + }" \ + "$GRID_BASE_URL/quotes" +``` + +2. Extract from the response: + - `QUOTE_ID`: the `id` field + - `TRANSACTION_ID`: the `transactionId` field + - `PAYMENT_ADDRESS`: from `paymentInstructions`, find the `BASE_WALLET` entry and extract `accountOrWalletInfo.address` + - `TOTAL_SENDING_AMOUNT`: the `totalSendingAmount` field (this is the micro-USDC amount to send) + +3. Record initial USD internal account balance: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + "$GRID_BASE_URL/customers/internal-accounts?customerId=$CUSTOMER_ID¤cy=USD" +``` + +Save `balance.amount` as `INITIAL_USD_BALANCE`. + +4. Send USDC to the payment instructions address: + +```bash +$BASE_HELPER send-usdc --to $PAYMENT_ADDRESS --amount $TOTAL_SENDING_AMOUNT +``` + +5. Poll USD internal account balance every 5 seconds, up to 120 seconds: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + "$GRID_BASE_URL/customers/internal-accounts?customerId=$CUSTOMER_ID¤cy=USD" +``` + +6. **PASS criteria:** USD internal account `balance.amount` increases above `INITIAL_USD_BALANCE`. + +--- + +### Test 5: USDC → USD Quote (Real-Time Funded → external USD bank account) + +**Goal:** Convert USDC to USD and send to an external bank account via ACH. + +**Steps:** + +1. Create an external USD bank account: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"customerId\": \"$CUSTOMER_ID\", + \"currency\": \"USD\", + \"accountInfo\": { + \"accountType\": \"USD_ACCOUNT\", + \"paymentRails\": [\"ACH\"], + \"routingNumber\": \"021000021\", + \"accountNumber\": \"123456789012\", + \"accountCategory\": \"CHECKING\", + \"beneficiary\": { + \"beneficiaryType\": \"INDIVIDUAL\", + \"fullName\": \"Base USDC Test User\", + \"birthDate\": \"1990-01-15\", + \"nationality\": \"US\" + } + } + }" \ + "$GRID_BASE_URL/customers/external-accounts" +``` + +Save the `id` as `USD_EXTERNAL_ID`. + +2. Create a real-time funded quote: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"source\": { + \"sourceType\": \"REALTIME_FUNDING\", + \"customerId\": \"$CUSTOMER_ID\", + \"currency\": \"USDC\", + \"cryptoNetwork\": \"BASE_TESTNET\" + }, + \"destination\": { + \"destinationType\": \"ACCOUNT\", + \"accountId\": \"$USD_EXTERNAL_ID\", + \"paymentRail\": \"ACH\" + }, + \"lockedCurrencySide\": \"RECEIVING\", + \"lockedCurrencyAmount\": 10, + \"purposeOfPayment\": \"GOODS_OR_SERVICES\" + }" \ + "$GRID_BASE_URL/quotes" +``` + +3. Extract `QUOTE_ID`, `TRANSACTION_ID`, `PAYMENT_ADDRESS`, and `TOTAL_SENDING_AMOUNT` as in Test 4. + +4. Send USDC to the payment instructions address: + +```bash +$BASE_HELPER send-usdc --to $PAYMENT_ADDRESS --amount $TOTAL_SENDING_AMOUNT +``` + +5. Poll transaction status every 5 seconds, up to 120 seconds: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + "$GRID_BASE_URL/transactions/$TRANSACTION_ID" +``` + +6. **PASS criteria:** Transaction status reaches `PROCESSING` or `COMPLETED`. + +--- + +### Test 6: USDC → MXN Quote (Real-Time Funded → external MXN CLABE account) + +**Goal:** Convert USDC to MXN and send to a Mexican bank account via SPEI. + +**Steps:** + +1. Create an external MXN account: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"customerId\": \"$CUSTOMER_ID\", + \"currency\": \"MXN\", + \"accountInfo\": { + \"accountType\": \"MXN_ACCOUNT\", + \"paymentRails\": [\"SPEI\"], + \"clabeNumber\": \"032180000118359719\", + \"beneficiary\": { + \"beneficiaryType\": \"INDIVIDUAL\", + \"fullName\": \"Base USDC Test User\", + \"birthDate\": \"1990-01-15\", + \"nationality\": \"MX\" + } + } + }" \ + "$GRID_BASE_URL/customers/external-accounts" +``` + +Save the `id` as `MXN_EXTERNAL_ID`. + +2. Create a real-time funded quote: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"source\": { + \"sourceType\": \"REALTIME_FUNDING\", + \"customerId\": \"$CUSTOMER_ID\", + \"currency\": \"USDC\", + \"cryptoNetwork\": \"BASE_TESTNET\" + }, + \"destination\": { + \"destinationType\": \"ACCOUNT\", + \"accountId\": \"$MXN_EXTERNAL_ID\", + \"paymentRail\": \"SPEI\" + }, + \"lockedCurrencySide\": \"RECEIVING\", + \"lockedCurrencyAmount\": 200, + \"purposeOfPayment\": \"GOODS_OR_SERVICES\" + }" \ + "$GRID_BASE_URL/quotes" +``` + +Note: `lockedCurrencyAmount: 200` = 2.00 MXN (smallest unit = centavos), roughly ~$0.10 USD. + +3. Extract `QUOTE_ID`, `TRANSACTION_ID`, `PAYMENT_ADDRESS`, and `TOTAL_SENDING_AMOUNT`. + +4. Send USDC: + +```bash +$BASE_HELPER send-usdc --to $PAYMENT_ADDRESS --amount $TOTAL_SENDING_AMOUNT +``` + +5. Poll transaction status every 5 seconds, up to 120 seconds: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + "$GRID_BASE_URL/transactions/$TRANSACTION_ID" +``` + +6. **PASS criteria:** Transaction status reaches `PROCESSING` or `COMPLETED`. + +--- + +### Test 7: USD → USDC Quote (Account-Funded → external Base wallet) + +**Goal:** Convert USD from internal account to USDC delivered to our external Base Sepolia wallet. + +**Steps:** + +1. Fund the USD internal account via sandbox endpoint: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d '{"amount": 100}' \ + "$GRID_BASE_URL/sandbox/internal-accounts/$USD_INTERNAL_ID/fund" +``` + +Verify the balance increased (response contains updated account). + +2. Record initial on-chain USDC balance: + +```bash +$BASE_HELPER usdc-balance +``` + +Save `raw` as `INITIAL_ONCHAIN_USDC_T7`. + +3. Ensure USDC external account exists (reuse `$USDC_EXTERNAL_ID` from Test 3). If Test 3 was skipped, create it now. + +4. Create an account-funded quote: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d "{ + \"source\": { + \"sourceType\": \"ACCOUNT\", + \"accountId\": \"$USD_INTERNAL_ID\" + }, + \"destination\": { + \"destinationType\": \"ACCOUNT\", + \"accountId\": \"$USDC_EXTERNAL_ID\" + }, + \"lockedCurrencySide\": \"SENDING\", + \"lockedCurrencyAmount\": 50, + \"purposeOfPayment\": \"GOODS_OR_SERVICES\" + }" \ + "$GRID_BASE_URL/quotes" +``` + +Save `QUOTE_ID` and `TRANSACTION_ID` from the response. + +5. Execute the quote: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST \ + "$GRID_BASE_URL/quotes/$QUOTE_ID/execute" +``` + +6. Poll on-chain USDC balance every 5 seconds, up to 120 seconds: + +```bash +$BASE_HELPER usdc-balance +``` + +7. **PASS criteria:** On-chain USDC balance (`raw`) increases above `INITIAL_ONCHAIN_USDC_T7`. + +--- + +## Results Summary + +After all tests complete, print a final results table: + +``` +| # | Test Case | Status | Details | +|---|----------------------------------------|--------|---------| +| 1 | Customer + USDC Account Creation | PASS/FAIL | ... | +| 2 | Fund Internal Account (Base USDC) | PASS/FAIL | ... | +| 3 | Transfer Out (USDC → Base wallet) | PASS/FAIL | ... | +| 4 | USDC → USD (RT funded → internal) | PASS/FAIL | ... | +| 5 | USDC → USD (RT funded → external bank) | PASS/FAIL | ... | +| 6 | USDC → MXN (RT funded → CLABE) | PASS/FAIL | ... | +| 7 | USD → USDC (Account funded → wallet) | PASS/FAIL | ... | +``` + +Include in Details: relevant amounts, transaction IDs, error messages, or timing info. + +## Error Handling + +- If a test fails, record the failure and continue to the next test (do not abort the entire suite). +- If a polling loop times out, record FAIL with "timeout after 120s" and the last observed state. +- If the `send-usdc` command fails, check ETH balance (may need testnet ETH for gas) and USDC balance (may be insufficient). +- If a quote returns an error about `totalSendingAmount` being too small or too large, adjust the `lockedCurrencyAmount` and retry once. +- Common API errors: + - `USER_NOT_FOUND`: sandbox VASP may not have the required user — note in results + - `INSUFFICIENT_BALANCE`: the internal account doesn't have enough funds — note in results + - `QUOTE_EXPIRED`: quote expired before funding — retry with faster execution + +## Amounts Reference + +All tests use small amounts to conserve testnet funds: +- Test 2: 0.50 USDC deposit (500000 micro-USDC) +- Test 3: 0.10 USDC transfer-out (100000 micro-USDC) +- Tests 4-5: ~$0.10 USD locked on receiving side (10 cents) +- Test 6: ~2.00 MXN locked on receiving side (~$0.10 USD) +- Test 7: $0.50 USD → USDC (50 cents) +- **Total USDC needed: ~1.0 USDC + gas (~0.001 ETH on Base Sepolia)** diff --git a/.claude/skills/grid-base-usdc-sandbox/base_helper.py b/.claude/skills/grid-base-usdc-sandbox/base_helper.py new file mode 100644 index 00000000..f856a57d --- /dev/null +++ b/.claude/skills/grid-base-usdc-sandbox/base_helper.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Base testnet CLI for Grid USDC sandbox testing. + +Subcommands: + wallet-address Print public address of loaded testnet key + eth-balance [--address] Print ETH balance on Base Sepolia + usdc-balance [--address] Print USDC balance (raw + human-readable) + send-usdc --to --amount Send USDC on Base Sepolia (amount in micro-USDC, 6 decimals) +""" + +import argparse +import json +import os +import sys +import time +from pathlib import Path + +try: + from web3 import Web3 + from eth_account import Account +except ImportError as e: + print(json.dumps({"error": "Missing dependencies. Install with: pip3 install web3", "detail": str(e)})) + sys.exit(1) + +BASE_SEPOLIA_RPC = "https://sepolia.base.org" +BASE_SEPOLIA_CHAIN_ID = 84532 +USDC_CONTRACT = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" +USDC_DECIMALS = 6 + +ERC20_ABI = [ + { + "constant": True, + "inputs": [{"name": "_owner", "type": "address"}], + "name": "balanceOf", + "outputs": [{"name": "balance", "type": "uint256"}], + "type": "function", + }, + { + "constant": False, + "inputs": [ + {"name": "_to", "type": "address"}, + {"name": "_value", "type": "uint256"}, + ], + "name": "transfer", + "outputs": [{"name": "", "type": "bool"}], + "type": "function", + }, +] + + +def load_account(creds_path=None): + creds_path = creds_path or os.path.expanduser("~/.grid-credentials") + with open(creds_path) as f: + creds = json.load(f) + private_key = creds.get("baseTestnetPrivateKey") + if not private_key: + print(json.dumps({"error": "baseTestnetPrivateKey not found in ~/.grid-credentials"})) + sys.exit(1) + if not private_key.startswith("0x"): + private_key = "0x" + private_key + return Account.from_key(private_key) + + +def get_web3(): + w3 = Web3(Web3.HTTPProvider(BASE_SEPOLIA_RPC)) + if not w3.is_connected(): + print(json.dumps({"error": "Failed to connect to Base Sepolia RPC", "rpc": BASE_SEPOLIA_RPC})) + sys.exit(1) + return w3 + + +def get_usdc_contract(w3): + return w3.eth.contract(address=Web3.to_checksum_address(USDC_CONTRACT), abi=ERC20_ABI) + + +def cmd_wallet_address(args): + acct = load_account() + print(json.dumps({"address": acct.address})) + + +def cmd_eth_balance(args): + w3 = get_web3() + if args.address: + address = Web3.to_checksum_address(args.address) + else: + acct = load_account() + address = acct.address + balance_wei = w3.eth.get_balance(address) + print(json.dumps({ + "address": address, + "wei": balance_wei, + "eth": float(Web3.from_wei(balance_wei, "ether")), + })) + + +def cmd_usdc_balance(args): + w3 = get_web3() + usdc = get_usdc_contract(w3) + if args.address: + address = Web3.to_checksum_address(args.address) + else: + acct = load_account() + address = acct.address + raw = usdc.functions.balanceOf(address).call() + print(json.dumps({ + "address": address, + "contract": USDC_CONTRACT, + "raw": raw, + "amount": raw / (10 ** USDC_DECIMALS), + "ui_amount": f"{raw / (10 ** USDC_DECIMALS):.6f}", + })) + + +def cmd_send_usdc(args): + acct = load_account() + w3 = get_web3() + usdc = get_usdc_contract(w3) + recipient = Web3.to_checksum_address(args.to) + amount = int(args.amount) + + nonce = w3.eth.get_transaction_count(acct.address) + + tx = usdc.functions.transfer(recipient, amount).build_transaction({ + "chainId": BASE_SEPOLIA_CHAIN_ID, + "from": acct.address, + "nonce": nonce, + "gas": 100_000, + "maxFeePerGas": w3.eth.gas_price * 2, + "maxPriorityFeePerGas": w3.to_wei(0.001, "gwei"), + }) + + signed = acct.sign_transaction(tx) + tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) + tx_hash_hex = tx_hash.hex() + + print(json.dumps({"status": "sent", "tx_hash": tx_hash_hex, "message": "Waiting for confirmation..."})) + + for _ in range(60): + time.sleep(2) + try: + receipt = w3.eth.get_transaction_receipt(tx_hash) + if receipt is not None: + if receipt["status"] == 1: + print(json.dumps({"status": "confirmed", "tx_hash": tx_hash_hex, "block": receipt["blockNumber"]})) + return + else: + print(json.dumps({"status": "failed", "tx_hash": tx_hash_hex, "receipt_status": receipt["status"]})) + sys.exit(1) + except Exception: + pass + + print(json.dumps({"status": "timeout", "tx_hash": tx_hash_hex, "message": "Transaction sent but confirmation timed out."})) + sys.exit(1) + + +def main(): + parser = argparse.ArgumentParser(description="Base Sepolia helper for Grid USDC sandbox testing") + sub = parser.add_subparsers(dest="command") + sub.required = True + + sub.add_parser("wallet-address", help="Print public address of loaded testnet key") + + eth_bal = sub.add_parser("eth-balance", help="Print ETH balance on Base Sepolia") + eth_bal.add_argument("--address", help="Address to check (default: own wallet)") + + usdc_bal = sub.add_parser("usdc-balance", help="Print USDC balance on Base Sepolia") + usdc_bal.add_argument("--address", help="Address to check (default: own wallet)") + + send = sub.add_parser("send-usdc", help="Send USDC on Base Sepolia") + send.add_argument("--to", required=True, help="Recipient address (0x...)") + send.add_argument("--amount", required=True, help="Amount in micro-USDC (6 decimals)") + + args = parser.parse_args() + + dispatch = { + "wallet-address": cmd_wallet_address, + "eth-balance": cmd_eth_balance, + "usdc-balance": cmd_usdc_balance, + "send-usdc": cmd_send_usdc, + } + dispatch[args.command](args) + + +if __name__ == "__main__": + main()