跳到主要内容
专题 · 如何在 ArcBlock 上构建去中心化市场?

技术附录:Exchange 与 Transfer 协议

Robert
ARCBlockchainDIDArchitecture

ArcBlock 把交换定义为一种链原生交易,而不是要求每个应用先部署自己的交换合约。对开发者,这意味着可以直接描述双方的资产包,再用协议约定的签名与验证流程交割。市场怎样发现双方、怎样报价,仍由应用决定。

本篇直接展示公开文档中的结构与 API。协议字段、SDK 参数和我们建议的应用工作流是三个层次;下面会分别说明。完整参考可从 Transaction Types、Understanding Transactions 和 SDK 交易 API继续阅读。

从链 1.0 的 ExchangeTx 到 ExchangeV2,交换始终是原生交易类型。2019 年的 DApps Workshop记录了早期 ExchangeTx;今天公开的 SDK 数据类型文档仍保留这个结构。早期每方的 ExchangeInfo 包括原生数量 value 和独立资产列表 assets,V2 的 ExchangeInfoV2 则增加 tokens,用于表达其他 token 的标识和数量。V2 是资产表达能力的演进,不是从这一代才开始支持交换。

交换的是两个资产包

公开协议中的核心结构如下。TokenInput 指定一种 token 的标识与数量;ExchangeInfoV2 描述一方交付的原生 token、独立资产和其他 token。

protobuf
message TokenInput {
  string address = 1;
  string value = 2;
}

message ExchangeInfoV2 {
  BigUint value = 1;
  repeated string assets = 2;
  repeated TokenInput tokens = 3;
}

message ExchangeV2Tx {
  string to = 1;
  ExchangeInfoV2 sender = 2;
  ExchangeInfoV2 receiver = 3;
  google.protobuf.Timestamp expired_at = 4;
  google.protobuf.Any data = 15;
}

ExchangeV2Tx 定义采用的并非“卖出一种 token,买入另一种”的固定页面模型。sender 与 receiver 各自是一个资产包,to 是交易对手。协议明确考虑了 token 换资产、token 加资产换资产、以及资产换资产等场景。

例如,Alice 可以提出用两项可转让资产换 Bob 的一定数量 token。钱包应把两边的完整资产包呈现给各自用户,而不是只显示一个抽象总价。是否能成交仍取决于余额、所有权、资产转让条件、token 访问规则和签名是否满足要求。原子性保证的是被允许的交换整体执行,不会让一个受限资产自动变成可转让资产。

把常见交换语义放在交易协议中,可以让应用共享验证规则。通用合约则把更多定制能力留给应用作者。两种方式只是把复杂性放在不同层:原生原语不自动拥有动态竞价和任意费用分配,通用合约也不必为每笔交易重新部署。

从交易内容到双方签名

交换结构放在交易的 itx 中。外层携带发送者、链标识、nonce 和签名等信息。以下为公开文档使用的类型表示;实际发送需要由 SDK 编码为交易数据,不是把这段 TypeScript 发给节点。

typescript
type Transaction = {
  from: string;
  pk: string;
  nonce: number;
  chainId: string;
  delegator: string;
  signature: string;
  signatures: Array<Multisig>;
  itx: Any;
};

signature 与 signatures 让我们能区分主签名和其他参与方的授权;itx 携带具体交易类型与内容。签名流程必须使用 SDK 对该类型定义的编码和签名范围,不能把签任意 JSON 文本当成协议签名。交易结构与签名

SDK 的高级 API 把已知对手的交换分为三个动作。下例按公开文档改写,只展示调用关系;assetAddress、钱包和已连接的 client 都需由应用准备。它不是供真实资金直接运行的完整脚本。

javascript
// Alice's environment: prepare the agreed offer for Bob.
const offerTx = await client.prepareExchange({
  receiver: bobAddress,
  offerAssets: [assetAddress],
  demandToken: 10,
  wallet: aliceWallet,
});

// Bob's environment: inspect the decoded terms before signing.
const finalTx = await client.finalizeExchange({
  tx: offerTx,
  wallet: bobWallet,
});

// Submit after both parties have authorized the transaction.
const txHash = await client.exchange({
  tx: finalTx,
  wallet: aliceWallet,
});

三个调用可以发生在不同参与者的环境里,offerTx 和 finalTx 通过通信传递。私钥不需要随消息传递。例子中的数量用于解释 API;生产应用应按对应 SDK 的单位约定处理数量,检查小数精度,并把签名前后的交易内容一致性作为安全条件。prepareExchange、finalizeExchange 与 exchange

这已经给“谈好以后同时交割”提供了有用的起点。它还不是一个匿名公开订单协议:prepareExchange 的公开参数要求 receiver。开放意图可以先在链外传播,等对手确定后再生成定向交易;不能假定删除接收者就得到任何人都能成交的挂单。

TransferV3 表达另一类组合

TransferV2 描述向一个接收方转移资产;TransferV3 则用多个输入与输出描述资产流向。它与 Exchange 的双边交换模型有关联,但并不互相等同。

protobuf
message TransactionInput {
  string owner = 1;
  repeated TokenInput tokens = 2;
  repeated string assets = 3;
}

message TransferV3Tx {
  repeated TransactionInput inputs = 1;
  repeated TransactionInput outputs = 2;
  google.protobuf.Any data = 15;
}

TransferV3Tx的输入、输出都列出 owner 和资产。它适合研究多个付款来源或固定分配:谁提供哪些 token,最后分别交给谁。应用仍需满足资产守恒与相应授权,不能凭一个 outputs 数组创造余额。

类型定义不是全部执行规则。当前协议还有输入、输出 owner 的约束,不能把同一 owner 同时出现在两边的任意 barter 图直接等同于一笔有效 TransferV3。普通输入授权之外,具有发行者权限的特定 token 也有自己的规则。应用要根据资产类型验证接受路径,而不是只检查 JSON 能否编码。

因此,明确的双方交换优先从 Exchange 模型理解,多来源支付或分配再考察 TransferV3。复杂多方交换、动态费用和部分成交需要分别设计,不应借“多入多出”四个字省去这些问题。

报价生命周期仍是应用协议的一部分

expired_at 出现在 Exchange 的结构中,但字段存在不等于所有部署版本都已强制执行该条件。生产系统必须验证目标链的到期、重放和撤销行为,不能仅在界面上把报价变灰,就称资金授权已经失效。若结算层不能强制所需条件,就要改变授权流程或补齐机制后再支持该产品行为。

对象谁解释它不能混淆的地方
公开意图发布与发现服务愿望不等于可动用资产的授权
双方报价与会话参与者的应用、代理及校验器同意聊天文字不等于完成协议签名
已签交易钱包与链的交易验证停止发布不一定撤销既有签名
成交结果链上确认与应用对账收到交易标识不等于确认成功

撤销、过期、部分成交、重新报价与并发占用需要一起考虑。一项资产不能因为代理同时谈成两笔交易,就被交付两次;一笔提交超时也不能被默认为失败。应用应保存会话与交易的关联,恢复时查询实际结果。

ArcBlock 的特色因此很具体:交换与一些组合转移已经有共同的协议语言,DID 和钱包让身份与授权可以衔接。开发者可以把精力放到这些原语之前的市场组织,以及之后的确认与恢复。原语提供结算基础,Blocklet 提供可交给用户运行的软件形式;两者之间,才是个人市场应用真正需要实现的工作。