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..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": "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", @@ -57,7 +58,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 +107,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..715c2186a 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 = + "https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup"; 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..74f48cb26 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"); @@ -40,3 +42,52 @@ 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(swUrl, { + scope: "/sw/", + }) + .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.warn("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/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 new file mode 100644 index 000000000..aafe28934 --- /dev/null +++ b/apps/namadillo/sw/indexedDb.ts @@ -0,0 +1,109 @@ +/** + * 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); + }; + }); + } +} 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 new file mode 100644 index 000000000..3d708d3d9 --- /dev/null +++ b/apps/namadillo/sw/sw.ts @@ -0,0 +1,84 @@ +/// +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); + +const MASP_MPC_URL = + isProxy === "true" ? + "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 logSuccess = ({ param, bytes }: MaspParamBytes): void => + console.info(`Fetched and stored ${param}`, bytes); + +const logError = (e: any) => console.error(e); + +const params = [MaspParam.Output, MaspParam.Spend, MaspParam.Convert]; + +(async () => { + 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 new file mode 100644 index 000000000..70e12e9c2 --- /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"] +}