Aptos开发入门教程:创建资源(一)

1年前
标签:比特币0619
文章来源: Odaily星球日报

原文作者: magnum6.eth

原文: Aptos Intros: An intro to building in Move/Aptos

本文目标:资源、能力、全局存储、单元测试

介绍

Move 语言使得在区块链世界中创造数字“事物”,以及拥有和转移它们变得非常容易。Move 是一种非常简单的语言——这是有意为之。复杂性总是引入额外风险,漏洞会在意想不到的应用中显现出来。我们看到过利用智能合约导致数十亿美元资产被盗的恐怖故事。因此,我们希望数字资产是安全的。Move 语言的简单性,为这种安全提供了一条实现路径。

本篇文章,首先比较理论,因为在深入研究代码之前,对于资源是什么、如何控制资源,有良好正确的感知非常重要。这会包括外行理论(例如“数字事物”)与学术理论(例如“具有受线性逻辑启发的语义的自定义资源类型”)。

资源:数字事物

Move 中的“资源”就是是一种“数字事物”。那东西可以是你想要或想象的任何东西:演唱会门票、NFT、一本书、两个企业之间的合同、社交媒体文章等。你能想到的东西,都可以是一种资源。我们以Alice 和 Bob 去参加一场演唱会,他们需要门票为例。在 Move 中,我们可以简单地这么创建门票类:

structConcertTickethaskey{
seat:vector,
ticket_code:vector,
}

Move 模块中的代码现在使我们能够向 Alice 和 Bob 发送门票。有了上面的结构,我们可以简单地创建一张门票:

publicfuncreate_ticket(account:&signer,seat:vector,ticket_code:vector):ConcertTicket{
letseat=vector;
letticket_code=vector;
ConcertTicket{seat,ticket_code}
}

在“外行理论”方法中,可以把资源想象成离散的物理对象,而不是像程序员一样去思考(例如“堆栈与堆”或“对象原型”等)。我们的“结构”是配方、建筑图纸、指令列表或任何你想使用的类比。让我们作个分解:

structWhatYouWantToCallIthasAbilities{
any_name_i_want:one_of_a_few_type_choices,
any_other_name_i_want:one_of_a_few_type_choices,
this_is_the_last_one_i_need:one_of_a_few_type_choices
}

你可以随意命名资源,但它必须以大写字母 A-Z 开头。之后,名称可以包含下划线 _、字母 a-z、字母 A-Z 或数字 0-9。该结构将具有某些“能力”,我们将在稍后介绍。但现在,只要知道这些能力将包括“key”、“store”、“copy”和“drop”中的一种或组合。

在结构中,你可以拥有任意数量的键值对(我不知道是否有键值对计数上限,但如果有的话,我怀疑它超出任何实际应用程序所要求的)。键名应该用蛇形命名(注意:这似乎是推荐的编码约定,而不是编译器要求)。每个键值对中的值必须是以下类型之一:

bool
u8
u64
u128
address
signer
vector:vector{non-referenceMoveTypeId}>
struct:{address}::{module_name}::{struct_name}::{generictypes}>
reference:immutable&andmutable&mutreferences.
generic_type_parameter:

我这里不展开讲,只会触及几个要点,因为我们稍后会深入研究类(class)。虽然结构看起来像一个 JSON 对象,但结构只能由顶级键值对组成,没有子键。例如:

structConcertTickethaskey{
seat:vector,
ticket_code:vector,
}

没问题,但是:

structConcertTickethaskey{
seat:{
row:u8,
seat_number:u8
}
ticket_code:vector,
}

是不行的。但是我们可以将结构作为值类型,因此我们可以通过以下方式得到相同的最终结果:

structSeathasstore{
row:u8,
seat_number:u8
}
structConcertTickethaskey{
seat:Seat,
ticket_code:vector,
}

要得到我们想要的结构似乎还有很长的路要走——那么为什么要这样做呢?这里涉及一个非常重要的原则,这对于许多开发人员来说可能是陌生的:

要点:一旦创建,结构内的键值对中的每个值都是一个“数字事物”,按照结构的能力不同可以被拥有、转移甚至销毁。这意味着什么?让我们看看另一个简单化的结构。我们似乎觉得创造一种新的加密货币应该很难,尤其时刚接触加密货币领域时。这是 TestCoin 的结构,一种我们现在在 devnet 上使用的代币:

structCoinhasstore{
value:u64
}

很简单,对吧?当然,除此之外还有其他围绕 Coin 构建的结构和功能可以用,但基本结构是一对简单地存有数字的键值。如果我想铸造价值 50 的 Coin,我只需:

letmy_coin=Coin{value:50}

变量my_coin现在包含一个值为 50 的 Coin 资源。现在,如果我们这样做:

letmy_other_coin=my_coin

我们的 javascript 经验可能会让我们得出这样的结论:我们现在有两个变量,my_coin和my_other_coin,每个变量都是 50,所以加起来我们有 100 的总 Coin 值。

对不起,这是错的。我们现在拥有的是价值为 50 的my_other_coin,而my_coin已经消失,无法访问,就像它从未存在过一样。因为在let my_other_coin = my_coin中所做的实际上是将值从一个变量“移动”(灯灭了)到另一个变量,并且由于 my_coin 为空,它不再有效。对于一些开发人员来说,这是一个新概念,可能需要花一分钟的时间来思考,但它是理解 Move 语言和围绕资源进行构建的基础。再次强调,从物理事物的角度考虑资源。如果结构是菜谱,则创建和赋值到变量就是做饭。我们不只是在操作数据,是在将代币从my_coin中取出并将其移动到my_other_coin中。

从开发人员的大脑中思考这个问题的另一种方法是了解编译器在背后为我们做了什么。在每个变量赋值中,Move 编译器都会推断“=”符号是“move”还是“copy”。虽然我们输入的是这样,但编译器在上面看到的是:

letmy_other_coin=movemy_coin;

因为my_coin是一种资源,编译器推断我们正在移动该值。现在,如果有一个非资源标量值,例如:

leta=1;
letb=a;

编译器实际看到的是

leta=1;
letb = copy a;

因为 'a' 是一个非来自资源的标量值。事实上,我们可以在我们的代码中显式地使用“move”和“copy”关键字,而不是依靠 Move 来推断我们正在尝试做什么。那么,我们是否可以通过执行以下操作回到我们的“快速致富加密方案”:

letmy_other_coin=copymy_coin;

对不起,还是不行,因为 Coin 结构不具有被复制的显式能力。还记得我们上面提到的“能力”以及“key、store、copy、drop”的四个选项吗?我们的 Coin 结构仅声明“存储”能力。因为没有“复制”能力,Move 将不会推断或允许进行复制——它只会将值从一个变量移动到另一个变量中。当你阅读有关 Move 的“安全保证”的内容时,他们说的就是这个意思。该语言不会让你意外(或出于恶意)复制不应复制的内容。

我们会详细介绍能力,但让我们继续这个移动值和资源的示例。如果我们像这样解构 my_coin 会怎样:

letCoin{value}=my_coin;

我们现在有一个变量值,它是一个 u64 数字,而 my_coin 已经消失,因为它现在是空的。所以,如果我们再尝试:

letmy_other_value=value;

即使 value 是 u64 标量,Move 编译器也会将赋值推断为:

letmy_other_value=movevalue;

为什么上一个 u64 赋值被推断为复制?即使 value 是 u64,因为它来自非复制资源,其内容将始终被视为非复制变量。这让我们回到了上面的要点——结构中的所有内容都继承了与结构本身相同的能力。

暂停时刻:如果你读到了这里并且感到迷糊——不用担心,这很正常。然而,除非这些概念对你来说非常清楚,否则在这里暂停一下可能是个好主意,让它沉淀一下,然后回来再读一遍。这些是基本概念,一旦掌握,将大大加快你使用 Move 的开发过程。

好了,所以现在很明显,无法通过简单地在 TestCoin 上进行算术来充实我的钱袋。但这没关系,因为我将部署我自己的TestCoin.move副本并开始以编程方式生成我所需要的所有 Coin。额,我不想打碎你的美梦,但就像你上周购买的、“带着反叛态度的香蕉,戴着墨镜,一边抽着烟”的 NFT 不会支付你在维尔京群岛度假的费用,部署你自己的 TestCoin 副本也是镜中花、水中月。除了 TestCoin 今天(并且可能永远)以 0.000000 美元交易的事实之外,结构仅在声明它的模块中才有意义。要点:结构只能由声明它们的模块创建/操作/销毁,并且这些模块由单个帐户拥有。

仅将结构命名为“Coin”并不能使其成为 TestCoin。如果我有一个名为ConcertTicket的结构,而你有一个名为ConcertTicket的结构——它们是不一样的。结构及其包含的值完全由帐户和创建结构的模块之间的链接标识(来自我们上面的结构:{address}::{module_name}::{struct_name})。唯一实际为 TestCoin 的 Coin 结构在这里:

AptosFramework::TestCoin::Coin

我可以从仓库中获取TestCoin.move的精确副本并将其部署到我拥有的帐户:

9b0a2b8dbf5ccadd1fd96b84b8bb9ff7::TestCoin::Coin

但是就 Aptos 交易而言,这两个结构彼此完全没有关系。我无法从9b0a2b8dbf5ccadd1fd96b84b8bb9ff7::TestCoin::withdraw中提取 Coin,然后尝试使用AptosFramework::TestCoin::deposit存入该 Coin,因为由于类型不兼容,交易将无效。

代码实现

我发布了一个仓库,这是从 aptos.dev 教程和 Aptos 团队提供的单元测试设置中汇总的 Move 项目模板结构。克隆该库以开始你的编程。每节课在教程仓库中都有一个单独的标签。如果你想跳到本章节的最终版本,可以在这里找到。模板最初的文件结构是这样的:

├──sources
├──Module.move
└──ModuleTests.move
├──src
├──
lib.rs├──main.rs
└──move_unit_tests.rs
├──.gitignore
├──Cargo.toml
├──Move.toml
├──README.md

本质上,我们在一些 Rust 资源之上构建了一个 Move 项目,这些资源有助于编译、测试和构建我们的 Move 代码。我怀疑在不久的将来我们将可以访问一个 Move CLI,它将为我们处理这些功能,所以除了Cargo.toml上的几个小点之外,我不会花任何时间在 Rust 文件上。在前三行我们有:

[package]
name="package_name"
version="0.0.0"

让给文件包一个名称和一个起始版本。我这里只是简单地使用“教程”,但你可以按照你的心意命名。该名称需要使用蛇形大小写以避免编译器警告。如果你不使用蛇形命名,它仍然会构建,但Cargo会觉得你作为开发人员有点半吊子。因此,让我们将Cargo.toml更改为

#/Cargo.toml
[package]
name="tutorial"
version="0.1.0"

让我们将Move.toml调整为:

#/Move.toml
[package]
name="tutorials"
version="0.1.0"
[addresses]
AptosFramework="0x1"
TicketTutorial="0xe110"
[dependencies]
AptosFramework={git="https://github.com/aptos-labs/aptos-core.git",subdir="aptos-move/framework/aptos-framework",rev="0b834fc218648d759e93a7d61a432d0dce6c9946"}

我们在这里给我们的包一个名子和版本号。另一个值得注意的是TicketTutorial = "0xe110"行。对于我们上面谈到的结构和函数路径({address}::{module_name}::{struct_name}),这是我们设置地址的地方。一旦我们编译项目(很快),我们会将字节码模块发布到Aptos区块链上的一个帐户。我们可以使用离散地址来调用结构和函数,例如:

0x95876b0492fe3912863e55bab6f74703::Module::Struct

但这有点麻烦。Move.toml为我们提供了一个将命名地址放入包帐户的地方。这里我们设置为TicketTutorial,它允许我们引用如下结构:

TicketTutorial::Module::Struct

当我们准备好部署时,我们将在Move.toml中写上我们要发布到的帐户的实际地址。但现在我们可以只使用占位符地址0xe110

我们在 Move.toml 中要做的另一件事是引用我们的依赖项。在几乎每个 move 项目中,你都将参考我们这里的 AptosFramework。我们还必须建立所有依赖项发布的地址。对于 AptosFramework,该地址是 0x1。

让我们将Module.move重命名为TicketTutorial.move。在TicketTutorial.move中,我们将第一行更改为:

moduleTicketTutorial::Tickets{

其中“TicketTutorial”是来自Move.toml的命名的所有者地址,“Tickets”是我们想要引用模块的方式。我们已经使用use Std::Signer声明了一个依赖项,它将在你构建的每个模块中使用。

我们现在已经处于起跑线上了,我们必须得到 Alice 和 Bob 的演唱会门票。让我们通过在 use 语句之后添加以下内容来创建我们的第一个结构:

structConcertTickethaskey{
seat:vector,
ticket_code:vector,
}

让我们在这里解决两个更重要的概念,能力和全局存储。正如我们所提到的,能力定义了我们可以用资源做的事情。我们定义了 ConcertTicket 的一个能力:“key”。这意味着我们可以在全局存储中保存一个 ConcertTicket 资源(基本上所有数据存储在 Aptos 链上的帐户中)。所以我可以创建一个 ConcertTicket 并将其移至 Alice 的帐户。

将“key”与“store”能力进行比较。“store”能力允许我创建 ConcertTicket,但我不能直接将其移到 Alice 的帐户中。我必须将它存储在另一个具有“key”能力的结构中。从那里你可能会猜到“copy”允许我复制资源,“drop”允许我销毁资源。有关能力的更多阅读内容,请在此处查看 Diem Move 语言参考:

能力类型

对于全局存储,可以参考这里:

全局存储结构

让我们创建一张票并将其交给 Alice。在刚刚 ConcertTicket 结构下方,添加以下内容:

publicfuncreate_ticket(recipient:&signer,seat:vector,ticket_code:vector){
move_to(recipient,ConcertTicket{seat,ticket_code})
}

我们'我们已经创建了第一个具有全局作用域(public scope)的函数,因此可以从模块外部调用它:

TicketTutorial::Tickets::create_ticket

作用域应该是一个熟悉的编程概念,所以我不会在这里介绍它,你可以在函数语言参考中深入了解细节:

函数

我们传入了一些参数。最重要的是recipient,它是对 Std::Signer 类型的引用。所有区块链的核心操作原则,每当我们对帐户进行任何写入类型的操作时,都必须对交易进行签名。当我们调用这个函数为 Alice 创建一张票时,我们需要传入一个对她的账户的引用作为签名者。我们不必担心她是否签署了交易,因为如果她没有签署,Aptos-VM 将永远不会调用我们的函数。其他两个参数是vector,用于保存票座分配和参考代码的值。(编者注:ConcertTicket 结构中的数据是任意的。是的,实际的票可能需要比这更多的数据,但我们现在不担心业务逻辑。随意添加你想要的所有数据字段。)在 Move 中没有字符串原素,所以 vector现在可以完成这项工作(稍后我们将讨论 Std::ASCII,这将使之更具可读性;我们现在将坚持使用 vector,所以我们可以感激 ASCII::String 稍后带来的帮助)。

我们从前面几段学到,可以通过以下方式创建资源:

ConcertTicket{seat,ticket_code}

我们在函数中内联(inline)创建资源,然后将其移动到 recipient 的帐户中。欢呼!我们正在移动我们的第一个资源!move_to 指令是一个全局存储操作符,它可以将资源移到一个帐户中。加上 move_from(我们将很快介绍),这就是我们在 Move 中移动事物的方式。我们将经常访问这些指令,但你现在可以在此处阅读详细信息:

全局存储操作符

Aptos 链和 Move 的一个相对独特的属性是你不能向任意帐户发送资源。如果你在其他链上有不请自来显示在你钱包上的 NFT 垃圾,那么你就会明白为什么这是一个很好的功能(无论谁一直在给我发 Solsweeps 卡通扫帚 NFT 垃圾,我正在看着你)。所有move_to和move_from指令都需要受影响帐户的签名。move_to 有一个接口:

move_to(&signer,T)

我们提供类并通过以下方式传递我们创建的 ConcertTicket 资源:

move_to(recipient,ConcertTicket{seat,ticket_code})

就是这样。我们现在可以创建和移动资源。它太简单了,真的让你想知道为什么我花了将近 3,000 个字才写出大约 6 行代码。现在,让我们看看它是否有效。

测试……测试一、二、三……

这玩意打开了吗??

我们将在本章节中介绍的最后一件事是快速过一下单元测试。我们可以使用一些编译器指令在模块内部进行设置。在我们的create_ticket函数下方,但在结束 } 之前,让我们添加以下内容:

#[test(recipient=@0x1)]
public(script)funsender_can_create_ticket(recipient:signer){
create_ticket(&recipient,b"A24",b"AB43C7F");
letrecipient_addr=Signer::address_of(&recipient);
assert!(exists(recipient_addr),1);
}

这是一个简单的内联单元测试,以确保我们的代码在编译和部署之前在基础层面上工作。第一行是编译器指令,指示下一个函数是一个测试:

#[test(recipient=@0x1)]

它还为我们提供了创建签名者的能力,我们可以使用 @0x1 地址表示法将其传递给测试函数。我们调用create_ticket函数来创建并提供签名者座位号“K24”,票证代码为“AB43C7F”(任意数据;如果你愿意,可以将自己放在前排)。b"string"是一个字符串文字运算符,它给我们创建一个向量。通过这个函数调用,我们创建了ConcertTicket并将其存储在地址 0x1 的收件人帐户中。

这是一个测试,所以我们必须确保它有效。我们使用函数Signer::address_of将“recipient”的地址存储在我们的变量recipient_addr中。然后我们可以使用exists来查看ConcertTicket资源是否实际存储在该地址。exists 指令是另一个具有exists(address): bool接口的全局存储操作符。传入我们的 类和我们正在检查的地址,会给我们一个关于该地址是否存在资源的 true/false 响应。

最后,Assert!是一个类似于宏的操作,可以让我们测试一个条件,条件不满足时将退出并返回错误代码。如果我敲了接近 4,000 字对你们这些超级大脑来说还不够,更多细节在这里:

Abort and Assert

在我们的测试中,我们使用exists函数来判断资源是否存在,表示测试成功。让我们运行那个测试(一定要先保存你的文件。如果你像我一样,也浪费了太长时间,困惑为什么你的更改不起作用,为什么运行测试 5 次后,还是只是看到 VS Code 选项卡的大白圆点的话)。

在项目目录中打开一个终端并运行:

cargotest

如果一切正常,你将得到以下输出:

Finishedtest[unoptimized+debuginfo]target(s)in0.50s
Runningunittests(target/debug/deps/tutorial-6df2116825e4520d)
running1test
CACHEDMoveStdlib
CACHEDCoreFramework
CACHEDAptosFramework
BUILDINGtutorials
RunningMoveunittests
[PASS]0xe110::Tickets::sender_can_create_ticket
Testresult:OK.Totaltests:1;passed:1;failed:0
testmove_unit_tests::move_unit_tests...ok
testresult:ok.1passed;0failed;0ignored;0measured;0filteredout;finishedin0.41s
Runningunittests(target/debug/deps/tutorial-b1774daddf2e13d8)
running0tests
testresult:ok.0passed;0failed;0ignored;0measured;0filteredout;finishedin0.00s
Doc-teststutorial
running0tests
testresult:ok.0passed;0failed;0ignored;0measured;0filteredout;finishedin0.00s

我们的测试设置正在多个地方尝试测试,但我们现在只专注于第一个测试并且(欢呼)它通过了!为了确保这一点,让我们把测试中的函数调用注释去掉

//create_ticket(&recipient,b"A24",b"AB43C7F");

并再次运行,得到这个输出:

RunningMoveunittests
[FAIL]0xe110::Tickets::sender_can_create_ticket
Testfailures:
Failuresin0xe110::Tickets:
┌──sender_can_create_ticket──────
│error[E11001]:testfailure
│┌─/Users/culbrethw/Development/Tutorials/Tickets/sources/TicketTutorial.move:42:3
││
│36│public(script)funsender_can_create_ticket(recipient:signer){
││------------------------Inthisfunctionin0xe110::Tickets
│·
│42│assert!(exists(recipient_addr),1);
││^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^Testwasnotexpectedtoabortbutitabortedwith1here


└──────────────────
Testresult:FAILED.Totaltests:1;passed:0;failed:1

这里显示失败!我们在错误消息中看到,Test 不应该中止,但它在此处以 1 中止,其中with 1是我们在Assert!失败时发出的错误代码。当然,有时我们希望测试在某些条件下失败,但我们的大脑需要看到全绿,以便我们知道一切都按计划进行。我们可以使用另一个编译器指令构建我们的测试,通过将我们的测试修改为:

#[test(recipient=@0x1)]
#[expected_failure(abort_code=1)]
public(script)funsender_can_create_ticket(recipient:signer){

其中abort_code是我们预期的错误。再次运行 cargo 测试,我们又回到了全绿:

RunningMoveunittests
[PASS]0xe110::Tickets::sender_can_create_ticket
Testresult:OK.Totaltests:1;passed:1;failed:0
testmove_unit_tests::move_unit_tests...ok

你可以在这里深入了解单元测试:Unit Tests diem.github.io

本章节谈了很多理论,但这至关重要。在下一章节中,我们将深入研究代码,让 Alice 和 Bob 能够购买门票,甚至可以交易或出售这些门票,并确保每个人都能在演唱会上获得他们想要的座位。敬请关注!

免责声明:本文章仅代表作者个人观点,不代表本平台的立场和观点。本文章仅供信息分享,不构成对任何人的任何投资建议。用户与作者之间的任何争议,与本平台无关。如网页中刊载的文章或图片涉及侵权,请提供相关的权利证明和身份证明发送邮件到support@aicoin.com,本平台相关工作人员将会进行核查。

评论

暂时没有评论,赶紧抢沙发吧!