feat: update payment handling and gRPC endpoint references

rabbitprincess committed Jun 6, 2026 at 22:02 UTC ac8458eaf17b4dba7b38d87c14c4b307281b6760
9 files changed +41 -63
cmd/payment-app/main.go
+4 -4
@@ -42,7 +42,7 @@ type paymentConfig struct {
42 x402Testnet bool
43 x402PayTo string
44 x402Amount string
45 - x402RPCs []string
45 + x402Endpoints []string
46 x402MaxTimeoutSeconds int
47 x402RequestTimeout int
48 }
@@ -76,9 +76,9 @@ func run(args []string) error {
76 utils.BoolFlag(fs, &cfg.x402Testnet, "x402-testnet", true, "use Sui testnet for x402 payments")
77 utils.StringFlag(fs, &cfg.x402PayTo, "x402-pay-to", "", "Sui USDC recipient address")
78 utils.StringFlag(fs, &cfg.x402Amount, "x402-amount", "0.01", "USDC amount")
79 - utils.RepeatedStringFlag(fs, &cfg.x402RPCs, "x402-rpc", "Sui RPC endpoint; repeat to try multiple endpoints before defaults")
79 + utils.RepeatedStringFlag(fs, &cfg.x402Endpoints, "x402-rpc", "Sui gRPC endpoint; repeat to try multiple endpoints before defaults")
80 fs.IntVar(&cfg.x402MaxTimeoutSeconds, "x402-max-timeout", 0, "x402 max payment timeout seconds advertised to clients")
81 - fs.IntVar(&cfg.x402RequestTimeout, "x402-request-timeout", 30, "Sui RPC and x402 verify/settle timeout seconds")
81 + fs.IntVar(&cfg.x402RequestTimeout, "x402-request-timeout", 30, "Sui gRPC and x402 verify/settle timeout seconds")
82
83 if err := utils.ParseFlagSet(fs, args, printUsage); err != nil {
84 if errors.Is(err, flag.ErrHelp) {
@@ -143,7 +143,7 @@ func runPaymentApp(ctx context.Context, cfg paymentConfig) error {
143 Amount: cfg.x402Amount,
144 MaxTimeoutSeconds: cfg.x402MaxTimeoutSeconds,
145 RequestTimeout: time.Duration(cfg.x402RequestTimeout) * time.Second,
146 - Endpoints: cfg.x402RPCs,
146 + Endpoints: cfg.x402Endpoints,
147 PhotoURL: cfg.photoURL,
148 })
149 if err != nil {
config.toml
+1 -1
@@ -2,7 +2,7 @@
2 # Bump protocol versions only when wire-level behavior changes.
3
4 [release]
5 -version = "v2.3.1"
5 +version = "v2.3.2"
6 base_url = "https://github.com/gosuda/portal-tunnel/releases"
7
8 [protocol]
frontend/src/components/Header.tsx
+1 -17
@@ -5,7 +5,7 @@ import { ThemeToggleButton } from "@/components/ThemeToggleButton";
5 import { useAuth } from "@/hooks/useAuth";
6 import { apiClient } from "@/lib/apiClient";
7 import { BROWSER_API_PATHS } from "@/lib/apiPaths";
8 -import type { DomainResponse, X402FacilitatorInfo } from "@/types/api";
8 +import type { DomainResponse } from "@/types/api";
9 import {
10 Tooltip,
11 TooltipContent,
@@ -22,14 +22,6 @@ interface HeaderProps {
22
23 const repoURL = "https://github.com/gosuda/portal-tunnel";
24
25 -function x402NetworkLabel(x402: X402FacilitatorInfo): string {
26 - const name =
27 - typeof x402.network_name === "string" ? x402.network_name.trim() : "";
28 - const network =
29 - typeof x402.network === "string" ? x402.network.trim() : "";
30 - return name || network || "enabled";
31 -}
32 -
25 export function Header({
26 title = "PORTAL",
27 isAdmin,
@@ -38,7 +30,6 @@ export function Header({
30 }: HeaderProps) {
31 const [releaseVersion, setReleaseVersion] = useState("");
32 const [ensVerified, setENSVerified] = useState(false);
41 - const [x402, setX402] = useState<X402FacilitatorInfo | null>(null);
33 const {
34 isAuthenticated,
35 isLoading,
@@ -80,13 +71,11 @@ export function Header({
71 : ""
72 );
73 setENSVerified(status?.ens?.verified === true);
83 - setX402(status?.x402?.enabled === true ? status.x402 : null);
74 }
75 } catch {
76 if (!cancelled) {
77 setReleaseVersion("");
78 setENSVerified(false);
89 - setX402(null);
79 }
80 }
81 })();
@@ -130,11 +119,6 @@ export function Header({
119 ENS verified
120 </span>
121 )}
133 - {x402 && (
134 - <span className="inline-flex h-6 items-center rounded-full bg-secondary px-2.5 text-xs font-semibold text-text-muted ring-1 ring-border/70">
135 - relay x402 {x402NetworkLabel(x402)}
136 - </span>
137 - )}
122 </div>
123 </div>
124 </div>
go.mod
+2 -1
@@ -16,7 +16,7 @@ require (
16 github.com/go-acme/lego/v4 v4.34.0
17 github.com/go-jose/go-jose/v4 v4.1.4
18 github.com/gosuda/keyless_tls v0.0.2-0.20260507061030-5128be6b5008
19 - github.com/gosuda/x402-facilitator v0.0.3-0.20260604031337-6baece37375a
19 + github.com/gosuda/x402-facilitator v0.0.4
20 github.com/hashicorp/yamux v0.1.2
21 github.com/hetznercloud/hcloud-go/v2 v2.40.0
22 github.com/knadh/koanf/parsers/toml/v2 v2.2.0
@@ -95,6 +95,7 @@ require (
95 github.com/go-openapi/spec v0.20.4 // indirect
96 github.com/go-openapi/swag v0.19.15 // indirect
97 github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
98 + github.com/golang/protobuf v1.5.4 // indirect
99 github.com/google/btree v1.1.2 // indirect
100 github.com/google/go-querystring v1.2.0 // indirect
101 github.com/google/s2a-go v0.1.9 // indirect
go.sum
+2 -2
@@ -218,8 +218,8 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U
218 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
219 github.com/gosuda/keyless_tls v0.0.2-0.20260507061030-5128be6b5008 h1:KuP/5VlPJwqZNyAV5U60C/j8Pc5O8ENkWPTgP7mEvj0=
220 github.com/gosuda/keyless_tls v0.0.2-0.20260507061030-5128be6b5008/go.mod h1:BOhUZgiAAQzxKO3QcC4fCXgd/+lqxgIu1OyIYTqtta8=
221 -github.com/gosuda/x402-facilitator v0.0.3-0.20260604031337-6baece37375a h1:AQoH9Wigm4LhKgHLDTP1/4fnuv7WXC9HA7HzvPEHbKY=
222 -github.com/gosuda/x402-facilitator v0.0.3-0.20260604031337-6baece37375a/go.mod h1:4hLowxzMiNVcLInkoD9BUDuGbzl86Vj3nO++QXh8OYg=
221 +github.com/gosuda/x402-facilitator v0.0.4 h1:EMW2xG6EVlphVYc2t7mIW7aimEkpjfIzwSpZ7I+VFx4=
222 +github.com/gosuda/x402-facilitator v0.0.4/go.mod h1:gTLrERyBeXt1BN6ax7INhx2WQnhfS/oE0MFZ8/l5Ra0=
223 github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
224 github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
225 github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
portal/x402/client.js
+2 -2
@@ -1,8 +1,8 @@
1 // Browser-only Sui wallet helper for Portal x402 routes.
2 // Native clients should call /x402/prepare, sign the returned transaction with
3 // their own Sui runtime, then send the resulting payload as X-PAYMENT.
4 -import { getWallets } from 'https://esm.sh/@wallet-standard/app@1.1.0';
5 -import { Transaction } from 'https://esm.sh/@mysten/sui@1.32.0/transactions';
4 +import { getWallets } from 'https://esm.sh/@wallet-standard/app@1.1.1';
5 +import { Transaction } from 'https://esm.sh/@mysten/sui@2.17.0/transactions';
6
7 const walletAPI = getWallets();
8
portal/x402/handler.go
+1 -5
@@ -118,11 +118,7 @@ func (h *USDCPaymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
118 }
119 defer cancel()
120
121 - paymentPayload, ok := h.payment.Verify(ctx, w, r)
122 - if !ok {
123 - return
124 - }
125 - settled, ok := h.payment.Settle(ctx, w, r, paymentPayload)
121 + settled, ok := h.payment.Settle(ctx, w, r)
122 if !ok {
123 return
124 }
portal/x402/payment.go
+27 -25
@@ -13,6 +13,7 @@ import (
13 facilitatorcore "github.com/gosuda/x402-facilitator/facilitator"
14 suischeme "github.com/gosuda/x402-facilitator/scheme/sui"
15 facilitatortypes "github.com/gosuda/x402-facilitator/types"
16 + "github.com/rs/zerolog/log"
17
18 "github.com/gosuda/portal-tunnel/v2/types"
19 "github.com/gosuda/portal-tunnel/v2/utils"
@@ -129,15 +130,11 @@ func FormatUSDCAtomicAmount(amount string) string {
130 return fmt.Sprintf("%f USDC", d)
131 }
132
132 -func (p *Payment) Verify(ctx context.Context, w http.ResponseWriter, r *http.Request) (*facilitatortypes.PaymentPayload, bool) {
133 +func (p *Payment) paymentPayloadFromRequest(w http.ResponseWriter, r *http.Request) (*facilitatortypes.PaymentPayload, bool) {
134 if p == nil {
135 http.Error(w, "payment is not configured", http.StatusInternalServerError)
136 return nil, false
137 }
137 - if p.facilitator == nil {
138 - http.Error(w, "x402 facilitator is not configured", http.StatusInternalServerError)
139 - return nil, false
140 - }
138
139 rawPayment := ""
140 for _, name := range []string{types.HeaderXPayment, types.HeaderPaymentSignature} {
@@ -178,29 +175,10 @@ func (p *Payment) Verify(ctx context.Context, w http.ResponseWriter, r *http.Req
175 p.writePaymentRequired(w, r, "invalid payment payload")
176 return nil, false
177 }
181 - verified, err := p.facilitator.Verify(ctx, payload, &p.requirements)
182 - if err != nil {
183 - http.Error(w, "verify x402 payment", http.StatusBadGateway)
184 - return nil, false
185 - }
186 - if verified == nil || !verified.IsValid {
187 - reason := "invalid payment"
188 - if verified != nil {
189 - reason = strings.TrimSpace(verified.InvalidReason)
190 - if reason == "" {
191 - reason = strings.TrimSpace(verified.InvalidMessage)
192 - }
193 - }
194 - if reason == "" {
195 - reason = "invalid payment"
196 - }
197 - p.writePaymentRequired(w, r, reason)
198 - return nil, false
199 - }
178 return payload, true
179 }
180
203 -func (p *Payment) Settle(ctx context.Context, w http.ResponseWriter, r *http.Request, payment *facilitatortypes.PaymentPayload) (*facilitatortypes.PaymentSettleResponse, bool) {
181 +func (p *Payment) Settle(ctx context.Context, w http.ResponseWriter, r *http.Request) (*facilitatortypes.PaymentSettleResponse, bool) {
182 if p == nil {
183 http.Error(w, "payment is not configured", http.StatusInternalServerError)
184 return nil, false
@@ -209,16 +187,40 @@ func (p *Payment) Settle(ctx context.Context, w http.ResponseWriter, r *http.Req
187 http.Error(w, "x402 facilitator is not configured", http.StatusInternalServerError)
188 return nil, false
189 }
190 + payment, ok := p.paymentPayloadFromRequest(w, r)
191 + if !ok {
192 + return nil, false
193 + }
194 if payment == nil {
195 http.Error(w, "x402 payment is missing", http.StatusInternalServerError)
196 return nil, false
197 }
198 settled, err := p.facilitator.Settle(ctx, payment, &p.requirements)
199 if err != nil {
200 + log.Warn().
201 + Err(err).
202 + Str("network", p.requirements.Network).
203 + Str("asset", p.requirements.Asset).
204 + Msg("settle x402 payment")
205 p.writePaymentRequired(w, r, "payment settlement failed")
206 return nil, false
207 }
208 if settled == nil || !settled.Success {
209 + event := log.Warn().
210 + Str("network", p.requirements.Network).
211 + Str("asset", p.requirements.Asset)
212 + if settled != nil {
213 + errorMessage := strings.TrimSpace(settled.ErrorMessage)
214 + if errorMessage == "" {
215 + errorMessage = "<empty>"
216 + }
217 + event = event.
218 + Str("reason", strings.TrimSpace(settled.ErrorReason)).
219 + Str("error_message", errorMessage).
220 + Str("payer", strings.TrimSpace(settled.Payer)).
221 + Str("transaction", strings.TrimSpace(settled.Transaction))
222 + }
223 + event.Msg("x402 payment settlement rejected")
224 p.writePaymentRequired(w, r, "payment settlement failed")
225 return nil, false
226 }
sdk/http.go
+1 -6
@@ -334,12 +334,7 @@ func (r *httpRoute) newHandler() http.Handler {
334 }
335 }
336
337 - paymentPayload, ok := r.payment.Verify(req.Context(), w, req)
338 - if !ok {
339 - return
340 - }
341 -
342 - settled, ok := r.payment.Settle(req.Context(), w, req, paymentPayload)
337 + settled, ok := r.payment.Settle(req.Context(), w, req)
338 if !ok {
339 return
340 }