]> git.ardis.lu - ens2hex/commitdiff
Use multicall3 for batching
authorArdis Lu <ardisglu@gmail.com>
Thu, 23 Nov 2023 05:57:15 +0000 (21:57 -0800)
committerArdis Lu <ardisglu@gmail.com>
Thu, 23 Nov 2023 05:57:15 +0000 (21:57 -0800)
JSON-RPC batching is usually capped at 10 but multicall3 can use max calldata.

index.html

index 74f226baff9fc3f4dcaac0644d4fb8cd3e52c593..5e072bd37604c4affa06693a5d72c0963cf7ebbf 100644 (file)
         <summary>How does it work?</summary>
         <div class="details-body">
           <p>For each ENS name in the list, this tool will calculate the name's <code>namehash</code> according to <a href="https://eips.ethereum.org/EIPS/eip-137">EIP-137</a>.</p>
-          <p>Then it will prepare a <a href="https://ethereum.org/en/developers/docs/apis/json-rpc/">JSON-RPC</a> request containing a payload to query the <a href="https://etherscan.io/address/0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41">ENS PublicResolver</a> smart contract for the hexadecimal addresses.</p>
+          <p>Then it will prepare a <a href="https://ethereum.org/en/developers/docs/apis/json-rpc/">JSON-RPC</a> request to query the <a href="https://www.multicall3.com/"><code>multicall3</code></a> smart contract, which in turn contains a batched call to the <a href="https://etherscan.io/address/0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41">ENS PublicResolver</a> smart contract for the hexadecimal addresses.</p>
           <p>Finally the tool sends the request to an <a href="https://ethereum.org/en/developers/docs/nodes-and-clients/">Ethereum node</a>, which executes the payload and provides a response.</p>
         </div>
       </details>
       return [...hash].map(v => v.toString(16).padStart(2, '0')).join(''); // Convert byte array into 0-padded string. Must spread typed array into non-typed array to allow .map() to output strings
     }
 
+    // Batch multiple "addr(bytes32)" calls to ENS Public Resolver using the multicall3 smart contract
+    async function multicall(rpc, names) {
+      // Construct payload
+      const multiCallPrefix =
+        '0x82ad56cb' + // aggregate3((address,bool,bytes)[])
+        '20'.padStart(64, '0') + // Pointer to next calldata slot
+        names.length.toString(16).padStart(64, '0'); // Length of tuple
+
+      let multiCallTupleOffsets = '';
+      let multiCallTuples = '';
+      for (const [index, name] of names.entries()) {
+        // Each tuple takes exactly 6 32-byte slots, starting after these offsets themselves end
+        multiCallTupleOffsets += ((6 * 32 * index) + (names.length * 32)).toString(16).padStart(64, '0');
+        multiCallTuples +=
+          '4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41'.padStart(64, '0') + // ENS PublicResolver
+          '0'.repeat(64) + // Hardcode allowFailure = false
+          '60'.padStart(64, '0') + // Pointer to next calldata slot
+          '24'.padStart(64, '0') + // 0x24 == 36 bytes immediately follow
+          `3b3b57de${namehash(name)}`.padEnd(64 * 2, '0'); // Calldata for "addr(bytes32)", filling 2 slots
+      }
+
+      const response = await fetch(rpc, {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({
+          jsonrpc: '2.0',
+          method: 'eth_call',
+          params: [
+            {
+              to: '0xcA11bde05977b3631167028862bE2a173976CA11', // multicall3
+              data: multiCallPrefix + multiCallTupleOffsets + multiCallTuples
+            },
+            'safe'
+          ],
+          id: 'multicall'
+        }),
+      }).then(r => r.json());
+
+      const addresses = {};
+      for (const [index, name] of names.entries()) {
+        const prefixOffset = 64 * (2 + names.length); // First 2 slots encode struct offset and length, then 1 slot for the offset of each result
+        const payloadOffset = 64 * (3 + index * 4); // First 3 slots of each result is multicall overhead, actual result starts in slot 4
+        const totalOffset = prefixOffset + payloadOffset;
+        addresses[name] = `0x${response.result.slice(totalOffset + 26, totalOffset + 66)}`; // Parse address from slot 4
+      }
+      return addresses;
+    }
+
     document.querySelector('form').addEventListener('submit', async event => {
       event.preventDefault();
 
         return;
       }
 
-      const batchPayload = cleaned.map((name, index) => ({
-        jsonrpc: '2.0',
-        method: 'eth_call',
-        params: [
-          {
-            to: '0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41', // ENS PublicResolver
-            data: `0x3b3b57de${namehash(name)}` // 0x3b3b57de is the byte signature for the function "addr(bytes32)" in ENS PublicResolver
-          },
-          'safe'
-        ],
-        id: index
-      }));
-
-      const batchResponse = await fetch(formData.get('rpc'), {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify(batchPayload),
-      })
-        .then(r => r.json())
+      const multicallResponse = await multicall(formData.get('rpc'), cleaned)
         .catch(e => {
           status.textContent = e;
           return;
       const table = document.querySelector('table');
       table.querySelector('tbody')?.remove(); // Clear previous rows, if any
       const body = table.createTBody();
-      for (const [index, name] of cleaned.entries()) {
+      for (const name of cleaned) {
         const row = body.insertRow();
         const ens = row.insertCell();
         const address = row.insertCell();
 
         ens.textContent = name;
-        const response = batchResponse.find(r => r.id === index);
-        address.textContent = `0x${response.result.slice(26)}`;
+        const response = multicallResponse[name];
+        address.textContent = response;
       }
 
       spinner.style.setProperty('display', 'none');
This page took 0.053671 seconds and 4 git commands to generate.