From 3cb2972911c204d0b6f624876074ea7a9793bae8 Mon Sep 17 00:00:00 2001 From: SaraJane Date: Fri, 16 Jan 2026 20:30:36 +0000 Subject: [PATCH] migrate for unifiedWallet to use-Wallet 4.4.0 for W3A (still UI bugs present) --- .../package-lock.json | 199 ++++++-- .../TokenizeRWATemplate-frontend/package.json | 11 +- .../TokenizeRWATemplate-frontend/src/App.tsx | 126 +++-- .../TokenizeRWATemplate-frontend/src/Home.tsx | 4 +- .../src/Layout.tsx | 18 +- .../src/components/Account.tsx | 9 +- .../src/components/ConnectWallet.tsx | 439 +++++++++++++----- .../src/components/TokenizeAsset.tsx | 49 +- .../src/components/Web3AuthButton.tsx | 288 ------------ .../src/components/Web3AuthProvider.tsx | 194 -------- .../src/hooks/useUnifiedWallet.ts | 47 -- .../src/hooks/useWeb3AuthHooks.ts | 411 ---------------- .../TokenizeRWATemplate-frontend/src/main.tsx | 2 +- .../src/utils/web3auth/algorandAdapter.ts | 159 ------- .../src/utils/web3auth/web3authConfig.ts | 109 ----- .../src/utils/web3auth/web3authIntegration.ts | 194 -------- .../vite.config.ts | 16 +- 17 files changed, 637 insertions(+), 1638 deletions(-) delete mode 100644 projects/TokenizeRWATemplate-frontend/src/components/Web3AuthButton.tsx delete mode 100644 projects/TokenizeRWATemplate-frontend/src/components/Web3AuthProvider.tsx delete mode 100644 projects/TokenizeRWATemplate-frontend/src/hooks/useUnifiedWallet.ts delete mode 100644 projects/TokenizeRWATemplate-frontend/src/hooks/useWeb3AuthHooks.ts delete mode 100644 projects/TokenizeRWATemplate-frontend/src/utils/web3auth/algorandAdapter.ts delete mode 100644 projects/TokenizeRWATemplate-frontend/src/utils/web3auth/web3authConfig.ts delete mode 100644 projects/TokenizeRWATemplate-frontend/src/utils/web3auth/web3authIntegration.ts diff --git a/projects/TokenizeRWATemplate-frontend/package-lock.json b/projects/TokenizeRWATemplate-frontend/package-lock.json index a584322..2b56109 100644 --- a/projects/TokenizeRWATemplate-frontend/package-lock.json +++ b/projects/TokenizeRWATemplate-frontend/package-lock.json @@ -9,13 +9,15 @@ "version": "0.1.0", "dependencies": { "@algorandfoundation/algokit-utils": "^9.0.0", + "@babel/runtime": "^7.28.6", "@blockshake/defly-connect": "^1.2.1", "@perawallet/connect": "^1.4.1", - "@txnlab/use-wallet": "^4.0.0", - "@txnlab/use-wallet-react": "^4.0.0", + "@txnlab/use-wallet": "^4.4.0", + "@txnlab/use-wallet-react": "^4.4.0", "@web3auth/base": "^9.7.0", "@web3auth/base-provider": "^9.7.0", "@web3auth/modal": "^9.7.0", + "@web3auth/single-factor-auth": "^9.5.0", "algosdk": "^3.0.0", "daisyui": "^4.0.0", "idb-keyval": "^6.2.2", @@ -25,7 +27,8 @@ "react-dom": "^18.2.0", "react-icons": "^5.5.0", "react-router-dom": "^7.11.0", - "tslib": "^2.6.2" + "tslib": "^2.6.2", + "tweetnacl": "^1.0.3" }, "devDependencies": { "@algorandfoundation/algokit-client-generator": "^5.0.0", @@ -38,11 +41,13 @@ "@typescript-eslint/parser": "^7.0.2", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.14", + "buffer": "^6.0.3", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "playwright": "^1.35.0", "postcss": "^8.4.24", + "process": "^0.11.10", "tailwindcss": "3.3.2", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", @@ -618,9 +623,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -3144,13 +3149,13 @@ "license": "MIT" }, "node_modules/@tanstack/react-store": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.3.tgz", - "integrity": "sha512-3Dnqtbw9P2P0gw8uUM8WP2fFfg8XMDSZCTsywRPZe/XqqYW8PGkXKZTvP0AHkE4mpqP9Y43GpOg9vwO44azu6Q==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.8.0.tgz", + "integrity": "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==", "license": "MIT", "dependencies": { - "@tanstack/store": "0.7.2", - "use-sync-external-store": "^1.5.0" + "@tanstack/store": "0.8.0", + "use-sync-external-store": "^1.6.0" }, "funding": { "type": "github", @@ -3162,9 +3167,9 @@ } }, "node_modules/@tanstack/store": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.2.tgz", - "integrity": "sha512-RP80Z30BYiPX2Pyo0Nyw4s1SJFH2jyM6f9i3HfX4pA+gm5jsnYryscdq2aIQLnL4TaGuQMO+zXmN9nh1Qck+Pg==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.8.0.tgz", + "integrity": "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==", "license": "MIT", "funding": { "type": "github", @@ -3214,6 +3219,19 @@ "npm": ">=9.x" } }, + "node_modules/@toruslabs/bs58": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@toruslabs/bs58/-/bs58-1.0.0.tgz", + "integrity": "sha512-osqIgm1MzEB6+fkaQeEUg4tuZXmhhXTn+K7+nZU7xDBcy+8Yr3eGNqJcQ4jds82g+dhkk2cBkge9sffv38iDQQ==", + "license": "MIT", + "engines": { + "node": ">=18.x", + "npm": ">=9.x" + }, + "peerDependencies": { + "@babel/runtime": "7.x" + } + }, "node_modules/@toruslabs/constants": { "version": "14.2.0", "resolved": "https://registry.npmjs.org/@toruslabs/constants/-/constants-14.2.0.tgz", @@ -3250,6 +3268,22 @@ "npm": ">=9.x" } }, + "node_modules/@toruslabs/fnd-base": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@toruslabs/fnd-base/-/fnd-base-14.2.0.tgz", + "integrity": "sha512-nqfcigOuz3pQJi+Q+tdCaDUVCaSUkGqqmw0bGnaKK2/PyXBlZhnEDzReM3aUbApJn3xitfrJEhnRvOJhzog/og==", + "license": "MIT", + "dependencies": { + "@toruslabs/constants": "^14.2.0" + }, + "engines": { + "node": ">=18.x", + "npm": ">=9.x" + }, + "peerDependencies": { + "@babel/runtime": "7.x" + } + }, "node_modules/@toruslabs/http-helpers": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/@toruslabs/http-helpers/-/http-helpers-7.0.1.tgz", @@ -3361,6 +3395,36 @@ "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", "license": "MIT" }, + "node_modules/@toruslabs/torus.js": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@toruslabs/torus.js/-/torus.js-15.1.1.tgz", + "integrity": "sha512-sLaXA1/R8KTTjU4t+teL3PPaJr2+j01QLYn5IY/t5uTD+1G2nzzfVWpkMDYrk9EfQYw0u4aKJ1lT7j9uKafMlg==", + "license": "MIT", + "dependencies": { + "@toruslabs/bs58": "^1.0.0", + "@toruslabs/constants": "^14.0.0", + "@toruslabs/eccrypto": "^5.0.4", + "@toruslabs/http-helpers": "^7.0.0", + "bn.js": "^5.2.1", + "elliptic": "^6.5.7", + "ethereum-cryptography": "^2.2.1", + "json-stable-stringify": "^1.1.1", + "loglevel": "^1.9.2" + }, + "engines": { + "node": ">=18.x", + "npm": ">=9.x" + }, + "peerDependencies": { + "@babel/runtime": "7.x" + } + }, + "node_modules/@toruslabs/torus.js/node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "license": "MIT" + }, "node_modules/@toruslabs/tweetnacl-js": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@toruslabs/tweetnacl-js/-/tweetnacl-js-1.0.4.tgz", @@ -3396,21 +3460,25 @@ "license": "MIT" }, "node_modules/@txnlab/use-wallet": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@txnlab/use-wallet/-/use-wallet-4.3.1.tgz", - "integrity": "sha512-kWDOauxROjwmnOmivp5iBAXqdksYHDXaP7P/AbX/uawnv+H+WQiP0dBUKnVLb//eOUbhq7QT33yUGGXC6QATgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@txnlab/use-wallet/-/use-wallet-4.4.0.tgz", + "integrity": "sha512-FMapmviqrLbKk80NsSM2hcChZl2W3fE52hTQ7+6LFfNiLbCQFH//mBZGipV5/bhyIx0pTgxcXMKygRprz6S9Fw==", "license": "MIT", "dependencies": { - "@tanstack/store": "0.7.2" + "@tanstack/store": "0.8.0" }, "peerDependencies": { "@agoralabs-sh/avm-web-provider": "^1.7.0", "@blockshake/defly-connect": "^1.2.1", "@perawallet/connect": "^1.4.1", "@walletconnect/modal": "^2.7.0", - "@walletconnect/sign-client": "^2.21.8", + "@walletconnect/sign-client": "^2.23.1", + "@web3auth/base": "^9.0.0", + "@web3auth/base-provider": "^9.0.0", + "@web3auth/modal": "^9.0.0", + "@web3auth/single-factor-auth": "^9.0.0", "algosdk": "^3.0.0", - "lute-connect": "^1.6.2" + "lute-connect": "^1.6.3" }, "peerDependenciesMeta": { "@agoralabs-sh/avm-web-provider": { @@ -3428,28 +3496,40 @@ "@walletconnect/sign-client": { "optional": true }, + "@web3auth/base": { + "optional": true + }, + "@web3auth/base-provider": { + "optional": true + }, + "@web3auth/modal": { + "optional": true + }, + "@web3auth/single-factor-auth": { + "optional": true + }, "lute-connect": { "optional": true } } }, "node_modules/@txnlab/use-wallet-react": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@txnlab/use-wallet-react/-/use-wallet-react-4.3.1.tgz", - "integrity": "sha512-Gmj1qBNykx2HbFS4FzzqCa8uWI4UkdYw2mk6a+62AVoKU2/hvPe9K6DvdDrc883dJoRSK0xPtSGjJ6lKxT1bCQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@txnlab/use-wallet-react/-/use-wallet-react-4.4.0.tgz", + "integrity": "sha512-IQX0cUB8bybnGluhN46t8kCEtBof9yxB1aK6ru4DeysUuTn9pkWAF5/bTDK15+lJTs/88+C4v26CUGvbiNAltQ==", "license": "MIT", "dependencies": { - "@tanstack/react-store": "0.7.3", - "@txnlab/use-wallet": "4.3.1" + "@tanstack/react-store": "0.8.0", + "@txnlab/use-wallet": "4.4.0" }, "peerDependencies": { "@blockshake/defly-connect": "^1.2.1", "@magic-ext/algorand": "^24.4.2", "@perawallet/connect": "^1.4.1", "@walletconnect/modal": "^2.7.0", - "@walletconnect/sign-client": "^2.21.8", + "@walletconnect/sign-client": "^2.23.1", "algosdk": "^3.0.0", - "lute-connect": "^1.6.2", + "lute-connect": "^1.6.3", "magic-sdk": "^29.4.2", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -5180,6 +5260,56 @@ } } }, + "node_modules/@web3auth/single-factor-auth": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@web3auth/single-factor-auth/-/single-factor-auth-9.5.0.tgz", + "integrity": "sha512-50cjvQ6kXDgbUXmjlVgxEN6qV8giMqDn6dc7IykANflLfTjjZKc8EKmis7CwRFlCIESGZ2vIOL4CFo0StHVvpg==", + "license": "ISC", + "dependencies": { + "@toruslabs/base-controllers": "^6.3.2", + "@toruslabs/constants": "^14.2.0", + "@toruslabs/fnd-base": "^14.2.0", + "@toruslabs/session-manager": "^3.2.0", + "@toruslabs/torus.js": "^15.1.1", + "@web3auth/auth": "^9.6.4", + "@web3auth/base": "^9.7.0", + "bs58": "^5.0.0" + }, + "engines": { + "node": ">=18.x", + "npm": ">=9.x" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.34.9" + }, + "peerDependencies": { + "@babel/runtime": "^7.x" + } + }, + "node_modules/@web3auth/single-factor-auth/node_modules/@toruslabs/base-controllers": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@toruslabs/base-controllers/-/base-controllers-6.3.3.tgz", + "integrity": "sha512-za1QD4jADtSWFMYVDZ9YZJMxzoBtT0T/SWkmQECOiAVtlUQgPn9naIqzc1ApBYdzwp9GUaxe4P1510R+5I/Ncg==", + "license": "ISC", + "dependencies": { + "@ethereumjs/util": "^9.1.0", + "@toruslabs/broadcast-channel": "^11.0.0", + "@toruslabs/http-helpers": "^7.0.0", + "@web3auth/auth": "^9.5.2", + "async-mutex": "^0.5.0", + "bignumber.js": "^9.1.2", + "bowser": "^2.11.0", + "jwt-decode": "^4.0.0", + "loglevel": "^1.9.2" + }, + "engines": { + "node": ">=18.x", + "npm": ">=9.x" + }, + "peerDependencies": { + "@babel/runtime": "7.x" + } + }, "node_modules/@web3auth/ui": { "version": "9.7.0", "resolved": "https://registry.npmjs.org/@web3auth/ui/-/ui-9.7.0.tgz", @@ -5633,6 +5763,12 @@ "dev": true, "license": "MIT" }, + "node_modules/base-x": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -5956,6 +6092,15 @@ "node": ">= 6" } }, + "node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "license": "MIT", + "dependencies": { + "base-x": "^4.0.0" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", diff --git a/projects/TokenizeRWATemplate-frontend/package.json b/projects/TokenizeRWATemplate-frontend/package.json index c36c349..3becf65 100644 --- a/projects/TokenizeRWATemplate-frontend/package.json +++ b/projects/TokenizeRWATemplate-frontend/package.json @@ -22,11 +22,13 @@ "@typescript-eslint/parser": "^7.0.2", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.14", + "buffer": "^6.0.3", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "playwright": "^1.35.0", "postcss": "^8.4.24", + "process": "^0.11.10", "tailwindcss": "3.3.2", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", @@ -36,13 +38,15 @@ }, "dependencies": { "@algorandfoundation/algokit-utils": "^9.0.0", + "@babel/runtime": "^7.28.6", "@blockshake/defly-connect": "^1.2.1", "@perawallet/connect": "^1.4.1", - "@txnlab/use-wallet": "^4.0.0", - "@txnlab/use-wallet-react": "^4.0.0", + "@txnlab/use-wallet": "^4.4.0", + "@txnlab/use-wallet-react": "^4.4.0", "@web3auth/base": "^9.7.0", "@web3auth/base-provider": "^9.7.0", "@web3auth/modal": "^9.7.0", + "@web3auth/single-factor-auth": "^9.5.0", "algosdk": "^3.0.0", "daisyui": "^4.0.0", "idb-keyval": "^6.2.2", @@ -52,7 +56,8 @@ "react-dom": "^18.2.0", "react-icons": "^5.5.0", "react-router-dom": "^7.11.0", - "tslib": "^2.6.2" + "tslib": "^2.6.2", + "tweetnacl": "^1.0.3" }, "scripts": { "generate:app-clients": "algokit project link --all", diff --git a/projects/TokenizeRWATemplate-frontend/src/App.tsx b/projects/TokenizeRWATemplate-frontend/src/App.tsx index c621da1..68d8bc1 100644 --- a/projects/TokenizeRWATemplate-frontend/src/App.tsx +++ b/projects/TokenizeRWATemplate-frontend/src/App.tsx @@ -1,71 +1,99 @@ import { SupportedWallet, WalletId, WalletManager, WalletProvider } from '@txnlab/use-wallet-react' import { SnackbarProvider } from 'notistack' +import { useMemo } from 'react' import { BrowserRouter, Route, Routes } from 'react-router-dom' -import { Web3AuthProvider } from './components/Web3AuthProvider' import Home from './Home' import Layout from './Layout' import TokenizePage from './TokenizePage' import { getAlgodConfigFromViteEnvironment, getKmdConfigFromViteEnvironment } from './utils/network/getAlgoClientConfigs' -// Configure supported wallets based on network environment -let supportedWallets: SupportedWallet[] -if (import.meta.env.VITE_ALGOD_NETWORK === 'localnet') { - // LocalNet: KMD wallet for local development - const kmdConfig = getKmdConfigFromViteEnvironment() - supportedWallets = [ - { - id: WalletId.KMD, - options: { - baseServer: kmdConfig.server, - token: String(kmdConfig.token), - port: String(kmdConfig.port), - }, - }, - { id: WalletId.LUTE }, - ] -} else { - // TestNet/MainNet: Browser extension wallets (Pera, Defly, Exodus, Lute) - supportedWallets = [{ id: WalletId.DEFLY }, { id: WalletId.PERA }, { id: WalletId.EXODUS }, { id: WalletId.LUTE }] -} +// Get Web3Auth client ID from environment +const web3AuthClientId = (import.meta.env.VITE_WEB3AUTH_CLIENT_ID ?? '').trim() /** - * Main App Component - * Sets up wallet provider and routing for the Tokenization dApp + * Build supported wallets list based on env/network. + * NOTE: Web3Auth defaults to sapphire_mainnet unless web3AuthNetwork is provided. */ +function buildSupportedWallets(): SupportedWallet[] { + if (import.meta.env.VITE_ALGOD_NETWORK === 'localnet') { + const kmdConfig = getKmdConfigFromViteEnvironment() + return [ + { + id: WalletId.KMD, + options: { + baseServer: kmdConfig.server, + token: String(kmdConfig.token), + port: String(kmdConfig.port), + }, + }, + { id: WalletId.LUTE }, + ] + } + + // TestNet/MainNet wallets + const wallets: SupportedWallet[] = [{ id: WalletId.PERA }, { id: WalletId.DEFLY }, { id: WalletId.LUTE }] + + // Only add Web3Auth if we actually have a client id + // use-wallet v4.4.0+ includes built-in Web3Auth provider + if (web3AuthClientId) { + wallets.push({ + id: WalletId.WEB3AUTH, + options: { + clientId: web3AuthClientId, + // Web3Auth network: 'sapphire_devnet' for development, 'sapphire_mainnet' for production + web3AuthNetwork: 'sapphire_devnet', + // Optional: Set default login provider (e.g., 'google' for direct Google login) + // If not set, shows full provider selection modal + // loginProvider: 'google', + // Optional: UI customization + uiConfig: { + appName: 'Tokenize RWA Template', + mode: 'auto', // 'auto' | 'light' | 'dark' + }, + }, + }) + } + + return wallets +} + export default function App() { const algodConfig = getAlgodConfigFromViteEnvironment() - const walletManager = new WalletManager({ - wallets: supportedWallets, - defaultNetwork: algodConfig.network, - networks: { - [algodConfig.network]: { - algod: { - baseServer: algodConfig.server, - port: algodConfig.port, - token: String(algodConfig.token), + const supportedWallets = useMemo(() => buildSupportedWallets(), []) + const walletManager = useMemo(() => { + const mgr = new WalletManager({ + wallets: supportedWallets, + defaultNetwork: algodConfig.network, + networks: { + [algodConfig.network]: { + algod: { + baseServer: algodConfig.server, + port: algodConfig.port, + token: String(algodConfig.token), + }, }, }, - }, - options: { - resetNetwork: true, - }, - }) + options: { + resetNetwork: true, + }, + }) + + return mgr + }, [algodConfig.network, algodConfig.server, algodConfig.port, algodConfig.token, supportedWallets]) return ( - - - - - }> - } /> - } /> - - - - - + + + + }> + } /> + } /> + + + + ) } diff --git a/projects/TokenizeRWATemplate-frontend/src/Home.tsx b/projects/TokenizeRWATemplate-frontend/src/Home.tsx index 40fa91a..177d2e7 100644 --- a/projects/TokenizeRWATemplate-frontend/src/Home.tsx +++ b/projects/TokenizeRWATemplate-frontend/src/Home.tsx @@ -1,4 +1,4 @@ -import { useUnifiedWallet } from './hooks/useUnifiedWallet' +import { useWallet } from '@txnlab/use-wallet-react' import { Link } from 'react-router-dom' /** @@ -7,7 +7,7 @@ import { Link } from 'react-router-dom' * Displays features, how it works, and CTAs to connect wallet and create assets */ export default function Home() { -const { activeAddress } = useUnifiedWallet() + const { activeAddress } = useWallet() return (
diff --git a/projects/TokenizeRWATemplate-frontend/src/Layout.tsx b/projects/TokenizeRWATemplate-frontend/src/Layout.tsx index 51ec351..a9fa693 100644 --- a/projects/TokenizeRWATemplate-frontend/src/Layout.tsx +++ b/projects/TokenizeRWATemplate-frontend/src/Layout.tsx @@ -1,18 +1,22 @@ +import { useWallet } from '@txnlab/use-wallet-react' import { useState } from 'react' import { NavLink, Outlet } from 'react-router-dom' import ConnectWallet from './components/ConnectWallet' import ThemeToggle from './components/ThemeToggle' -import { useUnifiedWallet } from './hooks/useUnifiedWallet' +import { ellipseAddress } from './utils/ellipseAddress' export default function Layout() { const [openWalletModal, setOpenWalletModal] = useState(false) - const { isConnected, activeAddress, userInfo } = useUnifiedWallet() + const { activeAddress, isActive } = useWallet() const toggleWalletModal = () => setOpenWalletModal(!openWalletModal) + // Check if wallet is connected - activeAddress is the primary indicator + // isActive might be undefined for Web3Auth, so we check activeAddress + const isConnected = Boolean(activeAddress) + // Helper to format address: "ZBC...WXYZ" - const displayAddress = - isConnected && activeAddress ? `${activeAddress.toString().slice(0, 4)}...${activeAddress.toString().slice(-4)}` : 'Sign in' + const displayAddress = isConnected && activeAddress ? ellipseAddress(activeAddress, 4) : 'Sign in' return (
@@ -41,7 +45,7 @@ export default function Layout() {
- {/* ONE Button to Rule Them All */} + {/* Sign In / Account Button */}
) diff --git a/projects/TokenizeRWATemplate-frontend/src/components/Account.tsx b/projects/TokenizeRWATemplate-frontend/src/components/Account.tsx index d3a396e..d7d7e64 100644 --- a/projects/TokenizeRWATemplate-frontend/src/components/Account.tsx +++ b/projects/TokenizeRWATemplate-frontend/src/components/Account.tsx @@ -1,5 +1,5 @@ +import { useWallet } from '@txnlab/use-wallet-react' import { useMemo, useState } from 'react' -import { useUnifiedWallet } from '../hooks/useUnifiedWallet' import { ellipseAddress } from '../utils/ellipseAddress' import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs' @@ -10,13 +10,13 @@ import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClien * and current network. * * Works for BOTH: - * - Web3Auth (Google login) + * - Web3Auth (Google login via use-wallet) * - Traditional wallets (Pera / Defly / etc) * * Address links to Lora explorer. */ const Account = () => { - const { activeAddress } = useUnifiedWallet() + const { activeAddress } = useWallet() const algoConfig = getAlgodConfigFromViteEnvironment() const [copied, setCopied] = useState(false) @@ -25,7 +25,7 @@ const Account = () => { return algoConfig.network === '' ? 'localnet' : algoConfig.network.toLowerCase() }, [algoConfig.network]) - // Normalize address to string (VERY IMPORTANT) + // Normalize address to string safely const address = typeof activeAddress === 'string' ? activeAddress : activeAddress ? String(activeAddress) : null if (!address) { @@ -40,7 +40,6 @@ const Account = () => { setCopied(true) setTimeout(() => setCopied(false), 1500) } catch (e) { - console.error('Failed to copy address', e) } } diff --git a/projects/TokenizeRWATemplate-frontend/src/components/ConnectWallet.tsx b/projects/TokenizeRWATemplate-frontend/src/components/ConnectWallet.tsx index c405ccd..b017315 100644 --- a/projects/TokenizeRWATemplate-frontend/src/components/ConnectWallet.tsx +++ b/projects/TokenizeRWATemplate-frontend/src/components/ConnectWallet.tsx @@ -1,163 +1,350 @@ -import { WalletId } from '@txnlab/use-wallet-react' -import { useEffect, useRef, useState } from 'react' -import { SocialLoginProvider, useUnifiedWallet } from '../hooks/useUnifiedWallet' -import Account from './Account' +import { useWallet, WalletId, type BaseWallet } from '@txnlab/use-wallet-react' +import { useMemo, useState } from 'react' +import { ellipseAddress } from '../utils/ellipseAddress' +import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs' -interface ConnectWalletInterface { +interface ConnectWalletProps { openModal: boolean closeModal: () => void } -const ConnectWallet = ({ openModal, closeModal }: ConnectWalletInterface) => { - const { isConnected, walletType, userInfo, traditionalWallets, connectGoogle, connectFacebook, connectGithub, disconnect } = - useUnifiedWallet() +const ConnectWallet = ({ openModal, closeModal }: ConnectWalletProps) => { + const { wallets, activeWallet, activeAddress, isReady, isActive, disconnect } = useWallet() - const [connectingProvider, setConnectingProvider] = useState(null) - const dialogRef = useRef(null) + const [connectingId, setConnectingId] = useState(null) + const [lastError, setLastError] = useState('') + const [copied, setCopied] = useState(false) + + // Get network config for Lora link + const algoConfig = getAlgodConfigFromViteEnvironment() + const networkName = useMemo(() => { + return algoConfig.network === '' ? 'localnet' : algoConfig.network.toLowerCase() + }, [algoConfig.network]) + + // Get all registered wallets + const visibleWallets = useMemo(() => { + return (wallets ?? []).filter(Boolean) + }, [wallets]) + + // Handle wallet connection + const handleConnect = async (wallet: BaseWallet) => { + setLastError('') + setConnectingId(wallet.id) - const handleSocialLogin = async (provider: SocialLoginProvider, connectFn: () => Promise) => { try { - setConnectingProvider(provider) - await connectFn() + if (typeof wallet.connect !== 'function') { + throw new Error(`Wallet "${wallet.id}" is missing connect().`) + } + + // For Web3Auth, try to access and manually initialize if needed + if (wallet.id === WalletId.WEB3AUTH) { + const walletAny = wallet as any + + // Check if we can access the internal Web3Auth instance + const web3AuthInstance = walletAny.web3Auth || walletAny._web3Auth || walletAny.instance + + if (web3AuthInstance) { + // If not initialized, try to initialize + if (web3AuthInstance.status !== 'ready' && typeof web3AuthInstance.init === 'function') { + try { + await web3AuthInstance.init() + } catch (initError) { + setLastError(`Web3Auth initialization failed: ${initError}`) + return + } + } + + // If initialized but not connected, try direct connect + if (web3AuthInstance.status === 'ready' && !web3AuthInstance.connected) { + try { + await web3AuthInstance.connect() + // Don't call wallet.connect() if we already connected directly + closeModal() + return + } catch (directConnectError) { + // Fall through to try wallet.connect() + } + } + } + } + + // Call connect - this should open the Web3Auth modal + const connectPromise = wallet.connect() + + // Add timeout for Web3Auth to detect if it hangs + if (wallet.id === WalletId.WEB3AUTH) { + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject( + new Error( + 'Web3Auth connection timed out after 30 seconds. The modal may not have opened. Check browser console for Web3Auth initialization errors.', + ), + ) + }, 30000) + }) + + await Promise.race([connectPromise, timeoutPromise]) + } else { + await connectPromise + } + + // For Web3Auth, verify the address was set + if (wallet.id === WalletId.WEB3AUTH) { + // Wait a bit to see if state updates + await new Promise((resolve) => setTimeout(resolve, 1000)) + + // Check if address was set + const hasAddress = activeAddress && activeAddress.length > 0 + + await new Promise((resolve) => setTimeout(resolve, 1000)) + + if (!hasAddress) { + return + } + } else { + await new Promise((resolve) => setTimeout(resolve, 500)) + } + closeModal() - } catch (error) { - throw new Error(`Failed to connect with ${provider}: ${error}`) + } catch (e: any) { + const msg = e?.message ? String(e.message) : String(e) + + // Provide more helpful error messages for Web3Auth + if (wallet.id === WalletId.WEB3AUTH) { + if (msg.includes('clientId') || msg.includes('Client ID')) { + setLastError('Web3Auth Client ID is missing or invalid. Check your .env file.') + } else if (msg.includes('network') || msg.includes('Network')) { + setLastError('Web3Auth network configuration error. Check your network settings.') + } else if (msg.includes('timeout')) { + setLastError('Web3Auth connection timed out. The login modal may not have opened. Check browser console for Web3Auth errors.') + } else { + setLastError(`Web3Auth connection failed: ${msg}`) + } + } else { + setLastError(`Failed to connect ${wallet.id}: ${msg}`) + } + } finally { + setConnectingId(null) } } - const socialOptions: { id: SocialLoginProvider; label: string; icon: string; action: () => Promise }[] = [ - { - id: 'google', - label: 'Continue with Google', - icon: 'https://www.gstatic.com/images/branding/product/1x/gsa_512dp.png', - action: connectGoogle, - }, - { - id: 'facebook', - label: 'Continue with Facebook', - icon: 'https://www.facebook.com/images/fb_icon_325x325.png', - action: connectFacebook, - }, - { - id: 'github', - label: 'Continue with GitHub', - icon: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png', - action: connectGithub, - }, - ] - - const formatSocialProvider = (provider?: string) => { - const normalized = provider?.toLowerCase() - switch (normalized) { - case 'google': - return 'Google' - case 'facebook': - return 'Facebook' - case 'github': - return 'GitHub' - default: - return 'Social Login' + // Handle disconnect + const handleDisconnect = async () => { + setLastError('') + try { + // For Web3Auth, we might need to use the wallet's disconnect method directly + if (activeWallet) { + // Try the wallet's disconnect method first + if (typeof (activeWallet as any).disconnect === 'function') { + await (activeWallet as any).disconnect() + } + // Fall back to hook's disconnect if available + else if (typeof disconnect === 'function') { + await disconnect() + } + } else if (typeof disconnect === 'function') { + await disconnect() + } + closeModal() + } catch (e: any) { + // Silently handle disconnect errors + closeModal() } } - useEffect(() => { - const dialog = dialogRef.current - if (!dialog) return - openModal ? dialog.showModal() : dialog.close() - }, [openModal]) + // Don't render if modal is closed + if (!openModal) return null + + // Check if wallet is connected - activeAddress is the primary indicator + // isActive might be undefined for Web3Auth, so we check activeAddress + const connected = Boolean(activeAddress) + + // Separate Web3Auth from traditional wallets for better UX + const web3AuthWallet = visibleWallets.find((w) => w.id === WalletId.WEB3AUTH) + const traditionalWallets = visibleWallets.filter((w) => w.id !== WalletId.WEB3AUTH) return ( - e.target === dialogRef.current && closeModal()} - > -
+
+
e.stopPropagation()} + > + {/* Header */}
-

{isConnected ? 'Account' : 'Sign in'}

-
-
- {isConnected ? ( - /* --- CONNECTED STATE --- */ -
-
- + {/* Error display */} + {lastError && ( +
+ {lastError} +
+ )} - {walletType === 'web3auth' && userInfo && ( -
- {userInfo.profileImage && ( - Profile - )} -
-

- Connected via {formatSocialProvider(userInfo.typeOfLogin)} -

-

{userInfo.email || userInfo.name}

-
-
+ {/* Connected state */} + {connected ? ( +
+
+
+
Connected
+
+ + Active +
+
+ + {/* Address with copy button */} +
+
+ {ellipseAddress(activeAddress || '', 8)} +
+ +
+ + {/* Full address (collapsible) */} +
+ + Show full address + +
+ {activeAddress} +
+
+ + {/* Wallet type and Lora link */} +
+
+ Wallet: {activeWallet?.metadata?.name ?? activeWallet?.id} +
+ {activeAddress && ( + + View on Lora + + + + )}
- -
- ) : ( - /* --- DISCONNECTED STATE --- */ - <> + + +
+ ) : ( +
+ {/* Google Sign In (Web3Auth) */} + {web3AuthWallet && (
-

Social Login

- {socialOptions.map((option) => ( + +
+ )} + + {/* Divider */} + {web3AuthWallet && traditionalWallets.length > 0 && ( +
+
+ or +
+
+ )} + + {/* Traditional Wallets */} + {traditionalWallets.length > 0 && ( +
+
Algorand Wallets
+ {traditionalWallets.map((w) => ( ))}
+ )} -
-
-
-
-
- Or use a wallet -
+ {/* Warning if Web3Auth didn't register */} + {!web3AuthWallet && ( +
+

+ Google sign-in is not available. Check console for Web3Auth errors. +

- -
- {traditionalWallets?.map((wallet) => ( - - ))} -
- - )} -
+ )} +
+ )}
-
+
) } diff --git a/projects/TokenizeRWATemplate-frontend/src/components/TokenizeAsset.tsx b/projects/TokenizeRWATemplate-frontend/src/components/TokenizeAsset.tsx index 3eeed64..310b38b 100644 --- a/projects/TokenizeRWATemplate-frontend/src/components/TokenizeAsset.tsx +++ b/projects/TokenizeRWATemplate-frontend/src/components/TokenizeAsset.tsx @@ -4,8 +4,8 @@ import { useSnackbar } from 'notistack' import { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { AiOutlineCloudUpload, AiOutlineInfoCircle, AiOutlineLoading3Quarters } from 'react-icons/ai' import { BsCoin } from 'react-icons/bs' -import { useUnifiedWallet } from '../hooks/useUnifiedWallet' import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs' +import { useWallet } from '@txnlab/use-wallet-react' /** * Type for created assets stored in browser localStorage @@ -175,8 +175,12 @@ export default function TokenizeAsset() { const [nftFreeze, setNftFreeze] = useState('') const [nftClawback, setNftClawback] = useState('') - // ===== Unified wallet (Web3Auth OR WalletConnect) ===== - const { signer, activeAddress } = useUnifiedWallet() + // ===== use-wallet (Web3Auth OR WalletConnect) ===== + // Use transactionSigner (not signer) - this is the correct property name from use-wallet + const { transactionSigner, activeAddress } = useWallet() + + // Alias for backward compatibility in the code + const signer = transactionSigner // ===== Notifications ===== const { enqueueSnackbar } = useSnackbar() @@ -283,7 +287,6 @@ export default function TokenizeAsset() { setHasCheckedUsdcOnChain(true) hasCheckedUsdcOnChainRef.current = true } catch (e) { - console.error('Failed to check USDC opt-in', e) // On error, set to not-opted-in but don't mark as checked // This allows retry on next render cycle setUsdcStatus('not-opted-in') @@ -385,10 +388,17 @@ export default function TokenizeAsset() { * Opt-in is an asset transfer of 0 USDC to self */ const handleOptInUsdc = async () => { - if (!signer || !activeAddress) { + // Check for activeAddress first (primary indicator of connection) + // transactionSigner might be available even if not explicitly set + if (!activeAddress) { enqueueSnackbar('Please connect a wallet or continue with Google first.', { variant: 'warning' }) return } + + if (!signer) { + enqueueSnackbar('Wallet signer not available. Please try reconnecting your wallet.', { variant: 'error' }) + return + } // Prevent duplicate transactions if already opted in if (usdcOptedIn) { @@ -438,7 +448,6 @@ export default function TokenizeAsset() { checkUsdcOptInStatus() }, 2000) } catch (e) { - console.error('USDC opt-in failed', e) enqueueSnackbar('USDC opt-in failed.', { variant: 'error' }) } finally { setUsdcOptInLoading(false) @@ -515,10 +524,16 @@ export default function TokenizeAsset() { * Adjusts total supply by decimals and saves asset to localStorage */ const handleTokenize = async () => { - if (!signer || !activeAddress) { + // Check for activeAddress first (primary indicator of connection) + if (!activeAddress) { enqueueSnackbar('Please connect a wallet or continue with Google first.', { variant: 'warning' }) return } + + if (!signer) { + enqueueSnackbar('Wallet signer not available. Please try reconnecting your wallet.', { variant: 'error' }) + return + } if (!assetName || !unitName) { enqueueSnackbar('Please enter an asset name and symbol.', { variant: 'warning' }) @@ -596,7 +611,6 @@ export default function TokenizeAsset() { resetDefaults() } catch (error) { - console.error(error) enqueueSnackbar('Failed to tokenize asset (ASA creation failed).', { variant: 'error' }) } finally { setLoading(false) @@ -608,10 +622,16 @@ export default function TokenizeAsset() { * Handles validation, amount conversion, and transaction submission */ const handleTransferAsset = async () => { - if (!signer || !activeAddress) { + // Check for activeAddress first (primary indicator of connection) + if (!activeAddress) { enqueueSnackbar('Please connect a wallet or continue with Google first.', { variant: 'warning' }) return } + + if (!signer) { + enqueueSnackbar('Wallet signer not available. Please try reconnecting your wallet.', { variant: 'error' }) + return + } if (!receiverAddress) { enqueueSnackbar('Please enter a recipient address.', { variant: 'warning' }) @@ -762,7 +782,6 @@ export default function TokenizeAsset() { setReceiverAddress('') setTransferAmount('1') } catch (error) { - console.error('Transfer failed', error) if (transferMode === 'algo') { enqueueSnackbar('ALGO send failed.', { variant: 'error' }) } else { @@ -787,10 +806,16 @@ export default function TokenizeAsset() { const handleDivClick = () => fileInputRef.current?.click() const handleMintNFT = async () => { - if (!signer || !activeAddress) { + // Check for activeAddress first (primary indicator of connection) + if (!activeAddress) { enqueueSnackbar('Please connect a wallet or continue with Google first.', { variant: 'warning' }) return } + + if (!signer) { + enqueueSnackbar('Wallet signer not available. Please try reconnecting your wallet.', { variant: 'error' }) + return + } if (!selectedFile) { enqueueSnackbar('Please select an image file to mint.', { variant: 'warning' }) @@ -843,7 +868,6 @@ export default function TokenizeAsset() { metadataUrl = data.metadataUrl if (!metadataUrl) throw new Error('Backend did not return a valid metadata URL') } catch (e: any) { - console.error(e) enqueueSnackbar('Error uploading to backend. If in Codespaces, make port 3001 Public.', { variant: 'error' }) setNftLoading(false) return @@ -917,7 +941,6 @@ export default function TokenizeAsset() { setPreviewUrl('') if (fileInputRef.current) fileInputRef.current.value = '' } catch (e: any) { - console.error(e) enqueueSnackbar(`Failed to mint NFT: ${e?.message || 'Unknown error'}`, { variant: 'error' }) } finally { setNftLoading(false) diff --git a/projects/TokenizeRWATemplate-frontend/src/components/Web3AuthButton.tsx b/projects/TokenizeRWATemplate-frontend/src/components/Web3AuthButton.tsx deleted file mode 100644 index 997349f..0000000 --- a/projects/TokenizeRWATemplate-frontend/src/components/Web3AuthButton.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import { useEffect, useState } from 'react' -import { AiOutlineLoading3Quarters } from 'react-icons/ai' -import { FaCheck, FaCopy, FaGoogle } from 'react-icons/fa' -import { useWeb3Auth } from './Web3AuthProvider' - -/** - * Web3AuthButton Component - * - * Displays "Sign in with Google" button when disconnected. - * Shows connected Algorand address with disconnect option when logged in. - * - * Features: - * - Wallet connection/disconnection with Web3Auth (Google OAuth) - * - Auto-generation of Algorand wallet from Google credentials - * - Loading states and error handling - * - * Usage: - * ```tsx - * - * ``` - */ -export function Web3AuthButton() { - const { isConnected, isLoading, error, algorandAccount, userInfo, login, logout } = useWeb3Auth() - const [isDropdownOpen, setIsDropdownOpen] = useState(false) - const [copied, setCopied] = useState(false) - - // Lora explorer base (TestNet) - const LORA_ACCOUNT_BASE = 'https://lora.algokit.io/testnet/account' - - // Close dropdown when clicking outside - useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - const target = e.target as HTMLElement - if (!target.closest('[data-dropdown]')) { - setIsDropdownOpen(false) - } - } - - document.addEventListener('click', handleClickOutside) - return () => document.removeEventListener('click', handleClickOutside) - }, []) - - // Get address as string safely - const getAddressString = (): string => { - if (!algorandAccount?.address) return '' - return String(algorandAccount.address) - } - - const getLoraAccountUrl = (address: string) => `${LORA_ACCOUNT_BASE}/${address}` - - // Handle login with error feedback - const handleLogin = async () => { - try { - await login() - } catch (err) { - console.error('Login error:', err) - } - } - - // Handle logout with error feedback - const handleLogout = async () => { - try { - await logout() - setIsDropdownOpen(false) - } catch (err) { - console.error('Logout error:', err) - } - } - - // Handle copy address with feedback - const handleCopyAddress = () => { - const address = getAddressString() - if (!address) return - - navigator.clipboard.writeText(address) - setCopied(true) - setTimeout(() => setCopied(false), 2000) - } - - // Show error state - if (error && !isConnected) { - return ( -
- - {error} -
- ) - } - - // Disconnected state: Show Google sign-in button - if (!isConnected) { - return ( - - ) - } - - // Connected state: Show address with dropdown menu - if (algorandAccount && isConnected) { - const address = getAddressString() - const firstLetter = address ? address[0].toUpperCase() : 'A' - const loraUrl = address ? getLoraAccountUrl(address) : '#' - - return ( -
- - - {isDropdownOpen && ( -
    - {/* User Info Header */} - {userInfo && (userInfo.name || userInfo.email) && ( - <> -
  • -
    - {userInfo.profileImage ? ( - Profile - ) : ( -
    - {firstLetter} -
    - )} -
    - {userInfo.name && {userInfo.name}} - {userInfo.email && {userInfo.email}} -
    -
    -
  • -
    - - )} - - {/* Address Section */} -
  • - Algorand Address -
  • - - {/* Clickable address -> Lora */} -
  • - setIsDropdownOpen(false)} - > - {address} - - -
  • - - {/* Copy Address Button */} -
  • - -
  • - - {/* Explicit "View on Lora" CTA (extra clarity) */} -
  • - setIsDropdownOpen(false)} - > - View on Lora - - -
  • - -
    - - {/* Disconnect Button */} -
  • - -
  • -
- )} -
- ) - } - - // Fallback: Loading state - if (isLoading) { - return ( - - ) - } - - return null -} - -export default Web3AuthButton diff --git a/projects/TokenizeRWATemplate-frontend/src/components/Web3AuthProvider.tsx b/projects/TokenizeRWATemplate-frontend/src/components/Web3AuthProvider.tsx deleted file mode 100644 index 9919b76..0000000 --- a/projects/TokenizeRWATemplate-frontend/src/components/Web3AuthProvider.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import { IProvider } from '@web3auth/base' -import { Web3Auth } from '@web3auth/modal' -import { createContext, ReactNode, useContext, useEffect, useState } from 'react' -import { AlgorandAccountFromWeb3Auth, getAlgorandAccount } from '../utils/web3auth/algorandAdapter' -import { getWeb3AuthUserInfo, initWeb3Auth, logoutFromWeb3Auth, Web3AuthUserInfo } from '../utils/web3auth/web3authConfig' - -interface Web3AuthContextType { - isConnected: boolean - isLoading: boolean - isInitialized: boolean - error: string | null - provider: IProvider | null - web3AuthInstance: Web3Auth | null - algorandAccount: AlgorandAccountFromWeb3Auth | null - userInfo: Web3AuthUserInfo | null - /** - * login handles both modal and direct social login. - * Passing arguments bypasses the Web3Auth modal. - */ - login: (adapter?: string, provider?: string) => Promise - logout: () => Promise - refreshUserInfo: () => Promise -} - -const Web3AuthContext = createContext(undefined) - -export function Web3AuthProvider({ children }: { children: ReactNode }) { - const [isInitialized, setIsInitialized] = useState(false) - const [isConnected, setIsConnected] = useState(false) - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(null) - - const [provider, setProvider] = useState(null) - const [web3AuthInstance, setWeb3AuthInstance] = useState(null) - const [algorandAccount, setAlgorandAccount] = useState(null) - const [userInfo, setUserInfo] = useState(null) - - // Initialization logic - useEffect(() => { - const initializeWeb3Auth = async () => { - try { - setIsLoading(true) - setError(null) - - const web3auth = await initWeb3Auth() - setWeb3AuthInstance(web3auth) - - if (web3auth.status === 'connected' && web3auth.provider) { - setProvider(web3auth.provider) - setIsConnected(true) - - try { - const account = await getAlgorandAccount(web3auth.provider) - setAlgorandAccount(account) - } catch (err) { - console.error('🎯 Account derivation error:', err) - setError('Failed to derive Algorand account. Please reconnect.') - } - - try { - const userInformation = await getWeb3AuthUserInfo() - if (userInformation) setUserInfo(userInformation) - } catch (err) { - console.error('🎯 Failed to fetch user info:', err) - } - } - - setIsInitialized(true) - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Failed to initialize Web3Auth' - console.error('🎯 WEB3AUTHPROVIDER: Initialization error:', err) - setError(errorMessage) - setIsInitialized(true) - } finally { - setIsLoading(false) - } - } - - initializeWeb3Auth() - }, []) - - /** - * Unified Login Function - * @param adapter - (Optional) e.g., WALLET_ADAPTERS.AUTH - * @param loginProvider - (Optional) e.g., 'google' - */ - const login = async (adapter?: string, loginProvider?: string) => { - if (!web3AuthInstance) { - setError('Web3Auth not initialized') - return - } - - try { - setIsLoading(true) - setError(null) - - let web3authProvider: IProvider | null - - // Check if we are triggering a specific social login (bypasses modal) - if (adapter && loginProvider) { - web3authProvider = await web3AuthInstance.connectTo(adapter, { - loginProvider: loginProvider, - }) - } else { - // Fallback to showing the default Web3Auth Modal - web3authProvider = await web3AuthInstance.connect() - } - - if (!web3authProvider) { - throw new Error('Failed to connect Web3Auth provider') - } - - setProvider(web3authProvider) - setIsConnected(true) - - // Post-connection: Derive Algorand Address and Fetch Profile - const account = await getAlgorandAccount(web3authProvider) - setAlgorandAccount(account) - - const userInformation = await getWeb3AuthUserInfo() - if (userInformation) setUserInfo(userInformation) - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Login failed' - console.error('🎯 LOGIN: Error:', err) - setError(errorMessage) - setIsConnected(false) - setProvider(null) - setAlgorandAccount(null) - setUserInfo(null) - } finally { - setIsLoading(false) - } - } - - const logout = async () => { - try { - setIsLoading(true) - setError(null) - - await logoutFromWeb3Auth() - - // Clear React state - setProvider(null) - setIsConnected(false) - setAlgorandAccount(null) - setUserInfo(null) - - /** - * ✅ Fix A (most reliable for templates): - * Force a full refresh after logout so Web3Auth doesn't get stuck - * in an in-between cached state (e.g. button stuck on "Connecting..."). - */ - window.location.reload() - } catch (err) { - console.error('🎯 LOGOUT: Error:', err) - setError(err instanceof Error ? err.message : 'Logout failed') - } finally { - setIsLoading(false) - } - } - - const refreshUserInfo = async () => { - try { - const userInformation = await getWeb3AuthUserInfo() - if (userInformation) setUserInfo(userInformation) - } catch (err) { - console.error('🎯 REFRESH: Failed:', err) - } - } - - const value: Web3AuthContextType = { - isConnected, - isLoading, - isInitialized, - error, - provider, - web3AuthInstance, - algorandAccount, - userInfo, - login, - logout, - refreshUserInfo, - } - - return {children} -} - -export function useWeb3Auth(): Web3AuthContextType { - const context = useContext(Web3AuthContext) - if (context === undefined) { - throw new Error('useWeb3Auth must be used within a Web3AuthProvider') - } - return context -} diff --git a/projects/TokenizeRWATemplate-frontend/src/hooks/useUnifiedWallet.ts b/projects/TokenizeRWATemplate-frontend/src/hooks/useUnifiedWallet.ts deleted file mode 100644 index 55075b3..0000000 --- a/projects/TokenizeRWATemplate-frontend/src/hooks/useUnifiedWallet.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useWallet } from '@txnlab/use-wallet-react' -import { useMemo } from 'react' -import { useWeb3Auth } from '../components/Web3AuthProvider' -import { createWeb3AuthSigner } from '../utils/web3auth/web3authIntegration' -import { WALLET_ADAPTERS } from '@web3auth/base' - -export type SocialLoginProvider = 'google' | 'facebook' | 'github' - -export function useUnifiedWallet() { - const { isConnected, algorandAccount, userInfo, login, logout, isLoading } = useWeb3Auth() - const traditional = useWallet() - - return useMemo(() => { - // Determine which source is actually providing an account - const isWeb3AuthActive = isConnected && !!algorandAccount - const isTraditionalActive = !!traditional.activeAddress - - const activeAddress = isWeb3AuthActive ? algorandAccount!.address : traditional.activeAddress || null - - const connectWithSocial = async (provider: SocialLoginProvider) => { - await login(WALLET_ADAPTERS.AUTH, provider) - } - - return { - activeAddress, - isConnected: !!activeAddress, - walletType: isWeb3AuthActive ? 'web3auth' : isTraditionalActive ? 'traditional' : null, - isLoading, - - // Connection Methods - connectSocial: connectWithSocial, - connectGoogle: async () => connectWithSocial('google'), - connectFacebook: async () => connectWithSocial('facebook'), - connectGithub: async () => connectWithSocial('github'), - - disconnect: async () => { - if (isWeb3AuthActive) await logout() - if (isTraditionalActive) await traditional.activeWallet?.disconnect() - }, - - // Metadata - userInfo, - traditionalWallets: traditional.wallets, - signer: isWeb3AuthActive ? createWeb3AuthSigner(algorandAccount) : traditional.transactionSigner, - } - }, [isConnected, algorandAccount, userInfo, traditional, isLoading]) -} diff --git a/projects/TokenizeRWATemplate-frontend/src/hooks/useWeb3AuthHooks.ts b/projects/TokenizeRWATemplate-frontend/src/hooks/useWeb3AuthHooks.ts deleted file mode 100644 index 0ad94d1..0000000 --- a/projects/TokenizeRWATemplate-frontend/src/hooks/useWeb3AuthHooks.ts +++ /dev/null @@ -1,411 +0,0 @@ -/** - * Custom Hooks for Web3Auth Integration - * - * These hooks provide convenient access to Web3Auth functionality - * and combine common patterns. - */ - -import { AlgorandClient } from '@algorandfoundation/algokit-utils' -import algosdk from 'algosdk' -import { useCallback, useEffect, useState } from 'react' -import { useWeb3Auth } from '../components/Web3AuthProvider' -import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs' -import { createWeb3AuthSigner, formatAmount, parseAmount } from '../utils/web3auth/web3authIntegration' - -/** - * Hook to get an initialized AlgorandClient using Web3Auth account - * - * @returns AlgorandClient instance or null if not connected - * - * @example - * ```typescript - * const algorand = useAlgorandClient(); - * - * if (!algorand) return

Not connected

; - * - * const result = await algorand.send.assetCreate({...}); - * ``` - */ -export function useAlgorandClient() { - const { isConnected } = useWeb3Auth() - const [client, setClient] = useState(null) - - useEffect(() => { - if (isConnected) { - const algodConfig = getAlgodConfigFromViteEnvironment() - const algorand = AlgorandClient.fromConfig({ algodConfig }) - setClient(algorand) - } else { - setClient(null) - } - }, [isConnected]) - - return client -} - -/** - * Hook to get an algosdk Algodv2 client using Web3Auth configuration - * - * @returns Algodv2 client instance - * - * @example - * ```typescript - * const algod = useAlgod(); - * const accountInfo = await algod.accountInformation(address).do(); - * ``` - */ -export function useAlgod() { - const algodConfig = getAlgodConfigFromViteEnvironment() - - return new algosdk.Algodv2(algodConfig.token, algodConfig.server, algodConfig.port) -} - -/** - * Hook to get account balance in Algos - * - * @returns { balance: string | null, loading: boolean, error: string | null, refetch: () => Promise } - * - * @example - * ```typescript - * const { balance, loading, error } = useAccountBalance(); - * - * if (loading) return

Loading...

; - * if (error) return

Error: {error}

; - * return

Balance: {balance} ALGO

; - * ``` - */ -export function useAccountBalance() { - const { algorandAccount } = useWeb3Auth() - const algod = useAlgod() - - const [balance, setBalance] = useState(null) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - - const fetchBalance = useCallback(async () => { - if (!algorandAccount?.address) { - setBalance(null) - return - } - - try { - setLoading(true) - setError(null) - - const accountInfo = await algod.accountInformation(algorandAccount.address).do() - const balanceInAlgos = formatAmount(BigInt(accountInfo.amount), 6) - - setBalance(balanceInAlgos) - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Failed to fetch balance' - setError(errorMessage) - setBalance(null) - } finally { - setLoading(false) - } - }, [algorandAccount?.address, algod]) - - // Fetch balance on mount and when account changes - useEffect(() => { - fetchBalance() - }, [fetchBalance]) - - return { - balance, - loading, - error, - refetch: fetchBalance, - } -} - -/** - * Hook to check if account has sufficient balance - * - * @param amount - Amount needed in Algos (string like "1.5") - * @param fee - Transaction fee in Algos (default "0.001") - * @returns { hasSufficientBalance: boolean, balance: string | null, required: string } - * - * @example - * ```typescript - * const { hasSufficientBalance } = useHasSufficientBalance("10"); - * - * if (!hasSufficientBalance) { - * return

Insufficient balance. Need at least 10 ALGO

; - * } - * ``` - */ -export function useHasSufficientBalance(amount: string, fee: string = '0.001') { - const { balance } = useAccountBalance() - - const hasSufficientBalance = (() => { - if (!balance) return false - - try { - const balanceBigInt = parseAmount(balance, 6) - const amountBigInt = parseAmount(amount, 6) - const feeBigInt = parseAmount(fee, 6) - - return balanceBigInt >= amountBigInt + feeBigInt - } catch { - return false - } - })() - - return { - hasSufficientBalance, - balance, - required: `${parseAmount(amount, 6)} (+ ${fee} fee)`, - } -} - -/** - * Hook to sign and submit transactions - * - * @returns { sendTransaction: (txns: Uint8Array[]) => Promise, loading: boolean, error: string | null } - * - * @example - * ```typescript - * const { sendTransaction, loading, error } = useSendTransaction(); - * - * const handleSend = async () => { - * const txnId = await sendTransaction([signedTxn]); - * console.log('Sent:', txnId); - * }; - * ``` - */ -export function useSendTransaction() { - const algod = useAlgod() - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - - const sendTransaction = useCallback( - async (transactions: Uint8Array[]): Promise => { - try { - setLoading(true) - setError(null) - - if (transactions.length === 0) { - throw new Error('No transactions to send') - } - - // Send the first transaction (or could batch if group) - const result = await algod.sendRawTransaction(transactions[0]).do() - - return result.txId - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Failed to send transaction' - setError(errorMessage) - throw err - } finally { - setLoading(false) - } - }, - [algod], - ) - - return { - sendTransaction, - loading, - error, - } -} - -/** - * Hook to wait for transaction confirmation - * - * @param txnId - Transaction ID to wait for - * @param timeout - Timeout in seconds (default: 30) - * @returns { confirmed: boolean, confirmation: any | null, loading: boolean, error: string | null } - * - * @example - * ```typescript - * const { confirmed, confirmation } = useWaitForConfirmation(txnId); - * - * if (confirmed) { - * console.log('Confirmed round:', confirmation['confirmed-round']); - * } - * ``` - */ -export function useWaitForConfirmation(txnId: string | null, timeout: number = 30) { - const algod = useAlgod() - - const [confirmed, setConfirmed] = useState(false) - const [confirmation, setConfirmation] = useState | null>(null) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - - useEffect(() => { - if (!txnId) return - - const waitForConfirmation = async () => { - try { - setLoading(true) - setError(null) - - const confirmation = await algosdk.waitForConfirmation(algod, txnId, timeout) - - setConfirmation(confirmation) - setConfirmed(true) - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Failed to confirm transaction' - setError(errorMessage) - setConfirmed(false) - } finally { - setLoading(false) - } - } - - waitForConfirmation() - }, [txnId, algod, timeout]) - - return { - confirmed, - confirmation, - loading, - error, - } -} - -/** - * Hook for creating and signing assets (ASAs) - * - * @returns { createAsset: (params: AssetCreateParams) => Promise, loading: boolean, error: string | null } - * - * @example - * ```typescript - * const { createAsset, loading } = useCreateAsset(); - * - * const assetId = await createAsset({ - * total: 1000000n, - * decimals: 6, - * assetName: 'My Token', - * unitName: 'MYT', - * }); - * ``` - */ -export function useCreateAsset() { - const { algorandAccount } = useWeb3Auth() - const algorand = useAlgorandClient() - - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - - const createAsset = useCallback( - async (params: { - total: bigint - decimals: number - assetName: string - unitName: string - url?: string - manager?: string - reserve?: string - freeze?: string - clawback?: string - }): Promise => { - if (!algorandAccount || !algorand) { - throw new Error('Not connected to Web3Auth') - } - - try { - setLoading(true) - setError(null) - - const signer = createWeb3AuthSigner(algorandAccount) - - const result = await algorand.send.assetCreate({ - sender: algorandAccount.address, - signer: signer, - ...params, - }) - - const assetId = result.confirmation?.assetIndex - - if (!assetId) { - throw new Error('Failed to get asset ID from confirmation') - } - - return assetId - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Failed to create asset' - setError(errorMessage) - throw err - } finally { - setLoading(false) - } - }, - [algorandAccount, algorand], - ) - - return { - createAsset, - loading, - error, - } -} - -/** - * Hook for transferring assets (ASAs or Algo) - * - * @returns { sendAsset: (params: AssetTransferParams) => Promise, loading: boolean, error: string | null } - * - * @example - * ```typescript - * const { sendAsset, loading } = useSendAsset(); - * - * const txnId = await sendAsset({ - * to: recipientAddress, - * assetId: 123456, - * amount: 100n, - * }); - * ``` - */ -export function useSendAsset() { - const { algorandAccount } = useWeb3Auth() - const algorand = useAlgorandClient() - - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - - const sendAsset = useCallback( - async (params: { to: string; assetId?: number; amount: bigint; closeRemainderTo?: string }): Promise => { - if (!algorandAccount || !algorand) { - throw new Error('Not connected to Web3Auth') - } - - try { - setLoading(true) - setError(null) - - const signer = createWeb3AuthSigner(algorandAccount) - - const result = params.assetId - ? await algorand.send.assetTransfer({ - sender: algorandAccount.address, - signer: signer, - ...params, - }) - : await algorand.send.payment({ - sender: algorandAccount.address, - signer: signer, - receiver: params.to, - amount: params.amount, - }) - - return result.txId - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Failed to send asset' - setError(errorMessage) - throw err - } finally { - setLoading(false) - } - }, - [algorandAccount, algorand], - ) - - return { - sendAsset, - loading, - error, - } -} diff --git a/projects/TokenizeRWATemplate-frontend/src/main.tsx b/projects/TokenizeRWATemplate-frontend/src/main.tsx index adf72ec..7023db3 100644 --- a/projects/TokenizeRWATemplate-frontend/src/main.tsx +++ b/projects/TokenizeRWATemplate-frontend/src/main.tsx @@ -1,8 +1,8 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' -import './styles/main.css' import ErrorBoundary from './components/ErrorBoundary' +import './styles/main.css' ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( diff --git a/projects/TokenizeRWATemplate-frontend/src/utils/web3auth/algorandAdapter.ts b/projects/TokenizeRWATemplate-frontend/src/utils/web3auth/algorandAdapter.ts deleted file mode 100644 index 02edc3b..0000000 --- a/projects/TokenizeRWATemplate-frontend/src/utils/web3auth/algorandAdapter.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { IProvider } from '@web3auth/base' -import algosdk from 'algosdk' - -/** - * Algorand Account derived from Web3Auth provider - * - * Contains the Algorand address, mnemonic, and secret key - * Can be used directly with AlgorandClient for signing transactions - */ -export interface AlgorandAccountFromWeb3Auth { - address: string - mnemonic: string - secretKey: Uint8Array -} - -/** - * Convert Web3Auth provider's private key to Algorand account - * - * Web3Auth returns a private key in hex format from the OpenLogin adapter. - * This function: - * 1. Extracts the private key from the provider - * 2. Converts it to an Algorand mnemonic using algosdk - * 3. Derives the account details from the mnemonic - * 4. Returns address, mnemonic, and secret key for Algorand use - * - * @param provider - Web3Auth IProvider instance - * @returns AlgorandAccountFromWeb3Auth with address, mnemonic, and secretKey - * @throws Error if provider is invalid or key conversion fails - * - * @example - * ```typescript - * const provider = web3authInstance.provider; - * const account = await getAlgorandAccount(provider); - * console.log('Algorand address:', account.address); - * // Use account.secretKey with algosdk to sign transactions - * ``` - */ -export async function getAlgorandAccount(provider: IProvider): Promise { - if (!provider) { - throw new Error('Provider is required to derive Algorand account') - } - - try { - // Get the private key from Web3Auth provider - // The private key is stored as a hex string in the provider's private key storage - const privKey = await provider.request({ - method: 'private_key', - }) - - if (!privKey || typeof privKey !== 'string') { - throw new Error('Failed to retrieve private key from Web3Auth provider') - } - - // Remove '0x' prefix if present - const cleanHexKey = privKey.startsWith('0x') ? privKey.slice(2) : privKey - - // Convert hex string to Uint8Array - const privateKeyBytes = new Uint8Array(Buffer.from(cleanHexKey, 'hex')) - - // Use only the first 32 bytes for Ed25519 key (Web3Auth may provide more) - const ed25519SecretKey = privateKeyBytes.slice(0, 32) - - // Convert Ed25519 private key to Algorand mnemonic - // This creates a standard BIP39/Algorand-compatible mnemonic - const mnemonic = algosdk.secretKeyToMnemonic(ed25519SecretKey) - - // Derive Algorand account from mnemonic - // This gives us the address that corresponds to this key - const accountFromMnemonic = algosdk.mnemonicToSecretKey(mnemonic) - - return { - address: accountFromMnemonic.addr, - mnemonic: mnemonic, - secretKey: accountFromMnemonic.sk, // This is the full 64-byte secret key (32-byte private + 32-byte public) - } - } catch (error) { - if (error instanceof Error) { - throw new Error(`Failed to derive Algorand account from Web3Auth: ${error.message}`) - } - throw error - } -} - -/** - * Create a transaction signer function compatible with AlgorandClient - * - * This function creates a signer that can be used with @algorandfoundation/algokit-utils - * for signing transactions with the Web3Auth-derived Algorand account. - * - * @param secretKey - The Algorand secret key from getAlgorandAccount() - * @returns A signer function that accepts transactions and returns signed transactions - * - * @example - * ```typescript - * const account = await getAlgorandAccount(provider); - * const signer = createAlgorandSigner(account.secretKey); - * - * const result = await algorand.send.assetCreate({ - * sender: account.address, - * signer: signer, - * total: BigInt(1000000), - * decimals: 6, - * assetName: 'My Token', - * unitName: 'MYT', - * }); - * ``` - */ -export function createAlgorandSigner(secretKey: Uint8Array) { - return async (transactions: Uint8Array[]): Promise => { - const signedTxns: Uint8Array[] = [] - - for (const txn of transactions) { - try { - // Sign each transaction with the secret key - const signedTxn = algosdk.signTransaction(txn, secretKey) - signedTxns.push(signedTxn.blob) - } catch (error) { - throw new Error(`Failed to sign transaction: ${error instanceof Error ? error.message : 'Unknown error'}`) - } - } - - return signedTxns - } -} - -/** - * Validate if an Algorand address is valid - * Useful for checking if account derivation succeeded - * - * @param address - The address to validate - * @returns boolean - */ -export function isValidAlgorandAddress(address: string): boolean { - if (!address || typeof address !== 'string') { - return false - } - - try { - algosdk.decodeAddress(address) - return true - } catch { - return false - } -} - -/** - * Get the public key from an Algorand secret key - * - * @param secretKey - The secret key (64 bytes) - * @returns Uint8Array The public key (32 bytes) - */ -export function getPublicKeyFromSecretKey(secretKey: Uint8Array): Uint8Array { - if (secretKey.length !== 64) { - throw new Error(`Invalid secret key length: expected 64 bytes, got ${secretKey.length}`) - } - - // The public key is the second 32 bytes of the secret key in Ed25519 - return secretKey.slice(32) -} diff --git a/projects/TokenizeRWATemplate-frontend/src/utils/web3auth/web3authConfig.ts b/projects/TokenizeRWATemplate-frontend/src/utils/web3auth/web3authConfig.ts deleted file mode 100644 index 38dc99b..0000000 --- a/projects/TokenizeRWATemplate-frontend/src/utils/web3auth/web3authConfig.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { CHAIN_NAMESPACES, IProvider, WEB3AUTH_NETWORK } from '@web3auth/base' -import { CommonPrivateKeyProvider } from '@web3auth/base-provider' -import { Web3Auth } from '@web3auth/modal' - -let web3authInstance: Web3Auth | null = null - -export async function initWeb3Auth(): Promise { - - if (web3authInstance) { - return web3authInstance - } - - const clientId = import.meta.env.VITE_WEB3AUTH_CLIENT_ID - - if (!clientId) { - const error = new Error('VITE_WEB3AUTH_CLIENT_ID is not configured') - console.error('❌ ERROR:', error.message) - throw error - } - - try { - - // Create the private key provider for Algorand - const privateKeyProvider = new CommonPrivateKeyProvider({ - config: { - chainConfig: { - chainNamespace: CHAIN_NAMESPACES.OTHER, - chainId: '0x1', - rpcTarget: 'https://testnet-api.algonode.cloud', - displayName: 'Algorand TestNet', - blockExplorerUrl: 'https://testnet.algoexplorer.io', - ticker: 'ALGO', - tickerName: 'Algorand', - }, - }, - }) - - const web3AuthConfig = { - clientId, - web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_DEVNET, - privateKeyProvider, // ← THIS IS REQUIRED! - uiConfig: { - appName: 'TokenizeRWA', - theme: { - primary: '#000000', - }, - mode: 'light' as const, - loginMethodsOrder: ['google', 'facebook', 'github', 'twitter'], - defaultLanguage: 'en', - }, - } - - web3authInstance = new Web3Auth(web3AuthConfig) - - await web3authInstance.initModal() - - return web3authInstance - } catch (error) { - throw error - } -} - -export function getWeb3AuthInstance(): Web3Auth | null { - return web3authInstance -} - -export function getWeb3AuthProvider(): IProvider | null { - const provider = web3authInstance?.provider || null - return provider -} - -export function isWeb3AuthConnected(): boolean { - const connected = web3authInstance?.status === 'connected' - return connected -} - -export interface Web3AuthUserInfo { - email?: string - name?: string - profileImage?: string - typeOfLogin?: string - [key: string]: unknown -} - -export async function getWeb3AuthUserInfo(): Promise { - if (!web3authInstance || !isWeb3AuthConnected()) { - return null - } - - try { - const userInfo = await web3authInstance.getUserInfo() - return userInfo as Web3AuthUserInfo - } catch (error) { - return null - } -} - -export async function logoutFromWeb3Auth(): Promise { - - if (!web3authInstance) { - return - } - - try { - await web3authInstance.logout() - } catch (error) { - throw error - } -} diff --git a/projects/TokenizeRWATemplate-frontend/src/utils/web3auth/web3authIntegration.ts b/projects/TokenizeRWATemplate-frontend/src/utils/web3auth/web3authIntegration.ts deleted file mode 100644 index 5345b0a..0000000 --- a/projects/TokenizeRWATemplate-frontend/src/utils/web3auth/web3authIntegration.ts +++ /dev/null @@ -1,194 +0,0 @@ -// web3authIntegration.ts -import algosdk, { TransactionSigner } from 'algosdk' -import { AlgorandAccountFromWeb3Auth } from './algorandAdapter' - -/** - * Integration Utilities for Web3Auth with AlgorandClient - * - * IMPORTANT: - * @algorandfoundation/algokit-utils AlgorandClient expects `signer` to be a *function* - * (algosdk.TransactionSigner), NOT an object like { sign: fn }. - * - * If you pass an object, you’ll hit: TypeError: signer is not a function - */ - -/** - * (Legacy) Your old shape (kept only for compatibility if other code uses it). - * AlgorandClient does NOT accept this shape as `signer`. - */ -export interface AlgorandTransactionSigner { - sign: (transactions: Uint8Array[]) => Promise - sender?: string -} - -/** - * ✅ Correct: Convert Web3Auth Algorand account to an AlgorandClient-compatible signer function. - * - * Returns algosdk.TransactionSigner which matches AlgoKit / AlgorandClient expectations: - * (txnGroup, indexesToSign) => Promise - * - * Use like: - * const signer = createWeb3AuthSigner(algorandAccount) - * await algorand.send.assetCreate({ sender: algorandAccount.address, signer, ... }) - */ -export function createWeb3AuthSigner(account: AlgorandAccountFromWeb3Auth): TransactionSigner { - // Web3Auth account should contain a Uint8Array secretKey (Algorand secret key). - // We build an algosdk basic account signer (official helper). - const sk = account.secretKey - const addr = account.address - - // If your secretKey is not a Uint8Array for some reason, try to coerce it. - // (This is defensive; ideally it is already Uint8Array.) - const secretKey: Uint8Array = - sk instanceof Uint8Array - ? sk - : // @ts-expect-error - allow Array fallback - Array.isArray(sk) - ? Uint8Array.from(sk) - : (() => { - throw new Error('Web3Auth secretKey is not a Uint8Array (or number[]). Cannot sign transactions.') - })() - - return algosdk.makeBasicAccountTransactionSigner({ - addr, - sk: secretKey, - }) -} - -/** - * (Optional helper) If you *still* want the old object shape for other code, - * this returns { sign, sender } — but DO NOT pass this as AlgorandClient `signer`. - */ -export function createWeb3AuthSignerObject(account: AlgorandAccountFromWeb3Auth): AlgorandTransactionSigner { - const signerFn = createWeb3AuthSigner(account) - - // Wrap TransactionSigner into "sign(bytes[])" style ONLY if you need it elsewhere - const sign = async (transactions: Uint8Array[]) => { - // These are already bytes; we need Transaction objects for TransactionSigner. - // This wrapper is best-effort and not recommended for AlgoKit usage. - const txns = transactions.map((b) => algosdk.decodeUnsignedTransaction(b)) - const signed = await signerFn(txns, txns.map((_, i) => i)) - return signed - } - - return { - sign, - sender: account.address, - } -} - -/** - * Create a multi-signature compatible signer for Web3Auth accounts - * - * For AlgoKit, you still want the signer to be a TransactionSigner function. - * This returns both the function and some metadata. - */ -export function createWeb3AuthMultiSigSigner(account: AlgorandAccountFromWeb3Auth) { - return { - signer: createWeb3AuthSigner(account), // ✅ TransactionSigner function - sender: account.address, - account, // original account for context - } -} - -/** - * Get account information needed for transaction construction - * - * Returns the public key and address in formats needed for - * transaction construction and verification - */ -export function getWeb3AuthAccountInfo(account: AlgorandAccountFromWeb3Auth) { - const decodedAddress = algosdk.decodeAddress(account.address) - - return { - address: account.address, - publicKeyBytes: decodedAddress.publicKey, - publicKeyBase64: Buffer.from(decodedAddress.publicKey).toString('base64'), - secretKeyHex: Buffer.from(account.secretKey).toString('hex'), - mnemonicPhrase: account.mnemonic, - } -} - -/** - * Verify that a transaction was signed by the Web3Auth account - * - * Useful for verification and testing - */ -export function verifyWeb3AuthSignature(signedTransaction: Uint8Array, account: AlgorandAccountFromWeb3Auth): boolean { - try { - const decodedTxn = algosdk.decodeSignedTransaction(signedTransaction) - - // In algosdk, signature can be represented differently depending on type. - // We’ll attempt to compare signer public key where available. - const txnSigner = decodedTxn.sig?.signers?.[0] ?? decodedTxn.sig?.signer - - if (!txnSigner) return false - - const decodedAddress = algosdk.decodeAddress(account.address) - return Buffer.from(txnSigner).equals(decodedAddress.publicKey) - } catch (error) { - console.error('Error verifying signature:', error) - return false - } -} - -/** - * Get transaction group size details - */ -export function analyzeTransactionGroup(transactions: Uint8Array[]) { - return { - count: transactions.length, - totalSize: transactions.reduce((sum, txn) => sum + txn.length, 0), - averageSize: transactions.reduce((sum, txn) => sum + txn.length, 0) / transactions.length, - } -} - -/** - * Format a transaction amount with proper decimals for display - */ -export function formatAmount(amount: bigint | number, decimals: number = 6): string { - const amountStr = amount.toString() - const decimalPoints = decimals - - if (amountStr.length <= decimalPoints) { - return `0.${amountStr.padStart(decimalPoints, '0')}` - } - - const integerPart = amountStr.slice(0, -decimalPoints) - const decimalPart = amountStr.slice(-decimalPoints) - - return `${integerPart}.${decimalPart}` -} - -/** - * Parse a user-input amount string to base units (reverse of formatAmount) - */ -export function parseAmount(amount: string, decimals: number = 6): bigint { - const trimmed = amount.trim() - - if (!trimmed) { - throw new Error('Amount is required') - } - - if (!/^\d+(\.\d+)?$/.test(trimmed)) { - throw new Error('Invalid amount format') - } - - const [integerPart = '0', decimalPart = ''] = trimmed.split('.') - - if (decimalPart.length > decimals) { - throw new Error(`Too many decimal places (maximum ${decimals})`) - } - - const paddedDecimal = decimalPart.padEnd(decimals, '0') - const combined = integerPart + paddedDecimal - return BigInt(combined) -} - -/** - * Check if Web3Auth account has sufficient balance for a transaction - */ -export function hasSufficientBalance(balance: bigint, requiredAmount: bigint, minFee: bigint = BigInt(1000)): boolean { - const totalRequired = requiredAmount + minFee - return balance >= totalRequired -} diff --git a/projects/TokenizeRWATemplate-frontend/vite.config.ts b/projects/TokenizeRWATemplate-frontend/vite.config.ts index e42dc7c..93d8938 100644 --- a/projects/TokenizeRWATemplate-frontend/vite.config.ts +++ b/projects/TokenizeRWATemplate-frontend/vite.config.ts @@ -7,9 +7,19 @@ export default defineConfig({ plugins: [ react(), nodePolyfills({ - globals: { - Buffer: true, - }, + protocolImports: true, }), ], + build: { + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + optimizeDeps: { + esbuildOptions: { + define: { + global: 'globalThis', + }, + }, + }, })