Add ERC-8021 builder code extraction support#161
Conversation
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
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
| export function extractBuilderCode( | ||
| data: string | undefined | null | ||
| ): string | undefined { |
There was a problem hiding this 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:
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.| // 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)); | ||
| } |
There was a problem hiding this 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:
// 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.| // Minimum suffix size: 1 byte codesLength + 1 byte schemaId + 16 bytes marker = 18 bytes = 36 hex chars | ||
| if (hex.length < 36) { | ||
| return undefined; |
There was a problem hiding this 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:
// 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.…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
| // 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; | ||
| } |
There was a problem hiding this 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:
| // 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.
Additional Comments (1)
The Because The fix is to extract 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 const builder_codes = extractBuilderCodes(
mutationType === "writeContract" ? variables.dataSuffix : data
);Prompt To Fix With AIThis 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
| } | ||
|
|
||
| // Extract builder codes from transaction data (ERC-8021) | ||
| const builder_codes = extractBuilderCodes(data); |
There was a problem hiding this 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:
// 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.| 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)); | ||
| } | ||
|
|
There was a problem hiding this 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:
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.… 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
| 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}`; | ||
| } |
There was a problem hiding this 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
dataSuffixis present - Handling when
dataSuffixis 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.| 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); |
There was a problem hiding this 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:
| 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.…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
| // 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) { |
There was a problem hiding this 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:
| // 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.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
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): ImplementsextractBuilderCodes()function that parses the ERC-8021 data suffix format to extract comma-separated builder code strings from transaction calldata0x80218021802180218021802180218021)0x) and unprefixed hex strings, case-insensitiveundefinedfor invalid or missing suffixesIntegration with transaction tracking: Updated
WagmiEventHandlerandFormoAnalyticsto extract and include builder codes in transaction eventsbuilder_codesfield to transaction event payloadsType definitions: Added
builder_codes?: string[]field toTransactionAPIEventinterfaceComprehensive test suite (
test/utils/builderCode.spec.ts): 172 lines of tests covering:Implementation Details
undefinedrather than throwing errors for malformed datahttps://claude.ai/code/session_01XRqHApjYMQaFJG9CLMwcSE
Note
Medium Risk
Adds new transaction parsing and threads a new optional
builder_codesfield through the analytics event pipeline; risk is mainly around calldata parsing/concatenation and potential compatibility issues with WagmiwriteContractvariable shapes.Overview
Adds ERC-8021 builder code support by introducing
extractBuilderCodes()and exporting it fromutils.Transaction tracking now attempts to parse builder codes from calldata and, when present, includes an optional
builder_codesfield in transaction events end-to-end (FormoAnalyticspayload building,TransactionAPIEventtyping, andEventFactory.generateTransactionEvent).For Wagmi
writeContracttracking, the handler now concatenatesdataSuffixonto encoded calldata (via newconcatCalldataWithSuffix()inwagmi/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 abuilder_codesfield 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, andisNaNguards.src/wagmi/utils.ts: NewconcatCalldataWithSuffixthat concatenates ABI-encoded calldata with the optionaldataSuffixfrom wagmi'swriteContractvariables usingviem.concatHexor a string fallback.src/wagmi/WagmiEventHandler.ts/src/FormoAnalytics.ts: Both transaction-tracking paths extract and propagatebuilder_codesto analytics events.src/types/events.ts:TransactionAPIEventgainsbuilder_codes?: string.Concerns:
registryAddress,chainId, andchainIdLengthfields 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.decodeCodeshelper silently drops empty segments from consecutive or trailing commas, meaning acodesLength-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-prefixeddataSuffixis passed: the viem path will throw (viem validates0xprefix at runtime), while the fallback strips the prefix correctly.Confidence Score: 3/5
Important Files Changed
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 dispatchedLast reviewed commit: 00108ee