ADR-42: Third Party Integration

More details about this document
Latest published version:
https://adr.decentraland.org/adr/ADR-42
Authors:
menduz
pentreathm
agusaldasoro
guidota
jmoguilevsky
LautaroPetaccio
nicosantangelo
fzavalia
cazala
nachomazzara
Feedback:
GitHub decentraland/adr (pull requests, new issue, open issues)
Edit this documentation:
GitHub View commits View commits on githistory.xyz

Statement of the problem

Multiple third parties with their own NFT contracts (ERC721, ERC1155, etc) want to be part of Decentraland, but the current Decentraland collections implementations are not always a good fit for their use cases. Therefore, a smart contract is going to be created to have a decentralized way where they can map 3d assets to their already created NFTs.

Third Party item URN

urn:decentraland:{protocol}:collections-thirdparty:{third-party-name}:{collection-id}:{item-id}

Examples

Considerations

Third Party resolver

Each Third Party will require to create and maintain an API with these endpoints:

It is recommended to accept any format for the :address parameter: checksummed, lowercased, uppercased, mixed, etc. You can always checksum and validate if it is a valid Ethereum address later.

@GET /registry/:registry-id/owners-bloom-filter

Request

GET /registry/:registry-id/owners-bloom-filter
Example: https://api.cryptohats.io/registry/cryptohats/owners-bloom-filter

Response

{
  "data": "00100000000000000000000000000000080010000800000000000000000000000080000000000000000000000000000000000000000000000000000000000002000000000004000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000800000000040000000000000000000000000000020000000000000280000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040040000000000000000000000000000000010000000000000000000000000000"
}

If the registry is invalid or non-existent, the data property should return an empty string.

{
  "data": ""
}

@GET /registry/:registry-id/address/:address/assets

Request

GET /registry/:registry-id/address/:address/assets
Example: https://api.cryptohats.io/registry/cryptohats/address/0x0f5d2fb29fb7d3cfee444a200298f468908cc942/assets

Response

{
  "address": "0x0f5d2fb29fb7d3cfee444a200298f468908cc942",
  "assets": [
    {
      "id": "0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd:0",
      "amount": 1,
      "urn": {
        "decentraland": "urn:decentraland:matic:collections-thirdparty:cryptohats:0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd:0"
      }
    },
    {
      "id": "0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd:1",
      "amount": 1,
      "urn": {
        "decentraland": "urn:decentraland:matic:collections-thirdparty:cryptohats:0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd:1"
      }
    }
  ],
  "total": 100,
  "page": 1,
  "next": "https://....&startAt=1234"
}

If the registry is invalid or the address does not own assets the assets prop should be an empty array. The next property should be a falsy value, preferebly an empty string for this scenario and when the last page is reached too.

{
  "address": "0x0f5d2fb29fb7d3cfee444a200298f468908cc942",
  "assets": [],
  "total": 0,
  "page": 1,
  "next": ""
}

GET /registry/:registry-id/address/:address/assets/:id

Request

GET /registry/:registry-id/address/:address/assets/:id
Example: https://api.cryptohats.io/registry/cryptohats/address/0x0f5d2fb29fb7d3cfee444a200298f468908cc942
/assets/0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd:1

Response

{
  "id": "0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd:1",
  "amount": 1,
  "urn": {
    "decentraland": "urn:decentraland:matic:collections-thirdparty:cryptohats:0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd:1"
  }
}

If the registry is invalid, the address does not own the asset, or the id non-existent the urn prop should set decentraland as an empty string.

{
  "id": "0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd:1",
  "amount": 0,
  "urn": {
    "decentraland": ""
  }
}

POST /registry/:registry-id/ownership

When provided with a list of owners and their respective items, the expected functionality is to determine whether each owner owns the corresponding item or not.

Request

POST /registry/:registry-id/ownership
Example: https://api.cryptohats.io/registry/cryptohats/ownership

body:

[
 { 
   "urn_decentraland": "urn:decentraland:matic:collections-thirdparty:cryptohats:0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd:1", 
   "address": "0x0f5d2fb29fb7d3cfee444a200298f468908cc942" 
 },
 {
   "urn_decentraland": "...", 
   "address": "..."
 },
 ...
]

Response

[
 { 
   "urn_decentraland": "urn:decentraland:matic:collections-thirdparty:cryptohats:0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd:1", 
   "address": "0x0f5d2fb29fb7d3cfee444a200298f468908cc942", 
   "owned": true
 },
 {
   "urn_decentraland": "...",
   "address": "...",
   "owned": [true|false]
 }
  ...
]

Third Party Collection Smart Contract Registry

Contract Implementation

The TPR smart contract is going to be deployed on Polygon and support meta-transactions with the EIP-712 to reduce operational costs. The contract can be easily migrated because it doesn't store or mint any tokens. The main purposes of this registry is to have an on-chain way to check whether an item has been approved by a committee member and therefore can be submitted to the Decentraland catalysts. And, to check whether a third party or item has been approved or rejected.

The contract is not a storage-gas-consumption top efficient because it prioritizes looping through third parties and items without the need of indexing historical data.

Roles

The TPR smart contract supports different roles:

Third Party Records

The third party record is going to be identified by a unique id. For simplicity and in order to support different uses cases, this id is going to be a string. By using a string, we can support ids as URNs, UUIDs, auto incremental values, etc. The current identifier used in Decentraland is the URN, therefore, an id urn like urn:decentraland:matic:collections-thirdparty1 is what we expect to be using.

Each third party record can only be added by a committee member and it has the following properties:

struct ThirdParty {
    string metadata;
    string resolver;
    uint256 maxItems;
    bool isApproved;
    mapping(address => bool) managers;
    mapping(string => Item) items;
    string[] itemIds;
    uint256 registered;
}

A third party record can't be removed but approved/rejected by a committee member.

As we mentioned with the items, the third parties can be looped off-chain without the need of indexing historic events.

Items

Items are going to be identifying with an id like the third party records. In order to support the concept of erc721 contracts or collections, the item id will looks like: collection:item. i.e: 0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd:1 (NFT smart contract: 0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd, NFT tokenId: 1), great_collection:type1, etc.

As you may notice, the concept of a collection is merely "virtual" and will be part of the item id. If there is a use case where a third party has multiple collections, they can be easily filtered by comparing strings off-chain.

Items can only be added to a third party if there are item slots available.

struct Item {
    string metadata;
    string contentHash;
    bool isApproved;
    uint256 registered;
}

Similar to third parties, items can't be removed but approved/rejected by committee members.

Catalyst acceptance criteria

Each deployment must check if the URN has collections-thirdparty in order to know that the tpr-graph should be used. The query to that subgraph must check:

  1. If there is a record with the urn:

urn:decentraland:matic:collections-thirdparty:cryptohats:0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd:0.

  1. If the content hash of the item with id 0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd:0 is the same as the content hash of the item that is being uploaded

License

Copyright and related rights waived via CC0-1.0. DRAFT Living