Theta Blockchain TNT20 Token Integration Guide

1. Introduction

Theta's latest release provides support for the Ethereum RPC API similar to Binance Smart Chain and Polygon. With it, Theta now supports the entire Etherum DApp dev stack including Metamask, Hardhat, Remix, Ethers.js, Web3.js, and Truffle Suite. Ethereum DApps that are ported over to Theta can use the same API calls to interact with Theta blockchain. This means Ethereum DApps can be deployed to Theta with no or minor modifications and tap into the growing Theta user and capital base. Please click here for more details.

1.1 Hosted ETH RPC API for Integration Tests

To facilitate DApp development, we provide hosted ETH RPC API services for integration test purposes:

Theta Mainnet
ETH RPC URL: https://eth-rpc-api.thetatoken.org/rpc
Explorer: https://explorer.thetatoken.org/
Chain ID: 361

Theta Testnet
ETH RPC URL: https://eth-rpc-api-testnet.thetatoken.org/rpc
Explorer: https://testnet-explorer.thetatoken.org/
Chain ID: 365

Through these hosted API services, you can interact with the Theta blockchain like you would with the Ethereum blockchain. Below are a few examples demonstrating querying the chain ID, synchronization status, block number, and TFuel balances against the Theta Mainnet.

# Query Chain ID
curl -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":67}' https://eth-rpc-api.thetatoken.org/rpc

# Query synchronization status
curl -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' https://eth-rpc-api.thetatoken.org/rpc

# Query block number
curl -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":83}' https://eth-rpc-api.thetatoken.org/rpc

# Query account TFuel balance (should return an integer which represents the current TFuel balance in wei)
curl -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0xc15149236229bd13f0aec783a9cc8e8059fb28da", "latest"],"id":1}' https://eth-rpc-api.thetatoken.org/rpc

1.2 In House ETH RPC API Deployment

For production deployments, we highly recommend setting up your own in-house Theta Ethereum RPC API, rather than using the above public endpoints. To enable the Theta ETH RPC API service, you’d need to run two software on the same machine, namely 1) the Theta Node, and 2) the Theta/Ethereum RPC adapter. The adapter is a middleware which translates the Theta native RPC APIs to the Ethereum RPC API. To setup in-house Theta Ethereum RPC API service, please follow the instructions in the link below:

https://docs.thetatoken.org/docs/setup-the-eth-rpc-adaptor-for-the-theta-mainnet

2. TNT-20 Token

With the full-EVM compatibility comes the TNT20 token, which is an ERC-20 like token standard on the Theta blockchain. TNT stands for Theta Network Token.

Here is TDROP, an TNT20 Token on the Theta Mainnet. Please click on the "Contract" tab see the verified Solidity source code:
https://explorer.thetatoken.org/account/0x1336739B05C7Ab8a526D40DCC0d04a826b5f8B03

Below is a Javascript code snippet which reads the total supply, name, symbol of the TDROP token using web3.js. The code snippet should look familiar to an Ethereum developer, as it is exactly the same code for querying the corresponding attributes of an ERC20 token on Ethereum. These queries can also be done using ethers.js.

Example 1: Read TNT20 Token

const Web3 = require('web3')
const web3 = new Web3('https://eth-rpc-api.thetatoken.org/rpc')
const chainID = 361 // for the Theta Mainnet

const abi = [{"inputs":[{"internalType":"string","name":"name_","type":"string"},{"internalType":"string","name":"symbol_","type":"string"},{"internalType":"uint8","name":"decimals_","type":"uint8"},{"internalType":"uint256","name":"initialSupply_","type":"uint256"},{"internalType":"bool","name":"mintable_","type":"bool"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"mint","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"mintable","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]
const address = '0x1336739B05C7Ab8a526D40DCC0d04a826b5f8B03' // The TDROP TNT20 token (similar to ERC20) smart contract

const contract = new web3.eth.Contract(abi, address)

contract.methods.totalSupply().call((err, result) => { 
    console.log("totalSupply:", result, "TDROP (wei)") 
})

contract.methods.name().call((err, result) => {
    console.log("name:", result)
})

contract.methods.symbol().call((err, result) => {
    console.log("symbol:", result)
})

contract.methods.balanceOf('0xc15149236229bd13f0aec783a9cc8e8059fb28da').call((err, result) => {
    console.log("balance of 0xc15149236229bd13f0aec783a9cc8e8059fb28da:", result, "TDROP (wei)")
})

Transferring TNT20 is also similar to transferring ERC20 tokens. Moreover, same as an ERC20 token on Ethereum, whenever there is a TNT20 token transfer between two wallets, the Transfer event will be triggered. This could be useful for detecting TNT20 token deposits. Here is an TNT20 token transfer example on the Theta explorer. Below we also provide a Javascript code snippet showing how to transfer the TDROP token between two wallets:

Example 2: Transfer TNT20 Tokens

const Web3 = require('web3');
const web3 = new Web3('https://eth-rpc-api.thetatoken.org/rpc')
const chainID = 361 // for the Theta Mainnet

// Variables definition
const senderPrivKey = '1111111111111111111111111111111111111111111111111111111111111111'
const senderAddr    = "0x19E7E376E7C213B7E7e7e46cc70A5dD086DAff2A"
const recipientAddr = "0x1563915e194D8CfBA1943570603F7606A3115508"

const tdropContractAddress = "0x1336739B05C7Ab8a526D40DCC0d04a826b5f8B03";
const abi = [{"inputs":[{"internalType":"string","name":"name_","type":"string"},{"internalType":"string","name":"symbol_","type":"string"},{"internalType":"uint8","name":"decimals_","type":"uint8"},{"internalType":"uint256","name":"initialSupply_","type":"uint256"},{"internalType":"bool","name":"mintable_","type":"bool"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"mint","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"mintable","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]
const contract = new web3.eth.Contract(abi, tdropContractAddress)

// Create transaction
const sendTDrop = async(tdropAmountInWei) => {
   console.log(`Attempting to send ${tdropAmountInWei} wei of TDROP from ${senderAddr} to ${recipientAddr}`);
  
   const count = await web3.eth.getTransactionCount(senderAddr);
   const createTransaction = await web3.eth.accounts.signTransaction({
        "from": senderAddr,
        "nonce": web3.utils.toHex(count),
        "gas": web3.utils.toHex(150000),
        "to": tdropContractAddress,
        "data": contract.methods.transfer(recipientAddr, tdropAmountInWei).encodeABI()
      },
      senderPrivKey
   );

   // Deploy transaction
   const createReceipt = await web3.eth.sendSignedTransaction(
      createTransaction.rawTransaction
   );

   console.log("");
   console.log("Transaction successful with hash:", createReceipt.transactionHash);
   console.log("");
   console.log("Transaction details:", JSON.stringify(createReceipt, null, "  "));
};

const tdropAmountInWei = "100" // Sending 100 wei of TDROP (TDROP has 18 decimals)
sendTDrop(tdropAmountInWei)

The following example script shows how to list all the events emitted by the TDROP contract in the specified block range (please avoid large block ranges, best below 200):

Example 3: List TNT20 Token Events

const Web3 = require('web3')
const web3 = new Web3('https://eth-rpc-api.thetatoken.org/rpc')
const chainID = 361 // for the Theta Mainnet

const tdropContractAddress = '0x1336739B05C7Ab8a526D40DCC0d04a826b5f8B03' // The TDROP TNT20 token (similar to ERC20) smart contract
const abi = [{"inputs":[{"internalType":"string","name":"name_","type":"string"},{"internalType":"string","name":"symbol_","type":"string"},{"internalType":"uint8","name":"decimals_","type":"uint8"},{"internalType":"uint256","name":"initialSupply_","type":"uint256"},{"internalType":"bool","name":"mintable_","type":"bool"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"mint","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"mintable","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]
const contract = new web3.eth.Contract(abi, tdropContractAddress)

let fromBlock = 14045180
let toBlock   = 14045280

// Get all the events emitted by the TDROP contract in the specified block range
contract.getPastEvents("allEvents", {
   fromBlock: fromBlock, 
   toBlock: toBlock}
).then(console.log);

3. Metamask Support

End users can send and receive TNT20 tokens using Metamask. More details here: https://medium.com/theta-network/theta-blockhain-now-accessible-through-metamask-plug-in-61b278633264

4. Explorer Support

The Theta explorer provides extensive details for TNT20 tokens and their transactions similar to EtherScan. Below are examples including a TNT20 token page, a transaction details page, and the TNT20 transaction list of an account:

TNT20 Token page
https://explorer.thetatoken.org/token/0x1336739B05C7Ab8a526D40DCC0d04a826b5f8B03

TNT20 Transaction detail page:
https://explorer.thetatoken.org/txs/0xce17464de92e51ad914fa724854d837b26e9feb96ef0b5f00d93332a7732691f

TNT20 Token transactions of an account (please click on the “TNT20 Token Txns” tab):
https://explorer.thetatoken.org/account/0xf5f0bd8ad98fd38306ec16608aa9f4e6bcff5a93

5. Smart Contract Transaction On-Chain Confirmation

The Theta blockchain supports the ETH-style transactions (those sent through Metamask, Web3.js, Ethers.js, etc) by translating them into the native smart contract format. This means the ETH-style transactions have two valid transaction hashes. Here is an example. As shown on the explorer, it has two hashes. You can query the Theta native RPC with either one to get the transaction details:

curl -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","method":"theta.GetTransaction","params":[{"hash":"0x73f20cee81d16f3927d89952fbc860bd344b645dc616864f2622792107251856"}],"id":1}' http://localhost:16888/rpc

curl -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","method":"theta.GetTransaction","params":[{"hash":"0xd5462bb2b91f7fb57b5428c6492513af54713c5d07f46e2af103cde428c4102f"}],"id":1}' http://localhost:16888/rpc

Moreover, the theta.GetBlockByHeight and theta.GetBlock RPC endpoint can return both hashes when its query parameter include_eth_tx_hashes is set to true. As the following example shows, the transaction details include both the hash and eth_tx_hash:

curl -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","method":"theta.GetBlockByHeight","params":[{"height":"14158920", "include_eth_tx_hashes":true}],"id":1}' http://localhost:16888/rpc

# The RPC API response
{
	"jsonrpc": "2.0",
	"id": 1,
	"result": {
		"chain_id": "mainnet",
		"epoch": "14244042",
		"height": "14158920",
		...,
		"status": 4,
		"transactions": [
		 ...,
		 {
			"raw": {...},
			"type": 7,
			"hash": "0x73f20cee81d16f3927d89952fbc860bd344b645dc616864f2622792107251856",
			"eth_tx_hash": "0xd5462bb2b91f7fb57b5428c6492513af54713c5d07f46e2af103cde428c4102f",
			"receipt": {...}
		}]
	}
}