main
go 135 lines 3.59 KB
Raw
1 package x402
2
3 import (
4 "context"
5 _ "embed"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "net/http"
10 "strings"
11
12 "github.com/gosuda/portal-tunnel/v2/types"
13 "github.com/gosuda/portal-tunnel/v2/utils"
14 )
15
16 //go:embed client.js
17 var clientJS []byte
18
19 // ServeClientJS serves the shared browser x402 wallet/payment client.
20 func ServeClientJS(w http.ResponseWriter, r *http.Request) {
21 if r.Method != http.MethodHead && !utils.RequireMethod(w, r, http.MethodGet) {
22 return
23 }
24 w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
25 w.Header().Set("Cache-Control", "no-store")
26 if r.Method == http.MethodHead {
27 return
28 }
29 _, _ = w.Write(clientJS)
30 }
31
32 // USDCPaymentHandler serves both the wallet prepare endpoint and one protected resource.
33 type USDCPaymentHandler struct {
34 payment *Payment
35 protectedPath string
36 method string
37 handler types.X402PaymentHandlerFunc
38 }
39
40 // NewUSDCPaymentHandler returns a complete HTTP handler for one Sui USDC x402 payment flow.
41 func NewUSDCPaymentHandler(payment types.X402Payment, protectedPath, protectedMethod string, handler types.X402PaymentHandlerFunc) (*USDCPaymentHandler, error) {
42 if handler == nil {
43 return nil, errors.New("USDC payment handler is required")
44 }
45 paid, err := NewUSDCPayment(payment)
46 if err != nil {
47 return nil, err
48 }
49 payment = paid.payment
50
51 protectedPath = strings.TrimSpace(protectedPath)
52 if protectedPath == "" {
53 protectedPath = payment.ResourcePath
54 }
55 if protectedPath == "" {
56 return nil, errors.New("USDC payment protected path is required")
57 }
58 if !strings.HasPrefix(protectedPath, "/") {
59 return nil, fmt.Errorf("USDC payment protected path %q must start with /", protectedPath)
60 }
61 protectedPath = utils.NormalizeURLPath(protectedPath)
62 if protectedPath == types.X402PreparePath {
63 return nil, fmt.Errorf("USDC payment protected path cannot be %s", types.X402PreparePath)
64 }
65
66 paid.payment.ResourcePath = protectedPath
67 return &USDCPaymentHandler{
68 payment: paid,
69 protectedPath: protectedPath,
70 method: strings.TrimSpace(protectedMethod),
71 handler: handler,
72 }, nil
73 }
74
75 // Payment returns the normalized payment contract used by this handler.
76 func (h *USDCPaymentHandler) Payment() types.X402Payment {
77 if h == nil || h.payment == nil {
78 return types.X402Payment{}
79 }
80 return h.payment.payment
81 }
82
83 func (h *USDCPaymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
84 if h == nil || h.payment == nil {
85 http.Error(w, "payment is not configured", http.StatusInternalServerError)
86 return
87 }
88
89 path := "/"
90 if r.URL != nil {
91 path = r.URL.Path
92 }
93 switch path {
94 case types.X402PreparePath:
95 if !utils.RequireMethod(w, r, http.MethodPost) {
96 return
97 }
98 var req types.X402PreparePaymentRequest
99 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
100 http.Error(w, "invalid payment prepare request", http.StatusBadRequest)
101 return
102 }
103 h.payment.WritePrepare(w, r, req.Sender, h.protectedPath)
104 case h.protectedPath:
105 if h.method != "" && !utils.RequireMethod(w, r, h.method) {
106 return
107 }
108 if h.handler == nil {
109 http.NotFound(w, r)
110 return
111 }
112
113 ctx := r.Context()
114 cancel := func() {}
115 payment := h.payment.payment
116 if payment.RequestTimeout > 0 {
117 ctx, cancel = context.WithTimeout(ctx, payment.RequestTimeout)
118 }
119 defer cancel()
120
121 settled, ok := h.payment.Settle(ctx, w, r)
122 if !ok {
123 return
124 }
125 utils.SetPaymentResponseHeaders(w.Header(), settled)
126
127 h.handler(w, r, types.X402PaymentResult{
128 TransactionID: strings.TrimSpace(settled.Transaction),
129 Network: string(settled.Network),
130 Payer: strings.TrimSpace(settled.Payer),
131 })
132 default:
133 http.NotFound(w, r)
134 }
135 }