1時間でWeaveDBを使ったTodoアプリを作る
WeaveDBとは現在絶賛開発中のDecentralized NoSQL データベースです。
Arweave上に構築されており、最速で1秒で読み込みと3秒で書き込むことができます。
そんなわかる人にはすごさがわかるWeaveDBですが、執筆時点でチュートリアルに何か所か詰まるところがあったので、僭越ながらそれらを補完しつつTodoアプリを作成するまでを記事にしました。
WeaveDB自体の解説などに関しては開発者の長澤さんが記事を書かれているので、そちらを参照ください
また、参考元のチュートリアルはこちらです。
開発環境
stack | version |
---|---|
node.js | v16.18.1 |
yarn | 1.22.19 |
git | --- |
補足としては筆者はWindows10とMacOS(M1)で動作確認を行っています。 |
1.Deploy Contract
まずWeaveDB自体をセットアップします。イメージとしてはFirebaseのプロジェクト作成です。
1.1 Setup Repository
WeaveDBのリポジトリをクローンしたあとパッケージをインストールします
git clone https://github.com/weavedb/weavedb.git
cd weavedb
yarn
1.2 ウォレット作成
WeaveDBをデプロイするためのArweaveウォレットを作成します。
node scripts/generate-wallet.js mainnet
成功すると/weavedb/scripts/.wallets/wallet-mainnet.json
にウォレットのデータが作成されます。
1.3 WeaveDBをデプロイする
いよいよWeaveDBをデプロイします。チュートリアルだとyarn deploy
を実行するように書かれていますが、これは罠です。
現在のバージョンのリポジトリだとIntmaxやその他もろもろの機能が追加されていて、互換性が失われています。そのため、次のステップでエラーが出ます。
なので/weavedb/scripts/deploy2.js
を作成して以下のスクリプトを貼り付けてください。
const fs = require("fs/promises");
const path = require("path");
const Arweave = require("arweave");
const { WarpNodeFactory } = require("warp-contracts");
const srcTxId = "G7aMmk1Fux6Dqw7M7CFoNQf5KZV2J1UCuZjfEn_VFVM";
const deploy = async () => {
const arweave = Arweave.init({
host: "arweave.net",
port: 443,
protocol: "https",
});
const warp = WarpNodeFactory.memCachedBased(arweave).useWarpGateway().build();
const wallet = JSON.parse(
await fs.readFile(
path.join(__dirname, ".wallets/wallet-mainnet.json"),
"utf-8"
)
);
const walletAddress = await arweave.wallets.jwkToAddress(wallet);
const stateFromFile = JSON.parse(
await fs.readFile(
path.join(__dirname, "../dist/warp/initial-state.json"),
"utf8"
)
);
const initialState = {
...stateFromFile,
...{
owner: walletAddress,
},
};
const res = await warp.createContract.deployFromSourceTx(
{
wallet,
initState: JSON.stringify(initialState),
srcTxId,
},
true
);
console.log(res);
};
deploy()
.catch(console.error)
.finally(() => process.exit(0));
そしてyarn build
したあとこのスクリプトを実行してください
yarn build
node scripts/deploy2.js
成功すると以下のような情報が出ると思います。
{
contractTxId: 'e2n9PMi9oYOfhLRMoEsoZSMHBt681-rdekZnzHd8uc4',
srcTxId: 'G7aMmk1Fux6Dqw7M7CFoNQf5KZV2J1UCuZjfEn_VFVM'
}
このうちのcontractTxId
プロパティは重要なのでどこかにメモしてください。
1.3.5 コントラクトの確認
先ほどデプロイされたコントラクトが本当にデプロイされたか確認しましょう。
SonARというサイトにアクセスしてください。
このサイトは他のチェーンでいうBlockChain Exploerで、デプロイされたコントラクトなどを詳しく調べることができます。
サイト上側の検索に先ほどのcontractTxId
を貼って下側に出てきたContract
の検索候補をクリックしてください。
するとページが遷移してコントラクトの詳細などを見ることができます。
1.4 WeaveDBをマイグレートする
WeaveDBは使用する前にあらかじめデータ構造とセキュリティルールを設定する必要があります。
SQLデータベースでいうマイングレートです。
weavedb/scripts/todo-setup.js
を開いてください。ここでは今回作成するTodoアプリのスキーマとセキュリティルールが定義されており、それらを設定する処理が書かれています。
以下のコマンドで設定できます。CONTRACT_TX_IDは先ほどのものと置き換えてください。
node scripts/todo-setup.js mainnet CONTRACT_TX_ID
先ほどのSonARのTransactionsタブで無事設定できているか確認できると思います。
これで難関のWeaveDBのセットアップは完了です!!
2. Todoアプリのフロントエンドを作成
2.1 Next.jsのプロジェクトを作成
今回の記事用に最低限の構成のNextJSのテンプレートを作成しました。
下のリンクからテンプレートをコピーして、それをパソコンにcloneして開いてください。
今後、レポぢ鳥ををweavedb-todo
として作成した場合の手順をか行きますが、適時置き換えてください。
git clone REPO_URL
cd weavedb-todo
yarn
2.2 パッケージインストール
WeaveDBのアクセスに必要なパッケージをインストールします。
yarn add localforage weavedb-sdk buffer ethers
2.3 SDKの型を作成
weavedb-sdk
はTypescriptの型が付属していないので、TypescriptからSDKを扱いづらいです。
そこで、簡易かつ私の推測ですがdtsファイルを作成したので、/weavedb-todo/src/types/weavedb-sdk.d.ts
に下記のコードを貼り付けてください。
interface Window {
ethereum: any | undefined;
}
declare var window: Window;
declare module "weavedb-sdk" {
export interface ArweaveConfig {
host?: string;
protocol?: string;
port?: string | number;
timeout?: number;
logging?: boolean;
logger?: Function;
network?: string;
}
export type SigningFunction = (tx: Transaction) => Promise<void>;
export interface ArWallet {
kty: string;
e: string;
n: string;
d?: string;
p?: string;
q?: string;
dp?: string;
dq?: string;
qi?: string;
}
export type WarpSigner = SigningFunction | ArweaveWallet | "use_wallet";
export interface EthWallet {
wallet: string;
privateKey: string;
}
export interface WeaveDBConfig {
arweave?: ArweaveConfig;
contractTxId: string; // maybe
wallet: WarpSigner;
name: string;
version: string;
EthWallet?: string | EthWallet; //maybe
web3?: any;
}
export type OP = {
__op: string;
} & any;
export default class SDK {
constructor(config: WeaveDBConfig);
cget<T = any>(path: string, ...query: string[][]): Promise<T>;
add<T = any>(data: T, path: string, user: EthWallet): Promise<void>;
update<T = any>(
data: T,
path: string,
id: string,
user: EthWallet
): Promise<void>;
delete(path: string, id: string, user: EthWallet): Promise<void>;
ts(): OP;
signer(): OP;
createTempAddress(
address: string
): Promise<{ tx: any; identity: any; err: any }>;
}
}
2.4 ページを作成
/weavedb-todo/src/pages/index.tsx
に下記のコードを貼り付けてください。
ログイン処理に関しては後で解説しますが、それ以外はFirestoreなどと大きく変わらないので省きます。
import { useState, useEffect, useCallback } from "react";
import lf from "localforage";
import SDK, { EthWallet } from "weavedb-sdk";
import { Buffer } from "buffer";
import { ethers } from "ethers";
import clsx from "clsx";
const contractTxId = WEAVEDB_CONTRACT_TX_ID
const arweave_wallet = ARWEAVE_WALLET_JSON
type Task = {
block: { height: number; timestamp: number };
data: {
date: number;
done: boolean;
task: string;
user_address: string;
};
id: string;
setter: string;
};
export default function Home() {
const [db, setDb] = useState<SDK | null>();
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState("");
const [input, setInput] = useState("");
const [user, setUser] = useState<EthWallet | null>(null);
const [tab, setTab] = useState<"ALL" | "MY">("ALL");
const getTasks = useCallback(async () => {
if (!db) return;
try {
setLoading("getTasks");
setTasks(await db.cget("tasks", ["date", "desc"]));
} catch (e) {
console.error(e);
} finally {
setLoading("");
}
}, [db]);
const getMyTasks = useCallback(async () => {
if (!db || !user) return;
try {
setLoading("getTasks");
setTasks(
await db.cget(
"tasks",
["user_address", "=", user.wallet.toLowerCase()],
["date", "desc"]
)
);
} catch (e) {
console.error(e);
} finally {
setLoading("");
}
}, [db, user]);
const addTask = async (task: string) => {
if (!db || !user) return;
setLoading("addTask");
try {
await db.add(
{
task,
date: db.ts(),
user_address: db.signer(),
done: false,
},
"tasks",
user
);
await getTasks();
} catch (e) {
console.error(e);
} finally {
setLoading("");
}
};
const deleteTask = async (id: string) => {
if (!db || !user) return;
await db.delete("tasks", id, user);
await getTasks();
};
const login = async () => {
if (!window.ethereum || !db) return;
setLoading("login");
try {
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const wallet_address = await provider.getSigner().getAddress();
const identityCache: any = await lf.getItem(
`temp_address:${contractTxId}:${wallet_address}`
);
if (identityCache)
return setUser({
wallet: wallet_address,
privateKey: identityCache.privateKey,
});
const { tx, identity } = await db.createTempAddress(wallet_address);
if (!tx || tx.err) throw new Error("Failed to relate address TX");
if (tx && !tx.err) {
await lf.setItem("temp_address:current", wallet_address);
await lf.setItem(`temp_address:${contractTxId}:${wallet_address}`, {
wallet: wallet_address,
privateKey: identity.privateKey,
});
setUser({
wallet: wallet_address,
privateKey: identity.privateKey,
});
}
} catch (e) {
console.error(e);
} finally {
setLoading("");
}
};
const checkUser = async () => {
const wallet_address = (await lf.getItem(`temp_address:current`)) as string;
if (!wallet_address) return;
const identity = (await lf.getItem(
`temp_address:${contractTxId}:${wallet_address}`
)) as any;
setUser({
wallet: wallet_address,
privateKey: identity.privateKey,
});
};
const submit = () => {
if (input) {
addTask(input);
setInput("");
}
};
useEffect(() => {
if (tab === "ALL") {
getTasks();
} else {
getMyTasks();
}
}, [tab, getTasks, getMyTasks]);
useEffect(() => {
if (typeof window === "undefined") return;
window.Buffer = Buffer;
checkUser();
const db = new SDK({
wallet: arweave_wallet,
name: "weavedb",
version: "1",
contractTxId,
arweave: {
host: "arweave.net",
port: 443,
protocol: "https",
},
});
setDb(db);
}, []);
return (
<div className="h-screen bg-base-200">
<div className="mx-auto w-full max-w-xl p-2 gap-2 flex flex-col">
<div className="flex w-full gap-2">
<input
className="input flex-1 input-bordered"
placeholder="Create New Era"
disabled={!user}
onChange={(e) => setInput(e.target.value)}
/>
{loading ? (
<button disabled className="btn btn-disabled loading"></button>
) : user ? (
<button className="btn" onClick={submit}>
Submit
</button>
) : (
<button className="btn btn-primary" onClick={login}>
Login
</button>
)}
</div>
<div className="flex gap-2">
<button
className={clsx(
"btn flex-1",
tab === "ALL" ? "btn-primary" : "btn-ghost"
)}
onClick={() => setTab("ALL")}
>
All Tasks
</button>
<button
className={clsx(
"btn flex-1",
tab === "MY" ? "btn-secondary" : "btn-ghost"
)}
onClick={() => setTab("MY")}
>
My Tasks
</button>
</div>
{tasks.map((task) => (
<div key={task.id} className="card flex-row bg-base-100 p-2 pl-6">
<div className="flex flex-col flex-1">
<div className="text-lg font-bold">{task.data.task}</div>
<div className="flex gap-2">
<div>{task.data.done.toString()}</div>
<div>{new Date(task.data.date * 1000).toLocaleString()}</div>
<div>
{task.data.user_address.slice(0, 6) +
"..." +
task.data.user_address.slice(-4)}{" "}
</div>
</div>
</div>
{task.data.user_address === user?.wallet.toLowerCase() && (
<button
className="btn btn-error btn-square text-xl"
onClick={() => deleteTask(task.id)}
>
🗑️
</button>
)}
</div>
))}
</div>
</div>
);
}
WEAVEDB_CONTRACT_TX_ID
とARWEAVE_WALLET_JSON
はそれぞれデプロイしたcontractTxId
とArweaveのウォレットの中身を貼り付けてください。
2.5 起動
yarn dev
成功すればTodoアプリが立ち上がるはずです。
2.6 ログイン処理の解説
ログイン処理のコアの部分は次の通りです
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const wallet_address = await provider.getSigner().getAddress();
const identityCache: any = await lf.getItem(
`temp_address:${contractTxId}:${wallet_address}`
);
if (identityCache)
return setUser({
wallet: wallet_address,
privateKey: identityCache.privateKey,
});
const { tx, identity } = await db.createTempAddress(wallet_address);
if (!tx || tx.err) throw new Error("Failed to relate address TX");
if (tx && !tx.err) {
await lf.setItem("temp_address:current", wallet_address);
await lf.setItem(`temp_address:${contractTxId}:${wallet_address}`, {
wallet: wallet_address,
privateKey: identity.privateKey,
});
setUser({
wallet: wallet_address,
privateKey: identity.privateKey,
});
}
WeaveDBでは署名によって仮のウォレットを作成してトランザクションを行います。
下記のコードでは以前に生成した仮ウォレットのキャッシュを読み込んでキャッシュがあるならそれでログインします。
const identityCache: any = await lf.getItem(
`temp_address:${contractTxId}:${wallet_address}`
);
if (identityCache)
return setUser({
wallet: wallet_address,
privateKey: identityCache.privateKey,
});
キャッシュがない場合、SDKのcreateTempAddress
メソッドを読みだして仮ウォレットを生成し、メインのアドレスと紐づける処理を行います。
そしてできたウォレットでログインします。
const { tx, identity } = await db.createTempAddress(wallet_address);
ログインの大まかな流れはこんな感じです。
終わりに
WeaveDBの簡単な使い方をまとめてみました。
もしなにかわからないことなどがあれば、Twitterで連絡ください。
Discussion