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)