main
go 158 lines 4.46 KB
Raw
1 package main
2
3 import (
4 "embed"
5 "encoding/json"
6 "html/template"
7 "io/fs"
8 "net/http"
9 "strings"
10 "time"
11
12 "github.com/gosuda/portal-tunnel/v2/portal/x402"
13 "github.com/gosuda/portal-tunnel/v2/types"
14 "github.com/gosuda/portal-tunnel/v2/utils"
15 )
16
17 //go:embed static/index.html static/photo.html static/style.css
18 var staticFiles embed.FS
19
20 const paidPhotoPath = "/paid/photo"
21
22 var (
23 indexPage = template.Must(template.ParseFS(staticFiles, "static/index.html"))
24 photoPage = template.Must(template.ParseFS(staticFiles, "static/photo.html"))
25 )
26
27 type paymentHandlerConfig struct {
28 Metadata types.LeaseMetadata
29 Testnet bool
30 PayTo string
31 Amount string
32 MaxTimeoutSeconds int
33 RequestTimeout time.Duration
34 Endpoints []string
35 PhotoURL string
36 }
37
38 type paymentHandler struct {
39 metadata types.LeaseMetadata
40 network string
41 networkName string
42 asset string
43 amount string
44 payTo string
45 photoURL string
46 }
47
48 type paymentPageData struct {
49 PageTitle string
50 PageDescription string
51 URL string
52 OGImage string
53 ProtectedPath string
54 Network string
55 NetworkName string
56 Asset string
57 Amount string
58 PhotoURL string
59 RecipientAddress string
60 TransactionID string
61 ConfigJSON template.JS
62 }
63
64 func newHandler(cfg paymentHandlerConfig) (http.Handler, error) {
65 handler := &paymentHandler{
66 metadata: cfg.Metadata.Copy(),
67 photoURL: strings.TrimSpace(cfg.PhotoURL),
68 }
69 paidPhotoHandler, err := x402.NewUSDCPaymentHandler(types.X402Payment{
70 Testnet: cfg.Testnet,
71 PayTo: cfg.PayTo,
72 Amount: cfg.Amount,
73 MaxTimeoutSeconds: cfg.MaxTimeoutSeconds,
74 RequestTimeout: cfg.RequestTimeout,
75 Endpoints: cfg.Endpoints,
76 ResourcePath: paidPhotoPath,
77 ResourceDescription: cfg.Metadata.Description,
78 ResourceMimeType: "text/html",
79 }, paidPhotoPath, http.MethodGet, handler.renderPaidPhoto)
80 if err != nil {
81 return nil, err
82 }
83 payment := paidPhotoHandler.Payment()
84 handler.network = payment.Network
85 handler.networkName = payment.NetworkName
86 handler.asset = payment.Asset
87 handler.amount = payment.Amount
88 handler.payTo = payment.PayTo
89
90 staticFS, err := fs.Sub(staticFiles, "static")
91 if err != nil {
92 return nil, err
93 }
94 mux := http.NewServeMux()
95 mux.Handle("/static/style.css", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
96 mux.HandleFunc(types.X402ClientPath, x402.ServeClientJS)
97 mux.Handle(types.X402PreparePath, paidPhotoHandler)
98 mux.HandleFunc("/", handler.handleIndex)
99 mux.Handle(paidPhotoPath, paidPhotoHandler)
100 return mux, nil
101 }
102
103 func (h *paymentHandler) handleIndex(w http.ResponseWriter, r *http.Request) {
104 if r.URL.Path != "/" {
105 http.NotFound(w, r)
106 return
107 }
108 if !utils.RequireMethod(w, r, http.MethodGet) {
109 return
110 }
111 data := h.newPaymentPageData(r)
112 w.Header().Set("Content-Type", "text/html; charset=utf-8")
113 w.Header().Set("Cache-Control", "no-store")
114 _ = indexPage.Execute(w, data)
115 }
116
117 func (h *paymentHandler) renderPaidPhoto(w http.ResponseWriter, r *http.Request, result types.X402PaymentResult) {
118 data := h.newPaymentPageData(r)
119 data.URL = utils.PublicURLForPath(r, paidPhotoPath)
120 data.TransactionID = result.TransactionID
121 w.Header().Set("Content-Type", "text/html; charset=utf-8")
122 w.Header().Set("Cache-Control", "no-store")
123 _ = photoPage.Execute(w, data)
124 }
125
126 func (h *paymentHandler) newPaymentPageData(r *http.Request) paymentPageData {
127 description := strings.TrimSpace(h.metadata.Description)
128 if description == "" {
129 description = "Connect a Sui wallet, settle USDC with x402, and reveal the protected image."
130 }
131 config := map[string]any{
132 "network": h.network,
133 "networkName": h.networkName,
134 "asset": h.asset,
135 "amount": h.amount,
136 "payTo": h.payTo,
137 "preparePath": types.X402PreparePath,
138 "protectedPath": paidPhotoPath,
139 }
140 configJSON, err := json.Marshal(config)
141 if err != nil {
142 configJSON = []byte("{}")
143 }
144 return paymentPageData{
145 PageTitle: "Portal Sui Wallet Payment",
146 PageDescription: description,
147 URL: utils.PublicURLForPath(r, "/"),
148 OGImage: h.metadata.Thumbnail,
149 ProtectedPath: paidPhotoPath,
150 Network: h.network,
151 NetworkName: h.networkName,
152 Asset: h.asset,
153 Amount: x402.FormatUSDCAtomicAmount(h.amount),
154 PhotoURL: h.photoURL,
155 RecipientAddress: h.payTo,
156 ConfigJSON: template.JS(string(configJSON)),
157 }
158 }