migrate for unifiedWallet to use-Wallet 4.4.0 for W3A (still UI bugs present)
This commit is contained in:
199
projects/TokenizeRWATemplate-frontend/package-lock.json
generated
199
projects/TokenizeRWATemplate-frontend/package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -1,71 +1,99 @@
|
|||||||
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()
|
||||||
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 }]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main App Component
|
* Build supported wallets list based on env/network.
|
||||||
* Sets up wallet provider and routing for the Tokenization dApp
|
* 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() {
|
export default function App() {
|
||||||
const algodConfig = getAlgodConfigFromViteEnvironment()
|
const algodConfig = getAlgodConfigFromViteEnvironment()
|
||||||
|
|
||||||
const walletManager = new WalletManager({
|
const supportedWallets = useMemo(() => buildSupportedWallets(), [])
|
||||||
wallets: supportedWallets,
|
const walletManager = useMemo(() => {
|
||||||
defaultNetwork: algodConfig.network,
|
const mgr = new WalletManager({
|
||||||
networks: {
|
wallets: supportedWallets,
|
||||||
[algodConfig.network]: {
|
defaultNetwork: algodConfig.network,
|
||||||
algod: {
|
networks: {
|
||||||
baseServer: algodConfig.server,
|
[algodConfig.network]: {
|
||||||
port: algodConfig.port,
|
algod: {
|
||||||
token: String(algodConfig.token),
|
baseServer: algodConfig.server,
|
||||||
|
port: algodConfig.port,
|
||||||
|
token: String(algodConfig.token),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
options: {
|
||||||
options: {
|
resetNetwork: true,
|
||||||
resetNetwork: true,
|
},
|
||||||
},
|
})
|
||||||
})
|
|
||||||
|
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>
|
<Route element={<Layout />}>
|
||||||
<Route element={<Layout />}>
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/tokenize" element={<TokenizePage />} />
|
||||||
<Route path="/tokenize" element={<TokenizePage />} />
|
</Route>
|
||||||
</Route>
|
</Routes>
|
||||||
</Routes>
|
</BrowserRouter>
|
||||||
</BrowserRouter>
|
</WalletProvider>
|
||||||
</WalletProvider>
|
|
||||||
</Web3AuthProvider>
|
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
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()
|
closeModal()
|
||||||
} catch (error) {
|
} catch (e: any) {
|
||||||
throw new Error(`Failed to connect with ${provider}: ${error}`)
|
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<void> }[] = [
|
// Handle disconnect
|
||||||
{
|
const handleDisconnect = async () => {
|
||||||
id: 'google',
|
setLastError('')
|
||||||
label: 'Continue with Google',
|
try {
|
||||||
icon: 'https://www.gstatic.com/images/branding/product/1x/gsa_512dp.png',
|
// For Web3Auth, we might need to use the wallet's disconnect method directly
|
||||||
action: connectGoogle,
|
if (activeWallet) {
|
||||||
},
|
// Try the wallet's disconnect method first
|
||||||
{
|
if (typeof (activeWallet as any).disconnect === 'function') {
|
||||||
id: 'facebook',
|
await (activeWallet as any).disconnect()
|
||||||
label: 'Continue with Facebook',
|
}
|
||||||
icon: 'https://www.facebook.com/images/fb_icon_325x325.png',
|
// Fall back to hook's disconnect if available
|
||||||
action: connectFacebook,
|
else if (typeof disconnect === 'function') {
|
||||||
},
|
await disconnect()
|
||||||
{
|
}
|
||||||
id: 'github',
|
} else if (typeof disconnect === 'function') {
|
||||||
label: 'Continue with GitHub',
|
await disconnect()
|
||||||
icon: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png',
|
}
|
||||||
action: connectGithub,
|
closeModal()
|
||||||
},
|
} catch (e: any) {
|
||||||
]
|
// Silently handle disconnect errors
|
||||||
|
closeModal()
|
||||||
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'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
// Don't render if modal is closed
|
||||||
const dialog = dialogRef.current
|
if (!openModal) return null
|
||||||
if (!dialog) return
|
|
||||||
openModal ? dialog.showModal() : dialog.close()
|
// Check if wallet is connected - activeAddress is the primary indicator
|
||||||
}, [openModal])
|
// 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>
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={disconnect} // Use the unified disconnect method
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
Disconnect
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
/* --- DISCONNECTED STATE --- */
|
<button
|
||||||
<>
|
onClick={handleDisconnect}
|
||||||
|
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
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<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>
|
<button
|
||||||
{socialOptions.map((option) => (
|
onClick={() => handleConnect(web3AuthWallet)}
|
||||||
|
disabled={!!connectingId}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{/* Google Icon */}
|
||||||
|
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="#4285F4"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#34A853"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#FBBC05"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#EA4335"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="font-semibold text-slate-700 dark:text-slate-200">
|
||||||
|
{connectingId === web3AuthWallet.id ? 'Connecting…' : 'Continue with Google'}
|
||||||
|
</span>
|
||||||
|
</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
|
<button
|
||||||
key={option.id}
|
key={w.id}
|
||||||
onClick={() => handleSocialLogin(option.id, option.action)}
|
onClick={() => handleConnect(w)}
|
||||||
disabled={!!connectingProvider}
|
disabled={!!connectingId}
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
<img src={option.icon} className="w-5 h-5" alt={option.label} />
|
<div className="flex items-center gap-3">
|
||||||
{connectingProvider === option.id ? 'Connecting...' : option.label}
|
{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>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="relative">
|
{/* Warning if Web3Auth didn't register */}
|
||||||
<div className="absolute inset-0 flex items-center">
|
{!web3AuthWallet && (
|
||||||
<div className="w-full border-t border-gray-100 dark:border-slate-800"></div>
|
<div className="mt-4 p-3 rounded-xl bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800">
|
||||||
</div>
|
<p className="text-xs text-amber-700 dark:text-amber-400">
|
||||||
<div className="relative flex justify-center text-xs uppercase">
|
Google sign-in is not available. Check console for Web3Auth errors.
|
||||||
<span className="bg-white dark:bg-slate-900 px-2 text-gray-400">Or use a wallet</span>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="grid grid-cols-1 gap-3">
|
</div>
|
||||||
{traditionalWallets?.map((wallet) => (
|
)}
|
||||||
<button
|
|
||||||
key={wallet.id}
|
|
||||||
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"
|
|
||||||
onClick={() => {
|
|
||||||
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>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,10 +388,17 @@ 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) {
|
||||||
@ -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,10 +524,16 @@ 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' })
|
||||||
@ -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,10 +622,16 @@ 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' })
|
||||||
@ -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,10 +806,16 @@ 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' })
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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])
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<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.
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user