使用 Arweave、Smartweave 和 Next.js 构建全栈永久应用程序
本教程的代码可以在这里找到
在本教程中,您将学习如何使用Smarweave、Warp和Next.js在 Arweave 上构建全栈 dapp 。
Smartweave TLDR
- 使用 JS、TS 或 Rust 编写智能合约
- 执行任意数量的计算,无需额外费用
- 永远不必担心 gas 优化
- 没有国家膨胀
- 可直接处理丰富的内容/大文件
- Warp提供增强功能(速度、缓存、sdks)
我们将要构建的应用程序是一个全栈博客,这意味着您将拥有一个开放、公共和可组合的后端,可以在任何地方传输和重复使用(不仅仅是在这个应用程序中)。
与大多数处理大量或任意量数据的区块链应用程序不同,Smartweave 允许将此应用程序的所有状态直接存储在链上。
我认为这是一个很好的例子,因为它既不会太基础以至于枯燥乏味,也不会太复杂以至于令人困惑。它展示了如何完成大多数你需要并希望理解的基本操作,以便构建更复杂、更精妙的应用程序。
关于 Arweave
Arweave是一种 web3 协议,允许开发人员永久存储图像、视频和 pdf 等文件以及单页 Web 应用程序。
Arweave 提出了永久网络的概念——一个永久的、全球性的、社区拥有的网络,任何人都可以为其做出贡献或获得报酬来维护。
智能编织
Arweave 还推出了SmartWeave:一种智能合约协议,允许开发人员在 Arweave 之上构建永久应用程序。
当您发布 Smartweave 合约时,程序的源代码及其初始状态将存储在 Arweave 交易中。
当用户向 SmartWeave 程序写入更新时,他们会将其输入写入新的 Arweave 交易。
为了计算合约状态,SmartWeave 客户端使用合约源代码按顺序执行输入的历史记录。无效交易将被忽略。
通过这样做,SmartWeave 将验证交易的责任推给了用户。
经
Warp(https://warp.cc/)是一种建立在 Arweave 之上的协议,旨在为 Smartweave 应用程序开发提供更好的 DX/UX。
Warp 主要由 3 层组成:
-
核心协议层是原始 SmartWeave 协议的实现,负责与部署在 Arweave 上的 SmartWeave 智能合约进行通信
-
缓存层 - 建立在核心协议层之上,允许分别缓存每个核心协议模块的结果。
这使得您可以从具有大量状态更新的合约中快速检索数据,同时还提供即时交易和合约可用性和最终性。
-
扩展层 - CLI、调试工具、不同的日志记录实现、所谓的“试运行”(即允许快速验证给定合约交互的结果而无需在 Arweave 上编写任何内容的操作)。
入门
现在我们对底层技术有了一些了解,让我们开始构建吧。
先决条件
要成功完成本教程,您应该在您的机器上安装 Node.js 16.17.0 或更高版本。
创建和配置项目
首先,让我们创建 Next.js 应用程序,对其进行配置,并安装依赖项。
npx create-next-app full-stack-arweave
进入新目录并安装以下依赖项:
npm install warp-contracts react-markdown uuid
配置 Next.js 应用
打开package.json
并添加以下配置:
"type": "module",
然后更新next.config.js
以使用 ES 模块来导出nextConfig
:
/* replace */
module.exports = nextConfig
/* with this*/
export default nextConfig
这将使 Next.js 应用程序能够使用 ES 模块。
接下来,将以下内容添加到.gitignore
文件:
wallet.json
testwallet.json
transactionid.js
切勿将钱包信息推送到 GitHub 等任何公共平台。本教程仅涉及
testnet
,但我们会提供代码供您推送到mainnet
。为了以防万一,我们将添加wallet.json
到.gitignore
。
扭曲合同
接下来,让我们创建并测试智能合约。
关于 Smartweave 合约
Smartweave 合约的工作原理如下。
1.应用程序的初始状态定义为 JSON 对象。
对于增加和减少数字的计数器应用程序来说,一些基本的初始状态可能看起来像这样:
{
"counter" : 0
}
2. Smartweave合约的逻辑写在一个名为的函数中handle
。
此函数定义了可在合约上调用的不同操作,用于操作状态。这些操作类似于普通智能合约或程序中的函数。每个操作都会以某种方式更新状态。
使用上述状态的计数器的基本处理程序可能看起来像这样:
export function handle(state, action) {
if (action.input.function === 'increment') {
state.counter += 1
}
if (action.input.function === 'decrement') {
state.counter -= 1
}
return { state }
}
在这个处理程序中,有两个操作 -increment
或decrement
。这里的逻辑非常简单。
3.要更新合约的状态,我们可以writeInteraction
从 Warp SDK 调用。
以下是在服务器上调用此函数时的基本示例:
import { WarpFactory } from 'warp-contracts'
const transactionId = "BA3EIfkKvlPXLk5sEN8loAmp2zr0MezSPhwaujTNli8"
import wallet from './wallet.json'
let warp = WarpFactory.forLocal()
const contract = warp.contract(transactionId).connect(wallet)
await contract.writeInteraction({
function: "decrement"
})
然后,我们可以随时读取状态:
const contract = warp.contract(transactionId).connect();
const { cachedValue } = await contract.readState();
撰写合同
现在我们已经对合约的工作原理有了基本的了解,让我们开始编写一些代码。
在项目的根目录中,创建一个名为 的新文件夹warp
。
在此文件夹中,创建一个名为的新文件contract.js
:
/* warp/contract.js */
export function handle(state, action) {
/* address of the caller is available in action.caller */
if (action.input.function === 'initialize') {
state.author = action.caller
}
if (action.input.function === 'createPost' && action.caller === state.author) {
const posts = state.posts
posts[action.input.post.id] = action.input.post
state.posts = posts
}
if (action.input.function === 'updatePost' && action.caller === state.author) {
const posts = state.posts
const postToUpdate = action.input.post
posts[postToUpdate.id] = postToUpdate
state.posts = posts
}
if (action.input.function === 'deletePost' && action.caller === state.author) {
const posts = state.posts
delete posts[action.input.post.id]
state.posts = posts
}
return { state }
}
这是我们的博客应用程序的合同。
我们有创建、更新和删除帖子(CRUD )的功能。我们还有一个initialize
函数,它添加了一条基本的授权规则,该规则仅允许博客所有者通过将合约部署者设置为所有者来调用这些函数。
warp
接下来,在名为的目录中创建一个文件state.json
并添加以下 JSON:
{
"posts": {},
"author": null
}
posts
这里,我们有一个设置为空对象的初始状态,并且author
设置为null。
我们的合同已经完成,现在让我们编写代码来部署、更新和读取合同的状态。
部署、更新和读取
configureWarpServer.js
接下来,在目录中创建一个名为的新文件warp
。
import { WarpFactory } from 'warp-contracts'
import fs from 'fs'
/*
* environment can be 'local' | 'testnet' | 'mainnet' | 'custom';
*/
const environment = process.env.WARPENV || 'testnet'
let warp
if (environment === 'testnet') {
warp = WarpFactory.forTestnet()
} else if (environment === 'mainnet') {
warp = WarpFactory.forMainnet()
} else {
throw Error('environment not set properly...')
}
async function configureWallet() {
try {
if (environment === 'testnet') {
/* for testing, generate a temporary wallet */
try {
return JSON.parse(fs.readFileSync('../testwallet.json', 'utf-8'))
} catch (err) {
const { jwk } = await warp.generateWallet()
fs.writeFileSync('../testwallet.json', JSON.stringify(jwk))
return jwk
}
} else if (environment === 'mainnet') {
/* for mainnet, retrieve a local wallet */
return JSON.parse(fs.readFileSync('../wallet.json', 'utf-8'))
} else {
throw Error('Wallet not configured properly...')
}
} catch (err) {
throw Error('Wallet not configured properly...', err)
}
}
export {
configureWallet,
warp
}
在这个文件中,我们warp
根据我们是否处于testing
或mainnet
(生产)环境中来配置服务器。
然后,我们有一个函数来配置用于部署合约的钱包。如果是测试,我们可以使用 自动启动一个测试钱包generateWallet
。如果是生产环境,我们可以选择在本地导入钱包。
现在我们已经能够配置钱包和 Warp 服务器,让我们创建部署合约的功能。
部署合约
deploy.js
在目录中创建一个新文件,warp
其代码如下:
import fs from 'fs'
import { configureWallet, warp } from './configureWarpServer.js'
async function deploy() {
const wallet = await configureWallet()
const state = fs.readFileSync('state.json', 'utf-8')
const contractsource = fs.readFileSync('contract.js', 'utf-8')
const { contractTxId } = await warp.createContract.deploy({
wallet,
initState: state,
src: contractsource
})
fs.writeFileSync('../transactionid.js', `export const transactionId = "${contractTxId}"`)
const contract = warp.contract(contractTxId).connect(wallet)
await contract.writeInteraction({
function: 'initialize'
})
const { cachedValue } = await contract.readState()
console.log('Contract state: ', cachedValue)
console.log('contractTxId: ', contractTxId)
}
deploy()
该deploy
函数将把合约部署到 Arweave,并将交易 ID 写入本地文件系统。
读取状态
接下来,我们创建一个名为的文件,read.js
其代码如下:
import { warp, configureWallet } from './configureWarpServer.js'
import { transactionId } from '../transactionid.js'
async function read() {
let wallet = await configureWallet()
const contract = warp.contract(transactionId).connect(wallet);
const { cachedValue } = await contract.readState();
console.log('Contract state: ', JSON.stringify(cachedValue))
}
read()
撰写更新
我们要编写的最后一个函数是创建新帖子。
在warp
目录中,创建一个名为的新文件,createPost.js
其代码如下:
import { warp, configureWallet } from './configureWarpServer.js'
import { transactionId } from '../transactionid.js'
import { v4 as uuid } from 'uuid'
async function createPost() {
let wallet = await configureWallet()
const contract = warp.contract(transactionId).connect(wallet)
await contract.writeInteraction({
function: "createPost",
post: {
title: "Hi from first post!",
content: "This is my first post!",
id: uuid()
}
})
}
createPost()
测试一下
现在我们可以测试一切了。
要部署合约,请从warp
目录运行以下命令:
node deploy
这应该将合约部署到测试网。
合约部署完成后,您可以使用Sonar 区块浏览器查看合约及其当前状态。合约交易 ID 将在 中提供
transactionid.js
。请务必切换到测试网以查看本次部署的合约。
接下来,我们来读取当前状态:
node read
合同的返回状态应该是这样的:
{"state":{"posts":{},"author":"-YzqAM_VDCqFZEk6iZ3B8Y-b6SxHoh0F1SvjOCW49nY"},"validity":{"36CmMGSlrGNvvCCfldtiUza4ZnQ9_bFW0YoEh8NCVe0":true},"errorMessages":{}}
现在,让我们创建一个帖子:
node createPost
现在,当我们读取更新状态时,我们应该看到更新状态下的新帖子:
node read
构建 Web 应用程序
现在我们了解了如何使用 Warp 部署和测试 Smartweave 合约,让我们构建一个与之交互并使用它前端应用程序。
由于我们正在构建的应用程序是一个博客,因此我们需要创建两个基本视图:
- 查看用户创建的帖子的视图。
- 允许用户创建帖子的视图。
我们还需要一个文件来保存我们将用来配置warp
客户端的函数(类似于我们之前为服务器配置 warp 的方式)。
在应用程序的根目录中创建一个名为的新文件configureWarpClient.js
并添加以下代码:
import { WarpFactory } from 'warp-contracts'
import { transactionId } from './transactionid'
import wallet from './testwallet'
/*
* environment can be 'local' | 'testnet' | 'mainnet' | 'custom';
*/
const environment = process.env.NEXT_PUBLIC_WARPENV || 'testnet'
let warp
let contract
async function getContract() {
if (environment == 'testnet') {
warp = WarpFactory.forTestnet()
contract = warp.contract(transactionId).connect(wallet)
} else if (environment === 'mainnet') {
warp = WarpFactory.forMainnet()
contract = warp.contract(transactionId).connect()
} else {
throw new Error('Environment configured improperly...')
}
return contract
}
export {
getContract
}
创建帖子
接下来在pages
目录中创建一个名为的新文件create-post.js
并添加以下代码:
import { useState } from 'react'
import { getContract } from '../configureWarpClient'
import { v4 as uuid } from 'uuid'
import { useRouter } from 'next/router'
export default function createPostComponent() {
const [post, updatePost] = useState({
title: '', content: ''
})
const router = useRouter()
async function createPost() {
if (!post.title || !post.content) return
post.id = uuid()
const contract = await getContract()
try {
const result = await contract.writeInteraction({
function: "createPost",
post
})
console.log('result:', result)
router.push('/')
} catch (err) {
console.log('error:', err)
}
}
return (
<div style={formContainerStyle}>
<input
value={post.title}
placeholder="Post title"
onChange={e => updatePost({ ...post, title: e.target.value})}
style={inputStyle}
/>
<textarea
value={post.content}
placeholder="Post content"
onChange={e => updatePost({ ...post, content: e.target.value})}
style={textAreaStyle}
/>
<button style={buttonStyle} onClick={createPost}>Create Post</button>
</div>
)
}
const formContainerStyle = {
width: '900px',
margin: '0 auto',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start'
}
const inputStyle = {
width: '300px',
padding: '8px',
fontSize: '18px',
border: 'none',
outline: 'none',
marginBottom: '20px'
}
const buttonStyle = {
width: '200px',
padding: '10px 0px'
}
const textAreaStyle = {
width: '100%',
height: '300px',
marginBottom: '20px',
padding: '20px'
}
阅读和显示帖子
接下来,pages/index.js
使用以下代码进行更新:
import { useEffect, useState } from 'react'
import { getContract } from '../configureWarpClient'
import ReactMarkdown from 'react-markdown'
export default function Home() {
const [posts, setPosts] = useState([])
useEffect(() => {
readState()
}, [])
async function readState() {
const contract = await getContract()
try {
const data = await contract.readState()
console.log('data: ', data)
const posts = Object.values(data.cachedValue.state.posts)
setPosts(posts)
console.log('posts: ', posts)
} catch (err) {
console.log('error: ', err)
}
}
return (
<div style={containerStyle}>
<h1 style={headingStyle}>PermaBlog</h1>
{
posts.map((post, index) => (
<div key={index} style={postStyle}>
<p style={titleStyle}>{post.title}</p>
<ReactMarkdown>
{post.content}
</ReactMarkdown>
</div>
))
}
</div>
)
}
const containerStyle = {
width: '900px',
margin: '0 auto'
}
const headingStyle = {
fontSize: '64px'
}
const postStyle = {
padding: '15px 0px 0px',
borderBottom: '1px solid rgba(255, 255, 255, .2)'
}
const titleStyle = {
fontSize: '34px',
marginBottom: '0px'
}
导航
接下来,更新pages/_app.js
import '../styles/globals.css'
import Link from 'next/link'
function MyApp({ Component, pageProps }) {
return (
<div>
<nav style={navStyle}>
<Link href="/" style={linkStyle}>
Home
</Link>
<Link href="/create-post" style={linkStyle}>
Create Post
</Link>
</nav>
<Component {...pageProps} />
</div>
)
}
const navStyle = {
padding: '30px 100px'
}
const linkStyle = {
marginRight: '30px'
}
export default MyApp
测试一下
现在让我们运行该应用程序并进行测试:
npm run dev
当应用程序加载时,服务器上创建的帖子应该在 UI 中呈现。
接下来,创建一个帖子。如果帖子创建成功,它会显示在主页的帖子列表中。
后续步骤
部署到主网
如果您想部署并连接到 Arweave 主网,请按照以下步骤操作:
-
从水龙头请求 AR 代币,从交易所购买,或在changeNOW等交易所兑换
-
将新钱包下载到名为 的文件中
wallet.json
。请务必将此文件添加到您的.gitignore
,并且永远不要公开或推送到 Git。 -
mainnet
在您将要部署的终端会话中将本地环境变量设置为:export WARPENV=mainnet
-
在应用程序的根目录中创建
.env.local
文件并添加以下环境变量:NEXT_PUBLIC_WARPENV=mainnet
-
从目录部署合约
warp
:node deploy
-
运行应用
npm run dev
学习资源
如果您想深入了解 Warp、Smartweave 和 Arweave,请访问Warp Academy
文章来源:https://dev.to/dabit3/building-full-stack-applications-with-arweave-and-nextjs-28hg