Todo Manager (Web Console)
How to build the simplest todo dapp with WeaveDB and Next.js using Web Console.
Deploy WeaveDB Contracts
Go to the cloud Web Console or you can also use your local Web Console.
Click DB
then +
to open up the contract deploy dialog.
Click Deploy
then Connect Owner Wallet
.
Connect with one of your crypto wallets. This is going to be the owner of the contract.
Then click Deploy DB Instance
.
Now a new WeaveDB contract is deployed on the mainnet. Click Sign In
to authenticate the owner wallet.
Database Structure
We will only use one collection tasks
to keep it simple.
Data Schemas
WeaveDB utilized JSON Schema to validate incoming data.
const schemas = {
type: "object",
required: ["task", "date", "user_address", "done"],
properties: {
task: {
type: "string",
},
user_address: {
type: "string",
},
date: {
type: "number",
},
done: {
type: "boolean",
},
},
}
tasks
collection must have 4 fields (task
,date
,user_address
,done
).
Access Control Rules
Access control rules are defined by JSONLogic.
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" },
],
},
}
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
Configure DB
First, set up tasks
collection. Click Data
then +
to open up a dialog. Enter tasks
in the name box and copy the following access control rules to the textarea. Then click Add
.
{"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"}]}}
Next, set up data schemas to task
collection. Click Schemas
then +
to open up a dialog. Enter tasks
in the name box and copy the following data schemas to the textarea. Then click `Add"
{"type":"object","required":["task","date","user_address","done"],"properties":{"task":{"type":"string"},"user_address":{"type":"string"},"date":{"type":"number"},"done":{"type":"boolean"}}}
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
.
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 @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)
let task = useRef()
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 usetask
- a new task name linked with the input formtabs
- 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}`,
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 = () => (
<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>
)
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 = () => (
<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>
)
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.