Files
nofx/mcp/blockrun_sol.go
tinkle-community 4774348ed6 refactor: centralize x402 payment flow into shared mcp/x402.go
Extract duplicated doRequestWithPayment/call/CallWithRequestFull/buildRequest/
setAuthHeader (~165 lines x3) into shared helpers in mcp/x402.go. Consolidate
shared types (x402v2PaymentRequired, x402AcceptOption, x402Resource) and remove
duplicate Solana types. Fix validAfter to 0 (official SDK standard), drain 402
body before retry, log Payment-Response tx hash, check Payment-Required before
X-Payment-Required.
2026-03-11 15:42:19 +08:00

278 lines
8.5 KiB
Go

package mcp
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"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(h http.Header) { x402SetAuthHeader(h) }
func (c *BlockRunSolClient) call(systemPrompt, userPrompt string) (string, error) {
return x402Call(c.Client, c.signSolanaPayment, "BlockRun Sol", systemPrompt, userPrompt)
}
func (c *BlockRunSolClient) CallWithRequestFull(req *Request) (*LLMResponse, error) {
return x402CallFull(c.Client, c.signSolanaPayment, "BlockRun Sol", req)
}
// signSolanaPayment parses the 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")
}
decoded, err := x402DecodeHeader(paymentHeaderB64)
if err != nil {
return "", err
}
var req x402v2PaymentRequired
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 *x402AcceptOption
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
}
func (c *BlockRunSolClient) buildRequest(url string, jsonData []byte) (*http.Request, error) {
return x402BuildRequest(url, jsonData)
}