基于比特币:使用 Stacks 进行全栈 Web3 开发简介
大多数人想到 Web3 开发时,首先想到的就是以太坊。但您是否知道,在比特币之上构建完全去中心化的 Web3 DApp 也是可能的?
Stacks 是比特币网络上最大的 web3 项目。Stacks 是一个区块链,它允许我们在比特币之上构建完全去中心化的应用程序,而无需修改比特币本身。
这是一个简单的陈述,但是这个简单的句子中却包含了许多令人惊叹的技术和许多有趣的、有时甚至是有争议的概念。
如果您还不熟悉 Stacks,我强烈建议您深入了解并学习基础知识。
从高层次上讲,Stacks 允许我们构建完全去中心化的软件,最终利用称为转移证明的独特共识机制在比特币上进行结算。
Stacks 的独特之处在于它是一条第 1 层链,拥有自己的代币,拥有自己的智能合约语言,并且所有 Stacks 交易最终都在比特币上结算。
这使我们能够构建由比特币保护的、具有完全表达力的智能合约和 dapp,而无需修改比特币本身。
如果您有兴趣深入了解 Stacks 的工作原理以及它与其他区块链技术的不同之处,我强烈推荐 Jude Nelson 的文章《Stacks 是什么样的区块链?》。
本教程将作为使用 Clarity、Next 和 Stacks.js 在其上实际构建事物的实用介绍。
我对 Stacks 了解得越多,参与到社区中,我就越对这个生态系统和技术着迷。
我为有兴趣开始在 Stacks 上构建 dapps 的开发人员编写了本教程。
它旨在成为一个入门教程,带您从 0 到一个完整但简单的 Stacks dapp。
如果你是一位经验丰富的 JS 开发者,并且对在 Stacks 上构建应用感兴趣,那么你应该很适合你。如果你有使用其他语言(例如 Solidity)编写智能合约的经验,那就更好了。
我编写本指南是为了帮助新开发人员尽快开始在 Stacks 上构建。
如果有人发现我犯了错误或者有些地方可以改进,请告诉我!
本教程是我正在编写的更广泛的全栈 Stacks 开发指南的 MVP,因此如果您认为某些内容可以改进、添加、删除等,请务必与我们联系。
什么是 Stacks?
从高层次上讲,它能够在比特币之上实现智能合约。
以太坊和比特币之间的主要区别之一是以太坊是可编程的并且具有创建智能合约的能力。
这就是 DeFi 市场在以太坊区块链上蓬勃发展的原因。人们可以用 Solidity 编写代码,并将其部署为智能合约,永久存在于区块链上。
Stacks 将这一功能引入比特币。Stacks 致力于在不修改比特币本身的情况下,将功能齐全的智能合约引入比特币。
Stacks 与 Lightning 和DLC等相关技术一起,组成了梦之队,将 DeFi 和智能合约带入世界上最安全的区块链和世界上最稳健的货币——比特币。
这是以太坊相对于比特币的主要优势之一,现在比特币也具备了这种能力,而无需(这是关键点)修改比特币本身。
想象一下,在以太坊生态系统蓬勃发展之时加入其中,那将是多么令人惊喜的机会。我相信,如今参与 Stacks 并在上面进行开发,其机会与它不相上下,甚至更大。
作为一个相关的好处,由于 Stacks 独特的转移证明共识机制,它实际上能够回收比特币的能源使用,因此它不会增加环境负荷,而是从比特币已经使用的电力中获得更多利益。
由于能源循环利用的特性,Stacks 的耗电量实际上甚至比 PoS 链还要低。用 Jude Nelson 的话来说,“比特币不是浪费能源,而是能源利用不足。”
Stacks 开发者生态系统
在整个教程中,我们将介绍使用其中一些工具构建完整(虽然是基础)应用程序的过程。
明晰
首先,Clarity 是 Stacks 上编写智能合约的语言。可以将其视为 Stacks 版的 Solidity。
它与 Solidity 和 JavaScript 等语言的感觉不同,因此一开始可能会感觉有点奇怪,但随着使用次数的增加,你会发现它变得更容易理解,而且简单性非常棒。
Clarity 的酷之处在于它被有意设计为编写安全的智能合约,并使编写危险代码变得困难,并且您可以在运行代码之前看到它的作用。
Stacks 区块链本身也具备一些有助于编写安全合约的功能,即后置条件。这些条件在合约运行结束时必须成立。如果不成立,合约函数就会终止。这可以帮助保障转账资金的安全。
单簧管
Clarinet 是一个 CLI,它允许我们轻松编写、测试和部署智能合约。
如果你来自以太坊世界,这与 Hardhat 或 Truffle 之类的东西类似。
Clarinet 是我们工具集的一部分,它允许我们在本地构建和测试我们的合约,并将它们部署到测试网,最后部署到主网。
本教程将使用 Clarinet,但不会涉及代码测试。测试是一个复杂的话题,值得专门写一篇文章来阐述。
如果您有兴趣将测试集成到这个项目中,Nikos Baxevanis已经撰写了一篇关于使用 Clarinet 进行测试的出色介绍,其中以我们在此构建的应用程序为起点。
Stacks.js
Stacks.js 是我们通过前端应用程序与智能合约进行交互的方式。
再次从以太坊世界来看,这可以与 ethers.js 或 web3.js 进行比较。
这将帮助我们完成诸如身份验证、与存储系统交互、与智能合约交互以及我们将要讨论的堆叠等其他事情。
实际上,我们将在本教程中使用几个不同的包,我们将在使用它们时逐一介绍它们。
盖亚
Gaia 是 Stack 的存储解决方案。它有点像一个混合系统,利用了 Amazon S3 等传统云存储。
Gaia 允许我们在链下存储应用程序和用户数据,但仍然可以从我们的 Stacks 应用程序安全地访问它。
区块链应该只用于存储关键元数据,因为直接在区块链上存储数据成本高昂且速度慢。
我们不会在本教程中使用 Gaia,因为我们不会存储任何链下数据,但我计划在未来编写一个结合使用 Gaia 存储的教程。
值得关注的杰出人物和组织
现在我们已经了解了开发者生态系统,如果您想沉浸在 Stacks 的世界中,这里列出了您绝对应该在 Twitter 上关注的人。
kenny - 无耻的插件,这就是我
Stacks
Hiro
Stacks Foundation
Stacks Accelerator
这只是触及皮毛,但它将是您融入社区的绝佳起点。
我们将要构建什么
在本系列中,我们将构建一款名为 Sup 的应用。Sup 是一款简单的应用,允许访问我们网站的访客支付指定数量的 STX 来发送消息。
因此,访问者将能够访问我们的网站并支付少量 STX 费用以在我们的网站上发布消息。
我们还将在同一页面上显示当前登录用户的消息。
这个基本的应用程序将是一个很好的用例,可以帮助您熟悉 Stacks 开发生态系统的各个部分以及 Clarity 语言的基础知识。
我们将在 Clarity 中编写智能合约,使用 Clarinet 作为开发环境,并使用 stacks.js 将前端连接到 Stacks 链。
对于前端,我们将使用 Next 和 Tailwind。
在学习本系列之前,您应该熟悉使用 JS 和 React 构建网站和应用程序的基础知识。
不需要具备 Stacks 或 Clarity 的现有知识,我们将在此介绍基础知识。
如果您在任何时候遇到困难、迷失或偏离轨道,您可以将您的代码与Sup GitHub repo进行比较。
如何使用本教程
如果您已经学习过许多教程,那么您可能熟悉教程地狱的概念。
在这种情况下,您会无休止地学习教程,但却感觉自己没有足够的能力真正构建任何东西。
我要告诉你一个秘密,教程实际上并不是学习东西的好方法。
那我为什么要写这个呢?
因为它们是了解某事物、从高层次了解其工作原理以及如何将多种技术结合在一起的好方法。
但要真正学会,你需要独自努力地构建一些东西。
因此,请阅读本教程并跟随它来熟悉它的工作原理。
然后,这一点至关重要,从头开始,自己去创造一些东西。一些与众不同的东西。
构建它并部署它以供全世界观看。
这会很困难而且令人沮丧,但这是真正学习的唯一方法。
如果您需要我,请随时联系我,我会尽力为您提供帮助。
Stacks Discord是另一个寻求帮助的好地方
我们开始吧。
项目设置
我们需要做的第一件事是使用 Clarinet 和 Next 设置我们的开发环境。
首先,创建一个主项目文件夹,其中将包含我们的智能合约代码和前端 Next 应用程序。
mkdir sup && cd sup
现在让我们通过运行以下命令来使用 Tailwind 设置 Next 应用:
npx create-next-app -e with-tailwindcss frontend
进入该目录并运行npm run dev
以确保一切正常。
如果一切正常,您应该能够访问localhost:3000
并查看默认模板:
保持该终端窗口打开并打开一个新的窗口以使用 Clarinet 设置我们的智能合约。
Clarinet 在编写智能合约方面非常实用,因为它允许我们将合约部署到我们自己的小型本地区块链上。我们可以立即创建账户并为其分配 STX,并且无需等待合约在实际运行的区块链上处理即可运行。
此外,如果您使用 VS Code,您可能需要设置Clarity 扩展。
如果您使用的是 Mac,则可以使用 Homebrew 安装 Clarinet。
brew install clarinet
一旦安装了 Clarinet,我们就可以通过进入主sup
文件夹并运行来实例化一个新项目:
clarinet new backend
要在其他系统上安装 Clarinet,请查看Hiro 的网站。
进入该目录,你会看到几个文件夹:contracts、settings 和 tests。你还会看到一个Clarinet.toml
Clarinet 配置文件。
如果我们查看设置文件夹,我们会看到一些与不同网络相对应的配置文件。
当我们使用 Clarinet 进行开发时,我们是在Devnet
网络中工作的,这是一个仅在内存中运行的本地 Stacks 网络。如果你打开该文件,你会看到我们正在设置一些模拟账户来使用。
[network]
name = "devnet"
[accounts.deployer]
mnemonic = "twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw"
balance = 100_000_000_000_000
# secret_key: 753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601
# stx_address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
# btc_address: mqVnk6NPRdhntvfm4hh9vvjiRkFDUuSYsH
[accounts.wallet_1]
mnemonic = "sell invite acquire kitten bamboo drastic jelly vivid peace spawn twice guilt pave pen trash pretty park cube fragile unaware remain midnight betray rebuild"
balance = 100_000_000_000_000
# secret_key: 7287ba251d44a4d3fd9276c88ce34c5c52a038955511cccaf77e61068649c17801
# stx_address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5
# btc_address: mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC
[accounts.wallet_2]
mnemonic = "hold excess usual excess ring elephant install account glad dry fragile donkey gaze humble truck breeze nation gasp vacuum limb head keep delay hospital"
balance = 100_000_000_000_000
# secret_key: 530d9f61984c888536871c6573073bdfc0058896dc1adfe9a6a10dfacadc209101
# stx_address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG
# btc_address: muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG
现在我们开始学习 Clarity 并撰写我们的第一份合同。我们会继续使用 Clarinet 并进一步学习。
我们的第一个清晰的智能合约
在我们开始之前,请确保您位于sup/backend
目录内。
首先,让我们创建我们的合约
clarinet contract new sup
此 sup 合同将处理用户向我们的网站发送消息的能力。
运行此程序后,您会注意到创建了一些东西。
首先,sup.clar
我们在 contract 文件夹中创建了一个文件。我们将在这里编写我们的合约。
其次,sup_test.ts
在 tests 文件夹中创建了一个文件。这样我们就可以使用 Clarinet 测试工具(一个用于测试 TypeScript 智能合约的工具)来测试我们的文件了。
如前所述,我们不会在本教程中进行测试,但clarinet test
如果您好奇,可以自己检查生成的代码并运行。
如果您有兴趣将测试集成到这个项目中,Nikos Baxevanis已经撰写了一篇关于使用 Clarinet 进行测试的出色介绍,其中以我们在此构建的应用程序为起点。
最后,它更新了我们的Clarinet.toml
文件以引用新合同:
[contracts.sup]
path = "contracts/sup.clar"
depends_on = []
Clarity 的设计目标是高度可读,旨在简化编写安全的智能合约。它是可判定的,这意味着您可以在运行代码之前确定其功能。
它默认是可预测和安全的,使得编写安全的智能合约变得尽可能简单,这非常重要,因为一旦部署,它们就会永远部署。
清晰度一开始可能看起来有点奇怪,特别是如果你来自 JavaScript 或 Solidity 的世界,但一旦你开始使用它,你就会爱上它的简洁和简单。
最好的学习方法是实践,所以让我们编写这个智能合约,我们将逐步介绍每一行代码的作用,并涵盖 Clarity 概念和语法。
打开sup.clar
创建的文件然后我们开始吧。
Clarinet 为我们生成了一些占位符内容,这为我们创建自己的合同提供了一个很好的大纲:
合同记录
首先,我们可以在描述前加上;;
,这是 Clarity 的代码注释语法。以下是一些关于如何有效地记录合同的有用信息。
我们将写:
“智能合约负责向区块链写入消息,并收取少量 STX 费用”
对于其余的代码,让我们仔细想想我们的智能合约实际上需要做什么。
我总是喜欢在开始编码之前写出代码的功能以帮助我开始。
通常我会在代码文件中的伪代码注释中执行此操作。
因此,对于 Sup,我们需要网站访问者能够向区块链发送消息。作为该过程的一部分,我们需要他们能够将一定数量的 STX 从他们的钱包转移到我们的钱包。
这会带来一些需要创建和使用的不同数据以及一些功能。
首先,我们需要用户能够验证自己的身份,并将他们的地址从前端传递到合约本身。
我们将稍后处理前端部分,但这将通过与 Hiro Web Wallet 集成来处理。
我们还需要他们能够输入消息并将其传递给我们的合同。
就功能而言,我们需要一个允许访问者发布消息的功能。
我们还需要几个 getter 函数,它们将处理检索我们存储的变量的值,例如消息。
定义变量和常量
首先,让我们将我们的地址(我们希望人们将 STX 转移到的地址)定义为一个常量。
Clarity 中的常量是在整个合同过程中不会改变但我们需要引用的值。
现在,让我们为我们的地址创建一个常量并用于存储错误消息。
如果您尚未安装和设置Hiro Web Wallet,请立即执行此操作,我们稍后会使用它。
现在,让我们在 DevNet 链上使用 Clarinet 生成的地址之一。我们可以使用文件中列出的accounts.deployer
地址Devnet.toml
。
(define-constant receiver-address 'ST3QFME3CANQFQNR86TYVKQYCFT7QX4PRXM1V9W6H)
这是我们的第一段 Clarity 代码。我们在这里做的是,我们要用 定义一个新的常量define-constant
。
我们希望命名该常量receiver-address
,并将其作为值为的主体ST3QFME3CANQFQNR86TYVKQYCFT7QX4PRXM1V9W6H
。Clarity 中的主体可以被视为与 Stacks 地址对应的用户帐户。
在这种情况下,您需要用Devnet.toml
文件中生成的地址之一替换该地址。
字符'
是我们表示正在设定主体的方式。
注意括号。Clarity 中的所有内容都包含在括号中。这需要一些时间来适应,而且可能有点难以跟踪。
如果您使用 VS Code,您可以启用设置来突出显示匹配的括号对,这可以更轻松地跟踪 Clarity 中的内容。
将以下内容添加到settings.json
VS Code 中的文件以启用此功能:
"editor.bracketPairColorization.enabled": true
请务必打开Devnet.toml
文件并添加该地址,而不是使用此处列出的地址。这是我本地 Clarinet 实例生成的地址,您的地址会有所不同。
接下来我们需要设置变量。常量和变量的主要区别在于,变量可以在整个合约过程中更改,而常量不能。
我喜欢在合同的顶部定义常量和变量,但如果您愿意,也可以在底部定义它们。
对于 Sup,我们只需要两个变量,一个用于存储我们收到的 sup 总量,一个用于存储每条消息的映射。
Clarity 中的映射可以将两种类型和数据片段映射在一起。在我们的例子中,我们希望将一条消息映射到一个主体。
因此,我们的应用程序的工作方式是,当访问者发布消息时,它会向我们的消息图中添加一个新项目,以访问者的地址为键,以消息内容为值。
我们不需要发件人地址的变量,因为 Clarity 有一个内置关键字tx-sender
可以帮我们获取该地址。
让我们在;; data maps and vars
评论下面定义这些。
(define-data-var total-sups uint u0)
(define-map messages principal (string-utf8 500))
这里的第一行表示我们要定义一个名为 total-sups 的新变量,我们希望它是一个无符号整数,并且我们希望默认值是一个无符号整数 0。
底线是说我们要定义一个名为 的新映射messages
。它将包含由主体(对应于最多 500 个字符的 utf8 字符串)组成的数据对。
我们可以通过查找地址来检索该字符串。
这意味着我们正在创建一个值映射,其中键是主体,值是字符串。
这就是我们将如何跟踪人们在网站上写的所有消息。
请记住,区块链在某种程度上是一个分布式数据库。因此,此映射中的数据将持久保存在区块链上。我们将在本教程的后面部分讨论如何读取它。
查阅Clarity Book来了解更多有关 Clarity 中可用的不同类型以及如何声明它们的信息。
附注:注意到这些地址是以 开头的ST
吗?这意味着我们处理的是测试网上的地址。如果是在主网上,地址会以SP
或SM
开头。只是需要注意一点。
定义函数
接下来让我们创建一个简单的 getter 函数来检索我们存储的变量。
我们希望能够从前端调用这个 getter 函数,所以它需要是公开的。
;; public functions
我们将在标题下方创建它。
(define-read-only (get-sups)
(var-get total-sups)
)
这些都非常简单,本质上是说我们想要定义一个新的只读函数(只读是因为我们不修改任何数据,只是读取它)称为get-sups
,然后使用检索该值var-get
。
我们还想设置一个 getter 函数来访问映射到当前登录用户的消息。
(define-read-only (get-message (who principal))
(map-get? messages who)
)
这map-get?
就是我们从 map 中检索值的方法,这里我们获取的条目的主值等于我们传递给函数的值。在这个read-only
函数中,我们传递了一个名为 的变量,who
其类型为principal
。这就是我们在创建的 map 中查找正确消息的方法。
只读函数可以在我们的合约内部和外部访问。
如果找到条目,它将返回一个可选值、一个值some
或一个none
值。同样,请参阅Clarity 手册以了解更多含义。
现在让我们创建实际处理将消息写入区块链并传输 STX 的函数。
(define-public (write-sup (message (string-utf8 500)) (price uint))
(begin
(try! (stx-transfer? price tx-sender receiver-address))
(map-set messages tx-sender message )
(var-set total-sups (+ (var-get total-sups) u1))
(ok "Sup written successfully")
)
)
好吧,这里发生了很多事,所以让我们来分解一下。
首先,我们定义一个名为的公共函数write-sup
,它将接受两个参数,即消息和价格。
然后我们开始一个begin
代码块,它将多个表达式分组,并返回最后一个表达式的值。这个简单的结构是必要的,因为函数体只能包含一个表达式。你可能熟悉其他编程语言,它们使用花括号 { ... } 来实现同样的目的。
该begin
代码块允许我们将多个函数调用包装在一个代码块中。要理解我的意思,请注释掉代码块的开头和结尾begin
,然后运行clarinet check
。你会看到一个错误,提示参数数量不正确。
该块的第一行是使用try!
函数来传输STX。
摘自《清晰书》:
该try!
函数接受一个optional
或一个响应类型,并将尝试解开它。解开是提取内部值并返回它的行为。
在本例中,我们尝试执行 stx-transfer? 函数。该函数返回一个响应类型。如果调用成功,它将返回(ok true)
。该try!
函数解包了内部值,因此返回 true。
如果失败,它将返回错误并退出write-sup
函数。所有以 ! 结尾的 Clarity 函数都可以提前退出控制流。
该stx-transfer?
函数接受几个参数:以微堆栈表示的价格、发送 STX 的主体和接收 STX 的主体。
messages
接下来,我们将在地图中设置与向传递的消息发送交易的人相对应的项目值。
total-sups
最后,我们使用运算符增加变量的值+
。
这意味着:将total-sups
值设置为当前total-sups
值加 1。
Clarity 中使用的波兰表示法可能需要一些时间来适应,但只要练习告诉自己代码在做什么,你就会很快掌握它。
一个有用的心理模型是将符号视为函数名称,将被操作的数字视为参数。
最后,如果函数成功运行,我们将返回一个字符串来表明这一点。
为了检查是否有任何语法错误,我们可以在项目文件夹clarinet check
中运行。backend
如果您一直按照步骤操作,它将输出语法正确,但我们对message
变量有一个警告:
这里发生了什么?
请记住,Clarity 旨在帮助我们编写安全的代码。秉承这一理念,Clarinet 也提供了一些检查来帮助我们实现同样的目标。
Clarinet 正在帮助我们防范不受信任的数据。盲目接受不受信任的用户输入是造成大量软件安全漏洞的根源。
由于这是一个公共函数,如果我们的数据被接受作为write-sup
函数的参数,那么所有数据都是不可信的。
之所以发出此警告,是因为最近 Clarinet 中新增了一项名为“check-checker”的功能。
当我们运行时,clarinet check
它将检查我们的代码是否正在检查不受信任的输入数据,以确保它是合法的。
在我们的例子中,我们有不受信任的输入源message
并使用它来修改我们的messages
地图。
为了清除它并通过检查,我们需要对不受信任的输入添加某种检查。然而,我们实际上希望用户能够在这里输入任何他们想要的内容,因为这正是添加消息的全部意义所在。
除了将这些数据分配给我们地图中的用户主体之外,我们不对这些数据做任何事情,并且用户应该能够写入他们想要的任何内容。
因此,为了解决这个错误并告诉检查器一切正常,我们可以在map-set
调用上方添加它。
;; #[allow(unchecked_data)]
这样,我们的检查就通过了。
作为开发人员,您有责任添加可能需要运行的任何检查,以确保只有您期望的数据才被允许传递给该函数。
了解此功能非常重要,因为它涉及安全隐患。我强烈建议您阅读Hiro 的相关文章。
你刚刚编写了一个 Clarity 智能合约。现在让我们切换到前端,以便实际使用它。
使用 Next 和 Stacks.js 创建前端
首先,让我们先运行一些样板前端代码。我们这里不打算学习 React,所以直接复制粘贴到frontend/pages/index.tsx
Next 应用的主页面即可。
import { useState, useEffect } from "react";
import Head from "next/head";
export default function Home() {
const [message, setMessage] = useState("");
const [price, setPrice] = useState(5);
const [userData, setUserData] = useState({});
const [loggedIn, setLoggedIn] = useState(false);
const handleMessageChange = (e) => {
setMessage(e.target.value);
};
const handlePriceChange = (e) => {
setPrice(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
// Do Stacks things
};
return (
<div className="flex flex-col items-center justify-center min-h-screen py-2">
<Head>
<title>Sup</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="flex flex-col items-center justify-center w-full flex-1 px-20 text-center">
<h1 className="text-6xl font-bold mb-24">Sup</h1>
<form onSubmit={handleSubmit}>
<p>
Say
<input
className="p-6 border rounded mx-2"
type="text"
value={message}
onChange={handleMessageChange}
placeholder="something"
/>
for
<input
className="p-6 border rounded mx-2"
type="number"
value={price}
onChange={handlePriceChange}
/>{" "}
STX
</p>
<button
type="submit"
className="p-6 bg-green-500 text-white mt-8 rounded"
>
Post Message
</button>
</form>
</main>
</div>
);
}
这只是设置一个基本的 React 表单。
现在我们可以开始添加一些与 Stacks 交互的功能。
验证
当我们构建 web3 应用时,我们的身份验证方式略有不同。我们不再像传统那样设置用户名、邮箱和密码,而是直接允许用户使用钱包连接到我们的应用。
如果您来自以太坊世界,您可能熟悉 MetaMask 或类似的基于浏览器的钱包。
在 Stacks 世界中最常见的钱包是 Hiro 构建的 Stacks 钱包。
Stacks.js 附带一个connect
包,我们将使用它来连接到 Hiro Wallet for Web 并使用我们的 Web 应用程序验证我们的钱包。
如果您对网络钱包身份验证的实际工作方式感到好奇,Stacks 网站有一些很棒的文档来解释该过程。
我们将重点关注在此实施该方案的实用性。
我们要做的第一件事是安装依赖项。确保你位于frontend
文件夹中并运行
npm install @stacks/connect
现在打开index
包含所有 React 代码的文件并进行设置。
在文件顶部另一个导入语句的正下方添加以下内容。
import { AppConfig, UserSession, showConnect } from '@stacks/connect';
这是我们国家声明的正上方:
const appConfig = new AppConfig(['publish_data']);
const userSession = new UserSession({ appConfig });
在生产应用程序中,您可能希望使用某种状态管理库(如 React Context 或 Zustand)使其在全球范围内可用。
我们在这里所做的是通过告知connect
我们需要publish_data
权限范围来初始化身份验证配置。这将允许我们实际发布数据并与应用程序交互。
接下来我们需要设置函数来处理身份验证。
在我们的功能下添加这个权利handleSubmit
。
function authenticate() {
showConnect({
appDetails: {
name: "Sup",
icon: "https://assets.website-files.com/618b0aafa4afde65f2fe38fe/618b0aafa4afde2ae1fe3a1f_icon-isotipo.svg",
},
redirectTo: "/",
onFinish: () => {
window.location.reload();
},
userSession,
});
}
useEffect(() => {
if (userSession.isSignInPending()) {
userSession.handlePendingSignIn().then((userData) => {
setUserData(userData);
});
} else if (userSession.isUserSignedIn()) {
setLoggedIn(true);
setUserData(userSession.loadUserData());
}
}, []);
这是我们单击按钮实际连接到钱包时调用的函数,我们将在一分钟内完成。
注意:如果您收到无法找到regenerator-runtime
包的错误,运行npm i regenerator-runtime
应该可以解决该问题。
这段代码解释起来很容易,但从高层次上讲,它调用了showConnect
触发 Hiro 钱包显示连接窗口的函数。钱包将负责处理身份验证功能。
完成后,它将运行该onFinish
函数,我们将所有需要完成的后续身份验证工作添加到该函数中。在本例中,我们重新加载页面,以便我们的userSession
请求被获取。
这段appDetails
代码传递了一些 Hiro 窗口会显示的内容。为了演示,我们只添加了 Stacks 的 logo。
第二部分是useEffect
在页面加载时运行的调用,并设置我们的用户会话数据,以及将我们的loggedIn
状态设置为true
。
现在我们需要添加一个按钮,方便用户连接到钱包。我们把它添加到 Suph1
标签的正上方。
<div className="flex w-full items-end justify-center">
<button
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded mb-6"
onClick={() => authenticate()}
>
Connect to Wallet
</button>
</div>
添加后,切换到网络钱包中的测试网络,看看它是否有效。
为了做到这一点,我们首先需要使用 Clarinet 运行一个开发区块链,这样我们就可以进行工作了。
确保已安装Docker并切换到该backend
文件夹。进入文件夹后,运行
clarinet integrate
这将在我们的系统上建立一个本地区块链以用于测试目的。
第一次运行需要一段时间,您将看到一个疯狂的仪表板,其中有系统日志、服务状态、内存池摘要和最小块浏览器。
一旦一切成功启动,我们就可以开始在本地 DevNet 链上与我们的应用程序进行交互。
现在让我们使用该数据,如果用户已登录则显示 Sup 表单,如果用户未登录则显示连接按钮。
我们希望稍微修改一下代码,让表单仅在使用钱包登录后才会显示。此外,我们希望仅在当前未连接钱包时才显示“连接钱包”按钮。
将文件中的代码替换index
为以下内容:
import { useState, useEffect } from "react";
import Head from "next/head";
import { AppConfig, UserSession, showConnect } from "@stacks/connect";
export default function Home() {
const appConfig = new AppConfig(["publish_data"]);
const userSession = new UserSession({ appConfig });
const [message, setMessage] = useState("");
const [price, setPrice] = useState(5);
const [userData, setUserData] = useState({});
const [loggedIn, setLoggedIn] = useState(false);
const handleMessageChange = (e) => {
setMessage(e.target.value);
};
const handlePriceChange = (e) => {
setPrice(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
// Do Stacks things
};
function authenticate() {
showConnect({
appDetails: {
name: "Sup",
icon: "https://assets.website-files.com/618b0aafa4afde65f2fe38fe/618b0aafa4afde2ae1fe3a1f_icon-isotipo.svg",
},
redirectTo: "/",
onFinish: () => {
window.location.reload();
},
userSession,
});
}
useEffect(() => {
if (userSession.isSignInPending()) {
userSession.handlePendingSignIn().then((userData) => {
setUserData(userData);
});
} else if (userSession.isUserSignedIn()) {
setLoggedIn(true);
setUserData(userSession.loadUserData());
}
}, []);
return (
<div className="flex flex-col items-center justify-center min-h-screen py-2">
<Head>
<title>Sup</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="flex flex-col items-center justify-center w-full flex-1 px-20 text-center">
<div className="flex flex-col w-full items-center justify-center">
<h1 className="text-6xl font-bold mb-24">Sup</h1>
{loggedIn ? (
<form onSubmit={handleSubmit}>
<p>
Say
<input
className="p-6 border rounded mx-2"
type="text"
value={message}
onChange={handleMessageChange}
placeholder="something"
/>
for
<input
className="p-6 border rounded mx-2"
type="number"
value={price}
onChange={handlePriceChange}
/>{" "}
STX
</p>
<button
type="submit"
className="p-6 bg-green-500 text-white mt-8 rounded"
>
Post Message
</button>
</form>
) : (
<button
className="bg-white-500 hover:bg-gray-300 border-black border-2 font-bold py-2 px-4 rounded mb-6"
onClick={() => authenticate()}
>
Connect to Wallet
</button>
)}
</div>
</main>
</div>
);
}
好的,我们已经进行了基本身份验证,如果我们连接到我们的钱包,我们将有条件地显示我们的表格。
现在让我们继续看看如何使用此表单通过支付 STX 来实际向链发送消息。
写入数据
我们要做的第一件事是设置我们的前端,以便当我们提交表单时,我们会向 Stacks 区块链生成一个新的交易。
我们需要进行大量的设置才能使其在我们的本地链上正常运行,所以让我们快速完成。
当我们运行时,clarinet integrate
我们开始在我们的机器上运行本地 Stacks 链http://localhost:3999
。
这给我们带来了很多好处,其中最主要的是我们可以测试和交互我们的智能合约,而不必将其部署到公共测试网络。
将 STX 添加到您的本地帐户
在我们的应用中,我们需要像登录用户一样进行交互。因此,我们需要将网页钱包挂接到本地 DevNet 链上,并以第一个账户身份登录。
具体操作如下:
Devnet
通过点击右上角的菜单按钮并选择“更改网络”并选择“Devnet”,切换到网络钱包用户界面中的网络。
然后确保该网络上存在一个用户帐户并且当前已登录。
接下来,点击相同的菜单按钮并选择“查看密钥”
这将显示用于验证该地址的密钥。获取该密钥并将其复制到文件memonic
中的字段中。Devnet.toml
accounts.wallet_1
最后,从 Hiro Web Wallet 中获取帐户地址并将其复制到stx_address
同一文件中的字段上。
执行此操作,重新启动 DevNet,您的帐户 1 中应该会有一堆 STX。
现在我们准备编写处理消息写入的前端代码。
首先我们需要导入一些新内容:
import { useState, useEffect } from "react";
import Head from "next/head";
import {
AppConfig,
UserSession,
showConnect,
openContractCall,
} from "@stacks/connect";
import { uintCV, stringUtf8CV } from "@stacks/transactions";
import { StacksMocknet } from "@stacks/network";
让我们逐一添加这些部分并检查它们的作用。
首先,我们需要使用 来设置我们的网络StacksMocknet
。Mocknet 与 localnet 或 DevNet 相同。
在状态变量声明下方添加:
// Set up the network
const network = new StacksMocknet();
请注意,在生产应用程序中,我们需要在此处设置某种环境变量,以根据我们所处的环境来设置网络。
接下来,让我们添加一些其他状态变量来存储合约地址和名称,以及 sup 消息本身和一些加载变量。在生产环境中,我们需要设置一些常量文件,并根据环境有条件地设置它们。
const [supContractAddress, setSupContractAddress] = useState(
"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM"
);
const [supContractName, setSupContractName] = useState("sup");
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
快速提示:请务必将此处的合约地址替换为系统上生成的部署者地址clarinet integrate
。
接下来我们需要更新我们的handleSubmit
函数来调用write-sup
我们合约中的函数:
const handleSubmit = async (e) => {
e.preventDefault();
const functionArgs = [stringUtf8CV(message), uintCV(price * 1000000)];
const options = {
contractAddress: supContractAddress,
contractName: "sup",
functionName: "write-sup",
functionArgs,
network,
appDetails: {
name: "Sup",
icon: window.location.origin + "/vercel.svg",
},
onFinish: (data) => {
console.log("Stacks Transaction:", data.stacksTransaction);
console.log("Transaction ID:", data.txId);
console.log("Raw transaction:", data.txRaw);
},
};
await openContractCall(options);
};
好吧,我们这里有一些事情要做。
首先,我们创建一个名为的常量,functionArgs
在其中设置需要传递给函数的参数。
我们将价格设置为等于,price * 1000000
因为在我们的合同中,价格以微栈表示。1,000,000 微栈等于 1 STX。
这里的另一个选择是将价格设置为 5000000 并在输入字段中进行转换。
当我们设置要传递给 Clarity 函数的参数时,我们需要将它们转换为 Clarity 变量。这就是我们在这里使用stringUtf8CV
和uintCV
函数所做的。
您可以看到此文件中包含的所有不同功能。
接下来,我们设置一个选项对象,在其中传递刚刚设置的所有内容,并在调用完成后执行一些操作。
最后我们真正调用合约。
但我们还有一点要补充。如果您开始提交此交易,您会在 Hiro Web Wallet 屏幕顶部看到一条消息,提示不会转移任何 stx:
但我们将价格定为 5 STX,那么这是怎么回事呢?
这些就是我在文章前面提到的后置条件。
如果我们现在尝试运行该程序,它将失败,因为提供的后置条件未满足。实际上,我们没有设置任何后置条件来告知我们的钱包我们将要转移一些 STX。
所以它默认认为不会传输任何内容。这些后置条件是 Clarity 众多内置功能之一,可以帮助我们编写安全的代码。
为了解决这个问题,我们需要添加一些后置条件。
在创建functionArgs
常量的位置下面添加以下内容:
const postConditionAddress = userSession.loadUserData().profile.stxAddress.testnet;
const postConditionCode = FungibleConditionCode.LessEqual;
const postConditionAmount = price * 1000000;
const postConditions = [
makeStandardSTXPostCondition(
postConditionAddress,
postConditionCode,
postConditionAmount
),
];
更新导入:
import { useState, useEffect, useCallback } from "react";
import Head from "next/head";
import {
AppConfig,
UserSession,
showConnect,
openContractCall,
} from "@stacks/connect";
import {
uintCV,
stringUtf8CV,
standardPrincipalCV,
hexToCV,
cvToHex,
makeStandardSTXPostCondition,
FungibleConditionCode,
} from "@stacks/transactions";
import { StacksMocknet } from "@stacks/network";
并将后置条件传递给我们的合约调用:
...
network,
postConditions,
appDetails: {
name: "Sup",
icon: window.location.origin + "/vercel.svg",
},
...
现在,如果我们运行它,我们将看到正确的后置条件消息,并且一切都应该成功运行。
太棒了!你刚刚创建了一个应用程序,它可以将数据写入 Stacks 区块链,并在此过程中成功传输 STX。
现在让我们来读取这些数据。
读取数据
读取数据更直接一些,因为我们将调用我们的一个read-only
函数(get-messages
在本例中),并且不必创建交易,因为我们不会修改区块链。
为此,我们可以使用@stacks/transactions
包中名为的函数callReadOnlyFunction
。
首先,继续导入它。
import {
uintCV,
stringUtf8CV,
standardPrincipalCV,
hexToCV,
cvToHex,
makeStandardSTXPostCondition,
FungibleConditionCode,
callReadOnlyFunction
} from "@stacks/transactions";
接下来我们需要通过运行在前端文件夹中安装另一个包npm i @use-it/interval
。
这是一个自定义钩子,可以很容易地在一定时间间隔内运行一个函数,我们将在一分钟内讲到它。
导入它,然后我们开始构建。
import useInterval from "@use-it/interval";
以下是我们处理获取消息的方法。我们将设置一个回调函数来获取与登录用户对应的消息。
然后,我们将在加载时以及每 30 秒调用一次该函数来检查它是否已更改。另一个选择是使用 API 的 websockets 功能,这将为该应用带来更好的用户体验。我把这个留给读者练习。
让我们在handleSubmit
调用下面添加它以使其运行。
const getMessage = useCallback(async () => {
if (
userSession &&
userSession.isUserSignedIn() &&
userSession.loadUserData()
) {
const userAddress = userSession.loadUserData().profile.stxAddress.testnet;
const clarityAddress = standardPrincipalCV(userAddress);
const options = {
contractAddress: supContractAddress,
contractName: supContractName,
functionName: "get-message",
network,
functionArgs: [clarityAddress],
senderAddress: userAddress,
};
const result = await callReadOnlyFunction(options);
console.log(result);
}
}, []);
// Run the getMessage function at load to get the message from the contract
useEffect(getMessage, [userSession]);
// Poll the Stacks API every 30 seconds looking for changes
useInterval(getMessage, 30000);
我们还需要添加一个新的状态变量来存储发布的消息。
const [postedMessage, setPostedMessage] = useState("none");
这里发生了很多事情,让我们来看一下。
我们在这里做的第一件事是定义一个useCallback
钩子。如果你跳到代码底部,我们会在userSession
变量发生变化时运行这个函数,并且每 30 秒重新运行一次。
该getMessage
函数本身首先检查我们是否已登录,然后使用一些选项设置我们的只读合约调用。
看看这里的选项。当我们使用 Clarity 值时,我们需要做一些工作将客户端的值转换为 Clarity 可以理解的格式,因此需要standardPrincipalCV(userAddress)
。
如果您运行所有这些,编写一个 sup,您应该会在交易完成处理后立即看到记录到控制台的值。
现在我们还有一步要走。我们将实际显示我们编写的消息。
我们正在将其引入,我们只需要添加一些逻辑,如果它存在则显示它,或者如果我们的智能合约返回则显示某种“未找到”消息none
。
现在我们需要更改console.log
语句以在函数完成时设置它get-message
。
if (result.value) {
setPostedMessage(result.value.data);
}
最后一步:如果消息存在,我们需要有条件地显示它。
将其添加到结束</form>
标签下方。您还需要将表单和此新添加的内容包装在一个片段中,以便 React 能够正确渲染。
<div className="mt-12">
{postedMessage !== "none" ? (
<p>You said "{postedMessage}"</p>
) : (
<p>You haven't posted anything yet.</p>
)}
</div>
现在,如果我们编写消息并等待一段时间,让交易在本地链上处理完毕,区块也结算完毕,我们就会在用户界面上看到它。请注意,这可能需要一两分钟,您可以在 Clarinet 控制台中监控进度。
恭喜!您刚刚创建了第一个完整的全栈 Stacks 应用。
如果您需要比较,所有这些代码都可以在 GitHub 上找到。这仅仅是您 Stacks 开发之旅的开始,还有很多东西需要学习,还有很多东西需要构建。
接下来去哪里
现在,您已经掌握了一个基本的 Stacks 应用。如果您刚刚开始 Stacks 开发之旅,我建议您从这里开始进行以下几项操作。
接入电源
我强烈推荐加入Stacks 的 Discord 账号。你可以在 Discord 上找到我,我的用户名是 kennny#0001(三个 n)。我的私信和好友请求已开放。
如果您有任何问题、反馈、建议,或者只是打个招呼,请随时在 Stacks Discord 中标记我或通过 DM 给我发消息。
我也很乐意在 Twitter 上进行联系。
添加功能
到目前为止,最好的学习方法是自己动手实践。你已经打下了坚实的基础,学习了基础知识,但你需要自己动手实践才能真正巩固所学知识。
以下是一些可以添加到此以进一步学习的功能的想法:
- 利用
get-sups
我们在前端创建的变量和 getter - 切换到 websockets
- 部署到测试网
- 运行您自己的 API 节点
研究这些主题和其他主题,如果您需要任何指导,请记得联系我们!
进一步了解清晰度
Stacks 基金会开设了一门名为 Clarity Universe 的课程,从头开始教您使用 Clarity 进行智能合约开发。
如果您有兴趣了解有关 Clarity 的更多信息,请注册等候名单。
从事真实项目
Stacks 基金会致力于资助那些为 Stacks 生态系统构建实用产品的人们。如果您准备好运用自己的知识为现实世界构建一些东西,欢迎申请资助。
继续学习和练习
最后,请确保你继续你的旅程,尽可能多地学习和构建。我将创建大量内容,旨在帮助开发者在 Stacks 上构建出色的产品。因此,如果你对如何改进本教程有任何反馈,或者对未来内容有任何建议,欢迎通过 Twitter 或 Discord 与我联系。
文章来源:https://dev.to/stacks/built-on-bitcoin-an-introduction-to-full-stack-web3-development-with-stacks-me9