Repository navigation
Expand file tree
/
Copy pathup-subscribe.html
More file actions
513 lines (496 loc) · 39.6 KB
/
Copy pathup-subscribe.html
File metadata and controls
513 lines (496 loc) · 39.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<script src="theme.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:type" content="website">
<meta property="og:image" content="https://crosschain-lukso.chainintegrate.it/og-image.png">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://crosschain-lukso.chainintegrate.it/og-image.png">
<meta name="referrer" content="no-referrer">
<meta name="robots" content="noindex, nofollow">
<title>Gas sponsorizzato (beta): iscrizione — ChainIntegrate</title>
<link rel="icon" href="favicon.ico">
<script src="https://cdnjs.cloudflare.com/ajax/libs/ethers/6.13.4/ethers.umd.min.js" integrity="sha384-6Zl0Pc8zjSz8KvmNeXRvUQgY4ryFb+BwDvKCmLYcBME0joAaru491tQgi9B7zsMM" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="chains.js"></script>
<script src="gas-relay-client.js"></script>
<style>
:root {
--bg: #0f1115; --panel: #171a21; --border: #2a2f3a; --field: #10131a; --chip: #2a2f3a; --disabled: #3a3f4b; --raised: #1d2330;
--text: #e6e8ec; --text-dim: #9aa1ad;
--accent: #5b8cff; --ok: #3fbf6f; --warn: #e0a83c; --err: #e05a5a;
}
* { box-sizing: border-box; }
body { background: var(--bg); color: var(--text); font-family: -apple-system, "Segoe UI", Roboto, sans-serif;
max-width: 760px; margin: 0 auto; padding: 32px 20px 80px; }
h1 { font-size: 20px; margin-bottom: 4px; }
.subtitle { color: var(--text-dim); font-size: 13px; margin-bottom: 28px; }
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 20px; margin-bottom: 20px; }
.panel h2 { font-size: 14px; margin: 0 0 14px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.03em; }
.note { font-size: 12.5px; color: var(--text-dim); line-height: 1.5; margin-bottom: 12px; }
label { display: block; font-size: 13px; color: var(--text-dim); margin: 14px 0 6px; }
select, input { width: 100%; background: var(--field); border: 1px solid var(--border); color: var(--text);
border-radius: 6px; padding: 10px 12px; font-size: 14px; font-family: inherit; }
button { width: 100%; background: var(--accent); color: white; border: none; border-radius: 6px;
padding: 12px; font-size: 14px; font-weight: 600; cursor: pointer; margin-top: 12px; }
button:disabled { background: var(--disabled); cursor: not-allowed; }
button.danger { background: var(--err); }
button.secondary { background: var(--chip); }
.status-box { background: var(--field); border: 1px solid var(--border); border-radius: 6px; padding: 12px;
font-family: monospace; font-size: 12.5px; margin-top: 12px; }
.status-box .empty { color: var(--text-dim); }
#log { font-family: monospace; font-size: 12.5px; white-space: pre-wrap; word-break: break-all; line-height: 1.6; }
.line-ok { color: var(--ok); } .line-warn { color: var(--warn); } .line-err { color: var(--err); } .line-dim { color: var(--text-dim); }
.line-compare { color: var(--warn); font-weight: 700; border: 1px solid var(--warn); border-radius: 6px; padding: 8px 10px; margin: 6px 0; }
.brand-row { margin-bottom: 24px; }
.brand-logo { display: block; width: 100%; height: auto; border-radius: 10px; }
.top-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.lang-btn { width: auto; background: var(--chip); padding: 6px 14px; font-size: 12px; margin-top: 0; flex-shrink: 0; }
.site-footer { margin-top: 40px; padding: 18px 20px; background: #f5f6f8; border: 1px solid #e3e5ea; border-radius: 10px; color: #4b4f58; }
.footer-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.footer-logo { height: 48px; width: auto; flex-shrink: 0; }
.footer-text { font-size: 13px; line-height: 1.6; }
.footer-li { display: inline-flex; vertical-align: middle; margin: 0 2px; }
.site-footer a { color: #2563eb; text-decoration: none; font-weight: 600; }
.kv { display: grid; grid-template-columns: 210px 1fr; gap: 6px 12px; font-size: 13px; margin-top: 10px; }
.kv .k { color: var(--text-dim); }
.kv .v { font-family: monospace; word-break: break-all; }
.kv .v.ok { color: var(--ok); } .kv .v.warn { color: var(--warn); } .kv .v.err { color: var(--err); }
.verdict { margin-top: 12px; font-weight: 700; }
.check-row { display: flex; align-items: center; gap: 8px; margin-top: 14px; font-size: 13.5px; }
.check-row input { width: auto; }
.warn-box { margin-top: 12px; padding: 12px 14px; border: 1px solid var(--err); border-radius: 8px; background: rgba(224,90,90,0.08); font-size: 13px; line-height: 1.5; }
.link-btn { display: block; text-align: center; background: var(--accent); color: white; border-radius: 6px; padding: 12px; font-size: 14px; font-weight: 600; text-decoration: none; margin-top: 12px; }
.link-btn[hidden], .status-box[hidden] { display: none; }
.plan { font-family: monospace; font-size: 12.5px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
@media (max-width: 560px) { .kv { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div class="brand-row">
<img src="banner.png" alt="ChainIntegrate" class="brand-logo" onerror="this.style.display='none'">
</div>
<div class="top-row">
<div>
<h1 data-i18n="h1">Gas sponsorizzato: iscrizione e ricarica (beta)</h1>
<div class="subtitle" data-i18n="subtitle"></div>
</div>
<button class="lang-btn" id="langToggle" data-i18n="langToggleBtn">English</button>
</div>
<div class="panel">
<h2 data-i18n="panel0Title">0 · Come funziona</h2>
<div class="note" data-i18n-html="panel0Note"></div>
<div class="status-box" id="priceBox"><span class="empty" data-i18n="priceLoading">Lettura del listino...</span></div>
<div class="warn-box" data-i18n-html="disclaimer"></div>
<div class="check-row"><input type="checkbox" id="ackRisk"><label for="ackRisk" style="margin:0; color: var(--text);" data-i18n="ackLabel">Ho letto e accetto</label></div>
</div>
<div class="panel">
<h2 data-i18n="panel1Title">1 · Rete, UP e controller</h2>
<label for="network" data-i18n="networkLabel">Rete su cui paghi</label>
<select id="network"></select>
<div class="note" id="networkNote" style="margin-top:8px;" data-i18n-html="networkNote"></div>
<label for="upAddress" data-i18n="upAddressLabel">Indirizzo della tua UP</label>
<input type="text" id="upAddress" placeholder="0x...">
<div class="status-box" id="balanceBox" hidden></div>
<div class="note" style="margin-top:8px;" data-i18n="upNote"></div>
<button id="connectSigner" data-i18n="connectSignerBtn">Connetti MetaMask (controller della UP)</button>
</div>
<div class="panel">
<h2 data-i18n="panel2Title">2 · Verifica</h2>
<div class="note" data-i18n="panel2Note"></div>
<div class="status-box" id="checkBox"><span class="empty" data-i18n="checkIdle">La verifica parte da sola quando rete, UP e MetaMask sono impostati.</span></div>
<a id="fixLink" class="link-btn" hidden target="_blank" rel="noopener"></a>
<button id="recheckBtn" class="secondary" data-i18n="recheckBtn">Ricontrolla</button>
</div>
<div class="panel">
<h2 data-i18n="panel3Title">3 · Email e pagamento</h2>
<label for="email" data-i18n="emailLabel">La tua email (per l'attivazione)</label>
<input type="email" id="email" placeholder="nome@esempio.it" autocomplete="email">
<div class="note" style="margin-top:10px;" data-i18n-html="privacyNote"></div>
<div class="check-row"><input type="checkbox" id="ackPrivacy"><label for="ackPrivacy" style="margin:0; color: var(--text);" data-i18n="ackPrivacy">Ho letto l'informativa e acconsento all'uso dell'email per l'attivazione</label></div>
<button id="payBtn" disabled data-i18n="payBtn">Paga 5 USDC e richiedi l'attivazione</button>
<div id="payHint" class="note" style="margin-top:8px;"></div>
<div id="log" class="line-dim" style="margin-top:10px;"></div>
</div>
<div class="site-footer">
<div class="footer-row">
<img src="logo.png" alt="ChainIntegrate" class="footer-logo" onerror="this.style.display='none'">
<div class="footer-text" data-i18n-html="footerHtml"></div>
</div>
</div>
<script>
const $ = (id) => document.getElementById(id);
const EXPLORER_PATH = "/tx/";
// ==================== WHAT THE PAGE TRUSTS ====================
// The payment goes to ChainIntegrate's UP, in Circle's native USDC. Both are written here: if the
// service ever answered other values, the page stops before anything is signed.
const RECEIVER = "0x4a2605796e0d91A9667d6E30365aEEC384C48c27";
const NATIVE_USDC = {
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base
137: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // Polygon PoS (native, not the old USDC.e)
43114: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", // Avalanche C-Chain
42161: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // Arbitrum One (native, not the old USDC.e)
5042: "0x3600000000000000000000000000000000000000", // Arc: the ERC-20 interface of USDC, the chain's gas token (6 decimals)
};
const AMOUNT = 5_000000n; // 5 USDC, 6 decimals
// Published price per operation, shown until the service answers with its own (the service's list wins).
const PRICE_LIST = { 8453: "0.01", 42161: "0.03", 137: "0.02", 43114: "0.02", 5042: "0.01" };
const ERC20 = new ethers.Interface(["function balanceOf(address) view returns (uint256)", "function transfer(address to, uint256 amount) returns (bool)"]);
const I18N = {
it: {
pageTitle: "Gas sponsorizzato (beta): iscrizione — ChainIntegrate",
h1: "Gas sponsorizzato: iscrizione e ricarica (beta)",
subtitle: "Paga 5 USDC dalla tua Universal Profile e ottieni un saldo con cui il sito paga il gas delle tue operazioni su Base, Arbitrum, Polygon, Avalanche e Arc. Servizio in beta.",
langToggleBtn: "English",
panel0Title: "0 · Come funziona",
panel0Note: "<b>Cosa compri.</b> 5 USDC diventano un <b>saldo unico</b> della tua UP, valido su tutte le reti disponibili (elenco qui sotto). Ogni operazione pagata dal sito scala il prezzo della rete su cui avviene, <b>solo quando viene eseguita</b>.<br><br><b>Per ricaricare il saldo usa questa stessa pagina:</b> ogni pagamento di 5 USDC si aggiunge al saldo che hai già.<br><br><b>Come controllare il saldo:</b> inserisci la tua UP in questa pagina: compare subito sotto l'indirizzo, senza MetaMask, oppure guardalo nell'opzione \"Paga il gas con il relayer del sito\" di UP Wallet, prima di ogni operazione. Ti mandiamo anche un'email quando scende a 2,5, a 1 e a 0,5 USDC.<br><br><b>Prima di pagare, sulla rete scelta:</b><ol style=\"margin:6px 0 0; padding-left:20px;\"><li>la tua UP deve esistere su quella rete (se l'hai creata su LUKSO, replicala con la pagina Deploy);</li><li>la UP deve avere la <b>configurazione 4337</b>. È <b>necessaria per la richiesta di attivazione</b> (il sito paga il gas di questo pagamento) <b>e per ogni uso successivo</b> del gas sponsorizzato. È una transazione una tantum per rete, pagata dal tuo controller (pochi centesimi);</li><li>la UP deve avere almeno 5 USDC <b>nativi di Circle</b> su quella rete (puoi mandarli da un exchange all'indirizzo della tua UP).</li></ol><br><b>Dopo il pagamento</b> verifichiamo la transazione e attiviamo il saldo: ricevi un'email di conferma quando è disponibile.",
priceLoading: "Lettura del listino...",
priceTitle: "Reti disponibili e prezzo per operazione",
priceItem: (v) => `${v.name}: ${v.price} USDC per operazione (con 5 USDC, ${v.ops} operazioni)`,
priceNone: "Il servizio di sponsorizzazione non risponde: riprova più tardi.",
disclaimer: "<b>⚠️ Leggi prima di pagare</b><ul style=\"margin:8px 0 0; padding-left:20px;\"><li><b>Beta.</b> Il servizio è in prova: funzionamento, reti e prezzi possono cambiare, e può essere sospeso. Le condizioni definitive del servizio non sono ancora pubblicate: paga solo l'importo dell'iscrizione e ricarica solo importi piccoli.</li><li><b>Servizio sperimentale.</b> I contratti usati (paymaster e Extension4337 di LUKSO) non hanno un audit professionale. Transazioni reali e irreversibili.</li><li><b>Il saldo si usa solo per il gas</b> delle operazioni della tua UP pagate dal sito, sulle reti del servizio.</li><li><b>Questa pagina non chiede mai chiavi private</b>: il tuo controller firma un messaggio in MetaMask.</li><li>Per problemi o domande: assistenza via Telegram (in fondo alla pagina).</li></ul>",
ackLabel: "Ho letto e accetto: uso questo servizio a mio rischio.",
ackRequired: "Prima accetta le condizioni nel pannello 0.",
panel1Title: "1 · Rete, UP e controller",
networkLabel: "Rete su cui paghi",
networkNote: "<b>La rete serve solo per questo pagamento.</b> Il saldo vale poi su tutte le reti disponibili, qualunque sia quella che scegli qui. Scegli la rete dove la tua UP ha gli USDC e la configurazione 4337.",
upAddressLabel: "Indirizzo della tua UP",
upNote: "Lo stesso indirizzo della tua UP su LUKSO. Il pagamento parte dalla UP: è così che il servizio sa quale UP attivare.",
connectSignerBtn: "Connetti MetaMask (controller della UP)",
noSignerFound: "Nessun wallet trovato: installa MetaMask.",
connectionRejected: "Connessione rifiutata in MetaMask.",
panel2Title: "2 · Verifica",
panel2Note: "Sola lettura: niente viene firmato. Se manca qualcosa, il pulsante qui sotto ti porta alla pagina giusta.",
checkIdle: "La verifica parte da sola quando rete, UP e MetaMask sono impostati.",
checkRunning: "Verifica in corso...",
recheckBtn: "Ricontrolla",
kNet: "Rete", kUp: "UP", kSigner: "Controller", kSvc: "Servizio", kSetup: "Configurazione 4337", kUsdc: "USDC nella UP", kStatus: "Iscrizione",
netOk: (v) => `${v.name} (chainId ${v.id})`,
walletWrong: (v) => `MetaMask è sulla rete ${v.got}, non su ${v.name} (${v.exp}): cambia rete in MetaMask`,
upMissing: "indirizzo non valido",
upNotDeployed: "la UP non esiste su questa rete: prima replicala (pagina Deploy)",
upOk: "esiste su questa rete",
signerNone: "collega MetaMask con un controller della UP",
svcDown: "il servizio di sponsorizzazione non risponde su questa rete",
subsClosed: "Iscrizioni non ancora aperte: il servizio funziona, ma non accetta ancora pagamenti. Riprova nei prossimi giorni.",
svcBadData: "il servizio indica un USDC o un destinatario diversi da quelli attesi: niente verrà firmato",
svcOk: (v) => `pagamento di ${v.amount} USDC alla UP di ChainIntegrate ${v.receiver}`,
setupOk: "pronta (estensione, EntryPoint e permesso 4337 del controller)",
setupMissing: (v) => `manca: ${v.what}. Serve per la richiesta di attivazione e per l'uso del gas sponsorizzato: una transazione una tantum, pagata dal controller.`,
usdcOk: (v) => `${v.bal} USDC (servono ${v.need})`,
usdcLow: (v) => `${v.bal} USDC: servono almeno ${v.need} USDC nativi (contratto ${v.token}) nella UP su questa rete`,
stNone: "non ancora iscritta",
balActive: (v) => `Saldo: ${v.bal} USDC (iscrizione attiva, vale su tutte le reti)`,
balLow: (v) => `Saldo: ${v.bal} USDC: non basta per un'operazione. Ricarica con questa pagina.`,
balLabel: "Iscrizione",
stRequested: "pagamento richiesto, in attesa di conferma sulla chain",
stPaid: "pagamento ricevuto: in attesa di attivazione (riceverai un'email)",
stActive: (v) => `attiva: saldo ${v.bal} USDC (questo pagamento è una ricarica)`,
fixDeploy: "Replica la UP su questa rete (pagina Deploy) →",
fixSetup: "Fai la configurazione 4337 (pagina Gas relay, sezione 4) →",
panel3Title: "3 · Email e pagamento",
emailLabel: "La tua email (per l'attivazione)",
privacyNote: "<b>Informativa.</b> L'email serve solo per comunicarti l'attivazione e per le comunicazioni su questo servizio. È conservata sul server del servizio, collegata alla tua UP; non è pubblicata né ceduta a terzi. Titolare: ChainIntegrate. Per farla cancellare scrivi all'assistenza (Telegram, in fondo alla pagina).",
ackPrivacy: "Ho letto l'informativa e acconsento all'uso dell'email per l'attivazione",
payBtn: "Paga 5 USDC e richiedi l'attivazione",
hintAck: "Accetta le condizioni (pannello 0).",
hintCheck: "Completa la verifica (pannello 2).",
hintEmail: "Scrivi un'email valida e accetta l'informativa.",
hintReady: "MetaMask ti chiederà di firmare un messaggio (nessun gas): il gas di questo pagamento lo paga il sito.",
hintPaid: "Il pagamento è già registrato e in attesa di attivazione: non serve pagare di nuovo.",
badEmail: "Email non valida.",
preparing: "Preparazione dell'operazione (5 USDC alla UP di ChainIntegrate)...",
waitingWatcher: "Operazione eseguita. Attendo che il servizio registri il pagamento (fino a qualche minuto)...",
paidOk: "✅ Pagamento registrato. Dopo la verifica della transazione attiviamo il saldo: riceverai un'email di conferma.",
paidUnknown: "⚠️ Il servizio non ha ancora visto il pagamento. Controlla l'esito sull'explorer (link qui sopra) e riapri questa pagina più tardi: lo stato si aggiorna da solo. Non pagare di nuovo.",
paidLater: "⚠️ Il pagamento è sulla chain ma il servizio non l'ha ancora registrato. Riapri questa pagina più tardi: lo stato si aggiorna da solo; non pagare di nuovo.",
opFailed: "❌ L'operazione non è riuscita: niente è stato pagato.",
genericError: (v) => `Errore: ${v.err}`,
editedDuringCheckLog: "❌ Dati cambiati mentre la preparazione era in corso: niente è stato inviato. Ripeti.",
footerHtml: `Servizi di consulenza in integrazione dati & blockchain a cura di ChainIntegrate <a href="https://www.linkedin.com/company/chainintegrate" target="_blank" rel="noopener" aria-label="LinkedIn" class="footer-li"><svg width="20" height="20" viewBox="0 0 20 20"><rect width="20" height="20" rx="4" fill="#0A66C2"/><text x="10" y="15" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="700" fill="#fff">in</text></svg></a> · <a href="https://t.me/Simone_1977_2" target="_blank" rel="noopener">Assistenza via Telegram</a> · <a href="https://github.com/ChainIntegrate/Cross_Chain" target="_blank" rel="noopener">Codice sorgente su GitHub</a>`,
},
en: {
pageTitle: "Sponsored gas (beta): subscription — ChainIntegrate",
h1: "Sponsored gas: subscription and top-up (beta)",
subtitle: "Pay 5 USDC from your Universal Profile and get a balance with which the site pays the gas of your operations on Base, Arbitrum, Polygon, Avalanche and Arc. Beta service.",
langToggleBtn: "Italiano",
panel0Title: "0 · How it works",
panel0Note: "<b>What you buy.</b> 5 USDC become <b>one balance</b> of your UP, valid on every available network (list below). Each operation paid by the site takes the price of the network where it runs, <b>only when it is executed</b>.<br><br><b>To top up your balance, use this same page:</b> each 5 USDC payment is added to the balance you already have.<br><br><b>How to check your balance:</b> enter your UP on this page: it shows right under the address, no MetaMask needed, or see it in the \"Pay the gas with the site relayer\" option of UP Wallet, before each operation. We also e-mail you when it goes down to 2.5, 1 and 0.5 USDC.<br><br><b>Before paying, on the chosen network:</b><ol style=\"margin:6px 0 0; padding-left:20px;\"><li>your UP must exist on that network (if you created it on LUKSO, replicate it with the Deploy page);</li><li>the UP must have the <b>4337 setup</b>. It is <b>needed for the activation request</b> (the site pays the gas of this payment) <b>and for every later use</b> of the sponsored gas. It is a one-time transaction per network, paid by your controller (a few cents);</li><li>the UP must hold at least 5 <b>Circle-native</b> USDC on that network (you can send them from an exchange to your UP's address).</li></ol><br><b>After the payment</b> we verify the transaction and activate the balance: you get a confirmation e-mail when it is available.",
priceLoading: "Reading the price list...",
priceTitle: "Available networks and price per operation",
priceItem: (v) => `${v.name}: ${v.price} USDC per operation (with 5 USDC, ${v.ops} operations)`,
priceNone: "The sponsor service does not answer: try again later.",
disclaimer: "<b>⚠️ Read before paying</b><ul style=\"margin:8px 0 0; padding-left:20px;\"><li><b>Beta.</b> The service is being tested: how it works, its networks and prices may change, and it may be suspended. The final terms of service are not published yet: pay only the subscription amount and top up only small amounts.</li><li><b>Experimental service.</b> The contracts used (the paymaster and LUKSO's Extension4337) have no professional audit. Real, irreversible transactions.</li><li><b>The balance is used only for the gas</b> of your UP's operations paid by the site, on the service's networks.</li><li><b>This page never asks for private keys</b>: your controller signs a message in MetaMask.</li><li>For problems or questions: support via Telegram (bottom of the page).</li></ul>",
ackLabel: "I have read and accept: I use this service at my own risk.",
ackRequired: "First accept the conditions in panel 0.",
panel1Title: "1 · Network, UP and controller",
networkLabel: "Network you pay on",
networkNote: "<b>The network is only for this payment.</b> The balance then works on every available network, whichever you choose here. Choose the network where your UP holds the USDC and has the 4337 setup.",
upAddressLabel: "Your UP's address",
upNote: "The same address as your UP on LUKSO. The payment comes from the UP: this is how the service knows which UP to activate.",
connectSignerBtn: "Connect MetaMask (the UP's controller)",
noSignerFound: "No wallet found: install MetaMask.",
connectionRejected: "Connection rejected in MetaMask.",
panel2Title: "2 · Check",
panel2Note: "Read-only: nothing is signed. If something is missing, the button below takes you to the right page.",
checkIdle: "The check starts by itself once network, UP and MetaMask are set.",
checkRunning: "Checking...",
recheckBtn: "Check again",
kNet: "Network", kUp: "UP", kSigner: "Controller", kSvc: "Service", kSetup: "4337 setup", kUsdc: "USDC in the UP", kStatus: "Subscription",
netOk: (v) => `${v.name} (chainId ${v.id})`,
walletWrong: (v) => `MetaMask is on network ${v.got}, not on ${v.name} (${v.exp}): switch network in MetaMask`,
upMissing: "invalid address",
upNotDeployed: "the UP does not exist on this network: replicate it first (Deploy page)",
upOk: "exists on this network",
signerNone: "connect MetaMask with a controller of the UP",
svcDown: "the sponsor service does not answer on this network",
subsClosed: "Subscriptions are not open yet: the service is running but does not accept payments yet. Try again in the next few days.",
svcBadData: "the service gives a USDC or a receiver different from the expected ones: nothing will be signed",
svcOk: (v) => `payment of ${v.amount} USDC to ChainIntegrate's UP ${v.receiver}`,
setupOk: "ready (extension, EntryPoint and the controller's 4337 permission)",
setupMissing: (v) => `missing: ${v.what}. Needed for the activation request and for using the sponsored gas: a one-time transaction, paid by the controller.`,
usdcOk: (v) => `${v.bal} USDC (${v.need} needed)`,
usdcLow: (v) => `${v.bal} USDC: at least ${v.need} Circle-native USDC (contract ${v.token}) are needed in the UP on this network`,
stNone: "not subscribed yet",
balActive: (v) => `Balance: ${v.bal} USDC (subscription active, valid on every network)`,
balLow: (v) => `Balance: ${v.bal} USDC: not enough for an operation. Top up with this page.`,
balLabel: "Subscription",
stRequested: "payment requested, waiting for confirmation on chain",
stPaid: "payment received: waiting for activation (you will get an e-mail)",
stActive: (v) => `active: balance ${v.bal} USDC (this payment is a top-up)`,
fixDeploy: "Replicate the UP on this network (Deploy page) →",
fixSetup: "Do the 4337 setup (Gas relay page, section 4) →",
panel3Title: "3 · E-mail and payment",
emailLabel: "Your e-mail (for the activation)",
privacyNote: "<b>Privacy notice.</b> The e-mail is used only to tell you about the activation and for communications about this service. It is kept on the service's server, linked to your UP; it is not published or given to third parties. Controller: ChainIntegrate. To have it deleted, write to support (Telegram, bottom of the page).",
ackPrivacy: "I have read the notice and agree to the use of the e-mail for the activation",
payBtn: "Pay 5 USDC and request activation",
hintAck: "Accept the conditions (panel 0).",
hintCheck: "Complete the check (panel 2).",
hintEmail: "Enter a valid e-mail and accept the notice.",
hintReady: "MetaMask will ask you to sign a message (no gas): the site pays the gas of this payment.",
hintPaid: "The payment is already recorded and waits for activation: no need to pay again.",
badEmail: "Invalid e-mail.",
preparing: "Preparing the operation (5 USDC to ChainIntegrate's UP)...",
waitingWatcher: "Operation executed. Waiting for the service to record the payment (up to a few minutes)...",
paidOk: "✅ Payment recorded. Once the transaction is verified we activate the balance: you will get a confirmation e-mail.",
paidUnknown: "⚠️ The service has not seen the payment yet. Check the outcome on the explorer (link above) and open this page again later: the status updates by itself. Do not pay again.",
paidLater: "⚠️ The payment is on chain but the service has not recorded it yet. Open this page again later: the status updates by itself; do not pay again.",
opFailed: "❌ The operation did not succeed: nothing was paid.",
genericError: (v) => `Error: ${v.err}`,
editedDuringCheckLog: "❌ Data changed while it was being prepared: nothing was sent. Repeat.",
footerHtml: `Data & blockchain integration consulting by ChainIntegrate <a href="https://www.linkedin.com/company/chainintegrate" target="_blank" rel="noopener" aria-label="LinkedIn" class="footer-li"><svg width="20" height="20" viewBox="0 0 20 20"><rect width="20" height="20" rx="4" fill="#0A66C2"/><text x="10" y="15" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="700" fill="#fff">in</text></svg></a> · <a href="https://t.me/Simone_1977_2" target="_blank" rel="noopener">Support via Telegram</a> · <a href="https://github.com/ChainIntegrate/Cross_Chain" target="_blank" rel="noopener">Source code on GitHub</a>`,
},
};
let LANG = "it";
function t(key, vars) { const e = I18N[LANG][key]; return typeof e === "function" ? e(vars || {}) : e; }
function applyI18n() {
document.documentElement.lang = LANG;
document.title = t("pageTitle");
document.querySelectorAll("[data-i18n]").forEach(el => { el.textContent = t(el.getAttribute("data-i18n")); });
document.querySelectorAll("[data-i18n-html]").forEach(el => { el.innerHTML = t(el.getAttribute("data-i18n-html")); });
renderPrices(); if (lastCheck) renderCheck(lastCheck); updateButtons();
}
// The check's lines are written in the page's language: run it again after a switch.
$("langToggle").addEventListener("click", () => { LANG = LANG === "it" ? "en" : "it"; applyI18n(); if (lastCheck) scheduleCheck(0); });
// Data from wallets, RPCs and the service is written with textContent, never innerHTML.
function setStatus(id, text, cls) { const el = $(id); el.textContent = ""; const s = document.createElement("span"); if (cls) s.className = cls; s.textContent = text; el.appendChild(s); }
function log(msg, cls) { const d = document.createElement("div"); if (cls) d.className = cls; d.textContent = msg; $("log").appendChild(d); }
const validEmail = (e) => e.length <= 254 && /^[^\s@<>()",;:\\[\]]+@[^\s@<>()",;:\\[\]]+\.[^\s@<>()",;:\\[\]]{2,}$/.test(e);
// ==================== networks and price list ====================
let nets = [], prices = null, priceState = "loading", busy = false;
async function loadNetworks() {
const info = await GasRelayClient.loadInfo(true);
nets = CHAINS.filter(c => NATIVE_USDC[c.chainId] && info && info.chains[String(c.chainId)] && info.chains[String(c.chainId)].sponsorPaymaster);
const sel = $("network"); sel.textContent = "";
nets.forEach(c => { const o = document.createElement("option"); o.value = c.key; o.textContent = `${c.name} (chainId ${c.chainId})`; sel.appendChild(o); });
const want = new URLSearchParams(location.search).get("network");
if (want && nets.some(c => c.key === want)) sel.value = want;
// Any address that is not a customer gets the subscription data, price list included.
if (nets.length) { const st = await GasRelayClient.sponsorStatus(nets[0].chainId, ethers.ZeroAddress); prices = st && st.subscription ? st.subscription.prices : null; priceState = prices ? "ok" : st ? "closed" : "down"; }
renderPrices();
}
function renderPrices() {
const pr = prices || PRICE_LIST;
const shown = (nets.length ? nets : CHAINS.filter(c => PRICE_LIST[c.chainId])).filter(c => pr[String(c.chainId)]);
const note = prices ? "" : priceState === "closed" ? t("subsClosed") : priceState === "down" || !nets.length ? t("priceNone") : "";
const box = $("priceBox"); box.textContent = "";
const h = document.createElement("div"); h.style.fontWeight = "700"; h.style.marginBottom = "6px"; h.textContent = t("priceTitle"); box.appendChild(h);
const ul = document.createElement("ul"); ul.style.margin = "0"; ul.style.paddingLeft = "20px";
shown.forEach(c => { const p = pr[String(c.chainId)]; const li = document.createElement("li"); li.textContent = t("priceItem", { name: c.name, price: p, ops: Math.floor(5 / Number(p)) }); ul.appendChild(li); });
box.appendChild(ul);
if (note) { const d = document.createElement("div"); d.className = "line-err"; d.style.marginTop = "6px"; d.textContent = note; $("priceBox").appendChild(d); }
}
const getNet = () => nets.find(c => c.key === $("network").value) || null;
// ==================== wallet ====================
let signerProvider = null, signerAddress = null;
const discovered = [];
window.addEventListener("eip6963:announceProvider", (e) => discovered.push(e.detail));
window.dispatchEvent(new Event("eip6963:requestProvider"));
$("connectSigner").addEventListener("click", async () => {
let candidate = discovered.find(p => !/lukso|universal profile/i.test(p.info.name))?.provider;
if (!candidate && window.ethereum && window.ethereum !== window.lukso) candidate = window.ethereum;
if (!candidate) { setStatus("checkBox", t("noSignerFound"), "line-err"); return; }
try {
const acc = await candidate.request({ method: "eth_requestAccounts" });
signerAddress = acc[0] ? ethers.getAddress(acc[0]) : null;
if (signerProvider !== candidate && candidate.on) {
candidate.on("accountsChanged", (a) => { signerAddress = a && a[0] ? ethers.getAddress(a[0]) : null; scheduleCheck(0); });
candidate.on("chainChanged", () => scheduleCheck(0));
}
signerProvider = candidate;
scheduleCheck(0);
} catch (e) { setStatus("checkBox", t("connectionRejected"), "line-err"); }
});
// ==================== check ====================
// Bumped by every change: a payment prepared while it changed is not sent (AUDIT M-2).
// The balance and status of the UP, read-only from the service: no wallet, no other check needed.
let balGen = 0, balTimer = null;
function refreshBalance(delay) {
const my = ++balGen; clearTimeout(balTimer);
balTimer = setTimeout(async () => {
const box = $("balanceBox"), net = getNet(), raw = $("upAddress").value.trim();
if (!net || !ethers.isAddress(raw)) { box.hidden = true; return; }
const st = await GasRelayClient.sponsorStatus(net.chainId, ethers.getAddress(raw));
if (my !== balGen) return;
const sub = st && st.subscription;
if (!sub) { box.hidden = true; return; }
let msg, cls;
if (sub.status === "active") {
const bal = st.balance || "0", low = st.reason === "balance used up";
msg = t(low ? "balLow" : "balActive", { bal }); cls = low ? "line-err" : "line-ok";
} else {
msg = `${t("balLabel")}: ${t(sub.status === "paid" ? "stPaid" : sub.status === "requested" ? "stRequested" : "stNone")}`; cls = sub.status === "paid" ? "line-warn" : "";
}
setStatus("balanceBox", msg, cls); box.hidden = false;
}, delay);
}
let gen = 0, check = null, lastCheck = null, checkRun = 0, checkTimer = null;
function scheduleCheck(delay = 500) { refreshBalance(delay); gen++; check = null; updateButtons(); clearTimeout(checkTimer); checkTimer = setTimeout(runCheck, delay); }
["upAddress"].forEach(id => $(id).addEventListener("input", () => scheduleCheck()));
$("network").addEventListener("change", () => scheduleCheck(0));
["email"].forEach(id => $(id).addEventListener("input", updateButtons));
["ackRisk", "ackPrivacy"].forEach(id => $(id).addEventListener("change", updateButtons));
$("recheckBtn").addEventListener("click", () => scheduleCheck(0));
async function runCheck() {
const run = ++checkRun;
setStatus("checkBox", t("checkRunning"), "empty");
let c;
try { c = await computeCheck(); } catch (e) { c = { rows: [["kNet", t("genericError", { err: e.shortMessage || e.message }), "err"]], ok: false }; }
if (run !== checkRun) return;
check = c.ok ? c : null; lastCheck = c;
renderCheck(c); updateButtons();
}
async function computeCheck() {
const rows = [], out = { rows, ok: false };
const net = getNet();
if (!net) { rows.push(["kNet", t("priceNone"), "err"]); return out; }
const provider = new ethers.JsonRpcProvider(net.rpc, undefined, { batchMaxCount: 1 });
const chainId = Number(await provider.send("eth_chainId", []));
if (chainId !== net.chainId) { rows.push(["kNet", `RPC chainId ${chainId}`, "err"]); return out; }
rows.push(["kNet", t("netOk", { name: net.name, id: chainId }), "ok"]);
const raw = $("upAddress").value.trim();
if (!ethers.isAddress(raw)) { rows.push(["kUp", t("upMissing"), "err"]); return out; }
const up = ethers.getAddress(raw);
if ((await provider.getCode(up)) === "0x") { rows.push(["kUp", t("upNotDeployed"), "err"]); out.fix = "deploy"; out.up = up; out.net = net; return out; }
rows.push(["kUp", `${up} — ${t("upOk")}`, "ok"]);
if (!signerAddress || !signerProvider) { rows.push(["kSigner", t("signerNone"), "warn"]); return out; }
const walletChain = parseInt(await signerProvider.request({ method: "eth_chainId" }), 16);
if (walletChain !== chainId) { rows.push(["kSigner", t("walletWrong", { got: walletChain, name: net.name, exp: chainId }), "err"]); return out; }
rows.push(["kSigner", signerAddress, "ok"]);
// The service: what to pay and to whom. Never trusted alone: checked against the page's own values.
const st = await GasRelayClient.sponsorStatus(chainId, up);
const sub = st && st.subscription;
if (!sub) { rows.push(["kSvc", t(st ? "subsClosed" : "svcDown"), "err"]); return out; }
if (!sub.usdc || ethers.getAddress(sub.usdc) !== NATIVE_USDC[chainId] || ethers.getAddress(sub.receiver) !== RECEIVER || ethers.parseUnits(String(sub.amount), 6) !== AMOUNT) { rows.push(["kSvc", t("svcBadData"), "err"]); return out; }
rows.push(["kSvc", t("svcOk", { amount: "5", receiver: RECEIVER }), "ok"]);
// The UP's 4337 setup, the paymaster and the controller: the relayer client's own checks.
const ready = await GasRelayClient.check(provider, { chainId, up, signer: signerAddress, forSubscription: true });
if (ready.state !== "ready") {
const what = (ready.reasons || []).map(([k, v]) => GasRelayClient.text(k, v)).join("; ") || t("svcDown");
rows.push(["kSetup", t("setupMissing", { what }), "err"]); out.fix = "setup"; out.up = up; out.net = net; return out;
}
rows.push(["kSetup", t("setupOk"), "ok"]);
out.status = sub.status;
const bal = await new ethers.Contract(NATIVE_USDC[chainId], ERC20, provider).balanceOf(up);
const enough = bal >= AMOUNT;
rows.push(["kUsdc", t(enough ? "usdcOk" : "usdcLow", { bal: ethers.formatUnits(bal, 6), need: "5", token: NATIVE_USDC[chainId] }), enough ? "ok" : "err"]);
const status = sub.status;
rows.push(["kStatus", status === "active" ? t("stActive", { bal: st.balance || "0" }) : t(status === "paid" ? "stPaid" : status === "requested" ? "stRequested" : "stNone"), status === "paid" ? "warn" : status === "active" ? "ok" : ""]);
Object.assign(out, { ok: enough, provider, chainId, up, net, ready, status });
return out;
}
function renderCheck(c) {
const box = $("checkBox"); box.textContent = "";
const grid = document.createElement("div"); grid.className = "kv";
for (const [k, v, cls] of c.rows) {
const kd = document.createElement("div"); kd.className = "k"; kd.textContent = t(k);
const vd = document.createElement("div"); vd.className = "v" + (cls ? " " + cls : ""); vd.textContent = v;
grid.append(kd, vd);
}
box.appendChild(grid);
const a = $("fixLink");
if (c.fix && c.up && c.net) {
const q = new URLSearchParams({ up: c.up, network: c.net.key });
a.href = c.fix === "deploy" ? `up-deploy-public.html?${q}` : `up-gas-relay.html?${q}`;
a.textContent = t(c.fix === "deploy" ? "fixDeploy" : "fixSetup");
a.hidden = false;
} else a.hidden = true;
}
function updateButtons() {
const email = $("email").value.trim();
let hint = "";
if (!$("ackRisk").checked) hint = t("hintAck");
else if (lastCheck && lastCheck.status === "paid") hint = t("hintPaid");
else if (!check) hint = t("hintCheck");
else if (!validEmail(email) || !$("ackPrivacy").checked) hint = t("hintEmail");
else hint = t("hintReady");
$("payHint").textContent = hint;
$("payBtn").disabled = busy || hint !== t("hintReady");
}
// ==================== payment ====================
$("payBtn").addEventListener("click", async () => {
if (busy || !check) return;
const email = $("email").value.trim().toLowerCase();
if (!validEmail(email)) { log(t("badEmail"), "line-err"); return; }
busy = true; updateButtons();
const g = gen;
$("log").textContent = "";
const c = check;
const fmt = (w) => `${ethers.formatEther(w)} ${c.net.currency || ""}`.trim();
try {
log(t("preparing"), "line-dim");
const data = ERC20.encodeFunctionData("transfer", [RECEIVER, AMOUNT]);
const ready = await GasRelayClient.check(c.provider, { chainId: c.chainId, up: c.up, signer: signerAddress, forSubscription: true });
if (ready.state !== "ready") throw new Error(t("hintCheck"));
const prep = await GasRelayClient.prepare(c.provider, { chainId: c.chainId, up: c.up, ready, to: NATIVE_USDC[c.chainId], value: 0n, data, fmt });
if (g !== gen) { log(t("editedDuringCheckLog"), "line-err"); return; }
log(prep.plan, "line-dim");
const hash = await GasRelayClient.signAndSend(signerProvider, { chainId: c.chainId, prep, signer: signerAddress, log, subscribe: { email } });
const url = getExplorerUrl(c.net, EXPLORER_PATH, hash);
if (url) log(url, "line-dim");
const res = await GasRelayClient.waitResult(c.provider, hash, prep.hash, { log, fmt });
// Outcome unknown (no receipt anywhere): the service reads the chain itself, so keep asking it.
if (!res.success && !res.unknown) { log(t("opFailed"), "line-err"); return; }
log(t("waitingWatcher"), "line-dim");
// The service records the payment within about a minute of the block.
for (let i = 0; i < 24; i++) {
await new Promise(r => setTimeout(r, 10000));
const st = await GasRelayClient.sponsorStatus(c.chainId, c.up);
const s = st && st.subscription && st.subscription.status;
if (s === "paid" || s === "active") { log(t("paidOk"), "line-ok"); scheduleCheck(0); return; }
}
log(t(res.unknown ? "paidUnknown" : "paidLater"), "line-warn");
} catch (e) {
log(e.message && e.message.length < 600 ? e.message : t("genericError", { err: e.shortMessage || e.message }), "line-err");
} finally { busy = false; updateButtons(); }
});
// ==================== start ====================
const q = new URLSearchParams(location.search);
if (q.get("up") && ethers.isAddress(q.get("up"))) $("upAddress").value = ethers.getAddress(q.get("up"));
applyI18n();
loadNetworks().then(() => scheduleCheck(0));
</script>
</body>
</html>