参数编码和解码

本节的主要内容是Tron网络中调用智能合约时传递参数的编码及解码介绍。

在通过HTTP接口triggersmartcontract调用智能合约时,需要传递编码后的parameter,在此我们通过USDT合约为例,提供javascript代码向开发者展示参数如何进行编码。具体有关solidity中参数编解码详细请参考[solidity文档]及[tronweb相关代码]。

参数编码

我们以USDT合约中的转账函数为例:

function transfer(address to, uint256 value) public returns (bool);

假设给地址412ed5dd8a98aea00ae32517742ea5289761b2710e转账50000 USDT,调用的triggersmartcontract接口如下:

curl -X POST \
-d '{
"contract_address":"412dd04f7b26176aa130823bcc67449d1f451eb98f",
"owner_address":"411fafb1e96dfe4f609e2259bfaf8c77b60c535b93",
"function_selector":"transfer(address,uint256)",
"parameter":"0000000000000000000000002ed5dd8a98aea00ae32517742ea5289761b2710e0000000000000000000000000000000000000000000000000000000ba43b7400",
"call_value":0,
"fee_limit":1000000000,
"call_token_value":0,
"token_id":0
}'

上面的命令中,parameter的编码需要根据ABI规则进行,规则比较复杂, 用户可以利用ethers库的进行编码,下面是示例代码:

//建议使用ethers4.0.47版本
var ethers = require('ethers')

const AbiCoder = ethers.utils.AbiCoder;
const ADDRESS_PREFIX_REGEX = /^(41)/;
const ADDRESS_PREFIX = "41";

async function encodeParams(inputs){
    let typesValues = inputs
    let parameters = ''

    if (typesValues.length == 0)
        return parameters
    const abiCoder = new AbiCoder();
    let types = [];
    const values = [];

    for (let i = 0; i < typesValues.length; i++) {
        let {type, value} = typesValues[i];
        if (type == 'address')
            value = value.replace(ADDRESS_PREFIX_REGEX, '0x');
        else if (type == 'address[]')
            value = value.map(v => toHex(v).replace(ADDRESS_PREFIX_REGEX, '0x'));
        types.push(type);
        values.push(value);
    }

    console.log(types, values)
    try {
        parameters = abiCoder.encode(types, values).replace(/^(0x)/, '');
    } catch (ex) {
        console.log(ex);
    }
    return parameters

}

async function main() {
    let inputs = [
        {type: 'address', value: "412ed5dd8a98aea00ae32517742ea5289761b2710e"},
        {type: 'uint256', value: 50000000000}
    ]
    let parameters = await encodeParams(inputs)
    console.log(parameters)

main()

示例代码输出:

0000000000000000000000002ed5dd8a98aea00ae32517742ea5289761b2710e0000000000000000000000000000000000000000000000000000000ba43b7400

#参数解码

HTTP接口

上面参数的编码小节,调用的triggersmartcontract生成交易对象,然后签名广播,交易成功上链后,可以通过gettransactionbyid获取链上的交易信息:

curl -X POST \
  https://api.trongrid.io/wallet/gettransactionbyid \
  -d '{"value" : "1472178f0845f0bfb15957059f3fe9c791e7e039f449c3d5a843aafbc8bbdeeb"}'

返回结果如下:

{
    "ret": [
        {
            "contractRet": "SUCCESS"
        }
    ],
    ..........
    "raw_data": {
        "contract": [
            {
                "parameter": {
                    "value": {
                        "data": "a9059cbb0000000000000000000000002ed5dd8a98aea00ae32517742ea5289761b2710e0000000000000000000000000000000000000000000000000000000ba43b7400",
                        "owner_address": "418a4a39b0e62a091608e9631ffd19427d2d338dbd",
                        "contract_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c"
                    },
                    "type_url": "type.googleapis.com/protocol.TriggerSmartContract"
                },
    ..........
}

RPC接口

同样以交易1472178f0845f0bfb15957059f3fe9c791e7e039f449c3d5a843aafbc8bbdeeb为例,TriggerSmartContract包含在Transaction.rawData.contract[0].parameter中。parameter的类型是com.google.protobuf.Any,需要先对其进行解码,以得到参数。

交易的parameter内容如下:

{
    type_url: "type.googleapis.com/protocol.TriggerSmartContract"
    value: 0a15418a4a39b0e62a091608e9631ffd19427d2d338dbd121541a614f803b6fd780986a42c78ec9c7f77e6ded13c2244a9059cbb0000000000000000000000002ed5dd8a98aea00ae32517742ea5289761b2710e0000000000000000000000000000000000000000000000000000000ba43b7400
}

Any.unpack(TriggerSmartContract.class)对其进行解码,可以得到TriggerSmartContract对象,再获取其data字段,便可获取到与上面HTTP接口相同的结果。

示例:

Any param = transaction.getRawData().getContractList().get(0).getParameter();
TriggerSmartContract contract = param.unpack(TriggerSmartContract.class);
ByteString data = contract.getData();

此时data的将内容即为a9059cbb0000000000000000000000002ed5dd8a98aea00ae32517742ea5289761b2710e0000000000000000000000000000000000000000000000000000000ba43b7400

解码

上面返回值中的的raw_data.contract[0].parameter.value.data字段就是调用transfer(address to, uint256 value) 函数的参数,但是data字段和参数的编码小节描述的triggersmartcontract的parameter字段并不一样,前面还多了a9059cbb这4个字节,这4个字节是方法ID,这源自ASCII格式的 transfer(address,uint256) 签名的 Keccak 哈希的前 4 字节,用于虚拟机对函数的寻址。

下面代码是对data字段进行解码,获取出transfer函数传递的参数:

var ethers = require('ethers')

const AbiCoder = ethers.utils.AbiCoder;
const ADDRESS_PREFIX_REGEX = /^(41)/;
const ADDRESS_PREFIX = "41";

//types:参数类型列表,如果函数有多个返回值,列表中类型的顺序应该符合定义的顺序
//output: 解码前的数据
//ignoreMethodHash:对函数返回值解码,ignoreMethodHash填写false,如果是对gettransactionbyid结果中的data字段解码时,ignoreMethodHash填写true

async function decodeParams(types, output, ignoreMethodHash) {

    if (!output || typeof output === 'boolean') {
        ignoreMethodHash = output;
        output = types;
    }

    if (ignoreMethodHash && output.replace(/^0x/, '').length % 64 === 8)
        output = '0x' + output.replace(/^0x/, '').substring(8);

    const abiCoder = new AbiCoder();

    if (output.replace(/^0x/, '').length % 64)
        throw new Error('The encoded string is not valid. Its length must be a multiple of 64.');
    return abiCoder.decode(types, output).reduce((obj, arg, index) => {
        if (types[index] == 'address')
            arg = ADDRESS_PREFIX + arg.substr(2).toLowerCase();
        obj.push(arg);
        return obj;
    }, []);
}


async function main() {

    let data = '0xa9059cbb0000000000000000000000004f53238d40e1a3cb8752a2be81f053e266d9ecab000000000000000000000000000000000000000000000000000000024dba7580'

    result = await decodeParams(['address', 'uint256'], data, true)
    console.log(result)
}

示例代码输出:

[ '414f53238d40e1a3cb8752a2be81f053e266d9ecab', BigNumber { _hex: '0x024dba7580' } ]

注意decodeParams函数的第三个参数ignoreMethodHash必须设置为true,目的是让decodeParams函数跳过前4字节,对后面的内容进行解码。

返回值参数解码

我们以USDT合约中的查询函数为例:

balanceOf(address who) public constant returns (uint)

假设查询410583A68A3BCD86C25AB1BEE482BAC04A216B0261的余额,调用的triggersmartcontract接口如下:

curl -X POST \
  https://api.trongrid.io/wallet/triggersmartcontract \
  -d '{
    "contract_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c",
    "owner_address": "411fafb1e96dfe4f609e2259bfaf8c77b60c535b93",
    "function_selector": "balanceOf(address)",
    "parameter": "0000000000000000000000000583a68a3bcd86c25ab1bee482bac04a216b0261",
    "call_value": 0,
    "fee_limit": 1000000000,
    "call_token_value": 0,
    "token_id": 0
}'

返回结果如下:

{
    "result": {
        "result": true
    },
    "constant_result": [
        "000000000000000000000000000000000000000000000000000196ca228159aa"
    ],
   ............

上面的返回值中constant_result就是balanceOf的返回值,下面是对constant_result解码的示例代码:

async function main() {
  //必须是0x开头
    let outputs = '0x000000000000000000000000000000000000000000000000000196ca228159aa'
    //
    //['uint256']是返回值类型的列表,如果有多个返回值,在按照顺序填写类型
    result = await decodeParams(['uint256'], outputs, false)
    console.log(result)
}

示例代码输出:

[ BigNumber { _hex: '0x0196ca228159aa' } ]