From 9948e07efce56ee23786d7bd9b8742e80e4a7894 Mon Sep 17 00:00:00 2001 From: David-patrick-chuks Date: Tue, 24 Mar 2026 07:00:58 +0100 Subject: [PATCH 1/5] feat(extension-wallet): add popup router and navigation --- apps/extension-wallet/package.json | 5 +- apps/extension-wallet/postcss.config.js | 2 +- apps/extension-wallet/src/App.tsx | 304 +--------- .../src/components/Navigation/NavBar.tsx | 41 ++ .../src/errors/__tests__/errors.test.ts | 19 +- .../src/errors/error-handler.ts | 45 +- apps/extension-wallet/src/index.ts | 3 + apps/extension-wallet/src/main.tsx | 6 +- .../extension-wallet/src/router/AuthGuard.tsx | 153 +++++ .../src/router/__tests__/router.test.tsx | 93 +++ apps/extension-wallet/src/router/index.tsx | 542 ++++++++++++++++++ .../src/screens/Settings/SecuritySettings.tsx | 191 ++++-- .../__tests__/TransactionDetail.test.tsx | 2 +- apps/extension-wallet/tailwind.config.js | 14 +- apps/extension-wallet/tsconfig.json | 3 +- apps/extension-wallet/vite.config.ts | 5 +- apps/extension-wallet/vitest.config.ts | 7 +- pnpm-lock.yaml | 46 +- 18 files changed, 1067 insertions(+), 414 deletions(-) create mode 100644 apps/extension-wallet/src/components/Navigation/NavBar.tsx create mode 100644 apps/extension-wallet/src/router/AuthGuard.tsx create mode 100644 apps/extension-wallet/src/router/__tests__/router.test.tsx create mode 100644 apps/extension-wallet/src/router/index.tsx diff --git a/apps/extension-wallet/package.json b/apps/extension-wallet/package.json index 4e305f6..935a951 100644 --- a/apps/extension-wallet/package.json +++ b/apps/extension-wallet/package.json @@ -18,7 +18,8 @@ "lucide-react": "^0.344.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-error-boundary": "^6.1.1" + "react-error-boundary": "^6.1.1", + "react-router-dom": "^6.20.0" }, "devDependencies": { "@eslint/js": "^9.0.0", @@ -43,4 +44,4 @@ "vite": "^5.0.0", "vitest": "^1.0.0" } -} \ No newline at end of file +} diff --git a/apps/extension-wallet/postcss.config.js b/apps/extension-wallet/postcss.config.js index 12a703d..2aa7205 100644 --- a/apps/extension-wallet/postcss.config.js +++ b/apps/extension-wallet/postcss.config.js @@ -1,4 +1,4 @@ -module.exports = { +export default { plugins: { tailwindcss: {}, autoprefixer: {}, diff --git a/apps/extension-wallet/src/App.tsx b/apps/extension-wallet/src/App.tsx index 1c71714..c80987a 100644 --- a/apps/extension-wallet/src/App.tsx +++ b/apps/extension-wallet/src/App.tsx @@ -1,303 +1 @@ -/** - * App Component Example - * - * Demonstrates how to use ErrorBoundary and error-handler in a React application. - * This example shows: - * 1. Wrapping the app with ErrorBoundary - * 2. Using error-handler in async functions - * 3. Implementing retry functionality - * 4. Handling different error categories - */ - -import { useState, useEffect, useCallback } from 'react'; -import { ErrorBoundary, useErrorHandler, handleError, ErrorCategory, withErrorHandling, createRetryable } from './errors'; - -/** - * Sample data type - */ -interface UserData { - id: string; - name: string; - balance: string; -} - -/** - * Example component that fetches data - demonstrates async error handling - * Uses the error-handler to classify and log errors - */ -function DataFetcher(): JSX.Element { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - // Use the error handler hook for manual error dispatching - const { dispatch, reset } = useErrorHandler(); - - const fetchData = useCallback(async () => { - setLoading(true); - setError(null); - - try { - // Simulate a network request that might fail - const response = await fetch('/api/user'); - - if (!response.ok) { - // Use the global error handler to classify the error - const errorInfo = handleError(new Error(`HTTP ${response.status}: ${response.statusText}`), 'fetchUserData'); - throw new Error(errorInfo.message); - } - - const userData = await response.json(); - setData(userData); - } catch (err) { - const handledError = handleError(err, 'fetchUserData'); - - // Log the error (handled by error-handler internally) - console.log('Error category:', handledError.category); - console.log('Recoverable:', handledError.recoverable); - - setError(handledError.originalError as Error); - } finally { - setLoading(false); - } - }, []); - - // Initial fetch on mount - useEffect(() => { - fetchData(); - }, [fetchData]); - - if (error) { - return ( -
-

Error: {error.message}

-
- - -
-
- ); - } - - if (loading) { - return
Loading...
; - } - - return ( -
-

User Data

- {data && ( -
    -
  • ID: {data.id}
  • -
  • Name: {data.name}
  • -
  • Balance: {data.balance}
  • -
- )} - -
- ); -} - -/** - * Example component using withErrorHandling HOC - * Wraps an async function with automatic error handling - */ -async function fetchUserBalance(userId: string): Promise { - // Simulate network call - const response = await fetch(`/api/balance/${userId}`); - - if (!response.ok) { - throw new Error('Failed to fetch balance'); - } - - const data = await response.json(); - return data.balance; -} - -// Wrap the function with error handling - using type assertion for demo -const fetchUserBalanceWithErrorHandling = withErrorHandling(fetchUserBalance as any, 'fetchUserBalance'); - -/** - * Example component using createRetryable - * Creates a function that automatically retries on failure - */ -async function submitTransaction(txData: object): Promise<{ txHash: string }> { - // Simulate transaction submission - const response = await fetch('/api/submit', { - method: 'POST', - body: JSON.stringify(txData), - }); - - if (!response.ok) { - throw new Error('Transaction failed'); - } - - return response.json(); -} - -// Create a retryable version that retries up to 3 times -const submitTransactionWithRetry = createRetryable(submitTransaction as any, 3, 1000); - -/** - * Transaction component - demonstrates retry functionality - */ -function TransactionComponent(): JSX.Element { - const [status, setStatus] = useState('idle'); - const [txHash, setTxHash] = useState(null); - - const handleSubmit = async () => { - setStatus('submitting'); - - const result = await submitTransactionWithRetry({ amount: 100 }) as { txHash: string }; - - if ('txHash' in result) { - setTxHash(result.txHash); - setStatus('success'); - } else { - setStatus('failed'); - } - }; - - return ( -
-

Transaction

-

Status: {status}

- {txHash &&

Tx Hash: {txHash}

} - -
- ); -} - -/** - * Main App component - wrapped with ErrorBoundary - * The ErrorBoundary will catch any rendering errors in children - */ -export function App(): JSX.Element { - // Callback for handling errors that escape component boundaries - const handleAppError = (error: Error, errorInfo: React.ErrorInfo) => { - console.error('App-level error:', error, errorInfo.componentStack); - }; - - // Callback for resetting app state - const handleReset = () => { - console.log('App reset requested'); - }; - - return ( -
-

Extension Wallet

- - {/* Wrap the entire app with ErrorBoundary */} - -
- {/* Example 1: Data fetching with manual error handling */} -
-

Data Fetcher Example

- -
- - {/* Example 2: Transaction with retry */} -
-

Transaction Example (with retry)

- -
- - {/* Example 3: Direct error handler usage */} -
-

Direct Error Handler Example

- -
-
-
-
- ); -} - -/** - * Component demonstrating direct use of error-handler - */ -function DirectErrorExample(): JSX.Element { - const [result, setResult] = useState(null); - - const testNetworkError = () => { - const errorInfo = handleError(new Error('ECONNREFUSED: Connection refused'), 'networkTest'); - setResult(`Category: ${errorInfo.category}, Recoverable: ${errorInfo.recoverable}`); - }; - - const testValidationError = () => { - const errorInfo = handleError(new Error('validation failed: invalid address'), 'validationTest'); - setResult(`Category: ${errorInfo.category}, Recoverable: ${errorInfo.recoverable}`); - }; - - const testContractError = () => { - const errorInfo = handleError(new Error('Contract: execution reverted'), 'contractTest'); - setResult(`Category: ${errorInfo.category}, Recoverable: ${errorInfo.recoverable}`); - }; - - const testUnknownError = () => { - const errorInfo = handleError(new Error('Something unexpected'), 'unknownTest'); - setResult(`Category: ${errorInfo.category}, Recoverable: ${errorInfo.recoverable}`); - }; - - return ( -
-
- - - - -
- {result && ( -

- {result} -

- )} -
- ); -} - -export default App; +export { ExtensionRouter as App } from './router'; diff --git a/apps/extension-wallet/src/components/Navigation/NavBar.tsx b/apps/extension-wallet/src/components/Navigation/NavBar.tsx new file mode 100644 index 0000000..b072f53 --- /dev/null +++ b/apps/extension-wallet/src/components/Navigation/NavBar.tsx @@ -0,0 +1,41 @@ +import { ArrowDownLeft, ArrowUpRight, History, Home, Settings } from 'lucide-react'; +import { NavLink } from 'react-router-dom'; + +const items = [ + { to: '/home', label: 'Home', icon: Home }, + { to: '/send', label: 'Send', icon: ArrowUpRight }, + { to: '/receive', label: 'Receive', icon: ArrowDownLeft }, + { to: '/history', label: 'History', icon: History }, + { to: '/settings', label: 'Settings', icon: Settings }, +]; + +export function NavBar() { + return ( + + ); +} diff --git a/apps/extension-wallet/src/errors/__tests__/errors.test.ts b/apps/extension-wallet/src/errors/__tests__/errors.test.ts index 0ad891f..6bd02a7 100644 --- a/apps/extension-wallet/src/errors/__tests__/errors.test.ts +++ b/apps/extension-wallet/src/errors/__tests__/errors.test.ts @@ -1,6 +1,6 @@ /** * Error Handling System Tests - * + * * Unit tests for: * - ErrorBoundary catching errors * - ErrorScreen UI rendering @@ -8,8 +8,17 @@ * - Recovery functionality */ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { ErrorHandler, ErrorCategory, handleError, classifyError, getErrorUserMessage, withErrorHandling, createRetryable, getErrorHandler } from '../error-handler'; +import { describe, it, expect, beforeEach } from 'vitest'; +import { + ErrorHandler, + ErrorCategory, + handleError, + classifyError, + getErrorUserMessage, + withErrorHandling, + createRetryable, + getErrorHandler, +} from '../error-handler'; import { getErrorMessage, ERROR_MESSAGES } from '../error-messages'; describe('ErrorHandler', () => { @@ -90,7 +99,7 @@ describe('ErrorHandler', () => { it('should extract node error code property', () => { const error = new Error('Test error'); - (error as any).code = 'ENOENT'; + (error as Error & { code?: string }).code = 'ENOENT'; const code = errorHandler.extractErrorCode(error); expect(code).toBe('ENOENT'); }); @@ -284,7 +293,7 @@ describe('Recovery functionality', () => { const result = await retryableFn(); expect(result).toHaveProperty('category'); - expect((result as any).category).toBe(ErrorCategory.VALIDATION); + expect((result as { category: ErrorCategory }).category).toBe(ErrorCategory.VALIDATION); }); }); }); diff --git a/apps/extension-wallet/src/errors/error-handler.ts b/apps/extension-wallet/src/errors/error-handler.ts index 4f3847b..5fa9367 100644 --- a/apps/extension-wallet/src/errors/error-handler.ts +++ b/apps/extension-wallet/src/errors/error-handler.ts @@ -1,11 +1,11 @@ /** * Error Handler Module - * + * * Provides global error classification and handling functionality. * Classifies errors into network, validation, or contract errors and logs them locally. */ -import { ErrorMessage, getErrorMessage, getFallbackErrorMessage } from './error-messages'; +import { ErrorMessage, getErrorMessage } from './error-messages'; /** * Error categories for classification @@ -122,17 +122,17 @@ export class ErrorHandler { const errorString = this.errorToString(error).toLowerCase(); // Check for network errors - if (NETWORK_ERROR_PATTERNS.some(pattern => errorString.includes(pattern.toLowerCase()))) { + if (NETWORK_ERROR_PATTERNS.some((pattern) => errorString.includes(pattern.toLowerCase()))) { return ErrorCategory.NETWORK; } // Check for contract errors - if (CONTRACT_ERROR_PATTERNS.some(pattern => errorString.includes(pattern.toLowerCase()))) { + if (CONTRACT_ERROR_PATTERNS.some((pattern) => errorString.includes(pattern.toLowerCase()))) { return ErrorCategory.CONTRACT; } // Check for validation errors - if (VALIDATION_ERROR_PATTERNS.some(pattern => errorString.includes(pattern.toLowerCase()))) { + if (VALIDATION_ERROR_PATTERNS.some((pattern) => errorString.includes(pattern.toLowerCase()))) { return ErrorCategory.VALIDATION; } @@ -151,13 +151,13 @@ export class ErrorHandler { if (codeMatch) { return codeMatch[1]; } - + // Check for HTTP status codes const statusMatch = error.message.match(/\b(4\d{2}|5\d{2})\b/); if (statusMatch) { return statusMatch[1]; } - + // Check for node error code property if ('code' in error && typeof error.code === 'string') { return error.code; @@ -203,9 +203,11 @@ export class ErrorHandler { * @returns Whether the error is recoverable */ isRecoverable(category: ErrorCategory): boolean { - return category === ErrorCategory.NETWORK || - category === ErrorCategory.VALIDATION || - category === ErrorCategory.CONTRACT; + return ( + category === ErrorCategory.NETWORK || + category === ErrorCategory.VALIDATION || + category === ErrorCategory.CONTRACT + ); } /** @@ -242,12 +244,12 @@ export class ErrorHandler { if (this.config.logToStorage) { this.errorLog.push(errorInfo); - + // Trim log if it exceeds max size if (this.errorLog.length > this.config.maxStoredErrors) { this.errorLog = this.errorLog.slice(-this.config.maxStoredErrors); } - + this.saveErrorLog(); } } @@ -287,10 +289,7 @@ export class ErrorHandler { */ private saveErrorLog(): void { try { - localStorage.setItem( - this.config.storageKey, - JSON.stringify(this.errorLog) - ); + localStorage.setItem(this.config.storageKey, JSON.stringify(this.errorLog)); } catch { // Ignore storage errors (e.g., quota exceeded) } @@ -367,7 +366,7 @@ export function withErrorHandling Promise) => Promise> { return async (...args: Parameters): Promise> => { try { - return await fn(...args) as ReturnType; + return (await fn(...args)) as ReturnType; } catch (error) { const errorInfo = handleError(error, context); return errorInfo as ErrorInfo; @@ -389,27 +388,27 @@ export function createRetryable Promise) => Promise> { return async (...args: Parameters): Promise> => { let lastError: unknown; - + for (let attempt = 0; attempt <= maxRetries; attempt++) { try { - return await fn(...args) as ReturnType; + return (await fn(...args)) as ReturnType; } catch (error) { lastError = error; - + // Don't retry on validation errors const category = classifyError(error); if (category === ErrorCategory.VALIDATION) { const errorInfo = handleError(error); return errorInfo as ErrorInfo; } - + // Wait before retrying if (attempt < maxRetries) { - await new Promise(resolve => setTimeout(resolve, delay * (attempt + 1))); + await new Promise((resolve) => setTimeout(resolve, delay * (attempt + 1))); } } } - + // All retries failed const errorInfo = handleError(lastError!, `Retry failed after ${maxRetries} attempts`); return errorInfo as ErrorInfo; diff --git a/apps/extension-wallet/src/index.ts b/apps/extension-wallet/src/index.ts index 213bc95..6084767 100644 --- a/apps/extension-wallet/src/index.ts +++ b/apps/extension-wallet/src/index.ts @@ -1,3 +1,6 @@ export * from './components/TransactionStatus'; +export * from './components/Navigation/NavBar'; +export * from './router'; +export * from './router/AuthGuard'; export * from './screens/TransactionDetail'; export * from './utils/explorer-links'; diff --git a/apps/extension-wallet/src/main.tsx b/apps/extension-wallet/src/main.tsx index d5e6a83..97b6f6d 100644 --- a/apps/extension-wallet/src/main.tsx +++ b/apps/extension-wallet/src/main.tsx @@ -1,12 +1,10 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import { SettingsScreen } from './screens/Settings/SettingsScreen'; +import { ExtensionRouter } from './router'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( -
- -
+
); diff --git a/apps/extension-wallet/src/router/AuthGuard.tsx b/apps/extension-wallet/src/router/AuthGuard.tsx new file mode 100644 index 0000000..2b69d6b --- /dev/null +++ b/apps/extension-wallet/src/router/AuthGuard.tsx @@ -0,0 +1,153 @@ +import * as React from 'react'; +import { Navigate, Outlet, useLocation } from 'react-router-dom'; + +export const AUTH_STORAGE_KEY = 'ancore_extension_auth'; + +export interface AuthState { + hasOnboarded: boolean; + isUnlocked: boolean; + walletName: string; + accountAddress: string; +} + +export const DEFAULT_AUTH_STATE: AuthState = { + hasOnboarded: false, + isUnlocked: false, + walletName: 'Ancore Wallet', + accountAddress: 'GCFX...WALLET', +}; + +interface AuthContextValue { + authState: AuthState; + completeOnboarding: (walletName: string) => void; + unlockWallet: () => void; + lockWallet: () => void; + resetWallet: () => void; +} + +const AuthContext = React.createContext(null); + +export function readAuthState(): AuthState { + if (typeof window === 'undefined') { + return DEFAULT_AUTH_STATE; + } + + try { + const raw = window.localStorage.getItem(AUTH_STORAGE_KEY); + if (!raw) { + return DEFAULT_AUTH_STATE; + } + + return { + ...DEFAULT_AUTH_STATE, + ...JSON.parse(raw), + }; + } catch { + return DEFAULT_AUTH_STATE; + } +} + +function writeAuthState(authState: AuthState): void { + window.localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(authState)); +} + +export function ExtensionAuthProvider({ children }: { children: React.ReactNode }) { + const [authState, setAuthState] = React.useState(readAuthState); + + React.useEffect(() => { + writeAuthState(authState); + }, [authState]); + + React.useEffect(() => { + function handleStorage(event: StorageEvent) { + if (event.key === AUTH_STORAGE_KEY) { + setAuthState(readAuthState()); + } + } + + window.addEventListener('storage', handleStorage); + return () => window.removeEventListener('storage', handleStorage); + }, []); + + const value = React.useMemo( + () => ({ + authState, + completeOnboarding: (walletName: string) => { + setAuthState({ + hasOnboarded: true, + isUnlocked: true, + walletName: walletName.trim() || DEFAULT_AUTH_STATE.walletName, + accountAddress: DEFAULT_AUTH_STATE.accountAddress, + }); + }, + unlockWallet: () => { + setAuthState((current) => ({ + ...current, + hasOnboarded: true, + isUnlocked: true, + })); + }, + lockWallet: () => { + setAuthState((current) => ({ + ...current, + isUnlocked: false, + })); + }, + resetWallet: () => { + setAuthState(DEFAULT_AUTH_STATE); + }, + }), + [authState] + ); + + return {children}; +} + +export function useExtensionAuth(): AuthContextValue { + const context = React.useContext(AuthContext); + + if (!context) { + throw new Error('useExtensionAuth must be used within ExtensionAuthProvider'); + } + + return context; +} + +export function AuthGuard() { + const { authState } = useExtensionAuth(); + const location = useLocation(); + + if (!authState.hasOnboarded) { + return ; + } + + if (!authState.isUnlocked) { + return ; + } + + return ; +} + +export function PublicOnlyGuard({ + children, + mode, +}: { + children: React.ReactElement; + mode: 'welcome' | 'create-account' | 'unlock'; +}) { + const { authState } = useExtensionAuth(); + + if (mode === 'unlock') { + if (authState.isUnlocked) { + return ; + } + + return children; + } + + if (authState.hasOnboarded) { + return ; + } + + return children; +} diff --git a/apps/extension-wallet/src/router/__tests__/router.test.tsx b/apps/extension-wallet/src/router/__tests__/router.test.tsx new file mode 100644 index 0000000..f4fa3a3 --- /dev/null +++ b/apps/extension-wallet/src/router/__tests__/router.test.tsx @@ -0,0 +1,93 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { AUTH_STORAGE_KEY, DEFAULT_AUTH_STATE } from '../AuthGuard'; +import { ExtensionRouterTestHarness } from '..'; + +function renderRouter(pathname: string, authState = DEFAULT_AUTH_STATE) { + window.localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(authState)); + return render(); +} + +describe('extension router', () => { + beforeEach(() => { + window.localStorage.clear(); + document.title = 'Ancore Extension'; + }); + + it('redirects first-time users to welcome when they hit a protected route', () => { + renderRouter('/home'); + + expect(screen.getByRole('heading', { name: /meet your ancore wallet/i })).toBeInTheDocument(); + expect(document.title).toBe('Welcome | Ancore Extension'); + }); + + it('redirects onboarded locked users to unlock', () => { + renderRouter('/send', { + ...DEFAULT_AUTH_STATE, + hasOnboarded: true, + walletName: 'Locked Wallet', + }); + + expect(screen.getByRole('heading', { name: /unlock wallet/i })).toBeInTheDocument(); + expect(document.title).toBe('Unlock Wallet | Ancore Extension'); + }); + + it('creates an account and lands on the protected home route', async () => { + const user = userEvent.setup(); + renderRouter('/create-account'); + + await user.clear(screen.getByLabelText(/wallet name/i)); + await user.type(screen.getByLabelText(/wallet name/i), 'Router Test Wallet'); + await user.click(screen.getByRole('button', { name: /create wallet/i })); + + expect(await screen.findByRole('heading', { name: /home/i })).toBeInTheDocument(); + expect(screen.getByText(/router test wallet/i)).toBeInTheDocument(); + expect(screen.getByTestId('nav-bar')).toBeInTheDocument(); + }); + + it('navigates between protected routes and updates titles', async () => { + const user = userEvent.setup(); + renderRouter('/home', { + ...DEFAULT_AUTH_STATE, + hasOnboarded: true, + isUnlocked: true, + }); + + await user.click(screen.getByRole('link', { name: /settings/i })); + + expect(await screen.findByRole('heading', { name: /settings/i })).toBeInTheDocument(); + expect(document.title).toBe('Settings | Ancore Extension'); + }); + + it('shows a 404 screen for unknown routes and recovers to the right fallback', async () => { + const user = userEvent.setup(); + renderRouter('/not-a-real-route', { + ...DEFAULT_AUTH_STATE, + hasOnboarded: true, + isUnlocked: true, + }); + + expect(screen.getByRole('heading', { name: '404' })).toBeInTheDocument(); + expect(document.title).toBe('Page Not Found | Ancore Extension'); + + await user.click(screen.getByRole('link', { name: /go back to safety/i })); + + expect(await screen.findByRole('heading', { name: /home/i })).toBeInTheDocument(); + }); + + it('supports back-style navigation for nested routes', async () => { + const user = userEvent.setup(); + renderRouter('/session-keys', { + ...DEFAULT_AUTH_STATE, + hasOnboarded: true, + isUnlocked: true, + }); + + expect(screen.getByRole('heading', { name: /session keys/i })).toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: /go back/i })); + + expect(await screen.findByRole('heading', { name: /settings/i })).toBeInTheDocument(); + }); +}); diff --git a/apps/extension-wallet/src/router/index.tsx b/apps/extension-wallet/src/router/index.tsx new file mode 100644 index 0000000..521d66a --- /dev/null +++ b/apps/extension-wallet/src/router/index.tsx @@ -0,0 +1,542 @@ +import * as React from 'react'; +import { + BrowserRouter, + Link, + MemoryRouter, + Navigate, + Outlet, + Route, + Routes, + useLocation, + useNavigate, +} from 'react-router-dom'; +import { + ArrowLeft, + ArrowRight, + Copy, + Lock, + PlusCircle, + ShieldCheck, + Sparkles, + Wallet, +} from 'lucide-react'; +import { AuthGuard, ExtensionAuthProvider, PublicOnlyGuard, useExtensionAuth } from './AuthGuard'; +import { NavBar } from '../components/Navigation/NavBar'; +import { SettingsScreen } from '../screens/Settings/SettingsScreen'; + +const APP_TITLE = 'Ancore Extension'; + +const pageTitles: Record = { + '/unlock': 'Unlock Wallet', + '/welcome': 'Welcome', + '/create-account': 'Create Account', + '/home': 'Home', + '/send': 'Send', + '/receive': 'Receive', + '/history': 'History', + '/settings': 'Settings', + '/session-keys': 'Session Keys', +}; + +function getPageTitle(pathname: string): string { + return pageTitles[pathname] ?? 'Page Not Found'; +} + +function TitleSync() { + const location = useLocation(); + + React.useEffect(() => { + document.title = `${getPageTitle(location.pathname)} | ${APP_TITLE}`; + }, [location.pathname]); + + return null; +} + +function PopupFrame({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function RootRedirect() { + const { authState } = useExtensionAuth(); + + if (!authState.hasOnboarded) { + return ; + } + + return ; +} + +function ProtectedLayout() { + return ( +
+
+ +
+ +
+ ); +} + +function PageScaffold({ + eyebrow, + title, + description, + children, + backTo, + rightAction, +}: { + eyebrow?: string; + title: string; + description: string; + children: React.ReactNode; + backTo?: string; + rightAction?: React.ReactNode; +}) { + const navigate = useNavigate(); + + return ( +
+
+
+ {backTo ? ( + + ) : ( + + )} + {rightAction ?? } +
+ {eyebrow ? ( +

+ {eyebrow} +

+ ) : null} +

{title}

+

{description}

+
+
{children}
+
+ ); +} + +function Card({ + title, + description, + children, +}: { + title: string; + description?: string; + children?: React.ReactNode; +}) { + return ( +
+

{title}

+ {description ?

{description}

: null} + {children ?
{children}
: null} +
+ ); +} + +function PrimaryButton({ + className, + type = 'button', + ...props +}: React.ButtonHTMLAttributes) { + return ( + + + ); +} + +function HomeScreen() { + const { authState, lockWallet } = useExtensionAuth(); + + return ( + + + + } + > + +
+

Available balance

+

1,245.80 XLM

+

{authState.accountAddress}

+
+
+
+ Send funds + Receive funds + View history + Session keys +
+
+ ); +} + +function SendScreen() { + return ( + + +
+ + + Review transaction +
+
+
+ ); +} + +function ReceiveScreen() { + return ( + + +
+ GCFX...WALLET +
+
+ + +
+
+
+ ); +} + +function HistoryScreen() { + const entries = [ + { id: '1', label: 'Received from Treasury', amount: '+320 XLM', date: 'Today' }, + { id: '2', label: 'Sent to Merchant', amount: '-48 XLM', date: 'Yesterday' }, + { id: '3', label: 'Session key refresh', amount: 'Security event', date: 'Mar 23' }, + ]; + + return ( + + +
+ {entries.map((entry) => ( +
+
+

{entry.label}

+

{entry.date}

+
+ {entry.amount} +
+ ))} +
+
+
+ ); +} + +function SessionKeysScreen() { + return ( + + +
+
+

Trading bot

+

Valid for 12 more hours

+
+
+

Automation script

+

Read-only access, expires tomorrow

+
+
+
+ + + Add session key + +
+ ); +} + +function NotFoundScreen() { + const { authState } = useExtensionAuth(); + const fallbackPath = !authState.hasOnboarded + ? '/welcome' + : authState.isUnlocked + ? '/home' + : '/unlock'; + + return ( + + + Go back to safety + + + ); +} + +export function ExtensionRouterContent() { + return ( + + + + } path="/" /> + + + + } + path="/welcome" + /> + + + + } + path="/create-account" + /> + + + + } + path="/unlock" + /> + }> + }> + } path="/home" /> + } path="/send" /> + } path="/receive" /> + } path="/history" /> + } path="/settings" /> + } path="/session-keys" /> + + + } path="*" /> + + + ); +} + +export function ExtensionRouter() { + return ( + + + + + + ); +} + +export function ExtensionRouterTestHarness({ initialEntries }: { initialEntries: string[] }) { + return ( + + + + + + ); +} diff --git a/apps/extension-wallet/src/screens/Settings/SecuritySettings.tsx b/apps/extension-wallet/src/screens/Settings/SecuritySettings.tsx index 942c193..580d7b2 100644 --- a/apps/extension-wallet/src/screens/Settings/SecuritySettings.tsx +++ b/apps/extension-wallet/src/screens/Settings/SecuritySettings.tsx @@ -3,12 +3,7 @@ import { AlertTriangle, Eye, EyeOff, Check, Copy } from 'lucide-react'; import { Button, Input } from '@ancore/ui-kit'; import { ScreenHeader } from './NetworkSettings'; -type SecurityView = - | 'menu' - | 'change-password' - | 'auto-lock' - | 'export-key' - | 'export-mnemonic'; +type SecurityView = 'menu' | 'change-password' | 'auto-lock' | 'export-key' | 'export-mnemonic'; interface SecuritySettingsProps { autoLockTimeout: number; @@ -35,8 +30,14 @@ function ChangePasswordView({ onDone }: { onDone: () => void }) { function handleSubmit(e: React.FormEvent) { e.preventDefault(); setError(''); - if (form.next.length < 8) { setError('Password must be at least 8 characters.'); return; } - if (form.next !== form.confirm) { setError('Passwords do not match.'); return; } + if (form.next.length < 8) { + setError('Password must be at least 8 characters.'); + return; + } + if (form.next !== form.confirm) { + setError('Passwords do not match.'); + return; + } setSuccess(true); } @@ -48,9 +49,13 @@ function ChangePasswordView({ onDone }: { onDone: () => void }) {

Password Updated

-

Your wallet password has been changed successfully.

+

+ Your wallet password has been changed successfully. +

- + ); } @@ -58,22 +63,30 @@ function ChangePasswordView({ onDone }: { onDone: () => void }) { return (
- + setForm((f) => ({ ...f, current: e.target.value }))} + onChange={(event: React.ChangeEvent) => + setForm((formState) => ({ ...formState, current: event.target.value })) + } />
- +
setForm((f) => ({ ...f, next: e.target.value }))} + onChange={(event: React.ChangeEvent) => + setForm((formState) => ({ ...formState, next: event.target.value })) + } className="pr-10" />
- + setForm((f) => ({ ...f, confirm: e.target.value }))} + onChange={(event: React.ChangeEvent) => + setForm((formState) => ({ ...formState, confirm: event.target.value })) + } />
{error && ( @@ -100,14 +117,24 @@ function ChangePasswordView({ onDone }: { onDone: () => void }) {

{error}

)} - +
); } // ── Auto-lock ──────────────────────────────────────────────────────────────── -function AutoLockView({ value, onChange, onDone }: { value: number; onChange: (v: number) => void; onDone: () => void }) { +function AutoLockView({ + value, + onChange, + onDone, +}: { + value: number; + onChange: (v: number) => void; + onDone: () => void; +}) { return (

@@ -118,7 +145,10 @@ function AutoLockView({ value, onChange, onDone }: { value: number; onChange: (v

- + ); } @@ -206,20 +252,24 @@ function ExportWarningView({
- + setPassword(e.target.value)} + onChange={(event: React.ChangeEvent) => setPassword(event.target.value)} />
- {error && ( -

{error}

- )} + {error &&

{error}

}
- - + +
); @@ -227,7 +277,11 @@ function ExportWarningView({ // ── SecuritySettings root ──────────────────────────────────────────────────── -export function SecuritySettings({ autoLockTimeout, onAutoLockChange, onBack }: SecuritySettingsProps) { +export function SecuritySettings({ + autoLockTimeout, + onAutoLockChange, + onBack, +}: SecuritySettingsProps) { const [view, setView] = React.useState('menu'); const titles: Record = { @@ -247,15 +301,14 @@ export function SecuritySettings({ autoLockTimeout, onAutoLockChange, onBack }:
- {view === 'menu' && ( - - )} + {view === 'menu' && } {view === 'change-password' && setView('menu')} />} {view === 'auto-lock' && ( - setView('menu')} /> + setView('menu')} + /> )} {view === 'export-key' && (
- onNavigate('change-password')} /> - onNavigate('auto-lock')} /> + onNavigate('change-password')} + /> + onNavigate('auto-lock')} + />
-

Danger Zone

+

+ Danger Zone +

- onNavigate('export-key')} danger /> - onNavigate('export-mnemonic')} danger /> + onNavigate('export-key')} + danger + /> + onNavigate('export-mnemonic')} + danger + />
); } -function MenuItem({ label, description, value, onClick, danger = false }: { - label: string; description?: string; value?: string; onClick: () => void; danger?: boolean; +function MenuItem({ + label, + description, + value, + onClick, + danger = false, +}: { + label: string; + description?: string; + value?: string; + onClick: () => void; + danger?: boolean; }) { return ( ); } diff --git a/apps/extension-wallet/src/screens/__tests__/TransactionDetail.test.tsx b/apps/extension-wallet/src/screens/__tests__/TransactionDetail.test.tsx index 2313eb4..23c83ac 100644 --- a/apps/extension-wallet/src/screens/__tests__/TransactionDetail.test.tsx +++ b/apps/extension-wallet/src/screens/__tests__/TransactionDetail.test.tsx @@ -1,6 +1,6 @@ -import * as React from 'react'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { describe, expect, it, vi } from 'vitest'; import { TransactionDetail } from '@/screens/TransactionDetail'; import { getTransactionExplorerLink } from '@/utils/explorer-links'; diff --git a/apps/extension-wallet/tailwind.config.js b/apps/extension-wallet/tailwind.config.js index f910b81..f2ff762 100644 --- a/apps/extension-wallet/tailwind.config.js +++ b/apps/extension-wallet/tailwind.config.js @@ -1,11 +1,9 @@ +import tailwindcssAnimate from 'tailwindcss-animate'; + /** @type {import('tailwindcss').Config} */ -module.exports = { +const config = { darkMode: ['class'], - content: [ - './index.html', - './src/**/*.{ts,tsx}', - '../../packages/ui-kit/src/**/*.{ts,tsx}', - ], + content: ['./index.html', './src/**/*.{ts,tsx}', '../../packages/ui-kit/src/**/*.{ts,tsx}'], theme: { extend: { colors: { @@ -46,5 +44,7 @@ module.exports = { }, }, }, - plugins: [require('tailwindcss-animate')], + plugins: [tailwindcssAnimate], }; + +export default config; diff --git a/apps/extension-wallet/tsconfig.json b/apps/extension-wallet/tsconfig.json index 312eced..436faeb 100644 --- a/apps/extension-wallet/tsconfig.json +++ b/apps/extension-wallet/tsconfig.json @@ -3,10 +3,11 @@ "compilerOptions": { "lib": ["ES2022", "DOM", "DOM.Iterable"], "jsx": "react-jsx", + "noEmit": true, "baseUrl": ".", "paths": { "@/*": ["./src/*"] } }, "include": ["src", "vitest.config.ts"] -} \ No newline at end of file +} diff --git a/apps/extension-wallet/vite.config.ts b/apps/extension-wallet/vite.config.ts index 929478d..c514305 100644 --- a/apps/extension-wallet/vite.config.ts +++ b/apps/extension-wallet/vite.config.ts @@ -1,10 +1,13 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const rootDir = fileURLToPath(new URL('.', import.meta.url)); export default defineConfig({ plugins: [react()], resolve: { - alias: { '@': resolve(__dirname, 'src') }, + alias: { '@': resolve(rootDir, 'src') }, }, }); diff --git a/apps/extension-wallet/vitest.config.ts b/apps/extension-wallet/vitest.config.ts index 24c8ea3..4106231 100644 --- a/apps/extension-wallet/vitest.config.ts +++ b/apps/extension-wallet/vitest.config.ts @@ -1,6 +1,9 @@ import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; import path from 'path'; +import { fileURLToPath } from 'url'; + +const rootDir = fileURLToPath(new URL('.', import.meta.url)); export default defineConfig({ plugins: [react()], @@ -12,7 +15,7 @@ export default defineConfig({ }, resolve: { alias: { - '@': path.resolve(__dirname, './src'), + '@': path.resolve(rootDir, './src'), }, }, -}); \ No newline at end of file +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e6dca9..ee1333a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,9 +43,6 @@ importers: '@ancore/ui-kit': specifier: workspace:* version: link:../../packages/ui-kit - '@ancore/ui-kit': - specifier: workspace:* - version: link:../../packages/ui-kit date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -61,6 +58,9 @@ importers: react-error-boundary: specifier: ^6.1.1 version: 6.1.1(react@18.3.1) + react-router-dom: + specifier: ^6.20.0 + version: 6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: '@eslint/js': specifier: ^9.0.0 @@ -122,15 +122,6 @@ importers: vite: specifier: ^5.0.0 version: 5.4.21(@types/node@25.3.1) - '@vitejs/plugin-react': - specifier: ^4.2.0 - version: 4.7.0(vite@5.4.21(@types/node@25.3.1)) - jsdom: - specifier: ^23.0.0 - version: 23.2.0 - typescript: - specifier: ^5.6.0 - version: 5.9.3 vitest: specifier: ^1.0.0 version: 1.6.1(@types/node@25.3.1)(jsdom@23.2.0) @@ -2216,6 +2207,10 @@ packages: '@radix-ui/rect@1.0.1': resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} + '@remix-run/router@1.23.2': + resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==} + engines: {node: '>=14.0.0'} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -5877,6 +5872,19 @@ packages: '@types/react': optional: true + react-router-dom@6.30.3: + resolution: {integrity: sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.30.3: + resolution: {integrity: sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -9046,6 +9054,8 @@ snapshots: dependencies: '@babel/runtime': 7.28.6 + '@remix-run/router@1.23.2': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/pluginutils@5.3.0(rollup@4.59.0)': @@ -13976,6 +13986,18 @@ snapshots: optionalDependencies: '@types/react': 18.3.28 + react-router-dom@6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.30.3(react@18.3.1) + + react-router@6.30.3(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.2 + react: 18.3.1 + react-style-singleton@2.2.3(@types/react@18.3.28)(react@18.3.1): dependencies: get-nonce: 1.0.1 From ccfd1d8aa0b7b61370432def441873e0422136b2 Mon Sep 17 00:00:00 2001 From: David-patrick-chuks Date: Tue, 24 Mar 2026 07:47:59 +0100 Subject: [PATCH 2/5] fix(ci): resolve router branch checks --- .../src/components/SettingsGroup.tsx | 8 +- .../src/components/TransactionStatus.tsx | 21 +- .../src/errors/ErrorBoundary.tsx | 59 +++-- .../src/errors/ErrorScreen.tsx | 52 ++--- .../src/errors/error-messages.ts | 33 +-- apps/extension-wallet/src/errors/index.ts | 30 ++- .../src/screens/Settings/AboutScreen.tsx | 22 +- .../src/screens/Settings/NetworkSettings.tsx | 17 +- .../src/screens/Settings/SettingsScreen.tsx | 21 +- .../Settings/__tests__/settings.test.tsx | 38 ++-- .../src/screens/TransactionDetail.tsx | 46 +--- contracts/README.md | 31 ++- .../test/test_add_session_key.1.json | 2 +- .../test_add_session_key_emits_event.1.json | 2 +- .../test/test_double_initialize.1.json | 7 +- .../test/test_execute_emits_event.1.json | 2 +- .../test_execute_rejects_invalid_nonce.1.json | 7 +- ...ute_validates_nonce_then_increments.1.json | 2 +- .../test/test_initialize.1.json | 8 +- .../test/test_initialize_emits_event.1.json | 6 +- .../test/test_refresh_session_key_ttl.1.json | 2 +- ...test_revoke_session_key_emits_event.1.json | 2 +- docs/PULL_REQUEST_WORKFLOW.md | 6 +- .../account-abstraction/eslint.config.cjs | 17 ++ packages/account-abstraction/package.json | 10 +- .../src/__tests__/account-contract.test.ts | 9 +- .../src/account-contract.ts | 19 +- packages/account-abstraction/src/errors.ts | 4 +- packages/account-abstraction/src/index.ts | 5 +- packages/account-abstraction/src/xdr-utils.ts | 32 +-- packages/core-sdk/README.md | 76 ++++--- packages/core-sdk/eslint.config.cjs | 23 ++ .../core-sdk/src/__tests__/builder.test.ts | 206 +++++------------- .../src/__tests__/integration.test.ts | 38 +--- .../src/account-transaction-builder.ts | 46 +--- packages/core-sdk/src/contract-params.ts | 16 +- packages/core-sdk/src/errors.ts | 2 +- .../src/storage/__tests__/manager.test.ts | 27 +-- .../src/storage/secure-storage-manager.ts | 31 +-- packages/core-sdk/src/storage/types.ts | 8 +- packages/core-sdk/tsconfig.json | 3 +- .../src/__tests__/password-strengh.test.ts | 1 - .../src/__tests__/verify-signature.test.ts | 18 +- packages/crypto/src/index.ts | 2 +- packages/crypto/src/password.ts | 37 ++-- packages/stellar/package.json | 2 +- packages/stellar/src/errors.ts | 5 +- packages/stellar/src/retry.ts | 8 +- packages/types/package.json | 2 +- .../src/__tests__/user-operation.test.ts | 10 +- packages/types/src/index.ts | 1 - packages/ui-kit/DESIGN_TOKENS.md | 90 ++++++-- packages/ui-kit/README.md | 37 ++-- packages/ui-kit/eslint.config.cjs | 15 ++ .../components/address-display.stories.tsx | 10 +- .../src/components/address-display.test.tsx | 4 +- .../ui-kit/src/components/address-display.tsx | 14 +- .../src/components/amount-input.stories.tsx | 32 +-- .../ui-kit/src/components/amount-input.tsx | 30 +-- packages/ui-kit/src/components/ui/badge.tsx | 10 +- packages/ui-kit/src/components/ui/button.tsx | 18 +- .../ui-kit/src/components/ui/card.stories.tsx | 13 +- packages/ui-kit/src/components/ui/card.tsx | 91 +++----- .../ui-kit/src/components/ui/input.test.tsx | 2 +- packages/ui-kit/src/components/ui/input.tsx | 3 +- .../src/components/ui/separator.stories.tsx | 4 +- .../ui-kit/src/components/ui/separator.tsx | 31 ++- packages/ui-kit/tailwind.config.js | 5 +- packages/ui-kit/tsconfig.json | 4 +- packages/ui-kit/tsup.config.ts | 1 - 70 files changed, 618 insertions(+), 878 deletions(-) diff --git a/apps/extension-wallet/src/components/SettingsGroup.tsx b/apps/extension-wallet/src/components/SettingsGroup.tsx index 9c29727..0f0826c 100644 --- a/apps/extension-wallet/src/components/SettingsGroup.tsx +++ b/apps/extension-wallet/src/components/SettingsGroup.tsx @@ -51,7 +51,9 @@ export function SettingItem({ onClick={onClick} > {icon && ( - + {icon} )} @@ -63,9 +65,7 @@ export function SettingItem({ {rightSlot ?? ( <> - {value !== undefined && ( - {value} - )} + {value !== undefined && {value}} {onClick && } )} diff --git a/apps/extension-wallet/src/components/TransactionStatus.tsx b/apps/extension-wallet/src/components/TransactionStatus.tsx index 9441c61..a794c52 100644 --- a/apps/extension-wallet/src/components/TransactionStatus.tsx +++ b/apps/extension-wallet/src/components/TransactionStatus.tsx @@ -2,11 +2,7 @@ import * as React from 'react'; import { Badge, cn } from '@ancore/ui-kit'; import { AlertCircle, CheckCircle2, Clock3, XCircle } from 'lucide-react'; -export type TransactionStatusKind = - | 'confirmed' - | 'pending' - | 'failed' - | 'cancelled'; +export type TransactionStatusKind = 'confirmed' | 'pending' | 'failed' | 'cancelled'; const STATUS_STYLES: Record< TransactionStatusKind, @@ -38,22 +34,21 @@ const STATUS_STYLES: Record< }, }; -export interface TransactionStatusProps - extends React.HTMLAttributes { +export interface TransactionStatusProps extends React.HTMLAttributes { status: TransactionStatusKind; } -export function TransactionStatus({ - status, - className, - ...props -}: TransactionStatusProps) { +export function TransactionStatus({ status, className, ...props }: TransactionStatusProps) { const { label, icon, className: statusClassName } = STATUS_STYLES[status]; return ( {icon} diff --git a/apps/extension-wallet/src/errors/ErrorBoundary.tsx b/apps/extension-wallet/src/errors/ErrorBoundary.tsx index 729c717..3747d1b 100644 --- a/apps/extension-wallet/src/errors/ErrorBoundary.tsx +++ b/apps/extension-wallet/src/errors/ErrorBoundary.tsx @@ -1,12 +1,12 @@ /** * ErrorBoundary Component - * + * * A reusable React ErrorBoundary that wraps the app and catches rendering errors. * Uses react-error-boundary library for robust error handling. */ import { ErrorBoundary as ReactErrorBoundary, useErrorBoundary } from 'react-error-boundary'; -import type { ErrorInfo, ReactNode } from 'react'; +import type { ComponentType, ErrorInfo, ReactElement, ReactNode } from 'react'; import { ErrorScreen } from './ErrorScreen'; import { ErrorCategory, handleError } from './error-handler'; @@ -23,7 +23,7 @@ export interface ErrorBoundaryProps { /** * ErrorBoundary component that catches React rendering errors - * + * * @example * ```tsx * @@ -31,11 +31,7 @@ export interface ErrorBoundaryProps { * * ``` */ -export function ErrorBoundary({ - children, - fallback, - onError, -}: ErrorBoundaryProps): JSX.Element { +export function ErrorBoundary({ children, fallback, onError }: ErrorBoundaryProps): ReactElement { // If there's a custom fallback component provided, use it if (fallback) { return ( @@ -87,20 +83,21 @@ interface ErrorFallbackProps { /** * Internal ErrorFallback component that displays the error */ -function ErrorFallback({ - error, - resetErrorBoundary, +function ErrorFallback({ + error, + resetErrorBoundary, onError, customFallback, -}: ErrorFallbackProps): JSX.Element { +}: ErrorFallbackProps): ReactElement { // Convert unknown error to Error object - const err = error && typeof error === 'object' && 'message' in error - ? error as globalThis.Error - : new Error(String(error)); + const err = + error && typeof error === 'object' && 'message' in error + ? (error as globalThis.Error) + : new Error(String(error)); // Handle the error through our error handler const errorInfo = handleError(err, 'React ErrorBoundary'); - + // Call onError callback if provided if (onError) { onError(err, { componentStack: err.stack || '' } as ErrorInfo); @@ -125,11 +122,11 @@ function ErrorFallback({ /** * Hook for handling async errors in components * Use this to catch errors in event handlers and async functions - * + * * @example * ```tsx * const { reset, dispatch } = useErrorHandler(); - * + * * const handleClick = async () => { * try { * await someAsyncOperation(); @@ -147,9 +144,10 @@ export function useErrorHandler() { return { /** Dispatch an error to the boundary */ dispatch: (error: unknown) => { - const err = error && typeof error === 'object' && 'message' in error - ? error as globalThis.Error - : new Error(String(error)); + const err = + error && typeof error === 'object' && 'message' in error + ? (error as globalThis.Error) + : new Error(String(error)); showBoundary(err); }, /** Clear the error boundary state */ @@ -161,17 +159,17 @@ export function useErrorHandler() { /** * Higher-order component wrapper for error handling async operations - * + * * @example * ```tsx * const WrappedComponent = withErrorBoundary(MyComponent); * ``` */ export function withErrorBoundary

( - Component: React.ComponentType

, + Component: ComponentType

, errorBoundaryProps?: Partial -): React.ComponentType

{ - return function WrappedComponent(props: P): JSX.Element { +): ComponentType

{ + return function WrappedComponent(props: P): ReactElement { return ( @@ -194,18 +192,11 @@ interface ErrorBoundaryResetProps { * A button component that resets the ErrorBoundary when clicked * Useful for triggering error recovery */ -export function ErrorBoundaryReset({ - children, - className -}: ErrorBoundaryResetProps): JSX.Element { +export function ErrorBoundaryReset({ children, className }: ErrorBoundaryResetProps): ReactElement { const { reset } = useErrorHandler(); return ( - ); diff --git a/apps/extension-wallet/src/errors/ErrorScreen.tsx b/apps/extension-wallet/src/errors/ErrorScreen.tsx index ff32d2c..4b6847c 100644 --- a/apps/extension-wallet/src/errors/ErrorScreen.tsx +++ b/apps/extension-wallet/src/errors/ErrorScreen.tsx @@ -1,13 +1,13 @@ /** * ErrorScreen Component - * + * * Displays user-friendly error messages with recovery options * (retry or reset). Used by ErrorBoundary as the fallback UI. */ import { Button } from '@ancore/ui-kit'; import { AlertTriangle, RotateCcw, RefreshCw, Info } from 'lucide-react'; -import { ReactNode } from 'react'; +import type { ReactElement, ReactNode } from 'react'; import { getErrorMessage, ErrorCategory } from './error-messages'; import { ErrorInfo, handleError } from './error-handler'; @@ -34,7 +34,7 @@ export interface ErrorScreenProps { /** * ErrorScreen displays a user-friendly error page with recovery options - * + * * @example * ```tsx * {/* Description */} -

- {displayDescription} -

+

{displayDescription}

{/* Recovery hint */} {userMessage.recoveryHint && ( @@ -144,20 +142,14 @@ export function ErrorScreen({

Error: {error?.message || 'Unknown error'}

- {error?.stack && ( -

{error.stack}

- )} + {error?.stack &&

{error.stack}

} )} {/* Additional children content */} - {children && ( -
- {children} -
- )} + {children &&
{children}
} @@ -181,7 +173,7 @@ interface ErrorCardProps { /** * ErrorCard - A compact inline error display component * Use this for inline errors within forms or smaller contexts - * + * * @example * ```tsx *
-

- {message} -

+

{message}

{onRetry && ( - @@ -80,7 +89,9 @@ export function NetworkSettings({ value, onChange, onBack }: NetworkSettingsProp : 'border-border bg-card hover:border-primary/40 hover:bg-accent/30' }`} > -
+
diff --git a/apps/extension-wallet/src/screens/Settings/SettingsScreen.tsx b/apps/extension-wallet/src/screens/Settings/SettingsScreen.tsx index 3fb5cd6..4a7d89c 100644 --- a/apps/extension-wallet/src/screens/Settings/SettingsScreen.tsx +++ b/apps/extension-wallet/src/screens/Settings/SettingsScreen.tsx @@ -1,12 +1,5 @@ import * as React from 'react'; -import { - Globe, - Lock, - Timer, - Key, - FileText, - Info, -} from 'lucide-react'; +import { Globe, Lock, Timer, Key, FileText, Info } from 'lucide-react'; import { SettingsGroup, SettingItem } from '../../components/SettingsGroup'; import { NetworkSettings } from './NetworkSettings'; import { SecuritySettings } from './SecuritySettings'; @@ -48,13 +41,9 @@ export function SettingsScreen() { return setView('root')} />; } - const networkLabel = - settings.network.charAt(0).toUpperCase() + settings.network.slice(1); + const networkLabel = settings.network.charAt(0).toUpperCase() + settings.network.slice(1); - const timeoutLabel = - settings.autoLockTimeout === 0 - ? 'Never' - : `${settings.autoLockTimeout} min`; + const timeoutLabel = settings.autoLockTimeout === 0 ? 'Never' : `${settings.autoLockTimeout} min`; return (
@@ -72,7 +61,9 @@ export function SettingsScreen() {

My Ancore Wallet

GBXXX...YYYY

- + {networkLabel}
diff --git a/apps/extension-wallet/src/screens/Settings/__tests__/settings.test.tsx b/apps/extension-wallet/src/screens/Settings/__tests__/settings.test.tsx index 4da80f3..6150f11 100644 --- a/apps/extension-wallet/src/screens/Settings/__tests__/settings.test.tsx +++ b/apps/extension-wallet/src/screens/Settings/__tests__/settings.test.tsx @@ -30,7 +30,10 @@ describe('useSettings', () => { }); it('rehydrates settings from localStorage', () => { - localStorage.setItem('ancore_settings', JSON.stringify({ network: 'mainnet', autoLockTimeout: 15 })); + localStorage.setItem( + 'ancore_settings', + JSON.stringify({ network: 'mainnet', autoLockTimeout: 15 }) + ); const { result } = renderHook(() => useSettings()); expect(result.current.settings.network).toBe('mainnet'); expect(result.current.settings.autoLockTimeout).toBe(15); @@ -78,9 +81,7 @@ describe('SettingItem', () => { describe('NetworkSettings', () => { it('shows current network as active', () => { - render( - - ); + render(); expect(screen.getByText('Testnet')).toBeInTheDocument(); // active network has a check icon inside a primary-colored circle expect(screen.getByText('Testnet').closest('button')).toHaveClass('border-primary'); @@ -89,18 +90,14 @@ describe('NetworkSettings', () => { it('switches to testnet without confirmation', async () => { const onChange = vi.fn(); const onBack = vi.fn(); - render( - - ); + render(); await userEvent.click(screen.getByText('Testnet')); expect(onChange).toHaveBeenCalledWith('testnet'); expect(onBack).toHaveBeenCalled(); }); it('shows mainnet warning before switching', async () => { - render( - - ); + render(); await userEvent.click(screen.getByText('Mainnet')); expect(screen.getByText(/switch to mainnet\?/i)).toBeInTheDocument(); }); @@ -108,9 +105,7 @@ describe('NetworkSettings', () => { it('confirms mainnet switch', async () => { const onChange = vi.fn(); const onBack = vi.fn(); - render( - - ); + render(); await userEvent.click(screen.getByText('Mainnet')); await userEvent.click(screen.getByRole('button', { name: /switch to mainnet/i })); expect(onChange).toHaveBeenCalledWith('mainnet'); @@ -119,9 +114,7 @@ describe('NetworkSettings', () => { it('cancels mainnet switch', async () => { const onChange = vi.fn(); - render( - - ); + render(); await userEvent.click(screen.getByText('Mainnet')); await userEvent.click(screen.getByRole('button', { name: /cancel/i })); expect(onChange).not.toHaveBeenCalled(); @@ -130,9 +123,7 @@ describe('NetworkSettings', () => { it('calls onBack when back button clicked', async () => { const onBack = vi.fn(); - render( - - ); + render(); await userEvent.click(screen.getByRole('button', { name: /go back/i })); expect(onBack).toHaveBeenCalled(); }); @@ -254,7 +245,9 @@ describe('SettingsScreen', () => { render(); // click the Network row button (the SettingItem, not the section heading) const networkButtons = screen.getAllByRole('button'); - const networkRowBtn = networkButtons.find((b) => b.textContent?.includes('Network') && b.textContent?.includes('Testnet')); + const networkRowBtn = networkButtons.find( + (b) => b.textContent?.includes('Network') && b.textContent?.includes('Testnet') + ); await userEvent.click(networkRowBtn!); expect(screen.getByText('Testnet')).toBeInTheDocument(); expect(screen.getByText('Mainnet')).toBeInTheDocument(); @@ -273,7 +266,10 @@ describe('SettingsScreen', () => { }); it('shows current network in root view', () => { - localStorage.setItem('ancore_settings', JSON.stringify({ network: 'mainnet', autoLockTimeout: 5 })); + localStorage.setItem( + 'ancore_settings', + JSON.stringify({ network: 'mainnet', autoLockTimeout: 5 }) + ); render(); expect(screen.getAllByText('Mainnet').length).toBeGreaterThanOrEqual(1); }); diff --git a/apps/extension-wallet/src/screens/TransactionDetail.tsx b/apps/extension-wallet/src/screens/TransactionDetail.tsx index a6e6cb9..49150a8 100644 --- a/apps/extension-wallet/src/screens/TransactionDetail.tsx +++ b/apps/extension-wallet/src/screens/TransactionDetail.tsx @@ -1,24 +1,10 @@ import * as React from 'react'; -import { - Button, - Card, - CardContent, - CardHeader, - CardTitle, - Separator, - cn, -} from '@ancore/ui-kit'; +import { Button, Card, CardContent, CardHeader, CardTitle, Separator, cn } from '@ancore/ui-kit'; import { format } from 'date-fns'; import { ArrowLeft, Copy, ExternalLink } from 'lucide-react'; -import { - TransactionStatus, - type TransactionStatusKind, -} from '@/components/TransactionStatus'; -import { - getTransactionExplorerLink, - type StellarNetwork, -} from '@/utils/explorer-links'; +import { TransactionStatus, type TransactionStatusKind } from '@/components/TransactionStatus'; +import { getTransactionExplorerLink, type StellarNetwork } from '@/utils/explorer-links'; export interface TransactionDetailData { id?: string; @@ -76,11 +62,7 @@ function truncateHash(hash: string): string { return `${hash.slice(0, 8)}...${hash.slice(-8)}`; } -export function TransactionDetail({ - transaction, - onBack, - className, -}: TransactionDetailProps) { +export function TransactionDetail({ transaction, onBack, className }: TransactionDetailProps) { const [copied, setCopied] = React.useState(false); const explorerLink = getTransactionExplorerLink( @@ -98,13 +80,7 @@ export function TransactionDetail({
- Details @@ -158,9 +134,7 @@ export function TransactionDetail({
Block
-
- {transaction.blockNumber ?? 'Not available'} -
+
{transaction.blockNumber ?? 'Not available'}
TX Hash
@@ -196,10 +170,4 @@ export function TransactionDetail({ ); } -export { - copyText, - formatDateTime, - getCounterpartyLabel, - getHeadline, - truncateHash, -}; +export { copyText, formatDateTime, getCounterpartyLabel, getHeadline, truncateHash }; diff --git a/contracts/README.md b/contracts/README.md index 8cd8d09..928c295 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -62,7 +62,6 @@ Run tests with output: cargo test -- --nocapture ``` - ### Prerequisites - Rust toolchain (1.74.0+) @@ -92,19 +91,25 @@ make deploy-testnet # Deploy contract to Soroban testnet #### Example Workflow 1. Build and optimize contract: - ```bash - make build - make optimize - ``` + +```bash +make build +make optimize +``` + 2. Setup local sandbox (optional): - ```bash - bash scripts/setup-local.sh - ``` + +```bash +bash scripts/setup-local.sh +``` + 3. Deploy to testnet: - ```bash - make deploy-testnet - ``` - - Contract ID will be written to `contract-deployment.json`. + +```bash +make deploy-testnet +``` + +- Contract ID will be written to `contract-deployment.json`. #### Environment Variables @@ -113,11 +118,13 @@ make deploy-testnet # Deploy contract to Soroban testnet #### Manual Steps (for reference) You can still use Soroban CLI directly for custom deployments: + ```bash soroban contract build soroban contract optimize --wasm target/wasm32-unknown-unknown/release/ancore_account.wasm soroban contract deploy --wasm target/wasm32-unknown-unknown/release/ancore_account.optimized.wasm --source --network testnet ``` + - Ed25519 validation (native Stellar) - Multi-signature - WebAuthn support diff --git a/contracts/account/test_snapshots/test/test_add_session_key.1.json b/contracts/account/test_snapshots/test/test_add_session_key.1.json index 7b56309..324b24f 100644 --- a/contracts/account/test_snapshots/test/test_add_session_key.1.json +++ b/contracts/account/test_snapshots/test/test_add_session_key.1.json @@ -452,4 +452,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/contracts/account/test_snapshots/test/test_add_session_key_emits_event.1.json b/contracts/account/test_snapshots/test/test_add_session_key_emits_event.1.json index 48279e9..c5a6d8c 100644 --- a/contracts/account/test_snapshots/test/test_add_session_key_emits_event.1.json +++ b/contracts/account/test_snapshots/test/test_add_session_key_emits_event.1.json @@ -377,4 +377,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/contracts/account/test_snapshots/test/test_double_initialize.1.json b/contracts/account/test_snapshots/test/test_double_initialize.1.json index d6b97d3..4d7cfb5 100644 --- a/contracts/account/test_snapshots/test/test_double_initialize.1.json +++ b/contracts/account/test_snapshots/test/test_double_initialize.1.json @@ -3,10 +3,7 @@ "address": 2, "nonce": 0 }, - "auth": [ - [], - [] - ], + "auth": [[], []], "ledger": { "protocol_version": 21, "sequence_number": 0, @@ -306,4 +303,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/contracts/account/test_snapshots/test/test_execute_emits_event.1.json b/contracts/account/test_snapshots/test/test_execute_emits_event.1.json index 0c531b6..3cb1b87 100644 --- a/contracts/account/test_snapshots/test/test_execute_emits_event.1.json +++ b/contracts/account/test_snapshots/test/test_execute_emits_event.1.json @@ -318,4 +318,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/contracts/account/test_snapshots/test/test_execute_rejects_invalid_nonce.1.json b/contracts/account/test_snapshots/test/test_execute_rejects_invalid_nonce.1.json index b1ceabb..acdd317 100644 --- a/contracts/account/test_snapshots/test/test_execute_rejects_invalid_nonce.1.json +++ b/contracts/account/test_snapshots/test/test_execute_rejects_invalid_nonce.1.json @@ -3,10 +3,7 @@ "address": 3, "nonce": 0 }, - "auth": [ - [], - [] - ], + "auth": [[], []], "ledger": { "protocol_version": 21, "sequence_number": 0, @@ -339,4 +336,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/contracts/account/test_snapshots/test/test_execute_validates_nonce_then_increments.1.json b/contracts/account/test_snapshots/test/test_execute_validates_nonce_then_increments.1.json index 2091ce3..f7308fc 100644 --- a/contracts/account/test_snapshots/test/test_execute_validates_nonce_then_increments.1.json +++ b/contracts/account/test_snapshots/test/test_execute_validates_nonce_then_increments.1.json @@ -446,4 +446,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/contracts/account/test_snapshots/test/test_initialize.1.json b/contracts/account/test_snapshots/test/test_initialize.1.json index c852514..894651e 100644 --- a/contracts/account/test_snapshots/test/test_initialize.1.json +++ b/contracts/account/test_snapshots/test/test_initialize.1.json @@ -3,11 +3,7 @@ "address": 2, "nonce": 0 }, - "auth": [ - [], - [], - [] - ], + "auth": [[], [], []], "ledger": { "protocol_version": 21, "sequence_number": 0, @@ -261,4 +257,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/contracts/account/test_snapshots/test/test_initialize_emits_event.1.json b/contracts/account/test_snapshots/test/test_initialize_emits_event.1.json index 42e0e2b..87b6b29 100644 --- a/contracts/account/test_snapshots/test/test_initialize_emits_event.1.json +++ b/contracts/account/test_snapshots/test/test_initialize_emits_event.1.json @@ -3,9 +3,7 @@ "address": 2, "nonce": 0 }, - "auth": [ - [] - ], + "auth": [[]], "ledger": { "protocol_version": 21, "sequence_number": 0, @@ -165,4 +163,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/contracts/account/test_snapshots/test/test_refresh_session_key_ttl.1.json b/contracts/account/test_snapshots/test/test_refresh_session_key_ttl.1.json index 5684c4d..86537ce 100644 --- a/contracts/account/test_snapshots/test/test_refresh_session_key_ttl.1.json +++ b/contracts/account/test_snapshots/test/test_refresh_session_key_ttl.1.json @@ -500,4 +500,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/contracts/account/test_snapshots/test/test_revoke_session_key_emits_event.1.json b/contracts/account/test_snapshots/test/test_revoke_session_key_emits_event.1.json index 3dd4123..dff6df8 100644 --- a/contracts/account/test_snapshots/test/test_revoke_session_key_emits_event.1.json +++ b/contracts/account/test_snapshots/test/test_revoke_session_key_emits_event.1.json @@ -426,4 +426,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/docs/PULL_REQUEST_WORKFLOW.md b/docs/PULL_REQUEST_WORKFLOW.md index e50fc5f..af65c49 100644 --- a/docs/PULL_REQUEST_WORKFLOW.md +++ b/docs/PULL_REQUEST_WORKFLOW.md @@ -42,8 +42,8 @@ git branch -d feat/your-feature-name # optional: delete local branch ## Branch naming -- `feat/thing` — new feature -- `fix/thing` — bugfix -- `chore/thing` — config, deps, tooling +- `feat/thing` — new feature +- `fix/thing` — bugfix +- `chore/thing` — config, deps, tooling Example: `feat/contract-execute-nonce`, `fix/account-abstraction-types`. diff --git a/packages/account-abstraction/eslint.config.cjs b/packages/account-abstraction/eslint.config.cjs index 474b560..cdd73e9 100644 --- a/packages/account-abstraction/eslint.config.cjs +++ b/packages/account-abstraction/eslint.config.cjs @@ -12,13 +12,30 @@ module.exports = [ ecmaVersion: 2020, sourceType: 'module', }, + globals: { + Buffer: 'readonly', + TextEncoder: 'readonly', + }, }, plugins: { '@typescript-eslint': tseslint, }, rules: { ...tseslint.configs.recommended.rules, + '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], }, }, + { + files: ['src/__tests__/**/*.ts'], + languageOptions: { + globals: { + describe: 'readonly', + beforeEach: 'readonly', + it: 'readonly', + expect: 'readonly', + jest: 'readonly', + }, + }, + }, ]; diff --git a/packages/account-abstraction/package.json b/packages/account-abstraction/package.json index f5f3a10..b36f11a 100644 --- a/packages/account-abstraction/package.json +++ b/packages/account-abstraction/package.json @@ -6,11 +6,11 @@ "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "scripts": { - "build": "tsup src/index.ts --format cjs,esm --dts --tsconfig tsconfig.json", - "dev": "tsup src/index.ts --format cjs,esm --dts --watch", - "test": "jest", - "lint": "eslint src/", - "clean": "rm -rf dist" + "build": "tsup src/index.ts --format cjs,esm --dts --tsconfig tsconfig.json", + "dev": "tsup src/index.ts --format cjs,esm --dts --watch", + "test": "jest", + "lint": "eslint src/", + "clean": "rm -rf dist" }, "keywords": [ "ancore", diff --git a/packages/account-abstraction/src/__tests__/account-contract.test.ts b/packages/account-abstraction/src/__tests__/account-contract.test.ts index 50a091e..976ce0c 100644 --- a/packages/account-abstraction/src/__tests__/account-contract.test.ts +++ b/packages/account-abstraction/src/__tests__/account-contract.test.ts @@ -50,10 +50,7 @@ describe('AccountContract', () => { it('returns method and to, function, args, expectedNonce', () => { const to = 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC'; const fn = 'transfer'; - const args: xdr.ScVal[] = [ - new Address(OWNER_ADDRESS).toScVal(), - xdr.ScVal.scvU32(100), - ]; + const args: xdr.ScVal[] = [new Address(OWNER_ADDRESS).toScVal(), xdr.ScVal.scvU32(100)]; const inv = contract.execute(to, fn, args, 0); expect(inv.method).toBe('execute'); expect(inv.args).toHaveLength(4); @@ -215,9 +212,7 @@ describe('XDR encoding helpers', () => { }); it('throws for wrong byte length', () => { - expect(() => - publicKeyToBytes32ScVal(new Uint8Array(16)) - ).toThrow(TypeError); + expect(() => publicKeyToBytes32ScVal(new Uint8Array(16))).toThrow(TypeError); }); }); diff --git a/packages/account-abstraction/src/account-contract.ts b/packages/account-abstraction/src/account-contract.ts index aef5788..242e9f1 100644 --- a/packages/account-abstraction/src/account-contract.ts +++ b/packages/account-abstraction/src/account-contract.ts @@ -4,12 +4,7 @@ */ import type { SessionKey } from '@ancore/types'; -import { - Account, - Contract, - TransactionBuilder, - xdr, -} from '@stellar/stellar-sdk'; +import { Account, Contract, TransactionBuilder, xdr } from '@stellar/stellar-sdk'; import { mapContractError } from './errors'; import { addressToScVal, @@ -67,12 +62,7 @@ export class AccountContract { * Build invocation for execute(to, function, args, expected_nonce). * Caller must pass the current nonce (e.g. from getNonce()) for replay protection. */ - execute( - to: string, - fn: string, - args: xdr.ScVal[], - expectedNonce: number - ): InvocationArgs { + execute(to: string, fn: string, args: xdr.ScVal[], expectedNonce: number): InvocationArgs { return { method: 'execute', args: [ @@ -197,10 +187,7 @@ export class AccountContract { const { server, sourceAccount } = options; const accountResponse = await server.getAccount(sourceAccount); - const account = new Account( - accountResponse.id, - accountResponse.sequence ?? '0' - ); + const account = new Account(accountResponse.id, accountResponse.sequence ?? '0'); const txBuilder = new TransactionBuilder(account, { fee: '100', diff --git a/packages/account-abstraction/src/errors.ts b/packages/account-abstraction/src/errors.ts index 08548ab..1130ff6 100644 --- a/packages/account-abstraction/src/errors.ts +++ b/packages/account-abstraction/src/errors.ts @@ -70,9 +70,7 @@ export class UnauthorizedError extends AccountContractError { */ export class SessionKeyNotFoundError extends AccountContractError { constructor(publicKey?: string) { - const msg = publicKey - ? `Session key not found: ${publicKey}` - : 'Session key not found'; + const msg = publicKey ? `Session key not found: ${publicKey}` : 'Session key not found'; super(msg, 'SESSION_KEY_NOT_FOUND'); this.name = 'SessionKeyNotFoundError'; Object.setPrototypeOf(this, SessionKeyNotFoundError.prototype); diff --git a/packages/account-abstraction/src/index.ts b/packages/account-abstraction/src/index.ts index 808148b..3bd420e 100644 --- a/packages/account-abstraction/src/index.ts +++ b/packages/account-abstraction/src/index.ts @@ -7,10 +7,7 @@ export const AA_VERSION = '0.1.0'; export { AccountContract } from './account-contract'; -export type { - AccountContractReadOptions, - InvocationArgs, -} from './account-contract'; +export type { AccountContractReadOptions, InvocationArgs } from './account-contract'; export { AccountContractError, diff --git a/packages/account-abstraction/src/xdr-utils.ts b/packages/account-abstraction/src/xdr-utils.ts index 0a1facc..a4e2e42 100644 --- a/packages/account-abstraction/src/xdr-utils.ts +++ b/packages/account-abstraction/src/xdr-utils.ts @@ -4,13 +4,7 @@ */ import type { SessionKey } from '@ancore/types'; -import { - Address, - nativeToScVal, - scValToNative, - StrKey, - xdr, -} from '@stellar/stellar-sdk'; +import { Address, nativeToScVal, scValToNative, StrKey, xdr } from '@stellar/stellar-sdk'; const BYTES_N_32_LENGTH = 32; @@ -25,9 +19,7 @@ export function addressToScVal(address: string): xdr.ScVal { * Encode a 32-byte session public key to ScVal (BytesN<32>). * Accepts Stellar public key string (G...) or raw 32-byte Uint8Array. */ -export function publicKeyToBytes32ScVal( - publicKey: string | Uint8Array -): xdr.ScVal { +export function publicKeyToBytes32ScVal(publicKey: string | Uint8Array): xdr.ScVal { let bytes: Uint8Array; if (typeof publicKey === 'string') { if (!StrKey.isValidEd25519PublicKey(publicKey)) { @@ -41,9 +33,7 @@ export function publicKeyToBytes32ScVal( bytes = publicKey; } if (bytes.length !== BYTES_N_32_LENGTH) { - throw new TypeError( - `Session key must be ${BYTES_N_32_LENGTH} bytes, got ${bytes.length}` - ); + throw new TypeError(`Session key must be ${BYTES_N_32_LENGTH} bytes, got ${bytes.length}`); } return xdr.ScVal.scvBytes(Buffer.from(bytes)); } @@ -120,16 +110,12 @@ export function scValToSessionKey(scVal: xdr.ScVal): SessionKey { const permissions = map.permissions; if (publicKey == null || expiresAt == null || permissions == null) { - throw new TypeError( - 'SessionKey map must have public_key, expires_at, permissions' - ); + throw new TypeError('SessionKey map must have public_key, expires_at, permissions'); } let publicKeyStr: string; if (publicKey instanceof Uint8Array || Buffer.isBuffer(publicKey)) { - publicKeyStr = StrKey.encodeEd25519PublicKey( - Buffer.from(publicKey as Uint8Array) - ); + publicKeyStr = StrKey.encodeEd25519PublicKey(Buffer.from(publicKey as Uint8Array)); } else { throw new TypeError('SessionKey.public_key must be 32 bytes'); } @@ -142,9 +128,7 @@ export function scValToSessionKey(scVal: xdr.ScVal): SessionKey { : Number(expiresAt); const permsArray = Array.isArray(permissions) - ? (permissions as number[]).map((p) => - typeof p === 'bigint' ? Number(p) : (p as number) - ) + ? (permissions as number[]).map((p) => (typeof p === 'bigint' ? Number(p) : (p as number))) : []; return { @@ -158,9 +142,7 @@ export function scValToSessionKey(scVal: xdr.ScVal): SessionKey { * Decode optional SessionKey (Option) from contract get_session_key. * Returns null if the option is None (void or missing). */ -export function scValToOptionalSessionKey( - scVal: xdr.ScVal -): SessionKey | null { +export function scValToOptionalSessionKey(scVal: xdr.ScVal): SessionKey | null { const native = scValToNative(scVal); if (native === null || native === undefined) return null; try { diff --git a/packages/core-sdk/README.md b/packages/core-sdk/README.md index 8e5c8cd..7336a76 100644 --- a/packages/core-sdk/README.md +++ b/packages/core-sdk/README.md @@ -33,14 +33,14 @@ const tx = await new AccountTransactionBuilder(sourceAccount, { ### What it does -| Feature | How | -|---|---| -| **Wraps Stellar SDK's TransactionBuilder** | Constructor creates an internal `TransactionBuilder` and delegates all operations to it | -| **Convenience methods** | `.addSessionKey()`, `.revokeSessionKey()`, `.execute()` encode contract params and call `Contract.call()` | -| **Automatic simulation** | `.build()` runs Soroban simulation and assembles the transaction with resource footprints & fees | -| **Passthrough** | `.addOperation()` lets you add **any** standard Stellar operation alongside contract calls | -| **Fluent API** | Every method returns `this` so you can chain just like the native builder | -| **Actionable errors** | Custom error classes (`SimulationFailedError`, `BuilderValidationError`, etc.) with human-readable messages | +| Feature | How | +| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | +| **Wraps Stellar SDK's TransactionBuilder** | Constructor creates an internal `TransactionBuilder` and delegates all operations to it | +| **Convenience methods** | `.addSessionKey()`, `.revokeSessionKey()`, `.execute()` encode contract params and call `Contract.call()` | +| **Automatic simulation** | `.build()` runs Soroban simulation and assembles the transaction with resource footprints & fees | +| **Passthrough** | `.addOperation()` lets you add **any** standard Stellar operation alongside contract calls | +| **Fluent API** | Every method returns `this` so you can chain just like the native builder | +| **Actionable errors** | Custom error classes (`SimulationFailedError`, `BuilderValidationError`, etc.) with human-readable messages | ### What it does NOT do @@ -76,15 +76,15 @@ const builder = new AccountTransactionBuilder( server, accountContractId: 'CABC...', // your deployed Ancore account contract networkPassphrase: Networks.TESTNET, - }, + } ); // 3. Add a session key const tx = await builder .addSessionKey( sessionKeypair.publicKey(), - [0, 1], // SEND_PAYMENT, MANAGE_DATA - Math.floor(Date.now() / 1000) + 3600, // 1 hour + [0, 1], // SEND_PAYMENT, MANAGE_DATA + Math.floor(Date.now() / 1000) + 3600 // 1 hour ) .addMemo(Memo.text('Add session key')) .build(); @@ -130,42 +130,48 @@ const tx = await new AccountTransactionBuilder(sourceAccount, opts) new AccountTransactionBuilder(sourceAccount: Account, options: AccountTransactionBuilderOptions) ``` -| Option | Type | Description | -|---|---|---| -| `server` | `SorobanRpc.Server` | Soroban RPC server instance | -| `accountContractId` | `string` | Contract ID (C…) of deployed Ancore account contract | -| `networkPassphrase` | `string` | Network passphrase (e.g. `Networks.TESTNET`) | -| `fee` | `string` | Base fee in stroops (default: `BASE_FEE`) | -| `timeoutSeconds` | `number` | Transaction timeout (default: 300) | +| Option | Type | Description | +| ------------------- | ------------------- | ---------------------------------------------------- | +| `server` | `SorobanRpc.Server` | Soroban RPC server instance | +| `accountContractId` | `string` | Contract ID (C…) of deployed Ancore account contract | +| `networkPassphrase` | `string` | Network passphrase (e.g. `Networks.TESTNET`) | +| `fee` | `string` | Base fee in stroops (default: `BASE_FEE`) | +| `timeoutSeconds` | `number` | Transaction timeout (default: 300) | #### Methods -| Method | Returns | Description | -|---|---|---| -| `.addSessionKey(publicKey, permissions, expiresAt)` | `this` | Invoke `add_session_key` on the contract | -| `.revokeSessionKey(publicKey)` | `this` | Invoke `revoke_session_key` on the contract | -| `.execute(sessionKeyPublicKey, operations)` | `this` | Invoke `execute` with session key authorization | -| `.addOperation(operation)` | `this` | Passthrough to Stellar SDK's `addOperation` | -| `.addMemo(memo)` | `this` | Passthrough to Stellar SDK's `addMemo` | -| `.setTimeout(seconds)` | `this` | Override transaction timeout | -| `.simulate()` | `Promise` | Run Soroban simulation | -| `.build()` | `Promise` | Simulate + assemble final transaction | +| Method | Returns | Description | +| --------------------------------------------------- | -------------------------------------- | ----------------------------------------------- | +| `.addSessionKey(publicKey, permissions, expiresAt)` | `this` | Invoke `add_session_key` on the contract | +| `.revokeSessionKey(publicKey)` | `this` | Invoke `revoke_session_key` on the contract | +| `.execute(sessionKeyPublicKey, operations)` | `this` | Invoke `execute` with session key authorization | +| `.addOperation(operation)` | `this` | Passthrough to Stellar SDK's `addOperation` | +| `.addMemo(memo)` | `this` | Passthrough to Stellar SDK's `addMemo` | +| `.setTimeout(seconds)` | `this` | Override transaction timeout | +| `.simulate()` | `Promise` | Run Soroban simulation | +| `.build()` | `Promise` | Simulate + assemble final transaction | ### Error Types -| Error | Code | When | -|---|---|---| -| `BuilderValidationError` | `BUILDER_VALIDATION` | Invalid params or no operations | -| `SimulationFailedError` | `SIMULATION_FAILED` | Soroban simulation returned an error | -| `SimulationExpiredError` | `SIMULATION_EXPIRED` | Simulation result requires ledger restoration | -| `TransactionSubmissionError` | `SUBMISSION_FAILED` | Network submission failed | +| Error | Code | When | +| ---------------------------- | -------------------- | --------------------------------------------- | +| `BuilderValidationError` | `BUILDER_VALIDATION` | Invalid params or no operations | +| `SimulationFailedError` | `SIMULATION_FAILED` | Soroban simulation returned an error | +| `SimulationExpiredError` | `SIMULATION_EXPIRED` | Simulation result requires ledger restoration | +| `TransactionSubmissionError` | `SUBMISSION_FAILED` | Network submission failed | ### Contract Parameter Helpers Exported for advanced use cases: ```ts -import { toScAddress, toScU64, toScU32, toScPermissionsVec, toScOperationsVec } from '@ancore/core-sdk'; +import { + toScAddress, + toScU64, + toScU32, + toScPermissionsVec, + toScOperationsVec, +} from '@ancore/core-sdk'; ``` ## Testing diff --git a/packages/core-sdk/eslint.config.cjs b/packages/core-sdk/eslint.config.cjs index 474b560..a70c6bc 100644 --- a/packages/core-sdk/eslint.config.cjs +++ b/packages/core-sdk/eslint.config.cjs @@ -12,6 +12,11 @@ module.exports = [ ecmaVersion: 2020, sourceType: 'module', }, + globals: { + console: 'readonly', + process: 'readonly', + Buffer: 'readonly', + }, }, plugins: { '@typescript-eslint': tseslint, @@ -19,6 +24,24 @@ module.exports = [ rules: { ...tseslint.configs.recommended.rules, '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + 'no-undef': 'off', + }, + }, + { + files: ['src/__tests__/**/*.ts'], + languageOptions: { + globals: { + describe: 'readonly', + it: 'readonly', + expect: 'readonly', + beforeAll: 'readonly', + beforeEach: 'readonly', + jest: 'readonly', + }, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-require-imports': 'off', }, }, ]; diff --git a/packages/core-sdk/src/__tests__/builder.test.ts b/packages/core-sdk/src/__tests__/builder.test.ts index 22557be..22b33fd 100644 --- a/packages/core-sdk/src/__tests__/builder.test.ts +++ b/packages/core-sdk/src/__tests__/builder.test.ts @@ -5,16 +5,7 @@ * offline and fast. */ -import { - Account, - Contract, - Keypair, - Memo, - Networks, - rpc, - StrKey, - xdr, -} from '@stellar/stellar-sdk'; +import { Account, Contract, Keypair, Memo, Networks, rpc, StrKey, xdr } from '@stellar/stellar-sdk'; import { AccountTransactionBuilder } from '../account-transaction-builder'; import { @@ -43,13 +34,10 @@ const SESSION_KEYPAIR = Keypair.random(); const SESSION_PUBLIC_KEY = SESSION_KEYPAIR.publicKey(); // Generate a valid contract strkey (C… address) -const TEST_CONTRACT_ID: string = StrKey.encodeContract( - require('crypto').randomBytes(32), -); +const TEST_CONTRACT_ID: string = StrKey.encodeContract(require('crypto').randomBytes(32)); // Valid ManageData operation XDR (hex) for operation passthrough tests -const MANAGE_DATA_OP_HEX = - '000000000000000a0000000474657374000000010000000376616c00'; +const MANAGE_DATA_OP_HEX = '000000000000000a0000000474657374000000010000000376616c00'; // --------------------------------------------------------------------------- // Helpers @@ -72,7 +60,7 @@ function makeServer(simulateResult?: any): rpc.Server { } function makeBuilderOptions( - serverOverride?: rpc.Server, + serverOverride?: rpc.Server ): ConstructorParameters[1] { return { server: serverOverride ?? makeServer(), @@ -168,9 +156,7 @@ describe('contract-params', () => { }); it('throws for non-array input', () => { - expect(() => toScPermissionsVec('not-array' as any)).toThrow( - /Permissions must be an array/, - ); + expect(() => toScPermissionsVec('not-array' as any)).toThrow(/Permissions must be an array/); }); it('throws if a permission value is invalid', () => { @@ -276,10 +262,7 @@ describe('AccountTransactionBuilder', () => { describe('constructor', () => { it('creates an instance with valid options', () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); expect(builder).toBeInstanceOf(AccountTransactionBuilder); }); @@ -289,7 +272,7 @@ describe('AccountTransactionBuilder', () => { new AccountTransactionBuilder(makeSourceAccount(), { ...makeBuilderOptions(), accountContractId: '', - }), + }) ).toThrow(BuilderValidationError); }); @@ -309,46 +292,30 @@ describe('AccountTransactionBuilder', () => { describe('addSessionKey', () => { it('returns this for chaining', () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); - const result = builder.addSessionKey( - SESSION_PUBLIC_KEY, - [0, 1], - Date.now() + 60_000, - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); + const result = builder.addSessionKey(SESSION_PUBLIC_KEY, [0, 1], Date.now() + 60_000); expect(result).toBe(builder); }); it('throws for invalid public key', () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); expect(() => builder.addSessionKey('BADKEY', [0], Date.now())).toThrow( - /Invalid Stellar public key/, + /Invalid Stellar public key/ ); }); it('throws for invalid permissions', () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); + expect(() => builder.addSessionKey(SESSION_PUBLIC_KEY, [-1], Date.now())).toThrow( + /Invalid u32 value/ ); - expect(() => - builder.addSessionKey(SESSION_PUBLIC_KEY, [-1], Date.now()), - ).toThrow(/Invalid u32 value/); }); it('throws for invalid expiration', () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); + expect(() => builder.addSessionKey(SESSION_PUBLIC_KEY, [0], -100)).toThrow( + /Invalid u64 value/ ); - expect(() => - builder.addSessionKey(SESSION_PUBLIC_KEY, [0], -100), - ).toThrow(/Invalid u64 value/); }); }); @@ -358,22 +325,14 @@ describe('AccountTransactionBuilder', () => { describe('revokeSessionKey', () => { it('returns this for chaining', () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); const result = builder.revokeSessionKey(SESSION_PUBLIC_KEY); expect(result).toBe(builder); }); it('throws for invalid public key', () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); - expect(() => builder.revokeSessionKey('')).toThrow( - /Invalid Stellar public key/, - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); + expect(() => builder.revokeSessionKey('')).toThrow(/Invalid Stellar public key/); }); }); @@ -383,34 +342,21 @@ describe('AccountTransactionBuilder', () => { describe('execute', () => { it('returns this for chaining', () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); const op = makeValidXdrOperation(); const result = builder.execute(SESSION_PUBLIC_KEY, [op]); expect(result).toBe(builder); }); it('throws for empty operations array', () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); - expect(() => builder.execute(SESSION_PUBLIC_KEY, [])).toThrow( - /non-empty array/, - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); + expect(() => builder.execute(SESSION_PUBLIC_KEY, [])).toThrow(/non-empty array/); }); it('throws for invalid session key', () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); const op = makeValidXdrOperation(); - expect(() => builder.execute('BADKEY', [op])).toThrow( - /Invalid Stellar public key/, - ); + expect(() => builder.execute('BADKEY', [op])).toThrow(/Invalid Stellar public key/); }); }); @@ -420,10 +366,7 @@ describe('AccountTransactionBuilder', () => { describe('addOperation', () => { it('returns this for chaining', () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); const contract = new Contract(TEST_CONTRACT_ID); const op = contract.call('some_method'); @@ -439,10 +382,7 @@ describe('AccountTransactionBuilder', () => { describe('addMemo', () => { it('returns this for chaining', () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); const result = builder.addMemo(Memo.text('hello')); expect(result).toBe(builder); }); @@ -454,10 +394,7 @@ describe('AccountTransactionBuilder', () => { describe('setTimeout', () => { it('returns this for chaining', () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); const result = builder.setTimeout(600); expect(result).toBe(builder); }); @@ -469,10 +406,7 @@ describe('AccountTransactionBuilder', () => { describe('simulate', () => { it('throws BuilderValidationError when no operations added', async () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); await expect(builder.simulate()).rejects.toThrow(BuilderValidationError); }); @@ -482,7 +416,7 @@ describe('AccountTransactionBuilder', () => { const server = makeServer(mockSimResponse); const builder = new AccountTransactionBuilder( makeSourceAccount(), - makeBuilderOptions(server), + makeBuilderOptions(server) ); builder.addSessionKey(SESSION_PUBLIC_KEY, [0], Date.now() + 60_000); @@ -499,19 +433,13 @@ describe('AccountTransactionBuilder', () => { describe('build', () => { it('throws BuilderValidationError when no operations added', async () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); await expect(builder.build()).rejects.toThrow(BuilderValidationError); }); it('includes actionable message when no operations added', async () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); await expect(builder.build()).rejects.toThrow(/zero operations/); }); @@ -525,20 +453,14 @@ describe('AccountTransactionBuilder', () => { }; const server = makeServer(errorResponse); - const isErrorSpy = jest - .spyOn(rpc.Api, 'isSimulationError') - .mockReturnValue(true); - const isRestoreSpy = jest - .spyOn(rpc.Api, 'isSimulationRestore') - .mockReturnValue(false); - const isSuccessSpy = jest - .spyOn(rpc.Api, 'isSimulationSuccess') - .mockReturnValue(false); + const isErrorSpy = jest.spyOn(rpc.Api, 'isSimulationError').mockReturnValue(true); + const isRestoreSpy = jest.spyOn(rpc.Api, 'isSimulationRestore').mockReturnValue(false); + const isSuccessSpy = jest.spyOn(rpc.Api, 'isSimulationSuccess').mockReturnValue(false); try { const builder = new AccountTransactionBuilder( makeSourceAccount(), - makeBuilderOptions(server), + makeBuilderOptions(server) ); builder.addSessionKey(SESSION_PUBLIC_KEY, [0], Date.now() + 60_000); @@ -554,20 +476,14 @@ describe('AccountTransactionBuilder', () => { const restoreResponse = { id: 'sim-restore', latestLedger: 100 }; const server = makeServer(restoreResponse); - const isErrorSpy = jest - .spyOn(rpc.Api, 'isSimulationError') - .mockReturnValue(false); - const isRestoreSpy = jest - .spyOn(rpc.Api, 'isSimulationRestore') - .mockReturnValue(true); - const isSuccessSpy = jest - .spyOn(rpc.Api, 'isSimulationSuccess') - .mockReturnValue(false); + const isErrorSpy = jest.spyOn(rpc.Api, 'isSimulationError').mockReturnValue(false); + const isRestoreSpy = jest.spyOn(rpc.Api, 'isSimulationRestore').mockReturnValue(true); + const isSuccessSpy = jest.spyOn(rpc.Api, 'isSimulationSuccess').mockReturnValue(false); try { const builder = new AccountTransactionBuilder( makeSourceAccount(), - makeBuilderOptions(server), + makeBuilderOptions(server) ); builder.addSessionKey(SESSION_PUBLIC_KEY, [0], Date.now() + 60_000); @@ -582,15 +498,9 @@ describe('AccountTransactionBuilder', () => { it('returns assembled Transaction on successful simulation', async () => { const server = makeServer({ id: 'sim-ok', latestLedger: 100 }); - const isErrorSpy = jest - .spyOn(rpc.Api, 'isSimulationError') - .mockReturnValue(false); - const isRestoreSpy = jest - .spyOn(rpc.Api, 'isSimulationRestore') - .mockReturnValue(false); - const isSuccessSpy = jest - .spyOn(rpc.Api, 'isSimulationSuccess') - .mockReturnValue(true); + const isErrorSpy = jest.spyOn(rpc.Api, 'isSimulationError').mockReturnValue(false); + const isRestoreSpy = jest.spyOn(rpc.Api, 'isSimulationRestore').mockReturnValue(false); + const isSuccessSpy = jest.spyOn(rpc.Api, 'isSimulationSuccess').mockReturnValue(true); // We can't spy on rpc.assembleTransaction directly (non-configurable), // so we mock the entire build flow by making the builder's internal @@ -601,7 +511,7 @@ describe('AccountTransactionBuilder', () => { try { const builder = new AccountTransactionBuilder( makeSourceAccount(), - makeBuilderOptions(server), + makeBuilderOptions(server) ); builder.addSessionKey(SESSION_PUBLIC_KEY, [0], Date.now() + 60_000); @@ -629,26 +539,18 @@ describe('AccountTransactionBuilder', () => { const weirdResponse = { id: 'sim-weird', latestLedger: 100 }; const server = makeServer(weirdResponse); - const isErrorSpy = jest - .spyOn(rpc.Api, 'isSimulationError') - .mockReturnValue(false); - const isRestoreSpy = jest - .spyOn(rpc.Api, 'isSimulationRestore') - .mockReturnValue(false); - const isSuccessSpy = jest - .spyOn(rpc.Api, 'isSimulationSuccess') - .mockReturnValue(false); + const isErrorSpy = jest.spyOn(rpc.Api, 'isSimulationError').mockReturnValue(false); + const isRestoreSpy = jest.spyOn(rpc.Api, 'isSimulationRestore').mockReturnValue(false); + const isSuccessSpy = jest.spyOn(rpc.Api, 'isSimulationSuccess').mockReturnValue(false); try { const builder = new AccountTransactionBuilder( makeSourceAccount(), - makeBuilderOptions(server), + makeBuilderOptions(server) ); builder.addSessionKey(SESSION_PUBLIC_KEY, [0], Date.now() + 60_000); - await expect(builder.build()).rejects.toThrow( - /Unexpected simulation response/, - ); + await expect(builder.build()).rejects.toThrow(/Unexpected simulation response/); } finally { isErrorSpy.mockRestore(); isRestoreSpy.mockRestore(); @@ -663,10 +565,7 @@ describe('AccountTransactionBuilder', () => { describe('fluent API', () => { it('supports chaining multiple convenience methods', () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); const thirdKey = Keypair.random().publicKey(); @@ -679,10 +578,7 @@ describe('AccountTransactionBuilder', () => { }); it('supports mixing convenience methods with passthrough', () => { - const builder = new AccountTransactionBuilder( - makeSourceAccount(), - makeBuilderOptions(), - ); + const builder = new AccountTransactionBuilder(makeSourceAccount(), makeBuilderOptions()); const contract = new Contract(TEST_CONTRACT_ID); const customOp = contract.call('custom_method'); diff --git a/packages/core-sdk/src/__tests__/integration.test.ts b/packages/core-sdk/src/__tests__/integration.test.ts index 106318b..886cddb 100644 --- a/packages/core-sdk/src/__tests__/integration.test.ts +++ b/packages/core-sdk/src/__tests__/integration.test.ts @@ -14,13 +14,7 @@ * TESTNET_SECRET_KEY=S... TESTNET_CONTRACT_ID=C... pnpm test:integration */ -import { - Account, - Keypair, - Memo, - Networks, - rpc, -} from '@stellar/stellar-sdk'; +import { Account, Keypair, Memo, Networks, rpc } from '@stellar/stellar-sdk'; import { AccountTransactionBuilder } from '../account-transaction-builder'; import { SimulationFailedError } from '../errors'; @@ -29,8 +23,7 @@ import { SimulationFailedError } from '../errors'; // Configuration // --------------------------------------------------------------------------- -const SOROBAN_RPC_URL = - process.env.SOROBAN_RPC_URL ?? 'https://soroban-testnet.stellar.org'; +const SOROBAN_RPC_URL = process.env.SOROBAN_RPC_URL ?? 'https://soroban-testnet.stellar.org'; const SECRET_KEY = process.env.TESTNET_SECRET_KEY; const CONTRACT_ID = process.env.TESTNET_CONTRACT_ID; @@ -50,10 +43,7 @@ describeIntegration('AccountTransactionBuilder – Testnet Integration', () => { // Fetch account from ledger const ledgerAccount = await server.getAccount(keypair.publicKey()); - sourceAccount = new Account( - ledgerAccount.accountId(), - ledgerAccount.sequenceNumber(), - ); + sourceAccount = new Account(ledgerAccount.accountId(), ledgerAccount.sequenceNumber()); }); // ----------------------------------------------------------------------- @@ -73,7 +63,7 @@ describeIntegration('AccountTransactionBuilder – Testnet Integration', () => { .addSessionKey( sessionKeypair.publicKey(), [0, 1], // SEND_PAYMENT, MANAGE_DATA - Math.floor(Date.now() / 1000) + 3600, // 1 hour from now + Math.floor(Date.now() / 1000) + 3600 // 1 hour from now ) .addMemo(Memo.text('integration-test')) .simulate(); @@ -99,11 +89,7 @@ describeIntegration('AccountTransactionBuilder – Testnet Integration', () => { try { const tx = await builder - .addSessionKey( - sessionKeypair.publicKey(), - [0], - Math.floor(Date.now() / 1000) + 3600, - ) + .addSessionKey(sessionKeypair.publicKey(), [0], Math.floor(Date.now() / 1000) + 3600) .build(); // If the contract is deployed and the simulation succeeds, we get a @@ -130,9 +116,7 @@ describeIntegration('AccountTransactionBuilder – Testnet Integration', () => { networkPassphrase: Networks.TESTNET, }); - const response = await builder - .revokeSessionKey(sessionKeypair.publicKey()) - .simulate(); + const response = await builder.revokeSessionKey(sessionKeypair.publicKey()).simulate(); expect(response).toBeDefined(); }); @@ -152,11 +136,7 @@ describeIntegration('AccountTransactionBuilder – Testnet Integration', () => { try { const tx = await builder - .addSessionKey( - sessionKeypair.publicKey(), - [0, 1, 2], - Math.floor(Date.now() / 1000) + 7200, - ) + .addSessionKey(sessionKeypair.publicKey(), [0, 1, 2], Math.floor(Date.now() / 1000) + 7200) .addMemo(Memo.text('submit-test')) .build(); @@ -167,9 +147,7 @@ describeIntegration('AccountTransactionBuilder – Testnet Integration', () => { const result = await server.sendTransaction(tx); expect(result).toBeDefined(); - expect(['PENDING', 'DUPLICATE', 'TRY_AGAIN_LATER', 'ERROR']).toContain( - result.status, - ); + expect(['PENDING', 'DUPLICATE', 'TRY_AGAIN_LATER', 'ERROR']).toContain(result.status); // If PENDING, we could poll for confirmation, but that's beyond the // scope of this test. We've validated the full build → sign → submit flow. diff --git a/packages/core-sdk/src/account-transaction-builder.ts b/packages/core-sdk/src/account-transaction-builder.ts index 206752e..d1508b0 100644 --- a/packages/core-sdk/src/account-transaction-builder.ts +++ b/packages/core-sdk/src/account-transaction-builder.ts @@ -28,18 +28,9 @@ import { xdr, } from '@stellar/stellar-sdk'; -import { - toScAddress, - toScOperationsVec, - toScPermissionsVec, - toScU64, -} from './contract-params'; +import { toScAddress, toScOperationsVec, toScPermissionsVec, toScU64 } from './contract-params'; -import { - BuilderValidationError, - SimulationExpiredError, - SimulationFailedError, -} from './errors'; +import { BuilderValidationError, SimulationExpiredError, SimulationFailedError } from './errors'; // --------------------------------------------------------------------------- // Options @@ -90,10 +81,7 @@ export class AccountTransactionBuilder { /** Whether setTimeout has already been applied to the inner builder. */ private timeoutApplied = false; - constructor( - sourceAccount: Account, - options: AccountTransactionBuilderOptions, - ) { + constructor(sourceAccount: Account, options: AccountTransactionBuilderOptions) { const { server, accountContractId, @@ -105,7 +93,7 @@ export class AccountTransactionBuilder { if (!accountContractId) { throw new BuilderValidationError( 'accountContractId is required. Provide the C… contract ID of your ' + - 'deployed Ancore account contract.', + 'deployed Ancore account contract.' ); } @@ -135,16 +123,12 @@ export class AccountTransactionBuilder { * @param expiresAt - Expiration timestamp (unix ms) * @returns `this` for chaining */ - addSessionKey( - publicKey: string, - permissions: number[], - expiresAt: number, - ): this { + addSessionKey(publicKey: string, permissions: number[], expiresAt: number): this { const operation = this.contract.call( 'add_session_key', toScAddress(publicKey), toScPermissionsVec(permissions), - toScU64(expiresAt), + toScU64(expiresAt) ); this.txBuilder.addOperation(operation); @@ -161,10 +145,7 @@ export class AccountTransactionBuilder { * @returns `this` for chaining */ revokeSessionKey(publicKey: string): this { - const operation = this.contract.call( - 'revoke_session_key', - toScAddress(publicKey), - ); + const operation = this.contract.call('revoke_session_key', toScAddress(publicKey)); this.txBuilder.addOperation(operation); this.operationCount++; @@ -180,14 +161,11 @@ export class AccountTransactionBuilder { * @param operations - Array of Stellar XDR operations to execute * @returns `this` for chaining */ - execute( - sessionKeyPublicKey: string, - operations: xdr.Operation[], - ): this { + execute(sessionKeyPublicKey: string, operations: xdr.Operation[]): this { const operation = this.contract.call( 'execute', toScAddress(sessionKeyPublicKey), - toScOperationsVec(operations), + toScOperationsVec(operations) ); this.txBuilder.addOperation(operation); @@ -280,7 +258,7 @@ export class AccountTransactionBuilder { // Handle simulation failure if (rpc.Api.isSimulationError(simulation)) { throw new SimulationFailedError( - (simulation as rpc.Api.SimulateTransactionErrorResponse).error, + (simulation as rpc.Api.SimulateTransactionErrorResponse).error ); } @@ -296,7 +274,7 @@ export class AccountTransactionBuilder { // Fallback – should not happen, but guard defensively throw new SimulationFailedError( - 'Unexpected simulation response shape. Please check Soroban RPC health.', + 'Unexpected simulation response shape. Please check Soroban RPC health.' ); } @@ -321,7 +299,7 @@ export class AccountTransactionBuilder { if (this.operationCount === 0) { throw new BuilderValidationError( 'Cannot simulate or build a transaction with zero operations. ' + - 'Use addSessionKey(), revokeSessionKey(), execute(), or addOperation() first.', + 'Use addSessionKey(), revokeSessionKey(), execute(), or addOperation() first.' ); } } diff --git a/packages/core-sdk/src/contract-params.ts b/packages/core-sdk/src/contract-params.ts index 81a267e..d60c645 100644 --- a/packages/core-sdk/src/contract-params.ts +++ b/packages/core-sdk/src/contract-params.ts @@ -23,9 +23,7 @@ import { Address, nativeToScVal, xdr } from '@stellar/stellar-sdk'; */ export function toScAddress(publicKey: string): xdr.ScVal { if (!publicKey || !publicKey.startsWith('G')) { - throw new Error( - `Invalid Stellar public key: expected a G… address, received "${publicKey}"`, - ); + throw new Error(`Invalid Stellar public key: expected a G… address, received "${publicKey}"`); } return xdr.ScVal.scvAddress(Address.fromString(publicKey).toScAddress()); @@ -44,9 +42,7 @@ export function toScAddress(publicKey: string): xdr.ScVal { */ export function toScU64(value: number): xdr.ScVal { if (!Number.isInteger(value) || value < 0) { - throw new Error( - `Invalid u64 value: expected a non-negative integer, received ${value}`, - ); + throw new Error(`Invalid u64 value: expected a non-negative integer, received ${value}`); } return nativeToScVal(value, { type: 'u64' }); @@ -61,9 +57,7 @@ export function toScU64(value: number): xdr.ScVal { */ export function toScU32(value: number): xdr.ScVal { if (!Number.isInteger(value) || value < 0 || value > 0xffff_ffff) { - throw new Error( - `Invalid u32 value: expected 0 ≤ n ≤ ${0xffff_ffff}, received ${value}`, - ); + throw new Error(`Invalid u32 value: expected 0 ≤ n ≤ ${0xffff_ffff}, received ${value}`); } return nativeToScVal(value, { type: 'u32' }); @@ -99,9 +93,7 @@ export function toScPermissionsVec(permissions: number[]): xdr.ScVal { */ export function toScOperationsVec(operations: xdr.Operation[]): xdr.ScVal { if (!Array.isArray(operations) || operations.length === 0) { - throw new Error( - 'Operations must be a non-empty array of xdr.Operation values', - ); + throw new Error('Operations must be a non-empty array of xdr.Operation values'); } const items = operations.map((op) => { diff --git a/packages/core-sdk/src/errors.ts b/packages/core-sdk/src/errors.ts index 6e1ea43..aa2d67f 100644 --- a/packages/core-sdk/src/errors.ts +++ b/packages/core-sdk/src/errors.ts @@ -64,7 +64,7 @@ export class SimulationExpiredError extends AncoreSdkError { 'SIMULATION_EXPIRED', 'The simulation result has expired or requires ledger entry restoration. ' + 'Please retry the transaction. If this persists the contract storage ' + - 'may need to be restored first.', + 'may need to be restored first.' ); this.name = 'SimulationExpiredError'; Object.setPrototypeOf(this, new.target.prototype); diff --git a/packages/core-sdk/src/storage/__tests__/manager.test.ts b/packages/core-sdk/src/storage/__tests__/manager.test.ts index a61c3d1..766e153 100644 --- a/packages/core-sdk/src/storage/__tests__/manager.test.ts +++ b/packages/core-sdk/src/storage/__tests__/manager.test.ts @@ -1,7 +1,8 @@ import { webcrypto } from 'crypto'; +import type { EncryptedPayload, StorageAdapter } from '../types'; if (!globalThis.crypto) { - // @ts-ignore + // @ts-expect-error Node test environment does not expose writable crypto typing. globalThis.crypto = webcrypto; } if (!globalThis.btoa) { @@ -12,16 +13,16 @@ if (!globalThis.atob) { } import { SecureStorageManager } from '../secure-storage-manager'; -import { StorageAdapter, AccountData, SessionKeysData } from '../types'; +import type { AccountData, SessionKeysData } from '../types'; class MockStorageAdapter implements StorageAdapter { - private store: Map = new Map(); + private store = new Map(); - async get(key: string): Promise { - return this.store.get(key) || null; + async get(key: string): Promise { + return (this.store.get(key) as T | undefined) ?? null; } - async set(key: string, value: any): Promise { + async set(key: string, value: T): Promise { this.store.set(key, value); } @@ -29,7 +30,7 @@ class MockStorageAdapter implements StorageAdapter { this.store.delete(key); } - public inspectStore(): Map { + public inspectStore(): Map { return this.store; } } @@ -51,9 +52,9 @@ describe('SecureStorageManager', () => { await manager.unlock(password); await manager.saveAccount(accountData); - const storedData = await storage.get('account'); + const storedData = await storage.get('account'); expect(storedData).toBeDefined(); - + // Ensure it's not plaintext const jsonStr = JSON.stringify(storedData); expect(jsonStr).not.toContain(accountData.privateKey); @@ -95,7 +96,7 @@ describe('SecureStorageManager', () => { it('should fail gracefully with the wrong password', async () => { await manager.unlock(password); await manager.saveAccount(accountData); - + manager.lock(); const newManager = new SecureStorageManager(storage); await newManager.unlock('wrong_password'); @@ -105,14 +106,14 @@ describe('SecureStorageManager', () => { it('should return null for non-existent items', async () => { await manager.unlock(password); - + const account = await manager.getAccount(); expect(account).toBeNull(); - + const sessionKeys = await manager.getSessionKeys(); expect(sessionKeys).toBeNull(); }); - + it('should not throw on unlock if already unlocked', async () => { await manager.unlock(password); await manager.unlock(password); // Should return early diff --git a/packages/core-sdk/src/storage/secure-storage-manager.ts b/packages/core-sdk/src/storage/secure-storage-manager.ts index d329795..d93bf92 100644 --- a/packages/core-sdk/src/storage/secure-storage-manager.ts +++ b/packages/core-sdk/src/storage/secure-storage-manager.ts @@ -1,7 +1,10 @@ import { AccountData, EncryptedPayload, SessionKeysData, StorageAdapter } from './types'; -function bufferToBase64(buffer: ArrayBuffer): string { - const bytes = new Uint8Array(buffer); +function bufferToBase64(buffer: BufferSource): string { + const bytes = + buffer instanceof ArrayBuffer + ? new Uint8Array(buffer) + : new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); let binary = ''; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); @@ -53,12 +56,12 @@ export class SecureStorageManager { return this.baseKey !== null; } - private async deriveAesKey(salt: Uint8Array | any): Promise { + private async deriveAesKey(salt: BufferSource): Promise { if (!this.baseKey) throw new Error('Storage manager is locked'); return globalThis.crypto.subtle.deriveKey( { name: 'PBKDF2', - salt: salt as any, + salt, iterations: 100000, hash: 'SHA-256', }, @@ -70,9 +73,9 @@ export class SecureStorageManager { } private async encryptData(plaintext: string): Promise { - const salt = globalThis.crypto.getRandomValues(new Uint8Array(16) as any); - const iv = globalThis.crypto.getRandomValues(new Uint8Array(12) as any); - + const salt = globalThis.crypto.getRandomValues(new Uint8Array(16)); + const iv = globalThis.crypto.getRandomValues(new Uint8Array(12)); + const aesKey = await this.deriveAesKey(salt); const encoder = new TextEncoder(); @@ -85,7 +88,7 @@ export class SecureStorageManager { return { salt: bufferToBase64(salt), iv: bufferToBase64(iv), - data: bufferToBase64(ciphertext) + data: bufferToBase64(ciphertext), }; } @@ -94,16 +97,16 @@ export class SecureStorageManager { const iv = base64ToBuffer(payload.iv); const ciphertext = base64ToBuffer(payload.data); - const aesKey = await this.deriveAesKey(new Uint8Array(salt) as any); + const aesKey = await this.deriveAesKey(new Uint8Array(salt)); try { const decryptedBuffer = await globalThis.crypto.subtle.decrypt( - { name: 'AES-GCM', iv: new Uint8Array(iv) as any }, + { name: 'AES-GCM', iv: new Uint8Array(iv) }, aesKey, - ciphertext as any + ciphertext ); return new TextDecoder().decode(decryptedBuffer); - } catch (error: any) { + } catch { throw new Error('Invalid password or corrupted data'); } } @@ -114,7 +117,7 @@ export class SecureStorageManager { } public async getAccount(): Promise { - const payload = await this.storage.get('account') as EncryptedPayload | null; + const payload = await this.storage.get('account'); if (!payload) return null; const json = await this.decryptData(payload); return JSON.parse(json); @@ -126,7 +129,7 @@ export class SecureStorageManager { } public async getSessionKeys(): Promise { - const payload = await this.storage.get('sessionKeys') as EncryptedPayload | null; + const payload = await this.storage.get('sessionKeys'); if (!payload) return null; const json = await this.decryptData(payload); return JSON.parse(json); diff --git a/packages/core-sdk/src/storage/types.ts b/packages/core-sdk/src/storage/types.ts index d34bdc2..cd55af7 100644 --- a/packages/core-sdk/src/storage/types.ts +++ b/packages/core-sdk/src/storage/types.ts @@ -8,17 +8,17 @@ export interface EncryptedPayload { } export interface StorageAdapter { - get(key: string): Promise; - set(key: string, value: any): Promise; + get(key: string): Promise; + set(key: string, value: T): Promise; remove(key: string): Promise; } export interface AccountData { privateKey: string; - [key: string]: any; + [key: string]: unknown; } export interface SessionKeysData { keys: Record; - [key: string]: any; + [key: string]: unknown; } diff --git a/packages/core-sdk/tsconfig.json b/packages/core-sdk/tsconfig.json index 7d45a4a..fcb3a56 100644 --- a/packages/core-sdk/tsconfig.json +++ b/packages/core-sdk/tsconfig.json @@ -9,8 +9,7 @@ "skipLibCheck": true, "esModuleInterop": true, "rootDir": "./src", - "outDir": "./dist" - "moduleResolution": "node", + "outDir": "./dist", "composite": true }, "include": ["src/**/*"], diff --git a/packages/crypto/src/__tests__/password-strengh.test.ts b/packages/crypto/src/__tests__/password-strengh.test.ts index 3d48b57..3311167 100644 --- a/packages/crypto/src/__tests__/password-strengh.test.ts +++ b/packages/crypto/src/__tests__/password-strengh.test.ts @@ -25,7 +25,6 @@ function assertNoPasswordLeak(password: string, result: PasswordValidationResult } describe('validatePasswordStrength()', () => { - describe('input edge cases', () => { it('rejects an empty string', () => { const result = validatePasswordStrength(''); diff --git a/packages/crypto/src/__tests__/verify-signature.test.ts b/packages/crypto/src/__tests__/verify-signature.test.ts index 11cfcf7..9f688ff 100644 --- a/packages/crypto/src/__tests__/verify-signature.test.ts +++ b/packages/crypto/src/__tests__/verify-signature.test.ts @@ -7,9 +7,7 @@ import { verifySignature } from '../signing'; describe('verifySignature', () => { it('returns true for a valid signature', async () => { - const privateKey = Buffer.from( - Array.from({ length: 32 }, (_, index) => index + 1) - ); + const privateKey = Buffer.from(Array.from({ length: 32 }, (_, index) => index + 1)); const message = new TextEncoder().encode('Authorize session key operation'); const keypair = Keypair.fromRawEd25519Seed(privateKey); const signature = keypair.sign(Buffer.from(message)); @@ -20,23 +18,15 @@ describe('verifySignature', () => { }); it('returns false for an invalid signature', async () => { - const privateKey = Buffer.from( - Array.from({ length: 32 }, (_, index) => index + 1) - ); - const otherPrivateKey = Buffer.from( - Array.from({ length: 32 }, (_, index) => index + 33) - ); + const privateKey = Buffer.from(Array.from({ length: 32 }, (_, index) => index + 1)); + const otherPrivateKey = Buffer.from(Array.from({ length: 32 }, (_, index) => index + 33)); const message = 'Authorize contract execution'; const keypair = Keypair.fromRawEd25519Seed(privateKey); const otherKeypair = Keypair.fromRawEd25519Seed(otherPrivateKey); const invalidSignature = otherKeypair.sign(Buffer.from(message)); await expect( - verifySignature( - message, - Buffer.from(invalidSignature).toString('hex'), - keypair.publicKey() - ) + verifySignature(message, Buffer.from(invalidSignature).toString('hex'), keypair.publicKey()) ).resolves.toBe(false); }); }); diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts index 1118309..c2ad2b8 100644 --- a/packages/crypto/src/index.ts +++ b/packages/crypto/src/index.ts @@ -7,4 +7,4 @@ export const CRYPTO_VERSION = '0.1.0'; export { verifySignature } from './signing'; -export { validatePasswordStrength } from "./password" +export { validatePasswordStrength } from './password'; diff --git a/packages/crypto/src/password.ts b/packages/crypto/src/password.ts index aa22e3b..1fac202 100644 --- a/packages/crypto/src/password.ts +++ b/packages/crypto/src/password.ts @@ -4,7 +4,7 @@ * */ -export type PasswordStrength = "weak" | "medium" | "strong"; +export type PasswordStrength = 'weak' | 'medium' | 'strong'; export interface PasswordValidationResult { valid: boolean; @@ -12,7 +12,6 @@ export interface PasswordValidationResult { reasons: string[]; } - const MIN_LENGTH = 12; const STRONG_LENGTH = 16; @@ -21,16 +20,14 @@ const STRONG_LENGTH = 16; * Stored as lowercase for case-insensitive comparison. */ const COMMON_WEAK_PATTERNS: RegExp[] = [ - /^(.)\1+$/, // All same character: "aaaaaa", "111111" + /^(.)\1+$/, // All same character: "aaaaaa", "111111" /^(012|123|234|345|456|567|678|789|890|987|876|765|654|543|432|321|210)+$/i, // Pure numeric sequences /^(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz)+$/i, // Pure alpha sequences /^(password|passw0rd|p@ssword|p@ssw0rd|qwerty|letmein|welcome|admin|login|iloveyou|monkey|dragon|master|sunshine|shadow|princess|football|baseball|superman|batman)+$/i, // Common dictionary passwords - /^[0-9]+$/, // Digits only - /^[a-zA-Z]+$/, // Letters only — no digits or symbols + /^[0-9]+$/, // Digits only + /^[a-zA-Z]+$/, // Letters only — no digits or symbols ]; - - function hasUppercase(password: string): boolean { return /[A-Z]/.test(password); } @@ -51,7 +48,6 @@ function matchesWeakPattern(password: string): boolean { return COMMON_WEAK_PATTERNS.some((pattern) => pattern.test(password)); } - /** * Validates the strength of a password. * @@ -70,12 +66,11 @@ function matchesWeakPattern(password: string): boolean { export function validatePasswordStrength(password: string): PasswordValidationResult { const reasons: string[] = []; - - if (typeof password !== "string" || password.length === 0) { + if (typeof password !== 'string' || password.length === 0) { return { valid: false, - strength: "weak", - reasons: ["Password must be a non-empty string."], + strength: 'weak', + reasons: ['Password must be a non-empty string.'], }; } @@ -84,27 +79,27 @@ export function validatePasswordStrength(password: string): PasswordValidationRe } if (!hasUppercase(password)) { - reasons.push("Password must contain at least one uppercase letter."); + reasons.push('Password must contain at least one uppercase letter.'); } if (!hasLowercase(password)) { - reasons.push("Password must contain at least one lowercase letter."); + reasons.push('Password must contain at least one lowercase letter.'); } if (!hasDigit(password)) { - reasons.push("Password must contain at least one digit."); + reasons.push('Password must contain at least one digit.'); } if (!hasSpecialChar(password)) { - reasons.push("Password must contain at least one special character."); + reasons.push('Password must contain at least one special character.'); } if (matchesWeakPattern(password)) { - reasons.push("Password matches a commonly used or easily guessable pattern."); + reasons.push('Password matches a commonly used or easily guessable pattern.'); } if (reasons.length > 0) { - return { valid: false, strength: "weak", reasons }; + return { valid: false, strength: 'weak', reasons }; } const isLongEnough = password.length >= STRONG_LENGTH; @@ -115,7 +110,7 @@ export function validatePasswordStrength(password: string): PasswordValidationRe hasSpecialChar(password); if (isLongEnough && hasVariety) { - return { valid: true, strength: "strong", reasons: [] }; + return { valid: true, strength: 'strong', reasons: [] }; } // Passes minimums but could be stronger @@ -126,5 +121,5 @@ export function validatePasswordStrength(password: string): PasswordValidationRe ); } - return { valid: true, strength: "medium", reasons: suggestions }; -} \ No newline at end of file + return { valid: true, strength: 'medium', reasons: suggestions }; +} diff --git a/packages/stellar/package.json b/packages/stellar/package.json index 17e7fa5..ff5fd03 100644 --- a/packages/stellar/package.json +++ b/packages/stellar/package.json @@ -41,4 +41,4 @@ "tsup": "^8.0.0", "typescript": "^5.6.0" } -} \ No newline at end of file +} diff --git a/packages/stellar/src/errors.ts b/packages/stellar/src/errors.ts index 43224c8..9854786 100644 --- a/packages/stellar/src/errors.ts +++ b/packages/stellar/src/errors.ts @@ -48,10 +48,7 @@ export class TransactionError extends StellarError { public readonly resultCode?: string; public readonly resultXdr?: string; - constructor( - message: string, - options?: { resultCode?: string; resultXdr?: string } - ) { + constructor(message: string, options?: { resultCode?: string; resultXdr?: string }) { super(message); this.name = 'TransactionError'; this.resultCode = options?.resultCode; diff --git a/packages/stellar/src/retry.ts b/packages/stellar/src/retry.ts index 8dc6d4b..4e56a31 100644 --- a/packages/stellar/src/retry.ts +++ b/packages/stellar/src/retry.ts @@ -24,8 +24,7 @@ const DEFAULT_OPTIONS: Required> = { /** * Sleep for a specified duration */ -const sleep = (ms: number): Promise => - new Promise((resolve) => setTimeout(resolve, ms)); +const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); /** * Calculate delay for a given attempt using exponential backoff @@ -58,10 +57,7 @@ export const calculateDelay = ( * ); * ``` */ -export async function withRetry( - fn: () => Promise, - options: RetryOptions = {} -): Promise { +export async function withRetry(fn: () => Promise, options: RetryOptions = {}): Promise { const { maxRetries, baseDelayMs, exponential } = { ...DEFAULT_OPTIONS, ...options, diff --git a/packages/types/package.json b/packages/types/package.json index c962518..ca2c252 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -41,4 +41,4 @@ "zod": "^3.22.0", "@stellar/stellar-sdk": "^12.0.0" } -} \ No newline at end of file +} diff --git a/packages/types/src/__tests__/user-operation.test.ts b/packages/types/src/__tests__/user-operation.test.ts index 9e27834..80f4e74 100644 --- a/packages/types/src/__tests__/user-operation.test.ts +++ b/packages/types/src/__tests__/user-operation.test.ts @@ -1,11 +1,5 @@ -import { - UserOperationSchema, - TransactionResultSchema, -} from '../user-operation'; -import { - isUserOperation, - isTransactionResult, -} from '../guards'; +import { UserOperationSchema, TransactionResultSchema } from '../user-operation'; +import { isUserOperation, isTransactionResult } from '../guards'; describe('UserOperation', () => { describe('UserOperationSchema', () => { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 40f6099..da90ea7 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -32,4 +32,3 @@ export * from './user-operation'; export * from './wallet'; export * from './guards'; export * from './schemas'; - diff --git a/packages/ui-kit/DESIGN_TOKENS.md b/packages/ui-kit/DESIGN_TOKENS.md index 9d31ade..247291b 100644 --- a/packages/ui-kit/DESIGN_TOKENS.md +++ b/packages/ui-kit/DESIGN_TOKENS.md @@ -9,33 +9,41 @@ All colors are defined as CSS variables in `src/styles/globals.css` and use HSL ### Brand Colors #### Primary (Stellar Purple) + The primary color represents the Stellar brand identity. **Light Mode:** + - Default: `hsl(262 83% 58%)` - #8B5CF6 - Foreground: `hsl(210 40% 98%)` - #F8FAFC **Dark Mode:** + - Default: `hsl(262 83% 58%)` - #8B5CF6 (same as light mode) - Foreground: `hsl(210 40% 98%)` - #F8FAFC **Usage:** + ```tsx ``` #### Secondary + Used for secondary actions and neutral emphasis. **Light Mode:** + - Default: `hsl(210 40% 96.1%)` - #F1F5F9 - Foreground: `hsl(222.2 47.4% 11.2%)` - #0F172A **Dark Mode:** + - Default: `hsl(217.2 32.6% 17.5%)` - #1E293B - Foreground: `hsl(210 40% 98%)` - #F8FAFC **Usage:** + ```tsx ``` @@ -43,51 +51,63 @@ Used for secondary actions and neutral emphasis. ### Semantic Colors #### Destructive (Error/Warning) + Used for destructive actions and error states. **Light Mode:** + - Default: `hsl(0 84.2% 60.2%)` - #EF4444 - Foreground: `hsl(210 40% 98%)` - #F8FAFC **Dark Mode:** + - Default: `hsl(0 62.8% 30.6%)` - #7F1D1D - Foreground: `hsl(210 40% 98%)` - #F8FAFC **Usage:** + ```tsx ``` #### Muted + Used for muted or de-emphasized content. **Light Mode:** + - Default: `hsl(210 40% 96.1%)` - #F1F5F9 - Foreground: `hsl(215.4 16.3% 46.9%)` - #64748B **Dark Mode:** + - Default: `hsl(217.2 32.6% 17.5%)` - #1E293B - Foreground: `hsl(215 20.2% 65.1%)` - #94A3B8 **Usage:** + ```tsx

Balance: 100 XLM

``` #### Accent + Used for hover states and highlights. **Light Mode:** + - Default: `hsl(210 40% 96.1%)` - #F1F5F9 - Foreground: `hsl(222.2 47.4% 11.2%)` - #0F172A **Dark Mode:** + - Default: `hsl(217.2 32.6% 17.5%)` - #1E293B - Foreground: `hsl(210 40% 98%)` - #F8FAFC ### Base Colors #### Background + Main application background color. **Light Mode:** `hsl(0 0% 100%)` - #FFFFFF @@ -95,6 +115,7 @@ Main application background color. **Dark Mode:** `hsl(222.2 84% 4.9%)` - #020617 #### Foreground + Main text color. **Light Mode:** `hsl(222.2 84% 4.9%)` - #020617 @@ -102,17 +123,21 @@ Main text color. **Dark Mode:** `hsl(210 40% 98%)` - #F8FAFC #### Border & Input + Default border and input background color. **Light Mode:** + - Border: `hsl(214.3 31.8% 91.4%)` - #E2E8F0 - Input: `hsl(214.3 31.8% 91.4%)` - #E2E8F0 **Dark Mode:** + - Border: `hsl(217.2 32.6% 17.5%)` - #1E293B - Input: `hsl(217.2 32.6% 17.5%)` - #1E293B #### Ring (Focus) + Color used for focus rings. **Light Mode:** `hsl(262 83% 58%)` - #8B5CF6 @@ -122,24 +147,30 @@ Color used for focus rings. ### Component-Specific Colors #### Card + Used for card components. **Light Mode:** + - Background: `hsl(0 0% 100%)` - #FFFFFF - Foreground: `hsl(222.2 84% 4.9%)` - #020617 **Dark Mode:** + - Background: `hsl(222.2 84% 4.9%)` - #020617 - Foreground: `hsl(210 40% 98%)` - #F8FAFC #### Popover + Used for popover/dropdown components. **Light Mode:** + - Background: `hsl(0 0% 100%)` - #FFFFFF - Foreground: `hsl(222.2 84% 4.9%)` - #020617 **Dark Mode:** + - Background: `hsl(222.2 84% 4.9%)` - #020617 - Foreground: `hsl(210 40% 98%)` - #F8FAFC @@ -153,6 +184,7 @@ Border radius is defined as a CSS variable `--radius` with computed values. - **Small (sm):** `calc(var(--radius) - 4px)` = `0.25rem` (4px) **Usage:** + ```tsx
Large radius
Medium radius
@@ -164,10 +196,12 @@ Border radius is defined as a CSS variable `--radius` with computed values. Typography scales use Tailwind CSS defaults. ### Font Families + - **Sans-serif:** System font stack (default) - **Mono:** Monospace font (for addresses, code) ### Font Sizes + - `text-xs`: 0.75rem (12px) - `text-sm`: 0.875rem (14px) - `text-base`: 1rem (16px) @@ -177,6 +211,7 @@ Typography scales use Tailwind CSS defaults. - `text-3xl`: 1.875rem (30px) ### Font Weights + - `font-normal`: 400 - `font-medium`: 500 - `font-semibold`: 600 @@ -187,6 +222,7 @@ Typography scales use Tailwind CSS defaults. Spacing uses Tailwind's default spacing scale (based on 0.25rem = 4px). Common values: + - `1`: 0.25rem (4px) - `2`: 0.5rem (8px) - `3`: 0.75rem (12px) @@ -197,6 +233,7 @@ Common values: ## Shadows Shadows use Tailwind CSS defaults: + - `shadow-sm`: Small shadow - `shadow`: Default shadow - `shadow-md`: Medium shadow @@ -208,15 +245,24 @@ Shadows use Tailwind CSS defaults: Custom animations are defined for certain components: ### Accordion Animations + ```css @keyframes accordion-down { - from { height: 0 } - to { height: var(--radix-accordion-content-height) } + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } } @keyframes accordion-up { - from { height: var(--radix-accordion-content-height) } - to { height: 0 } + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } } ``` @@ -247,16 +293,11 @@ import { Button } from '@ancore/ui-kit'; ```tsx import { cn } from '@ancore/ui-kit'; -
+
Custom styled div -
+
; ``` ### Extending Tokens @@ -297,21 +338,22 @@ All color tokens automatically adjust for dark mode. ## Token Reference Table -| Token | Light Mode | Dark Mode | Usage | -|-------|------------|-----------|-------| -| `--primary` | #8B5CF6 | #8B5CF6 | Primary actions, brand color | -| `--secondary` | #F1F5F9 | #1E293B | Secondary actions | -| `--destructive` | #EF4444 | #7F1D1D | Errors, destructive actions | -| `--muted` | #F1F5F9 | #1E293B | Muted backgrounds | -| `--accent` | #F1F5F9 | #1E293B | Hover states | -| `--background` | #FFFFFF | #020617 | Main background | -| `--foreground` | #020617 | #F8FAFC | Main text | -| `--border` | #E2E8F0 | #1E293B | Borders | -| `--ring` | #8B5CF6 | #8B5CF6 | Focus rings | +| Token | Light Mode | Dark Mode | Usage | +| --------------- | ---------- | --------- | ---------------------------- | +| `--primary` | #8B5CF6 | #8B5CF6 | Primary actions, brand color | +| `--secondary` | #F1F5F9 | #1E293B | Secondary actions | +| `--destructive` | #EF4444 | #7F1D1D | Errors, destructive actions | +| `--muted` | #F1F5F9 | #1E293B | Muted backgrounds | +| `--accent` | #F1F5F9 | #1E293B | Hover states | +| `--background` | #FFFFFF | #020617 | Main background | +| `--foreground` | #020617 | #F8FAFC | Main text | +| `--border` | #E2E8F0 | #1E293B | Borders | +| `--ring` | #8B5CF6 | #8B5CF6 | Focus rings | --- For implementation details, see the source files: + - Color definitions: `src/styles/globals.css` - Tailwind config: `tailwind.config.js` - Component usage: `README.md` diff --git a/packages/ui-kit/README.md b/packages/ui-kit/README.md index fb7c7ae..5413190 100644 --- a/packages/ui-kit/README.md +++ b/packages/ui-kit/README.md @@ -91,7 +91,14 @@ import { Input } from '@ancore/ui-kit'; Container component for grouping related content. ```tsx -import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@ancore/ui-kit'; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, +} from '@ancore/ui-kit'; @@ -104,7 +111,7 @@ import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } - +; ``` #### Badge @@ -147,10 +154,11 @@ import { AmountInput } from '@ancore/ui-kit'; value={amount} onChange={(e) => setAmount(e.target.value)} error={error} -/> +/>; ``` **Props:** + - `balance?: string` - Current balance to display - `asset?: string` - Asset symbol (e.g., 'XLM', 'USDC') - `label?: string` - Label for the input (default: 'Amount') @@ -169,10 +177,11 @@ import { AddressDisplay } from '@ancore/ui-kit'; label="Wallet Address" copyable={true} truncate={6} -/> +/>; ``` **Props:** + - `address: string` - The address to display (required) - `label?: string` - Label for the address - `copyable?: boolean` - Show copy button (default: true) @@ -185,23 +194,28 @@ import { AddressDisplay } from '@ancore/ui-kit'; The component library uses CSS variables for theming, supporting both light and dark modes. #### Primary (Stellar Purple) + - Light: `hsl(262 83% 58%)` - Dark: `hsl(262 83% 58%)` #### Secondary + - Light: `hsl(210 40% 96.1%)` - Dark: `hsl(217.2 32.6% 17.5%)` #### Destructive + - Light: `hsl(0 84.2% 60.2%)` - Dark: `hsl(0 62.8% 30.6%)` ### Border Radius + - `lg`: `0.5rem` - `md`: `calc(0.5rem - 2px)` - `sm`: `calc(0.5rem - 4px)` ### Typography + Inherits from Tailwind's default typography scale. ## Dark Mode @@ -209,9 +223,7 @@ Inherits from Tailwind's default typography scale. Enable dark mode by adding the `dark` class to your root element: ```tsx - - {/* Your app */} - +{/* Your app */} ``` Or toggle dynamically: @@ -219,7 +231,7 @@ Or toggle dynamically: ```tsx function ThemeToggle() { const [isDark, setIsDark] = useState(false); - + useEffect(() => { if (isDark) { document.documentElement.classList.add('dark'); @@ -227,7 +239,7 @@ function ThemeToggle() { document.documentElement.classList.remove('dark'); } }, [isDark]); - + return ; } ``` @@ -267,10 +279,7 @@ Extend the Tailwind configuration in your app: // tailwind.config.js module.exports = { presets: [require('@ancore/ui-kit/tailwind.config.js')], - content: [ - './src/**/*.{ts,tsx}', - './node_modules/@ancore/ui-kit/dist/**/*.{js,mjs}', - ], + content: ['./src/**/*.{ts,tsx}', './node_modules/@ancore/ui-kit/dist/**/*.{js,mjs}'], // Your custom configuration }; ``` @@ -285,7 +294,7 @@ import { cn } from '@ancore/ui-kit'; +; ``` ## Contributing diff --git a/packages/ui-kit/eslint.config.cjs b/packages/ui-kit/eslint.config.cjs index e5bdbe2..db13e71 100644 --- a/packages/ui-kit/eslint.config.cjs +++ b/packages/ui-kit/eslint.config.cjs @@ -1,6 +1,7 @@ const js = require('@eslint/js'); const tseslint = require('@typescript-eslint/eslint-plugin'); const tsparser = require('@typescript-eslint/parser'); +const globals = require('globals'); const react = require('eslint-plugin-react'); const reactHooks = require('eslint-plugin-react-hooks'); @@ -17,6 +18,10 @@ module.exports = [ jsx: true, }, }, + globals: { + ...globals.browser, + ...globals.node, + }, }, plugins: { '@typescript-eslint': tseslint, @@ -28,6 +33,7 @@ module.exports = [ ...react.configs.recommended.rules, ...reactHooks.configs.recommended.rules, 'react/react-in-jsx-scope': 'off', // Not needed in React 18+ + 'react/prop-types': 'off', '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], }, settings: { @@ -36,4 +42,13 @@ module.exports = [ }, }, }, + { + files: ['**/*.stories.tsx'], + rules: { + 'react-hooks/rules-of-hooks': 'off', + }, + }, + { + ignores: ['dist/**', 'node_modules/**', '*.config.js', '*.config.cjs', '*.config.ts'], + }, ]; diff --git a/packages/ui-kit/src/components/address-display.stories.tsx b/packages/ui-kit/src/components/address-display.stories.tsx index 54cc222..efece2c 100644 --- a/packages/ui-kit/src/components/address-display.stories.tsx +++ b/packages/ui-kit/src/components/address-display.stories.tsx @@ -79,10 +79,7 @@ export const ShortAddress: Story = { export const MultipleAddresses: Story = { render: () => (
- + (

Account Information

- +
Balance: 100.50 XLM diff --git a/packages/ui-kit/src/components/address-display.test.tsx b/packages/ui-kit/src/components/address-display.test.tsx index 99337e7..8a95dbe 100644 --- a/packages/ui-kit/src/components/address-display.test.tsx +++ b/packages/ui-kit/src/components/address-display.test.tsx @@ -51,9 +51,9 @@ describe('AddressDisplay', () => { render(); const copyButton = screen.getByLabelText('Copy address'); - + await user.click(copyButton); - + await waitFor(() => { expect(writeTextSpy).toHaveBeenCalledWith(sampleAddress); }); diff --git a/packages/ui-kit/src/components/address-display.tsx b/packages/ui-kit/src/components/address-display.tsx index c648651..a6c3709 100644 --- a/packages/ui-kit/src/components/address-display.tsx +++ b/packages/ui-kit/src/components/address-display.tsx @@ -2,8 +2,7 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; import { Copy, Check } from 'lucide-react'; -export interface AddressDisplayProps - extends React.HTMLAttributes { +export interface AddressDisplayProps extends React.HTMLAttributes { /** * The address to display */ @@ -27,10 +26,7 @@ export interface AddressDisplayProps * Features truncation, copy-to-clipboard functionality, and responsive design */ const AddressDisplay = React.forwardRef( - ( - { address, copyable = true, truncate = 6, label, className, ...props }, - ref - ) => { + ({ address, copyable = true, truncate = 6, label, className, ...props }, ref) => { const [copied, setCopied] = React.useState(false); const displayAddress = React.useMemo(() => { @@ -68,11 +64,7 @@ const AddressDisplay = React.forwardRef( className="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-8 w-8" aria-label="Copy address" > - {copied ? ( - - ) : ( - - )} + {copied ? : } )}
diff --git a/packages/ui-kit/src/components/amount-input.stories.tsx b/packages/ui-kit/src/components/amount-input.stories.tsx index 8a1e2ce..1193d7e 100644 --- a/packages/ui-kit/src/components/amount-input.stories.tsx +++ b/packages/ui-kit/src/components/amount-input.stories.tsx @@ -23,11 +23,7 @@ export const Default: Story = { const [value, setValue] = useState(''); return (
- setValue(e.target.value)} - /> + setValue(e.target.value)} />
); }, @@ -42,11 +38,7 @@ export const WithValue: Story = { const [value, setValue] = useState('25.00'); return (
- setValue(e.target.value)} - /> + setValue(e.target.value)} />
); }, @@ -62,11 +54,7 @@ export const WithError: Story = { const [value, setValue] = useState('150.00'); return (
- setValue(e.target.value)} - /> + setValue(e.target.value)} />
); }, @@ -76,7 +64,7 @@ export const DifferentAssets: Story = { render: () => { const [xlmValue, setXlmValue] = useState(''); const [usdcValue, setUsdcValue] = useState(''); - + return (
- setValue(e.target.value)} - /> + setValue(e.target.value)} />
); }, @@ -128,11 +112,7 @@ export const Disabled: Story = { const [value, setValue] = useState('10.00'); return (
- setValue(e.target.value)} - /> + setValue(e.target.value)} />
); }, diff --git a/packages/ui-kit/src/components/amount-input.tsx b/packages/ui-kit/src/components/amount-input.tsx index c0250a5..cb2ee8a 100644 --- a/packages/ui-kit/src/components/amount-input.tsx +++ b/packages/ui-kit/src/components/amount-input.tsx @@ -3,8 +3,10 @@ import { Input } from './ui/input'; import { Badge } from './ui/badge'; import { cn } from '@/lib/utils'; -export interface AmountInputProps - extends Omit, 'type'> { +export interface AmountInputProps extends Omit< + React.InputHTMLAttributes, + 'type' +> { /** * Current balance to display */ @@ -28,17 +30,7 @@ export interface AmountInputProps * Displays balance, asset badge, and handles numeric input validation */ const AmountInput = React.forwardRef( - ( - { - balance, - asset = 'XLM', - error, - label = 'Amount', - className, - ...props - }, - ref - ) => { + ({ balance, asset = 'XLM', error, label = 'Amount', className, ...props }, ref) => { return (
@@ -47,22 +39,14 @@ const AmountInput = React.forwardRef( {asset}
- +
{balance && (

Balance: {balance} {asset}

)} - {error && ( -

{error}

- )} + {error &&

{error}

}
); diff --git a/packages/ui-kit/src/components/ui/badge.tsx b/packages/ui-kit/src/components/ui/badge.tsx index 2eb790a..cd82d8a 100644 --- a/packages/ui-kit/src/components/ui/badge.tsx +++ b/packages/ui-kit/src/components/ui/badge.tsx @@ -8,8 +8,7 @@ const badgeVariants = cva( { variants: { variant: { - default: - 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', + default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', destructive: @@ -24,13 +23,10 @@ const badgeVariants = cva( ); export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} + extends React.HTMLAttributes, VariantProps {} function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ); + return
; } export { Badge, badgeVariants }; diff --git a/packages/ui-kit/src/components/ui/button.tsx b/packages/ui-kit/src/components/ui/button.tsx index a279bc3..9947c75 100644 --- a/packages/ui-kit/src/components/ui/button.tsx +++ b/packages/ui-kit/src/components/ui/button.tsx @@ -10,12 +10,9 @@ const buttonVariants = cva( variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', - destructive: - 'bg-destructive text-destructive-foreground hover:bg-destructive/90', - outline: - 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', - secondary: - 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', }, @@ -34,8 +31,7 @@ const buttonVariants = cva( ); export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { + extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; } @@ -43,11 +39,7 @@ const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : 'button'; return ( - + ); } ); diff --git a/packages/ui-kit/src/components/ui/card.stories.tsx b/packages/ui-kit/src/components/ui/card.stories.tsx index bda9cfe..7d9c045 100644 --- a/packages/ui-kit/src/components/ui/card.stories.tsx +++ b/packages/ui-kit/src/components/ui/card.stories.tsx @@ -1,12 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from './card'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './card'; import { Button } from './button'; const meta = { @@ -55,9 +48,7 @@ export const WithFooter: Story = { Send Transaction - - Send XLM to another Stellar address - + Send XLM to another Stellar address

Transaction details go here

diff --git a/packages/ui-kit/src/components/ui/card.tsx b/packages/ui-kit/src/components/ui/card.tsx index 5997334..f3a70ba 100644 --- a/packages/ui-kit/src/components/ui/card.tsx +++ b/packages/ui-kit/src/components/ui/card.tsx @@ -2,78 +2,55 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); Card.displayName = 'Card'; -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); CardHeader.displayName = 'CardHeader'; -const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)); +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +); CardTitle.displayName = 'CardTitle'; const CardDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -

+

)); CardDescription.displayName = 'CardDescription'; -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)); +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); CardContent.displayName = 'CardContent'; -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); CardFooter.displayName = 'CardFooter'; export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/packages/ui-kit/src/components/ui/input.test.tsx b/packages/ui-kit/src/components/ui/input.test.tsx index 092a112..9322581 100644 --- a/packages/ui-kit/src/components/ui/input.test.tsx +++ b/packages/ui-kit/src/components/ui/input.test.tsx @@ -18,7 +18,7 @@ describe('Input', () => { const user = userEvent.setup(); render(); const input = screen.getByPlaceholderText('Type here'); - + await user.type(input, 'Hello'); expect(input).toHaveValue('Hello'); }); diff --git a/packages/ui-kit/src/components/ui/input.tsx b/packages/ui-kit/src/components/ui/input.tsx index c982112..7f269d4 100644 --- a/packages/ui-kit/src/components/ui/input.tsx +++ b/packages/ui-kit/src/components/ui/input.tsx @@ -2,8 +2,7 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; -export interface InputProps - extends React.InputHTMLAttributes {} +export type InputProps = React.InputHTMLAttributes; const Input = React.forwardRef( ({ className, type, ...props }, ref) => { diff --git a/packages/ui-kit/src/components/ui/separator.stories.tsx b/packages/ui-kit/src/components/ui/separator.stories.tsx index ddd29d4..c253270 100644 --- a/packages/ui-kit/src/components/ui/separator.stories.tsx +++ b/packages/ui-kit/src/components/ui/separator.stories.tsx @@ -18,9 +18,7 @@ export const Horizontal: Story = {

Wallet Info

-

- View your wallet details -

+

View your wallet details

diff --git a/packages/ui-kit/src/components/ui/separator.tsx b/packages/ui-kit/src/components/ui/separator.tsx index aca42e9..466b405 100644 --- a/packages/ui-kit/src/components/ui/separator.tsx +++ b/packages/ui-kit/src/components/ui/separator.tsx @@ -6,24 +6,19 @@ import { cn } from '@/lib/utils'; const Separator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->( - ( - { className, orientation = 'horizontal', decorative = true, ...props }, - ref - ) => ( - - ) -); +>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => ( + +)); Separator.displayName = SeparatorPrimitive.Root.displayName; export { Separator }; diff --git a/packages/ui-kit/tailwind.config.js b/packages/ui-kit/tailwind.config.js index dc0c29d..b1fa581 100644 --- a/packages/ui-kit/tailwind.config.js +++ b/packages/ui-kit/tailwind.config.js @@ -1,10 +1,7 @@ /** @type {import('tailwindcss').Config} */ module.exports = { darkMode: ['class'], - content: [ - './src/**/*.{ts,tsx}', - './src/components/**/*.{ts,tsx}', - ], + content: ['./src/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'], prefix: '', theme: { container: { diff --git a/packages/ui-kit/tsconfig.json b/packages/ui-kit/tsconfig.json index 0ff7102..3c3e1ec 100644 --- a/packages/ui-kit/tsconfig.json +++ b/packages/ui-kit/tsconfig.json @@ -10,9 +10,7 @@ "@/*": ["./src/*"] } }, - "include": [ - "src/**/*" - ], + "include": ["src/**/*"], "exclude": [ "node_modules", "dist", diff --git a/packages/ui-kit/tsup.config.ts b/packages/ui-kit/tsup.config.ts index b09a33a..88dfb80 100644 --- a/packages/ui-kit/tsup.config.ts +++ b/packages/ui-kit/tsup.config.ts @@ -14,4 +14,3 @@ export default defineConfig({ clean: true, external: ['react', 'react-dom'], }); - From 5c6d008451000b736b0b0abb031f34276f6b85f5 Mon Sep 17 00:00:00 2001 From: David-patrick-chuks Date: Wed, 25 Mar 2026 04:25:18 +0100 Subject: [PATCH 3/5] fix(extension-wallet): provide toasts in router harness --- apps/extension-wallet/src/router/index.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/extension-wallet/src/router/index.tsx b/apps/extension-wallet/src/router/index.tsx index 521d66a..11df158 100644 --- a/apps/extension-wallet/src/router/index.tsx +++ b/apps/extension-wallet/src/router/index.tsx @@ -20,6 +20,7 @@ import { Sparkles, Wallet, } from 'lucide-react'; +import { NotificationProvider } from '@ancore/ui-kit'; import { AuthGuard, ExtensionAuthProvider, PublicOnlyGuard, useExtensionAuth } from './AuthGuard'; import { NavBar } from '../components/Navigation/NavBar'; import { SettingsScreen } from '../screens/Settings/SettingsScreen'; @@ -524,9 +525,11 @@ export function ExtensionRouterContent() { export function ExtensionRouter() { return ( - - - + + + + + ); } @@ -534,9 +537,11 @@ export function ExtensionRouter() { export function ExtensionRouterTestHarness({ initialEntries }: { initialEntries: string[] }) { return ( - - - + + + + + ); } From d49f5bad4afdf8435709222f50f43fea59070dc1 Mon Sep 17 00:00:00 2001 From: David-patrick-chuks Date: Wed, 25 Mar 2026 04:27:30 +0100 Subject: [PATCH 4/5] fix(ui-kit): disable prop-types lint for ts components --- packages/ui-kit/eslint.config.cjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui-kit/eslint.config.cjs b/packages/ui-kit/eslint.config.cjs index 3afc2dc..e9a2960 100644 --- a/packages/ui-kit/eslint.config.cjs +++ b/packages/ui-kit/eslint.config.cjs @@ -32,6 +32,7 @@ module.exports = [ ...react.configs.recommended.rules, ...reactHooks.configs.recommended.rules, 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], '@typescript-eslint/no-empty-object-type': 'off', }, From 7b3d2d94299767d99003dbea0a09aa61638b7e9c Mon Sep 17 00:00:00 2001 From: David-patrick-chuks Date: Wed, 25 Mar 2026 04:30:19 +0100 Subject: [PATCH 5/5] style(ui-kit): format form components --- packages/ui-kit/src/components/Form/AddressInput.tsx | 1 - packages/ui-kit/src/components/Form/AmountInput.tsx | 1 - packages/ui-kit/src/components/Form/PasswordInput.tsx | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/ui-kit/src/components/Form/AddressInput.tsx b/packages/ui-kit/src/components/Form/AddressInput.tsx index b057a88..a2e096b 100644 --- a/packages/ui-kit/src/components/Form/AddressInput.tsx +++ b/packages/ui-kit/src/components/Form/AddressInput.tsx @@ -10,7 +10,6 @@ import { cn } from '@/lib/utils'; /** Safely attempt to read the react-hook-form context without throwing. */ function useOptionalFormContext() { try { - return useFormContext(); } catch { return null; diff --git a/packages/ui-kit/src/components/Form/AmountInput.tsx b/packages/ui-kit/src/components/Form/AmountInput.tsx index 0b3226c..3131803 100644 --- a/packages/ui-kit/src/components/Form/AmountInput.tsx +++ b/packages/ui-kit/src/components/Form/AmountInput.tsx @@ -10,7 +10,6 @@ import { cn } from '@/lib/utils'; function useOptionalFormContext() { try { - return useFormContext(); } catch { return null; diff --git a/packages/ui-kit/src/components/Form/PasswordInput.tsx b/packages/ui-kit/src/components/Form/PasswordInput.tsx index e739084..2cb5842 100644 --- a/packages/ui-kit/src/components/Form/PasswordInput.tsx +++ b/packages/ui-kit/src/components/Form/PasswordInput.tsx @@ -11,7 +11,6 @@ import { getPasswordStrength } from './validation'; function useOptionalFormContext() { try { - return useFormContext(); } catch { return null;