Version
Show / Hide Table of Contents

Dealing with Asset Transactions

Overview

Neo3 has only one type of digital assets, i.e. NEP-5 assets, which are managed by BALANCE. The exchanges mainly deal with user balance queries, deposits, withdrawals, and other operations of this type assets.

Following flow charts show the work processes of these operations:

Network fee

The network fee, as a reward for the consensus nodes generating blocks, is charged when the user submits a transactions to Neo blockchain. There is a base fee for each transaction and the calculation formula is shown below. The transaction is only executed if the fee paid by the user is greater than or equal to the base fee; otherwise, the transaction will be treated as invalid.

NetworkFee = VerificationCost + tx.size * FeePerByte
  • VerficationCost:Fees for instructions executed by NeoVM to verify transaction signatures.

  • tx.size:The transaction data byte length

  • FeePerByte:Transaction fee per byte, currently defined as 0.00001 GAS in PolicyContract.

System fee

The system fee is charged for the instructions executed by NeoVM. For each instruction fee refer to System Fee . The total system fee you need to pay depends on the number and type of the instructions executed by your smart contract. The following figure shows the calculation formula:

SystemFee = InvocationCost = The sum of all executed opcode fee

Instructions fee

In Neo3, NeoVM instructions fee has decreased to 1/1000 of the original fee in Neo2, which significantly reduces the development cost.

In comparison with Neo2.x:

netfee

Dealing with query transactions

The way for a exchange itself to query balance of the user deposit address is different than the way it deal with the user's request of balance querying.

Querying the user deposit address balance

The exchange needs to do the following:

  1. Construct JSON files to invoke either of the following RPC methods:

    • getnep17balances

    • invokefunction

  2. Send the JSON files to Neo RPC server.

  3. Calculate the user balance according to the returned values.

Invoking getnep17balances to query

In JSON, a general getnep17balances request body is in the following form:

{
"jsonrpc": "2.0",
"method": "getnep17balances",
"params": ["NVfJmhP28Q9qva9Tdtpt3af4H1a3cp7Lih"],
"id": 1
}

After sending the request you will get the following response:

{
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "balance": [
            {
                "asset_hash": "0xf61eebf573ea36593fd43aa150c055ad7906ab83",
                "amount": "2",
                "last_updated_block": 52675
            },
            {
                "asset_hash": "0x70e2301955bf1e74cbb31d18c2f96972abadb328",
                "amount": "700000000",
                "last_updated_block": 52675
            }
        ],
        "address": "NVfJmhP28Q9qva9Tdtpt3af4H1a3cp7Lih"
    }
}

According to all the returned values, we can calculate the user balance as follows: The balance = 700000000/10⁸ NEO = 7 GAS, 2 NEO

Invoking invokefunction to query

In JSON, a general invokefunction request body is in the following form:

{
  "jsonrpc": "2.0",
  "method": "invokefunction",
  "params": [
    "script hash",
    "method name",
    [
      {
        "optional arguments"
      }
    ]
  ],
  "id": 1
}

You need to replace these strings when querying the user's balance:

  • script hash

    The script hash of the NEP-5 asset you are querying. For example:

    • NEO is0xf61eebf573ea36593fd43aa150c055ad7906ab83

    • GAS is0x70e2301955bf1e74cbb31d18c2f96972abadb328

  • method name

    The name of the method you are invoking. To query the user's balance, you need to invoke these three methods:

    balanceOf

    • Syntax: public static BigInteger balanceOf(byte[] account)

    • Remarks: "balanceOf" returns the token balance of the '''account'''.

    • Syntax: public static byte decimals()

    • Remarks: "decimals" returns the number of decimals used by the token.

    • Syntax: public static string symbol()

    • Remarks: "symbol" returns the token symbol.

    decimals

    symbol

  • optional arguments

    Optional. If the method you are invoking requires arguments, you can pass them by constructing these parameters into an array. For example, "balanceOf" in NEP-5 returns the token balance of the "account":

    public static BigInteger balanceOf(byte[] account)

    So you need to pass the account as an argument in the "balanceOf" method.

Example
Invoking balanceOf

Suppose the account address is AKibPRzkoZpHnPkF6qvuW2Q4hG9gKBwGpR, you need to convert it into Hash160 type and construct this parameter as a JSON object:

{
    "type": "Hash160",
    "value": "0x09ad8f0b21a7294b3e429f58eaa415ac4b327ec9"
}

Then you can construct the JSON message as the following:

Request Body:

{
  "jsonrpc": "2.0",
  "method": "invokefunction",
  "params": [
    "0xe9fa06842455ecf020b15d0e9b9c42de24ea3c6d",
    "balanceOf",
    [
      {
        "type": "Hash160",
        "value": "0x09ad8f0b21a7294b3e429f58eaa415ac4b327ec9"
      }
    ]
  ],
  "id": 3
}

After sending the request, depending on your smart contract code you will get the response like one of the following:

  • The ByteString value encoded by base64 is returned:

    { 
      "jsonrpc": "2.0", 
      "id": 3, 
      "result": { 
          "script": "14c97e324bac15a4ea589f423e4b29a7210b8fad0951c10962616c616e63654f66146d3cea24de429c9b0e5db120f0ec55248406fae968627d5b52", 
          "state": "HALT", 
          "gas_consumed": "8295750", 
          "stack": [ 
              { 
                  "type": "ByteString", 
                  "value": "AADBb/KGIw==" 
              } 
          ] 
      } 
    } 
    

    Decode the returned value ”AADBb/KGIw==“ with base64 and convert it to BigInteger you will get 1x1016 , and then divide it by 8-bit decimals you will get the NEP-5 assets balance 1x108 .

  • The Integer type value is returned:

    { 
      "jsonrpc": "2.0", 
      "id": 3, 
      "result": { 
          "script": "0c14b97b4acd7f820f61d2d4d4f9aea5eb50498ddf5511c00c0962616c616e63654f660c14ec99f691c0f7dfa41400473edd1c2afceb70c2d241627d5b52", 
          "state": "HALT", 
          "gasconsumed": "3738760", 
          "stack": [ 
              { 
                  "type": "Integer", 
                  "value": "10000000000000000" 
              } 
          ] 
      } 
    } 
    

    Divide the returned value by 8-bit decimals directly to get the NEP-5 assets balance.

Invoking decimals

Request Body:

{
  "jsonrpc": "2.0",
  "method": "invokefunction",
  "params": [
    "0xe9fa06842455ecf020b15d0e9b9c42de24ea3c6d",
    "decimals", 
    []
    ],
  "id": 2
}

After sending the request, you will get the following response:

{
    "jsonrpc": "2.0",
    "id": 3,
    "result": {
        "script": "00c108646563696d616c73146d3cea24de429c9b0e5db120f0ec55248406fae968627d5b52",
        "state": "HALT",
        "gas_consumed": "5673200",
        "stack": [
            {
                "type": "Integer",
                "value": "8"
            }
        ]
    }
}

It returns integer 8.

Invoking symbol

Request Body:

{
  "jsonrpc": "2.0",
  "method": "invokefunction",
  "params": [
    "0xe9fa06842455ecf020b15d0e9b9c42de24ea3c6d",
    "symbol", 
    []
    ],
  "id": 3
}

After sending the request, you will get the following response:

{
    "jsonrpc": "2.0",
    "id": 3,
    "result": {
        "script": "00c10673796d626f6c146d3cea24de429c9b0e5db120f0ec55248406fae968627d5b52",
        "state": "HALT",
        "gas_consumed": "8106560",
        "stack": [
            {
                "type": "ByteString",
                "value": "dDE="
            }
        ]
    }
}

It returns "dDE=" which can be decoded to "t1" with base64.

Calculating the User Balance

According to all the returned values, we can calculate the user balance as follows: The balance = return / 10decimals

Dealing with users' queries

The actual user balance in the exchange is recorded in the exchange database. The exchange needs to write programs to monitor each transaction of each block, record all deposits and withdrawals transactions in the database, and modify the user balance in the database accordingly.

Dealing with User Deposits

To get the user deposits information the exchange needs to do the following:

  1. Get each block details using the getblock API, including details of all the transactions in the block.

  2. Analyze each transaction type and filter out all transactions of the type "InvocationTransaction". Any transaction other than "InvocationTransaction" can not be a transfer transaction of NEP-5 assets.

  3. Invoke the getapplicationlog API to get the details of each "InvocationTransaction" transaction and analyze the transaction content to complete the user deposit.

Invoking getapplicationlog

This API is used to get transaction information.

After correctly installing the ApplicationLogs plug-in and starting the neo-cli node, you can find a folder "ApplicationLogs" is generated under the root path. The complete contract log is recorded in this directory, and each NEP-5 transaction is recorded in a leveldb file.

The following shows an example of the API invoking result.

{
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "txid": "0xd9aaa1243cae91e063a140239807a9de45f82850130ec36403f44770955dd2d7",
        "trigger": "Application",
        "vmstate": "HALT",
        "gasconsumed": "11819770",
        "stack": [],
        "notifications": [
            {
                "contract": "0xd2c270ebfc2a1cdd3e470014a4dff7c091f699ec",
                "eventname": "Transfer",
                "state": {
                    "type": "Array",
                    "value": [
                        {
                            "type": "ByteString",
                            "value": "uXtKzX+CD2HS1NT5rqXrUEmN31U="
                        },
                        {
                            "type": "ByteString",
                            "value": "7ztGBn8vR7L38EQqojcghdCHCO8="
                        },
                        {
                            "type": "Integer",
                            "value": "800000000000"
                        }
                    ]
                }
            }
        ]
    }
}
  • The failed NEP-5 transaction can also be recorded in blockchain, so you need to determine whether the vm status parameter "vmstate" is correct (HALT).

  • "vmstate" indicates the vm status after it executes the contract. If it contains "FAULT", that means the execution is failed and the transaction is invalid.

The parameters related to a transaction in the file are the following:

  • contract : the script hash of smart contract. For exchanges, it is the script hash of NEP17 assets type and the unique identity of the asset. For example, here "0xd2c270ebfc2a1cdd3e470014a4dff7c091f699ec" is the NEP17 asset script hash.

  • eventname : the event identifier of smart contact. Exchanges only need to listen on “transfer” transactions to find out users' transfer transactions.

  • The objects included in the "value" array of "state" are:

    [from account, to account, amount]

    • The first object in the array is the account address where the asset is transferred from. Its type "bytearray" and the value "uXtKzX+CD2HS1NT5rqXrUEmN31U=“ can be decoded to "NcphtjgTye3c3ZL5J5nDZhsf3UJMGAjd7o" with base64.

    • The second object in the array is the account address where the asset is transferred to. Its type "bytearray" and the value "7ztGBn8vR7L38EQqojcghdCHCO8=“ can be decoded to "Nhiuh11SHF4n9FE6G5LuFHHYc7Lgws9U1z" with base64. If the address is an exchange account address, it is a deposit transaction.

    • The last object in the array is the transfer amount, which type can be Integer or ByteString depending on the contract code. The conversion methods of the two types are different.

    In Neo, hexadecimal strings are processed in big-endian order if they are preceded by 0x, or little-endian order if they are not.

    { 
    "type": "ByteString", 
      "value": "uXtKzX+CD2HS1NT5rqXrUEmN31U=" 
    } 
    
    { 
    "type": "ByteString", 
      "value": "7ztGBn8vR7L38EQqojcghdCHCO8=" 
    } 
    

    For the type Integer, the value 800000000000 is actually 8000.00000000 since the decimal is 8 bit.

    For the type ByteString, the value AEC3Q7oA should be base64-decoded to "0040b743ba", and then the little endian string be reversed to "ba43b74000", which value is 8x1011. Since the decimal is 8 bit, the final value is 8000.00000000

    { 
      "type": "Integer", 
      "value": "800000000000" 
    } 
    

    or

    { 
      "type": "ByteString", 
      "value": "AEC3Q7oA" 
    } 
    

Regarding the data format conversion of the transfer in the file, you can refer to Neo3 data conversion .

Dealing with User Withdrawals

The exchange can choose one of the following way to send assets to users:

  • Neo-CLI command: send

  • RPC method: sendfrom

  • RPC method: sendtoaddress

  • RPC method: sendmany

Neo-CLI Command: send

Syntax

send <id|alias> <address> <amount>|all [from=null] [signerAccounts=null]

Parameters
  • id|alias : asset ID or asset abbreviations, e.g. neo, gas

  • address : address to transfer assets to

  • amount|all : transfer amount

  • from : address to transfer assets from

  • signerAccounts : signer's address

This command verifies the wallet password.

Example

Transfer 100 Neo to the address AMwS5twG1LLJA4USMPFf5UugfUvEfNDz6e:

neo> send a1760976db5fcdfab2a9930e8f6ce875b2d18225 AMwS5twG1LLJA4USMPFf5UugfUvEfNDz6e 100
password: ********
TXID: 0x8f831d8de723093316c05749a053a226514bc06338b2bceb50db690610e0b92f

If you are not sure of the asset ID, you can enter list asset to view all assets in the wallet.

In above example, we can also replace the asset ID with asset abbreviation, as shown below:

neo> send gas AMwS5twG1LLJA4USMPFf5UugfUvEfNDz6e 100
password: ********
TXID: 0xae0675797c2d738dcadb21cec3f1809ff453ac291046a05ac679cbd95b79c856

RPC Method: openwallet

Before you can invoke any of the wallet-related RPC methods you must invoke the method openwallet first.

The key "params" includes an array of two parameters.

"params":[path, password]

For example, to open the wallet a.json with a password 111111 , you can construct a JSON file as follows and send it to RPC server.

Request body:

{
  "jsonrpc": "2.0",
  "method": "openwallet",
  "params": ["a.json", "111111"],
  "id": 1
}

After sending the request, you will get the following response:

{
    "jsonrpc": "2.0",
    "id": 1,
    "result": true
}

RPC Method: sendfrom

The key "params" includes an array of four parameters.

"params":[script hash, address from, address to, amount]

For example, to send 10 NEO from NcphtjgTye3c3ZL5J5nDZhsf3UJMGAjd7o to Nhiuh11SHF4n9FE6G5LuFHHYc7Lgws9U1z, construct a JSON file as follows and send it to RPC server.

Request body:

{
  "jsonrpc": "2.0",
  "method": "sendfrom",
  "params": ["0xf61eebf573ea36593fd43aa150c055ad7906ab83","NcphtjgTye3c3ZL5J5nDZhsf3UJMGAjd7o","Nhiuh11SHF4n9FE6G5LuFHHYc7Lgws9U1z", 10],
  "id": 1
}

After sending the request, you will get the following response:

{
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "hash": "0x2dad82755c3b3e3233c10a49402bea9b8bb3f43b079102bbc3c5a50c3b522137",
        "size": 264,
        "version": 0,
        "nonce": 1073258915,
        "sender": "NcphtjgTye3c3ZL5J5nDZhsf3UJMGAjd7o",
        "sysfee": "9007990",
        "netfee": "1264390",
        "validuntilblock": 2107189,
        "attributes": [
            {
                "type": "Cosigner",
                "account": "0x55df8d4950eba5aef9d4d4d2610f827fcd4a7bb9",
                "scopes": "CalledByEntry"
            }
        ],
        "script": "GgwU7ztGBn8vR7L38EQqojcghdCHCO8MFLl7Ss1/gg9h0tTU+a6l61BJjd9VE8AMCHRyYW5zZmVyDBQlBZ7LSHjTqHX5HFHO3tMw1Fdf3kFifVtSOA==",
        "witnesses": [
            {
                "invocation": "DEBL7Fxz2ZyIgtz+kESSs8YjbJd5dcc13gpxOwrLjU+WiIa0fuFQSgHXM75S1Z21wDMvEirUHpU1rIYylfnQH6Ul",
                "verification": "DCECTLb+CYh0tAkrQbRliAmdLaB5NLR0FqIWxgiCPlnz/B4LQZVEDXg="
            }
        ]
    }
}

RPC Method: sendtoaddress

The key "params" includes an array of three parameters.

"params":[script hash, address, amount]

For example, to send 1000 GAS to Nhiuh11SHF4n9FE6G5LuFHHYc7Lgws9U1z , construct a JSON file as follows and send it to RPC server.

Request Body:

{
  "jsonrpc": "2.0",
  "method": "sendtoaddress",
  "params": ["0x70e2301955bf1e74cbb31d18c2f96972abadb328", "Nhiuh11SHF4n9FE6G5LuFHHYc7Lgws9U1z", 1000],
  "id": 1
}

After sending the request, you will get the following response:

{
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "hash": "0xda4de7d6fc3bcd0eba51a3dcba01eaba7d59467acf91525c5f3f0b56df06aec8",
        "size": 272,
        "version": 0,
        "nonce": 1325103139,
        "sender": "NcphtjgTye3c3ZL5J5nDZhsf3UJMGAjd7o",
        "sysfee": "9007990",
        "netfee": "1272390",
        "validuntilblock": 2107253,
        "attributes": [
            {
                "type": "Cosigner",
                "account": "0x55df8d4950eba5aef9d4d4d2610f827fcd4a7bb9",
                "scopes": "CalledByEntry"
            }
        ],
        "script": "AwDodkgXAAAADBTvO0YGfy9HsvfwRCqiNyCF0IcI7wwUuXtKzX+CD2HS1NT5rqXrUEmN31UTwAwIdHJhbnNmZXIMFLyvQdaEx9StbuDZnalwe50fDI5mQWJ9W1I4",
        "witnesses": [
            {
                "invocation": "DEBd+BDi7LWMQ5zzWxmzvH9zsO9fRZpdqn9SqnyEfSzazVnFsUlDJG7ik79epcqpF+IWGQJM1lS1oDeI4Eh/Yq04",
                "verification": "DCECTLb+CYh0tAkrQbRliAmdLaB5NLR0FqIWxgiCPlnz/B4LQZVEDXg="
            }
        ]
    }
}

RPC Method: sendmany

The key "params" includes an array of at least two parameter:

"params":[address from(optional), []]

For example, to send 100 NEO and 1000 GAS to Nhiuh11SHF4n9FE6G5LuFHHYc7Lgws9U1z from NcphtjgTye3c3ZL5J5nDZhsf3UJMGAjd7o, you can construct a JSON file as follows and send it to RPC server.

Request Body:

{
    "jsonrpc": "2.0",
    "method": "sendmany",
    "params": [
    "NcphtjgTye3c3ZL5J5nDZhsf3UJMGAjd7o",
        [
            {
                "asset": "0xf61eebf573ea36593fd43aa150c055ad7906ab83",
                "value": 100,
                "address": "Nhiuh11SHF4n9FE6G5LuFHHYc7Lgws9U1z"
            },
            {
                "asset": "0x70e2301955bf1e74cbb31d18c2f96972abadb328",
                "value": 1000,
                "address": "Nhiuh11SHF4n9FE6G5LuFHHYc7Lgws9U1z"
            }
        ]
    ],
    "id": 1
}

After sending the request, you will get the following response:

{
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "hash": "0xea4564840441713481363ffc0b3e2df95e5319af4d5da4189603c2333d6702f5",
        "size": 358,
        "version": 0,
        "nonce": 93745276,
        "sender": "NcphtjgTye3c3ZL5J5nDZhsf3UJMGAjd7o",
        "sysfee": "18015980",
        "netfee": "1358390",
        "validuntilblock": 2107284,
        "attributes": [
            {
                "type": "Cosigner",
                "account": "0x55df8d4950eba5aef9d4d4d2610f827fcd4a7bb9",
                "scopes": "CalledByEntry"
            }
        ],
        "script": "AGQMFO87RgZ/L0ey9/BEKqI3IIXQhwjvDBS5e0rNf4IPYdLU1PmupetQSY3fVRPADAh0cmFuc2ZlcgwUJQWey0h406h1+RxRzt7TMNRXX95BYn1bUjgDAOh2SBcAAAAMFO87RgZ/L0ey9/BEKqI3IIXQhwjvDBS5e0rNf4IPYdLU1PmupetQSY3fVRPADAh0cmFuc2ZlcgwUvK9B1oTH1K1u4NmdqXB7nR8MjmZBYn1bUjg=",
        "witnesses": [
            {
                "invocation": "DEA1J31Wq9CS6s7Zyzv71jS/LXbJroKgzMhTk176KaCNDBIas5kqBgsv0hHVxetxdwnapXU7Cui/9PlHr3fZNPf3",
                "verification": "DCECTLb+CYh0tAkrQbRliAmdLaB5NLR0FqIWxgiCPlnz/B4LQZVEDXg="
            }
        ]
    }
}

See Also

NEP17 Token Standard

Neo3 Data Conversion