Skip to content

Ink Explorer is an application that provides Ink contracts related information on Substrate based blockchains.

License

Notifications You must be signed in to change notification settings

blockcoders/ink-substrate-explorer-frontend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ink! Explorer Frontend

App Build Coverage Status Codacy Badge Docker Image Version Docker Image Size License

Demo

ink-explorer.blockcoders.io

About the explorer

Ink Explorer is an application that provides Ink contracts related information on Substrate based blockchains. It subscribes to blockchain and Ink modules events and store the information on its own PostgreSQL database. The backend exposes an API that can interact with the DB and run fast queries to get specific information in a short time.

The idea of this project is to provide a tool that allows developers of Ink! explore and analyze the contracts found on the blockchain. This tool can be used to analyze the contracts found on Substrate based blockchains that are using Ink! modules. It can also be used to analyze contracts that are on a local blockchain.

This project serves useful information that is not available anywhere else. Since the back end is in charge of obtaining information related to the balances, transactions and more, of the contracts that use Ink modules. Ink Explorer uses polkadot.js to communicate with the Substrate / Polkadot networks. It is safe to say that this project is a must.

Get Started

Running the service locally

Environment setup

  • Install Node.js
    • Recommended method is by using NVM
    • Recommendeded Node.js version is v16.13
  • Install Docker

Install all the dependencies

pnpm i --frozen-lockfile

Configure the environment variables

Note: The .env file has the configuration for GraphQL, the PostgreSQL database, Node and the RPC url of the Substrate Blockchain.

cp .env.sample .env

Service configurations

NODE_ENV=development
PORT=8080
LOG_NAME=ink-substrate-explorer-frontend
LOG_LEVEL=debug
SCHEMA_URL=https://ink-explorer-api.blockcoders.io/graphql

GraphQL configurations

GRAPHQL_DEBUG=true
GRAPHQL_PLAYGROUND=true
GRAPHQL_SORT_SCHEMA=true
GRAPHQL_INTROSPECTION=true

Database configurations

DATABASE_HOST=postgres
DATABASE_NAME=ink
DATABASE_USERNAME=root
DATABASE_PASSWORD=password
DATABASE_RETRY_ATTEMPTS=5
DATABASE_RETRY_DELAY=3000

Blockchain and Sync configurations

WS_PROVIDER=wss://rococo-contracts-rpc.polkadot.io
# Set to true to process every block from FIRST_BLOCK_TO_LOAD to the current block. Set to false to only start processing blocks from the last existing block in the database.
LOAD_ALL_BLOCKS=false
# Block number from which the service will start to process blocks. (Can be genesis or some other block. For example, the first block supporting contracts)
FIRST_BLOCK_TO_LOAD=0
# Number of blocks to process concurrently. This can speed up or down the syncing process.
BLOCK_CONCURRENCY=1000

Starting the project (DEV)

To run in dev mode the backend is still needed. For that, the docker-compose.yaml file already has all the required services.

Running this command will also start a container for the frontend (wo don't want to do this in DEV mode). To avoid this, comment the 'frontend' service. Then run the command:

docker-compose up -d

Once the service is running, pgAdmin can be accessed following the link that is shown in the terminal (In this case localhost:80).

pgAdmin

The credentials to access pgAdmin are (set in the docker-compose file):

  • PGADMIN_DEFAULT_EMAIL: "admin@admin.com"
  • PGADMIN_DEFAULT_PASSWORD: "admin"

Register a new server in pgAdmin and set the credentials for the PostgreSQL DB:

Right click on 'Servers' and select "Register" -> "Server"

pgAdmin

Set a name for the server (In this example "Docker")

pgAdmin

Set the credentials for the PostgreSQL DB (this can be found in the docker-compose file):

pgAdmin

Start a local Substrate Node (optional)

To run a local substrate node add to the docker-compose.yaml file the following service:

  substrate:
    image: blockcoders/substrate-contracts-node:latest
    restart: on-failure
    ports:
      - 9944:9944
    command: '--dev --ws-external'
    networks:
      ink-explorer-network:
        aliases:
          - "substrate"

This will start a new container with a local substrate node.

Another way to run a local node is with this paritytech guide.

Note: Change the WS_PROVIDER var in the .env file to be ws://127.0.0.1:9944

Start the service

To run the frontend service in DEV mode run:

  • pnpm start:dev

The service will reload if you make edits.

Note: A postgresDB up and running and a valid connection to a substrate node are required.

Starting the project (PROD)

To start both the Back-end and the Front-end services run:

  • docker-compose up -d

Running the Front-end service Docker image

Download the image from DockerHub

docker pull blockcoders/ink-substrate-explorer-frontend:latest

Run

# Create a docker network
docker network create ink-explorer-network

# Run the service
docker run -it -p 3000:3000 --network ink-explorer-network blockcoders/ink-substrate-explorer-frontend:latest

Verify the image started running

docker ps

The result should look like this:

CONTAINER ID   IMAGE                                    COMMAND                  CREATED          STATUS          PORTS                                       NAMES
f31a7d0fd6c8   blockcoders/ink-substrate-explorer-frontend   "docker-entrypoint.s…"   15 seconds ago   Up 14 seconds   0.0.0.0:3000->3000/tcp, :::3000->3000/tcp   funny_lumiere

Testing

Running the unit tests.

pnpm test

Running the test coverage.

pnpm test:cov

Testing the GraphQL queries.

{"level":30,"time":1664298430389,"pid":1388770,"hostname":"username","name":"ink-substrate-explorer-api","msg":"App listening on http://0.0.0.0:8080"}

Once the back-end service is running, the GraphQL Playground can be accessed at http://localhost:8080/graphql

backend

API Definition

With the service up and running an API is provided by using GraphQL queries.

Queries

Status: Retrieves the status of the application

query {
  status
}

Response:

{
  "data": {
    "status": "running"
  }
}

Version: Retrieves the version of the application

query {
  status
}

Response:

{
  "data": {
    "version": "v1.0.6"
  }
}

getBlock: Retrieves the block by hash

query {
  getBlock(hash: "0x0f615cf7edf8a1e8591893a594fe0ef67d5d56c4d9b1a89d8d120c5f821127fe") {
    hash
    number
    parentHash
    timestamp
    encodedLength
    transactions {
      hash
    }
  }
}

Response:

{
  "data": {
    "getBlock": {
      "hash": "0x0f615cf7edf8a1e8591893a594fe0ef67d5d56c4d9b1a89d8d120c5f821127fe",
      "number": 7,
      "parentHash": "0xd8ecc752f280a3786c5cdd4d441d71488414fd6132ace481dd6ddb23fd8000b0",
      "timestamp": 1666888006111,
      "encodedLength": 312,
      "transactions": [
        {
          "hash": "0xdb561ee4432e07a959292acf9895ce379e2474a52160b93fd62496806fdf26cd"
        },
        {
          "hash": "0x33831da6b804e82cd7613e0d780823c7455c773546ea5e76c945ed10a6f6554b"
        }
      ]
    }
  }
}

getBlocks: Retrieves blocks. Use 'skip' and 'take' to paginate. Use 'orderByNumber: false' to order by timestamp instead and 'orderAsc: true' to see older blocks first.

query {
  getBlocks(skip: 0, take: 1, orderByNumber: false, orderAsc: false) {
    hash
    number
    parentHash
    timestamp
    encodedLength
    transactions {
      hash
    }
  }
}

Response:

{
  "data": {
    "getBlocks": [
      {
        "hash": "0x0f615cf7edf8a1e8591893a594fe0ef67d5d56c4d9b1a89d8d120c5f821127fe",
        "number": 7,
        "parentHash": "0xd8ecc752f280a3786c5cdd4d441d71488414fd6132ace481dd6ddb23fd8000b0",
        "timestamp": 1666888006111,
        "encodedLength": 312,
        "transactions": [
          {
            "hash": "0xdb561ee4432e07a959292acf9895ce379e2474a52160b93fd62496806fdf26cd"
          },
          {
            "hash": "0x33831da6b804e82cd7613e0d780823c7455c773546ea5e76c945ed10a6f6554b"
          }
        ]
      }
    ]
  }
}

getTransaction: Retrieves a single transaction by hash

query {
  getTransaction(hash: "0x33831da6b804e82cd7613e0d780823c7455c773546ea5e76c945ed10a6f6554b") {
    args
    blockHash
    callIndex
    decimals
    encodedLength
    era
    events {
      method
    }
    hash
    method
    nonce
    section
    signature
    signer
    ss58
    timestamp
    tip
    tokens
    type
    version
  }
}

Response:

{
  "data": {
    "getTransaction": {
      "args": "{\"dest\":{\"id\":\"5DfG5TyaffuJ78rHP71cvkYEtktRkpeMiJNJyxd8Q5924GR8\"},\"value\":0,\"gas_limit\":75000000000,\"storage_deposit_limit\":null,\"data\":\"0x84a15da18eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48005039278c0400000000000000000000\"}",
      "blockHash": "0x0f615cf7edf8a1e8591893a594fe0ef67d5d56c4d9b1a89d8d120c5f821127fe",
      "callIndex": "7,0",
      "decimals": "12",
      "encodedLength": 201,
      "era": "{\"mortalEra\":\"0x0b00\"}",
      "events": [
        {
          "method": "ContractEmitted"
        }
      ],
      "hash": "0x33831da6b804e82cd7613e0d780823c7455c773546ea5e76c945ed10a6f6554b",
      "method": "call",
      "nonce": 3,
      "section": "contracts",
      "signature": "0x78582786706e947a6d77ac5b49ba140b4c88ebc644421136bbfa8b66577e1e3efdbc1d981948546fff41600be9a716e4c38a8531a867853f26ba11ee21128f82",
      "signer": "5GVmSPghWsjACADGYi78dmhuZEgfgDwfixR7BM3aMEoNuTBc",
      "ss58": "42",
      "timestamp": 1666888006111,
      "tip": 0,
      "tokens": "Unit",
      "type": 4,
      "version": 132
    }
  }
}

getTransactionsByContract: Retrieves a list of transactions of a contract.

query {
  getTransactionsByContract(
    address: "5DfG5TyaffuJ78rHP71cvkYEtktRkpeMiJNJyxd8Q5924GR8"
    skip: 0
    take: 1
    orderAsc: false
  ) {
    args
    blockHash
    callIndex
    decimals
    encodedLength
    era
    events {
      method
    }
    hash
    method
    nonce
    section
    signature
    signer
    ss58
    timestamp
    tip
    tokens
    type
    version
  }
}

Response:

{
  "data": {
    "getTransactionsByContract": [
      {
        "args": "{\"dest\":{\"id\":\"5DfG5TyaffuJ78rHP71cvkYEtktRkpeMiJNJyxd8Q5924GR8\"},\"value\":0,\"gas_limit\":75000000000,\"storage_deposit_limit\":null,\"data\":\"0x84a15da18eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48005039278c0400000000000000000000\"}",
        "blockHash": "0x0f615cf7edf8a1e8591893a594fe0ef67d5d56c4d9b1a89d8d120c5f821127fe",
        "callIndex": "7,0",
        "decimals": "12",
        "encodedLength": 201,
        "era": "{\"mortalEra\":\"0x0b00\"}",
        "events": [
          {
            "method": "ContractEmitted"
          }
        ],
        "hash": "0x33831da6b804e82cd7613e0d780823c7455c773546ea5e76c945ed10a6f6554b",
        "method": "call",
        "nonce": 3,
        "section": "contracts",
        "signature": "0x78582786706e947a6d77ac5b49ba140b4c88ebc644421136bbfa8b66577e1e3efdbc1d981948546fff41600be9a716e4c38a8531a867853f26ba11ee21128f82",
        "signer": "5GVmSPghWsjACADGYi78dmhuZEgfgDwfixR7BM3aMEoNuTBc",
        "ss58": "42",
        "timestamp": 1666888006111,
        "tip": 0,
        "tokens": "Unit",
        "type": 4,
        "version": 132
      }
    ]
  }
}

getTransactions: Retrieves transactions by block hash (use 'skip' and 'take' to paginate. use 'orderAsc' to see older or newer first)

query {
  getTransactions(skip: 0, take: 1, orderAsc: false) {
    args
    blockHash
    callIndex
    decimals
    encodedLength
    era
    events {
      method
    }
    hash
    method
    nonce
    section
    signature
    signer
    ss58
    timestamp
    tip
    tokens
    type
    version
  }
}

Response:

{
  "data": {
    "getTransactions": [
      {
        "args": "{\"now\":1666888006111}",
        "blockHash": "0x0f615cf7edf8a1e8591893a594fe0ef67d5d56c4d9b1a89d8d120c5f821127fe",
        "callIndex": "2,0",
        "decimals": "12",
        "encodedLength": 11,
        "era": "{\"immortalEra\":\"0x00\"}",
        "events": [],
        "hash": "0xdb561ee4432e07a959292acf9895ce379e2474a52160b93fd62496806fdf26cd",
        "method": "set",
        "nonce": 0,
        "section": "timestamp",
        "signature": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
        "signer": "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM",
        "ss58": "42",
        "timestamp": 1666888006111,
        "tip": 0,
        "tokens": "Unit",
        "type": 4,
        "version": 4
      }
    ]
  }
}

getEvent: Retrieves an event by id

query {
  getEvent(id: "81735cc9-76d3-5984-83af-5872bc9eaeb7") {
    id
    index
    method
    section
    timestamp
    topics
    transactionHash
    data
    decodedData
    formattedData
  }
}

Response:

{
  "data": {
    "getEvent": {
      "id": "81735cc9-76d3-5984-83af-5872bc9eaeb7",
      "index": "0x0703",
      "method": "ContractEmitted",
      "section": "contracts",
      "timestamp": 1666888006111,
      "topics": "[0x0045726332303a3a5472616e7366657200000000000000000000000000000000, 0x08be862c40d599dc6f4f28076712bb324c0cd2197c30f07459887b41fadff2c8, 0x2b00c7d40fe6d84d660f3e6bed90f218e022a0909f7e1a7ea35ada8b6e003564]",
      "transactionHash": "0x33831da6b804e82cd7613e0d780823c7455c773546ea5e76c945ed10a6f6554b",
      "data": "[\"5DfG5TyaffuJ78rHP71cvkYEtktRkpeMiJNJyxd8Q5924GR8\",\"0x0001c40e2006bbebf9022c317f9337ad376e56d392917e5ac1397fe09b07c765c050018eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48005039278c0400000000000000000000\"]",
      "decodedData": "{\"args\":[\"5GVmSPghWsjACADGYi78dmhuZEgfgDwfixR7BM3aMEoNuTBc\",\"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty\",\"0x00000000000000000000048c27395000\"],\"event\":{\"args\":[{\"name\":\"from\",\"type\":{\"sub\":{\"docs\":[],\"info\":10,\"type\":\"AccountId\",\"namespace\":\"ink_env::types::AccountId\",\"lookupIndex\":2,\"lookupNameRoot\":\"InkEnvAccountId\"},\"docs\":[],\"info\":9,\"type\":\"Option<AccountId>\",\"namespace\":\"Option\",\"lookupIndex\":11}},{\"name\":\"to\",\"type\":{\"sub\":{\"docs\":[],\"info\":10,\"type\":\"AccountId\",\"namespace\":\"ink_env::types::AccountId\",\"lookupIndex\":2,\"lookupNameRoot\":\"InkEnvAccountId\"},\"docs\":[],\"info\":9,\"type\":\"Option<AccountId>\",\"namespace\":\"Option\",\"lookupIndex\":11}},{\"name\":\"value\",\"type\":{\"info\":10,\"type\":\"Balance\"}}],\"docs\":[\" Event emitted when a token transfer occurs.\"],\"index\":0,\"identifier\":\"Transfer\"}}",
      "formattedData": "{\"from\":\"5GVmSPghWsjACADGYi78dmhuZEgfgDwfixR7BM3aMEoNuTBc\",\"to\":\"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty\",\"value\":5}"
    }
  }
}

getEvents: Retrieves events by contract address or transaction hash (use 'skip' and 'take' to paginate, 'orderAsc' to see older or newer first)

query {
  getEvents(contract: "5DfG5TyaffuJ78rHP71cvkYEtktRkpeMiJNJyxd8Q5924GR8", skip: 0, take: 1, orderAsc: false) {
    id
    index
    method
    section
    timestamp
    topics
    transactionHash
    data
    decodedData
    formattedData
  }
}

Response:

{
  "data": {
    "getEvents": [
      {
        "id": "81735cc9-76d3-5984-83af-5872bc9eaeb7",
        "index": "0x0703",
        "method": "ContractEmitted",
        "section": "contracts",
        "timestamp": 1666888006111,
        "topics": "[0x0045726332303a3a5472616e7366657200000000000000000000000000000000, 0x08be862c40d599dc6f4f28076712bb324c0cd2197c30f07459887b41fadff2c8, 0x2b00c7d40fe6d84d660f3e6bed90f218e022a0909f7e1a7ea35ada8b6e003564]",
        "transactionHash": "0x33831da6b804e82cd7613e0d780823c7455c773546ea5e76c945ed10a6f6554b",
        "data": "[\"5DfG5TyaffuJ78rHP71cvkYEtktRkpeMiJNJyxd8Q5924GR8\",\"0x0001c40e2006bbebf9022c317f9337ad376e56d392917e5ac1397fe09b07c765c050018eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48005039278c0400000000000000000000\"]",
        "decodedData": "{\"args\":[\"5GVmSPghWsjACADGYi78dmhuZEgfgDwfixR7BM3aMEoNuTBc\",\"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty\",\"0x00000000000000000000048c27395000\"],\"event\":{\"args\":[{\"name\":\"from\",\"type\":{\"sub\":{\"docs\":[],\"info\":10,\"type\":\"AccountId\",\"namespace\":\"ink_env::types::AccountId\",\"lookupIndex\":2,\"lookupNameRoot\":\"InkEnvAccountId\"},\"docs\":[],\"info\":9,\"type\":\"Option<AccountId>\",\"namespace\":\"Option\",\"lookupIndex\":11}},{\"name\":\"to\",\"type\":{\"sub\":{\"docs\":[],\"info\":10,\"type\":\"AccountId\",\"namespace\":\"ink_env::types::AccountId\",\"lookupIndex\":2,\"lookupNameRoot\":\"InkEnvAccountId\"},\"docs\":[],\"info\":9,\"type\":\"Option<AccountId>\",\"namespace\":\"Option\",\"lookupIndex\":11}},{\"name\":\"value\",\"type\":{\"info\":10,\"type\":\"Balance\"}}],\"docs\":[\" Event emitted when a token transfer occurs.\"],\"index\":0,\"identifier\":\"Transfer\"}}",
        "formattedData": "{\"from\":\"5GVmSPghWsjACADGYi78dmhuZEgfgDwfixR7BM3aMEoNuTBc\",\"to\":\"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty\",\"value\":5}"
      }
    ]
  }
}

getContract: Retrieves a contract by address

query {
  getContract(address: "5G24svh2w4QXNhsHU5XBxf8N3Sw2MPu7sAemofv1bCuyxAzc") {
    address
    metadata
    hasMetadata
  }
}

Response:

{
  "data": {
    "getContract": {
      "address": "5G24svh2w4QXNhsHU5XBxf8N3Sw2MPu7sAemofv1bCuyxAzc",
      "metadata": "{\n  \"source\": {\n    \"hash\": ...   }\n}\n",
      "hasMetadata": true
    }
  }
}

getContracts: Retrieves a list of contracts

query {
  getContracts(skip: 0, take: 10) {
    address
    metadata
    hasMetadata
    events {
      method
    }
  }
}

Response:

{
  "data": {
    "getContracts": [
      {
        "address": "5DfG5TyaffuJ78rHP71cvkYEtktRkpeMiJNJyxd8Q5924GR8",
        "hasMetadata": true,
        "metadata": "{ ... }",
        "events": [
          {
            "method": "ContractEmitted"
          },
        ]
      }
    ]
  }
}

getContractQueries: Retrieves a contract. If this contract has uploaded metadata it will also retrieve the queries and transaction methods that can be executed.

query {
  getContractQueries(address: "5DfG5TyaffuJ78rHP71cvkYEtktRkpeMiJNJyxd8Q5924GR8") {
    address
    hasMetadata
    queries {
      args
      docs
      method
      name
    }
  }
}

Response:

{
  "data": {
    "getContractQueries": {
      "address": "5DfG5TyaffuJ78rHP71cvkYEtktRkpeMiJNJyxd8Q5924GR8",
      "hasMetadata": true,
      "queries": [
        {
          "args": [],
          "docs": [
            " Returns the total token supply."
          ],
          "method": "totalSupply",
          "name": "Total supply"
        },
        {
          "args": [
            "{\"name\":\"to\",\"type\":{\"info\":10,\"type\":\"AccountId\"}}",
            "{\"name\":\"value\",\"type\":{\"info\":10,\"type\":\"Balance\"}}"
          ],
          "docs": [
            " Transfers `value` amount of tokens from the caller's account to account `to`.",
            "",
            " On success a `Transfer` event is emitted.",
            "",
            " # Errors",
            "",
            " Returns `InsufficientBalance` error if there are not enough tokens on",
            " the caller's account balance."
          ],
          "method": "transfer",
          "name": "Transfer"
        }
      ]
    }
  }
}

Mutations

decodeEvent: Decodes the event data for a specific event. Requires that the contract's metadata was already uploaded using the mutation uploadMetadata

mutation {
  decodeEvent(
    contractAddress: "5ELpkDtq7werhT5ybZZMbVBcQTPNomvJP7j5kJQifv7GzVik"
    id: "972e782c-2517-5648-9bf1-4c693d2fed90"
  )
}

Response:

{
  "data": {
    "decodeEvent": "{\"identifier\":\"Transfer\",\"decodedData\":{\"args\":[\"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty\",\"5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y\",\"0x000000000000000000002d79883d2000\"],\"event\":{\"args\":[{\"name\":\"from\",\"type\":{\"info\":9,\"lookupIndex\":11,\"type\":\"Option<AccountId>\",\"docs\":[],\"namespace\":\"Option\",\"sub\":{\"info\":10,\"lookupIndex\":2,\"type\":\"AccountId\",\"docs\":[],\"namespace\":\"ink_env::types::AccountId\",\"lookupNameRoot\":\"InkEnvAccountId\"}}},{\"name\":\"to\",\"type\":{\"info\":9,\"lookupIndex\":11,\"type\":\"Option<AccountId>\",\"docs\":[],\"namespace\":\"Option\",\"sub\":{\"info\":10,\"lookupIndex\":2,\"type\":\"AccountId\",\"docs\":[],\"namespace\":\"ink_env::types::AccountId\",\"lookupNameRoot\":\"InkEnvAccountId\"}}},{\"name\":\"value\",\"type\":{\"info\":10,\"type\":\"Balance\"}}],\"docs\":[\" Event emitted when a token transfer occurs.\"],\"identifier\":\"Transfer\",\"index\":0}},\"formattedData\":{\"from\":\"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty\",\"to\":\"5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y\",\"value\":50}}"
  }
}

decodeEvents: Decodes the events data for a specific contract (use 'skip' and 'take' to select the events and 'orderAsc' to order by timestamp). Requires that the contract's metadata was already uploaded using the mutation uploadMetadata

mutation {
  decodeEvents(contract: "5DfG5TyaffuJ78rHP71cvkYEtktRkpeMiJNJyxd8Q5924GR8", skip: 0, take: 1, orderAsc: false)
}

Response:

{
  "data": {
    "decodeEvents": "[{\"identifier\":\"Transfer\",\"decodedData\":{\"args\":[\"5GVmSPghWsjACADGYi78dmhuZEgfgDwfixR7BM3aMEoNuTBc\",\"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty\",\"0x00000000000000000000048c27395000\"],\"event\":{\"args\":[{\"name\":\"from\",\"type\":{\"info\":9,\"lookupIndex\":11,\"type\":\"Option<AccountId>\",\"docs\":[],\"namespace\":\"Option\",\"sub\":{\"info\":10,\"lookupIndex\":2,\"type\":\"AccountId\",\"docs\":[],\"namespace\":\"ink_env::types::AccountId\",\"lookupNameRoot\":\"InkEnvAccountId\"}}},{\"name\":\"to\",\"type\":{\"info\":9,\"lookupIndex\":11,\"type\":\"Option<AccountId>\",\"docs\":[],\"namespace\":\"Option\",\"sub\":{\"info\":10,\"lookupIndex\":2,\"type\":\"AccountId\",\"docs\":[],\"namespace\":\"ink_env::types::AccountId\",\"lookupNameRoot\":\"InkEnvAccountId\"}}},{\"name\":\"value\",\"type\":{\"info\":10,\"type\":\"Balance\"}}],\"docs\":[\" Event emitted when a token transfer occurs.\"],\"identifier\":\"Transfer\",\"index\":0}},\"formattedData\":{\"from\":\"5GVmSPghWsjACADGYi78dmhuZEgfgDwfixR7BM3aMEoNuTBc\",\"to\":\"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty\",\"value\":5}}]"
  }
}

uploadMetadata: To decode events it is necessary to upload the contract's ABI. Passing a base64 string ABI to this mutation will save that to DB. After that run a decodeEvents query to see the decoded data on the events.

mutation Upload {
  uploadMetadata(
    contractAddress: "5G24svh2w4QXNhsHU5XBxf8N3Sw2MPu7sAemofv1bCuyxAzc"
    metadata: "ewogICJzb3VyY2UiOiB7CiAgICAiaGFzaCI6I...(base64)"
  )
}

Response:

{
  "data": {
    "uploadMetadata": true
  }
}

About decoding events

To see the decoded data of the events there is one requirement, the contract metadata needs to uploaded at least once.

Example of an ERC20 contract metadata:

{
  "source": {
    "hash": "0x3aa1c8ba5f59034a42a93c00ee039a9464d6fa63d70b6889a2596f4528b28a19",
    "language": "ink! 3.3.0",
    "compiler": "rustc 1.64.0-nightly"
  },
  "contract": {
    "name": "erc20",
    "version": "0.1.0",
    "authors": [
      "[your_name] <[your_email]>"
    ]
  },
  "V3": {
    "spec": {
      "constructors": [
        {
          "args": [
            {
              "label": "initial_supply",
              "type": {
                "displayName": [
                  "Balance"
                ],
                "type": 0
              }
            }
          ],
          "docs": [
            "Creates a new ERC-20 contract with the specified initial supply."
          ],
          "label": "new",
          "payable": false,
          "selector": "0x9bae9d5e"
        }
      ],
      "docs": [],
      "events": [
        {
          "args": [
            {
              "docs": [],
              "indexed": true,
              "label": "from",
              "type": {
                "displayName": [
                  "Option"
                ],
                "type": 11
              }
            },
            {
              "docs": [],
              "indexed": true,
              "label": "to",
              "type": {
                "displayName": [
                  "Option"
                ],
                "type": 11
              }
            },
            {
              "docs": [],
              "indexed": false,
              "label": "value",
              "type": {
                "displayName": [
                  "Balance"
                ],
                "type": 0
              }
            }
          ],
          "docs": [
            " Event emitted when a token transfer occurs."
          ],
          "label": "Transfer"
        },
        {
          "args": [
            {
              "docs": [],
              "indexed": true,
              "label": "owner",
              "type": {
                "displayName": [
                  "AccountId"
                ],
                "type": 2
              }
            },
            {
              "docs": [],
              "indexed": true,
              "label": "spender",
              "type": {
                "displayName": [
                  "AccountId"
                ],
                "type": 2
              }
            },
            {
              "docs": [],
              "indexed": false,
              "label": "value",
              "type": {
                "displayName": [
                  "Balance"
                ],
                "type": 0
              }
            }
          ],
          "docs": [
            " Event emitted when an approval occurs that `spender` is allowed to withdraw",
            " up to the amount of `value` tokens from `owner`."
          ],
          "label": "Approval"
        }
      ],
      ...

Once it is uploaded the events can be decoded using the decodeEvent or decodeEvents mutation that can be found on section Mutations.

Note: The metadata should be uploaded as a base64 string.

For more on uploading the metadata go to the Mutations section a search for uploadMetadata.

Subscriptions

The first time the node is started, it may need to start from the block 0 and load all blocks (LOAD_ALL_BLOCKS env var should be set to true). If you want to start from a specific block, you can use the FIRST_BLOCK_TO_LOAD env var to start from another block.

In case of a downtime of the node, the subscriptions will be reconnected automatically recovering all new blocks from the last block that was processed.

Note: Load all blocks may take a long time depending on the number of blocks that need to be loaded. It is recommended to use a node with a fast internet connection. The node will be able to process all blocks in a few hours.

Some benchmarks

Using BLOCK_CONCURRENCY = 100

  • 100 blocks ~ 6 seconds
  • 1000 blocks ~ 30.5 seconds
  • 10000 blocks ~ 4:24 minutes
  • 100000 blocks ~ 39.57 minutes

Using BLOCK_CONCURRENCY = 1000

  • 100 blocks ~ 0.5 seconds
  • 1000 blocks ~ 5 seconds
  • 10000 blocks ~ 3 minutes
  • 100000 blocks ~ 24 minutes

Change Log

See Changelog for more information.

Contributing

Contributions welcome! See Contributing.

Collaborators

License

Licensed under the Apache 2.0 - see the LICENSE file for details.