commit 99907ee70679d16a1da3b3e38fd63c9ef666563c Author: jejolare Date: Wed Jul 24 19:37:25 2024 +0700 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76add87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..17ffce5 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ + +# ZanoWallet + +`zano_web3` is a TypeScript library for interacting with the ZanoWallet extension in the browser. It allows you to connect to a user's ZanoWallet, handle authentication, and manage wallet credentials. + +## Features + +- **Easy Integration**: Simplifies the process of connecting to the ZanoWallet extension. +- **Local Storage Support**: Optionally store wallet credentials in local storage. +- **Customizable**: Offers hooks for various connection lifecycle events. +- **Error Handling**: Provides a structured way to handle errors during the connection process. + +## Installation + +To install `zano_web3`, use npm or yarn: + +```bash +npm install zano_web3 +``` + +or + +```bash +yarn add zano_web3 +``` + +## Usage + +### Importing the Library + +```typescript +import ZanoWallet from 'zano_web3'; +``` + +### Creating a ZanoWallet Instance + +To create a `ZanoWallet` instance, you need to provide configuration options via the `ZanoWalletParams` interface. + +```typescript +const zanoWallet = new ZanoWallet({ + authPath: '/api/auth', // Custom server path for authentication + useLocalStorage: true, // Store wallet credentials in local storage (default: true) + aliasRequired: false, // Whether an alias is required (optional) + customLocalStorageKey: 'myWalletKey', // Custom key for local storage (optional) + customNonce: 'customNonceValue', // Custom nonce for signing (optional) + disableServerRequest: false, // Disable server request after signing (optional) + + onConnectStart: () => { + console.log('Connecting to ZanoWallet...'); + }, + onConnectEnd: (data) => { + console.log('Connected:', data); + }, + onConnectError: (error) => { + console.error('Connection error:', error); + }, + beforeConnect: async () => { + console.log('Preparing to connect...'); + }, + onLocalConnectEnd: (data) => { + console.log('Local connection established:', data); + } +}); +``` + +### Connecting to ZanoWallet + +To initiate the connection process, call the `connect` method: + +```typescript +await zanoWallet.connect(); +``` + +### Handling Wallet Credentials + +You can manually manage wallet credentials using `getSavedWalletCredentials` and `setWalletCredentials` methods: + +```typescript +const credentials = zanoWallet.getSavedWalletCredentials(); +if (credentials) { + console.log('Stored credentials:', credentials); +} + +zanoWallet.setWalletCredentials({ + nonce: 'newNonce', + signature: 'newSignature', + publicKey: 'newPublicKey' +}); +``` + +## Using the `useZanoWallet` Hook + +The `useZanoWallet` hook is a custom React hook provided by the `zano_web3` library. It simplifies the process of interacting with the ZanoWallet extension in a React application. + +This hook is designed to handle server-side rendering (SSR) limitations by ensuring that it only runs on the client-side. This means that any code using the `useZanoWallet` hook will not be executed during server-side rendering, but will work as expected once the application is running in the browser. + +To use the `useZanoWallet` hook, you can import it from the `zano_web3` library and call it within a functional component: + +```typescript +import { useZanoWallet } from 'zano_web3'; + +function MyComponent() { + + const wallet = useZanoWallet({ + // same params as for new ZanoWallet + }); + + return ( +
Your component...
+ ); +} +``` + + +## Requirements + +- ZanoWallet browser extension must be installed. + +## Contributing + +If you find any issues or want to contribute, please create a pull request or submit an issue. diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..cb32678 --- /dev/null +++ b/index.ts @@ -0,0 +1,6 @@ +import zanoWallet from "./src/zanoWallet"; + +import {useZanoWallet} from "./src/hooks"; +export {useZanoWallet}; + +export default zanoWallet; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ef79cea --- /dev/null +++ b/package-lock.json @@ -0,0 +1,102 @@ +{ + "name": "zano_web3", + "version": "2.3.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "zano_web3", + "version": "2.3.0", + "license": "ISC", + "dependencies": { + "react": "^18.3.1", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/uuid": "^10.0.0", + "typescript": "^5.5.4" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..87a4050 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "zano_web3", + "version": "2.3.0", + "description": "", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/jejolare/zano_web3.git" + }, + "keywords": [ + "zano", + "web3", + "crypto", + "blockchain", + "wallet" + ], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/react": "^18.3.3", + "@types/uuid": "^10.0.0", + "typescript": "^5.5.4" + }, + "dependencies": { + "react": "^18.3.1", + "uuid": "^10.0.0" + }, + "bugs": { + "url": "https://github.com/jejolare/zano_web3/issues" + }, + "homepage": "https://github.com/jejolare/zano_web3#readme" +} diff --git a/src/hooks.ts b/src/hooks.ts new file mode 100644 index 0000000..78196eb --- /dev/null +++ b/src/hooks.ts @@ -0,0 +1,18 @@ +import ZanoWallet, { ZanoWalletParams } from './zanoWallet'; +import { useEffect, useState } from 'react'; + +function useZanoWallet(params: ZanoWalletParams) { + const [zanoWallet, setZanoWallet] = useState(null); + + useEffect(() => { + if (typeof window === 'undefined') { + return; + } + + setZanoWallet(new ZanoWallet(params)); + }, []); + + return zanoWallet; +} + +export { useZanoWallet }; \ No newline at end of file diff --git a/src/zanoWallet.ts b/src/zanoWallet.ts new file mode 100644 index 0000000..4d5b541 --- /dev/null +++ b/src/zanoWallet.ts @@ -0,0 +1,192 @@ +import { v4 as uuidv4 } from 'uuid'; + +export interface ZanoWalletParams { + authPath: string; + useLocalStorage?: boolean; // default: true + aliasRequired?: boolean; + customLocalStorageKey?: string; + customNonce?: string; + customServerPath?: string; + disableServerRequest?: boolean; + + onConnectStart?: (...params: any) => any; + onConnectEnd?: (...params: any) => any; + onConnectError?: (...params: any) => any; + + beforeConnect?: (...params: any) => any; + onLocalConnectEnd?: (...params: any) => any; +} + +type GlobalWindow = Window & typeof globalThis; + +interface ZanoWindowParams { + request: (str: string, params?: any, timeoutMs?: number | null) => Promise; +} + +type ZanoWindow = Omit & { + zano: ZanoWindowParams +} + +interface WalletCredentials { + nonce: string; + signature: string; + publicKey: string; +} + +class ZanoWallet { + + private DEFAULT_LOCAL_STORAGE_KEY = "wallet"; + private localStorageKey: string; + + private params: ZanoWalletParams; + private zanoWallet: ZanoWindowParams; + + constructor(params: ZanoWalletParams) { + + if (typeof window === 'undefined') { + throw new Error('ZanoWallet can only be used in the browser'); + } + + if (!((window as unknown) as ZanoWindow).zano) { + throw new Error('ZanoWallet requires the ZanoWallet extension to be installed'); + } + + this.params = params; + this.zanoWallet = ((window as unknown) as ZanoWindow).zano; + this.localStorageKey = params.customLocalStorageKey || this.DEFAULT_LOCAL_STORAGE_KEY; + } + + + private handleError({ message } : { message: string }) { + if (this.params.onConnectError) { + this.params.onConnectError(message); + } else { + console.error(message); + } + } + + getSavedWalletCredentials() { + const savedWallet = localStorage.getItem(this.localStorageKey); + if (!savedWallet) return undefined; + try { + return JSON.parse(savedWallet) as WalletCredentials; + } catch { + return undefined; + } + } + + setWalletCredentials(credentials: WalletCredentials | undefined) { + if (credentials) { + localStorage.setItem(this.localStorageKey, JSON.stringify(credentials)); + } else { + localStorage.removeItem(this.localStorageKey); + } + } + + async connect() { + + if (this.params.beforeConnect) { + await this.params.beforeConnect(); + } + + if (this.params.onConnectStart) { + this.params.onConnectStart(); + } + + const walletData = (await ((window as unknown) as ZanoWindow).zano.request('GET_WALLET_DATA')).data; + + + if (!walletData?.address) { + return this.handleError({ message: 'Companion is offline' }); + } + + if (!walletData?.alias && this.params.aliasRequired) { + return this.handleError({ message: 'Alias not found' }); + } + + let nonce = ""; + let signature = ""; + let publicKey = ""; + + + const existingWallet = this.params.useLocalStorage ? this.getSavedWalletCredentials() : undefined; + + if (existingWallet) { + nonce = existingWallet.nonce; + signature = existingWallet.signature; + publicKey = existingWallet.publicKey; + } else { + const generatedNonce = this.params.customNonce || uuidv4(); + + const signResult = await this.zanoWallet.request( + 'REQUEST_MESSAGE_SIGN', + { + message: generatedNonce + }, + null + ); + + if (!signResult?.data?.result) { + return this.handleError({ message: 'Failed to sign message' }); + } + + nonce = generatedNonce; + signature = signResult.data.result.sig; + publicKey = signResult.data.result.pkey; + } + + + const serverData = { + alias: walletData.alias, + address: walletData.address, + signature, + publicKey, + message: nonce, + isSavedData: !!existingWallet + } + + if (this.params.onLocalConnectEnd) { + this.params.onLocalConnectEnd(serverData); + } + + if (!this.params.disableServerRequest) { + const result = await fetch( this.params.customServerPath || "/api/auth", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify( + { + data: serverData + } + ) + }) + .then(res => res.json()) + .catch((e) => ({ + success: false, + error: e.message + })); + + if (!result?.success || !result?.data) { + return this.handleError({ message: result.error }); + } + + if (!existingWallet && this.params.useLocalStorage) { + this.setWalletCredentials({ + publicKey, + signature, + nonce + }); + } + + if (this.params.onConnectEnd) { + this.params.onConnectEnd({ + ...serverData, + token: result.data.token + }); + } + } + } +} + +export default ZanoWallet; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3c97703 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "CommonJS", + "declaration": true, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "lib": ["ES6", "DOM"] + }, + "include": ["src/**/*", "index.ts"] +}