Todo Manager
How to build the simplest todo dapp with WeaveDB and Next.js.
Deploy WeaveDB Contracts
git clone https://github.com/weavedb/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 setsigner
date
must be theblock.timestamp
done
must default tofalse
- Only
done
can be updated totrue
by the task owner (user_address
) - Only the task owner (
user_address
) can delete the task
Set up Everything with Script
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 deploying 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
Create NextJS Project
Set up a next.js project with the app name todos
.
yarn create next-app todos
cd todos
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 directory to continue development.
We use these minimum dependencies.
- WeaveDB SDK - to connect with WeaveDB
- Buffer - a dependency for WeaveDB
- Ramda.js - functional programming utilities
- Chakra UI - UI library
- Ethers.js - to connect with Metamask
- localForage - IndexedDB wrapper to store a disposal wallet
yarn add ramda localforage weavedb-sdk buffer ethers@5.7.2 @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
db
- to assign the WeaveDB instance latercontractTxID
- WeaveDB contract tx id
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)
const tabs = isNil(user) ? ["All"] : ["All", "Yours"]
return (...)
}
user
- logged in usertasks
- tasks to dotab
- current page tabinitDB
- to determine if the WeaveDB is ready to usetabs
- 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({
contractTxId
})
await db.init()
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))
const linked = await db.getAddressLink(identity.address)
if (isNil(linked)) {
alert("something went wrong")
return
}
} 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}`,
JSON.parse(JSON.stringify(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
NavBar
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 = () => {
const [newTask, setNewTask] = useState("")
const handleAddBtnClick = async () => {
if (!/^\s*$/.test(newTask)) {
await addTask(newTask)
setNewTask("")
}
}
return (
<Flex mb={4}>
<Input
placeholder="Enter New Task"
value={newTask}
onChange={(e) => setNewTask(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={handleAddBtnClick}
>
add
</Flex>
</Flex>
)
}
Transactions
const Transactions = () => {
return (
<Flex justify="center" p={4}>
<Box
as="a"
target="_blank"
href={`https://sonar.warp.cc/?#/app/contract/${contractTxId}`}
sx={{ textDecoration: "underline" }}
>
view transactions
</Box>
</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
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
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({
contractTxId
})
await db.init()
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))
const linked = await db.getAddressLink(identity.address)
if (isNil(linked)) {
alert("something went wrong")
return
}
} 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 = () => {
const [newTask, setNewTask] = useState("")
const handleAddBtnClick = async () => {
if (!/^\s*$/.test(newTask)) {
await addTask(newTask)
setNewTask("")
}
}
return (
<Flex mb={4}>
<Input
placeholder="Enter New Task"
value={newTask}
onChange={(e) => setNewTask(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={handleAddBtnClick}
>
add
</Flex>
</Flex>
)
}
const Transactions = () => {
return (
<Flex justify="center" p={4}>
<Box
as="a"
target="_blank"
href={`https://sonar.warp.cc/?#/app/contract/${contractTxId}`}
sx={{ textDecoration: "underline" }}
>
view transactions
</Box>
</Flex>
)
}
return (
<ChakraProvider>
<NavBar />
<Flex mt="60px" justify="center" p={3}>
<Box w="100%" maxW="600px">
<Tabs />
{!isNil(user) ? <NewTask /> : null}
<Tasks />
</Box>
</Flex>
<Transactions />
</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.
You can also access the entire dapp code at /examples/todos.