From 04cda9dfd8fc8db11d80a6cd66e0f9f19ed0634b Mon Sep 17 00:00:00 2001 From: "Justin R. Evans" Date: Sat, 14 Sep 2024 11:06:12 +0200 Subject: [PATCH 1/5] feat: add service worker and logic to download & validate MASP params --- apps/namadillo/.gitignore | 1 + apps/namadillo/package.json | 5 +- apps/namadillo/scripts/startProxies.js | 23 +--- apps/namadillo/src/index.tsx | 13 +++ apps/namadillo/sw/constants.ts | 33 ++++++ apps/namadillo/sw/indexedDb.ts | 153 +++++++++++++++++++++++++ apps/namadillo/sw/sw.ts | 101 ++++++++++++++++ apps/namadillo/tsconfig.sw.json | 13 +++ 8 files changed, 318 insertions(+), 24 deletions(-) create mode 100644 apps/namadillo/sw/constants.ts create mode 100644 apps/namadillo/sw/indexedDb.ts create mode 100644 apps/namadillo/sw/sw.ts create mode 100644 apps/namadillo/tsconfig.sw.json diff --git a/apps/namadillo/.gitignore b/apps/namadillo/.gitignore index 320fb3dfb..bf66ad1c7 100644 --- a/apps/namadillo/.gitignore +++ b/apps/namadillo/.gitignore @@ -4,6 +4,7 @@ .pnp.js /build /dist +/public/sw .env /test-results/ /playwright-report/ diff --git a/apps/namadillo/package.json b/apps/namadillo/package.json index 9432e8996..a8601cc29 100644 --- a/apps/namadillo/package.json +++ b/apps/namadillo/package.json @@ -43,7 +43,7 @@ "release:dry-run": "release-it --verbose --dry-run --ci", "release:no-npm": "release-it --verbose --no-npm.publish --ci", "start:proxy": "node ./scripts/startProxies.js", - "dev": "vite", + "dev": "tsc --watch --project tsconfig.sw.json& vite", "preview": "vite preview", "dev:local": "NODE_ENV=development NAMADA_INTERFACE_LOCAL=\"true\" yarn dev", "dev:proxy": "NAMADA_INTERFACE_PROXY=true && ./scripts/start-proxies.sh && yarn dev:local", @@ -57,7 +57,6 @@ "test:watch": "yarn wasm:build:test && yarn jest --watchAll=true", "test:coverage": "yarn wasm:build:test && yarn test --coverage", "test:ci": "jest", - "test:watch-only": "yarn jest --watchAll=true", "e2e-test": "PLAYWRIGHT_BASE_URL=http://localhost:3000 yarn playwright test", "e2e-test:headed": "PLAYWRIGHT_BASE_URL=http://localhost:3000 yarn playwright test --project=chromium --headed", "wasm:build": "node ./scripts/build.js --release", @@ -107,12 +106,10 @@ "eslint-plugin-react-hooks": "^4.6.0", "globals": "^15.9.0", "history": "^5.3.0", - "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-create-mock-instance": "^2.0.0", "jest-environment-jsdom": "^29.7.0", "jest-fetch-mock": "^3.0.3", - "jest-transformer-svg": "^2.0.2", "local-cors-proxy": "^1.1.0", "postcss": "^8.4.32", "release-it": "^17.0.1", diff --git a/apps/namadillo/scripts/startProxies.js b/apps/namadillo/scripts/startProxies.js index 361592dd2..99795d48e 100644 --- a/apps/namadillo/scripts/startProxies.js +++ b/apps/namadillo/scripts/startProxies.js @@ -1,31 +1,14 @@ const { exec } = require("child_process"); require("dotenv").config(); -const { - NAMADA_INTERFACE_NAMADA_ALIAS = "Namada", - NAMADA_INTERFACE_NAMADA_URL, - NAMADA_INTERFACE_COSMOS_ALIAS = "Cosmos", - NAMADA_INTERFACE_COSMOS_URL, - NAMADA_INTERFACE_ETH_ALIAS = "Ethereum", - NAMADA_INTERFACE_ETH_URL, -} = process.env; +const { MASP_MPC_URL } = process.env; const proxyConfigs = [ { - alias: NAMADA_INTERFACE_NAMADA_ALIAS, - url: NAMADA_INTERFACE_NAMADA_URL, + alias: "MASP MPC URL", + url: MASP_MPC_URL, proxyPort: 8010, }, - { - alias: NAMADA_INTERFACE_COSMOS_ALIAS, - url: NAMADA_INTERFACE_COSMOS_URL, - proxyPort: 8011, - }, - { - alias: NAMADA_INTERFACE_ETH_ALIAS, - url: NAMADA_INTERFACE_ETH_URL, - proxyPort: 8012, - }, ]; proxyConfigs.forEach(({ alias, url, proxyPort }) => { diff --git a/apps/namadillo/src/index.tsx b/apps/namadillo/src/index.tsx index ed0a25a27..29e6e0a46 100644 --- a/apps/namadillo/src/index.tsx +++ b/apps/namadillo/src/index.tsx @@ -40,3 +40,16 @@ if (container) { ); }); } + +if ("serviceWorker" in navigator) { + window.addEventListener("load", () => { + navigator.serviceWorker + .register("/sw/sw.js", { scope: "/sw/" }) + .then((registration) => { + console.log("Service Worker registered: ", registration); + }) + .catch((error) => { + console.log("Service Worker registration failed: ", error); + }); + }); +} diff --git a/apps/namadillo/sw/constants.ts b/apps/namadillo/sw/constants.ts new file mode 100644 index 000000000..53da44cb6 --- /dev/null +++ b/apps/namadillo/sw/constants.ts @@ -0,0 +1,33 @@ +/** + * Define constants for MASP Params Storage + */ +const STORAGE_PREFIX = "/namadillo"; + +enum MaspParam { + Output = "masp-output.params", + Convert = "masp-convert.params", + Spend = "masp-spend.params", +} + +const MASP_PARAM_LEN: Record = +{ + [MaspParam.Output]: { + length: 16398620, + sha256sum: + "ed8b5d354017d808cfaf7b31eca5c511936e65ef6d276770251f5234ec5328b8", + }, + [MaspParam.Spend]: { + length: 49848572, + sha256sum: + "62b3c60ca54bd99eb390198e949660624612f7db7942db84595fa9f1b4a29fd8", + }, + [MaspParam.Convert]: { + length: 22570940, + sha256sum: + "8e049c905e0e46f27662c7577a4e3480c0047ee1171f7f6d9c5b0de757bf71f1", + }, +}; + +enum MsgType { + FetchParam = "fetch-params", +} diff --git a/apps/namadillo/sw/indexedDb.ts b/apps/namadillo/sw/indexedDb.ts new file mode 100644 index 000000000..c0e6ead36 --- /dev/null +++ b/apps/namadillo/sw/indexedDb.ts @@ -0,0 +1,153 @@ +/** + * Establish types for IndexedDB + */ +interface KVStore { + get(key: string): Promise; + set(key: string, data: U | null): Promise; + prefix(): string; +} + +let durability: IDBTransactionMode = "readwrite"; + +class IndexedDBKVStore implements KVStore { + protected cachedDB?: IDBDatabase; + + constructor(protected readonly _prefix: string) { } + + public async get(key: string): Promise { + const tx = (await this.getDB()).transaction([this.prefix()], "readonly"); + const store = tx.objectStore(this.prefix()); + + return new Promise((resolve, reject) => { + const request = store.get(key); + request.onerror = (event) => { + event.stopPropagation(); + + reject(event.target); + }; + request.onsuccess = () => { + if (!request.result) { + resolve(undefined); + } else { + resolve(request.result.data); + } + }; + }); + } + + public async set(key: string, data: U | null): Promise { + if (data === null) { + const tx = (await this.getDB()).transaction([this.prefix()], durability, { + durability: "strict", + }); + const store = tx.objectStore(this.prefix()); + + return new Promise((resolve, reject) => { + const request = store.delete(key); + request.onerror = (event) => { + event.stopPropagation(); + + reject(event.target); + }; + request.onsuccess = () => { + resolve(); + }; + }); + } else { + const tx = (await this.getDB()).transaction([this.prefix()], durability, { + durability: "strict", + }); + const store = tx.objectStore(this.prefix()); + + return new Promise((resolve, reject) => { + const request = store.put({ + key, + data, + }); + request.onerror = (event) => { + event.stopPropagation(); + + reject(event.target); + }; + request.onsuccess = () => { + resolve(); + }; + }); + } + } + + public prefix(): string { + return this._prefix; + } + + protected async getDB(): Promise { + if (this.cachedDB) { + return this.cachedDB; + } + + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.prefix()); + request.onerror = (event) => { + event.stopPropagation(); + reject(event.target); + }; + + request.onupgradeneeded = (event) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const db = event.target.result; + + db.createObjectStore(this.prefix(), { keyPath: "key" }); + }; + + request.onsuccess = () => { + this.cachedDB = request.result; + resolve(request.result); + }; + }); + } + + public static async durabilityCheck(): Promise { + const { TARGET } = process.env; + let isDurable: boolean; + + if (TARGET === "chrome") { + durability = "readwrite"; + isDurable = true; + } else { + const prefix = "durability-check"; + const db: IDBDatabase = await new Promise((resolve, reject) => { + const request = indexedDB.open(prefix); + request.onerror = (event) => { + event.stopPropagation(); + reject(event.target); + }; + + request.onupgradeneeded = (event) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const db = event.target.result; + + db.createObjectStore(prefix, { keyPath: "key" }); + }; + + request.onsuccess = () => { + resolve(request.result); + }; + }); + + try { + db.transaction([prefix], "readwriteflush" as IDBTransactionMode, { + durability: "strict", + }); + durability = "readwriteflush" as IDBTransactionMode; + isDurable = true; + } catch { + durability = "readwrite"; + isDurable = false; + } + } + + return isDurable; + } +} diff --git a/apps/namadillo/sw/sw.ts b/apps/namadillo/sw/sw.ts new file mode 100644 index 000000000..e34e6e29d --- /dev/null +++ b/apps/namadillo/sw/sw.ts @@ -0,0 +1,101 @@ +importScripts("indexedDb.js"); +importScripts("constants.js"); +// const { NAMADA_INTERFACE_PROXY: isProxy = false } = process.env; + +const isProxy = true; +const TRUSTED_SETUP_URL = + isProxy ? + "http://localhost:8010/proxy" + : "https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup"; + +const store = new IndexedDBKVStore(STORAGE_PREFIX); + +const resetMaspParamStore = async (): Promise => { + store.set(MaspParam.Output, null); + store.set(MaspParam.Spend, null); + store.set(MaspParam.Convert, null); +}; + +// resetMaspParamStore(); + +const sha256Hash = async (msg: Uint8Array): Promise => { + const hashBuffer = await crypto.subtle.digest("SHA-256", msg); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + // Return hash as hex + return hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join(""); +}; + +const fetchAndStoreMaspParam = async (maspParam: MaspParam): Promise => { + console.info(`Fetching ${maspParam}...`); + return fetch([TRUSTED_SETUP_URL, maspParam].join("/")) + .then(async (response) => { + if (response.ok) { + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("No readable stream returned!"); + } + return new ReadableStream({ + start(controller) { + return pump(); + function pump() { + return reader?.read().then(({ done, value }) => { + // When no more data needs to be consumed, close the stream + if (done) { + controller.close(); + return; + } + // Enqueue the next data chunk into our target stream + controller.enqueue(value); + return pump(); + }); + } + }, + }); + } + }) + .then((stream) => new Response(stream)) + .then((response) => response.blob()) + .then(async (blob) => { + const arrayBuffer = await blob.arrayBuffer(); + const data = new Uint8Array(arrayBuffer); + const { length, sha256sum } = MASP_PARAM_LEN[maspParam]; + + // Reject if invalid length (incomplete download or invalid) + console.log("Validating data length..."); + + if (length !== data.length) { + return Promise.reject( + `Invalid data length! Expected ${length}, received ${data.length}!` + ); + } + + // Reject if invalid hash (otherwise invalid data) + console.log("Validating sha256sum..."); + const hash = await sha256Hash(data); + + if (hash !== sha256sum) { + return Promise.reject( + `Invalid sha256sum! Expected ${sha256sum}, received ${hash}!` + ); + } + + console.info(`Storing ${maspParam} => `, data); + await store.set(maspParam, data); + console.info(`Successfully stored ${maspParam}`); + }); +}; + +(async () => { + const maspOutputParam = await store.get(MaspParam.Output); + console.log("Found output?: ", maspOutputParam); + + const maspSpendParam = await store.get(MaspParam.Spend); + console.log("Found spend?: ", maspSpendParam); + + const maspConvertParam = await store.get(MaspParam.Convert); + console.log("Found convert?", maspConvertParam); + + if (!maspOutputParam) await fetchAndStoreMaspParam(MaspParam.Output); + if (!maspSpendParam) await fetchAndStoreMaspParam(MaspParam.Spend); + if (!maspConvertParam) await fetchAndStoreMaspParam(MaspParam.Convert); +})(); diff --git a/apps/namadillo/tsconfig.sw.json b/apps/namadillo/tsconfig.sw.json new file mode 100644 index 000000000..ae9435c8f --- /dev/null +++ b/apps/namadillo/tsconfig.sw.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "baseUrl": "./sw", + "lib": ["ESNext", "WebWorker"], + "noEmit": false, + "strict": false, + "outDir": "public/sw", + "target": "ESNext", + "isolatedModules": false + }, + "include": ["sw"] +} From 51e83fcdea16c24c9e0359ddaf459912d3a5b61d Mon Sep 17 00:00:00 2001 From: "Justin R. Evans" Date: Sat, 14 Sep 2024 13:36:21 +0200 Subject: [PATCH 2/5] fix: clean fetch and validation --- apps/namadillo/sw/sw.ts | 122 +++++++++++++++++++++++++++++----------- 1 file changed, 90 insertions(+), 32 deletions(-) diff --git a/apps/namadillo/sw/sw.ts b/apps/namadillo/sw/sw.ts index e34e6e29d..c44eca45d 100644 --- a/apps/namadillo/sw/sw.ts +++ b/apps/namadillo/sw/sw.ts @@ -25,8 +25,16 @@ const sha256Hash = async (msg: Uint8Array): Promise => { return hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join(""); }; -const fetchAndStoreMaspParam = async (maspParam: MaspParam): Promise => { - console.info(`Fetching ${maspParam}...`); +type MaspParamBytes = { + param: MaspParam; + bytes: Uint8Array; +}; + +const fetchMaspParam = async ( + maspParam: MaspParam, + onRead?: (value?: Uint8Array) => void, + onComplete?: () => void +): Promise => { return fetch([TRUSTED_SETUP_URL, maspParam].join("/")) .then(async (response) => { if (response.ok) { @@ -39,9 +47,13 @@ const fetchAndStoreMaspParam = async (maspParam: MaspParam): Promise => { return pump(); function pump() { return reader?.read().then(({ done, value }) => { + // Invoke callback if provided + if (onRead && value) onRead(value); // When no more data needs to be consumed, close the stream if (done) { controller.close(); + // Invoke callback if provided + if (onComplete) onComplete(); return; } // Enqueue the next data chunk into our target stream @@ -57,45 +69,91 @@ const fetchAndStoreMaspParam = async (maspParam: MaspParam): Promise => { .then((response) => response.blob()) .then(async (blob) => { const arrayBuffer = await blob.arrayBuffer(); - const data = new Uint8Array(arrayBuffer); - const { length, sha256sum } = MASP_PARAM_LEN[maspParam]; + return { + param: maspParam, + bytes: new Uint8Array(arrayBuffer), + }; + }); +}; - // Reject if invalid length (incomplete download or invalid) - console.log("Validating data length..."); +const storeMaspParam = async ({ + param, + bytes, +}: MaspParamBytes): Promise => { + console.info(`Storing ${param} => `, bytes); + await store.set(param, bytes); + console.info(`Successfully stored ${param}`); - if (length !== data.length) { - return Promise.reject( - `Invalid data length! Expected ${length}, received ${data.length}!` - ); - } + return { + param, + bytes, + }; +}; - // Reject if invalid hash (otherwise invalid data) - console.log("Validating sha256sum..."); - const hash = await sha256Hash(data); +const validateMaspParamBytes = async ({ + param, + bytes, +}: MaspParamBytes): Promise => { + const { length, sha256sum } = MASP_PARAM_LEN[param]; - if (hash !== sha256sum) { - return Promise.reject( - `Invalid sha256sum! Expected ${sha256sum}, received ${hash}!` - ); - } + // Reject if invalid length (incomplete download or invalid) + console.log(`Validating data length for ${param}, expecting ${length}...`); - console.info(`Storing ${maspParam} => `, data); - await store.set(maspParam, data); - console.info(`Successfully stored ${maspParam}`); - }); + if (length !== bytes.length) { + return Promise.reject( + `Invalid data length! Expected ${length}, received ${bytes.length}!` + ); + } + + // Reject if invalid hash (otherwise invalid data) + console.log(`Validating sha256sum for ${param}, expecting ${sha256sum}...`); + const hash = await sha256Hash(bytes); + + if (hash !== sha256sum) { + return Promise.reject( + `Invalid sha256sum! Expected ${sha256sum}, received ${hash}!` + ); + } + + return { param, bytes }; }; +const logSuccess = ({ param, bytes }: MaspParamBytes): void => { + console.info(`Fetched and stored ${param}:`, bytes); +}; + +// MOVE TO INVOKE IN HANDLER (async () => { - const maspOutputParam = await store.get(MaspParam.Output); - console.log("Found output?: ", maspOutputParam); + const maspOutputParamBytes = await store.get(MaspParam.Output); + console.log("Found output?: ", maspOutputParamBytes); + + const maspSpendParamBytes = await store.get(MaspParam.Spend); + console.log("Found spend?: ", maspSpendParamBytes); + + const maspConvertParamBytes = await store.get(MaspParam.Convert); + console.log("Found convert?", maspConvertParamBytes); - const maspSpendParam = await store.get(MaspParam.Spend); - console.log("Found spend?: ", maspSpendParam); + if (!maspOutputParamBytes) { + await fetchMaspParam(MaspParam.Output) + .then(validateMaspParamBytes) + .then(storeMaspParam) + .then(logSuccess) + .catch((e) => console.error(e)); + } - const maspConvertParam = await store.get(MaspParam.Convert); - console.log("Found convert?", maspConvertParam); + if (!maspSpendParamBytes) { + await fetchMaspParam(MaspParam.Spend) + .then(validateMaspParamBytes) + .then(storeMaspParam) + .then(logSuccess) + .catch((e) => console.error(e)); + } - if (!maspOutputParam) await fetchAndStoreMaspParam(MaspParam.Output); - if (!maspSpendParam) await fetchAndStoreMaspParam(MaspParam.Spend); - if (!maspConvertParam) await fetchAndStoreMaspParam(MaspParam.Convert); + if (!maspConvertParamBytes) { + await fetchMaspParam(MaspParam.Convert) + .then(validateMaspParamBytes) + .then(storeMaspParam) + .then(logSuccess) + .catch((e) => console.error(e)); + } })(); From 4c8ea66f7e26d909e410ad1444aa37299798b0f7 Mon Sep 17 00:00:00 2001 From: "Justin R. Evans" Date: Sat, 14 Sep 2024 13:50:33 +0200 Subject: [PATCH 3/5] feat: pass isProxy to sw.js --- apps/namadillo/src/index.tsx | 11 ++++++++++- apps/namadillo/sw/sw.ts | 13 ++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/apps/namadillo/src/index.tsx b/apps/namadillo/src/index.tsx index 29e6e0a46..fe79096eb 100644 --- a/apps/namadillo/src/index.tsx +++ b/apps/namadillo/src/index.tsx @@ -14,6 +14,8 @@ import { IndexerLoader } from "App/Setup/IndexerLoader"; import { TomlConfigLoader } from "App/Setup/TomlConfigLoader"; import "./tailwind.css"; +const { NAMADA_INTERFACE_PROXY: isProxy = "false" } = process.env; + const router = getRouter(); const container = document.getElementById("root"); @@ -42,9 +44,16 @@ if (container) { } if ("serviceWorker" in navigator) { + const swConfig: Record = { + isProxy: isProxy, + }; + const swUrl = `/sw/sw.js?${new URLSearchParams(swConfig).toString()}`; + window.addEventListener("load", () => { navigator.serviceWorker - .register("/sw/sw.js", { scope: "/sw/" }) + .register(swUrl, { + scope: "/sw/", + }) .then((registration) => { console.log("Service Worker registered: ", registration); }) diff --git a/apps/namadillo/sw/sw.ts b/apps/namadillo/sw/sw.ts index c44eca45d..cb6c5c29e 100644 --- a/apps/namadillo/sw/sw.ts +++ b/apps/namadillo/sw/sw.ts @@ -1,10 +1,11 @@ importScripts("indexedDb.js"); importScripts("constants.js"); -// const { NAMADA_INTERFACE_PROXY: isProxy = false } = process.env; -const isProxy = true; -const TRUSTED_SETUP_URL = - isProxy ? +const urlParams = new URLSearchParams(location.search); +const { isProxy } = Object.fromEntries(urlParams); + +const MASP_MPC_URL = + isProxy === "true" ? "http://localhost:8010/proxy" : "https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup"; @@ -16,8 +17,6 @@ const resetMaspParamStore = async (): Promise => { store.set(MaspParam.Convert, null); }; -// resetMaspParamStore(); - const sha256Hash = async (msg: Uint8Array): Promise => { const hashBuffer = await crypto.subtle.digest("SHA-256", msg); const hashArray = Array.from(new Uint8Array(hashBuffer)); @@ -35,7 +34,7 @@ const fetchMaspParam = async ( onRead?: (value?: Uint8Array) => void, onComplete?: () => void ): Promise => { - return fetch([TRUSTED_SETUP_URL, maspParam].join("/")) + return fetch([MASP_MPC_URL, maspParam].join("/")) .then(async (response) => { if (response.ok) { const reader = response.body?.getReader(); From 90ba7734662f020efd1d0d961dff530629d70305 Mon Sep 17 00:00:00 2001 From: "Justin R. Evans" Date: Sat, 14 Sep 2024 13:58:45 +0200 Subject: [PATCH 4/5] fix: reorganize a bit --- apps/namadillo/sw/crypto.ts | 6 ++ apps/namadillo/sw/indexedDb.ts | 46 +--------- apps/namadillo/sw/masp.ts | 90 +++++++++++++++++++ apps/namadillo/sw/sw.ts | 154 ++++----------------------------- 4 files changed, 115 insertions(+), 181 deletions(-) create mode 100644 apps/namadillo/sw/crypto.ts create mode 100644 apps/namadillo/sw/masp.ts diff --git a/apps/namadillo/sw/crypto.ts b/apps/namadillo/sw/crypto.ts new file mode 100644 index 000000000..dea210e18 --- /dev/null +++ b/apps/namadillo/sw/crypto.ts @@ -0,0 +1,6 @@ +const sha256Hash = async (msg: Uint8Array): Promise => { + const hashBuffer = await crypto.subtle.digest("SHA-256", msg); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + // Return hash as hex + return hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join(""); +}; diff --git a/apps/namadillo/sw/indexedDb.ts b/apps/namadillo/sw/indexedDb.ts index c0e6ead36..aafe28934 100644 --- a/apps/namadillo/sw/indexedDb.ts +++ b/apps/namadillo/sw/indexedDb.ts @@ -12,7 +12,7 @@ let durability: IDBTransactionMode = "readwrite"; class IndexedDBKVStore implements KVStore { protected cachedDB?: IDBDatabase; - constructor(protected readonly _prefix: string) { } + constructor(protected readonly _prefix: string) {} public async get(key: string): Promise { const tx = (await this.getDB()).transaction([this.prefix()], "readonly"); @@ -106,48 +106,4 @@ class IndexedDBKVStore implements KVStore { }; }); } - - public static async durabilityCheck(): Promise { - const { TARGET } = process.env; - let isDurable: boolean; - - if (TARGET === "chrome") { - durability = "readwrite"; - isDurable = true; - } else { - const prefix = "durability-check"; - const db: IDBDatabase = await new Promise((resolve, reject) => { - const request = indexedDB.open(prefix); - request.onerror = (event) => { - event.stopPropagation(); - reject(event.target); - }; - - request.onupgradeneeded = (event) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const db = event.target.result; - - db.createObjectStore(prefix, { keyPath: "key" }); - }; - - request.onsuccess = () => { - resolve(request.result); - }; - }); - - try { - db.transaction([prefix], "readwriteflush" as IDBTransactionMode, { - durability: "strict", - }); - durability = "readwriteflush" as IDBTransactionMode; - isDurable = true; - } catch { - durability = "readwrite"; - isDurable = false; - } - } - - return isDurable; - } } diff --git a/apps/namadillo/sw/masp.ts b/apps/namadillo/sw/masp.ts new file mode 100644 index 000000000..013d06607 --- /dev/null +++ b/apps/namadillo/sw/masp.ts @@ -0,0 +1,90 @@ +type MaspParamBytes = { + param: MaspParam; + bytes: Uint8Array; +}; + +const fetchMaspParam = async ( + maspParam: MaspParam, + onRead?: (value?: Uint8Array) => void, + onComplete?: () => void +): Promise => { + return fetch([MASP_MPC_URL, maspParam].join("/")) + .then(async (response) => { + if (response.ok) { + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("No readable stream returned!"); + } + return new ReadableStream({ + start(controller) { + return pump(); + function pump() { + return reader?.read().then(({ done, value }) => { + // Invoke callback if provided + if (onRead && value) onRead(value); + // When no more data needs to be consumed, close the stream + if (done) { + controller.close(); + // Invoke callback if provided + if (onComplete) onComplete(); + return; + } + // Enqueue the next data chunk into our target stream + controller.enqueue(value); + return pump(); + }); + } + }, + }); + } + }) + .then((stream) => new Response(stream)) + .then((response) => response.blob()) + .then(async (blob) => { + const arrayBuffer = await blob.arrayBuffer(); + return { + param: maspParam, + bytes: new Uint8Array(arrayBuffer), + }; + }); +}; + +const storeMaspParam = async ({ + param, + bytes, +}: MaspParamBytes): Promise => { + console.info(`Storing ${param}...`); + await store.set(param, bytes); + return { + param, + bytes, + }; +}; + +const validateMaspParamBytes = async ({ + param, + bytes, +}: MaspParamBytes): Promise => { + const { length, sha256sum } = MASP_PARAM_LEN[param]; + + // Reject if invalid length (incomplete download or invalid) + console.log(`Validating data length for ${param}, expecting ${length}...`); + + if (length !== bytes.length) { + return Promise.reject( + `Invalid data length! Expected ${length}, received ${bytes.length}!` + ); + } + + // Reject if invalid hash (otherwise invalid data) + console.log(`Validating sha256sum for ${param}, expecting ${sha256sum}...`); + const hash = await sha256Hash(bytes); + + if (hash !== sha256sum) { + return Promise.reject( + `Invalid sha256sum! Expected ${sha256sum}, received ${hash}!` + ); + } + + return { param, bytes }; +}; diff --git a/apps/namadillo/sw/sw.ts b/apps/namadillo/sw/sw.ts index cb6c5c29e..54ac17e38 100644 --- a/apps/namadillo/sw/sw.ts +++ b/apps/namadillo/sw/sw.ts @@ -1,5 +1,8 @@ -importScripts("indexedDb.js"); importScripts("constants.js"); +importScripts("crypto.js"); +importScripts("fetch.js"); +importScripts("masp.js"); +importScripts("indexedDb.js"); const urlParams = new URLSearchParams(location.search); const { isProxy } = Object.fromEntries(urlParams); @@ -7,7 +10,7 @@ const { isProxy } = Object.fromEntries(urlParams); const MASP_MPC_URL = isProxy === "true" ? "http://localhost:8010/proxy" - : "https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup"; + : "https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup"; const store = new IndexedDBKVStore(STORAGE_PREFIX); @@ -17,142 +20,21 @@ const resetMaspParamStore = async (): Promise => { store.set(MaspParam.Convert, null); }; -const sha256Hash = async (msg: Uint8Array): Promise => { - const hashBuffer = await crypto.subtle.digest("SHA-256", msg); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - // Return hash as hex - return hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join(""); -}; - -type MaspParamBytes = { - param: MaspParam; - bytes: Uint8Array; -}; - -const fetchMaspParam = async ( - maspParam: MaspParam, - onRead?: (value?: Uint8Array) => void, - onComplete?: () => void -): Promise => { - return fetch([MASP_MPC_URL, maspParam].join("/")) - .then(async (response) => { - if (response.ok) { - const reader = response.body?.getReader(); - if (!reader) { - throw new Error("No readable stream returned!"); - } - return new ReadableStream({ - start(controller) { - return pump(); - function pump() { - return reader?.read().then(({ done, value }) => { - // Invoke callback if provided - if (onRead && value) onRead(value); - // When no more data needs to be consumed, close the stream - if (done) { - controller.close(); - // Invoke callback if provided - if (onComplete) onComplete(); - return; - } - // Enqueue the next data chunk into our target stream - controller.enqueue(value); - return pump(); - }); - } - }, - }); - } - }) - .then((stream) => new Response(stream)) - .then((response) => response.blob()) - .then(async (blob) => { - const arrayBuffer = await blob.arrayBuffer(); - return { - param: maspParam, - bytes: new Uint8Array(arrayBuffer), - }; - }); -}; - -const storeMaspParam = async ({ - param, - bytes, -}: MaspParamBytes): Promise => { - console.info(`Storing ${param} => `, bytes); - await store.set(param, bytes); - console.info(`Successfully stored ${param}`); - - return { - param, - bytes, - }; -}; - -const validateMaspParamBytes = async ({ - param, - bytes, -}: MaspParamBytes): Promise => { - const { length, sha256sum } = MASP_PARAM_LEN[param]; +// resetMaspParamStore(); - // Reject if invalid length (incomplete download or invalid) - console.log(`Validating data length for ${param}, expecting ${length}...`); +const logSuccess = ({ param, bytes }: MaspParamBytes): void => + console.info(`Fetched and stored ${param}`, bytes); - if (length !== bytes.length) { - return Promise.reject( - `Invalid data length! Expected ${length}, received ${bytes.length}!` - ); - } +const logError = (e: any) => console.error(e); - // Reject if invalid hash (otherwise invalid data) - console.log(`Validating sha256sum for ${param}, expecting ${sha256sum}...`); - const hash = await sha256Hash(bytes); - - if (hash !== sha256sum) { - return Promise.reject( - `Invalid sha256sum! Expected ${sha256sum}, received ${hash}!` - ); - } - - return { param, bytes }; -}; - -const logSuccess = ({ param, bytes }: MaspParamBytes): void => { - console.info(`Fetched and stored ${param}:`, bytes); -}; - -// MOVE TO INVOKE IN HANDLER (async () => { - const maspOutputParamBytes = await store.get(MaspParam.Output); - console.log("Found output?: ", maspOutputParamBytes); - - const maspSpendParamBytes = await store.get(MaspParam.Spend); - console.log("Found spend?: ", maspSpendParamBytes); - - const maspConvertParamBytes = await store.get(MaspParam.Convert); - console.log("Found convert?", maspConvertParamBytes); - - if (!maspOutputParamBytes) { - await fetchMaspParam(MaspParam.Output) - .then(validateMaspParamBytes) - .then(storeMaspParam) - .then(logSuccess) - .catch((e) => console.error(e)); - } - - if (!maspSpendParamBytes) { - await fetchMaspParam(MaspParam.Spend) - .then(validateMaspParamBytes) - .then(storeMaspParam) - .then(logSuccess) - .catch((e) => console.error(e)); - } - - if (!maspConvertParamBytes) { - await fetchMaspParam(MaspParam.Convert) - .then(validateMaspParamBytes) - .then(storeMaspParam) - .then(logSuccess) - .catch((e) => console.error(e)); - } + [MaspParam.Output, MaspParam.Spend, MaspParam.Convert].map(async (param) => { + if (!(await store.get(param))) { + await fetchMaspParam(param) + .then(validateMaspParamBytes) + .then(storeMaspParam) + .then(logSuccess) + .catch(logError); + } + }); })(); From 90317d81195577b7581ec2c74745663d974b3b6d Mon Sep 17 00:00:00 2001 From: "Justin R. Evans" Date: Sat, 14 Sep 2024 17:01:05 +0200 Subject: [PATCH 5/5] feat: begin adding messaging --- apps/namadillo/package.json | 5 +-- apps/namadillo/scripts/startProxies.js | 4 +-- apps/namadillo/src/index.tsx | 31 ++++++++++++++++- apps/namadillo/sw/sw.ts | 46 +++++++++++++++++++++++++- apps/namadillo/tsconfig.sw.json | 4 +-- 5 files changed, 82 insertions(+), 8 deletions(-) diff --git a/apps/namadillo/package.json b/apps/namadillo/package.json index a8601cc29..419fa277c 100644 --- a/apps/namadillo/package.json +++ b/apps/namadillo/package.json @@ -43,10 +43,11 @@ "release:dry-run": "release-it --verbose --dry-run --ci", "release:no-npm": "release-it --verbose --no-npm.publish --ci", "start:proxy": "node ./scripts/startProxies.js", - "dev": "tsc --watch --project tsconfig.sw.json& vite", + "dev": "yarn dev:sw& vite", + "dev:sw": "tsc --watch --project tsconfig.sw.json", "preview": "vite preview", "dev:local": "NODE_ENV=development NAMADA_INTERFACE_LOCAL=\"true\" yarn dev", - "dev:proxy": "NAMADA_INTERFACE_PROXY=true && ./scripts/start-proxies.sh && yarn dev:local", + "dev:proxy": "NAMADA_INTERFACE_PROXY=true && ./scripts/start-proxies.sh && yarn dev", "build": "NODE_ENV=production && yarn wasm:build && vite build", "build:only": "NODE_ENV=production && vite build", "lint": "eslint src --ext .ts,.tsx", diff --git a/apps/namadillo/scripts/startProxies.js b/apps/namadillo/scripts/startProxies.js index 99795d48e..715c2186a 100644 --- a/apps/namadillo/scripts/startProxies.js +++ b/apps/namadillo/scripts/startProxies.js @@ -1,7 +1,7 @@ const { exec } = require("child_process"); -require("dotenv").config(); -const { MASP_MPC_URL } = process.env; +const MASP_MPC_URL = + "https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup"; const proxyConfigs = [ { diff --git a/apps/namadillo/src/index.tsx b/apps/namadillo/src/index.tsx index fe79096eb..74f48cb26 100644 --- a/apps/namadillo/src/index.tsx +++ b/apps/namadillo/src/index.tsx @@ -56,9 +56,38 @@ if ("serviceWorker" in navigator) { }) .then((registration) => { console.log("Service Worker registered: ", registration); + + const msgChannel = new MessageChannel(); + registration.active?.postMessage( + { + type: "INIT_PORT", + }, + [msgChannel.port2] + ); + + msgChannel.port1.onmessage = (event) => { + switch (event.type) { + case "namadillo:hasMaspParamsResponse": + console.warn(`${event.data.param}: ${event.data.hasMaspParam}`); + break; + } + }; + + registration.active?.postMessage({ + type: "namadillo:hasMaspParams", + param: "masp-output.params", + }); + registration.active?.postMessage({ + type: "namadillo:hasMaspParams", + param: "masp-spend.params", + }); + registration.active?.postMessage({ + type: "namadillo:hasMaspParams", + param: "masp-convert.params", + }); }) .catch((error) => { - console.log("Service Worker registration failed: ", error); + console.warn("Service Worker registration failed: ", error); }); }); } diff --git a/apps/namadillo/sw/sw.ts b/apps/namadillo/sw/sw.ts index 54ac17e38..3d708d3d9 100644 --- a/apps/namadillo/sw/sw.ts +++ b/apps/namadillo/sw/sw.ts @@ -1,3 +1,4 @@ +/// importScripts("constants.js"); importScripts("crypto.js"); importScripts("fetch.js"); @@ -27,14 +28,57 @@ const logSuccess = ({ param, bytes }: MaspParamBytes): void => const logError = (e: any) => console.error(e); +const params = [MaspParam.Output, MaspParam.Spend, MaspParam.Convert]; + (async () => { - [MaspParam.Output, MaspParam.Spend, MaspParam.Convert].map(async (param) => { + params.forEach(async (param) => { if (!(await store.get(param))) { await fetchMaspParam(param) .then(validateMaspParamBytes) .then(storeMaspParam) .then(logSuccess) .catch(logError); + } else { + console.log(`Found ${param}`); } }); })(); + +/** + * EVENT HANDLERS + */ +const EVENT_PREFIX = "namadillo"; + +enum EventMessage { + FetchMaspParams = `${EVENT_PREFIX}:fetchMaspParams`, + HasMaspParams = `${EVENT_PREFIX}:hasMaspParams`, + HasMaspParamsResponse = `${EVENT_PREFIX}:hasMaspParamsResponse`, +} + +// PORT +let port: MessagePort; + +self.addEventListener("message", (event: MessageEvent) => { + console.log({ event }); + if (event.data && event.data.type == "INIT_PORT") { + port = event.ports[0]; + } + const { data } = event; + + switch (data.type as EventMessage) { + case EventMessage.FetchMaspParams: + console.log(`${EventMessage.FetchMaspParams}`); + break; + case EventMessage.HasMaspParams: { + const { param } = data; + store.get(param).then((record) => { + port.postMessage({ + type: EventMessage.HasMaspParamsResponse, + param, + hasMaspParam: record ? true : false, + }); + }); + break; + } + } +}); diff --git a/apps/namadillo/tsconfig.sw.json b/apps/namadillo/tsconfig.sw.json index ae9435c8f..70e12e9c2 100644 --- a/apps/namadillo/tsconfig.sw.json +++ b/apps/namadillo/tsconfig.sw.json @@ -2,11 +2,11 @@ "extends": "./tsconfig.json", "compilerOptions": { "baseUrl": "./sw", - "lib": ["ESNext", "WebWorker"], + "lib": ["esnext", "webworker"], "noEmit": false, "strict": false, "outDir": "public/sw", - "target": "ESNext", + "target": "esnext", "isolatedModules": false }, "include": ["sw"]