Revert "feat(x402): enhance gasless payment handling with balance checks and transaction expiration"
This reverts commit b76fca0c48b6e15243dd2f7ab19fde291dab19e5.
rabbitprincess committed
Jun 5, 2026 at 00:08 UTC
419bbc9993d927bf6889ee730b4d0b04f767205e
5 files changed
+63
-207
cmd/payment-app/static/index.html
+40
-137
@@ -99,127 +99,16 @@
99
statusEl.textContent = value;
100
}
101
102
- function pickAccount(accounts) {
103
- return accounts.find((candidate) => Array.isArray(candidate.chains) && candidate.chains.includes(config.network)) || accounts[0] || null;
104
- }
105
-
106
- function normalizeAccounts(value) {
107
- const accounts = Array.isArray(value) ? value : value?.accounts || (value?.account ? [value.account] : []);
108
- return accounts.map((account) => {
109
- if (typeof account === 'string') {
110
- return { address: account };
111
- }
112
- return account && typeof account.address === 'string' ? account : null;
113
- }).filter(Boolean);
114
- }
115
-
116
- function standardPaymentWallet(candidate, index) {
102
+ function supportsPayment(candidate) {
103
const features = candidate?.features || {};
118
- const connectFeature = features['standard:connect'];
119
- const signTransactionFeature = features['sui:signTransaction'];
120
- const signTransactionBlockFeature = features['sui:signTransactionBlock'];
121
- const connect = typeof connectFeature?.connect === 'function' ? connectFeature.connect.bind(connectFeature) : null;
122
- const signTransaction = typeof signTransactionFeature?.signTransaction === 'function' ? signTransactionFeature.signTransaction.bind(signTransactionFeature) : null;
123
- const signTransactionBlock = typeof signTransactionBlockFeature?.signTransactionBlock === 'function' ? signTransactionBlockFeature.signTransactionBlock.bind(signTransactionBlockFeature) : null;
124
- if (!connect || (!signTransaction && !signTransactionBlock)) {
125
- return null;
126
- }
127
-
128
- const executeTransactionFeature = features['sui:signAndExecuteTransaction'];
129
- const executeTransactionBlockFeature = features['sui:signAndExecuteTransactionBlock'];
130
- const executeTransaction = typeof executeTransactionFeature?.signAndExecuteTransaction === 'function' ? executeTransactionFeature.signAndExecuteTransaction.bind(executeTransactionFeature) : null;
131
- const executeTransactionBlock = typeof executeTransactionBlockFeature?.signAndExecuteTransactionBlock === 'function' ? executeTransactionBlockFeature.signAndExecuteTransactionBlock.bind(executeTransactionBlockFeature) : null;
132
- const name = candidate.name || `Wallet ${index + 1}`;
133
- return {
134
- id: `standard:${name}`,
135
- name,
136
- async connect() {
137
- const response = await connect();
138
- return pickAccount(normalizeAccounts(response?.accounts || candidate.accounts || []));
139
- },
140
- async signTransaction(account, transaction) {
141
- if (signTransaction) {
142
- return signTransaction({ transaction, account, chain: config.network });
143
- }
144
- return signTransactionBlock({ transactionBlock: transaction, account, chain: config.network });
145
- },
146
- executeTransaction: executeTransaction || executeTransactionBlock ? async (account, transaction) => {
147
- if (executeTransaction) {
148
- return executeTransaction({ transaction, account, chain: config.network });
149
- }
150
- return executeTransactionBlock({ transactionBlock: transaction, account, chain: config.network });
151
- } : null,
152
- };
153
- }
154
-
155
- function legacyPaymentWallet(globalName, label) {
156
- const candidate = window[globalName];
157
- if (!candidate || typeof candidate !== 'object') {
158
- return null;
159
- }
160
- const signTransaction = typeof candidate.signTransaction === 'function' ? candidate.signTransaction.bind(candidate) : null;
161
- const signTransactionBlock = typeof candidate.signTransactionBlock === 'function' ? candidate.signTransactionBlock.bind(candidate) : null;
162
- if (!signTransaction && !signTransactionBlock) {
163
- return null;
164
- }
165
-
166
- const connect = typeof candidate.connect === 'function' ? candidate.connect.bind(candidate) : null;
167
- const requestAccounts = typeof candidate.requestAccounts === 'function' ? candidate.requestAccounts.bind(candidate) : null;
168
- const getAccounts = typeof candidate.getAccounts === 'function' ? candidate.getAccounts.bind(candidate) : null;
169
- const executeTransaction = typeof candidate.signAndExecuteTransaction === 'function' ? candidate.signAndExecuteTransaction.bind(candidate) : null;
170
- const executeTransactionBlock = typeof candidate.signAndExecuteTransactionBlock === 'function' ? candidate.signAndExecuteTransactionBlock.bind(candidate) : null;
171
- const name = candidate.name || candidate.walletName || label;
172
- return {
173
- id: `legacy:${globalName}`,
174
- name,
175
- async connect() {
176
- const response = connect ? await connect() : (requestAccounts ? await requestAccounts() : null);
177
- let accounts = normalizeAccounts(response || candidate.accounts || candidate.account);
178
- if (accounts.length === 0 && getAccounts) {
179
- accounts = normalizeAccounts(await getAccounts());
180
- }
181
- return pickAccount(accounts);
182
- },
183
- async signTransaction(account, transaction) {
184
- if (signTransaction) {
185
- return signTransaction({ transaction, account, chain: config.network });
186
- }
187
- return signTransactionBlock({ transactionBlock: transaction, account, chain: config.network });
188
- },
189
- executeTransaction: executeTransaction || executeTransactionBlock ? async (account, transaction) => {
190
- if (executeTransaction) {
191
- return executeTransaction({ transaction, account, chain: config.network });
192
- }
193
- return executeTransactionBlock({ transactionBlock: transaction, account, chain: config.network });
194
- } : null,
195
- };
104
+ return Boolean(
105
+ features['standard:connect'] &&
106
+ (features['sui:signTransaction'] || features['sui:signTransactionBlock'])
107
+ );
108
}
109
110
function currentWallets() {
199
- const wallets = walletsApi.get().map(standardPaymentWallet).filter(Boolean);
200
- [
201
- ['suiWallet', 'Sui Wallet'],
202
- ['suiet', 'Suiet'],
203
- ['ethosWallet', 'Ethos'],
204
- ['martian', 'Martian'],
205
- ['surfWallet', 'Surf Wallet'],
206
- ['glassWallet', 'Glass Wallet'],
207
- ].forEach(([globalName, label]) => {
208
- const wallet = legacyPaymentWallet(globalName, label);
209
- if (wallet) {
210
- wallets.push(wallet);
211
- }
212
- });
213
-
214
- const seen = new Set();
215
- return wallets.filter((wallet) => {
216
- const key = wallet.name.trim().toLowerCase();
217
- if (seen.has(key)) {
218
- return false;
219
- }
220
- seen.add(key);
221
- return true;
222
- });
111
+ return walletsApi.get().filter(supportsPayment);
112
}
113
114
function refreshWallets() {
@@ -227,7 +116,7 @@
116
walletSelect.replaceChildren(...wallets.map((candidate, index) => {
117
const option = document.createElement('option');
118
option.value = String(index);
230
- option.textContent = candidate.name;
119
+ option.textContent = candidate.name || `Wallet ${index + 1}`;
120
return option;
121
}));
122
const hasWallets = wallets.length > 0;
@@ -237,12 +126,17 @@
126
setStatus(hasWallets ? 'Select a wallet and continue' : 'Install a Sui wallet extension');
127
}
128
129
+ function pickAccount(accounts) {
130
+ return accounts.find((candidate) => Array.isArray(candidate.chains) && candidate.chains.includes(config.network)) || accounts[0] || null;
131
+ }
132
+
133
async function connectWallet() {
134
const wallet = currentWallets()[Number(walletSelect.value)];
135
if (!wallet) {
136
throw new Error('No Sui wallet selected');
137
}
245
- const connectedAccount = await wallet.connect();
138
+ const response = await wallet.features['standard:connect'].connect();
139
+ const connectedAccount = pickAccount(response.accounts || wallet.accounts || []);
140
if (!connectedAccount) {
141
throw new Error('Connected wallet did not return an account');
142
}
@@ -368,29 +262,38 @@
262
}
263
264
async function signTransaction(wallet, account, transactionBytes) {
371
- return wallet.signTransaction(account, transactionFromBase64(transactionBytes));
372
- }
373
-
374
- async function executeSignedTransaction(signed, fallbackBytes) {
375
- const bytes = signed.bytes || signed.transactionBlockBytes || fallbackBytes;
376
- const transactionBytes = bytes instanceof Uint8Array ? bytesToBase64(bytes) : bytes;
377
- if (!transactionBytes || !signed.signature) {
378
- throw new Error('Wallet did not return a signed transaction');
265
+ const tx = transactionFromBase64(transactionBytes);
266
+ if (wallet.features['sui:signTransaction']) {
267
+ return wallet.features['sui:signTransaction'].signTransaction({
268
+ transaction: tx,
269
+ account,
270
+ chain: config.network,
271
+ });
272
}
380
- return suiRPC('sui_executeTransactionBlock', [
381
- transactionBytes,
382
- [signed.signature],
383
- { showEffects: true },
384
- 'WaitForLocalExecution',
385
- ]);
273
+ return wallet.features['sui:signTransactionBlock'].signTransactionBlock({
274
+ transactionBlock: tx,
275
+ account,
276
+ chain: config.network,
277
+ });
278
}
279
280
async function executePrepareTransaction(wallet, account, transactionBytes) {
389
- const transaction = transactionFromBase64(transactionBytes);
390
- if (wallet.executeTransaction) {
391
- return wallet.executeTransaction(account, transaction);
281
+ const tx = transactionFromBase64(transactionBytes);
282
+ if (wallet.features['sui:signAndExecuteTransaction']) {
283
+ return wallet.features['sui:signAndExecuteTransaction'].signAndExecuteTransaction({
284
+ transaction: tx,
285
+ account,
286
+ chain: config.network,
287
+ });
288
+ }
289
+ if (wallet.features['sui:signAndExecuteTransactionBlock']) {
290
+ return wallet.features['sui:signAndExecuteTransactionBlock'].signAndExecuteTransactionBlock({
291
+ transactionBlock: tx,
292
+ account,
293
+ chain: config.network,
294
+ });
295
}
393
- return executeSignedTransaction(await wallet.signTransaction(account, transaction), transactionBytes);
296
+ throw new Error('This wallet cannot execute the USDC prepare transaction');
297
}
298
299
function encodePaymentPayload(payload) {
frontend/nginx.conf
+1
-1
@@ -2,7 +2,7 @@ server {
2
listen 8080;
3
server_name _;
4
access_log off;
5
- error_log /var/log/nginx/error.log crit;
5
+ error_log /var/log/nginx/error.log warn;
6
7
root /usr/share/nginx/html;
8
index index.html;
go.mod
+1
-2
@@ -15,7 +15,7 @@ require (
15
github.com/go-acme/lego/v4 v4.34.0
16
github.com/go-jose/go-jose/v4 v4.1.4
17
github.com/gosuda/keyless_tls v0.0.2-0.20260507061030-5128be6b5008
18
- github.com/gosuda/x402-facilitator v0.0.3
18
+ github.com/gosuda/x402-facilitator v0.0.3-0.20260604031337-6baece37375a
19
github.com/hashicorp/yamux v0.1.2
20
github.com/hetznercloud/hcloud-go/v2 v2.40.0
21
github.com/knadh/koanf/parsers/toml/v2 v2.2.0
@@ -94,7 +94,6 @@ require (
94
github.com/go-openapi/spec v0.20.4 // indirect
95
github.com/go-openapi/swag v0.19.15 // indirect
96
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
97
- github.com/golang/protobuf v1.5.4 // indirect
97
github.com/google/btree v1.1.2 // indirect
98
github.com/google/go-querystring v1.2.0 // indirect
99
github.com/google/s2a-go v0.1.9 // indirect
go.sum
+6
-4
@@ -216,10 +216,10 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U
216
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
217
github.com/gosuda/keyless_tls v0.0.2-0.20260507061030-5128be6b5008 h1:KuP/5VlPJwqZNyAV5U60C/j8Pc5O8ENkWPTgP7mEvj0=
218
github.com/gosuda/keyless_tls v0.0.2-0.20260507061030-5128be6b5008/go.mod h1:BOhUZgiAAQzxKO3QcC4fCXgd/+lqxgIu1OyIYTqtta8=
219
-github.com/gosuda/x402-facilitator v0.0.3-0.20260604061634-d3d61cf24500 h1:JHtOeyajeYz21JVmsVSx7A1W7rP9I3jgKvIUn4KdQVo=
220
-github.com/gosuda/x402-facilitator v0.0.3-0.20260604061634-d3d61cf24500/go.mod h1:gTLrERyBeXt1BN6ax7INhx2WQnhfS/oE0MFZ8/l5Ra0=
221
-github.com/gosuda/x402-facilitator v0.0.3 h1:idC2ZIYRaHU8Y4ODxqBwMV9W7EGiRO9sgkqTg0sEQXg=
222
-github.com/gosuda/x402-facilitator v0.0.3/go.mod h1:gTLrERyBeXt1BN6ax7INhx2WQnhfS/oE0MFZ8/l5Ra0=
219
+github.com/gosuda/x402-facilitator v0.0.1 h1:Jo4ctVestDMw6B4dkvodZLj6a5rTt6efpoK4VzWoO5k=
220
+github.com/gosuda/x402-facilitator v0.0.1/go.mod h1:4hLowxzMiNVcLInkoD9BUDuGbzl86Vj3nO++QXh8OYg=
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=
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=
@@ -422,6 +422,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
422
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
423
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
424
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
425
+github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
426
+github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
427
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
428
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
429
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
portal/x402/payment.go
+15
-63
@@ -239,69 +239,23 @@ func (p *Payment) WritePrepare(w http.ResponseWriter, r *http.Request, sender, r
239
return
240
}
241
242
- amount, err := strconv.ParseUint(p.requirements.Amount, 10, 64)
243
- if err != nil || amount == 0 {
244
- http.Error(w, "payment amount is invalid", http.StatusInternalServerError)
245
- return
246
- }
247
- coinType, ok := suischeme.GetGaslessStablecoinType(p.requirements.Network, p.requirements.Asset)
248
- if !ok {
249
- http.Error(w, "payment asset is not supported", http.StatusInternalServerError)
250
- return
251
- }
252
- client, err := suischeme.NewClientForNetwork(p.requirements.Network, p.payment.Endpoints)
242
+ coinObjects, err := suischeme.ListOwnedGaslessStablecoinCoinObjects(ctx, p.requirements.Network, sender, p.requirements.Asset, p.payment.Endpoints)
243
if err != nil {
254
- http.Error(w, fmt.Sprintf("create Sui client: %v", err), http.StatusBadGateway)
255
- return
256
- }
257
- defer client.Close()
258
- info := suischeme.GetNetworkInfo(p.requirements.Network)
259
- if info == nil {
260
- http.Error(w, "payment network is not supported", http.StatusInternalServerError)
244
+ http.Error(w, fmt.Sprintf("list USDC coin objects: %v", err), http.StatusBadGateway)
245
return
246
}
263
- expiration, err := client.ResolveGaslessStablecoinExpiration(ctx, info.ChainDigest)
264
- if err != nil {
265
- http.Error(w, fmt.Sprintf("resolve payment transaction expiration: %v", err), http.StatusBadGateway)
266
- return
247
+ nonZeroCoinObjects := make([]suischeme.OwnedCoinObject, 0, len(coinObjects))
248
+ for _, coinObject := range coinObjects {
249
+ if coinObject.Balance == 0 {
250
+ continue
251
+ }
252
+ nonZeroCoinObjects = append(nonZeroCoinObjects, coinObject)
253
}
254
255
var prepareTransaction *struct {
256
Transaction string `json:"transaction"`
257
}
272
- balance, err := client.Balance(ctx, sender, coinType)
273
- if err != nil {
274
- http.Error(w, fmt.Sprintf("get USDC balance: %v", err), http.StatusBadGateway)
275
- return
276
- }
277
- if balance.AddressBalance < amount {
278
- needed := amount - balance.AddressBalance
279
- if balance.CoinBalance < needed {
280
- http.Error(w, fmt.Sprintf("insufficient USDC balance: need %d, address balance %d, coin object balance %d", amount, balance.AddressBalance, balance.CoinBalance), http.StatusBadRequest)
281
- return
282
- }
283
- coinObjects, err := client.ListOwnedCoinObjects(ctx, sender, coinType)
284
- if err != nil {
285
- http.Error(w, fmt.Sprintf("list USDC coin objects: %v", err), http.StatusBadGateway)
286
- return
287
- }
288
- nonZeroCoinObjects := make([]suischeme.OwnedCoinObject, 0, len(coinObjects))
289
- var prepareAmount uint64
290
- for _, coinObject := range coinObjects {
291
- if coinObject.Balance == 0 {
292
- continue
293
- }
294
- if prepareAmount > ^uint64(0)-coinObject.Balance {
295
- http.Error(w, "USDC coin object balance sum overflows uint64", http.StatusBadGateway)
296
- return
297
- }
298
- prepareAmount += coinObject.Balance
299
- nonZeroCoinObjects = append(nonZeroCoinObjects, coinObject)
300
- }
301
- if prepareAmount < needed {
302
- http.Error(w, fmt.Sprintf("insufficient USDC balance: need %d, address balance %d, coin object balance %d", amount, balance.AddressBalance, prepareAmount), http.StatusBadRequest)
303
- return
304
- }
258
+ if len(nonZeroCoinObjects) > 0 {
259
txBytes, err := suischeme.BuildCoinObjectsToAddressBalanceTransferTransaction(ctx, suischeme.CoinObjectsToAddressBalanceTransfer{
260
Sender: sender,
261
Recipient: sender,
@@ -309,7 +263,6 @@ func (p *Payment) WritePrepare(w http.ResponseWriter, r *http.Request, sender, r
263
Asset: p.requirements.Asset,
264
CoinObjects: nonZeroCoinObjects,
265
Endpoints: p.payment.Endpoints,
312
- Expiration: expiration,
266
})
267
if err != nil {
268
http.Error(w, fmt.Sprintf("build prepare transaction: %v", err), http.StatusBadGateway)
@@ -321,13 +274,12 @@ func (p *Payment) WritePrepare(w http.ResponseWriter, r *http.Request, sender, r
274
}
275
276
paymentTxBytes, err := suischeme.BuildGaslessStablecoinTransferTransaction(ctx, suischeme.GaslessStablecoinTransfer{
324
- Sender: sender,
325
- Recipient: p.requirements.PayTo,
326
- Network: p.requirements.Network,
327
- Asset: p.requirements.Asset,
328
- Amount: p.requirements.Amount,
329
- Endpoints: p.payment.Endpoints,
330
- Expiration: expiration,
277
+ Sender: sender,
278
+ Recipient: p.requirements.PayTo,
279
+ Network: p.requirements.Network,
280
+ Asset: p.requirements.Asset,
281
+ Amount: p.requirements.Amount,
282
+ Endpoints: p.payment.Endpoints,
283
})
284
if err != nil {
285
http.Error(w, fmt.Sprintf("build payment transaction: %v", err), http.StatusBadGateway)