WIP update - Web3Auth x Wallet Connect signing integration
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
import { SupportedWallet, WalletId, WalletManager, WalletProvider } from '@txnlab/use-wallet-react'
|
||||
import { SnackbarProvider } from 'notistack'
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
||||
import { Web3AuthProvider } from './components/Web3AuthProvider'
|
||||
import Home from './Home'
|
||||
import Layout from './Layout'
|
||||
import TokenizePage from './TokenizePage'
|
||||
@ -53,16 +54,18 @@ export default function App() {
|
||||
|
||||
return (
|
||||
<SnackbarProvider maxSnack={3}>
|
||||
<WalletProvider manager={walletManager}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<Layout />}>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/tokenize" element={<TokenizePage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</WalletProvider>
|
||||
<Web3AuthProvider>
|
||||
<WalletProvider manager={walletManager}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<Layout />}>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/tokenize" element={<TokenizePage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</WalletProvider>
|
||||
</Web3AuthProvider>
|
||||
</SnackbarProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,19 +1,25 @@
|
||||
import { useWallet } from '@txnlab/use-wallet-react'
|
||||
import { useState } from 'react'
|
||||
import { NavLink, Outlet } from 'react-router-dom'
|
||||
import ConnectWallet from './components/ConnectWallet'
|
||||
import ThemeToggle from './components/ThemeToggle'
|
||||
import Web3AuthButton from './components/Web3AuthButton'
|
||||
import { useUnifiedWallet } from './hooks/useUnifiedWallet'
|
||||
|
||||
/**
|
||||
* Main Layout Component
|
||||
* Wraps the entire app with navigation, footer, and wallet connection modal
|
||||
* Now with unified wallet support - shows mutual exclusion between Web3Auth and traditional wallets
|
||||
*/
|
||||
export default function Layout() {
|
||||
const [openWalletModal, setOpenWalletModal] = useState(false)
|
||||
const { activeAddress } = useWallet()
|
||||
const { walletType } = useUnifiedWallet()
|
||||
|
||||
const toggleWalletModal = () => setOpenWalletModal(!openWalletModal)
|
||||
|
||||
// Determine button states based on which wallet is active
|
||||
const isWeb3AuthActive = walletType === 'web3auth'
|
||||
const isTraditionalActive = walletType === 'traditional'
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-white dark:bg-slate-950 text-slate-900 dark:text-slate-100">
|
||||
{/* Navbar */}
|
||||
@ -47,11 +53,30 @@ export default function Layout() {
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<ThemeToggle />
|
||||
|
||||
{/* Web3Auth Button - disabled if traditional wallet is active */}
|
||||
<div className={isTraditionalActive ? 'opacity-50 pointer-events-none' : ''}>
|
||||
<Web3AuthButton />
|
||||
</div>
|
||||
|
||||
{/* Traditional Wallet Button - disabled if Web3Auth is active */}
|
||||
<button
|
||||
onClick={toggleWalletModal}
|
||||
className="px-4 py-2 bg-teal-600 text-white rounded-lg font-medium hover:bg-teal-700 transition text-sm shadow-sm"
|
||||
disabled={isWeb3AuthActive}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition text-sm shadow-sm ${
|
||||
isWeb3AuthActive
|
||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed dark:bg-slate-700 dark:text-slate-500'
|
||||
: isTraditionalActive
|
||||
? 'bg-teal-600 text-white hover:bg-teal-700'
|
||||
: 'bg-teal-600 text-white hover:bg-teal-700'
|
||||
}`}
|
||||
title={isWeb3AuthActive ? 'Using Web3Auth - disconnect to use traditional wallet' : undefined}
|
||||
>
|
||||
{activeAddress ? 'Wallet Connected' : 'Connect Wallet'}
|
||||
{isWeb3AuthActive
|
||||
? 'Using Web3Auth'
|
||||
: isTraditionalActive
|
||||
? 'Wallet Connected'
|
||||
: 'Connect Wallet'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { AlgorandClient } from '@algorandfoundation/algokit-utils'
|
||||
import { useWallet } from '@txnlab/use-wallet-react'
|
||||
import { sha512_256 } from 'js-sha512'
|
||||
import { useSnackbar } from 'notistack'
|
||||
import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { AiOutlineCloudUpload, AiOutlineInfoCircle, AiOutlineLoading3Quarters } from 'react-icons/ai'
|
||||
import { BsCoin } from 'react-icons/bs'
|
||||
import { useUnifiedWallet } from '../hooks/useUnifiedWallet'
|
||||
import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs'
|
||||
|
||||
/**
|
||||
@ -151,8 +151,10 @@ export default function TokenizeAsset() {
|
||||
const [nftFreeze, setNftFreeze] = useState<string>('')
|
||||
const [nftClawback, setNftClawback] = useState<string>('')
|
||||
|
||||
// ===== Wallet + notifications =====
|
||||
const { transactionSigner, activeAddress } = useWallet()
|
||||
// ===== Unified wallet (Web3Auth OR WalletConnect) =====
|
||||
const { signer, activeAddress } = useUnifiedWallet()
|
||||
|
||||
// ===== Notifications =====
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
|
||||
// ===== Algorand client =====
|
||||
@ -248,8 +250,8 @@ export default function TokenizeAsset() {
|
||||
* Adjusts total supply by decimals and saves asset to localStorage
|
||||
*/
|
||||
const handleTokenize = async () => {
|
||||
if (!transactionSigner || !activeAddress) {
|
||||
enqueueSnackbar('Please connect your wallet first.', { variant: 'warning' })
|
||||
if (!signer || !activeAddress) {
|
||||
enqueueSnackbar('Please connect a wallet or continue with Google first.', { variant: 'warning' })
|
||||
return
|
||||
}
|
||||
|
||||
@ -280,7 +282,7 @@ export default function TokenizeAsset() {
|
||||
|
||||
const createResult = await algorand.send.assetCreate({
|
||||
sender: activeAddress,
|
||||
signer: transactionSigner,
|
||||
signer,
|
||||
total: onChainTotal,
|
||||
decimals: d,
|
||||
assetName,
|
||||
@ -340,8 +342,8 @@ export default function TokenizeAsset() {
|
||||
* Transfer (Manual ASA / USDC ASA / ALGO payment)
|
||||
*/
|
||||
const handleTransferAsset = async () => {
|
||||
if (!transactionSigner || !activeAddress) {
|
||||
enqueueSnackbar('Please connect your wallet first.', { variant: 'warning' })
|
||||
if (!signer || !activeAddress) {
|
||||
enqueueSnackbar('Please connect a wallet or continue with Google first.', { variant: 'warning' })
|
||||
return
|
||||
}
|
||||
|
||||
@ -385,7 +387,7 @@ export default function TokenizeAsset() {
|
||||
|
||||
const result = await algorand.send.payment({
|
||||
sender: activeAddress,
|
||||
signer: transactionSigner,
|
||||
signer,
|
||||
receiver: receiverAddress,
|
||||
amount: { microAlgo: Number(microAlgos) },
|
||||
})
|
||||
@ -413,7 +415,7 @@ export default function TokenizeAsset() {
|
||||
|
||||
const result = await algorand.send.assetTransfer({
|
||||
sender: activeAddress,
|
||||
signer: transactionSigner,
|
||||
signer,
|
||||
assetId: TESTNET_USDC_ASSET_ID,
|
||||
receiver: receiverAddress,
|
||||
amount: usdcAmount,
|
||||
@ -441,7 +443,7 @@ export default function TokenizeAsset() {
|
||||
|
||||
const result = await algorand.send.assetTransfer({
|
||||
sender: activeAddress,
|
||||
signer: transactionSigner,
|
||||
signer,
|
||||
assetId: Number(transferAssetId),
|
||||
receiver: receiverAddress,
|
||||
amount: BigInt(transferAmount),
|
||||
@ -491,8 +493,8 @@ export default function TokenizeAsset() {
|
||||
const handleDivClick = () => fileInputRef.current?.click()
|
||||
|
||||
const handleMintNFT = async () => {
|
||||
if (!transactionSigner || !activeAddress) {
|
||||
enqueueSnackbar('Please connect wallet first', { variant: 'warning' })
|
||||
if (!signer || !activeAddress) {
|
||||
enqueueSnackbar('Please connect a wallet or continue with Google first.', { variant: 'warning' })
|
||||
return
|
||||
}
|
||||
|
||||
@ -563,7 +565,7 @@ export default function TokenizeAsset() {
|
||||
|
||||
const createNFTResult = await algorand.send.assetCreate({
|
||||
sender: activeAddress,
|
||||
signer: transactionSigner,
|
||||
signer,
|
||||
total: onChainTotal,
|
||||
decimals: d,
|
||||
assetName: nftName,
|
||||
@ -630,20 +632,11 @@ export default function TokenizeAsset() {
|
||||
|
||||
const canSubmit = !!assetName && !!unitName && !!total && !loading && !!activeAddress
|
||||
|
||||
const canMintNft =
|
||||
!!nftName &&
|
||||
!!nftUnit &&
|
||||
!!nftSupply &&
|
||||
!!nftDecimals &&
|
||||
!!selectedFile &&
|
||||
!!activeAddress &&
|
||||
!nftLoading
|
||||
const canMintNft = !!nftName && !!nftUnit && !!nftSupply && !!nftDecimals && !!selectedFile && !!activeAddress && !nftLoading
|
||||
|
||||
const transferAmountLabel =
|
||||
transferMode === 'algo' ? 'Amount (ALGO)' : transferMode === 'usdc' ? 'Amount (USDC)' : 'Amount'
|
||||
const transferAmountLabel = transferMode === 'algo' ? 'Amount (ALGO)' : transferMode === 'usdc' ? 'Amount (USDC)' : 'Amount'
|
||||
|
||||
const transferAssetIdLabel =
|
||||
transferMode === 'algo' ? 'Asset (ALGO)' : transferMode === 'usdc' ? 'Asset (USDC)' : 'Asset ID'
|
||||
const transferAssetIdLabel = transferMode === 'algo' ? 'Asset (ALGO)' : transferMode === 'usdc' ? 'Asset (USDC)' : 'Asset ID'
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-lg p-6 sm:p-8">
|
||||
@ -1008,7 +1001,11 @@ export default function TokenizeAsset() {
|
||||
onClick={handleDivClick}
|
||||
>
|
||||
{previewUrl ? (
|
||||
<img src={previewUrl} alt="NFT preview" className="rounded-lg max-h-48 object-contain shadow-sm bg-white dark:bg-slate-900" />
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt="NFT preview"
|
||||
className="rounded-lg max-h-48 object-contain shadow-sm bg-white dark:bg-slate-900"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<AiOutlineCloudUpload className="mx-auto h-12 w-12 text-slate-400" />
|
||||
@ -1245,7 +1242,13 @@ export default function TokenizeAsset() {
|
||||
: 'bg-teal-600 hover:bg-teal-700 text-white shadow-md'
|
||||
}`}
|
||||
>
|
||||
{transferLoading ? 'Transferring…' : transferMode === 'algo' ? 'Send ALGO' : transferMode === 'usdc' ? 'Send USDC' : 'Transfer Asset'}
|
||||
{transferLoading
|
||||
? 'Transferring…'
|
||||
: transferMode === 'algo'
|
||||
? 'Send ALGO'
|
||||
: transferMode === 'usdc'
|
||||
? 'Send USDC'
|
||||
: 'Transfer Asset'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@ -0,0 +1,284 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AiOutlineLoading3Quarters } from 'react-icons/ai'
|
||||
import { FaGoogle, FaCopy, FaCheck } 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
|
||||
* - Ellipsized address display for better UX
|
||||
* - Loading states and error handling
|
||||
* - Beautiful Google-style sign-in button
|
||||
*
|
||||
* 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)
|
||||
|
||||
// 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 ''
|
||||
|
||||
// Handle if address is an object (like from algosdk with publicKey property)
|
||||
if (typeof algorandAccount.address === 'object' && algorandAccount.address !== null) {
|
||||
// If it has a toString method, use it
|
||||
if ('toString' in algorandAccount.address && typeof algorandAccount.address.toString === 'function') {
|
||||
return algorandAccount.address.toString()
|
||||
}
|
||||
// If it has an addr property (algosdk Account object)
|
||||
if ('addr' in algorandAccount.address) {
|
||||
return String(algorandAccount.address.addr)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
return String(algorandAccount.address)
|
||||
}
|
||||
|
||||
// Ellipsize long addresses for better UI
|
||||
const ellipseAddress = (address: string = '', startChars = 6, endChars = 4): string => {
|
||||
if (!address || address.length <= startChars + endChars) {
|
||||
return address
|
||||
}
|
||||
return `${address.slice(0, startChars)}...${address.slice(-endChars)}`
|
||||
}
|
||||
|
||||
// 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'
|
||||
|
||||
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}`}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
<span className="font-mono text-sm font-medium">{ellipseAddress(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>
|
||||
<li>
|
||||
<div className="bg-base-200 rounded-lg p-2 font-mono text-xs break-all cursor-default hover:bg-base-200">
|
||||
{address}
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<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
|
||||
@ -0,0 +1,225 @@
|
||||
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: () => 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)
|
||||
|
||||
useEffect(() => {
|
||||
const initializeWeb3Auth = async () => {
|
||||
console.log('🎯 WEB3AUTHPROVIDER: Starting initialization')
|
||||
console.log('🎯 Environment variables:', {
|
||||
clientId: import.meta.env.VITE_WEB3AUTH_CLIENT_ID ? 'SET' : 'MISSING',
|
||||
mode: import.meta.env.MODE,
|
||||
dev: import.meta.env.DEV,
|
||||
})
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
console.log('🎯 Calling initWeb3Auth()...')
|
||||
const web3auth = await initWeb3Auth()
|
||||
console.log('🎯 initWeb3Auth() returned:', web3auth)
|
||||
|
||||
setWeb3AuthInstance(web3auth)
|
||||
|
||||
if (web3auth.status === 'connected' && web3auth.provider) {
|
||||
console.log('🎯 User already connected from previous session')
|
||||
setProvider(web3auth.provider)
|
||||
setIsConnected(true)
|
||||
|
||||
try {
|
||||
const account = await getAlgorandAccount(web3auth.provider)
|
||||
setAlgorandAccount(account)
|
||||
console.log('🎯 Algorand account derived:', account.address)
|
||||
} catch (err) {
|
||||
console.error('🎯 Failed to derive Algorand account:', err)
|
||||
setError('Failed to derive Algorand account. Please reconnect.')
|
||||
}
|
||||
|
||||
try {
|
||||
const userInformation = await getWeb3AuthUserInfo()
|
||||
if (userInformation) {
|
||||
setUserInfo(userInformation)
|
||||
console.log('🎯 User info fetched:', userInformation)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('🎯 Failed to fetch user info:', err)
|
||||
}
|
||||
}
|
||||
|
||||
setIsInitialized(true)
|
||||
console.log('🎯 WEB3AUTHPROVIDER: Initialization complete')
|
||||
} 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()
|
||||
}, [])
|
||||
|
||||
const login = async () => {
|
||||
console.log('🎯 LOGIN: Called')
|
||||
|
||||
if (!web3AuthInstance) {
|
||||
console.error('🎯 LOGIN: Web3Auth not initialized')
|
||||
setError('Web3Auth not initialized')
|
||||
return
|
||||
}
|
||||
|
||||
if (!isInitialized) {
|
||||
console.error('🎯 LOGIN: Web3Auth still initializing')
|
||||
setError('Web3Auth is still initializing, please try again')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
console.log('🎯 LOGIN: Calling web3AuthInstance.connect()...')
|
||||
const web3authProvider = await web3AuthInstance.connect()
|
||||
console.log('🎯 LOGIN: connect() returned:', web3authProvider ? 'PROVIDER' : 'NULL')
|
||||
|
||||
if (!web3authProvider) {
|
||||
throw new Error('Failed to connect Web3Auth provider')
|
||||
}
|
||||
|
||||
setProvider(web3authProvider)
|
||||
setIsConnected(true)
|
||||
|
||||
try {
|
||||
console.log('🎯 LOGIN: Deriving Algorand account...')
|
||||
const account = await getAlgorandAccount(web3authProvider)
|
||||
setAlgorandAccount(account)
|
||||
console.log('🎯 LOGIN: Successfully derived Algorand account:', account.address)
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to derive Algorand account'
|
||||
setError(errorMessage)
|
||||
console.error('🎯 LOGIN: Algorand account derivation error:', err)
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🎯 LOGIN: Fetching user info...')
|
||||
const userInformation = await getWeb3AuthUserInfo()
|
||||
if (userInformation) {
|
||||
setUserInfo(userInformation)
|
||||
console.log('🎯 LOGIN: User info fetched')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('🎯 LOGIN: Failed to fetch user info:', err)
|
||||
}
|
||||
|
||||
console.log('🎯 LOGIN: Complete')
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Login failed'
|
||||
console.error('🎯 LOGIN: Error:', err)
|
||||
setError(errorMessage)
|
||||
setIsConnected(false)
|
||||
setProvider(null)
|
||||
setAlgorandAccount(null)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
console.log('🎯 LOGOUT: Called')
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
await logoutFromWeb3Auth()
|
||||
|
||||
setProvider(null)
|
||||
setIsConnected(false)
|
||||
setAlgorandAccount(null)
|
||||
setUserInfo(null)
|
||||
|
||||
console.log('🎯 LOGOUT: Complete')
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Logout failed'
|
||||
console.error('🎯 LOGOUT: Error:', err)
|
||||
setError(errorMessage)
|
||||
|
||||
setProvider(null)
|
||||
setIsConnected(false)
|
||||
setAlgorandAccount(null)
|
||||
setUserInfo(null)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshUserInfo = async () => {
|
||||
console.log('🎯 REFRESH: Called')
|
||||
try {
|
||||
const userInformation = await getWeb3AuthUserInfo()
|
||||
if (userInformation) {
|
||||
setUserInfo(userInformation)
|
||||
console.log('🎯 REFRESH: User info refreshed')
|
||||
}
|
||||
} 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
|
||||
}
|
||||
@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Unified Wallet Hook
|
||||
*
|
||||
* Combines Web3Auth (Google OAuth) and traditional wallet (Pera/Defly/etc) into ONE interface.
|
||||
* Provides a single source of truth for activeAddress and signer across the entire app.
|
||||
*
|
||||
* Features:
|
||||
* - Returns ONE activeAddress (from either Web3Auth OR traditional wallet)
|
||||
* - Returns ONE signer (compatible with AlgorandClient)
|
||||
* - Indicates which wallet type is active
|
||||
* - Handles mutual exclusion (only one can be active at a time)
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* const { activeAddress, signer, walletType, isConnected } = useUnifiedWallet()
|
||||
*
|
||||
* // walletType will be: 'web3auth' | 'traditional' | null
|
||||
* // signer works with: algorand.send.assetCreate({ sender: activeAddress, signer, ... })
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { useWallet } from '@txnlab/use-wallet-react'
|
||||
import { useMemo } from 'react'
|
||||
import { useWeb3Auth } from '../components/Web3AuthProvider'
|
||||
import { createWeb3AuthSigner } from '../utils/web3auth/web3authIntegration'
|
||||
|
||||
export type WalletType = 'web3auth' | 'traditional' | null
|
||||
|
||||
export interface UnifiedWalletState {
|
||||
/** The active Algorand address (from either Web3Auth or traditional wallet) */
|
||||
activeAddress: string | null
|
||||
|
||||
/** Transaction signer compatible with AlgorandClient */
|
||||
signer: any | null
|
||||
|
||||
/** Which wallet system is currently active */
|
||||
walletType: WalletType
|
||||
|
||||
/** Whether any wallet is connected */
|
||||
isConnected: boolean
|
||||
|
||||
/** Loading state (either wallet system initializing/connecting) */
|
||||
isLoading: boolean
|
||||
|
||||
/** Error from either wallet system */
|
||||
error: string | null
|
||||
|
||||
/** Original Web3Auth data (for accessing userInfo, etc) */
|
||||
web3auth: {
|
||||
algorandAccount: ReturnType<typeof useWeb3Auth>['algorandAccount']
|
||||
userInfo: ReturnType<typeof useWeb3Auth>['userInfo']
|
||||
login: ReturnType<typeof useWeb3Auth>['login']
|
||||
logout: ReturnType<typeof useWeb3Auth>['logout']
|
||||
}
|
||||
|
||||
/** Original traditional wallet data (for accessing wallet-specific features) */
|
||||
traditional: {
|
||||
wallets: ReturnType<typeof useWallet>['wallets']
|
||||
activeWallet: ReturnType<typeof useWallet>['activeWallet']
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* useUnifiedWallet Hook
|
||||
*
|
||||
* Combines Web3Auth and traditional wallet into a single interface.
|
||||
* Priority: Web3Auth takes precedence if both are somehow connected.
|
||||
*
|
||||
* @returns UnifiedWalletState with activeAddress, signer, and wallet metadata
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In TokenizeAsset.tsx:
|
||||
* const { activeAddress, signer, walletType } = useUnifiedWallet()
|
||||
*
|
||||
* if (!activeAddress) {
|
||||
* return <p>Please connect a wallet</p>
|
||||
* }
|
||||
*
|
||||
* // Use signer with AlgorandClient - works with BOTH wallet types!
|
||||
* const result = await algorand.send.assetCreate({
|
||||
* sender: activeAddress,
|
||||
* signer: signer,
|
||||
* total: BigInt(1000000),
|
||||
* decimals: 6,
|
||||
* assetName: 'My Token',
|
||||
* unitName: 'MYT',
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function useUnifiedWallet(): UnifiedWalletState {
|
||||
// Get both wallet systems
|
||||
const web3auth = useWeb3Auth()
|
||||
const traditional = useWallet()
|
||||
|
||||
// Compute unified state
|
||||
const state = useMemo<UnifiedWalletState>(() => {
|
||||
// Priority 1: Web3Auth (if connected)
|
||||
if (web3auth.isConnected && web3auth.algorandAccount) {
|
||||
return {
|
||||
activeAddress: web3auth.algorandAccount.address,
|
||||
signer: createWeb3AuthSigner(web3auth.algorandAccount),
|
||||
walletType: 'web3auth',
|
||||
isConnected: true,
|
||||
isLoading: web3auth.isLoading,
|
||||
error: web3auth.error,
|
||||
web3auth: {
|
||||
algorandAccount: web3auth.algorandAccount,
|
||||
userInfo: web3auth.userInfo,
|
||||
login: web3auth.login,
|
||||
logout: web3auth.logout,
|
||||
},
|
||||
traditional: {
|
||||
wallets: traditional.wallets,
|
||||
activeWallet: traditional.activeWallet,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Traditional wallet (Pera/Defly/etc)
|
||||
if (traditional.activeAddress) {
|
||||
return {
|
||||
activeAddress: traditional.activeAddress,
|
||||
signer: traditional.transactionSigner,
|
||||
walletType: 'traditional',
|
||||
isConnected: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
web3auth: {
|
||||
algorandAccount: null,
|
||||
userInfo: null,
|
||||
login: web3auth.login,
|
||||
logout: web3auth.logout,
|
||||
},
|
||||
traditional: {
|
||||
wallets: traditional.wallets,
|
||||
activeWallet: traditional.activeWallet,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// No wallet connected
|
||||
return {
|
||||
activeAddress: null,
|
||||
signer: null,
|
||||
walletType: null,
|
||||
isConnected: false,
|
||||
isLoading: web3auth.isLoading,
|
||||
error: web3auth.error,
|
||||
web3auth: {
|
||||
algorandAccount: null,
|
||||
userInfo: null,
|
||||
login: web3auth.login,
|
||||
logout: web3auth.logout,
|
||||
},
|
||||
traditional: {
|
||||
wallets: traditional.wallets,
|
||||
activeWallet: traditional.activeWallet,
|
||||
},
|
||||
}
|
||||
}, [
|
||||
web3auth.isConnected,
|
||||
web3auth.algorandAccount,
|
||||
web3auth.isLoading,
|
||||
web3auth.error,
|
||||
web3auth.userInfo,
|
||||
web3auth.login,
|
||||
web3auth.logout,
|
||||
traditional.activeAddress,
|
||||
traditional.transactionSigner,
|
||||
traditional.wallets,
|
||||
traditional.activeWallet,
|
||||
])
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper hook: Get just the address (most common use case)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const address = useActiveAddress()
|
||||
* if (!address) return <ConnectButton />
|
||||
* ```
|
||||
*/
|
||||
export function useActiveAddress(): string | null {
|
||||
const { activeAddress } = useUnifiedWallet()
|
||||
return activeAddress
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper hook: Check if any wallet is connected
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const isConnected = useIsWalletConnected()
|
||||
* ```
|
||||
*/
|
||||
export function useIsWalletConnected(): boolean {
|
||||
const { isConnected } = useUnifiedWallet()
|
||||
return isConnected
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper hook: Get wallet type
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const walletType = useWalletType()
|
||||
* if (walletType === 'web3auth') {
|
||||
* // Show Google profile info
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useWalletType(): WalletType {
|
||||
const { walletType } = useUnifiedWallet()
|
||||
return walletType
|
||||
}
|
||||
@ -0,0 +1,411 @@
|
||||
/**
|
||||
* 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,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,159 @@
|
||||
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)
|
||||
}
|
||||
@ -0,0 +1,152 @@
|
||||
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> {
|
||||
console.log('========================================')
|
||||
console.log('🔧 STARTING WEB3AUTH INITIALIZATION')
|
||||
console.log('========================================')
|
||||
|
||||
if (web3authInstance) {
|
||||
console.log('✅ Web3Auth already initialized, returning existing instance')
|
||||
return web3authInstance
|
||||
}
|
||||
|
||||
const clientId = import.meta.env.VITE_WEB3AUTH_CLIENT_ID
|
||||
console.log('📋 Client ID check:', clientId ? '✅ SET' : '❌ MISSING')
|
||||
console.log('📋 Client ID length:', clientId?.length || 0)
|
||||
console.log('📋 Client ID (first 20 chars):', clientId?.substring(0, 20) + '...')
|
||||
|
||||
if (!clientId) {
|
||||
const error = new Error('VITE_WEB3AUTH_CLIENT_ID is not configured')
|
||||
console.error('❌ ERROR:', error.message)
|
||||
throw error
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('📦 Creating privateKeyProvider...')
|
||||
|
||||
// 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',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
console.log('✅ privateKeyProvider created')
|
||||
console.log('📦 Creating Web3Auth configuration object...')
|
||||
|
||||
const web3AuthConfig = {
|
||||
clientId,
|
||||
web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_DEVNET,
|
||||
privateKeyProvider, // ← THIS IS REQUIRED!
|
||||
uiConfig: {
|
||||
appName: 'TokenizeRWA',
|
||||
theme: {
|
||||
primary: '#000000',
|
||||
},
|
||||
mode: 'light' as const,
|
||||
loginMethodsOrder: ['google', 'github', 'twitter'],
|
||||
defaultLanguage: 'en',
|
||||
},
|
||||
}
|
||||
|
||||
console.log('📦 Config created with privateKeyProvider')
|
||||
console.log('🏗️ Instantiating Web3Auth...')
|
||||
|
||||
web3authInstance = new Web3Auth(web3AuthConfig)
|
||||
|
||||
console.log('✅ Web3Auth instance created successfully')
|
||||
console.log('📞 Calling initModal()...')
|
||||
|
||||
await web3authInstance.initModal()
|
||||
|
||||
console.log('✅ initModal() completed successfully')
|
||||
console.log('📊 Web3Auth status:', web3authInstance.status)
|
||||
console.log('📊 Web3Auth connected:', web3authInstance.connected)
|
||||
console.log('========================================')
|
||||
console.log('✅ WEB3AUTH INITIALIZATION COMPLETE')
|
||||
console.log('========================================')
|
||||
|
||||
return web3authInstance
|
||||
} catch (error) {
|
||||
console.error('========================================')
|
||||
console.error('❌ WEB3AUTH INITIALIZATION FAILED')
|
||||
console.error('========================================')
|
||||
console.error('Error type:', error?.constructor?.name)
|
||||
console.error('Error message:', error instanceof Error ? error.message : 'Unknown error')
|
||||
console.error('Full error:', error)
|
||||
console.error('Stack trace:', error instanceof Error ? error.stack : 'No stack trace')
|
||||
console.error('========================================')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function getWeb3AuthInstance(): Web3Auth | null {
|
||||
console.log('🔍 getWeb3AuthInstance() called, instance:', web3authInstance ? '✅ EXISTS' : '❌ NULL')
|
||||
return web3authInstance
|
||||
}
|
||||
|
||||
export function getWeb3AuthProvider(): IProvider | null {
|
||||
const provider = web3authInstance?.provider || null
|
||||
console.log('🔍 getWeb3AuthProvider() called, provider:', provider ? '✅ EXISTS' : '❌ NULL')
|
||||
return provider
|
||||
}
|
||||
|
||||
export function isWeb3AuthConnected(): boolean {
|
||||
const connected = web3authInstance?.status === 'connected'
|
||||
console.log('🔍 isWeb3AuthConnected() called, connected:', connected)
|
||||
return connected
|
||||
}
|
||||
|
||||
export interface Web3AuthUserInfo {
|
||||
email?: string
|
||||
name?: string
|
||||
profileImage?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export async function getWeb3AuthUserInfo(): Promise<Web3AuthUserInfo | null> {
|
||||
console.log('🔍 getWeb3AuthUserInfo() called')
|
||||
|
||||
if (!web3authInstance || !isWeb3AuthConnected()) {
|
||||
console.log('❌ Cannot get user info: not connected')
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const userInfo = await web3authInstance.getUserInfo()
|
||||
console.log('✅ User info retrieved:', userInfo)
|
||||
return userInfo as Web3AuthUserInfo
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to get user info:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function logoutFromWeb3Auth(): Promise<void> {
|
||||
console.log('🚪 logoutFromWeb3Auth() called')
|
||||
|
||||
if (!web3authInstance) {
|
||||
console.log('⚠️ No instance to logout from')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await web3authInstance.logout()
|
||||
console.log('✅ Logged out successfully')
|
||||
} catch (error) {
|
||||
console.error('❌ Logout failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,194 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user