]> git.ardis.lu - event-hashes/commitdiff
Initial commit
authorArdis Lu <ardisglu@gmail.com>
Fri, 5 Jan 2024 04:45:36 +0000 (20:45 -0800)
committerArdis Lu <ardisglu@gmail.com>
Fri, 5 Jan 2024 04:45:36 +0000 (20:45 -0800)
.github/etherscan.webp [new file with mode: 0644]
.github/event-hashes.webp [new file with mode: 0644]
Inter.woff2 [new file with mode: 0644]
Inter_600.woff2 [new file with mode: 0644]
Inter_700.woff2 [new file with mode: 0644]
README.md [new file with mode: 0644]
index.html [new file with mode: 0644]
keccak256.js [new file with mode: 0644]

diff --git a/.github/etherscan.webp b/.github/etherscan.webp
new file mode 100644 (file)
index 0000000..f26e360
Binary files /dev/null and b/.github/etherscan.webp differ
diff --git a/.github/event-hashes.webp b/.github/event-hashes.webp
new file mode 100644 (file)
index 0000000..68ad260
Binary files /dev/null and b/.github/event-hashes.webp differ
diff --git a/Inter.woff2 b/Inter.woff2
new file mode 100644 (file)
index 0000000..c659f5e
Binary files /dev/null and b/Inter.woff2 differ
diff --git a/Inter_600.woff2 b/Inter_600.woff2
new file mode 100644 (file)
index 0000000..fff3df9
Binary files /dev/null and b/Inter_600.woff2 differ
diff --git a/Inter_700.woff2 b/Inter_700.woff2
new file mode 100644 (file)
index 0000000..fff3df9
Binary files /dev/null and b/Inter_700.woff2 differ
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..2ad9318
--- /dev/null
+++ b/README.md
@@ -0,0 +1,32 @@
+# event-hashes
+
+This is a minimal web utility to calculate the `keccak256` hashes of the normalized event signatures of all the events in a Solidity code block. This hash can then be used with indexers like Etherscan to search for specific logs in the EVM history.
+
+> [!WARNING]<br>
+> This tool won't work with [user-defined value types](https://docs.soliditylang.org/en/latest/types.html#user-defined-value-types). For events that include these types, you must manually convert the user-defined value type into its equivalent primitive type.
+
+## Example
+
+![Demonstration of the event-hashes web UI hashing the ERC-20 Transfer event.](./.github/event-hashes.webp)
+
+Given the [standard ERC-20 `Transfer` event](https://eips.ethereum.org/EIPS/eip-20#events):
+
+```
+event Transfer(address indexed _from, address indexed _to, uint256 _value)
+```
+
+The tool will normalize the event signature (i.e., remove spaces, input names, and extraneous keywords):
+
+```
+Transfer(address,address,uint256)
+```
+
+Then calculate the `keccak256` hash of the UTF-8 encoded bytes of the normalized signature:
+
+```
+0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
+```
+
+This hash may then be used in another tool to filter a smart contract's logs, e.g. [in Etherscan](https://etherscan.io/address/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48#events):
+
+![The "Events" page of a token smart contract on Etherscan, filtered to only the events matching the previously calculated Keccak-256 hash.](./.github/etherscan.webp)
diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..e7c7a8c
--- /dev/null
@@ -0,0 +1,245 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <meta name="description" content="A minimal web utility to calculate the Keccak-256 hashes for all the events in a Solidity code block.">
+  <meta property="og:title" content="Event Hashes">
+  <meta property="og:description" content="A minimal web utility to calculate the Keccak-256 hashes for all the events in a Solidity code block.">
+  <meta property="og:type" content="website">
+  <meta property="og:url" content="https://event-hashes.ardis.lu/">
+  <link rel="icon" href="data:,">
+  <link rel="preload" href="/Inter.woff2" as="font" crossorigin>
+  <link rel="preload" href="/Inter_600.woff2" as="font" crossorigin>
+  <link rel="preload" href="/Inter_700.woff2" as="font" crossorigin>
+  <link rel="modulepreload" href="/keccak256.js">
+  <link rel="icon" href="data:,">
+  <title>Event Hashes</title>
+  <style>
+    @font-face {
+      font-family: 'Inter';
+      font-weight: 400;
+      src: url('/Inter.woff2') format('woff2');
+    }
+
+    @font-face {
+      font-family: 'Inter';
+      font-weight: 600;
+      src: url('/Inter_600.woff2') format('woff2');
+    }
+
+    @font-face {
+      font-family: 'Inter';
+      font-weight: 700;
+      src: url('/Inter_700.woff2') format('woff2');
+    }
+
+    * {
+      margin: 0;
+      box-sizing: border-box;
+      font-family: inherit;
+      font-size: 1rem;
+    }
+
+    :root {
+      font-family: 'Inter', sans-serif;
+    }
+
+    body {
+      min-block-size: 100dvb;
+      margin-inline: 8px;
+      background: hsl(216deg 50% 95%);
+      color: hsl(216deg 50% 5%);
+      display: grid;
+      grid-template-rows: auto 1fr auto;
+    }
+
+    header,
+    footer {
+      margin-block: 36px;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+    }
+
+    header h1 {
+      font-size: 2rem;
+    }
+
+    header p {
+      text-align: center;
+    }
+
+    main {
+      display: flex;
+      flex-direction: column;
+      inline-size: min(100dvi - 16px, 720px);
+      margin-inline: auto;
+      gap: 8px;
+    }
+
+    label {
+      font-weight: 700;
+    }
+
+    textarea {
+      align-self: center;
+      min-inline-size: 100%;
+      max-inline-size: calc(100dvi - 32px);
+      min-block-size: 15rem;
+      padding: 4px;
+      border-radius: 4px;
+      border: 1px solid hsl(216deg 50% 80%);
+    }
+
+    table {
+      display: none;
+      border-collapse: collapse;
+      overflow-x: auto;
+      align-self: center;
+    }
+
+    tr {
+      transition: background-color 400ms;
+    }
+
+    tr:hover,
+    tr:focus-within {
+      background: hsl(222deg 50% 90%);
+      transition: background-color 200ms;
+    }
+
+    th,
+    td {
+      line-height: 2;
+      padding-block: 8px;
+      padding-inline: 4px 16px;
+    }
+
+    a {
+      font-weight: 700;
+      text-decoration: none;
+    }
+
+    a:hover,
+    a:focus {
+      text-decoration: underline;
+    }
+  </style>
+</head>
+
+<body>
+  <header>
+    <h1>Event Hashes</h1>
+    <p>Enter Solidity code below to calculate the Keccak-256 hashes for all the events.</p>
+  </header>
+
+  <main>
+    <label for="code">Solidity code</label>
+    <textarea id="code" placeholder="event Transfer(address indexed _from, address indexed _to, uint256 _value)" autocomplete="off"></textarea>
+
+    <table>
+      <thead>
+        <tr>
+          <th>Normalized Event Signature</th>
+          <th>Keccak-256 Hash</th>
+        </tr>
+      </thead>
+      <tbody></tbody>
+    </table>
+  </main>
+
+  <footer>
+    <a href="https://github.com/ardislu/event-hashes">GitHub</a>
+  </footer>
+
+  <script type="module">
+    // From noble-hashes v1.3.3
+    import { keccak256 } from '/keccak256.js';
+
+    function normalizeEvent(event) {
+      // Clean all characters that definitely do not affect the signature
+      const commentIndex = event.indexOf('//');
+      event = commentIndex === -1 ? event : event.substring(0, commentIndex);
+      event = event.trim();
+
+      // Split the event name and the event topics
+      const eventRegex = /^(event )?(?<name>[a-zA-Z$_][a-zA-Z0-9$_]*)\((?<topics>.*?)(?!.+\))/;
+      const { name, topics } = eventRegex.exec(event).groups;
+
+      // Drop parameter names and extraneous whitespace, if there are any. Preserves tuple parentheses.
+      // E.g. "(uint abc, uint256, bool test)[]" --> ['(uint', 'uint256', 'bool)[]']
+      const topicArray = topics.split(',').map(topic => {
+        const trimmed = topic.trim();
+        const trailingParens = trimmed.match(/\)*[\[\]]*$/)[0] ?? '';
+        const split = trimmed.split(' ');
+        return `${split[0]}${split.length > 1 ? trailingParens : ''}`;
+      });
+
+      // Convert types from Solidity to ABI (e.g., 'uint' --> 'uint256')
+      // Reference: https://docs.soliditylang.org/en/latest/abi-spec.html#types
+      for (const [index, topic] of topicArray.entries()) {
+        if (/int[1-9]/.test(topic) || /uint[1-9]/.test(topic)) {
+          continue;
+        }
+        topicArray[index] = topic
+          .replace('int', 'int256') // Includes uint --> uint256
+          .replace('contract', 'address')
+          .replace('enum', 'uint8');
+      }
+
+      const types = topicArray.join(',');
+      return `${name}(${types})`;
+    }
+
+    function getEvents(code) {
+      const events = new Set();
+      for (let line of code.split('\n')) {
+        line = line.trim();
+        if (line === '') {
+          continue;
+        }
+        try {
+          const normalizedEvent = normalizeEvent(line);
+          events.add(normalizedEvent);
+        }
+        catch {
+          continue;
+        }
+      }
+      return Array.from(events);
+    }
+
+    function debounce(fn, wait) {
+      let timeoutId;
+      return () => {
+        clearTimeout(timeoutId);
+        timeoutId = setTimeout(fn, wait);
+      }
+    }
+
+    const encoder = new TextEncoder();
+    const textarea = document.querySelector('textarea');
+    const table = document.querySelector('table');
+    const tableBody = document.querySelector('table tbody');
+    textarea.addEventListener('input', debounce(() => {
+      table.style.setProperty('display', 'none');
+      tableBody.innerHTML = '';
+
+      const events = getEvents(textarea.value);
+      if (events.length === 0) {
+        return;
+      }
+
+      for (const event of events) {
+        const byteArray = keccak256(encoder.encode(event));
+        const hash = `0x${[...byteArray].map(b => b.toString(16).padStart(2, '0')).join('')}`;
+        tableBody.insertAdjacentHTML('beforeend', `<tr><td>${event}</td><td>${hash}</td></tr>`);
+      }
+      table.style.setProperty('display', 'revert');
+    }, 250));
+  </script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/keccak256.js b/keccak256.js
new file mode 100644 (file)
index 0000000..6531e46
--- /dev/null
@@ -0,0 +1,6 @@
+function g(t){if(!Number.isSafeInteger(t)||t<0)throw new Error(`Wrong positive integer: ${t}`)}function R(t){return t instanceof Uint8Array||t!=null&&typeof t=="object"&&t.constructor.name==="Uint8Array"}function b(t,...e){if(!R(t))throw new Error("Expected Uint8Array");if(e.length>0&&!e.includes(t.length))throw new Error(`Expected Uint8Array of length ${e}, not of length=${t.length}`)}function k(t,e=!0){if(t.destroyed)throw new Error("Hash instance has been destroyed");if(e&&t.finished)throw new Error("Hash#digest() has already been called")}function L(t,e){b(t);let n=e.outputLen;if(t.length<n)throw new Error(`digestInto() expects output buffer of length at least ${n}`)}var d=BigInt(4294967295),_=BigInt(32);function V(t,e=!1){return e?{h:Number(t&d),l:Number(t>>_&d)}:{h:Number(t>>_&d)|0,l:Number(t&d)|0}}function A(t,e=!1){let n=new Uint32Array(t.length),r=new Uint32Array(t.length);for(let s=0;s<t.length;s++){let{h:i,l:o}=V(t[s],e);[n[s],r[s]]=[i,o]}return[n,r]}var m=(t,e,n)=>t<<n|e>>>32-n,B=(t,e,n)=>e<<n|t>>>32-n,O=(t,e,n)=>e<<n-32|t>>>64-n,I=(t,e,n)=>t<<n-32|e>>>64-n;var E=t=>new Uint32Array(t.buffer,t.byteOffset,Math.floor(t.byteLength/4));function M(t){return t instanceof Uint8Array||t!=null&&typeof t=="object"&&t.constructor.name==="Uint8Array"}var W=new Uint8Array(new Uint32Array([287454020]).buffer)[0]===68;if(!W)throw new Error("Non little-endian hardware is not supported");function D(t){if(typeof t!="string")throw new Error(`utf8ToBytes expected string, got ${typeof t}`);return new Uint8Array(new TextEncoder().encode(t))}function w(t){if(typeof t=="string"&&(t=D(t)),!M(t))throw new Error(`expected Uint8Array, got ${typeof t}`);return t}var x=class{clone(){return this._cloneInto()}},et={}.toString;function U(t){let e=r=>t().update(w(r)).digest(),n=t();return e.outputLen=n.outputLen,e.blockLen=n.blockLen,e.create=()=>t(),e}function H(t){let e=(r,s)=>t(s).update(w(r)).digest(),n=t({});return e.outputLen=n.outputLen,e.blockLen=n.blockLen,e.create=r=>t(r),e}var[F,j,X]=[[],[],[]],P=BigInt(0),h=BigInt(1),v=BigInt(2),q=BigInt(7),z=BigInt(256),G=BigInt(113);for(let t=0,e=h,n=1,r=0;t<24;t++){[n,r]=[r,(2*n+3*r)%5],F.push(2*(5*r+n)),j.push((t+1)*(t+2)/2%64);let s=P;for(let i=0;i<7;i++)e=(e<<h^(e>>q)*G)%z,e&v&&(s^=h<<(h<<BigInt(i))-h);X.push(s)}var[J,K]=A(X,!0),S=(t,e,n)=>n>32?O(t,e,n):m(t,e,n),T=(t,e,n)=>n>32?I(t,e,n):B(t,e,n);function Q(t,e=24){let n=new Uint32Array(10);for(let r=24-e;r<24;r++){for(let o=0;o<10;o++)n[o]=t[o]^t[o+10]^t[o+20]^t[o+30]^t[o+40];for(let o=0;o<10;o+=2){let c=(o+8)%10,p=(o+2)%10,l=n[p],u=n[p+1],N=S(l,u,1)^n[c],C=T(l,u,1)^n[c+1];for(let a=0;a<50;a+=10)t[o+a]^=N,t[o+a+1]^=C}let s=t[2],i=t[3];for(let o=0;o<24;o++){let c=j[o],p=S(s,i,c),l=T(s,i,c),u=F[o];s=t[u],i=t[u+1],t[u]=p,t[u+1]=l}for(let o=0;o<50;o+=10){for(let c=0;c<10;c++)n[c]=t[o+c];for(let c=0;c<10;c++)t[o+c]^=~n[(c+2)%10]&n[(c+4)%10]}t[0]^=J[r],t[1]^=K[r]}n.fill(0)}var y=class t extends x{constructor(e,n,r,s=!1,i=24){if(super(),this.blockLen=e,this.suffix=n,this.outputLen=r,this.enableXOF=s,this.rounds=i,this.pos=0,this.posOut=0,this.finished=!1,this.destroyed=!1,g(r),0>=this.blockLen||this.blockLen>=200)throw new Error("Sha3 supports only keccak-f1600 function");this.state=new Uint8Array(200),this.state32=E(this.state)}keccak(){Q(this.state32,this.rounds),this.posOut=0,this.pos=0}update(e){k(this);let{blockLen:n,state:r}=this;e=w(e);let s=e.length;for(let i=0;i<s;){let o=Math.min(n-this.pos,s-i);for(let c=0;c<o;c++)r[this.pos++]^=e[i++];this.pos===n&&this.keccak()}return this}finish(){if(this.finished)return;this.finished=!0;let{state:e,suffix:n,pos:r,blockLen:s}=this;e[r]^=n,n&128&&r===s-1&&this.keccak(),e[s-1]^=128,this.keccak()}writeInto(e){k(this,!1),b(e),this.finish();let n=this.state,{blockLen:r}=this;for(let s=0,i=e.length;s<i;){this.posOut>=r&&this.keccak();let o=Math.min(r-this.posOut,i-s);e.set(n.subarray(this.posOut,this.posOut+o),s),this.posOut+=o,s+=o}return e}xofInto(e){if(!this.enableXOF)throw new Error("XOF is not possible for this instance");return this.writeInto(e)}xof(e){return g(e),this.xofInto(new Uint8Array(e))}digestInto(e){if(L(e,this),this.finished)throw new Error("digest() was already called");return this.writeInto(e),this.destroy(),e}digest(){return this.digestInto(new Uint8Array(this.outputLen))}destroy(){this.destroyed=!0,this.state.fill(0)}_cloneInto(e){let{blockLen:n,suffix:r,outputLen:s,rounds:i,enableXOF:o}=this;return e||(e=new t(n,r,s,o,i)),e.state32.set(this.state32),e.pos=this.pos,e.posOut=this.posOut,e.finished=this.finished,e.rounds=i,e.suffix=r,e.outputLen=s,e.enableXOF=o,e.destroyed=this.destroyed,e}},f=(t,e,n)=>U(()=>new y(e,t,n)),it=f(6,144,224/8),ct=f(6,136,256/8),ft=f(6,104,384/8),ut=f(6,72,512/8),ht=f(1,144,224/8),Y=f(1,136,256/8),pt=f(1,104,384/8),lt=f(1,72,512/8),$=(t,e,n)=>H((r={})=>new y(e,t,r.dkLen===void 0?n:r.dkLen,!0)),at=$(31,168,128/8),dt=$(31,136,256/8);export{Y as keccak256};
+/*! Bundled license information:
+
+@noble/hashes/esm/utils.js:
+  (*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) *)
+*/
This page took 0.048927 seconds and 4 git commands to generate.