feat: update payment app to use metadata thumbnail and enhance wallet account selection
rabbitprincess committed
Jun 5, 2026 at 00:39 UTC
d74afe3e50ef69590d13f7851556ad984e2839d8
3 files changed
+127
-6
cmd/payment-app/handler.go
+1
-1
@@ -275,7 +275,7 @@ func (h *paymentHandler) newPaymentPageData(r *http.Request) paymentPageData {
275
PageTitle: "Portal Sui Wallet Payment",
276
PageDescription: description,
277
URL: utils.PublicURLForPath(r, "/"),
278
- OGImage: h.photoURL,
278
+ OGImage: h.metadata.Thumbnail,
279
ProtectedPath: paidPhotoPath,
280
Network: h.network,
281
NetworkName: h.networkName,
cmd/payment-app/main.go
+2
-2
@@ -59,7 +59,7 @@ func run(args []string) error {
59
cfg := paymentConfig{}
60
fs := utils.NewFlagSet("payment-app", printUsage)
61
62
- utils.StringFlagEnv(fs, &cfg.relayURLs, "relays", "https://localhost", "additional relay API URLs (comma-separated; scheme omitted defaults to https; merged with bootstrap relays when discovery is enabled)", "RELAYS")
62
+ utils.StringFlagEnv(fs, &cfg.relayURLs, "relays", "https://gosunuts.xyz", "additional relay API URLs (comma-separated; scheme omitted defaults to https; merged with bootstrap relays when discovery is enabled)", "RELAYS")
63
utils.BoolFlagEnv(fs, &cfg.discovery, "discovery", false, "include bootstrap relays and enable discovery", "DISCOVERY")
64
utils.BoolFlagEnv(fs, &cfg.banMITM, "ban-mitm", false, "ban relay when the MITM self-probe detects TLS termination", "BAN_MITM")
65
utils.StringFlagEnv(fs, &cfg.identityPath, "identity-path", "identity.json", "identity json file path", "IDENTITY_PATH")
@@ -74,7 +74,7 @@ func run(args []string) error {
74
utils.StringFlag(fs, &cfg.photoURL, "photo-url", defaultPhotoURL, "image URL revealed after payment")
75
utils.BoolFlag(fs, &cfg.hide, "hide", false, "hide this lease from listings")
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")
77
+ utils.StringFlag(fs, &cfg.x402PayTo, "x402-pay-to", "0xdb3585edba7e946c7e5c3827bdc0fc92d20efbb88520a82699ef4504b61aada2", "Sui USDC recipient address")
78
utils.StringFlag(fs, &cfg.x402Amount, "x402-amount", "10000", "USDC amount in atomic units")
79
utils.RepeatedStringFlag(fs, &cfg.x402RPCs, "x402-rpc", "Sui RPC 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")
cmd/payment-app/static/index.html
+124
-3
@@ -51,6 +51,8 @@
51
<div class="checkout">
52
<label class="wallet-label" for="walletSelect">Wallet</label>
53
<select id="walletSelect" class="wallet-select"></select>
54
+ <label class="wallet-label" for="accountSelect">Address</label>
55
+ <select id="accountSelect" class="wallet-select" disabled></select>
56
<div class="actions">
57
<button id="unlockButton" class="primary-button" type="button" disabled>Connect and pay</button>
58
</div>
@@ -87,6 +89,7 @@
89
const config = JSON.parse(document.getElementById('payment-config').textContent || '{}');
90
const walletsApi = getWallets();
91
const walletSelect = document.getElementById('walletSelect');
92
+ const accountSelect = document.getElementById('accountSelect');
93
const unlockButton = document.getElementById('unlockButton');
94
const statusEl = document.getElementById('status');
95
const viewer = document.getElementById('viewer');
@@ -94,6 +97,8 @@
97
const paymentFrame = document.getElementById('paymentFrame');
98
const resetViewer = document.getElementById('resetViewer');
99
const viewerTitle = document.getElementById('viewerTitle');
100
+ let connectedWallet = null;
101
+ let connectedAccounts = [];
102
103
function setStatus(value) {
104
statusEl.textContent = value;
@@ -111,6 +116,14 @@
116
return walletsApi.get().filter(supportsPayment);
117
}
118
119
+ function resetAccountSelect() {
120
+ const option = document.createElement('option');
121
+ option.value = '';
122
+ option.textContent = 'Connect wallet first';
123
+ accountSelect.replaceChildren(option);
124
+ accountSelect.disabled = true;
125
+ }
126
+
127
function refreshWallets() {
128
const wallets = currentWallets();
129
walletSelect.replaceChildren(...wallets.map((candidate, index) => {
@@ -123,20 +136,115 @@
136
walletSelect.disabled = !hasWallets;
137
unlockButton.disabled = !hasWallets;
138
unlockButton.textContent = 'Connect and pay';
139
+ connectedWallet = null;
140
+ connectedAccounts = [];
141
+ resetAccountSelect();
142
setStatus(hasWallets ? 'Select a wallet and continue' : 'Install a Sui wallet extension');
143
}
144
145
+ function normalizeAccounts(value) {
146
+ const accounts = Array.isArray(value) ? value : (value ? [value] : []);
147
+ return accounts.map((account) => {
148
+ if (typeof account === 'string') {
149
+ return { address: account };
150
+ }
151
+ return account && typeof account.address === 'string' ? account : null;
152
+ }).filter(Boolean);
153
+ }
154
+
155
function pickAccount(accounts) {
156
return accounts.find((candidate) => Array.isArray(candidate.chains) && candidate.chains.includes(config.network)) || accounts[0] || null;
157
}
158
159
+ function compatibleAccounts(accounts) {
160
+ const matching = accounts.filter((account) => !Array.isArray(account.chains) || account.chains.includes(config.network));
161
+ return matching.length > 0 ? matching : accounts;
162
+ }
163
+
164
+ function accountKey(account) {
165
+ return String(account?.address || '').trim().toLowerCase();
166
+ }
167
+
168
+ function mergeAccounts(primary, secondary) {
169
+ const seen = new Set();
170
+ return [...primary, ...secondary].filter((account) => {
171
+ const key = accountKey(account);
172
+ if (!key || seen.has(key)) {
173
+ return false;
174
+ }
175
+ seen.add(key);
176
+ return true;
177
+ });
178
+ }
179
+
180
+ function shortAddress(value) {
181
+ const address = String(value || '').trim();
182
+ if (address.length <= 18) {
183
+ return address;
184
+ }
185
+ return `${address.slice(0, 10)}...${address.slice(-6)}`;
186
+ }
187
+
188
+ function setAccountOptions(accounts, preferredAddress) {
189
+ connectedAccounts = compatibleAccounts(accounts);
190
+ accountSelect.replaceChildren(...connectedAccounts.map((account) => {
191
+ const option = document.createElement('option');
192
+ option.value = account.address;
193
+ option.textContent = shortAddress(account.address);
194
+ return option;
195
+ }));
196
+ const preferred = String(preferredAddress || '').trim();
197
+ const preferredAccount = preferred ? accountByAddress(connectedAccounts, preferred) : null;
198
+ if (preferredAccount) {
199
+ accountSelect.value = preferredAccount.address;
200
+ }
201
+ accountSelect.disabled = connectedAccounts.length === 0;
202
+ }
203
+
204
+ function selectedAccountFrom(response) {
205
+ return pickAccount(normalizeAccounts(
206
+ response?.account || response?.selectedAccount || response?.currentAccount || response?.address
207
+ ));
208
+ }
209
+
210
+ function accountByAddress(accounts, address) {
211
+ const key = String(address || '').trim().toLowerCase();
212
+ return accounts.find((account) => account.address === address) ||
213
+ accounts.find((account) => accountKey(account) === key) ||
214
+ null;
215
+ }
216
+
217
+ function pickConnectedAccount(response, wallet, hadAccountSelection) {
218
+ const selectedAccount = selectedAccountFrom(response);
219
+ const responseAccounts = normalizeAccounts(response?.accounts);
220
+ const accounts = mergeAccounts(
221
+ mergeAccounts(selectedAccount ? [selectedAccount] : [], responseAccounts),
222
+ normalizeAccounts(wallet.accounts || [])
223
+ );
224
+ setAccountOptions(accounts, selectedAccount?.address || accountSelect.value);
225
+ if (!selectedAccount && connectedAccounts.length > 1 && !hadAccountSelection) {
226
+ return null;
227
+ }
228
+ const selectedAddress = accountSelect.value;
229
+ return accountByAddress(connectedAccounts, selectedAddress) ||
230
+ selectedAccount ||
231
+ pickAccount(connectedAccounts);
232
+ }
233
+
234
async function connectWallet() {
235
const wallet = currentWallets()[Number(walletSelect.value)];
236
if (!wallet) {
237
throw new Error('No Sui wallet selected');
238
}
239
+ const hadAccountSelection = connectedWallet === wallet && accountSelect.value !== '';
240
const response = await wallet.features['standard:connect'].connect();
139
- const connectedAccount = pickAccount(response.accounts || wallet.accounts || []);
241
+ connectedWallet = wallet;
242
+ const connectedAccount = pickConnectedAccount(response, wallet, hadAccountSelection);
243
+ if (connectedAccount === null) {
244
+ unlockButton.textContent = 'Pay with selected address';
245
+ setStatus('Select an address and continue');
246
+ return null;
247
+ }
248
if (!connectedAccount) {
249
throw new Error('Connected wallet did not return an account');
250
}
@@ -319,10 +427,15 @@
427
async function unlock() {
428
unlockButton.disabled = true;
429
walletSelect.disabled = true;
430
+ accountSelect.disabled = true;
431
paymentFrame.removeAttribute('srcdoc');
432
try {
433
setStatus('Connecting wallet');
325
- const { wallet, account } = await connectWallet();
434
+ const connection = await connectWallet();
435
+ if (!connection) {
436
+ return;
437
+ }
438
+ const { wallet, account } = connection;
439
440
viewer.classList.add('is-active');
441
resetViewer.hidden = false;
@@ -373,12 +486,20 @@
486
} finally {
487
const hasWallets = currentWallets().length > 0;
488
walletSelect.disabled = !hasWallets;
489
+ accountSelect.disabled = connectedAccounts.length === 0;
490
unlockButton.disabled = !hasWallets;
377
- unlockButton.textContent = 'Connect and pay';
491
+ unlockButton.textContent = connectedAccounts.length > 0 ? 'Pay with selected address' : 'Connect and pay';
492
}
493
}
494
495
unlockButton.addEventListener('click', unlock);
496
+ walletSelect.addEventListener('change', () => {
497
+ connectedWallet = null;
498
+ connectedAccounts = [];
499
+ resetAccountSelect();
500
+ unlockButton.textContent = 'Connect and pay';
501
+ setStatus('Select a wallet and continue');
502
+ });
503
504
resetViewer.addEventListener('click', () => {
505
viewer.classList.remove('is-active');