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');