diff --git a/examples/flush-notes/package-lock.json b/examples/flush-notes/package-lock.json index 91a66f0..b9fb282 100644 --- a/examples/flush-notes/package-lock.json +++ b/examples/flush-notes/package-lock.json @@ -9,7 +9,10 @@ "version": "0.1.0", "dependencies": { "@waku/interfaces": "^0.0.20", + "@waku/message-encryption": "^0.0.23", "@waku/sdk": "^0.0.21", + "@waku/utils": "^0.0.13", + "ethereum-cryptography": "^2.1.2", "next": "14.0.2", "react": "^18", "react-dom": "^18", @@ -921,6 +924,61 @@ "integrity": "sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==", "dev": true }, + "node_modules/@scure/base": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.3.tgz", + "integrity": "sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "dependencies": { + "@noble/curves": "~1.1.0", + "@noble/hashes": "~1.3.1", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@swc/helpers": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", @@ -1228,6 +1286,23 @@ "node": ">=18" } }, + "node_modules/@waku/message-encryption": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/@waku/message-encryption/-/message-encryption-0.0.23.tgz", + "integrity": "sha512-i/qMAjO8EVCMlA1BbuGT+uHxzdydYQVXM9mThgH+kTTPTW5gVAMdzFaGmbiHWT6kn3yAsWq5CBB7M3+1zqb8vQ==", + "dependencies": { + "@noble/secp256k1": "^1.7.1", + "@waku/core": "0.0.25", + "@waku/interfaces": "0.0.20", + "@waku/proto": "0.0.5", + "@waku/utils": "0.0.13", + "debug": "^4.3.4", + "js-sha3": "^0.9.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@waku/peer-exchange": { "version": "0.0.18", "resolved": "https://registry.npmjs.org/@waku/peer-exchange/-/peer-exchange-0.0.18.tgz", @@ -2650,6 +2725,39 @@ "node": ">=0.10.0" } }, + "node_modules/ethereum-cryptography": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz", + "integrity": "sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug==", + "dependencies": { + "@noble/curves": "1.1.0", + "@noble/hashes": "1.3.1", + "@scure/bip32": "1.3.1", + "@scure/bip39": "1.2.1" + } + }, + "node_modules/ethereum-cryptography/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/event-iterator": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/event-iterator/-/event-iterator-2.0.0.tgz", @@ -7256,6 +7364,45 @@ "integrity": "sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==", "dev": true }, + "@scure/base": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.3.tgz", + "integrity": "sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==" + }, + "@scure/bip32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "requires": { + "@noble/curves": "~1.1.0", + "@noble/hashes": "~1.3.1", + "@scure/base": "~1.1.0" + }, + "dependencies": { + "@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "requires": { + "@noble/hashes": "1.3.1" + } + }, + "@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==" + } + } + }, + "@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "requires": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + } + }, "@swc/helpers": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", @@ -7488,6 +7635,20 @@ "resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.20.tgz", "integrity": "sha512-6g2SRCKiAqtxElozXzPNHg68u/lxWSGL1LSXqwA0AAs+WYvK2vYfBM9ceUlbhDEk4ReCUAceUgZgdtdgKGflgA==" }, + "@waku/message-encryption": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/@waku/message-encryption/-/message-encryption-0.0.23.tgz", + "integrity": "sha512-i/qMAjO8EVCMlA1BbuGT+uHxzdydYQVXM9mThgH+kTTPTW5gVAMdzFaGmbiHWT6kn3yAsWq5CBB7M3+1zqb8vQ==", + "requires": { + "@noble/secp256k1": "^1.7.1", + "@waku/core": "0.0.25", + "@waku/interfaces": "0.0.20", + "@waku/proto": "0.0.5", + "@waku/utils": "0.0.13", + "debug": "^4.3.4", + "js-sha3": "^0.9.2" + } + }, "@waku/peer-exchange": { "version": "0.0.18", "resolved": "https://registry.npmjs.org/@waku/peer-exchange/-/peer-exchange-0.0.18.tgz", @@ -8541,6 +8702,32 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "ethereum-cryptography": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz", + "integrity": "sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug==", + "requires": { + "@noble/curves": "1.1.0", + "@noble/hashes": "1.3.1", + "@scure/bip32": "1.3.1", + "@scure/bip39": "1.2.1" + }, + "dependencies": { + "@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "requires": { + "@noble/hashes": "1.3.1" + } + }, + "@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==" + } + } + }, "event-iterator": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/event-iterator/-/event-iterator-2.0.0.tgz", diff --git a/examples/flush-notes/package.json b/examples/flush-notes/package.json index 1a85987..c7bc1f0 100644 --- a/examples/flush-notes/package.json +++ b/examples/flush-notes/package.json @@ -10,7 +10,10 @@ }, "dependencies": { "@waku/interfaces": "^0.0.20", + "@waku/message-encryption": "^0.0.23", "@waku/sdk": "^0.0.21", + "@waku/utils": "^0.0.13", + "ethereum-cryptography": "^2.1.2", "next": "14.0.2", "react": "^18", "react-dom": "^18", diff --git a/examples/flush-notes/src/app/Loading.tsx b/examples/flush-notes/src/app/Loading.tsx new file mode 100644 index 0000000..4b2ac80 --- /dev/null +++ b/examples/flush-notes/src/app/Loading.tsx @@ -0,0 +1,7 @@ +export const Loading = () => { + return ( +
+

Loading...

+
+ ); +}; diff --git a/examples/flush-notes/src/app/WakuProvider.tsx b/examples/flush-notes/src/app/WakuProvider.tsx index e4f0ce3..1d62cdb 100644 --- a/examples/flush-notes/src/app/WakuProvider.tsx +++ b/examples/flush-notes/src/app/WakuProvider.tsx @@ -2,13 +2,17 @@ import React from "react"; import { waku, Waku, WakuEvents } from "@/services/waku"; +import { WakuStatus } from "@/const"; +import { Loading } from "./Loading"; type WakuContextProps = { - status: string; + status: WakuStatus; + waku?: Waku; }; const WakuContext = React.createContext({ - status: "", + status: WakuStatus.Initializing, + waku: undefined, }); type WakuProviderProps = { @@ -16,14 +20,16 @@ type WakuProviderProps = { }; export const useWaku = () => { - const { status } = React.useContext(WakuContext); + const { status, waku } = React.useContext(WakuContext); - return { status }; + return { status, waku }; }; export const WakuProvider = (props: WakuProviderProps) => { const wakuRef = React.useRef(); - const [status, setStatus] = React.useState(""); + const [status, setStatus] = React.useState( + WakuStatus.Initializing + ); React.useEffect(() => { if (wakuRef.current) { @@ -43,8 +49,16 @@ export const WakuProvider = (props: WakuProviderProps) => { }; }, [wakuRef, setStatus]); + if (status === WakuStatus.Failed) { + return <>{status}; + } + + if (status !== WakuStatus.Connected) { + return ; + } + return ( - + {props.children} ); diff --git a/examples/flush-notes/src/app/create/page.tsx b/examples/flush-notes/src/app/create/page.tsx deleted file mode 100644 index 91f66b8..0000000 --- a/examples/flush-notes/src/app/create/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use client"; - -import { useWaku } from "@/app/WakuProvider"; - -export default function Create() { - const { status } = useWaku(); - return ( -
-

{status}

-
- ); -} diff --git a/examples/flush-notes/src/app/edit/page.tsx b/examples/flush-notes/src/app/edit/page.tsx deleted file mode 100644 index 9bf1250..0000000 --- a/examples/flush-notes/src/app/edit/page.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client"; - -import React from "react"; -import Markdown from "react-markdown"; -import { useRouter } from "next/navigation"; -import { useNoteHash } from "@/hooks/useNoteHash"; - -const t = ` -### Problem -Cannot read data returned from REST API in browser if request is done with CORS violation. - -# Impact -Prevents browsers from querying REST API. - -### To reproduce -0. Run nwaku REST API -1. Copy https://github.com/waku-org/waku-frontend/tree/weboko/follow-up -5. Wait for 10 seconds and see in console following error: - -It happens even though the request succeds. - - -### Expected behavior -Error should not happen. - -### Additional context -What happens is that browser implements Fetch API in the manner that when request is made to resource with CORS violation then even if it would succeed - client won't be able to read response data. -Spec of the Fetch API - https://fetch.spec.whatwg.org/#concept-filtered-response-opaque - - -### The fix -Considering we expect REST API to be run only on localhost we should add following HTTP header to allow web apps run on different port to be able to talk to the API. -`; - -const View = () => { - const router = useRouter(); - const noteHash = useNoteHash(); - - React.useEffect(() => { - if (!noteHash) { - router.replace("/404"); - } - }, [noteHash]); - - return {t}; -}; - -export default View; diff --git a/examples/flush-notes/src/app/globals.css b/examples/flush-notes/src/app/globals.css index e69de29..a8ac73d 100644 --- a/examples/flush-notes/src/app/globals.css +++ b/examples/flush-notes/src/app/globals.css @@ -0,0 +1,41 @@ +* { + box-sizing: border-box; +} + +.loading-block { + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} + +.note-info { + font-size: 0.8rem; + text-align: right; +} + +.create-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.password-value { + font-size: 1rem; + height: 2rem; + width: 250px; + padding: 5px; +} + +.save-note { + width: 100px; + height: 50px; + margin-bottom: 10px; +} + +.note-value { + width: 100%; + padding: 10px; + min-height: 500px; +} diff --git a/examples/flush-notes/src/app/page.tsx b/examples/flush-notes/src/app/page.tsx index e6d12ef..a985b94 100644 --- a/examples/flush-notes/src/app/page.tsx +++ b/examples/flush-notes/src/app/page.tsx @@ -1,22 +1,79 @@ "use client"; -import { useWaku } from "@/app/WakuProvider"; +import React from "react"; import { useRouter } from "next/navigation"; +import { notes } from "@/services/notes"; -export default function Home() { - const { status } = useWaku(); +// EgeLwHNbSwIzIz3M +// 3gxFeAHa8sOvTymg + +// encrypted nkt698RhpWory0yT +export default function Create() { const router = useRouter(); + const { note, onNoteChange } = useEditNote(); + const { password, onPasswordChange } = usePassword(); + + const onSave = async () => { + if (!note) { + return; + } + + try { + const id = await notes.createNote(note, password); + router.push(`/view/${id}`); + } catch (error) { + console.log("Failed to create a note:", error); + } + }; + return (
-

{status}

- - +

+ Your record will be stored for couple of days. Markdown is supported. +

+
+ + +
+
); } + +const useEditNote = () => { + const [state, setState] = React.useState(""); + + const onNoteChange = (event: React.FormEvent) => { + setState(event?.currentTarget?.value); + }; + + return { + note: state, + onNoteChange, + }; +}; + +const usePassword = () => { + const [password, setPassword] = React.useState(); + + const onPasswordChange = (event: React.FormEvent) => { + setPassword(event?.currentTarget?.value); + }; + + return { + password, + onPasswordChange, + }; +}; diff --git a/examples/flush-notes/src/app/view/page.tsx b/examples/flush-notes/src/app/view/page.tsx index 9bf1250..f58308d 100644 --- a/examples/flush-notes/src/app/view/page.tsx +++ b/examples/flush-notes/src/app/view/page.tsx @@ -4,45 +4,28 @@ import React from "react"; import Markdown from "react-markdown"; import { useRouter } from "next/navigation"; import { useNoteHash } from "@/hooks/useNoteHash"; - -const t = ` -### Problem -Cannot read data returned from REST API in browser if request is done with CORS violation. - -# Impact -Prevents browsers from querying REST API. - -### To reproduce -0. Run nwaku REST API -1. Copy https://github.com/waku-org/waku-frontend/tree/weboko/follow-up -5. Wait for 10 seconds and see in console following error: - -It happens even though the request succeds. - - -### Expected behavior -Error should not happen. - -### Additional context -What happens is that browser implements Fetch API in the manner that when request is made to resource with CORS violation then even if it would succeed - client won't be able to read response data. -Spec of the Fetch API - https://fetch.spec.whatwg.org/#concept-filtered-response-opaque - - -### The fix -Considering we expect REST API to be run only on localhost we should add following HTTP header to allow web apps run on different port to be able to talk to the API. -`; +import { notes } from "@/services/notes"; +import { Loading } from "../Loading"; const View = () => { const router = useRouter(); const noteHash = useNoteHash(); + const [note, setNote] = React.useState(""); React.useEffect(() => { if (!noteHash) { router.replace("/404"); + return; } - }, [noteHash]); - return {t}; + notes.readNote(noteHash).then((note) => setNote(note || "")); + }, [noteHash, setNote]); + + if (!note) { + return ; + } + + return {note}; }; export default View; diff --git a/examples/flush-notes/src/const.ts b/examples/flush-notes/src/const.ts new file mode 100644 index 0000000..c5122d5 --- /dev/null +++ b/examples/flush-notes/src/const.ts @@ -0,0 +1,8 @@ +export const CONTENT_TOPIC = "/flush-notes/1/note/proto"; + +export enum WakuStatus { + Initializing = "Initializing...", + WaitingForPeers = "Waiting for peers...", + Connected = "Connected", + Failed = "Failed to initialize(see logs)", +} diff --git a/examples/flush-notes/src/services/notes.ts b/examples/flush-notes/src/services/notes.ts new file mode 100644 index 0000000..f9fd074 --- /dev/null +++ b/examples/flush-notes/src/services/notes.ts @@ -0,0 +1,146 @@ +"use client"; + +import { waku } from "@/services/waku"; +import { CONTENT_TOPIC } from "@/const"; +import { encrypt, decrypt } from "ethereum-cryptography/aes"; +import { getRandomBytes } from "ethereum-cryptography/random"; +import { pbkdf2 } from "ethereum-cryptography/pbkdf2"; +import { + createDecoder, + createEncoder, + Decoder, + Encoder, + IDecodedMessage, + Unsubscribe, + utf8ToBytes, + bytesToUtf8, +} from "@waku/sdk"; +import { generateRandomString } from "@/utils"; +import { bytesToHex, hexToBytes } from "@waku/utils/bytes"; + +type Note = { + id: string; + content: string; + kdf: + | undefined + | { + iv: string; + dklen: number; + c: number; + salt: string; + }; +}; + +export class Notes { + private decoder: Decoder; + private encoder: Encoder; + private messages: IDecodedMessage[] = []; + private subscription: undefined | Unsubscribe; + + constructor() { + this.decoder = createDecoder(CONTENT_TOPIC); + this.encoder = createEncoder({ contentTopic: CONTENT_TOPIC }); + } + + public async createNote(content: string, password?: string): Promise { + const note = password + ? await this.encryptNote(content, password) + : { id: generateRandomString(), content, kdf: undefined }; + + await waku.send(this.encoder, { + payload: utf8ToBytes(JSON.stringify(note)), + }); + + return note.id; + } + + public async readNote(id: string): Promise { + await this.initMessages(); + + const message = this.messages + .map((m) => { + try { + return JSON.parse(bytesToUtf8(m.payload)) as Note; + } catch (error) { + console.log("Failed to read message:", error); + } + }) + .find((v) => { + if (v?.id === id) { + return true; + } + }); + + if (!message?.kdf) { + return message?.content; + } + + const password = window.prompt("This note is encrypted, need password:"); + + if (!password) { + console.log("No password was provided, stopping reading a note."); + return; + } + + return this.decryptNote(message, password); + } + + private async initMessages() { + if (this.subscription) { + return; + } + + this.messages = await waku.getHistory(this.decoder); + this.subscription = await waku.subscribe(this.decoder, (message) => { + this.messages.push(message); + }); + } + + private async encryptNote(content: string, password: string): Promise { + const iv = await getRandomBytes(16); + const salt = await getRandomBytes(32); + const c = 131072; + const dklen = 16; + const kdf = await pbkdf2( + utf8ToBytes(password.normalize("NFKC")), + salt, + c, + dklen, + "sha256" + ); + const encryptedContent = await encrypt(utf8ToBytes(content), kdf, iv); + + return { + id: generateRandomString(), + content: bytesToHex(encryptedContent), + kdf: { + c, + dklen, + iv: bytesToHex(iv), + salt: bytesToHex(salt), + }, + }; + } + + private async decryptNote(note: Note, password: string): Promise { + if (!note?.kdf) { + throw Error("Failed to decrypt a note, no kdf params found."); + } + + const iv = hexToBytes(note.kdf.iv); + const salt = hexToBytes(note.kdf.salt); + + const kdf = await pbkdf2( + utf8ToBytes(password.normalize("NFKC")), + salt, + note.kdf.c, + note.kdf.dklen, + "sha256" + ); + const decryptedContent = await decrypt(hexToBytes(note.content), kdf, iv); + + return bytesToUtf8(decryptedContent); + } +} + +export const notes = new Notes(); diff --git a/examples/flush-notes/src/services/waku.ts b/examples/flush-notes/src/services/waku.ts index 9fb4c92..8509944 100644 --- a/examples/flush-notes/src/services/waku.ts +++ b/examples/flush-notes/src/services/waku.ts @@ -1,6 +1,15 @@ "use client"; -import { LightNode, createLightNode, waitForRemotePeer } from "@waku/sdk"; +import { WakuStatus } from "@/const"; +import { + IDecoder, + IEncoder, + IMessage, + LightNode, + createLightNode, + waitForRemotePeer, + IDecodedMessage, +} from "@waku/sdk"; type EventListener = (event: CustomEvent) => void; @@ -24,17 +33,17 @@ export class Waku { this.initializing = true; try { - this.emitStatusEvent("Initializing..."); + this.emitStatusEvent(WakuStatus.Initializing); const node = await createLightNode({ defaultBootstrap: true }); await node.start(); - this.emitStatusEvent("Waiting for peers..."); + this.emitStatusEvent(WakuStatus.WaitingForPeers); await waitForRemotePeer(node); this.node = node; this.initialized = true; - this.emitStatusEvent("Connected"); + this.emitStatusEvent(WakuStatus.Connected); } catch (error) { console.error("Failed to initialize Waku node:", error); - this.emitStatusEvent("Failed to initialize(see logs)"); + this.emitStatusEvent(WakuStatus.Failed); } this.initializing = false; } @@ -47,11 +56,50 @@ export class Waku { return this.emitter.removeEventListener(event, fn as any); } + public send(encoder: IEncoder, message: IMessage) { + this.ensureWakuInitialized(); + return this.node?.lightPush.send(encoder, message); + } + + public async getHistory( + decoder: IDecoder + ): Promise { + this.ensureWakuInitialized(); + + let messages: IDecodedMessage[] = []; + for await (const promises of this.node!.store.queryGenerator([decoder])) { + const messagesRaw = await Promise.all(promises); + const filteredMessages = messagesRaw.filter( + (v): v is IDecodedMessage => !!v + ); + + messages = [...messages, ...filteredMessages]; + } + + return messages; + } + + public async subscribe( + decoder: IDecoder, + fn: (m: IDecodedMessage) => void + ) { + this.ensureWakuInitialized(); + return this.node!.filter.subscribe(decoder, fn); + } + private emitStatusEvent(payload: string) { this.emitter.dispatchEvent( new CustomEvent(WakuEvents.Status, { detail: payload }) ); } + + private ensureWakuInitialized() { + if (!waku.initialized) { + const message = "Waku is not initialized."; + console.log(message); + throw Error(message); + } + } } export const waku = new Waku(); diff --git a/examples/flush-notes/src/utils.ts b/examples/flush-notes/src/utils.ts new file mode 100644 index 0000000..c7a236d --- /dev/null +++ b/examples/flush-notes/src/utils.ts @@ -0,0 +1,10 @@ +export function generateRandomString(): string { + let result = ""; + let characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let charactersLength = characters.length; + for (let i = 0; i < 16; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +}