diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a433307 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,184 @@ +# Contributing to Solana Commerce Kit + +Thank you for your interest in contributing to Solana Commerce Kit. This document provides guidelines for contributing to the project. + +## Development Setup + +### Prerequisites +- Node.js 18 or higher +- pnpm 8 or higher + +### Getting Started + +1. Fork and clone the repository: +```bash +git clone https://github.com/your-username/commerce-kit.git +cd commerce-kit +``` + +2. Install dependencies: +```bash +pnpm install +``` + +3. Build all packages: +```bash +pnpm build +``` + +4. Run tests: +```bash +pnpm test +``` + +## Project Structure + +``` +commerce-kit/ +├── packages/ +│ ├── solana-commerce/ # Meta-package +│ ├── react/ # React components +│ ├── sdk/ # React hooks +│ ├── headless/ # Core logic +│ ├── connector/ # Wallet connection +│ └── solana-pay/ # Solana Pay protocol +└── ... +``` + +## Development Workflow + +### Working on a Package + +Navigate to the package directory and use the development scripts: + +```bash +cd packages/sdk +pnpm dev # Watch mode +pnpm test:watch # Test watch mode +pnpm type-check # Type checking +``` + +### Building + +Build individual packages: +```bash +cd packages/sdk +pnpm build +``` + +Build all packages: +```bash +pnpm build +``` + +### Testing + +Run tests for a specific package: +```bash +cd packages/sdk +pnpm test +``` + +Run all tests: +```bash +pnpm test +``` + +### Code Quality + +Before submitting a PR, ensure: +- All tests pass: `pnpm test` +- Code is formatted: `pnpm lint` (if configured) +- Code builds: `pnpm build` + +## Making Changes + +### Creating a Pull Request + +1. Create a new branch: +```bash +git checkout -b feature/your-feature-name +``` + +2. Make your changes and commit: +```bash +git add . +git commit -m "Description of changes" +``` + +3. Push to your fork: +```bash +git push origin feature/your-feature-name +``` + +4. Open a pull request on GitHub + +### Commit Messages + +Write clear, descriptive commit messages. + +### Code Style + +- Follow TypeScript best practices +- Use meaningful variable and function names +- Add JSDoc comments for public APIs +- Keep functions focused and composable + +## Package Guidelines + +### Adding New Features + +When adding features: +1. Add tests that cover the new functionality +2. Update relevant README files +3. Ensure TypeScript types are accurate +4. Consider backward compatibility + +### Breaking Changes + +If proposing breaking changes: +1. Clearly document the rationale +2. Provide migration path in PR description +3. Update version appropriately (major version bump) + +## Documentation + +When adding or modifying features: +- Update package README with examples +- Add JSDoc comments to new APIs +- Include TypeScript type examples + +## Testing + +### Writing Tests + +- Place tests in `__tests__` directories +- Use descriptive test names +- Test both success and error cases +- Mock external dependencies appropriately +- Utilize the test style of the existing tests + +Example: +```typescript +import { describe, it, expect } from 'vitest'; + +describe('functionName', () => { + it('should handle valid input correctly', () => { + // Test implementation + }); + + it('should throw error for invalid input', () => { + // Test implementation + }); +}); +``` + +## Questions and Help + +- Open an issue for bug reports or feature requests +- Use discussions for questions about usage +- Check existing issues before creating new ones + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/README.md b/README.md index e6f69f8..de64102 100644 --- a/README.md +++ b/README.md @@ -1,321 +1,203 @@ -# Commerce SDK +# Solana Commerce Kit + +Modern toolkit for building Solana commerce applications + + + +## Overview + +Solana Commerce Kit is a comprehensive TypeScript SDK for building e-commerce applications on Solana. It provides everything from low-level payment primitives to high-level React components, enabling developers to integrate payments, tips, and checkout flows with minimal configuration. + +Built on modern Solana libraries (@solana/kit, Wallet Standard) with a focus on type safety, developer experience, and production readiness. + +**Key Features:** +- Complete payment flows for tips, purchases, and cart checkout +- Production-ready React components with customizable theming +- Framework-agnostic commerce logic +- Wallet Standard integration for multi-wallet support +- Full Solana Pay protocol implementation +- TypeScript-first with comprehensive type definitions + +## Packages + +| Package | Description | Docs | +|---------|-------------|------| +| [@solana-commerce/solana-commerce](./) | All-in-one SDK with complete functionality | | +| [@solana-commerce/react](./packages/react) | React components for payments, tips, and checkout | [README](./packages/react/README.md) | +| [@solana-commerce/sdk](./packages/sdk) | Core React hooks for Solana development | [README](./packages/sdk/README.md) | +| [@solana-commerce/headless](./packages/headless) | Framework-agnostic commerce logic | [README](./packages/headless/README.md) | +| [@solana-commerce/connector](./packages/connector) | Wallet connection built on Wallet Standard | [README](./packages/connector/README.md) | +| [@solana-commerce/solana-pay](./packages/solana-pay) | Solana Pay protocol implementation | [README](./packages/solana-pay/README.md) | + +## Package Overview + +### @solana-commerce/solana-commerce +Meta-package that re-exports all functionality. Install this for complete access to the entire toolkit. + +### @solana-commerce/react +Complete UI components for commerce applications: +- PaymentButton with secure iframe architecture +- Tip modal with customizable amounts +- Wallet connection UI +- Transaction state management +- Customizable theming system + +### @solana-commerce/sdk +Type-safe React hooks for Solana development: +- Wallet management (`useWallet`, `useStandardWallets`) +- Account operations (`useBalance`, `useTransferSOL`) +- Token transfers (`useTransferToken`) +- RPC client access (`useArcClient`) + +### @solana-commerce/headless +Framework-agnostic commerce primitives: +- Payment flow logic +- Cart management +- Order processing +- Checkout calculations +- Type definitions + +### @solana-commerce/connector +Headless wallet connector with optional React support: +- Wallet Standard integration +- Multi-wallet detection and connection +- React provider and hooks +- Framework-agnostic core client + +### @solana-commerce/solana-pay +Complete Solana Pay protocol implementation: +- Payment URL creation and parsing +- QR code generation +- SOL and SPL token transfers +- Transaction building + +## Architecture -A comprehensive, production-ready Solana e-commerce SDK with full theming, multiple payment modes, and stablecoin support. - -## 🎯 Current Status: Production Ready - -✅ **Complete E-commerce Solution** - PaymentButton with tip/buyNow/cart modes -✅ **Comprehensive Theming System** - Colors, borders, fonts, custom styling -✅ **Stablecoin Support** - USDC, USDT, and SPL token integration -✅ **SSR-Safe Architecture** - Dialog-alpha system, no hydration issues -✅ **Professional Documentation** - Complete guides and API references -✅ **TypeScript Monorepo** - Full type safety across all packages - -## 🚀 Quick Start - -```bash -# Install dependencies -pnpm install - -# Start development (all packages + docs) -pnpm dev - -# Open documentation site -open http://localhost:3000 ``` - -### Simple Integration - -```tsx -import { PaymentButton } from '@solana-commerce/react'; - - { - console.log('Payment successful!', signature); - }} -/> -``` - -## 📦 Package Architecture - -``` -@solana-commerce/ -├── headless-sdk/ # Core payment logic, stablecoin support, utilities -├── react-sdk/ # PaymentButton, SolanaPayButton, examples -├── ui-primitives/ # Dialog-alpha system (SSR-safe, accessible) -└── docs/ # Complete documentation with interactive demos +commerce-kit/ # @solana-commerce/solana-commerce - all packages in one install +|---packages/ +│ ├── @solana-commerce/connector +│ ├── @solana-commerce/headless +│ ├── @solana-commerce/react +│ ├── @solana-commerce/sdk +│ └── @solana-commerce/solana-pay ``` -### Current Feature Set +**Choosing a Package:** +- Need everything? → `@solana-commerce/solana-commerce` +- Wallet connection? → `@solana-commerce/connector` +- Custom UI or non-React framework? → `@solana-commerce/headless` +- Building React app with UI? → `@solana-commerce/react` +- Need just hooks? → `@solana-commerce/sdk` +- Solana Pay protocol? → `@solana-commerce/solana-pay` -**✅ Complete E-commerce Components:** -- PaymentButton with multiple modes (tip/buyNow/cart) -- Product management with metadata support -- Merchant configuration and branding -- Custom trigger elements and positioning +## Usage Examples -**✅ Advanced Theming System:** -- Primary/secondary colors and backgrounds -- Border radius controls (none/sm/md/lg/xl) -- Font family customization -- Custom CSS style overrides +### E-commerce Checkout -**✅ Payment & Currency Support:** -- SOL native payments -- USDC/USDT stablecoin integration -- Configurable allowed mints -- Real Solana Pay URL generation - -**✅ Developer Experience:** -- Complete TypeScript definitions -- SSR-safe architecture (no hydration issues) -- Event handling for payment lifecycle -- Comprehensive documentation and examples - -## 🔧 Development - -### File Structure -``` -packages/ -├── headless-sdk/src/ -│ ├── index.ts # Enhanced payment requests & stablecoins -│ ├── blockchain-client.ts # Solana network integration -│ ├── payment-verification.ts # Transaction validation -│ └── transaction-builder.ts # Payment URL generation -├── react-sdk/src/ -│ ├── index.tsx # PaymentButton (main component) -│ ├── solana-pay-button.tsx # Simple tip button -│ └── examples.tsx # Usage examples for all modes -├── ui-primitives/src/ -│ ├── dialog-alpha/ # Modern dialog system -│ │ ├── dialog.tsx # Compound dialog component -│ │ ├── context.tsx # React context & state -│ │ ├── content.tsx # Modal content container -│ │ ├── backdrop.tsx # Modal backdrop/overlay -│ │ └── trigger.tsx # Dialog trigger element -│ └── react/index.tsx # Clean re-exports -└── apps/docs/ - ├── app/(home)/ # Interactive demo homepage - ├── content/docs/ # Comprehensive MDX guides - └── components/interactive-demo.tsx # Live PaymentButton demo -``` +```typescript +import { PaymentButton } from '@solana-commerce/react'; -### Key Commands -```bash -pnpm dev # Start all packages + docs with hot reload -pnpm build # Build all packages for production -pnpm clean # Clean build artifacts -cd apps/docs && pnpm dev # Documentation site only +function CheckoutButton() { + return ( + { + console.log('Payment successful!', signature); + }} + /> + ); +} ``` -## 🎯 Architecture Highlights - -### Dialog-Alpha System -**Problem Solved:** React re-rendering loops, SSR hydration mismatches, bundling issues +### Custom Integration with Hooks -**Solution:** Simplified, SSR-safe dialog system: ```typescript -// Clean, predictable state management -const [isOpen, setIsOpen] = useState(false); +import { ArcProvider, useWallet, useTransferSOL } from '@solana-commerce/sdk'; -// SSR-safe rendering with CSS visibility control - - {children} - +function CustomPayment() { + const { wallet, connect } = useWallet(); + const { transferSOL, isLoading } = useTransferSOL(); -// No createPortal bundling issues -// No useEffect hook order violations -// No server/client hydration mismatches -``` + const handlePayment = async () => { + if (!wallet) { + await connect(); + return; + } -### Commerce-First Design -```typescript -// Support for multiple e-commerce patterns -type CommerceMode = 'tip' | 'buyNow' | 'cart'; - -// Rich product metadata -interface Product { - id: string; - name: string; - description?: string; - price: number; // in lamports - currency?: string; // 'SOL' | mint address - image?: string; - metadata?: Record; -} + const { signature } = await transferSOL({ + to: 'merchant-address', + amount: BigInt(1_000_000_000) // 1 SOL + }); + + console.log('Payment sent:', signature); + }; -// Complete theming control -interface ThemeConfig { - primaryColor?: string; - backgroundColor?: string; - borderRadius?: 'none' | 'sm' | 'md' | 'lg' | 'xl'; - fontFamily?: string; + return ; } ``` -### Payment Flow Integration -```typescript -// Complete payment lifecycle events - {/* Show loading */}} - onPayment={(amount, currency, products) => {/* Track analytics */}} - onPaymentSuccess={(signature) => {/* Clear cart, redirect */}} - onPaymentError={(error) => {/* Handle failure */}} - onCancel={() => {/* Reset state */}} -/> -``` + -## 🎨 Current Demo - -Visit `http://localhost:3000` to see: -- **Interactive PaymentButton Demo** - Switch between tip/buyNow/cart modes -- **Live Theming Controls** - Customize colors, borders, positioning -- **Complete Documentation** - API references, guides, examples -- **Working Payment Flows** - Real Solana Pay URL generation -- **Stablecoin Integration** - USDC/USDT support examples - -## 🔧 Technology Stack - -- **Runtime:** Bun (3x faster than Node.js) -- **Monorepo:** Turborepo (cached parallel builds) -- **Frontend:** Next.js 15 + React 19 -- **Documentation:** Fumadocs (beautiful, fast docs) -- **UI:** Dialog-alpha system (SSR-safe, accessible) -- **Blockchain:** Solana Web3.js + Solana Pay standards -- **TypeScript:** Full type safety across all packages - -## 💡 Usage Examples - -### Tip/Donation Widget -```tsx - -``` +## Development -### Digital Product Sales -```tsx - -``` +### Prerequisites +- Node.js 18+ +- pnpm 8+ -### Multi-Product Cart -```tsx - +### Setup +```bash +git clone https://github.com/solana-foundation/commerce-kit.git +cd commerce-kit +pnpm install ``` -### Stablecoin Subscriptions -```tsx - +### Commands +```bash +pnpm build # Build all packages +pnpm test # Run all tests +pnpm dev # Watch mode for development +pnpm format # Format code +pnpm lint # Lint code ``` -## 🛠️ Team Development +### Working on Individual Packages -### Making Changes -1. **Edit source files** in `packages/*/src/` -2. **Changes auto-rebuild** and appear in docs instantly -3. **TypeScript errors** show in terminal immediately -4. **Test changes** at `http://localhost:3000` +Navigate to a package and use its scripts: -### Common Tasks ```bash -# Add new payment mode -packages/react-sdk/src/index.tsx # Update PaymentButton - -# Add new stablecoin support -packages/headless-sdk/src/index.ts # Update STABLECOINS config - -# Update theming options -packages/react-sdk/src/index.tsx # Update ThemeConfig interface - -# Add documentation -apps/docs/content/docs/new-guide.mdx - -# Test integration -# → Edit files → see changes at localhost:3000 +cd packages/sdk +pnpm dev # Watch mode +pnpm test:watch # Test watch mode ``` -### Debugging -- **Dialog issues:** Check dialog-alpha system, ensure hooks are called consistently -- **SSR problems:** Verify client-side only code in useEffect -- **Payment flow:** Check browser console for Solana Pay URLs -- **Hot reload not working:** Restart `pnpm dev` - -## 🎯 Production Deployment +## Documentation -The SDK is production-ready with: -- **Complete e-commerce features** (cart, products, payments) -- **Enterprise theming** (brand colors, custom styling) -- **Multi-currency support** (SOL, USDC, USDT, custom tokens) -- **SSR-safe architecture** (works with Next.js, Remix, etc.) -- **Comprehensive documentation** (guides, API references, examples) -- **TypeScript support** (full type safety) +Coming soon. -## 🌟 Community & Support +## Contributing -- **GitHub**: [Commerce SDK Repository](https://github.com/solana-commerce/react-sdk) -- **Documentation**: Complete guides at `localhost:3000` -- **Examples**: Live demos and code samples -- **Discord**: Join our developer community +Contributions are welcome. Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines. ---- +## License -**Building the future of Solana e-commerce with production-ready, developer-friendly tools!** 🚀 +MIT diff --git a/packages/connector/README.md b/packages/connector/README.md new file mode 100644 index 0000000..a034794 --- /dev/null +++ b/packages/connector/README.md @@ -0,0 +1,399 @@ +# @solana-commerce/connector + +Headless wallet connector built on Wallet Standard + + + +## Installation + +```bash +pnpm add @solana-commerce/connector +``` + +## Features + +- Wallet Standard integration +- Solana Kit and Commerce Kit integration +- Multi-wallet support and detection +- React provider and hooks +- Framework-agnostic core client +- Type-safe wallet interactions +- Auto-connect support +- Account change detection + +## Quick Start + +### Headless Usage + +```typescript +import { ConnectorClient } from '@solana-commerce/connector'; + +const connector = new ConnectorClient(); + +// Get current state (includes wallets list) +const state = connector.getConnectorState(); +console.log('Available wallets:', state.wallets); + +// Connect to a wallet by name +await connector.select('Phantom'); + +// Get updated state +const newState = connector.getConnectorState(); +console.log('Connected:', newState.connected); +console.log('Accounts:', newState.accounts); +``` + +### React Usage + +```typescript +import { ConnectorProvider, useConnector } from '@solana-commerce/connector'; + +function App() { + return ( + + + + ); +} + +function WalletComponent() { + const { wallets, select, disconnect, accounts, connected } = useConnector(); + + if (!connected) { + return ( +
+ {wallets.map(w => ( + + ))} +
+ ); + } + + return ( +
+

Connected: {accounts[0]?.address}

+ +
+ ); +} +``` + +## API + +### ConnectorClient + +Core headless client for wallet management. + +#### Constructor + +```typescript +import { ConnectorClient } from '@solana-commerce/connector'; + +const client = new ConnectorClient({ + autoConnect: true, // Auto-connect to last wallet + debug: false, // Enable debug logging + accountPollingIntervalMs: 1500, // Account polling interval + storage: window.localStorage // Custom storage (optional) +}); +``` + +#### Methods + +**`getConnectorState()`** + +Get current connector state including wallets, connection status, and accounts. + +```typescript +const state = client.getConnectorState(); + +// State structure: +// { +// wallets: WalletInfo[], // All detected wallets +// selectedWallet: Wallet | null, // Currently connected wallet +// connected: boolean, // Connection status +// connecting: boolean, // Connection in progress +// accounts: AccountInfo[], // Connected accounts +// selectedAccount: string | null // Currently selected account address +// } + +console.log('Available wallets:', state.wallets); +// [ +// { +// wallet: Wallet, // Wallet Standard object +// name: 'Phantom', +// icon: 'data:image/...', +// installed: true, +// connectable: true // Has required features +// }, +// ... +// ] +``` + +**`select(walletName)`** + +Connect to a wallet by name. + +```typescript +// List available wallets +const { wallets } = client.getConnectorState(); + +// Connect by name +await client.select('Phantom'); + +// Check connection +const { connected, accounts } = client.getConnectorState(); +if (connected) { + console.log('Connected to:', accounts[0].address); +} +``` + +**`disconnect()`** + +Disconnect from current wallet. + +```typescript +await client.disconnect(); + +const { connected } = client.getConnectorState(); +console.log('Connected:', connected); // false +``` + +**`selectAccount(address)`** + +Switch to a different account (for multi-account wallets). + +```typescript +const { accounts } = client.getConnectorState(); + +// Switch to second account +if (accounts.length > 1) { + await client.selectAccount(accounts[1].address); +} +``` + +**`subscribe(listener)`** + +Subscribe to state changes. + +```typescript +const unsubscribe = client.subscribe((state) => { + console.log('Wallets:', state.wallets.length); + console.log('Connected:', state.connected); + console.log('Accounts:', state.accounts.length); +}); + +// Later: cleanup +unsubscribe(); +``` + +**`destroy()`** + +Clean up all resources (event listeners, timers). + +```typescript +client.destroy(); +``` + +### React Hooks + +#### `useConnector()` + +Main hook for wallet interaction. Returns state plus action methods. + +```typescript +import { useConnector } from '@solana-commerce/connector'; + +function Component() { + const { + // State + wallets, // WalletInfo[] - Available wallets + selectedWallet, // Wallet | null - Connected wallet + accounts, // AccountInfo[] - Connected accounts + selectedAccount, // string | null - Selected account address + connected, // boolean - Connection status + connecting, // boolean - Connecting in progress + + // Actions + select, // (walletName: string) => Promise + disconnect, // () => Promise + selectAccount // (address: string) => Promise + } = useConnector(); + + return ( +
+ {!connected && wallets.map(w => ( + + ))} + + {connected && ( +
+

Address: {accounts[0]?.address}

+ {accounts.length > 1 && ( + + )} + +
+ )} +
+ ); +} +``` + +#### `useConnectorClient()` + +Access the underlying ConnectorClient instance. + +```typescript +import { useConnectorClient } from '@solana-commerce/connector'; + +function Component() { + const client = useConnectorClient(); + + const handleCustomAction = () => { + const state = client.getConnectorState(); + console.log('Current state:', state); + }; + + return ; +} +``` + +### React Provider + +#### `ConnectorProvider` + +Wrap your app with the connector provider. + +```typescript +import { ConnectorProvider } from '@solana-commerce/connector'; + +function App() { + return ( + + + + ); +} +``` + +### UI Components + +#### `WalletList` + +Pre-built wallet selection UI. + +```typescript +import { WalletList, useConnector } from '@solana-commerce/connector'; + +function WalletModal() { + const { wallets } = useConnector(); + + return ( + { + console.log('Selected:', wallet.name); + }} + /> + ); +} +``` + +#### `AccountDropdown` + +Pre-built account dropdown UI. + +```typescript +import { AccountDropdown } from '@solana-commerce/connector'; + +function MyComponent() { + return
; +} +``` + +## Configuration + +### Auto-Connect + +Automatically reconnect to the last used wallet on app load. + +```typescript + + + +``` + +### Custom Storage + +Use custom storage for persistence (useful for React Native or SSR). + +```typescript +const customStorage = { + getItem: (key: string) => AsyncStorage.getItem(key), + setItem: (key: string, value: string) => AsyncStorage.setItem(key, value), + removeItem: (key: string) => AsyncStorage.removeItem(key) +}; + + + + +``` + +### Account Polling + +Configure polling interval for account changes (when wallet doesn't support events). + +```typescript + + + +``` + +## Wallet Standard + +This connector implements the [Wallet Standard](https://github.com/wallet-standard/wallet-standard) specification, ensuring compatibility with all compliant wallets, e.g.: + +- Phantom +- Solflare +- Backpack +- Glow +- Brave Wallet +- Any Wallet Standard compatible wallet + +## Development + +```bash +pnpm install # Install dependencies +pnpm build # Build package +pnpm test # Run tests +pnpm test:watch # Run tests in watch mode +pnpm type-check # TypeScript validation +pnpm lint # Lint code +``` + +## License + +MIT diff --git a/packages/connector/src/__tests__/connector-client.test.ts b/packages/connector/src/__tests__/connector-client.test.ts index 57e89b9..95b5054 100644 --- a/packages/connector/src/__tests__/connector-client.test.ts +++ b/packages/connector/src/__tests__/connector-client.test.ts @@ -22,6 +22,9 @@ const mockWalletsApi = { off: vi.fn(), }; +// Type for mock wallet +type MockWallet = ReturnType; + // Mock wallet with Solana support const createMockWallet = (name: string, hasConnect = true, hasDisconnect = true, hasEvents = false) => ({ name, @@ -245,7 +248,7 @@ describe('ConnectorClient', () => { describe('Wallet Connection', () => { let client: ConnectorClient; - let mockWallet: any; + let mockWallet: MockWallet; beforeEach(() => { mockWallet = createMockWallet('Phantom'); @@ -265,7 +268,7 @@ describe('ConnectorClient', () => { }); it('should set connecting state during connection', async () => { - let resolveConnect: (value: any) => void; + let resolveConnect: ((value: { accounts: Array<{ address: string }> }) => void) | undefined; const connectPromise = new Promise(resolve => { resolveConnect = resolve; }); @@ -279,7 +282,7 @@ describe('ConnectorClient', () => { expect(client.getConnectorState().connected).toBe(false); // Resolve the connection - resolveConnect!({ + resolveConnect?.({ accounts: [ { address: TEST_ADDRESSES.PHANTOM_ACCOUNT_1, @@ -328,7 +331,7 @@ describe('ConnectorClient', () => { describe('Wallet Disconnection', () => { let client: ConnectorClient; - let mockWallet: any; + let mockWallet: MockWallet; beforeEach(async () => { mockWallet = createMockWallet('Phantom'); @@ -377,7 +380,7 @@ describe('ConnectorClient', () => { describe('Account Management', () => { let client: ConnectorClient; - let mockWallet: any; + let mockWallet: MockWallet; beforeEach(async () => { mockWallet = createMockWallet('Phantom'); @@ -575,9 +578,9 @@ describe('ConnectorClient', () => { }); it('should handle wallet change events', async () => { - let changeCallback: (properties: any) => void = () => {}; + let changeCallback: (properties: { accounts: Array<{ address: string }> }) => void = () => {}; const mockWallet = createMockWallet('Phantom', true, true, true); - mockWallet.features['standard:events']?.on.mockImplementation((event: string, callback: any) => { + mockWallet.features['standard:events']?.on.mockImplementation((event: string, callback: (properties: { accounts: Array<{ address: string }> }) => void) => { if (event === 'change') { changeCallback = callback; } @@ -659,7 +662,7 @@ describe('ConnectorClient', () => { describe('Cleanup', () => { it('should cleanup resources when destroyed', async () => { const mockWallet = createMockWallet('Phantom', true, true, true); - let eventUnsubscribe = vi.fn(); + const eventUnsubscribe = vi.fn(); mockWallet.features['standard:events'].on.mockReturnValue(eventUnsubscribe); mockWalletsApi.get.mockReturnValue([mockWallet]); diff --git a/packages/headless/README.md b/packages/headless/README.md new file mode 100644 index 0000000..2af80f5 --- /dev/null +++ b/packages/headless/README.md @@ -0,0 +1,133 @@ +# @solana-commerce/headless + +Framework-agnostic commerce primitives for Solana + + + +## Installation + +```bash +pnpm add @solana-commerce/headless +``` + +## Features + +- Framework-agnostic commerce logic +- Type-safe payment flows +- Cart and order management +- Payment verification on-chain +- Solana Pay URL generation (and QR code generation with customizable styling) +- No UI dependencies + +## Quick Start + +```typescript +import { createCommercePaymentRequest, createCartRequest } from '@solana-commerce/headless'; + +// Create a payment request +const payment = createCommercePaymentRequest({ + recipient: 'merchant-wallet-address', + amount: 10000000, // 0.01 SOL in lamports + currency: 'SOL', + label: 'Store Purchase', + message: 'Thank you for your order!' +}); + +// Create a cart checkout +const cart = createCartRequest( + 'merchant-wallet-address', + [ + { id: '1', name: 'Product A', price: 5000000 }, + { id: '2', name: 'Product B', price: 10000000 } + ], + { + currency: 'SOL', + label: 'Cart Checkout' + } +); +``` + +## API + +### Payment Functions + +- **`createCommercePaymentRequest(request)`** - Create a commerce payment request with Solana Pay URL +- **`verifyPayment(rpc, signature, expectedAmount?, expectedRecipient?, expectedMint?)`** - Verify a payment transaction on-chain +- **`waitForConfirmation(rpc, signature, timeoutMs?)`** - Wait for transaction confirmation + +### Solana Pay Functions + +- **`createSolanaPayRequest(request, options)`** - Create a Solana Pay request with styled QR code +- **`createQRPaymentRequest(recipientAddress, amount, tokenAddress?, options?)`** - Helper to create payment request with QR code from string inputs + +### Cart Functions + +- **`createCartRequest(recipient, products, options?)`** - Create a cart checkout request with multiple products + +### Tip Functions + +- **`createTipRequest(recipient, amount, options?)`** - Create a tip/donation request + +### Buy Now Functions + +- **`createBuyNowRequest(recipient, product, options?)`** - Create a single product purchase request + +## Example + +```typescript +import { + createCartRequest, + createCommercePaymentRequest, + verifyPayment +} from '@solana-commerce/headless'; +import { createSolanaClient } from 'gill'; + +// 1. Create cart +const cart = createCartRequest( + 'merchant-wallet', + [ + { id: '1', name: 'Product A', price: 50000000 }, + { id: '2', name: 'Product B', price: 30000000 } + ], + { currency: 'USDC', label: 'My Store' } +); + +// 2. Generate payment request +const payment = createCommercePaymentRequest({ + recipient: cart.recipient, + amount: cart.amount, + currency: cart.currency, + items: cart.products, + label: cart.label, + message: cart.message +}); + +// 3. Display payment.url as QR code or link + +// 4. After user pays, verify transaction +const client = createSolanaClient(/*...*/); +const verification = await verifyPayment( + client.rpc, + receivedSignature, + payment.amount, + payment.recipient +); + +if (verification.verified) { + // Process order + console.log('Order confirmed!'); +} +``` + +## Development + +```bash +pnpm install # Install dependencies +pnpm build # Build package +pnpm test # Run tests +pnpm test:watch # Test watch mode +``` + +## License + +MIT diff --git a/packages/react/README.md b/packages/react/README.md index ea65343..6c0c13d 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -1,72 +1,160 @@ # @solana-commerce/react -## Server-Side RPC URL Resolution +React SDK for Solana commerce applications -For better security and performance, RPC URLs are now resolved server-side: + -### 🎯 Benefits -- **🔒 Security**: Keep RPC API keys out of client bundle -- **⚡ Performance**: Server-side connection pooling and caching -- **🛡️ Rate Limiting**: Centralized rate limit management -- **🔧 Configuration**: Environment-based RPC selection +## Installation -### 🚀 Usage - -#### Option 1: Environment Variables (Recommended) ```bash -# .env.local -SOLANA_RPC_MAINNET=https://your-premium-rpc.com -SOLANA_RPC_DEVNET=https://your-dev-rpc.com -SOLANA_RPC_URL=https://fallback-rpc.com +pnpm add @solana-commerce/react ``` -#### Option 2: Explicit RPC URL +## Features + + + +- Complete payment UI components +- PaymentButton with multiple modes (tip, payment, cart) +- Secure iframe architecture +- Customizable theming system +- Server-side RPC URL resolution +- Transaction state management +- Wallet integration built-in +- TypeScript support +- Native support for USDC and USDT (mainnet and devnet) + +## Components + +### Core Components +- **PaymentButton** - Main payment component supporting tip, payment, and cart modes +- **TransactionSuccess** - Success state UI component +- **TransactionError** - Error state UI component +- Many UI Primitives + +### Hooks +- **useSolanaPay** - Solana Pay integration +- **useSolEquivalent** - Token to SOL conversion +- **useTipForm** - Tip form state management +- **usePaymentStatus** - Payment status tracking +- Several utility hooks for UI state management + +## API + +### PaymentButton + ```typescript - void + onPayment?: (amount: number, currency: string) => void + onPaymentSuccess?: (signature: string) => void + onPaymentError?: (error: Error) => void + onCancel?: () => void /> ``` -#### Option 3: API Route (Next.js) -Create `/pages/api/rpc-endpoints.ts` or `/app/api/rpc-endpoints/route.ts`: +### Configuration + +**Theme** ```typescript -import { POST } from '@solana-commerce/react/api/rpc-endpoints'; -export { POST }; +theme: { + primaryColor?: string; + backgroundColor?: string; + borderRadius?: 'none' | 'sm' | 'md' | 'lg' | 'xl'; + fontFamily?: string; +} ``` -### 🏗️ Architecture +## Examples -``` -┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ -│ Server │ │ PaymentButton │ │ SecureIframe │ -│ │ │ │ │ │ -│ ┌─────────────┐ │ │ ┌──────────────┐ │ │ ┌─────────────┐ │ -│ │ ENV vars │ │───▶│ │ RPC Resolver │ │───▶│ │ ArcProvider │ │ -│ │ • MAINNET │ │ │ │ │ │ │ │ │ │ -│ │ • DEVNET │ │ │ └──────────────┘ │ │ └─────────────┘ │ -│ │ • TESTNET │ │ │ │ │ │ -│ └─────────────┘ │ └──────────────────┘ └─────────────────┘ -└─────────────────┘ +### Simple Payment + +```typescript + { + console.log('Payment confirmed:', signature); + }} +/> ``` -### 🔄 Migration from Client-Side +### Tip Widget -**Before (❌ Client-side):** ```typescript -// RPC URL constructed in browser -const rpcUrl = config.rpcUrl || `https://api.${network}.solana.com`; + { + console.log('Thanks for the tip!'); + }} +/> ``` -**After (✅ Server-side):** +### Shopping Cart + ```typescript -// RPC URL resolved server-side before client creation -const resolvedUrl = await fetchRpcUrl({ network, priority: 'reliable' }); +function Cart() { + const [items, setItems] = useState([...]); + + return ( + { + setItems([]); + alert('Order placed!'); + }} + /> + ); +} +``` + +## Development + +```bash +pnpm install # Install dependencies +pnpm build # Build package +pnpm build:sdk # Build SDK package +pnpm build:iframe # Build iframe package +pnpm type-check # Type check +pnpm dev # Watch mode +pnpm test # Run tests +pnpm test:watch # Test watch mode +pnpm test:coverage # Coverage report ``` -### 🧪 Development +## License -The system gracefully falls back to public endpoints when server resolution fails, making development seamless while providing production benefits. \ No newline at end of file +MIT diff --git a/packages/react/src/components/ui/drawer-shell.tsx b/packages/react/src/components/ui/drawer-shell.tsx index 0eaefc3..69b97da 100644 --- a/packages/react/src/components/ui/drawer-shell.tsx +++ b/packages/react/src/components/ui/drawer-shell.tsx @@ -29,3 +29,4 @@ export function DrawerShell({ open, onOpenChange, trigger, children }: DrawerShe ); } + diff --git a/packages/react/src/components/ui/responsive-shell.tsx b/packages/react/src/components/ui/responsive-shell.tsx index 977ef39..66c401e 100644 --- a/packages/react/src/components/ui/responsive-shell.tsx +++ b/packages/react/src/components/ui/responsive-shell.tsx @@ -34,3 +34,4 @@ export function ResponsiveShell({ open, onOpenChange, trigger, children }: Respo ); } + diff --git a/packages/react/src/hooks/use-mobile-detection.ts b/packages/react/src/hooks/use-mobile-detection.ts index b420c2b..ce596ef 100644 --- a/packages/react/src/hooks/use-mobile-detection.ts +++ b/packages/react/src/hooks/use-mobile-detection.ts @@ -102,3 +102,4 @@ export function useMobileDetectionDetailed() { return state; } + diff --git a/packages/react/src/styles/components/drawer.css b/packages/react/src/styles/components/drawer.css index 8020982..6c0fbb0 100644 --- a/packages/react/src/styles/components/drawer.css +++ b/packages/react/src/styles/components/drawer.css @@ -96,3 +96,4 @@ transition: none !important; } } + diff --git a/packages/react/src/ui-primitives/drawer-alpha/close.tsx b/packages/react/src/ui-primitives/drawer-alpha/close.tsx index 230a3c6..617acd0 100644 --- a/packages/react/src/ui-primitives/drawer-alpha/close.tsx +++ b/packages/react/src/ui-primitives/drawer-alpha/close.tsx @@ -32,3 +32,4 @@ export function DrawerClose({ asChild, children, className, style }: DrawerClose ); } + diff --git a/packages/react/src/ui-primitives/drawer-alpha/context.tsx b/packages/react/src/ui-primitives/drawer-alpha/context.tsx index cea5174..d9825cc 100644 --- a/packages/react/src/ui-primitives/drawer-alpha/context.tsx +++ b/packages/react/src/ui-primitives/drawer-alpha/context.tsx @@ -10,3 +10,4 @@ export function useDrawer(): DrawerContextValue { } return context; } + diff --git a/packages/react/src/ui-primitives/drawer-alpha/drawer.tsx b/packages/react/src/ui-primitives/drawer-alpha/drawer.tsx index f9993a5..15bc360 100644 --- a/packages/react/src/ui-primitives/drawer-alpha/drawer.tsx +++ b/packages/react/src/ui-primitives/drawer-alpha/drawer.tsx @@ -7,3 +7,4 @@ import type { DrawerRootProps } from './types'; export function Drawer(props: DrawerRootProps) { return ; } + diff --git a/packages/react/src/ui-primitives/drawer-alpha/handle.tsx b/packages/react/src/ui-primitives/drawer-alpha/handle.tsx index 34ac35f..21e01b4 100644 --- a/packages/react/src/ui-primitives/drawer-alpha/handle.tsx +++ b/packages/react/src/ui-primitives/drawer-alpha/handle.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import type { DrawerHandleProps } from './types'; export function DrawerHandle({ className, style }: DrawerHandleProps) { @@ -18,3 +17,4 @@ export function DrawerHandle({ className, style }: DrawerHandleProps) { /> ); } + diff --git a/packages/react/src/ui-primitives/drawer-alpha/index.ts b/packages/react/src/ui-primitives/drawer-alpha/index.ts index 8ccd70a..e900caa 100644 --- a/packages/react/src/ui-primitives/drawer-alpha/index.ts +++ b/packages/react/src/ui-primitives/drawer-alpha/index.ts @@ -8,3 +8,4 @@ export { DrawerHandle } from './handle'; export { DrawerClose } from './close'; export { useDrawer } from './context'; export type * from './types'; + diff --git a/packages/react/src/ui-primitives/drawer-alpha/portal.tsx b/packages/react/src/ui-primitives/drawer-alpha/portal.tsx index 26cd7b8..48e729c 100644 --- a/packages/react/src/ui-primitives/drawer-alpha/portal.tsx +++ b/packages/react/src/ui-primitives/drawer-alpha/portal.tsx @@ -16,3 +16,4 @@ export function DrawerPortal({ children }: DrawerPortalProps) { return createPortal(children, document.body); } + diff --git a/packages/react/src/ui-primitives/drawer-alpha/root.tsx b/packages/react/src/ui-primitives/drawer-alpha/root.tsx index c0bf0ae..ffa42a2 100644 --- a/packages/react/src/ui-primitives/drawer-alpha/root.tsx +++ b/packages/react/src/ui-primitives/drawer-alpha/root.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useMemo } from 'react'; import { DrawerContext } from './context'; import type { DrawerRootProps } from './types'; @@ -27,3 +27,4 @@ export function DrawerRoot({ open, onOpenChange, children }: DrawerRootProps) { return {children}; } + diff --git a/packages/react/src/ui-primitives/drawer-alpha/trigger.tsx b/packages/react/src/ui-primitives/drawer-alpha/trigger.tsx index 1dc6224..61727b4 100644 --- a/packages/react/src/ui-primitives/drawer-alpha/trigger.tsx +++ b/packages/react/src/ui-primitives/drawer-alpha/trigger.tsx @@ -26,3 +26,4 @@ export function DrawerTrigger({ asChild, children, className, style }: DrawerTri ); } + diff --git a/packages/react/src/ui-primitives/drawer-alpha/types.ts b/packages/react/src/ui-primitives/drawer-alpha/types.ts index 8f2b93f..6e4aa4f 100644 --- a/packages/react/src/ui-primitives/drawer-alpha/types.ts +++ b/packages/react/src/ui-primitives/drawer-alpha/types.ts @@ -41,3 +41,4 @@ export interface DrawerHandleProps { className?: string; style?: React.CSSProperties; } + diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 0018c8f..0e3ee91 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -1,94 +1,61 @@ # @solana-commerce/sdk -**Modern React hooks for Solana development** - Type-safe, progressive complexity, built on Solana Kit 2.0 +Modern React hooks for Solana development - Type-safe, progressive complexity, built on Solana Kit -## 📦 Installation + + +## Installation ```bash -npm install @solana-commerce/sdk -# or -yarn add @solana-commerce/sdk -# or pnpm add @solana-commerce/sdk ``` -## 🚀 Quick Start +## Quick Start ```typescript -import { ArcProvider, useBalance, useWallet } from '@solana-commerce/sdk'; +import { ArcProvider, useTransferSOL } from '@solana-commerce/sdk'; function App() { return ( - + ); } -function WalletComponent() { - const { wallet, connect } = useWallet(); - const { balance } = useBalance(); +function TransferComponent() { + const { transferSOL } = useTransferSOL(); - return
Balance: {balance} SOL
; + return ; } ``` -## 📁 Package Exports +## Features -This package provides two import paths: +- Solana React Provider with automatic RPC client management +- Solana Kit/Gill-based Solana Hooks for common operations -### **Default Import** - Complete SDK +### Arc Provider ```typescript -import { ArcProvider, useBalance, useTransferSOL } from '@solana-commerce/sdk'; -``` - -- **Use When**: Building full-featured Solana apps -- **Includes**: All hooks, providers, and utilities - -### **`/react`** - React Hooks Only - -```typescript -import { useBalance, useWallet } from '@solana-commerce/sdk/react'; + + + ``` -- **Use When**: Building React apps with Solana -- **Includes**: All React hooks and providers - -## 🔧 Key Features - -- **🎯 Type Safety**: Built on Solana Kit 2.0 with full TypeScript support -- **⚡ Performance**: Optimized re-renders and intelligent caching -- **🌐 Context-Based**: No prop drilling, automatic state coordination -- **🚀 Modern Standards**: Wallet Standard compatible -- **🔌 Flexible**: Works with any RPC provider +### Hooks -## 📚 Core Hooks +- **useArcClient()** - Access RPC client and configuration +- **useTransferSOL()** - Transfer SOL with automatic retry +- **useTransferToken()** - Transfer SPL tokens with automatic retry +- **useStandardWallets()** - Wallet Standard integration -### Wallet Management -- `useWallet()` - Wallet connection and state -- `useStandardWallets()` - Wallet Standard integration - -### Account Operations -- `useBalance()` - Account balance monitoring -- `useTransferSOL()` - SOL transfers -- `useTransferToken()` - SPL token transfers - -### Network & Configuration -- `useArcClient()` - RPC client access -- Network utilities and cluster management - -## 🎯 Usage Patterns - -### Basic Balance Display -```typescript -function BalanceDisplay() { - const { balance, isLoading } = useBalance(); - - if (isLoading) return
Loading...
; - return
{balance} SOL
; -} -``` +## Examples ### SOL Transfer ```typescript @@ -115,24 +82,8 @@ function SendSOL() { } ``` -### Token Transfer -```typescript -function SendToken() { - const { transferToken } = useTransferToken(); - - const handleTransfer = async () => { - await transferToken({ - mint: 'token-mint-address', - to: 'recipient-address', - amount: BigInt(1_000_000) // Amount in token's minor units - }); - }; - - return ; -} -``` -## ⚙️ Configuration +## Configuration ### ArcProvider Setup ```typescript @@ -140,9 +91,9 @@ function App() { return ( @@ -151,64 +102,16 @@ function App() { } ``` -### Custom RPC Configuration -```typescript - - - -``` - -## 🔗 Integration with Commerce Kit - -This package is designed to work seamlessly with other `@solana-commerce` packages: - -```typescript -import { PaymentButton } from '@solana-commerce/react'; -import { ArcProvider } from '@solana-commerce/sdk'; - -function CommerceApp() { - return ( - - - - ); -} -``` - -## 🛠️ Development +## Development ```bash -# Install dependencies -pnpm install - -# Run tests -pnpm test - -# Build package -pnpm build - -# Type checking -pnpm type-check +pnpm build # Build package +pnpm dev # Watch mode +pnpm type-check # Type check +pnpm test # Run tests +pnpm lint # Lint code ``` -## 🤝 Related Packages - -- **[@solana-commerce/react](../react)** - Complete commerce UI components -- **[@solana-commerce/headless](../headless)** - Headless commerce logic -- **[@solana-commerce/connector](../connector)** - Wallet connection utilities -- **[@solana-commerce/solana-pay](../solana-pay)** - Solana Pay implementation - -## 📄 License +## License MIT \ No newline at end of file diff --git a/packages/sdk/src/__tests__/integration/core-integration.test.ts b/packages/sdk/src/__tests__/integration/core-integration.test.ts index f22a8e7..808f8f2 100644 --- a/packages/sdk/src/__tests__/integration/core-integration.test.ts +++ b/packages/sdk/src/__tests__/integration/core-integration.test.ts @@ -62,12 +62,12 @@ describe('Core Integration Tests', () => { it('should provide consistent network configurations', () => { const networks = ['devnet', 'mainnet', 'testnet']; - networks.forEach(network => { + for (const network of networks) { const clusterInfo = getClusterInfo(network); expect(clusterInfo.name).toBe(network); expect(clusterInfo.rpcUrl).toContain(network === 'mainnet' ? 'mainnet-beta' : network); expect(clusterInfo.wsUrl).toContain(network === 'mainnet' ? 'mainnet-beta' : network); - }); + } }); it('should handle network detection flags correctly', () => { @@ -123,12 +123,12 @@ describe('Core Integration Tests', () => { it('should validate cluster configuration consistency', () => { const networks = ['devnet', 'mainnet', 'testnet']; - networks.forEach(network => { + for (const network of networks) { const clusterInfo = getClusterInfo(network); expect(clusterInfo.name).toBe(network); expect(clusterInfo.rpcUrl).toContain(network === 'mainnet' ? 'mainnet-beta' : network); expect(clusterInfo.wsUrl).toContain(network === 'mainnet' ? 'mainnet-beta' : network); - }); + } }); it('should maintain consistent boolean flags', () => { diff --git a/packages/sdk/src/__tests__/setup.ts b/packages/sdk/src/__tests__/setup.ts index 3ee8557..239f9b0 100644 --- a/packages/sdk/src/__tests__/setup.ts +++ b/packages/sdk/src/__tests__/setup.ts @@ -15,16 +15,16 @@ global.WebSocket = vi.fn().mockImplementation(() => ({ send: vi.fn(), close: vi.fn(), readyState: WebSocket.OPEN, -})) as any; +})) as unknown as typeof WebSocket; // Mock fetch for RPC calls with proper responses -global.fetch = vi.fn().mockImplementation(async (url: string, options?: any) => { +global.fetch = vi.fn().mockImplementation(async (url: string, options?: RequestInit) => { // Mock common RPC responses - const body = options?.body ? JSON.parse(options.body) : {}; + const body = options?.body ? JSON.parse(options.body as string) : {}; const method = body.method || 'unknown'; // Return appropriate mock responses based on method - let result: any = null; + let result: unknown = null; switch (method) { case 'getLatestBlockhash': @@ -96,7 +96,7 @@ const mockDigest = vi.fn().mockImplementation(async (algorithm: string, data: Ar Object.defineProperty(global, 'crypto', { value: { - getRandomValues: vi.fn((arr: any) => { + getRandomValues: vi.fn((arr: Uint8Array) => { for (let i = 0; i < arr.length; i++) { arr[i] = Math.floor(Math.random() * 256); } @@ -114,7 +114,7 @@ Object.defineProperty(global, 'crypto', { // Create a shared mock RPC implementation that can be used across all tests const createMockRpc = () => ({ - sendRequest: vi.fn().mockImplementation(async (method: string, params?: any) => { + sendRequest: vi.fn().mockImplementation(async (method: string, params?: unknown[]) => { switch (method) { case 'getLatestBlockhash': return { @@ -127,10 +127,10 @@ const createMockRpc = () => ({ }; case 'sendTransaction': return 'mock-signature-12345'; - case 'getAccountInfo': + case 'getAccountInfo': { // Return null for non-existent accounts (for error testing) const address = params?.[0]; - if (address === 'MISSING_ACCOUNT' || address?.includes('missing')) { + if (address === 'MISSING_ACCOUNT' || (typeof address === 'string' && address?.includes('missing'))) { return { value: null }; } // Return proper RPC response format with value wrapper @@ -143,6 +143,7 @@ const createMockRpc = () => ({ rentEpoch: 200, }, }; + } case 'getSignatureStatuses': return { value: [{ confirmationStatus: 'confirmed', err: null }], @@ -150,7 +151,7 @@ const createMockRpc = () => ({ case 'getMultipleAccounts': return { value: - params?.[0]?.map(() => ({ + (params?.[0] as unknown[] | undefined)?.map(() => ({ data: ['', 'base64'], executable: false, lamports: 1000000000, @@ -181,7 +182,7 @@ vi.mock('../core/rpc-manager', () => ({ })); // Mock the ArcClientProvider and useArcClient hook -vi.mock('../core/arc-client-provider', () => ({ +vi.mock('../core/commerce-client-provider', () => ({ ArcClientProvider: ({ children }: { children: React.ReactNode }) => children, useArcClient: vi.fn(() => ({ network: { @@ -213,7 +214,7 @@ vi.mock('../core/arc-client-provider', () => ({ // Mock Solana address generation to prevent WebCrypto issues vi.mock('@solana-program/token', async importOriginal => { - const actual = (await importOriginal()) as any; + const actual = (await importOriginal()) as Record; return { ...actual, findAssociatedTokenPda: vi.fn().mockResolvedValue([ @@ -225,7 +226,7 @@ vi.mock('@solana-program/token', async importOriginal => { // Mock @solana/kit address functions vi.mock('@solana/kit', async importOriginal => { - const actual = (await importOriginal()) as any; + const actual = (await importOriginal()) as Record; return { ...actual, address: vi.fn((addr: string) => addr), @@ -239,5 +240,5 @@ vi.mock('@solana/kit', async importOriginal => { // Export mock utilities export const mockLocalStorage = localStorageMock; -export const mockFetch = global.fetch as any; -export const mockWebSocket = global.WebSocket as any; +export const mockFetch = global.fetch; +export const mockWebSocket = global.WebSocket; diff --git a/packages/sdk/src/__tests__/utils/sol-conversion.test.ts b/packages/sdk/src/__tests__/utils/sol-conversion.test.ts index 460aaa9..e0863de 100644 --- a/packages/sdk/src/__tests__/utils/sol-conversion.test.ts +++ b/packages/sdk/src/__tests__/utils/sol-conversion.test.ts @@ -5,9 +5,9 @@ import { describe, it, expect } from 'vitest'; /** * Extract and test the SOL to lamports conversion logic - * This is the same function from useTransferSOL but exported for testing + * This is the same function from useTransferSOL but copied here for testing */ -export function convertSOLToLamports(solAmount: string): bigint { +function convertSOLToLamports(solAmount: string): bigint { // Validation: empty or whitespace if (!solAmount || !solAmount.trim()) { throw new Error('Amount cannot be empty'); diff --git a/packages/sdk/src/core/__tests__/arc-provider.test.tsx b/packages/sdk/src/core/__tests__/arc-provider.test.tsx index 22e4159..8041dd0 100644 --- a/packages/sdk/src/core/__tests__/arc-provider.test.tsx +++ b/packages/sdk/src/core/__tests__/arc-provider.test.tsx @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; -import { ArcProvider } from '../arc-provider'; -import { useArcClient } from '../arc-client-provider'; +import { ArcProvider } from '../commerce-provider'; +import { useArcClient } from '../commerce-client-provider'; // Mock connector dependency vi.mock('@solana-commerce/connector', () => ({ diff --git a/packages/sdk/src/core/arc-client-provider.tsx b/packages/sdk/src/core/commerce-client-provider.tsx similarity index 97% rename from packages/sdk/src/core/arc-client-provider.tsx rename to packages/sdk/src/core/commerce-client-provider.tsx index 67d98d5..a0f20a8 100644 --- a/packages/sdk/src/core/arc-client-provider.tsx +++ b/packages/sdk/src/core/commerce-client-provider.tsx @@ -10,7 +10,7 @@ import React, { type ReactNode, } from 'react'; import { QueryClient, QueryClientProvider, type QueryClient as RQClient } from '@tanstack/react-query'; -import { ArcWebClient, type ArcWebClientConfig } from './arc-web-client'; +import { ArcWebClient, type ArcWebClientConfig } from './web-client'; import type { Address } from '@solana/kit'; // The context now only holds the client instance. diff --git a/packages/sdk/src/core/arc-provider.tsx b/packages/sdk/src/core/commerce-provider.tsx similarity index 94% rename from packages/sdk/src/core/arc-provider.tsx rename to packages/sdk/src/core/commerce-provider.tsx index 6223eac..a1c6bb7 100644 --- a/packages/sdk/src/core/arc-provider.tsx +++ b/packages/sdk/src/core/commerce-provider.tsx @@ -2,9 +2,9 @@ import React, { useMemo } from 'react'; import type { ReactNode } from 'react'; -import { ArcClientProvider, useArcClient } from './arc-client-provider'; +import { ArcClientProvider, useArcClient } from './commerce-client-provider'; import { useConnectorClient } from '@solana-commerce/connector'; -import type { ArcWebClientConfig } from './arc-web-client'; +import type { ArcWebClientConfig } from './web-client'; import type { QueryClient } from '@tanstack/react-query'; export type ArcProviderProps = { diff --git a/packages/sdk/src/core/error-handler.ts b/packages/sdk/src/core/error-handler.ts index 9962525..e5be3b1 100644 --- a/packages/sdk/src/core/error-handler.ts +++ b/packages/sdk/src/core/error-handler.ts @@ -6,7 +6,7 @@ * transaction errors, and wallet issues with intelligent retry mechanisms. */ -import { type Address } from '@solana/kit'; +import type { Address } from '@solana/kit'; // ===== ERROR CLASSIFICATION ===== @@ -291,7 +291,7 @@ export class ArcRetryManager { let lastError: ArcError | undefined; let attempt = 0; - while (attempt < retryConfig.maxAttempts!) { + while (attempt < (retryConfig.maxAttempts ?? 0)) { try { return await operation(); } catch (error) { @@ -304,7 +304,7 @@ export class ArcRetryManager { } // Don't retry if we've reached max attempts - if (attempt >= retryConfig.maxAttempts!) { + if (attempt >= (retryConfig.maxAttempts ?? 0)) { throw lastError; } @@ -322,7 +322,10 @@ export class ArcRetryManager { } } - throw lastError!; + if (lastError) { + throw lastError; + } + throw new Error('Operation failed with unknown error'); } private normalizeError(error: unknown, context: Partial): ArcError { @@ -367,13 +370,15 @@ export class ArcRetryManager { case ArcRetryStrategy.IMMEDIATE: return 0; - case ArcRetryStrategy.LINEAR_BACKOFF: - let linearDelay = config.baseDelay! * attempt; + case ArcRetryStrategy.LINEAR_BACKOFF: { + const linearDelay = (config.baseDelay ?? 0) * attempt; break; + } - case ArcRetryStrategy.EXPONENTIAL_BACKOFF: - let exponentialDelay = config.baseDelay! * Math.pow(2, attempt - 1); + case ArcRetryStrategy.EXPONENTIAL_BACKOFF: { + const exponentialDelay = (config.baseDelay ?? 0) * Math.pow(2, attempt - 1); break; + } case ArcRetryStrategy.CUSTOM: if (config.customRetryFn) { @@ -382,7 +387,7 @@ export class ArcRetryManager { return false; default: - let defaultDelay = config.baseDelay!; + const defaultDelay = config.baseDelay!; } // Apply delay calculation diff --git a/packages/sdk/src/core/rpc-manager.ts b/packages/sdk/src/core/rpc-manager.ts index 5727940..b8abf3a 100644 --- a/packages/sdk/src/core/rpc-manager.ts +++ b/packages/sdk/src/core/rpc-manager.ts @@ -17,7 +17,7 @@ type SolanaRpcSubscriptions = ReturnType; * @param commitment - Transaction confirmation level * @returns RPC client */ -export function createRpc(rpcUrl: string, commitment?: 'processed' | 'confirmed' | 'finalized'): SolanaRpc { +export function createRpc(rpcUrl: string, _commitment?: 'processed' | 'confirmed' | 'finalized'): SolanaRpc { return createSolanaRpc(rpcUrl); } @@ -30,7 +30,7 @@ export function createRpc(rpcUrl: string, commitment?: 'processed' | 'confirmed' */ export function createWebSocket( rpcUrl: string, - commitment?: 'processed' | 'confirmed' | 'finalized', + _commitment?: 'processed' | 'confirmed' | 'finalized', ): SolanaRpcSubscriptions { const wsUrl = rpcUrl.replace('https://', 'wss://').replace('http://', 'ws://'); return createSolanaRpcSubscriptions(wsUrl); @@ -43,6 +43,6 @@ export const getSharedWebSocket = createWebSocket; /** * No-op for backward compatibility - connection cleanup not needed with simple approach */ -export function releaseRpcConnection(rpcUrl: string, commitment?: 'processed' | 'confirmed' | 'finalized'): void { +export function releaseRpcConnection(_rpcUrl: string, _commitment?: 'processed' | 'confirmed' | 'finalized'): void { // No-op - simple RPC creation doesn't require cleanup } diff --git a/packages/sdk/src/core/transaction-builder.ts b/packages/sdk/src/core/transaction-builder.ts index c1ff9e2..ff159eb 100644 --- a/packages/sdk/src/core/transaction-builder.ts +++ b/packages/sdk/src/core/transaction-builder.ts @@ -142,8 +142,8 @@ export class TransactionBuilder { this.rpc = getSharedRpc(context.rpcUrl, context.commitment); this.rpcSubscriptions = getSharedWebSocket(context.rpcUrl, context.commitment); this.sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ - rpc: this.rpc as any, - rpcSubscriptions: this.rpcSubscriptions as any, + rpc: this.rpc as unknown as Parameters[0]['rpc'], + rpcSubscriptions: this.rpcSubscriptions as unknown as Parameters[0]['rpcSubscriptions'], }); } @@ -203,7 +203,7 @@ export class TransactionBuilder { } // Sign transaction with error handling - let signedTransaction: any; + let signedTransaction: Awaited>; let signature: string; try { @@ -229,7 +229,7 @@ export class TransactionBuilder { // Send and confirm transaction with enhanced error handling try { - await this.sendAndConfirmTransaction(signedTransaction, { + await this.sendAndConfirmTransaction(signedTransaction as Parameters[0], { commitment: this.context.commitment || 'confirmed', }); } catch (error) { @@ -367,7 +367,7 @@ export class TransactionBuilder { to: string | Address, amount: bigint, signer: TransactionSigner, - createAccountIfNeeded: boolean = true, + createAccountIfNeeded = true, ): Promise { const fromAddress = signer.address; const mintAddress = address(mint); diff --git a/packages/sdk/src/core/arc-web-client.ts b/packages/sdk/src/core/web-client.ts similarity index 85% rename from packages/sdk/src/core/arc-web-client.ts rename to packages/sdk/src/core/web-client.ts index edca811..553f9cd 100644 --- a/packages/sdk/src/core/arc-web-client.ts +++ b/packages/sdk/src/core/web-client.ts @@ -1,10 +1,11 @@ 'use client'; -import { type Address } from '@solana/kit'; -import { type SolanaClusterMoniker } from 'gill'; +import type { Address } from '@solana/kit'; +import type { SolanaClusterMoniker } from 'gill'; import { getClusterInfo, type ClusterInfo } from '../utils/cluster'; import { WalletStandardKitSigner, type StandardWalletInfo } from '../hooks/use-standard-wallets'; import type { ConnectorClient, ConnectorState } from '@solana-commerce/connector'; +import type { Wallet } from '@wallet-standard/base'; // Connector is the single source of truth; no Arc-managed persistence @@ -45,12 +46,12 @@ export interface ArcWebClientState { // Wallet State wallet: { wallets: StandardWalletInfo[]; - selectedWallet: any | null; + selectedWallet: Wallet | null; connected: boolean; connecting: boolean; address: Address | null; - signer: any | null; - accounts?: Array<{ address: Address; icon?: string; raw?: any }>; + signer: WalletStandardKitSigner | null; + accounts?: Array<{ address: Address; icon?: string; raw?: unknown }>; selectedAccount?: Address | null; capabilities?: { walletSupportsVersioned: boolean; @@ -134,7 +135,7 @@ export class ArcWebClient { if (this.state.config.debug) { console.log('[ArcWebClient] Initializing with connector:', { hasConnector: !!providedConnector, - connectorConnected: (providedConnector as any)?.getConnectorState?.()?.connected, + connectorConnected: (providedConnector as ConnectorClient | undefined)?.getSnapshot?.()?.connected, hasSubscribeMethod: typeof providedConnector?.subscribe === 'function', connectorType: providedConnector?.constructor?.name, }); @@ -161,13 +162,14 @@ export class ArcWebClient { const selectedAccount = s.selectedAccount; const accounts = s.accounts; - let signer: any = null; + let signer: WalletStandardKitSigner | null = null; let address: Address | null = null; if (connected && selectedWallet && selectedAccount) { - const rawAccount = (accounts as Array<{ address: string; raw: any }>).find( + const accountList = accounts as unknown as Array<{ address: string; raw?: unknown }>; + const rawAccount = accountList.find( a => a.address === selectedAccount, - )?.raw; - if (rawAccount) { + )?.raw as { address: string } | undefined; + if (rawAccount && 'address' in rawAccount) { signer = new WalletStandardKitSigner(rawAccount, selectedWallet); address = rawAccount.address as Address; } @@ -177,7 +179,7 @@ export class ArcWebClient { ...this.state, wallet: { ...this.state.wallet, - wallets: ((s.wallets as Array) || []).map((w: any) => ({ + wallets: ((s.wallets as StandardWalletInfo[]) || []).map((w: StandardWalletInfo) => ({ wallet: w.wallet, name: w.name as string, icon: (w.icon as string) || '', @@ -189,13 +191,13 @@ export class ArcWebClient { connecting, address, signer, - accounts: (accounts as Array).map((a: any) => ({ + accounts: (accounts as Array<{ address: string; icon?: string; raw?: unknown }>).map((a: { address: string; icon?: string; raw?: unknown }) => ({ address: a.address as Address, icon: a.icon as string | undefined, raw: a.raw, })), selectedAccount: selectedAccount as Address | null, - connectors: ((s.wallets as Array) || []).map((w: any) => ({ + connectors: ((s.wallets as StandardWalletInfo[]) || []).map((w: StandardWalletInfo) => ({ id: w.name as string, name: w.name as string, icon: (w.icon as string) || '', @@ -219,9 +221,9 @@ export class ArcWebClient { if (this.state.config.debug) { console.log('[ArcWebClient] Initial sync and subscribe to connector'); } - syncFromConnector((this.connector as any).getConnectorState()); + syncFromConnector((this.connector as ConnectorClient).getSnapshot()); - const unsubscribe = this.connector.subscribe((state: any) => { + const unsubscribe = this.connector.subscribe((state: ConnectorState) => { if (this.state.config.debug) { console.log('[ArcWebClient] Connector state changed, calling syncFromConnector'); } @@ -316,8 +318,8 @@ export class ArcWebClient { this.notify(); try { await this.connector?.select(walletName); - const s = this.connector?.getSnapshot() as any; - const rawAcc = (s?.accounts as Array)?.find((a: any) => a.address === s?.selectedAccount)?.raw; + const s = this.connector?.getSnapshot() as ConnectorState | undefined; + const rawAcc = (s?.accounts as Array<{ address: string; raw?: unknown }>)?.find((a: { address: string; raw?: unknown }) => a.address === s?.selectedAccount)?.raw; const w = s?.selectedWallet; const walletSupportsVersioned = rawAcc && w ? this.detectVersionedSupport(rawAcc, w) : true; this.state = { ...this.state, wallet: { ...this.state.wallet, capabilities: { walletSupportsVersioned } } }; @@ -341,8 +343,8 @@ export class ArcWebClient { try { await this.connector?.selectAccount(accountAddress as unknown as string); - const s = this.connector?.getSnapshot() as any; - const rawAcc = (s?.accounts as Array)?.find((a: any) => a.address === s?.selectedAccount)?.raw; + const s = this.connector?.getSnapshot() as ConnectorState | undefined; + const rawAcc = (s?.accounts as Array<{ address: string; raw?: unknown }>)?.find((a: { address: string; raw?: unknown }) => a.address === s?.selectedAccount)?.raw; const w = s?.selectedWallet; if (rawAcc && w) { const walletSupportsVersioned = this.detectVersionedSupport(rawAcc, w); @@ -371,7 +373,9 @@ export class ArcWebClient { if (this.state.config.debug) { console.log('[ArcWebClient] notify() called, calling', this.listeners.size, 'listeners'); } - this.listeners.forEach(listener => listener(this.state)); + for (const listener of this.listeners) { + listener(this.state); + } } // Removed getStorage() - unused since connector manages persistence @@ -379,10 +383,10 @@ export class ArcWebClient { /** * Best-effort detection of versioned (v0) transaction support from Wallet Standard features */ - private detectVersionedSupport(account: any, wallet: any): boolean { + private detectVersionedSupport(account: unknown, wallet: Wallet): boolean { try { - const features = (wallet?.features ?? {}) as Record; - const accountFeatures = (account?.features ?? {}) as Record; + const features = (wallet?.features ?? {}) as Record; + const accountFeatures = ((account as { features?: unknown })?.features ?? {}) as Record; const candidates = [ accountFeatures['solana:signAndSendTransaction'], accountFeatures['solana:signTransaction'], @@ -390,8 +394,9 @@ export class ArcWebClient { features['solana:signTransaction'], ].filter(Boolean); - const supports = (v: any): boolean => { - const versions = v?.supportedTransactionVersions ?? v?.supportedVersions; + const supports = (v: unknown): boolean => { + const versionObj = v as { supportedTransactionVersions?: unknown; supportedVersions?: unknown } | null | undefined; + const versions = versionObj?.supportedTransactionVersions ?? versionObj?.supportedVersions; if (!versions) return false; if (Array.isArray(versions)) return versions.includes(0) || versions.includes('v0') || versions.includes('0'); diff --git a/packages/sdk/src/hooks/__tests__/use-standard-wallets.test.ts b/packages/sdk/src/hooks/__tests__/use-standard-wallets.test.ts index f758c0c..9610ec2 100644 --- a/packages/sdk/src/hooks/__tests__/use-standard-wallets.test.ts +++ b/packages/sdk/src/hooks/__tests__/use-standard-wallets.test.ts @@ -23,7 +23,7 @@ const createMockWallet = (name: string, features: string[] = []): Wallet => ({ disconnect: vi.fn(), }; return acc; - }, {} as any), + }, {} as Record), chains: ['solana:mainnet', 'solana:devnet'], }); diff --git a/packages/sdk/src/hooks/__tests__/use-transfer-sol.test.tsx b/packages/sdk/src/hooks/__tests__/use-transfer-sol.test.tsx index 2ee8175..b9f1d62 100644 --- a/packages/sdk/src/hooks/__tests__/use-transfer-sol.test.tsx +++ b/packages/sdk/src/hooks/__tests__/use-transfer-sol.test.tsx @@ -5,7 +5,7 @@ import { useTransferSOL } from '../use-transfer-sol'; import { TestWrapper, createMockSigner, MOCK_ADDRESSES, MOCK_LAMPORTS } from '../../test-utils/mock-providers'; // Mock the core modules -vi.mock('../../core/arc-client-provider', () => ({ +vi.mock('../../core/commerce-client-provider', () => ({ useArcClient: () => ({ wallet: { address: MOCK_ADDRESSES.WALLET_1, @@ -54,7 +54,7 @@ vi.mock('../../utils/invalidate', () => ({ })), })); -const createWrapper = (props: any = {}) => { +const createWrapper = (props: Record = {}) => { // Create a fresh QueryClient for each test to ensure proper reset behavior const queryClient = new QueryClient({ defaultOptions: { @@ -143,7 +143,7 @@ describe('useTransferSOL', () => { amount: MOCK_LAMPORTS.ONE_SOL, }; - let transferResult: any; + let transferResult: unknown; await act(async () => { transferResult = await result.current.transferSOL(transferOptions); @@ -200,7 +200,7 @@ describe('useTransferSOL', () => { result.current.setAmountInput('1.0'); }); - let transferResult: any; + let transferResult: unknown; await act(async () => { transferResult = await result.current.transferFromInputs(); @@ -260,7 +260,7 @@ describe('useTransferSOL', () => { result.current.setAmountInput('0.5'); }); - let submitResult: any; + let submitResult: unknown; await act(async () => { submitResult = await result.current.handleSubmit(event); @@ -276,7 +276,7 @@ describe('useTransferSOL', () => { wrapper: createWrapper(), }); - let submitResult: any; + let submitResult: unknown; await act(async () => { submitResult = await result.current.handleSubmit(); @@ -295,7 +295,7 @@ describe('useTransferSOL', () => { result.current.setAmountInput('0.1'); }); - let submitResult: any; + let submitResult: unknown; await act(async () => { submitResult = await result.current.handleSubmit({}); @@ -344,7 +344,7 @@ describe('useTransferSOL', () => { expect(result.current.transferSOL).toBeDefined(); // Start transfer - don't await to catch loading state - let transferPromise: Promise; + let transferPromise: Promise; act(() => { transferPromise = result.current.transferSOL({ diff --git a/packages/sdk/src/hooks/__tests__/use-transfer-token.test.tsx b/packages/sdk/src/hooks/__tests__/use-transfer-token.test.tsx index 99bac70..628575f 100644 --- a/packages/sdk/src/hooks/__tests__/use-transfer-token.test.tsx +++ b/packages/sdk/src/hooks/__tests__/use-transfer-token.test.tsx @@ -5,7 +5,7 @@ import { useTransferToken, BlockhashExpirationError } from '../use-transfer-toke import { TestWrapper, MOCK_ADDRESSES, MOCK_LAMPORTS } from '../../test-utils/mock-providers'; // Mock the dependencies -vi.mock('../../core/arc-client-provider', () => ({ +vi.mock('../../core/commerce-client-provider', () => ({ useArcClient: () => ({ wallet: { address: MOCK_ADDRESSES.WALLET_1, @@ -322,7 +322,7 @@ describe('useTransferToken', () => { }; // Mock the useArcClient hook to return our mock transport - vi.mocked(vi.importActual('../../core/arc-client-provider')).then(module => { + vi.mocked(vi.importActual('../../core/commerce-client-provider')).then(module => { vi.spyOn(module, 'useArcClient').mockReturnValue({ wallet: { address: MOCK_ADDRESSES.WALLET_1, diff --git a/packages/sdk/src/hooks/use-standard-wallets.ts b/packages/sdk/src/hooks/use-standard-wallets.ts index b9e4a0c..a345b8e 100644 --- a/packages/sdk/src/hooks/use-standard-wallets.ts +++ b/packages/sdk/src/hooks/use-standard-wallets.ts @@ -3,6 +3,28 @@ import { useState, useEffect, useCallback } from 'react'; import { getWallets } from '@wallet-standard/app'; import type { Wallet } from '@wallet-standard/base'; + +// Type for wallet account from wallet-standard +interface WalletAccount { + address: string; + publicKey?: Uint8Array; + chains?: string[]; + features?: string[]; + label?: string; + icon?: string; +} + +// Type for wallet features with proper indexing +interface WalletFeatures { + [key: string]: unknown; + 'standard:connect'?: { + connect: () => Promise<{ accounts: WalletAccount[] }>; + }; + 'solana:signTransaction'?: { + signTransaction: (input: { transaction: Uint8Array; account: WalletAccount; chain?: string; options?: unknown }) => Promise<{ signedTransaction: Uint8Array }>; + }; +} + import { address, type Address, @@ -16,7 +38,7 @@ export class WalletStandardKitSigner { readonly address: Address; constructor( - private walletAccount: any, + private walletAccount: WalletAccount, private wallet: Wallet, ) { this.address = address(walletAccount.address); @@ -24,133 +46,129 @@ export class WalletStandardKitSigner { async modifyAndSignTransactions( transactions: readonly T[], - config?: any, + config?: unknown, ): Promise { - try { - const signTransactionFeature = (this.wallet.features as any)['solana:signTransaction']; - if (!signTransactionFeature) { - throw new Error(`Wallet ${this.wallet.name} does not support transaction signing`); - } + const signTransactionFeature = (this.wallet.features as WalletFeatures)['solana:signTransaction']; + if (!signTransactionFeature) { + throw new Error(`Wallet ${this.wallet.name} does not support transaction signing`); + } - const signedTransactions = []; + const signedTransactions = []; - for (const transaction of transactions) { - if (transaction.messageBytes instanceof Uint8Array) { - const signaturesCount = 1; - const totalLength = 1 + signaturesCount * 64 + transaction.messageBytes.length; - const serializedTransaction = new Uint8Array(totalLength); + for (const transaction of transactions) { + if (transaction.messageBytes instanceof Uint8Array) { + const signaturesCount = 1; + const totalLength = 1 + signaturesCount * 64 + transaction.messageBytes.length; + const serializedTransaction = new Uint8Array(totalLength); - let offset = 0; - serializedTransaction[offset] = signaturesCount; - offset += 1; + let offset = 0; + serializedTransaction[offset] = signaturesCount; + offset += 1; - for (let i = 0; i < signaturesCount; i++) { - serializedTransaction.fill(0, offset, offset + 64); - offset += 64; - } + for (let i = 0; i < signaturesCount; i++) { + serializedTransaction.fill(0, offset, offset + 64); + offset += 64; + } - serializedTransaction.set(transaction.messageBytes, offset); + serializedTransaction.set(transaction.messageBytes, offset); - const walletResult = await signTransactionFeature.signTransaction({ - account: this.walletAccount, - transaction: serializedTransaction, - }); + const walletResult = await signTransactionFeature.signTransaction({ + account: this.walletAccount, + transaction: serializedTransaction, + }); - let signedTransactionBytes: Uint8Array; + let signedTransactionBytes: Uint8Array; - if (Array.isArray(walletResult)) { - const firstResult = walletResult[0]; - if (firstResult?.signedTransaction instanceof Uint8Array) { - signedTransactionBytes = firstResult.signedTransaction; - } else { - throw new Error('Wallet returned invalid array result format'); - } - } else if (walletResult?.signedTransaction instanceof Uint8Array) { - signedTransactionBytes = walletResult.signedTransaction; - } else if (walletResult instanceof Uint8Array) { - signedTransactionBytes = walletResult; + if (Array.isArray(walletResult)) { + const firstResult = walletResult[0]; + if (firstResult?.signedTransaction instanceof Uint8Array) { + signedTransactionBytes = firstResult.signedTransaction; } else { - throw new Error('Wallet returned unexpected signing result format'); + throw new Error('Wallet returned invalid array result format'); } + } else if (walletResult?.signedTransaction instanceof Uint8Array) { + signedTransactionBytes = walletResult.signedTransaction; + } else if (walletResult instanceof Uint8Array) { + signedTransactionBytes = walletResult; + } else { + throw new Error('Wallet returned unexpected signing result format'); + } - if (signedTransactionBytes.length > 65) { - // Parse the wire format: [signature_count (short-u16)][signatures][message] - let offset = 0; - - // Read signature count as short-u16 (variable length 1-3 bytes) - let signatureCount = 0; - let byteCount = 0; - const MAX_VARINT_BYTES = 10; // Safety limit for varint decoding - let parseSuccessful = false; - - while (++byteCount <= MAX_VARINT_BYTES) { - const byteIndex = byteCount - 1; - - // Bounds check: ensure we don't read beyond array bounds - if (offset + byteIndex >= signedTransactionBytes.length) { - throw new Error( - 'Invalid transaction format: signature count extends beyond transaction bytes', - ); - } - - const currentByte = signedTransactionBytes[offset + byteIndex]; - if (currentByte === undefined) { - throw new Error( - 'Invalid transaction format: unexpected undefined byte in signature count', - ); - } - - const nextSevenBits = 0b1111111 & currentByte; - signatureCount |= nextSevenBits << (byteIndex * 7); - - if ((currentByte & 0b10000000) === 0) { - // No continuation bit, we're done - parseSuccessful = true; - break; - } + if (signedTransactionBytes.length > 65) { + // Parse the wire format: [signature_count (short-u16)][signatures][message] + let offset = 0; + + // Read signature count as short-u16 (variable length 1-3 bytes) + let signatureCount = 0; + let byteCount = 0; + const MAX_VARINT_BYTES = 10; // Safety limit for varint decoding + let parseSuccessful = false; + + while (++byteCount <= MAX_VARINT_BYTES) { + const byteIndex = byteCount - 1; + + // Bounds check: ensure we don't read beyond array bounds + if (offset + byteIndex >= signedTransactionBytes.length) { + throw new Error( + 'Invalid transaction format: signature count extends beyond transaction bytes', + ); } - if (!parseSuccessful) { + const currentByte = signedTransactionBytes[offset + byteIndex]; + if (currentByte === undefined) { throw new Error( - 'Invalid transaction format: signature count varint exceeded maximum length or failed to parse', + 'Invalid transaction format: unexpected undefined byte in signature count', ); } - // Only advance offset after successful parse - offset += byteCount; + const nextSevenBits = 0b1111111 & currentByte; + signatureCount |= nextSevenBits << (byteIndex * 7); - // Extract the first signature (64 bytes) - const signature = signedTransactionBytes.slice(offset, offset + 64) as SignatureBytes; + if ((currentByte & 0b10000000) === 0) { + // No continuation bit, we're done + parseSuccessful = true; + break; + } + } - // Extract the message bytes (everything after signatures) - const messageStartOffset = offset + signatureCount * 64; - const newMessageBytes = signedTransactionBytes.slice(messageStartOffset) as Uint8Array; + if (!parseSuccessful) { + throw new Error( + 'Invalid transaction format: signature count varint exceeded maximum length or failed to parse', + ); + } - // debug log removed + // Only advance offset after successful parse + offset += byteCount; - // The wallet may have modified the transaction, so we need to use the new message bytes - const signedTransaction = { - ...transaction, - messageBytes: newMessageBytes as unknown as TransactionMessageBytes, - signatures: { - ...transaction.signatures, - [this.address]: signature, - }, - } as T; + // Extract the first signature (64 bytes) + const signature = signedTransactionBytes.slice(offset, offset + 64) as SignatureBytes; - signedTransactions.push(signedTransaction); - } else { - throw new Error('Wallet returned invalid signed transaction format'); - } + // Extract the message bytes (everything after signatures) + const messageStartOffset = offset + signatureCount * 64; + const newMessageBytes = signedTransactionBytes.slice(messageStartOffset) as Uint8Array; + + // debug log removed + + // The wallet may have modified the transaction, so we need to use the new message bytes + const signedTransaction = { + ...transaction, + messageBytes: newMessageBytes as unknown as TransactionMessageBytes, + signatures: { + ...transaction.signatures, + [this.address]: signature, + }, + } as T; + + signedTransactions.push(signedTransaction); } else { - throw new Error('Transaction messageBytes must be Uint8Array for wallet signing'); + throw new Error('Wallet returned invalid signed transaction format'); + } + } else { + throw new Error('Transaction messageBytes must be Uint8Array for wallet signing'); } - } - - return signedTransactions as readonly T[]; - } catch (error) { - throw error; } + + return signedTransactions as readonly T[]; } } @@ -183,7 +201,7 @@ export function useStandardWallets(options: UseStandardWalletsOptions = {}): Use const [wallets, setWallets] = useState([]); const [selectedWallet, setSelectedWallet] = useState(null); const [connecting, setConnecting] = useState(false); - const [connectedAccount, setConnectedAccount] = useState(null); + const [connectedAccount, setConnectedAccount] = useState(null); const [signer, setSigner] = useState(null); useEffect(() => { @@ -195,7 +213,7 @@ export function useStandardWallets(options: UseStandardWalletsOptions = {}): Use const uniqueWallets = detectedWallets.reduce((acc, wallet) => { const existing = acc.find(w => w.name === wallet.name); if (!existing) { - acc.push(wallet); + acc.push(wallet); } return acc; }, [] as Wallet[]); @@ -203,11 +221,11 @@ export function useStandardWallets(options: UseStandardWalletsOptions = {}): Use const solanaCompatibleWallets = uniqueWallets.filter(wallet => { const features = Object.keys(wallet.features); const hasSolanaFeatures = features.some( - feature => - feature.includes('solana') || - feature.includes('connect') || - feature.includes('sign') || - feature.includes('standard'), + feature => + feature.includes('solana') || + feature.includes('connect') || + feature.includes('sign') || + feature.includes('standard'), ); return hasSolanaFeatures; }); @@ -243,12 +261,12 @@ export function useStandardWallets(options: UseStandardWalletsOptions = {}): Use setConnecting(true); - try { - const connectFeature = (wallet.features as any)['standard:connect']; - if (!connectFeature) { - throw new Error(`Wallet ${walletName} does not support standard connect`); - } + const connectFeature = (wallet.features as WalletFeatures)['standard:connect']; + if (!connectFeature) { + throw new Error(`Wallet ${walletName} does not support standard connect`); + } + try { const result = await connectFeature.connect(); const account = result.accounts[0]; const addressString = account.address; @@ -258,8 +276,6 @@ export function useStandardWallets(options: UseStandardWalletsOptions = {}): Use setSelectedWallet(wallet); setConnectedAccount(account); setSigner(kitSigner); - } catch (error) { - throw error; } finally { setConnecting(false); } @@ -268,13 +284,9 @@ export function useStandardWallets(options: UseStandardWalletsOptions = {}): Use ); const disconnect = useCallback(async () => { - try { - setSelectedWallet(null); - setConnectedAccount(null); - setSigner(null); - } catch (error) { - throw error; - } + setSelectedWallet(null); + setConnectedAccount(null); + setSigner(null); }, []); return { diff --git a/packages/sdk/src/hooks/use-transfer-sol.ts b/packages/sdk/src/hooks/use-transfer-sol.ts index be282d8..573897c 100644 --- a/packages/sdk/src/hooks/use-transfer-sol.ts +++ b/packages/sdk/src/hooks/use-transfer-sol.ts @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { useArcClient } from '../core/arc-client-provider'; +import { useArcClient } from '../core/commerce-client-provider'; import { releaseRpcConnection } from '../core/rpc-manager'; import { createTransactionBuilder, createTransactionContext } from '../core/transaction-builder'; import { address, type Address, type TransactionSigner } from '@solana/kit'; @@ -96,7 +96,7 @@ export interface UseTransferSOLReturn { transferFromInputs: () => Promise; } -export function useTransferSOL(initialToInput: string = '', initialAmountInput: string = ''): UseTransferSOLReturn { +export function useTransferSOL(initialToInput = '', initialAmountInput = ''): UseTransferSOLReturn { const { wallet, network, config } = useArcClient(); const queryClient = useQueryClient(); @@ -168,16 +168,12 @@ export function useTransferSOL(initialToInput: string = '', initialAmountInput: throw new Error('Both recipient address and amount are required'); } - try { - const amountInLamports = convertSOLToLamports(amountInput); + const amountInLamports = convertSOLToLamports(amountInput); - return await mutation.mutateAsync({ - to: toInput, - amount: amountInLamports, - }); - } catch (error) { - throw error; - } + return await mutation.mutateAsync({ + to: toInput, + amount: amountInLamports, + }); }, [toInput, amountInput, mutation.mutateAsync]); const handleSubmit = useCallback( diff --git a/packages/sdk/src/hooks/use-transfer-token.ts b/packages/sdk/src/hooks/use-transfer-token.ts index 2a543d7..0a2262a 100644 --- a/packages/sdk/src/hooks/use-transfer-token.ts +++ b/packages/sdk/src/hooks/use-transfer-token.ts @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { useArcClient } from '../core/arc-client-provider'; +import { useArcClient } from '../core/commerce-client-provider'; import { getSharedRpc, getSharedWebSocket, releaseRpcConnection } from '../core/rpc-manager'; import { sendAndConfirmTransactionFactory, @@ -121,9 +121,9 @@ export interface UseTransferTokenReturn { } export function useTransferToken( - initialMintInput: string = '', - initialToInput: string = '', - initialAmountInput: string = '', + initialMintInput = '', + initialToInput = '', + initialAmountInput = '', ): UseTransferTokenReturn { const client = useArcClient(); const { wallet, network, config } = client; @@ -191,7 +191,7 @@ export function useTransferToken( const waitForTransactionConfirmation = async ( signature: string, rpcClient: any, - maxWaitTime: number = 30000, + maxWaitTime = 30000, ) => { const startTime = Date.now(); let lastError: any; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 440f9b2..7ea8f82 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -6,8 +6,8 @@ */ // ===== CORE PROVIDER & HOOKS ===== -export { ArcProvider } from './core/arc-provider'; -export { useArcClient } from './core/arc-client-provider'; +export { ArcProvider } from './core/commerce-provider'; +export { useArcClient } from './core/commerce-client-provider'; export { useTransferSOL } from './hooks/use-transfer-sol'; export { useTransferToken } from './hooks/use-transfer-token'; @@ -22,8 +22,8 @@ export type { } from './hooks/use-transfer-token'; // Provider types -export type { ArcProviderProps } from './core/arc-provider'; -export type { ArcWebClientConfig } from './core/arc-web-client'; +export type { ArcProviderProps } from './core/commerce-provider'; +export type { ArcWebClientConfig } from './core/web-client'; export type { SolanaClusterMoniker } from 'gill'; // ===== ADDRESS HELPERS ===== diff --git a/packages/sdk/src/test-utils/mock-providers.tsx b/packages/sdk/src/test-utils/mock-providers.tsx index 3ca376e..c28628a 100644 --- a/packages/sdk/src/test-utils/mock-providers.tsx +++ b/packages/sdk/src/test-utils/mock-providers.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { vi } from 'vitest'; -import type { ArcWebClientConfig, ArcWebClientState } from '../core/arc-web-client'; -import { ArcClientProvider } from '../core/arc-client-provider'; +import { address } from '@solana/kit'; +import type { ArcWebClientConfig, ArcWebClientState } from '../core/web-client'; +import { ArcClientProvider } from '../core/commerce-client-provider'; // Mock Arc client state export const createMockArcState = (overrides: Partial = {}): ArcWebClientState => ({ @@ -15,16 +16,18 @@ export const createMockArcState = (overrides: Partial = {}): clusterInfo: { name: 'devnet', rpcUrl: 'https://api.devnet.solana.com', - websocketUrl: 'wss://api.devnet.solana.com', - canAirdrop: true, + wsUrl: 'wss://api.devnet.solana.com', + isMainnet: false, + isDevnet: true, + isTestnet: false, }, }, wallet: { - address: 'So11111111111111111111111111111111111111112', + wallets: [], + selectedWallet: null, + address: null, connected: false, connecting: false, - disconnecting: false, - info: null, signer: null, }, config: { @@ -43,7 +46,6 @@ export const createMockArcClient = (state: Partial = {}) => ( // Methods connect: vi.fn(), disconnect: vi.fn(), - setNetwork: vi.fn(), updateConfig: vi.fn(), // State setters (for internal use) @@ -54,7 +56,7 @@ export const createMockArcClient = (state: Partial = {}) => ( // Mock wallet signer export const createMockSigner = () => ({ - address: 'So11111111111111111111111111111111111111112', + address: address('So11111111111111111111111111111111111111112'), signTransaction: vi.fn(), signMessage: vi.fn(), }); @@ -94,7 +96,7 @@ export function TestWrapper({ children, arcState = {}, queryClient }: TestWrappe } // Create a simple mock provider that bypasses the real ArcClientProvider -const MockArcClientContext = React.createContext(null); +const MockArcClientContext = React.createContext | null>(null); function MockArcClientProvider({ children, @@ -108,7 +110,7 @@ function MockArcClientProvider({ } // Mock RPC responses -export const createMockRpcResponse = (result: any) => ({ +export const createMockRpcResponse = (result: unknown) => ({ jsonrpc: '2.0', id: 1, result, diff --git a/packages/sdk/src/types/index.ts b/packages/sdk/src/types/index.ts index 4939f62..385b231 100644 --- a/packages/sdk/src/types/index.ts +++ b/packages/sdk/src/types/index.ts @@ -25,6 +25,6 @@ export interface UseTransactionOptions { export interface SwapState { isLoading: boolean; error: Error | null; - quotes: any[]; - selectedQuote: any | null; + quotes: unknown[]; + selectedQuote: unknown | null; } diff --git a/packages/sdk/src/utils/schema-validation.ts b/packages/sdk/src/utils/schema-validation.ts index aa20009..5b94771 100644 --- a/packages/sdk/src/utils/schema-validation.ts +++ b/packages/sdk/src/utils/schema-validation.ts @@ -26,21 +26,21 @@ export function validateAndNormalizeAmount( } if (typeof amount === 'string') { - const parsed = parseFloat(amount); - if (isNaN(parsed) || parsed < 0) { + const parsed = Number.parseFloat(amount); + if (Number.isNaN(parsed) || parsed < 0) { throw new Error(`Invalid amount: ${amount}`); } // Convert to smallest units based on decimals - const normalizedAmount = BigInt(Math.floor(parsed * Math.pow(10, decimals))); + const normalizedAmount = BigInt(Math.floor(parsed * 10 ** decimals)); return { amount: normalizedAmount, decimals }; } if (typeof amount === 'number') { - if (isNaN(amount) || amount < 0 || !isFinite(amount)) { + if (Number.isNaN(amount) || amount < 0 || !Number.isFinite(amount)) { throw new Error(`Invalid amount: ${amount}`); } // Convert to smallest units based on decimals - const normalizedAmount = BigInt(Math.floor(amount * Math.pow(10, decimals))); + const normalizedAmount = BigInt(Math.floor(amount * 10 ** decimals)); return { amount: normalizedAmount, decimals }; } diff --git a/packages/solana-commerce/README.md b/packages/solana-commerce/README.md new file mode 100644 index 0000000..ba4cb04 --- /dev/null +++ b/packages/solana-commerce/README.md @@ -0,0 +1,142 @@ +# @solana-commerce/solana-commerce + +Complete Solana Commerce SDK - all packages in one install + + + +## Installation + +```bash +pnpm add @solana-commerce/solana-commerce +``` + +## What's Included + +This meta-package includes all Solana Commerce Kit functionality: + +- **@solana-commerce/react** - UI components and React integration +- **@solana-commerce/sdk** - Core React hooks for Solana development +- **@solana-commerce/headless** - Framework-agnostic commerce logic +- **@solana-commerce/connector** - Wallet Standard connection layer +- **@solana-commerce/solana-pay** - Solana Pay protocol implementation + +## Quick Start + +```typescript +import { PaymentButton, useWallet, useBalance } from '@solana-commerce/solana-commerce'; + +function App() { + return ( + { + console.log('Payment successful:', signature); + }} + /> + ); +} +``` + +## Usage + +All exports from individual packages are available through this meta-package: + +### Components + +```typescript +import { PaymentButton } from '@solana-commerce/solana-commerce'; + + +``` + +### Hooks + +```typescript +import { useWallet, useBalance, useTransferSOL } from '@solana-commerce'; + +function WalletInfo() { + const { wallet, connect } = useWallet(); + const { balance } = useBalance(); + const { transferSOL } = useTransferSOL(); + + return ( +
+

Balance: {balance} SOL

+ +
+ ); +} +``` + +### Headless Functions + +```typescript +import { createPayment, calculateTotal } from '@solana-commerce/solana-commerce'; + +const payment = createPayment({ + merchant: { wallet: 'address' }, + amount: 10.00, + currency: 'USDC' +}); + +const total = calculateTotal({ + items: [{ price: 19.99, quantity: 2 }], + tax: 0.08, + shipping: 5.00 +}); +``` + +### Solana Pay + +```typescript +import { createQR, encodeURL } from '@solana-commerce/solana-commerce'; + +const url = encodeURL({ + recipient: 'wallet-address', + amount: 10, + label: 'Store Purchase' +}); + +const qr = createQR(url); +``` + +## When to Use This Package + +**Use `@solana-commerce` when:** +- Building full-featured commerce applications +- You need components, hooks, and commerce logic +- Convenience over granular dependency control +- Rapid prototyping and development + +**Use individual packages when:** +- Building custom UI (→ use `@solana-commerce/headless`) +- Need only specific functionality (→ use individual packages) +- Optimizing bundle size for production +- Working with non-React frameworks + +## Related Packages + +Full documentation for each included package: + +- [@solana-commerce/react](../react) - React components +- [@solana-commerce/sdk](../sdk) - React hooks +- [@solana-commerce/headless](../headless) - Core commerce logic +- [@solana-commerce/connector](../connector) - Wallet connection +- [@solana-commerce/solana-pay](../solana-pay) - Solana Pay implementation + +## License + +MIT diff --git a/packages/solana-pay/README.md b/packages/solana-pay/README.md new file mode 100644 index 0000000..5e9156c --- /dev/null +++ b/packages/solana-pay/README.md @@ -0,0 +1,85 @@ +# @solana-commerce/solana-pay + +Solana Pay SDK implementation with QR code generation built on Solana Kit/Gill + + + +## Installation + +```bash +pnpm add @solana-commerce/solana-pay +``` + +## Features + +- Solana Pay URL encoding and parsing +- QR code generation with styling options +- SOL and SPL token transfer building +- Transaction request support +- TypeScript-first with complete type definitions +- Compliant with [Solana Pay specification](https://docs.solanapay.com/spec) + +## API + +### URL Functions + +- **encodeURL(fields)** - Create Solana Pay URL from payment parameters +- **parseURL(url)** - Parse Solana Pay URL into components + +### QR Code Generation + +- **createQR(url, size?, background?, color?)** - Generate basic QR code as data URL +- **createStyledQRCode(url, options)** - Generate QR code with custom styling (logo, colors, dot/corner styles) + +### Transaction Building + +- **createTransfer(params)** - Build SOL transfer transaction +- **createSOLTransfer(params)** - Build SOL transfer (alias) +- **createSPLTransfer(params)** - Build SPL token transfer transaction + +## Examples + +### Basic Payment URL + +```typescript +import { encodeURL, createQR } from '@solana-commerce/solana-pay'; + +const url = encodeURL({ + recipient: 'wallet-address', + amount: 0.1, + label: 'Store Purchase', + message: 'Thank you!' + // splToken: USDC_MINT, (optional) +}); + +const qr = await createQR(url); +``` + + +### Styled QR Code + +```typescript +import { createStyledQRCode } from '@solana-commerce/solana-pay'; + +const qr = await createStyledQRCode(url, { + width: 400, + color: { dark: '#9945FF', light: '#FFFFFF' }, + dotStyle: 'rounded', + cornerStyle: 'extra-rounded', + logo: 'https://mystore.com/logo.png', + logoSize: 80 +}); +``` + +## Development + +```bash +pnpm build # Build package +pnpm dev # Watch mode +pnpm test # Run tests +pnpm test:watch # Run tests in watch mode +``` + +## License + +MIT diff --git a/packages/tsconfig.json b/packages/tsconfig.json index f332619..6ccc571 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -1,12 +1,9 @@ { "extends": "../tsconfig.json", "compilerOptions": { - // Tell TypeScript to generate declaration files "declaration": true, "declarationMap": true, - // Tell TypeScript to only emit declarations, tsup will handle the JS "emitDeclarationOnly": true, - // Define where the output files go "outDir": "dist" }, "exclude": ["node_modules", "dist"]