From 5a1766ffd0bb2ddf6fce7d67f51657044a50860f Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 23 Mar 2026 21:18:16 -0700 Subject: [PATCH 1/6] 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/6] 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() From 567c13bca565b10f1e4fe3194e4bc002f64801e3 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Wed, 8 Apr 2026 09:56:37 -0700 Subject: [PATCH 3/6] Support non-sandbox for usdc base --- .../skills/grid-base-usdc-sandbox/SKILL.md | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/.claude/skills/grid-base-usdc-sandbox/SKILL.md b/.claude/skills/grid-base-usdc-sandbox/SKILL.md index 2629d3c0..862b0b71 100644 --- a/.claude/skills/grid-base-usdc-sandbox/SKILL.md +++ b/.claude/skills/grid-base-usdc-sandbox/SKILL.md @@ -29,6 +29,22 @@ 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) ``` +### 1b. Detect sandbox vs non-sandbox platform + +Try a sandbox endpoint to determine platform type. Save the result for use in tests that have sandbox-specific behavior. + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d '{"amount": 0}' \ + "$GRID_BASE_URL/sandbox/internal-accounts/dummy/fund" +``` + +- If the response contains `"not a sandbox platform"`, set `IS_SANDBOX=false` +- Otherwise (any other error like "not found", or success), set `IS_SANDBOX=true` + +Report the detected mode to the user (e.g., "Detected non-sandbox platform" or "Detected sandbox platform"). + ### 2. Verify Base testnet key exists ```bash @@ -193,7 +209,7 @@ $BASE_HELPER usdc-balance Save `raw` as `INITIAL_ONCHAIN_USDC`. -3. Transfer out 0.10 USDC: +3. Transfer out 0.20 USDC: ```bash curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ @@ -201,18 +217,20 @@ curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ -d "{ \"source\": {\"accountId\": \"$USDC_INTERNAL_ID\"}, \"destination\": {\"accountId\": \"$USDC_EXTERNAL_ID\"}, - \"amount\": 100000 + \"amount\": 200000 }" \ "$GRID_BASE_URL/transfer-out" ``` +Note: the amount must exceed the custody provider fee (~100100 micro-USDC), so 200000 is the minimum safe amount. + 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`. +5. **PASS criteria:** On-chain USDC balance (`raw`) increases above `INITIAL_ONCHAIN_USDC` (net amount will be ~99900 after fees). --- @@ -398,8 +416,7 @@ curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ }, \"destination\": { \"destinationType\": \"ACCOUNT\", - \"accountId\": \"$MXN_EXTERNAL_ID\", - \"paymentRail\": \"SPEI\" + \"accountId\": \"$MXN_EXTERNAL_ID\" }, \"lockedCurrencySide\": \"RECEIVING\", \"lockedCurrencyAmount\": 200, @@ -408,7 +425,7 @@ curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ "$GRID_BASE_URL/quotes" ``` -Note: `lockedCurrencyAmount: 200` = 2.00 MXN (smallest unit = centavos), roughly ~$0.10 USD. +Note: `lockedCurrencyAmount: 200` = 2.00 MXN (smallest unit = centavos), roughly ~$0.10 USD. Do not include `paymentRail` — the API infers it from the external account. 3. Extract `QUOTE_ID`, `TRANSACTION_ID`, `PAYMENT_ADDRESS`, and `TOTAL_SENDING_AMOUNT`. @@ -435,7 +452,9 @@ curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ **Steps:** -1. Fund the USD internal account via sandbox endpoint: +1. Fund the USD internal account: + +**If `IS_SANDBOX=true`:** Use the sandbox fund endpoint: ```bash curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ @@ -446,6 +465,8 @@ curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ Verify the balance increased (response contains updated account). +**If `IS_SANDBOX=false`:** Check the current USD internal account balance. If balance is 0, skip this test with a note: "SKIP: Non-sandbox platform — USD internal account has no balance. Requires a prior successful USDC→USD conversion (Test 4) or manual funding." If balance > 0, proceed. + 2. Record initial on-chain USDC balance: ```bash @@ -530,8 +551,8 @@ Include in Details: relevant amounts, transaction IDs, error messages, or timing 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) +- Test 3: 0.20 USDC transfer-out (200000 micro-USDC) — must exceed ~100100 custody fee - 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)** +- Test 7: $0.50 USD → USDC (50 cents) — requires sandbox or prior USD balance +- **Total USDC needed: ~1.2 USDC + gas (~0.001 ETH on Base Sepolia)** From 3af0133cda3a9c850c6ba6d040110f2c288ac5fd Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Wed, 8 Apr 2026 09:59:31 -0700 Subject: [PATCH 4/6] renames --- .../{grid-base-usdc-sandbox => grid-base-usdc-test}/SKILL.md | 0 .../base_helper.py | 0 .../{grid-usdc-sandbox => grid-solana-usdc-sandbox}/SKILL.md | 0 .../solana_helper.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename .claude/skills/{grid-base-usdc-sandbox => grid-base-usdc-test}/SKILL.md (100%) rename .claude/skills/{grid-base-usdc-sandbox => grid-base-usdc-test}/base_helper.py (100%) rename .claude/skills/{grid-usdc-sandbox => grid-solana-usdc-sandbox}/SKILL.md (100%) rename .claude/skills/{grid-usdc-sandbox => grid-solana-usdc-sandbox}/solana_helper.py (100%) diff --git a/.claude/skills/grid-base-usdc-sandbox/SKILL.md b/.claude/skills/grid-base-usdc-test/SKILL.md similarity index 100% rename from .claude/skills/grid-base-usdc-sandbox/SKILL.md rename to .claude/skills/grid-base-usdc-test/SKILL.md diff --git a/.claude/skills/grid-base-usdc-sandbox/base_helper.py b/.claude/skills/grid-base-usdc-test/base_helper.py similarity index 100% rename from .claude/skills/grid-base-usdc-sandbox/base_helper.py rename to .claude/skills/grid-base-usdc-test/base_helper.py diff --git a/.claude/skills/grid-usdc-sandbox/SKILL.md b/.claude/skills/grid-solana-usdc-sandbox/SKILL.md similarity index 100% rename from .claude/skills/grid-usdc-sandbox/SKILL.md rename to .claude/skills/grid-solana-usdc-sandbox/SKILL.md diff --git a/.claude/skills/grid-usdc-sandbox/solana_helper.py b/.claude/skills/grid-solana-usdc-sandbox/solana_helper.py similarity index 100% rename from .claude/skills/grid-usdc-sandbox/solana_helper.py rename to .claude/skills/grid-solana-usdc-sandbox/solana_helper.py From d1ac77fbe1936938c7cf1d9ce29efa2a24f1d3ec Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Sun, 12 Apr 2026 22:34:22 -0700 Subject: [PATCH 5/6] Add a generic grid-test skill that's more comprehensive over all quote paths --- .claude/skills/grid-test/SKILL.md | 274 +++++++ .../grid-test/references/test-catalog.md | 713 ++++++++++++++++++ .../skills/grid-test/scripts/base_helper.py | 209 +++++ .../grid-test/scripts/polygon_helper.py | 209 +++++ .../skills/grid-test/scripts/solana_helper.py | 276 +++++++ 5 files changed, 1681 insertions(+) create mode 100644 .claude/skills/grid-test/SKILL.md create mode 100644 .claude/skills/grid-test/references/test-catalog.md create mode 100644 .claude/skills/grid-test/scripts/base_helper.py create mode 100644 .claude/skills/grid-test/scripts/polygon_helper.py create mode 100644 .claude/skills/grid-test/scripts/solana_helper.py diff --git a/.claude/skills/grid-test/SKILL.md b/.claude/skills/grid-test/SKILL.md new file mode 100644 index 00000000..b30853ef --- /dev/null +++ b/.claude/skills/grid-test/SKILL.md @@ -0,0 +1,274 @@ +--- +name: grid-test +description: > + This skill should be used when the user asks to "test Grid", "run USDC tests", "test deposits", + "test withdrawals", "test Solana flows", "test Base flows", "test Polygon flows", "run e2e tests", + "test sandbox", "test USDC to USD", "test USDC to MXN", "run all Grid tests", "test transfer out", + "test realtime funding", "test quote flows", "test deposits and withdrawals", + "run sandbox tests", "test USDC sandbox", "test Grid API", "run e2e USDC test", + "test USDC on [chain]", or wants to verify Grid's USDC deposit/withdrawal/quote pipeline. + Even if the user mentions just one chain, one test, or one corridor, this skill applies. + This replaces both grid-solana-usdc-sandbox and grid-base-usdc-test. +allowed-tools: + - Bash + - Read + - Grep + - Glob + - WebFetch +--- + +# Grid API Test Suite + +End-to-end tests for USDC flows on Solana, Base, and Polygon: deposits, withdrawals, and cross-currency quotes using real testnet (or mainnet) funds. + +## Step 1: Parse the User's Prompt + +Determine what to run from the user's request: + +**Chains** (default: all available — see step 3 for which have keys): +- `solana`, `base`, `polygon`, or `all` +- Multiple chains: "test solana and base", "run base and polygon tests" + +**Tests** (default: all): +- By number: "run test 4 on solana" +- By name: "test deposits on base", "test USDC to MXN", "test transfer out" +- By category: "test all quote flows", "test RT funded flows", "test account-funded flows" + +**Test name → number mapping:** + +| # | Short Name | Keywords | +|---|-----------|----------| +| 1 | account-creation | customer, account, setup | +| 2 | deposit | deposit, fund, send USDC to Grid | +| 3 | transfer-out | withdraw, transfer out, send to wallet | +| 4 | usdc-to-usd-internal-rt | USDC→USD internal, RT funded internal | +| 5 | usdc-to-usd-bank-rt | USDC→USD bank, RT funded ACH, external bank | +| 6 | usdc-to-mxn-rt | USDC→MXN RT, SPEI, CLABE, Mexico RT | +| 7 | usd-to-usdc | USD→USDC, buy USDC, account funded wallet | +| 8 | usdc-to-usd-internal-acct | USDC→USD account funded, convert USDC balance | +| 9 | usdc-to-mxn-acct | USDC→MXN account funded, SPEI account funded | +| 10 | usdc-to-uma-rt | USDC→UMA RT, UMA realtime, send to UMA | +| 11 | usd-to-uma-acct | USD→UMA account funded, UMA payout | + +**Category shortcuts:** +- "quote flows" or "quotes" → tests 4-11 +- "RT funded" or "realtime" → tests 4-6, 10 +- "account funded" → tests 7-9, 11 +- "transfers" → tests 2-3 +- "UMA" → tests 10-11 + +## Step 2: Load 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) +``` + +## Step 3: Detect Environment + +### Sandbox vs non-sandbox + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST -H "Content-Type: application/json" \ + -d '{"amount": 1}' \ + "$GRID_BASE_URL/sandbox/internal-accounts/dummy/fund" +``` + +- Response contains `"not a sandbox platform"` → `IS_SANDBOX=false` +- Response contains `"not found"` or other non-platform error → `IS_SANDBOX=true` + +Use `amount: 1` (not 0) — a zero amount returns a validation error on both sandbox and non-sandbox, masking the real detection. + +Report the detected mode to the user. + +### Testnet vs mainnet + +Check `GRID_BASE_URL` and credential keys to determine network: +- If `IS_SANDBOX=true` or URL contains dev/staging → testnet networks +- If production URL + `IS_SANDBOX=false` → mainnet networks + +## Step 4: Set Up Each Selected Chain + +For each chain the user wants to test, set the chain-specific variables and verify prerequisites. + +### Chain Configuration Lookup + +**Testnet (sandbox/dev):** + +| Variable | Solana | Base | Polygon | +|---|---|---|---| +| `CRYPTO_NETWORK` | `SOLANA_DEVNET` | `BASE_TESTNET` | `POLYGON_TESTNET` | +| `WALLET_TYPE` | `SOLANA_WALLET` | `BASE_WALLET` | `POLYGON_WALLET` | +| `CRED_KEY` | `solanaDevnetPrivateKey` | `baseTestnetPrivateKey` | `polygonTestnetPrivateKey` | +| `HELPER_SCRIPT` | `scripts/solana_helper.py` | `scripts/base_helper.py` | `scripts/polygon_helper.py` | +| `GAS_CMD` | `sol-balance` | `eth-balance` | `pol-balance` | +| `GAS_TOKEN` | SOL | ETH | POL | +| `GAS_MIN` | 0.1 | 0.001 | 0.1 | +| `TRANSFER_OUT_AMT` | 100000 | 200000 | 200000 | +| `PIP_DEPS` | `solders solana base58` | `web3` | `web3` | + +**Mainnet (non-sandbox production):** + +| Variable | Solana | Base | Polygon | +|---|---|---|---| +| `CRYPTO_NETWORK` | `SOLANA_MAINNET` | `BASE_MAINNET` | `POLYGON_MAINNET` | +| `WALLET_TYPE` | `SOLANA_WALLET` | `BASE_WALLET` | `POLYGON_WALLET` | +| `CRED_KEY` | `solanaMainnetPrivateKey` | `baseMainnetPrivateKey` | `polygonMainnetPrivateKey` | +| Other vars | Same as testnet | Same as testnet | Same as testnet | + +### Per-chain prerequisites + +For each selected chain, run these checks. Skip a chain (with a warning) if its private key is missing. + +1. **Verify private key exists:** + ```bash + jq -r ".$CRED_KEY // empty" ~/.grid-credentials + ``` + If empty, warn the user and skip this chain. + +2. **Install dependencies:** + ```bash + pip3 install $PIP_DEPS 2>&1 | tail -5 + ``` + +3. **Define helper function** (pass `--mainnet` if running on mainnet): + ```bash + # Testnet: + chain_helper() { python3 /absolute/path/to/.claude/skills/grid-test/$HELPER_SCRIPT "$@"; } + # Mainnet: + chain_helper() { python3 /absolute/path/to/.claude/skills/grid-test/$HELPER_SCRIPT --mainnet "$@"; } + ``` + + Use a shell function (not a variable) so that arguments are word-split correctly. Then call as `chain_helper send-usdc --to ...`. All helper scripts accept `--mainnet` to switch RPC endpoints, chain IDs, USDC contract addresses, and credential keys automatically. + +4. **Check gas balance:** + ```bash + $CHAIN_HELPER $GAS_CMD + ``` + If below `GAS_MIN`, warn the user with instructions for obtaining testnet gas: + - Solana: `$CHAIN_HELPER airdrop-sol --amount 1000000000` + - Base: https://www.alchemy.com/faucets/base-sepolia + - Polygon: https://faucet.polygon.technology/ + +5. **Check USDC balance:** + ```bash + $CHAIN_HELPER usdc-balance + ``` + If `amount` < 1.0 USDC, warn the user. Testnet USDC sources: + - Solana: Solana devnet USDC faucet + - Base: https://faucet.circle.com/ (select Base Sepolia) + - Polygon: https://faucet.circle.com/ (select Polygon Amoy) + +6. **Get wallet address:** + ```bash + $CHAIN_HELPER wallet-address + ``` + Save as `WALLET_ADDRESS` for this chain. + +## Step 5: Run Tests + +Read `references/test-catalog.md` for detailed test steps. Each test is parameterized by chain variables set in Step 4. Run tests sequentially within each chain (later tests depend on state from earlier ones). + +**Dependency note:** If the user requests a specific test (e.g., test 4), also run its dependencies: +- Tests 2-11 depend on Test 1 (customer + account creation) +- Tests 3, 8, 9 depend on Test 2 (needs USDC in internal account) +- Tests 7, 11 need USD balance — either sandbox fund endpoint or a prior USDC→USD conversion (Test 4 or 8) +- Tests 10-11 need a valid UMA receiver address (defaults to `$test@sandbox.grid.uma.money`, overridable via `UMA_RECEIVER` env var) + +If running a subset, create the customer (Test 1) silently as setup, then run only the requested tests. + +**Multi-chain execution:** Run each chain fully before moving to the next. Set `CHAIN_PREFIX` per chain for unique customer IDs: +- Solana: `CHAIN_PREFIX="solana-test"` +- Base: `CHAIN_PREFIX="base-test"` +- Polygon: `CHAIN_PREFIX="polygon-test"` + +## Step 6: Results Summary + +After all tests complete, print a results table per chain: + +``` +## Solana Results +| # | Test Case | Status | Details | +|---|----------------------------------------|--------|---------| +| 1 | Customer + USDC Account Creation | PASS | ... | +| 2 | Fund Internal Account (deposit) | PASS | ... | +| 3 | Transfer Out (→ wallet) | PASS | ... | +| 4 | USDC → USD (RT funded → internal) | PASS | ... | +| 5 | USDC → USD (RT funded → external bank) | PASS | ... | +| 6 | USDC → MXN (RT funded → CLABE) | PASS | ... | +| 7 | USD → USDC (Account funded → wallet) | PASS | ... | +| 8 | USDC → USD (Account funded → internal) | PASS | ... | +| 9 | USDC → MXN (Account funded → CLABE) | PASS | ... | +| 10 | USDC → USD (RT funded → UMA) | PASS | ... | +| 11 | USD → USD (Account funded → UMA) | PASS | ... | + +## Base Results +... + +## Polygon Results +... +``` + +Include in Details: amounts, transaction IDs, error messages, or timing. + +If multiple chains were tested, add an aggregate summary: + +``` +## Summary +| Chain | Passed | Failed | Skipped | +|---------|--------|--------|---------| +| Solana | 7/7 | 0 | 0 | +| Base | 6/7 | 1 | 0 | +| Polygon | 0/7 | 0 | 7 | +``` + +## Error Handling + +- If a test fails, record the failure and continue to the next test. +- If a polling loop times out, record FAIL with "timeout after 120s" and the last observed state. +- If `send-usdc` fails, check gas balance (may need airdrop/faucet) and USDC balance. +- If a quote returns an error about `totalSendingAmount` being too small or too large, adjust `lockedCurrencyAmount` and retry once. +- Common API errors: + - `USER_NOT_FOUND`: sandbox VASP may not have the required user + - `INSUFFICIENT_BALANCE`: internal account doesn't have enough funds + - `QUOTE_EXPIRED`: quote expired before funding — retry with faster execution + +## Amounts Reference + +All tests use small amounts to conserve testnet funds: + +| Test | Amount | Notes | +|------|--------|-------| +| 2 (deposit) | 0.50 USDC (500000) | | +| 3 (transfer-out) | Solana: 0.10 USDC (100000), Base/Polygon: 0.20 USDC (200000) | Base/Polygon must exceed ~100100 custody fee | +| 4-5 (USDC→USD RT) | $0.10 locked receiving (10 cents) | | +| 6 (USDC→MXN RT) | 11.00 MXN locked receiving (1100 centavos, ~$0.55) | Some envs enforce 1100 minimum | +| 7 (USD→USDC) | $0.50 sending (50 cents) | Requires sandbox or prior USD balance | +| 8 (USDC→USD acct) | 0.05 USDC sending (50000) | Requires USDC from test 2 | +| 9 (USDC→MXN acct) | 0.05 USDC sending (50000) | Requires USDC from test 2 | +| 10 (USDC→UMA RT) | $0.10 locked receiving (10 cents) | Requires valid UMA receiver | +| 11 (USD→UMA acct) | $0.10 sending (10 cents) | Requires USD balance + valid UMA receiver | + +**Total per chain: ~1.3-1.5 USDC + gas fees** + +## Credential Schema + +`~/.grid-credentials` JSON file: + +```json +{ + "apiTokenId": "...", + "apiClientSecret": "...", + "baseUrl": "https://api.lightspark.com/grid/2025-10-13", + "solanaDevnetPrivateKey": "base58-encoded-64-byte-keypair", + "solanaMainnetPrivateKey": "base58-encoded-64-byte-keypair", + "baseTestnetPrivateKey": "hex-private-key-with-or-without-0x", + "baseMainnetPrivateKey": "hex-private-key-with-or-without-0x", + "polygonTestnetPrivateKey": "hex-private-key-with-or-without-0x", + "polygonMainnetPrivateKey": "hex-private-key-with-or-without-0x" +} +``` + +Only the keys for chains you want to test are required. The skill auto-skips chains without keys. diff --git a/.claude/skills/grid-test/references/test-catalog.md b/.claude/skills/grid-test/references/test-catalog.md new file mode 100644 index 00000000..3bacf6f4 --- /dev/null +++ b/.claude/skills/grid-test/references/test-catalog.md @@ -0,0 +1,713 @@ +# Test Catalog + +All tests use these chain variables (set per-chain in SKILL.md Step 4): +- `chain_helper` — shell function wrapping the chain's helper script (defined in SKILL.md Step 4) +- `$CRYPTO_NETWORK` — e.g., `SOLANA_DEVNET`, `BASE_TESTNET`, `POLYGON_TESTNET` +- `$WALLET_TYPE` — e.g., `SOLANA_WALLET`, `BASE_WALLET`, `POLYGON_WALLET` +- `$WALLET_ADDRESS` — the test wallet's on-chain address +- `$TRANSFER_OUT_AMT` — chain-specific minimum transfer-out amount +- `$IS_SANDBOX` — whether the platform is sandbox +- `$CHAIN_PREFIX` — unique prefix for this chain run (e.g., `solana-test`, `base-test`, `polygon-test`) + +API credentials are in environment variables: `$GRID_API_TOKEN_ID`, `$GRID_API_CLIENT_SECRET`, `$GRID_BASE_URL`. + +--- + +## Test 1: Customer + USDC Account Creation + +**Goal:** Create a customer and verify USDC internal account with chain-specific wallet funding instructions. + +**Steps:** + +1. Create a customer: + +```bash +PLATFORM_CUSTOMER_ID="$CHAIN_PREFIX-$(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\": \"Grid 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 `$WALLET_TYPE` and extract `accountOrWalletInfo.address` + +4. **PASS criteria:** + - Customer created successfully + - USDC internal account exists + - `fundingPaymentInstructions` contains a `$WALLET_TYPE` entry with a non-empty `address` + +--- + +## Test 2: Fund Internal Account with Real Testnet USDC + +**Goal:** Send real USDC on the chain's testnet 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 +chain_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 wallet) + +**Goal:** Withdraw USDC from internal account to an external wallet on this chain. + +**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\": \"$CRYPTO_NETWORK\", + \"accountInfo\": { + \"accountType\": \"$WALLET_TYPE\", + \"address\": \"$WALLET_ADDRESS\" + } + }" \ + "$GRID_BASE_URL/customers/external-accounts" +``` + +Save the `id` as `USDC_EXTERNAL_ID`. + +2. Record initial on-chain USDC balance: + +```bash +chain_helper usdc-balance +``` + +Save `raw` as `INITIAL_ONCHAIN_USDC`. + +3. Transfer out: + +```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\": $TRANSFER_OUT_AMT + }" \ + "$GRID_BASE_URL/transfer-out" +``` + +4. Poll on-chain USDC balance every 5 seconds, up to 120 seconds: + +```bash +chain_helper usdc-balance +``` + +5. **PASS criteria:** On-chain USDC balance (`raw`) increases above `INITIAL_ONCHAIN_USDC`. + +--- + +## Test 4: USDC -> USD Quote (Real-Time Funded -> internal USD account) + +**Goal:** Use real-time funding to convert USDC to USD, 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\": \"$CRYPTO_NETWORK\" + }, + \"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 `$WALLET_TYPE` entry and extract `accountOrWalletInfo.address` + - `TOTAL_SENDING_AMOUNT`: the `totalSendingAmount` field (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 +chain_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\": \"Grid 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\": \"$CRYPTO_NETWORK\" + }, + \"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 +chain_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\": \"Grid 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\": \"$CRYPTO_NETWORK\" + }, + \"destination\": { + \"destinationType\": \"ACCOUNT\", + \"accountId\": \"$MXN_EXTERNAL_ID\" + }, + \"lockedCurrencySide\": \"RECEIVING\", + \"lockedCurrencyAmount\": 1100, + \"purposeOfPayment\": \"GOODS_OR_SERVICES\" + }" \ + "$GRID_BASE_URL/quotes" +``` + +Note: `lockedCurrencyAmount: 1100` = 11.00 MXN (centavos), roughly ~$0.55 USD. Some environments enforce a minimum of 1100 centavos. If the quote returns `AMOUNT_OUT_OF_RANGE`, increase the amount to the specified minimum. Do not include `paymentRail` — the API infers it from the external account. + +3. Extract `QUOTE_ID`, `TRANSACTION_ID`, `PAYMENT_ADDRESS`, and `TOTAL_SENDING_AMOUNT`. + +4. Send USDC: + +```bash +chain_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 wallet) + +**Goal:** Convert USD from internal account to USDC delivered to our external wallet on this chain. + +**Steps:** + +1. Fund the USD internal account: + + **If `IS_SANDBOX=true`:** Use the sandbox fund 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. + + **If `IS_SANDBOX=false`:** Check the current USD internal account balance. If balance is 0, skip this test with: "SKIP: Non-sandbox platform with no USD balance. Requires a prior USDC->USD conversion (Test 4) or manual funding." If balance > 0, proceed. + +2. Record initial on-chain USDC balance: + +```bash +chain_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 using the same pattern as Test 3 step 1. + +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 +chain_helper usdc-balance +``` + +7. **PASS criteria:** On-chain USDC balance (`raw`) increases above `INITIAL_ONCHAIN_USDC_T7`. + +--- + +## Test 8: USDC -> USD Quote (Account-Funded -> internal USD account) + +**Goal:** Convert USDC from internal account to USD in the customer's internal USD account, using existing USDC balance (no real-time funding). + +**Steps:** + +1. Check USDC 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=USDC" +``` + +If `balance.amount` is 0, skip this test with: "SKIP: No USDC in internal account. Requires a prior deposit (Test 2)." + +2. 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_T8`. + +3. 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\": \"$USDC_INTERNAL_ID\" + }, + \"destination\": { + \"destinationType\": \"ACCOUNT\", + \"accountId\": \"$USD_INTERNAL_ID\" + }, + \"lockedCurrencySide\": \"SENDING\", + \"lockedCurrencyAmount\": 50000, + \"purposeOfPayment\": \"GOODS_OR_SERVICES\" + }" \ + "$GRID_BASE_URL/quotes" +``` + +Save `QUOTE_ID` and `TRANSACTION_ID`. + +4. Execute the quote: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST \ + "$GRID_BASE_URL/quotes/$QUOTE_ID/execute" +``` + +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_T8`. + +--- + +## Test 9: USDC -> MXN Quote (Account-Funded -> external MXN CLABE account) + +**Goal:** Convert USDC from internal account to MXN and send to a Mexican bank account, using existing USDC balance. + +**Steps:** + +1. Check USDC 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=USDC" +``` + +If `balance.amount` is 0, skip this test with: "SKIP: No USDC in internal account. Requires a prior deposit (Test 2)." + +2. Ensure MXN external account exists (reuse `$MXN_EXTERNAL_ID` from Test 6). If Test 6 was skipped, create it now: + +```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\": \"Grid Test User\", + \"birthDate\": \"1990-01-15\", + \"nationality\": \"MX\" + } + } + }" \ + "$GRID_BASE_URL/customers/external-accounts" +``` + +Save `id` as `MXN_EXTERNAL_ID`. + +3. 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\": \"$USDC_INTERNAL_ID\" + }, + \"destination\": { + \"destinationType\": \"ACCOUNT\", + \"accountId\": \"$MXN_EXTERNAL_ID\" + }, + \"lockedCurrencySide\": \"SENDING\", + \"lockedCurrencyAmount\": 50000, + \"purposeOfPayment\": \"GOODS_OR_SERVICES\" + }" \ + "$GRID_BASE_URL/quotes" +``` + +Save `QUOTE_ID` and `TRANSACTION_ID`. + +4. Execute the quote: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST \ + "$GRID_BASE_URL/quotes/$QUOTE_ID/execute" +``` + +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 10: USDC -> USD Quote (Real-Time Funded -> UMA address) + +**Goal:** Use real-time USDC funding to send USD to a UMA address. + +**Steps:** + +1. Look up a UMA receiver. Use `$UMA_RECEIVER` if set, otherwise default to `$test@sandbox.grid.uma.money`: + +```bash +UMA_RECEIVER="${UMA_RECEIVER:-\$test@sandbox.grid.uma.money}" +UMA_ENCODED=$(echo "$UMA_RECEIVER" | sed 's/\$/%24/g; s/@/%40/g') +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + "$GRID_BASE_URL/receiver/uma/$UMA_ENCODED" +``` + +If the lookup fails or returns an error, skip this test with: "SKIP: UMA receiver lookup failed. Set `UMA_RECEIVER` in environment or ensure sandbox UMA is available." + +Save the `id` as `LOOKUP_ID`. Note the supported receiving currencies from the response. + +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 "{ + \"lookupId\": \"$LOOKUP_ID\", + \"source\": { + \"sourceType\": \"REALTIME_FUNDING\", + \"customerId\": \"$CUSTOMER_ID\", + \"currency\": \"USDC\", + \"cryptoNetwork\": \"$CRYPTO_NETWORK\" + }, + \"destination\": { + \"destinationType\": \"UMA_ADDRESS\", + \"umaAddress\": \"$UMA_RECEIVER\", + \"currency\": \"USD\" + }, + \"lockedCurrencySide\": \"RECEIVING\", + \"lockedCurrencyAmount\": 10, + \"purposeOfPayment\": \"GOODS_OR_SERVICES\" + }" \ + "$GRID_BASE_URL/quotes" +``` + +3. Extract from the response: + - `QUOTE_ID`: the `id` field + - `TRANSACTION_ID`: the `transactionId` field + - `PAYMENT_ADDRESS`: from `paymentInstructions`, find the `$WALLET_TYPE` entry and extract `accountOrWalletInfo.address` + - `TOTAL_SENDING_AMOUNT`: the `totalSendingAmount` field + +4. Send USDC to the payment instructions address: + +```bash +chain_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 11: USD -> USD Quote (Account-Funded -> UMA address) + +**Goal:** Send USD from internal account to a UMA address. + +**Steps:** + +1. Check 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" +``` + +If `balance.amount` is 0: +- If `IS_SANDBOX=true`, fund it first: + ```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" + ``` +- If `IS_SANDBOX=false`, skip this test with: "SKIP: Non-sandbox platform with no USD balance." + +2. Look up a UMA receiver (reuse `$LOOKUP_ID` and `$UMA_RECEIVER` from Test 10). If Test 10 was skipped, perform the lookup now using the same pattern as Test 10 step 1. If lookup fails, skip this test. + +3. 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 "{ + \"lookupId\": \"$LOOKUP_ID\", + \"source\": { + \"sourceType\": \"ACCOUNT\", + \"accountId\": \"$USD_INTERNAL_ID\" + }, + \"destination\": { + \"destinationType\": \"UMA_ADDRESS\", + \"umaAddress\": \"$UMA_RECEIVER\", + \"currency\": \"USD\" + }, + \"lockedCurrencySide\": \"SENDING\", + \"lockedCurrencyAmount\": 10, + \"purposeOfPayment\": \"GOODS_OR_SERVICES\" + }" \ + "$GRID_BASE_URL/quotes" +``` + +Save `QUOTE_ID` and `TRANSACTION_ID`. + +4. Execute the quote: + +```bash +curl -s -u "$GRID_API_TOKEN_ID:$GRID_API_CLIENT_SECRET" \ + -X POST \ + "$GRID_BASE_URL/quotes/$QUOTE_ID/execute" +``` + +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`. diff --git a/.claude/skills/grid-test/scripts/base_helper.py b/.claude/skills/grid-test/scripts/base_helper.py new file mode 100644 index 00000000..2c43d639 --- /dev/null +++ b/.claude/skills/grid-test/scripts/base_helper.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +"""Base chain CLI for Grid USDC testing. + +Supports both Base Sepolia testnet (default) and Base mainnet (--mainnet flag). + +Subcommands: + wallet-address Print public address of loaded key + eth-balance [--address] Print ETH balance + usdc-balance [--address] Print USDC balance (raw + human-readable) + send-usdc --to --amount Send USDC (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) + +NETWORKS = { + "testnet": { + "rpc": "https://sepolia.base.org", + "chain_id": 84532, + "usdc_contract": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "cred_key": "baseTestnetPrivateKey", + "name": "Base Sepolia", + "priority_fee_gwei": 0.001, + }, + "mainnet": { + "rpc": "https://mainnet.base.org", + "chain_id": 8453, + "usdc_contract": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "cred_key": "baseMainnetPrivateKey", + "name": "Base Mainnet", + "priority_fee_gwei": 0.01, + }, +} + +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", + }, +] + +NET = None # set in main() + + +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(NET["cred_key"]) + if not private_key: + print(json.dumps({"error": f"{NET['cred_key']} 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(NET["rpc"])) + if not w3.is_connected(): + print(json.dumps({"error": f"Failed to connect to {NET['name']} RPC", "rpc": NET["rpc"]})) + sys.exit(1) + return w3 + + +def get_usdc_contract(w3): + return w3.eth.contract(address=Web3.to_checksum_address(NET["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": NET["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": NET["chain_id"], + "from": acct.address, + "nonce": nonce, + "gas": 100_000, + "maxFeePerGas": w3.eth.gas_price * 2, + "maxPriorityFeePerGas": w3.to_wei(NET["priority_fee_gwei"], "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(): + global NET + + parser = argparse.ArgumentParser(description="Base chain helper for Grid USDC testing") + parser.add_argument("--mainnet", action="store_true", help="Use Base mainnet instead of Sepolia testnet") + sub = parser.add_subparsers(dest="command") + sub.required = True + + sub.add_parser("wallet-address", help="Print public address of loaded key") + + eth_bal = sub.add_parser("eth-balance", help="Print ETH balance") + eth_bal.add_argument("--address", help="Address to check (default: own wallet)") + + usdc_bal = sub.add_parser("usdc-balance", help="Print USDC balance") + usdc_bal.add_argument("--address", help="Address to check (default: own wallet)") + + send = sub.add_parser("send-usdc", help="Send USDC") + 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() + NET = NETWORKS["mainnet"] if args.mainnet else NETWORKS["testnet"] + + 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() diff --git a/.claude/skills/grid-test/scripts/polygon_helper.py b/.claude/skills/grid-test/scripts/polygon_helper.py new file mode 100644 index 00000000..86bfcb0f --- /dev/null +++ b/.claude/skills/grid-test/scripts/polygon_helper.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +"""Polygon chain CLI for Grid USDC testing. + +Supports both Polygon Amoy testnet (default) and Polygon mainnet (--mainnet flag). + +Subcommands: + wallet-address Print public address of loaded key + pol-balance [--address] Print POL balance + usdc-balance [--address] Print USDC balance (raw + human-readable) + send-usdc --to --amount Send USDC (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) + +NETWORKS = { + "testnet": { + "rpc": "https://rpc-amoy.polygon.technology", + "chain_id": 80002, + "usdc_contract": "0x41E94Eb71898E8B51d136F15b58AAcb90f0b7e70", + "cred_key": "polygonTestnetPrivateKey", + "name": "Polygon Amoy", + "priority_fee_gwei": 30, + }, + "mainnet": { + "rpc": "https://polygon-rpc.com", + "chain_id": 137, + "usdc_contract": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + "cred_key": "polygonMainnetPrivateKey", + "name": "Polygon Mainnet", + "priority_fee_gwei": 30, + }, +} + +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", + }, +] + +NET = None # set in main() + + +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(NET["cred_key"]) or creds.get("polygonPrivateKey") + if not private_key: + print(json.dumps({"error": f"{NET['cred_key']} 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(NET["rpc"])) + if not w3.is_connected(): + print(json.dumps({"error": f"Failed to connect to {NET['name']} RPC", "rpc": NET["rpc"]})) + sys.exit(1) + return w3 + + +def get_usdc_contract(w3): + return w3.eth.contract(address=Web3.to_checksum_address(NET["usdc_contract"]), abi=ERC20_ABI) + + +def cmd_wallet_address(args): + acct = load_account() + print(json.dumps({"address": acct.address})) + + +def cmd_pol_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, + "pol": 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": NET["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": NET["chain_id"], + "from": acct.address, + "nonce": nonce, + "gas": 100_000, + "maxFeePerGas": w3.eth.gas_price * 2, + "maxPriorityFeePerGas": w3.to_wei(NET["priority_fee_gwei"], "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(): + global NET + + parser = argparse.ArgumentParser(description="Polygon chain helper for Grid USDC testing") + parser.add_argument("--mainnet", action="store_true", help="Use Polygon mainnet instead of Amoy testnet") + sub = parser.add_subparsers(dest="command") + sub.required = True + + sub.add_parser("wallet-address", help="Print public address of loaded key") + + pol_bal = sub.add_parser("pol-balance", help="Print POL balance") + pol_bal.add_argument("--address", help="Address to check (default: own wallet)") + + usdc_bal = sub.add_parser("usdc-balance", help="Print USDC balance") + usdc_bal.add_argument("--address", help="Address to check (default: own wallet)") + + send = sub.add_parser("send-usdc", help="Send USDC") + 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() + NET = NETWORKS["mainnet"] if args.mainnet else NETWORKS["testnet"] + + dispatch = { + "wallet-address": cmd_wallet_address, + "pol-balance": cmd_pol_balance, + "usdc-balance": cmd_usdc_balance, + "send-usdc": cmd_send_usdc, + } + dispatch[args.command](args) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/grid-test/scripts/solana_helper.py b/.claude/skills/grid-test/scripts/solana_helper.py new file mode 100644 index 00000000..3b2498c4 --- /dev/null +++ b/.claude/skills/grid-test/scripts/solana_helper.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +"""Solana CLI for Grid USDC testing. + +Supports both devnet (default) and mainnet (--mainnet flag). + +Subcommands: + wallet-address Print public key of loaded keypair + sol-balance [--address] Print SOL balance + usdc-balance [--address] Print USDC balance (raw + human-readable) + send-usdc --to --amount Send USDC (amount in micro-USDC) + airdrop-sol [--amount] Request devnet SOL airdrop (devnet only) +""" + +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) + +NETWORKS = { + "devnet": { + "rpc": "https://api.devnet.solana.com", + "usdc_mint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", + "cred_key": "solanaDevnetPrivateKey", + }, + "mainnet": { + "rpc": "https://api.mainnet-beta.solana.com", + "usdc_mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "cred_key": "solanaMainnetPrivateKey", + }, +} + +TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") +ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") +USDC_DECIMALS = 6 + +NET = None # set in main() + + +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(NET["cred_key"]) + if not secret_key: + print(json.dumps({"error": f"{NET['cred_key']} 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(NET["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 NET["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 NET["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: Mon, 13 Apr 2026 16:18:49 -0700 Subject: [PATCH 6/6] Fix amoy usdc contract --- .claude/skills/grid-test/scripts/polygon_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/grid-test/scripts/polygon_helper.py b/.claude/skills/grid-test/scripts/polygon_helper.py index 86bfcb0f..33ce20a6 100644 --- a/.claude/skills/grid-test/scripts/polygon_helper.py +++ b/.claude/skills/grid-test/scripts/polygon_helper.py @@ -28,7 +28,7 @@ "testnet": { "rpc": "https://rpc-amoy.polygon.technology", "chain_id": 80002, - "usdc_contract": "0x41E94Eb71898E8B51d136F15b58AAcb90f0b7e70", + "usdc_contract": "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582", "cred_key": "polygonTestnetPrivateKey", "name": "Polygon Amoy", "priority_fee_gwei": 30,