技术附录:Exchange 与 Transfer 协议
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。
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 发给节点。
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 都需由应用准备。它不是供真实资金直接运行的完整脚本。
// 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 的双边交换模型有关联,但并不互相等同。
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 提供可交给用户运行的软件形式;两者之间,才是个人市场应用真正需要实现的工作。