diff --git a/front/.env b/front/.env index d305c2d..6192b29 100644 --- a/front/.env +++ b/front/.env @@ -1,5 +1,5 @@ -VITE_SERVER_BASE_URL=http://localhost:4000 -VITE_WS_URL=ws://localhost:8081/ws +VITE_WALLET_SERVER_BASE_URL=http://localhost:4000 +VITE_WALLET_WS_URL=ws://localhost:8081/ws VITE_NODE_BASE_URL=http://localhost:4321 VITE_INDEXER_BASE_URL=http://localhost:4321 VITE_TX_EXPLORER_URL=http://localhost:8000 diff --git a/front/.env.production b/front/.env.production index fab04c2..7f51aad 100644 --- a/front/.env.production +++ b/front/.env.production @@ -1,5 +1,5 @@ -VITE_SERVER_BASE_URL=https://wallet.testnet.hyle.eu -VITE_WS_URL=wss://wallet.testnet.hyle.eu/ws +VITE_WALLET_SERVER_BASE_URL=https://wallet.testnet.hyle.eu +VITE_WALLET_WS_URL=wss://wallet.testnet.hyle.eu/ws VITE_NODE_BASE_URL=https://node.testnet.hyle.eu VITE_INDEXER_BASE_URL=https://indexer.testnet.hyle.eu VITE_TX_EXPLORER_URL=https://hyleou.hyle.eu/ diff --git a/front/bun.lockb b/front/bun.lockb index 79dbcad..b09b211 100755 Binary files a/front/bun.lockb and b/front/bun.lockb differ diff --git a/front/package.json b/front/package.json index 3cb915d..81d2535 100644 --- a/front/package.json +++ b/front/package.json @@ -1,5 +1,5 @@ { - "name": "hyle-wallet", + "name": "hyle-wallet-front", "private": true, "version": "0.0.0", "type": "module", @@ -17,24 +17,25 @@ "elliptic": "^6.6.1", "hyle": "^0.2.5", "hyle-check-secret": "^0.3.2", + "hyle-wallet": "file:../hyle-wallet", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-router-dom": "^7.5.0" + "react-router-dom": "^7.6.0" }, "devDependencies": { - "@eslint/js": "^9.24.0", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", - "@vitejs/plugin-react": "^4.4.0", + "@eslint/js": "^9.26.0", + "@types/react": "^19.1.4", + "@types/react-dom": "^19.1.5", + "@vitejs/plugin-react": "^4.4.1", "autoprefixer": "^10.4.21", - "eslint": "^9.24.0", + "eslint": "^9.26.0", "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^15.15.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.1.0", "postcss": "^8.5.3", - "tailwindcss": "^4.1.4", - "typescript": "~5.7.3", - "typescript-eslint": "^8.30.1", - "vite": "^6.2.6" + "tailwindcss": "^4.1.6", + "typescript": "~5.8.3", + "typescript-eslint": "^8.32.1", + "vite": "^6.3.5" } } diff --git a/front/src/App.tsx b/front/src/App.tsx index 83e7d3f..80ebac3 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -1,141 +1,90 @@ -import { useState, useEffect } from 'react'; -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { useEffect } from 'react'; +import { BrowserRouter, Routes, Route, useNavigate } from 'react-router-dom'; import './App.css'; -import { Balance } from './components/wallet/Balance'; -import { Send } from './components/wallet/Send'; -import { History } from './components/wallet/History'; -import { SessionKeys } from './components/wallet/SessionKeys'; -import { WalletLayout } from './components/layout/WalletLayout'; -import { Wallet, Transaction } from './types/wallet'; -import { indexerService } from './services/IndexerService'; -import { useConfig } from './hooks/useConfig'; -import { AppEvent, webSocketService } from './services/WebSocketService'; -import { ConnectWallet } from './components/connect/ConnectWallet'; -import { ConnectWalletExamples } from './components/connect/ConnectWalletExamples'; - -function App() { - const [wallet, setWallet] = useState(null); - const [balance, setBalance] = useState(0); - const [transactions, setTransactions] = useState([]); +import { WalletShowcase } from './components/WalletShowcase'; +import { useWalletBalance } from './hooks/useWalletBalance'; +import { useWalletTransactions } from './hooks/useWalletTransactions'; +import { useWebSocketConnection } from './hooks/useWebSocketConnection'; +import { getPublicRoutes, getProtectedRoutes, ROUTES } from './routes/routes'; +import { WalletProvider, useWallet } from 'hyle-wallet'; +import { useConfig } from 'hyle-wallet'; +import { LoadingErrorState } from './components/common/LoadingErrorState'; + +function AppContent() { const { isLoading: isLoadingConfig, error: configError } = useConfig(); - - // Function to fetch balance - const fetchBalance = async () => { - if (wallet) { - const balance = await indexerService.getBalance(wallet.address); - setBalance(balance); - } - }; - - // Function to fetch transaction history - const fetchTransactions = async () => { - if (wallet) { - const transactions = await indexerService.getTransactionHistory(wallet.address); - setTransactions(transactions); + const { wallet, logout, stage, error } = useWallet(); + const navigate = useNavigate(); + + // Use custom hooks + const { balance, fetchBalance } = useWalletBalance(wallet?.address); + const { + transactions, + handleTxEvent + } = useWalletTransactions(wallet?.address); + + // Setup WebSocket connection + useWebSocketConnection(wallet?.address, event => { + handleTxEvent(event); + // If transaction was successful, update balance + if (event.tx.status === 'Success') { + fetchBalance(); } - }; + }); - // Initialize WebSocket connection when wallet is set + // Redirect back to root on auth settlement error and show message via state useEffect(() => { - if (wallet) { - webSocketService.connect(wallet.address); - - const handleTxEvent = async (event: AppEvent['TxEvent']) => { - console.log('Received transaction event:', event); - if (event.tx.status === 'Success') { - // Update balance - await fetchBalance(); - } - - // Update transactions - const newTransaction: Transaction = event.tx; - - setTransactions(prevTransactions => { - const existingIndex = prevTransactions.findIndex(tx => tx.id === newTransaction.id); - if (existingIndex !== -1) { - console.log('Updating existing transaction'); - // Update existing transaction in-place - const updatedTransactions = [...prevTransactions]; - updatedTransactions[existingIndex] = newTransaction; - return updatedTransactions; - } else { - console.log('Adding new transaction'); - // Add new transaction at the beginning of the list - return [newTransaction, ...prevTransactions]; - } - }); - }; - - const unsubscribeTxEvents = webSocketService.subscribeToTxEvents(handleTxEvent); - - // Initial data fetch - fetchBalance(); - fetchTransactions(); - - return () => { - unsubscribeTxEvents(); - webSocketService.disconnect(); - }; + if (stage === 'error') { + navigate(ROUTES.ROOT, { state: { authError: error } }); } - }, [wallet]); - - const handleWalletLoggedIn = (loggedInWallet: Wallet) => { - setWallet(loggedInWallet); - localStorage.setItem('wallet', JSON.stringify(loggedInWallet)); - }; + }, [stage, error, navigate]); const handleLogout = () => { - setWallet(null); - localStorage.removeItem('wallet'); + logout(); + navigate(ROUTES.ROOT); }; - // Check if wallet exists in localStorage on component mount - useEffect(() => { - const storedWallet = localStorage.getItem('wallet'); - if (storedWallet) { - setWallet(JSON.parse(storedWallet)); - } - }, []); - if (isLoadingConfig) { - return
Loading configuration...
; + return ; } if (configError) { - return
Error loading configuration: {configError}
; + return ; } - return ( - - {/* Global connect wallet modal (renders its own button) */} - {!wallet && ( -
-
-

Wallet Integration

-

Connect to your wallet using a fully customizable modal component.

-
- -
- )} - - :
- } /> + // If wallet is not connected, show the showcase screen + if (!wallet) { + return ; + } - {wallet && ( - }> - } /> - } /> - } /> - } /> - } /> - - )} + // Generate routes based on authentication state + const publicRoutes = getPublicRoutes(); + const protectedRoutes = getProtectedRoutes(wallet, balance, transactions, handleLogout); + const allRoutes = [...publicRoutes, ...protectedRoutes]; + + return {allRoutes.map(route => + + {route.children?.map(childRoute => ( + + ))} + + )}; +} - } /> - +export default function App() { + return ( + + + + ); } - -export default App; diff --git a/front/src/components/WalletShowcase.tsx b/front/src/components/WalletShowcase.tsx new file mode 100644 index 0000000..623c1c4 --- /dev/null +++ b/front/src/components/WalletShowcase.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { HyleWallet } from 'hyle-wallet'; +import { useLocation } from 'react-router-dom'; + +type ProviderOption = 'password' | 'google' | 'github'; + +interface WalletShowcaseProps { + providers: ProviderOption[]; +} + +export const WalletShowcase: React.FC = ({ providers }) => { + const location = useLocation(); + const authError = (location.state as any)?.authError as string | undefined; + + return ( +
+
+

Wallet Integration

+

Connect to your wallet using the default modal or your own custom UI.

+
+ {authError &&
{authError}
} + +
+ ); +}; \ No newline at end of file diff --git a/front/src/components/auth/CreateWallet.tsx b/front/src/components/auth/CreateWallet.tsx deleted file mode 100644 index bf1b885..0000000 --- a/front/src/components/auth/CreateWallet.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { useState } from 'react'; -import { Buffer } from 'buffer'; -import { register, Wallet, walletContractName } from '../../types/wallet'; -import { build_proof_transaction, build_blob as check_secret_blob, register_contract } from 'hyle-check-secret'; -import { BlobTransaction } from 'hyle'; -import { nodeService } from '../../services/NodeService'; -import { webSocketService } from '../../services/WebSocketService'; - -interface CreateWalletProps { - onWalletCreated: (wallet: Wallet) => void; -} - -export const CreateWallet = ({ onWalletCreated }: CreateWalletProps) => { - const [username, setUsername] = useState('bob'); - const [password, setPassword] = useState('password123'); - const [confirmPassword, setConfirmPassword] = useState('password123'); - const [error, setError] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [status, setStatus] = useState(''); - const [transactionHash, setTransactionHash] = useState(''); - - const handleCreateWallet = async () => { - setError(''); - setIsLoading(true); - setStatus('Validating input...'); - - try { - if (!username || !password || !confirmPassword) { - setError('Please fill in all fields'); - return; - } - - if (password !== confirmPassword) { - setError('Passwords do not match'); - return; - } - - if (password.length < 8) { - setError('Password must be at least 8 characters long'); - return; - } - - setStatus('Generating wallet credentials...'); - const identity = `${username}@${walletContractName}`; - const blob0 = await check_secret_blob(identity, password); - const hash = Buffer.from(blob0.data).toString('hex'); - const blob1 = register(username, Date.now(), hash); - - const blobTx: BlobTransaction = { - identity, - blobs: [blob0, blob1], - } - - setStatus('Sending transaction...'); - await register_contract(nodeService.client as any); - const tx_hash = await nodeService.client.sendBlobTx(blobTx); - setTransactionHash(tx_hash); - - setStatus('Building proof transaction (this may take a few moments)...'); - const proofTx = await build_proof_transaction( - identity, - password, - tx_hash, - 0, - blobTx.blobs.length, - ); - - setStatus('Sending proof transaction...'); - await nodeService.client.sendProofTx(proofTx); - setStatus('Waiting for wallet creation confirmation...'); - - try { - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - unsubscribeWalletEvents(); - reject(new Error('Wallet creation timed out')); - }, 60000); - - webSocketService.connect(identity); - const unsubscribeWalletEvents = webSocketService.subscribeToWalletEvents((event) => { - if (event.event.startsWith('Successfully registered identity for account')) { - clearTimeout(timeout); - unsubscribeWalletEvents(); - webSocketService.disconnect(); - resolve(event); - } else { - clearTimeout(timeout); - unsubscribeWalletEvents(); - webSocketService.disconnect(); - reject(new Error('Wallet creation failed: ' + event.event)); - } - }); - }); - - setStatus('Wallet created successfully!'); - - const wallet: Wallet = { - username, - address: identity - }; - - onWalletCreated(wallet); - } catch (error) { - throw new Error(error instanceof Error ? error.message : 'Wallet creation failed'); - } - } catch (error) { - setError('Failed to create wallet. Please try again.'); - console.error('Error creating wallet:', error); - } finally { - setIsLoading(false); - } - }; - - return ( -
-

Create Your Wallet

-
-
- - setUsername(e.target.value)} - placeholder="Choose a username" - className="form-input" - /> -
-
- - setPassword(e.target.value)} - placeholder="Create a password" - className="form-input" - /> -
-
- - setConfirmPassword(e.target.value)} - placeholder="Confirm your password" - className="form-input" - /> -
- - {error &&
{error}
} - {status &&
{status}
} - - - - {transactionHash && ( - - )} -
-
- ); -}; diff --git a/front/src/components/auth/LoginWallet.tsx b/front/src/components/auth/LoginWallet.tsx deleted file mode 100644 index 0a942c2..0000000 --- a/front/src/components/auth/LoginWallet.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { useState } from 'react'; -import { verifyIdentity, Wallet, walletContractName } from '../../types/wallet'; -import { nodeService } from '../../services/NodeService'; -import { webSocketService } from '../../services/WebSocketService'; -import { build_proof_transaction, build_blob as check_secret_blob } from 'hyle-check-secret'; -import { BlobTransaction } from 'hyle'; - -interface LoginWalletProps { - onWalletLoggedIn: (wallet: Wallet) => void; -} - -export const LoginWallet = ({ onWalletLoggedIn }: LoginWalletProps) => { - const [username, setUsername] = useState('bob'); - const [password, setPassword] = useState('password123'); - const [error, setError] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [status, setStatus] = useState(''); - const [transactionHash, setTransactionHash] = useState(''); - - const handleLogin = async () => { - setError(''); - setIsLoading(true); - setStatus('Validating credentials...'); - - if (!username || !password) { - setError('Please fill in all fields'); - setIsLoading(false); - return; - } - const identity = `${username}@${walletContractName}`; - const blob0 = await check_secret_blob(identity, password); - const blob1 = verifyIdentity(username, Date.now()); - - const blobTx: BlobTransaction = { - identity, - blobs: [blob0, blob1], - } - - try { - setStatus('Verifying identity...'); - const tx_hash = await nodeService.client.sendBlobTx(blobTx); - setTransactionHash(tx_hash); - setStatus('Building proof transaction (this may take a few moments)...'); - const proofTx = await build_proof_transaction( - identity, - password, - tx_hash, - 0, - blobTx.blobs.length, - ); - setStatus('Sending proof transaction...'); - await nodeService.client.sendProofTx(proofTx); - setStatus('Waiting for transaction confirmation...'); - - try { - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - webSocketService.unsubscribeFromWalletEvents(); - reject(new Error('Identity verification timed out')); - }, 30000); - - webSocketService.connect(identity); - const unsubscribeWalletEvents = webSocketService.subscribeToWalletEvents((event) => { - if (event.event === 'Identity verified') { - clearTimeout(timeout); - unsubscribeWalletEvents(); - webSocketService.disconnect(); - resolve(event); - } - }); - }); - } catch (error) { - setError('' + error); - setStatus(''); - console.error('Transaction error:', error); - return; - } - setStatus('Logged in successfully!'); - - const wallet: Wallet = { - username, - address: identity - }; - - onWalletLoggedIn(wallet); - } catch (error) { - setError('Invalid credentials or wallet does not exist'); - console.error('Login error:', error); - } finally { - setIsLoading(false); - } - }; - - return ( -
-

Login to Your Wallet

-
-
- - setUsername(e.target.value)} - placeholder="Enter your username" - /> -
-
- - setPassword(e.target.value)} - placeholder="Enter your password" - /> -
- {error &&
{error}
} - {status &&
{status}
} - - {transactionHash && ( - - )} - -
-
- ); -}; - diff --git a/front/src/components/common/LoadingErrorState.tsx b/front/src/components/common/LoadingErrorState.tsx new file mode 100644 index 0000000..2fec52f --- /dev/null +++ b/front/src/components/common/LoadingErrorState.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +interface LoadingErrorStateProps { + isLoading: boolean; + error: string | null; + loadingMessage?: string; +} + +export const LoadingErrorState: React.FC = ({ + isLoading, + error, + loadingMessage = 'Loading...' +}) => { + if (isLoading) { + return
{loadingMessage}
; + } + + if (error) { + return
Error: {error}
; + } + + return null; +}; \ No newline at end of file diff --git a/front/src/components/connect/ConnectWallet.css b/front/src/components/connect/ConnectWallet.css deleted file mode 100644 index 8afd855..0000000 --- a/front/src/components/connect/ConnectWallet.css +++ /dev/null @@ -1,399 +0,0 @@ -/* === Design tokens & motion === */ -:root { - --color-primary: #FF594B; - --color-secondary: #FF9660; - --color-primary-emphasis: rgba(255, 89, 75, 0.2); - --radius-l: 24px; - --shadow-xl: 0 12px 32px rgba(0, 0, 0, 0.12); - --overlay-bg: rgba(0, 0, 0, 0.5); - --modal-bg: rgba(255, 255, 255, 0.75); - --anim-ease: cubic-bezier(.16,1,.3,1); - --anim-fast: 120ms; - --anim-normal: 220ms; -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes slideUp { - from { transform: translateY(24px) scale(0.98); opacity: 0; } - to { transform: translateY(0) scale(1); opacity: 1; } -} - -.connect-wallet-btn { - padding: 12px 24px; - background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-secondary) 100%); - color: #fff; - border: none; - border-radius: var(--radius-l); - cursor: pointer; - font-size: 16px; - font-weight: 600; - box-shadow: var(--shadow-xl); - transition: transform var(--anim-fast) var(--anim-ease), opacity var(--anim-fast) var(--anim-ease); -} - -.connect-wallet-btn:hover { - opacity: 0.9; - transform: scale(0.98); -} - -/* Overlay */ -.connect-wallet-overlay { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: var(--overlay-bg); - backdrop-filter: blur(8px) saturate(120%); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - animation: fadeIn var(--anim-normal) var(--anim-ease); -} - -/* Modal */ -.connect-wallet-modal { - background: var(--modal-bg); - backdrop-filter: blur(16px) saturate(180%); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: var(--radius-l); - box-shadow: var(--shadow-xl); - width: 90%; - max-width: 420px; - max-height: 90vh; - overflow-y: auto; - padding: 0 24px 28px; /* top padding removed: header owns it */ - position: relative; - animation: slideUp 0.3s var(--anim-ease); -} - -/* Modal header with brand gradient */ -.modal-header { - height: 56px; - background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-secondary) 100%); - border-top-left-radius: var(--radius-l); - border-top-right-radius: var(--radius-l); - display: flex; - align-items: center; - justify-content: center; - position: relative; - margin: 0 -24px 24px; /* stretch full width, then push content */ -} - -.modal-logo { - margin: 0; - display: flex; - justify-content: center; -} - -.connect-modal-close { - position: absolute; - right: 12px; - top: 50%; - transform: translateY(-50%); - background: transparent; - border: none; - color: #fff; - font-size: 24px; - cursor: pointer; - transition: transform var(--anim-fast) var(--anim-ease); -} - -.connect-modal-close:hover { - transform: translateY(-50%) rotate(45deg); -} - -.provider-selection h2 { - margin-top: 0; - text-align: center; - font-size: 24px; - color: #333; -} - -.password-provider-flow .auth-title { - margin: 0 0 20px 0; - text-align: center; - font-size: 24px; - color: #333; -} - -.provider-selection .subtitle { - text-align: center; - color: #666; - margin: 8px 0 24px; - font-size: 14px; -} - -.provider-grid { - display: flex; - flex-wrap: wrap; - gap: 12px; - justify-content: center; - margin-top: 16px; -} - -.connect-provider-btn { - flex: 1 1 40%; - padding: 12px 8px; - border: 1px solid #ccc; - border-radius: 4px; - background: #f9f9f9; - cursor: pointer; - font-size: 14px; -} - -.connect-provider-btn:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.coming-soon { - font-size: 12px; - color: #999; -} - -.password-provider-flow .switch-auth-button, -.provider-coming-soon .back-to-providers { - margin-top: 16px; - width: 100%; - padding: 8px; - border: none; - background: #eee; - cursor: pointer; - border-radius: 4px; -} - -/* Sleek link-style button for toggling between login and sign-up */ -.password-provider-flow .switch-auth-button { - /* link style button */ - background: none; - color: var(--color-primary); - font-size: 14px; - width: auto; - padding: 0; -} - -.password-provider-flow .switch-auth-button:hover { - opacity: 0.8; - text-decoration: none; -} - -/* Provider vertical list */ -.provider-list { - display: flex; - flex-direction: column; - gap: 12px; - margin-top: 16px; -} - -.provider-row { - width: 100%; - padding: 12px 16px; - border: 1px solid #e5e5e5; - border-radius: 8px; - background: #fff; - display: flex; - align-items: center; - justify-content: space-between; - font-size: 16px; - cursor: pointer; - transition: background 0.15s ease; -} - -.provider-row:hover:not(.disabled) { - background: #f2f2f2; -} - -.provider-row.disabled { - opacity: 0.6; - cursor: default; -} - -.provider-icon { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 50%; - background-color: #f5f5f5; - color: #333; -} - -.provider-row .provider-icon svg { - display: block; -} - -/* Email field styling */ -.provider-row:first-child { - position: relative; - background-color: white; - border-radius: 8px; - overflow: hidden; -} - -.label { - display: flex; - align-items: center; - gap: 12px; -} - -.row-arrow { - font-size: 20px; -} - -.password-provider-flow .wallet-login-container h1, -.password-provider-flow .wallet-creation-container h1 { - display: none; -} - -.password-provider-flow .wallet-creation-form p { - display: none; -} - -/* === Auth Components Styling === */ -.password-provider-flow .wallet-login-container, -.password-provider-flow .wallet-creation-container { - /* Already hides h1 */ -} - -.password-provider-flow .form-group { - margin-bottom: 10px; -} - -.password-provider-flow .form-group label { - display: block; - margin-bottom: 4px; - font-size: 14px; - font-weight: 500; - color: #333; -} - -.password-provider-flow input { - width: 100%; - height: 42px; - padding: 0 12px; - background: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - font-size: 15px; - transition: border-color var(--anim-fast) ease; - position: relative; -} - -.password-provider-flow .form-group { - position: relative; -} - -/* -.password-provider-flow .form-group::before { - content: ""; - position: absolute; - left: 12px; - top: 34px; - width: 16px; - height: 16px; - background-repeat: no-repeat; - background-position: center; - opacity: 0.5; -} - -.password-provider-flow .form-group:nth-of-type(1)::before { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E"); -} - -.password-provider-flow .form-group:nth-of-type(2)::before, -.password-provider-flow .form-group:nth-of-type(3)::before { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='11' width='18' height='11' rx='2' ry='2'%3E%3C/rect%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'%3E%3C/path%3E%3C/svg%3E"); -} -*/ - -.password-provider-flow input:focus { - outline: none; - border-color: var(--color-primary); - box-shadow: 0 0 0 3px var(--color-primary-emphasis); -} - -.password-provider-flow .login-wallet-button, -.password-provider-flow .create-wallet-button { - width: 100%; - height: 48px; - background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-secondary) 100%); - color: #fff; - border: none; - border-radius: 24px; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: transform var(--anim-fast) var(--anim-ease), opacity var(--anim-fast) var(--anim-ease); - margin: 0; - padding: 0; -} - -.password-provider-flow .login-wallet-button:hover, -.password-provider-flow .create-wallet-button:hover { - opacity: 0.9; - transform: translateY(-1px); -} - -.password-provider-flow .login-wallet-button:active, -.password-provider-flow .create-wallet-button:active { - transform: translateY(1px); -} - -.password-provider-flow .login-wallet-button:disabled, -.password-provider-flow .create-wallet-button:disabled { - opacity: 0.7; - cursor: not-allowed; - background: linear-gradient(90deg, #ccc 0%, #ddd 100%); -} - -.password-provider-flow .error-message { - color: #e53935; - margin: 8px 0; - padding: 8px 12px; - background-color: rgba(229, 57, 53, 0.1); - border-radius: 8px; - font-size: 14px; -} - -.password-provider-flow .status-message { - color: #2196F3; - margin: 8px 0; - padding: 8px 12px; - background-color: rgba(33, 150, 243, 0.1); - border-radius: 8px; - font-size: 14px; -} - -.password-provider-flow .transaction-hash { - margin-top: 16px; - font-size: 13px; - text-align: center; - opacity: 0.7; -} - -.password-provider-flow .transaction-hash a { - color: var(--color-primary); - text-decoration: none; -} - -.password-provider-flow .transaction-hash a:hover { - text-decoration: underline; -} - -/* Animation for form fields */ -@keyframes formFieldAppear { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} - -.password-provider-flow .form-group:nth-child(1) { animation: formFieldAppear 0.3s var(--anim-ease) 0.1s backwards; } -.password-provider-flow .form-group:nth-child(2) { animation: formFieldAppear 0.3s var(--anim-ease) 0.2s backwards; } -.password-provider-flow .form-group:nth-child(3) { animation: formFieldAppear 0.3s var(--anim-ease) 0.3s backwards; } -.password-provider-flow button { animation: formFieldAppear 0.3s var(--anim-ease) 0.4s backwards; } \ No newline at end of file diff --git a/front/src/components/connect/ConnectWalletExamples.css b/front/src/components/connect/ConnectWalletExamples.css deleted file mode 100644 index 9f735b2..0000000 --- a/front/src/components/connect/ConnectWalletExamples.css +++ /dev/null @@ -1,289 +0,0 @@ -.connect-examples-container { - max-width: 1200px; - margin: 0 auto; - padding: 2rem; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', sans-serif; -} - -.connect-examples-container h2 { - font-size: 2rem; - margin-bottom: 0.5rem; - color: #333; -} - -.connect-examples-container h3 { - font-size: 1.5rem; - margin: 2rem 0 1rem; - color: #444; -} - -.connect-examples-container p { - font-size: 1rem; - line-height: 1.5; - color: #666; - margin-bottom: 1.5rem; -} - -.connect-examples-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 1.5rem; -} - -.connect-example-card { - border: 1px solid #eaeaea; - border-radius: 8px; - padding: 1.5rem; - background: white; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); -} - -.connect-example-card h4 { - margin-top: 0; - margin-bottom: 1rem; - color: #333; -} - -.connect-example-preview { - display: flex; - justify-content: center; - margin-top: 1rem; - padding: 1rem; - background: #f9f9f9; - border-radius: 6px; -} - -.connect-example-code { - background: #f4f4f4; - padding: 0.75rem; - border-radius: 4px; - font-family: monospace; - font-size: 0.875rem; - overflow-x: auto; -} - -/* Button Variants */ -.connect-example-primary { - background: #4a90e2; - color: white; - border: none; - padding: 10px 16px; - font-size: 14px; - border-radius: 4px; - cursor: pointer; -} - -.connect-example-outline { - background: transparent; - color: #4a90e2; - border: 1px solid #4a90e2; - padding: 10px 16px; - font-size: 14px; - border-radius: 4px; - cursor: pointer; -} - -.connect-example-rounded { - background: #5a67d8; - color: white; - border: none; - padding: 10px 20px; - font-size: 14px; - border-radius: 30px; - cursor: pointer; -} - -.connect-example-with-icon { - display: flex; - align-items: center; - gap: 8px; - background: #38a169; - color: white; - border: none; - padding: 10px 16px; - font-size: 14px; - border-radius: 4px; - cursor: pointer; -} - -.connect-example-text { - background: transparent; - color: #4a5568; - border: none; - padding: 10px 0; - font-size: 14px; - text-decoration: underline; - cursor: pointer; -} - -.connect-example-dark { - background: #2d3748; - color: white; - border: none; - padding: 10px 16px; - font-size: 14px; - border-radius: 4px; - cursor: pointer; -} - -/* Navbar example */ -.connect-example-navbar { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem; - background: #f8f9fa; - border-radius: 8px; - margin-top: 1rem; -} - -.connect-example-logo { - font-weight: bold; - font-size: 1.25rem; - color: #333; -} - -.connect-example-nav-items { - display: flex; - align-items: center; - gap: 1.5rem; -} - -.connect-example-nav-link { - color: #4a5568; - text-decoration: none; - font-size: 0.9rem; -} - -.connect-example-nav-button { - background: #4a90e2; - color: white; - border: none; - padding: 8px 16px; - font-size: 0.9rem; - border-radius: 4px; - cursor: pointer; -} - -/* Add these new button styles */ - -.connect-example-gradient { - background: linear-gradient(90deg, #4a90e2 0%, #5a67d8 100%); - color: white; - border: none; - padding: 10px 20px; - font-size: 14px; - border-radius: 6px; - cursor: pointer; - box-shadow: 0 2px 10px rgba(74, 144, 226, 0.3); -} - -.connect-example-minimal-pill { - background: transparent; - color: #444; - border: none; - padding: 8px 16px; - font-size: 14px; - border-radius: 30px; - cursor: pointer; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - transition: all 0.2s ease; -} - -.connect-example-minimal-pill:hover { - background: #f0f0f0; - transform: translateY(-1px); -} - -.connect-example-branded { - display: flex; - align-items: center; - gap: 8px; - background: #2c3e50; - color: white; - border: none; - padding: 10px 18px; - font-size: 14px; - border-radius: 6px; - cursor: pointer; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); -} - -.branded-icon { - font-size: 16px; -} - -/* Mobile mockup */ -.connect-example-mobile { - width: 280px; - height: 500px; - border: 10px solid #222; - border-radius: 36px; - overflow: hidden; - margin: 1rem auto; - position: relative; - background-color: white; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); -} - -.mobile-statusbar { - background: #222; - color: white; - display: flex; - justify-content: space-between; - padding: 4px 12px; - font-size: 12px; -} - -.mobile-icons { - display: flex; - gap: 4px; -} - -.mobile-content { - padding: 12px; - height: calc(100% - 20px); - display: flex; - flex-direction: column; -} - -.mobile-app-header { - display: flex; - justify-content: space-between; - align-items: center; - padding-bottom: 12px; - border-bottom: 1px solid #eee; - margin-bottom: 16px; -} - -.mobile-app-title { - font-weight: bold; - font-size: 16px; -} - -.mobile-connect-button { - background: #4a90e2; - color: white; - border: none; - padding: 6px 12px; - font-size: 12px; - border-radius: 4px; - cursor: pointer; -} - -.mobile-app-body { - flex: 1; - display: flex; - flex-direction: column; - gap: 12px; -} - -.mobile-placeholder { - height: 80px; - background: #f5f5f5; - border-radius: 8px; -} - -.mobile-placeholder.short { - height: 40px; -} \ No newline at end of file diff --git a/front/src/components/connect/ConnectWalletExamples.tsx b/front/src/components/connect/ConnectWalletExamples.tsx deleted file mode 100644 index 33d7b6c..0000000 --- a/front/src/components/connect/ConnectWalletExamples.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import React from 'react'; -import { ConnectWallet, ProviderOption } from './ConnectWallet'; -import { Wallet } from '../../types/wallet'; -import './ConnectWalletExamples.css'; - -interface ConnectWalletExamplesProps { - onWalletConnected?: (wallet: Wallet) => void; -} - -export const ConnectWalletExamples: React.FC = ({ - onWalletConnected -}) => { - // Different provider combinations - const providerSets: { title: string; providers: ProviderOption[] }[] = [ - { title: 'All Providers', providers: ['password', 'google', 'github', 'x'] }, - { title: 'Social Only', providers: ['google', 'github', 'x'] }, - { title: 'Password Only', providers: ['password'] }, - { title: 'Google & GitHub', providers: ['google', 'github'] } - ]; - - // Example button render functions - const buttonVariants = [ - { - title: 'Primary Button', - render: ({ onClick }: { onClick: () => void }) => ( - - ) - }, - { - title: 'Outline Button', - render: ({ onClick }: { onClick: () => void }) => ( - - ) - }, - { - title: 'Rounded Button', - render: ({ onClick }: { onClick: () => void }) => ( - - ) - }, - { - title: 'With Icon', - render: ({ onClick }: { onClick: () => void }) => ( - - ) - }, - { - title: 'Text Only', - render: ({ onClick }: { onClick: () => void }) => ( - - ) - }, - { - title: 'Dark Mode', - render: ({ onClick }: { onClick: () => void }) => ( - - ) - }, - { - title: 'Gradient Button', - render: ({ onClick }: { onClick: () => void }) => ( - - ) - }, - { - title: 'Minimal Pill', - render: ({ onClick }: { onClick: () => void }) => ( - - ) - }, - { - title: 'Branded Button', - render: ({ onClick }: { onClick: () => void }) => ( - - ) - } - ]; - - return ( -
-

Connect Account Examples

-

The ConnectWallet component supports customizable buttons and provider lists.

- -

Button Style Variations

-
- {buttonVariants.map((variant, index) => ( -
-

{variant.title}

-
- -
-
- ))} -
- -

Provider Combinations

-
- {providerSets.map((set, index) => ( -
-

{set.title}

-
-              {`providers={${JSON.stringify(set.providers)}}`}
-            
-
- ( - - )} - onWalletConnected={onWalletConnected} - /> -
-
- ))} -
- -

Custom Integration Example

-
-
MyApp
-
- Home - Features - About - ( - - )} - onWalletConnected={onWalletConnected} - /> -
-
- -

Mobile Integration Example

-
-
- 9:41 -
- 📶 - 📡 - 🔋 -
-
-
-
-
Mobile App
- ( - - )} - onWalletConnected={onWalletConnected} - /> -
-
-
-
-
-
-
-
-
-
- ); -}; \ No newline at end of file diff --git a/front/src/components/layout/WalletLayout.tsx b/front/src/components/layout/WalletLayout.tsx index 0862d02..7d797ae 100644 --- a/front/src/components/layout/WalletLayout.tsx +++ b/front/src/components/layout/WalletLayout.tsx @@ -1,5 +1,5 @@ import { Outlet, useNavigate, useLocation } from 'react-router-dom'; -import { Wallet } from '../../types/wallet'; +import { Wallet } from 'hyle-wallet'; interface WalletLayoutProps { wallet: Wallet; diff --git a/front/src/components/wallet/Balance.tsx b/front/src/components/wallet/Balance.tsx index 92db5d9..da24567 100644 --- a/front/src/components/wallet/Balance.tsx +++ b/front/src/components/wallet/Balance.tsx @@ -1,4 +1,4 @@ -import { Wallet } from '../../types/wallet'; +import { Wallet } from 'hyle-wallet'; interface BalanceProps { wallet: Wallet; diff --git a/front/src/components/wallet/History.tsx b/front/src/components/wallet/History.tsx index 77ae81c..d5ba9b2 100644 --- a/front/src/components/wallet/History.tsx +++ b/front/src/components/wallet/History.tsx @@ -1,4 +1,4 @@ -import { Transaction } from '../../types/wallet'; +import { Transaction } from 'hyle-wallet'; interface HistoryProps { transactions: Transaction[]; diff --git a/front/src/components/wallet/Send.tsx b/front/src/components/wallet/Send.tsx index 1678334..4e4e4b4 100644 --- a/front/src/components/wallet/Send.tsx +++ b/front/src/components/wallet/Send.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Transaction, verifyIdentity, Wallet } from '../../types/wallet'; +import { Transaction, verifyIdentity, Wallet } from 'hyle-wallet'; import { blob_builder, BlobTransaction } from 'hyle' import { build_proof_transaction, build_blob as check_secret_blob } from 'hyle-check-secret'; import { nodeService } from '../../services/NodeService'; diff --git a/front/src/components/wallet/SessionKeys.tsx b/front/src/components/wallet/SessionKeys.tsx index b934210..5dc1808 100644 --- a/front/src/components/wallet/SessionKeys.tsx +++ b/front/src/components/wallet/SessionKeys.tsx @@ -1,9 +1,9 @@ import { useState, useEffect } from 'react'; -import { Wallet, addSessionKey, removeSessionKey, walletContractName } from '../../types/wallet'; +import { Wallet, addSessionKey, removeSessionKey } from 'hyle-wallet'; import { nodeService } from '../../services/NodeService'; import { indexerService } from '../../services/IndexerService'; import { webSocketService } from '../../services/WebSocketService'; -import { sessionKeyService } from '../../services/SessionKeyService'; +import { useSessionKey, walletContractName } from 'hyle-wallet'; import { build_proof_transaction, build_blob as check_secret_blob } from 'hyle-check-secret'; import { BlobTransaction } from 'hyle'; import './SessionKeys.css'; @@ -32,6 +32,8 @@ export const SessionKeys = ({ wallet }: SessionKeysProps) => { const [isLoading, setIsLoading] = useState(false); const [transactionHash, setTransactionHash] = useState(''); + const { generateSessionKey, clearSessionKey, createSignedBlobs } = useSessionKey(); + const fetchSessionKeys = async () => { try { const accountInfo = await indexerService.getAccountInfo(wallet.username); @@ -64,7 +66,8 @@ export const SessionKeys = ({ wallet }: SessionKeysProps) => { setTransactionHash(''); // Génère une nouvelle paire de clés - const publicKey = sessionKeyService.generateSessionKey(); + const [publicKey, privateKey] = generateSessionKey(); + localStorage.setItem(publicKey, privateKey); try { const identity = `${wallet.username}@${walletContractName}`; @@ -116,7 +119,7 @@ export const SessionKeys = ({ wallet }: SessionKeysProps) => { await fetchSessionKeys(); } catch (error) { setError('Failed to add session key: ' + error); - sessionKeyService.clear(publicKey); // Remove key from local storage if it fails + clearSessionKey(publicKey); // Remove key from local storage if it fails } finally { setIsLoading(false); } @@ -169,15 +172,20 @@ export const SessionKeys = ({ wallet }: SessionKeysProps) => { } }; - const handleSendTransactionWithSessionKey = async (key: string) => { + const handleSendTransactionWithSessionKey = async (publicKey: string) => { setIsLoading(true); setError(''); setStatus('Sending transaction...'); setTransactionHash(''); try { + const identity = `${wallet.username}@${walletContractName}`; - const [blob0, blob1] = sessionKeyService.useSessionKey(wallet.username, key, "Hello world!"); + const privateKey = localStorage.getItem(publicKey); + if (!privateKey) { + throw new Error('Private key not found in local storage'); + } + const [blob0, blob1] = createSignedBlobs(wallet.username, privateKey, "Hello world!"); const blobTx: BlobTransaction = { identity, diff --git a/front/src/hooks/useWalletBalance.ts b/front/src/hooks/useWalletBalance.ts new file mode 100644 index 0000000..36b2794 --- /dev/null +++ b/front/src/hooks/useWalletBalance.ts @@ -0,0 +1,33 @@ +import { useState, useEffect } from 'react'; +import { indexerService } from '../services/IndexerService'; + +export function useWalletBalance(address: string | undefined) { + const [balance, setBalance] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchBalance = async () => { + if (!address) return; + + setIsLoading(true); + setError(null); + + try { + const balance = await indexerService.getBalance(address); + setBalance(balance); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch balance'); + console.error('Error fetching balance:', err); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (address) { + fetchBalance(); + } + }, [address]); + + return { balance, isLoading, error, fetchBalance }; +} \ No newline at end of file diff --git a/front/src/hooks/useWalletTransactions.ts b/front/src/hooks/useWalletTransactions.ts new file mode 100644 index 0000000..92dfae8 --- /dev/null +++ b/front/src/hooks/useWalletTransactions.ts @@ -0,0 +1,61 @@ +import { useState, useEffect, useCallback } from 'react'; +import { Transaction } from 'hyle-wallet'; +import { indexerService } from '../services/IndexerService'; +import { AppEvent } from '../services/WebSocketService'; + +export function useWalletTransactions(address: string | undefined) { + const [transactions, setTransactions] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchTransactions = useCallback(async () => { + if (!address) return; + + setIsLoading(true); + setError(null); + + try { + const txHistory = await indexerService.getTransactionHistory(address); + setTransactions(txHistory); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch transaction history'); + console.error('Error fetching transactions:', err); + } finally { + setIsLoading(false); + } + }, [address]); + + const handleTxEvent = useCallback((event: AppEvent['TxEvent']) => { + console.log('Received transaction event:', event); + const newTransaction: Transaction = event.tx; + + setTransactions(prevTransactions => { + const existingIndex = prevTransactions.findIndex(tx => tx.id === newTransaction.id); + if (existingIndex !== -1) { + console.log('Updating existing transaction'); + // Update existing transaction in-place + const updatedTransactions = [...prevTransactions]; + updatedTransactions[existingIndex] = newTransaction; + return updatedTransactions; + } else { + console.log('Adding new transaction'); + // Add new transaction at the beginning of the list + return [newTransaction, ...prevTransactions]; + } + }); + }, []); + + useEffect(() => { + if (address) { + fetchTransactions(); + } + }, [address, fetchTransactions]); + + return { + transactions, + isLoading, + error, + fetchTransactions, + handleTxEvent + }; +} \ No newline at end of file diff --git a/front/src/hooks/useWebSocketConnection.ts b/front/src/hooks/useWebSocketConnection.ts new file mode 100644 index 0000000..fd46686 --- /dev/null +++ b/front/src/hooks/useWebSocketConnection.ts @@ -0,0 +1,36 @@ +import { useEffect, useCallback } from 'react'; +import { AppEvent, webSocketService } from '../services/WebSocketService'; + +type EventHandler = (event: AppEvent['TxEvent']) => void; + +export function useWebSocketConnection( + address: string | undefined, + onTxEvent: EventHandler +) { + const connect = useCallback(() => { + if (!address) return; + webSocketService.connect(address); + }, [address]); + + const disconnect = useCallback(() => { + webSocketService.disconnect(); + }, []); + + useEffect(() => { + if (!address) return; + + // Connect to WebSocket + connect(); + + // Subscribe to transaction events + const unsubscribeTxEvents = webSocketService.subscribeToTxEvents(onTxEvent); + + // Cleanup on unmount + return () => { + unsubscribeTxEvents(); + disconnect(); + }; + }, [address, connect, disconnect, onTxEvent]); + + return { connect, disconnect }; +} \ No newline at end of file diff --git a/front/src/routes/routes.tsx b/front/src/routes/routes.tsx new file mode 100644 index 0000000..ea87e89 --- /dev/null +++ b/front/src/routes/routes.tsx @@ -0,0 +1,41 @@ +import { Navigate, RouteObject } from 'react-router-dom'; +import { Balance } from '../components/wallet/Balance'; +import { Send } from '../components/wallet/Send'; +import { History } from '../components/wallet/History'; +import { SessionKeys } from '../components/wallet/SessionKeys'; +import { WalletLayout } from '../components/layout/WalletLayout'; +import { Wallet } from 'hyle-wallet'; + +// Route path constants +export const ROUTES = { + ROOT: '/', + WALLET: '/wallet', + BALANCE: '/wallet/balance', + SEND: '/wallet/send', + HISTORY: '/wallet/history', + SESSION_KEYS: '/wallet/session-keys', +}; + +export const getPublicRoutes = (): RouteObject[] => [ + { path: ROUTES.ROOT, element: }, + { path: '*', element: }, +]; + +export const getProtectedRoutes = ( + wallet: Wallet | null, + balance: number, + transactions: any[], + onLogout: () => void +): RouteObject[] => [ + { + path: ROUTES.WALLET, + element: , + children: [ + { path: 'balance', element: }, + { path: 'send', element: }, + { path: 'history', element: }, + { path: 'session-keys', element: }, + { index: true, element: }, + ], + }, +]; \ No newline at end of file diff --git a/front/src/services/IndexerService.ts b/front/src/services/IndexerService.ts index 89fab26..b35814d 100644 --- a/front/src/services/IndexerService.ts +++ b/front/src/services/IndexerService.ts @@ -1,5 +1,5 @@ import { IndexerApiHttpClient } from "hyle"; -import { Transaction, AuthMethod, walletContractName } from "../types/wallet"; +import { Transaction, AuthMethod, walletContractName } from "hyle-wallet"; interface BalanceResponse { account: string; @@ -33,7 +33,7 @@ class IndexerService { import.meta.env.VITE_INDEXER_BASE_URL, ); this.server = new IndexerApiHttpClient( - import.meta.env.VITE_SERVER_BASE_URL, + import.meta.env.VITE_WALLET_SERVER_BASE_URL, ); } diff --git a/front/src/services/WebSocketService.ts b/front/src/services/WebSocketService.ts index 1c3a0f7..2a20420 100644 --- a/front/src/services/WebSocketService.ts +++ b/front/src/services/WebSocketService.ts @@ -1,4 +1,4 @@ -import { Transaction } from "../types/wallet"; +import { Transaction } from 'hyle-wallet'; export interface AppEvent { TxEvent: { @@ -40,7 +40,7 @@ export class WebSocketService { } this.currentAccount = account; - this.ws = new WebSocket(import.meta.env.VITE_WS_URL); + this.ws = new WebSocket(import.meta.env.VITE_WALLET_WS_URL); this.ws.onopen = () => { console.log("WebSocket connected"); diff --git a/hyle-wallet/.env b/hyle-wallet/.env new file mode 100644 index 0000000..6192b29 --- /dev/null +++ b/hyle-wallet/.env @@ -0,0 +1,6 @@ +VITE_WALLET_SERVER_BASE_URL=http://localhost:4000 +VITE_WALLET_WS_URL=ws://localhost:8081/ws +VITE_NODE_BASE_URL=http://localhost:4321 +VITE_INDEXER_BASE_URL=http://localhost:4321 +VITE_TX_EXPLORER_URL=http://localhost:8000 +VITE_FAUCET_URL=http://localhost:5173 diff --git a/hyle-wallet/.gitignore b/hyle-wallet/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/hyle-wallet/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/hyle-wallet/bun.lockb b/hyle-wallet/bun.lockb new file mode 100755 index 0000000..55de28a Binary files /dev/null and b/hyle-wallet/bun.lockb differ diff --git a/hyle-wallet/lib.ts b/hyle-wallet/lib.ts new file mode 100644 index 0000000..7532e3a --- /dev/null +++ b/hyle-wallet/lib.ts @@ -0,0 +1,18 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { HyleWallet } from './src/components/HyleWallet'; +import type { ProviderOption } from './src/hooks/useWallet'; + +class HyleWalletElement extends HTMLElement { + connectedCallback() { + const mountPoint = document.createElement('div'); + this.appendChild(mountPoint); + + const providersAttr = this.getAttribute('providers'); + const providers = providersAttr ? providersAttr.split(',') as ProviderOption[] : ["password" as ProviderOption]; + + createRoot(mountPoint).render(React.createElement(HyleWallet, { providers })); + } +} + +customElements.define('hyle-wallet', HyleWalletElement); diff --git a/hyle-wallet/package.json b/hyle-wallet/package.json new file mode 100644 index 0000000..e544349 --- /dev/null +++ b/hyle-wallet/package.json @@ -0,0 +1,60 @@ +{ + "name": "hyle-wallet", + "version": "1.0.0", + "type": "module", + "description": "A reusable wallet component for React applications", + "main": "./dist/hyle-wallet.cjs.js", + "types": "dist/hyle-wallet.d.ts", + "module": "./dist/hyle-wallet.es.js", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/hyle-wallet.d.ts", + "import": "./dist/hyle-wallet.es.js", + "require": "./dist/hyle-wallet.cjs.js" + } + }, + "peerDependencies": { + "hyle-check-secret": "^0.3.2", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.5.0" + }, + "scripts": { + "clean": "rm -rf dist", + "dev": "vite", + "build": "vite build", + "build:lib": "tsc", + "prepublishOnly": "bun run build:lib", + "pub": "npm publish" + }, + "dependencies": { + "@types/crypto-js": "^4.2.2", + "@types/elliptic": "^6.4.18", + "@types/react-router-dom": "^5.3.3", + "crypto-js": "^4.2.0", + "elliptic": "^6.6.1", + "hyle": "^0.2.5" + }, + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/react": "^19.1.3", + "@types/react-dom": "^19.1.4", + "@vitejs/plugin-react": "^4.4.0", + "ajv": "^8.17.1", + "autoprefixer": "^10.4.21", + "eslint": "^9.24.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "postcss": "^8.5.3", + "tailwindcss": "^4.1.4", + "typescript": "^5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5", + "vite-plugin-css-injected-by-js": "^3.5.2", + "vite-plugin-dts": "^4.5.3" + } +} diff --git a/hyle-wallet/src/components/HyleWallet.css b/hyle-wallet/src/components/HyleWallet.css new file mode 100644 index 0000000..5130160 --- /dev/null +++ b/hyle-wallet/src/components/HyleWallet.css @@ -0,0 +1,452 @@ +/* === Design tokens & motion === */ +:root { + --color-primary: #FF594B; + --color-secondary: #FF9660; + --color-primary-emphasis: rgba(255, 89, 75, 0.2); + --radius-l: 24px; + --shadow-xl: 0 12px 32px rgba(0, 0, 0, 0.12); + --overlay-bg: rgba(0, 0, 0, 0.5); + --modal-bg: rgba(255, 255, 255, 0.75); + --anim-ease: cubic-bezier(.16,1,.3,1); + --anim-fast: 120ms; + --anim-normal: 220ms; + } + + @keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + + @keyframes slideUp { + from { transform: translateY(24px) scale(0.98); opacity: 0; } + to { transform: translateY(0) scale(1); opacity: 1; } + } + + .hyle-wallet-btn { + padding: 12px 24px; + background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-secondary) 100%); + color: #fff; + border: none; + border-radius: var(--radius-l); + cursor: pointer; + font-size: 16px; + font-weight: 600; + box-shadow: var(--shadow-xl); + transition: transform var(--anim-fast) var(--anim-ease), opacity var(--anim-fast) var(--anim-ease); + } + + .hyle-wallet-btn:hover { + opacity: 0.9; + transform: scale(0.98); + } + + /* Overlay */ + .hyle-wallet-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: var(--overlay-bg); + backdrop-filter: blur(8px) saturate(120%); + display: flex; + align-items: center; + justify-content: center; + z-index: 999999; + animation: fadeIn var(--anim-normal) var(--anim-ease); + } + + /* Modal */ + .hyle-wallet-modal { + background: var(--modal-bg); + backdrop-filter: blur(16px) saturate(180%); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: var(--radius-l); + box-shadow: var(--shadow-xl); + width: min(90%, 420px); + min-height: min-content; + max-height: 90vh; + overflow-y: auto; + overflow-x: hidden; + padding: 0 24px 28px; + position: relative; + animation: slideUp 0.3s var(--anim-ease); + margin: 16px; + display: flex; + flex-direction: column; + } + + /* Modal header with brand gradient */ + .modal-header { + height: 56px; + background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-secondary) 100%); + border-top-left-radius: var(--radius-l); + border-top-right-radius: var(--radius-l); + display: flex; + align-items: center; + justify-content: center; + position: relative; + margin: 0 -24px 24px; /* stretch full width, then push content */ + } + + .modal-logo { + margin: 0; + display: flex; + justify-content: center; + } + + .hyle-modal-close { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + background: transparent; + border: none; + color: #fff; + font-size: 24px; + cursor: pointer; + transition: transform var(--anim-fast) var(--anim-ease); + } + + .hyle-modal-close:hover { + transform: translateY(-50%) rotate(45deg); + } + + .provider-selection h2 { + margin-top: 0; + text-align: center; + font-size: 24px; + color: #333; + } + + .password-provider-flow .auth-title { + margin: 0 0 20px 0; + text-align: center; + font-size: 24px; + color: #333; + } + + .provider-selection .subtitle { + text-align: center; + color: #666; + margin: 8px 0 24px; + font-size: 14px; + } + + .provider-grid { + display: flex; + flex-wrap: wrap; + gap: 12px; + justify-content: center; + margin-top: 16px; + } + + .hyle-provider-btn { + flex: 1 1 40%; + padding: 12px 8px; + border: 1px solid #ccc; + border-radius: 4px; + background: #f9f9f9; + cursor: pointer; + font-size: 14px; + } + + .hyle-provider-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + .coming-soon { + font-size: 12px; + color: #999; + } + + .password-provider-flow .switch-auth-button, + .provider-coming-soon .back-to-providers { + margin-top: 16px; + width: 100%; + padding: 8px; + border: none; + background: #eee; + cursor: pointer; + border-radius: 4px; + } + + /* Sleek link-style button for toggling between login and sign-up */ + .password-provider-flow .switch-auth-button { + /* link style button */ + background: none; + color: var(--color-primary); + font-size: 14px; + width: auto; + padding: 0; + } + + .password-provider-flow .switch-auth-button:hover { + opacity: 0.8; + text-decoration: none; + } + + /* Provider vertical list */ + .provider-list { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 16px; + } + + .provider-row { + width: 100%; + padding: 12px 16px; + border: 1px solid #e5e5e5; + border-radius: 8px; + background: #fff; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 16px; + cursor: pointer; + transition: background 0.15s ease; + } + + .provider-row:hover:not(.disabled) { + background: #f2f2f2; + } + + .provider-row.disabled { + opacity: 0.6; + cursor: not-allowed; + } + + .provider-icon { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 50%; + background-color: #f5f5f5; + color: #333; + } + + .provider-row .provider-icon svg { + display: block; + } + + /* Email field styling */ + .provider-row:first-child { + position: relative; + background-color: white; + border-radius: 8px; + overflow: hidden; + } + + .label { + display: flex; + align-items: center; + gap: 12px; + } + + .row-arrow { + font-size: 20px; + } + + .password-provider-flow .wallet-login-container h1, + .password-provider-flow .wallet-creation-container h1 { + display: none; + } + + .password-provider-flow .wallet-creation-form p { + display: none; + } + + /* === Auth Components Styling === */ + .password-provider-flow .wallet-login-container, + .password-provider-flow .wallet-creation-container { + /* Already hides h1 */ + } + + .password-provider-flow .form-group { + margin-bottom: 10px; + } + + .password-provider-flow .form-group label { + display: block; + margin-bottom: 4px; + font-size: 14px; + font-weight: 500; + color: #333; + } + + .password-provider-flow input { + width: 100%; + height: 42px; + background: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 12px; + font-size: 15px; + transition: border-color var(--anim-fast) ease; + position: relative; + } + + .password-provider-flow .form-group { + position: relative; + } + + /* + .password-provider-flow .form-group::before { + content: ""; + position: absolute; + left: 12px; + top: 34px; + width: 16px; + height: 16px; + background-repeat: no-repeat; + background-position: center; + opacity: 0.5; + } + + .password-provider-flow .form-group:nth-of-type(1)::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E"); + } + + .password-provider-flow .form-group:nth-of-type(2)::before, + .password-provider-flow .form-group:nth-of-type(3)::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='11' width='18' height='11' rx='2' ry='2'%3E%3C/rect%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'%3E%3C/path%3E%3C/svg%3E"); + } + */ + + .password-provider-flow input:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-emphasis); + } + + .password-provider-flow .login-wallet-button, + .password-provider-flow .create-wallet-button { + width: 100%; + height: 48px; + background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-secondary) 100%); + color: #fff; + border: none; + border-radius: 24px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: transform var(--anim-fast) var(--anim-ease), opacity var(--anim-fast) var(--anim-ease); + margin: 0; + padding: 0; + } + + .password-provider-flow .login-wallet-button:hover, + .password-provider-flow .create-wallet-button:hover { + opacity: 0.9; + transform: translateY(-1px); + } + + .password-provider-flow .login-wallet-button:active, + .password-provider-flow .create-wallet-button:active { + transform: translateY(1px); + } + + .password-provider-flow .login-wallet-button:disabled, + .password-provider-flow .create-wallet-button:disabled { + opacity: 0.7; + cursor: not-allowed; + background: linear-gradient(90deg, #ccc 0%, #ddd 100%); + } + + .password-provider-flow .error-message { + color: #e53935; + margin: 8px 0; + padding: 8px 12px; + background-color: rgba(229, 57, 53, 0.1); + border-radius: 8px; + font-size: 14px; + } + + .password-provider-flow .status-message { + color: #2196F3; + margin: 8px 0; + padding: 8px 12px; + background-color: rgba(33, 150, 243, 0.1); + border-radius: 8px; + font-size: 14px; + } + + .password-provider-flow .transaction-hash { + margin-top: 16px; + font-size: 13px; + text-align: center; + opacity: 0.7; + } + + .password-provider-flow .transaction-hash a { + color: var(--color-primary); + text-decoration: none; + } + + .password-provider-flow .transaction-hash a:hover { + text-decoration: underline; + } + + /* Animation for form fields */ + @keyframes formFieldAppear { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } + } + + .password-provider-flow .form-group:nth-child(1) { animation: formFieldAppear 0.3s var(--anim-ease) 0.1s backwards; } + .password-provider-flow .form-group:nth-child(2) { animation: formFieldAppear 0.3s var(--anim-ease) 0.2s backwards; } + .password-provider-flow .form-group:nth-child(3) { animation: formFieldAppear 0.3s var(--anim-ease) 0.3s backwards; } + .password-provider-flow button { animation: formFieldAppear 0.3s var(--anim-ease) 0.4s backwards; } + +.password-provider-flow { + flex: 1; + display: flex; + flex-direction: column; + width: 100%; + min-height: 0; +} + +.password-provider-flow .wallet-login-container, +.password-provider-flow .wallet-creation-container { + flex: 1; + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px 0; +} + +.provider-selection { + flex: 1; + display: flex; + flex-direction: column; + gap: 16px; +} + +.provider-list { + flex: 1; + min-height: 0; + overflow-y: auto; + padding-right: 4px; + margin-right: -4px; +} + +.provider-row.disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.coming-soon { + font-size: 12px; + color: #666; + padding: 2px 8px; + border-radius: 4px; + background-color: #f0f0f0; +} + +.transaction-hash { + color: #2196f3; + text-decoration: none; +} \ No newline at end of file diff --git a/front/src/components/connect/ConnectWallet.tsx b/hyle-wallet/src/components/HyleWallet.tsx similarity index 77% rename from front/src/components/connect/ConnectWallet.tsx rename to hyle-wallet/src/components/HyleWallet.tsx index ff85b05..af75152 100644 --- a/front/src/components/connect/ConnectWallet.tsx +++ b/hyle-wallet/src/components/HyleWallet.tsx @@ -1,10 +1,10 @@ -import { useState } from 'react'; -import { Wallet } from '../../types/wallet'; -import { LoginWallet } from '../auth/LoginWallet'; -import { CreateWallet } from '../auth/CreateWallet'; -import './ConnectWallet.css'; - -export type ProviderOption = 'password' | 'google' | 'github' | 'x'; +import { useState, useEffect } from 'react'; +import { authProviderManager } from '../providers/AuthProviderManager'; +import { AuthForm } from './auth/AuthForm'; +import './HyleWallet.css'; +import { useConfig } from '../hooks/useConfig'; +import type { ProviderOption } from '../hooks/useWallet'; +import { useWallet } from '../hooks/useWallet'; // SVG Icons for providers const ProviderIcons = { @@ -36,51 +36,63 @@ const ProviderIcons = { ) }; -interface ConnectWalletProps { - /** - * List of providers to display in the selector. "password" will always be shown if omitted. - */ - providers?: ProviderOption[]; +interface HyleWalletProps { /** * Optional render prop that gives full control over the connect button UI. * If not supplied, a simple default button will be rendered. */ button?: (props: { onClick: () => void }) => React.ReactNode; /** - * Callback invoked when the user successfully connects (creates or logs-in) a wallet. + * Optional explicit provider list (e.g., ["password", "google"]). If omitted, available providers will be detected automatically. */ - onWalletConnected?: (wallet: Wallet) => void; + providers?: ProviderOption[]; } -export const ConnectWallet = ({ - providers = ['password'], +export const HyleWallet = ({ button, - onWalletConnected, -}: ConnectWalletProps) => { + providers, +}: HyleWalletProps) => { const [isOpen, setIsOpen] = useState(false); const [selectedProvider, setSelectedProvider] = useState(null); const [showLogin, setShowLogin] = useState(true); // true = login (default), false = create/sign-up + const { isLoading: isLoadingConfig, error: configError } = useConfig(); + const { wallet, logout } = useWallet(); + + const handleButtonClick = () => { + if (wallet) { + logout(); + } else { + setIsOpen(true); + } + }; + + // Get available providers dynamically + const availableProviders = authProviderManager.getAvailableProviders() as ProviderOption[]; - const openModal = () => setIsOpen(true); + // Close the modal automatically when the user is connected + useEffect(() => { + if (wallet && isOpen) { + setIsOpen(false); + } + }, [wallet, isOpen]); const closeModal = () => { setIsOpen(false); setSelectedProvider(null); setShowLogin(true); }; + + if (isLoadingConfig) { + return
Loading configuration...
; + } - const handleWalletConnected = (wallet: Wallet) => { - onWalletConnected?.(wallet); - closeModal(); - }; - - // Compose the list of providers, ensuring password is included if requested implicitly - const providerList: ProviderOption[] = Array.from( - new Set(['password', ...providers]) - ); + if (configError) { + return
Error loading configuration: {configError}
; + } - const renderProviderButton = (provider: ProviderOption) => { - const disabled = provider !== 'password'; + const renderProviderButton = (providerType: ProviderOption) => { + const provider = authProviderManager.getProvider(providerType); + const disabled = !provider?.isEnabled(); const config: Record = { password: { label: 'Password', icon: ProviderIcons.password }, @@ -89,14 +101,13 @@ export const ConnectWallet = ({ x: { label: 'X', icon: ProviderIcons.x }, }; - const { label, icon } = config[provider]; + const { label, icon } = config[providerType]; return ( )} {isOpen && ( -
-
e.stopPropagation()}> +
+
e.stopPropagation()}>
@@ -133,28 +144,29 @@ export const ConnectWallet = ({
-
- {/* Provider selection */} {selectedProvider === null && (

Sign in

- {providerList.map(renderProviderButton)} + {(providers ?? availableProviders).map(renderProviderButton)}
)} - {/* Password provider flow */} - {selectedProvider === 'password' && ( + {selectedProvider && (
{showLogin ? ( <>

Log in

- +
)} - - {/* Stub for other providers */} - {selectedProvider && selectedProvider !== 'password' && ( -
-

{selectedProvider} provider coming soon!

- -
- )}
)} ); -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/hyle-wallet/src/components/auth/AuthForm.css b/hyle-wallet/src/components/auth/AuthForm.css new file mode 100644 index 0000000..04b4758 --- /dev/null +++ b/hyle-wallet/src/components/auth/AuthForm.css @@ -0,0 +1,87 @@ +.auth-form { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.form-group label { + font-size: 14px; + font-weight: 500; + color: #333; +} + +.form-group input { + padding: 12px 16px; + border-radius: 8px; + border: 1px solid #e0e0e0; + font-size: 16px; + transition: border-color 0.2s ease; +} + +.form-group input:focus { + border-color: #007bff; + outline: none; + box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1); +} + +.form-group input::placeholder { + color: #aaa; +} + +.form-group input:disabled { + background-color: #f5f5f5; + cursor: not-allowed; +} + +.error-message { + color: #dc3545; + font-size: 14px; + padding: 8px; + border-radius: 4px; + background-color: rgba(220, 53, 69, 0.1); +} + +.status-message { + color: #0077ff; + font-size: 14px; +} + +.auth-submit-button { + margin-top: 8px; + padding: 14px 20px; + background-color: #0077ff; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +} + +.auth-submit-button:hover { + background-color: #0066dd; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.auth-submit-button:active { + background-color: #0055cc; + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} + +.auth-submit-button:disabled { + background-color: #cccccc; + cursor: not-allowed; + transform: none; + box-shadow: none; +} \ No newline at end of file diff --git a/hyle-wallet/src/components/auth/AuthForm.tsx b/hyle-wallet/src/components/auth/AuthForm.tsx new file mode 100644 index 0000000..c349fa2 --- /dev/null +++ b/hyle-wallet/src/components/auth/AuthForm.tsx @@ -0,0 +1,133 @@ +import React, { useState, useEffect } from 'react'; +import { AuthCredentials, AuthProvider } from '../../providers/BaseAuthProvider'; +import { useWallet, ProviderOption } from '../../hooks/useWallet'; +import { AuthStage } from '../../types/login'; +import './AuthForm.css'; + +interface AuthFormProps { + provider: AuthProvider; + mode: 'login' | 'register'; +} + +export const AuthForm: React.FC = ({ + provider, + mode, +}) => { + const { login, register: registerWallet, stage } = useWallet(); + const [credentials, setCredentials] = useState({ + username: 'bob', + password: 'password123', + confirmPassword: 'password123' + }); + const [error, setError] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Derive UI messaging from stage + const deriveStatusMessage = (stage: AuthStage): string => { + switch (stage) { + case 'submitting': + return 'Sending transaction...'; + case 'blobSent': + return 'Waiting for transaction confirmation...'; + case 'settled': + return 'Success!'; + case 'error': + return 'Error occurred'; + default: + return ''; + } + }; + + const statusMessage = deriveStatusMessage(stage); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setIsSubmitting(true); + + const authAction = mode === 'login' ? login : registerWallet; + + authAction(provider.type as ProviderOption, credentials).catch((err) => { + const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred'; + setError(errorMessage); + setIsSubmitting(false); + }); + }; + + // Reset local submitting flag whenever stage transitions away from 'submitting' + useEffect(() => { + if (stage !== 'submitting') { + setIsSubmitting(false); + } + }, [stage]); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setCredentials(prev => ({ + ...prev, + [name]: value + })); + }; + + return ( +
+
+ + +
+ +
+ + +
+ + {mode === 'register' && ( +
+ + +
+ )} + + {error &&
{error}
} + {statusMessage &&
{statusMessage}
} + + +
+ ); +}; \ No newline at end of file diff --git a/front/src/hooks/useConfig.ts b/hyle-wallet/src/hooks/useConfig.ts similarity index 100% rename from front/src/hooks/useConfig.ts rename to hyle-wallet/src/hooks/useConfig.ts diff --git a/hyle-wallet/src/hooks/useSessionKey.ts b/hyle-wallet/src/hooks/useSessionKey.ts new file mode 100644 index 0000000..9e359bf --- /dev/null +++ b/hyle-wallet/src/hooks/useSessionKey.ts @@ -0,0 +1,24 @@ +// Hook to use session key generation and utilities +import { useCallback } from 'react'; +import { sessionKeyService } from '../services/SessionKeyService'; +import type { Blob } from 'hyle'; + +export const useSessionKey = () => { + const generateSessionKey = useCallback((): [string, string] => { + return sessionKeyService.generateSessionKey(); + }, []); + + const clearSessionKey = useCallback((publicKey: string) => { + sessionKeyService.clear(publicKey); + }, []); + + const createSignedBlobs = useCallback((account: string, privateKey: string, message: string): [Blob, Blob] => { + return sessionKeyService.useSessionKey(account, privateKey, message); + }, []); + + return { + generateSessionKey, + clearSessionKey, + createSignedBlobs, + }; +}; \ No newline at end of file diff --git a/hyle-wallet/src/hooks/useWallet.tsx b/hyle-wallet/src/hooks/useWallet.tsx new file mode 100644 index 0000000..4343038 --- /dev/null +++ b/hyle-wallet/src/hooks/useWallet.tsx @@ -0,0 +1,132 @@ +// useWallet hook and WalletProvider implementation +import React, { createContext, useContext, useState, useCallback } from 'react'; +import type { Wallet } from '../types/wallet'; +import type { AuthCredentials } from '../providers/BaseAuthProvider'; +import { authProviderManager } from '../providers/AuthProviderManager'; +import { AuthStage } from '../types/login'; + +export type ProviderOption = 'password' | 'google' | 'github' | 'x'; + +interface WalletContextType { + wallet: Wallet | null; + isLoading: boolean; + error: string | null; + stage: AuthStage; + login: (provider: ProviderOption, credentials: AuthCredentials) => Promise; + register: (provider: ProviderOption, credentials: AuthCredentials) => Promise; + logout: () => void; +} + +const WalletContext = createContext(undefined); + +export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [wallet, setWallet] = useState(() => { + const storedWallet = localStorage.getItem('wallet'); + return storedWallet ? JSON.parse(storedWallet) : null; + }); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [stage, setStage] = useState('idle'); + + // Persist wallet when updated + React.useEffect(() => { + if (wallet) { + localStorage.setItem('wallet', JSON.stringify(wallet)); + } + }, [wallet]); + + const login = useCallback(async (provider: ProviderOption, credentials: AuthCredentials) => { + const authProvider = authProviderManager.getProvider(provider); + if (!authProvider) { + setError(`Provider ${provider} not found`); + return; + } + try { + setIsLoading(true); + setError(null); + setStage('submitting'); + + const resultPromise = authProvider.login(credentials as any, (optimisticWallet: Wallet) => { + // Called as soon as blob/proofs are sent + setWallet(optimisticWallet); + setStage('blobSent'); + setIsLoading(false); + }); + + const result = await resultPromise; + + if (result.success && result.wallet) { + // Settlement achieved + setWallet(result.wallet); + setStage('settled'); + } else { + setStage('error'); + setError(result.error ?? 'Login failed'); + setWallet(null); + } + } catch (e) { + setStage('error'); + setError(e instanceof Error ? e.message : 'Login failed'); + setWallet(null); + } finally { + setIsLoading(false); + } + }, []); + + const register = useCallback(async (provider: ProviderOption, credentials: AuthCredentials) => { + const authProvider = authProviderManager.getProvider(provider); + if (!authProvider) { + setError(`Provider ${provider} not found`); + return; + } + try { + setIsLoading(true); + setError(null); + setStage('submitting'); + + const resultPromise = authProvider.register(credentials as any, (optimisticWallet: Wallet) => { + setWallet(optimisticWallet); + setStage('blobSent'); + setIsLoading(false); + }); + + const result = await resultPromise; + + if (result.success && result.wallet) { + setWallet(result.wallet); + setStage('settled'); + } else { + setStage('error'); + setError(result.error ?? 'Registration failed'); + setWallet(null); + } + } catch (e) { + setStage('error'); + setError(e instanceof Error ? e.message : 'Registration failed'); + setWallet(null); + } finally { + setIsLoading(false); + } + }, []); + + const logout = useCallback(() => { + localStorage.removeItem('wallet'); + setWallet(null); + setError(null); + setStage('idle'); + }, []); + + return ( + + {children} + + ); +}; + +export const useWallet = (): WalletContextType => { + const ctx = useContext(WalletContext); + if (!ctx) { + throw new Error('useWallet must be used within a WalletProvider'); + } + return ctx; +}; \ No newline at end of file diff --git a/hyle-wallet/src/index.ts b/hyle-wallet/src/index.ts new file mode 100644 index 0000000..3c22348 --- /dev/null +++ b/hyle-wallet/src/index.ts @@ -0,0 +1,20 @@ +export { HyleWallet } from './components/HyleWallet'; +export { PasswordAuthProvider } from './providers/PasswordAuthProvider'; +export type { AuthProvider, AuthCredentials } from './types/auth'; +export type { AuthMethod, Wallet, WalletAction, Transaction } from './types/wallet'; +export { walletContractName } from './types/wallet'; +export { + register, + verifyIdentity, + addSessionKey, + removeSessionKey, + serializeSecp256k1Blob, + serializeIdentityAction, + deserializeIdentityAction, + setWalletContractName +} from './types/wallet'; +export type { ProviderOption } from './hooks/useWallet'; +export { WalletProvider, useWallet } from './hooks/useWallet'; +export { useSessionKey } from './hooks/useSessionKey'; +export { useConfig } from './hooks/useConfig'; +export { sessionKeyService } from './services/SessionKeyService'; \ No newline at end of file diff --git a/hyle-wallet/src/providers/AuthProviderManager.ts b/hyle-wallet/src/providers/AuthProviderManager.ts new file mode 100644 index 0000000..da21f15 --- /dev/null +++ b/hyle-wallet/src/providers/AuthProviderManager.ts @@ -0,0 +1,33 @@ +import { AuthProvider } from './BaseAuthProvider'; +import { PasswordAuthProvider } from './PasswordAuthProvider'; +import { GoogleAuthProvider } from './GoogleAuthProvider'; + +export class AuthProviderManager { + private providers: Map; + + constructor() { + this.providers = new Map(); + this.registerDefaultProviders(); + } + + private registerDefaultProviders() { + this.registerProvider(new PasswordAuthProvider()); + this.registerProvider(new GoogleAuthProvider()); + } + + registerProvider(provider: AuthProvider) { + this.providers.set(provider.type, provider); + } + + getProvider(type: string): AuthProvider | undefined { + return this.providers.get(type); + } + + getAvailableProviders(): string[] { + return Array.from(this.providers.keys()).filter(type => + this.providers.get(type)?.isEnabled() ?? false + ); + } +} + +export const authProviderManager = new AuthProviderManager(); \ No newline at end of file diff --git a/hyle-wallet/src/providers/BaseAuthProvider.ts b/hyle-wallet/src/providers/BaseAuthProvider.ts new file mode 100644 index 0000000..4f22977 --- /dev/null +++ b/hyle-wallet/src/providers/BaseAuthProvider.ts @@ -0,0 +1,19 @@ +import { Wallet } from '../types/wallet'; + +export interface AuthCredentials { + username: string; + [key: string]: any; +} + +export interface AuthResult { + success: boolean; + wallet?: Wallet; + error?: string; +} + +export interface AuthProvider { + type: string; + login(credentials: AuthCredentials, onBlobSent?: (wallet: Wallet) => void): Promise; + register(credentials: AuthCredentials, onBlobSent?: (wallet: Wallet) => void): Promise; + isEnabled(): boolean; +} \ No newline at end of file diff --git a/hyle-wallet/src/providers/GoogleAuthProvider.ts b/hyle-wallet/src/providers/GoogleAuthProvider.ts new file mode 100644 index 0000000..8844fac --- /dev/null +++ b/hyle-wallet/src/providers/GoogleAuthProvider.ts @@ -0,0 +1,24 @@ +import { AuthProvider, AuthCredentials, AuthResult } from './BaseAuthProvider'; +// import { Wallet } from '../types/wallet'; + +export interface GoogleAuthCredentials extends AuthCredentials { + googleToken: string; +} + +export class GoogleAuthProvider implements AuthProvider { + type = 'google'; + + isEnabled(): boolean { + return false; + } + + async login(_credentials: GoogleAuthCredentials): Promise { + // À implémenter avec l'authentification Google + throw new Error('Google authentication not implemented yet'); + } + + async register(_credentials: GoogleAuthCredentials): Promise { + // À implémenter avec l'authentification Google + throw new Error('Google authentication not implemented yet'); + } +} \ No newline at end of file diff --git a/hyle-wallet/src/providers/PasswordAuthProvider.ts b/hyle-wallet/src/providers/PasswordAuthProvider.ts new file mode 100644 index 0000000..2c7330e --- /dev/null +++ b/hyle-wallet/src/providers/PasswordAuthProvider.ts @@ -0,0 +1,182 @@ +import { Buffer } from 'buffer'; +import { AuthProvider, AuthCredentials, AuthResult } from './BaseAuthProvider'; +import { Wallet, register, verifyIdentity, walletContractName } from '../types/wallet'; +import { nodeService } from '../services/NodeService'; +import { webSocketService } from '../services/WebSocketService'; +import { build_proof_transaction, build_blob as check_secret_blob, register_contract } from 'hyle-check-secret'; +import { BlobTransaction } from 'hyle'; + +export interface PasswordAuthCredentials extends AuthCredentials { + password: string; + confirmPassword?: string; +} + +export class PasswordAuthProvider implements AuthProvider { + type = 'password'; + + isEnabled(): boolean { + return true; + } + + async login(credentials: PasswordAuthCredentials, onBlobSent?: (wallet: Wallet) => void): Promise { + try { + const { username, password } = credentials; + + if (!username || !password) { + return { success: false, error: 'Please fill in all fields' }; + } + + const identity = `${username}@${walletContractName}`; + const blob0 = await check_secret_blob(identity, password); + const blob1 = verifyIdentity(username, Date.now()); + + const blobTx: BlobTransaction = { + identity, + blobs: [blob0, blob1], + }; + + const tx_hash = await nodeService.client.sendBlobTx(blobTx); + + // Optimistic notification to the caller that the blobTx has been sent successfully + const optimisticWallet: Wallet = { + username, + address: identity, + }; + onBlobSent?.(optimisticWallet); + + // TODO: Execute noir circuit to make sure the blobTx is valid + + // Build and send the proof transaction (may take some time) + const proofTx = await build_proof_transaction( + identity, + password, + tx_hash, + 0, + blobTx.blobs.length, + ); + + await nodeService.client.sendProofTx(proofTx); + + // Wait for on-chain settlement (existing behaviour) + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + webSocketService.unsubscribeFromWalletEvents(); + reject(new Error('Identity verification timed out')); + }, 30000); + + webSocketService.connect(identity); + const unsubscribeWalletEvents = webSocketService.subscribeToWalletEvents((event) => { + const msg = event.event.toLowerCase(); + if (msg.includes('identity verified')) { + clearTimeout(timeout); + unsubscribeWalletEvents(); + webSocketService.disconnect(); + resolve(event); + } else if (msg.includes('failed') || msg.includes('error')) { + clearTimeout(timeout); + unsubscribeWalletEvents(); + webSocketService.disconnect(); + reject(new Error(event.event)); + } + }); + }); + + const wallet: Wallet = { + username, + address: identity + }; + + return { success: true, wallet }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Invalid credentials or wallet does not exist' + }; + } + } + + async register(credentials: PasswordAuthCredentials, onBlobSent?: (wallet: Wallet) => void): Promise { + try { + const { username, password, confirmPassword } = credentials; + + if (!username || !password || !confirmPassword) { + return { success: false, error: 'Please fill in all fields' }; + } + + if (password !== confirmPassword) { + return { success: false, error: 'Passwords do not match' }; + } + + if (password.length < 8) { + return { success: false, error: 'Password must be at least 8 characters long' }; + } + + const identity = `${username}@${walletContractName}`; + const blob0 = await check_secret_blob(identity, password); + const hash = Buffer.from(blob0.data).toString('hex'); + const blob1 = register(username, Date.now(), hash); + + const blobTx: BlobTransaction = { + identity, + blobs: [blob0, blob1], + }; + + await register_contract(nodeService.client as any); + const tx_hash = await nodeService.client.sendBlobTx(blobTx); + + // Optimistic notification to the caller that the blobTx has been sent successfully + const optimisticWallet: Wallet = { + username, + address: identity, + }; + onBlobSent?.(optimisticWallet); + + // Build and send the proof transaction (may take some time) + const proofTx = await build_proof_transaction( + identity, + password, + tx_hash, + 0, + blobTx.blobs.length, + ); + + await nodeService.client.sendProofTx(proofTx); + + // Wait for on-chain settlement (existing behaviour) + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + webSocketService.unsubscribeFromWalletEvents(); + reject(new Error('Wallet creation timed out')); + }, 60000); + + webSocketService.connect(identity); + const unsubscribeWalletEvents = webSocketService.subscribeToWalletEvents((event) => { + const msg = event.event.toLowerCase(); + if (msg.startsWith('successfully registered identity')) { + clearTimeout(timeout); + unsubscribeWalletEvents(); + webSocketService.disconnect(); + resolve(event); + } else if (msg.includes('failed') || msg.includes('error')) { + clearTimeout(timeout); + unsubscribeWalletEvents(); + webSocketService.disconnect(); + reject(new Error(event.event)); + } + }); + }); + + const wallet: Wallet = { + username, + address: identity + }; + + return { success: true, wallet }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create wallet' + }; + } + } +} \ No newline at end of file diff --git a/hyle-wallet/src/services/NodeService.ts b/hyle-wallet/src/services/NodeService.ts new file mode 100644 index 0000000..08cc9da --- /dev/null +++ b/hyle-wallet/src/services/NodeService.ts @@ -0,0 +1,11 @@ +import { NodeApiHttpClient } from 'hyle'; + +class NodeService { + client: NodeApiHttpClient; + + constructor() { + this.client = new NodeApiHttpClient(import.meta.env.VITE_NODE_BASE_URL); + } +} + +export const nodeService = new NodeService(); diff --git a/front/src/services/SessionKeyService.ts b/hyle-wallet/src/services/SessionKeyService.ts similarity index 78% rename from front/src/services/SessionKeyService.ts rename to hyle-wallet/src/services/SessionKeyService.ts index a6b15b5..ea22d7a 100644 --- a/front/src/services/SessionKeyService.ts +++ b/hyle-wallet/src/services/SessionKeyService.ts @@ -4,15 +4,14 @@ import { Secp256k1Blob, serializeIdentityAction, serializeSecp256k1Blob, WalletA import { Buffer } from 'buffer'; import { Blob } from "hyle"; -class SessionKeyService { +export class SessionKeyService { private ec: EC.ec; constructor() { this.ec = new EC.ec('secp256k1'); } - - generateSessionKey(): string { + generateSessionKey(): [string, string] { // Génère une paire de clés ECDSA const keyPair = this.ec.genKeyPair(); @@ -26,16 +25,10 @@ class SessionKeyService { throw new Error('Failed to generate public key'); } - localStorage.setItem(publicKey, privateKey); - - return publicKey; + return [publicKey, privateKey]; } - getSignedBlob(identity: string, message: string, publicKey: string): Secp256k1Blob { - const privateKey = localStorage.getItem(publicKey); - if (!privateKey) { - throw new Error('No session key or provided private key available'); - } + getSignedBlob(identity: string, message: string, privateKey: string): Secp256k1Blob { const hash = SHA256(message); const hashBytes = Buffer.from(hash.toString(), 'hex'); @@ -44,6 +37,7 @@ class SessionKeyService { } const keyPair = this.ec.keyFromPrivate(privateKey); + const publicKey = keyPair.getPublic(true, 'hex'); const signature = keyPair.sign(hash.toString()); // Normaliser s en utilisant min(s, n-s) @@ -61,17 +55,18 @@ class SessionKeyService { public_key: new Uint8Array(Buffer.from(publicKey, 'hex')), signature: signatureBytes, }; - console.log('secp256k1Blob', secp256k1Blob); return secp256k1Blob; } - useSessionKey(account: string, key: string, message: string): [Blob, Blob] { + useSessionKey(account: string, privateKey: string, message: string): [Blob, Blob] { + const publicKey = this.ec.keyFromPrivate(privateKey).getPublic(true, 'hex'); + const action: WalletAction = { - UseSessionKey: { account, key, message } + UseSessionKey: { account, key: publicKey, message } }; const identity = `${account}@${walletContractName}`; - const secp256k1Blob: Secp256k1Blob = this.getSignedBlob(identity, message, key); + const secp256k1Blob: Secp256k1Blob = this.getSignedBlob(identity, message, privateKey); const blob0: Blob = { contract_name: "secp256k1", data: serializeSecp256k1Blob(secp256k1Blob), diff --git a/hyle-wallet/src/services/WebSocketService.ts b/hyle-wallet/src/services/WebSocketService.ts new file mode 100644 index 0000000..f169218 --- /dev/null +++ b/hyle-wallet/src/services/WebSocketService.ts @@ -0,0 +1,128 @@ +import { Transaction } from "../types/wallet"; + +export interface AppEvent { + TxEvent: { + account: string; + tx: Transaction + }; + WalletEvent: { + account: string; + event: string; + }; +} + +interface RegisterTopicMessage { + RegisterTopic: string; +} + +type TxEventCallback = (event: AppEvent["TxEvent"]) => void; +type WalletEventCallback = (event: AppEvent["WalletEvent"]) => void; + +export class WebSocketService { + private ws: WebSocket | null = null; + private txEventCallbacks: TxEventCallback[] = []; + private walletEventCallbacks: WalletEventCallback[] = []; + private reconnectAttempts: number = 0; + private maxReconnectAttempts: number = 5; + private reconnectTimeout: number = 1000; + private currentAccount: string | null = null; + + constructor() {} + + connect(account: string) { + if (this.ws) { + console.log("WebSocket already connected"); + if (this.currentAccount != account) { + this.disconnect(); + } else { + return; + } + } + + this.currentAccount = account; + this.ws = new WebSocket(import.meta.env.VITE_WALLET_WS_URL); + + this.ws.onopen = () => { + console.log("WebSocket connected"); + this.reconnectAttempts = 0; + // Send registration message + const registerMessage: RegisterTopicMessage = { + RegisterTopic: account, + }; + this.ws?.send(JSON.stringify(registerMessage)); + }; + + this.ws.onmessage = (event) => { + try { + const data: AppEvent = JSON.parse(event.data); + if (data.TxEvent) { + this.txEventCallbacks.forEach(callback => callback(data.TxEvent)); + } + if (data.WalletEvent) { + this.walletEventCallbacks.forEach(callback => callback(data.WalletEvent)); + } + } catch (error) { + console.error("Error parsing WebSocket message:", error); + } + }; + + this.ws.onclose = () => { + console.log("WebSocket disconnected"); + this.handleReconnect(); + }; + + this.ws.onerror = (error) => { + console.error("WebSocket error:", error); + }; + } + + private handleReconnect() { + if ( + this.reconnectAttempts < this.maxReconnectAttempts && + this.currentAccount + ) { + this.reconnectAttempts++; + setTimeout(() => { + console.log( + `Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`, + ); + this.connect(this.currentAccount!); + }, this.reconnectTimeout * this.reconnectAttempts); + } + } + + subscribeToTxEvents(callback: TxEventCallback): () => void { + this.txEventCallbacks.push(callback); + return () => { + this.txEventCallbacks = this.txEventCallbacks.filter(cb => cb !== callback); + }; + } + + subscribeToWalletEvents(callback: WalletEventCallback): () => void { + this.walletEventCallbacks.push(callback); + return () => { + this.walletEventCallbacks = this.walletEventCallbacks.filter(cb => cb !== callback); + }; + } + + unsubscribeFromTxEvents() { + this.txEventCallbacks = []; + } + + unsubscribeFromWalletEvents() { + this.walletEventCallbacks = []; + } + + disconnect() { + if (this.ws) { + this.ws.close(); + this.ws = null; + this.currentAccount = null; + this.txEventCallbacks = []; + this.walletEventCallbacks = []; + } + } +} + +export const webSocketService = new WebSocketService(); + diff --git a/front/src/services/config.ts b/hyle-wallet/src/services/config.ts similarity index 78% rename from front/src/services/config.ts rename to hyle-wallet/src/services/config.ts index 959d085..ab53e17 100644 --- a/front/src/services/config.ts +++ b/hyle-wallet/src/services/config.ts @@ -4,7 +4,7 @@ interface Config { export async function fetchConfig(): Promise { const response = await fetch( - `${import.meta.env.VITE_SERVER_BASE_URL}/api/config`, + `${import.meta.env.VITE_WALLET_SERVER_BASE_URL}/api/config`, ); if (!response.ok) { throw new Error("Failed to fetch config"); diff --git a/hyle-wallet/src/types/auth.ts b/hyle-wallet/src/types/auth.ts new file mode 100644 index 0000000..ab4e14e --- /dev/null +++ b/hyle-wallet/src/types/auth.ts @@ -0,0 +1,13 @@ +import { Wallet } from './wallet'; + +export interface AuthCredentials { + type: string; + [key: string]: any; +} + +export interface AuthProvider { + type: string; + authenticate(): Promise; + verify(credentials: AuthCredentials): Promise; + disconnect(): void; +} \ No newline at end of file diff --git a/hyle-wallet/src/types/login.ts b/hyle-wallet/src/types/login.ts new file mode 100644 index 0000000..f992c0b --- /dev/null +++ b/hyle-wallet/src/types/login.ts @@ -0,0 +1,9 @@ +/** + * Represents the different stages of the authentication process + */ +export type AuthStage = + | 'idle' // Initial state, no authentication in progress + | 'submitting' // Authentication request is being sent + | 'blobSent' // Blob/proofs have been sent and we're waiting for confirmation + | 'settled' // Authentication has completed successfully + | 'error'; // An error occurred during authentication diff --git a/front/src/types/wallet.ts b/hyle-wallet/src/types/wallet.ts similarity index 89% rename from front/src/types/wallet.ts rename to hyle-wallet/src/types/wallet.ts index d2a4aba..0b72927 100644 --- a/front/src/types/wallet.ts +++ b/hyle-wallet/src/types/wallet.ts @@ -135,7 +135,21 @@ export const removeSessionKey = (account: string, key: string): Blob => { return blob; }; -// Removed the `useSessionKey` function as it has been moved to `SessionKeyService`. +// Store wallet in localStorage +export const storeWallet = (wallet: Wallet) => { + localStorage.setItem('wallet', JSON.stringify(wallet)); +}; + +// Get wallet from localStorage +export const getStoredWallet = (): Wallet | null => { + const storedWallet = localStorage.getItem('wallet'); + return storedWallet ? JSON.parse(storedWallet) : null; +}; + +// Clear wallet from localStorage +export const clearStoredWallet = () => { + localStorage.removeItem('wallet'); +}; // // Serialisation diff --git a/hyle-wallet/src/vite-env.d.ts b/hyle-wallet/src/vite-env.d.ts new file mode 100644 index 0000000..b5a5ac6 --- /dev/null +++ b/hyle-wallet/src/vite-env.d.ts @@ -0,0 +1,14 @@ +/// + +interface ImportMetaEnv { + readonly VITE_WALLET_SERVER_BASE_URL: string; + readonly VITE_WALLET_WS_URL: string; + readonly VITE_NODE_BASE_URL: string; + readonly VITE_INDEXER_BASE_URL: string; + readonly VITE_TX_EXPLORER_URL: string; + readonly VITE_FAUCET_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} \ No newline at end of file diff --git a/hyle-wallet/tsconfig.json b/hyle-wallet/tsconfig.json new file mode 100644 index 0000000..886e16d --- /dev/null +++ b/hyle-wallet/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["DOM", "ESNext"], + "declaration": true, + "declarationDir": "dist/types", + "emitDeclarationOnly": false, + "outDir": "dist", + "moduleResolution": "Node", + "esModuleInterop": true, + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src"], + "exclude": ["dist", "node_modules", "bun.lockb"] +} \ No newline at end of file diff --git a/hyle-wallet/vite.config.ts b/hyle-wallet/vite.config.ts new file mode 100644 index 0000000..866ad3f --- /dev/null +++ b/hyle-wallet/vite.config.ts @@ -0,0 +1,39 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import dts from 'vite-plugin-dts' +import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js' + +export default defineConfig({ + build: { + lib: { + entry: 'src/index.ts', + name: 'HyleWallet', + fileName: (format) => `hyle-wallet.${format}.js`, + formats: ['es', 'cjs'] + }, + rollupOptions: { + external: [ + 'react', + 'react-dom', + 'hyle-check-secret', + 'barretenberg', + 'barretenberg/threads' + ], + output: { + globals: { + react: 'React', + 'react-dom': 'ReactDOM' + } + } + }, + outDir: 'dist' + }, + plugins: [ + react(), + dts({ + entryRoot: 'src', + insertTypesEntry: true + }), + cssInjectedByJsPlugin() + ], +});