/ web / index.html
index.html
  1  <!DOCTYPE html>
  2  <html lang="en">
  3  
  4  <head>
  5      <meta charset="UTF-8">
  6      <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7      <script src="https://cdn.jsdelivr.net/npm/web3@4.6.0/dist/web3.min.js"
  8          integrity="sha512-DTSUnB4owPx74KyQnBL+XPSCI8elZpjR0dnHmSnHkmTirIzI/9ANYxuDZ87TVsQ1E+8rQB/V4EAaUfKN+h42+w=="
  9          crossorigin="anonymous"></script>
 10  
 11  
 12      <title>Fluence Token Proof Generator</title>
 13      <style>
 14          html {
 15              width: 100%;
 16              height: 100%;
 17          }
 18  
 19          body {
 20              font-family: Arial, sans-serif;
 21              margin: 0;
 22              padding: 0;
 23              background-color: #f0f0f0;
 24              width: 100%;
 25              height: 100%;
 26          }
 27  
 28          .outer {
 29              width: 100%;
 30              height: 100%;
 31              justify-content: center;
 32              align-items: center;
 33              text-align: center;
 34              display: flex;
 35          }
 36  
 37          .container {
 38              margin: auto;
 39          }
 40  
 41          input[type="text"] {
 42              padding: 10px;
 43              width: 320px;
 44              border-radius: 5px;
 45              border: 1px solid #ccc;
 46              font-size: 16px;
 47              margin-bottom: 10px;
 48              text-align: center;
 49              box-sizing: border-box;
 50          }
 51  
 52          input[type="submit"] {
 53              padding: 10px 20px;
 54              background-color: #4CAF50;
 55              color: white;
 56              border: none;
 57              border-radius: 5px;
 58              cursor: pointer;
 59              font-size: 16px;
 60              text-align: center;
 61              width: 320px;
 62              box-sizing: border-box;
 63          }
 64  
 65          input[type="submit"]:hover {
 66              background-color: #45a049;
 67          }
 68  
 69          .progress-container {
 70              width: 320px;
 71              margin: 20px auto;
 72              border: 1px solid gray;
 73              border-radius: 5px;
 74          }
 75  
 76          .progress-bar {
 77              width: 0;
 78              height: 20px;
 79              background-color: #4CAF50;
 80              border-radius: 5px;
 81              transition: width 0.3s ease;
 82          }
 83  
 84          .overlay {
 85              position: fixed;
 86              top: 0;
 87              left: 0;
 88              width: 100%;
 89              height: 100%;
 90              background-color: rgba(0, 0, 0, 0.5);
 91              justify-content: center;
 92              align-items: center;
 93              z-index: 999;
 94              overflow-y: auto;
 95          }
 96  
 97          .popup {
 98              background-color: white;
 99              padding: 20px;
100              border-radius: 5px;
101              box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
102              width: 50%;
103              margin: 10px auto;
104          }
105  
106          .code-container {
107              border: 1px solid #ccc;
108              border-radius: 10px;
109              margin-top: 1em;
110              margin-bottom: 1em;
111          }
112  
113          .right-button {
114              text-align: right;
115              padding-right: 5px;
116              padding-bottom: 5px;
117          }
118  
119          .right-button button {
120              padding: 10px 20px;
121              background-color: #4CAF50;
122              color: white;
123              border: none;
124              border-radius: 5px;
125              cursor: pointer;
126              font-size: 16px;
127              text-align: center;
128          }
129  
130          .right-button button:hover {
131              background-color: #45a049;
132  
133          }
134  
135          .right-button button.close {
136              background-color: red;
137          }
138  
139          .right-button button.close:hover {
140              background-color: #aa0000;
141          }
142  
143          code {
144              word-wrap: break-word;
145              white-space: normal;
146          }
147  
148          .popup input[type=text] {
149              box-sizing: border-box;
150          }
151      </style>
152  </head>
153  
154  <body>
155      <div class="outer">
156          <div class="container">
157              <form action="#">
158                  <input type="text" id="username" placeholder="GitHub username">
159                  <br>
160                  <input type="submit" id="submit" value="Check Eligibility">
161              </form>
162              <div class="progress-container" style="display: none;">
163                  <div class="progress-bar" id="progressBar"></div>
164              </div>
165              <script>
166                  let data = null;
167                  const web3 = new Web3();
168  
169                  const copyCode = e => {
170                      var codeElement = e.target.parentElement.parentElement.querySelector('code');
171                      var range = document.createRange();
172                      range.selectNode(codeElement);
173                      window.getSelection().removeAllRanges();
174                      window.getSelection().addRange(range);
175                      document.execCommand('copy');
176                      window.getSelection().removeAllRanges();
177                      alert('Copied!');
178                  }
179  
180                  const arraySmaller = (a, b) => {
181                      for (let i = 0; i < a.length; i++) {
182                          if (a[i] < b[i]) {
183                              return true;
184                          }
185                          if (b[i] < a[i]) {
186                              return false;
187                          }
188                      }
189                      return false;
190                  }
191  
192                  async function storeInIndexedDB(data, dbName, storeName, key) {
193                      // Open IndexedDB
194                      const dbOpenRequest = indexedDB.open(dbName);
195  
196                      return new Promise((resolve, reject) => {
197                          dbOpenRequest.onerror = function (event) {
198                              reject("Error opening IndexedDB database");
199                          };
200  
201                          dbOpenRequest.onsuccess = async function (event) {
202                              const db = event.target.result;
203  
204                              // Create object store
205                              const transaction = db.transaction([storeName], "readwrite");
206                              const objectStore = transaction.objectStore(storeName);
207  
208                              // Store the Uint8Array
209                              try {
210                                  await new Promise((resolve, reject) => {
211                                      const putRequest = objectStore.put(data, key);
212                                      putRequest.onsuccess = resolve;
213                                      putRequest.onerror = reject;
214                                  });
215                                  resolve("Data stored successfully");
216                              } catch (error) {
217                                  reject("Error storing data: " + error.message);
218                              }
219                          };
220  
221                          dbOpenRequest.onupgradeneeded = function (event) {
222                              const db = event.target.result;
223  
224                              // Create object store if not exists
225                              if (!db.objectStoreNames.contains(storeName)) {
226                                  db.createObjectStore(storeName);
227                              }
228                          };
229                      });
230                  }
231  
232                  async function getFromIndexedDB(dbName, storeName, key) {
233                      // Open IndexedDB
234                      const dbOpenRequest = indexedDB.open(dbName);
235  
236                      return new Promise((resolve, reject) => {
237                          dbOpenRequest.onerror = function (event) {
238                              reject("Error opening IndexedDB database");
239                          };
240  
241                          dbOpenRequest.onsuccess = async function (event) {
242                              const db = event.target.result;
243  
244                              const transaction = db.transaction([storeName], "readonly");
245                              const objectStore = transaction.objectStore(storeName);
246  
247                              try {
248                                  const getRequest = objectStore.get(key);
249                                  getRequest.onsuccess = function (event) {
250                                      const result = event.target.result;
251                                      if (result) {
252                                          resolve(result);
253                                      } else {
254                                          reject("Data not found in IndexedDB");
255                                      }
256                                  };
257                                  getRequest.onerror = function (event) {
258                                      reject("Error fetching data from IndexedDB");
259                                  };
260                              } catch (error) {
261                                  reject("Error fetching data: " + error.message);
262                              }
263                          };
264  
265                          dbOpenRequest.onupgradeneeded = function (event) {
266                              // Handle database upgrade if necessary
267                          };
268                      });
269                  }
270  
271  
272                  class MerkleTree {
273                      constructor(accounts) {
274                          this._tree = [];
275                          this._total_depth = 0;
276                          const nodes = this._createLeafs(accounts);
277                          this._genTree(nodes);
278                      }
279  
280                      _genTree(nodes) {
281                          this._tree.push(nodes);
282                          this._total_depth = Math.ceil(Math.log2(nodes.length));
283  
284                          while (nodes.length > 1) {
285                              nodes = this._genPrevNodes(nodes);
286                              this._tree.push(nodes);
287                          }
288                      }
289  
290                      _genPrevNodes(nodes) {
291                          const newNodes = [];
292                          const length = nodes.length;
293  
294                          for (let i = 0; i < length; i += 2) {
295                              if (length % 2 !== 0 && i + 1 >= length) {
296                                  newNodes.push(nodes[i]);
297                                  break;
298                              }
299  
300                              const a = nodes[i];
301                              const b = nodes[i + 1];
302  
303                              if (arraySmaller(a, b)) {
304                                  let result = new Uint8Array(64);
305                                  result.set(a);
306                                  result.set(b, 32);
307                                  newNodes.push(web3.utils.hexToBytes(web3.utils.sha3(result)));
308                              } else {
309                                  let result = new Uint8Array(64);
310                                  result.set(b);
311                                  result.set(a, 32);
312                                  newNodes.push(web3.utils.hexToBytes(web3.utils.sha3(result)));
313                              }
314                          }
315  
316                          return newNodes;
317                      }
318  
319                      _createLeafs(accounts) {
320                          return accounts.map((account, i) => {
321                              return web3.utils.hexToBytes(web3.utils.soliditySha3(
322                                  {
323                                      type: "uint32",
324                                      value: i
325                                  },
326                                  {
327                                      type: "bytes20",
328                                      value: account
329                                  }));
330                          });
331                      }
332  
333                      getProof(index) {
334                          const proof = [];
335                          for (let nodes of this._tree) {
336                              const length = nodes.length;
337                              if (length === 1) break;
338  
339                              if (length % 2 !== 0 && index === length - 1) {
340                                  index = Math.floor(index / 2);
341                                  continue;
342                              }
343  
344                              if (index % 2 === 0) {
345                                  proof.push(nodes[index + 1]);
346                              } else {
347                                  proof.push(nodes[index - 1]);
348                              }
349  
350                              index = Math.floor(index / 2);
351                          }
352                          return proof.map(node => web3.utils.bytesToHex(node));
353                      }
354  
355                      getRoot() {
356                          return this._tree[this._total_depth][0];
357                      }
358                  }
359  
360                  const generateProof = (popup, address, privateKey) => {
361                      popup.innerHTML = `
362                  <div>Congratulations! This is your proof which you can supply to <a href="https://claim.fluence.network/">claim.fluence.network</a>:</div>
363                  <div class="code-container">
364                      <pre><code class="proof"></code></pre>
365                      <div class="right-button"><button onclick="copyCode(event)">Copy</button></div>
366                  </div>
367                  `;
368  
369                      let objs = [...popup.querySelectorAll('.proof')];
370                      let proofElement = objs[objs.length - 1];
371                      let proof = generateProofFromAccount(address, privateKey);
372                      proofElement.textContent = proof;
373                  }
374  
375                  const enterAddress = (popup, privateKey) => {
376                      popup.innerHTML = `
377                  <div>Enter the Ethereum address to which you plan to receive the airdrop. You need to make a claim transaction from the entered address!</div>
378                  <form style="padding-top: 1em;">
379                      <input type="text" style="width: 100%;" class="ethaddr" placeholder="Ethereum Address" />
380                      <input type="submit" style="width: 100%;" value="Continue" />
381                  </form>
382                  `;
383  
384                      popup.querySelector("form").addEventListener("submit", e => {
385                          e.preventDefault();
386                          let ethAddr = popup.querySelector(".ethaddr").value;
387                          if (!ethAddr.match(/^0x[a-fA-F0-9]{40}$/)) {
388                              alert("Invalid format. Ethereum address must be of format ^0x[a-fA-F0-9]{40}$");
389                              return;
390                          }
391                          generateProof(popup, ethAddr, privateKey);
392                      });
393                  };
394  
395                  const showDecryptionInstructions = (popup, command) => {
396                      popup.innerHTML = `
397                  <div>Use the <a href="https://github.com/FiloSottile/age">age</a> tool to decrypt the payload. Replace <tt>&lt;path to private key&gt;</tt> with the path to your private key:</div>
398                  <div class="code-container">
399                      <pre><code class="command"></code></pre>
400                      <div class="right-button"><button onclick="copyCode(event)">Copy</button></div>
401                  </div>
402                  <form>
403                      <input type="text" style="width: 100%;" class="privkey" placeholder="Decrypted payload" />
404                      <input type="submit" style="width: 100%;" value="Continue" />
405                  </form>
406                  `;
407  
408                      let objs = [...popup.querySelectorAll('.command')];
409                      let commandElement = objs[objs.length - 1];
410                      commandElement.textContent = command;
411  
412                      popup.querySelector("form").addEventListener("submit", e => {
413                          e.preventDefault();
414                          let privKey = popup.querySelector(".privkey").value;
415                          if (!privKey.match(/^0x[a-fA-F0-9]{64}$/)) {
416                              alert("Invalid format. Decrypted payload must be of format ^0x[a-fA-F0-9]{64}$");
417                              return;
418                          }
419                          enterAddress(popup, privKey);
420                      });
421                  };
422  
423                  const showInstructions = pubKeysAndCommands => {
424                      let overlay = document.createElement("div");
425                      overlay.className = "overlay";
426  
427                      let popup = document.createElement("div");
428                      let close = document.createElement("div");
429                      close.innerHTML = '<div class="right-button"><button class="close">Close</button></div>';
430                      popup.appendChild(close);
431                      close.querySelector('button').addEventListener("click", () => {
432                          document.body.removeChild(overlay);
433                      });
434  
435                      popup.className = "popup";
436  
437                      overlay.appendChild(popup);
438  
439                      let popupInner = document.createElement("div");
440                      popup.appendChild(popupInner);
441                      popup = popupInner;
442  
443                      document.body.appendChild(overlay);
444  
445                      let innerHTML = `
446                  <h2>The account is eligible for receiving the airdrop!</h2>
447                  <div>The following SSH public keys have been included:</div>
448                  `;
449                      for (let [pubKey, command] of pubKeysAndCommands) {
450                          innerHTML += `
451                      <div class="code-container">
452                          <pre><code class="key"></code></pre>
453                          <div class="right-button"><button class="use-key">Use this key</button></div>
454                      </div>`;
455                      }
456  
457                      popup.innerHTML = innerHTML;
458  
459                      let i = 0;
460                      for (let [pubKey, command] of pubKeysAndCommands) {
461                          let pubKeyElement = [...popup.querySelectorAll('.key')][i];
462                          pubKeyElement.textContent = pubKey;
463  
464                          let useKeyElement = [...popup.querySelectorAll('.use-key')][i];
465                          useKeyElement.addEventListener("click", e => showDecryptionInstructions(popup, command));
466  
467                          i += 1;
468                      }
469                  };
470  
471                  const generateProofFromAccount = (ethereumAddress, privateKey) => {
472                      let account = web3.eth.accounts.privateKeyToAccount(privateKey);
473  
474                      let tempEthAddress = account.address.toLowerCase();
475                      let tree = new MerkleTree(data.addresses);
476                      let index = data.addresses.indexOf(tempEthAddress);
477                      let proof = tree.getProof(index);
478                      // String replacement for exact Python code behavior
479                      let base64MerkleProof = btoa(JSON.stringify(proof).replace(/","/g, '", "'));
480                      let signature = web3.eth.accounts.sign(ethereumAddress, privateKey).signature;
481  
482  
483                      return [
484                          index, tempEthAddress, signature, base64MerkleProof
485                      ].join(",");
486                  };
487  
488                  const finish = metadata => {
489                      let username = document.querySelector("#username").value.trim().toLowerCase();
490                      let eligibleUsers = Object.keys(metadata.encryptedKeys).filter(x => x.trim().toLowerCase() == username);
491                      if (eligibleUsers.length != 1) {
492                          alert("User not eligible");
493                          return;
494                      }
495  
496                      let userData = metadata.encryptedKeys[eligibleUsers[0]];
497                      let publicKeys = Object.keys(userData);
498                      let result = [];
499                      for (let i = 0; i < publicKeys.length; i++) {
500                          let publicKey = publicKeys[i];
501                          let encrytedFile = userData[publicKey];
502  
503                          let encryptedBase64 = btoa(encrytedFile);
504                          let command = 'echo "' + encryptedBase64 + '" | base64 --decode | age --decrypt --identity <path to private key>';
505                          result.push([publicKey, command]);
506                      }
507                      showInstructions(result);
508                  }
509  
510                  window.addEventListener("load", () => {
511                      getFromIndexedDB("metadata", "metadata", "metadata").then(d => {
512                          data = d
513                      });
514                  });
515  
516                  document.querySelector("form").addEventListener("submit", event => {
517                      event.preventDefault();
518  
519                      if (data != null) {
520                          finish(data);
521                          return;
522                      }
523  
524                      var progressBar = document.getElementById("progressBar");
525                      var progressContainer = document.querySelector(".progress-container");
526                      progressContainer.style.display = "block";
527  
528                      fetch("/metadata.json")
529                          .then(response => {
530                              const totalBytes = response.headers.get('content-length');
531                              let loadedBytes = 0;
532  
533                              const reader = response.body.getReader();
534  
535                              let byteStream = new Uint8Array();
536  
537                              const pump = () => {
538                                  return reader.read().then(({ value, done }) => {
539                                      if (done) {
540                                          progressBar.style.width = "100%";
541                                          progressContainer.style.display = "none";
542                                          let sha3sum = web3.utils.sha3(byteStream);
543                                          console.log("metadata.json SHA3", sha3sum);
544                                          if (sha3sum != "0x998318b214a74aab65f91bea0459815f899c7e6f279a0a77fb1aa55dbf7f77df") {
545                                              throw new Error("metadata.json integrity violated");
546                                          }
547                                          data = JSON.parse(new TextDecoder().decode(byteStream));
548                                          storeInIndexedDB(data, "metadata", "metadata", "metadata");
549                                          finish(data);
550                                          return;
551                                      }
552                                      loadedBytes += value.length;
553                                      progressBar.style.width = Math.floor((loadedBytes / totalBytes) * 100) + "%";
554                                      const newStream = new Uint8Array(byteStream.length + value.length);
555                                      newStream.set(byteStream);
556                                      newStream.set(value, byteStream.length);
557                                      byteStream = newStream;
558  
559                                      return pump();
560                                  });
561                              }
562  
563                              return pump();
564                          })
565                          .catch(error => {
566                              alert('Error downloading file: ' + error.toString());
567                          });
568                  });
569              </script>
570          </div>
571      </div>
572  </body>
573  
574  </html>