Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0ab9123
feat(gateway): implement supported chain IDs slice configuration mapp…
Stewartsson Jun 20, 2026
ab377b5
fix(verifier): validate incoming EIP-712 payload against supported ch…
Stewartsson Jun 20, 2026
ad60319
feat(web): support dynamic advertised gateway chain options in wallet…
Stewartsson Jun 20, 2026
89bfe25
fix(web): correct EIP-712 domain name configuration and verify contra…
Stewartsson Jun 20, 2026
293ec41
refactor(web): purge unused getDomainSeparator helper to satisfy stri…
Stewartsson Jun 20, 2026
9eabc47
fix(verifier): dynamically append environment expected chain ID into …
Stewartsson Jun 20, 2026
29db11a
feat(gateway): wire supported chain IDs slice into payment context re…
Stewartsson Jun 20, 2026
1d234dc
feat(web): add Ethereum and Optimism Sepolia network metadata to wall…
Stewartsson Jun 20, 2026
f222f9f
test(verifier): restore regression test coverage suites for multi-cha…
Stewartsson Jun 20, 2026
67e1af4
fix(verifier): dynamic check both EXPECTED_CHAIN_ID and CHAIN_ID fall…
Stewartsson Jun 20, 2026
dcce2c6
doc(gateway): register supportedChainIds inside PaymentDetails schema…
Stewartsson Jun 20, 2026
05797e2
fix(verifier): align timestamp error responses and status codes with …
Stewartsson Jun 20, 2026
954f7d9
fix(verifier): harden signature parsing checks and align timestamp er…
Stewartsson Jun 20, 2026
db96cd3
fix(gateway): cast payment.ChainID to int64 inside receipt generation…
Stewartsson Jun 22, 2026
3a55052
fix(verifier): handle malformed input data parameters safely to preve…
Stewartsson Jun 22, 2026
073af1f
Refactor error handling and improve server binding
Stewartsson Jun 22, 2026
acb1f65
Fix nonce handling and error codes in main.rs
Stewartsson Jun 22, 2026
f79b8d2
Enhance JSON rejection handling in verify_signature
Stewartsson Jun 22, 2026
54be7ba
Refactor code for improved readability and conciseness
Stewartsson Jun 22, 2026
08211cf
Add supportedChainIds to payment gateway schema
Stewartsson Jun 22, 2026
7c51359
Enhance config with dynamic chain ID registration
Stewartsson Jun 22, 2026
b2108f6
Add supportedChainIds to PaymentContext and payment
Stewartsson Jun 22, 2026
357706a
Merge branch 'main' into 160-multi-chain-payment-support
Stewartsson Jun 23, 2026
414035b
refactor(gateway): remove duplicate getEnvAsInt and getCacheEnabled h…
Stewartsson Jun 30, 2026
d9726ba
fix(verifier): separate Axum framework import paths for DefaultBodyLi…
Stewartsson Jun 30, 2026
79e9f7c
fix(verifier): isolate signed_req helper function scope block before …
Stewartsson Jun 30, 2026
9a0bfb9
doc(api): revert supportedChainIds required constraint to match Go ru…
Stewartsson Jun 30, 2026
632dae1
fix(verifier): return 503 SERVICE_UNAVAILABLE with nonce_store_unavai…
Stewartsson Jun 30, 2026
9c38d69
refactor(gateway): derive SupportedChainIds dynamically via getter fu…
Stewartsson Jun 30, 2026
0c73668
fix(verifier): normalize malformed signature errors as invalid_signat…
Stewartsson Jun 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions gateway/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"log"
"net/url"
"os"
"strconv"
"strings"
"time"
)
Expand All @@ -16,6 +17,33 @@ const (
receiptStoreModeMemory = "memory"
)

// GetSupportedChainIds defines the network IDs allowed for payment requests.
// 84532: Base Sepolia, 11155111: Ethereum Sepolia, 11155240: Optimism Sepolia.
func GetSupportedChainIds() []int64 {
chainIDs := []int64{84532, 11155111, 11155240}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the same Optimism Sepolia chain ID

This gateway allowlist advertises 11155240 as Optimism Sepolia, but the verifier's SUPPORTED_CHAINS and the web wallet metadata use 11155420. Once the supported-chain helper is wired into challenges or receipts, clients can be told to use a chain the verifier/web do not support, so Optimism Sepolia payments will fail unless this ID is made consistent.

Useful? React with 👍 / 👎.

envKeys := []string{"CHAIN_ID", "EXPECTED_CHAIN_ID"}
for _, key := range envKeys {
if val := os.Getenv(key); val != "" {
if id, err := strconv.ParseInt(val, 10, 64); err == nil {
found := false
for _, existing := range chainIDs {
if existing == id {
found = true
break
}
}
if !found {
chainIDs = append(chainIDs, id)
log.Printf("Dynamically registered chain ID %d from env var %s", id, key)
}
} else {
log.Printf("Warning: failed to parse %s=%s as int64", key, val)
}
}
}
return chainIDs
}

func getAllowedOrigins() []string {
raw := strings.TrimSpace(os.Getenv("ALLOWED_ORIGINS"))
if raw == "" {
Expand Down Expand Up @@ -87,9 +115,10 @@ func getPositiveTimeout(envKey string, defaultSeconds int) time.Duration {
}

// Timeout helpers (configurable via env vars)
func getRequestTimeout() time.Duration { return getPositiveTimeout("REQUEST_TIMEOUT_SECONDS", 60) }
func getAITimeout() time.Duration { return getPositiveTimeout("AI_REQUEST_TIMEOUT_SECONDS", 30) }
func getVerifierTimeout() time.Duration { return getPositiveTimeout("VERIFIER_TIMEOUT_SECONDS", 2) }
func getRequestTimeout() time.Duration { return getPositiveTimeout("REQUEST_TIMEOUT_SECONDS", 60) }
func getAITimeout() time.Duration { return getPositiveTimeout("AI_REQUEST_TIMEOUT_SECONDS", 30) }
func getVerifierTimeout() time.Duration { return getPositiveTimeout("VERIFIER_TIMEOUT_SECONDS", 2) }
func getHealthCheckTimeout() time.Duration {
return getPositiveTimeout("HEALTH_CHECK_TIMEOUT_SECONDS", 2)
}

16 changes: 15 additions & 1 deletion gateway/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,12 @@ components:
chainId:
type: integer
example: 84532
supportedChainIds:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit the documented chain list in 402 challenges

This adds supportedChainIds to the public payment-context schema, but gateway/main.go still defines and creates PaymentContext with only recipient, token, amount, nonce, chainId, and timestamp (repo-wide search shows no writer for this field). Clients relying on the new multi-chain metadata will never receive it in the 402 response unless the gateway struct and createPaymentContext() are updated too.

Useful? React with 👍 / 👎.

type: array
items:
type: integer
format: int64
description: List of supported blockchain network chain IDs for this payment challenge context.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
timestamp:
type: integer
format: uint64
Expand Down Expand Up @@ -456,7 +462,7 @@ components:
format: date-time
payment:
type: object
required: [payer, recipient, amount, token, chainId, nonce]
required: [payer, recipient, amount, token, chainId, supportedChainIds, nonce]
properties:
payer:
type: string
Expand All @@ -473,6 +479,12 @@ components:
chainId:
type: integer
example: 84532
supportedChainIds:
type: array
items:
type: integer
format: int64
description: List of supported blockchain network chain IDs for this payment challenge context.
nonce:
type: string
example: "550e8400-e29b-41d4-a716-446655440000"
Expand All @@ -489,3 +501,5 @@ components:
response_hash:
type: string
example: sha256:...


35 changes: 12 additions & 23 deletions gateway/receipt.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ type Receipt struct {

// PaymentDetails contains payment-related information
type PaymentDetails struct {
Payer string `json:"payer"`
Recipient string `json:"recipient"`
Amount string `json:"amount"`
Token string `json:"token"`
ChainID int `json:"chainId"`
Nonce string `json:"nonce"`
Payer string `json:"payer"`
Recipient string `json:"recipient"`
Amount string `json:"amount"`
Token string `json:"token"`
ChainID int64 `json:"chainId"`
ChainIDs []int64 `json:"supportedChainIds"` // Advertises multi-chain capability
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Nonce string `json:"nonce"`
}

// ServiceDetails contains service-related information
Expand All @@ -53,7 +54,8 @@ type ReceiptStore interface {
Close() error
}

// GenerateReceipt creates a new receipt for a successful payment
// GenerateReceipt creates a new receipt for a successful payment,
// wiring in the global SupportedChainIDs registry.
func GenerateReceipt(payment PaymentContext, payer string, endpoint string, reqBody, respBody []byte) (*SignedReceipt, error) {
receiptID, err := generateReceiptID()
if err != nil {
Expand All @@ -69,7 +71,8 @@ func GenerateReceipt(payment PaymentContext, payer string, endpoint string, reqB
Recipient: payment.Recipient,
Amount: payment.Amount,
Token: payment.Token,
ChainID: payment.ChainID,
ChainID: int64(payment.ChainID), // Fixed: Explicit int64 type cast resolves compiler type mismatch
ChainIDs: SupportedChainIDs, // Inject multi-chain registry

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reference the defined supported-chain helper

This field now references SupportedChainIDs, but the change only added GetSupportedChainIds() and there is no SupportedChainIDs symbol anywhere in gateway/; cd gateway && go test -c fails with undefined: SupportedChainIDs, so the gateway cannot build until this uses a defined value.

Useful? React with 👍 / 👎.

Nonce: payment.Nonce,
},
Service: ServiceDetails{
Expand All @@ -83,9 +86,7 @@ func GenerateReceipt(payment PaymentContext, payer string, endpoint string, reqB
}

// generateReceiptID generates a unique receipt ID with "rcpt_" prefix
// Returns error if random generation fails to prevent predictable IDs
func generateReceiptID() (string, error) {
// Generate 6 random bytes (12 hex characters)
bytes := make([]byte, 6)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate random receipt ID: %w", err)
Expand All @@ -96,42 +97,30 @@ func generateReceiptID() (string, error) {
// hashData computes SHA-256 hash of data and returns hex-encoded string
func hashData(data []byte) string {
if len(data) == 0 {
return "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // Empty hash
return "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
hash := sha256.Sum256(data)
return "sha256:" + hex.EncodeToString(hash[:])
}

// signReceipt signs a receipt using the server's private key
// NOTE: Go's json.Marshal is deterministic for structs - fields are always
// serialized in the order they are defined in the struct, ensuring consistent output.
// This guarantees consistent signatures across multiple marshaling operations.
func signReceipt(receipt Receipt) (*SignedReceipt, error) {
// Get server's private key
privateKey, err := getServerPrivateKey()
if err != nil {
return nil, fmt.Errorf("failed to load server private key: %w", err)
}

// Serialize receipt deterministically
// json.Marshal outputs struct fields in their declaration order
receiptBytes, err := json.Marshal(receipt)
if err != nil {
return nil, fmt.Errorf("failed to marshal receipt: %w", err)
}

// Hash the receipt using Keccak256 (Ethereum-compatible)
hash := crypto.Keccak256Hash(receiptBytes)

// Sign the hash using ECDSA
// SECURITY: crypto.Sign uses constant-time operations from go-ethereum's secp256k1 implementation
// This prevents timing attacks that could leak private key information
signature, err := crypto.Sign(hash.Bytes(), privateKey)
if err != nil {
return nil, fmt.Errorf("failed to sign receipt: %w", err)
}

// Get server's public key for verification
publicKey := privateKey.Public().(*ecdsa.PublicKey)
publicKeyBytes := crypto.FromECDSAPub(publicKey)

Expand Down
Loading
Loading