Skip to content

Instantly share code, notes, and snippets.

@alisinabh
Created February 6, 2024 05:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alisinabh/c34bf2073619306c500b690cd613e37c to your computer and use it in GitHub Desktop.
Save alisinabh/c34bf2073619306c500b690cd613e37c to your computer and use it in GitHub Desktop.
Load NFT Data using Elixir Ethers

Fetch NFT Metadata

Mix.install([
  :ethers,
  :kino,
  :req
])

Application.put_env(:ethereumex, :url, "https://cloudflare-eth.com/v1/mainnet")

A tiny IPFS/HTTP helper

defmodule Loader do
  def load("ipfs://" <> id) do
    Req.get!("https://ipfs.io/ipfs/#{id}").body
  end

  def load("http" <> _ = url) do
    Req.get!(url).body
  end
end
{:module, Loader, <<70, 79, 82, 49, 0, 0, 8, ...>>, {:load, 1}}

ERC721 Metadata

ERC-721 Tokens have a metadata standard which can be found in this link. This standard enforces a structure to the JSON value inside the token URI. (Token URI can be any valid URI and not necessarily an HTTP(s) URL)

contract_address = "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"
token_id = 10
10
Ethers.Contracts.ERC721.name()
|> Ethers.call!(to: contract_address)
"BoredApeYachtClub"
Ethers.Contracts.ERC721.symbol()
|> Ethers.call!(to: contract_address)
"BAYC"
token_uri =
  Ethers.Contracts.ERC721.token_uri(token_id)
  |> Ethers.call!(to: contract_address)
"ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/10"
token_data = Loader.load(token_uri)
%{
  "attributes" => [
    %{"trait_type" => "Clothes", "value" => "Navy Striped Tee"},
    %{"trait_type" => "Background", "value" => "Aquamarine"},
    %{"trait_type" => "Hat", "value" => "Bayc Hat Red"},
    %{"trait_type" => "Fur", "value" => "Dmt"},
    %{"trait_type" => "Eyes", "value" => "Eyepatch"},
    %{"trait_type" => "Mouth", "value" => "Bored"}
  ],
  "image" => "ipfs://QmPQdVU1riwzijhCs1Lk6CHmDo4LpmwPPLuDauY3i8gSzL"
}

The Json structure in ERC-721 tokens is very simple and conforms to the spec below.

{
    "title": "Asset Metadata",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "Identifies the asset to which this NFT represents"
        },
        "description": {
            "type": "string",
            "description": "Describes the asset to which this NFT represents"
        },
        "image": {
            "type": "string",
            "description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
        }
    }
}

Now we can even load the image.

Loader.load(token_data["image"])
<<137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 2, 119, 0, 0, 2, 119, 8, 6, 0,
  0, 0, 246, 202, 119, 98, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 11, 19, 0, 0, 11, 19, 1, ...>>

Now we can combine all these calls together for efficiency using Multicall.

calls = [
  {Ethers.Contracts.ERC721.name(), to: contract_address},
  {Ethers.Contracts.ERC721.symbol(), to: contract_address},
  {Ethers.Contracts.ERC721.token_uri(token_id), to: contract_address}
]

Ethers.Multicall.aggregate3(calls)
|> Ethers.call!()
|> Ethers.Multicall.decode(calls)
[
  true: "BoredApeYachtClub",
  true: "BAYC",
  true: "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/10"
]

ERC1155 Metadata

ERC-1155 Tokens have a metadata standard which can be found in this link. This standard enforces a structure to the JSON value inside the token URI. (Token URI can be any valid URI and not necessarily an HTTP(s) URL)

erc_1155_contract_address = "0x53894ec021245adb6a7c556bb0f0ad83544c0e33"
erc_1155_token_id = 1
1
token_uri =
  Ethers.Contracts.ERC1155.uri(erc_1155_token_id)
  |> Ethers.call!(to: erc_1155_contract_address)
"ipfs://bafkreiesnznz43fnowny77t442vhx46b2ksvbid53klekhq5c5rsymzuiq"
token_data = Loader.load(token_uri)
%{
  "description" => "Paddy's Pass is the doorway into the UA3 Community where art is our first love, but sharing wins with the community remains a priority! The UA3 ecosystem has been strategically structured through art, fashion, gaming and more to provide strategical one-of-a-kind revenue to the community! Paddy's community wins... While being BORED!",
  "image" => "ipfs://bafybeihhjtr2diy7cqmwx74hifobn4qigr3etwio2nfjaznnmdqieausja",
  "name" => "UA3 Paddy's Pass"
}
Loader.load(token_data["image"])
<<71, 73, 70, 56, 57, 97, 56, 4, 56, 4, 247, 184, 0, 0, 0, 0, 0, 0, 1, 2, 1, 1, 3, 1, 0, 5, 2, 1, 5,
  4, 3, 7, 5, 3, 10, 6, 2, 10, 7, 6, 11, 7, 4, 13, 10, 9, 16, 9, 2, 16, ...>>

Combine all Togather for efficiency without prior knowledge

Given a contract address, without prior knowledge we cannot know if that token is of ERC721 or ERC1155 type. This will not be a problem since we can still use all these calls in a Multicall3 aggregation and the unsupported ones can be ignored.

calls = [
  {Ethers.Contracts.ERC721.name(), to: erc_1155_contract_address},
  {Ethers.Contracts.ERC721.symbol(), to: erc_1155_contract_address},
  {Ethers.Contracts.ERC721.token_uri(erc_1155_token_id), to: erc_1155_contract_address},
  {Ethers.Contracts.ERC1155.uri(erc_1155_token_id), to: erc_1155_contract_address}
]

Ethers.Multicall.aggregate3(calls)
|> Ethers.call!()
|> Ethers.Multicall.decode(calls)
[
  false: nil,
  false: nil,
  false: nil,
  true: "ipfs://bafkreiesnznz43fnowny77t442vhx46b2ksvbid53klekhq5c5rsymzuiq"
]

The above example shows what the response will look like for an ERC1155 token.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment