# Collections

The ERC-721 Ethscriptions Collections protocol allows creators to build curated collections of ethscriptions with rich metadata and optional access control.

{% hint style="info" %}
Collections are an **AppChain-only** feature. They require smart contract execution on the L2.
{% endhint %}

## Overview

* **Collection**: A named set of ethscriptions with metadata (name, symbol, description, max supply)
* **Items**: Individual ethscriptions added to a collection
* **Merkle Enforcement**: Optional cryptographic restriction on which items can be added

## Creating a Collection

Use the `create_collection_and_add_self` operation to create a collection and add the first item in one transaction:

```
data:image/png;rule=esip6;p=erc-721-ethscriptions-collection;op=create_collection_and_add_self;d=<base64-json>;base64,<image-bytes>
```

{% hint style="info" %}
The `rule=esip6` parameter allows duplicate content. Without it, if the same data URI (including headers) was used in a previous ethscription, the new ethscription would be rejected as a duplicate. Uniqueness is based on SHA256 of the full data URI, not just the payload.
{% endhint %}

Where the base64-decoded `d` parameter contains:

```json
{
  "metadata": {
    "name": "My Collection",
    "symbol": "MYC",
    "max_supply": "100",
    "description": "A curated collection of digital artifacts",
    "logo_image_uri": "",
    "banner_image_uri": "",
    "background_color": "",
    "website_link": "https://example.com",
    "twitter_link": "myhandle",
    "discord_link": "https://discord.gg/...",
    "merkle_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "initial_owner": "0x1234567890abcdef1234567890abcdef12345678"
  },
  "item": {
    "item_index": "0",
    "name": "Item #1",
    "background_color": "#FF0000",
    "description": "The first item in the collection",
    "attributes": [
      { "trait_type": "Rarity", "value": "Legendary" },
      { "trait_type": "Color", "value": "Red" }
    ],
    "merkle_proof": []
  }
}
```

### Metadata Object Fields

All fields must be present in exact order. Use empty strings for optional values.

| Field              | Description                                                      |
| ------------------ | ---------------------------------------------------------------- |
| `name`             | Collection name                                                  |
| `symbol`           | Short symbol (e.g., "MYC")                                       |
| `max_supply`       | Maximum number of items (as string)                              |
| `description`      | Collection description (can be empty)                            |
| `logo_image_uri`   | Logo image as Data URI (can be empty)                            |
| `banner_image_uri` | Banner image as Data URI (can be empty)                          |
| `background_color` | Default background color (can be empty)                          |
| `website_link`     | Project website URL (can be empty)                               |
| `twitter_link`     | Twitter/X handle (can be empty)                                  |
| `discord_link`     | Discord invite URL (can be empty)                                |
| `merkle_root`      | Merkle root for access control (use zero bytes32 for owner-only) |
| `initial_owner`    | Address that will own the collection (lowercase)                 |

### Item Object Fields

| Field              | Description                                   |
| ------------------ | --------------------------------------------- |
| `item_index`       | Position in collection (0-indexed, as string) |
| `name`             | Item name                                     |
| `background_color` | Item-specific background color                |
| `description`      | Item description                              |
| `attributes`       | Array of `{ trait_type, value }` objects      |
| `merkle_proof`     | Array of proof hashes (for non-owner adds)    |

{% hint style="warning" %}
**Strict Key Order**: For JSON-based operations, keys must appear in exactly the order shown in the tables above. Attribute objects must use `{ "trait_type": "...", "value": "..." }` key order.
{% endhint %}

## Adding Items to a Collection

After creating a collection, add items with `add_self_to_collection`:

```
data:image/png;rule=esip6;p=erc-721-ethscriptions-collection;op=add_self_to_collection;d=<base64-json>;base64,<image-bytes>
```

Where the `d` parameter contains:

```json
{
  "collection_id": "0x...",
  "item": {
    "item_index": "1",
    "name": "Item #2",
    "background_color": "#00FF00",
    "description": "The second item",
    "attributes": [
      { "trait_type": "Rarity", "value": "Rare" },
      { "trait_type": "Color", "value": "Green" }
    ],
    "merkle_proof": []
  }
}
```

The `collection_id` is the L1 transaction hash of the collection creation.

## Merkle Proof Enforcement

When a collection has a non-zero `merkle_root`, non-owners must provide a merkle proof to add items. This ensures only pre-approved items with exact metadata can be added.

### How It Works

1. **Creator generates merkle tree** from approved items
2. **Each leaf** is computed from item metadata
3. **Creator sets merkle root** when creating collection
4. **Non-owners provide proofs** when adding items

### Merkle Leaf Computation

Each leaf is computed as:

```solidity
keccak256(abi.encode(
    contentHash,      // keccak256 of content bytes (bytes32)
    itemIndex,        // uint256
    name,             // string
    backgroundColor,  // string
    description,      // string
    attributes        // (string,string)[] - array of (trait_type, value) tuples
))
```

### Merkle Tree Structure

For a 3-item collection, the tree looks like:

```
        root
       /    \
    H(0,1)   leaf2
    /    \
 leaf0  leaf1
```

Where:

* **Proof for leaf0**: `[leaf1, leaf2]`
* **Proof for leaf1**: `[leaf0, leaf2]`
* **Proof for leaf2**: `[H(leaf0, leaf1)]`

### Pair Hashing

The merkle tree uses byte-wise ordering (same as OpenZeppelin):

```typescript
function hashPair(a: Hex, b: Hex): Hex {
  // Compare bytes, not strings
  const aBytes = hexToBytes(a);
  const bBytes = hexToBytes(b);
  let aLessThanB = false;
  for (let i = 0; i < 32; i++) {
    if (aBytes[i] !== bBytes[i]) {
      aLessThanB = aBytes[i] < bBytes[i];
      break;
    }
  }
  return keccak256(concat(aLessThanB ? [a, b] : [b, a]));
}
```

This ensures consistent proof verification regardless of sibling order.

### Adding Items with Proofs

Non-owners include the merkle proof in the `item` object:

```json
{
  "collection_id": "0x...",
  "item": {
    "item_index": "1",
    "name": "Item #2",
    "background_color": "#00FF00",
    "description": "The second item",
    "attributes": [
      { "trait_type": "Rarity", "value": "Rare" }
    ],
    "merkle_proof": ["0xaab5a305...", "0x58672b0c..."]
  }
}
```

### Owner Bypass

Collection owners can always add items without providing merkle proofs. This allows:

* Adding items not in the original tree
* Making corrections
* Flexibility for collection management

## Example: Creating a Merkle-Enforced Collection

This walkthrough creates a 3-item collection where:

| Item           | Index | Added By  | Merkle Proof Required? |
| -------------- | ----- | --------- | ---------------------- |
| Item 1 (Red)   | 0     | Owner     | No (owner bypass)      |
| Item 2 (Green) | 1     | Non-owner | Yes                    |
| Item 3 (Blue)  | 2     | Non-owner | Yes                    |

### Step 1: Compute Content Hashes

For each image, compute the keccak256 hash of the raw bytes:

```
Item 0 content hash: 0x666af27e...
Item 1 content hash: 0x06e51d26...
Item 2 content hash: 0x09ecc1a2...
```

### Step 2: Build Merkle Leaves

Compute each leaf from the item metadata:

```
Leaf 0: keccak256(abi.encode(0x666af27e..., 0, "Item #1", "#FF0000", "First item", [("Rarity", "Common")]))
        = 0xd9b535b9...

Leaf 1: keccak256(abi.encode(0x06e51d26..., 1, "Item #2", "#00FF00", "Second item", [("Rarity", "Rare")]))
        = 0xaab5a305...

Leaf 2: keccak256(abi.encode(0x09ecc1a2..., 2, "Item #3", "#0000FF", "Third item", [("Rarity", "Epic")]))
        = 0x58672b0c...
```

### Step 3: Compute Merkle Root

```
H(leaf0, leaf1) = 0x659a61c9...
Merkle Root = H(H(leaf0, leaf1), leaf2) = 0x06fbc22a...
```

### Step 4: Create Collection (Owner)

The owner creates the collection with the merkle root and adds the first item:

1. Send a 0 ETH transaction to any address
2. Include the hex-encoded Data URI with `op=create_collection_and_add_self`
3. The `merkle_root` is set to `0x06fbc22a...`
4. Save the transaction hash as `collection_id`

The owner doesn't need a merkle proof for their own item.

### Step 5: Add Items (Non-Owner)

A different address adds items 2 and 3 with merkle proofs:

For Item 2 (index 1):

```json
{
  "collection_id": "0x<tx-hash-from-step-4>",
  "item": {
    "item_index": "1",
    "name": "Item #2",
    "background_color": "#00FF00",
    "description": "Second item",
    "attributes": [
      { "trait_type": "Rarity", "value": "Rare" }
    ],
    "merkle_proof": ["0xd9b535b9...", "0x58672b0c..."]
  }
}
```

The proof must match exactly, and the metadata must match what was used to compute the leaf.

## Operations Reference

| Operation                        | Description                           |
| -------------------------------- | ------------------------------------- |
| `create_collection_and_add_self` | Create collection and add first item  |
| `add_self_to_collection`         | Add item to existing collection       |
| `edit_collection`                | Update collection metadata            |
| `edit_collection_item`           | Update item metadata                  |
| `transfer_ownership`             | Transfer collection ownership         |
| `renounce_ownership`             | Surrender ownership (to zero address) |
| `remove_items`                   | Delete items from collection          |
| `lock_collection`                | Prevent further additions             |

{% hint style="info" %}
**ESIP-6 is optional.** Add `rule=esip6` to your data URI only if you need to allow duplicate content (e.g., sending the same JSON command multiple times). Without it, an ethscription with identical content to an existing one will not be created. For image-based operations, add it to the header: `data:image/png;rule=esip6;p=...`. For text-based operations: `data:;rule=esip6,{...json...}`.
{% endhint %}

## Editing Collections

Update collection metadata with `edit_collection`. Send as a data URI:

```
data:;rule=esip6,{"p":"erc-721-ethscriptions-collection","op":"edit_collection",...}
```

JSON payload (all fields required; pass current values to keep them, empty strings will clear fields):

```json
{
  "p": "erc-721-ethscriptions-collection",
  "op": "edit_collection",
  "collection_id": "0x...",
  "description": "Updated description",
  "logo_image_uri": "",
  "banner_image_uri": "",
  "background_color": "",
  "website_link": "",
  "twitter_link": "",
  "discord_link": "",
  "merkle_root": "0x0000000000000000000000000000000000000000000000000000000000000000"
}
```

Only the collection owner can edit.

## Editing Items

Update item metadata with `edit_collection_item` (all fields required):

```json
{
  "p": "erc-721-ethscriptions-collection",
  "op": "edit_collection_item",
  "collection_id": "0x...",
  "item_index": "0",
  "name": "New Item Name",
  "background_color": "#FF0000",
  "description": "Updated description",
  "attributes": [
    { "trait_type": "Rarity", "value": "Legendary" }
  ]
}
```

Only the collection owner can edit items.

## Removing Items

Remove items with `remove_items` using ethscription IDs (transaction hashes):

```json
{
  "p": "erc-721-ethscriptions-collection",
  "op": "remove_items",
  "collection_id": "0x...",
  "ethscription_ids": [
    "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
    "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
  ]
}
```

Only the collection owner can remove items.

## Transferring Ownership

Transfer collection ownership with `transfer_ownership`:

```json
{
  "p": "erc-721-ethscriptions-collection",
  "op": "transfer_ownership",
  "collection_id": "0x...",
  "new_owner": "0x..."
}
```

Only the current owner can transfer ownership.

## Renouncing Ownership

Permanently surrender ownership with `renounce_ownership`:

```json
{
  "p": "erc-721-ethscriptions-collection",
  "op": "renounce_ownership",
  "collection_id": "0x..."
}
```

After renouncing, no one can edit the collection or add items (unless they have valid merkle proofs for a non-zero merkle root collection).

## Locking Collections

Once locked, no more items can be added:

```json
{
  "p": "erc-721-ethscriptions-collection",
  "op": "lock_collection",
  "collection_id": "0x..."
}
```

This is irreversible. Only the collection owner can lock.

## Error Handling

| Error                   | Cause                                                                                |
| ----------------------- | ------------------------------------------------------------------------------------ |
| `Invalid Merkle proof`  | Proof doesn't match root, or metadata differs from what was used to compute the leaf |
| `Merkle proof required` | Non-owner tried to add to a collection with zero merkle root (owner-only mode)       |
| `Item slot taken`       | Index already has an item                                                            |
| `Collection locked`     | Cannot add to locked collection                                                      |
| `Exceeds max supply`    | Collection is full                                                                   |
| `Not collection owner`  | Only owner can perform this operation                                                |

## Security Considerations

1. **Content Hash Verification** - The merkle leaf includes the content hash, ensuring exact image content is verified
2. **Metadata Binding** - All metadata is bound to the merkle proof and cannot be changed after the tree is computed
3. **Owner Bypass** - Collection owners can always add items, useful for corrections
4. **Locking** - Once locked, no more items can be added even with valid proofs
5. **Zero Merkle Root** - When `merkle_root` is zero, only the owner can add items

## Generating Merkle Trees (TypeScript)

Below is complete TypeScript code using [viem](https://viem.sh/) for generating merkle trees and collection calldata.

### Dependencies

```bash
npm install viem
```

### Helper Functions

```typescript
import {
  keccak256,
  encodeAbiParameters,
  stringToHex,
  concat,
  hexToBytes,
  type Hex,
} from 'viem';

/**
 * Compare two bytes32 values byte-by-byte (matches OpenZeppelin)
 */
function lt32(a: Hex, b: Hex): boolean {
  const aBytes = hexToBytes(a);
  const bBytes = hexToBytes(b);
  for (let i = 0; i < 32; i++) {
    if (aBytes[i] !== bBytes[i]) return aBytes[i] < bBytes[i];
  }
  return false;
}

/**
 * Hash pair with byte-wise ordering (matches OpenZeppelin MerkleProof)
 */
function hashPair(a: Hex, b: Hex): Hex {
  return keccak256(concat(lt32(a, b) ? [a, b] : [b, a]));
}

/**
 * Compute content hash from image bytes
 */
function computeContentHash(imageBase64: string): Hex {
  const imageBytes = Uint8Array.from(Buffer.from(imageBase64, 'base64'));
  return keccak256(imageBytes);
}

/**
 * Compute merkle leaf hash matching the Solidity contract
 */
function computeLeafHash(
  contentHash: Hex,
  itemIndex: bigint,
  name: string,
  backgroundColor: string,
  description: string,
  attributes: { traitType: string; value: string }[]
): Hex {
  const encoded = encodeAbiParameters(
    [
      { name: 'contentHash', type: 'bytes32' },
      { name: 'itemIndex', type: 'uint256' },
      { name: 'name', type: 'string' },
      { name: 'backgroundColor', type: 'string' },
      { name: 'description', type: 'string' },
      { name: 'attributes', type: 'tuple[]', components: [
        { name: 'traitType', type: 'string' },
        { name: 'value', type: 'string' },
      ]},
    ],
    [
      contentHash,
      itemIndex,
      name,
      backgroundColor,
      description,
      attributes.map(a => ({ traitType: a.traitType, value: a.value })),
    ]
  );
  return keccak256(encoded);
}

/**
 * Build merkle tree from 3 leaves
 *
 * Tree structure:
 *         root
 *        /    \
 *     H(0,1)   leaf2
 *    /    \
 * leaf0  leaf1
 */
function buildMerkleTree(leaves: [Hex, Hex, Hex]): {
  root: Hex;
  proofs: [Hex[], Hex[], Hex[]];
} {
  const [leaf0, leaf1, leaf2] = leaves;
  const h01 = hashPair(leaf0, leaf1);
  const root = hashPair(h01, leaf2);

  return {
    root,
    proofs: [
      [leaf1, leaf2],  // Proof for leaf0
      [leaf0, leaf2],  // Proof for leaf1
      [h01],           // Proof for leaf2
    ],
  };
}

/**
 * Generate data URI for collection operations
 */
function generateCollectionDataUri(
  operation: string,
  params: object,
  imageBase64: string
): string {
  const jsonBase64 = Buffer.from(JSON.stringify(params)).toString('base64');
  return `data:image/png;rule=esip6;p=erc-721-ethscriptions-collection;op=${operation};d=${jsonBase64};base64,${imageBase64}`;
}

/**
 * Convert data URI to hex calldata for transaction
 */
function dataUriToHex(dataUri: string): Hex {
  return stringToHex(dataUri);
}
```

### Complete Example

```typescript
// Sample 1x1 pixel PNGs (red, green, blue)
const IMAGES = [
  'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==',
  'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAEBgIApD5fRAAAAABJRU5ErkJggg==',
  'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPj/HwADBgIA/JDm2AAAAABJRU5ErkJggg==',
];

const ITEMS = [
  { name: 'Item #1', bg: '#FF0000', desc: 'First item', attrs: [{ traitType: 'Color', value: 'Red' }] },
  { name: 'Item #2', bg: '#00FF00', desc: 'Second item', attrs: [{ traitType: 'Color', value: 'Green' }] },
  { name: 'Item #3', bg: '#0000FF', desc: 'Third item', attrs: [{ traitType: 'Color', value: 'Blue' }] },
];

const OWNER_ADDRESS = '0xYourAddressHere';

// Step 1: Compute content hashes
const contentHashes = IMAGES.map(img => computeContentHash(img));
console.log('Content hashes:', contentHashes);

// Step 2: Compute merkle leaves
const leaves = ITEMS.map((item, i) => computeLeafHash(
  contentHashes[i],
  BigInt(i),
  item.name,
  item.bg,
  item.desc,
  item.attrs
)) as [Hex, Hex, Hex];
console.log('Leaves:', leaves);

// Step 3: Build merkle tree
const { root: merkleRoot, proofs } = buildMerkleTree(leaves);
console.log('Merkle root:', merkleRoot);
console.log('Proofs:', proofs);

// Step 4: Generate create collection calldata
const createParams = {
  metadata: {
    name: 'My Collection',
    symbol: 'MYC',
    max_supply: '3',
    description: 'A merkle-enforced collection',
    logo_image_uri: '',
    banner_image_uri: '',
    background_color: '',
    website_link: '',
    twitter_link: '',
    discord_link: '',
    merkle_root: merkleRoot,
    initial_owner: OWNER_ADDRESS.toLowerCase(),
  },
  item: {
    item_index: '0',
    name: ITEMS[0].name,
    background_color: ITEMS[0].bg,
    description: ITEMS[0].desc,
    attributes: ITEMS[0].attrs.map(a => ({ trait_type: a.traitType, value: a.value })),
    merkle_proof: [],  // Owner bypasses merkle check
  },
};

const createDataUri = generateCollectionDataUri(
  'create_collection_and_add_self',
  createParams,
  IMAGES[0]
);

console.log('Create collection data URI:', createDataUri);
console.log('Create collection hex:', dataUriToHex(createDataUri));

// Step 5: Generate add item calldata (for non-owner)
// Replace with actual collection_id after creating collection
const COLLECTION_ID = '0x<tx-hash-from-create>';

const addItemParams = {
  collection_id: COLLECTION_ID,
  item: {
    item_index: '1',
    name: ITEMS[1].name,
    background_color: ITEMS[1].bg,
    description: ITEMS[1].desc,
    attributes: ITEMS[1].attrs.map(a => ({ trait_type: a.traitType, value: a.value })),
    merkle_proof: proofs[1],  // Include proof for non-owner
  },
};

const addDataUri = generateCollectionDataUri(
  'add_self_to_collection',
  addItemParams,
  IMAGES[1]
);

console.log('Add item data URI:', addDataUri);
console.log('Add item hex:', dataUriToHex(addDataUri));
```

### Sending Transactions

To create an ethscription, send a 0 ETH transaction with the hex calldata:

```typescript
import { createWalletClient, http } from 'viem';
import { mainnet } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';

const account = privateKeyToAccount('0xYourPrivateKey');
const client = createWalletClient({
  account,
  chain: mainnet,
  transport: http('https://your-rpc-url'),
});

// Self-ethscription (send to yourself)
const txHash = await client.sendTransaction({
  to: account.address,
  data: dataUriToHex(createDataUri),
  value: 0n,
});

console.log('Transaction hash (this is the collection_id):', txHash);
```
