2.3 理解ABI文件

优质
小牛编辑
125浏览
2023-12-01

介绍

你先前使用ABI文件部署了eosio.token合约.这一教程会概述ABI文件是如何与eosio.token合约相关联的.

ABI文件能通过eosio.cdt提供的eosio-cpp工具生成.但是,有几种情况会导致生成的ABI失灵或生成失败.C++的高级设计模式会使其出差错以及自定义的类型有时也会引起ABI生成的问题.因此,了解ABI文件是如何工作对你来说很重要,这样你就在需要的时候去debug和修复了~

什么是ABI?

Application Binary Interface(ABI)是一个基于JSON描述,介绍如何将用户行为在JSON和二进制表示之间进行操作.ABI还描述了如何 to/from JSON转换数据库状态.一旦你通过ABI描述了你的合约,开发者和用户就有能力通过JSON与你的合约进行无缝交互.

安全说明

执行交易可以绕过ABI. 传递给合同的消息和action不需要遵循 ABI. ABI是一个指南,而不是看门人.

创建一个ABI文件

让我们以一个空的ABI开始,将其命名为eosio.token.abi

{
   "version": "eosio::abi/1.0",
   "types": [],
   "structs": [],
   "actions": [],
   "tables": [],
   "ricardian_clauses": [],
   "abi_extensions": [],
   "___comment" : ""
}

类型

ABI允许任何客户端或接口解释你的合约甚至为其创建GUI.为了使其以一致的方式工作,请将作为在任何public action或struct中使用的任何自定义类型的描述放到ABI.

内建类型

EOSIO实现了许多自定义内置函数. 内建类型不需要在ABI文件中进行描述. 如果你想对EOSIO的内建函数更熟悉, 他们被定义在 here

eosio.token为例,需要定义在ABI文件中的唯一类型是account_name.ABI使用"new_type_name"来描述特定类型,在该情况下就是account_name,以及account_namename类型的别称.

下面是描述该类型的JSON:

{
   "new_type_name": "account_name",
   "type": "name"
}

ABI文件现在看起来是这样的:

{
   "version": "eosio::abi/1.0",
   "types": [{
     "new_type_name": "account_name",
     "type": "name"
     }],
   "structs": [],
   "actions": [],
   "tables": [],
   "ricardian_clauses": [],
   "abi_extensions": []
}

Structs

暴露给ABI的Structs也需要被描述.通过查看eosio.token.hpp,能通过public actions快速确定用了哪些结构来作为工具.这对下一步来说非常重要.

一个定义在JSON中的struct的对象就像下面这样:

{
   "name": "issue", //The name 
   "base": "",             //Inheritance, parent struct
   "fields": []            //Array of field objects describing the struct's fields. 
}

Fields

{
   "name":"", // The field's name
   "type":""   // The field's type
}

eosio.token合约中,有几种struct需要定义.请注意,不是所有这些结构都被明确定义,一些对应于action的参数.以下是需要对eosio.token合约进行ABI描述的struct列表:

Implicit Structs

下面的struct是隐式的,因为struct从未在合约中明确定义.查看 create action, 你能找到两个参数,issueraccount_name类型的,maximum_supplyasset类型的.为了简洁,这个教程不会分解每个struct,但应用相同的逻辑,你可以得到以下结果:

create

{
  "name": "create",
  "base": "",
  "fields": [
    {
      "name":"issuer", 
      "type":"account_name"
    },
    {
      "name":"maximum_supply", 
      "type":"asset"
    }
  ]
}

issue

{
  "name": "issue",
  "base": "",
  "fields": [
    {
      "name":"to", 
      "type":"account_name"
    },
    {
      "name":"quantity", 
      "type":"asset"
    },
    {
      "name":"memo", 
      "type":"string"
    }
  ]
}

retire

{
  "name": "retire",
  "base": "",
  "fields": [
    {
      "name":"quantity", 
      "type":"asset"
    },
    {
      "name":"memo", 
      "type":"string"
    }
  ]
}

transfer

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

close

{
  "name": "close",
  "base": "",
  "fields": [
    {
      "name":"owner", 
      "type":"account_name"
    },
    {
      "name":"symbol", 
      "type":"symbol"
    }
  ]
 }

Explicit Structs

这种struct是显示定义的,因为他们是实例化多索引表的必要条件.对它们的描述和上方演示的隐式structs没有区别.

account

{
  "name": "account",
  "base": "",
  "fields": [
    {
      "name":"balance", 
      "type":"asset"
    }
  ]
}

currency_stats

{
  "name": "currency_stats",
  "base": "",
  "fields": [
    {
      "name":"supply", 
      "type":"asset"
    },
    {
      "name":"max_supply", 
      "type":"asset"
    },
    {
      "name":"issuer", 
      "type":"account_name"
    }
  ]
}

Action

action的JSON对象定义如下所示:

{
  "name": "transfer",             //The name of the action as defined in the contract
  "type": "transfer",             //The name of the implicit struct as described in the ABI
  "ricardian_contract": ""     //与此action相关的的可选ricardian子句,用于描述其预期的功能
}

通过聚集所有eosio.token合约 header file 的所有public函数描述,来描述eosio.token合约的actions.

根据它先前定义好的struct然后描述每个action的类型.在大多数情况下,函数名称和struct名称是相同的,但是没有要求他们是需要相同.

下面是链接到其源代码的actions列表,其中提示了JSON示例,以了解每个action是如何描述的.

create

{
  "name": "create",
  "type": "create",
  "ricardian_contract": ""
}

retire

{
  "name": "retire",
  "type": "retire",
  "ricardian_contract": ""
}

transfer

{
  "name": "transfer",
  "type": "transfer",
  "ricardian_contract": ""
}

close

{
  "name": "close",
  "type": "close",
  "ricardian_contract": ""
}

Tables

描述tables.这是table的JSON对象定义:

{
  "name": "",       //table的名字,在实例化的时候确定
  "type": "",             // table'对应的struct
  "index_type": "", //该表的主键类型
  "key_names" : [], //key names数组, 长度必须等同于 key_types 的成员数量
  "key_types" : []  //对应于key names数组成员的key types数组,长度必须等同于 key names数组
}

eosio.token合约会实例化两张表,accountsstat.

accounts是一个基于 account struct 的i64索引表,有一个 uint64 类型的主键 并且它的key被强行命名为"currency".

这是accounts表的ABI描述:

{
  "name": "accounts",
  "type": "account", // Corresponds to previously defined struct
  "index_type": "i64",
  "key_names" : ["currency"],
  "key_types" : ["uint64"]
}

stat是基于 currency_stats struct 的i64索引表,它有一个 uint64类型的主键 以及这个主键被强行命名为"currency".

这是stat表的ABI描述:

{
  "name": "stat",
  "type": "currency_stats",
  "index_type": "i64",
  "key_names" : ["currency"],
  "key_types" : ["uint64"]
}

你可能发现上面两张表有相同的"key name".为键命名相似的名称是象征性的,因为它可能暗示主观关系.与该实现一样,暗示任何给定值都可用于查询不同的表.

把它们放到一起

终于,ABI文件可以精确地描述eosio.token合约了.

{
  "version": "eosio::abi/1.0",
  "types": [
    {
      "new_type_name": "account_name",
      "type": "name"
    }
  ],
  "structs": [
    {
      "name": "create",
      "base": "",
      "fields": [
        {
          "name":"issuer", 
          "type":"account_name"
        },
        {
          "name":"maximum_supply", 
          "type":"asset"
        }
      ]
    },
    {
       "name": "issue",
       "base": "",
       "fields": [
          {
            "name":"to", 
            "type":"account_name"
          },
          {
            "name":"quantity", 
            "type":"asset"
          },
          {
            "name":"memo", 
            "type":"string"
          }
       ]
    },
    {
       "name": "retire",
       "base": "",
       "fields": [
          {
            "name":"quantity", 
            "type":"asset"
          },
          {
            "name":"memo", 
            "type":"string"
          }
       ]
    },
    {
       "name": "close",
       "base": "",
       "fields": [
          {
            "name":"owner", 
            "type":"account_name"
          },
          {
            "name":"symbol", 
            "type":"symbol"
          }
       ]
    },
    {
      "name": "transfer",
      "base": "",
      "fields": [
        {
          "name":"from", 
          "type":"account_name"
        },
        {
          "name":"to", 
          "type":"account_name"
        },
        {
          "name":"quantity", 
          "type":"asset"
        },
        {
          "name":"memo", 
          "type":"string"
        }
      ]
    },
    {
      "name": "account",
      "base": "",
      "fields": [
        {
          "name":"balance", 
          "type":"asset"
        }
      ]
    },
    {
      "name": "currency_stats",
      "base": "",
      "fields": [
        {
          "name":"supply", 
          "type":"asset"
        },
        {
          "name":"max_supply", 
          "type":"asset"
        },
        {
          "name":"issuer", 
          "type":"account_name"
        }
      ]
    }
  ],
  "actions": [
    {
      "name": "transfer",
      "type": "transfer",
      "ricardian_contract": ""
    },
    {
      "name": "issue",
      "type": "issue",
      "ricardian_contract": ""
    },
    {
      "name": "retire",
      "type": "retire",
      "ricardian_contract": ""
    },
    {
      "name": "create",
      "type": "create",
      "ricardian_contract": ""
    },
    {
      "name": "close",
      "type": "close",
      "ricardian_contract": ""
    }
  ],
  "tables": [
    {
      "name": "accounts",
      "type": "account",
      "index_type": "i64",
      "key_names" : ["currency"],
      "key_types" : ["uint64"]
    },
    {
      "name": "stat",
      "type": "currency_stats",
      "index_type": "i64",
      "key_names" : ["currency"],
      "key_types" : ["uint64"]
    }
  ],
  "ricardian_clauses": [],
  "abi_extensions": []
}

Token合约没包含的情况

Vectors

当在你的ABI文件中描述一个vetor时,简单的用[]来添加该类型,所以如果你需要描述一个权限级别的vector,你可以这样描述:permission_level[].

Struct Base

这是一个很少会用到的不值得一提的参数.你能使用base ABI struct property来引用另一个struct以继承,只要那个struct定义在同一个ABI文件中.Base不会做任何事情或者可能会因为你的智能合约逻辑不支持继承而抛出错误的潜在风险.

你可以在系统合约的source codeABI 中查看struct base的例子.

此处未涵盖的额外ABI属性

为了简洁,教程中省略了一些ABI属性,但是,有一个待定的ABI规范将完整地概述ABI的每个属性.

Ricardian Clauses

ricardian clauses 描述了特定action的预期结果.它也可用于在发送人和合约之间简历条款.

ABI Extensions

一个通用的"future proofing"的layer,允许旧的客户端跳过解析扩展数据的"chunk".目前来说,这个属性还没有使用.未来,在vector中的每一个扩展都会有它自己的"chunk",以让旧的客户端可以跳过它,以及更新的客户端能理解如何解释它(and newer clients that understand how to interpret it).

维护

你每次更改struct,添加table,添加action或为一个action添加parameters,使用一个新的类型,你都需要记住更新ABI文件.在许多情况下,更新ABI文件失败不会产生任何错误.

Trobleshooting

Table不返回任何rows

检查你的表是否精确地描述在文件中.例如,如果你使用cleos去添加一个table到一个合约中但是使用错误的定义然后从table中查询rows,你会拿到空的结果.当一个合约不能正确的在它的File中描述这些tables时,cleos不会在添加一个row或从row读取内容时生成任何错误.