Repository navigation
Expand file tree
/
Copy pathblock-0.html
More file actions
290 lines (265 loc) · 15.7 KB
/
Copy pathblock-0.html
File metadata and controls
290 lines (265 loc) · 15.7 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
<!-- FEE-FREE CREATE RETIRED → CreateRouter app -->
<meta http-equiv="refresh" content="0;url=/#creer">
<script>location.replace("/#creer");</script>
<!-- block-0.html — creer un block avec les parametres audites, DEPUIS TON PROPRE COMPTE.
================================================================================================
⛔ PREMIERE VERSION : ELLE NE MARCHAIT QUE POUR UNE SEULE ADRESSE. Le `from` etait celui de la
fiche auditee, et toute autre connexion recevait un refus AVANT que le wallet s ouvre — donc
aucun message, aucune demande de confirmation, l impression que rien ne se passe. Une page qui
refuse en silence ressemble a une page cassee.
⇒ L adresse d un B-20 derive de (variant, msg.sender, salt) : chaque compte a DEJA la sienne.
On construit donc la transaction POUR CELUI QUI SE CONNECTE, et on lui montre SON adresse.
⛔ LES PARAMETRES RESTENT CEUX QUI ONT ETE AUDITES — nom, symbole, supply, plafond, contractURI,
sel, et surtout `initialAdmin = address(0)`. Seul le DESTINATAIRE du mint change, parce qu il
doit etre celui qui signe. Le tout est reconstruit par `encodeur.js`, dont la sortie a ete
comparee OCTET POUR OCTET a `base-forge`.
⛔ ELLE NE DETIENT AUCUNE CLE. Elle encode, affiche, appelle `eth_sendTransaction`. Le wallet
montre la transaction et c est un humain qui accepte.
⛔ ET ELLE DEMANDE A LA CHAINE AVANT D OFFRIR QUOI QUE CE SOIT : la factory doit accepter le
calldata et rendre l adresse, cette adresse doit etre libre, et le solde doit couvrir le gas. -->
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>BLOCK 0 — create your block</title>
<style>
:root{color-scheme:dark}
body{margin:0;background:#07090f;color:#dce6f5;font:15px/1.55 ui-sans-serif,system-ui,sans-serif;padding:22px}
.w{max-width:720px;margin:0 auto}
h1{font-size:20px;margin:0 0 4px}
.sub{color:#7f8ba3;margin:0 0 20px}
.card{background:#0d1220;border:1px solid #1c2740;border-radius:14px;padding:16px;margin-bottom:14px}
.row{display:flex;gap:12px;padding:5px 0;border-bottom:1px solid #141c2e;font-size:13px}
.row:last-child{border:0}
.k{color:#7f8ba3;min-width:145px}
.v{flex:1;word-break:break-all;font-family:ui-monospace,monospace}
.p{display:inline-block;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:700}
.ok{background:#0d3320;color:#63e6a0}.bad{background:#3a1116;color:#ff8b96}
.warn{background:#3a2c0d;color:#ffcc55}.dim{background:#1a2133;color:#8290a8}
button{width:100%;padding:13px;border:0;border-radius:11px;font-size:15px;font-weight:700;cursor:pointer;margin-top:8px}
.go{background:#2f6ef0;color:#fff}.go:disabled{background:#1a2133;color:#6b788f;cursor:not-allowed}
.sec{background:#141c2e;color:#b8c6dc}
#msg{white-space:pre-wrap;font-size:13px;margin-top:12px;color:#9fb0c9}
label{display:block;color:#7f8ba3;font-size:12px;margin:12px 0 4px}
select{width:100%;padding:10px;border-radius:9px;background:#0d1220;color:#dce6f5;border:1px solid #1c2740;font-size:14px}
</style>
<div class="w">
<h1>BLOCK 0</h1>
<p class="sub">Creates a B-20 block with no administrator and no minter, in one transaction. The address is derived from <em>your</em> wallet — connect and you will see yours before anything is sent.</p>
<label for="reseau">Network</label>
<select id="reseau">
<option value="8453">Base mainnet — real, costs gas</option>
<option value="84532">Base Sepolia — free, for trying it</option>
</select>
<!-- ⛔ LE SEL ETAIT EN DUR, ET C EST CE QUI BLOQUAIT LES GENS. Le couple (ton adresse, ce sel)
fixe l adresse du block : si tu as deja utilise ce sel, la chaine refuse, et sans champ tu
n avais AUCUN recours — la page te disait « change the salt » sans te laisser le changer.
Une page qui nomme la solution sans l offrir est plus frustrante qu une page muette. -->
<label for="sel">Salt — it fixes your address. One salt per block, per wallet.</label>
<input id="sel" spellcheck="false" autocapitalize="off"
style="width:100%;padding:10px;border-radius:9px;background:#0d1220;color:#dce6f5;border:1px solid #1c2740;font-size:14px;font-family:ui-monospace,monospace;box-sizing:border-box">
<div class="card"><div id="fiche"></div></div>
<button class="sec" id="proposer">Find a salt that is free for me</button>
<button class="sec" id="connecter">Connect wallet</button>
<button class="go" id="envoyer" disabled>Connect first</button>
<div id="msg"></div>
</div>
<script type="module">
import { encodeCreateB20, paramsAsset, encodeUpdateSupplyCap, encodeUpdateContractURI,
encodeBatchMint, expliquerRefus } from './encodeur.js';
const RESEAUX = {
8453: { nom: 'Base mainnet', rpc: 'https://mainnet.base.org', hex: '0x2105', scan: 'https://basescan.org' },
84532: { nom: 'Base Sepolia', rpc: 'https://sepolia.base.org', hex: '0x14a34', scan: 'https://sepolia.basescan.org' },
};
const FACTORY = '0xb20f000000000000000000000000000000000000';
const $ = (s) => document.querySelector(s);
const ligne = (k, v) => `<div class="row"><span class="k">${k}</span><span class="v">${v}</span></div>`;
const p = (t, c) => `<span class="p ${c}">${t}</span>`;
const dire = (t) => { $('#msg').textContent = t; };
/* ⛔ LES PARAMETRES AUDITES, lus dans la fiche — pas reecrits ici. */
const modele = await (await fetch('./A-SIGNER-mainnet.json')).json();
let compte = null, plan = null;
let n = 0;
async function rpc(chaine, m, params) {
const r = await fetch(RESEAUX[chaine].rpc, { method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: ++n, method: m, params }) });
return r.json();
}
/**
* ⛔ LE REFUS PORTE LA REPONSE. `TokenAlreadyExists(address)` transporte l adresse du block qui
* occupe deja la place — mesure : `0x15ef3a57` suivi de l adresse sur 32 octets. Dire « change
* the salt » en jetant cette adresse, c est cacher a l utilisateur le block qu il a lui-meme
* cree. On la DECODE et on la lui montre.
* ⚠️ On ne devine pas : si la donnee est trop courte, on rend null plutot qu une adresse tronquee.
*/
function adresseDuRefus(donnee) {
const d = String(donnee || '');
if (!d.startsWith('0x15ef3a57') || d.length < 74) return null;
return '0x' + d.slice(34, 74);
}
/** Le calldata POUR CE SIGNATAIRE. Seul le destinataire du mint change. */
function calldataPour(qui) {
return encodeCreateB20({
variant: 0,
saltTexte: $('#sel').value.trim() || modele.sel,
/* ⛔ `admin = address(0)` : le block naît sans administrateur. Ce n est pas une option. */
params: paramsAsset({ nom: 'Tokenized Block', symbole: 'TBLOCK',
admin: '0x0000000000000000000000000000000000000000', decimales: 18 }),
initCalls: [
encodeUpdateContractURI(modele.contractURI),
encodeUpdateSupplyCap(BigInt(modele.supply)),
encodeBatchMint([qui], [BigInt(modele.supply)]),
],
});
}
async function verifier() {
const chaine = Number($('#reseau').value);
const R = RESEAUX[chaine];
$('#envoyer').disabled = true;
if (!compte) {
$('#fiche').innerHTML = ligne('network', R.nom + ' (' + chaine + ')')
+ ligne('creates', p('CONNECT — your address decides it', 'dim'))
+ ligne('supply', modele.supply + ' <span class="k">(cap = supply, admin = none)</span>')
+ ligne('metadata', modele.contractURI);
$('#envoyer').textContent = 'Connect first';
return dire('The block address is derived from the standard, YOUR wallet address, and the salt.\nConnect and it will be shown before anything is sent.');
}
$('#envoyer').textContent = 'Checking the chain…';
const data = calldataPour(compte);
const raisons = [];
/* ⛔ ON DEMANDE A LA FACTORY. Une adresse qu on calculerait soi-meme pourrait differer. */
const appel = await rpc(chaine, 'eth_call', [{ from: compte, to: FACTORY, data }, 'latest']);
const adresse = appel.error ? null : '0x' + String(appel.result).slice(-40);
let dejaLa = null;
if (!adresse) {
const d = appel.error?.data ? String(appel.error.data) : '';
dejaLa = adresseDuRefus(d);
raisons.push(dejaLa
? 'you already created a block with this salt'
: (expliquerRefus(d) || appel.error?.message || 'the factory refuses this calldata'));
}
const code = adresse ? (await rpc(chaine, 'eth_getCode', [adresse, 'latest'])).result : null;
const libre = code === '0x';
if (adresse && !libre) raisons.push('that address already exists — change the salt');
const g = await rpc(chaine, 'eth_estimateGas', [{ from: compte, to: FACTORY, data }]);
const prix = BigInt((await rpc(chaine, 'eth_gasPrice', [])).result);
const solde = BigInt((await rpc(chaine, 'eth_getBalance', [compte, 'latest'])).result);
const gaz = g.error ? null : BigInt(g.result);
const cout = gaz === null ? null : gaz * prix;
if (cout !== null && solde < cout) raisons.push('this account cannot cover the gas on ' + R.nom);
const eth = (w) => (Number(w) / 1e18).toFixed(9);
$('#fiche').innerHTML =
ligne('network', R.nom + ' (' + chaine + ')')
+ ligne('you', compte)
+ ligne('creates', adresse
? `<a href="${R.scan}/address/${adresse}" target="_blank" rel="noopener">${adresse}</a>`
+ (libre ? ' ' + p('FREE', 'ok') : ' ' + p('TAKEN', 'bad'))
: dejaLa
/* ⛔ On MONTRE le block existant plutot que de le taire : c est le sien, il l a cree. */
? p('ALREADY YOURS', 'warn') + ` <a href="${R.scan}/address/${dejaLa}" target="_blank" rel="noopener">${dejaLa}</a>`
: p('NOT COMPUTED', 'bad'))
+ ligne('to', FACTORY + ' <span class="k">(the B-20 factory)</span>')
+ ligne('value', '0 ' + p('SENDS NO ETH', 'ok'))
+ ligne('salt', modele.sel)
+ ligne('supply', modele.supply + ' <span class="k">(cap = supply, admin = none)</span>')
+ ligne('metadata', modele.contractURI)
+ ligne('gas', gaz === null ? p('NOT MEASURED', 'warn')
: gaz.toString() + ' × ' + prix + ' wei ≈ <b>' + eth(cout) + ' ETH</b>')
+ ligne('your balance', eth(solde) + ' ETH'
+ (cout && solde >= cout ? ' <span class="k">(' + (Number(solde) / Number(cout)).toFixed(0) + '× the cost)</span>' : ''))
+ ligne('calldata', data.slice(0, 40) + '… <span class="k">(' + (data.length / 2 - 1) + ' bytes)</span>');
const pret = raisons.length === 0 && adresse && libre;
plan = pret ? { chaine, data, adresse } : null;
$('#envoyer').disabled = !pret;
$('#envoyer').textContent = pret ? 'Create it — your wallet will ask' : 'Blocked: ' + raisons[0];
/* ⛔ UN REFUS DOIT DIRE QUOI FAIRE. « change the salt » sans indiquer lequel laisse l utilisateur
* deviner ce que seule la chaine sait. On nomme le geste et le bouton qui le fait. */
dire(pret ? 'Every check passed against ' + R.nom + ', just now.\nYour wallet will open. Re-read the destination before accepting.'
: '⛔ ' + raisons.join('\n⛔ ')
+ (dejaLa ? '\n\nThat block is yours — it is linked above.\nTo create ANOTHER one, change the salt, or press « Find a salt that is free for me ».' : ''));
}
$('#sel').value = modele.sel;
/* ⛔ TOUT CHANGEMENT DU SEL INVALIDE LE PLAN. Sans ca, le bouton garderait le calldata d avant
* et signerait un autre block que celui affiche — l ecart ne se verrait qu apres la transaction. */
let minuteur = null;
$('#sel').oninput = () => { plan = null; $('#envoyer').disabled = true;
$('#envoyer').textContent = 'Salt changed — re-checking…';
clearTimeout(minuteur); minuteur = setTimeout(verifier, 600); };
$('#reseau').onchange = () => { plan = null; verifier(); };
/**
* Propose un sel LIBRE pour ce compte, en demandant a la chaine — pas en inventant.
* ⛔ « change the salt » sans dire lequel laisse l utilisateur deviner, et il ne peut pas :
* seule la factory sait si un couple (adresse, sel) est pris.
* ⚠️ Borne : on essaie un nombre fini de variantes et on le DIT si aucune ne passe.
*/
async function proposerSel() {
if (!compte) return dire('Connect first — a free salt depends on your address.');
const chaine = Number($('#reseau').value);
const base = ($('#sel').value.trim() || modele.sel).replace(/-\d+$/, '');
dire('Asking the chain for a free salt…');
for (let i = 2; i <= 12; i++) {
const essai = base + '-' + i;
const d = encodeCreateB20({ variant: 0, saltTexte: essai,
params: paramsAsset({ nom: 'Tokenized Block', symbole: 'TBLOCK',
admin: '0x0000000000000000000000000000000000000000', decimales: 18 }),
initCalls: [encodeUpdateContractURI(modele.contractURI),
encodeUpdateSupplyCap(BigInt(modele.supply)),
encodeBatchMint([compte], [BigInt(modele.supply)])] });
const r = await rpc(chaine, 'eth_call', [{ from: compte, to: FACTORY, data: d }, 'latest']);
if (!r.error) { $('#sel').value = essai; dire('« ' + essai +' » is free for your address.'); return verifier(); }
}
dire('⛔ Tried « ' + base + '-2 » through « ' + base + '-12 », all taken.\nType something of your own.');
}
$('#proposer').onclick = proposerSel;
$('#connecter').onclick = async () => {
if (!window.ethereum) return dire("No wallet in this browser.\nOpen this page inside your wallet's browser.");
try {
[compte] = await window.ethereum.request({ method: 'eth_requestAccounts' });
dire('Connected: ' + compte);
await verifier();
} catch (e) {
const refus = e && (e.code === 4001 || /reject|denied/i.test(String(e.message || e)));
dire(refus ? 'You declined the connection.' : 'Could not connect: ' + (e.message || e));
}
};
$('#envoyer').onclick = async () => {
if (!plan) return dire('Nothing verified yet — press Connect.');
const R = RESEAUX[plan.chaine];
try {
const id = parseInt(await window.ethereum.request({ method: 'eth_chainId' }), 16);
if (id !== plan.chaine) {
dire('Switching your wallet to ' + R.nom + '…');
await window.ethereum.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: R.hex }] });
/* ⛔ ON REVERIFIE APRES L ALLER-RETOUR. Le wallet a pu changer de compte en meme temps,
* et un consentement donne avant un aller-retour n en est plus un apres. */
const id2 = parseInt(await window.ethereum.request({ method: 'eth_chainId' }), 16);
if (id2 !== plan.chaine) return dire('⛔ Wallet is still not on ' + R.nom + '. Nothing was sent.');
const [a] = await window.ethereum.request({ method: 'eth_accounts' });
if (!a || a.toLowerCase() !== compte.toLowerCase()) {
compte = a || null; await verifier();
return dire('⛔ Your wallet switched account. Everything was re-checked — press the button again.');
}
}
dire('Your wallet will open now.\nThe destination must be ' + FACTORY);
const hash = await window.ethereum.request({ method: 'eth_sendTransaction',
params: [{ from: compte, to: FACTORY, value: '0x0', data: plan.data }] });
dire('Sent: ' + hash + '\nWaiting for the receipt…');
for (let i = 0; i < 40; i++) {
await new Promise((r) => setTimeout(r, 3000));
const r = (await rpc(plan.chaine, 'eth_getTransactionReceipt', [hash])).result;
if (!r) continue;
/* ⛔ UN ENVOI REUSSI N EST PAS UNE CREATION. On relit le CODE a l adresse. */
const c = (await rpc(plan.chaine, 'eth_getCode', [plan.adresse, 'latest'])).result;
if (r.status === '0x1' && c === '0xef') {
return dire('✅ Your block exists.\n' + plan.adresse + '\ncode: ' + c
+ '\n' + R.scan + '/tx/' + hash);
}
return dire(r.status === '0x1'
? '⚠️ The transaction succeeded but the code at that address is "' + c + '", not 0xef.\n' + R.scan + '/tx/' + hash
: '⛔ REVERTED on-chain. Nothing was created.\n' + R.scan + '/tx/' + hash);
}
dire('Sent but no receipt yet.\n' + hash);
} catch (e) {
const refus = e && (e.code === 4001 || /reject|denied/i.test(String(e.message || e)));
dire(refus ? 'You declined. Nothing was sent.' : 'Failed: ' + (e.message || e));
}
};
verifier();
</script>