解决方案
无结果
解决方案预览图

去中心化交易所解决方案实例

简介

以无集中权限的去中心化方式交易数字商品,这是区块链技术的关键承诺之一。数字资产交易所和区块链以不同方式实现了这一承诺。对于高频率交易,很多工作在第 2 层应用执行,该层的交易最终在区块链上完成。对于低频率交易,大部分工作直接在链上执行。在 Algorand,数字资产交易所能以多种方式实现。此解决方案采用其中一种,将在区块链上存储和执行所有订单。本文旨在提供更具指导性的说明,帮助开发人员了解 Algorand 不同技术部分的关键概念,因此在操作方面相当简单。

在此解决方案中,我们将介绍如何连接买家和卖家,以促进在 Algorand 区块链上以去中心化方式交易资产。我们将使用限价订单合约作为交易方式,其中由买家开立订单,并指明要购买的资产和相应的兑换率。该订单随后保留在区块链上,可供相关资产的任何卖家后续执行订单。此解决方案还使买家能够随时关闭订单。

EditorImages/2020/10/28 19:05/gui.png

此应用涉及使用多种 Algorand 技术,包括无状态智能合约有状态智能合约原子交易标准资产、资产交易和 Algorand 索引器。我们将从总体应用设计开始,然后深入探讨如何构建应用的详细信息。

设计概述

要实施此解决方案,需要四项基本操作。用户应能够开立、关闭或执行订单,并可列出所有已开立订单。

注意

此示例使用硬编码地址,供用户开立和执行订单。此示例还采用与 Algorand TestNet 和 Indexer 的硬编码连接。这是为了简化此示例,但应用最好使用 AlgoSigner 管理账户,以及与应用部署者所用节点或某一可用 API 服务的连接。

1 - 开立订单

此解决方案可供买家创建限价订单,以在其中指明想要购买的 Algorand 资产。该订单还应包含买家愿意花费的最低和最高 microAlgo 数额和兑换率。此解决方案中使用简单的“N”和“D”符号来表示此兑换率。其中“N”表示资产数量,而“D”表示买家愿意花费的 micoAlgo 数额。输入后,用户便可下单。

该订单将转换为无状态合约,以用于委托授权。无状态合约也可用作托管或委托。对于托管,资金将转入某一账户,并由逻辑确定何时转出。对于委托,逻辑经用户签署,并可在之后用于从签署人账户取出资金。本例中,这两种方法均可使用。此解决方案实施了委托,其中逻辑经列出订单的用户签署。这称为创建逻辑签名。此逻辑签名随后保存到文件中,并推送至服务器以供日后使用。已签署的逻辑将在用户关闭订单或订单执行时删除。

作为此解决方案的一部分,其中的主有状态智能合约包含开立、关闭和执行订单的方法。无状态智能合约委托逻辑链接到此有状态智能合约。这是为了在不与有状态智能合约应用调用共用的情况下,使无状态委托逻辑无效。

用户开立订单时,将调用有状态智能合约以开立该订单。有状态智能合约在用户的本地存储中存储订单号。这就要求已开立订单的数量不得超过 16。其实可使用不同的订单号生成器来扩展这一范围,但为了简化和可读性而采用此限制。

EditorImages/2020/10/28 18:57/limitorder1.png

2 - 查看已开立订单

下单后,此解决方案提供一个列表框和一个刷新订单按钮。点击后,Web 应用调用 Algorand Indexer 来搜索已选择加入有状态智能合约的所有账户。这些账户经遍历,其本地存储值(已开立订单)读回并填充入列表框。

EditorImages/2020/10/28 18:58/limitorder2.png

3 - 执行已开立订单

已开立订单列出后,其他用户可登录 Web 应用,来选择并执行已开立订单。执行用户可指明准备出售多少资产,以及要求多少 microAlgo。如果所指定的数额高于初始限价订单的最高额或低于初始最低额,则执行失败。如果所指定的兑换率低于初始限价订单的规定,则执行也将失败。当执行用户按下执行订单按钮后,Web 应用将生成三项交易。一是调用有状态智能合约,以指明用户在执行特定订单。二是列出限价订单的用户对执行用户的付款交易,采用指定的 microAlgo 数额。三是从执行用户向列出限价订单的用户转移指定的资产数额。第一和第三项交易经执行用户签署。第二项交易(付款)经无状态智能合约逻辑签署,也就是列单用户之前在开立订单时所签署的逻辑。此逻辑签名读取自上传到服务器的文件。这三项交易以原子方式分组,并推送至服务器。对于多项原子交易,只要其中任何一项失败,则全部失败。这样,双方都可获得期望的结果。

第一项交易中的有状态智能合约将从列单用户的本地状态清除订单,而已签署的逻辑文件将从服务器删除。

EditorImages/2020/10/28 19:18/limitorder3.png

4 - 关闭订单

任何具有已开立订单的用户均可从订单列表选择此已开立订单,然后点击关闭订单按钮。这将直接为用户从有状态智能合约的本地状态移除已开立订单,并从服务器删除已签署的逻辑文件。

EditorImages/2020/10/28 19:21/limitorder4.png

信息

此应用使用 JavaScript SDK 实现对 Algorand 节点和 Indexer 的所有调用。

开立订单 - 第 1 步

第一步是用户开立订单,并在其中指明限价订单的一组标准。

EditorImages/2020/10/28 19:23/limitorder5.png

所需数据包括用户想要购买的资产 ID、愿意花费的最低和最高 microAlgo 数额,以及 ND 值。这两个值共同构成兑换率 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 步

下单后,其他用户可点击“刷新订单”按钮来查看该订单。也可定时执行上述同样功能,但为了简便,此解决方案不实施该功能。订单号就是订单开立时指定值的拼接。

EditorImages/2020/10/28 19:39/limitorder6.png

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 限额。如果违反了任何这些条件,都会因为逻辑错误而导致执行失败。此逻辑处于列单用户在下单时签署的无状态智能合约中。

EditorImages/2020/10/28 19:49/limitorder7.png

按“执行订单”按钮后,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 获取。请务必查看自述文件,了解如何设置和运行应用。

智能合约

TEAL

SDK

2020年12月04日