Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/purple-pillows-sparkle.md
Copy link
Member

@jxom jxom Dec 9, 2025

Choose a reason for hiding this comment

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

This is a breaking change. Can we add a short migration snippet here via a diff statement?

e.g.

{ 
- foo: 'bar',
+ baz: 'hi',
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you have an example of what you mean I could look at?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Took a shot @jxom . Let me know if this is what you were looking for

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"ox": minor
---

**Breaking:** Aligned to latest ERC-8021 specification. Modified `Attribution.toDataSuffix` parameters to include `codeRegistry` instead of `registryAddress`.

```diff ts twoslash
Attribution.toDataSuffix({
codes: ['baseapp', 'morpho'],
- registryAddress: '0xcccccccccccccccccccccccccccccccccccccccc',
+ codeRegistry: {
+ address: '0xcccccccccccccccccccccccccccccccccccccccc`
+ chainId: 8453,
+ }
})
```
92 changes: 77 additions & 15 deletions src/erc8021/Attribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,19 @@ export type AttributionSchemaId0 = {
export type AttributionSchemaId1 = {
/** Attribution codes identifying entities involved in the transaction. */
codes: readonly string[]
/** Address of the custom code registry contract. */
codeRegistryAddress: Address.Address
/* The custom code registry contract. */
codeRegistry: AttributionSchemaId1Registry
/** Schema identifier (1 for custom registry). */
id?: 1 | undefined
}

export type AttributionSchemaId1Registry = {
/** Address of the custom code registry contract. */
address: Address.Address
/** Chain Id of the chain the custom code registry contract is deployed on. */
chainId: number
}

/**
* Attribution schema identifier.
*
Expand Down Expand Up @@ -71,7 +78,10 @@ export const ercSuffixSize = /*#__PURE__*/ Hex.size(ercSuffix)
*
* const schemaId2 = Attribution.getSchemaId({
* codes: ['baseapp'],
* codeRegistryAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
* codeRegistry: {
* address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
* chainId: 8453,
* }
* })
* // @log: 1
* ```
Expand All @@ -80,7 +90,7 @@ export const ercSuffixSize = /*#__PURE__*/ Hex.size(ercSuffix)
* @returns The schema ID (0 for canonical registry, 1 for custom registry).
*/
export function getSchemaId(attribution: Attribution): SchemaId {
if ('codeRegistryAddress' in attribution) return 1
if ('codeRegistry' in attribution) return 1
return 0
}

Expand Down Expand Up @@ -111,7 +121,10 @@ export declare namespace getSchemaId {
*
* const suffix = Attribution.toDataSuffix({
* codes: ['baseapp'],
* codeRegistryAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
* codeRegistry: {
* address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
* chainId: 8453,
* }
* })
* ```
*
Expand All @@ -133,15 +146,17 @@ export function toDataSuffix(attribution: Attribution): Hex.Hex {
const schemaIdHex = Hex.fromNumber(schemaId, { size: 1 })

// Build the suffix based on schema
if (schemaId === 1)
// Schema 1: codeRegistryAddress ∥ codes ∥ codesLength ∥ schemaId ∥ ercSuffix
if (schemaId === 1) {
const schema1 = attribution as AttributionSchemaId1
// Schema 1: codeRegistryAddress (20 bytes) ∥ chainId ∥ chainIdLength (1 byte) ∥ codes ∥ codesLength (1 byte) ∥ schemaId (1 byte) ∥ ercSuffix
return Hex.concat(
attribution.codeRegistryAddress!.toLowerCase() as Address.Address,
registryToData(schema1.codeRegistry),
codesHex,
codesLengthHex,
schemaIdHex,
ercSuffix,
)
}

// Schema 0: codes ∥ codesLength ∥ schemaId ∥ ercSuffix
return Hex.concat(codesHex, codesLengthHex, schemaIdHex, ercSuffix)
Expand Down Expand Up @@ -179,11 +194,14 @@ export declare namespace toDataSuffix {
* import { Attribution } from 'ox/erc8021'
*
* const attribution = Attribution.fromData(
* '0xddddddddcccccccccccccccccccccccccccccccccccccccc626173656170702c6d6f7270686f0e0180218021802180218021802180218021'
* '0xddddddddcccccccccccccccccccccccccccccccccccccccc210502626173656170702C6D6F7270686F0E0180218021802180218021802180218021'
* )
* // @log: {
* // @log: codes: ['baseapp', 'morpho'],
* // @log: codeRegistryAddress: '0xcccccccccccccccccccccccccccccccccccccccc',
* // @log: registry: {
* // @log: address: '0xcccccccccccccccccccccccccccccccccccccccc`
* // @log: chainId: 8453,
* // @log: }
* // @log: id: 1
* // @log: }
* ```
Expand Down Expand Up @@ -225,7 +243,7 @@ export function fromData(data: Hex.Hex): Attribution | undefined {
}

// Schema 1: Custom registry
// Format: codeRegistryAddress (20 bytes) ∥ codes ∥ codesLength (1 byte) ∥ schemaId (1 byte) ∥ ercSuffix
// Format: codeRegistryAddress (20 bytes) ∥ chainId ∥ chainIdLength (1 byte) ∥ codes ∥ codesLength (1 byte) ∥ schemaId (1 byte) ∥ ercSuffix
if (schemaId === 1) {
// Extract codes length (1 byte before schema ID)
const codesLengthHex = Hex.slice(
Expand All @@ -242,13 +260,13 @@ export function fromData(data: Hex.Hex): Attribution | undefined {
const codesString = Hex.toString(codesHex)
const codes = codesString.length > 0 ? codesString.split(',') : []

// Extract registry address (20 bytes before codes)
const registryStart = codesStart - 20
const codeRegistryAddress = Hex.slice(data, registryStart, codesStart)
// Extract registry by reading backwards from just before codes
const codeRegistry = registryFromData(Hex.slice(data, 0, codesStart))
if (codeRegistry === undefined) return undefined

return {
codes,
codeRegistryAddress,
codeRegistry,
id: 1,
}
}
Expand All @@ -257,6 +275,50 @@ export function fromData(data: Hex.Hex): Attribution | undefined {
return undefined
}

function registryFromData(
data: Hex.Hex,
): AttributionSchemaId1Registry | undefined {
// Expect at least: address (20 bytes) + chainIdLen (1 byte)
const minRegistrySize = 20 + 1
if (Hex.size(data) < minRegistrySize) return undefined

// Read chainId length from the last byte of the registry segment
const chainIdLenHex = Hex.slice(data, -1)
const chainIdLen = Hex.toNumber(chainIdLenHex)

if (chainIdLen === 0) return undefined

// Validate we have enough bytes to cover address + chainId + chainIdLen
const totalRegistrySize = 20 + chainIdLen + 1
if (Hex.size(data) < totalRegistrySize) return undefined

// Address is located immediately before chainId and chainIdLen (read from back)
const addressStart = -(chainIdLen + 1 + 20)
const addressEnd = -(chainIdLen + 1)
const addressHex = Hex.slice(data, addressStart, addressEnd)
// Chain ID occupies the bytes preceding the final length byte (read from back)
const chainIdHex = Hex.slice(data, -(chainIdLen + 1), -1)

const codeRegistry: AttributionSchemaId1Registry = {
address: addressHex as Address.Address,
chainId: Hex.toNumber(chainIdHex),
}

return codeRegistry
}

function registryToData(registry: AttributionSchemaId1Registry): Hex.Hex {
const chainIdAsHex = Hex.fromNumber(registry.chainId)
const chainIdLen = Hex.size(chainIdAsHex)
// Need to padleft because the output of size may not be a full byte (1 -> 0x1 vs 0x01)
const paddedChainId = Hex.padLeft(chainIdAsHex, chainIdLen)
return Hex.concat(
registry.address as Hex.Hex,
paddedChainId,
Hex.fromNumber(chainIdLen, { size: 1 }),
)
}

export declare namespace fromData {
type ErrorType =
| Hex.slice.ErrorType
Expand Down
Loading