Token Tracking Solution

One highly controversial and challenging problem in Ethereum development is the tracking of all existing tokens. It is impossible for a dApp to do this efficiently without relying on intermediary servers, forcing it to program its frontend—the most centralized and difficult to govern part—with echelons of hardcoded addresses. This puts it in the uncomfortable and untenable position of arbitrarily prioritizing some projects over others in order to improve their user experience.

To resolve this, the architecture for ITEMs (native and wrapped) is based on a general-purpose factory standard that generates all of the standardized methodology for every token.

This means that dApps only need to follow these steps:

//First of all, let's suppose you have a 'configuration.json' file in your dapp, containing all the configuration parameters you need (e.g. ABIs, custom parameters)

var configuration = require("configuration.json");

//Get the Web3 engine
var Web3 = require('web3');

//Initialize your connection with a Ethereum node
var web3 = new Web3(configuration.web3URLConnectionString);

//Retrieve the last version of the EthItem Orchestrator Smart Contract
var ethItemOrchestrator = new web3.eth.Contract(configuration.ethItemOrchestratorABI, configuration.ethItemOrchestratorAddress);

//Prepare a var to collect all the ITEMs
var allCollections = [];                    

Phase 1) Retrieve all Wrapped ERC20 Collections directly by calling the Orchestrator. Each ERC20 Wrapped Collection is a Singleton, so there will always be just one active at a time, and its address is well-known. But the Orchestrator, of course, also collects all previous inactive versions (the last one is the currently active one).

//Call the Orchestrator and then the KnowledgeBase to retrieve all the ERC20Wrappers
var knowledgeBase = new web3.eth.Contract(configuration.EthItemKnowledgeBaseABI , await ethItemOrchestrator.knowledgeBase().call());
var ethItemERC20WrapperAddresses = await knowledgeBase.methods.erc20Wrappers().call();

for(var collectionAddress of ethItemERC20WrapperAddresses) {
//Normalize the address for eventual search by address purposes
collectionAddress = web3.utils.toChecksumAddress(collectionAddress);

//Collection category to distinguish all collection types, "W20" means that this Collection is a Wrapper of ERC20 Tokens
var collectionCategory = "W20";

var collectionABI = configuration.W20ABI;

//The needed basic info to operate are of course the Collection address, category and Smart Contract. They can be of course enriched
allCollections.push({
    address : collectionAddress,
    category : collectionCategory,
    contract : new web3.eth.Contract(collectionABI, collectionAddress)
});
}

Phase 2) Retrieve all the other Collections passing by their factories. There will always be just one active EthItem Factory at a time, but the Orchestrator, of course, also collects all previous, inactive versions (the last one is the currently active one).

var factoryAddresses = await 
ethItemOrchestrator.methods.factories().call();

Factories emit a specific event for every Collection type (including ERC20). First param is the Collection model address, second param is the Collection model version, third param is the Collection address, fourth param is the original Factory caller (the Orchestrator).

//First three params are indexed to make specific researches
//A key-value approach is used to keep track of the respective Collection ABI and Category
var ethItemFactoryEventsAndABIValues = {
    "NewNativeCreated(address,uint256,address,address)" : "NativeABI",
    "NewWrappedERC1155Created(address,uint256,address,address)" : "W1155ABI",
    "NewWrappedERC721Created(address,uint256,address,address)" : "W721ABI"/*,
    "NewWrappedERC20Created(address,uint256,address,address)" : "W20ABI"*/
};    

The last event is of course commented because we already retrieved all the ERC20 Wrappers in Phase 1. You can choose of course to receive the Collections of all Categories or just the ones you're interested in just by removing the irrelevant events in the ethItemFactoryEventsAndABIValues object above.

//Let's convert plain events in keccak256 topics so they can be used for the web3.eth.getPastLogs call
var ethItemFactoryTopicsAndCollectionABIValues = {};
Object.entries(ethItemFactoryEventsAndABIValues).forEach(it => ethItemFactoryTopicsAndCollectionABIValues[web3.utils.sha3(it[0])] = it[1]);

//First argument of the topics array is an array of three hash elements. This means that first log topic can be one of the passed arguments (like the "OR" operator in Web2 DB queries)
var topics = [Object.keys(ethItemFactoryTopicsAndCollectionABIValues)];

//Call the blockchain.
//Of course this is a generic-purpose code, it can be more efficient by using just the topics you need (e.g. give me only the Wrapped ERC721 Collections) or use from/to block tranches.
var logs = await web3.eth.getPastLogs({
    address : factoryAddresses,
    topics
});

//Navigate the logs to obtain info about collections
for (var log of logs) {
    //Retrieve the correct ABI using the first log topic through the key/value map previously made
    var collectionABIValue = ethItemFactoryTopicsAndCollectionABIValues[log.topics[0]];

    //As already said, last topic param is che Collection address
    var collectionAddress = web3.eth.abi.decodeParameter("address", log.topics[log.topics.length - 1]);

    //Make a checksum address for eventual off-chain research purposes
    collectionAddress = web3.utils.toChecksumAddress(collectionAddress);

    //Grab the correct Collection ABI from the configuration file
    var collectionABI = configuration[collectionABIValue];

    //Extract Collection Category label from the ABI value. Just remove the latest 3 "ABI" characters from the string
    var collectionCategory = collectionABIValue.split("ABI")[0];

    //Put everything in the allCollections array
    allCollections.push({
        address : collectionAddress,
        category : collectionCategory,
        contract : new web3.eth.Contract(collectionABI, collectionAddress)
    });
}

//Use all the collections as you wish
console.table(allCollections.map(it => {return {address : it.address, category : it.category}}));                 

This allows you to receive all of the tokens that exist in the Ethereum Ecosystem (wrapped into ITEMs), instead of relying on hardcoded lists of well-known tokens or white-listed decisions by dApp frontend owners.

You can also request all of the existing ITEMs from specific Collections using this call:

Request Every ITEM for a single collection:

//Grab the desired Collection addresses. You can choose any single Collection or group of Collections you want. In this case we grab all
var collectionAddresses = allCollections.map(it => it.address);

//The EthItem Token Standard implements the event NewItem(uint256 indexed objectId, address indexed interoperableInterfaceAddress) raised every time a new Item is created/wrapped for the first time
var topics = [web3.utils.sha3("NewItem(uint256,address)")];

var logs = await web3.eth.getPastLogs({
    address : collectionAddresses,
    topics
});

//Navigate logs
for(var log of logs) {

    //Get the Collection that created this item (the original event emitter)
    var collectionAddress = web3.utils.toChecksumAddress(log.address);
    var collection = allCollections.filter(it => it.address === collectionAddress)[0];

    //If not already done, initialize the items array in the Collection
    collection.items = collection.items || [];

    //Object Id is the first argument param of the Event
    var collectionItemObjectId = web3.eth.abi.decodeParameter("uint256", log.topics[1]);

    //Object ERC20 Wrapper is the second param of the Event
    var interoperableInterfaceAddress = web3.eth.abi.decodeParameter("uint256", log.topics[2]);

    //Create the contract
    var collectionItemInteroperableInterfaceContract = new web3.eth.Contract(configuration.IEthItemInteroperableInterfaceABI, interoperableInterfaceAddress);

    //Assemble the Collection Item, you can add all the additional info you want (e.g. cross-referencing the Collection this Item belongs to)
    var collectionItem = {
        objectId : collectionItemObjectId,
        address : collectionItemInteroperableAddress,
        contract : collectionItemInteroperableInterfaceContract
    };

    //Add every single Collection Item to the corresponding Collection's array
    collection.items.push(collectionItem);
}

Last updated