diff --git a/ens/abis.py b/ens/abis.py index cd6c71c7f3..0c4968f224 100644 --- a/ens/abis.py +++ b/ens/abis.py @@ -1209,3 +1209,23 @@ "type": "constructor", }, ] + +ERC_721_TOKEN_URI = [ + { + "name": "tokenURI", + "type": "function", + "stateMutability": "view", + "inputs": [{"name": "tokenId", "type": "uint256"}], + "outputs": [{"name": "", "type": "string"}], + } +] + +ERC_1155_TOKEN_URI = [ + { + "name": "uri", + "type": "function", + "stateMutability": "view", + "inputs": [{"name": "_id", "type": "uint256"}], + "outputs": [{"name": "", "type": "string"}], + } +] diff --git a/ens/async_ens.py b/ens/async_ens.py index 51739f94a1..5577a1ad94 100644 --- a/ens/async_ens.py +++ b/ens/async_ens.py @@ -1,6 +1,7 @@ from copy import ( deepcopy, ) +import re from typing import ( TYPE_CHECKING, Any, @@ -47,6 +48,7 @@ ) from ens.exceptions import ( AddressMismatch, + EnsAvatarError, ENSValueError, ResolverNotFound, UnauthorizedError, @@ -54,6 +56,8 @@ UnsupportedFunction, ) from ens.utils import ( + UriItem, + _parse_avatar_uri, address_in, address_to_reverse_domain, default, @@ -64,6 +68,7 @@ label_to_hash, normal_name_to_hash, normalize_name, + parse_nft_uri, raw_name_to_hash, ) @@ -431,6 +436,65 @@ async def set_text( name, r.functions.setText, (node, key, value), transact ) + async def get_avatar( + self, name: str, gateway_urls: Optional[dict[str, str]] = None + ) -> Optional[str]: + """ + Resolve the 'avatar' text record for `name` into a usable URI when possible. + For more details checkout out [ENSIP 15](https://docs.ens.domains/ensip/15/) + + :param str name: ENS name to look up + :param dict gateway_urls: optional mapping + of gateway overrides, + e.g. {"ipfs": "...", "arweave": "..."} + :return: resolved avatar URI or None + :raises EnsAvatarError: when avatar resolution fails in specific ways + """ + record = await self.get_text(name, "avatar") + if not record: + return None + gateway_urls = gateway_urls or {} + # If the avatar record contains an eip155 NFT URI, resolve token URI first + if re.search(r"eip155:", record, re.IGNORECASE): + return await self._parse_nft_avatar_uri(record, gateway_urls) + item = _parse_avatar_uri(record, gateway_urls) + if isinstance(item, UriItem): + return item.uri + raise EnsAvatarError(f"Unexpected avatar parse result for {name!r}") + + async def _parse_nft_avatar_uri( + self, record: str, gateway_urls: Optional[dict[str, str]] = None + ) -> Optional[str]: + parsed_nft = parse_nft_uri(record) + nft_uri: Optional[str] = None + if parsed_nft.namespace == "erc721": + nft_token_contract = self.w3.eth.contract( + abi=abis.ERC_721_TOKEN_URI, + address=self.w3.to_checksum_address(parsed_nft.contract_address), + ) + nft_uri = await nft_token_contract.functions.tokenURI( + int(parsed_nft.token_id) + ).call() + if parsed_nft.namespace == "erc1155": + nft_token_contract = self.w3.eth.contract( + abi=abis.ERC_1155_TOKEN_URI, + address=self.w3.to_checksum_address(parsed_nft.contract_address), + ) + nft_uri = await nft_token_contract.functions.uri( + int(parsed_nft.token_id) + ).call() + if not nft_uri: + raise EnsAvatarError( + f""" + Could not resolve to token uri + given contract address of {parsed_nft.contract_address} + """ + ) + avatar = _parse_avatar_uri(nft_uri, gateway_urls) + if isinstance(avatar, UriItem): + return avatar.uri + raise EnsAvatarError(f"Unexpected avatar parse result for {record}") + # -- private methods -- # async def _get_resolver( diff --git a/ens/constants.py b/ens/constants.py index c2415f36d8..6e7d39c05c 100644 --- a/ens/constants.py +++ b/ens/constants.py @@ -1,3 +1,5 @@ +import re + from eth_typing import ( ChecksumAddress, HexAddress, @@ -32,3 +34,30 @@ ENS_CONTENT_HASH_INTERFACE_ID = HexStr("0xbc1c58d1") ENS_MULTICHAIN_ADDRESS_INTERFACE_ID = HexStr("0xf1cb7e06") # ENSIP-9 ENS_EXTENDED_RESOLVER_INTERFACE_ID = HexStr("0x9061b923") # ENSIP-10 + +# --- avatar resolution regex --- # +NETWORK_REGEX = re.compile( + r""" + (?Phttps?:\/\/[^/]*|ipfs:\/|ipns:\/|ar:\/)? + (?P\/)? + (?Pipfs\/|ipns\/)? + (?P[\w\-.]+) + (?P\/.*)? + """, + re.VERBOSE, +) + +IPFS_HASH_REGEX = re.compile( + r""" + ^(Qm[1-9A-HJ-NP-Za-km-z]{44,} + |b[A-Za-z2-7]{58,} + |B[A-Z2-7]{58,} + |z[1-9A-HJ-NP-Za-km-z]{48,} + |F[0-9A-F]{50,}) + (\/(?P[\w\-.]+))? + (?P\/.*)?$ + """, + re.VERBOSE, +) +BASE64_REGEX = re.compile(r"^data:([a-zA-Z\-/+]*);base64,([^\"].*)") +DATA_URI_REGEX = re.compile(r"^data:([a-zA-Z\-/+]*)?(;[a-zA-Z0-9].*?)?(,)") diff --git a/ens/ens.py b/ens/ens.py index a5be5f713e..8b5866c4fb 100644 --- a/ens/ens.py +++ b/ens/ens.py @@ -1,6 +1,7 @@ from copy import ( deepcopy, ) +import re from typing import ( TYPE_CHECKING, Any, @@ -46,6 +47,7 @@ ) from .exceptions import ( AddressMismatch, + EnsAvatarError, ENSValueError, ResolverNotFound, UnauthorizedError, @@ -53,6 +55,8 @@ UnsupportedFunction, ) from .utils import ( + UriItem, + _parse_avatar_uri, address_in, address_to_reverse_domain, default, @@ -63,6 +67,7 @@ label_to_hash, normal_name_to_hash, normalize_name, + parse_nft_uri, raw_name_to_hash, ) @@ -422,7 +427,61 @@ def set_text( name, r.functions.setText, (node, key, value), transact ) + def get_avatar( + self, name: str, gateway_urls: Optional[dict[str, str]] = None + ) -> Optional[str]: + """ + Resolve the 'avatar' text record for `name` into a usable URI when possible. + For more details checkout out [ENSIP 15](https://docs.ens.domains/ensip/15/) + + :param str name: ENS name to look up + :param dict gateway_urls: optional mapping of gateway overrides, + e.g. {"ipfs": "...", "arweave": "..."} + :return: resolved avatar URI or None + :raises EnsAvatarError: when avatar resolution fails in specific ways + """ + record = self.get_text(name, "avatar") + if not record: + return None + gateway_urls = gateway_urls or {} + if re.search(r"eip155:", record, re.IGNORECASE): + return self._parse_nft_avatar_uri(record, gateway_urls) + item = _parse_avatar_uri(record, gateway_urls) + if isinstance(item, UriItem): + return item.uri + raise EnsAvatarError(f"Unexpected avatar parse result for {name!r}") + # -- private methods -- # + def _parse_nft_avatar_uri( + self, record: str, gateway_urls: Optional[dict[str, str]] = None + ) -> Optional[str]: + parsed_nft = parse_nft_uri(record) + nft_uri: Optional[str] = None + if parsed_nft.namespace == "erc721": + nft_token_contrat = self.w3.eth.contract( + abi=abis.ERC_721_TOKEN_URI, + address=self.w3.to_checksum_address(parsed_nft.contract_address), + ) + nft_uri = nft_token_contrat.functions.tokenURI( + int(parsed_nft.token_id) + ).call() + if parsed_nft.namespace == "erc1155": + nft_token_contrat = self.w3.eth.contract( + abi=abis.ERC_1155_TOKEN_URI, + address=self.w3.to_checksum_address(parsed_nft.contract_address), + ) + nft_uri = nft_token_contrat.functions.uri(int(parsed_nft.token_id)).call() + if not nft_uri: + raise EnsAvatarError( + f""" + Could not resolve to token uri + given contract address of {parsed_nft.contract_address} + """ + ) + avatar = _parse_avatar_uri(nft_uri, gateway_urls) + if isinstance(avatar, UriItem): + return avatar.uri + raise EnsAvatarError(f"Unexpected avatar parse result for {record}") def _get_resolver( self, diff --git a/ens/exceptions.py b/ens/exceptions.py index 8b11398386..d47d9a4d9f 100644 --- a/ens/exceptions.py +++ b/ens/exceptions.py @@ -104,3 +104,21 @@ class ENSValidationError(ENSException): """ Raised if there is a validation error """ + + +class EnsAvatarError(ENSException): + """ + Raised when an avatar resolution fails in specific ways + """ + + +class EnsAvatarUriResolutionError(ENSException): + """ + Raised when an avatar URI cannot be resolved to a usable form. + """ + + +class EnsAvatarInvalidNftUriError(ENSException): + """ + Raised when an NFT avatar URI is malformed or unsupported. + """ diff --git a/ens/utils.py b/ens/utils.py index 4e497595ac..1ddbb8b5ac 100644 --- a/ens/utils.py +++ b/ens/utils.py @@ -1,3 +1,7 @@ +import base64 +from dataclasses import ( + dataclass, +) from datetime import ( datetime, timezone, @@ -40,11 +44,16 @@ ACCEPTABLE_STALE_HOURS, AUCTION_START_GAS_CONSTANT, AUCTION_START_GAS_MARGINAL, + BASE64_REGEX, + DATA_URI_REGEX, EMPTY_ADDR_HEX, EMPTY_SHA3_BYTES, + IPFS_HASH_REGEX, + NETWORK_REGEX, REVERSE_REGISTRAR_DOMAIN, ) from .exceptions import ( + EnsAvatarInvalidNftUriError, ENSValidationError, InvalidName, ) @@ -341,3 +350,182 @@ def init_async_web3( ) return async_w3 + + +def _btoa_unicode(s: str) -> str: + """Base64 encode unicode string similar to btoa on JS for SVGs.""" + b = s.encode("utf-8") + return base64.b64encode(b).decode("ascii") + + +def get_gateway(custom: Optional[str], default_gateway: str) -> str: + """ + Return the gateway if avaiable, and also normalize it wihtout ending '/' + """ + if not custom: + return default_gateway + if custom.endswith("/"): + return custom[:-1] + return custom + + +@dataclass +class UriItem: + uri: str + is_on_chain: bool + is_encoded: bool + + +def _parse_avatar_uri( + uri: str, gateway_urls: Optional[dict[str, str]] = None +) -> UriItem: + """ + Parse a non-NFT avatar URI (IPFS, IPNS, Arweave, http(s), data URI, or inline SVG) + and return a UriItem describing the resolved form. + + Note: this does not attempt to fetch the content; it only resolves gateways + and normalizes inline SVG/data URIs. + """ + gateway_urls = gateway_urls or {} + + if BASE64_REGEX.search(uri): + return UriItem(uri=uri, is_on_chain=True, is_encoded=True) + + ipfs_gateway = get_gateway( + gateway_urls.get("ipfs") if gateway_urls else None, "https://ipfs.io" + ) + arweave_gateway = get_gateway( + gateway_urls.get("arweave") if gateway_urls else None, "https://arweave.net" + ) + m = NETWORK_REGEX.match(uri) + groups = m.groupdict() if m else {} + protocol = groups.get("protocol") + subpath = groups.get("subpath") + target = groups.get("target") + subtarget = groups.get("subtarget") or "" + is_ipns = protocol == "ipns:/" or subpath == "ipns/" + is_ipfs = ( + protocol == "ipfs:/" or subpath == "ipfs/" or bool(IPFS_HASH_REGEX.search(uri)) + ) + + # http(s) URIs that are not ipfs/ipns should be returned as-is + if uri.startswith("http") and not is_ipns and not is_ipfs: + replaced_uri = uri + if gateway_urls and gateway_urls.get("arweave"): + replaced_uri = uri.replace( + "https://arweave.net", gateway_urls.get("arweave") + ) + return UriItem(uri=replaced_uri, is_on_chain=False, is_encoded=False) + + """ + If the http URI actually contains an ipfs/ipns path, + normalize to chosen ipfs gateway + """ + if (is_ipns or is_ipfs) and target: + path_type = "ipns" if is_ipns else "ipfs" + return UriItem( + uri=f"{ipfs_gateway}/{path_type}/{target}{subtarget}", + is_on_chain=False, + is_encoded=False, + ) + + if protocol == "ar:/" and target: + return UriItem( + uri=f"{arweave_gateway}/{target}{subtarget or ''}", + is_on_chain=False, + is_encoded=False, + ) + + # Strip any leading data + parsed_uri = DATA_URI_REGEX.sub("", uri) + if parsed_uri.startswith(" ParsedNft: + """ + Parse an NFT URI according to CAIP-22 / CAIP-29 standards. + """ + uri = uri_ + + # Convert DID format to CAIP (replace did:nft: prefix and underscores) + if uri.startswith("did:nft:"): + uri = uri.replace("did:nft:", "").replace("_", "/") + + # Split the parts + try: + reference, asset_namespace, token_id = uri.split("/") + except ValueError: + raise EnsAvatarInvalidNftUriError("Invalid NFT URI format") + + try: + eip_namespace, chain_id = reference.split(":") + except ValueError: + raise EnsAvatarInvalidNftUriError("Invalid EIP reference in URI") + + try: + erc_namespace, contract_address = asset_namespace.split(":") + except ValueError: + raise EnsAvatarInvalidNftUriError("Invalid asset namespace in URI") + + # Validate parts + if not eip_namespace or eip_namespace.lower() != "eip155": + raise EnsAvatarInvalidNftUriError("Only EIP-155 supported") + if not chain_id: + raise EnsAvatarInvalidNftUriError("Chain ID not found") + if not contract_address: + raise EnsAvatarInvalidNftUriError("Contract address not found") + if not token_id: + raise EnsAvatarInvalidNftUriError("Token ID not found") + if not erc_namespace: + raise EnsAvatarInvalidNftUriError("ERC namespace not found") + + return ParsedNft( + chain_id=int(chain_id), + namespace=erc_namespace.lower(), + contract_address=contract_address, + token_id=token_id, + ) diff --git a/tests/ens/test_get_avatar.py b/tests/ens/test_get_avatar.py new file mode 100644 index 0000000000..639763d629 --- /dev/null +++ b/tests/ens/test_get_avatar.py @@ -0,0 +1,204 @@ +import pytest + +from web3._utils.contract_sources.contract_data.e_r_c_721_token_uri import ( + E_R_C721_TOKEN_URI_ABI, + E_R_C721_TOKEN_URI_BYTECODE, + E_R_C721_TOKEN_URI_RUNTIME, +) +from web3._utils.contract_sources.contract_data.e_r_c_1155_token_uri import ( + E_R_C1155_TOKEN_URI_ABI, + E_R_C1155_TOKEN_URI_BYTECODE, + E_R_C1155_TOKEN_URI_RUNTIME, +) + + +def test_get_avatar_image_uri_from_erc721(ens): + plain_image_uri = "https://euc.li/vitalik.eth" + address = ens.w3.eth.accounts[2] + tx_hash = ( + ens.w3.eth.contract( + bytecode=E_R_C721_TOKEN_URI_BYTECODE, + bytecode_runtime=E_R_C721_TOKEN_URI_RUNTIME, + abi=E_R_C721_TOKEN_URI_ABI, + ) + .constructor() + .transact({"from": address}) + ) + assert tx_hash + tx_receipt = ens.w3.eth.wait_for_transaction_receipt(tx_hash) + assert tx_receipt + erc721_resolver = ens.w3.eth.contract( + address=tx_receipt.contractAddress, abi=E_R_C721_TOKEN_URI_ABI + ) + update_uri_hash = erc721_resolver.functions.setUri(plain_image_uri).transact( + {"from": address} + ) + assert update_uri_hash + assert ens.w3.eth.wait_for_transaction_receipt(update_uri_hash) + avatar_uri = f"eip155:1/erc721:{erc721_resolver.address}/81123" + ens.setup_address("tester.eth", address) + ens.set_text("tester.eth", "avatar", avatar_uri, transact={"from": address}) + assert ens.get_avatar("tester.eth") == plain_image_uri + # clean up + ens.setup_address("tester.eth", None) + + +def test_get_avatar_image_uri_from_erc1155(ens): + plain_image_uri = "https://euc.li/vitalik.eth" + address = ens.w3.eth.accounts[2] + tx_hash = ( + ens.w3.eth.contract( + bytecode=E_R_C1155_TOKEN_URI_BYTECODE, + bytecode_runtime=E_R_C1155_TOKEN_URI_RUNTIME, + abi=E_R_C1155_TOKEN_URI_ABI, + ) + .constructor() + .transact({"from": address}) + ) + assert tx_hash + tx_receipt = ens.w3.eth.wait_for_transaction_receipt(tx_hash) + assert tx_receipt + erc1155_resolver = ens.w3.eth.contract( + address=tx_receipt.contractAddress, abi=E_R_C1155_TOKEN_URI_ABI + ) + update_uri_hash = erc1155_resolver.functions.setUri(plain_image_uri).transact( + {"from": address} + ) + assert update_uri_hash + assert ens.w3.eth.wait_for_transaction_receipt(update_uri_hash) + avatar_uri = f"eip155:1/erc1155:{erc1155_resolver.address}/81123167" + ens.setup_address("tester.eth", address) + ens.set_text("tester.eth", "avatar", avatar_uri, transact={"from": address}) + assert ens.get_avatar("tester.eth") == plain_image_uri + # clean up + ens.setup_address("tester.eth", None) + + +def test_get_avatar_image_uri_without_gateway(ens): + plain_image_uri = "https://euc.li/vitalik.eth" + accounts = ens.w3.eth.accounts + address = accounts[2] + ens.setup_address("tester.eth", address) + ens.set_text("tester.eth", "avatar", plain_image_uri, transact={"from": address}) + assert ens.get_avatar("tester.eth") == plain_image_uri + # clean up + ens.setup_address("tester.eth", None) + + +def test_get_avatar_for_uri_with_unparsable_uri(ens): + with pytest.raises(ValueError): + badly_formated_uri = "euc.li/vitalik.eth" + address = ens.w3.eth.accounts[2] + ens.setup_address("tester.eth", address) + ens.set_text( + "tester.eth", "avatar", badly_formated_uri, transact={"from": address} + ) + ens.get_avatar("tester.eth") + + +@pytest.mark.asyncio +async def test_async_get_avatar_image_uri_from_erc721(async_ens): + plain_image_uri = "https://euc.li/vitalik.eth" + accounts = await async_ens.w3.eth.accounts + address = accounts[2] + + tx_hash = ( + await async_ens.w3.eth.contract( + bytecode=E_R_C721_TOKEN_URI_BYTECODE, + bytecode_runtime=E_R_C721_TOKEN_URI_RUNTIME, + abi=E_R_C721_TOKEN_URI_ABI, + ) + .constructor() + .transact({"from": address}) + ) + assert tx_hash is not None + tx_receipt = await async_ens.w3.eth.wait_for_transaction_receipt(tx_hash) + assert tx_receipt is not None + erc721_resolver = async_ens.w3.eth.contract( + address=tx_receipt.contractAddress, abi=E_R_C721_TOKEN_URI_ABI + ) + update_uri_hash = await erc721_resolver.functions.setUri(plain_image_uri).transact( + {"from": address} + ) + assert update_uri_hash is not None + assert ( + await async_ens.w3.eth.wait_for_transaction_receipt(update_uri_hash) is not None + ) + avatar_uri = f"eip155:1/erc721:{erc721_resolver.address}/811231" + await async_ens.setup_address("tester.eth", address) + await async_ens.set_text( + "tester.eth", "avatar", avatar_uri, transact={"from": address} + ) + result = await async_ens.get_avatar("tester.eth") + assert result == plain_image_uri + # clean up + await async_ens.setup_address("tester.eth", None) + + +@pytest.mark.asyncio +async def test_async_get_avatar_image_uri_from_erc1155(async_ens): + plain_image_uri = "https://euc.li/vitalik.eth" + accounts = await async_ens.w3.eth.accounts + address = accounts[2] + + tx_hash = ( + await async_ens.w3.eth.contract( + bytecode=E_R_C1155_TOKEN_URI_BYTECODE, + bytecode_runtime=E_R_C1155_TOKEN_URI_RUNTIME, + abi=E_R_C1155_TOKEN_URI_ABI, + ) + .constructor() + .transact({"from": address}) + ) + assert tx_hash + tx_receipt = await async_ens.w3.eth.wait_for_transaction_receipt(tx_hash) + assert tx_receipt + erc1155_resolver = async_ens.w3.eth.contract( + address=tx_receipt.contractAddress, abi=E_R_C1155_TOKEN_URI_ABI + ) + update_uri_hash = await erc1155_resolver.functions.setUri(plain_image_uri).transact( + {"from": address} + ) + assert update_uri_hash + assert ( + await async_ens.w3.eth.wait_for_transaction_receipt(update_uri_hash) is not None + ) + avatar_uri = f"eip155:1/erc1155:{erc1155_resolver.address}/8112316" + await async_ens.setup_address("tester.eth", address) + await async_ens.set_text( + "tester.eth", "avatar", avatar_uri, transact={"from": address} + ) + result = await async_ens.get_avatar("tester.eth") + assert result == plain_image_uri + # clean up + await async_ens.setup_address("tester.eth", None) + + +@pytest.mark.asyncio +async def test_async_get_avatar_image_uri_without_gateway(async_ens): + plain_image_uri = "https://euc.li/vitalik.eth" + accounts = await async_ens.w3.eth.accounts + address = accounts[2] + await async_ens.setup_address("tester.eth", address) + await async_ens.set_text( + "tester.eth", "avatar", plain_image_uri, transact={"from": address} + ) + result = await async_ens.get_avatar("tester.eth") + assert result == plain_image_uri + # clean up + await async_ens.setup_address("tester.eth", None) + + +@pytest.mark.asyncio +async def test_async_get_avatar_for_uri_with_unparsable_uri(async_ens): + badly_formated_uri = "euc.li/vitalik.eth" + accounts = await async_ens.w3.eth.accounts + address = accounts[2] + await async_ens.setup_address("tester.eth", address) + with pytest.raises(ValueError): + await async_ens.set_text( + "tester.eth", "avatar", badly_formated_uri, transact={"from": address} + ) + await async_ens.get_avatar("tester.eth") + # clean up + await async_ens.setup_address("tester.eth", None) diff --git a/tests/ens/test_utils.py b/tests/ens/test_utils.py index 008cd34fe2..8101398750 100644 --- a/tests/ens/test_utils.py +++ b/tests/ens/test_utils.py @@ -1,4 +1,5 @@ import pytest +import base64 from unittest import ( mock, ) @@ -23,6 +24,8 @@ ENSValidationError, ) from ens.utils import ( + UriItem, + _parse_avatar_uri, dns_encode_name, ens_encode_name, init_async_web3, @@ -245,3 +248,45 @@ async def test_init_async_web3_with_provider_argument_adds_async_eth(): latest_block = await async_w3.eth.get_block("latest") assert latest_block assert is_integer(latest_block["number"]) + + +def test_ipfs_path_normalizes_to_gateway(): + # use a simple ipfs path form that network_regex will match (subpath "ipfs/") + uri = "ipfs/QmTestHash123" + item = _parse_avatar_uri(uri) + assert isinstance(item, UriItem) + assert item.uri == "https://ipfs.io/ipfs/QmTestHash123" + assert item.is_on_chain is False + assert item.is_encoded is False + + +def test_inline_svg_converted_to_data_uri(): + svg = "" + item = _parse_avatar_uri(svg) + assert isinstance(item, UriItem) + assert item.is_on_chain is True + assert item.is_encoded is False + assert item.uri.startswith("data:image/svg+xml;base64,") + # verify the payload decodes back to the original svg (utf-8) + payload_b64 = item.uri.split(",", 1)[1] + decoded = base64.b64decode(payload_b64).decode("utf-8") + assert decoded == svg + + +def test_data_base64_detected_as_encoded(): + data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA" + item = _parse_avatar_uri(data_uri) + assert isinstance(item, UriItem) + assert item.is_on_chain is True + assert item.is_encoded is True + assert item.uri == data_uri + + +def test_arweave_gateway_replacement(): + original = "https://arweave.net/someid/g.png" + custom = {"arweave": "https://mygateway.test"} + item = _parse_avatar_uri(original, gateway_urls=custom) + assert isinstance(item, UriItem) + assert item.uri == "https://mygateway.test/someid/g.png" + assert item.is_on_chain is False + assert item.is_encoded is False diff --git a/web3/_utils/contract_sources/ERC1155TokenUri.sol b/web3/_utils/contract_sources/ERC1155TokenUri.sol new file mode 100644 index 0000000000..b6e091225e --- /dev/null +++ b/web3/_utils/contract_sources/ERC1155TokenUri.sol @@ -0,0 +1,25 @@ +pragma solidity >=0.7.0; + +/** + * @title MinimalERC1155 + * @dev Minimal,-from-scratch ERC-1155-like implementation for Solidity 0.6.x + * Not a full-featured production library — but implements the core behavior + * required for minting and safe transfers (single + batch) and approvals. + */ +contract ERC1155TokenUri { + string private _uri = + "https://i.seadn.io/gcs/files/3ae7be6c41ad4767bf3ecbc0493b4bfb.png?w=4000&auto=format"; + mapping(uint256 => mapping(address => uint256)) private _balances; + mapping(address => mapping(address => bool)) private _operatorApprovals; + event URI(string value, uint256 indexed id); + + constructor() public {} + + function setUri(string memory new_uri) external { + _uri = new_uri; + } + + function uri(uint256 id) external view returns (string memory) { + return _uri; + } +} diff --git a/web3/_utils/contract_sources/ERC721TokenUri.sol b/web3/_utils/contract_sources/ERC721TokenUri.sol new file mode 100644 index 0000000000..fea038279d --- /dev/null +++ b/web3/_utils/contract_sources/ERC721TokenUri.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.7.0 <0.9.0; + +/** + * @title ERC721 + * @dev Minimal ERC-721 stub that only exposes `tokenURI(uint256)` + * Used for metadata or ENS avatar resolution tests. + */ +contract ERC721TokenUri { + // Static URI — same for all token IDs + string private _uri = + "https://i.seadn.io/gcs/files/3ae7be6c41ad4767bf3ecbc0493b4bfb.png?w=4000&auto=format"; + + constructor() {} + + function setUri(string memory new_uri) external { + _uri = new_uri; + } + + /** + * @notice Returns the same metadata URI for any tokenId. + */ + function tokenURI( + uint256 /* tokenId */ + ) external view returns (string memory) { + return _uri; + } +} diff --git a/web3/_utils/contract_sources/contract_data/e_r_c_1155_token_uri.py b/web3/_utils/contract_sources/contract_data/e_r_c_1155_token_uri.py new file mode 100644 index 0000000000..3c7167482e --- /dev/null +++ b/web3/_utils/contract_sources/contract_data/e_r_c_1155_token_uri.py @@ -0,0 +1,49 @@ +""" +Generated by `compile_contracts.py` script. +Compiled with Solidity v0.8.17. +""" + +# source: web3/_utils/contract_sources/ERC1155TokenUri.sol:ERC1155TokenUri +E_R_C1155_TOKEN_URI_BYTECODE = "0x608060405260405180608001604052806054815260200162000a9d60549139600090816200002e9190620002bd565b503480156200003c57600080fd5b50620003a4565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680620000c557607f821691505b602082108103620000db57620000da6200007d565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620001457fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000106565b62000151868362000106565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b60006200019e62000198620001928462000169565b62000173565b62000169565b9050919050565b6000819050919050565b620001ba836200017d565b620001d2620001c982620001a5565b84845462000113565b825550505050565b600090565b620001e9620001da565b620001f6818484620001af565b505050565b5b818110156200021e5762000212600082620001df565b600181019050620001fc565b5050565b601f8211156200026d576200023781620000e1565b6200024284620000f6565b8101602085101562000252578190505b6200026a6200026185620000f6565b830182620001fb565b50505b505050565b600082821c905092915050565b6000620002926000198460080262000272565b1980831691505092915050565b6000620002ad83836200027f565b9150826002028217905092915050565b620002c88262000043565b67ffffffffffffffff811115620002e457620002e36200004e565b5b620002f08254620000ac565b620002fd82828562000222565b600060209050601f83116001811462000335576000841562000320578287015190505b6200032c85826200029f565b8655506200039c565b601f1984166200034586620000e1565b60005b828110156200036f5784890151825560018201915060208501945060208101905062000348565b868310156200038f57848901516200038b601f8916826200027f565b8355505b6001600288020188555050505b505050505050565b6106e980620003b46000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80630e89341c1461003b5780639b642de11461006b575b600080fd5b61005560048036038101906100509190610178565b610087565b6040516100629190610235565b60405180910390f35b6100856004803603810190610080919061038c565b61011b565b005b60606000805461009690610404565b80601f01602080910402602001604051908101604052809291908181526020018280546100c290610404565b801561010f5780601f106100e45761010080835404028352916020019161010f565b820191906000526020600020905b8154815290600101906020018083116100f257829003601f168201915b50505050509050919050565b806000908161012a91906105e1565b5050565b6000604051905090565b600080fd5b600080fd5b6000819050919050565b61015581610142565b811461016057600080fd5b50565b6000813590506101728161014c565b92915050565b60006020828403121561018e5761018d610138565b5b600061019c84828501610163565b91505092915050565b600081519050919050565b600082825260208201905092915050565b60005b838110156101df5780820151818401526020810190506101c4565b60008484015250505050565b6000601f19601f8301169050919050565b6000610207826101a5565b61021181856101b0565b93506102218185602086016101c1565b61022a816101eb565b840191505092915050565b6000602082019050818103600083015261024f81846101fc565b905092915050565b600080fd5b600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b610299826101eb565b810181811067ffffffffffffffff821117156102b8576102b7610261565b5b80604052505050565b60006102cb61012e565b90506102d78282610290565b919050565b600067ffffffffffffffff8211156102f7576102f6610261565b5b610300826101eb565b9050602081019050919050565b82818337600083830152505050565b600061032f61032a846102dc565b6102c1565b90508281526020810184848401111561034b5761034a61025c565b5b61035684828561030d565b509392505050565b600082601f83011261037357610372610257565b5b813561038384826020860161031c565b91505092915050565b6000602082840312156103a2576103a1610138565b5b600082013567ffffffffffffffff8111156103c0576103bf61013d565b5b6103cc8482850161035e565b91505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000600282049050600182168061041c57607f821691505b60208210810361042f5761042e6103d5565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b6000600883026104977fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8261045a565b6104a1868361045a565b95508019841693508086168417925050509392505050565b6000819050919050565b60006104de6104d96104d484610142565b6104b9565b610142565b9050919050565b6000819050919050565b6104f8836104c3565b61050c610504826104e5565b848454610467565b825550505050565b600090565b610521610514565b61052c8184846104ef565b505050565b5b8181101561055057610545600082610519565b600181019050610532565b5050565b601f8211156105955761056681610435565b61056f8461044a565b8101602085101561057e578190505b61059261058a8561044a565b830182610531565b50505b505050565b600082821c905092915050565b60006105b86000198460080261059a565b1980831691505092915050565b60006105d183836105a7565b9150826002028217905092915050565b6105ea826101a5565b67ffffffffffffffff81111561060357610602610261565b5b61060d8254610404565b610618828285610554565b600060209050601f83116001811461064b5760008415610639578287015190505b61064385826105c5565b8655506106ab565b601f19841661065986610435565b60005b828110156106815784890151825560018201915060208501945060208101905061065c565b8683101561069e578489015161069a601f8916826105a7565b8355505b6001600288020188555050505b50505050505056fea2646970667358221220563ddc9f70f8cab76d19bc4a45300538b17f706208b798bf27475f0ee648fa2664736f6c6343000811003368747470733a2f2f692e736561646e2e696f2f6763732f66696c65732f33616537626536633431616434373637626633656362633034393362346266622e706e673f773d34303030266175746f3d666f726d6174" # noqa: E501 +E_R_C1155_TOKEN_URI_RUNTIME = "0x608060405234801561001057600080fd5b50600436106100365760003560e01c80630e89341c1461003b5780639b642de11461006b575b600080fd5b61005560048036038101906100509190610178565b610087565b6040516100629190610235565b60405180910390f35b6100856004803603810190610080919061038c565b61011b565b005b60606000805461009690610404565b80601f01602080910402602001604051908101604052809291908181526020018280546100c290610404565b801561010f5780601f106100e45761010080835404028352916020019161010f565b820191906000526020600020905b8154815290600101906020018083116100f257829003601f168201915b50505050509050919050565b806000908161012a91906105e1565b5050565b6000604051905090565b600080fd5b600080fd5b6000819050919050565b61015581610142565b811461016057600080fd5b50565b6000813590506101728161014c565b92915050565b60006020828403121561018e5761018d610138565b5b600061019c84828501610163565b91505092915050565b600081519050919050565b600082825260208201905092915050565b60005b838110156101df5780820151818401526020810190506101c4565b60008484015250505050565b6000601f19601f8301169050919050565b6000610207826101a5565b61021181856101b0565b93506102218185602086016101c1565b61022a816101eb565b840191505092915050565b6000602082019050818103600083015261024f81846101fc565b905092915050565b600080fd5b600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b610299826101eb565b810181811067ffffffffffffffff821117156102b8576102b7610261565b5b80604052505050565b60006102cb61012e565b90506102d78282610290565b919050565b600067ffffffffffffffff8211156102f7576102f6610261565b5b610300826101eb565b9050602081019050919050565b82818337600083830152505050565b600061032f61032a846102dc565b6102c1565b90508281526020810184848401111561034b5761034a61025c565b5b61035684828561030d565b509392505050565b600082601f83011261037357610372610257565b5b813561038384826020860161031c565b91505092915050565b6000602082840312156103a2576103a1610138565b5b600082013567ffffffffffffffff8111156103c0576103bf61013d565b5b6103cc8482850161035e565b91505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000600282049050600182168061041c57607f821691505b60208210810361042f5761042e6103d5565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b6000600883026104977fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8261045a565b6104a1868361045a565b95508019841693508086168417925050509392505050565b6000819050919050565b60006104de6104d96104d484610142565b6104b9565b610142565b9050919050565b6000819050919050565b6104f8836104c3565b61050c610504826104e5565b848454610467565b825550505050565b600090565b610521610514565b61052c8184846104ef565b505050565b5b8181101561055057610545600082610519565b600181019050610532565b5050565b601f8211156105955761056681610435565b61056f8461044a565b8101602085101561057e578190505b61059261058a8561044a565b830182610531565b50505b505050565b600082821c905092915050565b60006105b86000198460080261059a565b1980831691505092915050565b60006105d183836105a7565b9150826002028217905092915050565b6105ea826101a5565b67ffffffffffffffff81111561060357610602610261565b5b61060d8254610404565b610618828285610554565b600060209050601f83116001811461064b5760008415610639578287015190505b61064385826105c5565b8655506106ab565b601f19841661065986610435565b60005b828110156106815784890151825560018201915060208501945060208101905061065c565b8683101561069e578489015161069a601f8916826105a7565b8355505b6001600288020188555050505b50505050505056fea2646970667358221220563ddc9f70f8cab76d19bc4a45300538b17f706208b798bf27475f0ee648fa2664736f6c63430008110033" # noqa: E501 +E_R_C1155_TOKEN_URI_ABI = [ + {"inputs": [], "stateMutability": "nonpayable", "type": "constructor"}, + { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "string", + "name": "value", + "type": "string", + }, + { + "indexed": True, + "internalType": "uint256", + "name": "id", + "type": "uint256", + }, + ], + "name": "URI", + "type": "event", + }, + { + "inputs": [{"internalType": "string", "name": "new_uri", "type": "string"}], + "name": "setUri", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [{"internalType": "uint256", "name": "id", "type": "uint256"}], + "name": "uri", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "stateMutability": "view", + "type": "function", + }, +] +E_R_C1155_TOKEN_URI_DATA = { + "bytecode": E_R_C1155_TOKEN_URI_BYTECODE, + "bytecode_runtime": E_R_C1155_TOKEN_URI_RUNTIME, + "abi": E_R_C1155_TOKEN_URI_ABI, +} diff --git a/web3/_utils/contract_sources/contract_data/e_r_c_721_token_uri.py b/web3/_utils/contract_sources/contract_data/e_r_c_721_token_uri.py new file mode 100644 index 0000000000..262182c7e2 --- /dev/null +++ b/web3/_utils/contract_sources/contract_data/e_r_c_721_token_uri.py @@ -0,0 +1,30 @@ +""" +Generated by `compile_contracts.py` script. +Compiled with Solidity v0.8.17. +""" + +# source: web3/_utils/contract_sources/ERC721TokenUri.sol:ERC721TokenUri +E_R_C721_TOKEN_URI_BYTECODE = "0x608060405260405180608001604052806054815260200162000a9d60549139600090816200002e9190620002bd565b503480156200003c57600080fd5b50620003a4565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680620000c557607f821691505b602082108103620000db57620000da6200007d565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620001457fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000106565b62000151868362000106565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b60006200019e62000198620001928462000169565b62000173565b62000169565b9050919050565b6000819050919050565b620001ba836200017d565b620001d2620001c982620001a5565b84845462000113565b825550505050565b600090565b620001e9620001da565b620001f6818484620001af565b505050565b5b818110156200021e5762000212600082620001df565b600181019050620001fc565b5050565b601f8211156200026d576200023781620000e1565b6200024284620000f6565b8101602085101562000252578190505b6200026a6200026185620000f6565b830182620001fb565b50505b505050565b600082821c905092915050565b6000620002926000198460080262000272565b1980831691505092915050565b6000620002ad83836200027f565b9150826002028217905092915050565b620002c88262000043565b67ffffffffffffffff811115620002e457620002e36200004e565b5b620002f08254620000ac565b620002fd82828562000222565b600060209050601f83116001811462000335576000841562000320578287015190505b6200032c85826200029f565b8655506200039c565b601f1984166200034586620000e1565b60005b828110156200036f5784890151825560018201915060208501945060208101905062000348565b868310156200038f57848901516200038b601f8916826200027f565b8355505b6001600288020188555050505b505050505050565b6106e980620003b46000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80639b642de11461003b578063c87b56dd14610057575b600080fd5b61005560048036038101906100509190610288565b610087565b005b610071600480360381019061006c9190610307565b61009a565b60405161007e91906103b3565b60405180910390f35b806000908161009691906105e1565b5050565b6060600080546100a990610404565b80601f01602080910402602001604051908101604052809291908181526020018280546100d590610404565b80156101225780601f106100f757610100808354040283529160200191610122565b820191906000526020600020905b81548152906001019060200180831161010557829003601f168201915b50505050509050919050565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6101958261014c565b810181811067ffffffffffffffff821117156101b4576101b361015d565b5b80604052505050565b60006101c761012e565b90506101d3828261018c565b919050565b600067ffffffffffffffff8211156101f3576101f261015d565b5b6101fc8261014c565b9050602081019050919050565b82818337600083830152505050565b600061022b610226846101d8565b6101bd565b90508281526020810184848401111561024757610246610147565b5b610252848285610209565b509392505050565b600082601f83011261026f5761026e610142565b5b813561027f848260208601610218565b91505092915050565b60006020828403121561029e5761029d610138565b5b600082013567ffffffffffffffff8111156102bc576102bb61013d565b5b6102c88482850161025a565b91505092915050565b6000819050919050565b6102e4816102d1565b81146102ef57600080fd5b50565b600081359050610301816102db565b92915050565b60006020828403121561031d5761031c610138565b5b600061032b848285016102f2565b91505092915050565b600081519050919050565b600082825260208201905092915050565b60005b8381101561036e578082015181840152602081019050610353565b60008484015250505050565b600061038582610334565b61038f818561033f565b935061039f818560208601610350565b6103a88161014c565b840191505092915050565b600060208201905081810360008301526103cd818461037a565b905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000600282049050600182168061041c57607f821691505b60208210810361042f5761042e6103d5565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b6000600883026104977fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8261045a565b6104a1868361045a565b95508019841693508086168417925050509392505050565b6000819050919050565b60006104de6104d96104d4846102d1565b6104b9565b6102d1565b9050919050565b6000819050919050565b6104f8836104c3565b61050c610504826104e5565b848454610467565b825550505050565b600090565b610521610514565b61052c8184846104ef565b505050565b5b8181101561055057610545600082610519565b600181019050610532565b5050565b601f8211156105955761056681610435565b61056f8461044a565b8101602085101561057e578190505b61059261058a8561044a565b830182610531565b50505b505050565b600082821c905092915050565b60006105b86000198460080261059a565b1980831691505092915050565b60006105d183836105a7565b9150826002028217905092915050565b6105ea82610334565b67ffffffffffffffff8111156106035761060261015d565b5b61060d8254610404565b610618828285610554565b600060209050601f83116001811461064b5760008415610639578287015190505b61064385826105c5565b8655506106ab565b601f19841661065986610435565b60005b828110156106815784890151825560018201915060208501945060208101905061065c565b8683101561069e578489015161069a601f8916826105a7565b8355505b6001600288020188555050505b50505050505056fea26469706673582212200f9a92f57333b1e08914a228f427639773412bf130cd9a045cdf203780dcc14064736f6c6343000811003368747470733a2f2f692e736561646e2e696f2f6763732f66696c65732f33616537626536633431616434373637626633656362633034393362346266622e706e673f773d34303030266175746f3d666f726d6174" # noqa: E501 +E_R_C721_TOKEN_URI_RUNTIME = "0x608060405234801561001057600080fd5b50600436106100365760003560e01c80639b642de11461003b578063c87b56dd14610057575b600080fd5b61005560048036038101906100509190610288565b610087565b005b610071600480360381019061006c9190610307565b61009a565b60405161007e91906103b3565b60405180910390f35b806000908161009691906105e1565b5050565b6060600080546100a990610404565b80601f01602080910402602001604051908101604052809291908181526020018280546100d590610404565b80156101225780601f106100f757610100808354040283529160200191610122565b820191906000526020600020905b81548152906001019060200180831161010557829003601f168201915b50505050509050919050565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6101958261014c565b810181811067ffffffffffffffff821117156101b4576101b361015d565b5b80604052505050565b60006101c761012e565b90506101d3828261018c565b919050565b600067ffffffffffffffff8211156101f3576101f261015d565b5b6101fc8261014c565b9050602081019050919050565b82818337600083830152505050565b600061022b610226846101d8565b6101bd565b90508281526020810184848401111561024757610246610147565b5b610252848285610209565b509392505050565b600082601f83011261026f5761026e610142565b5b813561027f848260208601610218565b91505092915050565b60006020828403121561029e5761029d610138565b5b600082013567ffffffffffffffff8111156102bc576102bb61013d565b5b6102c88482850161025a565b91505092915050565b6000819050919050565b6102e4816102d1565b81146102ef57600080fd5b50565b600081359050610301816102db565b92915050565b60006020828403121561031d5761031c610138565b5b600061032b848285016102f2565b91505092915050565b600081519050919050565b600082825260208201905092915050565b60005b8381101561036e578082015181840152602081019050610353565b60008484015250505050565b600061038582610334565b61038f818561033f565b935061039f818560208601610350565b6103a88161014c565b840191505092915050565b600060208201905081810360008301526103cd818461037a565b905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000600282049050600182168061041c57607f821691505b60208210810361042f5761042e6103d5565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b6000600883026104977fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8261045a565b6104a1868361045a565b95508019841693508086168417925050509392505050565b6000819050919050565b60006104de6104d96104d4846102d1565b6104b9565b6102d1565b9050919050565b6000819050919050565b6104f8836104c3565b61050c610504826104e5565b848454610467565b825550505050565b600090565b610521610514565b61052c8184846104ef565b505050565b5b8181101561055057610545600082610519565b600181019050610532565b5050565b601f8211156105955761056681610435565b61056f8461044a565b8101602085101561057e578190505b61059261058a8561044a565b830182610531565b50505b505050565b600082821c905092915050565b60006105b86000198460080261059a565b1980831691505092915050565b60006105d183836105a7565b9150826002028217905092915050565b6105ea82610334565b67ffffffffffffffff8111156106035761060261015d565b5b61060d8254610404565b610618828285610554565b600060209050601f83116001811461064b5760008415610639578287015190505b61064385826105c5565b8655506106ab565b601f19841661065986610435565b60005b828110156106815784890151825560018201915060208501945060208101905061065c565b8683101561069e578489015161069a601f8916826105a7565b8355505b6001600288020188555050505b50505050505056fea26469706673582212200f9a92f57333b1e08914a228f427639773412bf130cd9a045cdf203780dcc14064736f6c63430008110033" # noqa: E501 +E_R_C721_TOKEN_URI_ABI = [ + {"inputs": [], "stateMutability": "nonpayable", "type": "constructor"}, + { + "inputs": [{"internalType": "string", "name": "new_uri", "type": "string"}], + "name": "setUri", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "name": "tokenURI", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "stateMutability": "view", + "type": "function", + }, +] +E_R_C721_TOKEN_URI_DATA = { + "bytecode": E_R_C721_TOKEN_URI_BYTECODE, + "bytecode_runtime": E_R_C721_TOKEN_URI_RUNTIME, + "abi": E_R_C721_TOKEN_URI_ABI, +}