Skip to main content

Todo Manager

How to build the simplest todo dapp with WeaveDB and Next.js.

Deploy WeaveDB Contracts

git clone https://github.com/asteroid-dao/weavedb.git
cd weavedb
yarn
node scripts/generate-wallet.js mainnet
yarn deploy

A new wallet is stored at /scripts/.wallets/wallet-mainnet.json.

yarn deploy returns contractTxId and srcTxId.

{ contractTxId, srcTxId }

Database Structure

We will only use one collection tasks to keep it simple.

Set up Data Schemas

const schemas = {
type: "object",
required: ["task", "date", "user_address", "done"],
properties: {
task: {
type: "string",
},
user_address: {
type: "string",
},
date: {
type: "number",
},
done: {
type: "boolean",
},
},
}
await db.setSchema(schemas, "tasks")
  • tasks collection must have 4 fields (task, date, user_address, done).

Set up Access Control Rules

const rules = {
"allow create": {
and: [
{
"==": [
{ var: "request.auth.signer" },
{ var: "resource.newData.user_address" },
],
},
{
"==": [
{ var: "request.block.timestamp" },
{ var: "resource.newData.date" },
],
},
{
"==": [{ var: "resource.newData.done" }, false],
},
],
},
"allow update": {
and: [
{
"==": [
{ var: "request.auth.signer" },
{ var: "resource.newData.user_address" },
],
},
{
"==": [{ var: "resource.newData.done" }, true],
},
],
},
"allow delete": {
"==": [
{ var: "request.auth.signer" },
{ var: "resource.data.user_address" },
],
},
}
await db.setRules(rules, "tasks")
  • user_address must be set signer
  • date must be the block.timestamp
  • done must default to false
  • Only done can be updated to true by the task owner (user_address)
  • Only the task owner (user_address) can delete the task

To set up the schemas and the rules, you can simply run the pre-defined script in the repo.

Replace CONTRACT_TX_ID with the contractTxId returned when deplying the WeaveDB contract.

node scripts/todo-setup.js mainnet CONTRACT_TX_ID

Now the DB setup is all done!

Query Data

Set a new task.

await db.add({
task: "task_name",
date: db.ts(),
user_address: db.signer(),
done: false
}, "tasks")

Mark a task done.

await db.update({ done: true }, "tasks", TASK_DOC_ID)

Get all tasks sorted by date.

const tasks = await db.get("tasks", ["date", "desc"])

Get all tasks of a user sorted by date.

const tasks = await db.get("tasks", ["user_address", "=", USER_ADDRESS], ["date", "desc"])

We will implement these queries in the frontend code.

Frontend Dapp

Set up a next.js project with the app name todos.

caution

You need to downgrade next package to 12.0 for compatibility with warp-contracts.

yarn create next-app todos
cd todos
yarn add next@12.0
yarn dev

Now your dapp should be running at http://localhost:3000.

For simplicity, we will write everything in one file at /page/index.js.

Install Dependencies

Open a new terminal and move to the root directry to continue depelopment.

We use these minimum dependencies.

yarn add ramda localforage weavedb-sdk buffer ethers @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6

Import Dependencies

Open /page/index.js and replace everything.

import { useRef, useState, useEffect } from "react"
import lf from "localforage"
import { isNil, map } from "ramda"
import SDK from "weavedb-sdk"
import { Buffer } from "buffer"
import { ethers } from "ethers"
import { Box, Flex, Input, ChakraProvider } from "@chakra-ui/react"

Define Variables

let db
const contractTxId = WEAVEDB_CONTRACT_TX_ID
const arweave_wallet = ARWEAVE_WALLET_JSON
  • db - to assign the WeaveDB instance later
  • contractTxID - WeaveDB contract tx id
  • arweave_wallet - JSON object generated at /weavedb/scripts/.wallets/wallet-mainnet.json
caution

Note that this is not a secure way to insert a wallet. Don't deploy this code to production environments.

We generated a disposal wallet for this tutorial and never funded it.

The correct approach is to set up a remote gRPC node and use the light client for browsers.

Define React States

export default function App() {
const [user, setUser] = useState(null)
const [tasks, setTasks] = useState([])
const [tab, setTab] = useState("All")
const [initDB, setInitDB] = useState(false)
let task = useRef()
const tabs = isNil(user) ? ["All"] : ["All", "Yours"]
return (...)
}
  • user - logged in user
  • tasks - tasks to do
  • tab - current page tab
  • initDB - to determine if the WeaveDB is ready to use
  • task - a new task name linked with the input form
  • tabs - page tab options, All to display everyone's tasks, Yours for only your tasks

Define Functions

setupWeaveDB

Buffer needs to be exposed to window.

  const setupWeaveDB = async () => {
window.Buffer = Buffer
db = new SDK({
wallet: arweave_wallet,
name: "weavedb",
version: "1",
contractTxId,
arweave: {
host: "arweave.net",
port: 443,
protocol: "https",
},
})
setInitDB(true)
}

getTasks

  const getTasks = async () => {
setTasks(await db.cget("tasks", ["date", "desc"]))
}

getMyTasks

  const getMyTasks = async () => {
setTasks(
await db.cget(
"tasks",
["user_address", "=", user.wallet.toLowerCase()],
["date", "desc"]
)
)
}

addTask

  const addTask = async task => {
await db.add(
{
task,
date: db.ts(),
user_address: db.signer(),
done: false,
},
"tasks",
user
)
await getTasks()
}

completeTask

  const completeTask = async id => {
await db.update(
{
done: true,
},
"tasks",
id,
user
)
await getTasks()
}

deleteTask

  const deleteTask = async id => {
await db.delete("tasks", id, user)
await getTasks()
}

login

We will generate a disposal account the first time a user logs in, link it with the Metamask address within WeaveDB, and save it locally in the browser's IndexedDB.

{ wallet, privateKey } is how we need to pass the user object to the SDK when making transactions, so we will save it like so.

  const login = async () => {
const provider = new ethers.providers.Web3Provider(window.ethereum, "any")
await provider.send("eth_requestAccounts", [])
const wallet_address = await provider.getSigner().getAddress()
let identity = await lf.getItem(
`temp_address:${contractTxId}:${wallet_address}`
)
let tx
let err
if (isNil(identity)) {
;({ tx, identity, err } = await db.createTempAddress(wallet_address))
} else {
await lf.setItem("temp_address:current", wallet_address)
setUser({
wallet: wallet_address,
privateKey: identity.privateKey,
})
return
}
if (!isNil(tx) && isNil(tx.err)) {
identity.tx = tx
identity.linked_address = wallet_address
await lf.setItem("temp_address:current", wallet_address)
await lf.setItem(
`temp_address:${contractTxId}:${wallet_address}`,
identity
)
setUser({
wallet: wallet_address,
privateKey: identity.privateKey,
})
}
}

logout

We will simply remove the current logged in state. The disposal address will be reused the next time the user logs in.

  const logout = async () => {
if (confirm("Would you like to sign out?")) {
await lf.removeItem("temp_address:current")
setUser(null, "temp_current")
}
}

checkUser

When the page is loaded, check if the user is logged in.

  const checkUser = async () => {
const wallet_address = await lf.getItem(`temp_address:current`)
if (!isNil(wallet_address)) {
const identity = await lf.getItem(
`temp_address:${contractTxId}:${wallet_address}`
)
if (!isNil(identity))
setUser({
wallet: wallet_address,
privateKey: identity.privateKey,
})
}
}

Define Reactive State Changes

  useEffect(() => {
checkUser()
setupWeaveDB()
}, [])

useEffect(() => {
if (initDB) {
if (tab === "All") {
getTasks()
} else {
getMyTasks()
}
}
}, [tab, initDB])
  • When the page is loaded, check if the user is logged in and set up WeaveDB.
  • Get specified tasks, when the page tab is switched.

Define React Components

  const NavBar = () => (
<Flex p={3} position="fixed" w="100%" sx={{ top: 0, left: 0 }}>
<Box flex={1} />
<Flex
bg="#111"
color="white"
py={2}
px={6}
sx={{
borderRadius: "5px",
cursor: "pointer",
":hover": { opacity: 0.75 },
}}
>
{!isNil(user) ? (
<Box onClick={() => logout()}>{user.wallet.slice(0, 7)}</Box>
) : (
<Box onClick={() => login()}>Connect Wallet</Box>
)}
</Flex>
</Flex>
)

Tabs

  const Tabs = () => (
<Flex justify="center" style={{ display: "flex" }} mb={4}>
{map(v => (
<Box
mx={2}
onClick={() => setTab(v)}
color={tab === v ? "red" : ""}
textDecoration={tab === v ? "underline" : ""}
sx={{ cursor: "pointer", ":hover": { opacity: 0.75 } }}
>
{v}
</Box>
))(tabs)}
</Flex>
)

Tasks

  const Tasks = () =>
map(v => (
<Flex sx={{ border: "1px solid #ddd", borderRadius: "5px" }} p={3} my={1}>
<Box
w="30px"
textAlign="center"
sx={{ cursor: "pointer", ":hover": { opacity: 0.75 } }}
>
{v.data.done ? (
"✅"
) : v.data.user_address !== user?.wallet.toLowerCase() ? null : (
<Box onClick={() => completeTask(v.id)}></Box>
)}
</Box>
<Box px={3} flex={1} style={{ marginLeft: "10px" }}>
{v.data.task}
</Box>
<Box w="100px" textAlign="center" style={{ marginLeft: "10px" }}>
{v.data.user_address.slice(0, 7)}
</Box>
<Box
w="50px"
textAlign="center"
sx={{ cursor: "pointer", ":hover": { opacity: 0.75 } }}
>
{v.data.user_address === user?.wallet.toLowerCase() ? (
<Box
style={{ marginLeft: "10px" }}
onClick={() => deleteTask(v.id)}
>

</Box>
) : null}
</Box>
</Flex>
))(tasks)

NewTask

  const NewTask = () => (
<Flex mb={4}>
<Input
placeholder="Enter New Task"
value={task.current}
onChange={e => {
task.current = e.target.value
}}
sx={{ borderRadius: "5px 0 0 5px" }}
/>
<Flex
bg="#111"
color="white"
py={2}
px={6}
sx={{
borderRadius: "0 5px 5px 0",
cursor: "pointer",
":hover": { opacity: 0.75 },
}}
onClick={async () => {
if (!/^\s*$/.test(task.current)) {
await addTask(task.current)
task.current = ""
}
}}
>
add
</Flex>
</Flex>
)

Return Components

  return (
<ChakraProvider>
<NavBar />
<Flex mt="60px" justify="center" p={3}>
<Box w="100%" maxW="600px">
<Tabs />
{!isNil(user) ? <NewTask /> : null}
<Tasks />
</Box>
</Flex>
</ChakraProvider>
)

The Complete Code

/pages/index.js
import { useRef, useState, useEffect } from "react"
import lf from "localforage"
import { isNil, map } from "ramda"
import SDK from "weavedb-sdk"
import { Buffer } from "buffer"
import { ethers } from "ethers"
import { Box, Flex, Input, ChakraProvider } from "@chakra-ui/react"

let db
const contractTxId = WEAVEDB_CONTRACT_TX_ID
const arweave_wallet = ARWEAVE_WALLET_JSON

export default function App() {
const [user, setUser] = useState(null)
const [tasks, setTasks] = useState([])
const [tab, setTab] = useState("All")
const [initDB, setInitDB] = useState(false)
let task = useRef()
const tabs = isNil(user) ? ["All"] : ["All", "Yours"]

const setupWeaveDB = async () => {
window.Buffer = Buffer
db = new SDK({
web3: web3,
wallet: arweave_wallet,
name: "weavedb",
version: "1",
contractTxId,
arweave: {
host: "arweave.net",
port: 443,
protocol: "https",
},
})
setInitDB(true)
}

const getTasks = async () => {
setTasks(await db.cget("tasks", ["date", "desc"]))
}

const getMyTasks = async () => {
setTasks(
await db.cget(
"tasks",
["user_address", "=", user.wallet.toLowerCase()],
["date", "desc"]
)
)
}

const addTask = async task => {
await db.add(
{
task,
date: db.ts(),
user_address: db.signer(),
done: false,
},
"tasks",
user
)
await getTasks()
}

const completeTask = async id => {
await db.update(
{
done: true,
},
"tasks",
id,
user
)
await getTasks()
}

const deleteTask = async id => {
await db.delete("tasks", id, user)
await getTasks()
}

const login = async () => {
const provider = new ethers.providers.Web3Provider(window.ethereum, "any")
await provider.send("eth_requestAccounts", [])
const wallet_address = await provider.getSigner().getAddress()
let identity = await lf.getItem(
`temp_address:${contractTxId}:${wallet_address}`
)
let tx
let err
if (isNil(identity)) {
;({ tx, identity, err } = await db.createTempAddress(wallet_address))
} else {
await lf.setItem("temp_address:current", wallet_address)
setUser({
wallet: wallet_address,
privateKey: identity.privateKey,
})
return
}
if (!isNil(tx) && isNil(tx.err)) {
identity.tx = tx
identity.linked_address = wallet_address
await lf.setItem("temp_address:current", wallet_address)
await lf.setItem(
`temp_address:${contractTxId}:${wallet_address}`,
identity
)
setUser({
wallet: wallet_address,
privateKey: identity.privateKey,
})
}
}

const logout = async () => {
if (confirm("Would you like to sign out?")) {
await lf.removeItem("temp_address:current")
setUser(null, "temp_current")
}
}

const checkUser = async () => {
const wallet_address = await lf.getItem(`temp_address:current`)
if (!isNil(wallet_address)) {
const identity = await lf.getItem(
`temp_address:${contractTxId}:${wallet_address}`
)
if (!isNil(identity))
setUser({
wallet: wallet_address,
privateKey: identity.privateKey,
})
}
}

useEffect(() => {
checkUser()
setupWeaveDB()
}, [])

useEffect(() => {
if (initDB) {
if (tab === "All") {
getTasks()
} else {
getMyTasks()
}
}
}, [tab, initDB])

const NavBar = () => (
<Flex p={3} position="fixed" w="100%" sx={{ top: 0, left: 0 }}>
<Box flex={1} />
<Flex
bg="#111"
color="white"
py={2}
px={6}
sx={{
borderRadius: "5px",
cursor: "pointer",
":hover": { opacity: 0.75 },
}}
>
{!isNil(user) ? (
<Box onClick={() => logout()}>{user.wallet.slice(0, 7)}</Box>
) : (
<Box onClick={() => login()}>Connect Wallet</Box>
)}
</Flex>
</Flex>
)

const Tabs = () => (
<Flex justify="center" style={{ display: "flex" }} mb={4}>
{map(v => (
<Box
mx={2}
onClick={() => setTab(v)}
color={tab === v ? "red" : ""}
textDecoration={tab === v ? "underline" : ""}
sx={{ cursor: "pointer", ":hover": { opacity: 0.75 } }}
>
{v}
</Box>
))(tabs)}
</Flex>
)

const Tasks = () =>
map(v => (
<Flex sx={{ border: "1px solid #ddd", borderRadius: "5px" }} p={3} my={1}>
<Box
w="30px"
textAlign="center"
sx={{ cursor: "pointer", ":hover": { opacity: 0.75 } }}
>
{v.data.done ? (
"✅"
) : v.data.user_address !== user?.wallet.toLowerCase() ? null : (
<Box onClick={() => completeTask(v.id)}></Box>
)}
</Box>
<Box px={3} flex={1} style={{ marginLeft: "10px" }}>
{v.data.task}
</Box>
<Box w="100px" textAlign="center" style={{ marginLeft: "10px" }}>
{v.data.user_address.slice(0, 7)}
</Box>
<Box
w="50px"
textAlign="center"
sx={{ cursor: "pointer", ":hover": { opacity: 0.75 } }}
>
{v.data.user_address === user?.wallet.toLowerCase() ? (
<Box
style={{ marginLeft: "10px" }}
onClick={() => deleteTask(v.id)}
>

</Box>
) : null}
</Box>
</Flex>
))(tasks)

const NewTask = () => (
<Flex mb={4}>
<Input
placeholder="Enter New Task"
value={task.current}
onChange={e => {
task.current = e.target.value
}}
sx={{ borderRadius: "5px 0 0 5px" }}
/>
<Flex
bg="#111"
color="white"
py={2}
px={6}
sx={{
borderRadius: "0 5px 5px 0",
cursor: "pointer",
":hover": { opacity: 0.75 },
}}
onClick={async () => {
if (!/^\s*$/.test(task.current)) {
await addTask(task.current)
task.current = ""
}
}}
>
add
</Flex>
</Flex>
)

return (
<ChakraProvider>
<NavBar />
<Flex mt="60px" justify="center" p={3}>
<Box w="100%" maxW="600px">
<Tabs />
{!isNil(user) ? <NewTask /> : null}
<Tasks />
</Box>
</Flex>
</ChakraProvider>
)
}

Congratulations!

Congrats! You have built a fully-decentralized Todo Manager Dapp from scratch using WeaveDB.

Go to localhost:3000 and see how it works.