智能合约开发语言 clarity 中文教程|写一个时间锁钱包

2021-09-16 12:00:36 +08:00
 gitandgit

原英文文档: https://book.clarity-lang.org/ch08-01-time-locked-wallet.html

时间锁钱包

随着时间的推移,区块高度可用于执行操作。如果您知道平均出块时间,那么您可以大致计算出在特定时间范围内将开采多少区块。我们将使用这个概念来创建一个在特定区块高度解锁的钱包合约程序。如果您想在特定时间段后将代币授予某人,这样的合约程序会很有用。想象一下,在未来,您想为孩子长大后留一些钱。现在,你可以通过智能合约程序做到这一点!让我们开始吧。

从我们的主 projects 文件夹中,我们创建一个新项目。

clarinet new timelocked-wallet

在 timelocked-wallet 文件夹中,我们使用以下命令创建合约程序文件:

clarinet contract new timelocked-wallet

功能

不要立即开始写代码,让我们花点时间考虑一下我们想要拥有的功能。

考虑到上述情况,合约程序将具有以下公有函数:

常量和变量

合约程序应该尽可能易于阅读和维护。因此,我们将大量使用常量来定义合约所有者和各种错误状态。错误可以采用以下形式:

需要两个数据变量将受益人和解锁高度存储为无符号整数。为合约程序的未初始化状态,我们将使受益人成为可选的主体用户类型。 (也就是说,在合约程序的所有者调用 lock 之前。)

;; Owner
(define-constant contract-owner tx-sender)

;; Errors
(define-constant err-owner-only (err u100))
(define-constant err-already-locked (err u101))
(define-constant err-unlock-in-past (err u102))
(define-constant err-no-value (err u103))
(define-constant err-beneficiary-only (err u104))
(define-constant err-unlock-height-not-reached (err u105))

;; Data
(define-data-var beneficiary (optional principal) none)
(define-data-var unlock-height uint u0)

错误代码本身是组成的。意味着由我们合同程序的前端应用程序处理。只要我们使用 (err ...) 响应类型,我们就确信任何可能的更改都会恢复

执行 lock

lock 函数只不过是将一些代币从 tx-sender 发送到自身并设置两个变量。但是,我们不能忘记检查是否设置了适当的条件。具体来说:

其中大部分转化为断言。因此,该功能实现如下:

(define-public (lock (new-beneficiary principal) (unlock-at uint) (amount uint))
    (begin
        (asserts! (is-eq tx-sender contract-owner) err-owner-only)
        (asserts! (is-none (var-get beneficiary)) err-already-locked)
        (asserts! (> unlock-at block-height) err-unlock-in-past)
        (asserts! (> amount u0) err-no-value)
        (try! (stx-transfer? amount tx-sender (as-contract tx-sender)))
        (var-set beneficiary (some new-beneficiary))
        (var-set unlock-height unlock-at)
        (ok true)
    )
)

请注意,我们如何使用我们之前定义的常量作为断言的抛出值?这允许一些非常清晰的代码。 (as-contract tx-sender) 部分为我们提供了合约程序的主体用户。

执行 bestow 函数

bestow 函数将很简单。它将检查 tx-sender 是否是当前受益人,如果是,则将受益人更新为通过的主体用户。需要记住的一个是主体用户被存储为(optional principal)。 因此,在进行比较之前,我们需要将 tx-sender 包装在 (some ...) 中。

(define-public (bestow (new-beneficiary principal))
    (begin
        (asserts! (is-eq (some tx-sender) (var-get beneficiary)) err-beneficiary-only)
        (var-set beneficiary (some new-beneficiary))
        (ok true)
    )
)

执行 claim 函数

最后,claim 函数应该检查 tx-sender 是否是受益人,以及是否已达到解锁高度。

(define-public (claim)
    (begin
        (asserts! (is-eq (some tx-sender) (var-get beneficiary)) err-beneficiary-only)
        (asserts! (>= block-height (var-get unlock-height)) err-unlock-height-not-reached)
        (as-contract (stx-transfer? (stx-get-balance tx-sender) tx-sender (unwrap-panic (var-get beneficiary))))
    )
)

人工测试

是时候进入 clarinet console 来测试合约程序了。

Contracts
+-------------------------------------------------------------+--------------------------------------+
| Contract identifier                                         | Public functions                     |
+-------------------------------------------------------------+--------------------------------------+
| ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.timelocked-wallet | (bestow (new-beneficiary principal)) |
|                                                             | (claim)                              |
|                                                             | (lock                                |
|                                                             |     (new-beneficiary principal)      |
|                                                             |     (unlock-at uint)                 |
|                                                             |     (amount uint))                   |
+-------------------------------------------------------------+--------------------------------------+

如果合约程序没有出现,则说明存在错误或语法错误。我们要使用 clarinet check 来追踪它们。

对于第一个测试,钱包将被部署者 (wallet_1) 之后的第一个主体用户锁定。我们可以选择一个非常低的区块高度,因为控制台会话总是从区块高度零开始。控制台交互将钱包锁定到高度 10,初始存款为 100 mSTX:

>> (contract-call? .timelocked-wallet lock 'ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK u10 u100)
Events emitted
{"type":"stx_transfer_event","stx_transfer_event":{"sender":"ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE","recipient":"ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.timelocked-wallet","amount":"100"}}

执行有效!我们要密切关注从 tx-sender 到合约程序的 STX 转移事件。可以使用管理命令 ::get_assets_maps 来验证合约程序的余额。

>> ::get_assets_maps
+-------------------------------------------------------------+---------+
| Address                                                     | STX     |
+-------------------------------------------------------------+---------+
| ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE (deployer)        | 999900  |
+-------------------------------------------------------------+---------+
| ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.timelocked-wallet | 100     |
+-------------------------------------------------------------+---------+

然后我们假设受益人的身份,看看我们是否可以认领钱包。 (请记住,在这种情况下必须指定完整的合约程序主体用户。)

>> ::set_tx_sender ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK
tx-sender switched to ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK
>> (contract-call? 'ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.timelocked-wallet claim)
(err u105)

尝试声明返回一个(错误 u105 )。这是未达到解锁高度相关的错误值。到现在为止还挺好。

REPL 中的块高度不会自行增加。可以使用 ::advance_chain_tip 模拟挖矿。让我们看看在将区块高度增加 10 后是否可以领取钱包。

>> ::advance_chain_tip 10
10 blocks simulated, new height: 10
>> (contract-call? 'ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.timelocked-wallet claim)
Events emitted
{"type":"stx_transfer_event","stx_transfer_event":{"sender":"ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.timelocked-wallet","recipient":"ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK","amount":"100"}}
(ok true)

ok 和 STX 转账事件证明它有效。你还可以检查资产地图以获得良好的衡量标准。

单元测试

我们确定以下情况以编写全面的单元测试。智能合约程序:

Clarinet 具有内置的 assertion 断言函数,可检查预期的 STX 转账事件是否确实发生。这些将用于保持单元测试简洁。

合约程序的测试文件始终位于 tests 文件夹中。它以合约命名:timelocked-wallet_test.ts 。清除文件,要确保将 import 语句保留在顶部。

测试 lock

我们首先编写涵盖不同 lock 情况的四个测试。

Clarinet.test({
    name: "Allows the contract owner to lock an amount",
    async fn(chain: Chain, accounts: Map<string, Account>) {
        const deployer = accounts.get('deployer')!;
        const beneficiary = accounts.get('wallet_1')!;
        const amount = 10;
        const block = chain.mineBlock([
            Tx.contractCall('timelocked-wallet', 'lock', [types.principal(beneficiary.address), types.uint(10), types.uint(amount)], deployer.address)
        ]);

        // The lock should be successful.
        block.receipts[0].result.expectOk().expectBool(true);
        // There should be a STX transfer of the amount specified.
        block.receipts[0].events.expectSTXTransferEvent(amount, deployer.address, `${deployer.address}.timelocked-wallet`);
    }
});

Clarinet.test({
    name: "Does not allow anyone else to lock an amount",
    async fn(chain: Chain, accounts: Map<string, Account>) {
        const accountA = accounts.get('wallet_1')!;
        const beneficiary = accounts.get('wallet_2')!;
        const block = chain.mineBlock([
            Tx.contractCall('timelocked-wallet', 'lock', [types.principal(beneficiary.address), types.uint(10), types.uint(10)], accountA.address)
        ]);

        // Should return err-owner-only (err u100).
        block.receipts[0].result.expectErr().expectUint(100);
    }
});

Clarinet.test({
    name: "Cannot lock more than once",
    async fn(chain: Chain, accounts: Map<string, Account>) {
        const deployer = accounts.get('deployer')!;
        const beneficiary = accounts.get('wallet_1')!;
        const amount = 10;
        const block = chain.mineBlock([
            Tx.contractCall('timelocked-wallet', 'lock', [types.principal(beneficiary.address), types.uint(10), types.uint(amount)], deployer.address),
            Tx.contractCall('timelocked-wallet', 'lock', [types.principal(beneficiary.address), types.uint(10), types.uint(amount)], deployer.address)
        ]);

        // The first lock worked and STX were transferred.
        block.receipts[0].result.expectOk().expectBool(true);
        block.receipts[0].events.expectSTXTransferEvent(amount, deployer.address, `${deployer.address}.timelocked-wallet`);

        // The second lock fails with err-already-locked (err u101).
        block.receipts[1].result.expectErr().expectUint(101);

        // Assert there are no transfer events.
        assertEquals(block.receipts[1].events.length, 0);
    }
});

Clarinet.test({
    name: "Unlock height cannot be in the past",
    async fn(chain: Chain, accounts: Map<string, Account>) {
        const deployer = accounts.get('deployer')!;
        const beneficiary = accounts.get('wallet_1')!;
        const targetBlockHeight = 10;
        const amount = 10;

        // Advance the chain until the unlock height plus one.
        chain.mineEmptyBlockUntil(targetBlockHeight + 1);

        const block = chain.mineBlock([
            Tx.contractCall('timelocked-wallet', 'lock', [types.principal(beneficiary.address), types.uint(targetBlockHeight), types.uint(amount)], deployer.address),
        ]);

        // The second lock fails with err-unlock-in-past (err u102).
        block.receipts[0].result.expectErr().expectUint(102);

        // Assert there are no transfer events.
        assertEquals(block.receipts[0].events.length, 0);
    }
});

测试 bestow

bestow 是一个简单的函数,允许受益人转让认领权。因此,我们必须确保只有受益人才能成功调用 bestow 。

Clarinet.test({
    name: "Allows the beneficiary to bestow the right to claim to someone else",
    async fn(chain: Chain, accounts: Map<string, Account>) {
        const deployer = accounts.get('deployer')!;
        const beneficiary = accounts.get('wallet_1')!;
        const newBeneficiary = accounts.get('wallet_2')!;
        const block = chain.mineBlock([
            Tx.contractCall('timelocked-wallet', 'lock', [types.principal(beneficiary.address), types.uint(10), types.uint(10)], deployer.address),
            Tx.contractCall('timelocked-wallet', 'bestow', [types.principal(newBeneficiary.address)], beneficiary.address)
        ]);

        // Both results are (ok true).
        block.receipts.map(({ result }) => result.expectOk().expectBool(true));
    }
});

Clarinet.test({
    name: "Does not allow anyone else to bestow the right to claim to someone else (not even the contract owner)",
    async fn(chain: Chain, accounts: Map<string, Account>) {
        const deployer = accounts.get('deployer')!;
        const beneficiary = accounts.get('wallet_1')!;
        const accountA = accounts.get('wallet_3')!;
        const block = chain.mineBlock([
            Tx.contractCall('timelocked-wallet', 'lock', [types.principal(beneficiary.address), types.uint(10), types.uint(10)], deployer.address),
            Tx.contractCall('timelocked-wallet', 'bestow', [types.principal(deployer.address)], deployer.address),
            Tx.contractCall('timelocked-wallet', 'bestow', [types.principal(accountA.address)], accountA.address)
        ]);

        // All but the first call fails with err-beneficiary-only (err u104).
        block.receipts.slice(1).map(({ result }) => result.expectErr().expectUint(104));
    }
});

测试 claim

对于 claim,我们会测试是否达到解锁高度的情况,只有受益人才能认领。

Clarinet.test({
    name: "Allows the beneficiary to claim the balance when the block-height is reached",
    async fn(chain: Chain, accounts: Map<string, Account>) {
        const deployer = accounts.get('deployer')!;
        const beneficiary = accounts.get('wallet_1')!;
        const targetBlockHeight = 10;
        const amount = 10;
        chain.mineBlock([
            Tx.contractCall('timelocked-wallet', 'lock', [types.principal(beneficiary.address), types.uint(targetBlockHeight), types.uint(amount)], deployer.address),
        ]);

        // Advance the chain until the unlock height.
        chain.mineEmptyBlockUntil(targetBlockHeight);

        const block = chain.mineBlock([
            Tx.contractCall('timelocked-wallet', 'claim', [], beneficiary.address),
        ]);

        // The claim was successful and the STX were transferred.
        block.receipts[0].result.expectOk().expectBool(true);
        block.receipts[0].events.expectSTXTransferEvent(amount, `${deployer.address}.timelocked-wallet`, beneficiary.address);
    }
});

Clarinet.test({
    name: "Does not allow the beneficiary to claim the balance before the block-height is reached",
    async fn(chain: Chain, accounts: Map<string, Account>) {
        const deployer = accounts.get('deployer')!;
        const beneficiary = accounts.get('wallet_1')!;
        const targetBlockHeight = 10;
        const amount = 10;
        chain.mineBlock([
            Tx.contractCall('timelocked-wallet', 'lock', [types.principal(beneficiary.address), types.uint(targetBlockHeight), types.uint(amount)], deployer.address),
        ]);

        // Advance the chain until the unlock height minus one.
        chain.mineEmptyBlockUntil(targetBlockHeight - 1);

        const block = chain.mineBlock([
            Tx.contractCall('timelocked-wallet', 'claim', [], beneficiary.address),
        ]);

        // Should return err-unlock-height-not-reached (err u105).
        block.receipts[0].result.expectErr().expectUint(105);
        assertEquals(block.receipts[0].events.length, 0);
    }
});

Clarinet.test({
    name: "Does not allow anyone else to claim the balance when the block-height is reached",
    async fn(chain: Chain, accounts: Map<string, Account>) {
        const deployer = accounts.get('deployer')!;
        const beneficiary = accounts.get('wallet_1')!;
        const other = accounts.get('wallet_2')!;
        const targetBlockHeight = 10;
        const amount = 10;
        chain.mineBlock([
            Tx.contractCall('timelocked-wallet', 'lock', [types.principal(beneficiary.address), types.uint(targetBlockHeight), types.uint(amount)], deployer.address),
        ]);

        // Advance the chain until the unlock height.
        chain.mineEmptyBlockUntil(targetBlockHeight);

        const block = chain.mineBlock([
            Tx.contractCall('timelocked-wallet', 'claim', [], other.address),
        ]);

        // Should return err-beneficiary-only (err u104).
        block.receipts[0].result.expectErr().expectUint(104);
        assertEquals(block.receipts[0].events.length, 0);
    }
});

该项目的完整源代码可以在这里找到 : https://github.com/clarity-lang/book/tree/main/projects/timelocked-wallet.

689 次点击
所在节点    区块链
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/802258

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX