Skip to content

Commit ef4555b

Browse files
committed
✨ feat: add Casper payment simulation and client integration
Implement Casper payment simulation in settler demo mode, introducing a new client for interacting with CSPR.cloud x402 Facilitator. - Add support for Casper network in blockchain package - Enhance header and middleware to handle new payment scheme - Update go.work to include settler command - Refactor main.go to incorporate new demo functionality
1 parent 4549f8e commit ef4555b

6 files changed

Lines changed: 258 additions & 20 deletions

File tree

cmd/settler/main.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,28 @@ func runConfig(args []string) {
9393
}
9494

9595
func runDemo(args []string) {
96+
fs := flag.NewFlagSet("demo", flag.ExitOnError)
97+
useCasper := fs.Bool("casper", false, "Simulate Casper agent payment verification loop")
98+
fs.Parse(args)
99+
100+
if *useCasper {
101+
fmt.Println("🎬 Starting SettlerEngine Agentic Demo (Casper-Native Mode)...")
102+
fmt.Println("🤖 [1/3] Generating Casper Ed25519 mock signer parameters...")
103+
mockSig := "dGVzdC1zaWduYXR1cmUtYmFzZTY0" // base64 payload "test-signature-base64"
104+
fmt.Printf("✅ Mock Signer Signature: %s\n", mockSig)
105+
106+
fmt.Println("💰 [2/3] Simulating Casper payment challenge parsing...")
107+
fmt.Println("Challenge schema matched: Accepts -> Scheme: casper-native, Network: casper-testnet")
108+
109+
fmt.Println("⚓ [3/3] Triggering Casper Facilitator Verification API...")
110+
// Print successful trace mimicking off-chain validation loop
111+
fmt.Println("✅ Casper Facilitator: Signature and Nonce validated successfully via off-chain credentials.")
112+
fmt.Println("✅ Casper Facilitator: Transferred 1,000,000,000 motes to recipient.")
113+
fmt.Println("🚀 SUCCESS! Casper transaction broadcasted.")
114+
fmt.Println("🔗 Transaction Hash: 0x9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b")
115+
return
116+
}
117+
96118
fmt.Println("🎬 Starting SettlerEngine Agentic Demo...")
97119

98120
cfg, _ := config.LoadConfig()
@@ -221,8 +243,8 @@ func runProxy(args []string) {
221243
target := fs.String("target", "http://localhost:8081", "Target URL to proxy to")
222244
listen := fs.String("listen", ":8080", "Listen address")
223245
recipient := fs.String("recipient", "0x1234567890AbcdEF1234567890aBcdef12345678", "Merchant recipient address")
224-
chainID := fs.Int64("chain-id", 84532, "Chain ID (default Base Sepolia)")
225-
asset := fs.String("asset", "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "Asset address (USDC)")
246+
chainIDStr := fs.String("chain-id", "84532", "Chain ID (default Base Sepolia or 'casper-testnet')")
247+
asset := fs.String("asset", "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "Asset address (USDC) or 'CSPR'")
226248
amount := fs.String("amount", "1000000", "Amount in atomic units")
227249
fs.Parse(args)
228250

@@ -248,9 +270,20 @@ func runProxy(args []string) {
248270

249271
proxy := httputil.NewSingleHostReverseProxy(targetURL)
250272

273+
var parsedChainID *big.Int
274+
if *chainIDStr == "casper-testnet" {
275+
parsedChainID = big.NewInt(0)
276+
} else {
277+
id, ok := new(big.Int).SetString(*chainIDStr, 10)
278+
if !ok {
279+
id = big.NewInt(84532)
280+
}
281+
parsedChainID = id
282+
}
283+
251284
cfg := x402.Config{
252285
DomainParams: crypto.DomainParams{
253-
ChainID: big.NewInt(*chainID),
286+
ChainID: parsedChainID,
254287
VerifyingContract: common.HexToAddress("0x0000000000000000000000000000000000000000"),
255288
},
256289
NonceExpiry: 5 * time.Minute,

go.work

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ use (
66
./apps/settlerd
77
./core
88
./pkg
9+
./cmd/settler
910
)

internal/ports/blockchain.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const (
1414
NetworkMonero ChainNetwork = "XMR"
1515
NetworkSolana ChainNetwork = "SOL"
1616
NetworkTron ChainNetwork = "TRX"
17+
NetworkCasper ChainNetwork = "CSPR"
1718
)
1819

1920
type InvoicePaymentSignal struct {

pkg/crypto/casper/client.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package casper
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"time"
9+
)
10+
11+
// PaymentDetails represents the payment transaction parameters verified/settled by CSPR.cloud x402 Facilitator.
12+
type PaymentDetails struct {
13+
Recipient string `json:"recipient"` // Casper public key hex
14+
Amount string `json:"amount"` // Amount in motes (1 CSPR = 10^9 motes)
15+
Asset string `json:"asset"` // e.g. "CSPR"
16+
Nonce string `json:"nonce"` // Unique session UUID for replay protection
17+
Network string `json:"network"` // e.g. "casper-testnet"
18+
}
19+
20+
// CasperFacilitatorClient represents the client wrapper for interacting with CSPR.cloud x402 Facilitator.
21+
type CasperFacilitatorClient struct {
22+
BaseURL string
23+
APIKey string
24+
HTTPClient *http.Client
25+
}
26+
27+
// NewCasperFacilitatorClient initializes a new Casper x402 Facilitator client.
28+
func NewCasperFacilitatorClient(baseURL, apiKey string) *CasperFacilitatorClient {
29+
if baseURL == "" {
30+
baseURL = "https://x402-facilitator.cspr.cloud"
31+
}
32+
return &CasperFacilitatorClient{
33+
BaseURL: baseURL,
34+
APIKey: apiKey,
35+
HTTPClient: &http.Client{
36+
Timeout: 10 * time.Second,
37+
},
38+
}
39+
}
40+
41+
type verifyRequest struct {
42+
Signature string `json:"signature"` // base64-encoded Casper Ed25519 signature
43+
Details PaymentDetails `json:"details"`
44+
}
45+
46+
type verifyResponse struct {
47+
Valid bool `json:"valid"`
48+
Error string `json:"error,omitempty"`
49+
}
50+
51+
type settleRequest struct {
52+
Signature string `json:"signature"` // base64-encoded Casper Ed25519 signature
53+
Details PaymentDetails `json:"details"`
54+
}
55+
56+
type settleResponse struct {
57+
TxHash string `json:"txHash"`
58+
Error string `json:"error,omitempty"`
59+
}
60+
61+
// VerifyPayload sends the payment signature and details to the facilitator for verification.
62+
func (c *CasperFacilitatorClient) VerifyPayload(signature string, details PaymentDetails) (bool, error) {
63+
url := fmt.Sprintf("%s/verify", c.BaseURL)
64+
reqBody, err := json.Marshal(verifyRequest{
65+
Signature: signature,
66+
Details: details,
67+
})
68+
if err != nil {
69+
return false, fmt.Errorf("failed to marshal verify request: %w", err)
70+
}
71+
72+
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody))
73+
if err != nil {
74+
return false, fmt.Errorf("failed to create http request: %w", err)
75+
}
76+
77+
req.Header.Set("Content-Type", "application/json")
78+
if c.APIKey != "" {
79+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.APIKey))
80+
}
81+
82+
resp, err := c.HTTPClient.Do(req)
83+
if err != nil {
84+
return false, fmt.Errorf("failed to send verify request: %w", err)
85+
}
86+
defer resp.Body.Close()
87+
88+
if resp.StatusCode != http.StatusOK {
89+
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
90+
}
91+
92+
var res verifyResponse
93+
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
94+
return false, fmt.Errorf("failed to decode verify response: %w", err)
95+
}
96+
97+
if res.Error != "" {
98+
return false, fmt.Errorf("facilitator error: %s", res.Error)
99+
}
100+
101+
return res.Valid, nil
102+
}
103+
104+
// SettlePayload forwards the signature and details to the facilitator to execute the Casper payment.
105+
func (c *CasperFacilitatorClient) SettlePayload(signature string, details PaymentDetails) (string, error) {
106+
url := fmt.Sprintf("%s/settle", c.BaseURL)
107+
reqBody, err := json.Marshal(settleRequest{
108+
Signature: signature,
109+
Details: details,
110+
})
111+
if err != nil {
112+
return "", fmt.Errorf("failed to marshal settle request: %w", err)
113+
}
114+
115+
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody))
116+
if err != nil {
117+
return "", fmt.Errorf("failed to create http request: %w", err)
118+
}
119+
120+
req.Header.Set("Content-Type", "application/json")
121+
if c.APIKey != "" {
122+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.APIKey))
123+
}
124+
125+
resp, err := c.HTTPClient.Do(req)
126+
if err != nil {
127+
return "", fmt.Errorf("failed to send settle request: %w", err)
128+
}
129+
defer resp.Body.Close()
130+
131+
if resp.StatusCode != http.StatusOK {
132+
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
133+
}
134+
135+
var res settleResponse
136+
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
137+
return "", fmt.Errorf("failed to decode settle response: %w", err)
138+
}
139+
140+
if res.Error != "" {
141+
return "", fmt.Errorf("facilitator error: %s", res.Error)
142+
}
143+
144+
return res.TxHash, nil
145+
}

pkg/x402/header.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import (
1111
const (
1212
HeaderPayment = "X-Payment"
1313
HeaderPaymentSignature = "X-Payment-Signature"
14+
HeaderPaymentRequired = "Payment-Required"
1415
)
1516

1617
// PaymentPayload represents the data extracted from the payment header.
1718
type PaymentPayload struct {
1819
Intent crypto.IntentToPay `json:"intent"`
1920
Signature string `json:"signature"`
21+
Scheme string `json:"scheme,omitempty"`
2022
}
2123

2224
// ParseHeader extracts and decodes the payment information from a request.
@@ -30,9 +32,14 @@ func ParseHeader(r *http.Request) (*PaymentPayload, error) {
3032
return &payload, nil
3133
}
3234

33-
// Fallback to separate signature header (simplified version)
34-
// This would require the intent to be reconstructible or passed elsewhere.
35-
// For MVP, we'll focus on the self-contained JSON payload.
35+
// Fallback to separate signature header (X-Payment-Signature)
36+
if sig := r.Header.Get(HeaderPaymentSignature); sig != "" {
37+
// Attempt to reconstruct or build a minimal payload using URL query parameters or default headers.
38+
return &PaymentPayload{
39+
Signature: sig,
40+
Scheme: "casper-native",
41+
}, nil
42+
}
3643

3744
return nil, fmt.Errorf("no payment header found")
3845
}

pkg/x402/middleware.go

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/ethereum/go-ethereum/common"
1313
"github.com/nathfavour/settlerengine/internal/ports"
1414
"github.com/nathfavour/settlerengine/pkg/crypto"
15+
"github.com/nathfavour/settlerengine/pkg/crypto/casper"
1516
"github.com/nathfavour/settlerengine/pkg/storage"
1617
)
1718

@@ -24,25 +25,28 @@ const (
2425

2526
// Config defines the configuration for the x402 middleware.
2627
type Config struct {
27-
DomainParams crypto.DomainParams
28-
NonceExpiry time.Duration
29-
Recipient string
30-
Asset string
31-
Amount string
32-
PriceResolver PriceResolver
33-
DB *storage.DB
34-
Registry ports.AgentRegistry
35-
MinReputation *big.Int // Optional: block agents with score below this
28+
DomainParams crypto.DomainParams
29+
NonceExpiry time.Duration
30+
Recipient string
31+
Asset string
32+
Amount string
33+
PriceResolver PriceResolver
34+
DB *storage.DB
35+
Registry ports.AgentRegistry
36+
MinReputation *big.Int // Optional: block agents with score below this
37+
CasperFacilitatorURL string
38+
CasperFacilitatorToken string
3639
}
3740

3841
// PriceResolver dynamically determines the payment requirements for a request.
3942
type PriceResolver func(r *http.Request) (amount, asset, recipient string, err error)
4043

4144
// Middleware handles the x402 handshake.
4245
type Middleware struct {
43-
config Config
44-
nonces *NonceManager
45-
verified sync.Map // Map of signature hash to Address
46+
config Config
47+
nonces *NonceManager
48+
verified sync.Map // Map of signature hash to Address
49+
casperClient *casper.CasperFacilitatorClient
4650
}
4751

4852
func NewMiddleware(cfg Config) *Middleware {
@@ -53,8 +57,9 @@ func NewMiddleware(cfg Config) *Middleware {
5357
}
5458

5559
return &Middleware{
56-
config: cfg,
57-
nonces: NewNonceManager(),
60+
config: cfg,
61+
nonces: NewNonceManager(),
62+
casperClient: casper.NewCasperFacilitatorClient(cfg.CasperFacilitatorURL, cfg.CasperFacilitatorToken),
5863
}
5964
}
6065

@@ -76,6 +81,38 @@ func (m *Middleware) Handler(next http.Handler) http.Handler {
7681
// 1. Try to parse payment header
7782
payload, err := ParseHeader(r)
7883
if err == nil {
84+
// Check if we are running in Casper mode (either explicitly marked, or chain-id is casper-testnet)
85+
isCasper := payload.Scheme == "casper-native" ||
86+
m.config.DomainParams.ChainID != nil && m.config.DomainParams.ChainID.String() == "0" && m.config.Asset == "CSPR" ||
87+
payload.Intent.Asset == "CSPR"
88+
89+
if isCasper {
90+
// Verify via Casper Facilitator
91+
details := casper.PaymentDetails{
92+
Recipient: m.config.Recipient,
93+
Amount: m.config.Amount,
94+
Asset: "CSPR",
95+
Nonce: payload.Intent.Nonce,
96+
Network: "casper-testnet",
97+
}
98+
if details.Nonce == "" {
99+
details.Nonce = "demo-nonce"
100+
}
101+
102+
valid, err := m.casperClient.VerifyPayload(payload.Signature, details)
103+
if err == nil && valid {
104+
// Auto settle/forward payment via facilitator
105+
txHash, err := m.casperClient.SettlePayload(payload.Signature, details)
106+
if err == nil && txHash != "" {
107+
fmt.Printf("✅ Casper Facilitator: Verified and settled transaction: %s\n", txHash)
108+
// Store dummy address for signer in context for downstream handlers
109+
ctx := context.WithValue(r.Context(), SignerContextKey, common.HexToAddress("0x0"))
110+
next.ServeHTTP(w, r.WithContext(ctx))
111+
return
112+
}
113+
}
114+
}
115+
79116
// 2. Check Cache & DB (Idempotency)
80117
if addr, ok := m.verified.Load(payload.Signature); ok {
81118
ctx := context.WithValue(r.Context(), SignerContextKey, addr.(common.Address))
@@ -145,6 +182,20 @@ func (m *Middleware) Handler(next http.Handler) http.Handler {
145182

146183
nonce, _ := m.nonces.Generate(m.config.NonceExpiry)
147184

185+
// Set the Payment-Required header formatted for Casper native agents
186+
casperChallenge := map[string]interface{}{
187+
"accepts": []map[string]interface{}{
188+
{
189+
"scheme": "casper-native",
190+
"networkId": "casper-testnet",
191+
"amount": amount,
192+
"recipient": recipient,
193+
},
194+
},
195+
}
196+
challengeBytes, _ := json.Marshal(casperChallenge)
197+
w.Header().Set(HeaderPaymentRequired, string(challengeBytes))
198+
148199
resp := ChallengeResponse{
149200
Status: http.StatusPaymentRequired,
150201
Title: "Payment Required",

0 commit comments

Comments
 (0)