Skip to content

Add ERC-8021 builder code extraction support#161

Merged
yosriady merged 9 commits intomainfrom
claude/builder-code-extraction-45Um9
Mar 2, 2026
Merged

Add ERC-8021 builder code extraction support#161
yosriady merged 9 commits intomainfrom
claude/builder-code-extraction-45Um9

Conversation

@yosriady
Copy link
Copy Markdown
Contributor

@yosriady yosriady commented Mar 2, 2026

Summary

This PR adds support for extracting builder codes from transaction calldata using the ERC-8021 standard. Builder codes are attribution identifiers that can be appended to transaction data to track which applications or services facilitated a transaction.

Key Changes

  • New utility module (src/utils/builderCode.ts): Implements extractBuilderCodes() function that parses the ERC-8021 data suffix format to extract comma-separated builder code strings from transaction calldata

    • Validates the 16-byte ERC marker (0x80218021802180218021802180218021)
    • Supports Schema 0 with ASCII-encoded codes delimited by commas
    • Handles both prefixed (0x) and unprefixed hex strings, case-insensitive
    • Returns undefined for invalid or missing suffixes
  • Integration with transaction tracking: Updated WagmiEventHandler and FormoAnalytics to extract and include builder codes in transaction events

    • Added builder_codes field to transaction event payloads
    • Extraction happens automatically during transaction tracking
  • Type definitions: Added builder_codes?: string[] field to TransactionAPIEvent interface

  • Comprehensive test suite (test/utils/builderCode.spec.ts): 172 lines of tests covering:

    • Valid ERC-8021 suffix parsing with single and multiple codes
    • Edge cases (empty codes, oversized lengths, unsupported schemas)
    • Invalid inputs (null, undefined, non-string, missing marker)
    • Real-world scenarios with function calldata

Implementation Details

  • The ERC-8021 suffix is parsed backwards from the end of calldata: marker (16 bytes) → schema ID (1 byte) → codes length (1 byte) → codes (variable)
  • Builder codes are extracted as ASCII strings and split on comma delimiters
  • The implementation is defensive, returning undefined rather than throwing errors for malformed data
  • Exported from main utils index for public API access

https://claude.ai/code/session_01XRqHApjYMQaFJG9CLMwcSE


Open with Devin

Note

Medium Risk
Adds new transaction parsing and threads a new optional builder_codes field through the analytics event pipeline; risk is mainly around calldata parsing/concatenation and potential compatibility issues with Wagmi writeContract variable shapes.

Overview
Adds ERC-8021 builder code support by introducing extractBuilderCodes() and exporting it from utils.

Transaction tracking now attempts to parse builder codes from calldata and, when present, includes an optional builder_codes field in transaction events end-to-end (FormoAnalytics payload building, TransactionAPIEvent typing, and EventFactory.generateTransactionEvent).

For Wagmi writeContract tracking, the handler now concatenates dataSuffix onto encoded calldata (via new concatCalldataWithSuffix() in wagmi/utils) so builder codes can be extracted consistently; tests were added for both the parser and calldata concatenation.

Written by Cursor Bugbot for commit 00108ee. This will update automatically on new commits. Configure here.

Greptile Summary

This PR adds ERC-8021 builder code extraction support, introducing a new extractBuilderCodes() utility that parses the ERC-8021 data suffix from transaction calldata and threads the result as a builder_codes field through the Wagmi and EIP-1193 transaction tracking flows.

Key changes:

  • src/utils/builderCode.ts: New parsing module with backward-reading logic (marker → schemaId → codesLength → codes), printable-ASCII validation, and isNaN guards.
  • src/wagmi/utils.ts: New concatCalldataWithSuffix that concatenates ABI-encoded calldata with the optional dataSuffix from wagmi's writeContract variables using viem.concatHex or a string fallback.
  • src/wagmi/WagmiEventHandler.ts / src/FormoAnalytics.ts: Both transaction-tracking paths extract and propagate builder_codes to analytics events.
  • src/types/events.ts: TransactionAPIEvent gains builder_codes?: string.

Concerns:

  • Schema 1 parsing treats registryAddress, chainId, and chainIdLength fields as opaque calldata and never validates them, meaning any calldata ending with [bytes][codesLength][0x01][ercMarker] passes as valid Schema 1 regardless of what precedes the codes.
  • The decodeCodes helper silently drops empty segments from consecutive or trailing commas, meaning a codesLength-declared byte count can disagree with the actual decoded output — malformed data is quietly normalised instead of rejected.
  • concatCalldataWithSuffix's viem and fallback paths diverge when a non-0x-prefixed dataSuffix is passed: the viem path will throw (viem validates 0x prefix at runtime), while the fallback strips the prefix correctly.

Confidence Score: 3/5

  • Mergeable but has spec-compliance and defensive-coding gaps that should be addressed before relying on Schema 1 extraction in production.
  • The Schema 0 parsing path is correct and well-tested. However, Schema 1 skips validation of required registryAddress/chainId fields, malformed data with consecutive/trailing commas is silently normalised rather than rejected, and the viem vs. fallback paths in concatCalldataWithSuffix diverge for non-0x-prefixed inputs. These are all confined to the new parsing module and utility, not the broader analytics pipeline, but they represent spec divergence and potential silent misparsing in edge cases.
  • src/utils/builderCode.ts (Schema 1 validation gap, comma normalisation) and src/wagmi/utils.ts (viem/fallback path divergence).

Important Files Changed

Filename Overview
src/utils/builderCode.ts New ERC-8021 parsing module; Schema 0 parsing is solid, but Schema 1 validation skips required registryAddress/chainId fields, and consecutive/trailing comma segments are silently normalised rather than rejected.
src/wagmi/WagmiEventHandler.ts Correctly integrates builder code extraction for both writeContract (via concatCalldataWithSuffix + extractBuilderCodes) and sendTransaction paths; builder_codes propagated through pending transactions and event payloads consistently.
src/wagmi/utils.ts New concatCalldataWithSuffix utility handles viem and fallback paths; viem path may throw for non-0x-prefixed suffix inputs due to type coercion, a path inconsistency worth hardening.
src/FormoAnalytics.ts builder_codes cleanly extracted in buildTransactionPayload via extractBuilderCodes and threaded through transaction() and buildTransactionEventPayload; no logic errors.
src/event/EventFactory.ts builder_codes added as positional parameter to generateTransactionEvent and correctly passed at the single call site; no breaking change to external API surface.
src/types/events.ts TransactionAPIEvent extended with builder_codes?: string; type is consistent with implementation (comma-separated string) though diverges from PR description (string[]).
src/utils/index.ts Straightforward barrel export of the new builderCode module; no issues.
test/utils/builderCode.spec.ts Comprehensive 298-line test suite covering Schema 0, Schema 1, invalid inputs, and edge cases with exact hex vectors; good coverage of the happy path and defensive returns.
test/wagmi/utils.spec.ts New concatCalldataWithSuffix tests cover undefined/empty/0x/prefixed/unprefixed suffix cases against the fallback path; does not test the viem concatHex code path with a non-0x-prefixed suffix.

Sequence Diagram

sequenceDiagram
    participant App
    participant WagmiEventHandler
    participant FormoAnalytics
    participant EventFactory
    participant extractBuilderCodes
    participant concatCalldataWithSuffix

    App->>WagmiEventHandler: writeContract({abi, fnName, args, dataSuffix})
    WagmiEventHandler->>concatCalldataWithSuffix: encodedData + dataSuffix
    concatCalldataWithSuffix-->>WagmiEventHandler: fullCalldata (with ERC-8021 suffix)
    WagmiEventHandler->>extractBuilderCodes: fullCalldata
    extractBuilderCodes-->>WagmiEventHandler: builder_codes (string | undefined)
    WagmiEventHandler->>FormoAnalytics: transaction({status, chainId, data, builder_codes, ...})

    App->>FormoAnalytics: sendTransaction({data, ...})
    FormoAnalytics->>extractBuilderCodes: data
    extractBuilderCodes-->>FormoAnalytics: builder_codes (string | undefined)
    FormoAnalytics->>EventFactory: create(TransactionAPIEvent with builder_codes)
    EventFactory->>EventFactory: generateTransactionEvent(..., builder_codes, ...)
    EventFactory-->>FormoAnalytics: enriched IFormoEvent
    FormoAnalytics-->>App: event dispatched
Loading

Last reviewed commit: 00108ee

Add support for Base builder codes (ERC-8021) by parsing the data suffix
appended to transaction calldata and including extracted codes as
`builder_codes` property on transaction events.

- Add `extractBuilderCodes()` utility that parses ERC-8021 suffixes
  (Schema 0: comma-delimited ASCII codes + 1-byte length + 1-byte
  schemaId + 16-byte marker)
- Add `builder_codes` field to `TransactionAPIEvent` type
- Integrate extraction in both EIP-1193 (`buildTransactionEventPayload`)
  and Wagmi (`WagmiEventHandler`) transaction flows
- Propagate through EventFactory and FormoAnalytics public API
- Add comprehensive tests for builder code extraction

https://claude.ai/code/session_01XRqHApjYMQaFJG9CLMwcSE
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

Change the builder code property from `builder_codes: string[]` to
`builder_code: string` containing a comma-separated value
(e.g. "uniswap,base") to match the expected event schema.

https://claude.ai/code/session_01XRqHApjYMQaFJG9CLMwcSE
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 files reviewed, 13 comments

Edit Code Review Agent Settings | Greptile

Comment thread src/utils/builderCode.ts Outdated
Comment on lines +38 to +40
export function extractBuilderCode(
data: string | undefined | null
): string | undefined {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return type creates a confusing join/split roundtrip

The function parses the ERC-8021 codes into a string[] array internally (lines 100–117), only to immediately re-join them with "," before returning. Callers who want individual codes must then split again on ",". The PR description also specifies builder_codes?: string[] as the intended type, but the implementation diverges from this.

Returning string[] directly would be more idiomatic TypeScript, eliminate ambiguity for consumers, and match the PR description:

export function extractBuilderCode(
  data: string | undefined | null
): string[] | undefined {

…and at the return site:

  return codes.length > 0 ? codes : undefined;

The TransactionAPIEvent field and every call-site would then change from builder_code?: string to builder_codes?: string[], which also resolves the misleading singular naming (builder_code) that is used to carry a potentially multi-value payload like "uniswap,base".

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/utils/builderCode.ts
Line: 38-40

Comment:
**Return type creates a confusing join/split roundtrip**

The function parses the ERC-8021 codes into a `string[]` array internally (lines 100–117), only to immediately re-join them with `","` before returning. Callers who want individual codes must then split again on `","`. The PR description also specifies `builder_codes?: string[]` as the intended type, but the implementation diverges from this.

Returning `string[]` directly would be more idiomatic TypeScript, eliminate ambiguity for consumers, and match the PR description:

```
export function extractBuilderCode(
  data: string | undefined | null
): string[] | undefined {
```

…and at the return site:

```typescript
  return codes.length > 0 ? codes : undefined;
```

The `TransactionAPIEvent` field and every call-site would then change from `builder_code?: string` to `builder_codes?: string[]`, which also resolves the misleading singular naming (`builder_code`) that is used to carry a potentially multi-value payload like `"uniswap,base"`.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/utils/builderCode.ts Outdated
Comment on lines +93 to +97
// Step 5: Decode ASCII codes, splitting on comma (0x2C)
const bytes: number[] = [];
for (let i = 0; i < codesHex.length; i += 2) {
bytes.push(parseInt(codesHex.slice(i, i + 2), 16));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No validation of ASCII range for decoded bytes

String.fromCharCode will accept any byte value (0–255). Non-printable control characters (0x00–0x1F, 0x7F) and extended-ASCII bytes (0x80–0xFF) in maliciously crafted calldata would silently produce garbage Unicode characters that get forwarded to the analytics API. Consider validating that every decoded byte is in the printable ASCII range (0x20–0x7E) and returning undefined if any byte fails that check:

  // Decode ASCII codes, splitting on comma (0x2C)
  const bytes: number[] = [];
  for (let i = 0; i < codesHex.length; i += 2) {
    const byte = parseInt(codesHex.slice(i, i + 2), 16);
    // Reject non-printable or non-ASCII bytes
    if (byte < 0x20 || byte > 0x7e) {
      return undefined;
    }
    bytes.push(byte);
  }
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/utils/builderCode.ts
Line: 93-97

Comment:
**No validation of ASCII range for decoded bytes**

`String.fromCharCode` will accept any byte value (0–255). Non-printable control characters (0x00–0x1F, 0x7F) and extended-ASCII bytes (0x80–0xFF) in maliciously crafted calldata would silently produce garbage Unicode characters that get forwarded to the analytics API. Consider validating that every decoded byte is in the printable ASCII range (0x20–0x7E) and returning `undefined` if any byte fails that check:

```typescript
  // Decode ASCII codes, splitting on comma (0x2C)
  const bytes: number[] = [];
  for (let i = 0; i < codesHex.length; i += 2) {
    const byte = parseInt(codesHex.slice(i, i + 2), 16);
    // Reject non-printable or non-ASCII bytes
    if (byte < 0x20 || byte > 0x7e) {
      return undefined;
    }
    bytes.push(byte);
  }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/utils/builderCode.ts Outdated
Comment on lines +50 to +52
// Minimum suffix size: 1 byte codesLength + 1 byte schemaId + 16 bytes marker = 18 bytes = 36 hex chars
if (hex.length < 36) {
return undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minimum-length comment is slightly misleading

The comment says "Minimum suffix size: 1 byte codesLength + 1 byte schemaId + 16 bytes marker = 18 bytes = 36 hex chars". However a codesLength of 0 is explicitly rejected on line 81, so 36 hex chars is actually the minimum for a structurally-present-but-always-rejected suffix. The true minimum for any successful extraction is 19 bytes (38 hex chars): at least 1 byte of codes + 1 byte codesLength + 1 byte schemaId + 16 bytes marker.

The guard itself is functionally safe (a 36-char input is always rejected by later checks), but updating the comment and tightening the bound avoids confusion:

  // Minimum suffix: 1+ byte codes + 1 byte codesLength + 1 byte schemaId + 16 bytes marker = 19 bytes = 38 hex chars
  if (hex.length < 38) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/utils/builderCode.ts
Line: 50-52

Comment:
**Minimum-length comment is slightly misleading**

The comment says "Minimum suffix size: 1 byte codesLength + 1 byte schemaId + 16 bytes marker = 18 bytes = 36 hex chars". However a `codesLength` of 0 is explicitly rejected on line 81, so 36 hex chars is actually the minimum for a structurally-present-but-always-rejected suffix. The true minimum for any successful extraction is 19 bytes (38 hex chars): at least 1 byte of codes + 1 byte codesLength + 1 byte schemaId + 16 bytes marker.

The guard itself is functionally safe (a 36-char input is always rejected by later checks), but updating the comment and tightening the bound avoids confusion:

```typescript
  // Minimum suffix: 1+ byte codes + 1 byte codesLength + 1 byte schemaId + 16 bytes marker = 19 bytes = 38 hex chars
  if (hex.length < 38) {
```

How can I resolve this? If you propose a fix, please make it concise.

claude added 2 commits March 2, 2026 05:15
…in length

- Reject non-printable or non-ASCII bytes (outside 0x20-0x7E) in codes
- Fix minimum length check to 38 hex chars (19 bytes: 1+ codes + 1
  codesLength + 1 schemaId + 16 marker)
- Add tests for non-printable and extended ASCII byte rejection

https://claude.ai/code/session_01XRqHApjYMQaFJG9CLMwcSE
Rename the property and function to `builder_codes` / `extractBuilderCodes`
while keeping the comma-separated string type. The EIP-1193 path already
extracts builder codes via `buildTransactionEventPayload` and propagates
them through all transaction statuses (STARTED, BROADCASTED, CONFIRMED,
REVERTED, REJECTED).

https://claude.ai/code/session_01XRqHApjYMQaFJG9CLMwcSE
Comment thread src/utils/builderCode.ts
Comment on lines +45 to +53
// Normalize: remove 0x prefix and work with lowercase hex
const hex = data.startsWith("0x") || data.startsWith("0X")
? data.slice(2).toLowerCase()
: data.toLowerCase();

// Minimum suffix: 1+ byte codes + 1 byte codesLength + 1 byte schemaId + 16 bytes marker = 19 bytes = 38 hex chars
if (hex.length < 38) {
return undefined;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing odd-length hex guard

After stripping the 0x prefix, there is no check that hex has an even number of characters (i.e., represents an integer number of bytes). If a malformed or odd-length string is passed, the marker, schemaId, codesLength, and codesHex slices will all straddle byte boundaries, producing values that don't correspond to any real byte. While valid EVM calldata is always byte-aligned, a defensive guard is cheap and avoids any theoretical misparse:

Suggested change
// Normalize: remove 0x prefix and work with lowercase hex
const hex = data.startsWith("0x") || data.startsWith("0X")
? data.slice(2).toLowerCase()
: data.toLowerCase();
// Minimum suffix: 1+ byte codes + 1 byte codesLength + 1 byte schemaId + 16 bytes marker = 19 bytes = 38 hex chars
if (hex.length < 38) {
return undefined;
}
// Normalize: remove 0x prefix and work with lowercase hex
const hex = data.startsWith("0x") || data.startsWith("0X")
? data.slice(2).toLowerCase()
: data.toLowerCase();
// Must be byte-aligned (even number of hex chars)
if (hex.length % 2 !== 0) {
return undefined;
}
// Minimum suffix: 1+ byte codes + 1 byte codesLength + 1 byte schemaId + 16 bytes marker = 19 bytes = 38 hex chars
if (hex.length < 38) {
return undefined;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/utils/builderCode.ts
Line: 45-53

Comment:
**Missing odd-length hex guard**

After stripping the `0x` prefix, there is no check that `hex` has an even number of characters (i.e., represents an integer number of bytes). If a malformed or odd-length string is passed, the marker, `schemaId`, `codesLength`, and `codesHex` slices will all straddle byte boundaries, producing values that don't correspond to any real byte. While valid EVM calldata is always byte-aligned, a defensive guard is cheap and avoids any theoretical misparse:

```suggestion
  // Normalize: remove 0x prefix and work with lowercase hex
  const hex = data.startsWith("0x") || data.startsWith("0X")
    ? data.slice(2).toLowerCase()
    : data.toLowerCase();

  // Must be byte-aligned (even number of hex chars)
  if (hex.length % 2 !== 0) {
    return undefined;
  }

  // Minimum suffix: 1+ byte codes + 1 byte codesLength + 1 byte schemaId + 16 bytes marker = 19 bytes = 38 hex chars
  if (hex.length < 38) {
    return undefined;
  }
```

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 2, 2026

Additional Comments (1)

src/wagmi/WagmiEventHandler.ts
Builder codes silently missed for writeContract + dataSuffix

The extractBuilderCodes call on line 653 operates on data, which for writeContract mutations is set to the ABI-encoded calldata produced by encodeWriteContractData(abi, fnName, args). However, Wagmi v2's writeContract accepts a dataSuffix parameter (available in variables) that gets appended to the encoded calldata before the transaction is broadcast. The ERC-8021 suffix is meant to be appended this way in Wagmi writeContract flows.

Because encodeWriteContractData doesn't receive dataSuffix, the data variable never contains the ERC-8021 bytes, and extractBuilderCodes(data) will always return undefined for writeContract transactions — even when the dApp correctly passed dataSuffix with an ERC-8021 payload.

The fix is to extract dataSuffix from variables and combine it with the encoded data before extracting builder codes:

if (mutationType === "writeContract") {
  const { abi, functionName: fnName, args, address: contractAddress, dataSuffix } = variables;
  to = contractAddress;
  function_name = fnName;

  if (abi && fnName) {
    function_args = extractFunctionArgs(abi, fnName, args);

    const encodedData = encodeWriteContractData(abi, fnName, args);
    if (encodedData) {
      // Combine encoded ABI data with dataSuffix (e.g., ERC-8021 builder codes)
      data = dataSuffix
        ? encodedData + (dataSuffix as string).replace(/^0x/i, "")
        : encodedData;
      logger.debug(
        "WagmiEventHandler: Encoded writeContract data",
        data.substring(0, 10)
      );
    }
  }
}

Alternatively, if your encodeWriteContractData already returns just the ABI bytes and you don't want to concatenate for the data field, you can at least scope the extraction to dataSuffix:

const builder_codes = extractBuilderCodes(
  mutationType === "writeContract" ? variables.dataSuffix : data
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/wagmi/WagmiEventHandler.ts
Line: 625-653

Comment:
**Builder codes silently missed for `writeContract` + `dataSuffix`**

The `extractBuilderCodes` call on line 653 operates on `data`, which for `writeContract` mutations is set to the ABI-encoded calldata produced by `encodeWriteContractData(abi, fnName, args)`. However, Wagmi v2's `writeContract` accepts a `dataSuffix` parameter (available in `variables`) that gets appended to the encoded calldata before the transaction is broadcast. The ERC-8021 suffix is meant to be appended this way in Wagmi `writeContract` flows.

Because `encodeWriteContractData` doesn't receive `dataSuffix`, the `data` variable never contains the ERC-8021 bytes, and `extractBuilderCodes(data)` will always return `undefined` for `writeContract` transactions — even when the dApp correctly passed `dataSuffix` with an ERC-8021 payload.

The fix is to extract `dataSuffix` from `variables` and combine it with the encoded data before extracting builder codes:

```typescript
if (mutationType === "writeContract") {
  const { abi, functionName: fnName, args, address: contractAddress, dataSuffix } = variables;
  to = contractAddress;
  function_name = fnName;

  if (abi && fnName) {
    function_args = extractFunctionArgs(abi, fnName, args);

    const encodedData = encodeWriteContractData(abi, fnName, args);
    if (encodedData) {
      // Combine encoded ABI data with dataSuffix (e.g., ERC-8021 builder codes)
      data = dataSuffix
        ? encodedData + (dataSuffix as string).replace(/^0x/i, "")
        : encodedData;
      logger.debug(
        "WagmiEventHandler: Encoded writeContract data",
        data.substring(0, 10)
      );
    }
  }
}
```

Alternatively, if your `encodeWriteContractData` already returns just the ABI bytes and you don't want to concatenate for the `data` field, you can at least scope the extraction to `dataSuffix`:

```typescript
const builder_codes = extractBuilderCodes(
  mutationType === "writeContract" ? variables.dataSuffix : data
);
```

How can I resolve this? If you propose a fix, please make it concise.

- Support schemaId 0x01 (custom registry) in addition to 0x00 (canonical)
  Both schemas have codes in the same backward-parse position
- Refactor shared codes parsing into readCodes() and decodeCodes() helpers
- Add exact hex test vectors matching the ERC-8021 spec:
  Schema 0: 0xdddddddd62617365617070070080218021...
  Schema 1: 0xddddddddcccc...21050262617365617070...0e0180218021...
- Add tests for unknown schemaId rejection (0x02, 0xFF)

https://claude.ai/code/session_01XRqHApjYMQaFJG9CLMwcSE
cursor[bot]

This comment was marked as resolved.

}

// Extract builder codes from transaction data (ERC-8021)
const builder_codes = extractBuilderCodes(data);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extractBuilderCodes always returns undefined for writeContract

For writeContract mutations, data is set by encodeWriteContractData(abi, fnName, args) (line 636), which produces standard ABI-encoded calldata. ABI-encoded calldata is generated deterministically from the function signature and arguments — it cannot contain an ERC-8021 suffix. As a result, extractBuilderCodes(data) will always return undefined on this path, making the builder code feature silently inactive for all writeContract calls.

Builder codes via ERC-8021 are only present when the dApp passes them as part of the raw calldata in a sendTransaction call. For the writeContract branch, the extractBuilderCodes call is a no-op and could be guarded to avoid unnecessary work:

// Only extract builder codes from raw sendTransaction data;
// writeContract data is ABI-encoded and never carries an ERC-8021 suffix
const builder_codes = mutationType === "sendTransaction"
  ? extractBuilderCodes(data)
  : undefined;

At minimum, this limitation should be documented so that consumers who rely on writeContract are not surprised by missing builder_codes in their analytics events.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/wagmi/WagmiEventHandler.ts
Line: 653

Comment:
**`extractBuilderCodes` always returns `undefined` for `writeContract`**

For `writeContract` mutations, `data` is set by `encodeWriteContractData(abi, fnName, args)` (line 636), which produces standard ABI-encoded calldata. ABI-encoded calldata is generated deterministically from the function signature and arguments — it cannot contain an ERC-8021 suffix. As a result, `extractBuilderCodes(data)` will always return `undefined` on this path, making the builder code feature silently inactive for all `writeContract` calls.

Builder codes via ERC-8021 are only present when the dApp passes them as part of the raw calldata in a `sendTransaction` call. For the `writeContract` branch, the `extractBuilderCodes` call is a no-op and could be guarded to avoid unnecessary work:

```typescript
// Only extract builder codes from raw sendTransaction data;
// writeContract data is ABI-encoded and never carries an ERC-8021 suffix
const builder_codes = mutationType === "sendTransaction"
  ? extractBuilderCodes(data)
  : undefined;
```

At minimum, this limitation should be documented so that consumers who rely on `writeContract` are not surprised by missing `builder_codes` in their analytics events.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/utils/builderCode.ts
Comment on lines +59 to +74
for (const byte of bytes) {
if (byte === COMMA_BYTE) {
if (current.length > 0) {
codes.push(String.fromCharCode(...current));
current = [];
}
} else {
current.push(byte);
}
}

// Push the last code segment
if (current.length > 0) {
codes.push(String.fromCharCode(...current));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing/leading comma codes are silently dropped

The comma-splitting logic skips empty segments: if a byte sequence starts or ends with a comma (e.g., ",base" or "base,"), the empty segment is silently discarded. This means a codesLength field that encodes "base," (6 bytes) will produce "base" — the reconstructed string's byte length disagrees with what codesLength declared. While this is unlikely in well-formed ERC-8021 data, malformed inputs pass the printable-ASCII check and are quietly normalised.

Consider returning undefined if any empty segment is detected after splitting, so callers get a clear signal that the codes field is malformed:

if (byte === COMMA_BYTE) {
  if (current.length === 0) {
    // Empty segment (leading or consecutive comma) — treat as invalid
    return undefined;
  }
  codes.push(String.fromCharCode(...current));
  current = [];
}

And after the loop, also return undefined if the trailing segment is empty (i.e., the last byte was a comma).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/utils/builderCode.ts
Line: 59-74

Comment:
**Trailing/leading comma codes are silently dropped**

The comma-splitting logic skips empty segments: if a byte sequence starts or ends with a comma (e.g., `",base"` or `"base,"`), the empty segment is silently discarded. This means a `codesLength` field that encodes `"base,"` (6 bytes) will produce `"base"` — the reconstructed string's byte length disagrees with what `codesLength` declared. While this is unlikely in well-formed ERC-8021 data, malformed inputs pass the printable-ASCII check and are quietly normalised.

Consider returning `undefined` if any empty segment is detected after splitting, so callers get a clear signal that the codes field is malformed:

```typescript
if (byte === COMMA_BYTE) {
  if (current.length === 0) {
    // Empty segment (leading or consecutive comma) — treat as invalid
    return undefined;
  }
  codes.push(String.fromCharCode(...current));
  current = [];
}
```

And after the loop, also return `undefined` if the trailing segment is empty (i.e., the last byte was a comma).

How can I resolve this? If you propose a fix, please make it concise.

yosriady added 2 commits March 2, 2026 15:37
… extraction

For writeContract mutations, data was reconstructed from ABI encode only;
dataSuffix (ERC-8021 builder codes) was not consistently incorporated before
extractBuilderCodes(), so builder codes were never extracted for writeContract.

- Add concatCalldataWithSuffix() in wagmi utils to append dataSuffix to
  encoded calldata using viem's concatHex when available
- Use it in WagmiEventHandler so extractBuilderCodes(data) sees full calldata
  including ERC-8021 suffix for writeContract transactions

Made-with: Cursor
Comment thread src/wagmi/utils.ts
Comment on lines +147 to +163
export function concatCalldataWithSuffix(
encodedData: string,
dataSuffix: string | undefined
): string {
if (!dataSuffix || dataSuffix === "0x" || dataSuffix.length <= 2) {
return encodedData;
}
const viem = tryLoadViem();
if (viem?.concatHex) {
return viem.concatHex([
encodedData as `0x${string}`,
dataSuffix as `0x${string}`,
]);
}
const suffixHex = dataSuffix.replace(/^0x/i, "");
return `${encodedData}${suffixHex}`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test coverage for this new function

This is a new utility function that's critical for the builder code feature (it enables ERC-8021 suffix support for writeContract calls), but there are no tests validating its behavior. Consider adding tests to verify:

  • Correct concatenation when dataSuffix is present
  • Handling when dataSuffix is empty, undefined, or "0x"
  • Behavior with and without viem available (both code paths)
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/wagmi/utils.ts
Line: 147-163

Comment:
No test coverage for this new function

This is a new utility function that's critical for the builder code feature (it enables ERC-8021 suffix support for `writeContract` calls), but there are no tests validating its behavior. Consider adding tests to verify:
- Correct concatenation when `dataSuffix` is present
- Handling when `dataSuffix` is empty, `undefined`, or `"0x"`
- Behavior with and without viem available (both code paths)

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/utils/builderCode.ts
Comment on lines +46 to +52
for (let i = 0; i < codesHex.length; i += 2) {
const byte = parseInt(codesHex.slice(i, i + 2), 16);
// Reject non-printable or non-ASCII bytes
if (byte < 0x20 || byte > 0x7e) {
return undefined;
}
bytes.push(byte);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing isNaN check after parseInt

If codesHex contains invalid hex characters (non 0-9, a-f), parseInt returns NaN. The ASCII range check (byte < 0x20 || byte > 0x7e) evaluates to false when byte is NaN, so invalid hex passes validation and NaN gets pushed to the bytes array. Later, String.fromCharCode(NaN) converts it to 0, producing a null character.

Add an explicit isNaN check:

Suggested change
for (let i = 0; i < codesHex.length; i += 2) {
const byte = parseInt(codesHex.slice(i, i + 2), 16);
// Reject non-printable or non-ASCII bytes
if (byte < 0x20 || byte > 0x7e) {
return undefined;
}
bytes.push(byte);
const byte = parseInt(codesHex.slice(i, i + 2), 16);
// Reject non-printable or non-ASCII bytes, and invalid hex
if (isNaN(byte) || byte < 0x20 || byte > 0x7e) {
return undefined;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/utils/builderCode.ts
Line: 46-52

Comment:
Missing `isNaN` check after `parseInt`

If `codesHex` contains invalid hex characters (non 0-9, a-f), `parseInt` returns `NaN`. The ASCII range check (`byte < 0x20 || byte > 0x7e`) evaluates to `false` when `byte` is `NaN`, so invalid hex passes validation and `NaN` gets pushed to the `bytes` array. Later, `String.fromCharCode(NaN)` converts it to `0`, producing a null character.

Add an explicit `isNaN` check:

```suggestion
    const byte = parseInt(codesHex.slice(i, i + 2), 16);
    // Reject non-printable or non-ASCII bytes, and invalid hex
    if (isNaN(byte) || byte < 0x20 || byte > 0x7e) {
      return undefined;
    }
```

How can I resolve this? If you propose a fix, please make it concise.

cursor[bot]

This comment was marked as resolved.

…dataWithSuffix

- builderCode: add isNaN(byte) check so invalid hex does not bypass ASCII validation
- wagmi/utils: add test coverage for concatCalldataWithSuffix (suffix present, empty/undefined/0x, fallback path)

Made-with: Cursor
Comment thread src/utils/builderCode.ts
Comment on lines +129 to +136
// Normalize: remove 0x prefix and work with lowercase hex
const hex =
data.startsWith("0x") || data.startsWith("0X")
? data.slice(2).toLowerCase()
: data.toLowerCase();

// Minimum suffix: 1+ byte codes + 1 byte codesLength + 1 byte schemaId + 16 bytes marker = 19 bytes = 38 hex chars
if (hex.length < 38) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add odd-length validation after prefix removal.

Odd-length hex causes misaligned parsing (marker, schemaId slices straddle byte boundaries). Currently returns undefined when marker check fails, but explicit validation improves debugging:

Suggested change
// Normalize: remove 0x prefix and work with lowercase hex
const hex =
data.startsWith("0x") || data.startsWith("0X")
? data.slice(2).toLowerCase()
: data.toLowerCase();
// Minimum suffix: 1+ byte codes + 1 byte codesLength + 1 byte schemaId + 16 bytes marker = 19 bytes = 38 hex chars
if (hex.length < 38) {
// Normalize: remove 0x prefix and work with lowercase hex
const hex =
data.startsWith("0x") || data.startsWith("0X")
? data.slice(2).toLowerCase()
: data.toLowerCase();
// Reject odd-length hex (not byte-aligned)
if (hex.length % 2 !== 0) {
return undefined;
}
// Minimum suffix: 1+ byte codes + 1 byte codesLength + 1 byte schemaId + 16 bytes marker = 19 bytes = 38 hex chars
if (hex.length < 38) {
return undefined;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/utils/builderCode.ts
Line: 129-136

Comment:
Add odd-length validation after prefix removal.

Odd-length hex causes misaligned parsing (marker, schemaId slices straddle byte boundaries). Currently returns `undefined` when marker check fails, but explicit validation improves debugging:

```suggestion
  // Normalize: remove 0x prefix and work with lowercase hex
  const hex =
    data.startsWith("0x") || data.startsWith("0X")
      ? data.slice(2).toLowerCase()
      : data.toLowerCase();

  // Reject odd-length hex (not byte-aligned)
  if (hex.length % 2 !== 0) {
    return undefined;
  }

  // Minimum suffix: 1+ byte codes + 1 byte codesLength + 1 byte schemaId + 16 bytes marker = 19 bytes = 38 hex chars
  if (hex.length < 38) {
    return undefined;
  }
```

How can I resolve this? If you propose a fix, please make it concise.

cursor[bot]

This comment was marked as resolved.

Remove dataSuffix.length <= 2 guard so valid 1-byte hex (e.g. 'ab') is
appended instead of silently ignored. Only skip when undefined or '0x'.

Made-with: Cursor
Comment thread src/utils/builderCode.ts
Comment thread src/utils/builderCode.ts
Comment thread src/wagmi/utils.ts
@yosriady yosriady merged commit 26bbe4c into main Mar 2, 2026
13 checks passed
@yosriady yosriady deleted the claude/builder-code-extraction-45Um9 branch March 2, 2026 09:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants