去中心化交易所解决方案实例
简介
以无集中权限的去中心化方式交易数字商品,这是区块链技术的关键承诺之一。数字资产交易所和区块链以不同方式实现了这一承诺。对于高频率交易,很多工作在第 2 层应用执行,该层的交易最终在区块链上完成。对于低频率交易,大部分工作直接在链上执行。在 Algorand,数字资产交易所能以多种方式实现。此解决方案采用其中一种,将在区块链上存储和执行所有订单。本文旨在提供更具指导性的说明,帮助开发人员了解 Algorand 不同技术部分的关键概念,因此在操作方面相当简单。
在此解决方案中,我们将介绍如何连接买家和卖家,以促进在 Algorand 区块链上以去中心化方式交易资产。我们将使用限价订单合约作为交易方式,其中由买家开立订单,并指明要购买的资产和相应的兑换率。该订单随后保留在区块链上,可供相关资产的任何卖家后续执行订单。此解决方案还使买家能够随时关闭订单。
此应用涉及使用多种 Algorand 技术,包括无状态智能合约、有状态智能合约、原子交易、标准资产、资产交易和 Algorand 索引器。我们将从总体应用设计开始,然后深入探讨如何构建应用的详细信息。
设计概述
要实施此解决方案,需要四项基本操作。用户应能够开立、关闭或执行订单,并可列出所有已开立订单。
注意
此示例使用硬编码地址,供用户开立和执行订单。此示例还采用与 Algorand TestNet 和 Indexer 的硬编码连接。这是为了简化此示例,但应用最好使用 AlgoSigner 管理账户,以及与应用部署者所用节点或某一可用 API 服务的连接。
1 - 开立订单
此解决方案可供买家创建限价订单,以在其中指明想要购买的 Algorand 资产。该订单还应包含买家愿意花费的最低和最高 microAlgo 数额和兑换率。此解决方案中使用简单的“N”和“D”符号来表示此兑换率。其中“N”表示资产数量,而“D”表示买家愿意花费的 micoAlgo 数额。输入后,用户便可下单。
该订单将转换为无状态合约,以用于委托授权。无状态合约也可用作托管或委托。对于托管,资金将转入某一账户,并由逻辑确定何时转出。对于委托,逻辑经用户签署,并可在之后用于从签署人账户取出资金。本例中,这两种方法均可使用。此解决方案实施了委托,其中逻辑经列出订单的用户签署。这称为创建逻辑签名。此逻辑签名随后保存到文件中,并推送至服务器以供日后使用。已签署的逻辑将在用户关闭订单或订单执行时删除。
作为此解决方案的一部分,其中的主有状态智能合约包含开立、关闭和执行订单的方法。无状态智能合约委托逻辑链接到此有状态智能合约。这是为了在不与有状态智能合约应用调用共用的情况下,使无状态委托逻辑无效。
用户开立订单时,将调用有状态智能合约以开立该订单。有状态智能合约在用户的本地存储中存储订单号。这就要求已开立订单的数量不得超过 16。其实可使用不同的订单号生成器来扩展这一范围,但为了简化和可读性而采用此限制。
2 - 查看已开立订单
下单后,此解决方案提供一个列表框和一个刷新订单按钮。点击后,Web 应用调用 Algorand Indexer 来搜索已选择加入有状态智能合约的所有账户。这些账户经遍历,其本地存储值(已开立订单)读回并填充入列表框。
3 - 执行已开立订单
已开立订单列出后,其他用户可登录 Web 应用,来选择并执行已开立订单。执行用户可指明准备出售多少资产,以及要求多少 microAlgo。如果所指定的数额高于初始限价订单的最高额或低于初始最低额,则执行失败。如果所指定的兑换率低于初始限价订单的规定,则执行也将失败。当执行用户按下执行订单按钮后,Web 应用将生成三项交易。一是调用有状态智能合约,以指明用户在执行特定订单。二是列出限价订单的用户对执行用户的付款交易,采用指定的 microAlgo 数额。三是从执行用户向列出限价订单的用户转移指定的资产数额。第一和第三项交易经执行用户签署。第二项交易(付款)经无状态智能合约逻辑签署,也就是列单用户之前在开立订单时所签署的逻辑。此逻辑签名读取自上传到服务器的文件。这三项交易以原子方式分组,并推送至服务器。对于多项原子交易,只要其中任何一项失败,则全部失败。这样,双方都可获得期望的结果。
第一项交易中的有状态智能合约将从列单用户的本地状态清除订单,而已签署的逻辑文件将从服务器删除。
4 - 关闭订单
任何具有已开立订单的用户均可从订单列表选择此已开立订单,然后点击关闭订单按钮。这将直接为用户从有状态智能合约的本地状态移除已开立订单,并从服务器删除已签署的逻辑文件。
信息
此应用使用 JavaScript SDK 实现对 Algorand 节点和 Indexer 的所有调用。
开立订单 - 第 1 步
第一步是用户开立订单,并在其中指明限价订单的一组标准。
所需数据包括用户想要购买的资产 ID、愿意花费的最低和最高 microAlgo 数额,以及 N
和 D
值。这两个值共同构成兑换率 N/D
。也就是说,对于每一份数量为 N
的资产,用户愿意花费数额为 D
的 microAlgo。按“下单”按钮后,Web 应用代码会使用以下基本 TEAL 模板字符串生成一个委托无状态智能合约。
信息
必须先选择加入资产才能进行交易。该应用不会处理此操作。
此合约代码将执行以下检查。
- 确认同时提交了三项交易。
- 第一项交易必须是调用有状态合约。
- 第二项必须是付款交易。
- 第三项必须是资产转移。
- 有状态应用调用必须是对 dex 有状态合约的 NoOp 调用。
- 这些交易均不得为 Rekey 操作。
- 付款交易的数额应介于所指定的最低值与最高值之间。
- 资产转移应与 GUI 中的指定相符。
- 确认兑换率正确。
let delegateTemplate = `#pragma version 2
// this delegate is
// only used on an execute order
global GroupSize
int 3
==
// The first transaction must be
// an ApplicationCall (ie call stateful smart contract)
gtxn 0 TypeEnum
int appl
==
&&
// The second transaction must be
// an payment tx
gtxn 1 TypeEnum
int pay
==
&&
// The third transaction must be
// an asset xfer tx
gtxn 2 TypeEnum
int axfer
==
&&
// The specific App ID must be called
// This should be changed after creation
// This links this contract to the stateful contract
gtxn 0 ApplicationID
int 12867764 //stateful contract app id
==
&&
// The application call must be
// A general application call
gtxn 0 OnCompletion
int NoOp
==
&&
// verify no transaction
// contains a rekey
gtxn 0 RekeyTo
global ZeroAddress
==
&&
gtxn 1 RekeyTo
global ZeroAddress
==
&&
gtxn 2 RekeyTo
global ZeroAddress
==
&&
bz fail
// min algos spent
gtxn 1 Amount
int <min>
>=
// max algos spent
gtxn 1 Amount
int <max>
<=
&&
// asset id to trade for
int <assetid>
gtxn 2 XferAsset
==
&&
bz fail
// handle the rate
// gtxn[2].AssetAmount * D >= gtxn[1].Amount * N
// N units of the asset per D microAlgos
gtxn 2 AssetAmount
int <D> // put D value here
mulw // AssetAmount * D => (high 64 bits, low 64 bits)
store 2 // move aside low 64 bits
store 1 // move aside high 64 bits
gtxn 1 Amount
int <N> // put N value here
mulw
store 4 // move aside low 64 bits
store 3 // move aside high 64 bits
// compare high bits to high bits
load 1
load 3
>
bnz done
load 1
load 3
==
load 2
load 4
>=
&& // high bits are equal and low bits are ok
bnz done
err
done:
int 1
return
fail:
int 0
return
`;
Web 应用中输入的值已代入合约中。此代码随后经编译。订单号基于用户提供的输入值而生成。此编译逻辑随后由下单用户签署。此经签署的逻辑(逻辑签名)将上传至服务器,以供日后使用。然后,Web 应用调用有状态智能合约应用以开立订单,并提供用户本地状态中存储的订单号。有状态智能合约代码中用于开立订单的部分如下所示。
信息
此示例不使用委托,而是可使用相同逻辑创建托管,但此时需向其转移最高数额资金。
此逻辑执行以下检查。
- 确认组中只有一项交易。
- 订单号应为合约调用的第二个自变量。
- 查看本地状态中是否已有相同订单。如果有则失败。
- 在本地状态中存储订单号。
open:
// only works for app call
global GroupSize
int 1
==
bz fail
int 0 //sender
txn ApplicationID //current smart contract
// 2nd arg is order number
txna ApplicationArgs 1
app_local_get_ex
// if the value already exists fail
bnz p_fail
pop
// store the order number as the key
int 0
txna ApplicationArgs 1
int 1
app_local_put
int 1
return
查看已开立订单 - 第 2 步
下单后,其他用户可点击“刷新订单”按钮来查看该订单。也可定时执行上述同样功能,但为了简便,此解决方案不实施该功能。订单号就是订单开立时指定值的拼接。
JavaScript SDK 用于连接 Indexer 以及搜索已选择加入有状态智能合约的所有账户。这些账户经遍历,并检查其本地状态,以查找当前开立的订单。源文件中提供了其他代码以简化此代码部分,使其仅依赖于 Indexer。这些其他代码已被注释掉,因为当前的索引器错误使其无法生效。此问题应该很快就能解决,之后即可用注释掉的代码替换现有的代码。
(async () => {
let accountInfo = await indexerClient.searchAccounts()
.applicationID(APPID).do();
console.log(accountInfo);
let accounts = accountInfo['accounts'];
numAccounts = accounts.length;
for (i = 0; i < numAccounts; i++) {
let add = accounts[i]['address'];
let accountInfoResponse = await algodClient.accountInformation(add).do();
for (let i = 0; i < accountInfoResponse['apps-local-state'].length; i++) {
if (accountInfoResponse['apps-local-state'][i].id == APPID) {
if (accountInfoResponse['apps-local-state'][i][`key-value`] != undefined) {
console.log("User's local state:");
for (let n = 0; n < accountInfoResponse['apps-local-state'][i][`key-value`].length; n++) {
console.log(accountInfoResponse['apps-local-state'][i][`key-value`][n]);
let kv = accountInfoResponse['apps-local-state'][i][`key-value`]
let ky = kv[n]['key'];
console.log(window.atob(ky));
let option = document.createElement('option');
option.value = add + "-" + window.atob(ky);
option.text = window.atob(ky);
ta.add(option);
}
}
}
}
}
})().catch(e => {
console.log(e);
});
执行已开立订单 - 第 3 步
用户可从已开立订单列表中选择要执行的订单。这需要填写一些输入框,来指明所交易的资产、资产数量以及执行限价订单的用户将收到的 microAlgo 数额。这些框已填入正确的初始兑换率,用户可进行修改以出售更多资产,但兑换率须至少与初始兑换率同样有利于列单用户,并符合最低和最高 microAlgo 限额。如果违反了任何这些条件,都会因为逻辑错误而导致执行失败。此逻辑处于列单用户在下单时签署的无状态智能合约中。
按“执行订单”按钮后,Web 应用加载列单用户之前签署的逻辑签名,并创建三项交易。第一是调用有状态智能合约,以传递两个参数:字符串“execute”和订单号。此外,还将向有状态智能合约的账户数组传递初始列单账户地址。也即是说,有状态智能合约将有权访问账户的本地状态,并可在订单执行后清除订单。
第二是列单用户账户对执行账户的付款交易,采用执行用户指定的数额。第三是从执行用户账户到列单用户账户的资产转移。这些交易经过分组和签署。第一和第三项交易由执行用户签署。第二项交易由下单时已签署的逻辑签名进行签署。这些交易随后提交至 Algorand 网络。它们要么全部成功,要么全部失败。
let params = await algodClient.getTransactionParams().do();
let appAccts = [];
appAccts.push(rec);
//call stateful contract
let transaction1 = algosdk.makeApplicationNoOpTxn(account.addr, params, appId, appArgs, appAccts)
//make payment tx signed with lsig
let transaction2 = algosdk.makePaymentTxnWithSuggestedParams(rec, account.addr, amount, undefined, undefined, params);
//make asset xfer
let transaction3 = algosdk.makeAssetTransferTxnWithSuggestedParams(account.addr, rec, undefined, undefined,
assetamount, undefined, assetId, params);
let txns = [transaction1, transaction2, transaction3];
// Group both transactions
let txgroup = algosdk.assignGroupID(txns);
// Sign each transaction in the group
let signedTx1 = transaction1.signTxn(executeAccount.sk)
let signedTx2 = algosdk.signLogicSigTransactionObject(txns[1], lsig);
let signedTx3 = transaction3.signTxn(executeAccount.sk)
// Combine the signed transactions
let signed = []
signed.push(signedTx1);
signed.push(signedTx2.blob);
signed.push(signedTx3);
let tx = await algodClient.sendRawTransaction(signed).do();
await waitForConfirmation(client, tx.txId);
await deleteLsigFile(fn);
交易成功后,已签署的无状态合约将从服务器中删除。
有状态合约中用于处理订单执行的部分如下所示。此代码将执行以下检查。
- 确认三项交易均以原子方式提交。
- 第一项必须是调用有状态合约。
- 第二项必须是付款交易。
- 第三项必须是资产转移交易。
- 确认存在订单。如果没有则失败。
- 从初始列单用户本地状态中删除已执行的订单。
execute:
// Must be three transactions
global GroupSize
int 3
==
// First Transaction must be a call to a stateful contract
gtxn 0 TypeEnum
int appl
==
&&
// The second transaction must be a payment transaction
gtxn 1 TypeEnum
int pay
==
&&
// The third transaction must be an asset transfer
gtxn 2 TypeEnum
int axfer
==
&&
bz fail
int 1 // Creator of order
txn ApplicationID // Current stateful smart contract
txna ApplicationArgs 1 // 2nd argument is order number
app_local_get_ex
bz p_fail // If the value doesnt exists fail
pop
// Delete the ordernumber
int 1 //creator of order
// 2nd arg is order number
txna ApplicationArgs 1
app_local_del
int 1
return
关闭已开立订单 - 第 4 步
订单的初始列单用户可随时从已开立订单列表中选择所下订单,并通过“关闭订单”按钮来取消订单。Web 应用将触发有状态智能合约调用,以传递两个参数:字符串“close”和订单号。然后,有状态合约将从用户的本地状态中删除订单号,且 Web 应用将从服务器删除之前签署的无状态合约。
有状态智能合约中用于处理关闭操作的部分如下所示。此代码将执行以下检查。
- 确认此为单一交易。
- 确认用户本地状态中确实存在该订单。如果没有则为失败。
- 从用户的本地状态中删除订单号。
close:
// only works for app call
global GroupSize
int 1
==
bz fail
int 0 //account that opened order
txn ApplicationID //current smart contract
// 2nd arg is order number
txna ApplicationArgs 1
app_local_get_ex
// if the value doesnt exists fail
bz p_fail
pop
// delete the ordernumber
int 0 //account that opened order
// 2nd arg is order number
txna ApplicationArgs 1
app_local_del
int 1
return
注意事项
该应用所含的错误检查很少,仅用于学习。在生产应用中使用此逻辑之前,应先进行额外的错误检查和处理。例如,订单号仅基于限价订单中所用的值来生成。在 Web 应用中,这会加上用户地址作为前缀,但列表框和有状态 TEAL 应用会移除该前缀。这将导致两位不同用户可能使用相同的订单号。虽然这不会产生问题,但可能会造成混淆。此外,逻辑签名非加密上传,虽然应该无关紧要,但最好还是确保其安全。
应用还对两个虚拟用户使用了硬编码地址,这方面也应该更改,以便与 AlgoSigner 集成。
总结
此简单易用的 DEX 应用展现了如何使用 Algorand 的多项第 1 层功能来实现实用的智能合约应用。应用的完整源代码可访问 Github 获取。请务必查看自述文件,了解如何设置和运行应用。