mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
refactor: restructure project directories for better modularity
- Delete llm/ dead code (3 files, zero references) - Split mcp/ into sub-packages: mcp/provider/ (8 providers) and mcp/payment/ (4 payment clients) with registry pattern - Export Client internal fields and ClientHooks interface for sub-package access - Split api/server.go (3892 lines) into 8 domain-specific handler files - Split trader/auto_trader.go (2296 lines) into 5 focused files - Reorganize web/src/components/ flat files into auth/, charts/, trader/, common/, modals/, backtest/ subdirectories - Update all consumer imports to use registry-based provider creation
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"golang.org/x/crypto/sha3"
|
||||
|
||||
"nofx/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
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 init() {
|
||||
mcp.RegisterProvider(mcp.ProviderBlockRunBase, func(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
return NewBlockRunBaseClientWithOptions(opts...)
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
*mcp.Client
|
||||
privateKey *ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
func (c *BlockRunBaseClient) BaseClient() *mcp.Client { return c.Client }
|
||||
|
||||
// NewBlockRunBaseClient creates a BlockRun Base wallet client (backward compatible).
|
||||
func NewBlockRunBaseClient() mcp.AIClient {
|
||||
return NewBlockRunBaseClientWithOptions()
|
||||
}
|
||||
|
||||
// NewBlockRunBaseClientWithOptions creates a BlockRun Base wallet client.
|
||||
func NewBlockRunBaseClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
baseOpts := []mcp.ClientOption{
|
||||
mcp.WithProvider(mcp.ProviderBlockRunBase),
|
||||
mcp.WithModel(DefaultBlockRunModel),
|
||||
mcp.WithBaseURL(DefaultBlockRunBaseURL),
|
||||
}
|
||||
allOpts := append(baseOpts, opts...)
|
||||
baseClient := mcp.NewClient(allOpts...).(*mcp.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).
|
||||
func (c *BlockRunBaseClient) SetAPIKey(apiKey string, customURL string, customModel string) {
|
||||
hexKey := strings.TrimPrefix(apiKey, "0x")
|
||||
privKey, err := crypto.HexToECDSA(hexKey)
|
||||
if err != nil {
|
||||
c.Log.Warnf("⚠️ [MCP] BlockRun Base: invalid private key: %v", err)
|
||||
} else {
|
||||
c.privateKey = privKey
|
||||
c.APIKey = apiKey
|
||||
addr := crypto.PubkeyToAddress(privKey.PublicKey).Hex()
|
||||
c.Log.Infof("🔧 [MCP] BlockRun Base wallet: %s", addr)
|
||||
}
|
||||
if customModel != "" {
|
||||
c.Model = customModel
|
||||
c.Log.Infof("🔧 [MCP] BlockRun Base model: %s", customModel)
|
||||
} else {
|
||||
c.Log.Infof("🔧 [MCP] BlockRun Base model: %s", DefaultBlockRunModel)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *BlockRunBaseClient) SetAuthHeader(h http.Header) { X402SetAuthHeader(h) }
|
||||
|
||||
func (c *BlockRunBaseClient) Call(systemPrompt, userPrompt string) (string, error) {
|
||||
return X402Call(c.Client, c.signPayment, "BlockRun Base", systemPrompt, userPrompt)
|
||||
}
|
||||
|
||||
func (c *BlockRunBaseClient) CallWithRequestFull(req *mcp.Request) (*mcp.LLMResponse, error) {
|
||||
return X402CallFull(c.Client, c.signPayment, "BlockRun Base", req)
|
||||
}
|
||||
|
||||
// signPayment parses the Payment-Required header (x402 v2) and returns a signed payment value.
|
||||
func (c *BlockRunBaseClient) signPayment(paymentHeaderB64 string) (string, error) {
|
||||
return SignBasePaymentHeader(c.privateKey, paymentHeaderB64, "BlockRun Base")
|
||||
}
|
||||
|
||||
// SignX402Payment is the shared EIP-712 signing logic for x402 v2 on Base USDC.
|
||||
// Used by both BlockRunBaseClient and Claw402Client.
|
||||
func SignX402Payment(privateKey *ecdsa.PrivateKey, senderAddr string, opt X402AcceptOption, resource *X402Resource) (string, error) {
|
||||
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 resource != nil {
|
||||
resourceURL = resource.URL
|
||||
resourceDesc = resource.Description
|
||||
resourceMime = resource.MimeType
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
validAfter := int64(0)
|
||||
validBefore := now + int64(maxTimeout)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
digest := make([]byte, 0, 66)
|
||||
digest = append(digest, 0x19, 0x01)
|
||||
digest = append(digest, domainSeparator...)
|
||||
digest = append(digest, structHash...)
|
||||
hash := keccak256Bytes(digest)
|
||||
|
||||
sig, err := crypto.Sign(hash, privateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign: %w", err)
|
||||
}
|
||||
if sig[64] < 27 {
|
||||
sig[64] += 27
|
||||
}
|
||||
|
||||
sigHex := "0x" + hex.EncodeToString(sig)
|
||||
|
||||
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) {
|
||||
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) {
|
||||
n := new(big.Int)
|
||||
if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
|
||||
if _, ok := n.SetString(s[2:], 16); ok {
|
||||
return n, nil
|
||||
}
|
||||
return nil, fmt.Errorf("cannot parse hex big.Int from %q", s)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (c *BlockRunBaseClient) BuildRequest(url string, jsonData []byte) (*http.Request, error) {
|
||||
return X402BuildRequest(url, jsonData)
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package payment
|
||||
|
||||
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"
|
||||
|
||||
"nofx/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
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)
|
||||
)
|
||||
|
||||
func init() {
|
||||
mcp.RegisterProvider(mcp.ProviderBlockRunSol, func(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
return NewBlockRunSolClientWithOptions(opts...)
|
||||
})
|
||||
}
|
||||
|
||||
// BlockRunSolClient implements AIClient using BlockRun's Solana x402 v2 payment protocol.
|
||||
type BlockRunSolClient struct {
|
||||
*mcp.Client
|
||||
keypair solana.PrivateKey
|
||||
}
|
||||
|
||||
func (c *BlockRunSolClient) BaseClient() *mcp.Client { return c.Client }
|
||||
|
||||
// NewBlockRunSolClient creates a BlockRun Solana wallet client (backward compatible).
|
||||
func NewBlockRunSolClient() mcp.AIClient {
|
||||
return NewBlockRunSolClientWithOptions()
|
||||
}
|
||||
|
||||
// NewBlockRunSolClientWithOptions creates a BlockRun Solana wallet client.
|
||||
func NewBlockRunSolClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
baseOpts := []mcp.ClientOption{
|
||||
mcp.WithProvider(mcp.ProviderBlockRunSol),
|
||||
mcp.WithModel(DefaultBlockRunModel),
|
||||
mcp.WithBaseURL(DefaultBlockRunSolURL),
|
||||
}
|
||||
allOpts := append(baseOpts, opts...)
|
||||
baseClient := mcp.NewClient(allOpts...).(*mcp.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).
|
||||
func (c *BlockRunSolClient) SetAPIKey(apiKey string, customURL string, customModel string) {
|
||||
kp, err := solana.PrivateKeyFromBase58(strings.TrimSpace(apiKey))
|
||||
if err != nil {
|
||||
c.Log.Warnf("⚠️ [MCP] BlockRun Sol: failed to parse private key: %v", err)
|
||||
return
|
||||
}
|
||||
c.keypair = kp
|
||||
c.APIKey = apiKey
|
||||
c.Log.Infof("🔧 [MCP] BlockRun Sol wallet: %s", kp.PublicKey().String())
|
||||
|
||||
if customModel != "" {
|
||||
c.Model = customModel
|
||||
c.Log.Infof("🔧 [MCP] BlockRun Sol model: %s", customModel)
|
||||
} else {
|
||||
c.Log.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 *mcp.Request) (*mcp.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.
|
||||
func (c *BlockRunSolClient) buildSolanaTransferTx(recipient, feePayer, amountStr string) (string, error) {
|
||||
ownerPubkey := c.keypair.PublicKey()
|
||||
|
||||
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)
|
||||
|
||||
var amountU64 uint64
|
||||
if _, err := fmt.Sscanf(amountStr, "%d", &amountU64); err != nil {
|
||||
return "", fmt.Errorf("invalid amount %q: %w", amountStr, err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
_, 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"nofx/mcp"
|
||||
"nofx/mcp/provider"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultClaw402URL = "https://claw402.ai"
|
||||
DefaultClaw402Model = "deepseek"
|
||||
)
|
||||
|
||||
// claw402ModelEndpoints maps user-friendly model names to claw402 API paths.
|
||||
var claw402ModelEndpoints = map[string]string{
|
||||
// OpenAI
|
||||
"gpt-5.4": "/api/v1/ai/openai/chat/5.4",
|
||||
"gpt-5.4-pro": "/api/v1/ai/openai/chat/5.4-pro",
|
||||
"gpt-5.3": "/api/v1/ai/openai/chat/5.3",
|
||||
"gpt-5-mini": "/api/v1/ai/openai/chat/5-mini",
|
||||
// Anthropic
|
||||
"claude-opus": "/api/v1/ai/anthropic/messages/opus",
|
||||
// DeepSeek
|
||||
"deepseek": "/api/v1/ai/deepseek/chat",
|
||||
"deepseek-reasoner": "/api/v1/ai/deepseek/chat/reasoner",
|
||||
// Qwen
|
||||
"qwen-max": "/api/v1/ai/qwen/chat/max",
|
||||
"qwen-plus": "/api/v1/ai/qwen/chat/plus",
|
||||
"qwen-turbo": "/api/v1/ai/qwen/chat/turbo",
|
||||
"qwen-flash": "/api/v1/ai/qwen/chat/flash",
|
||||
// Grok
|
||||
"grok-4.1": "/api/v1/ai/grok/chat/4.1",
|
||||
// Gemini
|
||||
"gemini-3.1-pro": "/api/v1/ai/gemini/chat/3.1-pro",
|
||||
// Kimi
|
||||
"kimi-k2.5": "/api/v1/ai/kimi/chat/k2.5",
|
||||
}
|
||||
|
||||
func init() {
|
||||
mcp.RegisterProvider(mcp.ProviderClaw402, func(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
return NewClaw402ClientWithOptions(opts...)
|
||||
})
|
||||
}
|
||||
|
||||
// Claw402Client implements AIClient using claw402.ai's x402 v2 USDC payment gateway.
|
||||
// When the selected model routes to an Anthropic endpoint, it automatically uses
|
||||
// the Anthropic wire format for requests and responses (via an internal ClaudeClient).
|
||||
type Claw402Client struct {
|
||||
*mcp.Client
|
||||
privateKey *ecdsa.PrivateKey
|
||||
claudeProxy *provider.ClaudeClient // non-nil when endpoint is /anthropic/
|
||||
}
|
||||
|
||||
func (c *Claw402Client) BaseClient() *mcp.Client { return c.Client }
|
||||
|
||||
// NewClaw402Client creates a claw402 client (backward compatible).
|
||||
func NewClaw402Client() mcp.AIClient {
|
||||
return NewClaw402ClientWithOptions()
|
||||
}
|
||||
|
||||
// NewClaw402ClientWithOptions creates a claw402 client with options.
|
||||
func NewClaw402ClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
baseOpts := []mcp.ClientOption{
|
||||
mcp.WithProvider(mcp.ProviderClaw402),
|
||||
mcp.WithModel(DefaultClaw402Model),
|
||||
mcp.WithBaseURL(DefaultClaw402URL),
|
||||
}
|
||||
allOpts := append(baseOpts, opts...)
|
||||
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
|
||||
baseClient.UseFullURL = true
|
||||
baseClient.BaseURL = DefaultClaw402URL + claw402ModelEndpoints[DefaultClaw402Model]
|
||||
|
||||
c := &Claw402Client{Client: baseClient}
|
||||
baseClient.Hooks = c
|
||||
return c
|
||||
}
|
||||
|
||||
// SetAPIKey stores the EVM private key and selects the model endpoint.
|
||||
func (c *Claw402Client) SetAPIKey(apiKey string, _ string, customModel string) {
|
||||
hexKey := strings.TrimPrefix(apiKey, "0x")
|
||||
privKey, err := crypto.HexToECDSA(hexKey)
|
||||
if err != nil {
|
||||
c.Log.Warnf("⚠️ [MCP] Claw402: invalid private key: %v", err)
|
||||
} else {
|
||||
c.privateKey = privKey
|
||||
c.APIKey = apiKey
|
||||
addr := crypto.PubkeyToAddress(privKey.PublicKey).Hex()
|
||||
c.Log.Infof("🔧 [MCP] Claw402 wallet: %s", addr)
|
||||
}
|
||||
if customModel != "" {
|
||||
c.Model = customModel
|
||||
}
|
||||
endpoint := c.resolveEndpoint()
|
||||
c.BaseURL = DefaultClaw402URL + endpoint
|
||||
|
||||
// Anthropic endpoints need different wire format (Messages API)
|
||||
if strings.Contains(endpoint, "/anthropic/") {
|
||||
c.claudeProxy = &provider.ClaudeClient{Client: c.Client}
|
||||
c.Log.Infof("🔧 [MCP] Claw402 model: %s → %s (Anthropic format)", c.Model, endpoint)
|
||||
} else {
|
||||
c.claudeProxy = nil
|
||||
c.Log.Infof("🔧 [MCP] Claw402 model: %s → %s", c.Model, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
// resolveEndpoint returns the API path for the configured model.
|
||||
func (c *Claw402Client) resolveEndpoint() string {
|
||||
if ep, ok := claw402ModelEndpoints[c.Model]; ok {
|
||||
return ep
|
||||
}
|
||||
// Allow raw path override (e.g. "/api/v1/ai/openai/chat/5.4")
|
||||
if strings.HasPrefix(c.Model, "/api/") {
|
||||
return c.Model
|
||||
}
|
||||
return claw402ModelEndpoints[DefaultClaw402Model]
|
||||
}
|
||||
|
||||
func (c *Claw402Client) SetAuthHeader(h http.Header) { X402SetAuthHeader(h) }
|
||||
|
||||
func (c *Claw402Client) Call(systemPrompt, userPrompt string) (string, error) {
|
||||
return X402Call(c.Client, c.signPayment, "Claw402", systemPrompt, userPrompt)
|
||||
}
|
||||
|
||||
func (c *Claw402Client) CallWithRequestFull(req *mcp.Request) (*mcp.LLMResponse, error) {
|
||||
return X402CallFull(c.Client, c.signPayment, "Claw402", req)
|
||||
}
|
||||
|
||||
// signPayment signs x402 v2 EIP-712 payment (same Base chain + USDC as BlockRunBase).
|
||||
func (c *Claw402Client) signPayment(paymentHeaderB64 string) (string, error) {
|
||||
return SignBasePaymentHeader(c.privateKey, paymentHeaderB64, "Claw402")
|
||||
}
|
||||
|
||||
// ── Format overrides for Anthropic endpoints ─────────────────────────────────
|
||||
|
||||
func (c *Claw402Client) BuildMCPRequestBody(systemPrompt, userPrompt string) map[string]any {
|
||||
if c.claudeProxy != nil {
|
||||
return c.claudeProxy.BuildMCPRequestBody(systemPrompt, userPrompt)
|
||||
}
|
||||
return c.Client.BuildMCPRequestBody(systemPrompt, userPrompt)
|
||||
}
|
||||
|
||||
func (c *Claw402Client) BuildRequestBodyFromRequest(req *mcp.Request) map[string]any {
|
||||
if c.claudeProxy != nil {
|
||||
return c.claudeProxy.BuildRequestBodyFromRequest(req)
|
||||
}
|
||||
return c.Client.BuildRequestBodyFromRequest(req)
|
||||
}
|
||||
|
||||
func (c *Claw402Client) ParseMCPResponse(body []byte) (string, error) {
|
||||
if c.claudeProxy != nil {
|
||||
return c.claudeProxy.ParseMCPResponse(body)
|
||||
}
|
||||
return c.Client.ParseMCPResponse(body)
|
||||
}
|
||||
|
||||
func (c *Claw402Client) ParseMCPResponseFull(body []byte) (*mcp.LLMResponse, error) {
|
||||
if c.claudeProxy != nil {
|
||||
return c.claudeProxy.ParseMCPResponseFull(body)
|
||||
}
|
||||
return c.Client.ParseMCPResponseFull(body)
|
||||
}
|
||||
|
||||
// BuildUrl returns the full claw402 endpoint URL.
|
||||
func (c *Claw402Client) BuildUrl() string {
|
||||
return c.BaseURL
|
||||
}
|
||||
|
||||
func (c *Claw402Client) BuildRequest(url string, jsonData []byte) (*http.Request, error) {
|
||||
return X402BuildRequest(url, jsonData)
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"nofx/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
// X402MaxPaymentRetries is the number of retries for 5xx errors on the
|
||||
// payment-signed request. The same payment signature is reused (no double-charge).
|
||||
X402MaxPaymentRetries = 3
|
||||
|
||||
// X402RetryBaseWait is the base wait between payment retry attempts.
|
||||
X402RetryBaseWait = 3 * time.Second
|
||||
)
|
||||
|
||||
// ── Shared x402 types ────────────────────────────────────────────────────────
|
||||
|
||||
// X402v2PaymentRequired is the structure of the Payment-Required header (x402 v2).
|
||||
type X402v2PaymentRequired struct {
|
||||
X402Version int `json:"x402Version"`
|
||||
Accepts []X402AcceptOption `json:"accepts"`
|
||||
Resource *X402Resource `json:"resource"`
|
||||
}
|
||||
|
||||
// X402AcceptOption is a payment option from the x402 v2 header.
|
||||
type X402AcceptOption 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"`
|
||||
}
|
||||
|
||||
// X402Resource describes the resource being paid for.
|
||||
type X402Resource struct {
|
||||
URL string `json:"url"`
|
||||
Description string `json:"description"`
|
||||
MimeType string `json:"mimeType"`
|
||||
}
|
||||
|
||||
// X402SignFunc is a callback that signs an x402 payment header and returns the
|
||||
// base64-encoded payment signature.
|
||||
type X402SignFunc func(paymentHeaderB64 string) (string, error)
|
||||
|
||||
// ── Shared x402 helpers ──────────────────────────────────────────────────────
|
||||
|
||||
// X402DecodeHeader decodes a base64-encoded x402 Payment-Required header,
|
||||
// trying RawStdEncoding first then StdEncoding as fallback.
|
||||
func X402DecodeHeader(b64 string) ([]byte, error) {
|
||||
decoded, err := base64.RawStdEncoding.DecodeString(b64)
|
||||
if err != nil {
|
||||
decoded, err = base64.StdEncoding.DecodeString(b64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to base64-decode payment header: %w", err)
|
||||
}
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
// SignBasePaymentHeader decodes a base64 x402 header, parses it, and signs with
|
||||
// EIP-712 (USDC TransferWithAuthorization). Shared by BlockRunBase and Claw402.
|
||||
func SignBasePaymentHeader(privateKey *ecdsa.PrivateKey, paymentHeaderB64 string, providerName string) (string, error) {
|
||||
if privateKey == nil {
|
||||
return "", fmt.Errorf("no private key set for %s wallet", providerName)
|
||||
}
|
||||
|
||||
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 payment header: %w", err)
|
||||
}
|
||||
if len(req.Accepts) == 0 {
|
||||
return "", fmt.Errorf("no payment options in x402 response")
|
||||
}
|
||||
|
||||
senderAddr := crypto.PubkeyToAddress(privateKey.PublicKey).Hex()
|
||||
return SignX402Payment(privateKey, senderAddr, req.Accepts[0], req.Resource)
|
||||
}
|
||||
|
||||
// DoX402Request executes an HTTP request and handles the x402 v2 payment flow.
|
||||
func DoX402Request(
|
||||
httpClient *http.Client,
|
||||
buildReqFn func() (*http.Request, error),
|
||||
signFn X402SignFunc,
|
||||
providerTag string,
|
||||
logger mcp.Logger,
|
||||
) ([]byte, error) {
|
||||
req, err := buildReqFn()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusPaymentRequired {
|
||||
paymentHeader := resp.Header.Get("Payment-Required")
|
||||
if paymentHeader == "" {
|
||||
paymentHeader = resp.Header.Get("X-Payment-Required")
|
||||
}
|
||||
if paymentHeader == "" {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("received 402 but no Payment-Required header found. Body: %s", string(body))
|
||||
}
|
||||
|
||||
// Drain 402 body to allow HTTP connection reuse.
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
|
||||
paymentSig, err := signFn(paymentHeader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign x402 payment: %w", err)
|
||||
}
|
||||
|
||||
// Retry loop for 5xx errors on the payment-signed request.
|
||||
var lastBody []byte
|
||||
var lastStatus int
|
||||
for attempt := 1; attempt <= X402MaxPaymentRetries; attempt++ {
|
||||
req2, err := buildReqFn()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build retry request: %w", err)
|
||||
}
|
||||
req2.Header.Set("X-Payment", paymentSig)
|
||||
req2.Header.Set("Payment-Signature", paymentSig)
|
||||
|
||||
resp2, err := httpClient.Do(req2)
|
||||
if err != nil {
|
||||
if attempt < X402MaxPaymentRetries {
|
||||
wait := X402RetryBaseWait * time.Duration(attempt)
|
||||
logger.Warnf("⚠️ [%s] Payment request failed: %v, retrying in %v (%d/%d)...",
|
||||
providerTag, err, wait, attempt+1, X402MaxPaymentRetries)
|
||||
time.Sleep(wait)
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("failed to send payment retry: %w", err)
|
||||
}
|
||||
|
||||
body2, readErr := io.ReadAll(resp2.Body)
|
||||
resp2.Body.Close()
|
||||
if readErr != nil {
|
||||
return nil, fmt.Errorf("failed to read payment retry response: %w", readErr)
|
||||
}
|
||||
|
||||
if resp2.StatusCode == http.StatusOK {
|
||||
if txHash := resp2.Header.Get("Payment-Response"); txHash != "" {
|
||||
logger.Infof("💰 [%s] Payment tx: %s", providerTag, txHash)
|
||||
}
|
||||
if attempt > 1 {
|
||||
logger.Infof("✅ [%s] Payment retry succeeded on attempt %d", providerTag, attempt)
|
||||
}
|
||||
return body2, nil
|
||||
}
|
||||
|
||||
lastBody = body2
|
||||
lastStatus = resp2.StatusCode
|
||||
|
||||
// Retry on 5xx server errors
|
||||
if resp2.StatusCode >= 500 && attempt < X402MaxPaymentRetries {
|
||||
wait := X402RetryBaseWait * time.Duration(attempt)
|
||||
logger.Warnf("⚠️ [%s] Server error (status %d), retrying in %v (%d/%d)...",
|
||||
providerTag, resp2.StatusCode, wait, attempt+1, X402MaxPaymentRetries)
|
||||
time.Sleep(wait)
|
||||
continue
|
||||
}
|
||||
|
||||
// Non-5xx error or final attempt — fail
|
||||
break
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%s payment retry failed (status %d): %s", providerTag, lastStatus, string(lastBody))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s API error (status %d): %s", providerTag, resp.StatusCode, string(body))
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// X402BuildRequest creates a POST request with Content-Type but no auth header.
|
||||
func X402BuildRequest(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
|
||||
}
|
||||
|
||||
// X402SetAuthHeader is a no-op — x402 providers authenticate via payment signing.
|
||||
func X402SetAuthHeader(_ http.Header) {}
|
||||
|
||||
// X402Call handles the x402 payment flow for the simple CallWithMessages path.
|
||||
func X402Call(c *mcp.Client, signFn X402SignFunc, tag string, systemPrompt, userPrompt string) (string, error) {
|
||||
c.Log.Infof("📡 [%s] Request AI Server: %s", tag, c.BaseURL)
|
||||
|
||||
requestBody := c.Hooks.BuildMCPRequestBody(systemPrompt, userPrompt)
|
||||
jsonData, err := c.Hooks.MarshalRequestBody(requestBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
body, err := DoX402Request(c.HTTPClient, func() (*http.Request, error) {
|
||||
return c.Hooks.BuildRequest(c.Hooks.BuildUrl(), jsonData)
|
||||
}, signFn, tag, c.Log)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return c.Hooks.ParseMCPResponse(body)
|
||||
}
|
||||
|
||||
// X402CallFull handles the x402 payment flow for the advanced Request path.
|
||||
func X402CallFull(c *mcp.Client, signFn X402SignFunc, tag string, req *mcp.Request) (*mcp.LLMResponse, error) {
|
||||
if c.APIKey == "" {
|
||||
return nil, fmt.Errorf("AI API key not set, please call SetAPIKey first")
|
||||
}
|
||||
if req.Model == "" {
|
||||
req.Model = c.Model
|
||||
}
|
||||
|
||||
c.Log.Infof("📡 [%s] Request AI (full): %s", tag, c.BaseURL)
|
||||
|
||||
requestBody := c.Hooks.BuildRequestBodyFromRequest(req)
|
||||
jsonData, err := c.Hooks.MarshalRequestBody(requestBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := DoX402Request(c.HTTPClient, func() (*http.Request, error) {
|
||||
return c.Hooks.BuildRequest(c.Hooks.BuildUrl(), jsonData)
|
||||
}, signFn, tag, c.Log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Hooks.ParseMCPResponseFull(body)
|
||||
}
|
||||
Reference in New Issue
Block a user