AlphaWallet / alpha-wallet-android

An advanced Ethereum mobile wallet
https://www.alphawallet.com
MIT License
584 stars 528 forks source link

ERC1155 Info #1924

Open JamesSmartCell opened 3 years ago

JamesSmartCell commented 3 years ago

@hboon Some notes on ERC1155 specifically for tracking balances and URI I made:

Check the Ethereum standard https://eips.ethereum.org/EIPS/eip-1155

Token name and symbol are usually the same as ERC20/ERC721:

    string public name;
    string public symbol;

However the ERC1155 spec in openzeppelin doesn't indicate these are required. Name is also supplied in the contractURI:

Here is an example of an overall contract URI metadata, however this route may or may not be present.

function contractURI() returns(string metaData)

-->

{
  "id": "0x3ec057be60db3a2d416e14a6e3ab7f37ffccd7b8",
  "name": "GenSys.X",
  "image": "ipfs://ipfs/QmdAyBpWz1iYdw7o8LYfryogBWus2jEribC8jva8EcNoKP",
  "external_link": "https://app.rarible.com/collection/0x3ec057be60db3a2d416e14a6e3ab7f37ffccd7b8"
}

From the Ethereum standard:

{
    "title": "Token Metadata",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "Identifies the asset to which this token represents"
        },
        "decimals": {
            "type": "integer",
            "description": "The number of decimal places that the token amount should display - e.g. 18, means to divide the token amount by 1000000000000000000 to get its user representation."
        },
        "description": {
            "type": "string",
            "description": "Describes the asset to which this token represents"
        },
        "image": {
            "type": "string",
            "description": "A URI pointing to a resource with mime type image/* representing the asset to which this token represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
        },
        "properties": {
            "type": "object",
            "description": "Arbitrary properties. Values may be strings, numbers, object or arrays."
        }
    }
}

Note the decimals field above!

Note that image is the equivalent of the token image in ERC721, so in the example of a print run of 50, the image is the actual print.

Here is an example of specific tokenUri

function uri(1) returns(string token Description Metadata for token index 1)

-->

{
  "name": "Queen iShoTas1.37",
  "description": "Queen iShoTas.1.37 was put into power during the Great Oracle War of 2023 by the powerful mystic, GoLd.eFi, who is her half sister. As queen of the New World, iShoTas fell in love with the General of the Imperials Army, angering GoLd.eFi, who wanted her to remain loyal to Cohort, whose physicists had first communicated with the Mystik race. The Queen turned her back on her half sister GoLd.eFi choosing love over blood and vowing to destroy Cohorts and her half sister for casting her out and to take control of the Oracle from “The Entity”.",
  "image": "ipfs://ipfs/QmVG9wXaEsQCEdBb6oweW7mSUYu3HtxmpPKtaTpkiZyaTs/image.png",
  "external_url": "https://app.rarible.com/token/0x3ec057be60db3a2d416e14a6e3ab7f37ffccd7b8:1",
  "attributes": [
    {
      "key": "Artist",
      "trait_type": "Artist",
      "value": "AnRKey X"
    },
    {
      "key": "Rarity",
      "trait_type": "Rarity",
      "value": "1 of 33"
    },
    {
      "key": "Set",
      "trait_type": "Set",
      "value": "GenSys.X"
    },
    {
      "key": "Power",
      "trait_type": "Power",
      "value": "300,000"
    },
    {
      "key": "6DoS",
      "trait_type": "6DoS",
      "value": "1º"
    },
    {
      "key": "Year",
      "trait_type": "Year",
      "value": "2020"
    }
  ]
}

Balance events:

event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value);

_operator: the sender of the transaction _from, _to : as expected _id: token Identifier _value: as expected

event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values);

As above, but note the array fields for _ids and _values

event URI(string _value, uint256 indexed _id);

Is emitted when the URI for a token changes. TODO: Find out if the unindexed string gives the actual string or is the hash value. If it's the actual string then it can be used to re-load the metadata.

Interface spec

    /*
        bytes4(keccak256("safeTransferFrom(address,address,uint256,uint256,bytes)")) ^
        bytes4(keccak256("safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)")) ^
        bytes4(keccak256("balanceOf(address,uint256)")) ^
        bytes4(keccak256("balanceOfBatch(address[],uint256[])")) ^
        bytes4(keccak256("setApprovalForAll(address,bool)")) ^
        bytes4(keccak256("isApprovedForAll(address,address)"));
    */
    bytes4 constant private INTERFACE_SIGNATURE_ERC1155 = 0xd9b67a26;
hboon commented 3 years ago

contractURI()

function contractURI() returns(string metaData)

Ok, but note that this is at the contract level so name and image are for the contract, not for the individual token types or token instances. (ie. we can use this to show that single row in the Wallet tab only), so if decimals is included here, we can't use it since there would be more than 1 token type (with 0 or more decimals value) in this same ERC1155 token-holding contract. [1]

Sometimes the URL (maybe just from Rarible?) have an {address} variable embedded. We'll need to substitute it with the contract address:

https://api-mainnet.rarible.com/contractMetadata/{address}

uri()

function uri(1) returns(string token Description Metadata for token index 1)

While the contracts I came across use uri(), the ERC1155 standard defines tokenURI(), so we'll have to support both.

Telling the difference between fungibles and non-fungibles

Like I mentioned separately, I couldn't find a way to tell them apart, given an _id. (maybe if decimals is included in the uri()/tokenURI() data? But I haven't found any fungible to verify against yet).

Fetching balances

We'll have to use event logs to figure this out. But blockscout.com (at least for xDai) supports getting ERC1155 transfers.

Couple of unknowns:

Sample contract: https://blockscout.com/xdai/mainnet/tokens/0x93d0c9a35c43f6BC999416A06aaDF21E68B29EBA/read-contract Transfers: https://blockscout.com/xdai/mainnet/tokens/0x93d0c9a35c43f6BC999416A06aaDF21E68B29EBA/token-transfers One of them: https://blockscout.com/xdai/mainnet/tx/0xb5d96a979406faff272cc68b9a143575dcc79f462bcaa38b6edb8b2b5a1ab7a3

GET(s):

Sender:

curl 'https://blockscout.com/xdai/mainnet/api?&module=account&action=tokentx&address=0x2a58ab26538a905f08ac7838b83c1c160430e744&sort=desc' | json_pp | clip

Receipient:

curl 'https://blockscout.com/xdai/mainnet/api?&module=account&action=tokentx&address=0x61cd76d2dab94cbf81ad45a8451f1467cead23fe&sort=desc' | json_pp | clip

(they both includes transaction 0xa5c15de2c4bff26810ebc3dd7df64d2c4a7afdff9ffb255c7905f501c7e80b86)

Event logs

{
  "name" : "TransferSingle",
  "inputs" : [
    {
      "type" : "address",
      "indexed" : true,
      "name" : "_operator"
    },
    {
      "type" : "address",
      "indexed" : true,
      "name" : "_from"
    },
    {
      "type" : "address",
        "indexed" : true,
        "name" : "_to"
    },
    {
      "type" : "uint256",
      "name" : "_id",
      "indexed" : false
    },
    {
      "type" : "uint256",
      "name" : "_value",
      "indexed" : false
    }
  ],
  "type" : "event",
  "anonymous" : false
},

{
  "name" : "TransferBatch",
  "inputs" : [
    {
      "type" : "address",
      "indexed" : true,
      "name" : "_operator"
    },
    {
      "type" : "address",
      "indexed" : true,
      "name" : "_from"
    },
    {
      "type" : "address",
        "indexed" : true,
        "name" : "_to"
    },
    {
      "type" : "uint256[]",
      "name" : "_ids",
      "indexed" : false
    },
    {
      "type" : "uint256[]",
      "name" : "_values",
      "indexed" : false
    }
  ],
  "type" : "event",
  "anonymous" : false
}

JavaScript code

TransferSingle:

  contractInstance1.getPastEvents('TransferSingle', {
    filter: {
      _to: owner,
    },
    fromBlock: 0,
    toBlock: 'latest'
  }, function(error, events){ console.log(error, events); })

TransferBatch:

  contractInstance1.getPastEvents('TransferBatch', {
    filter: {
      _to: owner,
    },
    fromBlock: 0,
    toBlock: 'latest'
  }, function(error, events){ console.log(error, events); })

Will need to filter for _from for both events too.

Getting Collection/Token type name

Referring to Tomek's designs where "Mathilde Cretier" the collection name is displayed. We wouldn't get such a representative name, it'll be something like "Enjin" or "Rarible" instead, because it is at the contract level, not at the collection/token type level.

[1] So if there's a fungible representing fiat (fractional dollars) instead of gold (discrete pieces), I don't know how to get/provide decimals for the fiat type... maybe it is included uri() or tokenURI()

Variables in URLs

Effect on UI

So based on the collection, collection name and contractURI() things we talked about, and referring to Tomek's design https://projects.invisionapp.com/share/HD10WWZ5JXA5#/screens, the UI flow would be the same, but the data displayed is slightly different because we can't tell multi-collections/multi-verse from collections. We have to treat each ERC1155 as a single multi-collection like that:

  1. Wallet tab display 1 row per ERC1155 contract (if "0xfaafdc07907ff5120a76b34b731b278c38d6043c", hardcode "Enjin", otherwise get from contractURI(), otherwise try name() (ouch Enjin doesn't have that too!), otherwise display the contract name.
  2. Tap on the row in Wallet tab (eg. Mathilde Cretier Art, which will actually display as "Enjin")
  3. Under Assets, it will show all the _id/token IDs under this contract. For "Enjin", it will show the tokens from "Mathilde Cretier" as well as "The Vault" and "Lost Relics" (for Tomek's wallet). If we had tapped on "GenSys.X" in the Wallet tab (which is another ERC1155 contract), the "Assets" tab will show a list of tokens from this contract.
hboon commented 3 years ago

Added section on "Event logs" above

hboon commented 3 years ago

Some notes for prosperity:


As for the collection name and contractURI():

So for GenSys.X, contractURI() returns https://api-mainnet.rarible.com/contractMetadata/{address}, sub in the contract and we get https://api-mainnet.rarible.com/contractMetadata/0x3ec057be60db3a2d416e14a6e3ab7f37ffccd7b8 which mentions "GenSys.X". So yes, this ERC1155 contract does indeed only hold 1 collection, can verify visually here too: https://rarible.com/gensysx.

Not the case for Enjin's which holds multiple collection, so contractURI() can't return anything meaningful except "Enjin". But looks like they don't implement contractURI() at all. So we'll have to hardcode the Enjin's contract to display "Enjin" like this https://opensea.io/collection/enjin or this https://opensea.io/assets/0xfaafdc07907ff5120a76b34b731b278c38d6043c/50659039041325876706985198754860174267763181649547768479226185909848571379712

hboon commented 3 years ago

Added sections:

hboon commented 3 years ago

Summarize the way ERC1155 transfers/tokens can be fetched:

OpenSea

mainnet + Rinkeby only

Event log

All

Blockscout (can't be used)

Presumably all chains that are available on Blockscout, but verified with xDai so far and verified to exclude TransferBatch, eg. https://blockscout.com/xdai/mainnet/tx/0x29a7352220c725e94d3ef2e17b6b4ca9250779264a571e8e2741c62ac356c098/logs. So it can't be used

hboon commented 3 years ago

@JamesSmartCell

  1. Did you say that you figured out how to tell which "collection" a token (tokenId/id) in a ERC1155 is grouped in?

  2. If you use events and not OpenSea, are you able to get the tokenURI/uri() for this token in the following wallet, as well as the image?

JamesSmartCell commented 2 years ago

@hboon re-looking at this, I provided all this info in a separate issue in the iOS repo.

Good to close? Or do you need more info on the heuristic? It won't catch 100% percent of groups, but it won't put the wrong NFTs together into groups, as it's quite conservative. It does help reduce the clutter a lot. I can provide a few more test collections I tested against.

hboon commented 2 years ago

Good to close? Or do you need more info on the heuristic?

Good to close, but could you like link to that iOS issue you mentioned? We aren't implementing the heuristics for now, but might check back later.