From 6f77ed2fcb09419cd254da05413f3a9804816d62 Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Tue, 10 Mar 2026 02:54:50 -0400 Subject: [PATCH] feat: add BlockRun wallet provider for pay-per-request AI access (#1408) Integrates BlockRun (blockrun.ai) as a new AI provider option via x402 micropayment protocol, allowing users to access top AI models with USDC without requiring individual API keys. - Add BlockRun Base (EVM) and Solana wallet providers to model selector - Implement x402 v2 EIP-712 payment signing for Base (mcp/blockrun_base.go) - Implement x402 v2 SPL TransferChecked signing for Solana (mcp/blockrun_sol.go) - Wire blockrun-base and blockrun-sol into trader factory (auto_trader.go) - Register both providers in supported models API (server.go) - Add BlockRun card UI with wallet key input in Step 0/1 of model config modal - Add BlockRun SVG icon and ModelIcons support - Add setup guides for Base and Solana wallet configuration (docs/) - Available flagship models: GPT-5.4, Claude Opus 4.6, Gemini 3.1 Pro, Grok 3, DeepSeek Chat, MiniMax M2.5 --- api/server.go | 3 + docs/getting-started/README.md | 13 + docs/getting-started/blockrun-base-wallet.md | 126 +++++ docs/getting-started/blockrun-sol-wallet.md | 120 +++++ go.mod | 20 + go.sum | 107 +++++ mcp/blockrun_base.go | 456 +++++++++++++++++++ mcp/blockrun_sol.go | 371 +++++++++++++++ trader/auto_trader.go | 10 + web/public/icons/blockrun.svg | 6 + web/src/components/AITradersPage.tsx | 176 +++++-- web/src/components/ModelIcons.tsx | 6 + 12 files changed, 1371 insertions(+), 43 deletions(-) create mode 100644 docs/getting-started/blockrun-base-wallet.md create mode 100644 docs/getting-started/blockrun-sol-wallet.md create mode 100644 mcp/blockrun_base.go create mode 100644 mcp/blockrun_sol.go create mode 100644 web/public/icons/blockrun.svg diff --git a/api/server.go b/api/server.go index c75a5dc8..09e9af85 100644 --- a/api/server.go +++ b/api/server.go @@ -143,6 +143,7 @@ func (s *Server) setupRoutes() { // Authentication related routes (no authentication required) api.POST("/register", s.handleRegister) api.POST("/login", s.handleLogin) + api.POST("/reset-password", s.handleResetPassword) // Routes requiring authentication protected := api.Group("/", s.authMiddleware()) @@ -3264,6 +3265,8 @@ func (s *Server) handleGetSupportedModels(c *gin.Context) { {"id": "grok", "name": "Grok (xAI)", "provider": "grok", "defaultModel": "grok-3-latest"}, {"id": "kimi", "name": "Kimi (Moonshot)", "provider": "kimi", "defaultModel": "moonshot-v1-auto"}, {"id": "minimax", "name": "MiniMax", "provider": "minimax", "defaultModel": "MiniMax-M2.5"}, + {"id": "blockrun-base", "name": "BlockRun (Base Wallet)", "provider": "blockrun-base", "defaultModel": "auto"}, + {"id": "blockrun-sol", "name": "BlockRun (Solana Wallet)", "provider": "blockrun-sol", "defaultModel": "auto"}, } c.JSON(http.StatusOK, supportedModels) diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 1f79a75f..a447028b 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -44,6 +44,19 @@ Use custom AI models or third-party OpenAI-compatible APIs: --- +### ๐Ÿ’ณ BlockRun Wallet (Pay-per-Request, No API Key) + +Access all top AI models by paying with USDC โ€” no API key signup required. + +| Provider | Guide | Payment Network | +|----------|-------|-----------------| +| BlockRun (Base Wallet) | [blockrun-base-wallet.md](blockrun-base-wallet.md) | Base (EVM) ยท USDC | +| BlockRun (Solana Wallet) | [blockrun-sol-wallet.md](blockrun-sol-wallet.md) | Solana ยท USDC | + +**How it works:** Each AI request automatically pays a micro-USDC fee via the [x402 payment protocol](https://blockrun.ai). Your private key signs the payment authorization โ€” no funds leave your wallet until the AI response is delivered. + +--- + ## ๐Ÿ”‘ Prerequisites Before starting, ensure you have: diff --git a/docs/getting-started/blockrun-base-wallet.md b/docs/getting-started/blockrun-base-wallet.md new file mode 100644 index 00000000..7487ccea --- /dev/null +++ b/docs/getting-started/blockrun-base-wallet.md @@ -0,0 +1,126 @@ +# BlockRun Base (EVM) Wallet Setup Guide + +This guide explains how to use a Base network EVM wallet to pay for AI usage through BlockRun โ€” no API key required. + +**Language:** [English](blockrun-base-wallet.md) | [ไธญๆ–‡](blockrun-base-wallet.zh-CN.md) + +## What is BlockRun? + +[BlockRun](https://blockrun.ai) is a decentralized AI inference gateway that lets you access top AI models (Claude, GPT, Gemini, Grok, DeepSeek, etc.) by paying per request with USDC โ€” no monthly subscriptions, no API key signups. + +NOFX integrates BlockRun via the **x402 micropayment protocol**: each AI inference request automatically pays a small USDC fee directly from your wallet. You only pay for what you use. + +## Why Use BlockRun? + +| Feature | Traditional API Key | BlockRun Wallet | +|---------|-------------------|-----------------| +| Setup | Register + billing | Just a wallet address | +| Cost model | Monthly subscription | Pay-per-request | +| Models | One provider | All top models | +| Privacy | Account required | Pseudonymous | +| Control | Rate limits apply | Your wallet, your budget | + +## Prerequisites + +- An EVM wallet with USDC on **Base network** (chain ID 8453) +- The wallet private key (hex format: `0x...`) + +### Getting USDC on Base + +1. Buy USDC on Coinbase and withdraw to Base, **or** +2. Bridge USDC from Ethereum using [bridge.base.org](https://bridge.base.org), **or** +3. Swap on [Aerodrome](https://aerodrome.finance) or [Uniswap](https://app.uniswap.org) on Base + +> **Tip:** A few dollars of USDC is enough to start โ€” each AI call costs fractions of a cent. + +## Step 1: Get Your Wallet Private Key + +> โš ๏ธ **Security Warning:** Never share your private key with anyone. Use a dedicated trading wallet, not your main holdings wallet. + +**Option A โ€” Create a new wallet (recommended):** +1. Open MetaMask โ†’ Create New Account +2. Go to Account Details โ†’ Export Private Key +3. Copy the hex key (starts with `0x`) + +**Option B โ€” Use an existing wallet:** +1. MetaMask โ†’ Account Details โ†’ Export Private Key +2. Enter your MetaMask password to reveal the key + +**Option C โ€” Generate via CLI:** +```bash +# Using cast (foundry) +cast wallet new +# Output: Address: 0x... | Private key: 0x... +``` + +## Step 2: Fund the Wallet with USDC on Base + +Send USDC to your wallet address on Base network: +- **USDC contract:** `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` +- **Network:** Base (chain ID 8453) +- **Recommended starting amount:** $5โ€“$20 USDC + +Check your balance at [basescan.org](https://basescan.org). + +## Step 3: Configure in NOFX + +1. Open NOFX at `http://localhost:3000` +2. Log in and go to **Config** tab +3. Click **+ Add AI Model** +4. In Step 0, scroll to **Via BlockRun Wallet** section +5. Select **BlockRun ยท Base Wallet** +6. In Step 1, configure: + - **Wallet Private Key:** Your hex private key (`0x...`) + - **Select Model:** Choose from Claude Opus, GPT-5.4, Gemini 3 Pro, Grok 3, DeepSeek R1, or leave as **Auto** for best available +7. Click **Save** + +## How Payment Works + +When NOFX sends an AI request: + +1. Request goes to `https://blockrun.ai/api/v1/chat/completions` +2. Server responds with HTTP `402 Payment Required` + payment details +3. NOFX signs a **ERC-3009 TransferWithAuthorization** (EIP-712) with your private key +4. Payment signature is attached and request is retried +5. BlockRun verifies the signature, routes the request to the AI model, and charges USDC + +> **Privacy:** Your private key never leaves your NOFX instance. Only the cryptographic signature is sent. + +## Available Models via BlockRun + +| Model ID | Provider | Use Case | +|----------|----------|----------| +| `gpt-5.4` | OpenAI | Flagship (default) | +| `claude-opus-4.6` | Anthropic | Flagship | +| `gemini-3.1-pro` | Google | Flagship | +| `grok-3` | xAI | Flagship | +| `deepseek-chat` | DeepSeek | Flagship | +| `minimax-m2.5` | MiniMax | Flagship | + +## Security Best Practices + +- โœ… Use a **dedicated wallet** with only trading budget, not your main wallet +- โœ… Keep only a small USDC balance (top up as needed) +- โœ… Your private key is encrypted at rest in NOFX's database +- โœ… Signatures are spend-limited โ€” each signature authorizes only the exact amount for one request +- โŒ Never export or share your private key outside of NOFX + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| `no private key set` | Check your key was saved correctly; re-enter in Config | +| `payment retry failed` | Ensure you have USDC on **Base** (not Ethereum mainnet) | +| `invalid private key` | Key must be hex format with `0x` prefix, 66 chars total | +| Payment deducted but no response | Check BlockRun status at [blockrun.ai](https://blockrun.ai) | +| Slow responses | Try selecting a specific model instead of "Auto" | + +## Monitoring Spend + +Check your USDC balance and transaction history at: +- [Basescan](https://basescan.org) โ€” search your wallet address +- [BlockRun dashboard](https://blockrun.ai) โ€” usage history + +--- + +[โ† Back to Getting Started](README.md) diff --git a/docs/getting-started/blockrun-sol-wallet.md b/docs/getting-started/blockrun-sol-wallet.md new file mode 100644 index 00000000..3e7b12b0 --- /dev/null +++ b/docs/getting-started/blockrun-sol-wallet.md @@ -0,0 +1,120 @@ +# BlockRun Solana Wallet Setup Guide + +This guide explains how to use a Solana wallet to pay for AI usage through BlockRun โ€” no API key required. + +**Language:** [English](blockrun-sol-wallet.md) | [ไธญๆ–‡](blockrun-sol-wallet.zh-CN.md) + +## What is BlockRun? + +[BlockRun](https://blockrun.ai) is a decentralized AI inference gateway that lets you access top AI models (Claude, GPT, Gemini, Grok, DeepSeek, etc.) by paying per request with USDC โ€” no monthly subscriptions, no API key signups. + +NOFX integrates BlockRun via the **x402 micropayment protocol** on Solana: each AI inference request automatically pays a small USDC fee directly from your wallet. + +## Prerequisites + +- A Solana wallet with USDC on **Solana mainnet** +- The wallet private key (base58-encoded, 64 bytes โ€” standard Solana keypair format) + +### Getting USDC on Solana + +1. Buy SOL on any exchange and withdraw to your Solana wallet, then swap to USDC on [Jupiter](https://jup.ag), **or** +2. Buy USDC directly on an exchange and withdraw to Solana, **or** +3. Bridge from other chains using [Wormhole](https://wormhole.com) + +> **Tip:** A few dollars of USDC is plenty to start. + +## Step 1: Export Your Solana Private Key + +> โš ๏ธ **Security Warning:** Use a dedicated wallet for NOFX โ€” not your main holdings wallet. + +**From Phantom Wallet:** +1. Open Phantom โ†’ Settings (gear icon) +2. Security & Privacy โ†’ Export Private Key +3. Enter your password +4. Copy the base58 key (looks like: `5J...` โ€” a long string of ~88 characters) + +**From Solflare:** +1. Settings โ†’ Export Private Key +2. The key is displayed in base58 format + +**From CLI (solana-keygen):** +```bash +# View existing keypair +cat ~/.config/solana/id.json +# This is a JSON array โ€” convert to base58 using: +solana-keygen pubkey ~/.config/solana/id.json +``` + +> **Note:** NOFX accepts the **base58-encoded 64-byte keypair** (as exported by Phantom/Solflare). This is the standard format for Solana private keys. + +## Step 2: Fund the Wallet with USDC on Solana + +Send USDC to your Solana wallet: +- **USDC SPL token mint:** `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` +- **Network:** Solana Mainnet +- **Recommended starting amount:** $5โ€“$20 USDC + +Check your balance at [solscan.io](https://solscan.io) or in your wallet app. + +## Step 3: Configure in NOFX + +1. Open NOFX at `http://localhost:3000` +2. Log in and go to **Config** tab +3. Click **+ Add AI Model** +4. In Step 0, scroll to **Via BlockRun Wallet** section +5. Select **BlockRun ยท Solana Wallet** +6. In Step 1, configure: + - **Wallet Private Key:** Your base58-encoded Solana private key + - **Select Model:** Choose from Claude Opus, GPT-5.4, Gemini 3 Pro, Grok 3, DeepSeek R1, or leave as **Auto** for best available +7. Click **Save** + +## How Payment Works + +When NOFX sends an AI request: + +1. Request goes to `https://sol.blockrun.ai/api/v1/chat/completions` +2. Server responds with HTTP `402 Payment Required` + payment details (nonce, recipient, amount) +3. NOFX signs the payment message `blockrun-payment:{nonce}:{recipient}:{amount}` with your **Ed25519** private key +4. Payment signature is attached and request is retried +5. BlockRun verifies the Ed25519 signature on-chain and routes to the AI model + +> **Privacy:** Your private key never leaves your NOFX instance. Only the cryptographic signature is sent. + +## Available Models via BlockRun + +| Model ID | Provider | Use Case | +|----------|----------|----------| +| `gpt-5.4` | OpenAI | Flagship (default) | +| `claude-opus-4.6` | Anthropic | Flagship | +| `gemini-3.1-pro` | Google | Flagship | +| `grok-3` | xAI | Flagship | +| `deepseek-chat` | DeepSeek | Flagship | +| `minimax-m2.5` | MiniMax | Flagship | + +## Security Best Practices + +- โœ… Use a **dedicated trading wallet** with only your AI budget +- โœ… Keep only a small USDC balance (top up as needed) +- โœ… Your private key is AES-256 encrypted at rest in NOFX's database +- โœ… Ed25519 signatures are one-time โ€” each authorizes only one specific payment +- โŒ Never use your main SOL holdings wallet as the NOFX trading wallet + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| `unexpected key length` | Ensure you exported the full 64-byte keypair (not just the 32-byte seed) | +| `failed to decode base58` | Key must be base58 encoded (standard Phantom/Solflare export format) | +| `payment retry failed` | Ensure you have USDC on **Solana mainnet** (not devnet) | +| No response from server | Check `sol.blockrun.ai` is reachable from your server | +| Slow responses | Try selecting a specific model instead of "Auto" | + +## Monitoring Spend + +Check your USDC balance and transaction history at: +- [Solscan](https://solscan.io) โ€” search your wallet address, filter by USDC token +- [BlockRun dashboard](https://blockrun.ai) โ€” usage history + +--- + +[โ† Back to Getting Started](README.md) diff --git a/go.mod b/go.mod index 35e958df..32d4c93b 100644 --- a/go.mod +++ b/go.mod @@ -21,11 +21,14 @@ require ( ) require ( + filippo.io/edwards25519 v1.0.0-rc.1 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/antihax/optional v1.0.0 // indirect github.com/armon/go-radix v1.0.0 // indirect github.com/bitly/go-simplejson v0.5.1 // indirect github.com/bits-and-blooms/bitset v1.24.0 // indirect + github.com/blendle/zapdriver v1.3.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/bybit-exchange/bybit.go.api v0.0.0-20250727214011-c9347d6804d6 // indirect github.com/bytedance/sonic v1.14.0 // indirect @@ -43,7 +46,11 @@ require ( github.com/elliottech/poseidon_crypto v0.0.11 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gagliardetto/binary v0.8.0 // indirect + github.com/gagliardetto/solana-go v1.14.0 // indirect + github.com/gagliardetto/treeout v0.1.4 // indirect github.com/gateio/gateapi-go/v6 v6.104.3 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -61,15 +68,20 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lib/pq v1.10.9 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -81,6 +93,7 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/sonirico/vago v0.10.0 // indirect github.com/sonirico/vago/lol v0.0.0-20250901170347-2d1d82c510bd // indirect + github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect github.com/supranational/blst v0.3.16 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect @@ -90,14 +103,21 @@ require ( go.elastic.co/apm/module/apmzerolog/v2 v2.7.1 // indirect go.elastic.co/apm/v2 v2.7.1 // indirect go.elastic.co/fastjson v1.5.1 // indirect + go.mongodb.org/mongo-driver v1.12.2 // indirect + go.uber.org/atomic v1.7.0 // indirect go.uber.org/mock v0.5.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/ratelimit v0.2.0 // indirect + go.uber.org/zap v1.21.0 // indirect golang.org/x/arch v0.20.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.36.0 // indirect + golang.org/x/term v0.35.0 // indirect golang.org/x/text v0.29.0 // indirect + golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.36.0 // indirect google.golang.org/protobuf v1.36.9 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index bf4a9098..48ab2cfd 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= @@ -8,16 +10,21 @@ github.com/adshao/go-binance/v2 v2.8.9 h1:NX+4u/LgEmrjTS7OMWU+9ZgfHKFM61RPhnr9/S github.com/adshao/go-binance/v2 v2.8.9/go.mod h1:XkkuecSyJKPolaCGf/q4ovJYB3t0P+7RUYTbGr+LMGM= github.com/agiledragon/gomonkey/v2 v2.13.0 h1:B24Jg6wBI1iB8EFR1c+/aoTg7QN/Cum7YffG8KMIyYo= github.com/agiledragon/gomonkey/v2 v2.13.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM= github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= +github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= @@ -66,10 +73,18 @@ github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo2 github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= +github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= +github.com/gagliardetto/solana-go v1.14.0 h1:3WfAi70jOOjAJ0deFMjdhFYlLXATF4tOQXsDNWJtOLw= +github.com/gagliardetto/solana-go v1.14.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= +github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= +github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= github.com/gateio/gateapi-go/v6 v6.104.3 h1:JQ2+s1pG4bL+JeLQyGy9c7YLr7hxRI8g7vkAuQYl75k= github.com/gateio/gateapi-go/v6 v6.104.3/go.mod h1:racCcjrdyOUbRDO5eCUGUiyDPrF/ZmwBj/bupPZTVLY= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= @@ -99,8 +114,10 @@ github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1 github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -137,10 +154,18 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -151,6 +176,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -166,6 +193,8 @@ github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuE github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= @@ -174,12 +203,18 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -203,6 +238,7 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -219,12 +255,15 @@ github.com/sonirico/vago v0.10.0 h1:y+4Wo56tK+88a5lUwVrZUO2RRLaPcBgjI5cupKpT1Oc= github.com/sonirico/vago v0.10.0/go.mod h1:HCfnyPHId7V+zBZ5BLfIsdHIO+ewo6+uhF1N0hxlldc= github.com/sonirico/vago/lol v0.0.0-20250901170347-2d1d82c510bd h1:rbvNORW8/0AtH/8W/SUwUykbuh2SeQBrNgFLqYpGTWY= github.com/sonirico/vago/lol v0.0.0-20250901170347-2d1d82c510bd/go.mod h1:pteYccB32seEf19i0TPk7DKdEZdWJ/n9K9DF8AFeXGU= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -233,6 +272,7 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= @@ -249,53 +289,120 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.elastic.co/apm/module/apmzerolog/v2 v2.7.1 h1:C9+KrlqS8F4SZFu+ct0Jmv2YLmzDhWsI8htK6exd3vg= go.elastic.co/apm/module/apmzerolog/v2 v2.7.1/go.mod h1:wXViB7paxMUrERgZrmUb+0FCqgb13Dull1JOOd8Hcj0= go.elastic.co/apm/v2 v2.7.1 h1:OFjARuESjBsxw7wHrEAnfSVNCHGBATXSI/kPvBARY/A= go.elastic.co/apm/v2 v2.7.1/go.mod h1:tQhBAjwh93b2leuAdzGwta/sP7Yc7QoKTSjeIHHDuog= go.elastic.co/fastjson v1.5.1 h1:zeh1xHrFH79aQ6Xsw7YxixvnOdAl3OSv0xch/jRDzko= go.elastic.co/fastjson v1.5.1/go.mod h1:WtvH5wz8z9pDOPqNYSYKoLLv/9zCWZLeejHWuvdL/EM= +go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws= +go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= +go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/dnaeon/go-vcr.v4 v4.0.5 h1:I0hpTIvD5rII+8LgYGrHMA2d4SQPoL6u7ZvJakWKsiA= gopkg.in/dnaeon/go-vcr.v4 v4.0.5/go.mod h1:dRos81TkW9C1WJt6tTaE+uV2Lo8qJT3AG2b35+CB/nQ= gopkg.in/dnaeon/go-vcr.v4 v4.0.6 h1:PiJkrakkmzc5s7EfBnZOnyiLwi7o7A9fwPzN0X2uwe0= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= diff --git a/mcp/blockrun_base.go b/mcp/blockrun_base.go new file mode 100644 index 00000000..89ecf561 --- /dev/null +++ b/mcp/blockrun_base.go @@ -0,0 +1,456 @@ +package mcp + +import ( + "bytes" + "crypto/ecdsa" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "math/big" + "net/http" + "strings" + "time" + + "github.com/ethereum/go-ethereum/crypto" + "golang.org/x/crypto/sha3" +) + +const ( + ProviderBlockRunBase = "blockrun-base" + DefaultBlockRunBaseURL = "https://blockrun.ai" + DefaultBlockRunModel = "gpt-5.4" + BlockRunChatEndpoint = "/api/v1/chat/completions" + BaseUSDCContract = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" + BaseChainID int64 = 8453 + BaseNetwork = "eip155:8453" +) + +// EIP-712 type hashes for USDC TransferWithAuthorization (ERC-3009) +var ( + eip712DomainTypeHash = keccak256String("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") + transferWithAuthTypeHash = keccak256String("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)") +) + +func keccak256String(s string) []byte { + h := sha3.NewLegacyKeccak256() + h.Write([]byte(s)) + return h.Sum(nil) +} + +func keccak256Bytes(data ...[]byte) []byte { + h := sha3.NewLegacyKeccak256() + for _, b := range data { + h.Write(b) + } + return h.Sum(nil) +} + +// BlockRunBaseClient implements AIClient using BlockRun's API with x402 v2 EIP-712 payment signing. +type BlockRunBaseClient struct { + *Client + privateKey *ecdsa.PrivateKey +} + +// NewBlockRunBaseClient creates a BlockRun Base wallet client (backward compatible). +func NewBlockRunBaseClient() AIClient { + return NewBlockRunBaseClientWithOptions() +} + +// NewBlockRunBaseClientWithOptions creates a BlockRun Base wallet client. +func NewBlockRunBaseClientWithOptions(opts ...ClientOption) AIClient { + baseOpts := []ClientOption{ + WithProvider(ProviderBlockRunBase), + WithModel(DefaultBlockRunModel), + WithBaseURL(DefaultBlockRunBaseURL), + } + allOpts := append(baseOpts, opts...) + baseClient := NewClient(allOpts...).(*Client) + baseClient.UseFullURL = true + baseClient.BaseURL = DefaultBlockRunBaseURL + BlockRunChatEndpoint + + c := &BlockRunBaseClient{Client: baseClient} + baseClient.hooks = c + return c +} + +// SetAPIKey stores the EVM private key (hex, with or without 0x prefix). +// customModel selects the AI model to use (e.g. "claude-sonnet-4.6"); empty means default. +func (c *BlockRunBaseClient) SetAPIKey(apiKey string, customURL string, customModel string) { + hexKey := strings.TrimPrefix(apiKey, "0x") + privKey, err := crypto.HexToECDSA(hexKey) + if err != nil { + c.logger.Warnf("โš ๏ธ [MCP] BlockRun Base: invalid private key: %v", err) + } else { + c.privateKey = privKey + c.APIKey = apiKey + addr := crypto.PubkeyToAddress(privKey.PublicKey).Hex() + c.logger.Infof("๐Ÿ”ง [MCP] BlockRun Base wallet: %s", addr) + } + if customModel != "" { + c.Model = customModel + c.logger.Infof("๐Ÿ”ง [MCP] BlockRun Base model: %s", customModel) + } else { + c.logger.Infof("๐Ÿ”ง [MCP] BlockRun Base model: %s", DefaultBlockRunModel) + } +} + +func (c *BlockRunBaseClient) setAuthHeader(reqHeaders http.Header) { + // No Bearer token โ€” payment is via x402 signing +} + +// call overrides the base call to handle HTTP 402 x402 v2 payment flow. +func (c *BlockRunBaseClient) call(systemPrompt, userPrompt string) (string, error) { + c.logger.Infof("๐Ÿ“ก [BlockRun Base] Request AI Server: %s", c.BaseURL) + + requestBody := c.hooks.buildMCPRequestBody(systemPrompt, userPrompt) + jsonData, err := c.hooks.marshalRequestBody(requestBody) + if err != nil { + return "", err + } + + url := c.hooks.buildUrl() + req, err := c.hooks.buildRequest(url, jsonData) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + // Handle x402 v2 Payment Required + if resp.StatusCode == http.StatusPaymentRequired { + paymentHeader := resp.Header.Get("X-Payment-Required") + if paymentHeader == "" { + return "", fmt.Errorf("received 402 but no X-Payment-Required header") + } + + paymentSig, err := c.signPayment(paymentHeader) + if err != nil { + return "", fmt.Errorf("failed to sign x402 payment: %w", err) + } + + req2, err := c.hooks.buildRequest(url, jsonData) + if err != nil { + return "", fmt.Errorf("failed to build retry request: %w", err) + } + req2.Header.Set("X-Payment", paymentSig) + + resp2, err := c.httpClient.Do(req2) + if err != nil { + return "", fmt.Errorf("failed to send payment retry: %w", err) + } + defer resp2.Body.Close() + + body2, err := io.ReadAll(resp2.Body) + if err != nil { + return "", fmt.Errorf("failed to read payment retry response: %w", err) + } + if resp2.StatusCode != http.StatusOK { + return "", fmt.Errorf("BlockRun payment retry failed (status %d): %s", resp2.StatusCode, string(body2)) + } + return c.hooks.parseMCPResponse(body2) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %w", err) + } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("BlockRun API error (status %d): %s", resp.StatusCode, string(body)) + } + return c.hooks.parseMCPResponse(body) +} + +// x402v2PaymentRequired is the structure of the X-Payment-Required header (x402 v2). +type x402v2PaymentRequired struct { + X402Version int `json:"x402Version"` + Accepts []struct { + Scheme string `json:"scheme"` + Network string `json:"network"` + Amount string `json:"amount"` + Asset string `json:"asset"` + PayTo string `json:"payTo"` + MaxTimeoutSeconds int `json:"maxTimeoutSeconds"` + Extra map[string]string `json:"extra"` + } `json:"accepts"` + Resource *struct { + URL string `json:"url"` + Description string `json:"description"` + MimeType string `json:"mimeType"` + } `json:"resource"` +} + +// signPayment parses the X-Payment-Required header (x402 v2) and returns a signed X-Payment value. +func (c *BlockRunBaseClient) signPayment(paymentHeaderB64 string) (string, error) { + if c.privateKey == nil { + return "", fmt.Errorf("no private key set for BlockRun Base wallet") + } + + // Decode base64 โ†’ JSON + decoded, err := base64.RawStdEncoding.DecodeString(paymentHeaderB64) + if err != nil { + decoded, err = base64.StdEncoding.DecodeString(paymentHeaderB64) + if err != nil { + return "", fmt.Errorf("failed to base64-decode payment header: %w", err) + } + } + + var req x402v2PaymentRequired + if err := json.Unmarshal(decoded, &req); err != nil { + return "", fmt.Errorf("failed to parse x402 v2 payment header: %w", err) + } + + if len(req.Accepts) == 0 { + return "", fmt.Errorf("no payment options in x402 response") + } + + opt := req.Accepts[0] + recipient := opt.PayTo + amount := opt.Amount + network := opt.Network + asset := opt.Asset + extra := opt.Extra + maxTimeout := opt.MaxTimeoutSeconds + if maxTimeout == 0 { + maxTimeout = 300 + } + + resourceURL := "" + resourceDesc := "" + resourceMime := "application/json" + if req.Resource != nil { + resourceURL = req.Resource.URL + resourceDesc = req.Resource.Description + resourceMime = req.Resource.MimeType + } + + // Timestamps: validAfter = now-600 (clock skew), validBefore = now+maxTimeout + now := time.Now().Unix() + validAfter := now - 600 + validBefore := now + int64(maxTimeout) + + // Random nonce (bytes32) + nonceBytes := make([]byte, 32) + if _, err := rand.Read(nonceBytes); err != nil { + return "", fmt.Errorf("failed to generate nonce: %w", err) + } + nonce := "0x" + hex.EncodeToString(nonceBytes) + + // Sender address + senderAddr := crypto.PubkeyToAddress(c.privateKey.PublicKey).Hex() + + // Build EIP-712 domain separator + domainName := "USD Coin" + domainVersion := "2" + if extra != nil { + if v, ok := extra["name"]; ok && v != "" { + domainName = v + } + if v, ok := extra["version"]; ok && v != "" { + domainVersion = v + } + } + + domainSeparator, err := buildDomainSeparatorDynamic(domainName, domainVersion, network, asset) + if err != nil { + return "", fmt.Errorf("failed to build domain separator: %w", err) + } + + // Build struct hash + amountBig, err := parseBigInt(amount) + if err != nil { + return "", fmt.Errorf("invalid amount: %w", err) + } + + structHash, err := buildTransferWithAuthHashDynamic(senderAddr, recipient, amountBig, validAfter, validBefore, nonce) + if err != nil { + return "", fmt.Errorf("failed to build struct hash: %w", err) + } + + // EIP-712 digest + digest := make([]byte, 0, 66) + digest = append(digest, 0x19, 0x01) + digest = append(digest, domainSeparator...) + digest = append(digest, structHash...) + hash := keccak256Bytes(digest) + + // Sign with secp256k1 + sig, err := crypto.Sign(hash, c.privateKey) + if err != nil { + return "", fmt.Errorf("failed to sign: %w", err) + } + // Adjust V: go-ethereum returns 0/1, EIP-712 expects 27/28 + if sig[64] < 27 { + sig[64] += 27 + } + + sigHex := "0x" + hex.EncodeToString(sig) + + // Build x402 v2 payment payload + paymentData := map[string]interface{}{ + "x402Version": 2, + "resource": map[string]string{ + "url": resourceURL, + "description": resourceDesc, + "mimeType": resourceMime, + }, + "accepted": map[string]interface{}{ + "scheme": "exact", + "network": network, + "amount": amount, + "asset": asset, + "payTo": recipient, + "maxTimeoutSeconds": maxTimeout, + "extra": extra, + }, + "payload": map[string]interface{}{ + "signature": sigHex, + "authorization": map[string]string{ + "from": senderAddr, + "to": recipient, + "value": amount, + "validAfter": fmt.Sprintf("%d", validAfter), + "validBefore": fmt.Sprintf("%d", validBefore), + "nonce": nonce, + }, + }, + "extensions": map[string]interface{}{}, + } + + resultJSON, err := json.Marshal(paymentData) + if err != nil { + return "", fmt.Errorf("failed to marshal payment result: %w", err) + } + + return base64.StdEncoding.EncodeToString(resultJSON), nil +} + +// buildDomainSeparatorDynamic builds the EIP-712 domain separator using runtime values. +func buildDomainSeparatorDynamic(name, version, network, asset string) ([]byte, error) { + // Extract chain ID from network string like "eip155:8453" + chainID := new(big.Int).SetInt64(BaseChainID) + if strings.HasPrefix(network, "eip155:") { + parts := strings.SplitN(network, ":", 2) + if len(parts) == 2 { + if n, ok := new(big.Int).SetString(parts[1], 10); ok { + chainID = n + } + } + } + + contractAddr, err := hex.DecodeString(strings.TrimPrefix(asset, "0x")) + if err != nil { + return nil, fmt.Errorf("invalid contract address: %w", err) + } + + nameHash := keccak256String(name) + versionHash := keccak256String(version) + + encoded := make([]byte, 0, 5*32) + encoded = append(encoded, leftPad32(eip712DomainTypeHash)...) + encoded = append(encoded, leftPad32(nameHash)...) + encoded = append(encoded, leftPad32(versionHash)...) + encoded = append(encoded, leftPad32(chainID.Bytes())...) + addrPadded := make([]byte, 32) + copy(addrPadded[32-len(contractAddr):], contractAddr) + encoded = append(encoded, addrPadded...) + + return keccak256Bytes(encoded), nil +} + +// buildTransferWithAuthHashDynamic builds the struct hash for TransferWithAuthorization. +func buildTransferWithAuthHashDynamic(from, to string, value *big.Int, validAfter, validBefore int64, nonce string) ([]byte, error) { + fromBytes, err := hexToAddress(from) + if err != nil { + return nil, fmt.Errorf("invalid from address: %w", err) + } + toBytes, err := hexToAddress(to) + if err != nil { + return nil, fmt.Errorf("invalid to address: %w", err) + } + nonceBytes, err := hexToBytes32(nonce) + if err != nil { + return nil, fmt.Errorf("invalid nonce: %w", err) + } + + validAfterBig := new(big.Int).SetInt64(validAfter) + validBeforeBig := new(big.Int).SetInt64(validBefore) + + encoded := make([]byte, 0, 7*32) + encoded = append(encoded, leftPad32(transferWithAuthTypeHash)...) + encoded = append(encoded, leftPad32(fromBytes)...) + encoded = append(encoded, leftPad32(toBytes)...) + encoded = append(encoded, leftPad32(value.Bytes())...) + encoded = append(encoded, leftPad32(validAfterBig.Bytes())...) + encoded = append(encoded, leftPad32(validBeforeBig.Bytes())...) + encoded = append(encoded, leftPad32(nonceBytes)...) + + return keccak256Bytes(encoded), nil +} + +func hexToAddress(s string) ([]byte, error) { + s = strings.TrimPrefix(s, "0x") + b, err := hex.DecodeString(s) + if err != nil { + return nil, err + } + if len(b) != 20 { + return nil, fmt.Errorf("address must be 20 bytes, got %d", len(b)) + } + return b, nil +} + +func hexToBytes32(s string) ([]byte, error) { + s = strings.TrimPrefix(s, "0x") + b, err := hex.DecodeString(s) + if err != nil { + return nil, err + } + if len(b) > 32 { + return nil, fmt.Errorf("nonce too long: %d bytes", len(b)) + } + return b, nil +} + +func parseBigInt(s string) (*big.Int, error) { + s = strings.TrimPrefix(s, "0x") + n := new(big.Int) + if _, ok := n.SetString(s, 16); ok { + return n, nil + } + if _, ok := n.SetString(s, 10); ok { + return n, nil + } + return nil, fmt.Errorf("cannot parse big.Int from %q", s) +} + +// leftPad32 pads a byte slice to 32 bytes on the left (ABI encoding). +func leftPad32(b []byte) []byte { + if len(b) >= 32 { + return b[:32] + } + padded := make([]byte, 32) + copy(padded[32-len(b):], b) + return padded +} + +// buildUrl returns the full BlockRun endpoint URL. +func (c *BlockRunBaseClient) buildUrl() string { + return DefaultBlockRunBaseURL + BlockRunChatEndpoint +} + +// buildRequest creates the HTTP request without an Authorization header. +func (c *BlockRunBaseClient) buildRequest(url string, jsonData []byte) (*http.Request, error) { + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("fail to build request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + return req, nil +} diff --git a/mcp/blockrun_sol.go b/mcp/blockrun_sol.go new file mode 100644 index 00000000..03dab8ec --- /dev/null +++ b/mcp/blockrun_sol.go @@ -0,0 +1,371 @@ +package mcp + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/programs/compute-budget" + "github.com/gagliardetto/solana-go/programs/token" + "github.com/gagliardetto/solana-go/rpc" +) + +const ( + ProviderBlockRunSol = "blockrun-sol" + DefaultBlockRunSolURL = "https://sol.blockrun.ai" + SolanaUSDCMint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + SolanaNetwork = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + SolanaMainnetRPC = "https://api.mainnet-beta.solana.com" + + // Compute budget defaults (match @x402/svm) + computeUnitLimit = uint32(8000) + computeUnitPrice = uint64(1) +) + +// BlockRunSolClient implements AIClient using BlockRun's Solana x402 v2 payment protocol. +type BlockRunSolClient struct { + *Client + keypair solana.PrivateKey +} + +// NewBlockRunSolClient creates a BlockRun Solana wallet client (backward compatible). +func NewBlockRunSolClient() AIClient { + return NewBlockRunSolClientWithOptions() +} + +// NewBlockRunSolClientWithOptions creates a BlockRun Solana wallet client. +func NewBlockRunSolClientWithOptions(opts ...ClientOption) AIClient { + baseOpts := []ClientOption{ + WithProvider(ProviderBlockRunSol), + WithModel(DefaultBlockRunModel), + WithBaseURL(DefaultBlockRunSolURL), + } + allOpts := append(baseOpts, opts...) + baseClient := NewClient(allOpts...).(*Client) + baseClient.UseFullURL = true + baseClient.BaseURL = DefaultBlockRunSolURL + BlockRunChatEndpoint + + c := &BlockRunSolClient{Client: baseClient} + baseClient.hooks = c + return c +} + +// SetAPIKey stores the Solana wallet private key (base58-encoded 64-byte keypair). +// customModel selects the AI model; empty means default. +func (c *BlockRunSolClient) SetAPIKey(apiKey string, customURL string, customModel string) { + kp, err := solana.PrivateKeyFromBase58(strings.TrimSpace(apiKey)) + if err != nil { + c.logger.Warnf("โš ๏ธ [MCP] BlockRun Sol: failed to parse private key: %v", err) + return + } + c.keypair = kp + c.APIKey = apiKey + c.logger.Infof("๐Ÿ”ง [MCP] BlockRun Sol wallet: %s", kp.PublicKey().String()) + + if customModel != "" { + c.Model = customModel + c.logger.Infof("๐Ÿ”ง [MCP] BlockRun Sol model: %s", customModel) + } else { + c.logger.Infof("๐Ÿ”ง [MCP] BlockRun Sol model: %s", DefaultBlockRunModel) + } +} + +func (c *BlockRunSolClient) setAuthHeader(reqHeaders http.Header) { + // No Bearer token โ€” payment is via x402 signing +} + +// call overrides the base call to handle HTTP 402 x402 v2 Solana payment flow. +func (c *BlockRunSolClient) call(systemPrompt, userPrompt string) (string, error) { + c.logger.Infof("๐Ÿ“ก [BlockRun Sol] Request AI Server: %s", c.BaseURL) + + requestBody := c.hooks.buildMCPRequestBody(systemPrompt, userPrompt) + jsonData, err := c.hooks.marshalRequestBody(requestBody) + if err != nil { + return "", err + } + + url := c.hooks.buildUrl() + req, err := c.hooks.buildRequest(url, jsonData) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + // Handle x402 v2 Payment Required + if resp.StatusCode == http.StatusPaymentRequired { + paymentHeader := resp.Header.Get("X-Payment-Required") + if paymentHeader == "" { + return "", fmt.Errorf("received 402 but no X-Payment-Required header") + } + + paymentSig, err := c.signSolanaPayment(paymentHeader) + if err != nil { + return "", fmt.Errorf("failed to sign Solana x402 payment: %w", err) + } + + req2, err := c.hooks.buildRequest(url, jsonData) + if err != nil { + return "", fmt.Errorf("failed to build retry request: %w", err) + } + req2.Header.Set("X-Payment", paymentSig) + + resp2, err := c.httpClient.Do(req2) + if err != nil { + return "", fmt.Errorf("failed to send payment retry: %w", err) + } + defer resp2.Body.Close() + + body2, err := io.ReadAll(resp2.Body) + if err != nil { + return "", fmt.Errorf("failed to read payment retry response: %w", err) + } + if resp2.StatusCode != http.StatusOK { + return "", fmt.Errorf("BlockRun Sol payment retry failed (status %d): %s", resp2.StatusCode, string(body2)) + } + return c.hooks.parseMCPResponse(body2) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %w", err) + } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("BlockRun Sol API error (status %d): %s", resp.StatusCode, string(body)) + } + return c.hooks.parseMCPResponse(body) +} + +// solanaPaymentOption is an entry in the accepts[] array of the x402 v2 response. +type solanaPaymentOption struct { + Scheme string `json:"scheme"` + Network string `json:"network"` + Amount string `json:"amount"` + Asset string `json:"asset"` + PayTo string `json:"payTo"` + MaxTimeoutSeconds int `json:"maxTimeoutSeconds"` + Extra map[string]string `json:"extra"` +} + +// x402v2SolanaRequired is the parsed X-Payment-Required header for Solana. +type x402v2SolanaRequired struct { + X402Version int `json:"x402Version"` + Accepts []solanaPaymentOption `json:"accepts"` + Resource *struct { + URL string `json:"url"` + Description string `json:"description"` + MimeType string `json:"mimeType"` + } `json:"resource"` +} + +// signSolanaPayment parses the X-Payment-Required header and builds a signed x402 v2 Solana payload. +func (c *BlockRunSolClient) signSolanaPayment(paymentHeaderB64 string) (string, error) { + if c.keypair == nil { + return "", fmt.Errorf("no private key set for BlockRun Sol wallet") + } + + // Decode base64 โ†’ JSON + decoded, err := base64.RawStdEncoding.DecodeString(paymentHeaderB64) + if err != nil { + decoded, err = base64.StdEncoding.DecodeString(paymentHeaderB64) + if err != nil { + return "", fmt.Errorf("failed to base64-decode payment header: %w", err) + } + } + + var req x402v2SolanaRequired + if err := json.Unmarshal(decoded, &req); err != nil { + return "", fmt.Errorf("failed to parse x402 v2 Solana header: %w", err) + } + + // Find the Solana option + var opt *solanaPaymentOption + for i := range req.Accepts { + if strings.HasPrefix(req.Accepts[i].Network, "solana:") { + opt = &req.Accepts[i] + break + } + } + if opt == nil { + return "", fmt.Errorf("no Solana payment option in x402 response") + } + + recipient := opt.PayTo + amount := opt.Amount + feePayer := "" + if opt.Extra != nil { + feePayer = opt.Extra["feePayer"] + } + if feePayer == "" { + return "", fmt.Errorf("feePayer missing from Solana x402 extra") + } + + maxTimeout := opt.MaxTimeoutSeconds + if maxTimeout == 0 { + maxTimeout = 300 + } + + resourceURL := DefaultBlockRunSolURL + BlockRunChatEndpoint + resourceDesc := "" + resourceMime := "application/json" + if req.Resource != nil { + resourceURL = req.Resource.URL + resourceDesc = req.Resource.Description + resourceMime = req.Resource.MimeType + } + + // Build the SPL TransferChecked transaction + txB64, err := c.buildSolanaTransferTx(recipient, feePayer, amount) + if err != nil { + return "", fmt.Errorf("failed to build Solana transfer tx: %w", err) + } + + // Build x402 v2 payment payload + paymentData := map[string]interface{}{ + "x402Version": 2, + "resource": map[string]string{ + "url": resourceURL, + "description": resourceDesc, + "mimeType": resourceMime, + }, + "accepted": map[string]interface{}{ + "scheme": "exact", + "network": SolanaNetwork, + "amount": amount, + "asset": SolanaUSDCMint, + "payTo": recipient, + "maxTimeoutSeconds": maxTimeout, + "extra": opt.Extra, + }, + "payload": map[string]string{ + "transaction": txB64, + }, + "extensions": map[string]interface{}{}, + } + + resultJSON, err := json.Marshal(paymentData) + if err != nil { + return "", fmt.Errorf("failed to marshal Solana payment: %w", err) + } + + return base64.StdEncoding.EncodeToString(resultJSON), nil +} + +// buildSolanaTransferTx builds a partial-signed VersionedTransaction for SPL USDC TransferChecked. +// The fee payer (CDP facilitator) slot is left with a zero signature; only the user signs. +func (c *BlockRunSolClient) buildSolanaTransferTx(recipient, feePayer, amountStr string) (string, error) { + ownerPubkey := c.keypair.PublicKey() + + // Parse recipient and feePayer + recipientPK, err := solana.PublicKeyFromBase58(recipient) + if err != nil { + return "", fmt.Errorf("invalid recipient address: %w", err) + } + feePayerPK, err := solana.PublicKeyFromBase58(feePayer) + if err != nil { + return "", fmt.Errorf("invalid feePayer address: %w", err) + } + mintPK := solana.MustPublicKeyFromBase58(SolanaUSDCMint) + + // Parse amount + var amountU64 uint64 + if _, err := fmt.Sscanf(amountStr, "%d", &amountU64); err != nil { + return "", fmt.Errorf("invalid amount %q: %w", amountStr, err) + } + + // Derive ATAs + sourceATA, _, err := solana.FindAssociatedTokenAddress(ownerPubkey, mintPK) + if err != nil { + return "", fmt.Errorf("failed to derive source ATA: %w", err) + } + destATA, _, err := solana.FindAssociatedTokenAddress(recipientPK, mintPK) + if err != nil { + return "", fmt.Errorf("failed to derive dest ATA: %w", err) + } + + // Fetch latest blockhash from Solana mainnet + rpcClient := rpc.New(SolanaMainnetRPC) + bhResp, err := rpcClient.GetLatestBlockhash(context.Background(), rpc.CommitmentFinalized) + if err != nil { + return "", fmt.Errorf("failed to fetch blockhash: %w", err) + } + recentBlockhash := bhResp.Value.Blockhash + + // Build instructions: ComputeBudgetSetLimit, ComputeBudgetSetPrice, TransferChecked + setLimitIx, err := computebudget.NewSetComputeUnitLimitInstruction(computeUnitLimit).ValidateAndBuild() + if err != nil { + return "", fmt.Errorf("failed to build SetComputeUnitLimit: %w", err) + } + setPriceIx, err := computebudget.NewSetComputeUnitPriceInstruction(computeUnitPrice).ValidateAndBuild() + if err != nil { + return "", fmt.Errorf("failed to build SetComputeUnitPrice: %w", err) + } + transferIx, err := token.NewTransferCheckedInstruction( + amountU64, + 6, // USDC decimals + sourceATA, + mintPK, + destATA, + ownerPubkey, + []solana.PublicKey{}, + ).ValidateAndBuild() + if err != nil { + return "", fmt.Errorf("failed to build TransferChecked: %w", err) + } + + // Build transaction with feePayer as payer (matches Python SDK) + tx, err := solana.NewTransaction( + []solana.Instruction{setLimitIx, setPriceIx, transferIx}, + recentBlockhash, + solana.TransactionPayer(feePayerPK), + ) + if err != nil { + return "", fmt.Errorf("failed to build transaction: %w", err) + } + + // Partial sign: user signs; fee_payer (CDP) co-signs on server side + // The transaction has 2 signers: [feePayer (index 0), owner (index 1)] + // We sign only our index (owner). + _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { + if key.Equals(ownerPubkey) { + return &c.keypair + } + return nil // feePayer will be signed by BlockRun CDP + }) + if err != nil { + return "", fmt.Errorf("failed to sign transaction: %w", err) + } + + // Serialize transaction + txBytes, err := tx.MarshalBinary() + if err != nil { + return "", fmt.Errorf("failed to serialize transaction: %w", err) + } + + return base64.StdEncoding.EncodeToString(txBytes), nil +} + +// buildUrl returns the full BlockRun Solana endpoint URL. +func (c *BlockRunSolClient) buildUrl() string { + return DefaultBlockRunSolURL + BlockRunChatEndpoint +} + +// buildRequest creates the HTTP request without an Authorization header. +func (c *BlockRunSolClient) buildRequest(url string, jsonData []byte) (*http.Request, error) { + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("fail to build request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + return req, nil +} diff --git a/trader/auto_trader.go b/trader/auto_trader.go index baedeaa6..d2b1b905 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -206,6 +206,16 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au mcpClient.SetAPIKey(config.CustomAPIKey, config.CustomAPIURL, config.CustomModelName) logger.Infof("๐Ÿค– [%s] Using MiniMax AI", config.Name) + case "blockrun-base": + mcpClient = mcp.NewBlockRunBaseClient() + mcpClient.SetAPIKey(config.CustomAPIKey, "", config.CustomModelName) + logger.Infof("๐Ÿค– [%s] Using BlockRun (Base Wallet) AI", config.Name) + + case "blockrun-sol": + mcpClient = mcp.NewBlockRunSolClient() + mcpClient.SetAPIKey(config.CustomAPIKey, "", config.CustomModelName) + logger.Infof("๐Ÿค– [%s] Using BlockRun (Solana Wallet) AI", config.Name) + case "qwen": mcpClient = mcp.NewQwenClient() apiKey := config.QwenKey diff --git a/web/public/icons/blockrun.svg b/web/public/icons/blockrun.svg new file mode 100644 index 00000000..403f0a5a --- /dev/null +++ b/web/public/icons/blockrun.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index 71a29fe1..72dfaf78 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -55,6 +55,16 @@ function getShortName(fullName: string): string { return parts.length > 1 ? parts[parts.length - 1] : fullName } +// Top models available through BlockRun wallet providers +const BLOCKRUN_MODELS = [ + { id: 'gpt-5.4', name: 'GPT-5.4', desc: 'OpenAI ยท Flagship' }, + { id: 'claude-opus-4.6', name: 'Claude Opus 4.6', desc: 'Anthropic ยท Flagship' }, + { id: 'gemini-3.1-pro', name: 'Gemini 3.1 Pro', desc: 'Google ยท Flagship' }, + { id: 'grok-3', name: 'Grok 3', desc: 'xAI ยท Flagship' }, + { id: 'deepseek-chat', name: 'DeepSeek Chat', desc: 'DeepSeek ยท Flagship' }, + { id: 'minimax-m2.5', name: 'MiniMax M2.5', desc: 'MiniMax ยท Flagship' }, +] + // AI Provider configuration - default models and API links const AI_PROVIDER_CONFIG: Record
- {availableModels.map((model) => ( + {availableModels.filter(m => !m.provider?.startsWith('blockrun')).map((model) => ( ))}
+ {availableModels.some(m => m.provider?.startsWith('blockrun')) && ( + <> +
+
+ + {language === 'zh' ? '้€š่ฟ‡้’ฑๅŒ…ๆ”ฏไป˜' : 'Via BlockRun Wallet'} + +
+
+
+ {availableModels.filter(m => m.provider?.startsWith('blockrun')).map((model) => ( + handleSelectModel(model.id)} + configured={configuredIds.has(model.id)} + /> + ))} +
+ + )}
{language === 'zh' ? 'ๅธฆ้‡‘่‰ฒๆ ‡่ฎฐ็š„ๆจกๅž‹ๅทฒ้…็ฝฎ' : 'Models with gold badge are already configured'}
@@ -1644,7 +1686,9 @@ function ModelConfigModal({ > - {language === 'zh' ? '่Žทๅ– API Key' : 'Get API Key'} + {selectedModel.provider?.startsWith('blockrun') + ? (language === 'zh' ? 'ๅผ€ๅง‹ไฝฟ็”จ' : 'Get Started') + : (language === 'zh' ? '่Žทๅ– API Key' : 'Get API Key')} )} @@ -1662,66 +1706,112 @@ function ModelConfigModal({
)} - {/* API Key */} + {/* API Key / Wallet Private Key */}
setApiKey(e.target.value)} - placeholder={t('enterAPIKey', language)} + placeholder={ + selectedModel.provider === 'blockrun-base' + ? '0x... (EVM private key)' + : selectedModel.provider === 'blockrun-sol' + ? 'bs58 encoded key (Solana)' + : t('enterAPIKey', language) + } className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
- {/* Custom Base URL */} -
- - setBaseUrl(e.target.value)} - placeholder={t('customBaseURLPlaceholder', language)} - className="w-full px-4 py-3 rounded-xl" - style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} - /> -
- {t('leaveBlankForDefault', language)} + {/* Custom Base URL (hidden for BlockRun) */} + {!selectedModel.provider?.startsWith('blockrun') && ( +
+ + setBaseUrl(e.target.value)} + placeholder={t('customBaseURLPlaceholder', language)} + className="w-full px-4 py-3 rounded-xl" + style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} + /> +
+ {t('leaveBlankForDefault', language)} +
-
+ )} - {/* Custom Model Name */} -
- - setModelName(e.target.value)} - placeholder={t('customModelNamePlaceholder', language)} - className="w-full px-4 py-3 rounded-xl" - style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} - /> -
- {t('leaveBlankForDefaultModel', language)} + {/* Custom Model Name (hidden for BlockRun) */} + {!selectedModel.provider?.startsWith('blockrun') && ( +
+ + setModelName(e.target.value)} + placeholder={t('customModelNamePlaceholder', language)} + className="w-full px-4 py-3 rounded-xl" + style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} + /> +
+ {t('leaveBlankForDefaultModel', language)} +
-
+ )} + + {/* BlockRun Model Selector */} + {selectedModel.provider?.startsWith('blockrun') && ( +
+ +
+ {BLOCKRUN_MODELS.map((m) => { + const isSelected = (modelName || BLOCKRUN_MODELS[0].id) === m.id + return ( + + ) + })} +
+
+ )} {/* Info Box */}
diff --git a/web/src/components/ModelIcons.tsx b/web/src/components/ModelIcons.tsx index b3cab90f..42721810 100644 --- a/web/src/components/ModelIcons.tsx +++ b/web/src/components/ModelIcons.tsx @@ -14,6 +14,8 @@ const MODEL_COLORS: Record = { grok: '#000000', openai: '#10A37F', minimax: '#E45735', + 'blockrun-base': '#2563EB', + 'blockrun-sol': '#9945FF', } // ่Žทๅ–AIๆจกๅž‹ๅ›พๆ ‡็š„ๅ‡ฝๆ•ฐ @@ -48,6 +50,10 @@ export const getModelIcon = (modelType: string, props: IconProps = {}) => { case 'minimax': iconPath = '/icons/minimax.svg' break + case 'blockrun-base': + case 'blockrun-sol': + iconPath = '/icons/blockrun.svg' + break default: return null }