WIP update - Web3Auth x Wallet Connect signing integration
This commit is contained in:
@ -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:
|
||||
|
||||
407
projects/TokenizeRWATemplate-frontend/WEB3AUTH_INTEGRATION.md
Normal file
407
projects/TokenizeRWATemplate-frontend/WEB3AUTH_INTEGRATION.md
Normal 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
|
||||
@ -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
|
||||
2541
projects/TokenizeRWATemplate-frontend/package-lock.json
generated
2541
projects/TokenizeRWATemplate-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
||||
@ -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,6 +54,7 @@ export default function App() {
|
||||
|
||||
return (
|
||||
<SnackbarProvider maxSnack={3}>
|
||||
<Web3AuthProvider>
|
||||
<WalletProvider manager={walletManager}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
@ -63,6 +65,7 @@ export default function App() {
|
||||
</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