This is the old Algorand Developer Portal. Please head over to dev.algorand.co to explore our newly rebuilt documentation site. Please excuse us as we continue to transition content to the new portal

创建文章

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Solution Thumbnail

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

简介

以无集中权限的去中心化方式交易数字商品,这是区块链技术的关键承诺之一。数字资产交易所和区块链以不同方式实现了这一承诺。对于高频率交易,很多工作在第 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 获取。请务必查看自述文件,了解如何设置和运行应用。