Zrc2 Wallet Support
Since most of the wallet front-ends are built using a JavaScript framework, most
developers would find the code found in the js
tab to be relevant to them. We
have included code snippets for some other languages as well in case you want to
handle these functionalities at the backend.
Note
It is assumed that you have already installed language-specific Zilliqa SDKs, e.g., zilliqa-js, laksaj, pyzil, gozilliqa.
Introduction to ZRC-2
ZRC-2 is the formal standard for Fungible Token in Zilliqa. It is an open standard for creating currencies on the Zilliqa blockchain.
The ZRC-2 standard allows for functionalities like
- Minting/burning tokens
- Transferring tokens from one account to another
- Querying account token balance
- Querying total token balances
- Approving third party to spend a certain amount of tokens
- Etc.
Examples of ZRC-2
- $XSGD - the first Singapore dollar-pegged stablecoin built by Xfers
- $gZIL - Governance ZIL token earned through Zilliqa Seed Node Staking Program
Getting Testnet ZRC-2 Tokens
ZRC-2 Specification
ZRC-2 fungible tokens contract consists of the following components in smart contracts as per the specification.
Error Codes
Name | Type | Code | Mandatory? |
---|---|---|---|
CodeIsSender |
Int32 |
-1 |
|
CodeInsufficientFunds |
Int32 |
-2 |
|
CodeInsufficientAllowance |
Int32 |
-3 |
|
CodeNotOwner |
Int32 |
-4 |
|
CodeNotApprovedOperator |
Int32 |
-5 |
Immutable Variables
Name | Type | Mandatory? |
---|---|---|
contract_owner |
ByStr20 |
|
name |
String |
|
symbol |
String |
|
decimals |
Uint32 |
|
init_supply |
Uint128 |
|
default_operators |
List ByStr20 |
Mutable Variables
Name | Type | Mandatory? |
---|---|---|
total_supply |
Uint128 |
|
balances |
Map ByStr20 Uint128 |
|
allowances |
Map ByStr20 (Map ByStr20 Uint128) |
|
operators |
Map ByStr20 (Map ByStr20 Unit) |
|
revoked_default_operators |
Map ByStr20 (Map ByStr20 Unit) |
Transitions and Events
Name | Events | Mandatory? |
---|---|---|
IsOperatorFor() |
||
Mint() |
Minted , Error |
|
Burn() |
Burnt , Error |
|
AuthorizeOperator() |
AuthorizeOperatorSuccess , Error |
|
RevokeOperator() |
RevokeOperatorSuccess , Error |
|
IncreaseAllowance() |
IncreasedAllowance , Error |
|
DecreaseAllowance() |
DecreasedAllowance , Error |
|
Transfer() |
TransferSuccess , Error |
|
TransferFrom() |
TransferFromSuccess , Error |
|
OperatorSend() |
OperatorSendSuccess , Error |
Checking if Contract is Compliant with ZRC-2 Standard
In order to check if the address entered by the user is a ZRC-2 compliant contract, we need to look at the contract code and check if it has the required properties.
Check Immutable Variables
For immutable variables, we'll use the GetSmartContractInit
method of the
Zilliqa API to get the immutable variables of the contract. The address of the
ZRC-2 contract referred below is 0x173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5
.
Example Request
curl -d '{
"id": "1",
"jsonrpc": "2.0",
"method": "GetSmartContractInit",
"params": ["173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5"]
}' -H "Content-Type: application/json" -X POST "https://api.zilliqa.com/"
const smartContractInit = await zilliqa.blockchain.getSmartContractInit(
"173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5"
);
console.log(smartContractInit.result);
public class App {
public static void main(String[] args) throws IOException {
HttpProvider client = new HttpProvider("https://api.zilliqa.com");
Rep<List<Contract.State>> smartContractInit = client.getSmartContractInit("173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5");
System.out.println(new Gson().toJson(smartContractInit));
}
}
from pyzil.zilliqa import chain
chain.set_active_chain(chain.MainNet)
print(chain.active_chain.api.GetSmartContractInit("173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5"))
func GetSmartContractInit() {
provider := NewProvider("https://api.zilliqa.com/")
response := provider.GetSmartContractInit("173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5")
result, _ := json.Marshal(response)
fmt.Println(string(result))
}
Example Response
{
"id": "1",
"jsonrpc": "2.0",
"result": [
{
"type": "Uint32",
"value": "0",
"vname": "_scilla_version"
},
{
"type": "ByStr20",
"value": "0x0f8167a0CBFfb8AB1d1919E31f83DC26C863D0F9",
"vname": "contract_owner"
},
{
"type": "String",
"value": "XSGD",
"vname": "name"
},
{
"type": "String",
"value": "XSGD",
"vname": "symbol"
},
{
"type": "Uint32",
"value": "6",
"vname": "decimals"
},
{
"type": "Uint128",
"value": "0",
"vname": "init_supply"
},
{
"type": "ByStr20",
"value": "0x0f8167a0CBFfb8AB1d1919E31f83DC26C863D0F9",
"vname": "init_implementation"
},
{
"type": "ByStr20",
"value": "0x0f8167a0CBFfb8AB1d1919E31f83DC26C863D0F9",
"vname": "init_admin"
},
{
"type": "BNum",
"value": "732529",
"vname": "_creation_block"
},
{
"type": "ByStr20",
"value": "0x173ca6770aa56eb00511dac8e6e13b3d7f16a5a5",
"vname": "_this_address"
}
]
}
The response received consists of the type
, value
, and vname
for each
immutable variable.
To check whether a contract is compliant with the ZRC-2 standard's immutable variables requirement, see if the contract implements the mandatory immutable variables.
Note
This code snippet is in JavaScript but the same logic can be applied in other languages.
let vNameArray = []; //Array to store immutable variable names
for (let i = 0; i < smartContractInit.result.length; i++) {
vNameArray[i] = smartContractInit.result[i].vname;
}
//check if the immutable variables: name, symbol, decimals & init_supply exist in the vName array.
let isZRC2 =
vNameArray.includes("name") &&
vNameArray.includes("symbol") &&
vNameArray.includes("decimals") &&
vNameArray.includes("init_supply");
console.log(isZRC2);
Check Mutable Variables
For mutable variables, we'll use the GetSmartContractState
method of the
Zilliqa API to get the mutable variables of the contract. The address of the
ZRC2 contract referred below is 0x173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5
.
Example Request
curl -d '{
"id": "1",
"jsonrpc": "2.0",
"method": "GetSmartContractState",
"params": ["173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5"]
}' -H "Content-Type: application/json" -X POST "https://api.zilliqa.com/"
const smartContractState = await zilliqa.blockchain.getSmartContractState(
"173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5"
);
console.log(smartContractState.result);
public class App {
public static void main(String[] args) throws IOException {
HttpProvider client = new HttpProvider("https://api.zilliqa.com");
String smartContractState = client.getSmartContractState("173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5");
System.out.println(smartContractState);
}
}
from pyzil.zilliqa import chain
chain.set_active_chain(chain.MainNet)
print(chain.active_chain.api.GetSmartContractState("173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5"))
func GetSmartContractState() {
provider := NewProvider("https://api.zilliqa.com/")
response := provider.GetSmartContractState("173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5")
result, _ := json.Marshal(response)
fmt.Println(string(result))
}
Example Response
{
"id": "1",
"jsonrpc": "2.0",
"result": {
"_balance": "0",
"admin": "0x2f604cbd408e2c8b7442b1b629a1288c44945130",
"allowances": {},
"balances": {
"0x05d087623bc636108d450e0550ddbfc03da99fa9": "10000000",
"0x0f8167a0cbffb8ab1d1919e31f83dc26c863d0f9": "24250000",
"0x18c241a5f0d6cf380f721618880f2c2b7aa5ea97": "9975750000",
"0x5abf71d798ca594b7317b04f52ad5a31fae62170": "10000000",
"0x8c3de413a9d8d602a1757210ab539853103e08d8": "250000000000",
"0x93eb1d0cb7ba3962fcc86cd28aa45b241c888277": "20000",
"0xab61c57a9a4b2742a4a325ecd9e77b5da67f663a": "980000",
"0xabe1e844c776e97beb619f3ca64faa2b3edc2840": "400000",
"0xc48565c853fe4ffa5d7eac33e255141134640ceb": "600000",
"0xf5f9e1ad8ea6439f625e12b8ef57e1e99ac2d383": "7000000"
},
"implementation": "0x3bd9ad6fee7bfdf5b5875828b555e4f702e427cd",
"total_supply": "260029000000"
}
}
The response received above consists of the mutable variables total_supply
,
balances
, and allowances
.
To check whether a contract is compliant with the ZRC-2 standard's mutable variables requirement, see if the contract implements the mandatory mutable variables.
Note
This code snippet is in JavaScript but the same logic can be applied in other languages.
let vNameArray = []; //Array to store mutable variable names
for (let i = 0; i < smartContractState.result.length; i++) {
vNameArray[i] = smartContractState.result[i].vname;
}
//check if the mutable variables: total_supply, balances & allowances exist in the vName array.
let isZRC2 =
vNameArray.includes("total_supply") &&
vNameArray.includes("balances") &&
vNameArray.includes("allowances");
console.log(isZRC2);
Check Transitions, Events, and Error Codes
Currently, you need to look at the smart contract code manually and check for transitions, events, and error codes in a contract. To check whether a contract is compliant with the ZRC-2 standard's error codes - as well transitions and events - requirement, see if the contract implements the mandatory error codes and mandatory transitions and events.
Integrating with ZRC-2 Fungible Tokens Contract
Fetch Token Supply
In a ZRC-2 fungible tokens contract, the mutable field total_supply
stores the
value of the current total supply. We'll use the GetSmartContractState
method
of the Zilliqa API to get the mutable variables of the contract. The address of
the ZRC-2 contract referred below is
0x173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5
.
Example Request
curl -d '{
"id": "1",
"jsonrpc": "2.0",
"method": "GetSmartContractState",
"params": ["173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5"]
}' -H "Content-Type: application/json" -X POST "https://api.zilliqa.com/"
const smartContractState = await zilliqa.blockchain.getSmartContractState(
"173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5"
);
console.log(smartContractState.result);
public class App {
public static void main(String[] args) throws IOException {
HttpProvider client = new HttpProvider("https://api.zilliqa.com");
String smartContractState = client.getSmartContractState("173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5");
System.out.println(smartContractState);
}
}
from pyzil.zilliqa import chain
chain.set_active_chain(chain.MainNet)
print(chain.active_chain.api.GetSmartContractState("173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5"))
func GetSmartContractState() {
provider := NewProvider("https://api.zilliqa.com/")
response := provider.GetSmartContractState("173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5")
result, _ := json.Marshal(response)
fmt.Println(string(result))
}
Example Response
{
"id": "1",
"jsonrpc": "2.0",
"result": {
"balances": {
"0x00034c431f642dfca0129694061263d72049a909": "1033606"
}
}
}
If the response json received above is stored in the variable
smartContractState
, total supply would then be:
let smartContractState =
await zilliqa.blockchain.getSmartContractState(contractAddress);
let totalSupply = smartContractState.result.total_supply; // Total Supply
Fetch Token Balance
In a ZRC-2 fungible tokens contract, the mutable field balances
(with data
type Map
) stores the mapping between addresses and their corresponding token
balances. We'll use the GetSmartContractState
method of the Zilliqa API to get
the mutable variables of the contract. The address of the ZRC-2 contract
referred below is 0x173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5
.
Example Request
curl -d '{
"id": "1",
"jsonrpc": "2.0",
"method": "GetSmartContractState",
"params": ["173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5"]
}' -H "Content-Type: application/json" -X POST "https://api.zilliqa.com/"
const smartContractState = await zilliqa.blockchain.getSmartContractState(
"173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5"
);
console.log(smartContractState.result);
public class App {
public static void main(String[] args) throws IOException {
HttpProvider client = new HttpProvider("https://api.zilliqa.com");
String smartContractState = client.getSmartContractState("173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5");
System.out.println(smartContractState);
}
}
from pyzil.zilliqa import chain
chain.set_active_chain(chain.MainNet)
print(chain.active_chain.api.GetSmartContractState("173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5"))
func GetSmartContractState() {
provider := NewProvider("https://api.zilliqa.com/")
response := provider.GetSmartContractState("173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5")
result, _ := json.Marshal(response)
fmt.Println(string(result))
}
Example Response
{
"id": "1",
"jsonrpc": "2.0",
"result": {
"_balance": "0",
"admin": "0x2f604cbd408e2c8b7442b1b629a1288c44945130",
"allowances": {},
"balances": {
"0x05d087623bc636108d450e0550ddbfc03da99fa9": "10000000",
"0x0f8167a0cbffb8ab1d1919e31f83dc26c863d0f9": "24250000",
"0x18c241a5f0d6cf380f721618880f2c2b7aa5ea97": "9975750000",
"0x5abf71d798ca594b7317b04f52ad5a31fae62170": "10000000",
"0x8c3de413a9d8d602a1757210ab539853103e08d8": "250000000000",
"0x93eb1d0cb7ba3962fcc86cd28aa45b241c888277": "20000",
"0xab61c57a9a4b2742a4a325ecd9e77b5da67f663a": "980000",
"0xabe1e844c776e97beb619f3ca64faa2b3edc2840": "400000",
"0xc48565c853fe4ffa5d7eac33e255141134640ceb": "600000",
"0xf5f9e1ad8ea6439f625e12b8ef57e1e99ac2d383": "7000000"
},
"implementation": "0x3bd9ad6fee7bfdf5b5875828b555e4f702e427cd",
"total_supply": "260029000000"
}
}
Fetch Token Balance For A Single Key
If the need to fetch a single token value from the balances
map arises.
Instead of requesting the whole map state over the network, using
getSmartContractSubState
is the most efficent method to use when you don't
need to fetch the whole map. It returns one value per key, instead of the entire
map. Allowing for quicker response times when only certain key values are
required to be fetched.
Example Request
curl -d '{
"id": "1",
"jsonrpc": "2.0",
"method": "GetSmartContractSubState",
"params": ["173ca6770aa56eb00511dac8e6e13b3d7f16a5a5", "balances", ["0x00034c431f642dfca0129694061263d72049a909, ..."] ]
}' -H "Content-Type: application/json" -X POST "https://api.zilliqa.com/"
const smartContractState = await zilliqa.blockchain.getSmartContractSubState(
"173Ca6770Aa56EB00511Dac8e6E13B3D7f16a5a5",
"balances",
["0x8c3de413a9d8d602a1757210ab539853103e08d8, ..."]
);
console.log(smartContractState.result);
Example Response
{
"id": "1",
"jsonrpc": "2.0",
"result": {
"0x8c3de413a9d8d602a1757210ab539853103e08d8": "250000000000",
...
}
}
Transfer Token
ZRC-2 contracts have the transfer
transition that allows users to transfer
tokens to another address by specifying the receiving address and amount.
The code snippet below illustrates how one can transfer ZRC-2 tokens to another address.
//zilliqa, privateKey, bytes, units, recipientAddress, sendingAmount are defined in the codebase before
zilliqa.wallet.addByPrivateKey(privateKey);
const CHAIN_ID = 1;
const MSG_VERSION = 1;
const VERSION = bytes.pack(CHAIN_ID, MSG_VERSION);
const myGasPrice = units.toQa("1000", units.Units.Li); // Gas Price that will be used by all transactions
try {
const contract = zilliqa.contracts.at(contractAddress);
const callTx = await contract.call(
"Transfer",
[
{
vname: "to",
type: "ByStr20",
value: recipientAddress,
},
{
vname: "amount",
type: "Uint128",
value: sendingAmount,
},
],
{
// amount, gasPrice and gasLimit must be explicitly provided
version: VERSION,
amount: new BN(0),
gasPrice: myGasPrice,
gasLimit: Long.fromNumber(10000),
}
);
console.log(JSON.stringify(callTx.TranID));
} catch (err) {
console.log(err);
}
List<Value> init = Arrays.asList();
Wallet wallet = new Wallet();
wallet.addByPrivateKey(privateKey);
ContractFactory factory = ContractFactory.builder().provider(new HttpProvider("https://api.zilliqa.com/")).signer(wallet).build();
Contract contract = factory.atContract(contractAddress, "", (Value[]) init.toArray(), "");
Integer nonce = Integer.valueOf(factory.getProvider().getBalance(address).getResult().getNonce());
CallParams params = CallParams.builder().nonce(String.valueOf(nonce + 1)).version(String.valueOf(pack(333, 1))).gasPrice("1000000000").gasLimit("1000").senderPubKey(publicKey).amount("0").build();
List<Value> values = Arrays.asList(Value.builder().vname("to").type("ByStr20").value(recipientAddress).build(), Value.builder().vname("amount").type("Uint128").value("10").build());
contract.call("Transfer", (Value[]) values.toArray(), params, 3000, 3);
wallet := account.NewWallet()
wallet.AddByPrivateKey(privateKey)
publickKey := keytools.GetPublicKeyFromPrivateKey(util.DecodeHex(privateKey), true)
address := keytools.GetAddressFromPublic(publickKey)
fmt.Println(address)
contract := Contract{
Address: "contractAddress",
Signer: wallet,
}
args := []core.ContractValue{
{
"to",
"ByStr20",
"0x" + address,
},
{
"amount",
"Uint128",
"10",
},
}
tx, err2 := contract.CallFor("Transfer", args, true, "0", TestNet)
assert.Nil(t, err2, err2)
tx.Confirm(tx.ID, 1000, 3, contract.Provider)
assert.True(t, tx.Status == core.Confirmed)
Adding Token Allowance
The Zilliqa blockchain allows transactions with smart contracts and those smart
contracts can be facilitated by third parties like a DEX or a protocol relayer –
permissions have to be granted to the third party by token owners before those
smart contracts can execute. Token allowance permission gives the dApp contract
the right to transfer the user's ZRC-2 token if the current approved allowance
is equal to or more than amount
requested to being transferred. The approval
is done by calling IncreaseAllowance
transition.
In the example below, the allowance amount is the same as the entire token supply. This is done so that the approval needs to only be done once per token contract, reducing the number of approval transactions required for users conducting multiple swaps.
However, you can make this value to be specific as well. Non-custodial control of the token should be ensured by the dApp contract itself, which does not allow for the transfer of tokens unless explicitly invoked by the sender.
let increaseAllowance = async function (contractAddress, spenderAddress) {
//contractAddress is the address of ZRC2 contract
//spenderAddress is the address of which you want to increase the allowance, eg: your dApp contract
const CHAIN_ID = 333;
const MSG_VERSION = 1;
const VERSION = bytes.pack(CHAIN_ID, MSG_VERSION);
const myGasPrice = units.toQa("2000", units.Units.Li);
let smartContractState = await zilliqa.blockchain.getSmartContractState(
contractAddress
);
let totalSupply = smartContractState.result.total_supply;
try {
const contract = zilliqa.contracts.at(contractAddress);
const callTx = await contract.call(
"IncreaseAllowance",
[
{
vname: "spender",
type: "ByStr20",
value: spenderAddress,
},
{
vname: "amount",
type: "Uint128",
value: totalSupply,
},
],
{
// amount, gasPrice and gasLimit must be explicitly provided
version: VERSION,
amount: new BN(0),
gasPrice: myGasPrice,
gasLimit: Long.fromNumber(10000),
}
);
console.log(JSON.stringify(callTx.TranID));
} catch (err) {
console.log(err);
}
};
Calling TransferFrom
TransferFrom
transition moves a given amount of tokens from one address to
another using the allowance mechanism. The caller must be an approved_spender
,
refer the section on Adding Token Allowance
if you want to add an address to become an approved_sender. Balance of recipient
will increase and the balance of token_owner
will decrease.
let transferFrom = async function (
contractAddress,
userAddress,
receiverAddress
) {
const CHAIN_ID = 333;
const MSG_VERSION = 1;
const VERSION = bytes.pack(CHAIN_ID, MSG_VERSION);
const myGasPrice = units.toQa("2000", units.Units.Li);
try {
const contract = zilliqa.contracts.at(contractAddress);
const callTx = await contract.call(
"TransferFrom",
[
{
vname: "from",
type: "ByStr20",
value: userAddress,
},
{
vname: "to",
type: "ByStr20",
value: receiverAddress,
},
{
vname: "amount",
type: "Uint128",
value: "1000",
},
],
{
// amount, gasPrice and gasLimit must be explicitly provided
version: VERSION,
amount: new BN(0),
gasPrice: myGasPrice,
gasLimit: Long.fromNumber(10000),
}
);
console.log(JSON.stringify(callTx.TranID));
} catch (err) {
console.log(err);
}
};
Tracking Incoming ZRC-2 Deposit
Please check the Tracking Incoming ZRC-2 Deposit subsection under exchanges section to track any new incoming deposit of a specific ZRC-2 token.
Getting your Token Listed
You can get your token listed on various Zilliqa ecosystem products to allow for easier recognition by your community.
Listing on Zilswap
To add your token to the token listed on Zilswap, please refer the README.md file on this repository.
Zilpay wallet also refers to the listed tokens list on Zilswap when deciding which tokens to add to their default list.
Listing on Viewblock
To add your token to Viewblock tokens listing page, please refer to the README.md file on this repository. Viewblock will assign a score to your token based on the stated token listing criteria.