Skip to Content
📢 Breaking change: Applications using LedgerJS for transport implementation should migrate to the Device Management Kit (DMK). Learn more.

How to write metadata manually

This guide shows you how to write an ERC-7730 descriptor by hand, for the contracts and EIP-712 messages the JSON Builder cannot cover.

Write the file manually when you need any of the following:

  • One file that covers multiple deployments or contract variants
  • EIP-712 or other structured messages
  • Display logic that relies on nested paths, byte slices, or conditional rules
  • Reusable enums, constants, or shared definitions across contracts
  • The addressMatcher or factory context options for proxy patterns

If instead you target a single contract with clean function signatures and minimal formatting, use the JSON Builder — it is faster.

Prerequisites

  • Contract ABI or EIP-712 schema for every interaction you plan to support
  • Chain IDs and contract addresses
  • A list of the user-facing fields each signer must display

Write the descriptor

The goal is a JSON file that turns raw calldata (for example 0x23b872dd…) into a readable display such as Send — Recipient: alice.eth, Amount: 500 USDT. This walkthrough uses Tether (USDT) deployed on three chains.

Define the context

The context section binds the metadata to one or more contract deployments.

{ "$schema": "https://eips.ethereum.org/assets/eip-7730/erc7730-v1.schema.json", "context": { "$id": "Tether USD", "contract": { "abi": "https://api.etherscan.io/api?module=contract&action=getabi&address=0xdac17f958d2ee523a2206206994597c13d831ec7", "deployments": [ { "chainId": 1, "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7" }, { "chainId": 137, "address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F" }, { "chainId": 42161, "address": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" } ] } } }

Working with EIP-712? Replace the contract block with an eip712 definition. See the context reference for the full schema, including the addressMatcher and factory options for proxy patterns.

Add protocol metadata

The metadata section introduces the protocol to users and stores reusable values such as token definitions and enums.

"metadata": { "owner": "Tether", "info": { "legalName": "Tether Limited", "url": "https://tether.to/", "deploymentDate": "2017-11-28T12:41:21Z" }, "token": { "ticker": "USDT", "name": "Tether USD", "decimals": 6 } }

The metadata section also supports enums (value-to-label mappings) and constants (reusable values). See the metadata reference.

Define the display rules

The display.formats section maps contract function parameters to the labels and formats shown on the signer.

"display": { "formats": { "transfer(address,uint256)": { "intent": "Send", "fields": [ { "path": "_to", "label": "Recipient", "format": "addressOrName" }, { "path": "_value", "label": "Amount", "format": "tokenAmount", "params": { "tokenPath": "@.to" } } ], "required": ["_to", "_value"], "excluded": [] } } }

You can key each entry in display.formats by Solidity declaration, canonical signature, or raw selector — use whichever form your tooling already produces:

"transfer(address _to,uint256 _value)": { }, // Solidity declaration "transfer(address,uint256)": { }, // Canonical signature "0xa9059cbb": { } // 4-byte selector

Every function parameter must appear in either fields or excluded. Validation fails if any parameter is left unaccounted for.

For the full list of format types, path syntax, array helpers, and advanced patterns, see the format types reference and path system guide.

Reuse definitions with includes (optional)

To share display logic across multiple files, extract common snippets into a helper file and reference it with includes:

{ "$schema": "https://eips.ethereum.org/assets/eip-7730/erc7730-v1.schema.json", "includes": "common-swap.json", "context": { "contract": { "deployments": [{ "chainId": 324, "address": "0x6fd4383cB451173D5f9304F041C7BCBf27d561fF" }] } } }

Shared definitions live under display.definitions in the included file:

{ "display": { "definitions": { "sendAmount": { "label": "Amount to Send", "format": "tokenAmount", "params": { "tokenPath": "desc.srcToken" } } }, "formats": { "swap(...)": { "intent": "Swap", "fields": [ { "path": "desc.amount", "$ref": "$.display.definitions.sendAmount" } ] } } } }

When the including file redeclares the same key, its version takes precedence. Use this to override only the fields that differ per chain while keeping shared logic in one place.

Validate and submit

Run the ERC-7730 validator on your completed file, then follow Validate and submit to open a pull request to the registry.

The complete file

Combining the sections above produces the full descriptor:

{ "$schema": "https://eips.ethereum.org/assets/eip-7730/erc7730-v1.schema.json", "context": { "$id": "Tether USD", "contract": { "abi": "https://api.etherscan.io/api?module=contract&action=getabi&address=0xdac17f958d2ee523a2206206994597c13d831ec7", "deployments": [ { "chainId": 1, "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7" }, { "chainId": 137, "address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F" }, { "chainId": 42161, "address": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" } ] } }, "metadata": { "owner": "Tether", "info": { "legalName": "Tether Limited", "url": "https://tether.to/", "deploymentDate": "2017-11-28T12:41:21Z" }, "token": { "ticker": "USDT", "name": "Tether USD", "decimals": 6 } }, "display": { "formats": { "transfer(address,uint256)": { "intent": "Send", "fields": [ { "path": "_to", "label": "Recipient", "format": "addressOrName" }, { "path": "_value", "label": "Amount", "format": "tokenAmount", "params": { "tokenPath": "@.to" } } ], "required": ["_to", "_value"], "excluded": [] } } } }

More examples

{ "$schema": "https://eips.ethereum.org/assets/eip-7730/erc7730-v1.schema.json", "context": { "eip712": { "deployments": [ { "chainId": 1, "address": "0x119c71d3bbac22029622cbaec24854d3d32d2828" }, { "chainId": 10, "address": "0x11431a89893025d2a48dca4eddc396f8c8117187" }, { "chainId": 56, "address": "0x1e38eff998df9d3669e32f4ff400031385bf6362" }, { "chainId": 137, "address": "0x94bc2a1c732bcad7343b25af48385fe76e08734f" }, { "chainId": 42161, "address": "0x7f069df72b7a39bce9806e3afaf579e54d8cf2b9" } ], "domain": { "name": "Permit2" }, "schemas": [{ "primaryType": "OrderStructure", "types": { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "version", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ], "OrderStructure": [ { "name": "salt", "type": "uint256" }, { "name": "maker", "type": "address" }, { "name": "receiver", "type": "address" }, { "name": "makerAsset", "type": "address" }, { "name": "takerAsset", "type": "address" }, { "name": "makingAmount", "type": "uint256" }, { "name": "takingAmount", "type": "uint256" }, { "name": "makerTraits", "type": "uint256" } ] } }] } }, "metadata": { "owner": "1inch Limit Order Protocol" }, "display": { "formats": { "OrderStructure": { "intent": "1inch Order", "fields": [ { "path": "maker", "label": "From", "format": "raw" }, { "path": "makingAmount", "label": "Send", "format": "tokenAmount", "params": { "tokenPath": "makerAsset" } }, { "path": "takingAmount", "label": "Receive minimum", "format": "tokenAmount", "params": { "tokenPath": "takerAsset" } }, { "path": "receiver", "label": "To", "format": "raw" } ], "excluded": ["salt", "makerTraits"], "required": ["maker", "makingAmount", "takingAmount", "receiver"] } } } }

Troubleshooting

Validation fails with “unaccounted parameter”

Every parameter in the function signature must appear in either fields or excluded. Check the error message for the parameter name, then either add it to fields with an appropriate format or add it to the excluded array.

The function signature in my display key does not match

The key in display.formats must exactly match the function’s canonical signature, Solidity declaration, or 4-byte selector. Common mistakes:

  • Parameter names included when the canonical form is expected: use transfer(address,uint256), not transfer(address to,uint256 value)
  • Tuple syntax differs from the ABI: use the full tuple expansion, for example (address,uint256), not SwapParams
  • Extra whitespace around commas or parentheses

Path expression returns nothing or fails validation

Check the path root: # for the decoded data being signed (bare paths are relative to it), $ for values in the file, @ for the transaction envelope. See the path system reference for the full syntax.

ABI URL returns an error during validation

If the ABI URL is not reachable or returns an error during validation, embed the ABI directly in the file as a JSON array instead of a URL string. You can copy the ABI from Etherscan’s contract page under the Contract tab.

Next steps

Your descriptor is complete. Validate it and submit it to the registry:

For the full specification — factories, matchers, typed-data options, enums, constants, and includes — see the ERC-7730 reference and the ERC-7730 EIP .

Last updated on
Ledger
Copyright © Ledger SAS. All rights reserved. Ledger, Ledger Stax, Ledger Flex, Ledger Nano, Ledger Nano S, Ledger OS, Ledger Wallet, [LEDGER] (logo), [L] (logo) are trademarks owned by Ledger SAS.