EOS合约开发 - 发布并调用无ABI合约

EOS合约开发 - 发布并调用无ABI合约

如果你进行过合约开发,那么你肯定至少听说过ABI。ABI的全称是Application Binary Interface,主要作用是将数据从JSON或其它人类可读格式转换成纯二进制。以太坊中,ABI并不存在于链上,而是通过链下的平台进行分发。譬如Etherscan便会在源码经过验证的合约下提供完整的ABI。而在EOS中,考虑到其重要性,ABI则被直接部署在区块链上。

为什么ABI这么重要呢?在传统的网络服务开发中,我们似乎并不需要这么一个东西。大部分网络请求都是直接通过JSON格式以文本发送。但你也许遇到过接口成为性能瓶颈的情况,这时候,你会将JSON格式的参数先在客户端转成二进制格式再发送给服务器。区块链恰恰是这么一个追求极致性能的平台,而ABI则是帮助前端软件将合约参数转为二进制的信息。

例如,以下是系统合约eosio.token的部分ABI:

{
    "structs": [
        {
            "name": "transfer",
            "base": "",
            "fields": [
                {
                    "name": "from",
                    "type": "account_name"
                },
                {
                    "name": "to",
                    "type": "account_name"
                },
                {
                    "name": "quantity",
                    "type": "asset"
                },
                {
                    "name": "memo",
                    "type": "string"
                }
            ]
        }
    ],
    "actions": [
        {
            "name": "transfer",
            "type": "transfer",
        }
    ]
}

该ABI中指明了接口transfer所使用的参数类型是transfer,而类型transfer又包含了4个字段:fromtoquantitymemo 。这样,在用户调用这个接口时,有了这些信息,前端软件便可以先把参数序列化成二进制数据,再向区块链节点进行提交。

事实上,如果你使用eosjs库进行合约操作,并在浏览器中监听网络请求,你会发现浏览器是先调用了get_abi接口,再发送push_transaction对合约进行操作的。

链上ABI在带来便利的同时,也带来了一些麻烦。对于私有合约,我们只希望把编译后的代码部署到区块链上,而不希望部署ABI。这是由于ABI会暴露合约的一些包括接口和表结构等信息。虽然本质上这些信息可以通过反编译技术获得而无需使用ABI,但提供ABI无疑大大降低了破译合约的门槛。


代码部署

默认情况下,直接使用cleos set contract指令会将ABI和代码一同发布,而这个行为无法被配置更改。这一点可以从cleos的源码中看出:

contractSubcommand->set_callback([&] {
    shouldSend = false;
    set_code_callback();
    set_abi_callback();
    std::cerr << localized("Publishing contract...") << std::endl;
    send_actions(std::move(actions), 10000, packed_transaction::zlib);
});

因此,要想实现仅部署代码,我们需要使用官方的JS库eosjs

至于该库的使用,包括如何获取Eoseos对象并进行相关配置等不在本文的涵盖范围内。请自行参考官方GitHub文档。接下来我会默认你已经成功创建了eos对象。

首先读取源码和ABI文件:

const fs = require("fs");
const wasm = fs.readFileSync("/path/to/wasm/file");
const abi = JSON.parse(fs.readFileSync("/path/to/abi/file"));

部署合约时,只调用如下代码即可:

eos.setcode("your_account_name", 0, 0, wasm);

成功运行后,可以看到只有代码被部署到区块链上了。


合约调用

现在出现了一个问题。照常使用eos.transaction调用该合约时会报错,提示没有相关的接口。这是由于前端无法从区块链上加载ABI文件。根据官方文档,我们可以通过这行语句实现本地加载ABI:

eos.fc.abiCache.abi("your_account_name", abi);

然而,你会发现,即便使用该命令缓存了ABI,调用合约时也还是会报一样的错误!这是由于该eos对象设定了httpEndPoint属性,每次调用合约前,都会先从区块链查询ABI并覆写本地的缓存,这使得我们的缓存直接失效了。

解决方法是新建一个不联网的eos对象coldEos。但这又有另一个问题:EOS中的交易都有一个header,而这个header中的信息需要联网才能获取。

于是,完整的解决方法应该是:使用现有的联网的eos对象获取header,将该header传递给不联网的coldEos,在coldEos中缓存并生成签名交易,最后传递给eos进行区块链广播。

首先定义一个函数用于获取交易header:

async function getHeader() {

    const expireInSeconds = 60 * 60 // 1 hour

    const info = await eos.getInfo({})
    const chainDate = new Date(info.head_block_time + 'Z')
    let expiration = new Date(chainDate.getTime() + expireInSeconds * 1000)
    expiration = expiration.toISOString().split('.')[0]

    const block = await eos.getBlock(info.last_irreversible_block_num)

    const transactionHeaders = {
        expiration,
        ref_block_num: info.last_irreversible_block_num & 0xFFFF,
        ref_block_prefix: block.ref_block_prefix
    }

    return transactionHeaders;
}

然后根据这个交易header创建coldEos对象并进行签名。这里,我们调用了合约内名为myaction的接口,并传递了一个字符串作为参数:

const txHeader = await getHeader();
const coldEos = Eos({
    httpEndpoint: null,
    chainId: "your_chain_id",
    keyProvider: "your_private_key",
    transactionHeaders: txHeader 
});

coldEos.fc.abiCache.abi("your_account_name", abi);

const myContract = await coldEos.contract("your_account_name");
const myTx = await myContract.myaction(
    "Hello",
    {
        authorization: "your_account_name@active"
    }
);

最后使用eos对已签名的交易进行广播:

await eos.pushTransaction(myTx.transaction);

尽管整个过程十分繁琐,但我们最终还是实现了无ABI合约部署和调用。接下来的文章里我们会继续进行EOS合约开发的探讨,欢迎关注本专栏,也欢迎往本专栏投稿技术文章,谢谢。

另外,本文已同步发表于:

Medium:

Medium - Jonathan LEImedium.com

文章被以下专栏收录