<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ethan Park</title>
    <description>The latest articles on DEV Community by Ethan Park (@ethanpark0301).</description>
    <link>https://dev.to/ethanpark0301</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4078313%2Fe1b10a95-1a57-4484-9563-e9d5fac190f1.jpg</url>
      <title>DEV Community: Ethan Park</title>
      <link>https://dev.to/ethanpark0301</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ethanpark0301"/>
    <language>en</language>
    <item>
      <title>Fix: A Missing .catch() That Crashes Your Entire Node.js Process (ethers.js WebSocket Provider)</title>
      <dc:creator>Ethan Park</dc:creator>
      <pubDate>Fri, 21 Aug 2026 16:21:59 +0000</pubDate>
      <link>https://dev.to/ethanpark0301/fix-a-missing-catch-that-crashes-your-entire-nodejs-process-ethersjs-websocket-provider-1mn8</link>
      <guid>https://dev.to/ethanpark0301/fix-a-missing-catch-that-crashes-your-entire-nodejs-process-ethersjs-websocket-provider-1mn8</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ethers-io/ethers.js" rel="noopener noreferrer"&gt;ethers.js&lt;/a&gt; is one of the most widely used Ethereum libraries, with over 3 million weekly downloads on npm. It provides the core infrastructure that thousands of dApps, wallets, and blockchain tools depend on for interacting with EVM chains.&lt;/p&gt;

&lt;p&gt;I discovered a bug in the &lt;code&gt;WebSocketProvider&lt;/code&gt; that causes &lt;strong&gt;uncatchable unhandled rejections&lt;/strong&gt; — silently crashing production Node.js processes with no way for application code to intercept the error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;When a WebSocket provider returns a JSON-RPC error to an &lt;code&gt;eth_subscribe&lt;/code&gt; call (auth failure, 5xx behind a gateway, unsupported method), the rejection has &lt;strong&gt;no handler&lt;/strong&gt; and becomes an unhandled rejection that crashes the process.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SocketSubscriber.start()&lt;/code&gt; stores the subscribe promise with only a &lt;code&gt;.then()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before fix — provider-socket.ts line 63&lt;/span&gt;
&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;filterId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eth_subscribe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;filterId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filterId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;filterId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;start()&lt;/code&gt; returns &lt;code&gt;void&lt;/code&gt; and &lt;code&gt;#filterId&lt;/code&gt; is private — no rejection handler exists and none can be attached from outside. &lt;code&gt;try/catch&lt;/code&gt; around &lt;code&gt;contract.on()&lt;/code&gt; or &lt;code&gt;provider.on()&lt;/code&gt; cannot catch it. The only place it surfaces is &lt;code&gt;process.on('unhandledRejection')&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For a long-running event listener, this turns a transient, retryable provider error into a &lt;strong&gt;hard process exit&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real-World Trigger
&lt;/h3&gt;

&lt;p&gt;This was observed during a provider regional incident. The WebSocket handshake succeeds, the provider reports ready, then &lt;code&gt;eth_subscribe&lt;/code&gt; is answered with an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;Error:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;could&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;coalesce&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(error=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-32608&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unknown response, status code: 500"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;payload=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eth_subscribe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PR: &lt;a href="https://github.com/ethers-io/ethers.js/pull/5180" rel="noopener noreferrer"&gt;ethers-io/ethers.js#5180&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix (Diff)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;     start(): void {
&lt;span class="gd"&gt;-        this.#filterId = this.#provider.send("eth_subscribe", this.filter).then((filterId) =&amp;gt; {;
&lt;/span&gt;&lt;span class="gi"&gt;+        const promise = this.#provider.send("eth_subscribe", this.filter).then((filterId) =&amp;gt; {
&lt;/span&gt;             this.#provider._register(filterId, this);
             return filterId;
&lt;span class="gi"&gt;+        }, (error) =&amp;gt; {
+            if (this.#filterId === promise) { this.#filterId = null; }
+            this.#provider.emit("error", makeError("failed to subscribe", "UNKNOWN_ERROR", {
+                error, info: { filter: this.filter }
+            }));
+            return null as any;
&lt;/span&gt;         });
&lt;span class="gi"&gt;+        this.#filterId = promise;
&lt;/span&gt;     }
&lt;span class="err"&gt;
&lt;/span&gt;     stop(): void {
&lt;span class="gd"&gt;-        (&amp;lt;Promise&amp;lt;number&amp;gt;&amp;gt;(this.#filterId)).then((filterId) =&amp;gt; {
-            if (this.#provider.destroyed) { return; }
-            this.#provider.send("eth_unsubscribe", [ filterId ]);
-        });
&lt;/span&gt;&lt;span class="gi"&gt;+        if (this.#filterId != null) {
+            this.#filterId.then((filterId) =&amp;gt; {
+                if (filterId == null || this.#provider.destroyed) { return; }
+                this.#provider.send("eth_unsubscribe", [ filterId ]);
+            }).catch(() =&amp;gt; { });
+        }
&lt;/span&gt;         this.#filterId = null;
     }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why &lt;code&gt;.then(onFulfilled, onRejected)&lt;/code&gt; instead of &lt;code&gt;.catch()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;My first attempt used &lt;code&gt;.catch()&lt;/code&gt; with a re-throw to keep the rejection chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Re-throw to preserve the rejection&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;strong&gt;re-introduced the exact same bug&lt;/strong&gt;. When &lt;code&gt;.catch()&lt;/code&gt; re-throws, it creates a new rejected promise. Since nobody handles that new promise, you get another unhandled rejection — the same crash, just one step further down the chain.&lt;/p&gt;

&lt;p&gt;The correct pattern is &lt;code&gt;.then(onFulfilled, onRejected)&lt;/code&gt; where &lt;code&gt;onRejected&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt; to &lt;strong&gt;absorb&lt;/strong&gt; the rejection. The promise fulfills with &lt;code&gt;null&lt;/code&gt; instead of rejecting, so there's nothing unhandled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identity Guard for Promise Safety
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;onRejected&lt;/code&gt; handler sets &lt;code&gt;this.#filterId = null&lt;/code&gt; to clean up. But what if &lt;code&gt;start()&lt;/code&gt; were called again before the first rejection settles? The stale handler would clobber the new subscription.&lt;/p&gt;

&lt;p&gt;I added an identity guard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;filterId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;filterId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures the handler only clears its own promise, never a subsequent one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hardening &lt;code&gt;stop()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The original &lt;code&gt;stop()&lt;/code&gt; had two issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No null check on &lt;code&gt;#filterId&lt;/code&gt; — calling &lt;code&gt;.then()&lt;/code&gt; on &lt;code&gt;null&lt;/code&gt; throws&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;.catch()&lt;/code&gt; — if the subscription had failed, the &lt;code&gt;eth_unsubscribe&lt;/code&gt; call would also produce an unhandled rejection&lt;/li&gt;
&lt;li&gt;No guard against sending &lt;code&gt;eth_unsubscribe&lt;/code&gt; with a null filter ID&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Tests Added
&lt;/h3&gt;

&lt;p&gt;Three tests with &lt;code&gt;unhandledRejection&lt;/code&gt; process listeners that fail the test if any unhandled rejection occurs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;eth_subscribe&lt;/code&gt; rejection emits provider &lt;code&gt;"error"&lt;/code&gt; event with correct message&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stop()&lt;/code&gt; on a failed subscription does not throw&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stop()&lt;/code&gt; on a failed subscription does not send &lt;code&gt;eth_unsubscribe&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Closes &lt;a href="https://github.com/ethers-io/ethers.js/issues/5178" rel="noopener noreferrer"&gt;#5178&lt;/a&gt;. Related to &lt;a href="https://github.com/ethers-io/ethers.js/issues/3924" rel="noopener noreferrer"&gt;#3924&lt;/a&gt; (the &lt;code&gt;FilterIdSubscriber&lt;/code&gt; variant, labelled &lt;code&gt;fixed/complete&lt;/code&gt;).&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>Decoding Ethereum transactions without an ABI</title>
      <dc:creator>Ethan Park</dc:creator>
      <pubDate>Sun, 16 Aug 2026 18:28:41 +0000</pubDate>
      <link>https://dev.to/ethanpark0301/decoding-ethereum-transactions-without-an-abi-3b3g</link>
      <guid>https://dev.to/ethanpark0301/decoding-ethereum-transactions-without-an-abi-3b3g</guid>
      <description>&lt;p&gt;You're staring at a transaction on Etherscan. The "Input Data" field is a&lt;br&gt;
  wall of hex. You don't have the contract's ABI. Now what?&lt;/p&gt;

&lt;p&gt;Turns out, you can decode most calldata without an ABI at all. Here's how&lt;br&gt;
  it works.&lt;/p&gt;

&lt;p&gt;The structure of calldata&lt;/p&gt;

&lt;p&gt;Every contract call follows the same format:&lt;/p&gt;

&lt;p&gt;0x + [4 bytes function selector] + [ABI-encoded parameters]&lt;/p&gt;

&lt;p&gt;The first 4 bytes are the keccak256 hash of the function signature,&lt;br&gt;
  truncated. For example:&lt;/p&gt;

&lt;p&gt;transfer(address,uint256)&lt;br&gt;
  → keccak256 → 0xa9059cbb...&lt;br&gt;
  → first 4 bytes → 0xa9059cbb&lt;/p&gt;

&lt;p&gt;The rest is parameters encoded according to the Solidity ABI spec — uint256&lt;br&gt;
  is 32 bytes zero-padded, address is 20 bytes left-padded to 32, dynamic&lt;br&gt;
  types get an offset pointer, etc.&lt;/p&gt;

&lt;p&gt;Step 1: Extract the selector&lt;/p&gt;

&lt;p&gt;const calldata = &lt;br&gt;
  '0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045&lt;br&gt;
  0000000000000000000000000000000000000000000000000de0b6b3a7640000'&lt;/p&gt;

&lt;p&gt;const selector = calldata.slice(0, 10) // '0xa9059cbb'&lt;br&gt;
  const params = calldata.slice(10)      // everything after&lt;/p&gt;

&lt;p&gt;That's it. 4 bytes = 8 hex chars + the 0x prefix = 10 characters.&lt;/p&gt;

&lt;p&gt;Step 2: Look up the signature&lt;/p&gt;

&lt;p&gt;The 4byte.directory maintains a database of known function signatures&lt;br&gt;
  mapped to their selectors. It's a free API:&lt;/p&gt;

&lt;p&gt;GET &lt;a href="https://www.4byte.directory/api/v1/signatures/?hex_signature=0xa9059cbb" rel="noopener noreferrer"&gt;https://www.4byte.directory/api/v1/signatures/?hex_signature=0xa9059cbb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Response:&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
    "results": [&lt;br&gt;
      { "text_signature": "transfer(address,uint256)" }&lt;br&gt;
    ]&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;Now you know the function name and parameter types — without ever seeing&lt;br&gt;
  the ABI.&lt;/p&gt;

&lt;p&gt;Step 3: Decode the parameters&lt;/p&gt;

&lt;p&gt;Once you have the signature transfer(address,uint256), you know the&lt;br&gt;
  parameter types. ABI decoding is mechanical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;address → take 32 bytes, last 20 bytes are the address&lt;/li&gt;
&lt;li&gt;uint256 → take 32 bytes, interpret as big-endian integer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;// Raw params (after selector):&lt;br&gt;
  // 000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045&lt;br&gt;
  // 0000000000000000000000000000000000000000000000000de0b6b3a7640000&lt;/p&gt;

&lt;p&gt;// Decoded:&lt;br&gt;
  // → address: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045&lt;br&gt;
  // → uint256: 1000000000000000000 (1 ETH in wei)&lt;/p&gt;

&lt;p&gt;The annoying parts&lt;/p&gt;

&lt;p&gt;This is straightforward for simple calls, but it gets messy fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collision: Multiple functions can share a selector.
transfer(address,uint256) has 3 entries in 4byte.directory. You need to try
decoding with each candidate and see which one doesn't throw.&lt;/li&gt;
&lt;li&gt;Dynamic types: string, bytes, and arrays use offset pointers. You have to
follow the pointer to find the actual data, then read a length prefix,
then the data.&lt;/li&gt;
&lt;li&gt;Nested tuples: Solidity structs encode as tuples with recursive offset
pointers. A function like
exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))
has nested encoding.&lt;/li&gt;
&lt;li&gt;No match: Custom errors, proxy calls, or recently deployed contracts
might not be in 4byte.directory yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Putting it together&lt;/p&gt;

&lt;p&gt;I wrote a package that handles all of this — selector lookup, candidate&lt;br&gt;
  ranking, ABI decoding with dynamic types, and fallback handling:&lt;/p&gt;

&lt;p&gt;import { decodeCalldata } from '@pulsadev/tx-decoder'&lt;/p&gt;

&lt;p&gt;const decoded = await decodeCalldata(&lt;br&gt;
    '0xa9059cbb000000000000000000000000d8da6bf269...'&lt;br&gt;
  )&lt;/p&gt;

&lt;p&gt;console.log(decoded)&lt;br&gt;
  // {&lt;br&gt;
  //   name: 'transfer',&lt;br&gt;
  //   signature: 'transfer(address,uint256)',&lt;br&gt;
  //   selector: '0xa9059cbb',&lt;br&gt;
  //   args: ['0xd8dA6BF2...', 1000000000000000000n]&lt;br&gt;
  // }&lt;/p&gt;

&lt;p&gt;It also handles event logs:&lt;/p&gt;

&lt;p&gt;import { decodeLog } from '@pulsadev/tx-decoder'&lt;/p&gt;

&lt;p&gt;const event = await decodeLog({&lt;br&gt;
    topics: ['0xddf252ad...', '0x000...sender', '0x000...receiver'],&lt;br&gt;
    data: '0x0000...amount'&lt;br&gt;
  })&lt;br&gt;
  // { name: 'Transfer', args: [...] }&lt;/p&gt;

&lt;p&gt;And error data from reverted transactions:&lt;/p&gt;

&lt;p&gt;import { decodeError } from '@pulsadev/tx-decoder'&lt;/p&gt;

&lt;p&gt;decodeError('0x08c379a0...')&lt;br&gt;
  // 'Insufficient balance'&lt;/p&gt;

&lt;p&gt;Zero dependencies, ~25 KB. Works with any RPC — you don't need ethers or&lt;br&gt;
  viem.&lt;/p&gt;

&lt;p&gt;The package is @pulsadev/tx-decoder if you want to look at the source. It's&lt;br&gt;
  MIT, and the src/ is included in the npm package so you can read every&lt;br&gt;
  line.&lt;/p&gt;




&lt;p&gt;Ethan Park — Senior Engineer at Pulsa Dev. We build zero-dependency EVM &lt;br&gt;
  primitives.&lt;/p&gt;

</description>
      <category>etherum</category>
      <category>typescript</category>
      <category>web3</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>On-chain contract security scanning without API keys</title>
      <dc:creator>Ethan Park</dc:creator>
      <pubDate>Sat, 15 Aug 2026 00:31:19 +0000</pubDate>
      <link>https://dev.to/ethanpark0301/on-chain-contract-security-scanning-without-api-keys-4n78</link>
      <guid>https://dev.to/ethanpark0301/on-chain-contract-security-scanning-without-api-keys-4n78</guid>
      <description>&lt;p&gt;Every honeypot checker on npm is an API wrapper. GoPlus, honeypot.is, TokenSniffer — they all call someone else's server. If that server goes down, adds a paywall, or starts rate limiting you, your tool is dead.&lt;/p&gt;

&lt;p&gt;We wanted something different. A scanner that runs entirely against an RPC endpoint. No API keys. No external services. Just &lt;code&gt;eth_getCode&lt;/code&gt;, &lt;code&gt;eth_call&lt;/code&gt;, and &lt;code&gt;eth_getStorageAt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's how we built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can you actually check on-chain?
&lt;/h2&gt;

&lt;p&gt;More than you'd think. An EVM contract's bytecode contains function selectors — the first 4 bytes of the keccak256 hash of each function signature. By extracting these selectors from raw bytecode, you can detect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mint functions&lt;/strong&gt; — &lt;code&gt;mint(address,uint256)&lt;/code&gt; = &lt;code&gt;0x40c10f19&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pause/unpause&lt;/strong&gt; — &lt;code&gt;pause()&lt;/code&gt; = &lt;code&gt;0x8456cb59&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blacklist&lt;/strong&gt; — &lt;code&gt;blacklist(address)&lt;/code&gt; = &lt;code&gt;0xf9f92be4&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fee manipulation&lt;/strong&gt; — &lt;code&gt;setTaxFeePercent(uint256)&lt;/code&gt; = &lt;code&gt;0x061c82d0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owner control&lt;/strong&gt; — &lt;code&gt;owner()&lt;/code&gt; = &lt;code&gt;0x8da5cb5b&lt;/code&gt;, &lt;code&gt;renounceOwnership()&lt;/code&gt; = &lt;code&gt;0x715018a6&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The extraction is straightforward. Solidity compilers emit &lt;code&gt;PUSH4&lt;/code&gt; (opcode &lt;code&gt;0x63&lt;/code&gt;) followed by 4 bytes, then &lt;code&gt;EQ&lt;/code&gt; (&lt;code&gt;0x14&lt;/code&gt;) for the function dispatcher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lookAhead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lookAhead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;14&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;selectors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Match these against a database of known dangerous selectors, and you've got a bytecode-level risk assessment without ever calling an external API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proxy detection is trickier than it looks
&lt;/h2&gt;

&lt;p&gt;USDC was our wake-up call. We implemented EIP-1967 storage slot checking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IMPLEMENTATION_SLOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;implAddr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getStorageAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rpcUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;IMPLEMENTATION_SLOT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worked great for most proxies. Then USDC returned zero. Turns out USDC uses a ZeppelinOS proxy pattern with a different storage slot: &lt;code&gt;keccak256("org.zeppelinos.proxy.implementation")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We ended up checking four patterns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;EIP-1167&lt;/strong&gt; — minimal proxy bytecode pattern (&lt;code&gt;363d3d373d3d3d363d73...&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EIP-1967&lt;/strong&gt; — standard transparent/UUPS proxy storage slot&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EIP-897&lt;/strong&gt; — &lt;code&gt;implementation()&lt;/code&gt; function call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ZeppelinOS&lt;/strong&gt; — &lt;code&gt;org.zeppelinos.proxy.implementation&lt;/code&gt; slot&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each contract only matches one, but you need all four to cover real-world usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The EIP-7702 surprise
&lt;/h2&gt;

&lt;p&gt;While testing, we pointed the scanner at Vitalik's address. Expected result: "not a contract." Actual result: it was flagged as a contract.&lt;/p&gt;

&lt;p&gt;Turns out Vitalik had set an EIP-7702 delegate designation — a short bytecode starting with &lt;code&gt;0xef0100&lt;/code&gt; followed by a 20-byte address. It's not a real contract, it's a delegation pointer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isContract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytecode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bytecode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;bytecode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bytecode&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="c1"&gt;// EIP-7702 delegate designation&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ef0100&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small edge case, but the kind that matters when you're scanning thousands of addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honeypot detection via DEX simulation
&lt;/h2&gt;

&lt;p&gt;The most interesting check is honeypot detection. The idea: simulate a buy and sell through a DEX router using &lt;code&gt;eth_call&lt;/code&gt;. If the sell reverts, it's a honeypot.&lt;/p&gt;

&lt;p&gt;We call &lt;code&gt;swapExactETHForTokensSupportingFeeOnTransferTokens&lt;/code&gt; (the fee-on-transfer variant, which handles tax tokens) with a simulation wallet address. If the buy succeeds but the sell fails, the token can't be sold — classic honeypot.&lt;/p&gt;

&lt;p&gt;This works without any API because &lt;code&gt;eth_call&lt;/code&gt; is a read-only simulation. No gas is spent, no transaction is broadcast. You're just asking the node "what would happen if...?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Risk scoring
&lt;/h2&gt;

&lt;p&gt;Each check contributes to a 0-100 risk score:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Honeypot detected&lt;/td&gt;
&lt;td&gt;+40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mint function exists&lt;/td&gt;
&lt;td&gt;+15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blacklist function&lt;/td&gt;
&lt;td&gt;+15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pause function&lt;/td&gt;
&lt;td&gt;+10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transfer fees&lt;/td&gt;
&lt;td&gt;+10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No liquidity&lt;/td&gt;
&lt;td&gt;+10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Owner not renounced&lt;/td&gt;
&lt;td&gt;+5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proxy contract&lt;/td&gt;
&lt;td&gt;+5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ownership renounced&lt;/td&gt;
&lt;td&gt;-10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A score of 0-10 is "safe," 75+ is "critical." It's opinionated, but it gives you a starting point for automated filtering.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we shipped
&lt;/h2&gt;

&lt;p&gt;The package runs all checks in parallel, supports 7 chains with built-in DEX configurations, and weighs about 28KB with zero dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scanContract&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pulsadev/contract-scanner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;scanContract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0xTokenAddress...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;rpcUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://ethereum-rpc.publicnode.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;riskLevel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// 'safe' | 'low' | 'medium' | 'high' | 'critical'&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;honeypot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isHoneypot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ownership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isRenounced&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dangerousFunctions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each check is also exported individually, so you can use just the parts you need.&lt;/p&gt;

&lt;p&gt;npm: &lt;a href="https://www.npmjs.com/package/@pulsadev/contract-scanner" rel="noopener noreferrer"&gt;@pulsadev/contract-scanner&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/pulsadev/contract-scanner" rel="noopener noreferrer"&gt;pulsadev/contract-scanner&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;If you're building DeFi tooling or security dashboards, I'd love to hear how you're handling on-chain analysis. What checks would you add?&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>web3</category>
      <category>ethereum</category>
    </item>
  </channel>
</rss>
