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
}