feat: add unified login ux/ui
This commit is contained in:
@ -1 +0,0 @@
|
|||||||
VITE_WEB3AUTH_CLIENT_ID=your-web3auth-client-id
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Web3Auth UI Demo</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
3528
examples/web3auth-ui-demo/package-lock.json
generated
3528
examples/web3auth-ui-demo/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "web3auth-ui-demo",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "tsc && vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@tokenizerwa/web3auth-algorand": "file:../../packages/web3auth-algorand",
|
|
||||||
"@tokenizerwa/web3auth-algorand-ui": "file:../../packages/web3auth-algorand-ui",
|
|
||||||
"@web3auth/base": "^9.7.0",
|
|
||||||
"@web3auth/base-provider": "^9.7.0",
|
|
||||||
"@web3auth/modal": "^9.7.0",
|
|
||||||
"algosdk": "^3.0.0",
|
|
||||||
"react-icons": "^5.5.0",
|
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/react": "^18.2.11",
|
|
||||||
"@types/react-dom": "^18.2.4",
|
|
||||||
"typescript": "^5.1.6",
|
|
||||||
"vite": "^5.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom/client'
|
|
||||||
import { Web3AuthGoogleButton } from '@tokenizerwa/web3auth-algorand-ui'
|
|
||||||
import { AlgorandWeb3AuthProvider } from '@tokenizerwa/web3auth-algorand'
|
|
||||||
import './styles.css'
|
|
||||||
|
|
||||||
const WEB3AUTH_CLIENT_ID = import.meta.env.VITE_WEB3AUTH_CLIENT_ID || ''
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<div className="page">
|
|
||||||
<header>
|
|
||||||
<h1>Algorand + Web3Auth Demo</h1>
|
|
||||||
<p>Sign in with Google and get an Algorand signer instantly.</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<AlgorandWeb3AuthProvider config={{ clientId: WEB3AUTH_CLIENT_ID }}>
|
|
||||||
<section className="card">
|
|
||||||
<h2>Connect</h2>
|
|
||||||
<p>Use the ready-made Google button from the UI package.</p>
|
|
||||||
<Web3AuthGoogleButton />
|
|
||||||
</section>
|
|
||||||
</AlgorandWeb3AuthProvider>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>,
|
|
||||||
)
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
:root {
|
|
||||||
color-scheme: light;
|
|
||||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
||||||
background: radial-gradient(circle at 20% 20%, #f2f7ff, #ffffff 45%), radial-gradient(circle at 80% 0%, #fef6e8, #ffffff 40%);
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 24px;
|
|
||||||
padding: 48px 16px 72px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
text-align: center;
|
|
||||||
max-width: 720px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 2.4rem;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
header p {
|
|
||||||
margin: 0;
|
|
||||||
color: #475569;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 480px;
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 16px 60px -32px rgba(15, 23, 42, 0.35);
|
|
||||||
padding: 24px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card p {
|
|
||||||
margin: 0;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"module": "ES2020",
|
|
||||||
"lib": ["ES2020", "DOM"],
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"types": ["vite/client"]
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { defineConfig } from 'vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
})
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@tokenizerwa/web3auth-algorand-ui",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Prebuilt React components for the @tokenizerwa/web3auth-algorand hook.",
|
|
||||||
"license": "MIT",
|
|
||||||
"type": "module",
|
|
||||||
"main": "./src/index.ts",
|
|
||||||
"module": "./src/index.ts",
|
|
||||||
"types": "./src/index.ts",
|
|
||||||
"exports": {
|
|
||||||
".": "./src/index.ts"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"sideEffects": false,
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=18.0.0",
|
|
||||||
"@tokenizerwa/web3auth-algorand": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc -p tsconfig.json",
|
|
||||||
"clean": "rimraf dist"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"react-icons": "^5.5.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^5.1.6",
|
|
||||||
"rimraf": "^5.0.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,235 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { AiOutlineLoading3Quarters } from 'react-icons/ai'
|
|
||||||
import { FaCheck, FaCopy, FaGoogle } from 'react-icons/fa'
|
|
||||||
import { useWeb3Auth } from '@tokenizerwa/web3auth-algorand'
|
|
||||||
|
|
||||||
export function Web3AuthGoogleButton() {
|
|
||||||
const { isConnected, isLoading, error, algorandAccount, userInfo, login, logout } = useWeb3Auth()
|
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
|
||||||
const [copied, setCopied] = useState(false)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const getAddressString = (): string => {
|
|
||||||
if (!algorandAccount?.address) return ''
|
|
||||||
if (typeof algorandAccount.address === 'object' && algorandAccount.address !== null) {
|
|
||||||
if ('toString' in algorandAccount.address && typeof algorandAccount.address.toString === 'function') {
|
|
||||||
return algorandAccount.address.toString()
|
|
||||||
}
|
|
||||||
if ('addr' in algorandAccount.address) {
|
|
||||||
return String(algorandAccount.address.addr)
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(algorandAccount.address)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
|
||||||
try {
|
|
||||||
await login()
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Login error:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
try {
|
|
||||||
await logout()
|
|
||||||
setIsDropdownOpen(false)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Logout error:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCopyAddress = () => {
|
|
||||||
const address = getAddressString()
|
|
||||||
if (!address) return
|
|
||||||
|
|
||||||
navigator.clipboard.writeText(address)
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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">
|
|
||||||
{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">
|
|
||||||
{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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Web3AuthGoogleButton
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from './Web3AuthGoogleButton'
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"module": "ES2020",
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"types": ["react"]
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"]
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
# Publishing to npm
|
|
||||||
|
|
||||||
This repo already has the package source under `packages/web3auth-algorand` and the UI add-on under `packages/web3auth-algorand-ui`. Both use plain TypeScript builds (no bundler).
|
|
||||||
|
|
||||||
## Prereqs
|
|
||||||
- npm account with 2FA configured (recommended).
|
|
||||||
- Access to the `@tokenizerwa` npm scope (or change `name` in each `package.json` to your scope).
|
|
||||||
- Node 20+.
|
|
||||||
|
|
||||||
## Build locally
|
|
||||||
```bash
|
|
||||||
cd packages/web3auth-algorand
|
|
||||||
npm install
|
|
||||||
npm run build # emits dist/
|
|
||||||
|
|
||||||
cd ../web3auth-algorand-ui
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
To double-check what will be published:
|
|
||||||
```bash
|
|
||||||
npm pack --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
## Publish steps
|
|
||||||
1) Set the version you want in each `package.json` (`version` field). Use semver.
|
|
||||||
2) Make sure `files` includes `dist` (already set) and that `dist` exists (run `npm run build`).
|
|
||||||
3) Log in once if needed: `npm login`.
|
|
||||||
4) From each package folder, publish:
|
|
||||||
```bash
|
|
||||||
cd packages/web3auth-algorand
|
|
||||||
npm publish --access public
|
|
||||||
|
|
||||||
cd ../web3auth-algorand-ui
|
|
||||||
npm publish --access public
|
|
||||||
```
|
|
||||||
|
|
||||||
## Releasing updates
|
|
||||||
- Bump the version in the package you changed.
|
|
||||||
- Rebuild that package.
|
|
||||||
- Publish only the changed package(s).
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
- 403 errors: ensure your npm token owns the scope or rename the package.
|
|
||||||
- Missing files in npm: verify `npm pack --dry-run` includes `dist` and `package.json` points to `dist/index.js`.
|
|
||||||
- Type errors: run `npm run build` to catch missing types before publishing.
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
# @tokenizerwa/web3auth-algorand
|
|
||||||
|
|
||||||
Algorand + Web3Auth hook and provider that turns a Google login into an Algorand signer. Ships the Web3Auth wiring, Algorand account derivation, and signer utilities so you can drop it into any React app.
|
|
||||||
|
|
||||||
## What’s inside
|
|
||||||
- `AlgorandWeb3AuthProvider` – wraps your app and bootstraps Web3Auth + Algorand account derivation.
|
|
||||||
- `useWeb3Auth()` – access connection state, Google profile info, and the derived Algorand account.
|
|
||||||
- `createWeb3AuthSigner(...)` – get an AlgoKit-compatible `TransactionSigner`.
|
|
||||||
- Helper utilities for balance/amount formatting and account inspection.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
```bash
|
|
||||||
npm install @tokenizerwa/web3auth-algorand
|
|
||||||
# peer deps you likely already have
|
|
||||||
npm install react @web3auth/modal
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quick start
|
|
||||||
```tsx
|
|
||||||
import { AlgorandWeb3AuthProvider, useWeb3Auth, createWeb3AuthSigner } from '@tokenizerwa/web3auth-algorand'
|
|
||||||
|
|
||||||
const WEB3AUTH_CLIENT_ID = import.meta.env.VITE_WEB3AUTH_CLIENT_ID
|
|
||||||
|
|
||||||
function ConnectButton() {
|
|
||||||
const { isConnected, algorandAccount, login, logout, isLoading } = useWeb3Auth()
|
|
||||||
|
|
||||||
if (isConnected && algorandAccount) {
|
|
||||||
const signer = createWeb3AuthSigner(algorandAccount)
|
|
||||||
// use signer with AlgoKit / algosdk
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button onClick={isConnected ? logout : login} disabled={isLoading}>
|
|
||||||
{isConnected ? 'Disconnect' : 'Sign in with Google'}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function App() {
|
|
||||||
return (
|
|
||||||
<AlgorandWeb3AuthProvider
|
|
||||||
config={{
|
|
||||||
clientId: WEB3AUTH_CLIENT_ID,
|
|
||||||
// optional overrides:
|
|
||||||
// web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_MAINNET,
|
|
||||||
// chainConfig: { rpcTarget: 'https://mainnet-api.algonode.cloud', displayName: 'Algorand MainNet' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ConnectButton />
|
|
||||||
</AlgorandWeb3AuthProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## UI add-on (optional)
|
|
||||||
If you want a plug-and-play Google button and dropdown, install the UI companion:
|
|
||||||
```bash
|
|
||||||
npm install @tokenizerwa/web3auth-algorand-ui
|
|
||||||
```
|
|
||||||
```tsx
|
|
||||||
import { AlgorandWeb3AuthProvider } from '@tokenizerwa/web3auth-algorand'
|
|
||||||
import { Web3AuthGoogleButton } from '@tokenizerwa/web3auth-algorand-ui'
|
|
||||||
|
|
||||||
<AlgorandWeb3AuthProvider config={{ clientId: WEB3AUTH_CLIENT_ID }}>
|
|
||||||
<Web3AuthGoogleButton />
|
|
||||||
</AlgorandWeb3AuthProvider>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- You need a Web3Auth client id from the Web3Auth dashboard.
|
|
||||||
- Default network is Sapphire Devnet + Algorand TestNet RPC; override via `config.chainConfig` and `config.web3AuthNetwork`.
|
|
||||||
- The provider must wrap any component that calls `useWeb3Auth`.
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@tokenizerwa/web3auth-algorand",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Algorand + Web3Auth React provider, hooks, and signer helpers for Google login flows.",
|
|
||||||
"license": "MIT",
|
|
||||||
"type": "module",
|
|
||||||
"main": "./src/index.ts",
|
|
||||||
"module": "./src/index.ts",
|
|
||||||
"types": "./src/index.ts",
|
|
||||||
"exports": {
|
|
||||||
".": "./src/index.ts"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"sideEffects": false,
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=18.0.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc -p tsconfig.json",
|
|
||||||
"clean": "rimraf dist"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@web3auth/base": "^9.7.0",
|
|
||||||
"@web3auth/base-provider": "^9.7.0",
|
|
||||||
"@web3auth/modal": "^9.7.0",
|
|
||||||
"algosdk": "^3.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^5.1.6",
|
|
||||||
"rimraf": "^5.0.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export * from './provider'
|
|
||||||
export * from './types'
|
|
||||||
export * from './utils/algorandAdapter'
|
|
||||||
export * from './utils/web3authIntegration'
|
|
||||||
export * from './web3authConfig'
|
|
||||||
@ -1,187 +0,0 @@
|
|||||||
import { Web3Auth } from '@web3auth/modal'
|
|
||||||
import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
|
|
||||||
import { AlgorandAccountFromWeb3Auth, getAlgorandAccount } from './utils/algorandAdapter'
|
|
||||||
import { getWeb3AuthUserInfo, initWeb3Auth, logoutFromWeb3Auth, Web3AuthUserInfo } from './web3authConfig'
|
|
||||||
import { type AlgorandWeb3AuthConfig } from './types'
|
|
||||||
|
|
||||||
export interface Web3AuthContextType {
|
|
||||||
isConnected: boolean
|
|
||||||
isLoading: boolean
|
|
||||||
isInitialized: boolean
|
|
||||||
error: string | 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 AlgorandWeb3AuthProvider({ config, children }: { config: AlgorandWeb3AuthConfig; 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 [web3AuthInstance, setWeb3AuthInstance] = useState<Web3Auth | null>(null)
|
|
||||||
const [algorandAccount, setAlgorandAccount] = useState<AlgorandAccountFromWeb3Auth | null>(null)
|
|
||||||
const [userInfo, setUserInfo] = useState<Web3AuthUserInfo | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const initializeWeb3Auth = async () => {
|
|
||||||
try {
|
|
||||||
if (!config?.clientId) {
|
|
||||||
setError('Web3Auth clientId is required')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
const web3auth = await initWeb3Auth(config)
|
|
||||||
|
|
||||||
setWeb3AuthInstance(web3auth)
|
|
||||||
|
|
||||||
if (web3auth.status === 'connected' && web3auth.provider) {
|
|
||||||
setIsConnected(true)
|
|
||||||
try {
|
|
||||||
const account = await getAlgorandAccount(web3auth.provider)
|
|
||||||
setAlgorandAccount(account)
|
|
||||||
} catch (err) {
|
|
||||||
setError('Failed to derive Algorand account. Please reconnect.')
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const userInformation = await getWeb3AuthUserInfo()
|
|
||||||
if (userInformation) {
|
|
||||||
setUserInfo(userInformation)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch user info:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsInitialized(true)
|
|
||||||
} catch (err) {
|
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Failed to initialize Web3Auth'
|
|
||||||
console.error('WEB3AUTH: Initialization error:', err)
|
|
||||||
setError(errorMessage)
|
|
||||||
setIsInitialized(true)
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeWeb3Auth()
|
|
||||||
}, [config])
|
|
||||||
|
|
||||||
const login = async () => {
|
|
||||||
if (!web3AuthInstance) {
|
|
||||||
setError('Web3Auth not initialized')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isInitialized) {
|
|
||||||
setError('Web3Auth is still initializing, please try again')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsLoading(true)
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
const web3authProvider = await web3AuthInstance.connect()
|
|
||||||
|
|
||||||
if (!web3authProvider) {
|
|
||||||
throw new Error('Failed to connect Web3Auth provider')
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsConnected(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const account = await getAlgorandAccount(web3authProvider)
|
|
||||||
setAlgorandAccount(account)
|
|
||||||
} catch (err) {
|
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Failed to derive Algorand account'
|
|
||||||
setError(errorMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const userInformation = await getWeb3AuthUserInfo()
|
|
||||||
if (userInformation) {
|
|
||||||
setUserInfo(userInformation)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('LOGIN: Failed to fetch user info:', err)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Login failed'
|
|
||||||
console.error('LOGIN: Error:', err)
|
|
||||||
setError(errorMessage)
|
|
||||||
setIsConnected(false)
|
|
||||||
setAlgorandAccount(null)
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const logout = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true)
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
await logoutFromWeb3Auth()
|
|
||||||
|
|
||||||
setIsConnected(false)
|
|
||||||
setAlgorandAccount(null)
|
|
||||||
setUserInfo(null)
|
|
||||||
} catch (err) {
|
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Logout failed'
|
|
||||||
console.error('LOGOUT: Error:', err)
|
|
||||||
setError(errorMessage)
|
|
||||||
setIsConnected(false)
|
|
||||||
setAlgorandAccount(null)
|
|
||||||
setUserInfo(null)
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshUserInfo = async () => {
|
|
||||||
try {
|
|
||||||
const userInformation = await getWeb3AuthUserInfo()
|
|
||||||
if (userInformation) {
|
|
||||||
setUserInfo(userInformation)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('REFRESH: Failed:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const value: Web3AuthContextType = {
|
|
||||||
isConnected,
|
|
||||||
isLoading,
|
|
||||||
isInitialized,
|
|
||||||
error,
|
|
||||||
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 an AlgorandWeb3AuthProvider')
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import { CHAIN_NAMESPACES, WEB3AUTH_NETWORK, type IWeb3AuthConfiguration } from '@web3auth/base'
|
|
||||||
import type { CommonPrivateKeyProviderConfig } from '@web3auth/base-provider'
|
|
||||||
|
|
||||||
export type { AlgorandAccountFromWeb3Auth } from './utils/algorandAdapter'
|
|
||||||
|
|
||||||
export interface AlgorandChainConfig extends CommonPrivateKeyProviderConfig['config']['chainConfig'] {}
|
|
||||||
|
|
||||||
export interface AlgorandWeb3AuthConfig {
|
|
||||||
clientId: string
|
|
||||||
web3AuthNetwork?: WEB3AUTH_NETWORK
|
|
||||||
chainConfig?: AlgorandChainConfig
|
|
||||||
uiConfig?: IWeb3AuthConfiguration['uiConfig']
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_ALGORAND_CHAIN_CONFIG: AlgorandChainConfig = {
|
|
||||||
chainNamespace: CHAIN_NAMESPACES.OTHER,
|
|
||||||
chainId: '0x1',
|
|
||||||
rpcTarget: 'https://testnet-api.algonode.cloud',
|
|
||||||
displayName: 'Algorand TestNet',
|
|
||||||
blockExplorerUrl: 'https://testnet.algoexplorer.io',
|
|
||||||
ticker: 'ALGO',
|
|
||||||
tickerName: 'Algorand',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_UI_CONFIG: IWeb3AuthConfiguration['uiConfig'] = {
|
|
||||||
appName: 'Algorand Web3Auth',
|
|
||||||
mode: 'light',
|
|
||||||
loginMethodsOrder: ['google'],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_WEB3AUTH_NETWORK = WEB3AUTH_NETWORK.SAPPHIRE_DEVNET
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
import { IProvider } from '@web3auth/base'
|
|
||||||
import algosdk from 'algosdk'
|
|
||||||
|
|
||||||
export interface AlgorandAccountFromWeb3Auth {
|
|
||||||
address: string
|
|
||||||
mnemonic: string
|
|
||||||
secretKey: Uint8Array
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAlgorandAccount(provider: IProvider): Promise<AlgorandAccountFromWeb3Auth> {
|
|
||||||
if (!provider) {
|
|
||||||
throw new Error('Provider is required to derive Algorand account')
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const privKey = await provider.request({
|
|
||||||
method: 'private_key',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!privKey || typeof privKey !== 'string') {
|
|
||||||
throw new Error('Failed to retrieve private key from Web3Auth provider')
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanHexKey = privKey.startsWith('0x') ? privKey.slice(2) : privKey
|
|
||||||
const privateKeyBytes = new Uint8Array(Buffer.from(cleanHexKey, 'hex'))
|
|
||||||
const ed25519SecretKey = privateKeyBytes.slice(0, 32)
|
|
||||||
const mnemonic = algosdk.secretKeyToMnemonic(ed25519SecretKey)
|
|
||||||
const accountFromMnemonic = algosdk.mnemonicToSecretKey(mnemonic)
|
|
||||||
|
|
||||||
return {
|
|
||||||
address: accountFromMnemonic.addr,
|
|
||||||
mnemonic: mnemonic,
|
|
||||||
secretKey: accountFromMnemonic.sk,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw new Error(`Failed to derive Algorand account from Web3Auth: ${error.message}`)
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createAlgorandSigner(secretKey: Uint8Array) {
|
|
||||||
return async (transactions: Uint8Array[]): Promise<Uint8Array[]> => {
|
|
||||||
const signedTxns: Uint8Array[] = []
|
|
||||||
|
|
||||||
for (const txn of transactions) {
|
|
||||||
try {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidAlgorandAddress(address: string): boolean {
|
|
||||||
if (!address || typeof address !== 'string') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
algosdk.decodeAddress(address)
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPublicKeyFromSecretKey(secretKey: Uint8Array): Uint8Array {
|
|
||||||
if (secretKey.length !== 64) {
|
|
||||||
throw new Error(`Invalid secret key length: expected 64 bytes, got ${secretKey.length}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return secretKey.slice(32)
|
|
||||||
}
|
|
||||||
@ -1,125 +0,0 @@
|
|||||||
import algosdk, { TransactionSigner } from 'algosdk'
|
|
||||||
import { type AlgorandAccountFromWeb3Auth } from './algorandAdapter'
|
|
||||||
|
|
||||||
export interface AlgorandTransactionSigner {
|
|
||||||
sign: (transactions: Uint8Array[]) => Promise<Uint8Array[]>
|
|
||||||
sender?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createWeb3AuthSigner(account: AlgorandAccountFromWeb3Auth): TransactionSigner {
|
|
||||||
const sk = account.secretKey
|
|
||||||
const addr = account.address
|
|
||||||
|
|
||||||
const secretKey: Uint8Array =
|
|
||||||
sk instanceof Uint8Array
|
|
||||||
? sk
|
|
||||||
: 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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createWeb3AuthSignerObject(account: AlgorandAccountFromWeb3Auth): AlgorandTransactionSigner {
|
|
||||||
const signerFn = createWeb3AuthSigner(account)
|
|
||||||
|
|
||||||
const sign = async (transactions: Uint8Array[]) => {
|
|
||||||
const txns = transactions.map((b) => algosdk.decodeUnsignedTransaction(b))
|
|
||||||
const signed = await signerFn(txns, txns.map((_, i) => i))
|
|
||||||
return signed
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
sign,
|
|
||||||
sender: account.address,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createWeb3AuthMultiSigSigner(account: AlgorandAccountFromWeb3Auth) {
|
|
||||||
return {
|
|
||||||
signer: createWeb3AuthSigner(account),
|
|
||||||
sender: account.address,
|
|
||||||
account,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function verifyWeb3AuthSignature(signedTransaction: Uint8Array, account: AlgorandAccountFromWeb3Auth): boolean {
|
|
||||||
try {
|
|
||||||
const decodedTxn = algosdk.decodeSignedTransaction(signedTransaction)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasSufficientBalance(balance: bigint, requiredAmount: bigint, minFee: bigint = BigInt(1000)): boolean {
|
|
||||||
const totalRequired = requiredAmount + minFee
|
|
||||||
return balance >= totalRequired
|
|
||||||
}
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
import { CHAIN_NAMESPACES, IProvider, WEB3AUTH_NETWORK, type IWeb3AuthConfiguration } from '@web3auth/base'
|
|
||||||
import { CommonPrivateKeyProvider, type CommonPrivateKeyProviderConfig } from '@web3auth/base-provider'
|
|
||||||
import { Web3Auth, type Web3AuthOptions } from '@web3auth/modal'
|
|
||||||
import {
|
|
||||||
DEFAULT_ALGORAND_CHAIN_CONFIG,
|
|
||||||
DEFAULT_UI_CONFIG,
|
|
||||||
DEFAULT_WEB3AUTH_NETWORK,
|
|
||||||
type AlgorandWeb3AuthConfig,
|
|
||||||
} from './types'
|
|
||||||
|
|
||||||
let web3authInstance: Web3Auth | null = null
|
|
||||||
|
|
||||||
const sanitizeChainConfig = (chainConfig?: CommonPrivateKeyProviderConfig['config']['chainConfig']) => {
|
|
||||||
if (!chainConfig) return DEFAULT_ALGORAND_CHAIN_CONFIG
|
|
||||||
return {
|
|
||||||
...DEFAULT_ALGORAND_CHAIN_CONFIG,
|
|
||||||
...chainConfig,
|
|
||||||
chainNamespace: chainConfig.chainNamespace ?? CHAIN_NAMESPACES.OTHER,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sanitizeUiConfig = (uiConfig?: IWeb3AuthConfiguration['uiConfig']) => ({
|
|
||||||
...DEFAULT_UI_CONFIG,
|
|
||||||
...uiConfig,
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function initWeb3Auth(config: AlgorandWeb3AuthConfig): Promise<Web3Auth> {
|
|
||||||
if (web3authInstance) {
|
|
||||||
return web3authInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
const chainConfig = sanitizeChainConfig(config.chainConfig)
|
|
||||||
const uiConfig = sanitizeUiConfig(config.uiConfig)
|
|
||||||
const web3AuthNetwork = config.web3AuthNetwork ?? DEFAULT_WEB3AUTH_NETWORK
|
|
||||||
|
|
||||||
const privateKeyProvider = new CommonPrivateKeyProvider({
|
|
||||||
config: {
|
|
||||||
chainConfig,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const web3AuthConfig: Web3AuthOptions = {
|
|
||||||
clientId: config.clientId,
|
|
||||||
web3AuthNetwork,
|
|
||||||
privateKeyProvider,
|
|
||||||
uiConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
web3authInstance = new Web3Auth(web3AuthConfig)
|
|
||||||
await web3authInstance.initModal()
|
|
||||||
|
|
||||||
return web3authInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWeb3AuthInstance(): Web3Auth | null {
|
|
||||||
return web3authInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWeb3AuthProvider(): IProvider | null {
|
|
||||||
return web3authInstance?.provider || null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isWeb3AuthConnected(): boolean {
|
|
||||||
return web3authInstance?.status === 'connected'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Web3AuthUserInfo {
|
|
||||||
email?: string
|
|
||||||
name?: string
|
|
||||||
profileImage?: string
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getWeb3AuthUserInfo(): Promise<Web3AuthUserInfo | null> {
|
|
||||||
if (!web3authInstance || !isWeb3AuthConnected()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const userInfo = await web3authInstance.getUserInfo()
|
|
||||||
return userInfo as Web3AuthUserInfo
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function logoutFromWeb3Auth(): Promise<void> {
|
|
||||||
if (!web3authInstance) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await web3authInstance.logout()
|
|
||||||
} finally {
|
|
||||||
web3authInstance = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"module": "ES2020",
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"types": ["react"]
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user