├── docs ├── static │ ├── .nojekyll │ └── img │ │ ├── favicon.ico │ │ ├── docusaurus.png │ │ └── docusaurus-social-card.jpg ├── docs │ └── intro.md ├── babel.config.js ├── tsconfig.json ├── src │ ├── pages │ │ ├── markdown-page.md │ │ ├── index.module.css │ │ └── index.js │ └── css │ │ └── custom.css ├── .gitignore ├── README.md ├── sidebars.js ├── package.json └── docusaurus.config.js ├── packages ├── web-wallet │ ├── dist │ │ ├── _redirects │ │ └── _headers │ ├── .env │ ├── src │ │ ├── config │ │ │ ├── index.ts │ │ │ ├── snap.ts │ │ │ └── constants.ts │ │ ├── types │ │ │ ├── vite-env.d.ts │ │ │ ├── index.ts │ │ │ ├── svg.d.ts │ │ │ ├── snap.ts │ │ │ └── custom.d.ts │ │ ├── utils │ │ │ ├── index.ts │ │ │ ├── zatsToZec.ts │ │ │ ├── balance.ts │ │ │ └── metamask.ts │ │ ├── assets │ │ │ ├── noise.png │ │ │ ├── zcash.png │ │ │ ├── chainsafe.png │ │ │ ├── diamond-bg.png │ │ │ ├── form-transfer.png │ │ │ ├── metaMask-logo.png │ │ │ ├── zcash-yellow.png │ │ │ ├── metamask-snaps-logo.png │ │ │ ├── icons │ │ │ │ ├── chevron.svg │ │ │ │ ├── circle.svg │ │ │ │ ├── shield.svg │ │ │ │ ├── check.svg │ │ │ │ ├── coins.svg │ │ │ │ ├── summary.svg │ │ │ │ ├── eye.svg │ │ │ │ ├── shield-divided.svg │ │ │ │ ├── arrow-receive.svg │ │ │ │ ├── arrow-transfer.svg │ │ │ │ ├── warning.svg │ │ │ │ ├── clock.svg │ │ │ │ ├── eye-slash.svg │ │ │ │ └── circle-dashed.svg │ │ │ ├── ellipse.svg │ │ │ └── index.ts │ │ ├── components │ │ │ ├── TransferCards │ │ │ │ ├── index.tsx │ │ │ │ ├── TransferResult.tsx │ │ │ │ ├── TransferConfirm.tsx │ │ │ │ └── TransferInput.tsx │ │ │ ├── index.ts │ │ │ ├── ErrorMessage │ │ │ │ └── ErrorMessage.tsx │ │ │ ├── Loader │ │ │ │ └── Loader.tsx │ │ │ ├── ProtectedRoute │ │ │ │ └── ProtectedRoute.tsx │ │ │ ├── Layout │ │ │ │ └── Layout.tsx │ │ │ ├── PageHeading │ │ │ │ └── PageHeading.tsx │ │ │ ├── Footer │ │ │ │ └── Footer.tsx │ │ │ ├── CopyButton │ │ │ │ └── CopyButton.tsx │ │ │ ├── Button │ │ │ │ └── Button.tsx │ │ │ ├── TransactionStatusCard │ │ │ │ └── TransactionStatusCard.tsx │ │ │ ├── Input │ │ │ │ └── Input.tsx │ │ │ ├── NavBar │ │ │ │ └── NavBar.tsx │ │ │ ├── Header │ │ │ │ └── Header.tsx │ │ │ ├── BlockHeightCard │ │ │ │ └── BlockHeightCard.tsx │ │ │ └── Select │ │ │ │ └── Select.tsx │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── snaps │ │ │ │ ├── useGetSnapState.ts │ │ │ │ ├── useInvokeSnap.ts │ │ │ │ ├── useRequest.ts │ │ │ │ ├── useRequestSnap.ts │ │ │ │ └── useMetaMask.ts │ │ │ └── useBalance.ts │ │ ├── pages │ │ │ ├── Dashboard.tsx │ │ │ ├── Receive │ │ │ │ ├── Tab.tsx │ │ │ │ ├── QrCode.tsx │ │ │ │ └── Receive.tsx │ │ │ ├── TransferBalance │ │ │ │ ├── useTransferBalanceForm.ts │ │ │ │ └── TransferBalance.tsx │ │ │ ├── AccountSummary.tsx │ │ │ └── Home.tsx │ │ ├── main.tsx │ │ ├── App.tsx │ │ ├── router │ │ │ └── index.tsx │ │ ├── styles │ │ │ └── index.css │ │ └── context │ │ │ └── MetamaskContext.tsx │ ├── .postcssrc.json │ ├── .parcelrc │ ├── .gitignore │ ├── index.html │ ├── eslint.config.js │ ├── tsconfig.json │ ├── server.js │ ├── README.md │ └── package.json ├── snap │ ├── .eslintrc.js │ ├── jest.config.js │ ├── src │ │ ├── rpc │ │ │ ├── getSnapState.tsx │ │ │ ├── setSnapState.tsx │ │ │ ├── getSeedFingerprint.tsx │ │ │ ├── getViewingKey.tsx │ │ │ ├── signPczt.tsx │ │ │ └── setBirthdayBlock.tsx │ │ ├── types.ts │ │ ├── utils │ │ │ ├── dialogs.tsx │ │ │ ├── initialiseWasm.ts │ │ │ ├── setSyncBlockHeight.ts │ │ │ ├── hexStringToUint8Array.ts │ │ │ └── getSeed.ts │ │ └── index.tsx │ ├── images │ │ └── logo.svg │ ├── snap.config.ts │ ├── build_prePublish.sh │ ├── tsconfig.json │ ├── build_local.sh │ ├── snap.manifest.json │ ├── README.md │ └── package.json └── e2e-tests │ ├── .gitignore │ ├── src │ ├── index.html │ └── index.js │ ├── dist │ └── serve.json │ ├── package.json │ ├── README.md │ ├── e2e │ ├── tx_request.spec.ts │ └── web_wallet.spec.ts │ └── playwright.config.ts ├── crates ├── webzjs-requests │ ├── src │ │ ├── lib.rs │ │ └── error.rs │ └── Cargo.toml ├── webzjs-wallet │ ├── src │ │ ├── bindgen │ │ │ ├── mod.rs │ │ │ └── proposal.rs │ │ ├── lib.rs │ │ └── init.rs │ └── Cargo.toml ├── webzjs-common │ ├── src │ │ ├── lib.rs │ │ ├── error.rs │ │ ├── pczt.rs │ │ └── network.rs │ └── Cargo.toml └── webzjs-keys │ ├── src │ ├── lib.rs │ └── error.rs │ └── Cargo.toml ├── scripts ├── copyright.txt └── add_license.sh ├── .prettierrc.js ├── .yarnrc.yml ├── rust-toolchain.toml ├── traefik ├── traefik.yml ├── docker-compose.yml └── dynamic.yml ├── .cargo └── config.toml ├── webdriver.json ├── .gitignore ├── .github ├── workflows │ ├── web-tests.yml │ ├── e2e-tests.yml │ ├── rust-checks.yml │ ├── deploy-demo.yml │ ├── check-snap-allowed-origins.yml │ ├── deploy-docs.yml │ └── check-snap-manifest.yml ├── pull_request_template.md └── actions │ └── install-rust-toolchain │ └── action.yml ├── package.json ├── LICENSE-MIT ├── add-worker-module.sh ├── justfile ├── protos └── compact_formats.proto └── README.md /docs/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/web-wallet/dist/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /crates/webzjs-requests/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod requests; 3 | -------------------------------------------------------------------------------- /packages/web-wallet/.env: -------------------------------------------------------------------------------- 1 | SNAP_ORIGIN="local:http://localhost:8080" -------------------------------------------------------------------------------- /packages/snap/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "@chainsafe", 3 | } -------------------------------------------------------------------------------- /packages/web-wallet/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export { defaultSnapOrigin } from './snap'; 2 | -------------------------------------------------------------------------------- /packages/web-wallet/src/types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/web-wallet/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export { type GetSnapsResponse, type Snap } from './snap'; 2 | -------------------------------------------------------------------------------- /packages/web-wallet/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './metamask'; 2 | export * from './balance'; 3 | -------------------------------------------------------------------------------- /docs/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Tutorial Intro 6 | 7 | TODO!! 8 | 9 | -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/WebZjs/HEAD/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /packages/web-wallet/.postcssrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "@tailwindcss/postcss": {} 4 | } 5 | } -------------------------------------------------------------------------------- /docs/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/WebZjs/HEAD/docs/static/img/docusaurus.png -------------------------------------------------------------------------------- /scripts/copyright.txt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 ChainSafe Systems 2 | // SPDX-License-Identifier: Apache-2.0, MIT 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | quoteProps: 'as-needed', 3 | singleQuote: true, 4 | tabWidth: 2, 5 | }; 6 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /crates/webzjs-wallet/src/bindgen/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | pub mod proposal; 4 | pub mod wallet; 5 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/WebZjs/HEAD/packages/web-wallet/src/assets/noise.png -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/zcash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/WebZjs/HEAD/packages/web-wallet/src/assets/zcash.png -------------------------------------------------------------------------------- /docs/static/img/docusaurus-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/WebZjs/HEAD/docs/static/img/docusaurus-social-card.jpg -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: 0 2 | 3 | enableGlobalCache: true 4 | 5 | nmMode: hardlinks-local 6 | 7 | nodeLinker: node-modules 8 | 9 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/chainsafe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/WebZjs/HEAD/packages/web-wallet/src/assets/chainsafe.png -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/diamond-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/WebZjs/HEAD/packages/web-wallet/src/assets/diamond-bg.png -------------------------------------------------------------------------------- /packages/web-wallet/src/config/snap.ts: -------------------------------------------------------------------------------- 1 | export const defaultSnapOrigin = 2 | process.env.SNAP_ORIGIN ?? `local:http://localhost:8080`; 3 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2025-01-07" 3 | components = ["rust-src"] 4 | targets = ["wasm32-unknown-unknown"] 5 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/form-transfer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/WebZjs/HEAD/packages/web-wallet/src/assets/form-transfer.png -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/metaMask-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/WebZjs/HEAD/packages/web-wallet/src/assets/metaMask-logo.png -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/zcash-yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/WebZjs/HEAD/packages/web-wallet/src/assets/zcash-yellow.png -------------------------------------------------------------------------------- /packages/e2e-tests/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /blob-report/ 5 | /playwright/.cache/ 6 | !dist/serve.json 7 | -------------------------------------------------------------------------------- /crates/webzjs-common/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod network; 3 | mod pczt; 4 | 5 | pub use error::Error; 6 | pub use network::Network; 7 | pub use pczt::Pczt; 8 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@docusaurus/tsconfig", 3 | "compilerOptions": { 4 | "baseUrl": "." 5 | }, 6 | "include": ["../packages/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/snap/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@metamask/snaps-jest', 3 | transform: { 4 | '^.+\\.(t|j)sx?$': 'ts-jest', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/metamask-snaps-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/WebZjs/HEAD/packages/web-wallet/src/assets/metamask-snaps-logo.png -------------------------------------------------------------------------------- /packages/web-wallet/src/components/TransferCards/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./TransferConfirm"; 2 | export * from "./TransferInput"; 3 | export * from "./TransferResult"; -------------------------------------------------------------------------------- /packages/web-wallet/.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default", 3 | "transformers": { 4 | "*.svg": ["...", "@parcel/transformer-svg-react"] 5 | } 6 | } -------------------------------------------------------------------------------- /docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /packages/web-wallet/dist/_headers: -------------------------------------------------------------------------------- 1 | /* 2 | Cross-Origin-Opener-Policy: same-origin 3 | Cross-Origin-Embedder-Policy: require-corp 4 | Cross-Origin-Resource-Policy: same-site -------------------------------------------------------------------------------- /packages/web-wallet/src/utils/zatsToZec.ts: -------------------------------------------------------------------------------- 1 | const ZATS_PER_ZEC = 100_000_000; 2 | 3 | export function zatsToZec(zats: number): number { 4 | return zats / ZATS_PER_ZEC; 5 | } 6 | -------------------------------------------------------------------------------- /packages/web-wallet/src/types/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import React from 'react'; 3 | export const ReactComponent: React.FC>; 4 | const src: string; 5 | export default ReactComponent; 6 | } 7 | -------------------------------------------------------------------------------- /packages/web-wallet/src/config/constants.ts: -------------------------------------------------------------------------------- 1 | export const MAINNET_LIGHTWALLETD_PROXY = 'https://zcash-mainnet.chainsafe.dev'; 2 | export const ZATOSHI_PER_ZEC = 1e8; 3 | export const RESCAN_INTERVAL = 35000; 4 | export const NU5_ACTIVATION = 1687104; 5 | -------------------------------------------------------------------------------- /crates/webzjs-keys/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 ChainSafe Systems 2 | // SPDX-License-Identifier: Apache-2.0, MIT 3 | 4 | mod error; 5 | mod keys; 6 | mod pczt_sign; 7 | 8 | pub use error::*; 9 | pub use keys::*; 10 | pub use pczt_sign::*; 11 | -------------------------------------------------------------------------------- /packages/web-wallet/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './snaps/useMetaMask.ts'; 2 | export * from './snaps/useRequest.ts'; 3 | export * from './snaps/useRequestSnap.ts'; 4 | export * from './snaps/useInvokeSnap.ts'; 5 | export * from './useWebzjsActions.ts'; 6 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/icons/chevron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/e2e-tests/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebZjs e2e testing 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /packages/snap/src/rpc/getSnapState.tsx: -------------------------------------------------------------------------------- 1 | import { Json } from '@metamask/snaps-sdk'; 2 | 3 | export async function getSnapState(): Promise { 4 | const state = (await snap.request({ 5 | method: 'snap_manageState', 6 | params: { 7 | operation: 'get', 8 | }, 9 | })) as unknown as Json; 10 | 11 | return state; 12 | } 13 | -------------------------------------------------------------------------------- /traefik/traefik.yml: -------------------------------------------------------------------------------- 1 | global: 2 | sendAnonymousUsage: false 3 | 4 | api: 5 | dashboard: true 6 | insecure: true 7 | 8 | log: 9 | level: INFO 10 | format: common 11 | 12 | entryPoints: 13 | web: 14 | address: :80 15 | 16 | providers: 17 | file: 18 | directory: /etc/traefik/ 19 | docker: 20 | exposedByDefault: false 21 | -------------------------------------------------------------------------------- /packages/web-wallet/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Layout/Layout'; 2 | export * from './Input/Input'; 3 | export * from './Select/Select'; 4 | export * from './Button/Button'; 5 | export * from './NavBar/NavBar'; 6 | export * from './TransactionStatusCard/TransactionStatusCard'; 7 | export * from './PageHeading/PageHeading'; 8 | export * from './Loader/Loader'; 9 | -------------------------------------------------------------------------------- /crates/webzjs-common/src/error.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::JsValue; 2 | 3 | #[derive(thiserror::Error, Debug)] 4 | pub enum Error { 5 | #[error("Invalid network string given: {0}")] 6 | InvalidNetwork(String), 7 | } 8 | 9 | impl From for JsValue { 10 | fn from(e: Error) -> Self { 11 | js_sys::Error::new(&e.to_string()).into() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/web-wallet/src/components/ErrorMessage/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ErrorMessageProps { 4 | text?: string; 5 | } 6 | 7 | function ErrorMessage({ text }: ErrorMessageProps): React.JSX.Element { 8 | return <>{text && {text}}; 9 | } 10 | 11 | export default ErrorMessage; 12 | -------------------------------------------------------------------------------- /packages/web-wallet/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/web-wallet/src/components/Loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Loader: React.FC = () => { 4 | return ( 5 |
6 |
9 |
10 | ); 11 | }; 12 | 13 | export default Loader; 14 | -------------------------------------------------------------------------------- /packages/web-wallet/src/types/snap.ts: -------------------------------------------------------------------------------- 1 | export type GetSnapsResponse = Record; 2 | 3 | export type Snap = { 4 | permissionName: string; 5 | id: string; 6 | version: string; 7 | initialPermissions: Record; 8 | }; 9 | 10 | export type SignPcztDetails = { 11 | pcztHexTring: string; 12 | signDetails: { 13 | recipient: string; 14 | amount: string; 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /traefik/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | grpc-proxy: 5 | image: "traefik:v3.1" 6 | container_name: "traefik" 7 | ports: 8 | - "1234:80" 9 | - "8080:8080" 10 | volumes: 11 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 12 | - "./traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro" 13 | - "./traefik/traefik.yml:/etc/traefik/traefik.yml:ro" 14 | -------------------------------------------------------------------------------- /packages/snap/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Json } from '@metamask/snaps-sdk'; 2 | 3 | export type SetBirthdayBlockParams = { latestBlock: number }; 4 | 5 | export type SignPcztParams = { 6 | pcztHexTring: string; 7 | signDetails: { 8 | recipient: string; 9 | amount: string; 10 | }; 11 | }; 12 | 13 | export interface SnapState extends Record { 14 | webWalletSyncStartBlock: string; 15 | } 16 | -------------------------------------------------------------------------------- /packages/e2e-tests/dist/serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": [ 3 | { 4 | "source": "**/*", 5 | "headers": [ 6 | { 7 | "key": "Cross-Origin-Opener-Policy", 8 | "value": "same-origin" 9 | }, 10 | { 11 | "key": "Cross-Origin-Embedder-Policy", 12 | "value": "require-corp" 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | rustflags = ["-C", "target-feature=+atomics,+bulk-memory,+mutable-globals"] 3 | 4 | # These are commented out and instead set in the justfile because we can't enable per-target unstable 5 | # features which are needed for WASM but not compatible with native builds. 6 | 7 | # [unstable] 8 | # build-std = ["panic_abort", "std"] 9 | 10 | # [build] 11 | # target = "wasm32-unknown-unknown" -------------------------------------------------------------------------------- /packages/web-wallet/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WebZjs Web Wallet 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/snap/src/rpc/setSnapState.tsx: -------------------------------------------------------------------------------- 1 | import { Json } from '@metamask/snaps-sdk'; 2 | import { SnapState } from 'src/types'; 3 | 4 | export async function setSnapState(newSnapState: SnapState): Promise { 5 | const state = (await snap.request({ 6 | method: 'snap_manageState', 7 | params: { 8 | operation: 'update', 9 | newState: newSnapState, 10 | }, 11 | })) as unknown as Json; 12 | 13 | return state; 14 | } 15 | -------------------------------------------------------------------------------- /packages/snap/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /webdriver.json: -------------------------------------------------------------------------------- 1 | { 2 | "moz:firefoxOptions": { 3 | "prefs": { 4 | "media.navigator.streams.fake": true, 5 | "media.navigator.permission.disabled": true 6 | }, 7 | "args": [] 8 | }, 9 | "goog:chromeOptions": { 10 | "args": [ 11 | "--use-fake-device-for-media-stream", 12 | "--use-fake-ui-for-media-stream", 13 | "--disable-timeouts-for-profiling" 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /packages/web-wallet/src/pages/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Outlet } from 'react-router-dom'; 3 | import NavBar from '../components/NavBar/NavBar'; 4 | 5 | const Dashboard: React.FC = () => { 6 | return ( 7 |
8 | 9 |
10 | 11 |
12 |
13 | ); 14 | }; 15 | 16 | export default Dashboard; 17 | -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /packages/web-wallet/src/types/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | ethereum: MetaMaskInpageProvider & { 4 | setProvider?: (provider: MetaMaskInpageProvider) => void; 5 | detected?: MetaMaskInpageProvider[]; 6 | providers?: MetaMaskInpageProvider[]; 7 | }; 8 | } 9 | 10 | interface WindowEventMap { 11 | 'eip6963:requestProvider': EIP6963RequestProviderEvent; 12 | 'eip6963:announceProvider': EIP6963AnnounceProviderEvent; 13 | } 14 | } 15 | 16 | export {}; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Common 2 | target 3 | node_modules 4 | .DS_STORE 5 | dist 6 | 7 | # Package Manager 8 | .parcel-cache 9 | .yarn/* 10 | !.yarn/patches 11 | !.yarn/plugins 12 | !.yarn/releases 13 | !.yarn/sdks 14 | !.yarn/versions 15 | 16 | # Developer tools 17 | .idea 18 | .vscode 19 | 20 | # e2e Tests package 21 | packages/e2e-tests/dist/* 22 | !/packages/e2e-tests/dist/serve.json 23 | 24 | # Web wallet 25 | packages/web-wallet/dist/* 26 | !packages/web-wallet/dist/_headers 27 | !packages/web-wallet/dist/_redirects 28 | 29 | -------------------------------------------------------------------------------- /packages/web-wallet/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | 3 | import { RouterProvider } from 'react-router-dom'; 4 | import { router } from './router'; 5 | import { WebZjsProvider } from './context/WebzjsContext'; 6 | import { MetaMaskProvider } from './context/MetamaskContext'; 7 | 8 | createRoot(document.getElementById('root')!).render( 9 | 10 | 11 | 12 | 13 | , 14 | ); 15 | -------------------------------------------------------------------------------- /packages/web-wallet/src/components/ProtectedRoute/ProtectedRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet } from 'react-router-dom'; 2 | import React from 'react'; 3 | import { useMetaMask } from '../../hooks'; 4 | 5 | const ProtectedRoute: React.FC<{ children?: React.ReactNode }> = ({ 6 | children, 7 | }) => { 8 | const { installedSnap } = useMetaMask(); 9 | 10 | if (!installedSnap) return ; 11 | 12 | return children ? <>{children} : ; 13 | }; 14 | 15 | export default ProtectedRoute; 16 | -------------------------------------------------------------------------------- /packages/snap/src/rpc/getSeedFingerprint.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | SeedFingerprint, 3 | } from '@chainsafe/webzjs-keys'; 4 | import { getSeed } from '../utils/getSeed'; 5 | 6 | export async function getSeedFingerprint(): Promise { 7 | 8 | const seed = await getSeed(); 9 | 10 | const seedFingerprint = new SeedFingerprint(seed); 11 | 12 | const seedFingerprintUint8 = seedFingerprint.to_bytes(); 13 | 14 | const seedFingerprintHexString = Buffer.from(seedFingerprintUint8).toString('hex'); 15 | 16 | return seedFingerprintHexString; 17 | } -------------------------------------------------------------------------------- /.github/workflows/web-tests.yml: -------------------------------------------------------------------------------- 1 | name: Web Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: main 7 | 8 | jobs: 9 | build-for-web: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: install wasm-pack 16 | uses: jetli/wasm-pack-action@v0.4.0 17 | with: 18 | version: latest 19 | 20 | - name: Install Just 21 | uses: extractions/setup-just@v2 22 | 23 | - name: Build 24 | run: just build 25 | -------------------------------------------------------------------------------- /packages/snap/snap.config.ts: -------------------------------------------------------------------------------- 1 | import type { SnapConfig } from '@metamask/snaps-cli'; 2 | import { resolve } from 'path'; 3 | 4 | const config: SnapConfig = { 5 | bundler: 'webpack', 6 | customizeWebpackConfig: (config) => { 7 | config.module?.rules?.push({ 8 | test: /\.wasm$/, 9 | type: 'asset/inline', 10 | }); 11 | return config; 12 | }, 13 | input: resolve(__dirname, 'src/index.tsx'), 14 | server: { 15 | port: 8080, 16 | }, 17 | polyfills: { 18 | buffer: true, 19 | }, 20 | }; 21 | 22 | export default config; 23 | -------------------------------------------------------------------------------- /packages/snap/build_prePublish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Ensure allowedOrigins only contains the production URL, then build the snap. 4 | 5 | set -euo pipefail 6 | 7 | MANIFEST_FILE="snap.manifest.json" 8 | 9 | echo "Ensuring allowedOrigins only contains https://webzjs.chainsafe.dev..." 10 | 11 | jq '.initialPermissions."endowment:rpc".allowedOrigins = ["https://webzjs.chainsafe.dev"]' "$MANIFEST_FILE" > "$MANIFEST_FILE.tmp" 12 | mv "$MANIFEST_FILE.tmp" "$MANIFEST_FILE" 13 | 14 | echo "Running mm-snap build..." 15 | mm-snap build 16 | 17 | echo "build_prePublish completed." 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/web-wallet/src/hooks/snaps/useGetSnapState.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useInvokeSnap } from './useInvokeSnap'; 3 | 4 | export interface SnapState { 5 | webWalletSyncStartBlock: string; 6 | } 7 | 8 | export const useGetSnapState = () => { 9 | const invokeSnap = useInvokeSnap(); 10 | 11 | const getSnapState = useCallback(async () => { 12 | const snapStateHome = (await invokeSnap({ 13 | method: 'getSnapStete', 14 | })) as unknown as SnapState; 15 | return snapStateHome; 16 | }, [invokeSnap]); 17 | 18 | return { getSnapState }; 19 | }; 20 | -------------------------------------------------------------------------------- /scripts/add_license.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Checks if the source code contains required license and adds it if necessary. 4 | # Returns 1 if there was a missing license, 0 otherwise. 5 | 6 | PAT_APA="^// Copyright 2024 ChainSafe Systems// SPDX-License-Identifier: Apache-2.0, MIT$" 7 | 8 | ret=0 9 | for file in $(git grep --cached -Il '' -- '*.rs'); do 10 | header=$(head -2 "$file" | tr -d '\n') 11 | if ! echo "$header" | grep -q "$PAT_APA"; then 12 | echo "$file was missing header" 13 | cat ./scripts/copyright.txt "$file" > temp 14 | mv temp "$file" 15 | ret=1 16 | fi 17 | done 18 | 19 | exit $ret 20 | -------------------------------------------------------------------------------- /packages/snap/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "esModuleInterop": true, 5 | "exactOptionalPropertyTypes": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "lib": ["ES2020", "DOM"], 8 | "module": "CommonJS", 9 | "moduleResolution": "node", 10 | "noEmit": true, 11 | "noErrorTruncation": true, 12 | "noUncheckedIndexedAccess": true, 13 | "skipLibCheck": true, 14 | "strict": true, 15 | "target": "es2020", 16 | "jsx": "react-jsx", 17 | "jsxImportSource": "@metamask/snaps-sdk" 18 | }, 19 | "include": ["**/*.ts", "**/*.tsx"] 20 | } 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Changes 2 | 3 | 9 | 10 | - 11 | - 12 | - 13 | 14 | ## Tests 15 | 16 | 21 | 22 | ``` 23 | 24 | ``` 25 | 26 | ## Issues 27 | 28 | 34 | 35 | - -------------------------------------------------------------------------------- /packages/web-wallet/src/components/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../Header/Header'; 3 | import Footer from '../Footer/Footer'; 4 | import { Outlet } from 'react-router-dom'; 5 | 6 | const Layout = ({ children }: React.PropsWithChildren): React.JSX.Element => { 7 | return ( 8 |
9 |
10 |
11 | {children ? children : } 12 |
13 |
14 |
15 | ); 16 | }; 17 | 18 | export default Layout; 19 | -------------------------------------------------------------------------------- /packages/snap/src/utils/dialogs.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Heading, Text, Link } from '@metamask/snaps-sdk/jsx'; 2 | 3 | export const installDialog = async () => { 4 | await snap.request({ 5 | method: 'snap_dialog', 6 | params: { 7 | type: 'alert', 8 | content: ( 9 | 10 | Thank you for installing Zcash Shielded Wallet snap 11 | 12 | This snap utilizes Zcash Web Wallet. Visit Zcash Web Wallet at{' '} 13 | 14 | webzjs.chainsafe.dev 15 | 16 | . 17 | 18 | 19 | ), 20 | }, 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/web-wallet/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useInterval } from 'usehooks-ts'; 2 | import { Outlet } from 'react-router-dom'; 3 | import { RESCAN_INTERVAL } from './config/constants'; 4 | import { useWebZjsActions } from './hooks'; 5 | import Layout from './components/Layout/Layout'; 6 | import { useMetaMaskContext } from './context/MetamaskContext'; 7 | 8 | function App() { 9 | const { triggerRescan } = useWebZjsActions(); 10 | const { installedSnap } = useMetaMaskContext(); 11 | 12 | useInterval(() => { 13 | triggerRescan(); 14 | }, installedSnap ? RESCAN_INTERVAL : null); 15 | 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /packages/e2e-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chainsafe/webzjs-e2e-tests", 3 | "version": "1.0.0", 4 | "description": "", 5 | "source": "src/index.html", 6 | "scripts": { 7 | "pretest": "parcel build --no-cache src/index.html", 8 | "test": "yarn pretest && playwright test" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@chainsafe/webzjs-keys": "workspace:^", 15 | "@chainsafe/webzjs-requests": "workspace:^", 16 | "@chainsafe/webzjs-wallet": "workspace:^", 17 | "@parcel/core": "^2.12.0", 18 | "@playwright/test": "^1.49.1", 19 | "@types/node": "^22.7.4", 20 | "parcel": "^2.12.0", 21 | "serve": "^14.2.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/snap/src/utils/initialiseWasm.ts: -------------------------------------------------------------------------------- 1 | import type { InitOutput } from '@chainsafe/webzjs-keys'; 2 | import { initSync } from '@chainsafe/webzjs-keys'; 3 | import wasmDataBase64 from '@chainsafe/webzjs-keys/webzjs_keys_bg.wasm'; 4 | 5 | export function initialiseWasm(): InitOutput { 6 | const base64String = wasmDataBase64 as unknown as string; 7 | // Check if the imported data is a data URL 8 | const base64Formatted = base64String.startsWith('data:') 9 | ? base64String.split(',')[1] 10 | : base64String; 11 | 12 | if (!base64Formatted) { 13 | throw new Error('Invalid WASM data'); 14 | } 15 | 16 | const wasmData = Buffer.from(base64Formatted, 'base64'); 17 | return initSync({ module: wasmData }); 18 | } 19 | -------------------------------------------------------------------------------- /packages/web-wallet/src/utils/balance.ts: -------------------------------------------------------------------------------- 1 | import { Decimal } from 'decimal.js'; 2 | 3 | const ZATS_PER_ZEC = 100_000_000; 4 | 5 | function zatsToZec(zats: number): number { 6 | return zats / ZATS_PER_ZEC; 7 | } 8 | 9 | function zecToZats(zecAmount: string): bigint { 10 | 11 | if (!/^\d+(\.\d+)?$/.test(zecAmount)) { 12 | throw new Error('Invalid ZEC format: must be positive number'); 13 | } 14 | 15 | const amount = new Decimal(zecAmount); 16 | 17 | if (amount.decimalPlaces() > 8) { 18 | throw new Error('Maximum 8 decimal places allowed'); 19 | } 20 | 21 | const zats = amount.mul(100_000_000).toDecimalPlaces(0, Decimal.ROUND_DOWN); 22 | return BigInt(zats.toFixed()); 23 | } 24 | 25 | export { zatsToZec, zecToZats }; -------------------------------------------------------------------------------- /packages/snap/build_local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to build the snap with localhost development origins 4 | 5 | echo "Adding localhost:3000 to allowed origins in snap.manifest.json..." 6 | 7 | # Use jq to modify the allowedOrigins array to include localhost:3000 8 | jq '.initialPermissions."endowment:rpc".allowedOrigins = ["https://webzjs.chainsafe.dev", "http://localhost:3000"]' snap.manifest.json > snap.manifest.json.tmp 9 | 10 | # Replace the original file with the modified version 11 | mv snap.manifest.json.tmp snap.manifest.json 12 | 13 | echo "Modified snap.manifest.json to include localhost:3000 for local development" 14 | 15 | # Run the build command 16 | echo "Running build..." 17 | yarn build 18 | 19 | echo "Build completed!" -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # WebZjs Doc Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Deployment to Github pages is automated via a [workflow](../.github/workflows/deploy-docs.yml) 30 | -------------------------------------------------------------------------------- /crates/webzjs-common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webzjs-common" 3 | version = "0.1.0" 4 | authors = ["ChainSafe Systems"] 5 | license = "MIT OR Apache-2.0" 6 | repository = "https://github.com/ChainSafe/WebZjs" 7 | description = "A browser client-side library for generating and manipulating zcash keys" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | serde.workspace = true 12 | zcash_primitives.workspace = true 13 | zcash_address.workspace = true 14 | zcash_protocol.workspace = true 15 | thiserror.workspace = true 16 | wasm-bindgen.workspace = true 17 | js-sys.workspace = true 18 | pczt = { workspace = true, default-features = false, features = ["orchard", "sapling", "transparent"] } 19 | serde-wasm-bindgen.workspace = true 20 | 21 | 22 | [lints] 23 | workspace = true -------------------------------------------------------------------------------- /packages/web-wallet/src/components/PageHeading/PageHeading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface PageHeadingProps { 4 | title: string; 5 | children?: React.ReactNode; 6 | } 7 | 8 | function PageHeading({ title, children }: PageHeadingProps) { 9 | return ( 10 |
11 |
12 |
13 |

14 | {title} 15 |

16 |
17 |
{children}
18 |
19 |
20 | ); 21 | } 22 | 23 | export default PageHeading; 24 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /crates/webzjs-wallet/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 ChainSafe Systems 2 | // SPDX-License-Identifier: Apache-2.0, MIT 3 | 4 | //! This is the top level documentation! 5 | 6 | #[cfg(feature = "wasm")] 7 | pub mod bindgen; 8 | 9 | mod error; 10 | pub mod init; 11 | 12 | pub mod wallet; 13 | pub use wallet::Wallet; 14 | 15 | use wasm_bindgen::prelude::*; 16 | 17 | /// The maximum number of checkpoints to store in each shard-tree 18 | pub const PRUNING_DEPTH: usize = 100; 19 | 20 | #[cfg(feature = "wasm-parallel")] 21 | pub use wasm_bindgen_rayon::init_thread_pool; 22 | // dummy NO-OP init_thread pool to maintain the same API between features 23 | #[cfg(not(feature = "wasm-parallel"))] 24 | #[wasm_bindgen(js_name = initThreadPool)] 25 | pub fn init_thread_pool(_threads: usize) {} 26 | 27 | #[wasm_bindgen] 28 | pub struct BlockRange(pub u32, pub u32); 29 | -------------------------------------------------------------------------------- /crates/webzjs-keys/src/error.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::JsValue; 2 | 3 | #[derive(thiserror::Error, Debug)] 4 | pub enum Error { 5 | #[error("webzjs-common crate gives error: {0}")] 6 | WebzJSCommon(#[from] webzjs_common::Error), 7 | #[error("Invalid account id")] 8 | AccountIdConversion(#[from] zcash_primitives::zip32::TryFromIntError), 9 | #[error("Failed to derive key from seed")] 10 | Derivation(#[from] zcash_keys::keys::DerivationError), 11 | #[error("Error attempting to decode key: {0}")] 12 | KeyDecoding(String), 13 | #[error("Failed to sign Pczt: {0}")] 14 | PcztSign(String), 15 | #[error("Error attempting to get seed fingerprint.")] 16 | SeedFingerprint, 17 | } 18 | 19 | impl From for JsValue { 20 | fn from(e: Error) -> Self { 21 | js_sys::Error::new(&e.to_string()).into() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/web-wallet/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /packages/web-wallet/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "src/types/*.d.ts"], 3 | "typeRoots": ["node_modules/@types", "src/types"], 4 | "compilerOptions": { 5 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 6 | "target": "ES2022", 7 | "useDefineForClassFields": true, 8 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 9 | "module": "ESNext", 10 | "skipLibCheck": true, 11 | 12 | /* Bundler mode */ 13 | "moduleResolution": "Bundler", 14 | "allowImportingTsExtensions": true, 15 | "isolatedModules": true, 16 | "moduleDetection": "force", 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "noUncheckedSideEffectImports": true, 26 | "baseUrl": ".", 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /.github/actions/install-rust-toolchain/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Install Rust Toolchain' 2 | description: 'Installs the toolchain defined in the rust-toolchain.toml file. Errors if that file is not present' 3 | 4 | inputs: 5 | components: 6 | description: Comma-separated list of components to be additionally installed 7 | required: false 8 | 9 | runs: 10 | using: "composite" 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Read toolchain file 15 | id: rust-toolchain 16 | run: | 17 | RUST_TOOLCHAIN=$(sed -n 's/^channel *= *"\(.*\)"$/\1/p' rust-toolchain.toml) 18 | echo "RUST_TOOLCHAIN=$RUST_TOOLCHAIN" >> $GITHUB_OUTPUT 19 | shell: bash 20 | 21 | - name: Install toolchain 22 | uses: dtolnay/rust-toolchain@master 23 | with: 24 | toolchain: ${{ steps.rust-toolchain.outputs.RUST_TOOLCHAIN }} 25 | components: ${{ inputs.components }} 26 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | export default sidebars; 34 | -------------------------------------------------------------------------------- /crates/webzjs-requests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webzjs-requests" 3 | version = "0.1.0" 4 | authors = ["ChainSafe Systems"] 5 | license = "MIT OR Apache-2.0" 6 | repository = "https://github.com/ChainSafe/WebZjs" 7 | description = "A browser client-side library for generating and manipulating zcash keys" 8 | edition = "2021" 9 | 10 | [lib] 11 | crate-type = ["cdylib", "rlib"] 12 | 13 | [package.metadata.wasm-pack.profile.release] 14 | wasm-opt = ["-O4", "-O4"] 15 | 16 | [dependencies] 17 | wasm-bindgen.workspace = true 18 | js-sys.workspace = true 19 | zcash_protocol.workspace = true 20 | zcash_address.workspace = true 21 | zcash_primitives.workspace = true 22 | zip321.workspace = true 23 | thiserror.workspace = true 24 | serde-wasm-bindgen.workspace = true 25 | 26 | # fixes "failed to resolve: use of undeclared crate or module `imp`" error 27 | getrandom = { version = "0.2", features = ["js"] } 28 | 29 | [lints] 30 | workspace = true -------------------------------------------------------------------------------- /packages/web-wallet/src/components/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChainsafePNG } from '../../assets'; 3 | 4 | const Footer = (): React.JSX.Element => { 5 | return ( 6 | 25 | ); 26 | }; 27 | 28 | export default Footer; 29 | -------------------------------------------------------------------------------- /packages/snap/src/utils/setSyncBlockHeight.ts: -------------------------------------------------------------------------------- 1 | //NU5 (Network Upgrade 5) (Block 1,687,104, May 31, 2022) 2 | const NU5_ACTIVATION = 1687104; 3 | 4 | export function setSyncBlockHeight( 5 | userInputCreationBlock: string | null, 6 | latestBlock: number, 7 | ): number { 8 | //In case input was empty, default to latestBlock 9 | if (userInputCreationBlock === null) return latestBlock; 10 | 11 | // Check if input is a valid number 12 | if (!/^\d+$/.test(userInputCreationBlock)) return latestBlock; 13 | 14 | const customBirthdayBlock = Number(userInputCreationBlock); 15 | 16 | // Check if custom block is higher than latest block 17 | if (customBirthdayBlock > latestBlock) return latestBlock; 18 | 19 | const latestAcceptableSyncBlock = NU5_ACTIVATION; 20 | 21 | //In case user entered older than acceptable block height 22 | return customBirthdayBlock > latestAcceptableSyncBlock 23 | ? customBirthdayBlock 24 | : latestAcceptableSyncBlock; 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "license": "Apache-2.0", 5 | "workspaces": [ 6 | "packages/*" 7 | ], 8 | "engines": { 9 | "node": ">=18.18.0" 10 | }, 11 | "devDependencies": { 12 | "prettier": "^3.3.3", 13 | "prettier-plugin-packagejson": "^2.5.3" 14 | }, 15 | "packageManager": "yarn@4.5.1", 16 | "scripts": { 17 | "dev": "yarn workspace @chainsafe/webzjs-web-wallet run dev & yarn run snap:start", 18 | "web-wallet:dev": "yarn workspace @chainsafe/webzjs-web-wallet run dev & yarn run snap:start", 19 | "web-wallet:build": "yarn workspace @chainsafe/webzjs-web-wallet run build", 20 | "test:e2e": "yarn workspace @chainsafe/webzjs-e2e-tests test", 21 | "snap:start": "yarn workspace @chainsafe/webzjs-zcash-snap run start", 22 | "snap:build": "yarn workspace @chainsafe/webzjs-zcash-snap run build", 23 | "just:build": "just build", 24 | "just:build-keys": "just build-keys" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/e2e-tests.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | pull_request: 4 | push: 5 | branches: main 6 | 7 | jobs: 8 | test: 9 | timeout-minutes: 60 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Install Just 15 | uses: extractions/setup-just@v2 16 | 17 | - name: install wasm-pack 18 | uses: jetli/wasm-pack-action@v0.4.0 19 | with: 20 | version: latest 21 | 22 | - name: build pkg 23 | run: just build 24 | 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: lts/* 28 | 29 | - run: corepack enable 30 | 31 | - name: Install dependencies 32 | run: yarn install --immutable 33 | 34 | - name: Install Playwright Browsers 35 | working-directory: ./packages/e2e-tests/ 36 | run: yarn playwright install --with-deps 37 | 38 | - name: Run Playwright tests 39 | if: false # Skip e2e test 40 | run: yarn run test:e2e 41 | -------------------------------------------------------------------------------- /crates/webzjs-requests/src/error.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::JsValue; 2 | 3 | #[derive(thiserror::Error, Debug)] 4 | pub enum Error { 5 | #[error("Error parsing zatoshi amount: {0}")] 6 | InvalidAmount(#[from] zcash_protocol::value::BalanceError), 7 | #[error("Attempted to create a transaction with a memo to an unsupported recipient. Only shielded addresses are supported.")] 8 | UnsupportedMemoRecipient, 9 | #[error("Error constructing ZIP321 transaction request: {0}")] 10 | Zip321(#[from] zip321::Zip321Error), 11 | #[error("Error decoding memo: {0}")] 12 | MemoDecoding(#[from] zcash_primitives::memo::Error), 13 | #[error("Error attempting to decode address: {0}")] 14 | AddressDecoding(#[from] zcash_address::ParseError), 15 | #[error("serde wasm-bindgen error")] 16 | SerdeWasmBindgen(#[from] serde_wasm_bindgen::Error), 17 | } 18 | 19 | impl From for JsValue { 20 | fn from(e: Error) -> Self { 21 | js_sys::Error::new(&e.to_string()).into() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /traefik/dynamic.yml: -------------------------------------------------------------------------------- 1 | http: 2 | routers: 3 | routerMainnet: 4 | service: srv-grpc-mainnet 5 | rule: PathPrefix(`/mainnet`) 6 | middlewares: 7 | - strip-network-prefix 8 | - "test-grpc-web" 9 | entryPoints: 10 | - web 11 | 12 | routerTestnet: 13 | service: srv-grpc-testnet 14 | rule: PathPrefix(`/testnet`) 15 | middlewares: 16 | - strip-network-prefix 17 | - "test-grpc-web" 18 | entryPoints: 19 | - web 20 | 21 | middlewares: 22 | test-grpc-web: 23 | grpcWeb: 24 | allowOrigins: 25 | - "*" 26 | 27 | strip-network-prefix: 28 | stripPrefix: 29 | prefixes: 30 | - "/mainnet" 31 | - "/testnet" 32 | 33 | services: 34 | srv-grpc-mainnet: 35 | loadBalancer: 36 | servers: 37 | - url: https://zec.rocks:443 38 | 39 | srv-grpc-testnet: 40 | loadBalancer: 41 | servers: 42 | - url: https://testnet.zec.rocks:443 -------------------------------------------------------------------------------- /.github/workflows/rust-checks.yml: -------------------------------------------------------------------------------- 1 | name: Rust-Checks 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: main 7 | 8 | jobs: 9 | fmt: 10 | runs-on: ubuntu-24.04 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Install Rust toolchain 16 | uses: ./.github/actions/install-rust-toolchain 17 | with: 18 | components: rustfmt 19 | 20 | - uses: actions-rs/cargo@v1 21 | with: 22 | command: fmt 23 | args: --all -- --check 24 | 25 | clippy: 26 | runs-on: ubuntu-24.04 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | 31 | - name: Install Rust toolchain 32 | uses: ./.github/actions/install-rust-toolchain 33 | with: 34 | components: clippy 35 | 36 | - uses: actions-rs/cargo@v1 37 | with: 38 | command: clippy 39 | args: --all --lib -- -D warnings -A deprecated -A unused-variables -A unused-imports 40 | -------------------------------------------------------------------------------- /packages/web-wallet/src/pages/Receive/Tab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CircleDashedSvg, CircleSvg } from '../../assets'; 3 | import cn from 'classnames'; 4 | 5 | interface TabProps { 6 | tabName: string; 7 | label: string; 8 | isActive: boolean; 9 | onClick: (key: string) => void; 10 | } 11 | 12 | const Tab: React.FC = ({ tabName, label, isActive, onClick }) => { 13 | return ( 14 |
onClick(tabName)} 16 | className={cn( 17 | 'px-4 py-2 justify-center items-center gap-1.5 flex rounded-3xl cursor-pointer', 18 | { 19 | 'bg-[#e8e8e8] text-black font-semibold': isActive, 20 | 'bg-transparent text-[#afafaf]': !isActive, 21 | }, 22 | )} 23 | > 24 |
25 | {isActive ? : } 26 |
{label}
27 |
28 |
29 | ); 30 | }; 31 | 32 | export default Tab; 33 | -------------------------------------------------------------------------------- /crates/webzjs-wallet/src/bindgen/proposal.rs: -------------------------------------------------------------------------------- 1 | use super::wallet::NoteRef; 2 | use wasm_bindgen::prelude::*; 3 | use zcash_client_backend::fees::StandardFeeRule; 4 | 5 | /// A handler to an immutable proposal. This can be passed to `create_proposed_transactions` to prove/authorize the transactions 6 | /// before they are sent to the network. 7 | /// 8 | /// The proposal can be reviewed by calling `describe` which will return a JSON object with the details of the proposal. 9 | #[wasm_bindgen] 10 | pub struct Proposal { 11 | inner: zcash_client_backend::proposal::Proposal, 12 | } 13 | 14 | impl From> for Proposal { 15 | fn from(inner: zcash_client_backend::proposal::Proposal) -> Self { 16 | Self { inner } 17 | } 18 | } 19 | 20 | impl From for zcash_client_backend::proposal::Proposal { 21 | fn from(proposal: Proposal) -> Self { 22 | proposal.inner 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/e2e-tests/src/index.js: -------------------------------------------------------------------------------- 1 | import initWasm, * as WebZJSWallet from '@chainsafe/webzjs-wallet'; 2 | import { initThreadPool, WebWallet } from '@chainsafe/webzjs-wallet'; 3 | import initKeys, * as WebZJSKeys from '@chainsafe/webzjs-keys'; 4 | import initRequests, * as WebZJSRequests from '@chainsafe/webzjs-requests'; 5 | 6 | window.WebZJSWallet = WebZJSWallet; 7 | window.WebZJSKeys = WebZJSKeys; 8 | window.WebZJSRequests = WebZJSRequests; 9 | 10 | const N_THREADS = 10; 11 | const MAINNET_LIGHTWALLETD_PROXY = 'https://zcash-mainnet.chainsafe.dev'; 12 | 13 | async function loadPage() { 14 | await new Promise((resolve) => { 15 | window.addEventListener('load', resolve); 16 | }); 17 | 18 | // Code to executed once the page has loaded 19 | await initWasm(); 20 | await initKeys(); 21 | await initRequests(); 22 | await initThreadPool(N_THREADS); 23 | 24 | window.webWallet = new WebWallet('main', MAINNET_LIGHTWALLETD_PROXY, 1); 25 | window.initialized = true; 26 | console.log('WebWallet initialized'); 27 | console.log(webWallet); 28 | } 29 | 30 | loadPage(); 31 | -------------------------------------------------------------------------------- /packages/snap/snap.manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.7", 3 | "description": "WebZjs Snap for Zcash wallet", 4 | "proposedName": "Zcash Shielded Wallet", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/ChainSafe/WebZjs.git" 8 | }, 9 | "source": { 10 | "shasum": "wTPZl0jiwxyFr3cUhss5/b0WjFGdZP9y/VIPafGtDAM=", 11 | "location": { 12 | "npm": { 13 | "filePath": "dist/bundle.js", 14 | "iconPath": "images/logo.svg", 15 | "packageName": "@chainsafe/webzjs-zcash-snap", 16 | "registry": "https://registry.npmjs.org/" 17 | } 18 | } 19 | }, 20 | "initialPermissions": { 21 | "snap_dialog": {}, 22 | "endowment:lifecycle-hooks": {}, 23 | "endowment:webassembly": {}, 24 | "endowment:rpc": { 25 | "allowedOrigins": [ 26 | "https://webzjs.chainsafe.dev" 27 | ] 28 | }, 29 | "snap_getBip44Entropy": [ 30 | { 31 | "coinType": 133 32 | } 33 | ], 34 | "snap_manageState": {} 35 | }, 36 | "platformVersion": "6.17.1", 37 | "manifestVersion": "0.1" 38 | } 39 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/icons/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/e2e-tests/README.md: -------------------------------------------------------------------------------- 1 | # WebZJS e2e Tests 2 | 3 | This package uses playwright to test the highest level API of WebZjs running in a browser. This allows testing the interaction of different WebWorkers with the page, as well as testing on different browsers. 4 | 5 | ## Writing Tests 6 | 7 | New tests should be added as files named `.spec.ts` inside the `e2e` directory. 8 | 9 | Tests should use the `page.evaluate` method from playwright to execute javascript code in the browser page where the wallet exists and return a result to the test runner to check. e.g. 10 | 11 | ```typescript 12 | test('Test some webzjs functionality..', async ({ page }) => { 13 | // code here runs in the test runner (node), not in the browser 14 | let result = await page.evaluate(async () => { 15 | // everything here executes in the web page 16 | // - do something with window.webWallet.. 17 | return // the result to the test runner 18 | }); 19 | expect(result).toBe(something); 20 | }); 21 | ``` 22 | 23 | The provided page has already initialized the Wasm environment and created a web wallet accessible in tests as `window.webWallet` 24 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ChainSafe Systems 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/web-wallet/src/hooks/snaps/useInvokeSnap.ts: -------------------------------------------------------------------------------- 1 | import { defaultSnapOrigin } from '../../config'; 2 | import { useRequest } from './useRequest.ts'; 3 | 4 | export type InvokeSnapParams = { 5 | method: string; 6 | params?: Record; 7 | }; 8 | 9 | /** 10 | * Utility hook to wrap the `wallet_invokeSnap` method. 11 | * 12 | * @param snapId - The Snap ID to invoke. Defaults to the snap ID specified in the 13 | * config. 14 | * @returns The invokeSnap wrapper method. 15 | */ 16 | export const useInvokeSnap = (snapId = defaultSnapOrigin) => { 17 | const request = useRequest(); 18 | 19 | /** 20 | * Invoke the requested Snap method. 21 | * 22 | * @param params - The invoke params. 23 | * @param params.method - The method name. 24 | * @param params.params - The method params. 25 | * @returns The Snap response. 26 | */ 27 | const invokeSnap = async ({ method, params }: InvokeSnapParams) => 28 | request({ 29 | method: 'wallet_invokeSnap', 30 | params: { 31 | snapId, 32 | request: params ? { method, params } : { method }, 33 | }, 34 | }); 35 | 36 | return invokeSnap; 37 | }; 38 | -------------------------------------------------------------------------------- /crates/webzjs-common/src/pczt.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use wasm_bindgen::prelude::*; 3 | 4 | #[wasm_bindgen] 5 | #[derive(Clone, Debug, Serialize, Deserialize)] 6 | pub struct Pczt(pczt::Pczt); 7 | 8 | impl From for Pczt { 9 | fn from(pczt: pczt::Pczt) -> Self { 10 | Self(pczt) 11 | } 12 | } 13 | 14 | impl From for pczt::Pczt { 15 | fn from(pczt: Pczt) -> Self { 16 | pczt.0 17 | } 18 | } 19 | 20 | #[wasm_bindgen] 21 | impl Pczt { 22 | /// Returns a JSON object with the details of the Pczt. 23 | pub fn to_json(&self) -> JsValue { 24 | serde_wasm_bindgen::to_value(&self).unwrap() 25 | } 26 | 27 | /// Returns a Pczt from a JSON object 28 | pub fn from_json(s: JsValue) -> Pczt { 29 | serde_wasm_bindgen::from_value(s).unwrap() 30 | } 31 | 32 | /// Returns the postcard serialization of the Pczt. 33 | pub fn serialize(&self) -> Vec { 34 | self.0.serialize() 35 | } 36 | 37 | /// Deserialize to a Pczt from postcard bytes. 38 | pub fn from_bytes(bytes: &[u8]) -> Pczt { 39 | Self(pczt::Pczt::parse(bytes).unwrap()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #25c2a0; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /packages/web-wallet/src/hooks/snaps/useRequest.ts: -------------------------------------------------------------------------------- 1 | import type { RequestArguments } from '@metamask/providers'; 2 | 3 | import { useMetaMaskContext } from '../../context/MetamaskContext'; 4 | 5 | export type Request = (params: RequestArguments) => Promise; 6 | 7 | /** 8 | * Utility hook to consume the provider `request` method with the available provider. 9 | * 10 | * @returns The `request` function. 11 | */ 12 | export const useRequest = () => { 13 | const { provider, setError } = useMetaMaskContext(); 14 | 15 | /** 16 | * `provider.request` wrapper. 17 | * 18 | * @param params - The request params. 19 | * @param params.method - The method to call. 20 | * @param params.params - The method params. 21 | * @returns The result of the request. 22 | */ 23 | 24 | const request: Request = async ({ method, params }) => { 25 | try { 26 | const data = 27 | (await provider?.request({ 28 | method, 29 | params, 30 | } as RequestArguments)) ?? null; 31 | 32 | return data; 33 | } catch (requestError) { 34 | setError(requestError as Error); 35 | 36 | throw requestError; 37 | } 38 | }; 39 | 40 | return request; 41 | }; 42 | -------------------------------------------------------------------------------- /crates/webzjs-keys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webzjs-keys" 3 | version = "0.1.0" 4 | authors = ["ChainSafe Systems"] 5 | license = "MIT OR Apache-2.0" 6 | repository = "https://github.com/ChainSafe/WebZjs" 7 | description = "A browser client-side library for generating and manipulating zcash keys" 8 | edition = "2021" 9 | 10 | [lib] 11 | crate-type = ["cdylib", "rlib"] 12 | 13 | [package.metadata.wasm-pack.profile.release] 14 | wasm-opt = ["-O4", "-O4"] 15 | 16 | [dependencies] 17 | webzjs-common = { path = "../webzjs-common" } 18 | 19 | js-sys.workspace = true 20 | thiserror.workspace = true 21 | wasm-bindgen.workspace = true 22 | zcash_primitives = { workspace = true, features = ["transparent-inputs"] } 23 | zcash_keys.workspace = true 24 | bip0039.workspace = true 25 | zip32 = "0.1" 26 | sapling = { workspace = true } 27 | bip32 = "0.5" 28 | pczt = { workspace = true, default-features = false, features = ["signer", "orchard", "sapling", "transparent", "tx-extractor"] } 29 | orchard = { version = "0.10.1", default-features = false } 30 | wasm-bindgen-futures = "0.4.43" 31 | 32 | # fixes "failed to resolve: use of undeclared crate or module `imp`" error 33 | getrandom = { version = "0.2", features = ["js"] } 34 | 35 | [lints] 36 | workspace = true -------------------------------------------------------------------------------- /packages/web-wallet/src/hooks/snaps/useRequestSnap.ts: -------------------------------------------------------------------------------- 1 | import { defaultSnapOrigin } from '../../config'; 2 | import type { Snap } from '../../types'; 3 | import { useMetaMaskContext } from '../../context/MetamaskContext'; 4 | import { useRequest } from './useRequest.ts'; 5 | 6 | /** 7 | * Utility hook to wrap the `wallet_requestSnaps` method. 8 | * 9 | * @param snapId - The requested Snap ID. Defaults to the snap ID specified in the 10 | * config. 11 | * @param version - The requested version. 12 | * @returns The `wallet_requestSnaps` wrapper. 13 | */ 14 | export const useRequestSnap = ( 15 | snapId = defaultSnapOrigin, 16 | version?: string, 17 | ) => { 18 | const request = useRequest(); 19 | const { setInstalledSnap } = useMetaMaskContext(); 20 | 21 | /** 22 | * Request the Snap. 23 | */ 24 | const requestSnap = async () => { 25 | const snaps = (await request({ 26 | method: 'wallet_requestSnaps', 27 | params: { 28 | [snapId]: version ? { version } : {}, 29 | }, 30 | })) as Record; 31 | 32 | // Updates the `installedSnap` context variable since we just installed the Snap. 33 | setInstalledSnap(snaps?.[snapId] ?? null); 34 | }; 35 | 36 | return requestSnap; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/web-wallet/src/components/CopyButton/CopyButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | interface CopyButtonProps { 4 | textToCopy: string; 5 | } 6 | 7 | const HIDE_IN_SECONDS = 2000; 8 | 9 | const CopyButton: React.FC = ({ textToCopy }) => { 10 | const [copied, setCopied] = useState(false); 11 | 12 | const handleCopy = async () => { 13 | try { 14 | await navigator.clipboard.writeText(textToCopy); 15 | setCopied(true); 16 | setTimeout(() => setCopied(false), HIDE_IN_SECONDS); 17 | } catch (err) { 18 | console.error('Failed to copy text:', err); 19 | } 20 | }; 21 | 22 | return ( 23 |
24 | {copied && ( 25 |
29 | Address copied! 30 |
31 | )} 32 | 38 |
39 | ); 40 | }; 41 | 42 | export default CopyButton; 43 | -------------------------------------------------------------------------------- /packages/web-wallet/src/router/index.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter } from 'react-router-dom'; 2 | import App from '../App'; 3 | import ProtectedRoute from '../components/ProtectedRoute/ProtectedRoute'; 4 | import Home from '../pages/Home'; 5 | import Dashboard from '../pages/Dashboard'; 6 | import AccountSummary from '../pages/AccountSummary'; 7 | import TransferBalance from '../pages/TransferBalance/TransferBalance'; 8 | import Receive from '../pages/Receive/Receive'; 9 | import { ShieldBalance } from 'src/pages/ShieldBalance/ShieldBalance'; 10 | 11 | const router = createBrowserRouter([ 12 | { 13 | path: '/', 14 | element: , 15 | children: [ 16 | { 17 | index: true, 18 | element: , 19 | }, 20 | { 21 | path: 'dashboard', 22 | element: ( 23 | 24 | 25 | 26 | ), 27 | children: [ 28 | { path: 'account-summary', element: }, 29 | { path: 'transfer-balance', element: }, 30 | { path: 'shield-balance', element: }, 31 | { path: 'receive', element: }, 32 | ], 33 | }, 34 | ], 35 | }, 36 | ]); 37 | 38 | export { router }; 39 | -------------------------------------------------------------------------------- /packages/snap/src/utils/hexStringToUint8Array.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a hexadecimal string to a Uint8Array. 3 | * @param hexString - The hexadecimal string to convert. 4 | * @returns A Uint8Array representing the binary data of the hex string. 5 | * @throws Will throw an error if the input string contains non-hex characters or has an odd length. 6 | */ 7 | export function hexStringToUint8Array(hexString: string): Uint8Array { 8 | // Remove any leading "0x" or "0X" if present 9 | if (hexString.startsWith('0x') || hexString.startsWith('0X')) { 10 | hexString = hexString.slice(2); 11 | } 12 | 13 | // Validate that the string contains only hexadecimal characters 14 | if (!/^[0-9a-fA-F]*$/.test(hexString)) { 15 | throw new Error('Hex string contains invalid characters'); 16 | } 17 | 18 | if (hexString.length % 2 !== 0) { 19 | throw new Error('Hex string must have an even length'); 20 | } 21 | 22 | const byteArray = new Uint8Array(hexString.length / 2); 23 | 24 | for (let i = 0; i < byteArray.length; i++) { 25 | const byte = hexString.slice(i * 2, i * 2 + 2); 26 | const byteValue = parseInt(byte, 16); 27 | 28 | if (isNaN(byteValue)) { 29 | throw new Error(`Invalid hex byte: "${byte}"`); 30 | } 31 | 32 | byteArray[i] = byteValue; 33 | } 34 | 35 | return byteArray; 36 | } 37 | -------------------------------------------------------------------------------- /docs/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Link from '@docusaurus/Link'; 3 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 4 | import Layout from '@theme/Layout'; 5 | 6 | import Heading from '@theme/Heading'; 7 | import styles from './index.module.css'; 8 | 9 | function HomepageHeader() { 10 | const {siteConfig} = useDocusaurusContext(); 11 | return ( 12 |
13 |
14 | 15 | {siteConfig.title} 16 | 17 |

{siteConfig.tagline}

18 |
19 | 22 | Beginner Tutorial - 5min ⏱️ 23 | 24 |
25 |
26 |
27 | ); 28 | } 29 | 30 | export default function Home() { 31 | const {siteConfig} = useDocusaurusContext(); 32 | return ( 33 | 36 | 37 |
38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webzjs-docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids" 15 | }, 16 | "dependencies": { 17 | "@docusaurus/core": "3.4.0", 18 | "@docusaurus/preset-classic": "3.4.0", 19 | "@mdx-js/react": "^3.0.0", 20 | "clsx": "^2.0.0", 21 | "docusaurus-plugin-typedoc-api": "^4.2.1", 22 | "prism-react-renderer": "^2.3.0", 23 | "react": "^18.0.0", 24 | "react-dom": "^18.0.0" 25 | }, 26 | "devDependencies": { 27 | "@docusaurus/module-type-aliases": "^3.4.0", 28 | "@docusaurus/tsconfig": "^3.4.0", 29 | "@docusaurus/types": "^3.4.0", 30 | "typescript": "^5.4.5" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.5%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 3 chrome version", 40 | "last 3 firefox version", 41 | "last 5 safari version" 42 | ] 43 | }, 44 | "engines": { 45 | "node": ">=18.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/icons/shield.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/web-wallet/src/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cn from 'classnames'; 3 | 4 | type ButtonVariant = 'primary' | 'secondary'; 5 | 6 | interface ButtonProps extends React.ButtonHTMLAttributes { 7 | onClick: () => void; 8 | label: string; 9 | classNames?: string; 10 | variant?: ButtonVariant; 11 | icon?: React.ReactNode; 12 | } 13 | 14 | function Button({ 15 | onClick, 16 | label, 17 | classNames = '', 18 | variant = 'primary', 19 | icon, 20 | ...rest 21 | }: ButtonProps) { 22 | const buttonClasses = cn( 23 | 'min-w-[228px] px-6 py-3 rounded-3xl text-base font-medium leading-normal', 24 | 'transition-all hover:transition-all', 25 | { 'cursor-not-allowed': rest.disabled, 'cursor-pointer': !rest.disabled }, 26 | { 27 | 'bg-[#0e0e0e] text-white border hover:bg-buttonBlackGradientHover': 28 | variant === 'primary', 29 | 'bg-transparent text-black hover:bg-[#fff7e6] border hover:border-[#ffa940]': 30 | variant === 'secondary', 31 | }, 32 | classNames, 33 | ); 34 | 35 | return ( 36 | 42 | ); 43 | } 44 | 45 | export default Button; 46 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/web-wallet/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { fileURLToPath } from 'url'; 3 | import { dirname, join } from 'path'; 4 | import { Parcel } from '@parcel/core'; 5 | import history from 'connect-history-api-fallback'; 6 | import dotenv from 'dotenv'; 7 | 8 | dotenv.config(); 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = dirname(__filename); 12 | 13 | async function run() { 14 | const bundler = new Parcel({ 15 | entries: 'index.html', 16 | defaultConfig: '@parcel/config-default', 17 | mode: 'development', 18 | defaultTargetOptions: { 19 | distDir: 'dist', 20 | }, 21 | }); 22 | 23 | // Watch in background 24 | await bundler.watch((err) => { 25 | if (err) { 26 | console.error('Build error:', err.diagnostics); 27 | } else { 28 | console.log('Parcel build successful'); 29 | } 30 | }); 31 | 32 | const app = express(); 33 | 34 | // Set custom headers (for WASM multi-threading) 35 | app.use((req, res, next) => { 36 | res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); 37 | res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); 38 | next(); 39 | }); 40 | 41 | app.use(history()); 42 | app.use(express.static(join(__dirname, 'dist'))); 43 | 44 | app.listen(3000, () => { 45 | console.log('Server running on http://localhost:3000'); 46 | }); 47 | } 48 | 49 | run().catch((err) => { 50 | console.error(err); 51 | process.exit(1); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/index.ts: -------------------------------------------------------------------------------- 1 | import ChainsafePNG from './chainsafe.png'; 2 | import FormTransferSvg from './form-transfer.svg'; 3 | import MetaMaskLogoPNG from './metaMask-logo.png'; 4 | import MetaMaskSnapsLogoPNG from './metamask-snaps-logo.png'; 5 | import ZcashPNG from './zcash.png'; 6 | import ZcashYellowPNG from './zcash-yellow.png'; 7 | 8 | // Icons 9 | import ArrowReceiveSvg from './icons/arrow-receive.svg'; 10 | import ArrowTransferSvg from './icons/arrow-transfer.svg'; 11 | import ClockSvg from './icons/clock.svg'; 12 | import ShieldSvg from './icons/shield.svg'; 13 | import ShieldDividedSvg from './icons/shield-divided.svg'; 14 | import SummarySvg from './icons/summary.svg'; 15 | import CoinsSvg from './icons/coins.svg'; 16 | import ChevronSVG from './icons/chevron.svg'; 17 | import CheckSVG from './icons/check.svg'; 18 | import WarningSVG from './icons/warning.svg'; 19 | import EyeSvg from './icons/eye.svg'; 20 | import EyeSlashSvg from './icons/eye-slash.svg'; 21 | import CircleSvg from './icons/circle.svg'; 22 | import CircleDashedSvg from './icons/circle-dashed.svg'; 23 | 24 | export { 25 | ChainsafePNG, 26 | MetaMaskLogoPNG, 27 | MetaMaskSnapsLogoPNG, 28 | ZcashPNG, 29 | ZcashYellowPNG, 30 | FormTransferSvg, 31 | ArrowReceiveSvg, 32 | ArrowTransferSvg, 33 | ClockSvg, 34 | ShieldSvg, 35 | ShieldDividedSvg, 36 | SummarySvg, 37 | CoinsSvg, 38 | ChevronSVG, 39 | CheckSVG, 40 | WarningSVG, 41 | EyeSvg, 42 | EyeSlashSvg, 43 | CircleSvg, 44 | CircleDashedSvg, 45 | }; 46 | -------------------------------------------------------------------------------- /packages/web-wallet/src/components/TransactionStatusCard/TransactionStatusCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface TransactionStatusCardProps { 4 | icon: React.JSX.Element; 5 | headText: string; 6 | statusMessage?: string; 7 | children?: React.ReactNode; 8 | } 9 | 10 | function TransactionStatusCard({ 11 | icon, 12 | headText, 13 | statusMessage, 14 | ...props 15 | }: TransactionStatusCardProps): React.JSX.Element { 16 | return ( 17 |
18 |
19 |
20 |
21 | {headText} 22 |
23 |
{icon}
24 |
25 |
26 |
27 | {statusMessage} 28 |
29 |
30 |
31 | {props.children} 32 |
33 |
34 |
35 | ); 36 | } 37 | 38 | export default TransactionStatusCard; 39 | -------------------------------------------------------------------------------- /packages/web-wallet/src/pages/Receive/QrCode.tsx: -------------------------------------------------------------------------------- 1 | import QRCode from 'react-qr-code'; 2 | import { EyeSlashSvg, EyeSvg } from '../../assets'; 3 | import { useState } from 'react'; 4 | import CopyButton from '../../components/CopyButton/CopyButton'; 5 | 6 | interface QrCodeProps { 7 | address: string; // Unified or transparent 8 | } 9 | 10 | function QrCode({ address }: QrCodeProps) { 11 | const [exposeAddress, setExposeAddress] = useState(false); 12 | 13 | return ( 14 | <> 15 |
16 | {address && } 17 |
18 |
19 |
20 | {exposeAddress ? ( 21 | {address} 22 | ) : ( 23 | 24 | {address} 25 | 26 | )} 27 |
28 |
setExposeAddress(!exposeAddress)} 31 | > 32 | {exposeAddress ? : } 33 |
34 |
35 | 36 |
37 |
38 | 39 | ); 40 | } 41 | export default QrCode; 42 | -------------------------------------------------------------------------------- /packages/e2e-tests/e2e/tx_request.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | import type * as WebZJSWallet from "@chainsafe/webzjs-wallet"; 4 | import type * as WebZJSRequests from "@chainsafe/webzjs-requests"; 5 | 6 | declare global { 7 | interface Window { 8 | initialized: boolean; 9 | WebZJSWallet: typeof WebZJSWallet; 10 | WebZJSRequests: typeof WebZJSRequests; 11 | } 12 | } 13 | 14 | test.beforeEach(async ({ page }) => { 15 | await page.goto("/"); 16 | await page.waitForFunction(() => window.initialized === true); 17 | }); 18 | 19 | test("decode from uri", async ({ page }) => { 20 | let result = await page.evaluate(async () => { 21 | const uri = 22 | "zcash:u1mcxxpa0wyyd3qpkl8rftsa6n7tkh9lv8u8j3zpd9f6qz37dqwur38w6tfl5rpv7m8g8mlca7nyn7qxr5qtjemjqehcttwpupz3fk76q8ft82yh4scnyxrxf2jgywgr5f9ttzh8ah8ljpmr8jzzypm2gdkcfxyh4ad93c889qv3l4pa748945c372ku7kdglu388zsjvrg9dskr0v9zj?amount=1&message=Thank%20you%20for%20your%20purchase"; 23 | let request = window.WebZJSRequests.TransactionRequest.from_uri(uri); 24 | return { 25 | total: request.total(), 26 | to: request.payment_requests()[0].recipient_address(), 27 | message: request.payment_requests()[0].message(), 28 | }; 29 | }); 30 | expect(result.total).toBe(100000000n); // 1 ZEC 31 | expect(result.to).toBe( 32 | "u1mcxxpa0wyyd3qpkl8rftsa6n7tkh9lv8u8j3zpd9f6qz37dqwur38w6tfl5rpv7m8g8mlca7nyn7qxr5qtjemjqehcttwpupz3fk76q8ft82yh4scnyxrxf2jgywgr5f9ttzh8ah8ljpmr8jzzypm2gdkcfxyh4ad93c889qv3l4pa748945c372ku7kdglu388zsjvrg9dskr0v9zj" 33 | ); 34 | expect(result.message).toBe("Thank you for your purchase"); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/web-wallet/src/components/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ErrorMessage from '../ErrorMessage/ErrorMessage'; 3 | 4 | interface InputProps extends React.InputHTMLAttributes { 5 | label?: string; 6 | error?: string; 7 | containerClassName?: string; 8 | labelClassName?: string; 9 | inputClassName?: string; 10 | suffix?: string; 11 | id: string; 12 | } 13 | 14 | const Input: React.FC = ({ 15 | label, 16 | error, 17 | containerClassName = '', 18 | labelClassName = '', 19 | inputClassName = '', 20 | suffix = '', 21 | id, 22 | ...props 23 | }) => { 24 | return ( 25 |
26 | {label && ( 27 | 33 | )} 34 |
35 | 41 | 45 | {suffix} 46 | 47 |
48 | 49 |
50 | ); 51 | }; 52 | 53 | export default Input; 54 | -------------------------------------------------------------------------------- /packages/web-wallet/src/hooks/snaps/useMetaMask.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | import { useMetaMaskContext } from '../../context/MetamaskContext'; 4 | import { useRequest } from './useRequest.ts'; 5 | import { GetSnapsResponse } from '../../types'; 6 | import { defaultSnapOrigin } from '../../config'; 7 | 8 | /** 9 | * A Hook to retrieve useful data from MetaMask. 10 | * @returns The information. 11 | */ 12 | export const useMetaMask = () => { 13 | const { provider, setInstalledSnap, installedSnap } = useMetaMaskContext(); 14 | const request = useRequest(); 15 | const [isFlask, setIsFlask] = useState(false); 16 | 17 | const snapsDetected = provider !== null; 18 | 19 | /** 20 | * Detect if the version of MetaMask is Flask. 21 | */ 22 | const detectFlask = async () => { 23 | const clientVersion = await request({ 24 | method: 'web3_clientVersion', 25 | }); 26 | 27 | const isFlaskDetected = (clientVersion as string[])?.includes('flask'); 28 | 29 | setIsFlask(isFlaskDetected); 30 | }; 31 | 32 | /** 33 | * Get the Snap informations from MetaMask. 34 | */ 35 | const getSnap = async () => { 36 | const snaps = (await request({ 37 | method: 'wallet_getSnaps', 38 | })) as GetSnapsResponse; 39 | 40 | setInstalledSnap(snaps[defaultSnapOrigin] ?? null); 41 | }; 42 | 43 | useEffect(() => { 44 | const detect = async () => { 45 | if (provider) { 46 | await detectFlask(); 47 | await getSnap(); 48 | } 49 | }; 50 | 51 | detect().catch(console.error); 52 | }, [provider]); 53 | 54 | return { isFlask, snapsDetected, installedSnap, getSnap }; 55 | }; 56 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/icons/coins.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/deploy-demo.yml: -------------------------------------------------------------------------------- 1 | name: Build and Web Wallet demo to Netlify 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ['main'] 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Install Just 17 | uses: extractions/setup-just@v2 18 | 19 | - name: install wasm-pack 20 | uses: jetli/wasm-pack-action@v0.4.0 21 | with: 22 | version: latest 23 | 24 | - name: build pkg 25 | run: just build 26 | 27 | - uses: actions/setup-node@v4 28 | with: 29 | node-version: lts/* 30 | - run: corepack enable 31 | 32 | - name: Install dependencies 33 | run: yarn install --immutable 34 | 35 | - name: Build 36 | run: yarn web-wallet:build 37 | env: 38 | SNAP_ORIGIN: npm:@chainsafe/webzjs-zcash-snap 39 | 40 | - name: Deploy to Netlify 41 | uses: nwtgck/actions-netlify@v3.0 42 | with: 43 | publish-dir: './packages/web-wallet/dist' 44 | production-branch: main 45 | github-token: ${{ secrets.GITHUB_TOKEN }} 46 | deploy-message: "Deploy from GitHub Actions" 47 | enable-pull-request-comment: false 48 | enable-commit-comment: true 49 | overwrites-pull-request-comment: true 50 | env: 51 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 52 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 53 | timeout-minutes: 1 54 | -------------------------------------------------------------------------------- /packages/snap/src/rpc/getViewingKey.tsx: -------------------------------------------------------------------------------- 1 | import { UnifiedSpendingKey } from '@chainsafe/webzjs-keys'; 2 | import { getSeed } from '../utils/getSeed'; 3 | import { Box, Copyable, Divider, Heading, Text } from '@metamask/snaps-sdk/jsx'; 4 | 5 | type Network = 'main' | 'test'; 6 | 7 | export async function getViewingKey( 8 | origin: string, 9 | network: Network = 'main', 10 | accountIndex: number = 0, 11 | ) { 12 | 13 | try { 14 | // Retrieve the BIP-44 entropy from MetaMask 15 | const seed = await getSeed(); 16 | 17 | // Generate the UnifiedSpendingKey and obtain the Viewing Key 18 | const spendingKey = new UnifiedSpendingKey(network, seed, accountIndex); 19 | const viewingKey = spendingKey.to_unified_full_viewing_key().encode(network); 20 | 21 | const dialogApproved = await snap.request({ 22 | method: 'snap_dialog', 23 | params: { 24 | type: 'confirmation', 25 | content: ( 26 | 27 | Reveal Viewing Key to the {origin} 28 | 29 | Web wallet {origin} needs access to the Viewing Key, approve this dialog to give permission. 30 | Viewing Key is used to create a new account in the Zcash Web Wallet. Web wallet account is serialized and stored only locally. Viewing Key is not sent, logged or stored in any way. 31 | 32 | 33 | 34 | ), 35 | }, 36 | }); 37 | 38 | if (!dialogApproved) { 39 | throw new Error('User rejected'); 40 | } 41 | 42 | return viewingKey 43 | } catch (error) { 44 | throw new Error('Failed to generate Viewing Key'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/icons/summary.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/check-snap-allowed-origins.yml: -------------------------------------------------------------------------------- 1 | name: Enforce Snap allowedOrigins 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | paths: 7 | - 'packages/snap/snap.manifest.json' 8 | - '.github/workflows/check-snap-allowed-origins.yml' 9 | push: 10 | branches: [ main ] 11 | paths: 12 | - 'packages/snap/snap.manifest.json' 13 | 14 | jobs: 15 | check-allowed-origins: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Verify allowedOrigins in snap.manifest.json 22 | shell: bash 23 | run: | 24 | set -euo pipefail 25 | FILE="packages/snap/snap.manifest.json" 26 | 27 | if [ ! -f "$FILE" ]; then 28 | echo "::error title=Missing file::$FILE not found" 29 | exit 1 30 | fi 31 | 32 | if ! command -v jq >/dev/null 2>&1; then 33 | echo "Installing jq..." 34 | sudo apt-get update -y 35 | sudo apt-get install -y jq 36 | fi 37 | 38 | expected='["https://webzjs.chainsafe.dev"]' 39 | actual=$(jq -c '.initialPermissions["endowment:rpc"].allowedOrigins' "$FILE") 40 | 41 | echo "allowedOrigins in manifest: $actual" 42 | 43 | if [ "$actual" != "$expected" ]; then 44 | echo "::error title=Invalid allowedOrigins::For merges to main, allowedOrigins must be $expected. Found: $actual" 45 | if echo "$actual" | grep -qi "localhost"; then 46 | echo "::error title=localhost detected::Remove any localhost origins from allowedOrigins." 47 | fi 48 | exit 1 49 | fi 50 | 51 | echo "allowedOrigins are valid." 52 | 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ['main'] 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | # Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | jobs: 17 | deploy: 18 | environment: 19 | name: github-pages 20 | url: ${{ steps.deployment.outputs.page_url }} 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | 26 | - name: Install Just 27 | uses: extractions/setup-just@v2 28 | 29 | - name: install wasm-pack 30 | uses: jetli/wasm-pack-action@v0.4.0 31 | with: 32 | version: latest 33 | 34 | - name: build pkg 35 | run: just build 36 | 37 | - name: Set up Node.js 38 | uses: actions/setup-node@v3 39 | with: 40 | node-version: 20.x 41 | cache: 'npm' 42 | cache-dependency-path: './docs/package-lock.json' 43 | 44 | - name: Install dependencies 45 | working-directory: ./docs 46 | run: npm install --frozen-lockfile --non-interactive 47 | 48 | - name: Build 49 | working-directory: ./docs 50 | run: npm run build 51 | 52 | # 👆 Build steps 53 | - name: Setup Pages 54 | uses: actions/configure-pages@v3 55 | 56 | - name: Upload artifact 57 | uses: actions/upload-pages-artifact@v3 58 | with: 59 | path: ./docs/build 60 | 61 | - name: Deploy to GitHub Pages 62 | id: deployment 63 | uses: actions/deploy-pages@v4 64 | -------------------------------------------------------------------------------- /packages/web-wallet/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /packages/snap/src/utils/getSeed.ts: -------------------------------------------------------------------------------- 1 | import { hexStringToUint8Array } from './hexStringToUint8Array'; 2 | 3 | export async function getSeed(): Promise { 4 | const entropyNode = await snap.request({ 5 | method: 'snap_getBip44Entropy', 6 | params: { coinType: 133 }, // 133 is the coin type for Zcash https://github.com/satoshilabs/slips/blob/master/slip-0044.md 7 | }); 8 | 9 | if ( 10 | typeof entropyNode !== 'object' || 11 | entropyNode === null || 12 | !('privateKey' in entropyNode) 13 | ) { 14 | throw new Error('Invalid entropy structure received from MetaMask'); 15 | } 16 | 17 | const { privateKey } = entropyNode; 18 | const validatedKey = validatePrivateKey(privateKey); 19 | const seed = hexStringToUint8Array(validatedKey); 20 | 21 | if (seed.length !== 32) { 22 | throw new Error( 23 | `Invalid seed length: expected 32 bytes, got ${seed.length} bytes`, 24 | ); 25 | } 26 | 27 | return seed; 28 | } 29 | 30 | function validatePrivateKey(privateKey: string | undefined): string { 31 | if (typeof privateKey !== 'string') { 32 | throw new Error('privateKey must be a string'); 33 | } 34 | 35 | // Check for '0x' prefix and remove it if present 36 | if (privateKey.startsWith('0x') || privateKey.startsWith('0X')) { 37 | privateKey = privateKey.slice(2); 38 | } 39 | 40 | // Validate the length of privateKey after removing '0x' prefix 41 | if (privateKey.length !== 64) { 42 | throw new Error( 43 | `Invalid privateKey length: expected 64 characters without prefix, got ${privateKey.length}`, 44 | ); 45 | } 46 | 47 | // Validate that privateKey contains only valid hex characters 48 | if (!/^[0-9a-fA-F]+$/.test(privateKey)) { 49 | throw new Error('privateKey contains invalid characters'); 50 | } 51 | 52 | return privateKey; 53 | } 54 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/icons/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/icons/shield-divided.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/web-wallet/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chainsafe/webzjs-web-wallet", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "source": [ 7 | "index.html", 8 | "../../node_modules/@chainsafe/webzjs-wallet/snippets/wasm-bindgen-rayon-3e04391371ad0a8e/src/workerHelpers.worker.js" 9 | ], 10 | "scripts": { 11 | "dev": "node server.js", 12 | "build": "parcel build index.html --dist-dir dist" 13 | }, 14 | "dependencies": { 15 | "@chainsafe/webzjs-keys": "workspace:^", 16 | "@chainsafe/webzjs-wallet": "workspace:^", 17 | "@metamask/providers": "^18.2.0", 18 | "classnames": "^2.5.1", 19 | "decimal.js": "^10.5.0", 20 | "idb-keyval": "^6.2.1", 21 | "react": "^18.3.1", 22 | "react-dom": "^18.3.1", 23 | "react-hot-toast": "^2.5.1", 24 | "react-qr-code": "^2.0.15", 25 | "react-router-dom": "^6.27.0", 26 | "usehooks-ts": "^3.1.0" 27 | }, 28 | "devDependencies": { 29 | "@eslint/js": "^9.13.0", 30 | "@parcel/core": "^2.13.3", 31 | "@parcel/transformer-svg-react": "^2.13.3", 32 | "@tailwindcss/postcss": "^4.0.0", 33 | "@types/express": "^4", 34 | "@types/node": "^22.10.10", 35 | "@types/react": "^19.0.8", 36 | "@types/react-dom": "^19.0.3", 37 | "@types/styled-components": "^5.1.34", 38 | "@typescript-eslint/eslint-plugin": "^8.21.0", 39 | "@typescript-eslint/parser": "^8.21.0", 40 | "connect-history-api-fallback": "^2.0.0", 41 | "dotenv": "^16.4.7", 42 | "eslint": "^9.18.0", 43 | "eslint-config-react-app": "^7.0.1", 44 | "eslint-plugin-react-hooks": "^5.0.0", 45 | "eslint-plugin-react-refresh": "^0.4.14", 46 | "express": "^4.21.2", 47 | "globals": "^15.11.0", 48 | "parcel": "^2.13.3", 49 | "path": "^0.12.7", 50 | "svgo": "^3", 51 | "tailwindcss": "^4.0.0", 52 | "typescript": "^5.7.3", 53 | "typescript-eslint": "^8.11.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/snap/src/rpc/signPczt.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Copyable, Divider, Heading, Text } from '@metamask/snaps-sdk/jsx'; 2 | import { 3 | SeedFingerprint, 4 | UnifiedSpendingKey, 5 | pczt_sign, 6 | Pczt, 7 | } from '@chainsafe/webzjs-keys'; 8 | import { getSeed } from '../utils/getSeed'; 9 | import { SignPcztParams } from 'src/types'; 10 | 11 | 12 | 13 | export async function signPczt({ pcztHexTring, signDetails }: SignPcztParams, origin: string): Promise { 14 | 15 | const result = await snap.request({ 16 | method: 'snap_dialog', 17 | params: { 18 | type: 'confirmation', 19 | content: ( 20 | 21 | Sing PCZT 22 | 23 | Origin: {origin} 24 | Recipient: {signDetails.recipient} 25 | Amount: {signDetails.amount} 26 | 27 | PCZT hex to sign 28 | 29 | 30 | ), 31 | }, 32 | }); 33 | 34 | if (!result) { 35 | throw new Error('User rejected'); 36 | } 37 | 38 | const seed = await getSeed(); 39 | 40 | // Generate the UnifiedSpendingKey and obtain the Viewing Key 41 | const spendingKey = new UnifiedSpendingKey('main', seed, 0); 42 | const seedFp = new SeedFingerprint(seed); 43 | 44 | if (!/^[0-9a-fA-F]+$/.test(pcztHexTring)) { 45 | throw new Error('pcztHexTring must be valid hex'); 46 | } 47 | 48 | const pcztBuffer = Buffer.from(pcztHexTring, 'hex'); 49 | 50 | 51 | const pcztUint8Array = new Uint8Array(pcztBuffer); 52 | 53 | const pczt = Pczt.from_bytes(pcztUint8Array) 54 | 55 | const pcztSigned = await pczt_sign('main', pczt, spendingKey, seedFp); 56 | 57 | const pcztUint8Signed = pcztSigned.serialize(); 58 | 59 | const pcztHexStringSigned = Buffer.from(pcztUint8Signed).toString('hex'); 60 | 61 | 62 | return pcztHexStringSigned; 63 | } -------------------------------------------------------------------------------- /packages/web-wallet/src/components/NavBar/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cn from 'classnames'; 3 | import { NavLink } from 'react-router-dom'; 4 | 5 | import { 6 | ArrowReceiveSvg, 7 | ArrowTransferSvg, 8 | SummarySvg, 9 | ShieldSvg 10 | } from '../../assets'; 11 | 12 | interface NavItem { 13 | to: string; 14 | label: string; 15 | icon: React.JSX.Element; 16 | } 17 | 18 | const navItems: NavItem[] = [ 19 | { 20 | to: 'account-summary', 21 | label: 'Account Summary', 22 | icon: , 23 | }, 24 | { 25 | to: 'transfer-balance', 26 | label: 'Transfer Balance', 27 | icon: , 28 | }, 29 | { 30 | to: 'shield-balance', 31 | label: 'Shield Balance', 32 | icon: , 33 | }, 34 | { 35 | to: 'receive', 36 | label: 'Receive', 37 | icon: , 38 | } 39 | ]; 40 | 41 | function NavBar() { 42 | return ( 43 | 70 | ); 71 | } 72 | 73 | export default NavBar; 74 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/icons/arrow-receive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/icons/arrow-transfer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/webzjs-common/src/network.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 ChainSafe Systems 2 | // SPDX-License-Identifier: Apache-2.0, MIT 3 | 4 | use crate::error::Error; 5 | use serde::{Deserialize, Serialize}; 6 | use std::str::FromStr; 7 | use zcash_protocol::consensus::{self, Parameters}; 8 | 9 | /// Enum representing the network type 10 | /// This is used instead of the `consensus::Network` enum so we can derive 11 | /// custom serialization and deserialization and from string impls 12 | #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)] 13 | pub enum Network { 14 | #[default] 15 | MainNetwork, 16 | TestNetwork, 17 | } 18 | 19 | impl FromStr for Network { 20 | type Err = Error; 21 | 22 | fn from_str(s: &str) -> Result { 23 | match s { 24 | "main" => Ok(Network::MainNetwork), 25 | "test" => Ok(Network::TestNetwork), 26 | _ => Err(Error::InvalidNetwork(s.to_string())), 27 | } 28 | } 29 | } 30 | 31 | impl Parameters for Network { 32 | fn network_type(&self) -> zcash_protocol::consensus::NetworkType { 33 | match self { 34 | Network::MainNetwork => zcash_protocol::consensus::NetworkType::Main, 35 | Network::TestNetwork => zcash_protocol::consensus::NetworkType::Test, 36 | } 37 | } 38 | 39 | fn activation_height(&self, nu: consensus::NetworkUpgrade) -> Option { 40 | match self { 41 | Network::MainNetwork => { 42 | zcash_primitives::consensus::Network::MainNetwork.activation_height(nu) 43 | } 44 | Network::TestNetwork => { 45 | zcash_primitives::consensus::Network::TestNetwork.activation_height(nu) 46 | } 47 | } 48 | } 49 | } 50 | 51 | impl From for consensus::Network { 52 | fn from(network: Network) -> Self { 53 | match network { 54 | Network::MainNetwork => consensus::Network::MainNetwork, 55 | Network::TestNetwork => consensus::Network::TestNetwork, 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /add-worker-module.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Find the wasm_thread directory regardless of the hash 4 | WASM_THREAD_DIR=$(find packages/webzjs-wallet/snippets -type d -name "wasm_thread-*" | head -n 1) 5 | 6 | if [ -z "$WASM_THREAD_DIR" ]; then 7 | echo "Error: Could not find wasm_thread directory" 8 | exit 1 9 | fi 10 | 11 | # Create the directory structure 12 | mkdir -p "$WASM_THREAD_DIR/src/wasm32/js" 13 | 14 | # Create the worker module file 15 | cat > "$WASM_THREAD_DIR/src/wasm32/js/web_worker_module.bundler.js" << 'EOL' 16 | // synchronously, using the browser, import wasm_bindgen shim JS scripts 17 | import init, { wasm_thread_entry_point } from "../../../../../"; 18 | // Wait for the main thread to send us the shared module/memory and work context. 19 | // Once we've got it, initialize it all with the `wasm_bindgen` global we imported via 20 | // `importScripts`. 21 | self.onmessage = event => { 22 | let [ module, memory, work, thread_key ] = event.data; 23 | init(module, memory).catch(err => { 24 | console.log(err); 25 | const error = new Error(err.message); 26 | error.customProperty = "This error right here!"; 27 | // Propagate to main `onerror`: 28 | setTimeout(() => { 29 | throw error; 30 | }); 31 | // Rethrow to keep promise rejected and prevent execution of further commands: 32 | throw error; 33 | }).then(() => { 34 | // Enter rust code by calling entry point defined in `lib.rs`. 35 | // This executes closure defined by work context. 36 | wasm_thread_entry_point(work); 37 | }); 38 | }; 39 | self.onunhandledrejection = function(e) { 40 | console.error('Worker unhandled rejection:', e.reason); 41 | throw e.reason; 42 | }; 43 | self.onerror = function(e) { 44 | console.error('Worker error:', e.message); 45 | throw e; 46 | }; 47 | 48 | self.onended = function(e) { 49 | console.error('Worker ended:', e.message); 50 | throw e; 51 | } 52 | EOL 53 | 54 | echo "Added worker module to: $WASM_THREAD_DIR/src/wasm32/js/web_worker_module.bundler.js" 55 | -------------------------------------------------------------------------------- /packages/web-wallet/src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, useLocation } from 'react-router-dom'; 3 | import { MetaMaskLogoPNG, MetaMaskSnapsLogoPNG, ZcashPNG } from '../../assets'; 4 | 5 | const Header = (): React.JSX.Element => { 6 | const location = useLocation(); 7 | const isHomePage = location.pathname === '/'; 8 | 9 | return ( 10 |
11 | 12 |
13 | Zcash logo 18 |
19 | {isHomePage ? ( 20 | {'MetaMaskSnapsLogoSvg'} 21 | ) : ( 22 | MetaMask logo 27 | )} 28 |
29 |
30 | 31 | {isHomePage ?? ( 32 | 55 | )} 56 |
57 | ); 58 | }; 59 | 60 | export default Header; 61 | -------------------------------------------------------------------------------- /packages/snap/README.md: -------------------------------------------------------------------------------- 1 | # WebZjs Zcash Snap 2 | 3 | ## 🔐 Overview 4 | 5 | WebZjs Zcash Snap is a MetaMask Snap that brings Zcash functionality directly into the MetaMask browser extension. WebZjs is the first JavaScript SDK for Zcash, enabling seamless integration of Zcash privacy features for web users. 6 | 7 | ## 📘 Project Description 8 | 9 | Snap uses a Rust library [WebZjs](https://github.com/ChainSafe/WebZjs) compiled to WebAssembly (Wasm). It is meant to be used in conjunction with WebZjs web-wallet. 10 | 11 | ## 🛠 Prerequisites 12 | 13 | [WebZjs](https://github.com/ChainSafe/WebZjs) 14 | 15 | - Node.js 16 | - Yarn 17 | - MetaMask Browser Extension (MetaMask Flask for development purposes) [Install MM Flask](https://docs.metamask.io/snaps/get-started/install-flask/) 18 | 19 | ## 🔨 Development 20 | 21 | For local development, you need to add `http://localhost:3000` to the `allowedOrigins` in `snap.manifest.json`. The `endowment:rpc` section should look like this: 22 | 23 | ```json 24 | "endowment:rpc": { 25 | "allowedOrigins": ["https://webzjs.chainsafe.dev", "http://localhost:3000"] 26 | } 27 | ``` 28 | 29 | ### Build Scripts 30 | 31 | - **`yarn build`** - Standard build for production (only allows production origins) 32 | - **`yarn build:local`** - Build for local development (automatically adds localhost:3000 to allowedOrigins) 33 | - **`yarn build:prePublish`** - Pre-publish build that ensures `allowedOrigins` is reset to `["https://webzjs.chainsafe.dev"]` and then runs `mm-snap build` 34 | 35 | The `build:local` script will: 36 | 1. Create a backup of the original `snap.manifest.json` 37 | 2. Modify the manifest to include `http://localhost:3000` in allowedOrigins 38 | 3. Run the build process 39 | 40 | The `build:prePublish` script will: 41 | 1. Overwrite `allowedOrigins` in `snap.manifest.json` to only `["https://webzjs.chainsafe.dev"]` 42 | 2. Run the production build via `mm-snap build` 43 | 44 | ### Development Steps 45 | 46 | 1. Install dependencies with `yarn install` 47 | 2. For local development: Build with `yarn build:local` 48 | 3. For production: Build with `yarn build` 49 | 4. Host snap on localhost http://localhost:8080 `yarn serve` 50 | -------------------------------------------------------------------------------- /packages/web-wallet/src/pages/TransferBalance/useTransferBalanceForm.ts: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { PcztTransferStatus, usePczt } from '../../hooks/usePCZT'; 3 | 4 | export interface TransferBalanceFormData { 5 | amount: string; 6 | recipient: string; 7 | } 8 | 9 | export type TransferBalanceFormHandleChange = ( 10 | input: keyof TransferBalanceFormData, 11 | ) => ( 12 | e: 13 | | React.ChangeEvent< 14 | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement 15 | > 16 | | string, 17 | ) => void; 18 | 19 | export interface TransferBalanceFormType { 20 | currentStep: number; 21 | formData: TransferBalanceFormData; 22 | pcztTransferStatus: PcztTransferStatus; 23 | nextStep: () => void; 24 | handleChange: TransferBalanceFormHandleChange; 25 | submitForm: () => void; 26 | resetForm: () => void; 27 | } 28 | 29 | export enum TransferStep { 30 | INPUT, 31 | CONFIRM, 32 | RESULT 33 | } 34 | 35 | const useTransferBalanceForm = (): TransferBalanceFormType => { 36 | const { handlePcztTransaction, pcztTransferStatus } = usePczt(); 37 | const [currentStep, setCurrentStep] = useState(0); 38 | const [formData, setFormData] = useState({ 39 | amount: '', 40 | recipient: '', 41 | }); 42 | 43 | const nextStep = () => setCurrentStep((prev) => prev + 1); 44 | 45 | const handleChange: TransferBalanceFormHandleChange = (input) => (e) => { 46 | if (typeof e === 'string') { 47 | setFormData({ ...formData, [input]: e }); 48 | } else { 49 | setFormData({ ...formData, [input]: e.target.value }); 50 | } 51 | }; 52 | 53 | const submitForm = () => { 54 | const { amount, recipient } = formData; 55 | //TODO - get accoundId it from state 56 | handlePcztTransaction(1, recipient, amount); 57 | }; 58 | 59 | 60 | const resetForm = () => { 61 | setFormData({ 62 | amount: '', 63 | recipient: '', 64 | }); 65 | setCurrentStep(TransferStep.INPUT); 66 | }; 67 | 68 | return { 69 | currentStep, 70 | formData, 71 | pcztTransferStatus, 72 | nextStep, 73 | handleChange, 74 | submitForm, 75 | resetForm, 76 | }; 77 | }; 78 | 79 | export default useTransferBalanceForm; 80 | -------------------------------------------------------------------------------- /packages/web-wallet/src/styles/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,100..900&family=Roboto:wdth,wght@75..100,100..900&display=swap'); 2 | @import 'tailwindcss'; 3 | 4 | @layer base { 5 | :root { 6 | --font-sans: 'Roboto', 'sans-serif'; 7 | --font-body: 'Roboto', 'sans-serif'; 8 | --font-inter: 'Inter', 'sans-serif'; 9 | --buttonBlackGradient: radial-gradient( 10 | 82.64% 800.96% at 50% -550.96%, 11 | #000 0%, 12 | rgba(59, 59, 59, 0.81) 55%, 13 | #000 100% 14 | ); 15 | --buttonBlackGradientHover: radial-gradient( 16 | 82.64% 800.96% at 50% -550.96%, 17 | #000 0%, 18 | rgba(59, 59, 59, 0.81) 74%, 19 | #000 100% 20 | ); 21 | } 22 | } 23 | 24 | @layer utilities { 25 | .navbar-link:hover svg path { 26 | fill: #e27625; 27 | } 28 | .navbar-link-active svg path { 29 | fill: #e27625; 30 | } 31 | .bg-button-black-gradient { 32 | background-image: var(--buttonBlackGradient); 33 | } 34 | 35 | .bg-button-black-gradient-hover:hover { 36 | background-image: var(--buttonBlackGradientHover); 37 | } 38 | } 39 | 40 | .home-page { 41 | header { 42 | border-bottom: none; 43 | } 44 | } 45 | 46 | body { 47 | background: linear-gradient(#fff 0%, #bcefef 187.66%); 48 | position: relative; 49 | background: #fafafa url('../assets/diamond-bg.png') no-repeat center center 50 | fixed; 51 | background-size: 400%; 52 | &.home-page-bg { 53 | background: linear-gradient(180deg, #fff 0%, #bcefef 187.66%); 54 | 55 | /* Noise background */ 56 | 57 | &:before { 58 | content: ''; 59 | position: absolute; 60 | width: 100vw; 61 | height: 100vh; 62 | z-index: -1; 63 | opacity: 0.25; 64 | background: url('../assets/noise.png') lightgray 0 0 / 15px 15px repeat; 65 | mix-blend-mode: color-burn; 66 | } 67 | 68 | /* Yellow oval shape background */ 69 | 70 | &:after { 71 | content: ''; 72 | position: absolute; 73 | width: 75.5vw; 74 | height: 88.1875rem; 75 | top: 20rem; 76 | left: 2.5rem; 77 | z-index: -1; 78 | border-radius: 88.1875rem; 79 | background: rgba(187, 160, 17, 0.5); 80 | filter: blur(274px); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/web-wallet/src/hooks/useBalance.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useMemo } from 'react'; 2 | import { useWebZjsContext } from '../context/WebzjsContext'; 3 | 4 | type BalanceType = { 5 | shieldedBalance: number; 6 | unshieldedBalance: number; 7 | totalBalance: number; 8 | saplingBalance: number; 9 | orchardBalance: number; 10 | loading: boolean; 11 | error: string | null; 12 | }; 13 | 14 | const useBalance = () => { 15 | const { state } = useWebZjsContext(); 16 | 17 | const [balances, setBalances] = useState({ 18 | shieldedBalance: 0, 19 | unshieldedBalance: 0, 20 | totalBalance: 0, 21 | saplingBalance: 0, 22 | orchardBalance: 0, 23 | loading: true, 24 | error: null, 25 | }); 26 | 27 | const activeBalanceReport = useMemo(() => { 28 | return state.summary?.account_balances.find( 29 | ([accountId]: [number]) => accountId === state.activeAccount, 30 | ); 31 | }, [state.activeAccount, state.chainHeight, state.summary?.account_balances]); 32 | 33 | // Compute shielded, unshielded, and total balances 34 | const { 35 | shieldedBalance, 36 | unshieldedBalance, 37 | totalBalance, 38 | saplingBalance, 39 | orchardBalance, 40 | } = useMemo(() => { 41 | const shielded = activeBalanceReport 42 | ? activeBalanceReport[1].sapling_balance + 43 | activeBalanceReport[1].orchard_balance 44 | : 0; 45 | 46 | const unshielded = activeBalanceReport?.[1]?.unshielded_balance || 0; 47 | 48 | return { 49 | shieldedBalance: shielded, 50 | unshieldedBalance: unshielded, 51 | totalBalance: shielded + unshielded, 52 | saplingBalance: activeBalanceReport?.[1]?.sapling_balance || 0, 53 | orchardBalance: activeBalanceReport?.[1]?.orchard_balance || 0, 54 | }; 55 | }, [activeBalanceReport]); 56 | 57 | useEffect(() => { 58 | setBalances({ 59 | shieldedBalance, 60 | unshieldedBalance, 61 | totalBalance, 62 | saplingBalance, 63 | orchardBalance, 64 | loading: false, 65 | error: null, 66 | }); 67 | }, [ 68 | shieldedBalance, 69 | unshieldedBalance, 70 | totalBalance, 71 | saplingBalance, 72 | orchardBalance, 73 | ]); 74 | 75 | return balances; 76 | }; 77 | 78 | export default useBalance; 79 | -------------------------------------------------------------------------------- /packages/web-wallet/src/pages/TransferBalance/TransferBalance.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ZcashYellowPNG } from '../../assets'; 3 | import useTransferBalanceForm, { TransferStep } from './useTransferBalanceForm'; 4 | import PageHeading from '../../components/PageHeading/PageHeading'; 5 | import useBalance from '../../hooks/useBalance'; 6 | import { zatsToZec } from '../../utils'; 7 | import { TransferInput, TransferConfirm, TransferResult } from 'src/components/TransferCards'; 8 | 9 | function TransferBalance(): React.JSX.Element { 10 | const { shieldedBalance } = useBalance(); 11 | const { 12 | currentStep, 13 | formData, 14 | pcztTransferStatus, 15 | nextStep, 16 | handleChange, 17 | resetForm, 18 | submitForm, 19 | } = useTransferBalanceForm(); 20 | 21 | 22 | return ( 23 |
24 | {currentStep !== 3 && ( 25 | 26 |
27 | 28 | Available shielded balance: 29 | 30 |
31 | Zcash Yellow 36 | 37 | {zatsToZec(shieldedBalance)} ZEC 38 | 39 |
40 |
41 |
42 | )} 43 | {currentStep === TransferStep.INPUT && ( 44 | 49 | )} 50 | {currentStep === TransferStep.CONFIRM && ( 51 | 56 | )} 57 | {currentStep === TransferStep.RESULT && ( 58 | )} 62 |
63 | ); 64 | } 65 | 66 | export default TransferBalance; 67 | -------------------------------------------------------------------------------- /packages/snap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chainsafe/webzjs-zcash-snap", 3 | "version": "0.2.7", 4 | "description": "Zcash Metmamask Snap that utilizes WebZjs.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/ChainSafe/WebZjs.git" 8 | }, 9 | "license": "(MIT-0 OR Apache-2.0)", 10 | "main": "./dist/bundle.js", 11 | "files": [ 12 | "dist/", 13 | "images/logo.svg", 14 | "snap.manifest.json" 15 | ], 16 | "scripts": { 17 | "allow-scripts": "yarn workspace root allow-scripts", 18 | "build": "mm-snap build", 19 | "build:clean": "yarn clean && yarn build", 20 | "build:prePublish": "bash ./build_prePublish.sh", 21 | "build:local": "./build_local.sh", 22 | "clean": "rimraf dist", 23 | "lint": "eslint --color --ext .ts src/", 24 | "lint:fix": "yarn run lint --fix", 25 | "prepublishOnly": "mm-snap manifest", 26 | "serve": "mm-snap serve", 27 | "start": "mm-snap watch", 28 | "test": "jest" 29 | }, 30 | "dependencies": { 31 | "@chainsafe/webzjs-keys": "0.1.0", 32 | "@metamask/snaps-sdk": "^6.17.1", 33 | "buffer": "^6.0.3", 34 | "superstruct": "^2.0.2" 35 | }, 36 | "devDependencies": { 37 | "@chainsafe/eslint-config": "^2.2.4", 38 | "@jest/globals": "^29.5.0", 39 | "@metamask/auto-changelog": "^4.0.0", 40 | "@metamask/eslint-config": "^14.0.0", 41 | "@metamask/eslint-config-jest": "^14.0.0", 42 | "@metamask/eslint-config-nodejs": "^14.0.0", 43 | "@metamask/eslint-config-typescript": "^14.0.0", 44 | "@metamask/snaps-cli": "^6.5.2", 45 | "@metamask/snaps-jest": "^8.8.1", 46 | "@types/react": "18.2.4", 47 | "@types/react-dom": "18.2.4", 48 | "@typescript-eslint/eslint-plugin": "^6.21.0", 49 | "@typescript-eslint/parser": "^5.42.1", 50 | "eslint": "8", 51 | "eslint-config-prettier": "^9.1.0", 52 | "eslint-plugin-import": "~2.31.0", 53 | "eslint-plugin-jest": "^27.1.5", 54 | "eslint-plugin-jsdoc": "^41.1.2", 55 | "eslint-plugin-n": "^15.7.0", 56 | "eslint-plugin-prettier": "^5.2.1", 57 | "eslint-plugin-promise": "^6.1.1", 58 | "jest": "^29.5.0", 59 | "prettier": "^3.3.3", 60 | "prettier-plugin-packagejson": "^2.5.3", 61 | "rimraf": "^3.0.2", 62 | "ts-jest": "^29.1.0", 63 | "typescript": "^4.7.4" 64 | }, 65 | "publishConfig": { 66 | "access": "public", 67 | "registry": "https://registry.npmjs.org/" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/e2e-tests/e2e/web_wallet.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { WebWallet } from '@chainsafe/webzjs-wallet'; 3 | import type * as WebZJSWallet from '@chainsafe/webzjs-wallet'; 4 | 5 | import type * as WebZJSKeys from '@chainsafe/webzjs-keys'; 6 | 7 | declare global { 8 | interface Window { 9 | webWallet: WebWallet; 10 | WebZJSKeys: typeof WebZJSKeys; 11 | WebZJSWallet: typeof WebZJSWallet; 12 | } 13 | } 14 | 15 | const SEED = 16 | 'mix sample clay sweet planet lava giraffe hand fashion switch away pool rookie earth purity truly square trumpet goose move actor save jaguar volume'; 17 | const BIRTHDAY = 2657762; 18 | 19 | test.beforeEach(async ({ page }) => { 20 | await page.goto('/'); 21 | await page.waitForFunction(() => (window as any).initialized === true); 22 | await page.evaluate( 23 | async ({ seed, birthday }) => { 24 | await window.webWallet.create_account('account-0', seed, 0, birthday); 25 | }, 26 | { seed: SEED, birthday: BIRTHDAY }, 27 | ); 28 | }); 29 | 30 | test('Account was added', async ({ page }) => { 31 | let result = await page.evaluate(async () => { 32 | let summary = await window.webWallet.get_wallet_summary(); 33 | return summary?.account_balances.length; 34 | }); 35 | expect(result).toBe(1); 36 | }); 37 | 38 | test('Wallet can be serialized', async ({ page }) => { 39 | let result = await page.evaluate(async () => { 40 | let bytes = await window.webWallet.db_to_bytes(); 41 | return bytes; 42 | }); 43 | }); 44 | 45 | test('Accont can be added from ufvk', async ({ page }) => { 46 | let result = await page.evaluate(async () => { 47 | let seed = new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1)); 48 | let birthday = 2657762; 49 | let usk = new window.WebZJSKeys.UnifiedSpendingKey('main', seed, 0); 50 | let ufvk = usk.to_unified_full_viewing_key(); 51 | 52 | const keysSeedFingerprint = new window.WebZJSKeys.SeedFingerprint(seed); 53 | const seedFingerprint = window.WebZJSWallet.SeedFingerprint.from_bytes( 54 | keysSeedFingerprint.to_bytes(), 55 | ); 56 | 57 | 58 | await window.webWallet.create_account_ufvk( 59 | 'account-0', 60 | ufvk.encode('main'), 61 | seedFingerprint, 62 | 0, 63 | birthday, 64 | ); 65 | 66 | 67 | let summary = await window.webWallet.get_wallet_summary(); 68 | return summary?.account_balances.length; 69 | }); 70 | expect(result).toBe(2); 71 | }); 72 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/icons/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/icons/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/web-wallet/src/components/TransferCards/TransferResult.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { TransferBalanceFormType } from '../../pages/TransferBalance/useTransferBalanceForm'; 3 | import React from 'react'; 4 | import { CheckSVG, WarningSVG } from '../../assets'; 5 | import Button from '../Button/Button'; 6 | import TransactionStatusCard from '../TransactionStatusCard/TransactionStatusCard'; 7 | import { PcztTransferStatus } from 'src/hooks/usePCZT'; 8 | import Loader from 'src/components/Loader/Loader'; 9 | 10 | interface TransferResultProps { 11 | pcztTransferStatus: PcztTransferStatus; 12 | resetForm: TransferBalanceFormType['resetForm']; 13 | isShieldTransaction?: boolean; 14 | } 15 | 16 | export function TransferResult({ 17 | pcztTransferStatus, 18 | resetForm, 19 | isShieldTransaction, 20 | }: TransferResultProps): React.JSX.Element { 21 | const navigate = useNavigate(); 22 | 23 | const actionWord = isShieldTransaction ? 'Shielding' : 'Transfer'; 24 | 25 | switch (pcztTransferStatus) { 26 | case PcztTransferStatus.SEND_SUCCESSFUL: 27 | return } 31 | > 32 |
59 | 60 | 61 | 62 | ); 63 | } 64 | 65 | -------------------------------------------------------------------------------- /packages/snap/src/rpc/setBirthdayBlock.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @metamask/snaps-sdk */ 2 | import { 3 | Form, 4 | Box, 5 | Heading, 6 | Input, 7 | Button, 8 | Text, 9 | Bold, 10 | Divider, 11 | } from '@metamask/snaps-sdk/jsx'; 12 | import { setSyncBlockHeight } from '../utils/setSyncBlockHeight'; 13 | 14 | type BirthdayBlockForm = { customBirthdayBlock: string | null }; 15 | 16 | type SetBirthdayBlockParams = { latestBlock: number }; 17 | 18 | export async function setBirthdayBlock({ 19 | latestBlock, 20 | }: SetBirthdayBlockParams): Promise { 21 | const interfaceId = await snap.request({ 22 | method: 'snap_createInterface', 23 | params: { 24 | ui: ( 25 |
26 | 27 | Optional syncing block height 28 | 29 | If you already created Zcash Web Wallet account with this MetaMask 30 | seed you can enter optional birthday block of that Wallet. 31 | 32 | 33 | Syncing proccess will start from that block. 34 | 35 | {!!latestBlock && ( 36 | 37 | Latest block: {latestBlock.toString()} 38 | 39 | )} 40 | 47 | 48 | 51 |
52 | ), 53 | }, 54 | }); 55 | 56 | let customBirthdayBlock: string | null; 57 | try { 58 | const dialogResponse = (await snap.request({ 59 | method: 'snap_dialog', 60 | params: { 61 | id: interfaceId, 62 | }, 63 | })) as BirthdayBlockForm; 64 | customBirthdayBlock = dialogResponse.customBirthdayBlock; 65 | } catch (error) { 66 | console.log('No custom birthday block provided, using latest block'); 67 | customBirthdayBlock = null; 68 | } 69 | 70 | const webWalletSyncStartBlock = setSyncBlockHeight(customBirthdayBlock, latestBlock); 71 | 72 | await snap.request({ 73 | method: 'snap_manageState', 74 | params: { 75 | operation: 'update', 76 | newState: { webWalletSyncStartBlock }, 77 | }, 78 | }); 79 | 80 | return webWalletSyncStartBlock; 81 | } 82 | -------------------------------------------------------------------------------- /.github/workflows/check-snap-manifest.yml: -------------------------------------------------------------------------------- 1 | name: Check Snap Manifest 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'packages/snap/snap.manifest.json' 7 | - '.github/workflows/check-snap-manifest.yml' 8 | push: 9 | branches: 10 | - main 11 | - master 12 | paths: 13 | - 'packages/snap/snap.manifest.json' 14 | - '.github/workflows/check-snap-manifest.yml' 15 | 16 | jobs: 17 | check-manifest: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v4 23 | 24 | - name: Check snap manifest allowedOrigins 25 | run: | 26 | cd packages/snap 27 | 28 | # Check if jq is available 29 | if ! command -v jq &> /dev/null; then 30 | echo "jq is not installed. Installing..." 31 | sudo apt-get update && sudo apt-get install -y jq 32 | fi 33 | 34 | # Extract allowedOrigins from the manifest 35 | ALLOWED_ORIGINS=$(jq -r '.initialPermissions."endowment:rpc".allowedOrigins[]' snap.manifest.json) 36 | 37 | echo "Current allowedOrigins in snap.manifest.json:" 38 | echo "$ALLOWED_ORIGINS" 39 | 40 | # Check if localhost is present 41 | if echo "$ALLOWED_ORIGINS" | grep -q "localhost"; then 42 | echo "❌ ERROR: localhost found in allowedOrigins. This should not be in production!" 43 | echo "Please ensure snap.manifest.json only contains production URLs." 44 | echo "Expected: [\"https://webzjs.chainsafe.dev\"]" 45 | exit 1 46 | fi 47 | 48 | # Check if the production URL is present 49 | if ! echo "$ALLOWED_ORIGINS" | grep -q "https://webzjs.chainsafe.dev"; then 50 | echo "❌ ERROR: Production URL 'https://webzjs.chainsafe.dev' not found in allowedOrigins!" 51 | exit 1 52 | fi 53 | 54 | # Check if there are any unexpected URLs 55 | UNEXPECTED_URLS=$(echo "$ALLOWED_ORIGINS" | grep -v "https://webzjs.chainsafe.dev" || true) 56 | if [ -n "$UNEXPECTED_URLS" ]; then 57 | echo "❌ ERROR: Unexpected URLs found in allowedOrigins:" 58 | echo "$UNEXPECTED_URLS" 59 | echo "Expected only: https://webzjs.chainsafe.dev" 60 | exit 1 61 | fi 62 | 63 | echo "✅ SUCCESS: snap.manifest.json has correct allowedOrigins configuration" 64 | echo "Found: https://webzjs.chainsafe.dev" -------------------------------------------------------------------------------- /crates/webzjs-wallet/src/init.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 ChainSafe Systems 2 | // SPDX-License-Identifier: Apache-2.0, MIT 3 | 4 | use wasm_bindgen::prelude::*; 5 | 6 | use tracing::level_filters::LevelFilter; 7 | 8 | use tracing_subscriber::prelude::*; 9 | use tracing_subscriber::EnvFilter; 10 | 11 | fn set_panic_hook() { 12 | // When the `console_error_panic_hook` feature is enabled, we can call the 13 | // `set_panic_hook` function at least once during initialization, and then 14 | // we will get better error messages if our code ever panics. 15 | // 16 | // For more details see 17 | // https://github.com/rustwasm/console_error_panic_hook#readme 18 | #[cfg(feature = "console_error_panic_hook")] 19 | console_error_panic_hook::set_once(); 20 | } 21 | 22 | fn setup_tracing() { 23 | #[cfg(not(feature = "wasm"))] 24 | let subscriber = { 25 | let filter_layer = EnvFilter::builder() 26 | .with_default_directive(LevelFilter::INFO.into()) 27 | .from_env() 28 | .unwrap(); 29 | let fmt_layer = tracing_subscriber::fmt::layer().with_ansi(true); 30 | tracing_subscriber::registry() 31 | .with(filter_layer) 32 | .with(fmt_layer) 33 | }; 34 | 35 | #[cfg(feature = "wasm")] 36 | let subscriber = { 37 | use tracing_subscriber::fmt::format::Pretty; 38 | use tracing_web::{performance_layer, MakeWebConsoleWriter}; 39 | 40 | // For WASM, we must set the directives here at compile time. 41 | let filter_layer = EnvFilter::default() 42 | .add_directive(LevelFilter::INFO.into()) 43 | .add_directive("zcash_client_memory=info".parse().unwrap()) 44 | .add_directive("zcash_client_backend::scanning=debug".parse().unwrap()) 45 | .add_directive("zcash_client_backend::sync=debug".parse().unwrap()); 46 | 47 | let fmt_layer = tracing_subscriber::fmt::layer() 48 | .with_ansi(false) // Only partially supported across browsers 49 | .without_time() // std::time is not available in browsers 50 | .with_writer(MakeWebConsoleWriter::new()); // write events to the console 51 | 52 | let perf_layer = performance_layer().with_details_from_fields(Pretty::default()); 53 | 54 | tracing_subscriber::registry() 55 | .with(filter_layer) 56 | .with(fmt_layer) 57 | .with(perf_layer) 58 | }; 59 | 60 | subscriber.init(); 61 | } 62 | 63 | #[wasm_bindgen(start)] 64 | pub fn start() { 65 | set_panic_hook(); 66 | setup_tracing(); 67 | } 68 | -------------------------------------------------------------------------------- /packages/web-wallet/src/pages/Receive/Receive.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useWebZjsActions } from '../../hooks'; 3 | import QrCode from './QrCode'; 4 | import PageHeading from '../../components/PageHeading/PageHeading'; 5 | import Loader from '../../components/Loader/Loader'; 6 | import Tab from './Tab'; 7 | 8 | enum AddressType { 9 | UNIFIED = 'unified', 10 | TRANSPARENT = 'transparent', 11 | } 12 | 13 | function Receive(): React.JSX.Element { 14 | const [loading, setLoading] = useState(true); 15 | const [activeTab, setActiveTab] = useState(AddressType.UNIFIED); 16 | const [addresses, setAddresses] = useState<{ 17 | unifiedAddress: string; 18 | transparentAddress: string; 19 | }>({ 20 | unifiedAddress: '', 21 | transparentAddress: '', 22 | }); 23 | const { getAccountData } = useWebZjsActions(); 24 | 25 | useEffect(() => { 26 | const fetchData = async () => { 27 | const data = await getAccountData(); 28 | if (data) 29 | setAddresses({ 30 | unifiedAddress: data.unifiedAddress, 31 | transparentAddress: data.transparentAddress, 32 | }); 33 | setLoading(false); 34 | }; 35 | fetchData(); 36 | }, [getAccountData]); 37 | 38 | const tabs = { 39 | [AddressType.UNIFIED]: { 40 | label: 'Unified Address', 41 | }, 42 | [AddressType.TRANSPARENT]: { 43 | label: 'Transparent Address', 44 | }, 45 | }; 46 | 47 | return ( 48 | <> 49 | 50 |
51 | {loading ? ( 52 | 53 | ) : ( 54 | <> 55 |
56 | {Object.keys(tabs).map((tab) => ( 57 | setActiveTab(tab as AddressType)} 63 | /> 64 | ))} 65 |
66 | {activeTab === AddressType.UNIFIED && ( 67 | 68 | )} 69 | {activeTab === AddressType.TRANSPARENT && ( 70 | 71 | )} 72 | 73 | )} 74 |
75 | 76 | ); 77 | } 78 | 79 | export default Receive; 80 | -------------------------------------------------------------------------------- /packages/web-wallet/src/context/MetamaskContext.tsx: -------------------------------------------------------------------------------- 1 | import type { MetaMaskInpageProvider } from '@metamask/providers'; 2 | import type { ReactNode } from 'react'; 3 | import { createContext, useContext, useEffect, useState } from 'react'; 4 | 5 | import type { Snap } from '../types'; 6 | import { getSnapsProvider } from '../utils'; 7 | import { SnapState } from 'src/hooks/snaps/useGetSnapState'; 8 | 9 | type MetaMaskContextType = { 10 | provider: MetaMaskInpageProvider | null; 11 | installedSnap: Snap | null; 12 | error: Error | null; 13 | snapState: SnapState | null; 14 | setSnapState: (SnapState: SnapState) => void; 15 | setInstalledSnap: (snap: Snap | null) => void; 16 | setError: (error: Error) => void; 17 | }; 18 | 19 | const MetaMaskContext = createContext({ 20 | provider: null, 21 | installedSnap: null, 22 | error: null, 23 | snapState: null, 24 | setSnapState: () => {}, 25 | setInstalledSnap: () => {}, 26 | setError: () => {}, 27 | }); 28 | 29 | /** 30 | * MetaMask context provider to handle MetaMask and snap status. 31 | * 32 | * @param props - React Props. 33 | * @param props.children - React component to be wrapped by the Provider. 34 | * @returns JSX. 35 | */ 36 | export const MetaMaskProvider = ({ children }: { children: ReactNode }) => { 37 | // const { getSnapState } = useGetSnapState(); 38 | 39 | const [provider, setProvider] = useState(null); 40 | const [installedSnap, setInstalledSnap] = useState(null); 41 | const [error, setError] = useState(null); 42 | const [snapState, setSnapState] = useState(null); 43 | 44 | useEffect(() => { 45 | getSnapsProvider().then(setProvider).catch(console.error); 46 | }, []); 47 | 48 | useEffect(() => { 49 | if (error) { 50 | const timeout = setTimeout(() => { 51 | setError(null); 52 | }, 10000); 53 | 54 | return () => { 55 | clearTimeout(timeout); 56 | }; 57 | } 58 | 59 | return undefined; 60 | }, [error]); 61 | 62 | return ( 63 | 74 | {children} 75 | 76 | ); 77 | }; 78 | 79 | /** 80 | * Utility hook to consume the MetaMask context. 81 | * 82 | * @returns The MetaMask context. 83 | */ 84 | export function useMetaMaskContext() { 85 | const context = useContext(MetaMaskContext); 86 | 87 | if (context === undefined) { 88 | throw new Error( 89 | 'useMetaMaskContext must be called within a MetaMaskProvider', 90 | ); 91 | } 92 | 93 | return context; 94 | } 95 | -------------------------------------------------------------------------------- /packages/web-wallet/src/pages/AccountSummary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { zatsToZec } from '../utils'; 3 | import { CoinsSvg, ShieldDividedSvg, ShieldSvg } from '../assets'; 4 | import useBalance from '../hooks/useBalance'; 5 | import { useWebZjsContext } from 'src/context/WebzjsContext'; 6 | import { BlockHeightCard } from 'src/components/BlockHeightCard/BlockHeightCard'; 7 | import { useMetaMaskContext } from 'src/context/MetamaskContext'; 8 | 9 | interface BalanceCard { 10 | name: string; 11 | icon: React.JSX.Element; 12 | balance: number; 13 | } 14 | 15 | function AccountSummary() { 16 | const { totalBalance, unshieldedBalance, shieldedBalance } = useBalance(); 17 | const { state } = useWebZjsContext(); 18 | const { snapState } = useMetaMaskContext(); 19 | 20 | const BalanceCards: BalanceCard[] = [ 21 | { 22 | name: 'Account Balance', 23 | icon: , 24 | balance: totalBalance, 25 | }, 26 | { 27 | name: 'Shielded Balance', 28 | icon: , 29 | balance: shieldedBalance, 30 | }, 31 | { 32 | name: 'Unshielded Balance', 33 | icon: , 34 | balance: unshieldedBalance, 35 | }, 36 | ]; 37 | 38 | const renderBalanceCard = ({ name, balance, icon }: BalanceCard) => { 39 | return ( 40 |
44 | {icon} 45 |
46 | {name} 47 |
48 |
49 |
50 | {zatsToZec(balance)} ZEC 51 |
52 |
53 |
54 | ); 55 | }; 56 | 57 | return ( 58 |
59 |
60 |
61 |
62 | Account summary 63 |
64 |
65 |
66 |
71 | {BalanceCards.map((card) => renderBalanceCard(card))} 72 |
73 | 77 |
78 | ); 79 | } 80 | 81 | export default AccountSummary; 82 | -------------------------------------------------------------------------------- /packages/web-wallet/src/components/BlockHeightCard/BlockHeightCard.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { WebZjsState } from 'src/context/WebzjsContext'; 3 | 4 | export const BlockHeightCard: FC<{ 5 | state: WebZjsState; 6 | syncedFrom?: string; 7 | }> = ({ state, syncedFrom }) => { 8 | return ( 9 |
10 | {state.syncInProgress ? ( 11 |
12 | 18 | 26 | 31 | 32 | 33 | Sync in progress... 34 | 35 |
36 | ) : null} 37 |
38 | Chain Height 39 |
40 |
41 |
42 | {state.chainHeight ? '' + state.chainHeight : '?'} 43 |
44 |
45 |
46 | Synced Height 47 |
48 |
49 |
50 | {state.summary?.fully_scanned_height 51 | ? state.summary?.fully_scanned_height 52 | : '?'} 53 |
54 |
55 | {syncedFrom && ( 56 | <> 57 |
58 | Sync Start Block 59 |
60 |
61 |
62 | {syncedFrom} 63 |
64 |
65 | 66 | )} 67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /packages/snap/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { getViewingKey } from './rpc/getViewingKey'; 2 | import { InitOutput } from '@chainsafe/webzjs-keys'; 3 | import { initialiseWasm } from './utils/initialiseWasm'; 4 | import { 5 | OnRpcRequestHandler, 6 | OnUserInputHandler, 7 | UserInputEventType, 8 | } from '@metamask/snaps-sdk'; 9 | import { setBirthdayBlock } from './rpc/setBirthdayBlock'; 10 | import { getSnapState } from './rpc/getSnapState'; 11 | import { SetBirthdayBlockParams, SignPcztParams, SnapState } from './types'; 12 | import { setSnapState } from './rpc/setSnapState'; 13 | import { signPczt } from './rpc/signPczt' 14 | 15 | import { assert, object, number, optional, string } from 'superstruct'; 16 | import { getSeedFingerprint } from './rpc/getSeedFingerprint'; 17 | import type { OnInstallHandler } from "@metamask/snaps-sdk"; 18 | import { installDialog } from './utils/dialogs'; 19 | 20 | let wasm: InitOutput; 21 | 22 | /** 23 | * Handle incoming JSON-RPC requests, sent through `wallet_invokeSnap`. 24 | * 25 | * 26 | * invoked the snap. 27 | * @param args.request - A validated JSON-RPC request object. 28 | * @returns The ViewingKey 29 | * @throws If the request method is not valid for this snap. 30 | */ 31 | export const onRpcRequest: OnRpcRequestHandler = async ({ request, origin }) => { 32 | if (!wasm) { 33 | wasm = initialiseWasm(); 34 | } 35 | 36 | switch (request.method) { 37 | case 'getViewingKey': 38 | return await getViewingKey(origin); 39 | case 'signPczt': 40 | assert(request.params, object({ 41 | pcztHexTring: string(), 42 | signDetails: object({ 43 | recipient: string(), 44 | amount: string() 45 | }), 46 | })); 47 | return await signPczt(request.params as SignPcztParams, origin); 48 | case 'getSeedFingerprint': 49 | return await getSeedFingerprint(); 50 | case 'setBirthdayBlock': 51 | assert(request.params, object({ latestBlock: optional(number()) })); 52 | return await setBirthdayBlock(request.params as SetBirthdayBlockParams); 53 | case 'getSnapStete': 54 | return await getSnapState(); 55 | case 'setSnapStete': 56 | const setSnapStateParams = request.params as unknown as SnapState; 57 | return await setSnapState(setSnapStateParams); 58 | default: 59 | throw new Error('Method not found.'); 60 | } 61 | }; 62 | 63 | export const onUserInput: OnUserInputHandler = async ({ id, event }) => { 64 | if (event.type === UserInputEventType.FormSubmitEvent) { 65 | switch (event.name) { 66 | case 'birthday-block-form': 67 | await snap.request({ 68 | method: 'snap_resolveInterface', 69 | params: { 70 | id, 71 | value: event.value, 72 | }, 73 | }); 74 | 75 | default: 76 | break; 77 | } 78 | } 79 | }; 80 | 81 | export const onInstall: OnInstallHandler = async (args) => { 82 | if (args.origin === 'https://webzjs.chainsafe.dev') return; 83 | 84 | await installDialog(); 85 | }; 86 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | default: 2 | just --list 3 | 4 | build: 5 | just build-wallet 6 | just build-keys 7 | just build-requests 8 | 9 | build-wallet *features: 10 | cd crates/webzjs-wallet && wasm-pack build -t web --release --scope chainsafe --out-dir ../../packages/webzjs-wallet --no-default-features --features="wasm wasm-parallel {{features}}" -Z build-std="panic_abort,std" 11 | ./add-worker-module.sh 12 | 13 | build-keys *features: 14 | cd crates/webzjs-keys && wasm-pack build -t web --release --scope chainsafe --out-dir ../../packages/webzjs-keys --no-default-features --features="{{features}}" -Z build-std="panic_abort,std" 15 | 16 | build-requests *features: 17 | cd crates/webzjs-requests && wasm-pack build -t web --release --scope chainsafe --out-dir ../../packages/webzjs-requests --no-default-features --features="{{features}}" -Z build-std="panic_abort,std" 18 | 19 | # All Wasm Tests 20 | test-web *features: 21 | WASM_BINDGEN_TEST_TIMEOUT=99999 wasm-pack test --release --firefox --no-default-features --features "wasm no-bundler {{features}}" -Z build-std="panic_abort,std" 22 | 23 | # sync message board in the web: addigional args: 24 | test-message-board-web *features: 25 | WASM_BINDGEN_TEST_TIMEOUT=99999 wasm-pack test --release --chrome --no-default-features --features "wasm no-bundler {{features}}" -Z build-std="panic_abort,std" --test message-board-sync 26 | 27 | # simple example in the web: additional args: 28 | test-simple-web *features: 29 | WASM_BINDGEN_TEST_TIMEOUT=99999 wasm-pack test --release --chrome --no-default-features --features "wasm no-bundler {{features}}" -Z build-std="panic_abort,std" --test simple-sync-and-send 30 | 31 | # simple example: additional args:, sqlite-db 32 | example-simple *features: 33 | RUST_LOG="info,zcash_client_backend::sync=debug" cargo run -r --example simple-sync --features "native {{features}}" 34 | 35 | # sync the message board: additional args:, sqlite-db 36 | example-message-board *features: 37 | RUST_LOG=info,zcash_client_backend::sync=debug cargo run -r --example message-board-sync --features "native {{features}}" 38 | 39 | alias c := check 40 | 41 | check: 42 | cargo check 43 | 44 | lint: 45 | cargo clippy 46 | 47 | alias cw := check-wasm 48 | 49 | check-wasm: 50 | cargo check --no-default-features --features="wasm-parallel,no-bundler" --target=wasm32-unknown-unknown 51 | 52 | # run a local proxy to the mainnet lightwalletd server on port 443 53 | run-proxy: 54 | grpcwebproxy --backend_max_call_recv_msg_size=10485760 --server_http_max_write_timeout=1000s --server_http_max_read_timeout=1000s \ 55 | --backend_addr=zec.rocks:443 --run_tls_server=false --backend_tls --allow_all_origins --server_http_debug_port 443 56 | # run a local proxy to the testnet lightwalletd server on port 443 57 | run-test-proxy: 58 | grpcwebproxy --backend_max_call_recv_msg_size=10485760 --server_http_max_write_timeout=1000s --server_http_max_read_timeout=1000s \ 59 | --backend_addr=testnet.zec.rocks:443 --run_tls_server=false --backend_tls --allow_all_origins --server_http_debug_port 443 60 | -------------------------------------------------------------------------------- /packages/web-wallet/src/components/TransferCards/TransferInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | TransferBalanceFormData, 4 | TransferBalanceFormHandleChange, 5 | } from '../../pages/TransferBalance/useTransferBalanceForm'; 6 | import Input from '../Input/Input'; 7 | import Button from '../Button/Button'; 8 | 9 | interface TransferInputProps { 10 | formData: TransferBalanceFormData; 11 | handleChange: TransferBalanceFormHandleChange; 12 | nextStep: () => void; 13 | } 14 | 15 | export function TransferInput({ 16 | formData: { recipient, amount}, 17 | nextStep, 18 | handleChange, 19 | }: TransferInputProps): React.JSX.Element { 20 | 21 | const [errors, setErrors] = useState({ 22 | recipient: '', 23 | transactionType: '', 24 | amount: '', 25 | }); 26 | 27 | const validateFields = () => { 28 | const newErrors = { 29 | recipient: '', 30 | transactionType: '', 31 | amount: '', 32 | }; 33 | 34 | if (!recipient) { 35 | newErrors.recipient = 'Please enter a valid address'; 36 | } 37 | 38 | if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) { 39 | newErrors.amount = 'Please enter an valid amount to transfer'; 40 | } 41 | 42 | setErrors(newErrors); 43 | 44 | return !Object.values(newErrors).some((error) => error !== ''); 45 | }; 46 | 47 | const handleContinue = () => { 48 | 49 | if (validateFields()) nextStep(); 50 | }; 51 | 52 | return ( 53 |
54 |
55 |
56 |
57 |
58 | handleChange('recipient')(event)} 65 | /> 66 |
67 |
68 |
69 |
70 | handleChange('amount')(event)} 78 | /> 79 |
80 |
81 |
82 |
83 |
85 |
86 |
87 | ); 88 | } 89 | 90 | -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/icons/eye-slash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/web-wallet/src/utils/metamask.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | EIP6963AnnounceProviderEvent, 3 | MetaMaskInpageProvider, 4 | } from '@metamask/providers'; 5 | 6 | /** 7 | * Check if the current provider supports snaps by calling `wallet_getSnaps`. 8 | * 9 | * @param provider - The provider to use to check for snaps support. Defaults to 10 | * `window.ethereum`. 11 | * @returns True if the provider supports snaps, false otherwise. 12 | */ 13 | export async function hasSnapsSupport( 14 | provider: MetaMaskInpageProvider = window.ethereum, 15 | ) { 16 | try { 17 | await provider.request({ 18 | method: 'wallet_getSnaps', 19 | }); 20 | 21 | return true; 22 | } catch { 23 | return false; 24 | } 25 | } 26 | 27 | /** 28 | * Get a MetaMask provider using EIP6963. This will return the first provider 29 | * reporting as MetaMask. If no provider is found after 500ms, this will 30 | * return null instead. 31 | * 32 | * @returns A MetaMask provider if found, otherwise null. 33 | */ 34 | export async function getMetaMaskEIP6963Provider() { 35 | return new Promise((rawResolve) => { 36 | // Timeout looking for providers after 500ms 37 | const timeout = setTimeout(() => { 38 | resolve(null); 39 | }, 500); 40 | 41 | /** 42 | * Resolve the promise with a MetaMask provider and clean up. 43 | * 44 | * @param provider - A MetaMask provider if found, otherwise null. 45 | */ 46 | function resolve(provider: MetaMaskInpageProvider | null) { 47 | window.removeEventListener( 48 | 'eip6963:announceProvider', 49 | onAnnounceProvider, 50 | ); 51 | clearTimeout(timeout); 52 | rawResolve(provider); 53 | } 54 | 55 | /** 56 | * Listener for the EIP6963 announceProvider event. 57 | * 58 | * Resolves the promise if a MetaMask provider is found. 59 | * 60 | * @param event - The EIP6963 announceProvider event. 61 | * @param event.detail - The details of the EIP6963 announceProvider event. 62 | */ 63 | function onAnnounceProvider({ detail }: EIP6963AnnounceProviderEvent) { 64 | if (!detail) { 65 | return; 66 | } 67 | 68 | const { info, provider } = detail; 69 | 70 | if (info.rdns.includes('io.metamask')) { 71 | resolve(provider); 72 | } 73 | } 74 | 75 | window.addEventListener('eip6963:announceProvider', onAnnounceProvider); 76 | 77 | window.dispatchEvent(new Event('eip6963:requestProvider')); 78 | }); 79 | } 80 | 81 | /** 82 | * Get a provider that supports snaps. This will loop through all the detected 83 | * providers and return the first one that supports snaps. 84 | * 85 | * @returns The provider, or `null` if no provider supports snaps. 86 | */ 87 | export async function getSnapsProvider() { 88 | if (typeof window === 'undefined') { 89 | return null; 90 | } 91 | 92 | if (await hasSnapsSupport()) { 93 | return window.ethereum; 94 | } 95 | 96 | if (window.ethereum?.detected) { 97 | for (const provider of window.ethereum.detected) { 98 | if (await hasSnapsSupport(provider)) { 99 | return provider; 100 | } 101 | } 102 | } 103 | 104 | if (window.ethereum?.providers) { 105 | for (const provider of window.ethereum.providers) { 106 | if (await hasSnapsSupport(provider)) { 107 | return provider; 108 | } 109 | } 110 | } 111 | 112 | const eip6963Provider = await getMetaMaskEIP6963Provider(); 113 | 114 | if (eip6963Provider && (await hasSnapsSupport(eip6963Provider))) { 115 | return eip6963Provider; 116 | } 117 | 118 | return null; 119 | } 120 | -------------------------------------------------------------------------------- /crates/webzjs-wallet/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webzjs-wallet" 3 | version = "0.1.0" 4 | authors = ["ChainSafe Systems"] 5 | license = "MIT OR Apache-2.0" 6 | repository = "https://github.com/ChainSafe/WebZjs" 7 | description = "A browser client-side library for implementing Zcash wallets" 8 | edition = "2021" 9 | 10 | [lib] 11 | crate-type = ["cdylib", "rlib"] 12 | 13 | [package.metadata.wasm-pack.profile.release] 14 | wasm-opt = ["-O4", "-O4"] 15 | 16 | [features] 17 | default = ["native", "multicore"] 18 | 19 | multicore = ["zcash_proofs/multicore", "zcash_primitives/multicore", "zcash_client_memory/multicore"] 20 | 21 | 22 | # WASM specific features 23 | wasm = ["console_error_panic_hook", "dep:tracing-web", "zcash_client_backend/wasm-bindgen"] 24 | wasm-parallel = ["wasm", "wasm-bindgen-rayon", "multicore"] 25 | native = ["tonic/channel", "tonic/gzip", "tonic/tls-webpki-roots", "tokio/macros", "tokio/rt", "tokio/rt-multi-thread"] 26 | sqlite-db = ["dep:zcash_client_sqlite"] 27 | console_error_panic_hook = ["dep:console_error_panic_hook"] 28 | no-bundler = ["wasm-bindgen-rayon?/no-bundler", "wasm_thread/no-bundler"] 29 | 30 | [dependencies] 31 | webzjs-common = { path = "../webzjs-common" } 32 | webzjs-keys = { path = "../webzjs-keys" } 33 | ## Web dependencies 34 | wasm-bindgen.workspace = true 35 | js-sys.workspace = true 36 | web-sys.workspace = true 37 | 38 | wasm-bindgen-futures = "0.4.43" 39 | wasm-bindgen-rayon = { version = "1.3", optional = true } 40 | 41 | # WASM specific dependencies 42 | tracing-web = { version = "0.1.3", optional = true } 43 | console_error_panic_hook = { version = "0.1.7", optional = true } 44 | tonic-web-wasm-client = "0.6.0" 45 | tokio_with_wasm = { version = "0.7.1", features = ["rt", "rt-multi-thread", "sync", "macros", "time"] } 46 | 47 | ## Zcash dependencies 48 | 49 | zcash_keys = { workspace = true, features = ["transparent-inputs", "orchard", "sapling", "unstable"] } 50 | zcash_client_backend = { workspace = true, default-features = false, features = ["sync", "lightwalletd-tonic", "wasm-bindgen", "orchard", "pczt", "transparent-inputs"] } 51 | zcash_client_memory = { workspace = true, features = ["orchard", "transparent-inputs"] } 52 | zcash_primitives = { workspace = true } 53 | zcash_address = { workspace = true } 54 | zcash_protocol = { workspace = true, default-features = false } 55 | zcash_proofs = { workspace = true, default-features = false, features = ["bundled-prover", "multicore"] } 56 | zip321 = { workspace = true } 57 | zip32 = { workspace = true } 58 | pczt = { workspace = true, default-features = false, features = ["orchard", "sapling", "transparent"] } 59 | orchard = { version = "0.10.1", default-features = false } 60 | sapling = { workspace = true } 61 | bip32 = "0.5" 62 | ## gRPC Web dependencies 63 | prost = { version = "0.13", default-features = false } 64 | tonic = { version = "0.12", default-features = false, features = [ 65 | "prost", 66 | ] } 67 | 68 | 69 | # Used in Native tests 70 | tokio.workspace = true 71 | zcash_client_sqlite = { git = "https://github.com/ChainSafe/librustzcash", rev = "46e8ee0937b61fdbb417df7c663f62e6945d8090", default-features = false, features = ["unstable", "orchard"], optional = true } 72 | 73 | getrandom = { version = "0.2", features = ["js"] } 74 | thiserror.workspace = true 75 | indexed_db_futures = "0.5.0" 76 | sha2 = "0.10" 77 | ripemd = "0.1" 78 | bip0039 = "0.12.0" 79 | secrecy = "0.8.0" 80 | futures-util = "0.3.30" 81 | nonempty = "0.11" 82 | hex = "0.4.3" 83 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 84 | tracing = "0.1.40" 85 | rayon = { version = "1.8", features = ["web_spin_lock"] } 86 | subtle = "2.6.1" 87 | wasm_thread = { git = "https://github.com/ec2/wasm_thread", rev = "9e432077948d927d49373d1d039c23447d3648df", default-features = false, features = ["keep_worker_alive", "es_modules"] } 88 | 89 | wasm_sync = "0.1.2" 90 | http = { version = "1.1.0", default-features = false } 91 | serde.workspace = true 92 | postcard = { version = "1.0.10", features = ["alloc"] } 93 | serde-wasm-bindgen.workspace = true 94 | 95 | [lints] 96 | workspace = true -------------------------------------------------------------------------------- /packages/web-wallet/src/assets/icons/circle-dashed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /protos/compact_formats.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | 5 | syntax = "proto3"; 6 | package cash.z.wallet.sdk.rpc; 7 | option go_package = "lightwalletd/walletrpc"; 8 | option swift_prefix = ""; 9 | 10 | // Remember that proto3 fields are all optional. A field that is not present will be set to its zero value. 11 | // bytes fields of hashes are in canonical little-endian format. 12 | 13 | // ChainMetadata represents information about the state of the chain as of a given block. 14 | message ChainMetadata { 15 | uint32 saplingCommitmentTreeSize = 1; // the size of the Sapling note commitment tree as of the end of this block 16 | uint32 orchardCommitmentTreeSize = 2; // the size of the Orchard note commitment tree as of the end of this block 17 | } 18 | 19 | // CompactBlock is a packaging of ONLY the data from a block that's needed to: 20 | // 1. Detect a payment to your shielded Sapling address 21 | // 2. Detect a spend of your shielded Sapling notes 22 | // 3. Update your witnesses to generate new Sapling spend proofs. 23 | message CompactBlock { 24 | uint32 protoVersion = 1; // the version of this wire format, for storage 25 | uint64 height = 2; // the height of this block 26 | bytes hash = 3; // the ID (hash) of this block, same as in block explorers 27 | bytes prevHash = 4; // the ID (hash) of this block's predecessor 28 | uint32 time = 5; // Unix epoch time when the block was mined 29 | bytes header = 6; // (hash, prevHash, and time) OR (full header) 30 | repeated CompactTx vtx = 7; // zero or more compact transactions from this block 31 | ChainMetadata chainMetadata = 8; // information about the state of the chain as of this block 32 | } 33 | 34 | // CompactTx contains the minimum information for a wallet to know if this transaction 35 | // is relevant to it (either pays to it or spends from it) via shielded elements 36 | // only. This message will not encode a transparent-to-transparent transaction. 37 | message CompactTx { 38 | // Index and hash will allow the receiver to call out to chain 39 | // explorers or other data structures to retrieve more information 40 | // about this transaction. 41 | uint64 index = 1; // the index within the full block 42 | bytes hash = 2; // the ID (hash) of this transaction, same as in block explorers 43 | 44 | // The transaction fee: present if server can provide. In the case of a 45 | // stateless server and a transaction with transparent inputs, this will be 46 | // unset because the calculation requires reference to prior transactions. 47 | // If there are no transparent inputs, the fee will be calculable as: 48 | // valueBalanceSapling + valueBalanceOrchard + sum(vPubNew) - sum(vPubOld) - sum(tOut) 49 | uint32 fee = 3; 50 | 51 | repeated CompactSaplingSpend spends = 4; 52 | repeated CompactSaplingOutput outputs = 5; 53 | repeated CompactOrchardAction actions = 6; 54 | } 55 | 56 | // CompactSaplingSpend is a Sapling Spend Description as described in 7.3 of the Zcash 57 | // protocol specification. 58 | message CompactSaplingSpend { 59 | bytes nf = 1; // nullifier (see the Zcash protocol specification) 60 | } 61 | 62 | // output encodes the `cmu` field, `ephemeralKey` field, and a 52-byte prefix of the 63 | // `encCiphertext` field of a Sapling Output Description. These fields are described in 64 | // section 7.4 of the Zcash protocol spec: 65 | // https://zips.z.cash/protocol/protocol.pdf#outputencodingandconsensus 66 | // Total size is 116 bytes. 67 | message CompactSaplingOutput { 68 | bytes cmu = 1; // note commitment u-coordinate 69 | bytes ephemeralKey = 2; // ephemeral public key 70 | bytes ciphertext = 3; // first 52 bytes of ciphertext 71 | } 72 | 73 | // https://github.com/zcash/zips/blob/main/zip-0225.rst#orchard-action-description-orchardaction 74 | // (but not all fields are needed) 75 | message CompactOrchardAction { 76 | bytes nullifier = 1; // [32] The nullifier of the input note 77 | bytes cmx = 2; // [32] The x-coordinate of the note commitment for the output note 78 | bytes ephemeralKey = 3; // [32] An encoding of an ephemeral Pallas public key 79 | bytes ciphertext = 4; // [52] The first 52 bytes of the encCiphertext field 80 | } 81 | -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // `@type` JSDoc annotations allow editor autocompletion and type checking 3 | // (when paired with `@ts-check`). 4 | // There are various equivalent ways to declare your Docusaurus config. 5 | // See: https://docusaurus.io/docs/api/docusaurus-config 6 | 7 | import {themes as prismThemes} from 'prism-react-renderer'; 8 | const { join } = require('path'); 9 | 10 | const organizationName = "ChainSafe"; 11 | const projectName = "WebZjs"; 12 | 13 | /** @type {import('@docusaurus/types').Config} */ 14 | const config = { 15 | title: 'WebZjs Documentation', 16 | tagline: 'WebZjs is a JavaScript library for interacting with the Zcash blockchain', 17 | favicon: 'img/favicon.ico', 18 | 19 | // Set the production url of your site here 20 | url: `https://${organizationName}.github.io`, 21 | // Set the // pathname under which your site is served 22 | // For GitHub pages deployment, it is often '//' 23 | baseUrl: `/${projectName}/`, 24 | 25 | // GitHub pages deployment config. 26 | // If you aren't using GitHub pages, you don't need these. 27 | organizationName, // Usually your GitHub org/user name. 28 | projectName, // Usually your repo name. 29 | 30 | onBrokenLinks: 'throw', 31 | onBrokenMarkdownLinks: 'warn', 32 | 33 | // Even if you don't use internationalization, you can use this field to set 34 | // useful metadata like html lang. For example, if your site is Chinese, you 35 | // may want to replace "en" with "zh-Hans". 36 | i18n: { 37 | defaultLocale: 'en', 38 | locales: ['en'], 39 | }, 40 | 41 | plugins: [ 42 | [ 43 | 'docusaurus-plugin-typedoc-api', 44 | { 45 | projectRoot: join(__dirname, '..'), 46 | // Monorepo 47 | packages: [ { 48 | path: 'packages/webzjs-wallet', 49 | entry: 'webzjs_wallet.d.ts', 50 | },{ 51 | path: 'packages/webzjs-keys', 52 | entry: 'webzjs_keys.d.ts', 53 | }, { 54 | path: 'packages/webzjs-requests', 55 | entry: 'webzjs_requests.d.ts', 56 | }], 57 | minimal: false, 58 | debug: true, 59 | changelogs: true, 60 | readmes: false, 61 | tsconfigName: 'docs/tsconfig.json', 62 | }, 63 | ] 64 | ], 65 | 66 | presets: [ 67 | [ 68 | 'classic', 69 | /** @type {import('@docusaurus/preset-classic').Options} */ 70 | ({ 71 | docs: { 72 | sidebarPath: './sidebars.js', 73 | // Please change this to your repo. 74 | // Remove this to remove the "edit this page" links. 75 | editUrl: 76 | 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/', 77 | }, 78 | theme: { 79 | customCss: './src/css/custom.css', 80 | }, 81 | }), 82 | ], 83 | ], 84 | 85 | themeConfig: 86 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 87 | ({ 88 | // Replace with your project's social card 89 | image: 'img/docusaurus-social-card.jpg', 90 | navbar: { 91 | title: 'WebZ.js Documentation', 92 | // logo: { 93 | // alt: 'My Site Logo', 94 | // src: 'img/logo.svg', 95 | // }, 96 | items: [ 97 | { 98 | type: 'docSidebar', 99 | sidebarId: 'tutorialSidebar', 100 | position: 'left', 101 | label: 'Guides and Tutorials', 102 | }, 103 | { 104 | to: 'api', 105 | label: 'API', 106 | position: 'left', 107 | }, 108 | { 109 | href: 'https://github.com/ChainSafe/WebZjs', 110 | label: 'GitHub', 111 | position: 'right', 112 | }, 113 | ], 114 | }, 115 | footer: { 116 | style: 'dark', 117 | links: [ 118 | { 119 | title: 'Docs', 120 | items: [ 121 | { 122 | label: 'Guides and Tutorials', 123 | to: '/docs/intro', 124 | }, 125 | { 126 | label: 'API', 127 | to: '/api', 128 | }, 129 | ], 130 | }, 131 | { 132 | title: 'More', 133 | items: [ 134 | { 135 | label: 'GitHub', 136 | href: 'https://github.com/facebook/docusaurus', 137 | }, 138 | ], 139 | }, 140 | ], 141 | copyright: `Copyright © ${new Date().getFullYear()} ChainSafe Systems, Inc. Built with Docusaurus.`, 142 | }, 143 | prism: { 144 | theme: prismThemes.github, 145 | darkTheme: prismThemes.dracula, 146 | }, 147 | }), 148 | }; 149 | 150 | export default config; 151 | -------------------------------------------------------------------------------- /packages/web-wallet/src/components/Select/Select.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { ChevronSVG } from '../../assets'; 3 | import ErrorMessage from '../ErrorMessage/ErrorMessage'; 4 | 5 | interface Option { 6 | value: string; 7 | label: string; 8 | } 9 | 10 | interface OptionWithBalance extends Option { 11 | balance: number; 12 | } 13 | 14 | interface SelectProps extends React.SelectHTMLAttributes { 15 | label?: string; 16 | error?: string; 17 | options: Option[] | OptionWithBalance[]; 18 | containerClassName?: string; 19 | labelClassName?: string; 20 | dropdownClassName?: string; 21 | defaultOption?: Option | OptionWithBalance; 22 | handleChange: (option: string) => void; 23 | selectedSuffix?: string | React.ReactNode; 24 | suffixOptions?: { label: string; value: string | React.JSX.Element }[]; 25 | } 26 | 27 | interface DropdownOptionProps { 28 | option: Option; 29 | handleSelectOption: (option: Option) => void; 30 | suffixOptions?: { label: string; value: string | React.JSX.Element }[]; 31 | } 32 | 33 | const useOutsideClick = ( 34 | ref: React.RefObject, 35 | callback: () => void, 36 | ) => { 37 | useEffect(() => { 38 | const handleClickOutside = (event: MouseEvent) => { 39 | if (ref && ref.current && !ref.current.contains(event.target as Node)) { 40 | callback(); 41 | } 42 | }; 43 | document.addEventListener('mousedown', handleClickOutside); 44 | return () => document.removeEventListener('mousedown', handleClickOutside); 45 | }, [ref, callback]); 46 | }; 47 | 48 | const DropdownOption: React.FC = ({ 49 | option, 50 | handleSelectOption, 51 | suffixOptions, 52 | }) => ( 53 |
handleSelectOption(option)} 56 | > 57 | 58 | {option.label} 59 | 60 | {suffixOptions && ( 61 |
62 | {suffixOptions.map(({ label, value }) => { 63 | if (label === option.value) return
{value}
; 64 | })} 65 |
66 | )} 67 |
68 | ); 69 | 70 | const Select: React.FC = ({ 71 | label, 72 | error, 73 | options, 74 | defaultOption, 75 | containerClassName = '', 76 | labelClassName = '', 77 | dropdownClassName = '', 78 | selectedSuffix = '', 79 | suffixOptions, 80 | handleChange, 81 | }) => { 82 | const [isOpen, setIsOpen] = useState(false); 83 | const [selected, setSelected] = useState