# Aggregated Ethereum Identity Context - Full > Updated at: 22:35 05/28/25 # docs.efp.app llms-full.txt > Ethereum Follow Protocol (EFP) is an onchain social graph protocol for Ethereum accounts. **Is EFP a social network?** > No, EFP is just a social graph. It has no names or profiles (use [ENS](https://ens.domains/) for that), no authentication protocol (use [SIWE](https://login.xyz/)), nor posting or tweeting. It's a primitive of the Ethereum identity stack meant to be combined with others elements in that stack in third party apps. [This article](https://mirror.xyz/brantly.eth/7nJZCqyvhbdTIfq4oSnNEjlUUyxS9sf3pTHcBNi8Te8) explains the vision. **Can I post or tweet on EFP?** > No, EFP is just a social graph (e.g. who follows who). However, web3 social networks could use EFP as their social graph (just as they could use ENS for their usernames and profiles). **So I followed some people on EFP, now what?** > In the EFP app, there's an Onchain Feed (powered by [Interface](https://interface.social/)) showing the onchain activity of the people you follow, plus the Leaderboard that shows how you stack up against other EFP users. But the most important place to use EFP is in other apps that integrate it. We keep a non-exhaustive list of apps that have integrated EFP on our homepage. **How can my app integrate EFP?** > Any way you want; EFP is open protocol. Ideas include: using EFP to provide additional context for who an Ethereum account is (e.g. showing a user's EFP follower and following counts on their profile, showing friends of friends that follow them, etc), using followings for contacts or message filtering, recommendations, etc. [Our API](https://docs.efp.app/api/) is a convenient way to get EFP data. Message us on Discord or elsewhere to let us know you integrated EFP and we'll put your logo and link on our website. **Is EFP centralized?** > No. The core components of EFP are all onchain and decentralized. Our team maintains and runs an Indexer that mirrors all onchain EFP data to an offchain database for easy access and analysis for serving EFP data through our API. Our indexer is opensource, and anyone can spin up their own indexer or build their own. **Is there an organization behind the creation and maintenance of EFP?** > Yes, EFP is developed by the non-profit corporation Follow Protocol Foundation. **How is EFP funded?** > So far, it has been funded mostly by grants from the ENS DAO. We list the sources of our major grants on our homepage. **What is the relationship of EFP to ENS?** > EFP has a close relationship to ENS: EFP is designed to completement ENS and other elements of the Ethereum identity stack, e.g. EFP has no names or profiles, since it assumes composability with ENS; the ENS DAO has provided large grants to the development of EFP; and the creator of EFP, brantly.eth, used to be on the ENS core team and is still involved in the ENS DAO. **Can I have more than one EFP List?** > Yes, but it's usually not needed since you can use tags to sort different groups of people you follow in your one list. If you do have more than one List, only one can be designated as your Primary List (the list that represents your Ethereum account) at a time. **Can I follow other identifiers besides Ethereum addresses?** > Right now, EFP only support Ethereum addresses, but we plan to support other identifiers (ENS names, NFT smart contracts, etc) in the future. **Can I see the EFP code?** > Yep, it's all [open source on Github](https://github.com/ethereumfollowprotocol). **Can I reset a list? As in, reset the roles and clear all the list records.** > If you are the Owner role of the list, then yes. This is useful if you bought an EFP List number on a secondary market, or just want to start over who you're following but keep your same list number. Here's how: 1) In the Connect Wallet menu, ensure you have selected the list you want to reset. 2) Go to My Profile in the nav bar, click the 3 dot menu next to your name, and select List Settings. 3) Click Edit Settings, then Reset List. You'll then be prompted to do two transactions. **I'm bored.!** > After you've set up your list on EFP, [check this out](https://hackertyper.net/). --- Ethereum Follow Protocol (EFP) is an onchain social graph protocol for Ethereum accounts. ## EFP List NFT Users mint an EFP List NFT to create an **EFP List**. Minting an EFP List NFT is free (plus gas). ### Roles Every EFP List has three roles, each of which are held by an Ethereum address. 1. **Owner:** - Is the owner of the EFP List NFT - Can transfer ownership of the EFP List NFT - Can edit the List Storage Location, which stores the records for that list, as well as who the Manager and User are 2. **Manager:** - This is set in the List Records contract, not the NFT - Is the manager of the EFP list records and metadata - Can transfer the Manager role to another address - Can set or update the user - Can add/remove list records and add/remove tags - Can add metadata key/value to the list 3. **User:** - This is set in the List Records contract, not the NFT - The Ethereum address for whom the list is intended; the Ethereum address that is following the Ethereum addresses in the EFP List. Typically, all three roles (Owner, Manager, User) are the same Ethereum address, but they can be different. --- ## List Storage Location Your EFP List NFT specifies a **List Storage Location** where the **List Records** are stored, which can be one of the following: - Ethereum L1 smart contract - Ethereum L2 smart contract - CCIP-read pointers for off-chain storage (future) The List Storage Location itself (the smart contract or off-chain system) must specify a Manager role, an Ethereum account that is able to edit the List Records. Typically, the Manager will be the same Ethereum account as the Owner and User roles of the EFP List NFT, but they can be different. To prevent frontrunning, a user should first claim a slot number in their chosen List Storage Location, then mint their EFP NFT and set their List Storage Location (with the chain, smart contract address, and secured slot number). --- ## List Records An EFP List is formed from a set of **List Records**. Each record has a record type, but only one record type is supported at launch: - **Address Record** - Contains an Ethereum address, with zero or more tags. - These records are typically interpreted as a "follow" of the specified Ethereum address, with some exceptions explained in the Followers section below. --- ## Order of operations While a user may interact with the EFP smart contracts in any order (no order is enforced in the smart contracts), it's recommended that to prevent frontrunning a user should first claim a slot number in their chosen List Storage Location, then next mint their EFP NFT and set their List Storage Location (with the chain, smart contract address, and secured slot number). --- ## Tags A **Tag** is a string associated with a List Record in an EFP list. Tags only count for an account if that account is also followed by the user, otherwise they're not counted. List Records can have zero or more tags. A few tags are standardized with specified semantics. Users may also set custom tags. ## Standard Tags - **no tag** - If a List Record has no tags, it is interpreted as a simple follow without further categorization. - **"block"** - This tag means neither the user nor the blocked account should see each other’s activity in a relevant app. - List Records with this tag are not included in Followers count, even if the List Record has other tags. - If both “block” and “mute” tags are present, “block” takes precedence. - **"mute"** - This tag means the user shouldn't see the muted account’s activity in a relevant app, but the muted account might still be able to see the user’s activity. - List Records with this tag are not included in Followers count, even if the List Record has other tags. - If both “block” and “mute” tags are present, “block” takes precedence. - **"top8"** - This tag means the account should appear in the user's "Top 8" in UIs that support it. - If a user has more than eight followed accounts with the "top8" tag, then only show the eight most recent should be included in a "Top 8" displayed in a UI. ### Custom Tags Users can use additional arbitrary custom tags. A custom tag can be any UTF-8 string with the following constraints: - maximum length of 255 bytes - no leading or trailing whitespace - more constraints to be added as needed --- ## Account Metadata EFP provides an Account Metadata contract that allows users to set EFP-related metadata specific to their Ethereum account, namely to specify a Primary List. ### Primary List Determining if a list is a Primary List is a two step process: the Ethereum account that set it as the Primary List in Account Metadata must match the User role for the list. Apps should first check the Primary List value for an Ethereum account, and if set, default to using that EFP List for that Ethereum account. Only Primary Lists are counted as Followers. --- ## Social Graph The social graph is formed from the union of all Primary Lists. The User role of each Primary List determines which Ethereum account is following the Ethereum addresses in that List. ### Followers **Followers** is the total number of EFP Primary Lists that follow a particular account, excluding those whoe block or mute the account. ### Following **Following** is the total number of unique Ethereum accounts followed by a list, excluding accounts tagged with “block” or “mute”. --- ## Base Colors | Color | Hex | | ------------- | ------- | | Dark Grey | #333333 | | Yellow | #FFF500 | | Pink | #FF79C9 | | Follow Button | #FFE066 | | Addition | #A1F783 | | Deletion | #FF7C7C | | Text Neutral | #999999 | ## Theme Colors | Theme | Neutral | Text | Grey | NavItem | | --------- | ------- | ------- | ------- | --------- | | Light | #ffffff | #000000 | #E4E4E7 | #b4b4b4 | | Dark | #333333 | #ffffff | #71717A | #94a3b822 | | Halloween | #000000 | #ffffff | #61616A | #94a3b822 | --- List of **EFP Logos** in various file formats. | Format | SVG | PNG | | --- | --- | --- | | Logo With Text (light) | Full Logo SVG **Download** | Full Logo PNG **Download** | | Logo With Text (dark) | Full Logo SVG **Download** | Full Logo PNG **Download** | | Logo Only | Logo SVG **Download** | Logo PNG **Download** | --- EFP is for everyone, and that includes translating the app to make it more easily accessible to a wider range of users. Please consider using your expertise to help us do that! *Before submitting a translation, please read this entire page carefully.* ## Rewards Those who either submit a full translation or make a significant contribution to one may earn: - Up to **$200** (paid in Ethereum L1 USDC) - A special **EFP Translator POAP** Whether a submitter earns one, both, or part the above rewards is determined at our discretion, based on the quality of the submission and extent of their contribution. If you earn a reward, we'll send it to you soon after your translation has been merged to our app. ## Qualifying Languages We now have a large number of languages, thanks to everyone who contributed! Going forward, we will focus on languages with a significant number of speakers and/or significant number of crypto users. Check which languages we already have and only submit if you think it meets the above criteria. Feel free to email us asking us if the language would be accepted before you make the translation. ## How to Submit or Contribute **Before submitting a full translation for a new language, check to see if we already have a translation for it.** You can check the language selector menu in the app or the [translations folder on Github](https://github.com/ethereumfollowprotocol/app/tree/main/public/locales), and you may want to check [pull requests](https://github.com/ethereumfollowprotocol/app/pulls) to see if one is pending approval. **Only submit or contribute to translations for languages of which you are a native speaker** (or, in the case of dead or fictional languages, a fluent speaker). Don't use ChatGPT, Google Translate, or similar services. EFP uses niche, technical language, so **we need crypto-natives who can give us the correct translations**. You can use one of two methods for submitting a translation: ### 1) Github Make a copy of [this folder](https://github.com/ethereumfollowprotocol/app/tree/main/public/locales/en) which contains the English translation, translate all the words and phrases on the right side of the ":" in each line from English to the new language, and submit your translation as a pull request. Please kee all quotes, line breaks, and commas in place. Be sure to include your ENS name (for Ethereum USDC) and an email address (for POAP claim link) in your pull request so that, if accepted, we can send you your rewards. If you'd like to keep that information private, feel free to email them to [translations@ethfollow.xyz](mailto:translations@ethfollow.xyz). ### 2) Email If you don't know how to use Github, go to [this page](https://github.com/ethereumfollowprotocol/app/blob/main/public/locales/en/translations.json), copy and paste the text into an email, translate all the words and phrases on the right side of the ":" in each line from English to the new language, and email the translation to us at [translations@ethfollow.xyz](mailto:translations@ethfollow.xyz). Please kee all quotes, line breaks, and commas in place. Be sure to include your ENS name (for Ethereum USDC) and an email address (for POAP claim link) in your pull request so that, if accepted, we can send you your rewards. ## Corrections If you are a native speaker of a language and see an improper translation, you may submit a correction by the above mentioned methods. Your potential reward will be proportionate to your contribution. ## Ongoing expansions to translations As we add new features to the app, we will add new text in English. If a translation doesn't exist for a particular word or phrase, the app will fallback to the English translation. So **if you see English when using another language in the app, it's likely the language's translation file needs a translation added for that word or phrase**. If you are a native speaker of a language and see a missing translation, you may submit an update by the above mentioned methods. Your potential reward will be proportionate to your contribution. --- A **List Op** is a structure used to encapsulate an operation performed on a List. It includes the following fields: - `version`: A `uint8` representing the version of the List Op. This byte defines the schema of the subsequent bytes for encoding/decoding. This is used to ensure compatibility and facilitate future upgrades. - `opcode`: A `uint8` indicating the operation code. This defines the action to be taken using the List Op. - `data`: A `bytes` array which holds the operation-specific data. For instance, if the operation involves adding a List Record, this field would contain the encoded List Record. The version is always `1`. ## Operation Codes There are four operations defined at this time: | Code | Operation | Data | | ------- | ------------- | ------------------------------------ | | 0 | Reserved | N/A | | 1 | Add record | Encoded `ListRecord` | | 2 | Remove record | Encoded `ListRecord` | | 3 | Tag record | Encoded `ListRecord` followed by tag | | 4 | Untag record | Encoded `ListRecord` followed by tag | | 5 - 255 | Reserved | N/A | ## Encoding `ListOps` are encoded as byte arrays, starting with a one-byte `version` and a one-byte `opcode`, followed by the `data` of variable length. ``` +------------------+-----------------+-------------------------------+ | version (1 byte) | opcode (1 byte) | data (variable length) | +------------------+-----------------+-------------------------------+ ``` The encoding of a `ListOp` is designed to be flexible, accommodating various types of operations and their corresponding data structures. | Byte(s) | Description | | ------- | ------------------------------- | | 0 | `ListOp` version (1 byte) | | 1 | Operation code (1 byte) | | 2 - N | Encoded operation-specific data | The `2 - N` byte range is variable and depends on the operation being performed. ### Example - Add Record The following is an example of an encoded `ListOp` for adding a `ListRecord` of type 1 (address record) to a list: | Byte(s) | Description | Value | | ------- | --------------------------------- | -------------------------------------------- | | 0 | `ListOp` version (1 byte) | `0x01` | | 1 | Operation code (1 byte) | `0x01` | | 2 | `ListRecord` version (1 byte) | `0x01` | | 3 | `ListRecord` record type (1 byte) | `0x01` | | 4 - 23 | `ListRecord` data (20 bytes) | `0x00000000000000000000000000000000DeaDBeef` | ### Example - Remove Record The following is an example of an encoded `ListOp` for removing a `ListRecord` of type 1 (address record) from a list: | Byte(s) | Description | Value | | ------- | --------------------------------- | -------------------------------------------- | | 0 | `ListOp` version (1 byte) | `0x01` | | 1 | Operation code (1 byte) | `0x02` | | 2 | `ListRecord` version (1 byte) | `0x01` | | 3 | `ListRecord` record type (1 byte) | `0x01` | | 4 - 23 | `ListRecord` data (20 bytes) | `0x00000000000000000000000000000000DeaDBeef` | ### Example - Tag Record The following is an example of an encoded `ListOp` for tagging a `ListRecord` of type 1 (address record) in a list: | Byte(s) | Description | Value | | ------- | --------------------------------- | -------------------------------------------- | | 0 | `ListOp` version (1 byte) | `0x01` | | 1 | Operation code (1 byte) | `0x03` | | 2 | `ListRecord` version (1 byte) | `0x01` | | 3 | `ListRecord` record type (1 byte) | `0x01` | | 4 - 23 | `ListRecord` data (20 bytes) | `0x00000000000000000000000000000000DeaDBeef` | | 24 - N | Tag (variable) (UTF-8) | `0x746167` ("tag") | The tag should be encoded as UTF-8. ### Example - Untag Record The following is an example of an encoded `ListOp` for untagging a `ListRecord` of type 1 (address record) in a list: | Byte(s) | Description | Value | | ------- | --------------------------------- | -------------------------------------------- | | 0 | `ListOp` version (1 byte) | `0x01` | | 1 | Operation code (1 byte) | `0x04` | | 2 | `ListRecord` version (1 byte) | `0x01` | | 3 | `ListRecord` record type (1 byte) | `0x01` | | 4 - 23 | `ListRecord` data (20 bytes) | `0x00000000000000000000000000000000DeaDBeef` | | 24 - N | Tag (variable) (UTF-8) | `0x746167` ("tag") | The tag should be encoded as UTF-8. ## Code List Op can be represented as a type in any programming language. Here are some examples: #### Go ```go type ListOp struct { Version uint8 Opcode uint8 Data []byte } ``` #### Python ```python class ListOp: version: int # 0-255 opcode: int # 0-255 data: bytes ``` #### Rust ```rust struct ListOp { version: u8, opcode: u8, data: Vec, } ``` #### Solidity ```solidity /** * @dev The EFP contracts don't use this struct; they only store list ops * as `bytes` with the version, opcode, and data fields tightly packed * into a single `bytes` value. However, this struct can be useful for * offchain processing with foundry or other Solidity tooling */ struct ListOp { uint8 version; uint8 opcode; bytes data; } ``` #### TypeScript ```typescript type ListOp = { version: number // 0-255 opcode: number // 0-255 data: Uint8Array } ``` --- Every Ethereum account (address) automatically is supported by EFP. Users may follow any other address and any address can mint an EFP List NFT. It useful to store EFP-related metadata that is specific to an account, such as a user's primary EFP list. This is key-value metadata for an account is called **Account Metadata**. Any Ethereum account may store key-value data in the `EFPAccountMetadata` contract specific to their account. Data is stored by `string` key and `bytes` value, for each Ethereum account. This allows for the storage of account-specific EFP configuration or preference data. ## Global Keys Global Keys must be made up of lowercase letters, numbers and the hyphen (-) character. There is only one global key for account metadata currently defined. | Key | Description | | -------------- | --------------------------------------------------------------- | | `primary-list` | The 32-byte token ID of the primary EFP List NFT for an account | ## primary-list The `primary-list` key is used to specify the Primary List for an account. (Note: For a list to be considered a Primary List, the User role for the list must match the account that specified the list as its Primary List in Account Metadata. Only Primary Lists are counted in Followers.) The Primary List is represented as a 32-byte token id. | Byte(s) | Description | Value | | --- | --- | --- | | 0-31 | Token ID (32 bytes) | 0x0000000000000000000000000000000000000000000000000000000000000001 | ### Code with example code shown below: ```solidity // set the primary EFP List for the caller's address efpAccountMetadata.setValue("primary-list", abi.encodePacked(tokenId)); ``` By reading the `primary-list` key for a given address, a client can determine the primary EFP List for that address. ```solidity address addr =
uint primaryEfpListTokenId = abi.decode(efpAccountMetadata.getValue(addr, "primary-list"), (uint)); // validate: primary EFP List must exist require(primaryEfpListTokenId < efpListRegistry.totalSupply()); // validate the user for this EFP List is the caller address user = abi.decode(efpListMetadata.getValue(primaryEfpListTokenId, "user"), (address)); require(user == addr); ``` ## Custom Keys The format for custom keys are undefined at this time but will be defined in the future. --- Every EFP List has three roles, each of which are held by an Ethereum address. 1. **Owner** - Is the ERC-721 owner of the EFP List NFT - Can transfer ownership of the EFP List NFT - Can set or update the List Storage Location 2. **Manager** - Set in the List Metadata, stored in the List Storage Location - Is the manager of the List Records and List Metadata - Can transfer the Manager role to another address - Can set or update the User role - Can add or remove list records to the list - Can add or remove tags - Can set or update List Metadata 3. **User** - Set in the List Metadata, stored in the List Storage Location - Is the Ethereum address for whom the list is intended; the Ethereum account that is following the Ethereum accounts (list records) in the list. Typically, all three roles (Owner, Manager, User) are the same Ethereum account, but they can be different. The Owner role is the most important since it controls the List Storage Location, so use a secure wallet (e.g. hardware wallet) to hold the EFP List NFT and always exercise caution when transferring ownership or updating the List Storage Location. Only you are responsible for securing your EFP List NFT. --- Every EFP List has associated key-value metadata called **List Metadata**. List Metadata is stored onchain alongside List Records in the same contract, but in a different format (key-value pairs). Data is stored with a `string` key and `bytes` value, for each EFP List. Only the Manager of the EFP List can set or update the metadata for a list. This allows Managers to store list-specific configuration or preference data. ## Global keys Global Keys must be made up of lowercase letters, numbers and the hyphen (-) character. There are currently two global keys defined for List Metadata: | Key | Description | | --------- | ---------------------------------------------------------------------- | | `manager` | The Ethereum address of the Manager role associated with the EFP List, which is able to edit List Records.| | `user` | The Ethereum address of the User role associated with the EFP List, which is the Ethereum account that is following the Ethereum accounts in the list.| ### "manager" The `manager` key is used to store the Manager role associated with an EFP List. The manager is represented as a 20-byte address. | Byte(s) | Description | Value | | ------- | ------------------ | ------------------------------------------ | | 0-19 | Address (20 bytes) | 0x00000000000000000000000000000000DeaDBeef | #### Code ```solidity // set the manager for the EFP list efpListMetadata.setValue(tokenId, "manager", abi.encodePacked(manager)); ``` By reading the `manager` key for a given List, a client can determine the Manager associated with that list. ```solidity address manager = abi.decode(efpListMetadata.getValue(tokenId, "manager"), (address)); ``` ### "user" The `user` key is used to store the User role associated with an EFP List. The user is represented as a 20-byte address. | Byte(s) | Description | Value | | ------- | ------------------ | ------------------------------------------ | | 0-19 | Address (20 bytes) | 0x00000000000000000000000000000000DeaDBeef | #### Code with example code shown below: ```solidity // set the user for the EFP List efpListMetadata.setValue(tokenId, "user", abi.encodePacked(user)); ``` By reading the `user` key for a given EFP List, a client can determine the User associated with that list. ```solidity address user = abi.decode(efpListMetadata.getValue(tokenId, "user"), (address)); ``` ## Future This pattern can be extended to support other list-specific metadata such as a name or description. ## Custom Keys The format for custom keys are undefined at this time but will be defined in the future. --- The **EFP List Registry** List Registry is an Ethereum ERC-721 contract `EFPListRegistry` where the NFT represents ownership of an EFP List. Users mint an EFP List NFT to create an **EFP List**. Minting an EFP List NFT is free (plus gas). --- A **List Record** is a fundamental EFP data structure representing a record in an EFP List. Each List Record consists of the following three components: - `version`: A `uint8` representing the version of the List Record. This is used to ensure compatibility and facilitate future upgrades. - `record_type`: A `uint8` indicating the type of record. This serves as an identifier for the kind of data the record holds. - `data`: A `bytes` array containing the actual data of the record. The structure of this data depends on the record type. The version is always `1`. ## Record Types There is only one record type defined at this time: | Type | Description | Data | Data Length | | ------- | ----------- | --------------- | ----------- | | 0 | Reserved | N/A | N/A | | 1 | Address | 20-byte address | 20 | | 2 - 255 | Reserved | N/A | N/A | Record types 0 and 2-255 are reserved for future use. To illustrate the design, however, consider hypothetical list record types: - a subscription to another EFP List, where the `data` field would contain the 32-byte token ID of the corresponding EFP NFT. - an encrypted list record, where the `data` field would contain a list record encrypted with the public key of the list owner/manager/user (for privacy). - an ERC-721 NFT token, where the `data` field would contain the 20-byte address of the ERC-721 contract, and the 32-byte token ID. - an ERC-1155 token, where the `data` field would contain the 20-byte address of the ERC-1155 contract, the 32-byte token ID (exclude token amount). - an ENS name, where the `data` field would contain the normalized string of the ENS name. - a DNS name, where the `data` field would contain the normalized string of the DNS name. - an IP address, where the `data` field would contain the IP address string. - an email address, where the `data` field would contain the email address string. - a torrent magnet link, where the `data` field would contain the magnet link string. - a git repository URL, where the `data` field would contain the git remote URL string. - an RSS feed, where the `data` field would contain the string URL of the RSS feed. - an Atom feed, where the `data` field would contain the string URL of the Atom feed. - a DID record, where the `data` field would contain the DID string. - a custom record, where the `data` field would contain arbitrary or custom data. Clients may support some or all of these record types depending on use case (once more than one record type is defined). ### Address Record (Type 1) The following is an example of an encoded `ListRecord` of type 1 (address record): | Byte(s) | Description | Value | | ------- | --------------------------------- | -------------------------------------------- | | 0 | `ListRecord` version (1 byte) | `0x01` | | 1 | `ListRecord` record_type (1 byte) | `0x01` | | 2 - 21 | `ListRecord` data (20 bytes) | `0x00000000000000000000000000000000DeaDBeef` | ## Validation `version` should always be `1`. `record_type` should always be `1` until other record types are defined. `data` should always be exactly 20 bytes for record type `1`. If any of the above conditions are not met, the `ListRecord` is considered invalid and should not be processed. ## Encoding Onchain, list records are packed into byte arrays with the **version** and **record type** prepended to **data** to form an array with `2 + data.length` bytes. ``` +------------------+---------------------+------------------------+ | version (1 byte) | recordType (1 byte) | data (variable length) | +------------------+---------------------+------------------------+ ``` This byte array will itself be a subarray of the list op data. ## Decoding Managers have permissions to upload arbitrary list record data, so clients should be prepared to handle unexpected data. When decoding a `ListRecord`, the `version` and `recordType` fields should be checked to ensure compatibility. The length of the `data` field should be checked to ensure it is the expected length for the given `recordType`. If the length of the `data` field is unexpected, the `ListRecord` should generally be ignored and not processed. ## Tags A `Tag` is a string associated with a `ListRecord` in a list. A `ListRecord` can have multiple tags associated with it. A `Tag` is represented as a string. ### Normalization Tags are normalized by converting them to lowercase and removing leading and trailing whitespace. Tags should be normalized before they are encoded into a `ListOp`. ## Code List Record can be represented as a type in any programming language. Here are some examples: ### Go ```go type ListRecord struct { Version uint8 RecordType uint8 Data []byte } ``` ### Python ```python class ListRecord: version: int # 0-255 record_type: int # 0-255 data: bytes ``` ### Rust ```rust struct ListRecord { version: u8, record_type: u8, data: Vec, } ``` ### Solidity ```solidity /** * The EFP contracts don't use this struct; they only store list ops * as `bytes` and do not store list records directly. However, this * struct can be useful for offchain processing with foundry or other * Solidity tooling */ struct ListRecord { uint8 version; uint8 recordType; bytes data; } ``` ### TypeScript ```typescript type ListRecord = { version: number // 0-255 recordType: number // 0-255 data: Uint8Array } ``` --- A **List Storage Location** defines where the list records and metadata are stored for an EFP List. The List Storage Location value is stored in the main List Registry contract and can be changed at any time by the List NFT owner. ## Onchain Storage All EFP data is stored on either Ethereum L1 or a supported L2. An EFP List can be uniquely specified via three pieces of data: - `chain_id`: The 32-byte EVM chain ID of the chain where the list is stored. - `contract_address`: The 20-byte EVM address of the contract where the list is stored. - `slot`: A 32-byte value that specifies the storage slot of the list within the contract. This disambiguates multiple lists stored within the same contract and de-couples it from the EFP List NFT token id which is stored on Ethereum and inaccessible on L2s. The following chains are supported: | Chain ID | Name | Layer | Type | 32-Byte Chain ID | | --- | --- | --- | --- | --- | | 1 | Ethereum | L1 | Ethereum | `0x0000000000000000000000000000000000000000000000000000000000000001` | | 8453 | Base | L2 | Ethereum | `0x0000000000000000000000000000000000000000000000000000000000002105` | | 10 | OP Mainnet | L2 | Ethereum | `0x000000000000000000000000000000000000000000000000000000000000000a` | ## Serialization List Storage Locations are encoded in a versioned, flexible data structure. Each List Storage Location is encoded as a `bytes` array with the following structure: - `version`: A `uint8` representing the version of the List Storage Location. This is used to ensure compatibility and facilitate future upgrades. - `location_type`: A `uint8` indicating the type of list storage location. This serves as an identifier for the kind of data the data field contains. - `data`: A `bytes` array containing the actual data of the list storage location. The structure of this data depends on the location type. The version is always `1`. The location type is always `1`. Since only one location type is currently supported, the `data` field is always a `bytes` array of length `32 + 20 + 32 = 84`. ## Location Types There is only one location type which covers both Ethereum and L2s. | Location Type | Description | Data | | ------------- | ------------ | ---------------------------------------------------------- | | 0 | Reserved | N/A | | 1 | EVM contract | 32-byte chain ID + 20-byte contract address + 32-byte slot | | 2 - 255 | Reserved | N/A | ## Code List Storage Location can be represented as a type in any programming language. Here are some examples: #### Go ```go type ListStorageLocation struct { Version uint8 LocationType uint8 Data []byte } ``` #### Python ```python class ListStorageLocation: version: int # 0-255 location_type: int # 0-255 data: bytes ``` #### Rust ```rust struct ListStorageLocation { version: u8, location_type: u8, data: Vec, } ``` #### Solidity ```solidity /** * The EFP contracts don't use this struct; they only store list * storage locations as `bytes` with the version, location type, * and data fields tightly packed into a single `bytes` value * however, this struct can be useful for offchain processing with * foundry or other Solidity tooling */ struct ListStorageLocation { uint8 version; uint8 locationType; bytes data; } ``` #### TypeScript ```typescript type ListStorageLocation = { version: number // 0-255 locationType: number // 0-255 data: Uint8Array } ``` --- **Account** (or **Ethereum account**) > An EVM account. There are no "EFP accounts". **Account Metadata** > EFP provides an Account Metadata contract that allows users to set EFP-related metadata specific to their Ethereum account, such as identifying the Primary List for that Ethereum account. (see [Account Metadata](https://docs.efp.app/design/account-metadata/)) **Address** (or **Ethereum address**) > An EVM address (e.g. 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045). There are no "EFP addresses". **Block** > A standard tag that means neither the user nor the blocked account should see each other’s activity in a relevant app; lists with this tag are not included in Followers count, even if the list tags the account with other tags; if both “block” and “mute” tags are present, “block” takes precedence. (see [Tags](https://docs.efp.app/design/tags/)) **EFP app** > Web app that enables someone to create and manage a List on EFP. (see [EFP website](https://efp.app/)) **Ethereum Follow Protocol** (or **EFP**) > An onchain social graph protocol for Ethereum accounts. The protocol whose docs you are currently reading. (see [EFP website](https://efp.app/)) **Ethereum Name Service** (or **ENS**) > A distributed, open, and extensible naming system based on the Ethereum blockchain. EFP is designed to complement ENS, and the EFP web app makes extensive use of ENS for names, avatar, and other profile data. (see [ENS website](https://ens.domains/)) **Followers** > The total number of Primary Lists that follow a particular Ethereum account, excluding lists that tag them with "block" or "mute". **Following** > The total number of Ethereum accounts followed by a list, excluding accounts tagged with "block" or "mute". **List** > An NFT in the EFP List Registry and its accompanying List Storage Location data. **List Metadata** > Metadata for a list, stored alongside List Records in a list's List Storage Location, that specifies the Ethereum accounts set for the roles Manager and User. **List Number** > The token ID for the NFT that represents a list. **List Ops** (or **List Operation**) > The same as a **List Record**; An operation performed on an EFP List, such as appending a list record (for following a particular Ethereum account, unfollowing, tagging, untagging). **List Records** > The set of list operations stored at your List Storage Location. (see [List Records](https://docs.efp.app/design/list-records/)) **List Registry** > An Ethereum ERC-721 contract where the NFT represents ownership of a particular list. **List Settings** > A term used in the EFP app that refers to a variety of settings for a list, including: the list's List Storage Location; whether the list is set as a Primary List; the Ethereum accounts set for the roles Owner, Manager, and User. **List Storage Location** > Where the records for a list are stored. (see [List Storage Location](https://docs.efp.app/design/list-storage-location/)) **Manager** > The Ethereum account that can edit a list's records. By default when a list is created, all three roles (Owner, Manager, and User) are set as the Ethereum account that created the list. Typically, all three roles remain the same account, but they can be changed to be different. (see [Roles](https://docs.efp.app/design/roles/)) **Mute** > A standard tag that means the user shouldn’t see the muted account’s activity in a relevant app, but the muted account might still be able to see the user’s activity; lists with this tag are not included in Followers count, even if the list tags the account with other tags; if both “block” and “mute” tags are present, “block” takes precedence. (see [Tags](https://docs.efp.app/design/tags/)) **Mutuals** > Ethereum accounts that an Ethereum account follows who follow them back. **Owner** > The Ethereum account that ultimately controls a List. By default when a list is created, all three roles (Owner, Manager, and User) are set as the Ethereum account that created the list. Typically, all three roles remain the same account, but they can be changed to be different. (see [Roles](https://docs.efp.app/design/roles/)) **Primary List** > The one list an Ethereum account has confirmed to represent who they follow. While an Ethereum account may have roles in more than one list, an Ethereum account can only have one Primary List. Only Primary Lists count as Followers. (see [Account Metadata](https://docs.efp.app/design/account-metadata/)) **Social Graph** > The sum total of relationships between Ethereum accounts held in EFP, counting only Primary Lists. Any list that is not set as a Primary List is not included. **Tag** > A Tag is a string associated with a List Record in an EFP list. Tags only count for an account if that account is also followed by the user, otherwise they're not counted. List Records can have zero or more tags. A few tags are standardized with specified semantics. Users may also set custom tags. (see [Tags](https://docs.efp.app/design/tags/)) **Top8** (or, **Top 8**) > The tag "top8" means the account should appear in the user's "Top 8" in UIs that support it. If a user has more than eight followed accounts with the "top8" tag, then only show the eight most recent should be included in a "Top 8" displayed in a UI. (see [Tags](https://docs.efp.app/design/tags/)) **User** > The Ethereum account for whom the list is intended; the Ethereum account that is following the Ethereum accounts in the list. By default when a list is created, all three roles (Owner, Manager, and User) are set as the Ethereum account that created the list. Typically, all three roles remain the same account, but they can be changed to be different. (see [Roles](https://docs.efp.app/design/roles/)) --- A **Tag** is a string associated with a List Record in an EFP list. Tags only count for an account if that account is also followed by the user, otherwise they're not counted. List Records can have zero or more tags. A few tags are standardized with specified semantics. Users may also set custom tags. ## Standard Tags - **no tag** - If a List Record has no tags, it is interpreted as a simple follow without further categorization. - **"block"** - This tag means neither the user nor the blocked account should see each other’s activity in a relevant app. - List Records with this tag are not included in Followers count, even if the List Record has other tags. - If both “block” and “mute” tags are present, “block” takes precedence. - **"mute"** - This tag means the user shouldn't see the muted account’s activity in a relevant app, but the muted account might still be able to see the user’s activity. - List Records with this tag are not included in Followers count, even if the List Record has other tags. - If both “block” and “mute” tags are present, “block” takes precedence. - **"top8"** - This tag means the account should appear in the user's "Top 8" in UIs that support it. - If a user has more than eight followed accounts with the "top8" tag, then only show the eight most recent should be included in a "Top 8" displayed in a UI. ## Custom Tags Users can create additional arbitrary custom tags. Any tag which violates these constraints is ignored. ### Constraints A custom tag can be any UTF-8 string with the following constraints: - maximum length of 255 bytes - no leading or trailing whitespace - more constraints to be added as needed ## Normalization Tags are normalized by converting them to lowercase and removing leading and trailing whitespace. Tags should be normalized before they are encoded into a List Op. ## Encoding Tags are encoded as UTF-8 strings. | Tag | Encoding | | ------- | ---------------- | | `block` | `62 6c 6f 63 6b` | | `mute` | `6d 75 74 65` | ## Restrictions Tags are restricted to alpha-numerics (upper and lowercase A - Z, 0 - 9) and _most_ emojis. Neither the UI nor the API will accept anything other than these characters. --- --- ## Rewards Up to **$10,000 USD** (paid in Ethereum L1 based USDC) for each verified bug in the smart contracts listed below, depending on the severity and impact of the vulnerability, determined at our discretion. ## Qualifying Smart Contracts - [EFPListRegistry.sol](https://github.com/ethereumfollowprotocol/contracts/blob/master/src/EFPListRegistry.sol) - [EFPAccountMetadata.sol](https://github.com/ethereumfollowprotocol/contracts/blob/master/src/EFPAccountMetadata.sol) - [EFPListMinter.sol](https://github.com/ethereumfollowprotocol/contracts/blob/master/src/EFPListMinter.sol) - [EFPListRecords.sol](https://github.com/ethereumfollowprotocol/contracts/blob/master/src/EFPListRecords.sol) Bugs, fixes, or suggested improvements to the EFP app, Indexer, API, and other EFP-related code can be reported as issues on their respective repos and are not covered by this bug bounty program. Suggestions for improvements to the smart contracts above are welcome to be posted as issues on their repos but do not qualify for the bug bounty. ## How to Report a Bug Email us at [bugbounty@ethfollow.xyz](mailto:bugbounty@ethfollow.xyz). Your report should include: - "Bug Report" or similar in the subject line - A detailed description of the vulnerability - Steps to reproduce the issue - Potential impact and severity assessment - Any suggested fixes or mitigations - An ENS name or Ethereum address that can receive Ethereum L1 based USDC (should your bug report be accepted) Provide as much detail as possible to help us understand and resolve the issue efficiently. ## Eligibility and Responsible Disclosure To be eligible for a reward, you must adhere to the following rules: - Only report vulnerabilities related to Ethereum Follow Protocol's smart contracts listed above. - Do not publicly disclose the vulnerability until we have had adequate time to investigate and deploy a fix. - Avoid any actions that would disrupt our services or compromise user data. --- A valid EFP list operation has four primary components that must exist in order for it to be considered `active`. - a valid [list storage location](/design/list-storage-location) (denoting chainId, listRecords contract address, and slot) - an [account metadata record](/design/account-metadata) specifying a tokenId as a user's primary list - a [list metadata record](/design/list-metadata) in the listRecords contract for 'user' role for the slot - a [list record](/design/list-records) with a specified slot that matches the list metadata record and the list storage location. This list record must have been created on the list records contract and chain specified in the list storage location The user and manager roles of a list can be different, this means that the sender of a transaction that applies list operations to a list is not necessarily the same address that is displayed as the user. ![Active List Operation](/active-list-op.png) ### Connecting the dots In order to validate a list operation, the data in each one of these components must match the others where specified. #### If User Address is known: If the only known information is a user address, then the following steps can be take to determine the user's list, list storage location and slot 1. Retrieve the user's primary list by querying the account metadata contract using the user address. 2. From the primary list, obtain the list storage location from the registry contract which includes the chainId, listRecords contract address, and slot using function getListStorageLocation(tokenId). 3. Query the list metadata record in the listRecords contract using the slot to verify the 'user' role. 4. Retrieve the list record from the listRecords contract using the slot and ensure it matches the list metadata record and list storage location in the registry. #### If List Id is known: If the only known information is a list number, then the following steps can be taken to determine the list user 1. Obtain the list storage location from the registry contract which includes the chainId, listRecords contract address, and slot using function getListStorageLocation(tokenId). 2. Query the list metadata record in the listRecords contract using the slot to verify the 'user' role. While at this step we will know the user address, we do not yet know if the user role is valid without checking for the existance of a 'primary-list' record that matches this list id and the user's address. 3. Retrieve the user's primary list by querying the account metadata contract using the user address. 4. Retrieve the list record from the listRecords contract using the slot and ensure it matches the list metadata record and list storage location in the registry. #### If Slot is known: If the only known information is the slot (i.e. you're looking at a raw list operation and want to determine the list to which it applies) 1. Query the list metadata record in the listRecords contract using the slot to find the 'user' role. While at this step we will know the user address, we do not yet know if the user role is valid without checking for the existance of a 'primary-list' record that matches this list id and the user's address. 2. Retrieve the user's primary list by querying the account metadata contract using the user address. ### Interpreting an example List Operation A List Record specifies a slot (which is specific to a list's list storage location) and an operation which describes the action being taken and the address it relates to. For instance the record below is a follow record for address `0x983110309620d911731ac0932219af06091b6744` ```solidity { slot: 38587947120907837207653958898632315929230182373855930657826753963097023554830, op: 0x01010101983110309620d911731ac0932219af06091b6744 } ``` The `op` data of this list record can be further broken down and abstracted into its constituant parts ```solidity { slot: 38587947120907837207653958898632315929230182373855930657826753963097023554830, op: { OpVersion: 0x01, OpCode: 0x01, // 0x01: Follow, 0x02: Unfollow, 0x03: Tag, 0x04: Untag RecordVersion: 0x01, RecordType: 0x01, // 0x01: Address Record RecordData: 0x983110309620d911731ac0932219af06091b6744 } } ``` This record only tells us the `slot` of the list that's doing the following but it tells us nothing about which account or list is doing the following. To determine the list doing the following we need to join the slot, chain id and the address of the list records contract and construct a list storage location. ### Interpreting an example List Storage Location ```solidity 0x010100000000000000000000000000000000000000000000000000000000000000015289fe5dabc021d02fddf23d4a4df96f4e0f17ef5550010c08608cc567bf432829280f99b40f7717290d6313134992e4971fa50e ``` This list storage location can be interpreted as follows ```solidity { Version: 0x01, Type: 0x01, Chain: 0x0000000000000000000000000000000000000000000000000000000000000001, ListRecordsContract: 0x5289fe5dabc021d02fddf23d4a4df96f4e0f17ef, Slot: 0x5550010c08608cc567bf432829280f99b40f7717290d6313134992e4971fa50e // 38587947120907837207653958898632315929230182373855930657826753963097023554830 } ``` ## Constructing State History A repository demonstrating practical application of this page that can be found here: [https://github.com/ethereumfollowprotocol/onchain](https://github.com/ethereumfollowprotocol/onchain) ### Interacting with Contracts directly Calling the Account Metadata contract to fetch a user's primary list: ```ts import { evmClients } from '#/clients/viem/index' import { efpAccountMetadataAbi } from '#/abi/generated/index' import { env } from '#/env.ts' const tokenId = await evmClients['8453']().readContract({ address: env.ACCOUNT_METADATA, abi: efpAccountMetadataAbi, functionName: 'getValue', args: [ env.USER_ADDRESS as `0x${string}`, 'primary-list' ] }) ``` Calling the List Registry contract to fetch a list's list storage location: ```ts import { evmClients } from '#/clients/viem/index' import { efpListRegistryAbi } from '#/abi/generated/index' import { env } from '#/env.ts' const listStorageLocation = await evmClients['8453']().readContract({ address: env.REGISTRY, abi: efpListRegistryAbi, functionName: 'getListStorageLocation', args: [ BigInt(tokenId) ] }) ``` Calling the List Records contract to fetch a list slot's 'user' role: ```ts import { evmClients } from '#/clients/viem/index' import { efpListRecordsAbi } from '#/abi/generated/index' import { env } from '#/env.ts' const listUser = await evmClients[parsedLsl.chainId.toString() as keyof typeof evmClients]().readContract({ address: parsedLsl.listRecordsContract as `0x${string}`, abi: efpListRecordsAbi, functionName: 'getListUser', args: [ parsedLsl.slot ] }) ``` ### Relevant Contract Events Basic EFP state for any user's followers and followings can also be constructed by 'listening' for specific events emitted by the EFP contracts and then interpreting them according to the protocol rules as stated above EFPListRecords (Base, Optimism and Ethereum Mainnet) `ListOp(uint256 indexed slot, bytes op)` `UpdateListMetadata(uint256 indexed slot, string key, bytes value)` EFPAccountMetadata (Base) `UpdateAccountMetadata(address indexed addr, string key, bytes value)` EFPListRegistry (Base) `UpdateListStorageLocation(uint256 indexed tokenId, bytes listStorageLocation)` --- --- ## Railway Template Deploying the EFP-Silo template on Railway will set up all of the aforementioned components with default parameters set. [EFP-Silo](https://railway.app/template/pDGEZm?referralCode=AavWEU) Click the link or the button below. [![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/pDGEZm?referralCode=AavWEU) #### Configuration The template will run with default values for most of the components but you will need to set primary and secondary rpc urls for all three chains that EFP is using (Base, Optimism and Ethereum Mainnet). Find the section for 'Indexer-8453', click the 'configure' button and set the rpc endpoint for all six fields accordingly. These can be Alchemy or Infura urls or local ethereum nodes if you're testing at home. ![Indexer Params](/set-rpc.png) Once all sections display 'Ready to be deployed' the 'Deploy' button at the bottom will be unlocked and you can click it to proceed. ![Deployed Environment](/deployed-environment.png) ### Syncing It should take under an hour to sync all EFP data, you can still call the api but the data will not be up to date. ### Setup API URL Enable public networking for the API by clicking on the API section, then the 'Settings' tab and scrolling down to 'Networking'. Click 'Generate Domain' to have Railway create a random public link for you. Or click 'Custom Domain' to use an already existing domain name. ![Setup API URL](/setup-api-url.png) Once your link is generated, you can use it to call your API the same way you would call the official EFP API. Just swap in your new link in the place of 'api.ethfollow.xyz' `api.ethfollow.xyz/api/v1/stats` -> `my-generated-api-link.railway.app/api/v1/stats` ![Generate Link](/api-link-generated.png) ![Call Endpoint](/api-link-call.png) ### Setup ENS Worker The API uses V3X Labs' [enstate](https://github.com/v3xlabs/enstate) to provide ENS data. This service is available at https://ens.ethfollow.xyz. EFP is happy to provide use of this endpoint to the community but please do not abuse it. Enable public networking for the ENS Worker by clicking on the 'enstate' section, then the 'Settings' tab and scrolling down to 'Networking'. Click 'Generate Domain' to have Railway create a random public link for you. Or click 'Custom Domain' to use an already existing domain name. Once public networking is enabled for the API and ENS Worker, they should be redeployed as well as the service manager. Do this by clicking into each of the services, and clicking the three dot menu on the right hand side of your currently deployed instance. Click 'Redeploy' on the menu. ![Three dot menu](/redeploy.png) ### Service Manager The services module handles several jobs that run on intervals: - Building the list of all addresses in the EFP database - Building the Leaderboard Ranking - Tracking Mutual follows - Shuffling the recommended accounts list - Refreshing ENS metadata - Building a list of accounts with recent activity The intervals for running these jobs can be adjusted to suit your needs. Additionally the accounts that appear in the recommended follows can be customized by forking the services repo and editing '/src/services/recommended/lists.ts'. Once you've edited the file, go to the 'Settings' tab on the services module and update the 'Source Repo' to point to your forked github repository. ![Services Settings](/services-settings-repo.png) ### ENV variables Most of the Environment variables are set appropriately and should not be changed unless you know exactly what you're doing. However there are a few settings than can be customized to fit your needs. As a general rule, if they're not listed below then you probably should leave them as is. #### Indexers (Indexer-base, Indexer-op, Indexer-eth) | Variable Name | Description | |---------------------------------|-------------------------------------------------------------------------| | `PRIMARY_RPC_BASE` | Primary RPC URL for Base chain | | `PRIMARY_RPC_OP` | Primary RPC URL for Optimism chain | | `PRIMARY_RPC_ETH` | Primary RPC URL for Ethereum mainnet | | `SECONDARY_RPC_BASE` | Fallback RPC URL for Base chain | | `SECONDARY_RPC_OP` | Fallback RPC URL for Optimism chain | | `SECONDARY_RPC_ETH` | Fallback RPC URL for Ethereum mainnet | | `RECOVER_HISTORY` | Boolean Flag to start the indexer in recovery mode | | `START_BLOCK` | Block number to start recovering history from, if in recovery mode | | `BATCH_SIZE` | Number of events to batch before uploading | | `CHAIN_ID` | Chain id (8453, 10 or 1) | | `RECORDS_ONLY` | Boolean flag that specifies whether the indexer should listen for just ListOps | #### Service Manager | Variable Name | Description | |---------------------------------|----------------------------------------------------------| | `ENS_API_URL` | URL for the ENS worker to use for lookups | | `EFP_CACHE_INTERVAL` | Interval (in milliseconds) to update address cache | | `EFP_MUTUALS_INTERVAL` | Interval (in milliseconds) to update mutual follows | | `LEADERBOARD_RANKING_INTERVAL` | Interval (in milliseconds) to update leaderboard | | `RECENT_FOLLOWS_INTERVAL` | Interval (in milliseconds) to update recent activity | | `RECOMMENDED_INTERVAL` | Interval (in milliseconds) to update recommended accts | | `ENSMETADATA_INTERVAL` | Interval (in milliseconds) to update ENS metadata cache | | `HEARTBEAT_INTERVAL` | Interval (in milliseconds) to call heartbeat URL | #### API | Variable Name | Description | |---------------------------------|---------------------------------------------------------| | `CACHE_TTL` | Amount of seconds to wait before expiring cache record | | `POAP_API_TOKEN` | Auth token for POAP.xyz api | | `ENS_API_URL` | URL for the ENS worker to use for lookups | #### PgBouncer (see [PgBouncer Docs](https://www.pgbouncer.org/config.html) for more information) | Variable Name | Description | |-----------------------------------|-----------------------------------------------------| | `PGBOUNCER_DEFAULT_POOL_SIZE` | Amount of connections per pool | | `PGBOUNCER_MAX_CLIENT_CONN` | Maximum number of client connections | | `PGBOUNCER_POOL_MODE` | 'Session' or 'Transaction' | | `PGBOUNCER_QUERY_TIMEOUT` | Amount of seconds to wait before dropping query | --- # EFP Emergency Response So, you've found a critical bug and something is seriously wrong with the data coming from your latest greatest contract. In most cases, the `pause` function can be called on an affected contract from the [EFP multisig](https://docs.efp.app/production/multisig/) and the contract can simply be redeployed with a fix. Then the new contract and data can be used in the indexer in place of the bad one. However in some cases, there may be correct data from a bad contract that needs to be preserved. Its important to understand how the entire EFP system works to be able to judge whether data from a questionable contract needs to be saved. ## Affected Contracts Determine which contracts are affected. Having to pause one of the contracts is not the end of the world, and properly identifying the extent of the bug will help prevent introducing second order issues in contract interactions. For instance, if a bug is found in a second or third iteration of the list records contract this would only require pausing that specific contract and not all previous deployed versions or unaffected contracts like the list registry or list minter. This would limit the affected users to only those who have specified the affected list records contract as their list storage location. #### List Minter Contract BASE: [0xDb17Bfc64aBf7B7F080a49f0Bbbf799dDbb48Ce5](https://basescan.org/address/0xDb17Bfc64aBf7B7F080a49f0Bbbf799dDbb48Ce5) If the bug is in a List Minter contract: Call the `pause` function in the List Minter contract and record the tx hash and block height at the time of the tx. - If there is bad list data in any of the lists minted from the affected list minter contract, users will still be able to reset their lists. - If lists were minted to an incorrect recipient, the users can mint new lists from the new contract. - The old list minter contract should be marked as 'deprecated' and removed from any relevant list records indexers in all regions. If a new list minter is deployed, the account metadata contract proxy records must also be updated. In the AccountMetadata contract, `removeProxy()` should be called with the old list minter's contract address, and `addProxy()` should be called with the new list minter's contract address. It should also be noted that if the list minter contract is paused, users will still be able to mint new lists by interacting directly with the registry, list records and account metadata contracts. If minting needs to be completely disabled, the registry contract must also be paused. #### List Records Contract(s) BASE: [0x41Aa48Ef3c0446b46a5b1cc6337FF3d3716E2A33](https://basescan.org/address/0x41Aa48Ef3c0446b46a5b1cc6337FF3d3716E2A33) OP: [0x4Ca00413d850DcFa3516E14d21DAE2772F2aCb85](https://optimistic.etherscan.io/address/0x4Ca00413d850DcFa3516E14d21DAE2772F2aCb85) MAINNET: [0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF](https://etherscan.io/address/0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF) If the bug is in a List Records contract: Determine how many of the list records contracts are affected. Call the `pause` function in the List Records contract and record the tx hash and block height at the time of the tx. - If there is bad list operation data in any of the list records from the affected list record contract, users may have to re-execute any list operations that were called against the affected list operations contract - If the list records contract is deprecated, users may have to migrate their existing list data from the old list records contract. - The old list records contract should be marked as 'deprecated' and removed from any relevant list records indexers in all regions. #### Account Metadata Contract BASE: [0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF](https://basescan.org/address/0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF) If the bug is in the Account Metadata contract: Call the `pause` function in the Account Metadata contract and record the tx hash and block height at the time of the tx. - If there is bad account metadata in any of the records from the affected accouint metadata contract, users may have to re-execute any transactions that were called against the affected contract - If the list records contract is deprecated, users may have to migrate their existing list data from the old list records contract. #### List Registry Contract BASE: [0x0E688f5DCa4a0a4729946ACbC44C792341714e08](https://basescan.org/address/0x0E688f5DCa4a0a4729946ACbC44C792341714e08) If the bug is in the List Registry contract: Call the `pause` function in the Account Metadata contract and record the tx hash and block height at the time of the tx. - If there is a bug in the list registry contract, it is likely that the entire set of lists and user data will have to be migrated to a new contract - Once the list registry contract is paused, users will no longer be able to mint new lists. However they will still be able to interact with existing lists (update roles, add/remove list operations) ### Worst Case Scenario If the nature of a bug is undetermined and all other avenues are exhausted, there remains a final option of pausing ALL contracts and preventing interaction with any of the EFP contracts. This is a last ditch effort that should only be pursued if there are no other options, as pausing all contracts will prevent users from interacting with any aspect of the EFP system Pausing and unpausing all contracts will not compromise data, as long as all contracts are unpaused. It is recommended that the contracts be unpaused in the following order, with minimal time elapsed between the execution of each respective `unpause` contract call: 1. Any List Records contracts 2. Account Metadata 3. List Registry 4. List Minter ## Response All `pause` and `unpause` function calls must be executed with EFP multisig via [Safe Wallet](https://app.safe.global). The function selectors for both `pause` and `unpause` functions are the same across all contracts, respectively. ### Pause Function Function selector `0x8456cb59` The `pause` transaction can be found in the EFP Safe Wallet under 'Saved Transactions' in the transaction builder ### Unpause Function Function selector `0x3f4ba83a` The `unpause` transaction can be found in the EFP Safe Wallet under 'Saved Transactions' in the transaction builder ## Further Considerations - EFP Indexers recognize `Pause` and `Unpause` events and consume records appropriately based on the emission of these events. However, if a list records contract is found to have a bug, there may be intact data that needs to be recorded and preserved prior to the emission of a `Pause` event, or after an `Unpause` event. Indexers may need to be updated to disregard events after a certain block height in order to exclude bad data that occurred prior to a `Pause` event from being consumed and processed. - Ethereum Identity Kit should have a lockdown mode where all contract addresses are zeroed out to prevent transaction prompting, or users receive a 'Maintenance' message. - efp.app should similarly have a 'Maintenance' mode where a banner is displayed notifying users that the api is available but minting new lists and creating list operations are temporarily disabled. - As the usage of EFP evolves, it is expected that users will interact with efp.app less and interact with the EFP system through third party applications and services. This means that over time more users will interact with EFP via the use of Ethereum Identity Kit components. Care should be taken to ensure that EIK supports various failure modes, and all component functionality can be enabled or disabled as necessary. --- ![EFP Backend Design](/backend-design.png) The backend architecture for EFP is comprised of the following components - [Database](https://hub.docker.com/_/postgres) (Postgres) - [PGBouncer](https://hub.docker.com/r/pgbouncer/pgbouncer) (a connection pooler for Postgres) - [EFP Indexers](https://github.com/ethereumfollowprotocol/indexer) for Base, Optimism and Ethereum Mainnet - [EFP Services](https://github.com/ethereumfollowprotocol/services) (updates ens data, leaderboard, cache, mutuals counts) - [EFP API](https://github.com/ethereumfollowprotocol/api) (Can be deployed as a cloudflare worker) - [Ens Worker](https://github.com/v3xlabs/enstate) (Can be deployed as a cloudflare worker) - [Redis](https://hub.docker.com/_/redis) Cache for the API and ENS Worker All of these components can be set up separately, whether locally or using external hosts. The quickest way to stand up all of the backend infrastructure is to use the [EFP-Silo Railway template](https://railway.app/template/pDGEZm?referralCode=AavWEU) ### Indexers EFP has Registry and AccountMetadata contracts deployed on Base and ListRecords contracts deployed on three chains: Base, Optimism And Ethereum Mainnet. To capture and store all EFP data we run three separate instances of our indexer service, one for each chain. ### Database EFP Data is stored in a Postgres Database, queries and schema can be found in the [EFP Indexer Repo](https://github.com/ethereumfollowprotocol/indexer/tree/develop/db) ### Connection Pooling PgBouncer is used to handle connection pooling for the database. This improves availability for the database over the api by sharing client connections, saving overhead on connection/reconnection. ### Data Services The services module handles several jobs that run on intervals: - Building the list of all addresses in the EFP database - Building the Leaderboard Ranking - Tracking Mutual follows - Shuffling the recommended accounts list - Refreshing ENS metadata - Building a list of accounts with recent activity The intervals for running these jobs can be adjusted to suit your needs. Additionally the accounts that appear in the recommended follows can be customized by forking the services repo and editing '/src/services/recommended/lists.ts'. Once you've edited the file, go to the 'Settings' tab on the services module and update the 'Source Repo' to point to your forked github repository. ![Services Settings](/services-settings-repo.png) ### API EFP core team provides a public API endpoint at [`https://api.ethfollow.xyz/api/v1`](https://api.ethfollow.xyz/api/v1). If you are a developer, you are free to use this endpoint to retrieve EFP data. #### Commonly used endpoints - [User Stats](https://docs.ethfollow.xyz/api/users/stats/): the follower and following counts of a particular user - [User Following](https://docs.ethfollow.xyz/api/users/following/): list of the accounts a particular user follows - [User Followers](https://docs.ethfollow.xyz/api/users/followers/): list of the accounts that follow a particular user - [User ENS data](https://docs.ethfollow.xyz/api/users/ens/): the ENS data for a particular user ### ENS Worker The API uses V3X Labs' [enstate](https://github.com/v3xlabs/enstate) to provide ENS data. This service is available at https://ens.ethfollow.xyz. EFP is happy to provide use of this endpoint to the community but please do not abuse it. Anyone can deploy this ENS worker repository to cloudflare or host their own instance locally. Read more about deploying [here](https://github.com/v3xlabs/enstate?tab=readme-ov-file#-cloudflare-workers). ### Redis Cache The Redis cache is used by both the API and the ENS Worker to cache responses for faster future retrieval. --- The EFP multisig is a set of Gnosis Safes on Base, OP Mainnet, and Ethereum Mainnet, and each requires 3 out of 4 signatories to execute a transaction. ### Multisig Wallet Addresses | Chain | Address | Gnosis Safe | | --- | --- | --- | | Base | 0x860bFe7019d6264A991277937ea6002714C3c508 | [Link](https://app.safe.global/home?safe=base:0x860bFe7019d6264A991277937ea6002714C3c508) | | OP Mainnet | 0x860bFe7019d6264A991277937ea6002714C3c508 | [Link](https://app.safe.global/home?safe=oeth:0x860bFe7019d6264A991277937ea6002714C3c508) | | Ethereum Mainnet | 0xeaa7B3B7f9A6c9782Fc17A49C4dfC170193d69Dd | [Link](https://app.safe.global/home?safe=eth:0xeaa7B3B7f9A6c9782Fc17A49C4dfC170193d69Dd) | ### Signers The signers are the same on all three chains and are controlled by members of the [EFP core team](https://efp.app/team). | Address | Owner | | ------------------------------------------ | ------------------ | | 0x983110309620D911731Ac0932219af06091b6744 | brantly.eth | | 0x96184444629F3489c4dE199871E6F99568229d8f | brantly.eth | | 0xC9C3A4337a1bba75D0860A1A81f7B990dc607334 | 0xthrpw.eth | | 0x5B0f3DBdD49614476e4f5fF5Db6fe13d41fCB516 | encrypteddegen.eth | ### EFP Contracts owned by Multisig ##### Base (Chain ID: 8453) | Name | Address | | ------------------ | ------------------------------------------ | | EFPAccountMetadata | 0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF | | EFPListRecords | 0x41Aa48Ef3c0446b46a5b1cc6337FF3d3716E2A33 | | EFPListRegistry | 0x0E688f5DCa4a0a4729946ACbC44C792341714e08 | | EFPListMinter | 0xDb17Bfc64aBf7B7F080a49f0Bbbf799dDbb48Ce5 | | TokenURIProvider | 0xC8BA343aeaF2b3b3EC79C25f05F4Ef459D9c7eFB | ##### OP Mainnet (Chain ID: 10) | Name | Address | | -------------- | ------------------------------------------ | | EFPListRecords | 0x4Ca00413d850DcFa3516E14d21DAE2772F2aCb85 | ##### Ethereum Mainnet (Chain ID: 1) | Name | Address | | -------------- | ------------------------------------------ | | EFPListRecords | 0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF | ### Privileges For each of the chains, the multisig has privileged control over the following functions in each of the contracts listed below. ### EFPAccountMetadata --- ##### Pause Allows the owner to cease all contract interaction ``` /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } ``` ##### Unpause Allows the owner to resume all contract interaction ``` /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } ``` ##### Add Proxy Allows the owner to add an account to a list of privileged addresses which are allowed to update account metadata. This allows the List Minter to set default lists and metadata during the minting process. ``` /** * @dev Add proxy address. * @param proxy The proxy address. */ function addProxy(address proxy) external whenNotPaused onlyOwner { proxies[proxy] = true; emit ProxyAdded(proxy); } ``` ##### Remove Proxy Allows the owner to remove an account from the list of privileged proxy addresses ``` /** * @dev Remove proxy address. * @param proxy The proxy address. */ function removeProxy(address proxy) external whenNotPaused onlyOwner { proxies[proxy] = false; emit ProxyRemoved(proxy); } ``` ### EFPListRecords --- ##### Pause Allows the owner to cease all contract interaction ``` /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } ``` ##### Unpause Allows the owner to resume all contract interaction ``` /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } ``` ### EFPListRegistry --- ##### Pause Allows the owner to cease all contract interaction ``` /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } ``` ##### Unpause Allows the owner to resume all contract interaction ``` /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } ``` ##### Set TokenURIProvider Allows the owner to set the TokenURIProvider contract address ``` /** * @notice Sets the token URI provider. * @param tokenURIProvider_ The new token URI provider. */ function setTokenURIProvider(address tokenURIProvider_) external onlyOwner { tokenURIProvider = ITokenURIProvider(tokenURIProvider_); emit TokenURIProviderChange(tokenURIProvider_); } ``` ##### Set PriceOracle Allows the owner to set the `priceOracle` contract address. This contract is currentlet unset. ``` /** * @notice Sets the price oracle. * @param priceOracle_ The new price oracle. */ function setPriceOracle(address priceOracle_) external whenNotPaused onlyOwner { priceOracle = IEFPListNFTPriceOracle(priceOracle_); emit PriceOracleChange(priceOracle_); } ``` ##### Withdraw Allows the owner to withdraw Ether from the contract ``` /** * @notice Withdraws Ether from the contract. * * @param recipient The address to send the Ether to. * @param amount The amount of Ether to send. * @return Whether the transfer succeeded. */ function withdraw(address payable recipient, uint256 amount) public onlyOwner returns (bool) { require(amount <= address(this).balance, 'Insufficient balance'); (bool sent,) = recipient.call{value: amount}(''); require(sent, 'Failed to send Ether'); return sent; } ``` ##### Set MintState Allows the owner to modify limitations when minting lists ``` /// @notice Sets the mint state. /// @param _mintState The new mint state. function setMintState(MintState _mintState) external whenNotPaused onlyOwner { mintState = _mintState; emit MintStateChange(_mintState); } ``` ##### Set MaxMintBatchSize Allows the owner to modify limitations on the maximum number of lists that can be minted in one transanction ``` /// @notice Sets the max mint batch size. /// @param _maxMintBatchSize The new max mint batch size. function setMaxMintBatchSize(uint256 _maxMintBatchSize) external whenNotPaused onlyOwner { maxMintBatchSize = _maxMintBatchSize; emit MaxMintBatchSizeChange(_maxMintBatchSize); } ``` ### EFPListMinter --- ##### Pause Allows the owner to cease all contract interaction ``` /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } ``` ##### Unpause Allows the owner to resume all contract interaction ``` /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } ``` ### TokenURIProvider --- ##### Set BaseURI Allows the owner to set the BaseURI use for contructing token URIs ``` /** * @dev Sets the base URI for token URIs * @param baseURI The new base URI */ function setBaseURI(string memory baseURI) external onlyOwner { _baseURI = baseURI; } ``` --- ![Follow Bot Start](/follow-bot-start.png) The **EFP Follow Bot** is a telegram bot that enables users to subscribe to Ethereum addresses or ENS names, allowing them to stay updated with the latest follows, tags, and other activities within the Ethereum Follow Protocol (EFP) ecosystem. This bot simplifies tracking and engagement, making it easier for users to stay informed about their favorite EFP accounts. Get started by interacting with the bot directly at [https://t.me/efp_follow_bot](https://t.me/efp_follow_bot) ## Commands Here are the available commands to interact with the **EFP Follow Bot**: ### subscribe ![Follow Bot Subscribe](/follow-bot-subscribe.png) `/subscribe ` Subscribe to updates for a specific Ethereum address or ENS name. Use this command to start receiving notifications about activities related to the specified account. Multiple accounts can be subscribed to at one time `/sub ` Alias for /subscribe. Functions the same way as /subscribe. ### unsubscribe ![Follow Bot Unsubscribe](/follow-bot-unsubscribe.png) `/unsubscribe ` Unsubscribe from updates for a specific Ethereum address or ENS name. Use this command to stop receiving notifications for a specific account. `/unsub ` Alias for /unsubscribe. Functions the same way as /unsubscribe. `/unsub all` Unsubscribe from all accounts. This command will stop all notifications for the current chat. ### details `/details ` Get details for a specific Ethereum address or ENS name ### list ![Follow Bot List](/follow-bot-list.png) `/list` List all subscriptions for this chat. Use this command to view all the Ethereum addresses or ENS names you are currently subscribed to. ### help `/help` Show this help message. Use this command to display a list of all available commands and their descriptions. --- ### EFP Contracts EFP currently has smart contracts on three chains: Base, OP Mainnet, and Ethereum. --- #### Base (Chain ID: 8453) | Name | Address | | -------------------- | ------------------------------------------ | | EFPAccountMetadata | 0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF | | EFPListRecords | 0x41Aa48Ef3c0446b46a5b1cc6337FF3d3716E2A33 | | EFPListRegistry | 0x0E688f5DCa4a0a4729946ACbC44C792341714e08 | | EFPListMinter | 0xDb17Bfc64aBf7B7F080a49f0Bbbf799dDbb48Ce5 | | TokenURIProvider | 0xC8BA343aeaF2b3b3EC79C25f05F4Ef459D9c7eFB | #### OP Mainnet (Chain ID: 10) | Name | Address | | -------------------- | ------------------------------------------ | | EFPListRecords | 0x4Ca00413d850DcFa3516E14d21DAE2772F2aCb85 | #### Ethereum Mainnet (Chain ID: 1) | Name | Address | | -------------------- | ------------------------------------------ | | EFPListRecords | 0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF | ### Testnet Deployments #### Base Sepolia (Chain ID: 84532) | Name | Address | | -------------------- | ------------------------------------------ | | EFPAccountMetadata | 0xDAf8088C4DCC8113F49192336cd594300464af8D | | EFPListRecords | 0x63B4e2Bb1E9b9D02AEF3Dc473c5B4b590219FA5e | | EFPListRegistry | 0xDdD39d838909bdFF7b067a5A42DC92Ad4823a26d | | EFPListMinter | 0x0c3301561B8e132fe18d97E69d95F5f1F2849f9b | | TokenURIProvider | 0x3c5A8bB78Ad55C8c0a8Acd0D5afeDFB95470a591 | #### OP Sepolia (Chain ID: 11155420) | Name | Address | | -------------------- | ------------------------------------------ | | EFPListRecords | 0x2f644bfec9C8E9ad822744E17d9Bf27A42e039fE | #### Ethereum Sepolia (Chain ID: 11155111) | Name | Address | | -------------------- | ------------------------------------------ | | EFPListRecords | 0xf8c6aa2a83799d0f984CA501F85f9e634F97FEf2 | --- EFP provides an open source indexer and API for indexing and retrieving EFP data. ### Public API EFP core team provides a public API endpoint at [`https://api.efp.app/api/v1`](https://api.efp.app/api/v1). If you are a developer, you are free to use this endpoint to retrieve EFP data. ### Commonly used endpoints - [User Stats](https://docs.efp.app/api/users/stats/): the follower and following counts of a particular user - [User Following](https://docs.efp.app/api/users/following/): list of the accounts a particular user follows - [User Followers](https://docs.efp.app/api/users/followers/): list of the accounts that follow a particular user - [User ENS data](https://docs.efp.app/api/users/ens/): the ENS data for a particular user ### Self-hosting You may also fork EFP's Indexer/API source code and deploy yourself. - [`ethereumfollowprotocol/api`](https://github.com/ethereumfollowprotocol/api) - [`ethereumfollowprotocol/indexer`](https://github.com/ethereumfollowprotocol/indexer) ### KV cache The API uses Cloudflare KV storage to cache some endpoint's responses. When one of these endpoints is called the cache is checked and if there is no record the data is fetched from the database. If a cached record is found it is returned immediately. All cache records are set to expire 5 minutes after they are created. The cached record for each of these endpoints can be refreshed by adding `cache=fresh` to the query params. For example: Get the cached record ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/1/details ``` Get latest data and reset the cached record ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/1/details?cache=fresh ``` ### Questions If you come across any issues, please reach out to us in our [Discord](https://discord.com/invite/ZUyG3mSXFD). --- #### /stats Get global EFP statistics. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/stats ``` ```jsonc // sample response { "stats": { "address_count": "8999", "list_count": "733", "list_op_count": "50284", }, } ``` --- #### /token/metadata/\:token_id Get NFT metadata for a specified token id #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/token/metadata/4 ``` ```jsonc // sample response { "name": "EFP List #4", "description": "Ethereum Follow Protocol (EFP) is an onchain social graph protocol for Ethereum accounts.", "image": "https://api.ethfollow.xyz/api/v1/token/image/3", "external_url": "https://efp.app/3", "attributes": [ { "trait_type": "User", "value": "brantly.eth", }, { "trait_type": "Primary List", "value": true, }, { "trait_type": "Followers", "value": 368, }, { "trait_type": "Following", "value": 1079, }, { "trait_type": "Mutuals Rank", "value": "1", }, { "trait_type": "Followers Rank", "value": "1", }, { "trait_type": "Following Rank", "value": "6", }, { "trait_type": "Blocked Rank", "value": "7", }, ], } ``` --- #### /token/image/\:token_id Get NFT image for a specified token id #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/token/image/3 ``` #### Response This endpoint responds with a formatted svg of content-type `image/svg+xml;charset=utf-8` --- #### /leaderboard/blocked Get leaderboard of users ranked according to count of users that blocked them. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/blocked ``` ```jsonc // sample response [ { "rank": 1, "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "blocked_by_count": "8" }, { "rank": 2, "address": "0xa7860e99e3ce0752d1ac53b974e309fff80277c6", "blocked_by_count": "4" }, { "rank": 3, "address": "0x3276e82ebb1b4b9f01ab9286ed6bcc6603e368e2", "blocked_by_count": "2" }, { "rank": 4, "address": "0x7265a60acaeaf3a5e18e10bc1128e72f27b2e176", "blocked_by_count": "2" }, ... ] ``` --- #### /leaderboard/all Get addresses and ens names of all leaderboard records. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/leaderboard/all ``` ```jsonc // sample response { "results": [ { "address": "0x0ad4bb5ceabfdb5020b01e6dc5e32526eb10e5d1", "name": "0xsailormoon.eth" }, { "address": "0xd1efdd037566b0c75cebace9150d26ea0153faa9", "name": "designer.eth" }, { "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "name": "garypalmerjr.eth" }, { "address": "0x14546125429faac7f3aa78da1807069692ec7464", "name": "grado.eth" }, ... ] } ``` --- #### /leaderboard/ranked Get leaderboard of users ranked according to count of mutual follows. Includes rankings for mutuals, followers, following, blocked and tagged as 'top8'. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 50. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `sort` (string, optional): Specifies ranking to sort on, possible values are 'mutuals', 'followers', 'following', 'blocked' and 'top8', default value is 'mutuals'. - `direction` (string, optional): Specifies direction to sort results, possible values are 'ASC' or 'DESC' default value is 'DESC'. - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/ranked?sort=mutuals&direction=desc ``` ```jsonc // sample response { "last_updated": "2024-09-18T19:11:43.210Z", "results": [ { "address": "0x983110309620d911731ac0932219af06091b6744", "name": "brantly.eth", "avatar": "https://euc.li/brantly.eth", "header": "https://i.imgur.com/Quo06x2.png", "mutuals_rank": "1", "followers_rank": "1", "following_rank": "6", "blocks_rank": "7", "top8_rank": "1", "mutuals": "293", "following": "1079", "followers": "366", "blocks": "1", "top8": "45", "updated_at": "2024-09-18T19:11:43.210Z" }, { "address": "0xd1efdd037566b0c75cebace9150d26ea0153faa9", "name": "designer.eth", "avatar": "https://euc.li/designer.eth", "header": null, "mutuals_rank": "2", "followers_rank": "6", "following_rank": "4", "blocks_rank": "7", "top8_rank": "7", "mutuals": "147", "following": "1556", "followers": "182", "blocks": "1", "top8": "7", "updated_at": "2024-09-18T19:11:43.210Z" }, { "address": "0x2a59071ff48936c6838dcac425fa0df6ea5979bf", "name": "mely.eth", "avatar": "https://euc.li/mely.eth", "header": "https://i.imgur.com/T2H8N2H.jpeg", "mutuals_rank": "3", "followers_rank": "12", "following_rank": "28", "blocks_rank": "7", "top8_rank": "5", "mutuals": "124", "following": "354", "followers": "138", "blocks": "1", "top8": "11", "updated_at": "2024-09-18T19:11:43.210Z" }, ... ] } ``` --- #### /leaderboard/search Search for leaderboard addresses and ENS names by a specified search term. #### Query Parameters - `term` (string, optional): Specifies the string to search for in a leaderboard address or ENS name. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/search?term=eth ``` ```jsonc // sample response { "last_updated": "2024-09-18T19:11:43.210Z", "results": [ { "address": "0x983110309620d911731ac0932219af06091b6744", "name": "brantly.eth", "avatar": "https://euc.li/brantly.eth", "header": "https://i.imgur.com/Quo06x2.png", "mutuals_rank": "1", "followers_rank": "1", "following_rank": "6", "blocks_rank": "7", "mutuals": "293", "following": "1079", "followers": "366", "blocks": "1", "updated_at": "2024-09-18T19:11:43.210Z" }, { "address": "0xd1efdd037566b0c75cebace9150d26ea0153faa9", "name": "designer.eth", "avatar": "https://euc.li/designer.eth", "header": null, "mutuals_rank": "2", "followers_rank": "6", "following_rank": "4", "blocks_rank": "7", "mutuals": "147", "following": "1556", "followers": "182", "blocks": "1", "updated_at": "2024-09-18T19:11:43.210Z" }, { "address": "0x2a59071ff48936c6838dcac425fa0df6ea5979bf", "name": "mely.eth", "avatar": "https://euc.li/mely.eth", "header": "https://i.imgur.com/T2H8N2H.jpeg", "mutuals_rank": "3", "followers_rank": "12", "following_rank": "28", "blocks_rank": "7", "mutuals": "124", "following": "354", "followers": "138", "blocks": "1", "updated_at": "2024-09-18T19:11:43.210Z" }, ... ] } ``` --- #### /leaderboard/muted Get leaderboard of users ranked according to count of users that muted them. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/muted ``` ```jsonc // sample response [ { "rank": 1, "address": "0x949e2988b857af2a3c9429e763d13202b7b25c88", "muted_by_count": "1", }, ] ``` --- #### /leaderboard/count Get count of all accounts in the leaderboard. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/count ``` ```jsonc // sample response { "leaderboardCount": "8788", } ``` --- #### /leaderboard/mutes Get leaderboard of users ranked according to count of users that they muted. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/mutes ``` ```jsonc // sample response [ { "rank": 2, "address": "0xfa35730094b7e0fc1b97f55663aad15e2d2e3e29", "mutes_count": "3", }, { "rank": 3, "address": "0x983110309620d911731ac0932219af06091b6744", "mutes_count": "2", }, { "rank": 4, "address": "0x2933387ec4c9bbc4a8200cfd77db53d7bc8ebc11", "mutes_count": "1", }, ] ``` --- #### /leaderboard/following Get leaderboard of users ranked according to following counts. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/following ``` ```jsonc // sample response [ { "rank": 1, "address": "0x44a3a18df15ae79bbc6c660db59428fe9a181864", "following_count": "5639" }, { "rank": 2, "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "following_count": "3403" }, { "rank": 3, "address": "0xd4713cca4068700cf722f8c2b6c05f948b75321b", "following_count": "2782" }, { "rank": 4, "address": "0xd1efdd037566b0c75cebace9150d26ea0153faa9", "following_count": "1556" }, ... ] ``` --- #### /leaderboard/blocks Get leaderboard of users ranked according to count of users that they blocked. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/blocks ``` ```jsonc // sample response [ { "rank": 1, "address": "0xc808ffa16d6773d6a9109b1ab92e839157eb0954", "blocks_count": "119" }, { "rank": 2, "address": "0xfa35730094b7e0fc1b97f55663aad15e2d2e3e29", "blocks_count": "3" }, { "rank": 3, "address": "0x983110309620d911731ac0932219af06091b6744", "blocks_count": "2" }, { "rank": 4, "address": "0x2933387ec4c9bbc4a8200cfd77db53d7bc8ebc11", "blocks_count": "1" }, ... ] ``` --- #### /leaderboard/followers Get leaderboard of users ranked according to follower counts. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/followers ``` ```jsonc // sample response [ { "rank": 1, "address": "0x983110309620d911731ac0932219af06091b6744", "followers_count": "365" }, { "rank": 2, "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", "followers_count": "285" }, { "rank": 3, "address": "0x849151d7d0bf1f34b70d5cad5149d28cc2308bf1", "followers_count": "270" }, { "rank": 4, "address": "0x5b76f5b8fc9d700624f78208132f91ad4e61a1f0", "followers_count": "228" }, ... ] ``` --- WIP --- #### /lists/\:token_id/recommended Get recommended users for a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/1/recommended ``` ```jsonc // sample response { "recommended": [ { "name": "swindler.eth", "address": "0xf972bf8592c3171b378e97bb869a980c3f476583", "avatar": "https://rainbow.mypinata.cloud/ipfs/QmcSAHrGGdXJRPmxYUk1R86Wqpfgg4TPMAXC6MfQHPugvF", "header": "https://rainbow.mypinata.cloud/ipfs/QmVDbkDutSk4phVohMs76jV4RgT3bpSdzHnesBHFxW6jRL", "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, { "name": "gratefulape.eth", "address": "0x52a4c418576dc46e4116ececc6f68d1c9b9636ed", "avatar": "https://euc.li/gratefulape.eth", "header": null, "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, { "name": "treeskulltown.eth", "address": "0x2dacc0b072146b40e60b8596b99756112d45c924", "avatar": "https://euc.li/treeskulltown.eth", "header": null, "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, ... ] } ``` --- #### /lists/\:token_id/stats Get stats of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `live` (bool, optional): Specifies whether to calculate stats or return cached values - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/3/stats?live=true ``` ```jsonc // sample response { "followers_count": "115", "following_count": "569", } ``` --- #### /lists/\:token_id/details Get details of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/3/details ``` ```jsonc // sample response { "address": "0xc9c3a4337a1bba75d0860a1a81f7b990dc607334", "ens": { "name": "0xthrpw.eth", "avatar": "https://euc.li/0xthrpw.eth", "header": "https://storage.googleapis.com/nftimagebucket/tokens/0xb7def63a9040ad5dc431aff79045617922f4023a/1897.svg", "display": "0xthrpw.eth", "records": { "avatar": "https://euc.li/0xthrpw.eth", "com.github": "0xthrpw", "com.twitter": "0xthrpw", "description": "Took the mirrors off my cadillac because I don't like looking like I look back.", "header": "https://storage.googleapis.com/nftimagebucket/tokens/0xb7def63a9040ad5dc431aff79045617922f4023a/1897.svg", "name": "throw;", }, "chains": { "eth": "0xC9C3A4337a1bba75D0860A1A81f7B990dc607334", }, "fresh": 1726680254493, "resolver": "0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63", "errors": {}, }, "ranks": { "mutuals_rank": "4", "followers_rank": "17", "following_rank": "18", "top8_rank": "12", "blocks_rank": "7", }, "primary_list": "3", } ``` --- #### /lists/\:token_id/account Get account information by their EFP list id #### Path Parameters - `token_id` (string): The EFP List of the account #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/3/details ``` ```jsonc // sample response { "address": "0xc9c3a4337a1bba75d0860a1a81f7b990dc607334", "ens": { "name": "0xthrpw.eth", "avatar": "https://euc.li/0xthrpw.eth", "header": "https://storage.googleapis.com/nftimagebucket/tokens/0xb7def63a9040ad5dc431aff79045617922f4023a/1897.svg", "display": "0xthrpw.eth", "records": { "avatar": "https://euc.li/0xthrpw.eth", "com.github": "0xthrpw", "com.twitter": "0xthrpw", "description": "Took the mirrors off my cadillac because I don't like looking like I look back.", "header": "https://storage.googleapis.com/nftimagebucket/tokens/0xb7def63a9040ad5dc431aff79045617922f4023a/1897.svg", "name": "throw;", }, "chains": { "eth": "0xC9C3A4337a1bba75D0860A1A81f7B990dc607334", }, "fresh": 1726679594366, "resolver": "0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63", "errors": {}, }, "ranks": { "mutuals_rank": "4", "followers_rank": "17", "following_rank": "18", "top8_rank": "12", "blocks_rank": "7", }, "primary_list": "3", } ``` --- #### /lists/\:token_id/allFollowingAddresses Get all accounts in list format, that are being followed (including blocked and muted) by a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/4/allFollowingAddresses ``` ```jsonc // sample response [ "0xc6ed8696c4885dcafdc73c5ef28511e02568b472", "0x1cbf9983e0d59276a58db8e8529706004fcb1837", "0x27d311b8958ca479615522304b442e530c8073fe", "0x47c0224f6c298c4b03f2fbbb986815859a0abd20", "0x50e97e480661533b5382e33705e4ce1eb182222e", "0x8480d20583a3138fef7c23eed8f17bf3c01e73b7", "0x97b5c5ac8813bf5aaf689bbb697b56f8d897baef", "0xa6bcb89f21e0bf71e08ded426c142757791e17cf", "0xdc27cb447d713a8320db054a39ab6a42e0af49cb", "0x02ca10c62f160cdd126d1e44ef42224cac745ac8", "0x0433062f9f466c4a184b2ba0e4da38efea5e2f87", ... ] ``` --- #### /lists/\:token_id/searchFollowers Search for followers of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `term` (string, optional): Specifies the string to search for in a followers address or ENS name #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/4/searchFollowers?term=crypt ``` ```jsonc // sample response { "followers": [ { "efp_list_nft_token_id": "99", "address": "0x19cf388796c31fa7a583270d82659ecd2b4fd490", "ens": { "name": "cryptomandias.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/cryptomandias.eth" }, "tags": [], "is_following": true, "is_blocked": false, "is_muted": false }, { "efp_list_nft_token_id": "553", "address": "0x3b30d44df9afffc07a51457e18410c4ca0f90896", "ens": { "name": "cryptodeadbeat.eth", "avatar": "https://euc.li/cryptodeadbeat.eth" }, "tags": [], "is_following": true, "is_blocked": false, "is_muted": false }, { "efp_list_nft_token_id": "88", "address": "0x5b0f3dbdd49614476e4f5ff5db6fe13d41fcb516", "ens": { "name": "efp.encrypteddegen.eth", "avatar": "https://euc.li/efp.encrypteddegen.eth" }, "tags": [ "top8" ], "is_following": true, "is_blocked": false, "is_muted": false }, ... ] } ``` --- #### /lists/\:token_id/searchFollowing Search for following of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `term` (string, optional): Specifies the string to search for in a following address or ENS name #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/3/searchFollowing?term=bran ``` ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0x0ae93a80ef639c07ecf969735c9b3cc90ef6d803", "tags": [], "ens": { "name": "ens.brantly.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/ens.brantly.eth", }, }, { "version": 1, "record_type": "address", "data": "0xe0308a8a9095e4fc554fefdfafc819ff7b0f7103", "tags": [], "ens": { "name": "libran.eth", "avatar": "https://euc.li/libran.eth", }, }, { "version": 1, "record_type": "address", "data": "0x983110309620d911731ac0932219af06091b6744", "tags": ["top8"], "ens": { "name": "brantly.eth", "avatar": "https://euc.li/brantly.eth", }, }, ], } ``` --- #### /lists/\:token_id/allFollowers Get all followers (including blocked and muted) of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/4/allFollowers ``` ```jsonc // sample response { "followers": [ { "efp_list_nft_token_id": "15", "address": "0x849151d7d0bf1f34b70d5cad5149d28cc2308bf1", "tags": [], "is_following": true, "is_blocked": false, "is_muted": false, "updated_at": "2024-09-24T14:45:33.882Z" }, { "efp_list_nft_token_id": "294", "address": "0xa7860e99e3ce0752d1ac53b974e309fff80277c6", "tags": [ "top8" ], "is_following": false, "is_blocked": true, "is_muted": false, "updated_at": "2024-09-24T14:45:33.882Z" }, { "efp_list_nft_token_id": "55", "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "tags": [ "bff", "top8" ], "is_following": false, "is_blocked": true, "is_muted": false, "updated_at": "2024-09-24T16:45:26.088Z" }, ... ] } ``` --- #### /lists/\:token_id/taggedAs Get the tags that are applied to a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/41/taggedAs ``` ```jsonc // sample response { "token_id": "41", "tags": ["top8", "block"], "tagCounts": [ { "tag": "top8", "count": 7, }, { "tag": "block", "count": 1, }, ], "taggedAddresses": [ { "address": "0xf9a24785cab3ed0921c41fb84dedfea935a4ad1b", "tag": "top8", }, { "address": "0x8eddf5431f5b31933bfbd8111d54fc6e9456e6c1", "tag": "top8", }, { "address": "0x8b24b1686832757e2f6d640e11e88e7f0064594a", "tag": "top8", }, { "address": "0x60377ec355857c2d06d1ce28555f624257344b0d", "tag": "top8", }, { "address": "0xfa1afc4534fc9f80a552e61dd04cd8a172c821a6", "tag": "top8", }, { "address": "0xc808ffa16d6773d6a9109b1ab92e839157eb0954", "tag": "block", }, { "address": "0x983110309620d911731ac0932219af06091b6744", "tag": "top8", }, { "address": "0x2a59071ff48936c6838dcac425fa0df6ea5979bf", "tag": "top8", }, ], } ``` --- #### /lists/\:token_id/tags Get the tags of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/3/tags ``` ```jsonc // sample response { "token_id": "3", "tags": ["vogu", "top8"], "tagCounts": [ { "tag": "vogu", "count": 1, }, { "tag": "top8", "count": 8, }, ], "taggedAddresses": [ { "address": "0x0f2e3e67cb000993d07e60261748963d3f4bd6d9", "tag": "vogu", }, { "address": "0x71adb34117c9408e74ed112b327a0ec97cef8fa1", "tag": "top8", }, { "address": "0x8f5906963ae276e1631efa8ff1a9cae6499ec5e3", "tag": "top8", }, { "address": "0x983110309620d911731ac0932219af06091b6744", "tag": "top8", }, { "address": "0xbe4f0cdf3834bd876813a1037137dcfad79acd99", "tag": "top8", }, { "address": "0xc983ebc9db969782d994627bdffec0ae6efee1b3", "tag": "top8", }, { "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", "tag": "top8", }, { "address": "0xe2cded674643743ec1316858dfd4fd2116932e63", "tag": "top8", }, { "address": "0xeb6b293e9bb1d71240953c8306ad2c8ac523516a", "tag": "top8", }, ], } ``` --- #### /lists/\:token_id/\:addressOrENS/buttonState Get the following state between a given list and a given user. #### Path Parameters - `token_id` (string): The EFP List of the account - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/3/brantly.eth/buttonState ``` ```jsonc // sample response { "token_id": "3", "address": "0x983110309620d911731ac0932219af06091b6744", "state": { "follow": true, "block": false, "mute": false, }, } ``` --- #### /lists/\:token_id/following Get accounts being followed (excluding blocked and muted) by a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. - `sort` (string, optional): Specifies how the results should be sorted, possible values 'latest', 'earliest', 'followers'. If not specified, default sort is 'latest' - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/4/following ``` ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0xc6ed8696c4885dcafdc73c5ef28511e02568b472", "tags": [] }, { "version": 1, "record_type": "address", "data": "0x1cbf9983e0d59276a58db8e8529706004fcb1837", "tags": [] }, { "version": 1, "record_type": "address", "data": "0x27d311b8958ca479615522304b442e530c8073fe", "tags": [] }, ... ] } ``` --- #### /lists/\:token_id/allFollowing Get all accounts being followed (including blocked and muted) by a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/4/allFollowing ``` ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0xad73eafcac4f4c6755dfc61770875fb8b6bc8a25", "tags": [] }, { "version": 1, "record_type": "address", "data": "0xfee41e0f01112d9bdaa73a5a368f4afb4d9baa08", "tags": [] }, { "version": 1, "record_type": "address", "data": "0xf972bf8592c3171b378e97bb869a980c3f476583", "tags": [] }, ... ] } ``` --- #### /lists/\:token_id/\:addressOrENS/followerState Get the follower state between a given list and a given user. #### Path Parameters - `token_id` (string): The EFP List of the account - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/3/encrypteddegen.eth/followerState ``` ```jsonc // sample response { "token_id": "3", "address": "0xc983ebc9db969782d994627bdffec0ae6efee1b3", "state": { "follow": true, "block": false, "mute": false, }, } ``` --- #### /lists/\:token_id/followers Get followers (excluding blocked and muted) of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. - `sort` (string, optional): Specifies how the results should be sorted, possible values 'latest', 'earliest', 'followers'. If not specified, default sort is 'latest' - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/4/followers ``` ```jsonc // sample response { "followers": [ { "efp_list_nft_token_id": "325", "address": "0x9b2fb7a8d227cdaa8002f80e8c8a99a19bb1b969", "tags": [], "is_following": true, "is_blocked": false, "is_muted": false, "updated_at": "2024-09-24T16:45:26.088Z" }, { "efp_list_nft_token_id": "728", "address": "0xca034d4438719391b5e7589242a36ec535ed6836", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-09-24T16:45:26.088Z" }, { "efp_list_nft_token_id": "723", "address": "0x8901083bb577b335a5f6fddde705c00efe8c33d9", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-09-24T16:45:26.088Z" }, ... ] } ``` --- #### /discover Get recently active accounts to follow. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/discover ``` ```jsonc // sample response { "latestFollows": [ { "address": "0xa8b4756959e1192042fc2a8a103dfe2bddf128e8", "name": "caveman.eth", "avatar": "https://euc.li/caveman.eth", "header": "https://i.imgur.com/KYD6snF.jpeg", "followers": "162", "following": "482" }, { "address": "0x8513eef11bba6a57845d10780e7e889e3be289e8", "name": "oandrade.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/oandrade.eth", "header": null, "followers": "5", "following": "15" }, { "address": "0xb6518c8304992da58de9055f1db80a37609f00a2", "name": "silvr.eth", "avatar": "https://euc.li/silvr.eth", "header": null, "followers": "1", "following": "0" }, { "address": "0x8eddf5431f5b31933bfbd8111d54fc6e9456e6c1", "name": "slowsort.eth", "avatar": "https://euc.li/slowsort.eth", "header": null, "followers": "49", "following": "565" }, { "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "name": "garypalmerjr.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/garypalmerjr.eth", "header": null, "followers": "152", "following": "3403" }, ... ] } ``` --- #### /exportState/\:token_id Get all accounts that are being followed by EFP list id, excludes blocks and mutes #### Path Parameters - `token_id` (string): The EFP List of the account #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/exportState/333 ``` ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0x4c47ab777f1f64d1f3d6efbf1cc7ab5a5224af4a", "tags": [], }, { "version": 1, "record_type": "address", "data": "0x5b76f5b8fc9d700624f78208132f91ad4e61a1f0", "tags": [], }, { "version": 1, "record_type": "address", "data": "0x27cd3a463df1b3f6c95a222616d474be009c2cbb", "tags": [], }, { "version": 1, "record_type": "address", "data": "0xd1efdd037566b0c75cebace9150d26ea0153faa9", "tags": [], }, ], } ``` --- ### /users/\:addressOrENS/list-records Get the list records of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/encrypteddegen.eth/list-records ``` ```jsonc // sample response { "records": [ { "version": 1, "record_type": "address", "data": "0x0116acf39cf70fefc3c23c88a37e59474e8e17be", "tags": null }, { "version": 1, "record_type": "address", "data": "0x021021ccee934b346160342f8d7f59f514c08c56", "tags": null }, { "version": 1, "record_type": "address", "data": "0x025376e7e7f161a198fb5fc90a220a553836d11a", "tags": null }, ... ] } ``` --- ### /users/\:addressOrENS/recommended Get recommended users for a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/recommended ``` ```jsonc // sample response { "recommended": [ { "name": "swindler.eth", "address": "0xf972bf8592c3171b378e97bb869a980c3f476583", "avatar": "https://rainbow.mypinata.cloud/ipfs/QmcSAHrGGdXJRPmxYUk1R86Wqpfgg4TPMAXC6MfQHPugvF", "header": "https://rainbow.mypinata.cloud/ipfs/QmVDbkDutSk4phVohMs76jV4RgT3bpSdzHnesBHFxW6jRL", "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, { "name": "gratefulape.eth", "address": "0x52a4c418576dc46e4116ececc6f68d1c9b9636ed", "avatar": "https://euc.li/gratefulape.eth", "header": null, "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, { "name": "treeskulltown.eth", "address": "0x2dacc0b072146b40e60b8596b99756112d45c924", "avatar": "https://euc.li/treeskulltown.eth", "header": null, "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, ... ] } ``` --- ### /users/\:addressOrENS/lists Get the lists of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/brantly.eth/lists ``` ```jsonc // sample response { "primary_list": "4", "lists": ["4", "107"], } ``` --- ### /users/\:addressOrENS/notifications Get incoming actions received from other users by Address or ENS Name #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `opcode` (number, optional): Specifies the type of operation to return [1 for follow, 2 for un-follow, 3 for tag, 4 for un-tag, 0 for any] - `interval` (string, optional): Specifies the time range of notifications to return [hour, day, week, month, all]. - `start` (number, optional): Specifies the starting timestamp to begin the interval, default value is now. [unix timestamp ex. 1741159543] - `tag` (string, optional): Specifies a single tag string, of which each account in the response should have at least one. - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Default Values A request to this endpoint with no query parameters specified will default to: - opcode = all - interval = week - start = (current timestamp of now) - tag = "" - limit = 10 - offset = 0 Take care not to request tags joined with incorrect opcodes i.e. requests with a tag specified but opcode = 2 will not return data #### Sample Query: No params set ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/notifications ``` ```jsonc // sample response { "summary": { "interval": "168:00:00(hrs)", "opcode": "all", "total": 10, "total_follows": 9, "total_unfollows": 0, "total_tags": 1, "total_untags": 0 }, "notifications": [ { "address": "0x8004f955c7ed19b465f1f30ad7d04c6d2edc4e81", "name": "jackflash.eth", "avatar": "https://euc.li/jackflash.eth", "token_id": "25556", "action": "tag", "opcode": 3, "op": "0x01030101c9c3a4337a1bba75d0860a1a81f7b990dc607334646576", "tag": "dev", "updated_at": "2024-12-04T04:07:55.948Z" }, { "address": "0xce89b39a2c5d66040093df8013f02d1f0a124200", "name": "nomamkin.eth", "avatar": "https://ens.xyz/nomamkin.eth", "token_id": "28502", "action": "follow", "opcode": 1, "op": "0x01010101c9c3a4337a1bba75d0860a1a81f7b990dc607334", "tag": "", "updated_at": "2024-12-03T12:15:42.656Z" }, { "address": "0x69a00fafe7e935fde9ecb5c53f23e0e409a33e12", "name": "myavocado.eth", "avatar": "https://euc.li/myavocado.eth", "token_id": "28491", "action": "follow", "opcode": 1, "op": "0x01010101c9c3a4337a1bba75d0860a1a81f7b990dc607334", "tag": "", "updated_at": "2024-12-03T11:28:45.493Z" }, ... ] } ``` #### Sample Query: Follows in last week ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/notifications?opcode=1&interval=week ``` ```jsonc // sample response { "summary": { "interval": "168:00:00(hrs)", "opcode": "1", "total": 10, "total_follows": 10, "total_unfollows": 0, "total_tags": 0, "total_untags": 0 }, "notifications": [ { "address": "0xce89b39a2c5d66040093df8013f02d1f0a124200", "name": "nomamkin.eth", "avatar": "https://ens.xyz/nomamkin.eth", "token_id": "28502", "action": "follow", "opcode": 1, "op": "0x01010101c9c3a4337a1bba75d0860a1a81f7b990dc607334", "tag": "", "updated_at": "2024-12-03T12:15:42.656Z" }, { "address": "0x69a00fafe7e935fde9ecb5c53f23e0e409a33e12", "name": "myavocado.eth", "avatar": "https://euc.li/myavocado.eth", "token_id": "28491", "action": "follow", "opcode": 1, "op": "0x01010101c9c3a4337a1bba75d0860a1a81f7b990dc607334", "tag": "", "updated_at": "2024-12-03T11:28:45.493Z" }, { "address": "0x11a0cbe3548636d02506e945c77b17c5d3fd5fd5", "name": "senior01.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/senior01.eth", "token_id": "31911", "action": "follow", "opcode": 1, "op": "0x01010101c9c3a4337a1bba75d0860a1a81f7b990dc607334", "tag": "", "updated_at": "2024-12-03T11:22:00.378Z" }, ... ] } ``` #### Sample Query: All cases where tag is 'top8' ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/notifications?interval=all&tag=top8 ``` ```jsonc // sample response { "summary": { "interval": "999:00:00(hrs)", "opcode": "all", "total": 4, "total_follows": 0, "total_unfollows": 0, "total_tags": 4, "total_untags": 0 }, "notifications": [ { "address": "0x2e711004fef4751b62aeb3608d722d22ce536d84", "name": "10bitcoin.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/10bitcoin.eth", "token_id": "30340", "action": "tag", "opcode": 3, "op": "0x01030101c9c3a4337a1bba75d0860a1a81f7b990dc607334746f7038", "tag": "top8", "updated_at": "2024-11-22T07:30:15.626Z" }, { "address": "0x9a4c6ec8af626ae0c214c3bdd14ac56be15aaefd", "name": "lagovskiiigor.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/lagovskiiigor.eth", "token_id": "30054", "action": "tag", "opcode": 3, "op": "0x01030101c9c3a4337a1bba75d0860a1a81f7b990dc607334746f7038", "tag": "top8", "updated_at": "2024-11-21T06:01:02.407Z" }, ... ] } ``` --- ### /users/\:addressOrENS/stats Get stats of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `live` (bool, optional): Specifies whether to calculate stats or return cached values - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/limes.eth/stats ``` ```jsonc // sample response { "followers_count": "104", "following_count": "26", } ``` --- ### /users/\:addressOrENS/details Get account details, populates most of the data on a profile card #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/details ``` ```jsonc // sample response { "address": "0xc9c3a4337a1bba75d0860a1a81f7b990dc607334", "ens": { "name": "0xthrpw.eth", "avatar": "https://euc.li/0xthrpw.eth", "records": { "avatar": "https://euc.li/0xthrpw.eth", "com.github": "0xthrpw", "com.twitter": "0xthrpw", "description": "Took the mirrors off my cadillac because I don't like looking like I look back.", "header": "https://storage.googleapis.com/nftimagebucket/tokens/0xb7def63a9040ad5dc431aff79045617922f4023a/1897.svg", "name": "throw;", }, "updated_at": "2024-09-18T02:12:57.934Z", }, "ranks": { "mutuals_rank": "6", "followers_rank": "19", "following_rank": "18", "top8_rank": "12", "blocks_rank": "7", }, "primary_list": "3", } ``` --- ### /users/\:addressOrENS/commonFollowers Get common followers that are shared by two accounts #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `leader` (address, required): Specifies the account whose followers should be compared #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/commonFollowers?leader=0x0312567d78ff0c9ce0bd62a250df5c6474c71334 ``` ```jsonc // sample response { "results": [ { "address": "0x0312567d78ff0c9ce0bd62a250df5c6474c71334", "name": "pepe.eth", "avatar": "https://preview.redd.it/23tzr9qimgf51.png?auto=webp&s=d5475b2c247d3f3b4c8d9d3d3cae2521e15437ef", "mutuals_rank": "10" }, { "address": "0x038b716928a41ea42253ac043af4f8fdcd940098", "name": "aaron.box", "avatar": "https://metadata.ens.domains/mainnet/avatar/aaron.box", "mutuals_rank": "108" }, { "address": "0x074470b9a32f68de86fac393a10d5cea01c54269", "name": "pawswap.eth", "avatar": "https://euc.li/pawswap.eth", "mutuals_rank": "18" }, { "address": "0x074631095645e426e50b478d40301dd35e74f24c", "name": "pasqui.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/pasqui.eth", "mutuals_rank": "201" }, ... ], "length": 76 } ``` --- ### /users/\:addressOrENS/account Get account information by Address or ENS Name #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh copy curl http://api.ethfollow.xyz/api/v1/users/dr3a.eth/account ``` ```jsonc // sample response { "address": "0xeb6b293e9bb1d71240953c8306ad2c8ac523516a", "ens": { "name": "dr3a.eth", "avatar": "https://avatar-upload.ens-cf.workers.dev/mainnet/dr3a.eth?timestamp=1681787731836", "records": { "avatar": "https://avatar-upload.ens-cf.workers.dev/mainnet/dr3a.eth?timestamp=1681787731836", "com.discord": "dr3a.eth", "com.twitter": "dr3a_eth", "description": "dr3a.eth 💙", "email": "dr3a.eth@skiff.com", "name": "drea", "org.telegram": "dr3adoteth", "url": "https://dr3a.eth.limo", }, "updated_at": "2024-09-18T01:54:52.959Z", }, } ``` --- ### /users/\:addressOrENS/searchFollowers Search for followers of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `term` (string, optional): Specifies the string to search for in a followers address or ENS name #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/searchFollowers?term=brant ``` ```jsonc // sample response { "followers": [ { "efp_list_nft_token_id": "4", "address": "0x983110309620d911731ac0932219af06091b6744", "ens": { "name": "brantly.eth", "avatar": "https://euc.li/brantly.eth", }, "tags": [], "is_following": true, "is_blocked": false, "is_muted": false, }, ], } ``` --- ### /users/\:addressOrENS/primary-list Get the primary list of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/encrypteddegen.eth/primary-list ``` ```jsonc // sample response { "primary_list": "1", } ``` --- ### /users/\:addressOrENS/searchFollowing Search for following of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `term` (string, optional): Specifies the string to search for in a following address or ENS name #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/searchFollowing?term=degen ``` ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0x96053204967c30079529adddc56f6a37380205af", "tags": [], "ens": { "name": "alphadegen.eth", "avatar": "https://bafybeiaqof5u4bj57t36pt2t7egerky6epvutg7yb4suljnnjuqboymhvi.ipfs.dweb.link/af1f0c19f22c6ee0ea9a9e5f89d585df1ab8c677ef0ef7f0a448cce0fef21a71.png" } }, { "version": 1, "record_type": "address", "data": "0x69207d197063c6b207ff206fdba916e1700d60fa", "tags": [], "ens": { "name": "degenfam.eth", "avatar": "https://codemakes.art/image/quasars/2631" } }, { "version": 1, "record_type": "address", "data": "0x70bb434ea7b7f14709ed0dd17cc54056812cf4ad", "tags": [], "ens": { "name": "teradegen.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/teradegen.eth" } }, } ``` --- ### /users/\:addressOrENS/ens Get the ENS data of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/brantly.eth/ens ``` ```jsonc // sample response { "ens": { "name": "brantly.eth", "address": "0x983110309620d911731ac0932219af06091b6744", "avatar": "https://euc.li/brantly.eth", "records": { "avatar": "https://euc.li/brantly.eth", "com.discord": "brantly.eth", "com.github": "brantlymillegan", "com.twitter": "brantlymillegan", "description": "Catholic, husband, father | building @efp.eth | ENS (DAO delegate, former core team) | Sign-in with Ethereum (creator)", "email": "me@brantly.xyz", "header": "https://i.imgur.com/Quo06x2.png", "location": "USA", "name": "Brantly Millegan", "org.telegram": "brantlymillegan", "url": "https://ethfollow.xyz/", }, "updated_at": "2024-09-18T03:40:58.807Z", }, } ``` --- ### /users/\:addressOrENS/taggedAs Get the tags that are applied to a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/taggedAs ``` ```jsonc // sample response { "address": "0xc9c3a4337a1bba75d0860a1a81f7b990dc607334", "tags": ["top8"], "tagCounts": [ { "tag": "top8", "count": 5, }, ], "taggedAddresses": [ { "address": "0x5a3bf42028901447434d12c5459954e667e5c518", "tag": "top8", }, { "address": "0x71adb34117c9408e74ed112b327a0ec97cef8fa1", "tag": "top8", }, { "address": "0x8eddf5431f5b31933bfbd8111d54fc6e9456e6c1", "tag": "top8", }, { "address": "0xfa1afc4534fc9f80a552e61dd04cd8a172c821a6", "tag": "top8", }, { "address": "0xc983ebc9db969782d994627bdffec0ae6efee1b3", "tag": "top8", }, ], } ``` --- ### /users/\:addressOrENS/tags Get the tags of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/brantly.eth/tags ``` ```jsonc // sample response { "address": "0x983110309620d911731ac0932219af06091b6744", "tags": [ "top8", "block", "degen" ], "tagCounts": [ { "tag": "top8", "count": 8 }, { "tag": "block", "count": 2 }, { "tag": "degen", "count": 4 } ], "taggedAddresses": [ { "address": "0x44a3a18df15ae79bbc6c660db59428fe9a181864", "tag": "top8" }, { "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "tag": "block" }, { "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "tag": "degen" }, { "address": "0x60377ec355857c2d06d1ce28555f624257344b0d", "tag": "top8" }, ... ] } ``` --- ### /users/\:addressOrENS/following Get following by Address or ENS Name #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/dr3a.eth/following ``` #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. - `sort` (string, optional): Specifies how the results should be sorted, possible values 'latest', 'earliest', 'followers'. If not specified, default sort is 'latest' - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0x983110309620d911731ac0932219af06091b6744", "tags": ["efp", "ens"], }, { "version": 1, "record_type": "address", "data": "0xbdb41bff7e828e2dc2d15eb67257455db818f1dc", "tags": ["efp", "ens"], }, { "version": 1, "record_type": "address", "data": "0xf4212614c7fe0b3feef75057e88b2e77a7e23e83", "tags": ["efp"], }, ], } ``` --- ### /users/\:addressOrENS/\:addressOrENS2/followerState Get the follower state between two users. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. - `addressOrENS2` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/brantly.eth/followerState ``` ```jsonc // sample response { "addressUser": "0xc9c3a4337a1bba75d0860a1a81f7b990dc607334", "addressFollower": "0x983110309620d911731ac0932219af06091b6744", "state": { "follow": true, "block": false, "mute": false, }, } ``` --- ### /users/\:addressOrENS/followers Get followers by Address or ENS Name #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. - `sort` (string, optional): Specifies how the results should be sorted, possible values 'latest', 'earliest', 'followers'. If not specified, default sort is 'latest' - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl http://api.ethfollow.xyz/api/v1/users/dr3a.eth/followers ``` ```jsonc // sample response { "followers": [ { "efp_list_nft_token_id": "5895", "address": "0xd56c76b3f924e8f84a02654ff072a363a84b91d9", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-10-14T19:45:38.617Z", }, { "efp_list_nft_token_id": "6337", "address": "0x907ed289f363dbdb2ab1230dfbd2f77a05cda82d", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-10-14T17:47:20.727Z", }, { "efp_list_nft_token_id": "13986", "address": "0x7766ef005ec1b38a8472831e2f0631b12c811a5f", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-10-14T17:12:28.718Z", }, { "efp_list_nft_token_id": "6323", "address": "0x4e203e4f4bbf119f4e83763d5b143316b3b3c6cc", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-10-14T15:06:03.633Z", }, ], } ``` --- # Github Repositories - https://github.com/ethereumfollowprotocol/app - https://github.com/ethereumfollowprotocol/docs - https://github.com/ethereumfollowprotocol/api - https://github.com/ethereumfollowprotocol/services - https://github.com/ethereumfollowprotocol/indexer - https://github.com/ethereumfollowprotocol/notification-service - https://github.com/ethereumfollowprotocol/follow-bot - https://github.com/ethereumfollowprotocol/contracts - https://github.com/ethereumfollowprotocol/replay - https://github.com/ethereumfollowprotocol/onchain --- # Contracts > Deployed solidity contracts used by the Ethereum Follow Protocol. ## EFPListRecords.sol ```solc // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0 ^0.8.23; // lib/openzeppelin-contracts/contracts/utils/Context.sol // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) /** * @dev Provides information about the current execution context, including the * sender of the transaction and its data. While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct * manner, since when dealing with meta-transactions the account sending and * paying for execution may not be the actual sender (as far as an application * is concerned). * * This contract is only required for intermediate, library-like contracts. */ abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } } // src/interfaces/IEFPListRecords.sol /** * @title IEFPListMetadata */ interface IEFPListMetadata { event UpdateListMetadata(uint256 indexed slot, string key, bytes value); struct KeyValue { string key; bytes value; } function getMetadataValue(uint256 slot, string calldata key) external view returns (bytes memory); function getMetadataValues(uint256 slot, string[] calldata keys) external view returns (bytes[] memory); function setMetadataValue(uint256 slot, string calldata key, bytes calldata value) external; function setMetadataValues(uint256 slot, KeyValue[] calldata records) external; // List Manager Functions function claimListManager(uint256 slot) external; function claimListManagerForAddress(uint256 slot, address manager) external; function getListManager(uint256 slot) external view returns (address); function setListManager(uint256 slot, address manager) external; // List User Functions function getListUser(uint256 slot) external view returns (address); function setListUser(uint256 slot, address user) external; } /** * @title IEFPListRecords * @notice Interface for the ListRecords contract. */ interface IEFPListRecords is IEFPListMetadata { // Events event ListOp(uint256 indexed slot, bytes op); // List Operation Functions - Read function getListOpCount(uint256 slot) external view returns (uint256); function getListOp(uint256 slot, uint256 index) external view returns (bytes memory); function getListOpsInRange(uint256 slot, uint256 start, uint256 end) external view returns (bytes[] memory); function getAllListOps(uint256 slot) external view returns (bytes[] memory); // List Operation Functions - Write function applyListOp(uint256 slot, bytes calldata op) external; function applyListOps(uint256 slot, bytes[] calldata ops) external; function setMetadataValuesAndApplyListOps(uint256 slot, KeyValue[] calldata records, bytes[] calldata ops) external; } // lib/openzeppelin-contracts/contracts/access/Ownable.sol // OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol) /** * @dev Contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * By default, the owner account will be the one that deploys the contract. This * can later be changed with {transferOwnership}. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be applied to your functions to restrict their use to * the owner. */ abstract contract Ownable is Context { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev Initializes the contract setting the deployer as the initial owner. */ constructor() { _transferOwnership(_msgSender()); } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { _checkOwner(); _; } /** * @dev Returns the address of the current owner. */ function owner() public view virtual returns (address) { return _owner; } /** * @dev Throws if the sender is not the owner. */ function _checkOwner() internal view virtual { require(owner() == _msgSender(), "Ownable: caller is not the owner"); } /** * @dev Leaves the contract without owner. It will not be possible to call * `onlyOwner` functions. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, * thereby disabling any functionality that is only available to the owner. */ function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), "Ownable: new owner is the zero address"); _transferOwnership(newOwner); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Internal function without access restriction. */ function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } } // lib/openzeppelin-contracts/contracts/security/Pausable.sol // OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) /** * @dev Contract module which allows children to implement an emergency stop * mechanism that can be triggered by an authorized account. * * This module is used through inheritance. It will make available the * modifiers `whenNotPaused` and `whenPaused`, which can be applied to * the functions of your contract. Note that they will not be pausable by * simply including this module, only once the modifiers are put in place. */ abstract contract Pausable is Context { /** * @dev Emitted when the pause is triggered by `account`. */ event Paused(address account); /** * @dev Emitted when the pause is lifted by `account`. */ event Unpaused(address account); bool private _paused; /** * @dev Initializes the contract in unpaused state. */ constructor() { _paused = false; } /** * @dev Modifier to make a function callable only when the contract is not paused. * * Requirements: * * - The contract must not be paused. */ modifier whenNotPaused() { _requireNotPaused(); _; } /** * @dev Modifier to make a function callable only when the contract is paused. * * Requirements: * * - The contract must be paused. */ modifier whenPaused() { _requirePaused(); _; } /** * @dev Returns true if the contract is paused, and false otherwise. */ function paused() public view virtual returns (bool) { return _paused; } /** * @dev Throws if the contract is paused. */ function _requireNotPaused() internal view virtual { require(!paused(), "Pausable: paused"); } /** * @dev Throws if the contract is not paused. */ function _requirePaused() internal view virtual { require(paused(), "Pausable: not paused"); } /** * @dev Triggers stopped state. * * Requirements: * * - The contract must not be paused. */ function _pause() internal virtual whenNotPaused { _paused = true; emit Paused(_msgSender()); } /** * @dev Returns to normal state. * * Requirements: * * - The contract must be paused. */ function _unpause() internal virtual whenPaused { _paused = false; emit Unpaused(_msgSender()); } } // src/lib/ENSReverseClaimer.sol interface ENS { /** * @dev Returns the address that owns the specified node. * @param node The specified node. * @return address of the owner. */ function owner(bytes32 node) external view returns (address); } interface IReverseRegistrar { /** * @dev Transfers ownership of the reverse ENS record associated with the * calling account. * @param owner The address to set as the owner of the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function claim(address owner) external returns (bytes32); /** * @dev Sets the `name()` record for the reverse ENS record associated with * the calling account. First updates the resolver to the default reverse * resolver if necessary. * @param name The name to set for this address. * @return The ENS node hash of the reverse record. */ function setName(string memory name) external returns (bytes32); } /** * @title ENSReverseClaimer * @dev This contract is used to claim reverse ENS records. */ abstract contract ENSReverseClaimer is Ownable { /// @dev The namehash of 'addr.reverse', the domain at which reverse records /// are stored in ENS. bytes32 constant ADDR_REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2; /** * @dev Transfers ownership of the reverse ENS record associated with the * contract. * @param ens The ENS registry. * @param claimant The address to set as the owner of the reverse record in * ENS. * @return The ENS node hash of the reverse record. */ function claimReverseENS(ENS ens, address claimant) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).claim(claimant); } /** * @dev Sets the reverse ENS record associated with the contract. * @param ens The ENS registry. * @param name The name to set as the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function setReverseENS(ENS ens, string calldata name) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).setName(name); } } // src/EFPListRecords.sol /** * @title ListMetadata * @author Cory Gabrielsen (cory.eth) * @custom:contributor throw; (0xthrpw.eth) * @custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM * * @notice Manages key-value pairs associated with EFP List NFTs. * Provides functionalities for list managers to set and retrieve metadata for their lists. */ abstract contract ListMetadata is IEFPListMetadata, Pausable, Ownable { error SlotAlreadyClaimed(uint256 slot, address manager); // error NotListManager(address manager); /////////////////////////////////////////////////////////////////////////// // Data Structures /////////////////////////////////////////////////////////////////////////// /// @dev The key-value set for each token ID mapping(uint256 => mapping(string => bytes)) private values; ///////////////////////////////////////////////////////////////////////////// // Pausable ///////////////////////////////////////////////////////////////////////////// /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } ///////////////////////////////////////////////////////////////////////////// // Helpers ///////////////////////////////////////////////////////////////////////////// function bytesToAddress(bytes memory b) internal pure returns (address) { require(b.length == 20, 'Invalid length'); address addr; assembly { addr := mload(add(b, 20)) } return addr; } ///////////////////////////////////////////////////////////////////////////// // Getters ///////////////////////////////////////////////////////////////////////////// /** * @dev Retrieves metadata value for token ID and key. * @param tokenId The token Id to query. * @param key The key to query. * @return The associated value. */ function getMetadataValue(uint256 tokenId, string calldata key) external view returns (bytes memory) { return values[tokenId][key]; } /** * @dev Retrieves metadata values for token ID and keys. * @param tokenId The token Id to query. * @param keys The keys to query. * @return The associated values. */ function getMetadataValues(uint256 tokenId, string[] calldata keys) external view returns (bytes[] memory) { uint256 length = keys.length; bytes[] memory result = new bytes[](length); for (uint256 i = 0; i < length;) { string calldata key = keys[i]; result[i] = values[tokenId][key]; unchecked { ++i; } } return result; } ///////////////////////////////////////////////////////////////////////////// // Setters ///////////////////////////////////////////////////////////////////////////// /** * @dev Sets metadata records for token ID with the unique key key to value, * overwriting anything previously stored for token ID and key. To clear a * field, set it to the empty string. * @param slot The slot corresponding to the list to update. * @param key The key to set. * @param value The value to set. */ function _setMetadataValue(uint256 slot, string memory key, bytes memory value) internal { values[slot][key] = value; emit UpdateListMetadata(slot, key, value); } /** * @dev Sets metadata records for token ID with the unique key key to value, * overwriting anything previously stored for token ID and key. To clear a * field, set it to the empty string. Only callable by the list manager. * @param slot The slot corresponding to the list to update. * @param key The key to set. * @param value The value to set. */ function setMetadataValue(uint256 slot, string calldata key, bytes calldata value) external whenNotPaused onlyListManager(slot) { _setMetadataValue(slot, key, value); } /** * @dev Sets an array of metadata records for a token ID. Each record is a * key/value pair. * @param slot The slot corresponding to the list to update. * @param records The records to set. */ function _setMetadataValues(uint256 slot, KeyValue[] calldata records) internal { uint256 length = records.length; for (uint256 i = 0; i < length;) { KeyValue calldata record = records[i]; _setMetadataValue(slot, record.key, record.value); unchecked { ++i; } } } /** * @dev Sets an array of metadata records for a token ID. Each record is a * key/value pair. Only callable by the list manager. * @param slot The slot corresponding to the list to update. * @param records The records to set. */ function setMetadataValues(uint256 slot, KeyValue[] calldata records) external whenNotPaused onlyListManager(slot) { _setMetadataValues(slot, records); } /////////////////////////////////////////////////////////////////////////// // Modifiers /////////////////////////////////////////////////////////////////////////// /** * @notice Ensures that the caller is the manager of the specified list. * @param slot The unique identifier of the list. * @dev Used to restrict function access to the list's manager. */ modifier onlyListManager(uint256 slot) { bytes memory existing = values[slot]['manager']; // if not set, claim for msg.sender if (existing.length != 20) { _claimListManager(slot, msg.sender); } else { address existingManager = bytesToAddress(existing); if (existingManager == address(0)) { _claimListManager(slot, msg.sender); } else { require(existingManager == msg.sender, 'Not list manager'); } } _; } /////////////////////////////////////////////////////////////////////////// // List Manager - Claim /////////////////////////////////////////////////////////////////////////// /** * @notice Allows an address to claim management of an unclaimed list slot. * @param slot The slot that the sender wishes to claim. * @param manager The address to be set as the manager. * @dev This function establishes the first-come-first-serve basis for slot claiming. */ function _claimListManager(uint256 slot, address manager) internal { bytes memory existing = values[slot]['manager']; // require(existing.length != 20 || bytesToAddress(existing) == manager, "slot already claimed"); if (existing.length == 20) { address existingManager = bytesToAddress(existing); if (existingManager != manager) { revert SlotAlreadyClaimed(slot, existingManager); } } _setMetadataValue(slot, 'manager', abi.encodePacked(manager)); } /** * @notice Allows the sender to claim management of an unclaimed list slot. * @param slot The slot that the sender wishes to claim. */ function claimListManager(uint256 slot) external whenNotPaused { _claimListManager(slot, msg.sender); } /** * @notice Allows the sender to transfer management of a list to a new address. * @param slot The list's unique identifier. * @param manager The address to be set as the new manager. */ function claimListManagerForAddress(uint256 slot, address manager) external whenNotPaused { _claimListManager(slot, manager); } /////////////////////////////////////////////////////////////////////////// // List Manager - Read /////////////////////////////////////////////////////////////////////////// /** * @notice Retrieves the address of the manager for a specified list slot. * @param slot The list's unique identifier. * @return The address of the manager. */ function getListManager(uint256 slot) external view returns (address) { bytes memory existing = values[slot]['manager']; return existing.length != 20 ? address(0) : bytesToAddress(existing); } /////////////////////////////////////////////////////////////////////////// // List Manager - Write /////////////////////////////////////////////////////////////////////////// /** * @notice Allows the current manager to transfer management of a list to a new address. * @param slot The list's unique identifier. * @param manager The address to be set as the new manager. * @dev Only the current manager can transfer their management role. */ function setListManager(uint256 slot, address manager) external whenNotPaused onlyListManager(slot) { _setMetadataValue(slot, 'manager', abi.encodePacked(manager)); } /////////////////////////////////////////////////////////////////////////// // List User - Read /////////////////////////////////////////////////////////////////////////// /** * @notice Retrieves the address of the list user for a specified list * slot. * @param slot The list's unique identifier. * @return The address of the list user. */ function getListUser(uint256 slot) external view returns (address) { bytes memory existing = values[slot]['user']; return existing.length != 20 ? address(0) : bytesToAddress(existing); } /////////////////////////////////////////////////////////////////////////// // List Manager - Write /////////////////////////////////////////////////////////////////////////// /** * @notice Allows the current manager to change the list user to a new * address. * @param slot The list's unique identifier. * @param user The address to be set as the new list user. * @dev Only the current manager can change the list user. */ function setListUser(uint256 slot, address user) external whenNotPaused onlyListManager(slot) { _setMetadataValue(slot, 'user', abi.encodePacked(user)); } } /** * @title EFPListRecords * @notice Manages a dynamic list of records associated with EFP List NFTs. * Provides functionalities for list managers to apply operations to their lists. */ abstract contract ListRecords is IEFPListRecords, ListMetadata { /////////////////////////////////////////////////////////////////////////// // Data Structures /////////////////////////////////////////////////////////////////////////// /// @notice Stores a sequence of operations for each list identified by its slot. /// @dev Each list can have multiple operations performed over time. mapping(uint256 => bytes[]) public listOps; /////////////////////////////////////////////////////////////////////////// // List Operation Functions - Read /////////////////////////////////////////////////////////////////////////// /** * @notice Retrieves the number of operations performed on a list. * @param slot The list's unique identifier. * @return The number of operations performed on the list. */ function getListOpCount(uint256 slot) external view returns (uint256) { return listOps[slot].length; } /** * @notice Retrieves the operation at a specified index for a list. * @param slot The list's unique identifier. * @param index The index of the operation to be retrieved. * @return The operation at the specified index. */ function getListOp(uint256 slot, uint256 index) external view returns (bytes memory) { return listOps[slot][index]; } /** * @notice Retrieves a range of operations for a list. * @param slot The list's unique identifier. * @param start The starting index of the range. * @param end The ending index of the range. * @return The operations in the specified range. */ function getListOpsInRange(uint256 slot, uint256 start, uint256 end) external view returns (bytes[] memory) { if (start > end) { revert('Invalid range'); } bytes[] memory ops = new bytes[](end - start); for (uint256 i = start; i < end;) { ops[i - start] = listOps[slot][i]; unchecked { ++i; } } return ops; } /** * @notice Retrieves all operations for a list. * @param slot The list's unique identifier. * @return The operations performed on the list. */ function getAllListOps(uint256 slot) external view returns (bytes[] memory) { return listOps[slot]; } /////////////////////////////////////////////////////////////////////////// // List Operation Functions - Write /////////////////////////////////////////////////////////////////////////// /** * @notice Applies a single operation to the list. * @param slot The list's unique identifier. * @param op The operation to be applied. */ function _applyListOp(uint256 slot, bytes calldata op) internal { listOps[slot].push(op); emit ListOp(slot, op); } /** * @notice Public wrapper for `_applyOp`, enabling list managers to apply a single operation. * @param slot The list's unique identifier. * @param op The operation to be applied. */ function applyListOp(uint256 slot, bytes calldata op) external whenNotPaused onlyListManager(slot) { _applyListOp(slot, op); } /** * @notice Allows list managers to apply multiple operations in a single transaction. * @param slot The list's unique identifier. * @param ops An array of operations to be applied. */ function _applyListOps(uint256 slot, bytes[] calldata ops) internal { uint256 len = ops.length; for (uint256 i = 0; i < len;) { _applyListOp(slot, ops[i]); unchecked { ++i; } } } /** * @notice Allows list managers to apply multiple operations in a single transaction. * @param slot The list's unique identifier. * @param ops An array of operations to be applied. */ function applyListOps(uint256 slot, bytes[] calldata ops) external whenNotPaused onlyListManager(slot) { _applyListOps(slot, ops); } /** * @notice Allows list managers to set metadata values and apply list ops * in a single transaction. * @param slot The list's unique identifier. * @param records An array of key-value pairs to set. * @param ops An array of operations to be applied. */ function setMetadataValuesAndApplyListOps(uint256 slot, KeyValue[] calldata records, bytes[] calldata ops) external whenNotPaused onlyListManager(slot) { _setMetadataValues(slot, records); _applyListOps(slot, ops); } } contract EFPListRecords is IEFPListRecords, ListRecords, ENSReverseClaimer {} ``` ## EFPListRegistry.sol ```solc // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0 ^0.8.23 ^0.8.4; // lib/openzeppelin-contracts/contracts/utils/Context.sol // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) /** * @dev Provides information about the current execution context, including the * sender of the transaction and its data. While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct * manner, since when dealing with meta-transactions the account sending and * paying for execution may not be the actual sender (as far as an application * is concerned). * * This contract is only required for intermediate, library-like contracts. */ abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } } // src/interfaces/IEFPListNFTPriceOracle.sol /** * @title IEFPListNFTPriceOracle */ interface IEFPListNFTPriceOracle { function getPrice() external view returns (uint256); function getBatchPrice(uint256 quantity) external view returns (uint256); } // src/interfaces/IEFPListRegistry.sol /** * @title EFPListRegistry * @notice A registry connecting token IDs with data such as managers, users, and list locations. */ interface IEFPListRegistry { /////////////////////////////////////////////////////////////////////////// // Enums /////////////////////////////////////////////////////////////////////////// enum MintState { Disabled, OwnerOnly, PublicMint, PublicBatch } /////////////////////////////////////////////////////////////////////////// // Events /////////////////////////////////////////////////////////////////////////// /// @notice Emitted when a list storage location is set event UpdateListStorageLocation(uint256 indexed tokenId, bytes listStorageLocation); /////////////////////////////////////////////////////////////////////////// // ListStorageLocation /////////////////////////////////////////////////////////////////////////// function getListStorageLocation(uint256 tokenId) external view returns (bytes memory); function setListStorageLocation(uint256 tokenId, bytes calldata listStorageLocation) external; /////////////////////////////////////////////////////////////////////////// // Mint /////////////////////////////////////////////////////////////////////////// /// @notice Fetches the mint state. function getMintState() external view returns (MintState); /// @notice Sets the mint state. /// @param _mintState The new mint state. function setMintState(MintState _mintState) external; /// @notice Fetches the max mint batch size. function getMaxMintBatchSize() external view returns (uint256); /// @notice Sets the max mint batch size. /// @param _maxMintBatchSize The new max mint batch size. function setMaxMintBatchSize(uint256 _maxMintBatchSize) external; /// @notice Mints a new token. function mint(bytes calldata listStorageLocation) external payable; /** * @notice Mints a new token to the given address. * @param recipient The address to mint the token to. */ function mintTo(address recipient, bytes calldata listStorageLocation) external payable; /// @notice Mints a new token to the given address. function mintBatch(uint256 quantity) external payable; /// @notice Mints a new token to the given address. function mintBatchTo(address recipient, uint256 quantity) external payable; } // lib/ERC721A/contracts/IERC721A.sol // ERC721A Contracts v4.2.3 // Creator: Chiru Labs /** * @dev Interface of ERC721A. */ interface IERC721A { /** * The caller must own the token or be an approved operator. */ error ApprovalCallerNotOwnerNorApproved(); /** * The token does not exist. */ error ApprovalQueryForNonexistentToken(); /** * Cannot query the balance for the zero address. */ error BalanceQueryForZeroAddress(); /** * Cannot mint to the zero address. */ error MintToZeroAddress(); /** * The quantity of tokens minted must be more than zero. */ error MintZeroQuantity(); /** * The token does not exist. */ error OwnerQueryForNonexistentToken(); /** * The caller must own the token or be an approved operator. */ error TransferCallerNotOwnerNorApproved(); /** * The token must be owned by `from`. */ error TransferFromIncorrectOwner(); /** * Cannot safely transfer to a contract that does not implement the * ERC721Receiver interface. */ error TransferToNonERC721ReceiverImplementer(); /** * Cannot transfer to the zero address. */ error TransferToZeroAddress(); /** * The token does not exist. */ error URIQueryForNonexistentToken(); /** * The `quantity` minted with ERC2309 exceeds the safety limit. */ error MintERC2309QuantityExceedsLimit(); /** * The `extraData` cannot be set on an unintialized ownership slot. */ error OwnershipNotInitializedForExtraData(); // ============================================================= // STRUCTS // ============================================================= struct TokenOwnership { // The address of the owner. address addr; // Stores the start time of ownership with minimal overhead for tokenomics. uint64 startTimestamp; // Whether the token has been burned. bool burned; // Arbitrary data similar to `startTimestamp` that can be set via {_extraData}. uint24 extraData; } // ============================================================= // TOKEN COUNTERS // ============================================================= /** * @dev Returns the total number of tokens in existence. * Burned tokens will reduce the count. * To get the total number of tokens minted, please see {_totalMinted}. */ function totalSupply() external view returns (uint256); // ============================================================= // IERC165 // ============================================================= /** * @dev Returns true if this contract implements the interface defined by * `interfaceId`. See the corresponding * [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified) * to learn more about how these ids are created. * * This function call must use less than 30000 gas. */ function supportsInterface(bytes4 interfaceId) external view returns (bool); // ============================================================= // IERC721 // ============================================================= /** * @dev Emitted when `tokenId` token is transferred from `from` to `to`. */ event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); /** * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. */ event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); /** * @dev Emitted when `owner` enables or disables * (`approved`) `operator` to manage all of its assets. */ event ApprovalForAll(address indexed owner, address indexed operator, bool approved); /** * @dev Returns the number of tokens in `owner`'s account. */ function balanceOf(address owner) external view returns (uint256 balance); /** * @dev Returns the owner of the `tokenId` token. * * Requirements: * * - `tokenId` must exist. */ function ownerOf(uint256 tokenId) external view returns (address owner); /** * @dev Safely transfers `tokenId` token from `from` to `to`, * checking first that contract recipients are aware of the ERC721 protocol * to prevent tokens from being forever locked. * * Requirements: * * - `from` cannot be the zero address. * - `to` cannot be the zero address. * - `tokenId` token must exist and be owned by `from`. * - If the caller is not `from`, it must be have been allowed to move * this token by either {approve} or {setApprovalForAll}. * - If `to` refers to a smart contract, it must implement * {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. * * Emits a {Transfer} event. */ function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data ) external payable; /** * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. */ function safeTransferFrom( address from, address to, uint256 tokenId ) external payable; /** * @dev Transfers `tokenId` from `from` to `to`. * * WARNING: Usage of this method is discouraged, use {safeTransferFrom} * whenever possible. * * Requirements: * * - `from` cannot be the zero address. * - `to` cannot be the zero address. * - `tokenId` token must be owned by `from`. * - If the caller is not `from`, it must be approved to move this token * by either {approve} or {setApprovalForAll}. * * Emits a {Transfer} event. */ function transferFrom( address from, address to, uint256 tokenId ) external payable; /** * @dev Gives permission to `to` to transfer `tokenId` token to another account. * The approval is cleared when the token is transferred. * * Only a single account can be approved at a time, so approving the * zero address clears previous approvals. * * Requirements: * * - The caller must own the token or be an approved operator. * - `tokenId` must exist. * * Emits an {Approval} event. */ function approve(address to, uint256 tokenId) external payable; /** * @dev Approve or remove `operator` as an operator for the caller. * Operators can call {transferFrom} or {safeTransferFrom} * for any token owned by the caller. * * Requirements: * * - The `operator` cannot be the caller. * * Emits an {ApprovalForAll} event. */ function setApprovalForAll(address operator, bool _approved) external; /** * @dev Returns the account approved for `tokenId` token. * * Requirements: * * - `tokenId` must exist. */ function getApproved(uint256 tokenId) external view returns (address operator); /** * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. * * See {setApprovalForAll}. */ function isApprovedForAll(address owner, address operator) external view returns (bool); // ============================================================= // IERC721Metadata // ============================================================= /** * @dev Returns the token collection name. */ function name() external view returns (string memory); /** * @dev Returns the token collection symbol. */ function symbol() external view returns (string memory); /** * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. */ function tokenURI(uint256 tokenId) external view returns (string memory); // ============================================================= // IERC2309 // ============================================================= /** * @dev Emitted when tokens in `fromTokenId` to `toTokenId` * (inclusive) is transferred from `from` to `to`, as defined in the * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309) standard. * * See {_mintERC2309} for more details. */ event ConsecutiveTransfer(uint256 indexed fromTokenId, uint256 toTokenId, address indexed from, address indexed to); } // src/interfaces/ITokenURIProvider.sol interface ITokenURIProvider { function tokenURI(uint256 tokenId) external view returns (string memory); } // lib/ERC721A/contracts/ERC721A.sol // ERC721A Contracts v4.2.3 // Creator: Chiru Labs /** * @dev Interface of ERC721 token receiver. */ interface ERC721A__IERC721Receiver { function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4); } /** * @title ERC721A * * @dev Implementation of the [ERC721](https://eips.ethereum.org/EIPS/eip-721) * Non-Fungible Token Standard, including the Metadata extension. * Optimized for lower gas during batch mints. * * Token IDs are minted in sequential order (e.g. 0, 1, 2, 3, ...) * starting from `_startTokenId()`. * * Assumptions: * * - An owner cannot have more than 2**64 - 1 (max value of uint64) of supply. * - The maximum token ID cannot exceed 2**256 - 1 (max value of uint256). */ contract ERC721A is IERC721A { // Bypass for a `--via-ir` bug (https://github.com/chiru-labs/ERC721A/pull/364). struct TokenApprovalRef { address value; } // ============================================================= // CONSTANTS // ============================================================= // Mask of an entry in packed address data. uint256 private constant _BITMASK_ADDRESS_DATA_ENTRY = (1 << 64) - 1; // The bit position of `numberMinted` in packed address data. uint256 private constant _BITPOS_NUMBER_MINTED = 64; // The bit position of `numberBurned` in packed address data. uint256 private constant _BITPOS_NUMBER_BURNED = 128; // The bit position of `aux` in packed address data. uint256 private constant _BITPOS_AUX = 192; // Mask of all 256 bits in packed address data except the 64 bits for `aux`. uint256 private constant _BITMASK_AUX_COMPLEMENT = (1 << 192) - 1; // The bit position of `startTimestamp` in packed ownership. uint256 private constant _BITPOS_START_TIMESTAMP = 160; // The bit mask of the `burned` bit in packed ownership. uint256 private constant _BITMASK_BURNED = 1 << 224; // The bit position of the `nextInitialized` bit in packed ownership. uint256 private constant _BITPOS_NEXT_INITIALIZED = 225; // The bit mask of the `nextInitialized` bit in packed ownership. uint256 private constant _BITMASK_NEXT_INITIALIZED = 1 << 225; // The bit position of `extraData` in packed ownership. uint256 private constant _BITPOS_EXTRA_DATA = 232; // Mask of all 256 bits in a packed ownership except the 24 bits for `extraData`. uint256 private constant _BITMASK_EXTRA_DATA_COMPLEMENT = (1 << 232) - 1; // The mask of the lower 160 bits for addresses. uint256 private constant _BITMASK_ADDRESS = (1 << 160) - 1; // The maximum `quantity` that can be minted with {_mintERC2309}. // This limit is to prevent overflows on the address data entries. // For a limit of 5000, a total of 3.689e15 calls to {_mintERC2309} // is required to cause an overflow, which is unrealistic. uint256 private constant _MAX_MINT_ERC2309_QUANTITY_LIMIT = 5000; // The `Transfer` event signature is given by: // `keccak256(bytes("Transfer(address,address,uint256)"))`. bytes32 private constant _TRANSFER_EVENT_SIGNATURE = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef; // ============================================================= // STORAGE // ============================================================= // The next token ID to be minted. uint256 private _currentIndex; // The number of tokens burned. uint256 private _burnCounter; // Token name string private _name; // Token symbol string private _symbol; // Mapping from token ID to ownership details // An empty struct value does not necessarily mean the token is unowned. // See {_packedOwnershipOf} implementation for details. // // Bits Layout: // - [0..159] `addr` // - [160..223] `startTimestamp` // - [224] `burned` // - [225] `nextInitialized` // - [232..255] `extraData` mapping(uint256 => uint256) private _packedOwnerships; // Mapping owner address to address data. // // Bits Layout: // - [0..63] `balance` // - [64..127] `numberMinted` // - [128..191] `numberBurned` // - [192..255] `aux` mapping(address => uint256) private _packedAddressData; // Mapping from token ID to approved address. mapping(uint256 => TokenApprovalRef) private _tokenApprovals; // Mapping from owner to operator approvals mapping(address => mapping(address => bool)) private _operatorApprovals; // ============================================================= // CONSTRUCTOR // ============================================================= constructor(string memory name_, string memory symbol_) { _name = name_; _symbol = symbol_; _currentIndex = _startTokenId(); } // ============================================================= // TOKEN COUNTING OPERATIONS // ============================================================= /** * @dev Returns the starting token ID. * To change the starting token ID, please override this function. */ function _startTokenId() internal view virtual returns (uint256) { return 0; } /** * @dev Returns the next token ID to be minted. */ function _nextTokenId() internal view virtual returns (uint256) { return _currentIndex; } /** * @dev Returns the total number of tokens in existence. * Burned tokens will reduce the count. * To get the total number of tokens minted, please see {_totalMinted}. */ function totalSupply() public view virtual override returns (uint256) { // Counter underflow is impossible as _burnCounter cannot be incremented // more than `_currentIndex - _startTokenId()` times. unchecked { return _currentIndex - _burnCounter - _startTokenId(); } } /** * @dev Returns the total amount of tokens minted in the contract. */ function _totalMinted() internal view virtual returns (uint256) { // Counter underflow is impossible as `_currentIndex` does not decrement, // and it is initialized to `_startTokenId()`. unchecked { return _currentIndex - _startTokenId(); } } /** * @dev Returns the total number of tokens burned. */ function _totalBurned() internal view virtual returns (uint256) { return _burnCounter; } // ============================================================= // ADDRESS DATA OPERATIONS // ============================================================= /** * @dev Returns the number of tokens in `owner`'s account. */ function balanceOf(address owner) public view virtual override returns (uint256) { if (owner == address(0)) revert BalanceQueryForZeroAddress(); return _packedAddressData[owner] & _BITMASK_ADDRESS_DATA_ENTRY; } /** * Returns the number of tokens minted by `owner`. */ function _numberMinted(address owner) internal view returns (uint256) { return (_packedAddressData[owner] >> _BITPOS_NUMBER_MINTED) & _BITMASK_ADDRESS_DATA_ENTRY; } /** * Returns the number of tokens burned by or on behalf of `owner`. */ function _numberBurned(address owner) internal view returns (uint256) { return (_packedAddressData[owner] >> _BITPOS_NUMBER_BURNED) & _BITMASK_ADDRESS_DATA_ENTRY; } /** * Returns the auxiliary data for `owner`. (e.g. number of whitelist mint slots used). */ function _getAux(address owner) internal view returns (uint64) { return uint64(_packedAddressData[owner] >> _BITPOS_AUX); } /** * Sets the auxiliary data for `owner`. (e.g. number of whitelist mint slots used). * If there are multiple variables, please pack them into a uint64. */ function _setAux(address owner, uint64 aux) internal virtual { uint256 packed = _packedAddressData[owner]; uint256 auxCasted; // Cast `aux` with assembly to avoid redundant masking. assembly { auxCasted := aux } packed = (packed & _BITMASK_AUX_COMPLEMENT) | (auxCasted << _BITPOS_AUX); _packedAddressData[owner] = packed; } // ============================================================= // IERC165 // ============================================================= /** * @dev Returns true if this contract implements the interface defined by * `interfaceId`. See the corresponding * [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified) * to learn more about how these ids are created. * * This function call must use less than 30000 gas. */ function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { // The interface IDs are constants representing the first 4 bytes // of the XOR of all function selectors in the interface. // See: [ERC165](https://eips.ethereum.org/EIPS/eip-165) // (e.g. `bytes4(i.functionA.selector ^ i.functionB.selector ^ ...)`) return interfaceId == 0x01ffc9a7 || // ERC165 interface ID for ERC165. interfaceId == 0x80ac58cd || // ERC165 interface ID for ERC721. interfaceId == 0x5b5e139f; // ERC165 interface ID for ERC721Metadata. } // ============================================================= // IERC721Metadata // ============================================================= /** * @dev Returns the token collection name. */ function name() public view virtual override returns (string memory) { return _name; } /** * @dev Returns the token collection symbol. */ function symbol() public view virtual override returns (string memory) { return _symbol; } /** * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. */ function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); string memory baseURI = _baseURI(); return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId))) : ''; } /** * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each * token will be the concatenation of the `baseURI` and the `tokenId`. Empty * by default, it can be overridden in child contracts. */ function _baseURI() internal view virtual returns (string memory) { return ''; } // ============================================================= // OWNERSHIPS OPERATIONS // ============================================================= /** * @dev Returns the owner of the `tokenId` token. * * Requirements: * * - `tokenId` must exist. */ function ownerOf(uint256 tokenId) public view virtual override returns (address) { return address(uint160(_packedOwnershipOf(tokenId))); } /** * @dev Gas spent here starts off proportional to the maximum mint batch size. * It gradually moves to O(1) as tokens get transferred around over time. */ function _ownershipOf(uint256 tokenId) internal view virtual returns (TokenOwnership memory) { return _unpackedOwnership(_packedOwnershipOf(tokenId)); } /** * @dev Returns the unpacked `TokenOwnership` struct at `index`. */ function _ownershipAt(uint256 index) internal view virtual returns (TokenOwnership memory) { return _unpackedOwnership(_packedOwnerships[index]); } /** * @dev Initializes the ownership slot minted at `index` for efficiency purposes. */ function _initializeOwnershipAt(uint256 index) internal virtual { if (_packedOwnerships[index] == 0) { _packedOwnerships[index] = _packedOwnershipOf(index); } } /** * Returns the packed ownership data of `tokenId`. */ function _packedOwnershipOf(uint256 tokenId) private view returns (uint256) { uint256 curr = tokenId; unchecked { if (_startTokenId() <= curr) if (curr < _currentIndex) { uint256 packed = _packedOwnerships[curr]; // If not burned. if (packed & _BITMASK_BURNED == 0) { // Invariant: // There will always be an initialized ownership slot // (i.e. `ownership.addr != address(0) && ownership.burned == false`) // before an unintialized ownership slot // (i.e. `ownership.addr == address(0) && ownership.burned == false`) // Hence, `curr` will not underflow. // // We can directly compare the packed value. // If the address is zero, packed will be zero. while (packed == 0) { packed = _packedOwnerships[--curr]; } return packed; } } } revert OwnerQueryForNonexistentToken(); } /** * @dev Returns the unpacked `TokenOwnership` struct from `packed`. */ function _unpackedOwnership(uint256 packed) private pure returns (TokenOwnership memory ownership) { ownership.addr = address(uint160(packed)); ownership.startTimestamp = uint64(packed >> _BITPOS_START_TIMESTAMP); ownership.burned = packed & _BITMASK_BURNED != 0; ownership.extraData = uint24(packed >> _BITPOS_EXTRA_DATA); } /** * @dev Packs ownership data into a single uint256. */ function _packOwnershipData(address owner, uint256 flags) private view returns (uint256 result) { assembly { // Mask `owner` to the lower 160 bits, in case the upper bits somehow aren't clean. owner := and(owner, _BITMASK_ADDRESS) // `owner | (block.timestamp << _BITPOS_START_TIMESTAMP) | flags`. result := or(owner, or(shl(_BITPOS_START_TIMESTAMP, timestamp()), flags)) } } /** * @dev Returns the `nextInitialized` flag set if `quantity` equals 1. */ function _nextInitializedFlag(uint256 quantity) private pure returns (uint256 result) { // For branchless setting of the `nextInitialized` flag. assembly { // `(quantity == 1) << _BITPOS_NEXT_INITIALIZED`. result := shl(_BITPOS_NEXT_INITIALIZED, eq(quantity, 1)) } } // ============================================================= // APPROVAL OPERATIONS // ============================================================= /** * @dev Gives permission to `to` to transfer `tokenId` token to another account. * The approval is cleared when the token is transferred. * * Only a single account can be approved at a time, so approving the * zero address clears previous approvals. * * Requirements: * * - The caller must own the token or be an approved operator. * - `tokenId` must exist. * * Emits an {Approval} event. */ function approve(address to, uint256 tokenId) public payable virtual override { address owner = ownerOf(tokenId); if (_msgSenderERC721A() != owner) if (!isApprovedForAll(owner, _msgSenderERC721A())) { revert ApprovalCallerNotOwnerNorApproved(); } _tokenApprovals[tokenId].value = to; emit Approval(owner, to, tokenId); } /** * @dev Returns the account approved for `tokenId` token. * * Requirements: * * - `tokenId` must exist. */ function getApproved(uint256 tokenId) public view virtual override returns (address) { if (!_exists(tokenId)) revert ApprovalQueryForNonexistentToken(); return _tokenApprovals[tokenId].value; } /** * @dev Approve or remove `operator` as an operator for the caller. * Operators can call {transferFrom} or {safeTransferFrom} * for any token owned by the caller. * * Requirements: * * - The `operator` cannot be the caller. * * Emits an {ApprovalForAll} event. */ function setApprovalForAll(address operator, bool approved) public virtual override { _operatorApprovals[_msgSenderERC721A()][operator] = approved; emit ApprovalForAll(_msgSenderERC721A(), operator, approved); } /** * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. * * See {setApprovalForAll}. */ function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { return _operatorApprovals[owner][operator]; } /** * @dev Returns whether `tokenId` exists. * * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. * * Tokens start existing when they are minted. See {_mint}. */ function _exists(uint256 tokenId) internal view virtual returns (bool) { return _startTokenId() <= tokenId && tokenId < _currentIndex && // If within bounds, _packedOwnerships[tokenId] & _BITMASK_BURNED == 0; // and not burned. } /** * @dev Returns whether `msgSender` is equal to `approvedAddress` or `owner`. */ function _isSenderApprovedOrOwner( address approvedAddress, address owner, address msgSender ) private pure returns (bool result) { assembly { // Mask `owner` to the lower 160 bits, in case the upper bits somehow aren't clean. owner := and(owner, _BITMASK_ADDRESS) // Mask `msgSender` to the lower 160 bits, in case the upper bits somehow aren't clean. msgSender := and(msgSender, _BITMASK_ADDRESS) // `msgSender == owner || msgSender == approvedAddress`. result := or(eq(msgSender, owner), eq(msgSender, approvedAddress)) } } /** * @dev Returns the storage slot and value for the approved address of `tokenId`. */ function _getApprovedSlotAndAddress(uint256 tokenId) private view returns (uint256 approvedAddressSlot, address approvedAddress) { TokenApprovalRef storage tokenApproval = _tokenApprovals[tokenId]; // The following is equivalent to `approvedAddress = _tokenApprovals[tokenId].value`. assembly { approvedAddressSlot := tokenApproval.slot approvedAddress := sload(approvedAddressSlot) } } // ============================================================= // TRANSFER OPERATIONS // ============================================================= /** * @dev Transfers `tokenId` from `from` to `to`. * * Requirements: * * - `from` cannot be the zero address. * - `to` cannot be the zero address. * - `tokenId` token must be owned by `from`. * - If the caller is not `from`, it must be approved to move this token * by either {approve} or {setApprovalForAll}. * * Emits a {Transfer} event. */ function transferFrom( address from, address to, uint256 tokenId ) public payable virtual override { uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); if (address(uint160(prevOwnershipPacked)) != from) revert TransferFromIncorrectOwner(); (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); // The nested ifs save around 20+ gas over a compound boolean condition. if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) if (!isApprovedForAll(from, _msgSenderERC721A())) revert TransferCallerNotOwnerNorApproved(); if (to == address(0)) revert TransferToZeroAddress(); _beforeTokenTransfers(from, to, tokenId, 1); // Clear approvals from the previous owner. assembly { if approvedAddress { // This is equivalent to `delete _tokenApprovals[tokenId]`. sstore(approvedAddressSlot, 0) } } // Underflow of the sender's balance is impossible because we check for // ownership above and the recipient's balance can't realistically overflow. // Counter overflow is incredibly unrealistic as `tokenId` would have to be 2**256. unchecked { // We can directly increment and decrement the balances. --_packedAddressData[from]; // Updates: `balance -= 1`. ++_packedAddressData[to]; // Updates: `balance += 1`. // Updates: // - `address` to the next owner. // - `startTimestamp` to the timestamp of transfering. // - `burned` to `false`. // - `nextInitialized` to `true`. _packedOwnerships[tokenId] = _packOwnershipData( to, _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked) ); // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { uint256 nextTokenId = tokenId + 1; // If the next slot's address is zero and not burned (i.e. packed value is zero). if (_packedOwnerships[nextTokenId] == 0) { // If the next slot is within bounds. if (nextTokenId != _currentIndex) { // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. _packedOwnerships[nextTokenId] = prevOwnershipPacked; } } } } emit Transfer(from, to, tokenId); _afterTokenTransfers(from, to, tokenId, 1); } /** * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. */ function safeTransferFrom( address from, address to, uint256 tokenId ) public payable virtual override { safeTransferFrom(from, to, tokenId, ''); } /** * @dev Safely transfers `tokenId` token from `from` to `to`. * * Requirements: * * - `from` cannot be the zero address. * - `to` cannot be the zero address. * - `tokenId` token must exist and be owned by `from`. * - If the caller is not `from`, it must be approved to move this token * by either {approve} or {setApprovalForAll}. * - If `to` refers to a smart contract, it must implement * {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. * * Emits a {Transfer} event. */ function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory _data ) public payable virtual override { transferFrom(from, to, tokenId); if (to.code.length != 0) if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { revert TransferToNonERC721ReceiverImplementer(); } } /** * @dev Hook that is called before a set of serially-ordered token IDs * are about to be transferred. This includes minting. * And also called before burning one token. * * `startTokenId` - the first token ID to be transferred. * `quantity` - the amount to be transferred. * * Calling conditions: * * - When `from` and `to` are both non-zero, `from`'s `tokenId` will be * transferred to `to`. * - When `from` is zero, `tokenId` will be minted for `to`. * - When `to` is zero, `tokenId` will be burned by `from`. * - `from` and `to` are never both zero. */ function _beforeTokenTransfers( address from, address to, uint256 startTokenId, uint256 quantity ) internal virtual {} /** * @dev Hook that is called after a set of serially-ordered token IDs * have been transferred. This includes minting. * And also called after one token has been burned. * * `startTokenId` - the first token ID to be transferred. * `quantity` - the amount to be transferred. * * Calling conditions: * * - When `from` and `to` are both non-zero, `from`'s `tokenId` has been * transferred to `to`. * - When `from` is zero, `tokenId` has been minted for `to`. * - When `to` is zero, `tokenId` has been burned by `from`. * - `from` and `to` are never both zero. */ function _afterTokenTransfers( address from, address to, uint256 startTokenId, uint256 quantity ) internal virtual {} /** * @dev Private function to invoke {IERC721Receiver-onERC721Received} on a target contract. * * `from` - Previous owner of the given token ID. * `to` - Target address that will receive the token. * `tokenId` - Token ID to be transferred. * `_data` - Optional data to send along with the call. * * Returns whether the call correctly returned the expected magic value. */ function _checkContractOnERC721Received( address from, address to, uint256 tokenId, bytes memory _data ) private returns (bool) { try ERC721A__IERC721Receiver(to).onERC721Received(_msgSenderERC721A(), from, tokenId, _data) returns ( bytes4 retval ) { return retval == ERC721A__IERC721Receiver(to).onERC721Received.selector; } catch (bytes memory reason) { if (reason.length == 0) { revert TransferToNonERC721ReceiverImplementer(); } else { assembly { revert(add(32, reason), mload(reason)) } } } } // ============================================================= // MINT OPERATIONS // ============================================================= /** * @dev Mints `quantity` tokens and transfers them to `to`. * * Requirements: * * - `to` cannot be the zero address. * - `quantity` must be greater than 0. * * Emits a {Transfer} event for each mint. */ function _mint(address to, uint256 quantity) internal virtual { uint256 startTokenId = _currentIndex; if (quantity == 0) revert MintZeroQuantity(); _beforeTokenTransfers(address(0), to, startTokenId, quantity); // Overflows are incredibly unrealistic. // `balance` and `numberMinted` have a maximum limit of 2**64. // `tokenId` has a maximum limit of 2**256. unchecked { // Updates: // - `balance += quantity`. // - `numberMinted += quantity`. // // We can directly add to the `balance` and `numberMinted`. _packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1); // Updates: // - `address` to the owner. // - `startTimestamp` to the timestamp of minting. // - `burned` to `false`. // - `nextInitialized` to `quantity == 1`. _packedOwnerships[startTokenId] = _packOwnershipData( to, _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0) ); uint256 toMasked; uint256 end = startTokenId + quantity; // Use assembly to loop and emit the `Transfer` event for gas savings. // The duplicated `log4` removes an extra check and reduces stack juggling. // The assembly, together with the surrounding Solidity code, have been // delicately arranged to nudge the compiler into producing optimized opcodes. assembly { // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean. toMasked := and(to, _BITMASK_ADDRESS) // Emit the `Transfer` event. log4( 0, // Start of data (0, since no data). 0, // End of data (0, since no data). _TRANSFER_EVENT_SIGNATURE, // Signature. 0, // `address(0)`. toMasked, // `to`. startTokenId // `tokenId`. ) // The `iszero(eq(,))` check ensures that large values of `quantity` // that overflows uint256 will make the loop run out of gas. // The compiler will optimize the `iszero` away for performance. for { let tokenId := add(startTokenId, 1) } iszero(eq(tokenId, end)) { tokenId := add(tokenId, 1) } { // Emit the `Transfer` event. Similar to above. log4(0, 0, _TRANSFER_EVENT_SIGNATURE, 0, toMasked, tokenId) } } if (toMasked == 0) revert MintToZeroAddress(); _currentIndex = end; } _afterTokenTransfers(address(0), to, startTokenId, quantity); } /** * @dev Mints `quantity` tokens and transfers them to `to`. * * This function is intended for efficient minting only during contract creation. * * It emits only one {ConsecutiveTransfer} as defined in * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309), * instead of a sequence of {Transfer} event(s). * * Calling this function outside of contract creation WILL make your contract * non-compliant with the ERC721 standard. * For full ERC721 compliance, substituting ERC721 {Transfer} event(s) with the ERC2309 * {ConsecutiveTransfer} event is only permissible during contract creation. * * Requirements: * * - `to` cannot be the zero address. * - `quantity` must be greater than 0. * * Emits a {ConsecutiveTransfer} event. */ function _mintERC2309(address to, uint256 quantity) internal virtual { uint256 startTokenId = _currentIndex; if (to == address(0)) revert MintToZeroAddress(); if (quantity == 0) revert MintZeroQuantity(); if (quantity > _MAX_MINT_ERC2309_QUANTITY_LIMIT) revert MintERC2309QuantityExceedsLimit(); _beforeTokenTransfers(address(0), to, startTokenId, quantity); // Overflows are unrealistic due to the above check for `quantity` to be below the limit. unchecked { // Updates: // - `balance += quantity`. // - `numberMinted += quantity`. // // We can directly add to the `balance` and `numberMinted`. _packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1); // Updates: // - `address` to the owner. // - `startTimestamp` to the timestamp of minting. // - `burned` to `false`. // - `nextInitialized` to `quantity == 1`. _packedOwnerships[startTokenId] = _packOwnershipData( to, _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0) ); emit ConsecutiveTransfer(startTokenId, startTokenId + quantity - 1, address(0), to); _currentIndex = startTokenId + quantity; } _afterTokenTransfers(address(0), to, startTokenId, quantity); } /** * @dev Safely mints `quantity` tokens and transfers them to `to`. * * Requirements: * * - If `to` refers to a smart contract, it must implement * {IERC721Receiver-onERC721Received}, which is called for each safe transfer. * - `quantity` must be greater than 0. * * See {_mint}. * * Emits a {Transfer} event for each mint. */ function _safeMint( address to, uint256 quantity, bytes memory _data ) internal virtual { _mint(to, quantity); unchecked { if (to.code.length != 0) { uint256 end = _currentIndex; uint256 index = end - quantity; do { if (!_checkContractOnERC721Received(address(0), to, index++, _data)) { revert TransferToNonERC721ReceiverImplementer(); } } while (index < end); // Reentrancy protection. if (_currentIndex != end) revert(); } } } /** * @dev Equivalent to `_safeMint(to, quantity, '')`. */ function _safeMint(address to, uint256 quantity) internal virtual { _safeMint(to, quantity, ''); } // ============================================================= // BURN OPERATIONS // ============================================================= /** * @dev Equivalent to `_burn(tokenId, false)`. */ function _burn(uint256 tokenId) internal virtual { _burn(tokenId, false); } /** * @dev Destroys `tokenId`. * The approval is cleared when the token is burned. * * Requirements: * * - `tokenId` must exist. * * Emits a {Transfer} event. */ function _burn(uint256 tokenId, bool approvalCheck) internal virtual { uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); address from = address(uint160(prevOwnershipPacked)); (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); if (approvalCheck) { // The nested ifs save around 20+ gas over a compound boolean condition. if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) if (!isApprovedForAll(from, _msgSenderERC721A())) revert TransferCallerNotOwnerNorApproved(); } _beforeTokenTransfers(from, address(0), tokenId, 1); // Clear approvals from the previous owner. assembly { if approvedAddress { // This is equivalent to `delete _tokenApprovals[tokenId]`. sstore(approvedAddressSlot, 0) } } // Underflow of the sender's balance is impossible because we check for // ownership above and the recipient's balance can't realistically overflow. // Counter overflow is incredibly unrealistic as `tokenId` would have to be 2**256. unchecked { // Updates: // - `balance -= 1`. // - `numberBurned += 1`. // // We can directly decrement the balance, and increment the number burned. // This is equivalent to `packed -= 1; packed += 1 << _BITPOS_NUMBER_BURNED;`. _packedAddressData[from] += (1 << _BITPOS_NUMBER_BURNED) - 1; // Updates: // - `address` to the last owner. // - `startTimestamp` to the timestamp of burning. // - `burned` to `true`. // - `nextInitialized` to `true`. _packedOwnerships[tokenId] = _packOwnershipData( from, (_BITMASK_BURNED | _BITMASK_NEXT_INITIALIZED) | _nextExtraData(from, address(0), prevOwnershipPacked) ); // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { uint256 nextTokenId = tokenId + 1; // If the next slot's address is zero and not burned (i.e. packed value is zero). if (_packedOwnerships[nextTokenId] == 0) { // If the next slot is within bounds. if (nextTokenId != _currentIndex) { // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. _packedOwnerships[nextTokenId] = prevOwnershipPacked; } } } } emit Transfer(from, address(0), tokenId); _afterTokenTransfers(from, address(0), tokenId, 1); // Overflow not possible, as _burnCounter cannot be exceed _currentIndex times. unchecked { _burnCounter++; } } // ============================================================= // EXTRA DATA OPERATIONS // ============================================================= /** * @dev Directly sets the extra data for the ownership data `index`. */ function _setExtraDataAt(uint256 index, uint24 extraData) internal virtual { uint256 packed = _packedOwnerships[index]; if (packed == 0) revert OwnershipNotInitializedForExtraData(); uint256 extraDataCasted; // Cast `extraData` with assembly to avoid redundant masking. assembly { extraDataCasted := extraData } packed = (packed & _BITMASK_EXTRA_DATA_COMPLEMENT) | (extraDataCasted << _BITPOS_EXTRA_DATA); _packedOwnerships[index] = packed; } /** * @dev Called during each token transfer to set the 24bit `extraData` field. * Intended to be overridden by the cosumer contract. * * `previousExtraData` - the value of `extraData` before transfer. * * Calling conditions: * * - When `from` and `to` are both non-zero, `from`'s `tokenId` will be * transferred to `to`. * - When `from` is zero, `tokenId` will be minted for `to`. * - When `to` is zero, `tokenId` will be burned by `from`. * - `from` and `to` are never both zero. */ function _extraData( address from, address to, uint24 previousExtraData ) internal view virtual returns (uint24) {} /** * @dev Returns the next extra data for the packed ownership data. * The returned result is shifted into position. */ function _nextExtraData( address from, address to, uint256 prevOwnershipPacked ) private view returns (uint256) { uint24 extraData = uint24(prevOwnershipPacked >> _BITPOS_EXTRA_DATA); return uint256(_extraData(from, to, extraData)) << _BITPOS_EXTRA_DATA; } // ============================================================= // OTHER OPERATIONS // ============================================================= /** * @dev Returns the message sender (defaults to `msg.sender`). * * If you are writing GSN compatible contracts, you need to override this function. */ function _msgSenderERC721A() internal view virtual returns (address) { return msg.sender; } /** * @dev Converts a uint256 to its ASCII string decimal representation. */ function _toString(uint256 value) internal pure virtual returns (string memory str) { assembly { // The maximum value of a uint256 contains 78 digits (1 byte per digit), but // we allocate 0xa0 bytes to keep the free memory pointer 32-byte word aligned. // We will need 1 word for the trailing zeros padding, 1 word for the length, // and 3 words for a maximum of 78 digits. Total: 5 * 0x20 = 0xa0. let m := add(mload(0x40), 0xa0) // Update the free memory pointer to allocate. mstore(0x40, m) // Assign the `str` to the end. str := sub(m, 0x20) // Zeroize the slot after the string. mstore(str, 0) // Cache the end of the memory to calculate the length later. let end := str // We write the string from rightmost digit to leftmost digit. // The following is essentially a do-while loop that also handles the zero case. // prettier-ignore for { let temp := value } 1 {} { str := sub(str, 1) // Write the character to the pointer. // The ASCII index of the '0' character is 48. mstore8(str, add(48, mod(temp, 10))) // Keep dividing `temp` until zero. temp := div(temp, 10) // prettier-ignore if iszero(temp) { break } } let length := sub(end, str) // Move the pointer 32 bytes leftwards to make room for the length. str := sub(str, 0x20) // Store the length. mstore(str, length) } } } // lib/ERC721A/contracts/interfaces/IERC721A.sol // ERC721A Contracts v4.2.3 // Creator: Chiru Labs // lib/ERC721A/contracts/extensions/IERC721AQueryable.sol // ERC721A Contracts v4.2.3 // Creator: Chiru Labs /** * @dev Interface of ERC721AQueryable. */ interface IERC721AQueryable is IERC721A { /** * Invalid query range (`start` >= `stop`). */ error InvalidQueryRange(); /** * @dev Returns the `TokenOwnership` struct at `tokenId` without reverting. * * If the `tokenId` is out of bounds: * * - `addr = address(0)` * - `startTimestamp = 0` * - `burned = false` * - `extraData = 0` * * If the `tokenId` is burned: * * - `addr =
` * - `startTimestamp = ` * - `burned = true` * - `extraData = ` * * Otherwise: * * - `addr =
` * - `startTimestamp = ` * - `burned = false` * - `extraData = ` */ function explicitOwnershipOf(uint256 tokenId) external view returns (TokenOwnership memory); /** * @dev Returns an array of `TokenOwnership` structs at `tokenIds` in order. * See {ERC721AQueryable-explicitOwnershipOf} */ function explicitOwnershipsOf(uint256[] memory tokenIds) external view returns (TokenOwnership[] memory); /** * @dev Returns an array of token IDs owned by `owner`, * in the range [`start`, `stop`) * (i.e. `start <= tokenId < stop`). * * This function allows for tokens to be queried if the collection * grows too big for a single call of {ERC721AQueryable-tokensOfOwner}. * * Requirements: * * - `start < stop` */ function tokensOfOwnerIn( address owner, uint256 start, uint256 stop ) external view returns (uint256[] memory); /** * @dev Returns an array of token IDs owned by `owner`. * * This function scans the ownership mapping and is O(`totalSupply`) in complexity. * It is meant to be called off-chain. * * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into * multiple smaller scans if the collection is large enough to cause * an out-of-gas error (10K collections should be fine). */ function tokensOfOwner(address owner) external view returns (uint256[] memory); } // lib/openzeppelin-contracts/contracts/access/Ownable.sol // OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol) /** * @dev Contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * By default, the owner account will be the one that deploys the contract. This * can later be changed with {transferOwnership}. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be applied to your functions to restrict their use to * the owner. */ abstract contract Ownable is Context { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev Initializes the contract setting the deployer as the initial owner. */ constructor() { _transferOwnership(_msgSender()); } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { _checkOwner(); _; } /** * @dev Returns the address of the current owner. */ function owner() public view virtual returns (address) { return _owner; } /** * @dev Throws if the sender is not the owner. */ function _checkOwner() internal view virtual { require(owner() == _msgSender(), "Ownable: caller is not the owner"); } /** * @dev Leaves the contract without owner. It will not be possible to call * `onlyOwner` functions. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, * thereby disabling any functionality that is only available to the owner. */ function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), "Ownable: new owner is the zero address"); _transferOwnership(newOwner); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Internal function without access restriction. */ function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } } // lib/openzeppelin-contracts/contracts/security/Pausable.sol // OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) /** * @dev Contract module which allows children to implement an emergency stop * mechanism that can be triggered by an authorized account. * * This module is used through inheritance. It will make available the * modifiers `whenNotPaused` and `whenPaused`, which can be applied to * the functions of your contract. Note that they will not be pausable by * simply including this module, only once the modifiers are put in place. */ abstract contract Pausable is Context { /** * @dev Emitted when the pause is triggered by `account`. */ event Paused(address account); /** * @dev Emitted when the pause is lifted by `account`. */ event Unpaused(address account); bool private _paused; /** * @dev Initializes the contract in unpaused state. */ constructor() { _paused = false; } /** * @dev Modifier to make a function callable only when the contract is not paused. * * Requirements: * * - The contract must not be paused. */ modifier whenNotPaused() { _requireNotPaused(); _; } /** * @dev Modifier to make a function callable only when the contract is paused. * * Requirements: * * - The contract must be paused. */ modifier whenPaused() { _requirePaused(); _; } /** * @dev Returns true if the contract is paused, and false otherwise. */ function paused() public view virtual returns (bool) { return _paused; } /** * @dev Throws if the contract is paused. */ function _requireNotPaused() internal view virtual { require(!paused(), "Pausable: paused"); } /** * @dev Throws if the contract is not paused. */ function _requirePaused() internal view virtual { require(paused(), "Pausable: not paused"); } /** * @dev Triggers stopped state. * * Requirements: * * - The contract must not be paused. */ function _pause() internal virtual whenNotPaused { _paused = true; emit Paused(_msgSender()); } /** * @dev Returns to normal state. * * Requirements: * * - The contract must be paused. */ function _unpause() internal virtual whenPaused { _paused = false; emit Unpaused(_msgSender()); } } // src/lib/ENSReverseClaimer.sol interface ENS { /** * @dev Returns the address that owns the specified node. * @param node The specified node. * @return address of the owner. */ function owner(bytes32 node) external view returns (address); } interface IReverseRegistrar { /** * @dev Transfers ownership of the reverse ENS record associated with the * calling account. * @param owner The address to set as the owner of the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function claim(address owner) external returns (bytes32); /** * @dev Sets the `name()` record for the reverse ENS record associated with * the calling account. First updates the resolver to the default reverse * resolver if necessary. * @param name The name to set for this address. * @return The ENS node hash of the reverse record. */ function setName(string memory name) external returns (bytes32); } /** * @title ENSReverseClaimer * @dev This contract is used to claim reverse ENS records. */ abstract contract ENSReverseClaimer is Ownable { /// @dev The namehash of 'addr.reverse', the domain at which reverse records /// are stored in ENS. bytes32 constant ADDR_REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2; /** * @dev Transfers ownership of the reverse ENS record associated with the * contract. * @param ens The ENS registry. * @param claimant The address to set as the owner of the reverse record in * ENS. * @return The ENS node hash of the reverse record. */ function claimReverseENS(ENS ens, address claimant) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).claim(claimant); } /** * @dev Sets the reverse ENS record associated with the contract. * @param ens The ENS registry. * @param name The name to set as the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function setReverseENS(ENS ens, string calldata name) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).setName(name); } } // lib/ERC721A/contracts/extensions/ERC721AQueryable.sol // ERC721A Contracts v4.2.3 // Creator: Chiru Labs /** * @title ERC721AQueryable. * * @dev ERC721A subclass with convenience query functions. */ abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable { /** * @dev Returns the `TokenOwnership` struct at `tokenId` without reverting. * * If the `tokenId` is out of bounds: * * - `addr = address(0)` * - `startTimestamp = 0` * - `burned = false` * - `extraData = 0` * * If the `tokenId` is burned: * * - `addr =
` * - `startTimestamp = ` * - `burned = true` * - `extraData = ` * * Otherwise: * * - `addr =
` * - `startTimestamp = ` * - `burned = false` * - `extraData = ` */ function explicitOwnershipOf(uint256 tokenId) public view virtual override returns (TokenOwnership memory) { TokenOwnership memory ownership; if (tokenId < _startTokenId() || tokenId >= _nextTokenId()) { return ownership; } ownership = _ownershipAt(tokenId); if (ownership.burned) { return ownership; } return _ownershipOf(tokenId); } /** * @dev Returns an array of `TokenOwnership` structs at `tokenIds` in order. * See {ERC721AQueryable-explicitOwnershipOf} */ function explicitOwnershipsOf(uint256[] calldata tokenIds) external view virtual override returns (TokenOwnership[] memory) { unchecked { uint256 tokenIdsLength = tokenIds.length; TokenOwnership[] memory ownerships = new TokenOwnership[](tokenIdsLength); for (uint256 i; i != tokenIdsLength; ++i) { ownerships[i] = explicitOwnershipOf(tokenIds[i]); } return ownerships; } } /** * @dev Returns an array of token IDs owned by `owner`, * in the range [`start`, `stop`) * (i.e. `start <= tokenId < stop`). * * This function allows for tokens to be queried if the collection * grows too big for a single call of {ERC721AQueryable-tokensOfOwner}. * * Requirements: * * - `start < stop` */ function tokensOfOwnerIn( address owner, uint256 start, uint256 stop ) external view virtual override returns (uint256[] memory) { unchecked { if (start >= stop) revert InvalidQueryRange(); uint256 tokenIdsIdx; uint256 stopLimit = _nextTokenId(); // Set `start = max(start, _startTokenId())`. if (start < _startTokenId()) { start = _startTokenId(); } // Set `stop = min(stop, stopLimit)`. if (stop > stopLimit) { stop = stopLimit; } uint256 tokenIdsMaxLength = balanceOf(owner); // Set `tokenIdsMaxLength = min(balanceOf(owner), stop - start)`, // to cater for cases where `balanceOf(owner)` is too big. if (start < stop) { uint256 rangeLength = stop - start; if (rangeLength < tokenIdsMaxLength) { tokenIdsMaxLength = rangeLength; } } else { tokenIdsMaxLength = 0; } uint256[] memory tokenIds = new uint256[](tokenIdsMaxLength); if (tokenIdsMaxLength == 0) { return tokenIds; } // We need to call `explicitOwnershipOf(start)`, // because the slot at `start` may not be initialized. TokenOwnership memory ownership = explicitOwnershipOf(start); address currOwnershipAddr; // If the starting slot exists (i.e. not burned), initialize `currOwnershipAddr`. // `ownership.address` will not be zero, as `start` is clamped to the valid token ID range. if (!ownership.burned) { currOwnershipAddr = ownership.addr; } for (uint256 i = start; i != stop && tokenIdsIdx != tokenIdsMaxLength; ++i) { ownership = _ownershipAt(i); if (ownership.burned) { continue; } if (ownership.addr != address(0)) { currOwnershipAddr = ownership.addr; } if (currOwnershipAddr == owner) { tokenIds[tokenIdsIdx++] = i; } } // Downsize the array to fit. assembly { mstore(tokenIds, tokenIdsIdx) } return tokenIds; } } /** * @dev Returns an array of token IDs owned by `owner`. * * This function scans the ownership mapping and is O(`totalSupply`) in complexity. * It is meant to be called off-chain. * * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into * multiple smaller scans if the collection is large enough to cause * an out-of-gas error (10K collections should be fine). */ function tokensOfOwner(address owner) external view virtual override returns (uint256[] memory) { unchecked { uint256 tokenIdsIdx; address currOwnershipAddr; uint256 tokenIdsLength = balanceOf(owner); uint256[] memory tokenIds = new uint256[](tokenIdsLength); TokenOwnership memory ownership; for (uint256 i = _startTokenId(); tokenIdsIdx != tokenIdsLength; ++i) { ownership = _ownershipAt(i); if (ownership.burned) { continue; } if (ownership.addr != address(0)) { currOwnershipAddr = ownership.addr; } if (currOwnershipAddr == owner) { tokenIds[tokenIdsIdx++] = i; } } return tokenIds; } } } // src/EFPListRegistry.sol /** * @title EFPListRegistry * @author Cory Gabrielsen (cory.eth) * @custom:contributor throw; (0xthrpw.eth) * @custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM * * @notice The EPF List Registry is an ERC721A contract representing ownership * of an EFP List. EFP List NFT owners may set the List Storage Location * associated with their EFP List by calling setListStorageLocation. */ contract EFPListRegistry is IEFPListRegistry, ERC721A, ERC721AQueryable, ENSReverseClaimer, Pausable { /////////////////////////////////////////////////////////////////////////// // Events /////////////////////////////////////////////////////////////////////////// /// @notice Emitted when the mint batch size is changed. event MaxMintBatchSizeChange(uint256 maxMintBatchSize); /// @notice Emitted when the mint state is changed. event MintStateChange(MintState mintState); /// @notice Emitted when the price oracle is changed. event PriceOracleChange(address priceOracle); /// @notice Emitted when the token URI provider is changed. event TokenURIProviderChange(address tokenURIProvider); /////////////////////////////////////////////////////////////////////////// // Data Structures /////////////////////////////////////////////////////////////////////////// /// @notice The state of minting. MintState private mintState = MintState.Disabled; /// @notice The maximum number of tokens that can be minted in a single batch. uint256 private maxMintBatchSize = 10000; /// @notice The price oracle. If set, the price oracle is used to determine /// the price of minting. IEFPListNFTPriceOracle private priceOracle; /// @notice The token URI provider. ITokenURIProvider public tokenURIProvider; /// @notice The list storage location associated with a token. mapping(uint256 => bytes) private tokenIdToListStorageLocation; /////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////// /// @notice Constructs a new ListRegistry and sets its name and symbol. constructor() ERC721A('EFP', 'EFP') {} ///////////////////////////////////////////////////////////////////////////// // Pausable ///////////////////////////////////////////////////////////////////////////// /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } /////////////////////////////////////////////////////////////////////////// // token uri provider getter/setter /////////////////////////////////////////////////////////////////////////// /** * @notice Sets the token URI provider. * @param tokenURIProvider_ The new token URI provider. */ function setTokenURIProvider(address tokenURIProvider_) external onlyOwner { tokenURIProvider = ITokenURIProvider(tokenURIProvider_); emit TokenURIProviderChange(tokenURIProvider_); } /** * @dev Overrides the tokenURI function to delegate the call to the * TokenURIProvider contract. This allows the tokenURI logic to be * upgradeable. * @param tokenId The token ID for which the URI is requested. * @return A string representing the token URI. */ function tokenURI(uint256 tokenId) public view override(IERC721A, ERC721A) returns (string memory) { require(address(tokenURIProvider) != address(0), 'TokenURI provider is not set'); return tokenURIProvider.tokenURI(tokenId); } /////////////////////////////////////////////////////////////////////////// // price oracle getter/setter /////////////////////////////////////////////////////////////////////////// /// @notice Fetches the price oracle. function getPriceOracle() external view returns (address) { return address(priceOracle); } /** * @notice Sets the price oracle. * @param priceOracle_ The new price oracle. */ function setPriceOracle(address priceOracle_) external whenNotPaused onlyOwner { priceOracle = IEFPListNFTPriceOracle(priceOracle_); emit PriceOracleChange(priceOracle_); } /** * @notice Fetches the price of minting a token. */ function _getPrice(uint256 quantity) internal view returns (uint256) { return (address(priceOracle) != address(0)) ? quantity == 1 ? priceOracle.getPrice() : priceOracle.getBatchPrice(quantity) : 0; } /** * @notice Withdraws Ether from the contract. * * @param recipient The address to send the Ether to. * @param amount The amount of Ether to send. * @return Whether the transfer succeeded. */ function withdraw(address payable recipient, uint256 amount) public onlyOwner returns (bool) { require(amount <= address(this).balance, 'Insufficient balance'); (bool sent,) = recipient.call{value: amount}(''); require(sent, 'Failed to send Ether'); return sent; } /////////////////////////////////////////////////////////////////////////// // Modifiers /////////////////////////////////////////////////////////////////////////// /// @notice Restrict access to the owner of a specific token. modifier onlyTokenOwner(uint256 tokenId) { require(ownerOf(tokenId) == msg.sender, 'EFP: caller is not the owner'); _; } /// @notice Restrict mint if minting is disabled OR restricted to owner && caller is not owner. modifier mintAllowed() { require(mintState != MintState.Disabled, 'EFP: minting is disabled'); require(mintState != MintState.OwnerOnly || msg.sender == owner(), 'EFP: minting is restricted to owner'); // else PublicMint allowed // else PublicBatch allowed _; } /// @notice Restrict mint if minting is disabled OR restricted to owner && caller is not owner OR restricted to public single modifier mintBatchAllowed() { require(mintState != MintState.Disabled, 'EFP: minting is disabled'); require(mintState != MintState.OwnerOnly || msg.sender == owner(), 'EFP: minting is restricted to owner'); require(mintState != MintState.PublicMint || msg.sender == owner(), 'EFP: batch minting is restricted to owner'); // else PublicBatch allowed _; } /////////////////////////////////////////////////////////////////////////// // ListStorageLocation /////////////////////////////////////////////////////////////////////////// /** * @notice Fetches the list location associated with a specific token. * @param tokenId The ID of the token. * @return The list location. */ function getListStorageLocation(uint256 tokenId) external view override returns (bytes memory) { return tokenIdToListStorageLocation[tokenId]; } /** * @notice Associates a token with a list storage location. * @param tokenId The ID of the token. * @param listStorageLocation The list storage location to be associated with the token. */ function setListStorageLocation(uint256 tokenId, bytes calldata listStorageLocation) external override whenNotPaused onlyTokenOwner(tokenId) { _setListStorageLocation(tokenId, listStorageLocation); } /** * @notice Associates a token with a list storage location. * @param tokenId The ID of the token. * @param listStorageLocation The list storage location to be associated with the token. */ function _setListStorageLocation(uint256 tokenId, bytes calldata listStorageLocation) internal { tokenIdToListStorageLocation[tokenId] = listStorageLocation; emit UpdateListStorageLocation(tokenId, listStorageLocation); } /////////////////////////////////////////////////////////////////////////// // Mint /////////////////////////////////////////////////////////////////////////// /// @notice Fetches the mint state. function getMintState() external view returns (MintState) { return mintState; } /// @notice Sets the mint state. /// @param _mintState The new mint state. function setMintState(MintState _mintState) external whenNotPaused onlyOwner { mintState = _mintState; emit MintStateChange(_mintState); } /// @notice Fetches the max mint batch size. function getMaxMintBatchSize() external view returns (uint256) { return maxMintBatchSize; } /// @notice Sets the max mint batch size. /// @param _maxMintBatchSize The new max mint batch size. function setMaxMintBatchSize(uint256 _maxMintBatchSize) external whenNotPaused onlyOwner { maxMintBatchSize = _maxMintBatchSize; emit MaxMintBatchSizeChange(_maxMintBatchSize); } /** * @notice Mints a new token. * @param listStorageLocation The list storage location to be associated with the token. */ function mint(bytes calldata listStorageLocation) external payable whenNotPaused mintAllowed { uint256 tokenId = totalSupply(); uint256 price = _getPrice(1); require(msg.value >= price, 'insufficient funds'); _safeMint(msg.sender, 1); _setListStorageLocation(tokenId, listStorageLocation); } /** * @notice Mints a new token to the given address. * @param recipient The address to mint the token to. * @param listStorageLocation The list storage location to be associated with the token. */ function mintTo(address recipient, bytes calldata listStorageLocation) external payable whenNotPaused mintAllowed { uint256 tokenId = totalSupply(); uint256 price = _getPrice(1); require(msg.value >= price, 'insufficient funds'); _safeMint(recipient, 1); _setListStorageLocation(tokenId, listStorageLocation); } /// @notice Mints a batch of new tokens. /// @param quantity The number of tokens to mint. function mintBatch(uint256 quantity) external payable whenNotPaused mintBatchAllowed { require(quantity <= maxMintBatchSize, 'batch size too big'); uint256 price = _getPrice(quantity); require(msg.value >= price, 'insufficient funds'); _safeMint(msg.sender, quantity); // leave tokenIdToListStorageLocation unset for these tokens } /// @notice Mints a batch of new tokens. /// @param recipient The address to mint the tokens to. /// @param quantity The number of tokens to mint. function mintBatchTo(address recipient, uint256 quantity) external payable whenNotPaused mintBatchAllowed { require(quantity <= maxMintBatchSize, 'batch size too big'); uint256 price = _getPrice(quantity); require(msg.value >= price, 'insufficient funds'); _safeMint(recipient, quantity); // leave tokenIdToListStorageLocation unset for these tokens } } ``` ## EFPListMinter.sol ```solc // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0 ^0.8.23; // lib/openzeppelin-contracts/contracts/utils/Context.sol // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) /** * @dev Provides information about the current execution context, including the * sender of the transaction and its data. While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct * manner, since when dealing with meta-transactions the account sending and * paying for execution may not be the actual sender (as far as an application * is concerned). * * This contract is only required for intermediate, library-like contracts. */ abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } } // src/interfaces/IEFPAccountMetadata.sol /** * @title IEFPAccountMetadata */ interface IEFPAccountMetadata { function addProxy(address proxy) external; function removeProxy(address proxy) external; function isProxy(address proxy) external view returns (bool); event UpdateAccountMetadata(address indexed addr, string key, bytes value); /** * @title Key-value Record * @notice A key-value string pair. */ struct KeyValue { string key; bytes value; } function getValue(address addr, string calldata key) external view returns (bytes memory); function setValue(string calldata key, bytes calldata value) external; function setValueForAddress(address addr, string calldata key, bytes calldata value) external; function setValues(KeyValue[] calldata records) external; function setValuesForAddress(address addr, KeyValue[] calldata records) external; } // src/interfaces/IEFPListRecords.sol /** * @title IEFPListMetadata */ interface IEFPListMetadata { event UpdateListMetadata(uint256 indexed slot, string key, bytes value); struct KeyValue { string key; bytes value; } function getMetadataValue(uint256 slot, string calldata key) external view returns (bytes memory); function getMetadataValues(uint256 slot, string[] calldata keys) external view returns (bytes[] memory); function setMetadataValue(uint256 slot, string calldata key, bytes calldata value) external; function setMetadataValues(uint256 slot, KeyValue[] calldata records) external; // List Manager Functions function claimListManager(uint256 slot) external; function claimListManagerForAddress(uint256 slot, address manager) external; function getListManager(uint256 slot) external view returns (address); function setListManager(uint256 slot, address manager) external; // List User Functions function getListUser(uint256 slot) external view returns (address); function setListUser(uint256 slot, address user) external; } /** * @title IEFPListRecords * @notice Interface for the ListRecords contract. */ interface IEFPListRecords is IEFPListMetadata { // Events event ListOp(uint256 indexed slot, bytes op); // List Operation Functions - Read function getListOpCount(uint256 slot) external view returns (uint256); function getListOp(uint256 slot, uint256 index) external view returns (bytes memory); function getListOpsInRange(uint256 slot, uint256 start, uint256 end) external view returns (bytes[] memory); function getAllListOps(uint256 slot) external view returns (bytes[] memory); // List Operation Functions - Write function applyListOp(uint256 slot, bytes calldata op) external; function applyListOps(uint256 slot, bytes[] calldata ops) external; function setMetadataValuesAndApplyListOps(uint256 slot, KeyValue[] calldata records, bytes[] calldata ops) external; } // src/interfaces/IEFPListRegistry.sol /** * @title EFPListRegistry * @notice A registry connecting token IDs with data such as managers, users, and list locations. */ interface IEFPListRegistry { /////////////////////////////////////////////////////////////////////////// // Enums /////////////////////////////////////////////////////////////////////////// enum MintState { Disabled, OwnerOnly, PublicMint, PublicBatch } /////////////////////////////////////////////////////////////////////////// // Events /////////////////////////////////////////////////////////////////////////// /// @notice Emitted when a list storage location is set event UpdateListStorageLocation(uint256 indexed tokenId, bytes listStorageLocation); /////////////////////////////////////////////////////////////////////////// // ListStorageLocation /////////////////////////////////////////////////////////////////////////// function getListStorageLocation(uint256 tokenId) external view returns (bytes memory); function setListStorageLocation(uint256 tokenId, bytes calldata listStorageLocation) external; /////////////////////////////////////////////////////////////////////////// // Mint /////////////////////////////////////////////////////////////////////////// /// @notice Fetches the mint state. function getMintState() external view returns (MintState); /// @notice Sets the mint state. /// @param _mintState The new mint state. function setMintState(MintState _mintState) external; /// @notice Fetches the max mint batch size. function getMaxMintBatchSize() external view returns (uint256); /// @notice Sets the max mint batch size. /// @param _maxMintBatchSize The new max mint batch size. function setMaxMintBatchSize(uint256 _maxMintBatchSize) external; /// @notice Mints a new token. function mint(bytes calldata listStorageLocation) external payable; /** * @notice Mints a new token to the given address. * @param recipient The address to mint the token to. */ function mintTo(address recipient, bytes calldata listStorageLocation) external payable; /// @notice Mints a new token to the given address. function mintBatch(uint256 quantity) external payable; /// @notice Mints a new token to the given address. function mintBatchTo(address recipient, uint256 quantity) external payable; } // lib/openzeppelin-contracts/contracts/access/Ownable.sol // OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol) /** * @dev Contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * By default, the owner account will be the one that deploys the contract. This * can later be changed with {transferOwnership}. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be applied to your functions to restrict their use to * the owner. */ abstract contract Ownable is Context { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev Initializes the contract setting the deployer as the initial owner. */ constructor() { _transferOwnership(_msgSender()); } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { _checkOwner(); _; } /** * @dev Returns the address of the current owner. */ function owner() public view virtual returns (address) { return _owner; } /** * @dev Throws if the sender is not the owner. */ function _checkOwner() internal view virtual { require(owner() == _msgSender(), "Ownable: caller is not the owner"); } /** * @dev Leaves the contract without owner. It will not be possible to call * `onlyOwner` functions. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, * thereby disabling any functionality that is only available to the owner. */ function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), "Ownable: new owner is the zero address"); _transferOwnership(newOwner); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Internal function without access restriction. */ function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } } // lib/openzeppelin-contracts/contracts/security/Pausable.sol // OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) /** * @dev Contract module which allows children to implement an emergency stop * mechanism that can be triggered by an authorized account. * * This module is used through inheritance. It will make available the * modifiers `whenNotPaused` and `whenPaused`, which can be applied to * the functions of your contract. Note that they will not be pausable by * simply including this module, only once the modifiers are put in place. */ abstract contract Pausable is Context { /** * @dev Emitted when the pause is triggered by `account`. */ event Paused(address account); /** * @dev Emitted when the pause is lifted by `account`. */ event Unpaused(address account); bool private _paused; /** * @dev Initializes the contract in unpaused state. */ constructor() { _paused = false; } /** * @dev Modifier to make a function callable only when the contract is not paused. * * Requirements: * * - The contract must not be paused. */ modifier whenNotPaused() { _requireNotPaused(); _; } /** * @dev Modifier to make a function callable only when the contract is paused. * * Requirements: * * - The contract must be paused. */ modifier whenPaused() { _requirePaused(); _; } /** * @dev Returns true if the contract is paused, and false otherwise. */ function paused() public view virtual returns (bool) { return _paused; } /** * @dev Throws if the contract is paused. */ function _requireNotPaused() internal view virtual { require(!paused(), "Pausable: paused"); } /** * @dev Throws if the contract is not paused. */ function _requirePaused() internal view virtual { require(paused(), "Pausable: not paused"); } /** * @dev Triggers stopped state. * * Requirements: * * - The contract must not be paused. */ function _pause() internal virtual whenNotPaused { _paused = true; emit Paused(_msgSender()); } /** * @dev Returns to normal state. * * Requirements: * * - The contract must be paused. */ function _unpause() internal virtual whenPaused { _paused = false; emit Unpaused(_msgSender()); } } // src/lib/ENSReverseClaimer.sol interface ENS { /** * @dev Returns the address that owns the specified node. * @param node The specified node. * @return address of the owner. */ function owner(bytes32 node) external view returns (address); } interface IReverseRegistrar { /** * @dev Transfers ownership of the reverse ENS record associated with the * calling account. * @param owner The address to set as the owner of the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function claim(address owner) external returns (bytes32); /** * @dev Sets the `name()` record for the reverse ENS record associated with * the calling account. First updates the resolver to the default reverse * resolver if necessary. * @param name The name to set for this address. * @return The ENS node hash of the reverse record. */ function setName(string memory name) external returns (bytes32); } /** * @title ENSReverseClaimer * @dev This contract is used to claim reverse ENS records. */ abstract contract ENSReverseClaimer is Ownable { /// @dev The namehash of 'addr.reverse', the domain at which reverse records /// are stored in ENS. bytes32 constant ADDR_REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2; /** * @dev Transfers ownership of the reverse ENS record associated with the * contract. * @param ens The ENS registry. * @param claimant The address to set as the owner of the reverse record in * ENS. * @return The ENS node hash of the reverse record. */ function claimReverseENS(ENS ens, address claimant) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).claim(claimant); } /** * @dev Sets the reverse ENS record associated with the contract. * @param ens The ENS registry. * @param name The name to set as the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function setReverseENS(ENS ens, string calldata name) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).setName(name); } } // src/EFPListMinter.sol interface IEFPListRegistry_ERC721 is IEFPListRegistry { function ownerOf(uint256 tokenId) external view returns (address); function totalSupply() external view returns (uint256); } /** * @title EFPListMetadata * @author Cory Gabrielsen (cory.eth) * @custom:contributor throw; (0xthrpw.eth) * @custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM * * @notice This contract mints and assigns primary lists to users, and sets * EFP List metadata. */ contract EFPListMinter is ENSReverseClaimer, Pausable { IEFPListRegistry_ERC721 public registry; IEFPAccountMetadata public accountMetadata; IEFPListRecords public listRecordsL1; constructor(address _registryAddress, address _accountMetadataAddress, address _listRecordsL1) { registry = IEFPListRegistry_ERC721(_registryAddress); accountMetadata = IEFPAccountMetadata(_accountMetadataAddress); listRecordsL1 = IEFPListRecords(_listRecordsL1); } ///////////////////////////////////////////////////////////////////////////// // Pausable ///////////////////////////////////////////////////////////////////////////// /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } ///////////////////////////////////////////////////////////////////////////// // minting ///////////////////////////////////////////////////////////////////////////// /** * @dev Decode a list storage location with no metadata. * @param listStorageLocation The storage location of the list. * @return slot The slot of the list. * @return contractAddress The contract address of the list. */ function decodeL1ListStorageLocation(bytes calldata listStorageLocation) internal pure returns (uint256, address) { // the list storage location is // - version (1 byte) // - list storate location type (1 byte) // - chain id (32 bytes) // - contract address (20 bytes) // - slot (32 bytes) require(listStorageLocation.length == 1 + 1 + 32 + 20 + 32, 'EFPListMinter: invalid list storage location'); require(listStorageLocation[0] == 0x01, 'EFPListMinter: invalid list storage location version'); require(listStorageLocation[1] == 0x01, 'EFPListMinter: invalid list storage location type'); address contractAddress = _bytesToAddress(listStorageLocation, 34); uint256 slot = _bytesToUint(listStorageLocation, 54); return (slot, contractAddress); } /** * @dev Mint a primary list. * @param listStorageLocation The storage location of the list. */ function easyMint(bytes calldata listStorageLocation) public payable whenNotPaused { // validate the list storage location (uint256 slot, address recordsContract) = decodeL1ListStorageLocation(listStorageLocation); uint256 tokenId = registry.totalSupply(); registry.mintTo{value: msg.value}(msg.sender, listStorageLocation); _setDefaultListForAccount(msg.sender, tokenId); if (recordsContract == address(listRecordsL1)) { listRecordsL1.setListUser(slot, msg.sender); listRecordsL1.setListManager(slot, msg.sender); } } /** * @dev Mint a primary list to a specific address. * @param to The address to mint the list to. * @param listStorageLocation The storage location of the list. */ function easyMintTo(address to, bytes calldata listStorageLocation) public payable whenNotPaused { // validate the list storage location (uint256 slot, address recordsContract) = decodeL1ListStorageLocation(listStorageLocation); uint256 tokenId = registry.totalSupply(); registry.mintTo{value: msg.value}(to, listStorageLocation); _setDefaultListForAccount(msg.sender, tokenId); if (recordsContract == address(listRecordsL1)) { listRecordsL1.setListUser(slot, msg.sender); listRecordsL1.setListManager(slot, msg.sender); } } /** * @dev Mint a primary list without metadata. * @param listStorageLocation The storage location of the list. */ function mintPrimaryListNoMeta(bytes calldata listStorageLocation) public payable whenNotPaused { // validate the list storage location decodeL1ListStorageLocation(listStorageLocation); uint256 tokenId = registry.totalSupply(); _setDefaultListForAccount(msg.sender, tokenId); registry.mintTo{value: msg.value}(msg.sender, listStorageLocation); } /** * @dev Mint a primary list without metadata to a specific address. * @param listStorageLocation The storage location of the list. */ function mintNoMeta(bytes calldata listStorageLocation) public payable whenNotPaused { // validate the list storage location decodeL1ListStorageLocation(listStorageLocation); registry.mintTo{value: msg.value}(msg.sender, listStorageLocation); } /** * @dev Mint a primary list without metadata to a specific address. * @param to The address to mint the list to. * @param listStorageLocation The storage location of the list. */ function mintToNoMeta(address to, bytes calldata listStorageLocation) public payable whenNotPaused { // validate the list storage location decodeL1ListStorageLocation(listStorageLocation); registry.mintTo{value: msg.value}(to, listStorageLocation); } /** * @dev Set the default list for an account. * @param to The address to set the default list for. * @param tokenId The token ID of the list. */ function _setDefaultListForAccount(address to, uint256 tokenId) internal { accountMetadata.setValueForAddress(to, 'primary-list', abi.encodePacked(tokenId)); } function _getChainId() internal view returns (uint256) { uint256 id; assembly { id := chainid() } return id; } // Generalized function to convert bytes to uint256 with a given offset function _bytesToUint(bytes memory data, uint256 offset) internal pure returns (uint256) { require(data.length >= offset + 32, 'Data too short'); uint256 value; assembly { value := mload(add(data, add(32, offset))) } return value; } // Helper function to convert bytes to address with a given offset function _bytesToAddress(bytes memory data, uint256 offset) internal pure returns (address addr) { require(data.length >= offset + 20, 'Data too short'); assembly { // Extract 20 bytes from the specified offset addr := mload(add(add(data, 20), offset)) // clear the 12 least significant bits of the address addr := and(addr, 0x000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) } return addr; } } ``` ## EFPAccountMetadata.sol ```solc // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0 ^0.8.23; // lib/openzeppelin-contracts/contracts/utils/Context.sol // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) /** * @dev Provides information about the current execution context, including the * sender of the transaction and its data. While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct * manner, since when dealing with meta-transactions the account sending and * paying for execution may not be the actual sender (as far as an application * is concerned). * * This contract is only required for intermediate, library-like contracts. */ abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } } // src/interfaces/IEFPAccountMetadata.sol /** * @title IEFPAccountMetadata */ interface IEFPAccountMetadata { function addProxy(address proxy) external; function removeProxy(address proxy) external; function isProxy(address proxy) external view returns (bool); event UpdateAccountMetadata(address indexed addr, string key, bytes value); /** * @title Key-value Record * @notice A key-value string pair. */ struct KeyValue { string key; bytes value; } function getValue(address addr, string calldata key) external view returns (bytes memory); function setValue(string calldata key, bytes calldata value) external; function setValueForAddress(address addr, string calldata key, bytes calldata value) external; function setValues(KeyValue[] calldata records) external; function setValuesForAddress(address addr, KeyValue[] calldata records) external; } // lib/openzeppelin-contracts/contracts/access/Ownable.sol // OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol) /** * @dev Contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * By default, the owner account will be the one that deploys the contract. This * can later be changed with {transferOwnership}. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be applied to your functions to restrict their use to * the owner. */ abstract contract Ownable is Context { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev Initializes the contract setting the deployer as the initial owner. */ constructor() { _transferOwnership(_msgSender()); } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { _checkOwner(); _; } /** * @dev Returns the address of the current owner. */ function owner() public view virtual returns (address) { return _owner; } /** * @dev Throws if the sender is not the owner. */ function _checkOwner() internal view virtual { require(owner() == _msgSender(), "Ownable: caller is not the owner"); } /** * @dev Leaves the contract without owner. It will not be possible to call * `onlyOwner` functions. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, * thereby disabling any functionality that is only available to the owner. */ function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), "Ownable: new owner is the zero address"); _transferOwnership(newOwner); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Internal function without access restriction. */ function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } } // lib/openzeppelin-contracts/contracts/security/Pausable.sol // OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) /** * @dev Contract module which allows children to implement an emergency stop * mechanism that can be triggered by an authorized account. * * This module is used through inheritance. It will make available the * modifiers `whenNotPaused` and `whenPaused`, which can be applied to * the functions of your contract. Note that they will not be pausable by * simply including this module, only once the modifiers are put in place. */ abstract contract Pausable is Context { /** * @dev Emitted when the pause is triggered by `account`. */ event Paused(address account); /** * @dev Emitted when the pause is lifted by `account`. */ event Unpaused(address account); bool private _paused; /** * @dev Initializes the contract in unpaused state. */ constructor() { _paused = false; } /** * @dev Modifier to make a function callable only when the contract is not paused. * * Requirements: * * - The contract must not be paused. */ modifier whenNotPaused() { _requireNotPaused(); _; } /** * @dev Modifier to make a function callable only when the contract is paused. * * Requirements: * * - The contract must be paused. */ modifier whenPaused() { _requirePaused(); _; } /** * @dev Returns true if the contract is paused, and false otherwise. */ function paused() public view virtual returns (bool) { return _paused; } /** * @dev Throws if the contract is paused. */ function _requireNotPaused() internal view virtual { require(!paused(), "Pausable: paused"); } /** * @dev Throws if the contract is not paused. */ function _requirePaused() internal view virtual { require(paused(), "Pausable: not paused"); } /** * @dev Triggers stopped state. * * Requirements: * * - The contract must not be paused. */ function _pause() internal virtual whenNotPaused { _paused = true; emit Paused(_msgSender()); } /** * @dev Returns to normal state. * * Requirements: * * - The contract must be paused. */ function _unpause() internal virtual whenPaused { _paused = false; emit Unpaused(_msgSender()); } } // src/lib/ENSReverseClaimer.sol interface ENS { /** * @dev Returns the address that owns the specified node. * @param node The specified node. * @return address of the owner. */ function owner(bytes32 node) external view returns (address); } interface IReverseRegistrar { /** * @dev Transfers ownership of the reverse ENS record associated with the * calling account. * @param owner The address to set as the owner of the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function claim(address owner) external returns (bytes32); /** * @dev Sets the `name()` record for the reverse ENS record associated with * the calling account. First updates the resolver to the default reverse * resolver if necessary. * @param name The name to set for this address. * @return The ENS node hash of the reverse record. */ function setName(string memory name) external returns (bytes32); } /** * @title ENSReverseClaimer * @dev This contract is used to claim reverse ENS records. */ abstract contract ENSReverseClaimer is Ownable { /// @dev The namehash of 'addr.reverse', the domain at which reverse records /// are stored in ENS. bytes32 constant ADDR_REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2; /** * @dev Transfers ownership of the reverse ENS record associated with the * contract. * @param ens The ENS registry. * @param claimant The address to set as the owner of the reverse record in * ENS. * @return The ENS node hash of the reverse record. */ function claimReverseENS(ENS ens, address claimant) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).claim(claimant); } /** * @dev Sets the reverse ENS record associated with the contract. * @param ens The ENS registry. * @param name The name to set as the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function setReverseENS(ENS ens, string calldata name) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).setName(name); } } // src/EFPAccountMetadata.sol /** * @title EFPListMetadata * @author Cory Gabrielsen (cory.eth) * @custom:contributor throw; (0xthrpw.eth) * @custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM * * @notice This contract stores records as key/value pairs, by 32-byte * EFP List Token ID. */ contract EFPAccountMetadata is IEFPAccountMetadata, ENSReverseClaimer, Pausable { event ProxyAdded(address proxy); event ProxyRemoved(address proxy); /// @dev The key-value set for each address mapping(address => mapping(string => bytes)) private values; mapping(address => bool) private proxies; ///////////////////////////////////////////////////////////////////////////// // Pausable ///////////////////////////////////////////////////////////////////////////// /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } ///////////////////////////////////////////////////////////////////////////// // add/remove proxy ///////////////////////////////////////////////////////////////////////////// /** * @dev Add proxy address. * @param proxy The proxy address. */ function addProxy(address proxy) external whenNotPaused onlyOwner { proxies[proxy] = true; emit ProxyAdded(proxy); } /** * @dev Remove proxy address. * @param proxy The proxy address. */ function removeProxy(address proxy) external whenNotPaused onlyOwner { proxies[proxy] = false; emit ProxyRemoved(proxy); } /** * @dev Check if the address is a proxy. * @param proxy The address to check. * @return True if the address is a proxy, false otherwise. */ function isProxy(address proxy) external view returns (bool) { return proxies[proxy]; } ///////////////////////////////////////////////////////////////////////////// // Modifier ///////////////////////////////////////////////////////////////////////////// modifier onlyCallerOrProxy(address addr) { require(addr == msg.sender || proxies[msg.sender], 'not allowed'); _; } ///////////////////////////////////////////////////////////////////////////// // Getters ///////////////////////////////////////////////////////////////////////////// /** * @dev Retrieves value for address and key. * @param addr The address to query. * @param key The key to query. * @return The associated value. */ function getValue(address addr, string calldata key) external view returns (bytes memory) { return values[addr][key]; } /** * @dev Retrieves values for address and keys. * @param addr The address to query. * @param keys The keys to query. * @return The associated values. */ function getValues(address addr, string[] calldata keys) external view returns (bytes[] memory) { uint256 length = keys.length; bytes[] memory result = new bytes[](length); for (uint256 i = 0; i < length;) { string calldata key = keys[i]; result[i] = values[addr][key]; unchecked { ++i; } } return result; } ///////////////////////////////////////////////////////////////////////////// // Setters ///////////////////////////////////////////////////////////////////////////// /** * @dev Sets records for address with the unique key key to value, * overwriting anything previously stored for address and key. To clear a * field, set it to the empty string. * @param addr The address to update. * @param key The key to set. * @param value The value to set. */ function _setValue(address addr, string calldata key, bytes calldata value) internal { values[addr][key] = value; emit UpdateAccountMetadata(addr, key, value); } /** * @dev Sets records for caller address with the unique key key to value, * overwriting anything previously stored for address and key. To clear a * field, set it to the empty string. * Only callable by the token owner. * @param key The key to set. * @param value The value to set. */ function setValue(string calldata key, bytes calldata value) external whenNotPaused { _setValue(msg.sender, key, value); } /** * @dev Sets records for address with the unique key key to value, * overwriting anything previously stored for address and key. To clear a * field, set it to the empty string. * Only callable by the token owner. * @param addr The address to update. * @param key The key to set. * @param value The value to set. */ function setValueForAddress(address addr, string calldata key, bytes calldata value) external onlyCallerOrProxy(addr) whenNotPaused { _setValue(addr, key, value); } /** * @dev Sets an array of records for the caller address. Each record is a key/value pair. * Only callable by the token owner. * @param records The records to set. */ function setValues(KeyValue[] calldata records) external whenNotPaused { uint256 length = records.length; for (uint256 i = 0; i < length;) { KeyValue calldata record = records[i]; _setValue(msg.sender, record.key, record.value); unchecked { ++i; } } } /** * @dev Sets an array of records for a address. Each record is a key/value pair. * Only callable by the token owner. * @param addr The address to update. * @param records The records to set. */ function setValuesForAddress(address addr, KeyValue[] calldata records) external whenNotPaused onlyCallerOrProxy(addr) { uint256 length = records.length; for (uint256 i = 0; i < length;) { KeyValue calldata record = records[i]; _setValue(addr, record.key, record.value); unchecked { ++i; } } } } ``` ## TokenURIProvider.sol ```solc // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0 ^0.8.23; // lib/openzeppelin-contracts/contracts/utils/Context.sol // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) /** * @dev Provides information about the current execution context, including the * sender of the transaction and its data. While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct * manner, since when dealing with meta-transactions the account sending and * paying for execution may not be the actual sender (as far as an application * is concerned). * * This contract is only required for intermediate, library-like contracts. */ abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } } // src/interfaces/ITokenURIProvider.sol interface ITokenURIProvider { function tokenURI(uint256 tokenId) external view returns (string memory); } // lib/openzeppelin-contracts/contracts/utils/math/Math.sol // OpenZeppelin Contracts (last updated v4.9.0) (utils/math/Math.sol) /** * @dev Standard math utilities missing in the Solidity language. */ library Math { enum Rounding { Down, // Toward negative infinity Up, // Toward infinity Zero // Toward zero } /** * @dev Returns the largest of two numbers. */ function max(uint256 a, uint256 b) internal pure returns (uint256) { return a > b ? a : b; } /** * @dev Returns the smallest of two numbers. */ function min(uint256 a, uint256 b) internal pure returns (uint256) { return a < b ? a : b; } /** * @dev Returns the average of two numbers. The result is rounded towards * zero. */ function average(uint256 a, uint256 b) internal pure returns (uint256) { // (a + b) / 2 can overflow. return (a & b) + (a ^ b) / 2; } /** * @dev Returns the ceiling of the division of two numbers. * * This differs from standard division with `/` in that it rounds up instead * of rounding down. */ function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { // (a + b - 1) / b can overflow on addition, so we distribute. return a == 0 ? 0 : (a - 1) / b + 1; } /** * @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 * @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) * with further edits by Uniswap Labs also under MIT license. */ function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) { unchecked { // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256 // variables such that product = prod1 * 2^256 + prod0. uint256 prod0; // Least significant 256 bits of the product uint256 prod1; // Most significant 256 bits of the product assembly { let mm := mulmod(x, y, not(0)) prod0 := mul(x, y) prod1 := sub(sub(mm, prod0), lt(mm, prod0)) } // Handle non-overflow cases, 256 by 256 division. if (prod1 == 0) { // Solidity will revert if denominator == 0, unlike the div opcode on its own. // The surrounding unchecked block does not change this fact. // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic. return prod0 / denominator; } // Make sure the result is less than 2^256. Also prevents denominator == 0. require(denominator > prod1, "Math: mulDiv overflow"); /////////////////////////////////////////////// // 512 by 256 division. /////////////////////////////////////////////// // Make division exact by subtracting the remainder from [prod1 prod0]. uint256 remainder; assembly { // Compute remainder using mulmod. remainder := mulmod(x, y, denominator) // Subtract 256 bit number from 512 bit number. prod1 := sub(prod1, gt(remainder, prod0)) prod0 := sub(prod0, remainder) } // Factor powers of two out of denominator and compute largest power of two divisor of denominator. Always >= 1. // See https://cs.stackexchange.com/q/138556/92363. // Does not overflow because the denominator cannot be zero at this stage in the function. uint256 twos = denominator & (~denominator + 1); assembly { // Divide denominator by twos. denominator := div(denominator, twos) // Divide [prod1 prod0] by twos. prod0 := div(prod0, twos) // Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one. twos := add(div(sub(0, twos), twos), 1) } // Shift in bits from prod1 into prod0. prod0 |= prod1 * twos; // Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such // that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for // four bits. That is, denominator * inv = 1 mod 2^4. uint256 inverse = (3 * denominator) ^ 2; // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also works // in modular arithmetic, doubling the correct bits in each step. inverse *= 2 - denominator * inverse; // inverse mod 2^8 inverse *= 2 - denominator * inverse; // inverse mod 2^16 inverse *= 2 - denominator * inverse; // inverse mod 2^32 inverse *= 2 - denominator * inverse; // inverse mod 2^64 inverse *= 2 - denominator * inverse; // inverse mod 2^128 inverse *= 2 - denominator * inverse; // inverse mod 2^256 // Because the division is now exact we can divide by multiplying with the modular inverse of denominator. // This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is // less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1 // is no longer required. result = prod0 * inverse; return result; } } /** * @notice Calculates x * y / denominator with full precision, following the selected rounding direction. */ function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) { uint256 result = mulDiv(x, y, denominator); if (rounding == Rounding.Up && mulmod(x, y, denominator) > 0) { result += 1; } return result; } /** * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded down. * * Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11). */ function sqrt(uint256 a) internal pure returns (uint256) { if (a == 0) { return 0; } // For our first guess, we get the biggest power of 2 which is smaller than the square root of the target. // // We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have // `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`. // // This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)` // → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))` // → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)` // // Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit. uint256 result = 1 << (log2(a) >> 1); // At this point `result` is an estimation with one bit of precision. We know the true value is a uint128, // since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at // every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision // into the expected uint128 result. unchecked { result = (result + a / result) >> 1; result = (result + a / result) >> 1; result = (result + a / result) >> 1; result = (result + a / result) >> 1; result = (result + a / result) >> 1; result = (result + a / result) >> 1; result = (result + a / result) >> 1; return min(result, a / result); } } /** * @notice Calculates sqrt(a), following the selected rounding direction. */ function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) { unchecked { uint256 result = sqrt(a); return result + (rounding == Rounding.Up && result * result < a ? 1 : 0); } } /** * @dev Return the log in base 2, rounded down, of a positive value. * Returns 0 if given 0. */ function log2(uint256 value) internal pure returns (uint256) { uint256 result = 0; unchecked { if (value >> 128 > 0) { value >>= 128; result += 128; } if (value >> 64 > 0) { value >>= 64; result += 64; } if (value >> 32 > 0) { value >>= 32; result += 32; } if (value >> 16 > 0) { value >>= 16; result += 16; } if (value >> 8 > 0) { value >>= 8; result += 8; } if (value >> 4 > 0) { value >>= 4; result += 4; } if (value >> 2 > 0) { value >>= 2; result += 2; } if (value >> 1 > 0) { result += 1; } } return result; } /** * @dev Return the log in base 2, following the selected rounding direction, of a positive value. * Returns 0 if given 0. */ function log2(uint256 value, Rounding rounding) internal pure returns (uint256) { unchecked { uint256 result = log2(value); return result + (rounding == Rounding.Up && 1 << result < value ? 1 : 0); } } /** * @dev Return the log in base 10, rounded down, of a positive value. * Returns 0 if given 0. */ function log10(uint256 value) internal pure returns (uint256) { uint256 result = 0; unchecked { if (value >= 10 ** 64) { value /= 10 ** 64; result += 64; } if (value >= 10 ** 32) { value /= 10 ** 32; result += 32; } if (value >= 10 ** 16) { value /= 10 ** 16; result += 16; } if (value >= 10 ** 8) { value /= 10 ** 8; result += 8; } if (value >= 10 ** 4) { value /= 10 ** 4; result += 4; } if (value >= 10 ** 2) { value /= 10 ** 2; result += 2; } if (value >= 10 ** 1) { result += 1; } } return result; } /** * @dev Return the log in base 10, following the selected rounding direction, of a positive value. * Returns 0 if given 0. */ function log10(uint256 value, Rounding rounding) internal pure returns (uint256) { unchecked { uint256 result = log10(value); return result + (rounding == Rounding.Up && 10 ** result < value ? 1 : 0); } } /** * @dev Return the log in base 256, rounded down, of a positive value. * Returns 0 if given 0. * * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string. */ function log256(uint256 value) internal pure returns (uint256) { uint256 result = 0; unchecked { if (value >> 128 > 0) { value >>= 128; result += 16; } if (value >> 64 > 0) { value >>= 64; result += 8; } if (value >> 32 > 0) { value >>= 32; result += 4; } if (value >> 16 > 0) { value >>= 16; result += 2; } if (value >> 8 > 0) { result += 1; } } return result; } /** * @dev Return the log in base 256, following the selected rounding direction, of a positive value. * Returns 0 if given 0. */ function log256(uint256 value, Rounding rounding) internal pure returns (uint256) { unchecked { uint256 result = log256(value); return result + (rounding == Rounding.Up && 1 << (result << 3) < value ? 1 : 0); } } } // lib/openzeppelin-contracts/contracts/utils/math/SignedMath.sol // OpenZeppelin Contracts (last updated v4.8.0) (utils/math/SignedMath.sol) /** * @dev Standard signed math utilities missing in the Solidity language. */ library SignedMath { /** * @dev Returns the largest of two signed numbers. */ function max(int256 a, int256 b) internal pure returns (int256) { return a > b ? a : b; } /** * @dev Returns the smallest of two signed numbers. */ function min(int256 a, int256 b) internal pure returns (int256) { return a < b ? a : b; } /** * @dev Returns the average of two signed numbers without overflow. * The result is rounded towards zero. */ function average(int256 a, int256 b) internal pure returns (int256) { // Formula from the book "Hacker's Delight" int256 x = (a & b) + ((a ^ b) >> 1); return x + (int256(uint256(x) >> 255) & (a ^ b)); } /** * @dev Returns the absolute unsigned value of a signed value. */ function abs(int256 n) internal pure returns (uint256) { unchecked { // must be unchecked in order to support `n = type(int256).min` return uint256(n >= 0 ? n : -n); } } } // lib/openzeppelin-contracts/contracts/access/Ownable.sol // OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol) /** * @dev Contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * By default, the owner account will be the one that deploys the contract. This * can later be changed with {transferOwnership}. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be applied to your functions to restrict their use to * the owner. */ abstract contract Ownable is Context { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev Initializes the contract setting the deployer as the initial owner. */ constructor() { _transferOwnership(_msgSender()); } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { _checkOwner(); _; } /** * @dev Returns the address of the current owner. */ function owner() public view virtual returns (address) { return _owner; } /** * @dev Throws if the sender is not the owner. */ function _checkOwner() internal view virtual { require(owner() == _msgSender(), "Ownable: caller is not the owner"); } /** * @dev Leaves the contract without owner. It will not be possible to call * `onlyOwner` functions. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, * thereby disabling any functionality that is only available to the owner. */ function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), "Ownable: new owner is the zero address"); _transferOwnership(newOwner); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Internal function without access restriction. */ function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } } // lib/openzeppelin-contracts/contracts/utils/Strings.sol // OpenZeppelin Contracts (last updated v4.9.0) (utils/Strings.sol) /** * @dev String operations. */ library Strings { bytes16 private constant _SYMBOLS = "0123456789abcdef"; uint8 private constant _ADDRESS_LENGTH = 20; /** * @dev Converts a `uint256` to its ASCII `string` decimal representation. */ function toString(uint256 value) internal pure returns (string memory) { unchecked { uint256 length = Math.log10(value) + 1; string memory buffer = new string(length); uint256 ptr; /// @solidity memory-safe-assembly assembly { ptr := add(buffer, add(32, length)) } while (true) { ptr--; /// @solidity memory-safe-assembly assembly { mstore8(ptr, byte(mod(value, 10), _SYMBOLS)) } value /= 10; if (value == 0) break; } return buffer; } } /** * @dev Converts a `int256` to its ASCII `string` decimal representation. */ function toString(int256 value) internal pure returns (string memory) { return string(abi.encodePacked(value < 0 ? "-" : "", toString(SignedMath.abs(value)))); } /** * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. */ function toHexString(uint256 value) internal pure returns (string memory) { unchecked { return toHexString(value, Math.log256(value) + 1); } } /** * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. */ function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { bytes memory buffer = new bytes(2 * length + 2); buffer[0] = "0"; buffer[1] = "x"; for (uint256 i = 2 * length + 1; i > 1; --i) { buffer[i] = _SYMBOLS[value & 0xf]; value >>= 4; } require(value == 0, "Strings: hex length insufficient"); return string(buffer); } /** * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation. */ function toHexString(address addr) internal pure returns (string memory) { return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH); } /** * @dev Returns true if the two strings are equal. */ function equal(string memory a, string memory b) internal pure returns (bool) { return keccak256(bytes(a)) == keccak256(bytes(b)); } } // src/TokenURIProvider.sol /** * @title TokenURIProvider * @author throw; (0xthrpw.eth) * @custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM * * @notice This contract allows the owner to set a base URI for token URIs and * returns the token URI for a given token ID. Separating this functionality allows * the logic for generating token URIs to be upgradable. */ contract TokenURIProvider is ITokenURIProvider, Ownable { string private _baseURI; using Strings for uint256; /** * @dev Constructor * @param baseURI The base URI for token URIs */ constructor(string memory baseURI) { _baseURI = baseURI; } /** * @dev Returns the token URI for a given token ID * @param tokenId The token ID * @return The token URI */ function tokenURI(uint256 tokenId) external view override returns (string memory) { return string(abi.encodePacked(_baseURI, tokenId.toString())); } /** * @dev Sets the base URI for token URIs * @param baseURI The new base URI */ function setBaseURI(string memory baseURI) external onlyOwner { _baseURI = baseURI; } } ``` --- # ethidentitykit.com llms-full.txt > Ethereum Identity Kit facilitates the integration of Ethereum identity features into applications via a React component library, providing developers with tools and documentation for user profile management and on-chain transactions. # useFollowButton The `useFollowButton` hook manages the state and actions for a follow button component. It determines the current follow state between a `lookupAddress` and a `connectedAddress`, and provides functions to handle follow, unfollow, block, and mute actions. ### Add to your project ```tsx copy import { useFollowButton } from 'ethereum-identity-kit' export default function FollowButtonComponent() { const { buttonText, buttonState, handleAction, isLoading, disableHover, setDisableHover } = useFollowButton({ lookupAddress: '0x1234...abcd', connectedAddress: '0xabcd...1234', }) // Create your own loading states if (isLoading) return
Loading...
return ( ) } ``` ## Parameters | Parameter | Description | Required | Default Value | | ------------------ | ------------------------------------------------------------------------- | -------- | ------------- | | `lookupAddress` | Ethereum address to manage the follow state for. | Yes | - | | `connectedAddress` | Ethereum address of the currently connected user. | No | - | | `selectedList` | List number to manage the follow state for; defaults to the primary list. | No | - | ## Return Values | Return Value | Description | | ----------------- | -------------------------------------------------------------------------------------------- | | `buttonText` | The text to display on the follow button, indicating the current or pending follow state. | | `buttonState` | The current state of the button, such as 'Follow', 'Following', 'Blocked', etc. | | `handleAction` | Function to handle the button click action, updating the follow state accordingly. | | `isLoading` | Boolean indicating if the follow state is currently loading. | | `pendingState` | The pending state of the follow action, if any (e.g., 'Pending Following', 'Pending Block'). | | `disableHover` | Boolean indicating if hover effects should be disabled. (it is disabled after a click) | | `setDisableHover` | Function to set the `disableHover` state. | ### Notes - Ensure that the `lookupAddress` and `connectedAddress` are valid Ethereum addresses. - The `handleAction` function manages the follow, unfollow, block, and mute actions based on the current state. - The `disableHover` state can be used to control hover effects on the button during certain actions as the state is set to true every time the button is clicked. --- # useProfileDetails The `useProfileDetails` hook fetches and manages the profile details for a given Ethereum address or ENS name, including ENS data and primary list information. ### Add to your project ```tsx copy import { useProfileDetails } from 'ethereum-identity-kit' export default function ProfileComponent() { const { ens, address, primaryList, detailsLoading, refreshProfileDetails } = useProfileDetails({ addressOrName: 'vitalik.eth', }) if (detailsLoading) return
Loading...
// Create your own loading states // --- Your component code here --- } ``` ## Parameters | Parameter | Description | Required | Default Value | | ----------------------- | -------------------------------------------------------------------------------- | -------- | ------------- | | `addressOrName` | Ethereum Address or ENS name to fetch profile details for. | Yes | - | | `list` | List number to fetch profile details for; overrides `addressOrName` if provided. | No | - | | `prefetchedData` | Prefetched profile data to use initially. | No | - | | `refetchPrefetchedData` | Function to refetch prefetched profile data. | No | - | ## Return Values | Return Value | Description | | ----------------------- | ---------------------------------------------------------------- | | `ens` | ENS data for the profile, including name and records. | | `address` | Ethereum address associated with the profile. | | `primaryList` | Primary list number associated with the profile. | | `detailsLoading` | Boolean indicating if the profile details are currently loading. | | `refreshProfileDetails` | Function to manually refresh the profile details. | --- # useFollowingState The `useFollowingState` hook fetches and manages the following state between a given Ethereum address or ENS name and the connected user, indicating if the user follows, blocks, or mutes the address. ### Add to your project ```tsx copy import { useFollowingState } from 'ethereum-identity-kit' export default function FollowingStateComponent() { const { state, isLoading } = useFollowingState({ lookupAddressOrName: 'vitalik.eth', connectedAddress: '0x1234...abcd', }) // Create your own loading states if (isLoading) return
Loading...
// --- Your component code here --- } ``` ## Parameters | Parameter | Description | Required | Default Value | | --------------------- | ------------------------------------------------------------------------------------------ | -------- | ------------- | | `lookupAddressOrName` | Ethereum Address or ENS name to check the following state for. | Yes | - | | `connectedAddress` | Ethereum address of the currently connected user. | Yes | - | | `list` | List number to check the following state for; overrides `lookupAddressOrName` if provided. | No | - | ## Return Values | Return Value | Description | | ------------ | ---------------------------------------------------------------------------------- | | `state` | The following state, indicating if the user follows, blocks, or mutes the address. | | `isLoading` | Boolean indicating if the following state is currently loading. | --- # useFollowerState The `useFollowerState` hook fetches and manages the follower state between a given Ethereum address or ENS name and the connected user, indicating if the user is followed, blocked, or muted by the address. ### Add to your project ```tsx copy import { useFollowerState } from 'ethereum-identity-kit' export default function FollowerStateComponent() { const { followState, followerTag, isFollowerStateLoading } = useFollowerState({ addressOrName: 'vitalik.eth', connectedAddress: '0x1234...abcd', }) // Create your own loading states if (isFollowerStateLoading) return
Loading...
// --- Your component code here --- } ``` ## Parameters | Parameter | Description | Required | Default Value | | ------------------ | ----------------------------------------------------------------------------------- | -------- | ------------- | | `addressOrName` | Ethereum Address or ENS name to check the follower state for. | Yes | - | | `connectedAddress` | Ethereum address of the currently connected user. | Yes | - | | `list` | List number to check the follower state for; overrides `addressOrName` if provided. | No | - | ## Return Values | Return Value | Description | | ------------------------ | ----------------------------------------------------------------------------------------- | | `followState` | The follower state, indicating if the user is followed, blocked, or muted by the address. | | `followerTag` | Object containing text and className for displaying the follower state. | | `isFollowerStateLoading` | Boolean indicating if the follower state is currently loading. | --- # useProfileStats The `useProfileStats` hook fetches and manages the follower and following statistics for a given Ethereum address or ENS name. ### Add to your project ```tsx copy import { useProfileStats } from 'ethereum-identity-kit' export default function StatsComponent() { const { followers, following, statsLoading, refreshProfileStats } = useProfileStats({ addressOrName: 'vitalik.eth', }) // Create your own loading states if (statsLoading) return
Loading...
// --- Your component code here --- } ``` ## Parameters | Parameter | Description | Required | Default Value | | ----------------------- | ------------------------------------------------------------------------------ | -------- | ------------- | | `addressOrName` | Ethereum Address or ENS name to fetch profile stats for. | Yes | - | | `list` | List number to fetch profile stats for; overrides `addressOrName` if provided. | No | - | | `prefetchedData` | Prefetched stats data to use initially. | No | - | | `refetchPrefetchedData` | Function to refetch prefetched stats data. | No | - | ## Return Values | Return Value | Description | | --------------------- | -------------------------------------------------------------- | | `followers` | Number of followers for the profile. | | `following` | Number of accounts the profile is following. | | `statsLoading` | Boolean indicating if the profile stats are currently loading. | | `refreshProfileStats` | Function to manually refresh the profile stats. | --- # useTransactions The `useTransactions` hook provides access to the transaction context, allowing components to manage and interact with on-chain transactions. It offers state management and utility functions for handling transaction modals, batching, and more. ### Add to your project ```tsx copy import { useTransactions } from 'ethereum-identity-kit' export default function TransactionComponent() { const { txModalOpen, setTxModalOpen, pendingTxs, addTransactions, goToNextTransaction, resetTransactions, isCheckoutFinished, } = useTransactions() // Example usage if (txModalOpen) { return
Transaction Modal is Open
} return } ``` ## Return Values | Return Value | Description | | -------------------------- | -------------------------------------------------------------- | | `txModalOpen` | Boolean indicating if the transaction modal is open. | | `setTxModalOpen` | Function to set the `txModalOpen` state. | | `pendingTxs` | Array of pending transactions. | | `addTransactions` | Function to add new transactions (any transaction). | | `goToNextTransaction` | Function to proceed to the next transaction. | | `resetTransactions` | Function to reset all transactions. | | `isCheckoutFinished` | Boolean indicating if the checkout process is finished. | | `selectedChainId` | ID of the selected chain for EFP list transactions. | | `setSelectedChainId` | Function to set the `selectedChainId`. | | `currentTxIndex` | Index of the current transaction being processed. | | `setCurrentTxIndex` | Function to set the `currentTxIndex`. | | `lists` | EFP lists of the connected user. | | `listsLoading` | Boolean indicating if the lists are loading. | | `addListOpsTransaction` | Function to add a list operations transaction. | | `removeTransactions` | Function to remove transactions by their IDs. | | `removeListOpsTransaction` | Function to remove list operations transactions by their data. | | `selectedList` | Currently selected list. | | `setSelectedList` | Function to set the `selectedList`. | | `nonce` | Nonce for the current transaction. | | `setIsCheckoutFinished` | Function to set the `isCheckoutFinished` state. | ### `txModalOpen` **Description**: A boolean indicating whether the transaction modal is currently open. **Example**: ```tsx const { txModalOpen, setTxModalOpen } = useTransactions() // Open the transaction modal setTxModalOpen(true) ``` ### `setTxModalOpen` **Description**: A function to set the `txModalOpen` state, controlling the visibility of the transaction modal. **Example**: ```tsx setTxModalOpen(false) // Closes the transaction modal ``` ### `pendingTxs` **Description**: An array of pending transactions that are queued for processing. **Example**: ```tsx const { pendingTxs } = useTransactions() console.log(pendingTxs) // Logs the list of pending transactions ``` ### `addTransactions` **Description**: A function to add new transactions to the pending transactions list. You can add any transaction in the following format. **Example**: ```tsx const txs = [ { id: 'tx1', title: 'Transaction', // Title of the transaction to be displayed in the modal description: 'This transaction will do something', // Description of the transaction to be displayed in the modal address: '0x123', // Contract address abi: contractAbi, // ABI of the contract chainId: 1, // Chain ID functionName: 'function', // Function name args: [arg1, arg2], // Arguments to be passed to the function }, // ... ] addTransactions(txs) ``` ### `addListOpsTransaction` **Description**: A function to add a list operations to pending transactions. This is handled by the Follow Button component, however you can use it to add a list operations transaction manually. **Example**: ```tsx import { useTransactions, listOpAddListRecord, listOpRemoveListRecord, listOpAddTag, listOpRemoveTag, } from 'ethereum-identity-kit' const { addListOpsTransaction } = useTransactions() const listOps = [] listOps.push(listOpAddListRecord('0x1234...')) // Add a list record - follow listOps.push(listOpRemoveListRecord('0x1234...')) // Remove a list record - unfollow listOps.push(listOpAddTag('0x1234...', 'myTag')) // Add a tag listOps.push(listOpRemoveTag('0x1234...', 'myTag')) // Remove a tag addListOpsTransaction(listOps) ``` ### `removeTransactions` **Description**: A function to remove transactions by their IDs. **Example**: ```tsx removeTransactions(['tx1', 'tx2']) ``` ### `removeListOpsTransaction` **Description**: A function to remove list operations transactions by their data. **Example**: ```tsx import { useTransactions, listOpAddListRecord, listOpRemoveListRecord, listOpAddTag, listOpRemoveTag, } from 'ethereum-identity-kit' const { removeListOpsTransaction } = useTransactions() const listOpsData = [] listOpsData.push(listOpAddListRecord('0x1234...').data) // Add a list record - follow listOpsData.push(listOpRemoveListRecord('0x1234...').data) // Remove a list record - unfollow listOpsData.push(listOpAddTag('0x1234...', 'myTag').data) // Add a tag listOpsData.push(listOpRemoveTag('0x1234...', 'myTag').data) // Remove a tag removeListOpsTransaction(listOpsData) ``` ### `goToNextTransaction` **Description**: A function to proceed to the next transaction in the queue. **Example**: ```tsx goToNextTransaction() // Moves to the next transaction ``` ### `resetTransactions` **Description**: A function to reset all transactions, optionally keeping the modal open. **Example**: ```tsx resetTransactions() // Resets all transactions and closes the modal resetTransactions(true) // Resets all transactions but keeps the modal open ``` ### `isCheckoutFinished` **Description**: A boolean indicating if the checkout process is complete. **Example**: ```tsx const { isCheckoutFinished } = useTransactions() if (isCheckoutFinished) { console.log('Checkout is complete') } ``` ### `selectedChainId` **Description**: The ID of the selected blockchain network for transactions. **Example**: ```tsx const { selectedChainId, setSelectedChainId } = useTransactions() setSelectedChainId(1) // Sets the selected chain to Ethereum Mainnet ``` ### `setSelectedChainId` **Description**: A function to set the `selectedChainId`. **Example**: ```tsx setSelectedChainId(137) // Sets the selected chain to Polygon ``` ### `currentTxIndex` **Description**: The index of the current transaction being processed. **Example**: ```tsx const { currentTxIndex } = useTransactions() console.log(`Current transaction index: ${currentTxIndex}`) ``` ### `setCurrentTxIndex` **Description**: A function to set the `currentTxIndex`. **Example**: ```tsx setCurrentTxIndex(2) // Sets the current transaction index to 2 ``` ### `lists` **Description**: The EFP lists associated with the connected user. **Example**: ```tsx const { lists } = useTransactions() console.log(lists) // Logs the user's EFP lists ``` ### `listsLoading` **Description**: A boolean indicating if the lists are currently loading. **Example**: ```tsx const { listsLoading } = useTransactions() if (listsLoading) { console.log('Lists are loading...') } ``` ### `selectedList` **Description**: The currently selected list for operations. **Example**: ```tsx const { selectedList, setSelectedList } = useTransactions() setSelectedList('myList') ``` ### `setSelectedList` **Description**: A function to set the `selectedList`. **Example**: ```tsx setSelectedList('newList') ``` ### `nonce` **Description**: The nonce for the current transaction. **Example**: ```tsx const { nonce } = useTransactions() console.log(`Current nonce: ${nonce}`) ``` ### `setIsCheckoutFinished` **Description**: A function to set the `isCheckoutFinished` state. **Example**: ```tsx setIsCheckoutFinished(true) // Marks the checkout as finished ``` ### `followingAddressesToFetchFresh` **Description**: An array of addresses that need fresh data fetching. **Example**: ```tsx const { followingAddressesToFetchFresh } = useTransactions() console.log(followingAddressesToFetchFresh) // Logs addresses to fetch fresh data for ``` ### Notes You can use all of the returned values to create your own custom transaction modal, however the hook is mostly used to manage the Ethereum Identity Kit [transaction modal](https://ethidentitykit.com/docs/components/transaction-modal) and the transaction state. - Ensure that the `useTransactions` hook is used within a `TransactionProvider` to access the transaction context. - The `txModalOpen` state controls the visibility of the transaction modal. - Use the provided functions to manage transactions, such as adding, removing, and resetting them. --- ## Interpreting EFP Data A valid EFP list operation has four primary components that must exist in order for it to be considered `active`. - a valid [list storage location](https://docs.efp.app/design/list-storage-location) (denoting chainId, listRecords contract address, and slot) - an [account metadata record](https://docs.efp.app/design/account-metadata) specifying a tokenId as a user's primary list - a [list metadata record](https://docs.efp.app/design/list-metadata) in the listRecords contract for 'user' role for the slot - a [list record](https://docs.efp.app/design/list-records) with a specified slot that matches the list metadata record and the list storage location. This list record must have been created on the list records contract and chain specified in the list storage location The user and manager roles of a list can be different, this means that the sender of a transaction that applies list operations to a list is not necessarily the same address that is displayed as the user. ![Active List Operation](/active-list-op.png) ### Connecting the dots In order to validate a list operation, the data in each one of these components must match the others where specified. #### If User Address is known: If the only known information is a user address, then the following steps can be take to determine the user's list, list storage location and slot 1. Retrieve the user's primary list by querying the account metadata contract using the user address. 2. From the primary list, obtain the list storage location from the registry contract which includes the chainId, listRecords contract address, and slot using function getListStorageLocation(tokenId). 3. Query the list metadata record in the listRecords contract using the slot to verify the 'user' role. 4. Retrieve the list record from the listRecords contract using the slot and ensure it matches the list metadata record and list storage location in the registry. #### If List Id is known: If the only known information is a list number, then the following steps can be taken to determine the list user 1. Obtain the list storage location from the registry contract which includes the chainId, listRecords contract address, and slot using function getListStorageLocation(tokenId). 2. Query the list metadata record in the listRecords contract using the slot to verify the 'user' role. While at this step we will know the user address, we do not yet know if the user role is valid without checking for the existance of a 'primary-list' record that matches this list id and the user's address. 3. Retrieve the user's primary list by querying the account metadata contract using the user address. 4. Retrieve the list record from the listRecords contract using the slot and ensure it matches the list metadata record and list storage location in the registry. #### If Slot is known: If the only known information is the slot (i.e. you're looking at a raw list operation and want to determine the list to which it applies) 1. Query the list metadata record in the listRecords contract using the slot to find the 'user' role. While at this step we will know the user address, we do not yet know if the user role is valid without checking for the existance of a 'primary-list' record that matches this list id and the user's address. 2. Retrieve the user's primary list by querying the account metadata contract using the user address. ### Interpreting an example List Operation A List Record specifies a slot (which is specific to a list's list storage location) and an operation which describes the action being taken and the address it relates to. For instance the record below is a follow record for address `0x983110309620d911731ac0932219af06091b6744` ```solidity { slot: 38587947120907837207653958898632315929230182373855930657826753963097023554830, op: 0x01010101983110309620d911731ac0932219af06091b6744 } ``` The `op` data of this list record can be further broken down and abstracted into its constituant parts ```solidity { slot: 38587947120907837207653958898632315929230182373855930657826753963097023554830, op: { OpVersion: 0x01, OpCode: 0x01, // 0x01: Follow, 0x02: Unfollow, 0x03: Tag, 0x04: Untag RecordVersion: 0x01, RecordType: 0x01, // 0x01: Address Record RecordData: 0x983110309620d911731ac0932219af06091b6744 } } ``` This record only tells us the `slot` of the list that's doing the following but it tells us nothing about which account or list is doing the following. To determine the list doing the following we need to join the slot, chain id and the address of the list records contract and construct a list storage location. ### Interpreting an example List Storage Location ```solidity 0x010100000000000000000000000000000000000000000000000000000000000000015289fe5dabc021d02fddf23d4a4df96f4e0f17ef5550010c08608cc567bf432829280f99b40f7717290d6313134992e4971fa50e ``` This list storage location can be interpreted as follows ```solidity { Version: 0x01, Type: 0x01, Chain: 0x0000000000000000000000000000000000000000000000000000000000000001, ListRecordsContract: 0x5289fe5dabc021d02fddf23d4a4df96f4e0f17ef, Slot: 0x5550010c08608cc567bf432829280f99b40f7717290d6313134992e4971fa50e // 38587947120907837207653958898632315929230182373855930657826753963097023554830 } ``` ## Constructing State History A repository demonstrating practical application of this page that can be found here: [https://github.com/ethereumfollowprotocol/onchain](https://github.com/ethereumfollowprotocol/onchain) ### Interacting with Contracts directly Calling the Account Metadata contract to fetch a user's primary list: ```ts import { evmClients } from '#/clients/viem/index' import { efpAccountMetadataAbi } from '#/abi/generated/index' import { env } from '#/env.ts' const tokenId = await evmClients['8453']().readContract({ address: env.ACCOUNT_METADATA, abi: efpAccountMetadataAbi, functionName: 'getValue', args: [env.USER_ADDRESS as `0x${string}`, 'primary-list'], }) ``` Calling the List Registry contract to fetch a list's list storage location: ```ts import { evmClients } from '#/clients/viem/index' import { efpListRegistryAbi } from '#/abi/generated/index' import { env } from '#/env.ts' const listStorageLocation = await evmClients['8453']().readContract({ address: env.REGISTRY, abi: efpListRegistryAbi, functionName: 'getListStorageLocation', args: [BigInt(tokenId)], }) ``` Calling the List Records contract to fetch a list slot's 'user' role: ```ts import { evmClients } from '#/clients/viem/index' import { efpListRecordsAbi } from '#/abi/generated/index' import { env } from '#/env.ts' const listUser = await evmClients[parsedLsl.chainId.toString() as keyof typeof evmClients]().readContract({ address: parsedLsl.listRecordsContract as `0x${string}`, abi: efpListRecordsAbi, functionName: 'getListUser', args: [parsedLsl.slot], }) ``` ### Relevant Contract Events Basic EFP state for any user's followers and followings can also be constructed by 'listening' for specific events emitted by the EFP contracts and then interpreting them according to the protocol rules as stated above EFPListRecords (Base, Optimism and Ethereum Mainnet) `ListOp(uint256 indexed slot, bytes op)` `UpdateListMetadata(uint256 indexed slot, string key, bytes value)` EFPAccountMetadata (Base) `UpdateAccountMetadata(address indexed addr, string key, bytes value)` EFPListRegistry (Base) `UpdateListStorageLocation(uint256 indexed tokenId, bytes listStorageLocation)` --- ## EFP Railway Template Deploying the EFP-Silo template on Railway will set up all of the aforementioned components with default parameters set. [EFP-Silo](https://railway.app/template/pDGEZm?referralCode=AavWEU) Click the link or the button below. [![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/pDGEZm?referralCode=AavWEU) ### Configuration The template will run with default values for most of the components but you will need to set primary and secondary rpc urls for all three chains that EFP is using (Base, Optimism and Ethereum Mainnet). Find the section for 'Indexer-8453', click the 'configure' button and set the rpc endpoint for all six fields accordingly. These can be Alchemy or Infura urls or local ethereum nodes if you're testing at home. ![Indexer Params](/set-rpc.png) Once all sections display 'Ready to be deployed' the 'Deploy' button at the bottom will be unlocked and you can click it to proceed. ![Deployed Environment](/deployed-environment.png) ### Syncing It should take under an hour to sync all EFP data, you can still call the api but the data will not be up to date. ### Setup API URL Enable public networking for the API by clicking on the API section, then the 'Settings' tab and scrolling down to 'Networking'. Click 'Generate Domain' to have Railway create a random public link for you. Or click 'Custom Domain' to use an already existing domain name. ![Setup API URL](/setup-api-url.png) Once your link is generated, you can use it to call your API the same way you would call the official EFP API. Just swap in your new link in the place of 'api.ethfollow.xyz' `api.ethfollow.xyz/api/v1/stats` -> `my-generated-api-link.railway.app/api/v1/stats` ![Generate Link](/api-link-generated.png) ![Call Endpoint](/api-link-call.png) ### Setup ENS Worker The API uses V3X Labs' [enstate](https://github.com/v3xlabs/enstate) to provide ENS data. This service is available at https://ens.efp.app. EFP is happy to provide use of this endpoint to the community but please do not abuse it. Enable public networking for the ENS Worker by clicking on the 'enstate' section, then the 'Settings' tab and scrolling down to 'Networking'. Click 'Generate Domain' to have Railway create a random public link for you. Or click 'Custom Domain' to use an already existing domain name. Once public networking is enabled for the API and ENS Worker, they should be redeployed as well as the service manager. Do this by clicking into each of the services, and clicking the three dot menu on the right hand side of your currently deployed instance. Click 'Redeploy' on the menu. ![Three dot menu](/redeploy.png) ### Service Manager The services module handles several jobs that run on intervals: - Building the list of all addresses in the EFP database - Building the Leaderboard Ranking - Tracking Mutual follows - Shuffling the recommended accounts list - Refreshing ENS metadata - Building a list of accounts with recent activity The intervals for running these jobs can be adjusted to suit your needs. Additionally the accounts that appear in the recommended follows can be customized by forking the services repo and editing '/src/services/recommended/lists.ts'. Once you've edited the file, go to the 'Settings' tab on the services module and update the 'Source Repo' to point to your forked github repository. ![Services Settings](/services-settings-repo.png) ### ENV variables Most of the Environment variables are set appropriately and should not be changed unless you know exactly what you're doing. However there are a few settings than can be customized to fit your needs. As a general rule, if they're not listed below then you probably should leave them as is. #### Indexers (Indexer-base, Indexer-op, Indexer-eth) | Variable Name | Description | | -------------------- | ------------------------------------------------------------------------------ | | `PRIMARY_RPC_BASE` | Primary RPC URL for Base chain | | `PRIMARY_RPC_OP` | Primary RPC URL for Optimism chain | | `PRIMARY_RPC_ETH` | Primary RPC URL for Ethereum mainnet | | `SECONDARY_RPC_BASE` | Fallback RPC URL for Base chain | | `SECONDARY_RPC_OP` | Fallback RPC URL for Optimism chain | | `SECONDARY_RPC_ETH` | Fallback RPC URL for Ethereum mainnet | | `RECOVER_HISTORY` | Boolean Flag to start the indexer in recovery mode | | `START_BLOCK` | Block number to start recovering history from, if in recovery mode | | `BATCH_SIZE` | Number of events to batch before uploading | | `CHAIN_ID` | Chain id (8453, 10 or 1) | | `RECORDS_ONLY` | Boolean flag that specifies whether the indexer should listen for just ListOps | #### Service Manager | Variable Name | Description | | ------------------------------ | ------------------------------------------------------- | | `ENS_API_URL` | URL for the ENS worker to use for lookups | | `EFP_CACHE_INTERVAL` | Interval (in milliseconds) to update address cache | | `EFP_MUTUALS_INTERVAL` | Interval (in milliseconds) to update mutual follows | | `LEADERBOARD_RANKING_INTERVAL` | Interval (in milliseconds) to update leaderboard | | `RECENT_FOLLOWS_INTERVAL` | Interval (in milliseconds) to update recent activity | | `RECOMMENDED_INTERVAL` | Interval (in milliseconds) to update recommended accts | | `ENSMETADATA_INTERVAL` | Interval (in milliseconds) to update ENS metadata cache | | `HEARTBEAT_INTERVAL` | Interval (in milliseconds) to call heartbeat URL | #### API | Variable Name | Description | | ---------------- | ------------------------------------------------------ | | `CACHE_TTL` | Amount of seconds to wait before expiring cache record | | `POAP_API_TOKEN` | Auth token for POAP.xyz api | | `ENS_API_URL` | URL for the ENS worker to use for lookups | #### PgBouncer (see [PgBouncer Docs](https://www.pgbouncer.org/config.html) for more information) | Variable Name | Description | |-----------------------------------|-----------------------------------------------------| | `PGBOUNCER_DEFAULT_POOL_SIZE` | Amount of connections per pool | | `PGBOUNCER_MAX_CLIENT_CONN` | Maximum number of client connections | | `PGBOUNCER_POOL_MODE` | 'Session' or 'Transaction' | | `PGBOUNCER_QUERY_TIMEOUT` | Amount of seconds to wait before dropping query | --- ## EFP Infrastructure ![EFP Backend Design](/backend-design.png) The backend architecture for EFP is comprised of the following components - [Database](https://hub.docker.com/_/postgres) (Postgres) - [PGBouncer](https://hub.docker.com/r/pgbouncer/pgbouncer) (a connection pooler for Postgres) - [EFP Indexers](https://github.com/ethereumfollowprotocol/indexer) for Base, Optimism and Ethereum Mainnet - [EFP Services](https://github.com/ethereumfollowprotocol/services) (updates ens data, leaderboard, cache, mutuals counts) - [EFP API](https://github.com/ethereumfollowprotocol/api) (Can be deployed as a cloudflare worker) - [Ens Worker](https://github.com/v3xlabs/enstate) (Can be deployed as a cloudflare worker) - [Redis](https://hub.docker.com/_/redis) Cache for the API and ENS Worker All of these components can be set up separately, whether locally or using external hosts. The quickest way to stand up all of the backend infrastructure is to use the [EFP-Silo Railway template](https://railway.app/template/pDGEZm?referralCode=AavWEU) ### Indexers EFP has Registry and AccountMetadata contracts deployed on Base and ListRecords contracts deployed on three chains: Base, Optimism And Ethereum Mainnet. To capture and store all EFP data we run three separate instances of our indexer service, one for each chain. ### Database EFP Data is stored in a Postgres Database, queries and schema can be found in the [EFP Indexer Repo](https://github.com/ethereumfollowprotocol/indexer/tree/develop/db) ### Connection Pooling PgBouncer is used to handle connection pooling for the database. This improves availability for the database over the api by sharing client connections, saving overhead on connection/reconnection. ### Data Services The services module handles several jobs that run on intervals: - Building the list of all addresses in the EFP database - Building the Leaderboard Ranking - Tracking Mutual follows - Shuffling the recommended accounts list - Refreshing ENS metadata - Building a list of accounts with recent activity ### API EFP core team provides a public API endpoint at [`https://api.ethfollow.xyz/api/v1`](https://api.ethfollow.xyz/api/v1). If you are a developer, you are free to use this endpoint to retrieve EFP data. #### Commonly used endpoints - [User Stats](https://ethidentitykit.com/docs/api/users/stats/): the follower and following counts of a particular user - [User Following](https://ethidentitykit.com/docs/api/users/following/): list of the accounts a particular user follows - [User Followers](https://ethidentitykit.com/docs/api/users/followers/): list of the accounts that follow a particular user - [User ENS data](https://ethidentitykit.com/docs/api/users/ens/): the ENS data for a particular user ### ENS Worker The API uses V3X Labs' [enstate](https://github.com/v3xlabs/enstate) to provide ENS data. This service is available at https://ens.efp.app. EFP is happy to provide use of this endpoint to the community but please do not abuse it. Anyone can deploy this ENS worker repository to cloudflare or host their own instance locally. Read more about deploying [here](https://github.com/v3xlabs/enstate?tab=readme-ov-file#-cloudflare-workers). ### Redis Cache The Redis cache is used by both the API and the ENS Worker to cache responses for faster future retrieval. --- # Welcome to Ethereum Identity Kit Ethereum Identity Kit allows you to easily implement the Ethereum identity stack into your application. > [!NOTE] **Ethereum Identity Kit is a React component library. Supprt for other frameworks coming soon.** ## Getting Started ### Install Install the library using your package manager. ```sh npm2yarn copy npm install ethereum-identity-kit wagmi viem@2.x @tanstack/react-query ``` ### Setup Library uses [Tanstack Query](https://tanstack.com/query) for data fetching, [Wagmi](https://wagmi.sh/) for wallet connection and handling onchain transactions, and a [Transaction provider](https://ethidentitykit.com/docs/transaction-provider) so you need to setup a query client and provider, [Wagmi provider](https://wagmi.sh/react/api/WagmiProvider) with your [Wagmi config](https://wagmi.sh/react/api/createConfig), and add Transaction Provider to your app. ```tsx copy import { WagmiProvider } from 'wagmi' import { wagmiConfig } from '#/lib/wagmi' import { TransactionProvider } from 'ethereum-identity-kit' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' const queryClient = new QueryClient() export default function App({ Component, pageProps }: AppProps) { return ( ) } ``` ### Apply Styles Add the following to your `layout.tsx` or `_app.tsx` file: ```tsx copy import 'ethereum-identity-kit/css' ``` If you are using `dark` className prop to handle dark mode, componentss will automatically adapt to the color scheme of your application. ### You're all set! Library is typed with TypeScript, which allows for autocompletion and type safety. ```tsx copy import { ProfileCard } from 'ethereum-identity-kit' export default function Home() { return // or 0x983110309620d911731ac0932219af06091b6744 } ``` ## Troubleshooting If you are hitting the following error trying to run your project: ``` Error: cannot use import statement outside a module ``` Make sure to transpile `ethereum-identity-kit` in your `next.config.mjs`: ```js copy transpilePackages: ['ethereum-identity-kit'], ``` Are you experiencing issues with the library? [Report an issue](https://github.com/ethereumidentitykit/identity-kit/issues/new) --- # Media Kit The Media Kit provides official Ethereum Identity Kit logos and branding assets. These assets are available in both light and dark mode variations to ensure optimal visibility across different themes and backgrounds. ## Logos Our official logos are available in both standalone and text variants. Choose the appropriate version based on your needs and the background color of your application. | Mode | Type | Preview | Download | | ----- | -------------- | -------------------------------------------------------------------- | --------------------------------------------------------- | | Light | Logo with Text | | Download | | Light | Logo | | Download | | Dark | Logo | | Download | | Dark | Logo with Text | | Download | ## Powered By Badges If you're integrating Ethereum Identity Kit into your application, you can showcase this using our "Powered By" badges. These badges come in different styles to match your application's design. | Mode | Type | Preview | Download | | ----- | ------------------ | --------------------------------------------------------------------------- | ---------------------------------------------------------------- | | Light | Logo with Text | | Download | | Light | Rounded Inner Text | | Download | | Light | Rounded Outer Text | | Download | | Dark | Logo with Text | | Download | | Dark | Rounded Inner Text | | Download | | Dark | Rounded Outer Text | | Download | --- # List Operations The list operations module provides utility functions for creating list operations in the EFP (Ethereum Follow Protocol) system. ## Functions ### listOpAddListRecord Creates a list operation to add a new record to a list. ```tsx const op = listOpAddListRecord('0x...') ``` #### Parameters | Parameter | Type | Description | | --------- | ------- | --------------------------------------- | | `address` | Address | The Ethereum address to add to the list | #### Return Value Returns a `ListOpType` object: ```tsx { version: 1, opcode: Opcode.FOLLOW, data: address } ``` ### listOpRemoveListRecord Creates a list operation to remove a record from a list. ```tsx const op = listOpRemoveListRecord('0x...') ``` #### Parameters | Parameter | Type | Description | | --------- | ------- | -------------------------------------------- | | `address` | Address | The Ethereum address to remove from the list | #### Return Value Returns a `ListOpType` object: ```tsx { version: 1, opcode: Opcode.UNFOLLOW, data: address } ``` ### listOpAddTag Creates a list operation to add a tag to a list record. ```tsx const op = listOpAddTag('0x...', 'friend') ``` #### Parameters | Parameter | Type | Description | | --------- | ------- | --------------------------- | | `address` | Address | The Ethereum address to tag | | `tag` | string | The tag to add | #### Return Value Returns a `ListOpType` object: ```tsx { version: 1, opcode: Opcode.TAG, data: `${address}${toHex(tag).slice(2)}` } ``` ### listOpRemoveTag Creates a list operation to remove a tag from a list record. ```tsx const op = listOpRemoveTag('0x...', 'friend') ``` #### Parameters | Parameter | Type | Description | | --------- | ------- | ------------------------------------------- | | `address` | Address | The Ethereum address to remove the tag from | | `tag` | string | The tag to remove | #### Return Value Returns a `ListOpType` object: ```tsx { version: 1, opcode: Opcode.UNTAG, data: `${address}${toHex(tag).slice(2)}` } ``` ## What are list operations? List operations are a way to manage lists on the EFP (Ethereum Follow Protocol). They are used to add, remove, and tag records in a list. - [EFP Docs - List Operations](https://docs.efp.app/design/list-ops/) --- # Transactions The transactions module provides utility functions for handling EFP (Ethereum Follow Protocol) list operations and transactions. ## Functions ### formatListOpsTransaction Formats a list operations transaction with the provided parameters. Use this only if you are trying to execute EFP transactions yourself. If you are using [TransactionProvider](/docs/components/transaction-provider) with [useTransaction](/docs/hooks/use-transaction), and are using the [addlistopstransaction](/docs/hooks/useTransactions#addlistopstransaction) function, you don't need to use it. ```tsx const transaction = formatListOpsTransaction({ nonce: 1n, chainId: 1, listOps: [{ opcode: 1, data: '0x...' }], connectedAddress: '0x...', isMintingNewList: false, }) ``` #### Parameters | Parameter | Type | Description | | ------------------ | ------------ | -------------------------------------- | | `nonce` | bigint | The nonce for the transaction | | `chainId` | number | The chain ID to use | | `listOps` | ListOpType[] | Array of list operations to perform | | `connectedAddress` | Address | The connected wallet address | | `isMintingNewList` | boolean | Whether this is for minting a new list | ### getListOpData Generates list operation data from an address and optional tag. ```tsx const data = getListOpData('0x...', 'tag') ``` #### Parameters | Parameter | Type | Description | | --------- | ------- | ----------------------- | | `address` | Address | The Ethereum address | | `tag` | string | Optional tag to include | ### getListOpsFromTransaction Extracts list operations from a transaction. ```tsx const listOps = getListOpsFromTransaction(transaction) ``` #### Parameters | Parameter | Type | Description | | ------------- | --------------- | ------------------------------------------ | | `transaction` | TransactionType | The transaction to extract operations from | ### getMintTxNonce Extracts the nonce from a mint transaction. ```tsx const nonce = getMintTxNonce(transaction) ``` ### getMintTxChainId Extracts the chain ID from a mint transaction. ```tsx const chainId = getMintTxChainId(transaction) ``` ### getMintTxRecordsAddress Extracts the records address from a mint transaction. ```tsx const recordsAddress = getMintTxRecordsAddress(transaction) ``` ### getPendingTxAddresses Gets all addresses from pending list update transactions. ```tsx const addresses = getPendingTxAddresses(transactions) ``` ### extractAddressAndTag Extracts address and tag from list operation data. ```tsx const { address, tag } = extractAddressAndTag(data) ``` ### getPendingTxListOps Gets all list operations from pending transactions. ```tsx const listOps = getPendingTxListOps(transactions) ``` ### getPendingTxAddressesAndTags Gets all addresses and tags from pending transactions. ```tsx const addressesAndTags = getPendingTxAddressesAndTags(transactions) ``` ### prepareMintTransaction Prepares a mint transaction with the given parameters. ```tsx const mintTx = prepareMintTransaction(mintNonce, chainId) ``` #### Parameters | Parameter | Type | Description | | ----------- | ------ | ---------------------------------- | | `mintNonce` | bigint | The nonce for the mint transaction | | `chainId` | number | Optional chain ID to use | ### transformTxsForLocalStorage Transforms transactions for local storage by converting bigint values to strings. ```tsx const transformedTxs = transformTxsForLocalStorage(transactions) ``` #### Parameters | Parameter | Type | Description | | --------- | ----------------- | ---------------------------------- | | `txs` | TransactionType[] | Array of transactions to transform | --- # Validity The validity module provides utility functions for validating various types of data in the application. ## Functions ### isLinkValid Checks if a given link is a valid http or https link, or is pulling from public or assets folders. This is used to filter out ipfs or invalid links and fall back to the [ENS Metadata Service](https://support.ens.domains/en/articles/8228750-the-ens-metadata-service) (if there is an ENS name). ```tsx const isValid = isLinkValid('https://example.com') // true const isValid = isLinkValid('invalid-link') // false ``` #### Parameters | Parameter | Type | Description | | --------- | ------ | -------------------- | | `link` | string | The link to validate | #### Return Value Returns `true` if the link contains one of the following prefixes: - `https://` - `http://` - `public/` - `/assets` Returns `false` if the link is undefined or doesn't contain any of the valid prefixes. ### isValidEnsName Validates if a given string is a valid [ENS (Ethereum Name Service)](https://ens.domains/) name. ```tsx const isValid = isValidEnsName('vitalik.eth') // true const isValid = isValidEnsName('invalid-ens') // false ``` #### Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------ | | `name` | string | The ENS name to validate | #### Return Value Returns `true` if the name is a valid ENS name, `false` otherwise. Uses the `normalize` function from `viem/ens` for validation. --- # List Storage Location The list storage location module provides functionality to retrieve the storage location of an EFP list from the list registry contract. ## Functions ### getListStorageLocation Retrieves the chain ID and storage slot for a given list number from the EFP List Registry contract. ```tsx const { chainId, slot } = await getListStorageLocation('1') ``` #### Parameters | Parameter | Type | Description | | --------- | ------ | ----------------------------------------------- | | `list` | string | The list number to get the storage location for | #### Return Value Returns an object containing: - `chainId`: number - The chain ID where the list is stored - `slot`: bigint - The storage slot where the list data is stored #### Example ```tsx const location = await getListStorageLocation('1') console.log(location) // { // chainId: 1, // slot: 123456789n // } ``` #### Technical Details The function: 1. Creates a public client for the default chain 2. Gets the list registry contract instance 3. Calls `getListStorageLocation` on the contract with the list number 4. Extracts the chain ID and slot from the returned storage location 5. Returns the parsed data For more information about list storage locations, see the [EFP documentation](https://docs.efp.app/design/list-storage-location/). ## What is a list storage location? A list storage location is a unique identifier for a list on a specific chain. It is a combination of a chain ID and a storage slot. The chain ID is the ID of the chain where the list is stored, and the storage slot is the slot where the list data is stored. - [EFP Docs - List Storage Location](https://docs.efp.app/design/list-storage-location/) --- # Formatters The formatters module provides utility functions for formatting various types of data in the application. ## Functions ### formatNumber Formats a number to include separators ("," for thousands, "." for decimals) ```tsx const formattedNumber = formatNumber(1234.56) // "1,234.56" (en-US) ``` #### Parameters | Parameter | Type | Description | | --------- | ------ | -------------------- | | `number` | number | The number to format | ### formatFollowersYouKnowText Formats the text shown in the common followers component next to the avatars. ```tsx const text = formatFollowersYouKnowText(3) // "1 other you know follows them" ``` #### Parameters | Parameter | Type | Description | | -------------- | ------ | ------------------------------ | | `resultLength` | number | The number of common followers | #### Return Values | Result Length | Return Value | | ------------- | --------------------------------- | | 0 | "No common followers" | | 1 | " follows them" | | 2 | " follow them" | | 3 | "1 other you know follows them" | | >3 | "{n} others you know follow them" | ### formatQueryParams Formats query parameters for API calls. ```tsx const queryString = formatQueryParams({ address: '0x...', tags: ['tag1', 'tag2'], limit: 10, }) // "address=0x...&tags=tag1,tag2&limit=10" ``` #### Parameters | Parameter | Type | Description | | --------- | ----------------------------------------------------------------- | ---------------------------------- | | `inputs` | Record | Object containing query parameters | #### Return Value Returns a URL-encoded query string with all non-null/undefined parameters. --- # Profile The profile module provides utility functions for handling profile-related operations. ## Functions ### defaultOnStatClick Opens a new tab with the EFP profile page for a given address or ENS name, focusing on a specific stat tab. ```tsx defaultOnStatClick({ addressOrName: 'vitalik.eth', stat: 'followers', }) ``` #### Parameters | Parameter | Type | Description | | --------------- | ------ | ---------------------------------------------------- | | `addressOrName` | string | Ethereum address or ENS name to open the profile for | | `stat` | string | The stat tab to focus on in the profile page | #### Behavior The function: 1. Opens a new tab with the URL `https://efp.app/{addressOrName}?tab={stat}` 2. Uses `noopener,noreferrer` for security when opening the new tab #### Example ```tsx // Opens https://efp.app/vitalik.eth?tab=followers in a new tab defaultOnStatClick({ addressOrName: 'vitalik.eth', stat: 'followers', }) ``` --- # Generate Slot The generateSlot utility generates a random storage slot value using keccak256 and the current timestamp. ## Functions ### generateSlot Generates a random storage slot value for use in EFP list storage or other unique slot requirements. ```tsx const slot = generateSlot() ``` #### Return Value Returns a `bigint` representing a random storage slot value. #### Example ```tsx const slot = generateSlot() console.log(slot) // e.g., 12345678901234567890n ``` #### Technical Details - Uses `keccak256` hash of the current timestamp multiplied by a random number. - Ensures the result fits within 255 bits. --- # fetchFollowersYouKnow Fetches the list of followers that you (connected address) follow, who also follow another address. ## Usage ```tsx const followersYouKnow = await fetchFollowersYouKnow('0x...', 'vitalik.eth') ``` ## Parameters | Parameter | Type | Description | | --------------------- | ------- | ---------------------------------------------------------- | | `connectedAddress` | Address | Your connected wallet address | | `lookupAddressOrName` | string | Ethereum address or ENS name to find common followers with | ## Return Value Returns a `FollowersYouKnowResponse` object containing the list of common followers, or `noFollowersYouKnow` if the request fails. ## Example Response ```tsx { followers: [ { address: '0x...', name: 'vitalik.eth', avatar: 'https://example.com/avatar.png', mutuals_rank: '1', header: 'https://example.com/header.png', }, ] } ``` --- # fetchFollowState Fetches the follow state between two addresses or a list and an address. ## Usage ```tsx const followState = await fetchFollowState({ lookupAddressOrName: 'vitalik.eth', connectedAddress: '0x...', type: 'following', }) // or for a list const listFollowState = await fetchFollowState({ lookupAddressOrName: 'vitalik.eth', list: 1, type: 'following', }) // or with fresh data const freshState = await fetchFollowState({ lookupAddressOrName: 'vitalik.eth', connectedAddress: '0x...', type: 'following', fresh: true, }) ``` ## Parameters | Parameter | Type | Description | | --------------------- | ------------------------- | ------------------------------------------------------ | | `lookupAddressOrName` | string | Ethereum address or ENS name to check follow state for | | `connectedAddress` | string | Optional connected wallet address | | `list` | number | Optional list number to check follow state for | | `type` | 'following' \| 'follower' | Type of follow state to check | | `fresh` | boolean | Optional flag to fetch fresh data bypassing cache | ## Return Value Returns a `FollowStatusResponse` object containing the follow state, or `null` if: - The request fails - `type` is 'following' and no `list` is provided - Neither `connectedAddress` nor `list` is provided ## Example Response The following response is the connected address following the lookup address. ```tsx { token_id: '123', address: '0x...', state: { follow: true, block: false, mute: false, }; } ``` --- # fetchProfileEFPPoaps Fetches EFP [POAPs (Proof of Attendance Protocol)](https://poap.xyz/) badges for a given address, ENS name, or list. ## Usage ```tsx const poaps = await fetchProfileEFPPoaps('vitalik.eth') // or for a list const listPoaps = await fetchProfileEFPPoaps('0x...', 1) // or with fresh data const freshPoaps = await fetchProfileEFPPoaps('vitalik.eth', undefined, true) ``` ## Parameters | Parameter | Type | Description | | --------------- | ------------------------- | ------------------------------------------------- | | `addressOrName` | Address \| string \| null | Ethereum address or ENS name to fetch POAPs for | | `list` | number \| null | Optional list number to fetch POAPs for | | `fresh` | boolean | Optional flag to fetch fresh data bypassing cache | ## Return Value Returns an array of `ProfileEFPPoapResponse` objects containing POAP badges, or an empty array if the request fails. ## Example Response ```tsx ;[ { eventId: '123', participated: 'EFP Early Adopter', collection: { event: { id: 123, fancy_id: 'EFP Early Adopter', name: 'EFP Early Adopter', event_url: 'https://example.com', image_url: 'https://example.com/image.png', country: 'United States', city: 'San Francisco', description: 'EFP Early Adopter', year: 2021, start_date: '2021-01-01', end_date: '2021-01-01', expiry_date: '2021-01-01', }; tokenId: '123', owner: '0x...', } }, ] ``` --- # fetchEthPrice Fetches the current ETH price in USD from the CoinGecko API. ## Usage ```tsx const ethPrice = await fetchEthPrice() ``` ## Return Value Returns the current ETH price in USD as a number, or `null` if the request fails. ## Example Response ```tsx 2500.42 // Current ETH price in USD ``` --- # fetchProfileDetails Fetches profile details for a given Ethereum address, ENS name, or list number. ## Usage ```tsx const profileDetails = await fetchProfileDetails('vitalik.eth') // or const listDetails = await fetchProfileDetails('0x...', 1) // or with fresh data const freshDetails = await fetchProfileDetails('vitalik.eth', undefined, true) ``` ## Parameters | Parameter | Type | Description | | --------------- | --------------- | ------------------------------------------------- | | `addressOrName` | string | Ethereum address or ENS name to fetch details for | | `list` | ProfileListType | Optional list number to fetch details for | | `fresh` | boolean | Optional flag to fetch fresh data bypassing cache | ## Return Value Returns a `ProfileDetailsResponse` object containing the profile details, or `null` if the request fails. ## Example Response ```tsx { address: '0x...', ens: { name: 'user.eth', avatar: 'https://example.com/avatar.png', header: 'https://example.com/header.png', records: { // ... other ENS records }, }, primary_list: '123', ranks: { mutuals_rank: 1, followers_rank: 1, following_rank: 1, blocks_rank: 1, top8_rank: 1, }, } ``` --- # fetchProfileLists Fetches all lists associated with a given address or ENS name. ## Usage ```tsx const lists = await fetchProfileLists('vitalik.eth') // or with fresh data const freshLists = await fetchProfileLists('vitalik.eth', true) ``` ## Parameters | Parameter | Type | Description | | --------------- | ------- | ------------------------------------------------- | | `addressOrName` | string | Ethereum address or ENS name to fetch lists for | | `fresh` | boolean | Optional flag to fetch fresh data bypassing cache | ## Return Value Returns a `ProfileListsResponse` object containing: - `primary_list`: The primary list number (or null if none) - `lists`: Array of all lists associated with the address ## Example Response ```tsx { primary_list: "123", lists: [ "123", "456", ] } ``` --- # fetchProfileStats Fetches profile statistics (followers and following counts) for a given address, ENS name, or list. ## Usage ```tsx const stats = await fetchProfileStats('vitalik.eth') // or for a list const listStats = await fetchProfileStats('0x...', 1) // or with live data const liveStats = await fetchProfileStats('vitalik.eth', undefined, true) ``` ## Parameters | Parameter | Type | Description | | --------------- | --------------- | ------------------------------------------------ | | `addressOrName` | string | Ethereum address or ENS name to fetch stats for | | `list` | ProfileListType | Optional list number to fetch stats for | | `isLive` | boolean | Optional flag to fetch live data bypassing cache | ## Return Value Returns a `StatsResponse` object containing follower and following counts, or default values if the request fails. ## Example Response ```tsx { followers_count: 1234, following_count: 567 } ``` --- # fetchAccount Fetches condensed account information for a given address, ENS name, or list. ## Usage ```tsx const account = await fetchAccount('vitalik.eth') // or for a list const listAccount = await fetchAccount('0x...', 1) ``` ## Parameters | Parameter | Type | Description | | --------------- | --------------- | ------------------------------------------------- | | `addressOrName` | string | Ethereum address or ENS name to fetch account for | | `list` | ProfileListType | Optional list number to fetch account for | ## Return Value Returns an `AccountResponseType` object containing account information, or `null` if the request fails. ## Example Response ```tsx { address: '0x...', ens: { name: 'user.eth', avatar: 'https://example.com/avatar.png', }, primary_list: '1', } ``` --- # fetchRecommended Fetches recommended profiles or recent activity from the EFP API, with optional pagination. ## Usage ```tsx // Fetch recommended profiles for a user const recommended = await fetchRecommended('recommended', 'vitalik.eth') // Fetch recommended profiles for a list const listRecommended = await fetchRecommended('recommended', '0x...', '1') // Fetch recent activity const recentActivity = await fetchRecommended('discover') // With pagination const paginatedResults = await fetchRecommended('recommended', 'vitalik.eth', undefined, 20, 2) ``` ## Parameters | Parameter | Type | Description | | --------------- | --------------------------- | ------------------------------------------------------- | | `endpoint` | 'discover' \| 'recommended' | The type of recommendations to fetch | | `addressOrName` | string \| Address | Optional address or ENS name to get recommendations for | | `list` | string \| null | Optional list number to get recommendations for | | `limit` | number | Optional number of results per page (default: 10) | | `pageParam` | number | Optional page number for pagination (default: 1) | ## Return Value Returns an array of recommended profiles or latest follows, or an empty array if the request fails. ## Example Response ```tsx ;[ { address: '0x...', name: 'user.eth', avatar: 'https://example.com/avatar.png', header: 'https://example.com/header.png', }, ] ``` --- # fetchAllFollowersYouKnow Fetches a paginated list of followers that you (connected address) follow, who also follow another address, with optional search functionality. ## Usage ```tsx const { followersYouKnow, nextPageParam } = await fetchAllFollowersYouKnow({ connectedAddress: '0x...', lookupAddressOrName: 'vitalik.eth', limit: 20, search: 'search term', pageParam: 0, }) ``` ## Parameters | Parameter | Type | Description | | --------------------- | ----------------- | --------------------------------------------------------------- | | `connectedAddress` | Address | Your connected wallet address | | `lookupAddressOrName` | Address \| string | Ethereum address or ENS name to find common followers with | | `limit` | number | Optional number of results per page (defaults to `FETCH_LIMIT`) | | `search` | string | Optional search term to filter followers | | `pageParam` | number | Page number for pagination (0-based) | ## Return Value Returns an object containing: - `followersYouKnow`: Array of `FollowerYouKnow` objects - `nextPageParam`: Next page number for pagination ## Example Response ```tsx { followersYouKnow: [ { address: '0x...', name: 'vitalik.eth', avatar: 'https://example.com/avatar.png', mutuals_rank: '1', header: 'https://example.com/header.png', }, ], nextPageParam: 1 } ``` --- #### /discover Get recently active accounts to follow. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/discover ``` ```jsonc // sample response { "latestFollows": [ { "address": "0xa8b4756959e1192042fc2a8a103dfe2bddf128e8", "name": "caveman.eth", "avatar": "https://euc.li/caveman.eth", "header": "https://i.imgur.com/KYD6snF.jpeg", "followers": "162", "following": "482" }, { "address": "0x8513eef11bba6a57845d10780e7e889e3be289e8", "name": "oandrade.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/oandrade.eth", "header": null, "followers": "5", "following": "15" }, { "address": "0xb6518c8304992da58de9055f1db80a37609f00a2", "name": "silvr.eth", "avatar": "https://euc.li/silvr.eth", "header": null, "followers": "1", "following": "0" }, { "address": "0x8eddf5431f5b31933bfbd8111d54fc6e9456e6c1", "name": "slowsort.eth", "avatar": "https://euc.li/slowsort.eth", "header": null, "followers": "49", "following": "565" }, { "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "name": "garypalmerjr.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/garypalmerjr.eth", "header": null, "followers": "152", "following": "3403" }, ... ] } ``` --- #### /token/metadata/\:token_id Get NFT metadata for a specified token id #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/token/metadata/4 ``` ```jsonc // sample response { "name": "EFP List #4", "description": "Ethereum Follow Protocol (EFP) is an onchain social graph protocol for Ethereum accounts.", "image": "https://api.ethfollow.xyz/api/v1/token/image/3", "external_url": "https://efp.app/3", "attributes": [ { "trait_type": "User", "value": "brantly.eth", }, { "trait_type": "Primary List", "value": true, }, { "trait_type": "Followers", "value": 368, }, { "trait_type": "Following", "value": 1079, }, { "trait_type": "Mutuals Rank", "value": "1", }, { "trait_type": "Followers Rank", "value": "1", }, { "trait_type": "Following Rank", "value": "6", }, { "trait_type": "Blocked Rank", "value": "7", }, ], } ``` --- #### /token/image/\:token_id Get NFT image for a specified token id #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/token/image/3 ``` #### Response This endpoint responds with a formatted svg of content-type `image/svg+xml;charset=utf-8` --- WIP --- #### /stats Get global EFP statistics. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/stats ``` ```jsonc // sample response { "stats": { "address_count": "8999", "list_count": "733", "list_op_count": "50284", }, } ``` --- #### /exportState/\:token_id Get all accounts that are being followed by EFP list id, excludes blocks and mutes #### Path Parameters - `token_id` (string): The EFP List of the account #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/exportState/333 ``` ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0x4c47ab777f1f64d1f3d6efbf1cc7ab5a5224af4a", "tags": [], }, { "version": 1, "record_type": "address", "data": "0x5b76f5b8fc9d700624f78208132f91ad4e61a1f0", "tags": [], }, { "version": 1, "record_type": "address", "data": "0x27cd3a463df1b3f6c95a222616d474be009c2cbb", "tags": [], }, { "version": 1, "record_type": "address", "data": "0xd1efdd037566b0c75cebace9150d26ea0153faa9", "tags": [], }, ], } ``` --- #### /lists/\:token_id/recommended Get recommended users for a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/1/recommended ``` ```jsonc // sample response { "recommended": [ { "name": "swindler.eth", "address": "0xf972bf8592c3171b378e97bb869a980c3f476583", "avatar": "https://rainbow.mypinata.cloud/ipfs/QmcSAHrGGdXJRPmxYUk1R86Wqpfgg4TPMAXC6MfQHPugvF", "header": "https://rainbow.mypinata.cloud/ipfs/QmVDbkDutSk4phVohMs76jV4RgT3bpSdzHnesBHFxW6jRL", "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, { "name": "gratefulape.eth", "address": "0x52a4c418576dc46e4116ececc6f68d1c9b9636ed", "avatar": "https://euc.li/gratefulape.eth", "header": null, "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, { "name": "treeskulltown.eth", "address": "0x2dacc0b072146b40e60b8596b99756112d45c924", "avatar": "https://euc.li/treeskulltown.eth", "header": null, "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, ... ] } ``` --- #### /lists/\:token_id/latestFollowers Get the latest followers (excluding blocked and muted) of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/4/latestFollowers ``` ```jsonc // sample response { "followers": [ { "address": "0xc6fbb49a26ffdba6b7f2bd1a85219831b2a2a7aa", "efp_list_nft_token_id": "26281", "updated_at": "2024-10-29T21:44:59.600Z" }, { "address": "0x410169a13a34b3760dd258343853b8895fe74adb", "efp_list_nft_token_id": "26267", "updated_at": "2024-10-29T14:00:40.549Z" }, { "address": "0x5cacbc9de3c67a2b8de680913e2d844c5cc8b6cc", "efp_list_nft_token_id": "26266", "updated_at": "2024-10-29T14:00:40.549Z" }, { "address": "0x1248ebadcba3ff83720b5be154dc8b74bbfa11c3", "efp_list_nft_token_id": "26265", "updated_at": "2024-10-29T13:58:40.542Z" }, ... ] } ``` --- #### /lists/\:token_id/stats Get stats of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `live` (bool, optional): Specifies whether to calculate stats or return cached values - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/3/stats?live=true ``` ```jsonc // sample response { "followers_count": "115", "following_count": "569", } ``` --- #### /lists/\:token_id/details Get details of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/3/details ``` ```jsonc // sample response { "address": "0xc9c3a4337a1bba75d0860a1a81f7b990dc607334", "ens": { "name": "0xthrpw.eth", "avatar": "https://euc.li/0xthrpw.eth", "header": "https://storage.googleapis.com/nftimagebucket/tokens/0xb7def63a9040ad5dc431aff79045617922f4023a/1897.svg", "display": "0xthrpw.eth", "records": { "avatar": "https://euc.li/0xthrpw.eth", "com.github": "0xthrpw", "com.twitter": "0xthrpw", "description": "Took the mirrors off my cadillac because I don't like looking like I look back.", "header": "https://storage.googleapis.com/nftimagebucket/tokens/0xb7def63a9040ad5dc431aff79045617922f4023a/1897.svg", "name": "throw;", }, "chains": { "eth": "0xC9C3A4337a1bba75D0860A1A81f7B990dc607334", }, "fresh": 1726680254493, "resolver": "0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63", "errors": {}, }, "ranks": { "mutuals_rank": "4", "followers_rank": "17", "following_rank": "18", "top8_rank": "12", "blocks_rank": "7", }, "primary_list": "3", } ``` --- #### /lists/\:token_id/account Get account information by their EFP list id #### Path Parameters - `token_id` (string): The EFP List of the account #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/3/details ``` ```jsonc // sample response { "address": "0xc9c3a4337a1bba75d0860a1a81f7b990dc607334", "ens": { "name": "0xthrpw.eth", "avatar": "https://euc.li/0xthrpw.eth", "header": "https://storage.googleapis.com/nftimagebucket/tokens/0xb7def63a9040ad5dc431aff79045617922f4023a/1897.svg", "display": "0xthrpw.eth", "records": { "avatar": "https://euc.li/0xthrpw.eth", "com.github": "0xthrpw", "com.twitter": "0xthrpw", "description": "Took the mirrors off my cadillac because I don't like looking like I look back.", "header": "https://storage.googleapis.com/nftimagebucket/tokens/0xb7def63a9040ad5dc431aff79045617922f4023a/1897.svg", "name": "throw;", }, "chains": { "eth": "0xC9C3A4337a1bba75D0860A1A81f7B990dc607334", }, "fresh": 1726679594366, "resolver": "0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63", "errors": {}, }, "ranks": { "mutuals_rank": "4", "followers_rank": "17", "following_rank": "18", "top8_rank": "12", "blocks_rank": "7", }, "primary_list": "3", } ``` --- #### /lists/\:token_id/allFollowingAddresses Get all accounts in list format, that are being followed (including blocked and muted) by a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/4/allFollowingAddresses ``` ```jsonc // sample response [ "0xc6ed8696c4885dcafdc73c5ef28511e02568b472", "0x1cbf9983e0d59276a58db8e8529706004fcb1837", "0x27d311b8958ca479615522304b442e530c8073fe", "0x47c0224f6c298c4b03f2fbbb986815859a0abd20", "0x50e97e480661533b5382e33705e4ce1eb182222e", "0x8480d20583a3138fef7c23eed8f17bf3c01e73b7", "0x97b5c5ac8813bf5aaf689bbb697b56f8d897baef", "0xa6bcb89f21e0bf71e08ded426c142757791e17cf", "0xdc27cb447d713a8320db054a39ab6a42e0af49cb", "0x02ca10c62f160cdd126d1e44ef42224cac745ac8", "0x0433062f9f466c4a184b2ba0e4da38efea5e2f87", ... ] ``` --- #### /lists/\:token_id/searchFollowers Search for followers of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `term` (string, optional): Specifies the string to search for in a followers address or ENS name #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/4/searchFollowers?term=crypt ``` ```jsonc // sample response { "followers": [ { "efp_list_nft_token_id": "99", "address": "0x19cf388796c31fa7a583270d82659ecd2b4fd490", "ens": { "name": "cryptomandias.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/cryptomandias.eth" }, "tags": [], "is_following": true, "is_blocked": false, "is_muted": false }, { "efp_list_nft_token_id": "553", "address": "0x3b30d44df9afffc07a51457e18410c4ca0f90896", "ens": { "name": "cryptodeadbeat.eth", "avatar": "https://euc.li/cryptodeadbeat.eth" }, "tags": [], "is_following": true, "is_blocked": false, "is_muted": false }, { "efp_list_nft_token_id": "88", "address": "0x5b0f3dbdd49614476e4f5ff5db6fe13d41fcb516", "ens": { "name": "efp.encrypteddegen.eth", "avatar": "https://euc.li/efp.encrypteddegen.eth" }, "tags": [ "top8" ], "is_following": true, "is_blocked": false, "is_muted": false }, ... ] } ``` --- #### /lists/\:token_id/badges Get EFP POAPs of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/9/badges ``` ```jsonc // sample response { "poaps": [ { "eventId": "177709", "participated": true, "collection": { "event": { "id": 177709, "fancy_id": "efp-beta-tester-2024", "name": "EFP Beta Tester", "event_url": "https://ethfollow.xyz", "image_url": "https://assets.poap.xyz/335d8e5a-920f-4c62-aa41-d8745f350a26.png", "country": "", "city": "", "description": "Thank you for helping us test EFP on testnet before our mainnet launch! You are truly an OG. 🤝", "year": 2024, "start_date": "06-Sep-2024", "end_date": "21-Sep-2024", "expiry_date": "21-Sep-2025" }, "tokenId": "7189762", "owner": "0xC9C3A4337a1bba75D0860A1A81f7B990dc607334" } }, { "eventId": "178064", "participated": false, "collection": null }, { "eventId": "178065", "participated": false, "collection": null } ... ] } ``` --- #### /lists/\:token_id/searchFollowing Search for following of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `term` (string, optional): Specifies the string to search for in a following address or ENS name #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/3/searchFollowing?term=bran ``` ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0x0ae93a80ef639c07ecf969735c9b3cc90ef6d803", "tags": [], "ens": { "name": "ens.brantly.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/ens.brantly.eth", }, }, { "version": 1, "record_type": "address", "data": "0xe0308a8a9095e4fc554fefdfafc819ff7b0f7103", "tags": [], "ens": { "name": "libran.eth", "avatar": "https://euc.li/libran.eth", }, }, { "version": 1, "record_type": "address", "data": "0x983110309620d911731ac0932219af06091b6744", "tags": ["top8"], "ens": { "name": "brantly.eth", "avatar": "https://euc.li/brantly.eth", }, }, ], } ``` --- #### /lists/\:token_id/allFollowers Get all followers (including blocked and muted) of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/4/allFollowers ``` ```jsonc // sample response { "followers": [ { "efp_list_nft_token_id": "15", "address": "0x849151d7d0bf1f34b70d5cad5149d28cc2308bf1", "tags": [], "is_following": true, "is_blocked": false, "is_muted": false, "updated_at": "2024-09-24T14:45:33.882Z" }, { "efp_list_nft_token_id": "294", "address": "0xa7860e99e3ce0752d1ac53b974e309fff80277c6", "tags": [ "top8" ], "is_following": false, "is_blocked": true, "is_muted": false, "updated_at": "2024-09-24T14:45:33.882Z" }, { "efp_list_nft_token_id": "55", "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "tags": [ "bff", "top8" ], "is_following": false, "is_blocked": true, "is_muted": false, "updated_at": "2024-09-24T16:45:26.088Z" }, ... ] } ``` --- #### /lists/\:token_id/taggedAs Get the tags that are applied to a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/41/taggedAs ``` ```jsonc // sample response { "token_id": "41", "tags": ["top8", "block"], "tagCounts": [ { "tag": "top8", "count": 7, }, { "tag": "block", "count": 1, }, ], "taggedAddresses": [ { "address": "0xf9a24785cab3ed0921c41fb84dedfea935a4ad1b", "tag": "top8", }, { "address": "0x8eddf5431f5b31933bfbd8111d54fc6e9456e6c1", "tag": "top8", }, { "address": "0x8b24b1686832757e2f6d640e11e88e7f0064594a", "tag": "top8", }, { "address": "0x60377ec355857c2d06d1ce28555f624257344b0d", "tag": "top8", }, { "address": "0xfa1afc4534fc9f80a552e61dd04cd8a172c821a6", "tag": "top8", }, { "address": "0xc808ffa16d6773d6a9109b1ab92e839157eb0954", "tag": "block", }, { "address": "0x983110309620d911731ac0932219af06091b6744", "tag": "top8", }, { "address": "0x2a59071ff48936c6838dcac425fa0df6ea5979bf", "tag": "top8", }, ], } ``` --- #### /lists/\:token_id/tags Get the tags of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/3/tags ``` ```jsonc // sample response { "token_id": "3", "tags": ["vogu", "top8"], "tagCounts": [ { "tag": "vogu", "count": 1, }, { "tag": "top8", "count": 8, }, ], "taggedAddresses": [ { "address": "0x0f2e3e67cb000993d07e60261748963d3f4bd6d9", "tag": "vogu", }, { "address": "0x71adb34117c9408e74ed112b327a0ec97cef8fa1", "tag": "top8", }, { "address": "0x8f5906963ae276e1631efa8ff1a9cae6499ec5e3", "tag": "top8", }, { "address": "0x983110309620d911731ac0932219af06091b6744", "tag": "top8", }, { "address": "0xbe4f0cdf3834bd876813a1037137dcfad79acd99", "tag": "top8", }, { "address": "0xc983ebc9db969782d994627bdffec0ae6efee1b3", "tag": "top8", }, { "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", "tag": "top8", }, { "address": "0xe2cded674643743ec1316858dfd4fd2116932e63", "tag": "top8", }, { "address": "0xeb6b293e9bb1d71240953c8306ad2c8ac523516a", "tag": "top8", }, ], } ``` --- #### /lists/\:token_id/\:addressOrENS/buttonState Get the following state between a given list and a given user. #### Path Parameters - `token_id` (string): The EFP List of the account - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/3/brantly.eth/buttonState ``` ```jsonc // sample response { "token_id": "3", "address": "0x983110309620d911731ac0932219af06091b6744", "state": { "follow": true, "block": false, "mute": false, }, } ``` --- #### /lists/\:token_id/following Get accounts being followed (excluding blocked and muted) by a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. - `sort` (string, optional): Specifies how the results should be sorted, possible values 'latest', 'earliest', 'followers'. If not specified, default sort is 'latest' - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/4/following ``` ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0xc6ed8696c4885dcafdc73c5ef28511e02568b472", "tags": [] }, { "version": 1, "record_type": "address", "data": "0x1cbf9983e0d59276a58db8e8529706004fcb1837", "tags": [] }, { "version": 1, "record_type": "address", "data": "0x27d311b8958ca479615522304b442e530c8073fe", "tags": [] }, ... ] } ``` --- #### /lists/\:token_id/allFollowing Get all accounts being followed (including blocked and muted) by a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/4/allFollowing ``` ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0xad73eafcac4f4c6755dfc61770875fb8b6bc8a25", "tags": [] }, { "version": 1, "record_type": "address", "data": "0xfee41e0f01112d9bdaa73a5a368f4afb4d9baa08", "tags": [] }, { "version": 1, "record_type": "address", "data": "0xf972bf8592c3171b378e97bb869a980c3f476583", "tags": [] }, ... ] } ``` --- #### /lists/\:token_id/\:addressOrENS/followerState Get the follower state between a given list and a given user. #### Path Parameters - `token_id` (string): The EFP List of the account - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/3/encrypteddegen.eth/followerState ``` ```jsonc // sample response { "token_id": "3", "address": "0xc983ebc9db969782d994627bdffec0ae6efee1b3", "state": { "follow": true, "block": false, "mute": false, }, } ``` --- #### /lists/\:token_id/followers Get followers (excluding blocked and muted) of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. - `sort` (string, optional): Specifies how the results should be sorted, possible values 'latest', 'earliest', 'followers'. If not specified, default sort is 'latest' - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/lists/4/followers ``` ```jsonc // sample response { "followers": [ { "efp_list_nft_token_id": "325", "address": "0x9b2fb7a8d227cdaa8002f80e8c8a99a19bb1b969", "tags": [], "is_following": true, "is_blocked": false, "is_muted": false, "updated_at": "2024-09-24T16:45:26.088Z" }, { "efp_list_nft_token_id": "728", "address": "0xca034d4438719391b5e7589242a36ec535ed6836", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-09-24T16:45:26.088Z" }, { "efp_list_nft_token_id": "723", "address": "0x8901083bb577b335a5f6fddde705c00efe8c33d9", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-09-24T16:45:26.088Z" }, ... ] } ``` --- ### /users/\:addressOrENS/list-records Get the list records of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/encrypteddegen.eth/list-records ``` ```jsonc // sample response { "records": [ { "version": 1, "record_type": "address", "data": "0x0116acf39cf70fefc3c23c88a37e59474e8e17be", "tags": null }, { "version": 1, "record_type": "address", "data": "0x021021ccee934b346160342f8d7f59f514c08c56", "tags": null }, { "version": 1, "record_type": "address", "data": "0x025376e7e7f161a198fb5fc90a220a553836d11a", "tags": null }, ... ] } ``` --- ### /users/\:addressOrENS/recommended Get recommended users for a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/recommended ``` ```jsonc // sample response { "recommended": [ { "name": "swindler.eth", "address": "0xf972bf8592c3171b378e97bb869a980c3f476583", "avatar": "https://rainbow.mypinata.cloud/ipfs/QmcSAHrGGdXJRPmxYUk1R86Wqpfgg4TPMAXC6MfQHPugvF", "header": "https://rainbow.mypinata.cloud/ipfs/QmVDbkDutSk4phVohMs76jV4RgT3bpSdzHnesBHFxW6jRL", "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, { "name": "gratefulape.eth", "address": "0x52a4c418576dc46e4116ececc6f68d1c9b9636ed", "avatar": "https://euc.li/gratefulape.eth", "header": null, "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, { "name": "treeskulltown.eth", "address": "0x2dacc0b072146b40e60b8596b99756112d45c924", "avatar": "https://euc.li/treeskulltown.eth", "header": null, "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, ... } ``` --- ### /users/\:addressOrENS/lists Get the lists of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/brantly.eth/lists ``` ```jsonc // sample response { "primary_list": "4", "lists": ["4", "107"], } ``` --- ### /users/\:addressOrENS/latestFollowers Get a user's latest followers by Address or ENS Name #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl http://api.ethfollow.xyz/api/v1/users/dr3a.eth/latestFollowers ``` ```jsonc // sample response { "followers": [ { "address": "0x9bfd90ab010c63e85453f3e40be4e3106ccf895c", "efp_list_nft_token_id": "3413", "updated_at": "2024-10-29T16:13:15.040Z" }, { "address": "0x2e11e3b40ca0c4aba93a2cd7c9046394b8dd7501", "efp_list_nft_token_id": "26124", "updated_at": "2024-10-28T15:26:19.215Z" }, { "address": "0xfdacb11c3b6703bc5525b650bd231046b3f878cd", "efp_list_nft_token_id": "26123", "updated_at": "2024-10-28T15:24:07.275Z" }, { "address": "0x22d10dd07ad572f6d53a70a4093e2d8761a3e650", "efp_list_nft_token_id": "26110", "updated_at": "2024-10-28T12:53:02.481Z" }, ... ] } ``` --- ### /users/\:addressOrENS/notifications Get incoming actions received from other users by Address or ENS Name #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `opcode` (number, optional): Specifies the type of operation to return [1 for follow, 2 for un-follow, 3 for tag, 4 for un-tag, 0 for any] - `interval` (string, optional): Specifies the time range of notifications to return [hour, day, week, month, all]. - `start` (number, optional): Specifies the starting timestamp to begin the interval, default value is now. [unix timestamp ex. 1741159543] - `tag` (string, optional): Specifies a single tag string, of which each account in the response should have at least one. - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Default Values A request to this endpoint with no query parameters specified will default to: - opcode = all - interval = week - start = (current timestamp of now) - tag = "" - limit = 10 - offset = 0 Take care not to request tags joined with incorrect opcodes i.e. requests with a tag specified but opcode = 2 will not return data #### Sample Query: No params set ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/notifications ``` ```jsonc // sample response { "summary": { "interval": "168:00:00(hrs)", "opcode": "all", "total": 10, "total_follows": 9, "total_unfollows": 0, "total_tags": 1, "total_untags": 0 }, "notifications": [ { "address": "0x8004f955c7ed19b465f1f30ad7d04c6d2edc4e81", "name": "jackflash.eth", "avatar": "https://euc.li/jackflash.eth", "token_id": "25556", "action": "tag", "opcode": 3, "op": "0x01030101c9c3a4337a1bba75d0860a1a81f7b990dc607334646576", "tag": "dev", "updated_at": "2024-12-04T04:07:55.948Z" }, { "address": "0xce89b39a2c5d66040093df8013f02d1f0a124200", "name": "nomamkin.eth", "avatar": "https://ens.xyz/nomamkin.eth", "token_id": "28502", "action": "follow", "opcode": 1, "op": "0x01010101c9c3a4337a1bba75d0860a1a81f7b990dc607334", "tag": "", "updated_at": "2024-12-03T12:15:42.656Z" }, { "address": "0x69a00fafe7e935fde9ecb5c53f23e0e409a33e12", "name": "myavocado.eth", "avatar": "https://euc.li/myavocado.eth", "token_id": "28491", "action": "follow", "opcode": 1, "op": "0x01010101c9c3a4337a1bba75d0860a1a81f7b990dc607334", "tag": "", "updated_at": "2024-12-03T11:28:45.493Z" }, ... ] } ``` #### Sample Query: Follows in last week ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/notifications?opcode=1&interval=week ``` ```jsonc // sample response { "summary": { "interval": "168:00:00(hrs)", "opcode": "1", "total": 10, "total_follows": 10, "total_unfollows": 0, "total_tags": 0, "total_untags": 0 }, "notifications": [ { "address": "0xce89b39a2c5d66040093df8013f02d1f0a124200", "name": "nomamkin.eth", "avatar": "https://ens.xyz/nomamkin.eth", "token_id": "28502", "action": "follow", "opcode": 1, "op": "0x01010101c9c3a4337a1bba75d0860a1a81f7b990dc607334", "tag": "", "updated_at": "2024-12-03T12:15:42.656Z" }, { "address": "0x69a00fafe7e935fde9ecb5c53f23e0e409a33e12", "name": "myavocado.eth", "avatar": "https://euc.li/myavocado.eth", "token_id": "28491", "action": "follow", "opcode": 1, "op": "0x01010101c9c3a4337a1bba75d0860a1a81f7b990dc607334", "tag": "", "updated_at": "2024-12-03T11:28:45.493Z" }, { "address": "0x11a0cbe3548636d02506e945c77b17c5d3fd5fd5", "name": "senior01.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/senior01.eth", "token_id": "31911", "action": "follow", "opcode": 1, "op": "0x01010101c9c3a4337a1bba75d0860a1a81f7b990dc607334", "tag": "", "updated_at": "2024-12-03T11:22:00.378Z" }, ... ] } ``` #### Sample Query: All cases where tag is 'top8' ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/notifications?interval=all&tag=top8 ``` ```jsonc // sample response { "summary": { "interval": "999:00:00(hrs)", "opcode": "all", "total": 4, "total_follows": 0, "total_unfollows": 0, "total_tags": 4, "total_untags": 0 }, "notifications": [ { "address": "0x2e711004fef4751b62aeb3608d722d22ce536d84", "name": "10bitcoin.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/10bitcoin.eth", "token_id": "30340", "action": "tag", "opcode": 3, "op": "0x01030101c9c3a4337a1bba75d0860a1a81f7b990dc607334746f7038", "tag": "top8", "updated_at": "2024-11-22T07:30:15.626Z" }, { "address": "0x9a4c6ec8af626ae0c214c3bdd14ac56be15aaefd", "name": "lagovskiiigor.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/lagovskiiigor.eth", "token_id": "30054", "action": "tag", "opcode": 3, "op": "0x01030101c9c3a4337a1bba75d0860a1a81f7b990dc607334746f7038", "tag": "top8", "updated_at": "2024-11-21T06:01:02.407Z" }, ... ] } ``` --- ### /users/\:addressOrENS/stats Get stats of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `live` (bool, optional): Specifies whether to calculate stats or return cached values - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/limes.eth/stats ``` ```jsonc // sample response { "followers_count": "104", "following_count": "26", } ``` --- ### /users/\:addressOrENS/details Get account details, populates most of the data on a profile card #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/details ``` ```jsonc // sample response { "address": "0xc9c3a4337a1bba75d0860a1a81f7b990dc607334", "ens": { "name": "0xthrpw.eth", "avatar": "https://euc.li/0xthrpw.eth", "records": { "avatar": "https://euc.li/0xthrpw.eth", "com.github": "0xthrpw", "com.twitter": "0xthrpw", "description": "Took the mirrors off my cadillac because I don't like looking like I look back.", "header": "https://storage.googleapis.com/nftimagebucket/tokens/0xb7def63a9040ad5dc431aff79045617922f4023a/1897.svg", "name": "throw;", }, "updated_at": "2024-09-18T02:12:57.934Z", }, "ranks": { "mutuals_rank": "6", "followers_rank": "19", "following_rank": "18", "top8_rank": "12", "blocks_rank": "7", }, "primary_list": "3", } ``` --- ### /users/\:addressOrENS/commonFollowers Get common followers that are shared by two accounts #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `leader` (address, required): Specifies the account whose followers should be compared #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/commonFollowers?leader=0x0312567d78ff0c9ce0bd62a250df5c6474c71334 ``` ```jsonc // sample response { "results": [ { "address": "0x0312567d78ff0c9ce0bd62a250df5c6474c71334", "name": "pepe.eth", "avatar": "https://preview.redd.it/23tzr9qimgf51.png?auto=webp&s=d5475b2c247d3f3b4c8d9d3d3cae2521e15437ef", "mutuals_rank": "10" }, { "address": "0x038b716928a41ea42253ac043af4f8fdcd940098", "name": "aaron.box", "avatar": "https://metadata.ens.domains/mainnet/avatar/aaron.box", "mutuals_rank": "108" }, { "address": "0x074470b9a32f68de86fac393a10d5cea01c54269", "name": "pawswap.eth", "avatar": "https://euc.li/pawswap.eth", "mutuals_rank": "18" }, { "address": "0x074631095645e426e50b478d40301dd35e74f24c", "name": "pasqui.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/pasqui.eth", "mutuals_rank": "201" }, ... ], "length": 76 } ``` --- ### /users/\:addressOrENS/qr Get a QR code that links to a user's profile page. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/brantly.eth/qr ``` #### Response This endpoint responds with a formatted svg of content-type `image/svg+xml;charset=utf-8` ![Example QR code response](/assets/qrcode.png) --- ### /users/\:addressOrENS/account Get account information by Address or ENS Name #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh curl http://api.ethfollow.xyz/api/v1/users/dr3a.eth/account ``` ```jsonc // sample response { "address": "0xeb6b293e9bb1d71240953c8306ad2c8ac523516a", "ens": { "name": "dr3a.eth", "avatar": "https://avatar-upload.ens-cf.workers.dev/mainnet/dr3a.eth?timestamp=1681787731836", "records": { "avatar": "https://avatar-upload.ens-cf.workers.dev/mainnet/dr3a.eth?timestamp=1681787731836", "com.discord": "dr3a.eth", "com.twitter": "dr3a_eth", "description": "dr3a.eth 💙", "email": "dr3a.eth@skiff.com", "name": "drea", "org.telegram": "dr3adoteth", "url": "https://dr3a.eth.limo", }, "updated_at": "2024-09-18T01:54:52.959Z", }, } ``` --- ### /users/\:addressOrENS/searchFollowers Search for followers of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `term` (string, optional): Specifies the string to search for in a followers address or ENS name #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/searchFollowers?term=brant ``` ```jsonc // sample response { "followers": [ { "efp_list_nft_token_id": "4", "address": "0x983110309620d911731ac0932219af06091b6744", "ens": { "name": "brantly.eth", "avatar": "https://euc.li/brantly.eth", }, "tags": [], "is_following": true, "is_blocked": false, "is_muted": false, }, ], } ``` --- ### /users/\:addressOrENS/badges Get EFP POAPs of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/badges ``` ```jsonc // sample response { "poaps": [ { "eventId": "177709", "participated": true, "collection": { "event": { "id": 177709, "fancy_id": "efp-beta-tester-2024", "name": "EFP Beta Tester", "event_url": "https://ethfollow.xyz", "image_url": "https://assets.poap.xyz/335d8e5a-920f-4c62-aa41-d8745f350a26.png", "country": "", "city": "", "description": "Thank you for helping us test EFP on testnet before our mainnet launch! You are truly an OG. 🤝", "year": 2024, "start_date": "06-Sep-2024", "end_date": "21-Sep-2024", "expiry_date": "21-Sep-2025" }, "tokenId": "7189762", "owner": "0xC9C3A4337a1bba75D0860A1A81f7B990dc607334" } }, { "eventId": "178064", "participated": false, "collection": null }, { "eventId": "178065", "participated": false, "collection": null } ... ] } ``` --- ### /users/\:addressOrENS/primary-list Get the primary list of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/encrypteddegen.eth/primary-list ``` ```jsonc // sample response { "primary_list": "1", } ``` --- ### /users/\:addressOrENS/searchFollowing Search for following of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `term` (string, optional): Specifies the string to search for in a following address or ENS name #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/searchFollowing?term=degen ``` ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0x96053204967c30079529adddc56f6a37380205af", "tags": [], "ens": { "name": "alphadegen.eth", "avatar": "https://bafybeiaqof5u4bj57t36pt2t7egerky6epvutg7yb4suljnnjuqboymhvi.ipfs.dweb.link/af1f0c19f22c6ee0ea9a9e5f89d585df1ab8c677ef0ef7f0a448cce0fef21a71.png" } }, { "version": 1, "record_type": "address", "data": "0x69207d197063c6b207ff206fdba916e1700d60fa", "tags": [], "ens": { "name": "degenfam.eth", "avatar": "https://codemakes.art/image/quasars/2631" } }, { "version": 1, "record_type": "address", "data": "0x70bb434ea7b7f14709ed0dd17cc54056812cf4ad", "tags": [], "ens": { "name": "teradegen.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/teradegen.eth" } }, } ``` --- ### /users/\:addressOrENS/ens Get the ENS data of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/brantly.eth/ens ``` ```jsonc // sample response { "ens": { "name": "brantly.eth", "address": "0x983110309620d911731ac0932219af06091b6744", "avatar": "https://euc.li/brantly.eth", "records": { "avatar": "https://euc.li/brantly.eth", "com.discord": "brantly.eth", "com.github": "brantlymillegan", "com.twitter": "brantlymillegan", "description": "Catholic, husband, father | building @efp.eth | ENS (DAO delegate, former core team) | Sign-in with Ethereum (creator)", "email": "me@brantly.xyz", "header": "https://i.imgur.com/Quo06x2.png", "location": "USA", "name": "Brantly Millegan", "org.telegram": "brantlymillegan", "url": "https://efp.app/", }, "updated_at": "2024-09-18T03:40:58.807Z", }, } ``` --- ### /users/\:addressOrENS/taggedAs Get the tags that are applied to a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/taggedAs ``` ```jsonc // sample response { "address": "0xc9c3a4337a1bba75d0860a1a81f7b990dc607334", "tags": ["top8"], "tagCounts": [ { "tag": "top8", "count": 5, }, ], "taggedAddresses": [ { "address": "0x5a3bf42028901447434d12c5459954e667e5c518", "tag": "top8", }, { "address": "0x71adb34117c9408e74ed112b327a0ec97cef8fa1", "tag": "top8", }, { "address": "0x8eddf5431f5b31933bfbd8111d54fc6e9456e6c1", "tag": "top8", }, { "address": "0xfa1afc4534fc9f80a552e61dd04cd8a172c821a6", "tag": "top8", }, { "address": "0xc983ebc9db969782d994627bdffec0ae6efee1b3", "tag": "top8", }, ], } ``` --- ### /users/\:addressOrENS/tags Get the tags of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/brantly.eth/tags ``` ```jsonc // sample response { "address": "0x983110309620d911731ac0932219af06091b6744", "tags": [ "top8", "block", "degen" ], "tagCounts": [ { "tag": "top8", "count": 8 }, { "tag": "block", "count": 2 }, { "tag": "degen", "count": 4 } ], "taggedAddresses": [ { "address": "0x44a3a18df15ae79bbc6c660db59428fe9a181864", "tag": "top8" }, { "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "tag": "block" }, { "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "tag": "degen" }, { "address": "0x60377ec355857c2d06d1ce28555f624257344b0d", "tag": "top8" }, ... ] } ``` --- ### /users/\:addressOrENS/following Get following by Address or ENS Name #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/dr3a.eth/following ``` #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. - `sort` (string, optional): Specifies how the results should be sorted, possible values 'latest', 'earliest', 'followers'. If not specified, default sort is 'latest' - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0x983110309620d911731ac0932219af06091b6744", "tags": ["efp", "ens"], }, { "version": 1, "record_type": "address", "data": "0xbdb41bff7e828e2dc2d15eb67257455db818f1dc", "tags": ["efp", "ens"], }, { "version": 1, "record_type": "address", "data": "0xf4212614c7fe0b3feef75057e88b2e77a7e23e83", "tags": ["efp"], }, ], } ``` --- ### /users/\:addressOrENS/\:addressOrENS2/followerState Get the follower state between two users. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. - `addressOrENS2` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/brantly.eth/followerState ``` ```jsonc // sample response { "addressUser": "0xc9c3a4337a1bba75d0860a1a81f7b990dc607334", "addressFollower": "0x983110309620d911731ac0932219af06091b6744", "state": { "follow": true, "block": false, "mute": false, }, } ``` --- ### /users/\:addressOrENS/followers Get followers by Address or ENS Name #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. - `sort` (string, optional): Specifies how the results should be sorted, possible values 'latest', 'earliest', 'followers'. If not specified, default sort is 'latest' - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl http://api.ethfollow.xyz/api/v1/users/dr3a.eth/followers ``` ```jsonc // sample response { "followers": [ { "efp_list_nft_token_id": "5895", "address": "0xd56c76b3f924e8f84a02654ff072a363a84b91d9", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-10-14T19:45:38.617Z", }, { "efp_list_nft_token_id": "6337", "address": "0x907ed289f363dbdb2ab1230dfbd2f77a05cda82d", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-10-14T17:47:20.727Z", }, { "efp_list_nft_token_id": "13986", "address": "0x7766ef005ec1b38a8472831e2f0631b12c811a5f", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-10-14T17:12:28.718Z", }, { "efp_list_nft_token_id": "6323", "address": "0x4e203e4f4bbf119f4e83763d5b143316b3b3c6cc", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-10-14T15:06:03.633Z", }, ], } ``` --- EFP provides an open source indexer and API for indexing and retrieving EFP data. ### Public API EFP core team provides a public API endpoint at [`https://api.ethfollow.xyz/api/v1`](https://api.ethfollow.xyz/api/v1). If you are a developer, you are free to use this endpoint to retrieve EFP data. ### Commonly used endpoints - [User Stats](https://ethidentitykit.com/docs/api/users/stats/): the follower and following counts of a particular user - [User Following](https://ethidentitykit.com/docs/api/users/following/): list of the accounts a particular user follows - [User Followers](https://ethidentitykit.com/docs/api/users/followers/): list of the accounts that follow a particular user - [User ENS data](https://ethidentitykit.com/docs/api/users/ens/): the ENS data for a particular user ### Self-hosting You may also fork EFP's Indexer/API source code and deploy yourself. - [`ethereumfollowprotocol/api`](https://github.com/ethereumfollowprotocol/api) - [`ethereumfollowprotocol/indexer`](https://github.com/ethereumfollowprotocol/indexer) ### KV cache The API uses Cloudflare KV storage to cache some endpoint's responses. When one of these endpoints is called the cache is checked and if there is no record the data is fetched from the database. If a cached record is found it is returned immediately. All cache records are set to expire 5 minutes after they are created. The cached record for each of these endpoints can be refreshed by adding `cache=fresh` to the query params. For example: Get the cached record ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/1/details ``` Get latest data and reset the cached record ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/1/details?cache=fresh ``` ### Questions If you come across any issues, please reach out to us in our [Discord](https://discord.com/invite/ZUyG3mSXFD). --- #### /leaderboard/blocked Get leaderboard of users ranked according to count of users that blocked them. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/leaderboard/blocked ``` ```jsonc // sample response [ { "rank": 1, "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "blocked_by_count": "8" }, { "rank": 2, "address": "0xa7860e99e3ce0752d1ac53b974e309fff80277c6", "blocked_by_count": "4" }, { "rank": 3, "address": "0x3276e82ebb1b4b9f01ab9286ed6bcc6603e368e2", "blocked_by_count": "2" }, { "rank": 4, "address": "0x7265a60acaeaf3a5e18e10bc1128e72f27b2e176", "blocked_by_count": "2" }, ... ] ``` --- #### /leaderboard/all Get addresses and ens names of all leaderboard records. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/leaderboard/all ``` ```jsonc // sample response { "results": [ { "address": "0x0ad4bb5ceabfdb5020b01e6dc5e32526eb10e5d1", "name": "0xsailormoon.eth" }, { "address": "0xd1efdd037566b0c75cebace9150d26ea0153faa9", "name": "designer.eth" }, { "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "name": "garypalmerjr.eth" }, { "address": "0x14546125429faac7f3aa78da1807069692ec7464", "name": "grado.eth" }, ... ] } ``` --- #### /leaderboard/ranked Get leaderboard of users ranked according to count of mutual follows. Includes rankings for mutuals, followers, following, blocked and tagged as 'top8'. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 50. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `sort` (string, optional): Specifies ranking to sort on, possible values are 'mutuals', 'followers', 'following', 'blocked' and 'top8', default value is 'mutuals'. - `direction` (string, optional): Specifies direction to sort results, possible values are 'ASC' or 'DESC' default value is 'DESC'. - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/leaderboard/ranked?sort=mutuals&direction=desc ``` ```jsonc // sample response { "last_updated": "2024-09-18T19:11:43.210Z", "results": [ { "address": "0x983110309620d911731ac0932219af06091b6744", "name": "brantly.eth", "avatar": "https://euc.li/brantly.eth", "header": "https://i.imgur.com/Quo06x2.png", "mutuals_rank": "1", "followers_rank": "1", "following_rank": "6", "blocks_rank": "7", "top8_rank": "1", "mutuals": "293", "following": "1079", "followers": "366", "blocks": "1", "top8": "45", "updated_at": "2024-09-18T19:11:43.210Z" }, { "address": "0xd1efdd037566b0c75cebace9150d26ea0153faa9", "name": "designer.eth", "avatar": "https://euc.li/designer.eth", "header": null, "mutuals_rank": "2", "followers_rank": "6", "following_rank": "4", "blocks_rank": "7", "top8_rank": "7", "mutuals": "147", "following": "1556", "followers": "182", "blocks": "1", "top8": "7", "updated_at": "2024-09-18T19:11:43.210Z" }, { "address": "0x2a59071ff48936c6838dcac425fa0df6ea5979bf", "name": "mely.eth", "avatar": "https://euc.li/mely.eth", "header": "https://i.imgur.com/T2H8N2H.jpeg", "mutuals_rank": "3", "followers_rank": "12", "following_rank": "28", "blocks_rank": "7", "top8_rank": "5", "mutuals": "124", "following": "354", "followers": "138", "blocks": "1", "top8": "11", "updated_at": "2024-09-18T19:11:43.210Z" }, ... ] } ``` --- #### /leaderboard/search Search for leaderboard addresses and ENS names by a specified search term. #### Query Parameters - `term` (string, optional): Specifies the string to search for in a leaderboard address or ENS name. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/leaderboard/search?term=eth ``` ```jsonc // sample response { "last_updated": "2024-09-18T19:11:43.210Z", "results": [ { "address": "0x983110309620d911731ac0932219af06091b6744", "name": "brantly.eth", "avatar": "https://euc.li/brantly.eth", "header": "https://i.imgur.com/Quo06x2.png", "mutuals_rank": "1", "followers_rank": "1", "following_rank": "6", "blocks_rank": "7", "mutuals": "293", "following": "1079", "followers": "366", "blocks": "1", "updated_at": "2024-09-18T19:11:43.210Z" }, { "address": "0xd1efdd037566b0c75cebace9150d26ea0153faa9", "name": "designer.eth", "avatar": "https://euc.li/designer.eth", "header": null, "mutuals_rank": "2", "followers_rank": "6", "following_rank": "4", "blocks_rank": "7", "mutuals": "147", "following": "1556", "followers": "182", "blocks": "1", "updated_at": "2024-09-18T19:11:43.210Z" }, { "address": "0x2a59071ff48936c6838dcac425fa0df6ea5979bf", "name": "mely.eth", "avatar": "https://euc.li/mely.eth", "header": "https://i.imgur.com/T2H8N2H.jpeg", "mutuals_rank": "3", "followers_rank": "12", "following_rank": "28", "blocks_rank": "7", "mutuals": "124", "following": "354", "followers": "138", "blocks": "1", "updated_at": "2024-09-18T19:11:43.210Z" }, ... ] } ``` --- #### /leaderboard/muted Get leaderboard of users ranked according to count of users that muted them. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/leaderboard/muted ``` ```jsonc // sample response [ { "rank": 1, "address": "0x949e2988b857af2a3c9429e763d13202b7b25c88", "muted_by_count": "1", }, ] ``` --- #### /leaderboard/count Get count of all accounts in the leaderboard. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/leaderboard/count ``` ```jsonc // sample response { "leaderboardCount": "8788", } ``` --- #### /leaderboard/mutes Get leaderboard of users ranked according to count of users that they muted. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/leaderboard/mutes ``` ```jsonc // sample response [ { "rank": 2, "address": "0xfa35730094b7e0fc1b97f55663aad15e2d2e3e29", "mutes_count": "3", }, { "rank": 3, "address": "0x983110309620d911731ac0932219af06091b6744", "mutes_count": "2", }, { "rank": 4, "address": "0x2933387ec4c9bbc4a8200cfd77db53d7bc8ebc11", "mutes_count": "1", }, ] ``` --- #### /leaderboard/following Get leaderboard of users ranked according to following counts. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/leaderboard/following ``` ```jsonc // sample response [ { "rank": 1, "address": "0x44a3a18df15ae79bbc6c660db59428fe9a181864", "following_count": "5639" }, { "rank": 2, "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "following_count": "3403" }, { "rank": 3, "address": "0xd4713cca4068700cf722f8c2b6c05f948b75321b", "following_count": "2782" }, { "rank": 4, "address": "0xd1efdd037566b0c75cebace9150d26ea0153faa9", "following_count": "1556" }, ... ] ``` --- #### /leaderboard/blocks Get leaderboard of users ranked according to count of users that they blocked. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/leaderboard/blocks ``` ```jsonc // sample response [ { "rank": 1, "address": "0xc808ffa16d6773d6a9109b1ab92e839157eb0954", "blocks_count": "119" }, { "rank": 2, "address": "0xfa35730094b7e0fc1b97f55663aad15e2d2e3e29", "blocks_count": "3" }, { "rank": 3, "address": "0x983110309620d911731ac0932219af06091b6744", "blocks_count": "2" }, { "rank": 4, "address": "0x2933387ec4c9bbc4a8200cfd77db53d7bc8ebc11", "blocks_count": "1" }, ... ] ``` --- #### /leaderboard/followers Get leaderboard of users ranked according to follower counts. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/leaderboard/followers ``` ```jsonc // sample response [ { "rank": 1, "address": "0x983110309620d911731ac0932219af06091b6744", "followers_count": 365 }, { "rank": 2, "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", "followers_count": 285 }, { "rank": 3, "address": "0x849151d7d0bf1f34b70d5cad5149d28cc2308bf1", "followers_count": 270 }, { "rank": 4, "address": "0x5b76f5b8fc9d700624f78208132f91ad4e61a1f0", "followers_count": 228 }, ... ] ``` --- # Followers you know The Followers you know component displays a list of common followers between two Ethereum addresses or ENS names, showing their avatars and names. Followers you know is a relation of addresses that `connectedAddress` follows and `lookupAddress` is being followed by. ### Add to your project ```tsx copy import { FollowersYouKnow } from 'ethereum-identity-kit' export default function Home() { return } ``` Try it out! {' - https://playground.ethidentitykit.com/?path=/story/molecules-followers-you-know--followers-you-know'}
{ window.open(`https://efp.app/${address}`, '_blank') }} />
## Parameters | Parameter | Description | Required | Default Value | | ------------------ | ------------------------------------------------------------------------- | -------- | ------------- | | `lookupAddress` | The Ethereum address or ENS name to find common followers with. | Yes | - | | `connectedAddress` | The Ethereum address of the currently connected user. | No | - | | `displayEmpty` | Whether to display the component when there are no common followers. | No | true | | `hasModal` | Whether to display the modal with all common followers. | No | false | | `onProfileClick` | The function to call when a profile is clicked. | No | - | | `className` | Additional CSS class names to apply to the component. | No | - | | `props` | Additional props for the `
` element containing the common followers. | No | - | --- # Profile Socials The Profile Socials component displays the social links of a user, including URLs and decentralized web links. ### Add to your project ```tsx copy import { ProfileSocials } from 'ethereum-identity-kit' export default function Home() { return } ``` Try it out! {' - https://playground.ethidentitykit.com/?path=/docs/molecules-profile-socials--component-docs'}
## Parameters | Parameter | Description | Required | Default Value | | ------------- | ---------------------------------------------------------------------------- | -------- | ------------- | | `userAddress` | The Ethereum address of the user. | Yes | - | | `name` | The ENS name of the user. | No | - | | `records` | The records of the user, including URLs and content hashes. | Yes | - | | `darkMode` | Whether the profile is in dark mode. | No | false | | `includeUrls` | Whether to include the URLs in the profile. | No | false | | `iconSize` | The size of the icons displayed. | No | 32 | | `isLoading` | Whether the profile is loading, showing placeholders instead of actual data. | No | false | ### Styling The component uses predefined styles and can be further customized using the `className` prop. The appearance of the modal can be manually toggled between light and dark modes using the `darkMode` prop, however, if you have a `dark` class applied in your application, the modal will automatically use that. --- # Transaction Modal The Transaction Modal component allows users to initiate and manage on-chain transactions. It provides a user interface for viewing transaction details, selecting chains, and managing transaction batches. ## Add to your project ```tsx copy import TransactionModal from 'identity-kit/src/components/transaction-modal/TransactionModal' export default function App() { return } ``` Try it out! { ' - https://playground.ethidentitykit.com/?path=/story/organisms-follow-button-transaction-modal--follow-button-single-tx' }
The transaction modal displayed here has the "batchTransactions" prop set to false. Meaning it will open only if you submit a transaction. Pressing the follow button will open the transaction modal.
### First time user flow
## Parameters | Parameter | Description | Required | Default Value | | --------------------- | -------------------------------------------------------------- | -------- | ------------- | | `darkMode` | Enables dark mode styling for the transaction modal. | No | false | | `showRecommendations` | Enables recommendations for the cart modal. | No | true | | `onCartProfileClick` | The function to call when a profile is clicked. | No | - | | `showPoapClaim` | Whether to show the POAP claim modal after minting a new list. | No | false | | `className` | Additional CSS class names to apply to the transaction modal. | No | - | | `props` | Additional HTML div element props. | No | - | ### Features - **Batch Transactions**: Allows users to manage multiple transactions at once. - **Chain Selector**: Provides an interface for selecting the blockchain network. - **Transaction Summary**: Displays a summary of all pending transactions. - **Transaction Flow**: Nice transaction flow with good visual feedback and animations. ### Styling The component uses predefined styles and can be further customized using the `className` prop. The appearance of the modal can be manually toggled between light and dark modes using the `darkMode` prop, however, if you have a `dark` class applied in your application, the modal will automatically use that. ### Notes - The modal is controlled by the `txModalOpen` state from the [`useTransactions`](https://ethidentitykit.com/docs/hooks/useTransactions) context, which determines its visibility. - Ensure that the `TransactionModal` is wrapped within a [TransactionProvider](https://ethidentitykit.com/docs/components/transaction-provider) that supplies the necessary transaction context. Transaction Provider parameters: - batchTransactions (boolean) - Whether to batch transactions or not (default: false). This will allow yout to either execute transactions one by one or batch them together and have a cart-like experience. --- # Avatar The Avatar component displays an avatar image for a given Ethereum address or ENS name, with support for fallback images. ### Add to your project ```tsx copy import { Avatar } from 'ethereum-identity-kit' export default function Home() { return } ``` Try it out! {' - https://playground.ethidentitykit.com/?path=/docs/molecules-avatar--component-docs'}
## Parameters | Parameter | Description | Required | Default Value | | ---------- | ------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------- | | `address` | The Ethereum address for the profile of the avatar. | Yes | - | | `src` | The source URL of the avatar image. | No | `https://metadata.ens.domains/mainnet/avatar/{name}` | | `name` | The ENS name of the avatar. | No | - | | `fallback` | The fallback image to display if the avatar image cannot be loaded. | No | [https://efp.app/assets/art/default-avatar.svg](https://efp.app/assets/art/default-avatar.svg) | | `style` | Inline styles to apply to the avatar. | No | - | | `props` | Additional props for the `
` element containing the avatar. | No | - | --- # Profile Stats The Profile Stats component displays follower and following statistics for a given Ethereum address or ENS name. ### Add to your project ```tsx copy import { ProfileStats } from 'ethereum-identity-kit' export default function Home() { return } ``` Try it out! {' - https://playground.ethidentitykit.com/?path=/docs/molecules-profile-stats--component-docs'}
## Parameters | Parameter | Description | Required | Default Value | | -------------------- | ------------------------------------------------------------------------------------------------------- | -------- | -------------------- | | `addressOrName` | Address or ENS name to lookup stats for. | Yes | - | | `list` | List to lookup stats for; overrides `addressOrName` if provided. | No | - | | `containerDirection` | Direction of the container layout (e.g., 'row', 'column'). | No | 'row' | | `statsDirection` | Direction of the stats layout (e.g., 'row', 'column'). | No | 'column' | | `statsStyle` | Inline styles for the stats section. | No | - | | `containerStyle` | Inline styles for the container. | No | - | | `onStatClick` | Function to be called when a stat is clicked; defaults to navigating to EFP profile with selected stat. | No | `defaultOnStatClick` | | `props` | Additional props for the `
` element. | No | - | --- # Follower Tag The Follower Tag component displays the relationship status between a given Ethereum address and the connected user, such as whether the user follows, blocks, or mutes the address. ### Add to your project ```tsx copy import { FollowerTag } from 'ethereum-identity-kit' export default function Home() { return ( ) } ``` Try it out! {' - https://playground.ethidentitykit.com/?path=/docs/molecules-follower-tag--component-docs'}
## Parameters | Parameter | Description | Required | Default Value | | ------------------ | --------------------------------------------------------------------- | -------- | ------------- | | `addressOrName` | The Ethereum address or ENS name of the follower. | Yes | - | | `connectedAddress` | The Ethereum address of the currently connected user. | Yes | - | | `list` | The list of the user (selected list in EFP app). | No | - | | `showLoading` | Whether to show a loading state while fetching follower status. | No | false | | `className` | Additional CSS class names to apply to the component. | No | - | | `props` | Additional props for the `
` element containing the follower tag. | No | - | --- # Full Width Profile The Full Width Profile component displays a comprehensive Ethereum Profile with ENS and EFP details like: - Avatar and Header Image - Name and Status - Role (if provided) - Description (Bio) - Follower State (follows you, blocks you, mutes you) - Profile Statistics (Followers, Following) - Common Followers - Social Media Links - POAP Badges ### Add to your project ```tsx copy import { FullWidthProfile } from 'ethereum-identity-kit' export default function Home() { return } ``` Try it out! { ' - https://playground.ethidentitykit.com/?path=/story/organisms-full-width-profile--full-width-profile-by-address' } ## Parameters | Parameter | Description | Required | Default Value | | --------------------- | --------------------------------------------------------------------------------------------------- | -------- | ------------- | | `addressOrName` | Ethereum Address or ENS name to fetch profile data for. | Yes | - | | `list` | Search profile data by list number; overrides `addressOrName` if provided. | No | - | | `connectedAddress` | Address of the user connected to the app. | No | - | | `darkMode` | Enables dark mode styling for the profile. | No | false | | `role` | Additional information to display on the profile (used for roles on ethid.org). | No | - | | `showFollowerState` | Shows follower state tag (follows you, blocks you, mutes you). | No | false | | `onStatClick` | Action to perform when a stat is clicked; defaults to navigating to EFP profile with selected stat. | No | - | | `selectedList` | List number selected in your application for the connected user. | No | - | | `onProfileClick` | Action to perform when the profile is clicked. | No | - | | `showPoaps` | Shows EFP related POAPs on the profile. | No | true | | `alignProfileContent` | Aligns the profile content when max-width is surpassed (center, start, end). | No | 'center' | | `options` | Additional options for profile data and functionality. See `ProfileCardOption` type for details: | No | - | | | - `followButton`: React node for a follow button. | | | | | - `nameMenu`: React node for a name menu. | | | | | - `profileData`: Prefetched profile data. | | | | | - `prefetchedProfileLoading`: Loading state for prefetched profile data. | | | | | - `refetchProfileData`: Function to refetch profile data. | | | | | - `statsData`: Prefetched stats data. | | | | | - `openListSettings`: Function to open list settings modal. | | | | `className` | Additional CSS class names to apply to the component. | No | - | | `style` | Inline CSS styles to apply to the component. | No | - | ### Styling The component uses predefined styles and can be customized using the `className` and `style` props. The appearance can be manually toggled between light and dark modes using the `darkMode` prop. The component also supports responsive design with different layouts for desktop and mobile views. --- # Follow Button The Follow Button component allows users to manage their social connection with another user. It displays the current relationship status between the `lookupAddress` and the `connectedAddress` and provides actions to change this state. ### Add to your project ### Add Transaction Modal component Follow button will work with the Transaction Modal component to handle the transaction flow, therefore you need to add the Transaction Modal component to your project. Make sure to add the Transaction Provider to your project as well. For all the other providers take a look at the [setup](https://ethidentitykit.com/docs#setup) documentation. ```tsx copy import { TransactionProvider, TransactionModal } from 'ethereum-identity-kit' export default function App() { return ( // Other Providers ) } ``` ### Add Follow Button component Add the follow button wherever you wish, give it the `lookupAddress` and `connectedAddress` props, and you are good to go. ```tsx copy import FollowButton from 'ethereum-identity-kit' export default function YourComponent() { return ( alert('Please connect your wallet')} /> ) } ``` Try it out! { ' - https://playground.ethidentitykit.com/?path=/story/organisms-follow-button-transaction-modal--follow-button-single-tx' }
Follow button for vitalik.eth - 0xd8da6bf26964af9d7eed9e03e53415d37aa96045
## Parameters | Parameter | Description | Required | Default Value | | --------------------- | ------------------------------------------------------------------------------ | -------- | ------------- | | `lookupAddress` | The address of the user to follow or unfollow. | Yes | - | | `connectedAddress` | The address of the currently connected user. | No | - | | `selectedList` | The EFP list number to manage follow state for. (defaults to connectedAddress) | No | - | | `disabled` | Disables the button if set to true. | No | false | | `onDisconnectedClick` | Function to call when the button is clicked and the user is not connected. | No | - | | `sounds` | Object containing sound files to play on button actions. | No | - | | `customClassName` | Custom CSS class names to apply to the button. (overwrites the default styles) | No | - | | `customLoader` | Custom loader component to display while loading. | No | - | | `props` | Additional HTML button element props. | No | - | ### Styling The component uses predefined styles from `FOLLOW_BUTTON_STYLES` and can be further customized using the `className` prop. The button's appearance changes based on its state (e.g., pending, disabled). ### Sound Effects You can provide sound effects for different button states by passing a `sounds` object. Each key in the object corresponds to a [button state](https://ethidentitykit.com/docs/hooks/useFollowButton), and the value is the path to the sound file. ```tsx copy ``` ### Custom Loader If you want to use a custom loader while the button is in a loading state, pass a React component to the `customLoader` prop. ### Notes - Ensure that the `lookupAddress` and `connectedAddress` are valid Ethereum addresses. - The `onDisconnectedClick` function is optional but recommended to handle cases where the user is not connected. - The `sounds` prop is optional and can enhance user experience with auditory feedback. --- # Transaction Provider The Transaction Provider component supplies the necessary context for managing on-chain transactions within the application. It provides state management and utility functions for handling transaction modals, batching, and more. ## Add to your project ```tsx copy import { TransactionProvider } from 'identity-kit/src/context/transactionContext' export default function App() { return {/* Your application components */} } ``` ## Parameters | Parameter | Description | Required | Default Value | | ------------------- | ---------------------------------------------------------------------- | -------- | ------------- | | `batchTransactions` | Enables batching of transactions if set to true. | No | false | | `paymasterService` | The paymaster service to use for sponsored transactions. (string) | No | - | | `defaultChainId` | The default chain ID to use for the transaction provider. (number) | No | - | | `children` | The child components that will have access to the transaction context. | Yes | - | ## Usage The `TransactionProvider` component is used to wrap parts of your application that require access to transaction-related state and functions. It manages the state of transaction modals, pending transactions, and provides utility functions for transaction operations. ### Features - **Transaction Modal Management**: Controls the visibility and state of transaction modals. - **Batch Transactions**: Supports batching of multiple transactions for a streamlined user experience. - **Paymaster Service**: Supports paymaster services for sponsored transactions. [(https://docs.cdp.coinbase.com/paymaster/docs/welcome)](https://docs.cdp.coinbase.com/paymaster/docs/welcome) - **Default Chain ID**: The default chain ID to use if the user does not have an EFP list yet (Has to mint the new list). - **Transaction State Management**: Provides state and functions for managing pending transactions, current transaction index, and more. ### Batching Transactions If you set `batchTransactions` to `true`, the `TransactionProvider` will manage a batch of transactions. This will allow you to either execute transactions one by one or batch them together and have a cart-like experience. ```tsx ``` You will have to provide a button in your application that will open the transaction modal. ```tsx import { useTransactions } from 'ethereum-identity-kit' /* ... your code ... */ const { setTxModalOpen } = useTransactions() /* ... your code ... */ ``` ### Context Values The `TransactionProvider` supplies the following context values: - `txModalOpen`: Boolean indicating if the transaction modal is open. - `batchTransactions`: Boolean indicating if transactions are batched. - `pendingTxs`: Array of pending transactions. - `currentTxIndex`: Index of the current transaction being processed. - `selectedChainId`: ID of the selected blockchain network. - `addTransactions`: Function to add new transactions. - `resetTransactions`: Function to reset all transactions. - `goToNextTransaction`: Function to proceed to the next transaction. - `lists`: The EFP lists of the connected user. - `listsLoading`: Boolean indicating if the EFP lists of the connected user are loading. ### Notes - Ensure that the `TransactionProvider` wraps components that need access to transaction context. - The `batchTransactions` parameter determines if transactions should be batched. - Use the `useTransactions` hook to access the context values and functions within your components. --- # Profile Card The Profile Card component displays a user's ENS and EFP details like: - Primary List Number (EFP List) - Avatar - Header Image - Name - Description (Bio) - Links - Socials - Followers - Following - Common Followers ### Add to your project ```tsx copy import { ProfileCard } from 'ethereum-identity-kit' export default function Home() { return } ``` Try it out! {' - https://playground.ethidentitykit.com/?path=/docs/organisms-profile-card--component-docs'}
## Parameters | Parameter | Description | Required | Default Value | | ------------------------- | --------------------------------------------------------------------------------------------------- | -------- | -------------------- | | `addressOrName` | Ethereum Address or ENS name to fetch profile data for. | Yes | - | | `list` | Search profile data by list number; overrides `addressOrName` if provided. | No | - | | `connectedAddress` | Address of the user connected to the app. | No | - | | `darkMode` | Enables dark mode styling for the profile card. | No | false | | `showFollowerState` | Shows follower state tag (e.g., follows you, blocks you, mutes you). | No | - | | `onStatClick` | Action to perform when a stat is clicked; defaults to navigating to EFP profile with selected stat. | No | `defaultOnStatClick` | | `hasCommonFollowersModal` | Whether to show the [common followers modal](./common-followers.mdx). | No | true | | `options` | Additional options for profile data and stats. See `ProfileCardOption` type for details: | No | - | | | - `followButton`: React node for a follow button. | | | | | - `nameMenu`: React node for a name menu. | | | | | - `profileData`: Prefetched profile data. | | | | | - `prefetchedProfileLoading`: Loading state for prefetched profile data. | | | | | - `refetchProfileData`: Function to refetch profile data. | | | | | - `statsData`: Prefetched stats data. | | | | | - `prefetchedStatsLoading`: Loading state for prefetched stats data. | | | | | - `refetchStatsData`: Function to refetch stats data. | | | | `className` | Additional CSS class names to apply to the component. | No | - | | `style` | Inline CSS styles to apply to the component. | No | - | | `props` | Additional props for the `
` element. | No | - | ### Styling The component uses predefined styles and can be further customized using the `className` prop. The appearance of the modal can be manually toggled between light and dark modes using the `darkMode` prop, however, if you have a `dark` class applied in your application, the modal will automatically use that. --- # docs.ens.domains llms.txt > The Ethereum Name Service (ENS) is a distributed, open, and extensible naming system based on the Ethereum blockchain. ENS maps human-readable names like 'alice.eth' to machine-readable identifiers such as Ethereum addresses, other cryptocurrency addresses, content hashes, metadata, and more. # Reverse Registrar [Registrar responsible for Primary Names] Reverse resolution in ENS - the process of mapping from an Ethereum address (eg, 0x1234...) to an ENS name - is handled using a special namespace, .addr.reverse. A special-purpose registrar controls this namespace and allocates subdomains to any caller based on their address. The reverse registrar is specified in EIP 181. For example, the account `0x314159265dd8dbb310642f98f50c066173c1259b` can claim `314159265dd8dbb310642f98f50c066173c1259b.addr.reverse`. After doing so, it can configure a resolver and expose metadata, such as a canonical ENS name for this address. The reverse registrar provides functions to `claim` a reverse record, as well as a convenience function (`setName`) to configure the record as it's most commonly used, as a way of specifying a canonical name for an address. ## Lookup Primary Name If you are interested in [querying the primary name for an address](/web/reverse) you can checkout the web section. ## Set Primary Name ```solidity function setName(string memory name) public returns (bytes32) ``` Configures the caller's reverse ENS record to point to the provided `name`. This convenience function streamlines the process of setting up a reverse record for the common case where a user only wants to configure a reverse name and nothing else. It performs the following steps: 1. Sets the reverse record for the caller to be owned by the ReverseRegistrar. 2. Sets the reverse record for the caller to have `defaultResolver` as its resolver. 3. Sets the `name()` field in the `defaultResolver` for the caller's reverse record to `name`. In short, after calling this, a user has a fully configured reverse record claiming the provided name as that account's canonical name. Users wanting more flexibility will need to use `claim` or `claimWithResolver` and configure records manually on their chosen resolver contract. ## Multichain Considerations The current infrastructure for primary names across multiple chains is being worked on. ## Other Functions ### Claim Address ```solidity function claim(address owner) public returns (bytes32); ``` Claims the caller's address in the reverse registrar, assigning ownership of the reverse record to `owner`. Equivalent to calling `claimWithResolver(owner, 0)`. ```solidity function claimWithResolver(address owner, address resolver) public returns (bytes32) ``` Claims the caller's address in the reverse registrar, assigning `ownership` of the reverse record to owner. If `resolver` is nonzero, also updates the record's resolver. After calling this function: - The reverse record for the caller (1234....addr.reverse) is owned by `owner`. - If `resolver` is nonzero, the reverse record for the caller has its resolver set to `resolver`; otherwise it is left unchanged. ### Get Default Resolver ```solidity Resolver public defaultResolver; ``` Returns the address of the resolver contract that the `ReverseRegistrar` uses for `setName`. --- # DNS Registrar In [DNS on ENS](/learn/dns) we learned how ENS aims to extend the functionality of the DNS. On this page we will explore the implementation of DNSSEC, the DNSRegistrar, and the building blocks for gasless DNSSEC. :::note Not all top level domains support DNSSEC, and some might have custom ENS implementations. Please refer to the [TLD List](/dns/tlds) for TLD-specific information. ::: ## DNSSEC DNSSEC (Domain Name System Security Extensions) is an added layer of security on top of DNS that allows for cryptographic verification of records. It establishes a chain of trust from the root key (which is signed by ICANN) down to each key. All ENS-enabled DNS names are required to use DNSSEC, and the [DNSSECOracle](https://github.com/ensdomains/ens-contracts/tree/master/contracts/dnssec-oracle) is responsible for verifying the signatures. ### Claiming a Name Onchain Since 2021, it has been possible to [import a DNS name](/learn/dns#importing-a-dns-name) and use that as an ENS name. This process involves enabling DNSSEC, setting a specific TXT record, and submitting a proof to the [DNSRegistrar](https://github.com/ensdomains/ens-contracts/tree/master/contracts/dnsregistrar) smart contract. Expect your `TXT` record to look something like this: ``` TXT @ _ens a=<eth-address> ``` You can learn more about [how to import a DNS name](/learn/dns#importing-a-dns-name) in the DNS section, or see how to [programmatically complete these steps](#programming-dnssec-proofs). There is no ENS protocol fee to import a DNS name, but it requires a large amount of gas (up to a few million) to submit the proof onchain. Continue reading to learn how this has been mitigated. ## Offchain Verification (Gasless) [EP 5.1](/dao/proposals/5.1) introduced a new DNSSECOracle and DNSRegistrar which makes it possible to perform DNSSEC verification at query time, enabling truly free usage of DNS names in ENS. We call this "gasless DNSSEC". It works by enabling [wildcard resolution](/ensip/10) at the DNS TLD level. During the [name resolution process](/resolution), if a name doesn't already exist onchain but has been configured for usage in ENS, the DNSSEC proof will be fetched offchain via [CCIP Read](https://eips.ethereum.org/EIPS/eip-3668) and then verified onchain with the same DNSSECOracle mentioned above. ### Import a DNS name gaslessly To configure a DNS name for usage in ENS, simply add a `TXT` record with the following format: ``` TXT @ ENS1 <resolver-address> ``` The `resolver-address` implementation is customizable just like any other ENS resolver. To get started quickly, a special ExtendedDNSResolver has been deployed which allows users to specify an ETH address that the name should resolve to within the same `TXT` record. To use this setup, simply add a record with the following format: ``` TXT @ ENS1 <extended-resolver-address> <eth-address> TXT @ ENS1 0x238A8F792dFA6033814B18618aD4100654aeef01 0x225f137127d9067788314bc7fcc1f36746a3c3B5 ``` ## Other ### TLD Ownership You can lookup the `owner` of any TLD by calling the `Registry.owner(bytes32 node)` function. If at least one domain has been imported for this TLD (via the onchain method), the owner will be either the `DNSRegistrar` or a smart contract controlled by the respective registry operator. If a TLD has not yet been activated, the `owner` will return `0x0` and it may require one user to import a name onchain to activate the TLD. See the [supported TLD list](/dns/tlds) for more info. ### Programming DNSSEC Proofs To help you interact with DNSSEC data and the DNSRegistrar, we provide a few libraries. - [DNSProvejs](https://github.com/ensdomains/dnsprovejs) = A library for querying and validating DNSSEC data from DNS - [ENSjs](https://github.com/ensdomains/ensjs) = A library for interacting with ENS smart contracts #### Retrieving a proof ```ts import { addEnsContracts } from '@ensdomains/ensjs' import { getDnsImportData } from '@ensdomains/ensjs/dns' import { createPublicClient, http } from 'viem' import { mainnet } from 'viem/chains' const client = createPublicClient({ chain: addEnsContracts(mainnet), transport: http(), }) const dnsImportData = await getDnsImportData(client, { name: 'example.com', }) ``` #### Submitting the proof to the DNSRegistrar ```ts import { addEnsContracts } from '@ensdomains/ensjs' import { getDnsImportData, importDnsName } from '@ensdomains/ensjs/dns' import { createPublicClient, createWalletClient, custom, http } from 'viem' import { mainnet } from 'viem/chains' const mainnetWithEns = addEnsContracts(mainnet) const client = createPublicClient({ chain: mainnetWithEns, transport: http(), }) const wallet = createWalletClient({ chain: mainnetWithEns, transport: custom(window.ethereum), }) const dnsImportData = await getDnsImportData(client, { name: 'example.com', }) await importDnsName(wallet, { name: 'example.com', dnsImportData, }) ``` ## Other functions ```ts // Get the list of suffixes DNSRegistrar.suffixes // Get Oracle DNSRegistrar.oracle ``` ```ts DNSRegistrar.claim(bytes name, bytes proof) DNSRegistrar.proveAndClaim(bytes name, tuple[] input, bytes proof) DNSRegistrar.proveAndClaimWithResolver(bytes name, tuple[] input, bytes proof, address resolver, address addr) ``` --- import { FiBookOpen, FiClock, FiHash } from 'react-icons/fi' import { Card } from '../../components/ui/Card' # ETH Registrar [Smart contracts responsible for the ".eth" TLD] The ETH Registrar is a special registrar. It allows for trustless on-chain name registration and is in charge of the ".eth" TLD. ## BaseRegistrar vs Controller The ETH Registrar is split into two contracts. The [BaseRegistrar](https://github.com/ensdomains/ens-contracts/blob/staging/contracts/ethregistrar/BaseRegistrarImplementation.sol) and the [ETHRegistrarController](https://github.com/ensdomains/ens-contracts/blob/staging/contracts/ethregistrar/ETHRegistrarController.sol). The BaseRegistrar is responsible for name ownership, transfers, etc (ownership related), while the Controller is responsible for registration & renewal (pricing related). This separation is done to reduce the attack surface of the registrar, and provides users with the guarantees of continued ownership of a name so long as the registrar is in place. ### Controllers The [ETHRegistrarController](https://github.com/ensdomains/ens-contracts/blob/staging/contracts/ethregistrar/ETHRegistrarController.sol) is the main controller for the ETH Registrar, and provides a straightforward registration and renewal mechanism. ## Pricing Structure The ETH Registrar charges a fee for registration. This fee is paid in ETH and is set to prevent spamming the registrar. Any protocol fees are sent to the ENS Treasury. ### Pricing Oracle Initially, a single pricing oracle was deployed, the [StablePriceOracle](https://github.com/ensdomains/ens-contracts/blob/master/contracts/ethregistrar/StablePriceOracle.sol). This contract has owner-set prices for each name length (1, 2, 3, 4, 5 or more). Users do not have to interact with this oracle directly, as the controller provides functionality to determine the pricing for a registration or renewal. ### 3, 4, and 5 Letter Names The ETH Registrar has special pricing for 3, 4, and 5 (and more) letter names. At the time of writing, a `5+` letter `.eth` will cost you `5 USD` per year. A `4` letter `160 USD` per year, and a `3` letter `640 USD` per year. This pricing structure is done to promote market diversity as there are an exponentially less amount of names the shorter they become. The minimum length of a name is 3 characters. | Name Length | Price (USD) | | ----------- | ----------- | | 5+ | 5 | | 4 | 160 | | 3 | 640 | ### Premium & Auctions In addition to length-based pricing the ETH Registrar also has a premium pricing structure. 90 days after a name expires (aka after the grace period), the name will go into a Temporary Premium Auction. The Auction is a 21 day dutch auction, meaning that the price starts high (~100 Million USD) and exponentially decrease till it hits 0 or a bid goes through. This is done to prevent sniping of names, and ensures the name goes to the highest bidder fairly.
You can read more about the temporary premium in [this article](https://support.ens.domains/en/articles/7900612-temporary-premium). ### Where does the money go? Upon registration funds are sent to the ETHRegistrarController. The controller then sends the funds to the ENS Treasury (anyone can call the `withdraw` method to trigger this). Income from the ETH Registrar is used to fund the development of ENS, its ecosystem, and other public goods. Read more about our spending in [Article III of the Constitution](/dao/constitution#iii-income-funds-ens-and-other-public-goods). ## ERC721 and NFTs In the early days of ENS, the ERC721 standard did not exist. The original ETH Registrar formed the pre-cursor to the ERC721 standard. As we witnessed the ERC721 being standardized, support for it was added to the ETH Registrar. Today, users can interact with the ETH Registrar to transfer their name just like with any other ERC721 token. ## Registering a Name Registering a name is a trustless process that takes place onchain (more on this below). Some open source frontends for registering names are the [ENS Manager App](https://app.ens.domains/), [ENS Fairy](https://ensfairy.xyz/), Rainbow Wallet. The process of registering a `.eth` name uses a commit-reveal process.
Commit
Wait
Reveal
### Commit-Reveal The ETHRegistrarController, the highest level contract that users register names through, implements a commit reveal scheme to prevent frontrunning registrations. We first call the `commit` function with an opaque bit of data (the `commitmenthash`), wait 60 seconds, and then call the `register` function. The `commit` function takes a commitment hash, which can be generated using the `makeCommitment` function. The commitment hash is opaque and revealed during the `register` function. The commit-reveal process is to prevent a malicious actor from seeing your `register` transaction in the public mempool and frontrunning it. ```solidity ETHRegistrarController.makeCommitment( name string, owner address, duration uint256, secret bytes32, resolver address, data bytes[], reverseRecord bool, ownerControlledFuses uint16 ) // For example makeCommitment( "myname", // "myname.eth" but only the label 0x1234..., // The address you want to own the name 31536000, // 1 year (in seconds) 0x1234..., // A randomly generated 32 byte secret you create 0x1234..., // The address of the resolver you want to use [0x8b95dd71...], // Encoded function calls you want to pass to the resolver, like `setAddr()` false, // Whether or not to set the new name as your primary name 0 // The NameWrapper fuses you want to set ); ``` Once you have calculated the commitment hash, submit the `commit` transaction. ```solidity ETHRegistrarController.commit(commitment bytes32) ``` After having committed, it is required to wait at least the `MIN_COMMITMENT_AGE` (60 seconds) before making the subsequent `register` transaction. ### Registering Once you have made the onchain commitment and waited 60 seconds, you can register your name. Registration takes in the same parameters as the `makeCommitment` function above. Before initiating registration, ensure that: - `available(label)` == `true`, where `label` is "name" in "name.eth" - `duration` >= `MIN_REGISTRATION_DURATION` - `commitments[commitment]` is between 1 min and 24 hrs old - `msg.value` >= `rentPrice(name, duration)` + `5-10% (slippage)` Because the rent price is paid in ETH but denominated in USD, callers are recommended to send slightly more than the value returned by `rentPrice` to avoid issues with fast price changes. A premium of 3-5% will likely be sufficient. Any excess funds sent during registration are automatically returned to the caller. ```solidity ETHRegistrarController.register( name string, owner address, duration uint256, secret bytes32, resolver address, data bytes[], reverseRecord bool, ownerControlledFuses uint16 ) // For example register( "myname", // "myname.eth" but only the label 0x1234..., // The address you want to own the name 31536000, // 1 year (in seconds) 0x1234..., // The same secret you used in the `commit` transaction 0x1234..., // The address of the resolver you want to use [0x8b95dd71...], // Encoded function calls you want to pass to the resolver, like `setAddr()` false, // Whether or not to set the new name as your primary name 0 // The NameWrapper fuses you want to set ); ``` ## Renewing a Name ```solidity ETHRegistrarController.renew() ``` Any user can renew a domain, not just the owner. This means that if you want to ensure a name doesn't expire you can renew it for someone. By allowing renewal for any arbitrary amount of time users can ensure their name will not expire. As per the separation between registry and controller, even with upgraded controller your name will still be yours. ## Other features ```solidity ETHRegistrarController.MIN_COMMITMENT_AGE uint ETHRegistrarController.MAX_COMMITMENT_AGE uint ETHRegistrarController.MIN_REGISTRATION_DURATION uint // Get Commitment Timestamp ETHRegistrarController.commitments mapping(bytes32=>uint) // Get Rent Price ETHRegistrarController.rentPrice(string name, uint duration) view returns (uint) // Check Name Validity ETHRegistrarController.valid(string name) view returns (bool) // Check Name Availability // Returns true if the name is both valid and available for registration by this controller. ETHRegistrarController.available(string name) view returns (bool) // Calculate Commitment Hash ETHRegistrarController.makeCommitment(string name, address owner, uint256 duration, bytes32 secret, address resolver, bytes[] data, bool reverseRecord, uint16 ownerControlledFuses) view returns (bytes32) // Get Name Expiry (unix timestamp at which registration expires) BaseRegistrar.nameExpires(uint256 label) view returns (uint) // Check Name Availability (less specific, use ETHRegistrarController.available instead) BaseRegistrar.available(uint256 label) view returns (bool) // Get Transfer Period End (unix timestamp at which transfer period (from legacy registrar) ends) BaseRegistrar.transferPeriodEnds uint // Get Controller Status BaseRegistrar.controllers mapping(address=>bool) // Check Token Approval BaseRegistrar.getApproved(uint256 tokenId) view returns (address operator) // Check All Tokens Approval BaseRegistrar.isApprovedForAll(address owner, address operator) view returns (bool) // Get Token Owner BaseRegistrar.ownerOf(uint256 tokenId) view returns (address) // Get Token URI BaseRegistrar.tokenURI(uint256 tokenId) view returns (string) ``` Writable ```solidity // Transfer a Name BaseRegistrar.transferFrom(address from, address to, uint256 tokenId) BaseRegistrar.safeTransferFrom(address from, address to, uint256 tokenId) BaseRegistrar.safeTransferFrom(address from, address to, uint256 tokenId, bytes _data) // Approve Operator BaseRegistrar.approve(address to, uint256 tokenId) // Set Approval For All BaseRegistrar.setApprovalForAll(address operator, bool approved) // Reclaim ENS Record BaseRegistrar.reclaim(uint256 label) ``` Events ```solidity // BaseRegistrar event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); event NameMigrated(uint256 indexed hash, address indexed owner, uint expires); event NameRegistered(uint256 indexed hash, address indexed owner, uint expires); event NameRenewed(uint256 indexed hash, uint expires); // Controller event NameRegistered(string name, bytes32 indexed label, address indexed owner, uint cost, uint expires); event NameRenewed(string name, bytes32 indexed label, uint cost, uint expires); ``` --- # The Registry [Root Registry of the Ethereum Name Service] The ENS registry is the core contract that lies at the heart of ENS resolution. All ENS lookups start by querying the registry. The registry maintains a list of domains, recording the owner, resolver, and TTL for each, and allows the owner of a domain to make changes to that data. The ENS registry is specified in [EIP 137](https://eips.ethereum.org/EIPS/eip-137). ## Why Registries? Top-Level Domains (TLDs), like `.eth`, `.com`, and `.test`, are owned by smart contracts called registrars, which specify rules governing the allocation of their names. Anyone may, by following the rules imposed by these registrar contracts, obtain ownership of a domain for their own use. | TLD | Registrar Contract | | ------------------- | -------------------------------------- | | `[root]` | [The Registry](/registry/ens) | | `.eth` | [ETH Registry](/registry/eth) | | `.com`, `.xyz`, etc | [DNS Registrar](/registry/dns) | | `.addr.reverse` | [Reverse Registrar](/registry/reverse) | ## Who owns the root Registry? The [ENS Registry](https://etherscan.io/address/0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e) is owned by the [ENS Root](https://etherscan.io/address/0xaB528d626EC275E3faD363fF1393A41F581c5897) which is owned by the [ENS DAO Wallet](https://etherscan.io/address/0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7). To verify this you can run the `owner` function on the registry & root contracts. ## Other Functions ```ts // Get Owner ENS.owner(bytes32 node) view returns (address) // Get Resolver ENS.resolver(bytes32 node) view returns (address) // Get TTL ENS.ttl(bytes32 node) view returns (uint64) // Get Approval ENS.isApprovedForAll(address owner, address operator) view returns (bool) // Check Record Existence ENS.recordExists(bytes32 node) view returns (bool) ``` ```ts // Set Owner (only callable by current owner) ENS.setOwner(bytes32 node, address owner) // Set Resolver ENS.setResolver(bytes32 node, address resolver) // Set TTL ENS.setTTL(bytes32 node, uint64 ttl) // Set Subnode Owner ENS.setSubnodeOwner(bytes32 node, bytes32 label, address owner) // Set Multiple (convenience function (setResolver, setTTL, setOwner)) ENS.setRecord(bytes32 node, address owner, address resolver, uint64 ttl) // Set Multiple Subnode ENS.setSubnodeRecord(bytes32 node, bytes32 label, address owner, address resolver, uint64 ttl) // Set Approval ENS.setApprovalForAll(address operator, bool approved) ``` Events ```ts // Transfer Event event Transfer(bytes32 indexed node, address owner) // New Resolver Event event NewResolver(bytes32 indexed node, address resolver) // New TTL Event event NewTTL(bytes32 indexed node, uint64 ttl) // New Owner Event event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner) ``` --- # FAQ ## Which wallets and dApps support ENS? ENS is supported by a wide range of wallets and dApps, some notable ones can be found on the [integrations page](https://ens.domains/). This page is currently under construction however a link to add yourself will be put here soon. ## Can I hold my name with one address, and point it at the other? Yes, you can hold your name with one address and point it at another. Simply visit the [ENS Manager App](https://ens.app/) and update the appropriate address record (by chain) for your name to point to the address you wish. ## Once I own a name, can I create my own subdomains? Yes. You can create whatever subdomains you wish and assign ownership of them to other people if you desire. You can even set up your own registrar for your domain. Some resolvers might provide even more advanced features, read more [about Resolvers](/resolvers/quickstart). ## Can I change the address my name points to after I've bought it? Yes, you can update the addresses and other resources pointed to by your name at any time. To update your name checkout the [ENS Manager App](https://ens.app/). ## ETH Registration ### Why are names registered as hashes? Hashes provide a fixed length identifier that can easily be passed around between contracts with fixed overhead and no issues passing around variable-length strings. Read more about [labelhash, namehash, and encodings](/resolution/names). ### What characters are supported? ENS names are generally encoded using UTS-46. This means there is partial support for Unicode characters, including emoji. However technically possible to register any name, names that are not valid UTS-46 will not be resolvable by most resolvers. Therefore it is generally recommended for apps that implement registration to limit the characters that can be registered to ensure a smooth experience. To read more about supported characters [name normalization](/resolution/names). ### What does it cost to register a .eth domain? Currently, registration costs are set at the following prices: - 5+ character .eth names: $5 in ETH per year. - 4 character .eth names: $160 in ETH per year. - 3 character .eth names: $640 in ETH per year. 3 and 4 character names have higher pricing to reflect the small number of these names available. To read more about the pricing structure of .eth names [read more about pricing](/registry/eth) ### How long can I register a name for? You can register a name for as long as you would like. There is no maximum registration duration. ### What happens if I forget to renew my name? If you forget to renew your name, it will be released back to the public pool of available names. Luckily the expiration process has a 90 day grace period. This means that once the name expires the original owner has 90 days to renew the name before it is released. After the grace period, the name is released for registration by anyone with a temporary premium which decreases over a 21 days period. The released name continues to resolve your ETH address until the new owner overwrites it. ### In what way could I lose access to my name? The .eth registrar is built to ensure once issued, a name cannot be revoked or taken away from its owner. Potential loss can occur if the owner loses access to their private key, or if the owner forgets to renew their name. ## Root Registry ### Who owns the ENS rootnode? What powers does it grant them? The ENS rootnode is currently owned by the ENS DAO. It used to be owned by the ENS Multi-sig, a group of keyholders from different parts of the ecosystem, however as of [EP4.10](/dao/proposals/4.10) the ownership has been transferred to the ENS DAO. Ownership of the rootnode grants the ability to do the following: - Control allocation and replacement of TLDs other than .eth - this is required to implement DNSSEC integration. - Enable and disable controllers for the .eth registrar, which affect registration and renewal policies for .eth names. - Update the pricing for .eth names. - Receive and manage registration revenue. ### Can I register a TLD of my own within ENS? Yes and No, We consider ENS to be part of the 'global namespace' in co-existence with DNS, and it is our priority to not pollute the namespace. ENS-specific TLDs are restricted to only '.eth' on Mainnet Ethereum, or .eth and .test on testnets. By default ENS allows users to [import their DNS name](/learn/dns) through the use of the [DNS Registrar](/registry/dns). Existing DNS TLDs can [reach out to us](mailto:info@ens.domains) to take control of their TLD. ## What are the differences between ENS and other naming services such as Namecoin or Handshake? ENS complements and extends the usefulness of DNS with decentralised, trustworthy name resolution for web3 resources such as blockchain addresses and distributed content, while Namecoin and Handshake are efforts to replace all or part of DNS with a blockchain-based alternative. ## Governance Token ### Can I recover tokens accidentally sent to the wrong address? The answer depends on the address the token was sent to. If you accidentally sent the token to the token.ensdao.eth address (0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72) or the wallet.ensdao.eth address (0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7) then the tokens might be recoverable. Contact the [Meta-governance working group](/dao/stewards/) at the [ENS Forum](https://discuss.ens.domains) and explain the situation. Tokens can only be sent back to the address they were sent from, so if it was sent from an exchange, contact your exchange support to make sure the address can receive tokens. If the tokens were sent to the null address (0x000..) or an address with a typo, then the tokens are unrecoverable and there's nothing that anyone can do. If the tokens were sent to an exchange or a third party, then contact that third party for help. --- import { Badge } from '../../components/ui/Badge' import { Card } from '../../components/ui/Card' # Hosting a Decentralized Website [Introduction to hosting a decentralized website using ENS] ## ContentHash The ContentHash is a very popular component of an ENS name, first introduced in [ENSIP-7](/ensip/7). It can be queried by hitting the [contenthash(bytes32)](/resolvers/interfaces#0xbc1c58d1) function on a name's resolver. You can also [set the contenthash on a name](/resolvers/interfaces#0x304e6ade) if the resolver supports it. {['ipfs://qMhx...', 'bzz:/2477', 'ar://HGa8...'].map((tag) => ( {tag} ))} ## Hosting & Pinning When it comes to hosting your files there are many options to choose from. {['IPFS / Filecoin', 'Swarm', 'Arweave'].map((tag) => ( {tag} ))} Popular options include [IPFS](https://ipfs.io), [Swarm](https://ethswarm.org), and [Arweave](https://arweave.org). Depending on what option you go with your files are either permanently stored on a network, or require to be actively stored on at least one machine, also known as "pinning". ### Deploy your sites Several helpful tools and platforms exist that you can use to deploy your website to IPFS, Swarm, or Arweave. Most notably [fleek](https://fleek.xyz), [Pinata](https://pinata.cloud), and [Blumen](https://blumen.stauro.dev/). Helping you easily deploy your website to a decentralized storage network. ## Setting your ContentHash If you are using the public resolver (the default for names registered using the ENS Manager App), you can set the contenthash directly from within the [ENS Manager App](https://app.ens.domains). If you are using a custom resolver, or are writing your own resolver you will be able to have more fine grained control over the contenthash field. See [ENSIP-7](/ensip/7) for more information on the contenthash field. ## Browser Support & Gateways In the ideal world every browser supports decentralized websites out of the box. If you are using [MetaMask](https://metamask.io) or [Brave Browser](https://brave.com) you can already access IPFS websites directly. On non-conforming browsers you can use a gateway such as [eth.link](https://eth.link) or [eth.limo](https://eth.limo) to access your website. You can test if your browser supports decentralized websites by visiting [ens.eth](https://ens.eth) or use a gateway via [ens.eth.link](https://ens.eth.link). --- import { Table } from '../../components/ui/Table' import ensips from '../../data/generated/ensips.json' # ENS Improvement Proposals This page contains a summary of all the ENS Improvement Proposals (ENSIPs) that have been proposed, and their current status. Improvement Proposals have included anything from new contract features, to text record standards, protocol features, and more. ## ENSIPs [ensip.title, ensip.status])} /> ## Propose an ENSIP Feel free to [open a pull request](https://github.com/ensdomains/ensips/pulls) on the `ensdomains/ensips` repository. --- import { Button } from '../components/ui/Button' # 🪲 Bug Bounty Program The ENS bug bounty program rewards anyone who finds a bug in covered ENS smart contracts and ENS Labs assets. This page provides a brief overview of the program which is operated by Immunefi and ENS Labs. [See the full program](https://immunefi.com/bug-bounty/ens) ## Bounties 💸 Reward sizes are guided by the rules below, but are in the end, determined at the sole discretion of the ENS Labs team. ### Smart Contracts - **Critical**: up to $250,000 USD - **High**: up to $150,000 USD - **Medium**: up to $100,000 USD ### Websites and Applications - **Critical**: up to $50,000 USD - **High**: up to $20,000 USD - **Medium**: up to $5,000 USD - **Low**: up to $1,000 USD The ENS Labs team reserves the right to adjust bounty amounts at any time in the future. --- # 📝 Changelog This page contains a list of changes and events that happened to the ENS protocol & ecosystem. ## Dentity Announcement On August 21st, 2024 the ENS Labs team announced a new integration with Dentity, an independent identity provider that allows users to verify information and share it on their ENS profile.0 This integration leverages a draft ENSIP that allows for W3C Verifiable Credentials to be stored inside ENS profiles. ## ENSv2 Announcement On March 28th, 2024 the ENS Labs team announced our plans and roadmap for scaling ENS to the entire internet and beyond. This involves migrating .eth registrations to a brand new system, in addition to improving support for existing L2 solutions. You can read more [on our blog](https://blog.ens.domains/post/ensv2), [on X](https://twitter.com/ensdomains/status/1795440186513576318), and [the forums](https://discuss.ens.domains/t/technical-feedback-thread-for-ensv2/19233). --- # Creating a Subname Registrar In the [Use Cases](/wrapper/usecases#sell-or-rent-subnames) section, we talked about the ability to stand up your own "registrar" to allow other people to register/claim subnames automatically. Maybe you want to give wrapped subnames out for free, or maybe you want to charge for them. Maybe you want to apply specific rules to the subnames, such as only allowing alphanumeric names. All of this is possible, and this article will break down what you need to do. It's recommended to first read the [Use Cases](/wrapper/usecases#sell-or-rent-subnames) section to get an overview of the decisions you'll need to make. ## Prerequisites This guide assumes that your parent name (such as `myname.eth`) is already wrapped. If you're not sure whether your name is wrapped, look at the More tab on the Manager app. If the name is unwrapped, it will say so, and it will show you a "Wrap Name" button. If you want to issue [Emancipated](/wrapper/states#emancipated) subnames, or subnames with [any other fuses](/wrapper/fuses) burned, then your parent name must first be [Locked](/wrapper/states#locked). You can do this on the Permissions tab in the ENS manager app. :::note Locking your name (in other words revoking the permission to unwrap) is an **irreversible** change. After you lock the name, you will no longer be able to unwrap it. This is a security guarantee for the holders of all subnames. It ensures that the owner of the parent name cannot get around the security guarantees of the Name Wrapper. Best to do this on a testnet (Sepolia/Holesky) name first, for development or testing purposes. ::: ## Creating and Deploying your Registrar Contract In order to create a new subname, your contract should call either `setSubnodeOwner` or `setSubnodeRecord` on the [NameWrapper contract](/learn/deployments#deployments). Also pass in the fuses and expiry at the same time, as needed. ```solidity NameWrapper.setSubnodeOwner(bytes32 parentNode, string label, address owner, uint32 fuses, uint64 expiry) // For example setSubnodeOwner( 0x6cbc..., // The namehash of the parent node, e.g. "myname.eth" "sub", // The label of the subname to create 0x1234..., // The address you want to be the owner of the new subname 65536, // The fuse bits OR'd together, that you want to burn 2021232060 // The expiry for the subname ) NameWrapper.setSubnodeRecord(bytes32 parentNode, string label, address owner, address resolver, uint64 ttl, uint32 fuses, uint64 expiry) // For example setSubnodeRecord( 0x6cbc..., // The namehash of the parent node, e.g. "myname.eth" "sub", // The label of the subname to create 0x1234..., // The address you want to be the owner of the new subname 0x5678..., // The address of the resolver to set for the new subname 0, // The TTL to set for the new subname 65536, // The fuse bits OR'd together, that you want to burn 2021232060 // The expiry for the subname ) ``` Your public-facing registration function would typically take at _least_ the parent node (namehash) and subname label as inputs, such as: ```solidity register(bytes32 parentNode, string calldata label) ``` Then under the hood, your contract will call `setSubnodeRecord` and fill in the rest of the parameters on behalf of the user: - owner: Typically the caller account, `msg.sender` - resolver: Typically the default public resolver, `resolver.eth` - ttl: 0 - fuses: Up to you and your goals. See the [Use Cases](/wrapper/usecases#sell-or-rent-subnames) section for a discussion on this. Typically 65536 for an enamcipated rental subname, or 327680 for an emancipated "forever" name. - expiry: Up to you and your goals. If you are renting subnames for a particular length of time, this expiry would reflect that. If you are allowing registration of "forever" names, then you can just set the expiry equal to the parent name's current expiry. Of course, if you want to give the registrant more power/convenience, you could allow some of those parameters to be passed in to your public register function as well. ### Setting Resolver Records If you want your subname registrar to set records on a subname in the same registration transaction, then the flow will be slightly different. In that case, perform these steps: - Call `setSubnodeOwner`, setting the _contract itself_ (`address(this)`) as the owner of the subname, temporarily. This first step is needed for the default Public Resolver so that the contract has the authority to set records for the subname. - Call whatever [resolver methods](/resolvers/interacting) you need to. Perhaps these are records that you want to be pre-set on your subnames (such as an ETH address that the subname points to). Or perhaps these are records that you allow the registrant to pass in, so that they can register their subname and set whatever records they want all in one transaction. - Call `setSubnodeRecord`, but this time set the owner to the actual intended owner of the subname. This is the point at which you should set the appropriate fuses and expiry you want to, as well. In addition, you will need to make sure your contract follows the [ERC-1155 Token Receiver rules](https://eips.ethereum.org/EIPS/eip-1155#erc-1155-token-receiver). This means implementing the `onERC1155Received` and `onERC1155BatchReceived` methods, and signaling support for them in your ERC-165 `supportsInterface` method. OpenZeppelin has an easy abstract contract you can include for all this: [ERC1155Holder.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155/utils/ERC1155Holder.sol) ### Taking fees If you are setting up a "rental" registrar, then your registration function should require a certain amount of ETH to be sent in as well. Alternatively, you could choose to allow users to spend ERC-20 tokens instead. To accomplish that, you would typically call the ERC-20 method `transferFrom` on the token contract. This also means that the registrant would first need to approve your contract as a spender for that token, meaning they would need to execute a separate approval transaction first (either to approve unlimited spending, or to approve the specific number of tokens needed to register the subname). ### Reference Implementation Luckily, you don't need to start from scratch! The ENS Labs devs have created some example contracts you can start from: https://github.com/ensdomains/ens-contracts/tree/feature/subdomain-registrar/contracts/subdomainregistrar These contracts include two different implementations: #### Forever Subname Registrar This is a basic FIFS (First in first serve) registrar. The registration can take a fixed fee, or this fee can be set to 0 if you wish for subnames to be free. Names automatically are set to the parent's expiry can the fuse for `CAN_EXTEND_EXPIRY` will be burnt on registration so the user can extend their expiry if the parent also extends theirs. For a better UX, it is recommended that the parent sets their expiration as high as possible to allow their users to not have to think about renewing. #### Rental Subname Registrar This is a basic FIFS (First in first serve) registrar. The key difference between this and the ForeverSubdomainRegistrar is that it does not auto-burn the `CAN_EXTEND_EXPIRY` fuse and instead exposes a `renew()` function that allows paid renewal. This registrar also needs to be paired with a rental-based pricing contract. For simplicity, the deployer can deploy this pricing contract and the UI can pass this address through to `setupDomain()` when a new user wants to setup a subname. ## Setting Everything Up Once you have a parent name ready and a subname registrar contract deployed, then you just need a few extra steps to set everything up: ### (If needed) Call setupDomain on your contract This will only apply to you if you have a specific `setupDomain` method or something similar on your contract, such as the [reference implementation](/wrapper/creating-subname-registrar#reference-implementation) contracts do. Calling this method will "enable" a specific parent name in your subname registrar. It can also allow you to set or update the pricing terms or beneficiary account, if needed. ### Approve your contract Call `setApprovalForAll` on the NameWrapper contract, approving your subname registrar contract as an operator for any names you own. This allows you to keep ownership of the parent name, and just delegate subname creation to your contract. ### (If needed) Approve token spending If your registrar contract takes ERC-20 tokens as a registration fee, then a potential registrant will need to approve your contract as a spender first. ### Register a subname Finally, the registrant will call your public registration method. Upon transaction success, they will own the wrapped name (ERC-1155 NFT) with whatever fuse/expiry guarantees that you setup in your registrar. If you are allowing "forever" subnames to be registered (meaning that you've burned the `CAN_EXTEND_EXPIRY` fuse on the subnames), then the registrant can extend their own expiry at any time. Note that a subname's expiry can be set up to a maximum of whatever the parent name's expiry is. And that's it! --- # Name Wrapper Fuses A "fuse" is a permission or perk that can be granted/revoked on a name. As the name implies, once the fuse is "burned", it cannot be unburned. Fuses will only reset when the **expiry** is reached. In the ENS Manager UI, this is available in the "Permissions" section of the name. By **wrapped expiry**, we mean that for .eth second-level names (like `name.eth`), this is the end of the 90-day grace period, the time at which the .eth 2LD is truly released. For all other names (such as subnames), there is no grace period, so the expiry is just the expiration date for that specific subname. For example, by default when you wrap a name, you can transfer that NFT around freely, just as you can with other NFTs. However, if the **`CANNOT_TRANSFER`** fuse is burned, then the NFT becomes non-transferrable. In the ENS Manager UI, you would do this by revoking the "Can send this name" permission. In order to burn fuses on a name, the parent name must be **Locked** (meaning, you cannot unwrap the name). The reason is, if the parent name was not locked, then the owner of the parent name could simply get around the constraints of the Name Wrapper by unwrapping the name, and replacing/revoking subnames against the core ENS Registry. There are parent-controlled and owner-controlled fuses: ## Parent-Controlled Fuses Only the owner of the parent name can burn one of these fuses on a name. These can generally be thought of as "perks" that can be granted to a name, though they can be used in other ways. | Fuse name | Description | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`PARENT_CANNOT_CONTROL`** | Allows a parent owner to **Emancipate** a child name. After this is burned, the parent will no longer be able to burn any further fuses, and will no longer be able to replace/delete the child name. This fuse must be burned in order for any owner-controlled fuses to be burned on the name. | | **`IS_DOT_ETH`** | This fuse cannot be burned by users of the Name Wrapper, it is only set internally when a .eth 2LD is wrapped. | | **`CAN_EXTEND_EXPIRY`** | The owner of the child name will be able to extend their own expiry. Normally, only the parent owner can extend the expiry of a child name. See the [Expiry](/wrapper/expiry) section for more information. | | **Custom Fuses** | There are 13 other parent-controlled fuses that are not reserved, and can be used in any custom way you want! | ## Owner-Controlled Fuses Either the owner of the name or the owner of the parent name can burn one of these fuses. These can generally be thought of as "permissions" that can be revoked on a name, though they can be used in other ways. | Fuse name | Description | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`CANNOT_UNWRAP`** | The name will now be **Locked**, and can no longer be unwrapped. This fuse must be burned in order for any other owner-controlled fuses to be burned on the name. | | **`CANNOT_BURN_FUSES`** | No further fuses can be burned on the name. | | **`CANNOT_TRANSFER`** | The name (wrapped NFT) can no longer be transferred. | | **`CANNOT_SET_RESOLVER`** | The resolver contract for the name can no longer be updated. | | **`CANNOT_SET_TTL`** | The TTL for the name can no longer be updated. | | **`CANNOT_CREATE_SUBDOMAIN`** | New subdomains can no longer be created. | | **`CANNOT_APPROVE`** | The approved "subname renewal manager" for the name can no longer be updated. See the [Approved Operators](#approved-operators) section for more information. | | **Custom Fuses** | There are 9 other owner-controlled fuses that are not reserved, and can be used in any custom way you want! | ## The Emancipated and Locked States This is also covered in the Wrapped States section, but here is a quick recap: All .eth second-level names (like `name.eth`) are automatically placed into the Emancipated state when wrapped. **Emancipated** means that the parent no longer has control over the child name. It can no longer burn any fuses or replace the subname, up until the expiry. A name is Emancipated when the parent burns the **`PARENT_CANNOT_CONTROL`** (PCC) fuse. The parent must first be in the Locked state to be able to do this. **Locked** means that the name cannot be unwrapped. This provides assurance to subnames that the parent owner cannot unwrap and then, for example, start replacing subnames directly against the registry. An Emancipated name is Locked when the **`CANNOT_UNWRAP`** (CU) fuse is burned. Think of the special PCC / CU fuses recursively: - To burn owner-controlled or subname fuses, CU must be burned. - To burn CU, PCC must be burned. - Only the parent can burn PCC on the child name, and only if CU is first burned on the parent. - Only the grandparent can burn PCC on the parent name, and only if CU is first burned on the grandparent. - And so on... Follow that chain up until you hit a .eth second-level name like `name.eth`, since .eth second-level names will have PCC automatically burned when wrapping. The parent `eth` node is already in the Locked state. A parent name can burn all the fuses it needs to on a child name in one transaction. This can be done when the subname is created, or on an existing subname that has not yet been Emancipated. ## DNS Domains and Fuses Currently, only .eth names support fuses, because only the `eth` node is on-chain native and completely locked beyond anyone's control. Technically speaking, the owner of a DNS TLD has the ability to burn fuses on that TLD in the Name Wrapper, and set it to the "Locked" state. And then from there, all subnames under that DNS TLD _will_ be able to use fuses. The DNS TLD owner would need to: - Request the Controller of that TLD from the ENS DAO - Wrap the TLD node in the Name Wrapper - Burn the **`PARENT_CANNOT_CONTROL`** and **`CANNOT_UNWRAP`** fuses on the wrapped TLD to lock it However, this still does not have all the immutable guarantees that .eth names do. This is because for DNS names, the "source of truth" always lies not in the Ethereum network, but in the DNS network, and the DNS root zone governed by ICANN stakeholders. So even if the DNS TLD owner "Locks" that TLD in the ENS Name Wrapper, if that TLD were to ever change ownership on the DNS side, then (per the [ENS DAO Constitution](https://docs.ens.domains/v/governance/ens-dao-constitution#iv.-ens-integrates-with-the-global-namespace)) the new owner would be able to override control of that TLD on the ENS side, unwrap it, and replace/revoke all 2LDs. This is just something to keep in mind for wrapped DNS domains. Even if wrapped DNS domains do not support fuses, you can still use them as ERC-1155 NFTs. They will still have their own NFT metadata and show up in your wallet, with whatever avatar you have set, etc. They just won't have all the extra functionality that comes with the fuse/permission system. --- import { Card } from '../../components/ui/Card' # Name Wrapper Expiry In order to burn any fuses on a name, you must also set an **expiry** on it. This expiry determines how long any burned fuses are active for, and may also determine whether the name itself has expired. If the name is a .eth 2LD, then the expiry will automatically be set to the same expiry in the .eth Registrar. But for all other names, the parent can choose what expiry to set for a child name. ## Max Expiry for Subnames By default, the expiry for a name can only be set by the parent, and can only be increased, not decreased. The maximum value for the expiry of a name is the expiry of its parent name. For example, say a name expires in 5 years. The owner of the name can then set the expiry of its subnames to a maximum of 5 years as well. But the parent could also choose to set the expiry to something less. Let's say the parent sets the expiry of one of its subnames to 2 years. Then in turn, the owner of the subname can set the expiry of its own subnames up to a maximum of 2 years, but it could also set it to something less, like 1 year. Expiry Diagram The parent can set a different expiry for different subnames too, just as it can burn different fuses for different subnames. ## Renewals When a wrapped .eth second-level name (like `name.eth`) is renewed, that new expiry is automatically set in the Name Wrapper as well as in the .eth Registrar. However, the expiry for any other .eth names (like `sub.name.eth`) will not be automatically extended when the parent expiry is extended. The parent can extend the expiry for an existing subname at any time, even if the subname has been emancipated. The parent can also choose to approve a separate contract to allow the expiry for subnames to be extended by the subname owner or other accounts. That is basically how .eth second-level names work: Since the `eth` node is locked in the registrar contract and has the Name Wrapper (which exposes a renew method) approved as a controller, .eth second-level names can be directly renewed by their owners. The parent can further lock this approved contract in by burning the **`CANNOT_APPROVE`** fuse. There is also a special parent-controlled fuse called **`CAN_EXTEND_EXPIRY`**. If the parent burns this fuse on a subname, then the owner of that subname (or any approved controller) can also extend the expiry. So, if you are running a subname registrar and you want to enable "unruggable renewals", you can use one of the above options (or both). ## Special Cases for .eth 2LDs For .eth second-level names, the end of the name's grace period (from the .eth Registrar) is used for the expiry inside of the Name Wrapper. So if the name's expiration date in the Registrar is January 1st, then the expiry in the Name Wrapper will reflect that date _plus_ the grace period (currently 90 days, so approximately April 1st, depending on the year). When the name's expiration date (from the .eth Registrar) has been reached, and the name is now in the grace period, all Name Wrapper operations on the name will be restricted. The owner will _not_ yet lose ownership of the name, but they will also not be able to unwrap or update the name until it has been renewed. ## Expiry Implications When a name is merely **Wrapped** but not **Emancipated** or **Locked**, parent-controlled fuses can still be burned. This means that the parent can burn a custom fuse for a limited amount of time. When the expiry (end of grace period for .eth 2LDs) is reached, all fuses will be reset, but the name will otherwise be unaffected. When a name is **Emancipated** or **Locked**, the expiry has an important additional effect. In this scenario, when the expiry (end of grace period for .eth 2LDs) has been reached, **the name itself will expire**, and the owner **loses ownership** of the name. --- # Name Wrapper Use-Cases ## Lock the resolved records for a name By default, newly registered names will use the Public Resolver, which just allows the current manager/controller of the name to update any records. However, in some cases perhaps you want to make sure that a name resolves to specific records and **never** changes. You can accomplish this with the **`CANNOT_SET_RESOLVER`** fuse. Say you own `mycoolcontract.eth` representing a smart contract. You can use ENS subnames to refer to specific versions of that contract, like `1.mycoolcontract.eth`. And perhaps you want those versioned subnames to always point to: - The ETH address of that immutable contract - The ABI for that contract - The contenthash for some versioned documentation page - etc. One way to do this is just to make sure the name is **Locked**, all the records are set correctly, and then transfer the owner to some burn address so it can never be updated again. But of course this isn't ideal, because maybe there are some records that you _do_ want to update in the future. Or maybe you still want to keep ownership of that subname for other reasons. Instead of essentially burning the name, you could create a custom resolver that locks in certain records forever. Then: 1. Set the resolver of that name to your custom contract 2. Set the records however you want and lock them into the resolver 3. Burn these fuses on the name: - `PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_SET_RESOLVER` Now you can still keep ownership and even some limited management power over the name, while still guaranteeing that the ETH address, ABI, and whatever other records are completely immutable, as long as the expiry is set appropriately. ## Issue subdomains as tickets to an event Maybe you have `mycoolevent.eth` and you want to issue tickets like `1.ticket.2023.mycoolevent.eth`. If you want, you can choose to not Emancipate those subnames, but still burn some custom parent-controlled fuses. Those fuses might: - Indicate what "tier" their event ticket is - Maybe they can upgrade their ticket to a higher tier, which would burn some additional fuses - Allow them access to the express line or some VIP room - Maybe even automatically via some smart door When you burn those fuses, perhaps you also set the expiry to the day after the event ends. Or, maybe you want your attendees to be able to keep their subnames as a souvenir or proof-of-attendance! If so, then instead of letting the names expire at the end of the event, you could extend the expiry and burn some additional fuses to allow the attendees to keep them forever! In that case you might want to burn these fuses: - `CAN_EXTEND_EXPIRY | PARENT_CANNOT_CONTROL` If you want those tickets to be non-transferrable (soulbound to the address that attended), then burn these fuses: - `CAN_EXTEND_EXPIRY | PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_TRANSFER` ## Sell or rent subnames ### I want to sell / rent out subnames! Say you own the wrapped name `verypopularname.eth`. Obviously you can just manually create wrapped subnames like `my.verypopularname.eth` and then sell them on an NFT marketplace. But that sure doesn't scale well. To accomplish this, you will want to create a **subname registrar**. This is a contract that will handle all the registration / renewal for you, and then users will be able to interact with that contract in order to register their own subnames. In fact, this is exactly how .eth 2LDs are registered. The owner of the `eth` TLD (the NFT contract) delegates registration / renewal to the ETHRegistrarController contract. It is acting as a subname registrar for the name `eth`. Your contract would expose a `register` method that anyone can call. Under the hood it will use the [setSubnodeOwner](https://github.com/ensdomains/ens-contracts/tree/master/contracts/wrapper#setsubnodeowner) or [setSubnodeRecord](https://github.com/ensdomains/ens-contracts/tree/master/contracts/wrapper#setsubnoderecord) methods to create subnames, passing in the **fuses** and **expiry** you want to set. ### What fuses should I burn??? First, note that if you want to burn any fuses on subnames, then your name must be **Locked** (meaning **`CANNOT_UNWRAP`** is burned). Assuming that you want your subnames to be "unruggable", such that you cannot replace / revoke them, then you will want to burn **`PARENT_CANNOT_CONTROL`** on the subnames. This will place them in the **Emancipated** state upon registration. If you want to sell "forever" subnames, where users register once and can then keep them for as long as they wish, then you can consider burning the **`CAN_EXTEND_EXPIRY`** fuse. This will allow the subname owner to extend their own expiry whenever they want. The max expiry is the expiry of the parent name, but the .eth Registrar allows _anyone_ to renew/extend a .eth 2LD as well. If you just want to **rent** subnames, then do not burn **`CAN_EXTEND_EXPIRY`**. Instead, you could include a `renew` method on your contract that users can call for another fee. If you want to enable "unruggable renewals" for your registrar, to guarantee that users will always be able to renew, then you can call `approve` on the Name Wrapper and approve your registrar contract as the "subname renewal manager" for your name. Then, burn the **`CANNOT_APPROVE`** fuse on your name, to guarantee that you can never revoke that contract for subname renewals. See the Approved Operators section above for more info. If you want to impose other restrictions on your registered subnames, then you can burn the **`CANNOT_UNWRAP`** fuse to Lock the subname, and also burn whatever other fuses you want. For example, if you want to prevent owners of your subnames (like `my.verypopularname.eth` from creating their own subnames (like `buy.my.verypopularname.eth`), then you would burn **`CANNOT_UNWRAP`** and **`CANNOT_CREATE_SUBDOMAIN`**. To recap on fuses... - Sell permanent names: - `CAN_EXTEND_EXPIRY | PARENT_CANNOT_CONTROL` - Sell permanent names, but prevent them from creating their own subnames: - `CAN_EXTEND_EXPIRY | PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_CREATE_SUBDOMAIN` - Rent out names: - `PARENT_CANNOT_CONTROL` - Rent out names, but prevent them from transferring or reselling them: - `PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_TRANSFER` And so on, it's up to you. You can also burn whatever custom parent-controlled or owner-controlled fuses you want to. ### Can I customize my own rules and fees? Yes! It's your registrar contract, so you can impose whatever rules and fees you want. For example, the .eth Registrar imposes a 3-character minimum on all names, as well as a custom fee structure and a temporary premium auction upon expiration. By default there is no character limit on subnames, but your contract could have its own rules and fee structure or whatever you want. For example, you can: - Allow or disallow specific addresses from registering / renewing - Only allow registration based on some custom criteria like holding a specific NFT - Custom length restrictions like only 3+ characters or < 100 characters - Only allow names with characters `[a-z0-9]` and nothing else - Use a custom fee structure based on: - The length of the name - The specific characters that are in the name, like emojis - A pre-curated list of "good" names like people's first names - And whatever other rules you want. ### More information See this page for a step-by-step guide on creating and setting up your own subname registrar: Creating a Subname Registrar There is even a set of reference implementation contracts you can use as a starting base! ## Give subnames out to NFT holders ### I want to give subnames out to all of my DAO members / NFT holders! Say you own the wrapped name `mycoolnft.eth`, representing a popular NFT project you created. You want to distribute subnames like `6529.mycoolnft.eth` to all holders. One option is to just bulk create the subnames and drop the wrapped NFTs into their wallets. This might be good at least as an initial drop, because then the holders don't need to interact with any contract or spend any gas, you're doing that for them! To create the subnames, you'd use the setSubnodeOwner or setSubnodeRecord methods. You must also decide: ### How much control over the subnames do you want to relinquish? Do you want to be able to revoke subnames? Or do you want them to be completely outside your control? One thing to consider is whether you want the **current** holder of your NFT to always be able to claim/reclaim the corresponding ENS subname. If so, then you will **not** want to Emancipate those subnames (in other words, do not burn **`PARENT_CANNOT_CONTROL`**). If the subname is Emancipated, then the NFT holder could sell/transfer the NFT but keep the subname (up until the expiry). To make it easy for anyone to claim/reclaim a subname after your initial drop, you can set up a contract for this. ### Setting up a subname claim contract The claim method of your contract could: 1. Call `ownerOf` or `balanceOf` on your NFT contract to get or verify the current owner of the NFT 2. Call `ownerOf` or `balanceOf` on the ENS Name Wrapper contract to get or verify the current owner of the wrapped subname - If both owner addresses are the same, just return, nothing to do 3. Call `setSubnodeOwner` or `setSubnodeRecord` on the ENS Name Wrapper: - **owner:** The current owner of the NFT - **fuses:** What fuses you want to burn (if any) on that subname. If you burn any fuses, you must also set an expiry. - **expiry:** When the subname will expire. Then, to give that contract access to create subnames on your behalf, you would call `setApprovalForAll` on the Name Wrapper to approve your contract as an operator. Now, even if the NFT gets sold / transferred, the new owner will be able to claim their `mycoolnft.eth` subname at any time. In addition, if you expand your NFT collection in the future and there are new owners, then those new owners would be able to claim their subnames as well. If you are creating a new NFT contract, you could even bake this functionality **directly into the NFT contract** too, instead of needing a separate contract! By doing this, you wouldn't need a separate `claim` method either, your NFT contract would just **automatically transfer the wrapped ENS subname** whenever the NFT itself gets transferred! ### Giving your subname owners perks If you decide to not Emancipate the subnames that you issue, you _will_ still be able to burn any Parent-Controlled Fuses. There are 13 unreserved parent-controlled fuses that you can use however you wish! For example, perhaps you want to grant on-chain "perks" or "roles" to certain holders. You would call [setChildFuses](https://github.com/ensdomains/ens-contracts/tree/master/contracts/wrapper#setchildfuses) on the Name Wrapper and pass in the fuses you want to burn, and the expiry. This means that those "perks" or "roles" can also be time-boxed if you want. Maybe a perk expires in 1 week or something, up to you. There is also the reserved **`CAN_EXTEND_EXPIRY`** parent-controlled fuse. If you burn this, then the subname owner will be able to extend their own expiry whenever they want. --- # Name Wrapper Contract Details The Name Wrapper contract is deployed on these chains: - Mainnet: [wrapper.ens.eth](https://etherscan.io/address/0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401#code) ( `0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401` ) - Sepolia: [wrapper.ens.eth](https://sepolia.etherscan.io/address/0x0635513f179D50A207757E05759CbD106d7dFcE8#code) ( `0x0635513f179D50A207757E05759CbD106d7dFcE8` ) ## Wrapping and Unwrapping When wrapping a .eth 2LD, you transfer the Owner (Registrant) of the ERC-721 NFT to the Name Wrapper contract. The contract will then automatically take over the Manager (Controller) for the name as well. You can do this by calling the wrapETH2LD method. Or, you can directly transfer the ERC-721 NFT to the Name Wrapper contract. In return, the contract issues you an ERC-1155 NFT. ```solidity NameWrapper.wrapETH2LD(string label, address wrappedOwner, uint16 ownerControlledFuses, address resolver) // For example wrapETH2LD( "myname", // "myname.eth" but only the label 0x1234..., // The address you want to own the wrapped name 0, // The owner-controlled fuse bits OR'd together, that you want to burn 0x1234... // The address of the resolver you want to use ) ``` When wrapping any other ENS name, you transfer the Manager (Controller) of the name to the Name Wrapper contract. You can do this by calling the wrap method. In return, the contract issues you an ERC-1155 NFT. ```solidity NameWrapper.wrap(bytes name, address wrappedOwner, address resolver) // For example wrapETH2LD( 0x03737562046e616d650365746800, // The DNS-encoded version of "sub.myname.eth" 0x1234..., // The address you want to own the wrapped name 0x1234... // The address of the resolver you want to use ) ``` As the owner of the wrapped name, you can unwrap at any time by calling either unwrapETH2LD or unwrap. You can do this as long as the permission to unwrap has not been revoked. ```solidity NameWrapper.unwrapETH2LD(bytes32 labelhash, address registrant, address controller) // For example unwrapETH2LD( 0x952f..., // "myname.eth" but only the labelhash: keccak256('myname') 0x1234..., // The address you want to own the unwrapped name 0x1234... // The address you want to be the manager of the unwrapped name ) NameWrapper.unwrap(bytes32 parentNode, bytes32 labelhash, address controller) // For example unwrap( 0x6cbc..., // The namehash of the parent node, e.g. "myname.eth" 0xfa1e..., // The labelhash of the child to unwrap, e.g. keccak256('sub') 0x1234... // The address you want to be the manager of the unwrapped name ) ``` ## Burning Fuses / Setting Expiry If you are wrapping an existing .eth 2LD, then you can pass in the owner-controlled fuses at that time, see the above [Wrapping and Unwrapping](#wrapping-and-unwrapping) section. If you are creating a new subname, and you want to burn fuses at the same time, see the below [Creating Subnames](#creating-subnames) section. For other existing wrapped names, you can burn fuses with either the `setFuses` or `setChildFuses` methods. The `setFuses` method is used for a name that you own, but you do not necessarily own the parent of. You have the ability to burn any [Owner-Controlled Fuses](/wrapper/fuses#owner-controlled-fuses) you want. Note that your name must first be [Emancipated](/wrapper/states#emancipated) in order for you to be able to burn any owner-controlled fuses. All .eth 2LDs are automatically emancipated upon wrapping. When burning owner-controlled fuses, at a minimum you must burn the **`CANNOT_UNWRAP`** fuse (if it has not already been burned). ```solidity NameWrapper.setFuses(bytes32 node, uint16 ownerControlledFuses) // For example setFuses( 0x6cbc..., // The namehash of the node, e.g. "myname.eth" 1 // The owner-controlled fuse bits OR'd together, that you want to burn ) ``` The `setChildFuses` method is used for a subname that you own the parent of. As long as the subname has not yet been [Emancipated](/wrapper/states#emancipated), you can burn whatever [Parent-Controlled Fuses](/wrapper/fuses#parent-controlled-fuses) and [Owner-Controlled Fuses](/wrapper/fuses#owner-controlled-fuses) you want. At the same time, you must set an expiry for those fuses, if one is not already set. Note that your name must first be [Locked](/wrapper/states#locked) in order for you to burn fuses on any subnames. If you are only burning parent-controlled fuses, then there are no further restrictions. However, if you are burning owner-controlled fuses, then you must at a minimum burn both **`PARENT_CANNOT_CONTROL`** and **`CANNOT_UNWRAP`** on the subname to lock it at the same time. ```solidity NameWrapper.setChildFuses(bytes32 parentNode, bytes32 labelhash, uint32 fuses, uint64 expiry) // For example setChildFuses( 0x6cbc..., // The namehash of the parent node, e.g. "myname.eth" 0xfa1e..., // The labelhash of the child, e.g. keccak256('sub') 65537, // The fuse bits OR'd together, that you want to burn 2021232060 // The expiry for the subname ) ``` ## Creating Subnames This is done very similarly to how unwrapped subnames are created. You call either `setSubnodeOwner` or `setSubnodeRecord` on the wrapper contract. When a name is wrapped, all subnames created will also be wrapped by default. You can also pass in the fuses and expiry at the same time, so that the subname will be created in the fuse/permission state that you want, without needing to perform an extra transaction. ```solidity NameWrapper.setSubnodeOwner(bytes32 parentNode, string label, address owner, uint32 fuses, uint64 expiry) // For example setSubnodeOwner( 0x6cbc..., // The namehash of the parent node, e.g. "myname.eth" "sub", // The label of the subname to create 0x1234..., // The address you want to be the owner of the new subname 65536, // The fuse bits OR'd together, that you want to burn 2021232060 // The expiry for the subname ) NameWrapper.setSubnodeRecord(bytes32 parentNode, string label, address owner, address resolver, uint64 ttl, uint32 fuses, uint64 expiry) // For example setSubnodeRecord( 0x6cbc..., // The namehash of the parent node, e.g. "myname.eth" "sub", // The label of the subname to create 0x1234..., // The address you want to be the owner of the new subname 0x5678..., // The address of the resolver to set for the new subname 0, // The TTL to set for the new subname 65536, // The fuse bits OR'd together, that you want to burn 2021232060 // The expiry for the subname ) ``` ## Approved Operators ### Full-Control Operator Batch Approvals Your wrapped name is an ERC-1155 NFT that supports the `setApprovalForAll` method. When you approve an address using this method, it will have **full control** over all wrapped ENS names that you own. This method is typically used by NFT marketplace contracts. ### Name-Specific Subname Renewal Manager Approvals The Name Wrapper also supports the ERC-721 `approve` method. This method is used to approve a single "Subname Renewal Manager" for a specific name. The "Renewal Manager" does not have full control over your wrapped name, it can only set / extend the expiry on subnames. Further, if you burn the **`CANNOT_APPROVE`** fuse on your name, then the approved renewal manager can no longer be changed. You can use this to "lock in" that contract, so that you can guarantee to all subname owners that renewals/extensions can always be done. This approved renewal manager will be reset if the wrapped NFT is burned or re-minted, which happens if you unwrap the name, or if an expired name gets re-registered. It will also be reset if the wrapped NFT is transferred, **unless** the **`CANNOT_APPROVE`** fuse is burned. ### Example - Subname Registrar Contract You can use these operator approval methods to setup a separate contract that can take certain actions on your behalf. One example is setting up a "subname registrar" to allow users to register/renew subnames. That subname registrar contract would act on your behalf and allow users to register subnames. To allow this, you would call `setApprovalForAll` to give that contract full control over your name (and thus the ability to create subnames). Then, to enable "unruggable renewals", you could call `approve` on that same contract (or a separate one specific to renewals if you wish) and burn **`CANNOT_APPROVE`** to lock in subname renewals for that contract. If you need to later on, you would still be able to revoke with `setApprovalForAll`. So the contract would lose full control over your name (and the ability to create new subnames), but it would still be able to perpetually renew/extend existing subnames. And you can do all of this **without** needing to send your wrapped NFT to that contract. --- # Wrapped States ```mermaid graph LR; unregistered((Unregistered)); unwrapped(Unwrapped); wrapped(Wrapped); emancipated(Emancipated); locked(Locked); unregistered--->|register|unwrapped; unwrapped--->|wrap|wrapped; wrapped--->|unwrap|unwrapped; wrapped--->|protect|emancipated; emancipated--->|lock|locked; emancipated--->|unwrap|unwrapped; emancipated--->|expire|unregistered; locked-->|expire|unregistered; ``` Taking the Name Wrapper into account, an ENS name can be in one of these possible states: ### Unregistered The name has not even been registered/created yet, or it has expired. ### Unwrapped The name exists and has not expired (in the case of .eth second-level names). The Name Wrapper contract does not have ownership over the name. You own the name in the registry and/or .eth registrar. ### Wrapped The Name Wrapper contract has ownership of the name (in the registry/registrar). You are issued an ERC-1155 NFT in return, which proves that you are the actual owner. You can unwrap the name at any time, which burns the ERC-1155 NFT, and returns ownership in the registry/registrar back to you. If your name is a subname like `sub.name.eth`, then the owner of `name.eth` can technically replace the subname and transfer it to a different owner. In addition, the parent owner can burn parent-controlled fuses on your name. ### Emancipated The owner of the parent name is no longer able to replace this name, or burn any additional fuses on it. All .eth second-level names (like `name.eth`) are automatically put into the Emancipated state when they are wrapped. The name can still be unwrapped and rewrapped by the owner. ### Locked The name can no longer be unwrapped. The owner can now burn owner-controlled fuses on the name. Fuses for subnames of this name can now be burned as well. --- import { Card } from '../../components/ui/Card' # Name Wrapper Overview :::note Are you looking for user-facing guides on how to interact with the Name Wrapper in the ENS Manager App? If so, see here instead: Name Wrapper Guides :::