Код для этого урока можно найти здесь
В этом уроке вы узнаете, как создавать полностековые dapps на Arweave с помощью Smarweave, Warp и Next.js.
Краткое описание Smartweave
- Создавайте смарт-контракты на JS, TS или Rust
- Выполнять произвольные объемы вычислений без дополнительной платы
- Никогда не нужно беспокоиться об оптимизации газа
- Никакого раздувания состояния
- Может напрямую обрабатывать богатый контент / большие файлы
- Warp предлагает улучшения (скорость, кэширование, sdks)
Приложение, которое мы будем строить, — это блог полного стека, что означает, что у вас будет открытый, публичный и композитный бэк-энд, который можно передавать и повторно использовать где угодно (не только в этом приложении).
В отличие от большинства блокчейн-приложений, работающих с большими или произвольными объемами данных, Smartweave позволяет хранить все состояние для этого приложения непосредственно на цепочке.
Я думаю, что это хороший пример, поскольку он не слишком простой, чтобы быть скучным, но и не слишком сложный, чтобы быть запутанным. Он показывает, как сделать большинство основных вещей, которые вам понадобятся и которые вы захотите понять для создания более сложных и изощренных приложений в будущем.
- Об Arweave
- Smartweave
- Warp
- Начало работы
- Предварительные условия
- Создание и настройка проекта
- Настройка приложения Next.js
- Контракты искривления
- О контрактах Smartweave
- Написание контракта
- Развертывание, обновление и чтение
- Развертывание контракта
- Чтение состояния
- Написание обновления
- Тестирование
- Создание веб-приложения
- Создание поста
- Чтение и отображение постов
- Навигация
- Тестирование
- Следующие шаги
- Развертывание в mainnet
- Учебные ресурсы
Об Arweave
Arweave — это протокол web3, который позволяет разработчикам постоянно хранить файлы, такие как изображения, видео и pdf, а также одностраничные веб-приложения.
Arweave представил идею permaweb — постоянной, глобальной, принадлежащей сообществу сети, в которую каждый может внести свой вклад или получать деньги за ее поддержание.
Smartweave
Arweave также представила SmartWeave: протокол смарт-контрактов, который позволяет разработчикам создавать постоянные приложения на базе Arweave.
Когда вы публикуете контракт Smartweave, исходный код программы и ее начальное состояние хранятся в транзакции Arweave.
Когда пользователь пишет обновление для программы SmartWeave, он записывает свои данные в виде новой транзакции Arweave.
Для вычисления состояния контракта клиент SmartWeave использует исходный код контракта для последовательного выполнения истории входов. Недействительные транзакции игнорируются.
Таким образом, SmartWeave перекладывает ответственность за проверку транзакций на пользователей.
Warp
Warp (https://warp.cc/) — это протокол, построенный поверх Arweave и предназначенный для улучшения DX/UX при разработке приложений Smartweave.
Warp состоит из 3 основных слоев:
-
Слой основного протокола — это реализация оригинального протокола SmartWeave, который отвечает за связь со смарт-контрактами SmartWeave, развернутыми на Arweave.
-
Слой Caching — построен поверх слоя Core Protocol и позволяет кэшировать результаты каждого из модулей Core Protocol отдельно.
Это позволяет быстро извлекать данные из контрактов с большим количеством обновлений состояния, а также обеспечивает мгновенную доступность и окончательность транзакций и контрактов.
-
Уровень расширений — CLI, средства отладки, различные реализации протоколирования, так называемые «сухие прогоны» (т.е. действия, позволяющие быстро проверить результат взаимодействия с контрактом, ничего не записывая в Arweave).
Начало работы
Теперь, когда мы знаем немного о базовой технологии, давайте начнем строить.
Предварительные условия
Для успешного прохождения этого руководства на вашей машине должен быть установлен Node.js 16.17.0 или выше.
Я рекомендую использовать nvm или fnm для управления версиями Node.js.
Создание и настройка проекта
Чтобы начать работу, давайте сначала создадим приложение 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 Modules для экспорта 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.testing.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
(production).
Затем у нас есть функция, которая настраивает кошелек, который мы будем использовать для развертывания контракта. Если мы тестируем, мы можем просто создать тестовый кошелек автоматически с помощью 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 и запишет идентификатор транзакции в локальную файловую систему.
Чтение состояния
Далее создадим файл с именем read.js
со следующим кодом:
import { warp } from './configureWarpServer.js'
import { transactionId } from '../transactionid.js'
async function read() {
const contract = warp.contract(transactionId).connect();
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
Это приведет к развертыванию контракта в testnet.
После развертывания контракта вы можете использовать проводник блоков Sonar для просмотра контракта и его текущего состояния. ID транзакции контракта будет доступен в
transactionid.js
. Обязательно переключитесь на testnet, чтобы просмотреть контракт из этого развертывания.
Далее, давайте прочитаем текущее состояние:
node read
Возвращенное состояние контракта должно выглядеть примерно так:
{"state":{"posts":{},"author":"-YzqAM_VDCqFZEk6iZ3B8Y-b6SxHoh0F1SvjOCW49nY"},"validity":{"36CmMGSlrGNvvCCfldtiUza4ZnQ9_bFW0YoEh8NCVe0":true},"errorMessages":{}}
Теперь давайте создадим пост:
node createPost
Теперь, когда мы читаем обновленное состояние, мы должны увидеть новый пост в обновленном состоянии:
node read
Создание веб-приложения
Теперь, когда мы поняли, как развернуть и протестировать контракт Smartweave с помощью Warp, давайте создадим внешнее приложение, которое будет взаимодействовать с ним и использовать его.
Поскольку приложение, которое мы создаем, является блогом, нам нужно создать два основных представления:
- Представление для просмотра постов, созданных пользователем.
- Представление, позволяющее пользователям создавать посты.
Нам также понадобится файл для хранения функции, которую мы будем использовать для настройки 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="/">
<a style={linkStyle}>
Home
</a>
</Link>
<Link href="/create-post" >
<a style={linkStyle}>
Create Post
</a>
</Link>
</nav>
<Component {...pageProps} />
</div>
)
}
const navStyle = {
padding: '30px 100px'
}
const linkStyle = {
marginRight: '30px'
}
export default MyApp
Тестирование
Теперь давайте запустим приложение и протестируем его:
npm run dev
Когда приложение загрузится, пост, созданный на сервере, должен отобразиться в пользовательском интерфейсе.
Далее создайте пост. Если пост успешно создан, он должен появиться в списке постов на главной странице.
Следующие шаги
Развертывание в mainnet
Если вы хотите развернуть и подключиться к сети Arweave mainnet, выполните следующие шаги:
-
Скачайте кошелек ArConnect
-
Запросите 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.