WIP update - Web3Auth x Wallet Connect signing integration

This commit is contained in:
SaraJane
2026-01-13 10:44:18 +00:00
parent 6386a98986
commit 06a4b7ef67
15 changed files with 4914 additions and 164 deletions

View File

@ -46,6 +46,11 @@ VITE_INDEXER_TOKEN=""
VITE_INDEXER_SERVER="https://testnet-idx.algonode.cloud"
VITE_INDEXER_PORT=""
# Web3Auth Configuration
# Get your Client ID from https://dashboard.web3auth.io
# Web3Auth enables social login (Google, GitHub, etc.) that auto-generates Algorand wallets
VITE_WEB3AUTH_CLIENT_ID=
# # ======================
# # MainNet configuration:

View File

@ -0,0 +1,407 @@
# Web3Auth Integration Guide
## Overview
Web3Auth has been successfully integrated into your Algorand tokenization dApp. This allows users to sign in with Google (or other social logins) and automatically get an Algorand wallet generated from their Web3Auth credentials.
## Installation
### 1. Install Dependencies
Run the following command in your `TokenizeRWATemplate-frontend` directory:
```bash
npm install @web3auth/modal @web3auth/base @web3auth/openlogin-adapter
```
### 2. Get Web3Auth Client ID
1. Go to [Web3Auth Dashboard](https://dashboard.web3auth.io)
2. Create a new application or use an existing one
3. Copy your **Client ID**
4. Add it to your `.env` file:
```dotenv
VITE_WEB3AUTH_CLIENT_ID=your_client_id_here
```
## File Structure Created
```
src/
├── utils/
│ └── web3auth/
│ ├── web3authConfig.ts # Web3Auth initialization and config
│ ├── algorandAdapter.ts # Convert Web3Auth keys to Algorand format
│ └── web3authIntegration.ts # Integration helpers for AlgorandClient
├── components/
│ ├── Web3AuthProvider.tsx # React Context Provider
│ └── Web3AuthButton.tsx # Connect/Disconnect button component
└── hooks/
└── (useWeb3Auth exported from Web3AuthProvider)
```
## Files Created
### 1. `src/utils/web3auth/web3authConfig.ts`
Initializes Web3Auth with:
- Google OAuth via OpenLogin adapter
- Algorand TestNet configuration (chainNamespace: OTHER)
- Sapphire DevNet for development
- Functions: `initWeb3Auth()`, `getWeb3AuthProvider()`, `isWeb3AuthConnected()`, etc.
### 2. `src/utils/web3auth/algorandAdapter.ts`
Converts Web3Auth keys to Algorand format:
- `getAlgorandAccount(provider)` - Extracts private key and converts to Algorand account
- `createAlgorandSigner(secretKey)` - Creates a signer for transactions
- Helper functions for validation and key derivation
### 3. `src/utils/web3auth/web3authIntegration.ts`
Integration utilities for AlgorandClient:
- `createWeb3AuthSigner(account)` - Drop-in signer for AlgorandClient
- Amount formatting and parsing utilities
- Transaction verification and analysis helpers
- Balance checking utilities
### 4. `src/components/Web3AuthProvider.tsx`
React Context Provider with:
- State management for Web3Auth and Algorand account
- `login()` / `logout()` functions
- `useWeb3Auth()` custom hook
- Automatic user info fetching from OAuth provider
### 5. `src/components/Web3AuthButton.tsx`
UI Component with:
- "Sign in with Google" button when disconnected
- Address display with dropdown menu when connected
- Profile picture, name, and email display
- Copy address and disconnect buttons
- Loading and error states
## Updated Files
### `src/App.tsx`
Wrapped the app with `<Web3AuthProvider>` to provide Web3Auth context throughout the application:
```tsx
<Web3AuthProvider>
<WalletProvider manager={walletManager}>{/* your routes */}</WalletProvider>
</Web3AuthProvider>
```
### `.env.template`
Added:
```dotenv
VITE_WEB3AUTH_CLIENT_ID=""
```
## Usage Examples
### Basic Usage in Components
```typescript
import { useWeb3Auth } from './components/Web3AuthProvider'
export function MyComponent() {
const { isConnected, algorandAccount, login, logout } = useWeb3Auth()
if (!isConnected) {
return <button onClick={login}>Sign in with Google</button>
}
return (
<div>
<p>Connected: {algorandAccount?.address}</p>
<button onClick={logout}>Disconnect</button>
</div>
)
}
```
### Using with AlgorandClient
```typescript
import { AlgorandClient } from '@algorandfoundation/algokit-utils'
import { createWeb3AuthSigner } from './utils/web3auth/web3authIntegration'
import { useWeb3Auth } from './components/Web3AuthProvider'
export function TokenizeAsset() {
const { algorandAccount } = useWeb3Auth()
if (!algorandAccount) {
return <Web3AuthButton />
}
// Create signer from Web3Auth account
const signer = createWeb3AuthSigner(algorandAccount)
const algodConfig = getAlgodConfigFromViteEnvironment()
const algorand = AlgorandClient.fromConfig({ algodConfig })
const handleCreateAsset = async () => {
try {
const result = await algorand.send.assetCreate({
sender: algorandAccount.address,
signer: signer,
total: BigInt(1000000),
decimals: 6,
assetName: 'My Token',
unitName: 'MYT',
})
console.log('Asset created:', result.confirmation?.assetIndex)
} catch (error) {
console.error('Failed to create asset:', error)
}
}
return <button onClick={handleCreateAsset}>Create Asset</button>
}
```
### Add Web3AuthButton to Your UI
```typescript
import Web3AuthButton from './components/Web3AuthButton'
export function Header() {
return (
<header>
<h1>My Tokenization App</h1>
<Web3AuthButton /> {/* Shows login/account dropdown */}
</header>
)
}
```
### Access User Information
```typescript
import { useWeb3Auth } from './components/Web3AuthProvider'
export function Profile() {
const { userInfo, algorandAccount } = useWeb3Auth()
if (!userInfo) return <p>Not logged in</p>
return (
<div>
{userInfo.profileImage && <img src={userInfo.profileImage} alt="Profile" />}
<p>Name: {userInfo.name}</p>
<p>Email: {userInfo.email}</p>
<p>Algorand Address: {algorandAccount?.address}</p>
</div>
)
}
```
## Key Features
### ✅ Multi-Wallet Support
Web3Auth works **alongside** existing `@txnlab/use-wallet-react`:
- Users can use traditional wallets (Pera, Defly, etc.)
- **OR** sign in with Google/social logins
- Both options available simultaneously
### ✅ Automatic Wallet Generation
When users sign in with Google:
1. Web3Auth generates a secure private key
2. Private key is converted to Algorand mnemonic
3. Algorand account is derived automatically
4. No seed phrases to manage - ties to Google account
### ✅ TestNet Ready
By default, Web3Auth is configured for:
- **Development**: Sapphire DevNet
- **Algorand Network**: TestNet (https://testnet-api.algonode.cloud)
- Switch to SAPPHIRE network for production
### ✅ Full TypeScript Support
All files include complete TypeScript types:
- `AlgorandAccountFromWeb3Auth` interface
- `Web3AuthContextType` for context
- Proper error handling and null checks
### ✅ Integration with AlgorandClient
Drop-in replacement for transaction signing:
```typescript
const signer = createWeb3AuthSigner(algorandAccount)
// Works with existing AlgorandClient code
await algorand.send.assetCreate({
sender: algorandAccount.address,
signer: signer,
// ... other params
})
```
## Configuration
### Change Network (Development → Production)
In `src/utils/web3auth/web3authConfig.ts`:
```typescript
// For development:
web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_DEVNET
// For production:
web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE
```
Also change in `OpenloginAdapter`:
```typescript
network: 'sapphire_devnet' // → 'sapphire'
```
### Add More OAuth Providers
In `src/utils/web3auth/web3authConfig.ts`, add to `loginConfig`:
```typescript
loginConfig: {
google: { ... },
github: {
name: 'GitHub Login',
verifier: 'web3auth-github-demo',
typeOfLogin: 'github',
clientId: clientId,
},
// Add more providers...
}
```
### Customize UI
In `src/utils/web3auth/web3authConfig.ts`, modify `uiConfig`:
```typescript
uiConfig: {
appName: 'Your App Name',
appLogo: 'https://your-logo-url.png',
primaryButtonColour: '#FF6B35',
dark: false,
theme: 'light',
}
```
### Customize Button Styling
In `src/components/Web3AuthButton.tsx`, modify Tailwind classes:
```tsx
// Change button colors, sizes, styles as needed
<button className="btn btn-sm btn-primary gap-2">{/* Your customizations */}</button>
```
## Error Handling
All functions include error handling:
```typescript
const { error } = useWeb3Auth()
if (error) {
return <div className="alert alert-error">{error}</div>
}
```
Common errors:
- `VITE_WEB3AUTH_CLIENT_ID is not configured` - Add Client ID to .env
- `Failed to derive Algorand account` - Web3Auth provider issue
- `Failed to connect Web3Auth provider` - Network or service issue
## Testing
### Test Web3Auth Login Locally
1. Install dependencies: `npm install`
2. Add `VITE_WEB3AUTH_CLIENT_ID` to `.env`
3. Run dev server: `npm run dev`
4. Click "Sign in with Google" button
5. Complete Google OAuth flow
6. See Algorand address in dropdown
### Test with TestNet
1. Ensure `.env` has TestNet configuration
2. Get TestNet tokens from [Algorand Testnet Dispenser](https://dispenser.algorand-testnet.com)
3. Use Algorand address from Web3Auth
4. Create assets or transactions
## Next Steps
1. **Install dependencies**: Run `npm install` command above
2. **Get Web3Auth Client ID**: Register at https://dashboard.web3auth.io
3. **Add to .env**: Set `VITE_WEB3AUTH_CLIENT_ID` in your `.env` file
4. **Add to UI**: Import and use `Web3AuthButton` in your header/navbar
5. **Use in components**: Import `useWeb3Auth()` to access account and signing capabilities
6. **Test**: Run `npm run dev` and test the login flow
## Troubleshooting
### Web3Auth Modal Not Appearing
- Check that `VITE_WEB3AUTH_CLIENT_ID` is set in `.env`
- Ensure Web3AuthProvider wraps your components
- Check browser console for initialization errors
### Algorand Account Derivation Fails
- Verify provider is properly connected
- Check that private key extraction works in your environment
- Try logout and login again
### Transactions Won't Sign
- Ensure `algorandAccount` is not null
- Verify signer is created from correct account
- Check transaction format is compatible with algosdk
### Network Issues
- For LocalNet: Update RPC target in web3authConfig.ts
- For TestNet: Verify Algonode endpoints are accessible
- Check firewall/CORS settings if making cross-origin requests
## Additional Resources
- [Web3Auth Documentation](https://web3auth.io/docs)
- [Web3Auth Dashboard](https://dashboard.web3auth.io)
- [Algorand Developer Docs](https://developer.algorand.org)
- [AlgoKit Utils](https://github.com/algorandfoundation/algokit-utils-ts)
- [algosdk.js](https://github.com/algorand/js-algorand-sdk)
## Support
For issues or questions:
1. Check the troubleshooting section above
2. Review [Web3Auth GitHub Issues](https://github.com/Web3Auth/web3auth-web/issues)
3. Consult [Algorand Community Forums](https://forum.algorand.org)
4. Check browser console for detailed error messages

View File

@ -0,0 +1,363 @@
# Web3Auth Quick Reference
## Installation & Setup
### 1. Install Dependencies
```bash
npm install @web3auth/modal @web3auth/base @web3auth/openlogin-adapter
```
### 2. Get Client ID
1. Go to https://dashboard.web3auth.io
2. Create a new application
3. Copy your **Client ID**
### 3. Add to .env
```dotenv
VITE_WEB3AUTH_CLIENT_ID=your_client_id_here
```
## Files Created
| File | Purpose |
| ------------------------------------------- | ----------------------------- |
| `src/utils/web3auth/web3authConfig.ts` | Web3Auth initialization |
| `src/utils/web3auth/algorandAdapter.ts` | Key conversion utilities |
| `src/utils/web3auth/web3authIntegration.ts` | AlgorandClient integration |
| `src/components/Web3AuthProvider.tsx` | React Context Provider |
| `src/components/Web3AuthButton.tsx` | Login/Logout UI button |
| `src/hooks/useWeb3AuthHooks.ts` | Custom hooks for common tasks |
| `src/components/Web3AuthExamples.tsx` | Implementation examples |
## Most Common Usage Patterns
### Pattern 1: Basic Login Button
```tsx
import Web3AuthButton from './components/Web3AuthButton'
export function Header() {
return (
<header>
<h1>My App</h1>
<Web3AuthButton />
</header>
)
}
```
### Pattern 2: Check if User is Logged In
```tsx
import { useWeb3Auth } from './components/Web3AuthProvider'
export function MyComponent() {
const { isConnected, algorandAccount } = useWeb3Auth()
if (!isConnected) {
return <p>Please sign in</p>
}
return <p>Address: {algorandAccount?.address}</p>
}
```
### Pattern 3: Create Asset with Web3Auth
```tsx
import { useWeb3Auth } from './components/Web3AuthProvider'
import { createWeb3AuthSigner } from './utils/web3auth/web3authIntegration'
import { AlgorandClient } from '@algorandfoundation/algokit-utils'
import { getAlgodConfigFromViteEnvironment } from './utils/network/getAlgoClientConfigs'
export function CreateAsset() {
const { algorandAccount } = useWeb3Auth()
const handleCreate = async () => {
const algodConfig = getAlgodConfigFromViteEnvironment()
const algorand = AlgorandClient.fromConfig({ algodConfig })
const signer = createWeb3AuthSigner(algorandAccount)
const result = await algorand.send.assetCreate({
sender: algorandAccount.address,
signer: signer,
total: BigInt(1000000),
decimals: 6,
assetName: 'My Token',
unitName: 'MYT',
})
console.log('Asset ID:', result.confirmation?.assetIndex)
}
return <button onClick={handleCreate}>Create Asset</button>
}
```
### Pattern 4: Using Custom Hooks
```tsx
import { useAccountBalance, useCreateAsset } from './hooks/useWeb3AuthHooks'
export function Dashboard() {
const { balance, loading: balanceLoading } = useAccountBalance()
const { createAsset, loading: createLoading } = useCreateAsset()
return (
<div>
<p>Balance: {balance} ALGO</p>
<button
onClick={() =>
createAsset({
total: 1000000n,
decimals: 6,
assetName: 'Token',
unitName: 'TKN',
})
}
disabled={createLoading}
>
Create Asset
</button>
</div>
)
}
```
## useWeb3Auth() Hook
Main context hook - use this everywhere!
```typescript
const {
// Connection state
isConnected: boolean,
isLoading: boolean,
isInitialized: boolean,
error: string | null,
// Data
provider: IProvider | null,
web3AuthInstance: Web3Auth | null,
algorandAccount: AlgorandAccountFromWeb3Auth | null,
userInfo: Web3AuthUserInfo | null,
// Functions
login: () => Promise<void>,
logout: () => Promise<void>,
refreshUserInfo: () => Promise<void>,
} = useWeb3Auth()
```
## Custom Hooks Reference
### useAlgorandClient()
Get initialized AlgorandClient for Web3Auth account
```typescript
const algorand = useAlgorandClient()
// Use with algorand.send.assetCreate(...), etc.
```
### useAlgod()
Get algosdk Algodv2 client
```typescript
const algod = useAlgod()
const accountInfo = await algod.accountInformation(address).do()
```
### useAccountBalance()
Get account balance in Algos
```typescript
const { balance, loading, error, refetch } = useAccountBalance()
// balance is a string like "123.456789"
```
### useHasSufficientBalance(amount, fee)
Check if account has enough funds
```typescript
const { hasSufficientBalance } = useHasSufficientBalance('10') // 10 ALGO
if (!hasSufficientBalance) {
/* show error */
}
```
### useSendTransaction()
Sign and submit transactions
```typescript
const { sendTransaction, loading } = useSendTransaction()
const txnId = await sendTransaction([signedTxn])
```
### useWaitForConfirmation(txnId)
Wait for transaction confirmation
```typescript
const { confirmed, confirmation } = useWaitForConfirmation(txnId)
if (confirmed) console.log('Round:', confirmation['confirmed-round'])
```
### useCreateAsset()
Create a new ASA
```typescript
const { createAsset, loading } = useCreateAsset()
const assetId = await createAsset({
total: 1000000n,
decimals: 6,
assetName: 'My Token',
unitName: 'MYT',
})
```
### useSendAsset()
Transfer ASA or Algo
```typescript
const { sendAsset, loading } = useSendAsset()
const txnId = await sendAsset({
to: recipientAddress,
assetId: 12345,
amount: 100n,
})
```
## Integration Utilities Reference
### createWeb3AuthSigner(account)
Create signer for AlgorandClient
```typescript
const signer = createWeb3AuthSigner(algorandAccount)
// Use with: algorand.send.assetCreate({ ..., signer })
```
### getWeb3AuthAccountInfo(account)
Get account info in various formats
```typescript
const { address, publicKeyBytes, publicKeyBase64, secretKeyHex, mnemonicPhrase } = getWeb3AuthAccountInfo(account)
```
### formatAmount(amount, decimals)
Format amount for display
```typescript
formatAmount(BigInt(1000000), 6) // Returns "1.000000"
```
### parseAmount(amount, decimals)
Parse user input to base units
```typescript
parseAmount('1.5', 6) // Returns 1500000n
```
### verifyWeb3AuthSignature(signedTxn, account)
Verify transaction was signed by account
```typescript
const isValid = verifyWeb3AuthSignature(signedTxn, account)
```
### hasSufficientBalance(balance, required, fee)
Check balance programmatically
```typescript
if (hasSufficientBalance(balanceBigInt, requiredBigInt)) {
// Proceed
}
```
## Common Issues & Solutions
| Issue | Solution |
| ---------------------------- | -------------------------------------------------------------- |
| Web3Auth modal not appearing | Check `VITE_WEB3AUTH_CLIENT_ID` is set in `.env` |
| Account derivation fails | Verify provider is connected and try logout/login again |
| Transactions won't sign | Ensure `algorandAccount` is not null before creating signer |
| Network errors | Verify Algonode endpoints are accessible (may be rate-limited) |
| TypeScript errors | Check you're using the correct types from imports |
## Network Configuration
### Switch to Different Network
In `src/utils/web3auth/web3authConfig.ts`:
**For TestNet (development):**
```typescript
web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_DEVNET // ← Already set
```
**For MainNet (production):**
```typescript
web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE
```
Also update in OpenloginAdapter:
```typescript
network: 'sapphire' // from 'sapphire_devnet'
```
## Environment Variables
```dotenv
# Required
VITE_WEB3AUTH_CLIENT_ID=<your-client-id>
# Optional (Web3Auth uses defaults)
# VITE_WEB3AUTH_NETWORK=sapphire_devnet
# VITE_WEB3AUTH_ENV=development
```
## Troubleshooting Checklist
- [ ] Dependencies installed: `npm install @web3auth/modal @web3auth/base @web3auth/openlogin-adapter`
- [ ] Client ID added to `.env`
- [ ] Web3AuthProvider wraps your app in App.tsx
- [ ] Browser console shows no errors
- [ ] Test with `Web3AuthButton` component first
- [ ] Check network configuration matches your desired network
- [ ] Try logout and login again for persistent issues
## Next Steps After Setup
1. **Add to UI**: Import `Web3AuthButton` in your header/navbar
2. **Test Login**: Click button and complete Google OAuth flow
3. **Verify Address**: See Algorand address in dropdown menu
4. **Try Transactions**: Use `useCreateAsset()` hook to create ASA
5. **Get TestNet Funds**: Visit [Algorand TestNet Dispenser](https://dispenser.algorand-testnet.com)
6. **Go Live**: Switch Web3Auth to SAPPHIRE network for production
## Resources
- Web3Auth Docs: https://web3auth.io/docs
- Web3Auth Dashboard: https://dashboard.web3auth.io
- AlgoKit Utils: https://github.com/algorandfoundation/algokit-utils-ts
- algosdk.js: https://github.com/algorand/js-algorand-sdk

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,9 @@
"@perawallet/connect": "^1.4.1",
"@txnlab/use-wallet": "^4.0.0",
"@txnlab/use-wallet-react": "^4.0.0",
"@web3auth/base": "^9.7.0",
"@web3auth/base-provider": "^9.7.0",
"@web3auth/modal": "^9.7.0",
"algosdk": "^3.0.0",
"daisyui": "^4.0.0",
"lute-connect": "^1.6.3",

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

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

View File

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

View File

@ -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,
}
}

View File

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

View File

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

View File

@ -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, youll hit: TypeError: signer is not a function
*/
/**
* (Legacy) Your old shape (kept only for compatibility if other code uses it).
* AlgorandClient does NOT accept this shape as `signer`.
*/
export interface AlgorandTransactionSigner {
sign: (transactions: Uint8Array[]) => Promise<Uint8Array[]>
sender?: string
}
/**
* ✅ Correct: Convert Web3Auth Algorand account to an AlgorandClient-compatible signer function.
*
* Returns algosdk.TransactionSigner which matches AlgoKit / AlgorandClient expectations:
* (txnGroup, indexesToSign) => Promise<Uint8Array[]>
*
* Use like:
* const signer = createWeb3AuthSigner(algorandAccount)
* await algorand.send.assetCreate({ sender: algorandAccount.address, signer, ... })
*/
export function createWeb3AuthSigner(account: AlgorandAccountFromWeb3Auth): TransactionSigner {
// Web3Auth account should contain a Uint8Array secretKey (Algorand secret key).
// We build an algosdk basic account signer (official helper).
const sk = account.secretKey
const addr = account.address
// If your secretKey is not a Uint8Array for some reason, try to coerce it.
// (This is defensive; ideally it is already Uint8Array.)
const secretKey: Uint8Array =
sk instanceof Uint8Array
? sk
: // @ts-expect-error - allow Array<number> fallback
Array.isArray(sk)
? Uint8Array.from(sk)
: (() => {
throw new Error('Web3Auth secretKey is not a Uint8Array (or number[]). Cannot sign transactions.')
})()
return algosdk.makeBasicAccountTransactionSigner({
addr,
sk: secretKey,
})
}
/**
* (Optional helper) If you *still* want the old object shape for other code,
* this returns { sign, sender } — but DO NOT pass this as AlgorandClient `signer`.
*/
export function createWeb3AuthSignerObject(account: AlgorandAccountFromWeb3Auth): AlgorandTransactionSigner {
const signerFn = createWeb3AuthSigner(account)
// Wrap TransactionSigner into "sign(bytes[])" style ONLY if you need it elsewhere
const sign = async (transactions: Uint8Array[]) => {
// These are already bytes; we need Transaction objects for TransactionSigner.
// This wrapper is best-effort and not recommended for AlgoKit usage.
const txns = transactions.map((b) => algosdk.decodeUnsignedTransaction(b))
const signed = await signerFn(txns, txns.map((_, i) => i))
return signed
}
return {
sign,
sender: account.address,
}
}
/**
* Create a multi-signature compatible signer for Web3Auth accounts
*
* For AlgoKit, you still want the signer to be a TransactionSigner function.
* This returns both the function and some metadata.
*/
export function createWeb3AuthMultiSigSigner(account: AlgorandAccountFromWeb3Auth) {
return {
signer: createWeb3AuthSigner(account), // ✅ TransactionSigner function
sender: account.address,
account, // original account for context
}
}
/**
* Get account information needed for transaction construction
*
* Returns the public key and address in formats needed for
* transaction construction and verification
*/
export function getWeb3AuthAccountInfo(account: AlgorandAccountFromWeb3Auth) {
const decodedAddress = algosdk.decodeAddress(account.address)
return {
address: account.address,
publicKeyBytes: decodedAddress.publicKey,
publicKeyBase64: Buffer.from(decodedAddress.publicKey).toString('base64'),
secretKeyHex: Buffer.from(account.secretKey).toString('hex'),
mnemonicPhrase: account.mnemonic,
}
}
/**
* Verify that a transaction was signed by the Web3Auth account
*
* Useful for verification and testing
*/
export function verifyWeb3AuthSignature(signedTransaction: Uint8Array, account: AlgorandAccountFromWeb3Auth): boolean {
try {
const decodedTxn = algosdk.decodeSignedTransaction(signedTransaction)
// In algosdk, signature can be represented differently depending on type.
// Well attempt to compare signer public key where available.
const txnSigner = decodedTxn.sig?.signers?.[0] ?? decodedTxn.sig?.signer
if (!txnSigner) return false
const decodedAddress = algosdk.decodeAddress(account.address)
return Buffer.from(txnSigner).equals(decodedAddress.publicKey)
} catch (error) {
console.error('Error verifying signature:', error)
return false
}
}
/**
* Get transaction group size details
*/
export function analyzeTransactionGroup(transactions: Uint8Array[]) {
return {
count: transactions.length,
totalSize: transactions.reduce((sum, txn) => sum + txn.length, 0),
averageSize: transactions.reduce((sum, txn) => sum + txn.length, 0) / transactions.length,
}
}
/**
* Format a transaction amount with proper decimals for display
*/
export function formatAmount(amount: bigint | number, decimals: number = 6): string {
const amountStr = amount.toString()
const decimalPoints = decimals
if (amountStr.length <= decimalPoints) {
return `0.${amountStr.padStart(decimalPoints, '0')}`
}
const integerPart = amountStr.slice(0, -decimalPoints)
const decimalPart = amountStr.slice(-decimalPoints)
return `${integerPart}.${decimalPart}`
}
/**
* Parse a user-input amount string to base units (reverse of formatAmount)
*/
export function parseAmount(amount: string, decimals: number = 6): bigint {
const trimmed = amount.trim()
if (!trimmed) {
throw new Error('Amount is required')
}
if (!/^\d+(\.\d+)?$/.test(trimmed)) {
throw new Error('Invalid amount format')
}
const [integerPart = '0', decimalPart = ''] = trimmed.split('.')
if (decimalPart.length > decimals) {
throw new Error(`Too many decimal places (maximum ${decimals})`)
}
const paddedDecimal = decimalPart.padEnd(decimals, '0')
const combined = integerPart + paddedDecimal
return BigInt(combined)
}
/**
* Check if Web3Auth account has sufficient balance for a transaction
*/
export function hasSufficientBalance(balance: bigint, requiredAmount: bigint, minFee: bigint = BigInt(1000)): boolean {
const totalRequired = requiredAmount + minFee
return balance >= totalRequired
}