migrate for unifiedWallet to use-Wallet 4.4.0 for W3A (still UI bugs present)

This commit is contained in:
SaraJane
2026-01-16 20:30:36 +00:00
parent 00c87280f6
commit 3cb2972911
17 changed files with 637 additions and 1638 deletions

View File

@ -9,13 +9,15 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@algorandfoundation/algokit-utils": "^9.0.0", "@algorandfoundation/algokit-utils": "^9.0.0",
"@babel/runtime": "^7.28.6",
"@blockshake/defly-connect": "^1.2.1", "@blockshake/defly-connect": "^1.2.1",
"@perawallet/connect": "^1.4.1", "@perawallet/connect": "^1.4.1",
"@txnlab/use-wallet": "^4.0.0", "@txnlab/use-wallet": "^4.4.0",
"@txnlab/use-wallet-react": "^4.0.0", "@txnlab/use-wallet-react": "^4.4.0",
"@web3auth/base": "^9.7.0", "@web3auth/base": "^9.7.0",
"@web3auth/base-provider": "^9.7.0", "@web3auth/base-provider": "^9.7.0",
"@web3auth/modal": "^9.7.0", "@web3auth/modal": "^9.7.0",
"@web3auth/single-factor-auth": "^9.5.0",
"algosdk": "^3.0.0", "algosdk": "^3.0.0",
"daisyui": "^4.0.0", "daisyui": "^4.0.0",
"idb-keyval": "^6.2.2", "idb-keyval": "^6.2.2",
@ -25,7 +27,8 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router-dom": "^7.11.0", "react-router-dom": "^7.11.0",
"tslib": "^2.6.2" "tslib": "^2.6.2",
"tweetnacl": "^1.0.3"
}, },
"devDependencies": { "devDependencies": {
"@algorandfoundation/algokit-client-generator": "^5.0.0", "@algorandfoundation/algokit-client-generator": "^5.0.0",
@ -38,11 +41,13 @@
"@typescript-eslint/parser": "^7.0.2", "@typescript-eslint/parser": "^7.0.2",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"buffer": "^6.0.3",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"playwright": "^1.35.0", "playwright": "^1.35.0",
"postcss": "^8.4.24", "postcss": "^8.4.24",
"process": "^0.11.10",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.2",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
@ -618,9 +623,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.28.4", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -3144,13 +3149,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@tanstack/react-store": { "node_modules/@tanstack/react-store": {
"version": "0.7.3", "version": "0.8.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.3.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.8.0.tgz",
"integrity": "sha512-3Dnqtbw9P2P0gw8uUM8WP2fFfg8XMDSZCTsywRPZe/XqqYW8PGkXKZTvP0AHkE4mpqP9Y43GpOg9vwO44azu6Q==", "integrity": "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/store": "0.7.2", "@tanstack/store": "0.8.0",
"use-sync-external-store": "^1.5.0" "use-sync-external-store": "^1.6.0"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@ -3162,9 +3167,9 @@
} }
}, },
"node_modules/@tanstack/store": { "node_modules/@tanstack/store": {
"version": "0.7.2", "version": "0.8.0",
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.2.tgz", "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.8.0.tgz",
"integrity": "sha512-RP80Z30BYiPX2Pyo0Nyw4s1SJFH2jyM6f9i3HfX4pA+gm5jsnYryscdq2aIQLnL4TaGuQMO+zXmN9nh1Qck+Pg==", "integrity": "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
@ -3214,6 +3219,19 @@
"npm": ">=9.x" "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": { "node_modules/@toruslabs/constants": {
"version": "14.2.0", "version": "14.2.0",
"resolved": "https://registry.npmjs.org/@toruslabs/constants/-/constants-14.2.0.tgz", "resolved": "https://registry.npmjs.org/@toruslabs/constants/-/constants-14.2.0.tgz",
@ -3250,6 +3268,22 @@
"npm": ">=9.x" "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": { "node_modules/@toruslabs/http-helpers": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@toruslabs/http-helpers/-/http-helpers-7.0.1.tgz", "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==", "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==",
"license": "MIT" "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": { "node_modules/@toruslabs/tweetnacl-js": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@toruslabs/tweetnacl-js/-/tweetnacl-js-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@toruslabs/tweetnacl-js/-/tweetnacl-js-1.0.4.tgz",
@ -3396,21 +3460,25 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@txnlab/use-wallet": { "node_modules/@txnlab/use-wallet": {
"version": "4.3.1", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/@txnlab/use-wallet/-/use-wallet-4.3.1.tgz", "resolved": "https://registry.npmjs.org/@txnlab/use-wallet/-/use-wallet-4.4.0.tgz",
"integrity": "sha512-kWDOauxROjwmnOmivp5iBAXqdksYHDXaP7P/AbX/uawnv+H+WQiP0dBUKnVLb//eOUbhq7QT33yUGGXC6QATgQ==", "integrity": "sha512-FMapmviqrLbKk80NsSM2hcChZl2W3fE52hTQ7+6LFfNiLbCQFH//mBZGipV5/bhyIx0pTgxcXMKygRprz6S9Fw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/store": "0.7.2" "@tanstack/store": "0.8.0"
}, },
"peerDependencies": { "peerDependencies": {
"@agoralabs-sh/avm-web-provider": "^1.7.0", "@agoralabs-sh/avm-web-provider": "^1.7.0",
"@blockshake/defly-connect": "^1.2.1", "@blockshake/defly-connect": "^1.2.1",
"@perawallet/connect": "^1.4.1", "@perawallet/connect": "^1.4.1",
"@walletconnect/modal": "^2.7.0", "@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", "algosdk": "^3.0.0",
"lute-connect": "^1.6.2" "lute-connect": "^1.6.3"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@agoralabs-sh/avm-web-provider": { "@agoralabs-sh/avm-web-provider": {
@ -3428,28 +3496,40 @@
"@walletconnect/sign-client": { "@walletconnect/sign-client": {
"optional": true "optional": true
}, },
"@web3auth/base": {
"optional": true
},
"@web3auth/base-provider": {
"optional": true
},
"@web3auth/modal": {
"optional": true
},
"@web3auth/single-factor-auth": {
"optional": true
},
"lute-connect": { "lute-connect": {
"optional": true "optional": true
} }
} }
}, },
"node_modules/@txnlab/use-wallet-react": { "node_modules/@txnlab/use-wallet-react": {
"version": "4.3.1", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/@txnlab/use-wallet-react/-/use-wallet-react-4.3.1.tgz", "resolved": "https://registry.npmjs.org/@txnlab/use-wallet-react/-/use-wallet-react-4.4.0.tgz",
"integrity": "sha512-Gmj1qBNykx2HbFS4FzzqCa8uWI4UkdYw2mk6a+62AVoKU2/hvPe9K6DvdDrc883dJoRSK0xPtSGjJ6lKxT1bCQ==", "integrity": "sha512-IQX0cUB8bybnGluhN46t8kCEtBof9yxB1aK6ru4DeysUuTn9pkWAF5/bTDK15+lJTs/88+C4v26CUGvbiNAltQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/react-store": "0.7.3", "@tanstack/react-store": "0.8.0",
"@txnlab/use-wallet": "4.3.1" "@txnlab/use-wallet": "4.4.0"
}, },
"peerDependencies": { "peerDependencies": {
"@blockshake/defly-connect": "^1.2.1", "@blockshake/defly-connect": "^1.2.1",
"@magic-ext/algorand": "^24.4.2", "@magic-ext/algorand": "^24.4.2",
"@perawallet/connect": "^1.4.1", "@perawallet/connect": "^1.4.1",
"@walletconnect/modal": "^2.7.0", "@walletconnect/modal": "^2.7.0",
"@walletconnect/sign-client": "^2.21.8", "@walletconnect/sign-client": "^2.23.1",
"algosdk": "^3.0.0", "algosdk": "^3.0.0",
"lute-connect": "^1.6.2", "lute-connect": "^1.6.3",
"magic-sdk": "^29.4.2", "magic-sdk": "^29.4.2",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^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": { "node_modules/@web3auth/ui": {
"version": "9.7.0", "version": "9.7.0",
"resolved": "https://registry.npmjs.org/@web3auth/ui/-/ui-9.7.0.tgz", "resolved": "https://registry.npmjs.org/@web3auth/ui/-/ui-9.7.0.tgz",
@ -5633,6 +5763,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -5956,6 +6092,15 @@
"node": ">= 6" "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": { "node_modules/bser": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",

View File

@ -22,11 +22,13 @@
"@typescript-eslint/parser": "^7.0.2", "@typescript-eslint/parser": "^7.0.2",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"buffer": "^6.0.3",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"playwright": "^1.35.0", "playwright": "^1.35.0",
"postcss": "^8.4.24", "postcss": "^8.4.24",
"process": "^0.11.10",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.2",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
@ -36,13 +38,15 @@
}, },
"dependencies": { "dependencies": {
"@algorandfoundation/algokit-utils": "^9.0.0", "@algorandfoundation/algokit-utils": "^9.0.0",
"@babel/runtime": "^7.28.6",
"@blockshake/defly-connect": "^1.2.1", "@blockshake/defly-connect": "^1.2.1",
"@perawallet/connect": "^1.4.1", "@perawallet/connect": "^1.4.1",
"@txnlab/use-wallet": "^4.0.0", "@txnlab/use-wallet": "^4.4.0",
"@txnlab/use-wallet-react": "^4.0.0", "@txnlab/use-wallet-react": "^4.4.0",
"@web3auth/base": "^9.7.0", "@web3auth/base": "^9.7.0",
"@web3auth/base-provider": "^9.7.0", "@web3auth/base-provider": "^9.7.0",
"@web3auth/modal": "^9.7.0", "@web3auth/modal": "^9.7.0",
"@web3auth/single-factor-auth": "^9.5.0",
"algosdk": "^3.0.0", "algosdk": "^3.0.0",
"daisyui": "^4.0.0", "daisyui": "^4.0.0",
"idb-keyval": "^6.2.2", "idb-keyval": "^6.2.2",
@ -52,7 +56,8 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router-dom": "^7.11.0", "react-router-dom": "^7.11.0",
"tslib": "^2.6.2" "tslib": "^2.6.2",
"tweetnacl": "^1.0.3"
}, },
"scripts": { "scripts": {
"generate:app-clients": "algokit project link --all", "generate:app-clients": "algokit project link --all",

View File

@ -1,18 +1,23 @@
import { SupportedWallet, WalletId, WalletManager, WalletProvider } from '@txnlab/use-wallet-react' import { SupportedWallet, WalletId, WalletManager, WalletProvider } from '@txnlab/use-wallet-react'
import { SnackbarProvider } from 'notistack' import { SnackbarProvider } from 'notistack'
import { useMemo } from 'react'
import { BrowserRouter, Route, Routes } from 'react-router-dom' import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { Web3AuthProvider } from './components/Web3AuthProvider'
import Home from './Home' import Home from './Home'
import Layout from './Layout' import Layout from './Layout'
import TokenizePage from './TokenizePage' import TokenizePage from './TokenizePage'
import { getAlgodConfigFromViteEnvironment, getKmdConfigFromViteEnvironment } from './utils/network/getAlgoClientConfigs' import { getAlgodConfigFromViteEnvironment, getKmdConfigFromViteEnvironment } from './utils/network/getAlgoClientConfigs'
// Configure supported wallets based on network environment // Get Web3Auth client ID from environment
let supportedWallets: SupportedWallet[] const web3AuthClientId = (import.meta.env.VITE_WEB3AUTH_CLIENT_ID ?? '').trim()
/**
* 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') { if (import.meta.env.VITE_ALGOD_NETWORK === 'localnet') {
// LocalNet: KMD wallet for local development
const kmdConfig = getKmdConfigFromViteEnvironment() const kmdConfig = getKmdConfigFromViteEnvironment()
supportedWallets = [ return [
{ {
id: WalletId.KMD, id: WalletId.KMD,
options: { options: {
@ -23,19 +28,41 @@ if (import.meta.env.VITE_ALGOD_NETWORK === 'localnet') {
}, },
{ id: WalletId.LUTE }, { 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 }]
} }
/** // TestNet/MainNet wallets
* Main App Component const wallets: SupportedWallet[] = [{ id: WalletId.PERA }, { id: WalletId.DEFLY }, { id: WalletId.LUTE }]
* Sets up wallet provider and routing for the Tokenization dApp
*/ // 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() { export default function App() {
const algodConfig = getAlgodConfigFromViteEnvironment() const algodConfig = getAlgodConfigFromViteEnvironment()
const walletManager = new WalletManager({ const supportedWallets = useMemo(() => buildSupportedWallets(), [])
const walletManager = useMemo(() => {
const mgr = new WalletManager({
wallets: supportedWallets, wallets: supportedWallets,
defaultNetwork: algodConfig.network, defaultNetwork: algodConfig.network,
networks: { networks: {
@ -52,9 +79,11 @@ export default function App() {
}, },
}) })
return mgr
}, [algodConfig.network, algodConfig.server, algodConfig.port, algodConfig.token, supportedWallets])
return ( return (
<SnackbarProvider maxSnack={3}> <SnackbarProvider maxSnack={3}>
<Web3AuthProvider>
<WalletProvider manager={walletManager}> <WalletProvider manager={walletManager}>
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
@ -65,7 +94,6 @@ export default function App() {
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</WalletProvider> </WalletProvider>
</Web3AuthProvider>
</SnackbarProvider> </SnackbarProvider>
) )
} }

View File

@ -1,4 +1,4 @@
import { useUnifiedWallet } from './hooks/useUnifiedWallet' import { useWallet } from '@txnlab/use-wallet-react'
import { Link } from 'react-router-dom' 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 * Displays features, how it works, and CTAs to connect wallet and create assets
*/ */
export default function Home() { export default function Home() {
const { activeAddress } = useUnifiedWallet() const { activeAddress } = useWallet()
return ( return (
<div className="bg-white dark:bg-slate-950"> <div className="bg-white dark:bg-slate-950">

View File

@ -1,18 +1,22 @@
import { useWallet } from '@txnlab/use-wallet-react'
import { useState } from 'react' import { useState } from 'react'
import { NavLink, Outlet } from 'react-router-dom' import { NavLink, Outlet } from 'react-router-dom'
import ConnectWallet from './components/ConnectWallet' import ConnectWallet from './components/ConnectWallet'
import ThemeToggle from './components/ThemeToggle' import ThemeToggle from './components/ThemeToggle'
import { useUnifiedWallet } from './hooks/useUnifiedWallet' import { ellipseAddress } from './utils/ellipseAddress'
export default function Layout() { export default function Layout() {
const [openWalletModal, setOpenWalletModal] = useState(false) const [openWalletModal, setOpenWalletModal] = useState(false)
const { isConnected, activeAddress, userInfo } = useUnifiedWallet() const { activeAddress, isActive } = useWallet()
const toggleWalletModal = () => setOpenWalletModal(!openWalletModal) 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" // Helper to format address: "ZBC...WXYZ"
const displayAddress = const displayAddress = isConnected && activeAddress ? ellipseAddress(activeAddress, 4) : 'Sign in'
isConnected && activeAddress ? `${activeAddress.toString().slice(0, 4)}...${activeAddress.toString().slice(-4)}` : 'Sign in'
return ( return (
<div className="min-h-screen flex flex-col bg-white dark:bg-slate-950 text-slate-900 dark:text-slate-100"> <div className="min-h-screen flex flex-col bg-white dark:bg-slate-950 text-slate-900 dark:text-slate-100">
@ -41,7 +45,7 @@ export default function Layout() {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<ThemeToggle /> <ThemeToggle />
{/* ONE Button to Rule Them All */} {/* Sign In / Account Button */}
<button <button
onClick={toggleWalletModal} onClick={toggleWalletModal}
className={`flex items-center gap-2 px-5 py-2 rounded-xl font-bold text-sm transition shadow-sm border ${ className={`flex items-center gap-2 px-5 py-2 rounded-xl font-bold text-sm transition shadow-sm border ${
@ -61,7 +65,7 @@ export default function Layout() {
<Outlet /> <Outlet />
</main> </main>
{/* Footer (Simplified) */} {/* Footer */}
<footer className="bg-slate-900 text-slate-400 py-12 px-6 border-t border-slate-800"> <footer className="bg-slate-900 text-slate-400 py-12 px-6 border-t border-slate-800">
<div className="max-w-7xl mx-auto grid gap-8 md:grid-cols-3"> <div className="max-w-7xl mx-auto grid gap-8 md:grid-cols-3">
<div> <div>
@ -78,7 +82,7 @@ export default function Layout() {
</div> </div>
</footer> </footer>
{/* The Unified Modal */} {/* Wallet Modal */}
<ConnectWallet openModal={openWalletModal} closeModal={toggleWalletModal} /> <ConnectWallet openModal={openWalletModal} closeModal={toggleWalletModal} />
</div> </div>
) )

View File

@ -1,5 +1,5 @@
import { useWallet } from '@txnlab/use-wallet-react'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useUnifiedWallet } from '../hooks/useUnifiedWallet'
import { ellipseAddress } from '../utils/ellipseAddress' import { ellipseAddress } from '../utils/ellipseAddress'
import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs' import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs'
@ -10,13 +10,13 @@ import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClien
* and current network. * and current network.
* *
* Works for BOTH: * Works for BOTH:
* - Web3Auth (Google login) * - Web3Auth (Google login via use-wallet)
* - Traditional wallets (Pera / Defly / etc) * - Traditional wallets (Pera / Defly / etc)
* *
* Address links to Lora explorer. * Address links to Lora explorer.
*/ */
const Account = () => { const Account = () => {
const { activeAddress } = useUnifiedWallet() const { activeAddress } = useWallet()
const algoConfig = getAlgodConfigFromViteEnvironment() const algoConfig = getAlgodConfigFromViteEnvironment()
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
@ -25,7 +25,7 @@ const Account = () => {
return algoConfig.network === '' ? 'localnet' : algoConfig.network.toLowerCase() return algoConfig.network === '' ? 'localnet' : algoConfig.network.toLowerCase()
}, [algoConfig.network]) }, [algoConfig.network])
// Normalize address to string (VERY IMPORTANT) // Normalize address to string safely
const address = typeof activeAddress === 'string' ? activeAddress : activeAddress ? String(activeAddress) : null const address = typeof activeAddress === 'string' ? activeAddress : activeAddress ? String(activeAddress) : null
if (!address) { if (!address) {
@ -40,7 +40,6 @@ const Account = () => {
setCopied(true) setCopied(true)
setTimeout(() => setCopied(false), 1500) setTimeout(() => setCopied(false), 1500)
} catch (e) { } catch (e) {
console.error('Failed to copy address', e)
} }
} }

View File

@ -1,163 +1,350 @@
import { WalletId } from '@txnlab/use-wallet-react' import { useWallet, WalletId, type BaseWallet } from '@txnlab/use-wallet-react'
import { useEffect, useRef, useState } from 'react' import { useMemo, useState } from 'react'
import { SocialLoginProvider, useUnifiedWallet } from '../hooks/useUnifiedWallet' import { ellipseAddress } from '../utils/ellipseAddress'
import Account from './Account' import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs'
interface ConnectWalletInterface { interface ConnectWalletProps {
openModal: boolean openModal: boolean
closeModal: () => void closeModal: () => void
} }
const ConnectWallet = ({ openModal, closeModal }: ConnectWalletInterface) => { const ConnectWallet = ({ openModal, closeModal }: ConnectWalletProps) => {
const { isConnected, walletType, userInfo, traditionalWallets, connectGoogle, connectFacebook, connectGithub, disconnect } = const { wallets, activeWallet, activeAddress, isReady, isActive, disconnect } = useWallet()
useUnifiedWallet()
const [connectingProvider, setConnectingProvider] = useState<SocialLoginProvider | null>(null) const [connectingId, setConnectingId] = useState<string | null>(null)
const dialogRef = useRef<HTMLDialogElement>(null) const [lastError, setLastError] = useState<string>('')
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<void>) => {
try { try {
setConnectingProvider(provider) if (typeof wallet.connect !== 'function') {
await connectFn() 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() closeModal()
} catch (error) { return
throw new Error(`Failed to connect with ${provider}: ${error}`) } catch (directConnectError) {
// Fall through to try wallet.connect()
}
}
} }
} }
const socialOptions: { id: SocialLoginProvider; label: string; icon: string; action: () => Promise<void> }[] = [ // Call connect - this should open the Web3Auth modal
{ const connectPromise = wallet.connect()
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) => { // Add timeout for Web3Auth to detect if it hangs
const normalized = provider?.toLowerCase() if (wallet.id === WalletId.WEB3AUTH) {
switch (normalized) { const timeoutPromise = new Promise((_, reject) => {
case 'google': setTimeout(() => {
return 'Google' reject(
case 'facebook': new Error(
return 'Facebook' 'Web3Auth connection timed out after 30 seconds. The modal may not have opened. Check browser console for Web3Auth initialization errors.',
case 'github': ),
return 'GitHub' )
default: }, 30000)
return 'Social Login' })
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 (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)
} }
} }
useEffect(() => { // Handle disconnect
const dialog = dialogRef.current const handleDisconnect = async () => {
if (!dialog) return setLastError('')
openModal ? dialog.showModal() : dialog.close() try {
}, [openModal]) // 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()
}
}
// 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 ( return (
<dialog <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" onClick={closeModal}>
ref={dialogRef} <div
className="fixed inset-0 w-full max-w-md mx-auto my-auto rounded-3xl bg-white dark:bg-slate-900 shadow-2xl border-none p-0 backdrop:bg-gray-900/50 backdrop:backdrop-blur-sm" className="w-full max-w-md rounded-3xl bg-white dark:bg-slate-900 shadow-2xl border border-slate-200 dark:border-slate-700 p-6"
onClick={(e) => e.target === dialogRef.current && closeModal()} onClick={(e) => e.stopPropagation()}
> >
<div className="p-8"> {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h3 className="text-2xl font-bold text-gray-900 dark:text-white">{isConnected ? 'Account' : 'Sign in'}</h3> <h3 className="text-xl font-bold text-slate-900 dark:text-white">{connected ? 'Account' : 'Sign in'}</h3>
<button onClick={closeModal} className="p-2 hover:bg-gray-100 dark:hover:bg-slate-800 rounded-full transition"> <button onClick={closeModal} className="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-full transition">
<span className="text-xl text-gray-500"></span> <span className="text-xl text-slate-500"></span>
</button> </button>
</div> </div>
<div className="space-y-6"> {/* Error display */}
{isConnected ? ( {lastError && (
/* --- CONNECTED STATE --- */ <div className="mb-4 rounded-xl border border-red-200 bg-red-50 dark:bg-red-900/20 dark:border-red-800 p-3 text-sm text-red-700 dark:text-red-400">
<div className="space-y-4"> {lastError}
<div className="bg-slate-50 dark:bg-slate-800 rounded-2xl p-4 border border-slate-100 dark:border-slate-700"> </div>
<Account /> )}
{walletType === 'web3auth' && userInfo && ( {/* Connected state */}
<div className="mt-3 pt-3 border-t border-slate-200 dark:border-slate-700 flex items-center gap-3"> {connected ? (
{userInfo.profileImage && ( <div className="space-y-4">
<img src={userInfo.profileImage} alt="Profile" className="w-8 h-8 rounded-full border border-white" /> <div className="rounded-2xl border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50 p-4">
)} <div className="flex items-center justify-between mb-3">
<div className="overflow-hidden"> <div className="text-xs uppercase tracking-wider text-slate-500 dark:text-slate-400 font-bold">Connected</div>
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-bold"> <div className="flex items-center gap-2">
Connected via {formatSocialProvider(userInfo.typeOfLogin)} <span className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse" />
</p> <span className="text-xs text-emerald-600 dark:text-emerald-400 font-semibold">Active</span>
<p className="text-sm font-medium dark:text-slate-200 truncate">{userInfo.email || userInfo.name}</p>
</div> </div>
</div> </div>
{/* Address with copy button */}
<div className="flex items-center gap-2 mb-3">
<div className="font-mono text-sm break-all text-slate-900 dark:text-white flex-1">
{ellipseAddress(activeAddress || '', 8)}
</div>
<button
onClick={async () => {
if (activeAddress) {
try {
await navigator.clipboard.writeText(activeAddress)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (e) {
// Silently handle copy errors
}
}
}}
className="px-3 py-1.5 text-xs font-semibold rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-600 transition"
title={copied ? 'Copied!' : 'Copy address'}
>
{copied ? '✓ Copied' : 'Copy'}
</button>
</div>
{/* Full address (collapsible) */}
<details className="mb-3">
<summary className="text-xs text-slate-500 dark:text-slate-400 cursor-pointer hover:text-slate-700 dark:hover:text-slate-300">
Show full address
</summary>
<div className="mt-2 font-mono text-xs break-all text-slate-600 dark:text-slate-400 p-2 bg-slate-100 dark:bg-slate-900 rounded">
{activeAddress}
</div>
</details>
{/* Wallet type and Lora link */}
<div className="flex items-center justify-between pt-3 border-t border-slate-200 dark:border-slate-700">
<div className="text-sm text-slate-700 dark:text-slate-200">
Wallet: <span className="font-semibold">{activeWallet?.metadata?.name ?? activeWallet?.id}</span>
</div>
{activeAddress && (
<a
href={`https://lora.algokit.io/${networkName}/account/${activeAddress}/`}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-semibold text-teal-600 dark:text-teal-400 hover:text-teal-700 dark:hover:text-teal-300 transition flex items-center gap-1"
>
View on Lora
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
)} )}
</div> </div>
</div>
<button <button
onClick={disconnect} // Use the unified disconnect method onClick={handleDisconnect}
className="w-full py-3 bg-red-50 text-red-600 hover:bg-red-100 dark:bg-red-900/20 dark:text-red-400 font-semibold rounded-xl transition" className="w-full py-3 rounded-xl font-semibold bg-red-50 text-red-600 hover:bg-red-100 dark:bg-red-900/20 dark:text-red-400 dark:hover:bg-red-900/30 transition"
> >
Disconnect Disconnect
</button> </button>
</div> </div>
) : ( ) : (
/* --- DISCONNECTED STATE --- */ <div className="space-y-4">
<> {/* Google Sign In (Web3Auth) */}
{web3AuthWallet && (
<div className="space-y-3"> <div className="space-y-3">
<p className="text-xs font-bold uppercase tracking-wider text-gray-400 px-1">Social Login</p>
{socialOptions.map((option) => (
<button <button
key={option.id} onClick={() => handleConnect(web3AuthWallet)}
onClick={() => handleSocialLogin(option.id, option.action)} disabled={!!connectingId}
disabled={!!connectingProvider} className="w-full flex items-center justify-center gap-3 p-4 rounded-xl bg-white dark:bg-slate-800 border-2 border-slate-200 dark:border-slate-600 hover:border-blue-500 dark:hover:border-blue-500 hover:bg-blue-50/50 dark:hover:bg-blue-900/20 transition disabled:opacity-60 shadow-sm"
className="w-full flex items-center justify-center gap-3 px-4 py-3.5 rounded-xl border border-gray-200 dark:border-slate-700 hover:bg-gray-50 dark:hover:bg-slate-800 transition shadow-sm font-medium text-gray-700 dark:text-slate-200 disabled:opacity-50"
> >
<img src={option.icon} className="w-5 h-5" alt={option.label} /> {/* Google Icon */}
{connectingProvider === option.id ? 'Connecting...' : option.label} <svg className="w-5 h-5" viewBox="0 0 24 24">
</button> <path
))} fill="#4285F4"
</div> d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<div className="relative"> <path
<div className="absolute inset-0 flex items-center"> fill="#34A853"
<div className="w-full border-t border-gray-100 dark:border-slate-800"></div> d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
</div> />
<div className="relative flex justify-center text-xs uppercase"> <path
<span className="bg-white dark:bg-slate-900 px-2 text-gray-400">Or use a wallet</span> fill="#FBBC05"
</div> d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
</div> />
<path
<div className="grid grid-cols-1 gap-3"> fill="#EA4335"
{traditionalWallets?.map((wallet) => ( d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
<button />
key={wallet.id} </svg>
className="flex items-center gap-4 p-4 rounded-xl border border-gray-100 dark:border-slate-800 hover:border-indigo-500 dark:hover:border-indigo-500 hover:bg-indigo-50/30 transition group" <span className="font-semibold text-slate-700 dark:text-slate-200">
onClick={() => { {connectingId === web3AuthWallet.id ? 'Connecting…' : 'Continue with Google'}
closeModal()
wallet.connect()
}}
>
<img src={wallet.metadata.icon} alt={wallet.id} className="w-10 h-10 rounded-lg group-hover:scale-110 transition" />
<span className="font-semibold text-gray-800 dark:text-slate-200">
{wallet.id === WalletId.KMD ? 'LocalNet' : wallet.metadata.name}
</span> </span>
</button> </button>
</div>
)}
{/* Divider */}
{web3AuthWallet && traditionalWallets.length > 0 && (
<div className="flex items-center gap-3 my-4">
<div className="flex-1 h-px bg-slate-200 dark:bg-slate-700" />
<span className="text-xs text-slate-400 dark:text-slate-500 uppercase tracking-wider">or</span>
<div className="flex-1 h-px bg-slate-200 dark:bg-slate-700" />
</div>
)}
{/* Traditional Wallets */}
{traditionalWallets.length > 0 && (
<div className="space-y-2">
<div className="text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 px-1">Algorand Wallets</div>
{traditionalWallets.map((w) => (
<button
key={w.id}
onClick={() => handleConnect(w)}
disabled={!!connectingId}
className="w-full flex items-center justify-between gap-4 p-3 rounded-xl border border-slate-200 dark:border-slate-700 hover:border-teal-500 hover:bg-teal-50/30 dark:hover:border-teal-500 dark:hover:bg-teal-900/10 transition disabled:opacity-60"
>
<div className="flex items-center gap-3">
{w.metadata?.icon ? (
<img src={w.metadata.icon} alt={w.id} className="w-8 h-8 rounded-lg" />
) : (
<div className="w-8 h-8 rounded-lg bg-slate-200 dark:bg-slate-700" />
)}
<span className="font-semibold text-slate-900 dark:text-white">{w.metadata?.name ?? w.id}</span>
</div>
<span className="text-sm text-slate-500 dark:text-slate-400">{connectingId === w.id ? 'Connecting…' : 'Connect'}</span>
</button>
))} ))}
</div> </div>
</> )}
{/* Warning if Web3Auth didn't register */}
{!web3AuthWallet && (
<div className="mt-4 p-3 rounded-xl bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800">
<p className="text-xs text-amber-700 dark:text-amber-400">
Google sign-in is not available. Check console for Web3Auth errors.
</p>
</div>
)}
</div>
)} )}
</div> </div>
</div> </div>
</dialog>
) )
} }

View File

@ -4,8 +4,8 @@ import { useSnackbar } from 'notistack'
import { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { AiOutlineCloudUpload, AiOutlineInfoCircle, AiOutlineLoading3Quarters } from 'react-icons/ai' import { AiOutlineCloudUpload, AiOutlineInfoCircle, AiOutlineLoading3Quarters } from 'react-icons/ai'
import { BsCoin } from 'react-icons/bs' import { BsCoin } from 'react-icons/bs'
import { useUnifiedWallet } from '../hooks/useUnifiedWallet'
import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs' import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs'
import { useWallet } from '@txnlab/use-wallet-react'
/** /**
* Type for created assets stored in browser localStorage * Type for created assets stored in browser localStorage
@ -175,8 +175,12 @@ export default function TokenizeAsset() {
const [nftFreeze, setNftFreeze] = useState<string>('') const [nftFreeze, setNftFreeze] = useState<string>('')
const [nftClawback, setNftClawback] = useState<string>('') const [nftClawback, setNftClawback] = useState<string>('')
// ===== Unified wallet (Web3Auth OR WalletConnect) ===== // ===== use-wallet (Web3Auth OR WalletConnect) =====
const { signer, activeAddress } = useUnifiedWallet() // 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 ===== // ===== Notifications =====
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
@ -283,7 +287,6 @@ export default function TokenizeAsset() {
setHasCheckedUsdcOnChain(true) setHasCheckedUsdcOnChain(true)
hasCheckedUsdcOnChainRef.current = true hasCheckedUsdcOnChainRef.current = true
} catch (e) { } catch (e) {
console.error('Failed to check USDC opt-in', e)
// On error, set to not-opted-in but don't mark as checked // On error, set to not-opted-in but don't mark as checked
// This allows retry on next render cycle // This allows retry on next render cycle
setUsdcStatus('not-opted-in') setUsdcStatus('not-opted-in')
@ -385,11 +388,18 @@ export default function TokenizeAsset() {
* Opt-in is an asset transfer of 0 USDC to self * Opt-in is an asset transfer of 0 USDC to self
*/ */
const handleOptInUsdc = async () => { 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' }) enqueueSnackbar('Please connect a wallet or continue with Google first.', { variant: 'warning' })
return return
} }
if (!signer) {
enqueueSnackbar('Wallet signer not available. Please try reconnecting your wallet.', { variant: 'error' })
return
}
// Prevent duplicate transactions if already opted in // Prevent duplicate transactions if already opted in
if (usdcOptedIn) { if (usdcOptedIn) {
enqueueSnackbar('You are already opted in to USDC ✅', { variant: 'info' }) enqueueSnackbar('You are already opted in to USDC ✅', { variant: 'info' })
@ -438,7 +448,6 @@ export default function TokenizeAsset() {
checkUsdcOptInStatus() checkUsdcOptInStatus()
}, 2000) }, 2000)
} catch (e) { } catch (e) {
console.error('USDC opt-in failed', e)
enqueueSnackbar('USDC opt-in failed.', { variant: 'error' }) enqueueSnackbar('USDC opt-in failed.', { variant: 'error' })
} finally { } finally {
setUsdcOptInLoading(false) setUsdcOptInLoading(false)
@ -515,11 +524,17 @@ export default function TokenizeAsset() {
* Adjusts total supply by decimals and saves asset to localStorage * Adjusts total supply by decimals and saves asset to localStorage
*/ */
const handleTokenize = async () => { 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' }) enqueueSnackbar('Please connect a wallet or continue with Google first.', { variant: 'warning' })
return return
} }
if (!signer) {
enqueueSnackbar('Wallet signer not available. Please try reconnecting your wallet.', { variant: 'error' })
return
}
if (!assetName || !unitName) { if (!assetName || !unitName) {
enqueueSnackbar('Please enter an asset name and symbol.', { variant: 'warning' }) enqueueSnackbar('Please enter an asset name and symbol.', { variant: 'warning' })
return return
@ -596,7 +611,6 @@ export default function TokenizeAsset() {
resetDefaults() resetDefaults()
} catch (error) { } catch (error) {
console.error(error)
enqueueSnackbar('Failed to tokenize asset (ASA creation failed).', { variant: 'error' }) enqueueSnackbar('Failed to tokenize asset (ASA creation failed).', { variant: 'error' })
} finally { } finally {
setLoading(false) setLoading(false)
@ -608,11 +622,17 @@ export default function TokenizeAsset() {
* Handles validation, amount conversion, and transaction submission * Handles validation, amount conversion, and transaction submission
*/ */
const handleTransferAsset = async () => { 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' }) enqueueSnackbar('Please connect a wallet or continue with Google first.', { variant: 'warning' })
return return
} }
if (!signer) {
enqueueSnackbar('Wallet signer not available. Please try reconnecting your wallet.', { variant: 'error' })
return
}
if (!receiverAddress) { if (!receiverAddress) {
enqueueSnackbar('Please enter a recipient address.', { variant: 'warning' }) enqueueSnackbar('Please enter a recipient address.', { variant: 'warning' })
return return
@ -762,7 +782,6 @@ export default function TokenizeAsset() {
setReceiverAddress('') setReceiverAddress('')
setTransferAmount('1') setTransferAmount('1')
} catch (error) { } catch (error) {
console.error('Transfer failed', error)
if (transferMode === 'algo') { if (transferMode === 'algo') {
enqueueSnackbar('ALGO send failed.', { variant: 'error' }) enqueueSnackbar('ALGO send failed.', { variant: 'error' })
} else { } else {
@ -787,11 +806,17 @@ export default function TokenizeAsset() {
const handleDivClick = () => fileInputRef.current?.click() const handleDivClick = () => fileInputRef.current?.click()
const handleMintNFT = async () => { 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' }) enqueueSnackbar('Please connect a wallet or continue with Google first.', { variant: 'warning' })
return return
} }
if (!signer) {
enqueueSnackbar('Wallet signer not available. Please try reconnecting your wallet.', { variant: 'error' })
return
}
if (!selectedFile) { if (!selectedFile) {
enqueueSnackbar('Please select an image file to mint.', { variant: 'warning' }) enqueueSnackbar('Please select an image file to mint.', { variant: 'warning' })
return return
@ -843,7 +868,6 @@ export default function TokenizeAsset() {
metadataUrl = data.metadataUrl metadataUrl = data.metadataUrl
if (!metadataUrl) throw new Error('Backend did not return a valid metadata URL') if (!metadataUrl) throw new Error('Backend did not return a valid metadata URL')
} catch (e: any) { } catch (e: any) {
console.error(e)
enqueueSnackbar('Error uploading to backend. If in Codespaces, make port 3001 Public.', { variant: 'error' }) enqueueSnackbar('Error uploading to backend. If in Codespaces, make port 3001 Public.', { variant: 'error' })
setNftLoading(false) setNftLoading(false)
return return
@ -917,7 +941,6 @@ export default function TokenizeAsset() {
setPreviewUrl('') setPreviewUrl('')
if (fileInputRef.current) fileInputRef.current.value = '' if (fileInputRef.current) fileInputRef.current.value = ''
} catch (e: any) { } catch (e: any) {
console.error(e)
enqueueSnackbar(`Failed to mint NFT: ${e?.message || 'Unknown error'}`, { variant: 'error' }) enqueueSnackbar(`Failed to mint NFT: ${e?.message || 'Unknown error'}`, { variant: 'error' })
} finally { } finally {
setNftLoading(false) setNftLoading(false)

View File

@ -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
* <Web3AuthButton />
* ```
*/
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 (
<div className="flex items-center gap-2">
<button onClick={handleLogin} disabled={isLoading} className="btn btn-sm btn-outline btn-error">
{isLoading ? (
<>
<AiOutlineLoading3Quarters className="animate-spin" />
Connecting...
</>
) : (
'Retry Login'
)}
</button>
<span className="text-xs text-error max-w-xs truncate">{error}</span>
</div>
)
}
// Disconnected state: Show Google sign-in button
if (!isConnected) {
return (
<button
onClick={handleLogin}
disabled={isLoading}
className="btn btn-sm bg-white hover:bg-gray-50 text-gray-700 border border-gray-300 gap-2 font-medium shadow-sm transition-all"
title="Sign in with your Google account to create an Algorand wallet"
>
{isLoading ? (
<>
<AiOutlineLoading3Quarters className="animate-spin text-gray-600" />
<span>Connecting...</span>
</>
) : (
<>
<FaGoogle className="text-lg text-blue-500" />
<span>Sign in with Google</span>
</>
)}
</button>
)
}
// 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 (
<div className="dropdown dropdown-end" data-dropdown>
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="btn btn-sm btn-ghost gap-2 hover:bg-base-200"
title={`Connected: ${address} ${userInfo?.email ?? ''}`}
>
<div className="flex items-center gap-2">
{/* Profile picture - always show first letter of address */}
{userInfo?.profileImage ? (
<img
src={userInfo.profileImage}
alt="Profile"
className="w-6 h-6 rounded-full object-cover ring-2 ring-primary ring-offset-1"
/>
) : (
<div className="w-6 h-6 rounded-full bg-primary text-primary-content flex items-center justify-center text-xs font-bold">
{firstLetter}
</div>
)}
{/* Keep address visible in navbar, but not a link here (button toggles dropdown) */}
<span className="font-mono text-sm font-medium">{address}</span>
<svg
className={`w-4 h-4 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
{isDropdownOpen && (
<ul className="dropdown-content menu p-2 shadow-lg bg-base-100 rounded-box w-72 border border-base-300 mt-2">
{/* User Info Header */}
{userInfo && (userInfo.name || userInfo.email) && (
<>
<li className="menu-title px-3 py-2">
<div className="flex items-center gap-3">
{userInfo.profileImage ? (
<img
src={userInfo.profileImage}
alt="Profile"
className="w-10 h-10 rounded-full object-cover ring-2 ring-primary"
/>
) : (
<div className="w-10 h-10 rounded-full bg-primary text-primary-content flex items-center justify-center text-lg font-bold">
{firstLetter}
</div>
)}
<div className="flex flex-col">
{userInfo.name && <span className="font-semibold text-base-content">{userInfo.name}</span>}
{userInfo.email && <span className="text-xs text-base-content/70 break-all">{userInfo.email}</span>}
</div>
</div>
</li>
<div className="divider my-1"></div>
</>
)}
{/* Address Section */}
<li className="menu-title px-3">
<span className="text-xs uppercase">Algorand Address</span>
</li>
{/* Clickable address -> Lora */}
<li>
<a
href={loraUrl}
target="_blank"
rel="noopener noreferrer"
className="bg-base-200 rounded-lg p-2 font-mono text-xs break-all hover:bg-base-300 transition"
title="View account on Lora explorer"
onClick={() => setIsDropdownOpen(false)}
>
{address}
<span className="ml-2 opacity-70"></span>
</a>
</li>
{/* Copy Address Button */}
<li>
<button onClick={handleCopyAddress} className="text-sm gap-2">
{copied ? (
<>
<FaCheck className="text-success" />
<span>Copied!</span>
</>
) : (
<>
<FaCopy />
<span>Copy Address</span>
</>
)}
</button>
</li>
{/* Explicit "View on Lora" CTA (extra clarity) */}
<li>
<a
href={loraUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm gap-2"
title="Open in Lora explorer"
onClick={() => setIsDropdownOpen(false)}
>
<span>View on Lora</span>
<span className="opacity-70"></span>
</a>
</li>
<div className="divider my-1"></div>
{/* Disconnect Button */}
<li>
<button onClick={handleLogout} disabled={isLoading} className="text-sm text-error hover:bg-error/10 gap-2">
{isLoading ? (
<>
<AiOutlineLoading3Quarters className="animate-spin" />
<span>Disconnecting...</span>
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
<span>Disconnect</span>
</>
)}
</button>
</li>
</ul>
)}
</div>
)
}
// Fallback: Loading state
if (isLoading) {
return (
<button disabled className="btn btn-sm btn-ghost gap-2">
<AiOutlineLoading3Quarters className="animate-spin" />
<span>Initializing...</span>
</button>
)
}
return null
}
export default Web3AuthButton

View File

@ -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<void>
logout: () => Promise<void>
refreshUserInfo: () => Promise<void>
}
const Web3AuthContext = createContext<Web3AuthContextType | undefined>(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<string | null>(null)
const [provider, setProvider] = useState<IProvider | null>(null)
const [web3AuthInstance, setWeb3AuthInstance] = useState<Web3Auth | null>(null)
const [algorandAccount, setAlgorandAccount] = useState<AlgorandAccountFromWeb3Auth | null>(null)
const [userInfo, setUserInfo] = useState<Web3AuthUserInfo | null>(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 <Web3AuthContext.Provider value={value}>{children}</Web3AuthContext.Provider>
}
export function useWeb3Auth(): Web3AuthContextType {
const context = useContext(Web3AuthContext)
if (context === undefined) {
throw new Error('useWeb3Auth must be used within a Web3AuthProvider')
}
return context
}

View File

@ -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])
}

View File

@ -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 <p>Not connected</p>;
*
* const result = await algorand.send.assetCreate({...});
* ```
*/
export function useAlgorandClient() {
const { isConnected } = useWeb3Auth()
const [client, setClient] = useState<AlgorandClient | null>(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<void> }
*
* @example
* ```typescript
* const { balance, loading, error } = useAccountBalance();
*
* if (loading) return <p>Loading...</p>;
* if (error) return <p>Error: {error}</p>;
* return <p>Balance: {balance} ALGO</p>;
* ```
*/
export function useAccountBalance() {
const { algorandAccount } = useWeb3Auth()
const algod = useAlgod()
const [balance, setBalance] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(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 <p>Insufficient balance. Need at least 10 ALGO</p>;
* }
* ```
*/
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<string>, 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<string | null>(null)
const sendTransaction = useCallback(
async (transactions: Uint8Array[]): Promise<string> => {
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<Record<string, unknown> | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(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<number>, 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<string | null>(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<number> => {
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<string>, 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<string | null>(null)
const sendAsset = useCallback(
async (params: { to: string; assetId?: number; amount: bigint; closeRemainderTo?: string }): Promise<string> => {
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,
}
}

View File

@ -1,8 +1,8 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App' import App from './App'
import './styles/main.css'
import ErrorBoundary from './components/ErrorBoundary' import ErrorBoundary from './components/ErrorBoundary'
import './styles/main.css'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode> <React.StrictMode>

View File

@ -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<AlgorandAccountFromWeb3Auth> {
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<Uint8Array[]> => {
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)
}

View File

@ -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<Web3Auth> {
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<Web3AuthUserInfo | null> {
if (!web3authInstance || !isWeb3AuthConnected()) {
return null
}
try {
const userInfo = await web3authInstance.getUserInfo()
return userInfo as Web3AuthUserInfo
} catch (error) {
return null
}
}
export async function logoutFromWeb3Auth(): Promise<void> {
if (!web3authInstance) {
return
}
try {
await web3authInstance.logout()
} catch (error) {
throw error
}
}

View File

@ -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, youll 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<Uint8Array[]>
sender?: string
}
/**
* ✅ Correct: Convert Web3Auth Algorand account to an AlgorandClient-compatible signer function.
*
* Returns algosdk.TransactionSigner which matches AlgoKit / AlgorandClient expectations:
* (txnGroup, indexesToSign) => Promise<Uint8Array[]>
*
* 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<number> 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.
// Well 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
}

View File

@ -7,9 +7,19 @@ export default defineConfig({
plugins: [ plugins: [
react(), react(),
nodePolyfills({ nodePolyfills({
globals: { protocolImports: true,
Buffer: true,
},
}), }),
], ],
build: {
commonjsOptions: {
transformMixedEsModules: true,
},
},
optimizeDeps: {
esbuildOptions: {
define: {
global: 'globalThis',
},
},
},
}) })