diff --git a/.gitignore b/.gitignore index 407b0f14d..fe4c45cc8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ node_modules dist/ .env .DS_Store -.pnpm-store \ No newline at end of file +.pnpm-store +.claude +.serena \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..3414b444a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,417 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +RozoAI Intent Pay SDK (`@rozoai/intent-pay`) is a cross-chain crypto payment React SDK enabling seamless payments from 1000+ tokens with single transactions. This is a fork of Daimo Pay, originally based on ConnectKit. + +**Key capabilities:** +- Cross-chain payments (EVM, Solana, Stellar) in under 1 minute +- Single transaction flow - no multiple wallet steps +- Permissionless - never holds funds +- Works with major wallets and exchanges + +## Development Commands + +### Monorepo Commands (from root) + +```bash +# Build both packages +pnpm build + +# Development mode (all packages in parallel) +pnpm dev + +# Development mode (specific packages) +pnpm dev:common # Build @rozoai/intent-common in watch mode +pnpm dev:pay # Build @rozoai/intent-pay in watch mode +pnpm dev:example # Run Next.js example app + +# Install dependencies +pnpm install:local + +# Linting +pnpm run lint + +# Release (build + publish both packages) +pnpm release + +# Cleanup +pnpm clean # Remove all node_modules +pnpm clean:deps # Remove node_modules, dist, build, .next +pnpm clean:all # Full cleanup including lockfile +``` + +### Package: @rozoai/intent-pay (packages/connectkit) + +```bash +cd packages/connectkit + +# Development with watch mode (uses rollup) +pnpm dev + +# Build for production +pnpm build + +# Lint +pnpm lint + +# Release new version (uses bumpp) +pnpm release +``` + +### Package: @rozoai/intent-common (packages/pay-common) + +```bash +cd packages/pay-common + +# Development with watch mode (TypeScript) +pnpm dev + +# Build +pnpm build + +# Run tests +pnpm test + +# Lint +pnpm lint +``` + +### Example App (examples/nextjs-app) + +```bash +cd examples/nextjs-app + +# Development (uses local packages via NEXT_USE_LOCAL_PACKAGES) +pnpm dev + +# Build +pnpm build + +# Start production server +pnpm start + +# Lint +pnpm lint +``` + +### Smart Contracts (packages/contract) + +Uses Foundry for Solidity development. + +```bash +cd packages/contract + +# See Makefile for available commands +make +``` + +## Architecture + +### Monorepo Structure + +- **packages/connectkit** - Main SDK package (`@rozoai/intent-pay`) +- **packages/pay-common** - Shared types and utilities (`@rozoai/intent-common`) +- **packages/contract** - Solidity smart contracts (Foundry project) +- **examples/nextjs-app** - Next.js integration example + +### Core Architecture Patterns + +#### 1. Payment State Machine (FSM) + +Location: `packages/connectkit/src/payment/paymentFsm.ts` + +State flow: +``` +preview → payment_unpaid → payment_started → payment_completed/payment_bounced +``` + +**Critical rules:** +- Cannot go from `error` to `payment_unpaid` without providing order +- Cannot skip from `preview` to `payment_started` without going through `payment_unpaid` +- Cross-chain switches require resetting old payment to `payment_unpaid` before starting new one + +See `packages/connectkit/PAYMENT_FLOW.md` for detailed state transition diagrams. + +#### 2. Multi-Chain Provider System + +Location: `packages/connectkit/src/provider/` + +Three separate context providers: +- **Web3ContextProvider** - EVM chains via Wagmi v2 +- **SolanaContextProvider** - Solana via @solana/wallet-adapter-react +- **StellarContextProvider** - Stellar via @stellar/stellar-sdk + +**RozoPayProvider** wraps all three and provides unified interface. + +#### 3. Payment Flow Routing + +Location: `packages/connectkit/src/constants/routes.ts` + +Modal navigation flow: +``` +SELECT_METHOD → SELECT_TOKEN → SELECT_AMOUNT → WAITING_WALLET → CONFIRMATION +``` + +Alternative flows for Solana/Stellar, exchanges, and deposit addresses. + +#### 4. Hooks-Based State Management + +Main hooks (location: `packages/connectkit/src/hooks/`): + +- **useRozoPay** (`useDaimoPay.tsx`) - Core payment management + - `createPreviewOrder()` - Start new payment + - `setPayId()` - Resume existing order + - `hydrateOrder()` - Lock in payment details + - `paySource()` - Trigger payment search + - `payWallet()` - Execute wallet payment + - `resetPayment()` - Reset and start new flow + +- **useRozoPayStatus** (`useDaimoPayStatus.ts`) - Payment status tracking + - Returns: payment_unpaid/started/completed/bounced + +- **useRozoPayUI** (`useDaimoPayUI.tsx`) - UI state + - `openRozoPay()` / `closeRozoPay()` - Modal control + +- **usePaymentState** (`usePaymentState.ts`) - Payment parameters management + - Manages `currPayParams` state + - Handles payment reset logic + - Clears selected options on reset + +- **useTokenOptions** (`useTokenOptions.tsx`) - Token selection logic + - Manages loading state for token options + - Filters and sorts available tokens + - Handles preferred tokens/chains + +- **useStellarDestination** (`useStellarDestination.ts`) - Address routing + - Derives destination address from payParams + - Determines payment direction (Stellar ↔ EVM) + - Returns memoized values based on payParams + +#### 5. Payment Options System + +**Wallet Payment Options** (`useWalletPaymentOptions.ts`): +- EVM wallets: MetaMask, Coinbase Wallet, Trust, Rainbow, etc. +- Solana wallets: Phantom, Backpack, Solflare +- Stellar wallets: Via stellar-wallets-kit +- Active chains: Base (8453), Polygon (137), Solana, Stellar + +**External Payment Options** (`useExternalPaymentOptions.ts`): +- Exchanges: Coinbase, Binance, Lemon +- ZKP2P apps: Venmo, CashApp, MercadoPago, Revolut, Wise +- Other: RampNetwork, deposit addresses + +### Component Structure + +Location: `packages/connectkit/src/components/` + +``` +components/ +├── DaimoPayButton/ # Main entry point - RozoPayButton component +├── DaimoPayModal/ # Payment modal container +├── Pages/ # Modal pages for each step +│ ├── SelectMethod/ # Choose payment method +│ ├── SelectToken/ # Choose token to pay with +│ ├── SelectAmount/ # Enter payment amount +│ ├── WaitingWallet/ # Wait for wallet confirmation +│ ├── Confirmation/ # Payment confirmed +│ ├── Stellar/ # Stellar-specific flows +│ │ └── PayWithStellarToken/ +│ └── Solana/ # Solana-specific flows +│ └── PayWithSolanaToken/ +├── Common/ # Reusable UI components +└── Spinners/ # Loading animations +``` + +### Key Configuration Files + +- **packages/connectkit/src/defaultConfig.ts** - Auto-generates wagmi config for EVM chains +- **packages/connectkit/src/constants/rozoConfig.ts** - API URLs, token configs +- **packages/connectkit/src/constants/routes.ts** - Payment flow navigation +- **packages/connectkit/rollup.config.js** - Build configuration + +### API Integration + +Base URL: `intentapiv2.rozo.ai/functions/v1/` + +Authentication via `appId` parameter passed to RozoPayButton. + +### Cross-Chain Payment Handling + +**Direct payment**: User pays on same chain as destination +**Cross-chain payment**: User pays on different chain, Rozo handles bridging + +Logic: If `order.preferredChainId` exists and differs from selected token's chainId, use cross-chain flow via `createPayment()` API. + +**Important**: When switching chains during an active payment, must transition old payment to `payment_unpaid` before starting new payment with `payment_started`. + +## Common Development Tasks + +### Adding Support for a New Chain + +1. Add chain config to `defaultConfig.ts` +2. Add token options to `useTokenOptions.tsx` +3. Update wallet connectors if needed in `defaultConnectors.ts` +4. Test payment flow with example app + +### Debugging Payment State Issues + +1. Check payment FSM state in `paymentStore.ts` +2. Enable logging in payment components (look for `log?.()` statements) +3. Monitor state transitions in browser DevTools +4. Verify `payParams` are updating correctly +5. Check destination address derivation in `useStellarDestination` + +Common issues documented in `packages/connectkit/PAYMENT_FLOW.md` under "Common Issues & Solutions". + +### Testing Payment Flows + +Use the Next.js example app: + +```bash +# Terminal 1: Run SDK in dev mode +cd packages/connectkit +pnpm dev + +# Terminal 2: Run example app +cd examples/nextjs-app +pnpm dev +``` + +Changes to SDK are immediately reflected in example app (hot reload). + +Test different scenarios: +- EVM chain payments (Base, Polygon) +- Solana payments (USDC) +- Stellar payments (XLM, USDC) +- Cross-chain payments +- Different wallets +- Mobile deep-linking + +## Important Implementation Notes + +### Dependency Array Issues (Fixed in v0.0.22+) + +Prior to v0.0.22, inline objects in RozoPayButton props caused infinite re-renders. Fixed by using `JSON.stringify()` in dependency arrays for: +- `metadata` +- `preferredTokens` +- `paymentOptions` + +### State Management Patterns + +Uses React Context + custom hooks pattern: +- Centralized state in `paymentStore.ts` +- Event-driven updates via `paymentEventEmitter.ts` +- Side effects in `paymentEffects.ts` + +### Error Handling + +Payment errors transition to `error` state. To recover: +1. Must provide both `paymentId` and `order` when calling `setPaymentUnpaid()` +2. Check if user rejected transaction vs. actual failure +3. Set appropriate error state: `RequestCancelled` or `RequestFailed` + +### Theme System + +Location: `packages/connectkit/src/styles/` + +8 built-in themes: auto, web95, retro, soft, midnight, minimal, rounded, nouns + +Custom themes via styled-components theme provider. + +## Tech Stack + +- **Frontend**: React 18+, TypeScript, styled-components +- **Web3 - EVM**: Wagmi v2, Viem v2, @tanstack/react-query v5 +- **Web3 - Solana**: @solana/wallet-adapter-react, @solana/web3.js +- **Web3 - Stellar**: @stellar/stellar-sdk, @creit.tech/stellar-wallets-kit +- **UI**: Framer Motion (animations), QR code generation +- **API**: tRPC client +- **Build**: Rollup with TypeScript, terser +- **Contracts**: Foundry (Solidity) +- **Testing**: tape (for pay-common package) + +## Entry Points + +Main export: `packages/connectkit/src/index.ts` +- Exports: RozoPayProvider, RozoPayButton, hooks, utilities + +World integration: `packages/connectkit/src/world.ts` +- For WorldCoin minikit integration + +## Typical Usage Pattern + +```tsx +import { RozoPayProvider, RozoPayButton } from '@rozoai/intent-pay'; + +// Wrap your app + + + + +// Use the payment button + console.log(event)} + onPaymentCompleted={(event) => console.log(event)} +/> +``` + +## Build System Notes + +- Main package uses Rollup with TypeScript plugin +- Builds to `packages/connectkit/build/` +- Type declarations generated via rollup-plugin-dts +- Bundle analysis available via rollup-plugin-visualizer +- Watch mode in development for hot reload + +## Husky & Pre-commit Hooks + +Configured in `.husky/` directory with lint-staged. + +Only lints files in `packages/connectkit/**/*.{js,jsx,ts,tsx}` on commit. + +## Package Manager + +**IMPORTANT**: This project uses pnpm with workspaces. + +Always use `pnpm` (not npm or yarn). + +Configured version: pnpm@10.26.0 (see packageManager field in root package.json) + +## Git Workflow + +Main branch: `master` + +When creating PRs, target the `master` branch. + +## Important Files to Reference + +- `.cursorrules` - Comprehensive project documentation and patterns +- `packages/connectkit/PAYMENT_FLOW.md` - Detailed payment flow diagrams and state transitions +- `packages/connectkit/README.md` - Public SDK documentation +- `CHANGELOG.md` - Version history and changes + +## Contract Information + +Smart contracts are noncustodial and audited. + +Location: `packages/contract/` + +Audit: Nethermind, 2025 Apr (see README) + +## License + +BSD-2-Clause (see LICENSE file) + +## Credits + +Forked from: +1. Daimo Pay by Daimo (https://github.com/daimo-eth/pay) +2. ConnectKit by Family (https://family.co) diff --git a/examples/nextjs-app/package.json b/examples/nextjs-app/package.json index a88b248ad..e79cd52e6 100644 --- a/examples/nextjs-app/package.json +++ b/examples/nextjs-app/package.json @@ -13,8 +13,9 @@ "@farcaster/frame-sdk": "^0.0.26", "@headlessui/react": "^2.2.0", "@rainbow-me/rainbowkit": "^2.2.8", - "@rozoai/intent-common": "0.1.7", - "@rozoai/intent-pay": "0.1.10", + "@rozoai/intent-common": "0.1.10", + "@rozoai/intent-pay": "0.1.15-beta.13", + "@stellar/stellar-sdk": "^14.4.3", "@tanstack/react-query": "^5.51.11", "@types/react-syntax-highlighter": "^15.5.13", "@wagmi/core": "^2.22.0", diff --git a/examples/nextjs-app/src/app/basic/page.tsx b/examples/nextjs-app/src/app/basic/page.tsx index 6dbd8bf7c..be1f18bf6 100644 --- a/examples/nextjs-app/src/app/basic/page.tsx +++ b/examples/nextjs-app/src/app/basic/page.tsx @@ -4,6 +4,7 @@ import * as Tokens from "@rozoai/intent-common"; import { baseEURC, FeeType, + getChainById, getChainName, getChainNativeToken, getKnownToken, @@ -14,10 +15,8 @@ import { TokenSymbol, } from "@rozoai/intent-common"; import { - isEvmChain, - isSolanaChain, - isStellarChain, RozoPayButton, + useRozoConnectStellar, useRozoPayUI, } from "@rozoai/intent-pay"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -40,8 +39,12 @@ type Config = { * Generates TypeScript code snippet for implementing RozoPayButton */ const generateCodeSnippet = (config: Config): string => { - const isEvm = isEvmChain(config.chainId); - const isSolana = isSolanaChain(config.chainId); + const chain = getChainById(config.chainId); + + if (!chain) return ""; + + const isEvm = chain.type === "evm"; + const isSolana = chain.type === "solana"; // For EVM chains, use getAddress helper // For non-EVM chains, use string directly @@ -176,6 +179,152 @@ const CodeSnippetDisplay = ({ code }: { code: string }) => { ); }; +/** + * Simple Connect Stellar Wallet Component + */ +const ConnectStellarWallet = () => { + const { + kit, + isConnected, + publicKey, + connector, + setConnector, + disconnect, + setPublicKey, + } = useRozoConnectStellar(); + const [wallets, setWallets] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [showWallets, setShowWallets] = useState(false); + + // Fetch available wallets + useEffect(() => { + const fetchWallets = async () => { + if (!kit) return; + setIsLoading(true); + try { + const availableWallets = await kit.getSupportedWallets(); + setWallets(availableWallets.filter((w: any) => w.isAvailable)); + } catch (error) { + console.error("Error fetching Stellar wallets:", error); + } finally { + setIsLoading(false); + } + }; + + fetchWallets(); + }, [kit]); + + const handleConnect = async (wallet: any) => { + try { + if (!kit) return; + + kit.setWallet(wallet.id); + const { address } = await kit.getAddress(); + setPublicKey(address); + await setConnector(wallet); + setShowWallets(false); + } catch (error) { + console.error("Error connecting wallet:", error); + } + }; + + const handleDisconnect = async () => { + try { + await disconnect(); + } catch (error) { + console.error("Error disconnecting wallet:", error); + } + }; + + if (isConnected && publicKey) { + return ( +
+

+ Stellar Wallet Connected +

+
+
+ Wallet: + + {connector?.name || "Unknown"} + +
+
+ Address: + + {publicKey} + +
+ +
+
+ ); + } + + return ( +
+

+ Connect Stellar Wallet +

+ {isLoading ? ( +

Loading wallets...

+ ) : ( + <> + {!showWallets ? ( + + ) : ( +
+ + {wallets.length === 0 ? ( +

+ No Stellar wallets detected. Please install a Stellar wallet + extension. +

+ ) : ( +
+ {wallets.map((wallet) => ( + + ))} +
+ )} +
+ )} + + )} +
+ ); +}; + export default function DemoBasic() { const [isConfigOpen, setIsConfigOpen] = useState(false); const [config, setConfig] = usePersistedConfig("rozo-basic-config", { @@ -224,7 +373,9 @@ export default function DemoBasic() { setParsedConfig(configWithSymbols); // NOTE: This is used to reset the payment state when the config changes - const isEvm = isEvmChain(newConfig.chainId); + const chain = getChainById(newConfig.chainId); + if (!chain) return; + const isEvm = chain.type === "evm"; const payParams: any = { toChain: newConfig.chainId, toUnits: newConfig.amount, @@ -248,6 +399,16 @@ export default function DemoBasic() { [setConfig, resetPayment, preferredSymbol] ); + const isSolanaChain = useCallback((chainId: number) => { + const chain = getChainById(chainId); + return chain?.type === "solana"; + }, []); + + const isStellarChain = useCallback((chainId: number) => { + const chain = getChainById(chainId); + return chain?.type === "stellar"; + }, []); + useEffect(() => { const handleEscapeKey = (event: KeyboardEvent) => { if (event.key === "Escape" && isConfigOpen) { @@ -331,6 +492,10 @@ export default function DemoBasic() { return false; } + const chain = getChainById(parsedConfig.chainId); + if (!chain) return false; + const isEvm = chain.type === "evm"; + const destinationToken = getKnownToken( parsedConfig.chainId, parsedConfig.tokenAddress @@ -339,10 +504,7 @@ export default function DemoBasic() { if (!destinationToken) return false; // Check if it's Base EURC - if ( - parsedConfig.chainId === baseEURC.chainId && - isEvmChain(parsedConfig.chainId) - ) { + if (parsedConfig.chainId === baseEURC.chainId && isEvm) { try { return ( getAddress(destinationToken.token) === getAddress(baseEURC.token) @@ -436,6 +598,9 @@ export default function DemoBasic() { )} + {/* Connect Stellar Wallet Section */} + + {/* Main Content */}
{/* Payment Button Section */} diff --git a/examples/nextjs-app/src/app/basic/providers.tsx b/examples/nextjs-app/src/app/basic/providers.tsx index 49a7f2731..ccb1de061 100644 --- a/examples/nextjs-app/src/app/basic/providers.tsx +++ b/examples/nextjs-app/src/app/basic/providers.tsx @@ -1,5 +1,9 @@ "use client"; +import { + WalletConnectAllowedMethods, + WalletConnectModule, +} from "@creit.tech/stellar-wallets-kit/modules/walletconnect.module"; import { getDefaultConfig as getDefaultConfigRozo, RozoPayProvider, @@ -8,6 +12,35 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { type ReactNode } from "react"; import { createConfig, WagmiProvider } from "wagmi"; +import { + allowAllModules, + FREIGHTER_ID, + StellarWalletsKit, + WalletNetwork, +} from "@creit.tech/stellar-wallets-kit"; + +const stellarKit = new StellarWalletsKit({ + network: WalletNetwork.PUBLIC, + selectedWalletId: FREIGHTER_ID, + modules: [ + ...allowAllModules(), + new WalletConnectModule({ + url: + typeof window !== "undefined" + ? window.location.origin + : "https://intents.rozo.ai", + projectId: "7440dd8acf85933ffcc775ec6675d4a9", + method: WalletConnectAllowedMethods.SIGN, + description: "ROZO Intents - Transfer USDC across chains", + name: "ROZO Intents", + icons: [ + "https://imagedelivery.net/AKLvTMvIg6yc9W08fHl1Tg/fdfef53e-91c2-4abc-aec0-6902a26d6c00/80x", + ], + network: WalletNetwork.PUBLIC, + }), + ], +}); + export const rozoPayConfig = createConfig( getDefaultConfigRozo({ appName: "Rozo Pay Basic Demo", @@ -20,7 +53,12 @@ export function Providers(props: { children: ReactNode }) { return ( - + {props.children} diff --git a/examples/nextjs-app/src/app/config-panel.tsx b/examples/nextjs-app/src/app/config-panel.tsx index 0717d4ba0..508edd8ea 100644 --- a/examples/nextjs-app/src/app/config-panel.tsx +++ b/examples/nextjs-app/src/app/config-panel.tsx @@ -4,12 +4,7 @@ import { supportedPayoutTokens, Token, } from "@rozoai/intent-common"; -import { - isEvmChain, - isSolanaChain, - isStellarChain, - validateAddressForChain, -} from "@rozoai/intent-pay"; +import { validateAddressForChain } from "@rozoai/intent-pay"; import { useCallback, useEffect, useMemo, useState } from "react"; import { isAddress } from "viem"; @@ -70,7 +65,9 @@ export function ConfigPanel({ // Validate token address based on chain type if (parsed.chainId !== 0) { - const isEvm = isEvmChain(parsed.chainId); + const chain = getChainById(parsed.chainId); + if (!chain) return; + const isEvm = chain.type === "evm"; if (isEvm && !isAddress(parsed.tokenAddress)) { Object.assign(parsedConfig, { tokenAddress: "", @@ -141,15 +138,7 @@ export function ConfigPanel({ const isValid = validateAddressForChain(chainId, address); if (!isValid) { - if (isEvmChain(chainId)) { - setAddressError("Invalid EVM address"); - } else if (isSolanaChain(chainId)) { - setAddressError("Invalid Solana address"); - } else if (isStellarChain(chainId)) { - setAddressError("Invalid Stellar address"); - } else { - setAddressError("Invalid address format"); - } + setAddressError("Invalid address format"); return false; } @@ -172,15 +161,10 @@ export function ConfigPanel({ e.preventDefault(); if (!config.recipientAddress) { - if (isEvmChain(config.chainId)) { - alert("Please enter a valid EVM address"); - } else if (isSolanaChain(config.chainId)) { - alert("Please enter a valid Solana address"); - } else if (isStellarChain(config.chainId)) { - alert("Please enter a valid Stellar address"); - } else { - alert("Please enter a valid recipient address"); - } + const chain = getChainById(config.chainId); + if (!chain) return; + + alert("Please enter a valid recipient address"); return; } @@ -190,15 +174,7 @@ export function ConfigPanel({ config.recipientAddress ); if (!isValid) { - if (isEvmChain(config.chainId)) { - alert("Please enter a valid EVM address (0x... format)"); - } else if (isSolanaChain(config.chainId)) { - alert("Please enter a valid Solana address (Base58 format)"); - } else if (isStellarChain(config.chainId)) { - alert("Please enter a valid Stellar address (G... format)"); - } else { - alert("Please enter a valid address"); - } + alert("Please enter a valid address"); return; } @@ -329,9 +305,6 @@ export function ConfigPanel({
{addressError && (

{addressError}

)} - {config.chainId > 0 && !addressError && ( -

- {isEvmChain(config.chainId) && - "Enter a valid EVM address (0x followed by 40 hex characters)"} - {isSolanaChain(config.chainId) && - "Enter a valid Solana address (Base58 encoded, 32-44 characters)"} - {isStellarChain(config.chainId) && - "Enter a valid Stellar address (starts with G, 56 characters)"} -

- )}
)} diff --git a/examples/nextjs-app/src/app/deposit/page.tsx b/examples/nextjs-app/src/app/deposit/page.tsx index 8d0ea0aa4..2f624d288 100644 --- a/examples/nextjs-app/src/app/deposit/page.tsx +++ b/examples/nextjs-app/src/app/deposit/page.tsx @@ -2,19 +2,14 @@ import * as Tokens from "@rozoai/intent-common"; import { + getChainById, getChainName, getChainNativeToken, knownTokens, rozoSolana, rozoStellar, } from "@rozoai/intent-common"; -import { - isEvmChain, - isSolanaChain, - isStellarChain, - RozoPayButton, - useRozoPayUI, -} from "@rozoai/intent-pay"; +import { RozoPayButton, useRozoPayUI } from "@rozoai/intent-pay"; import { useEffect, useState } from "react"; import { Address, getAddress } from "viem"; import { Text, TextLink } from "../../shared/tailwind-catalyst/text"; @@ -46,7 +41,11 @@ export default function DemoDeposit() { setParsedConfig(config); // NOTE: This is used to reset the payment state when the config changes - const isEvm = isEvmChain(config.chainId); + const chain = getChainById(config.chainId); + if (!chain) return; + const isEvm = chain.type === "evm"; + const isSolana = chain.type === "solana"; + const isStellar = chain.type === "stellar"; const payParams: any = { toChain: config.chainId, }; @@ -57,9 +56,9 @@ export default function DemoDeposit() { } else { payParams.toAddress = config.recipientAddress; payParams.toToken = config.tokenAddress; - if (isStellarChain(config.chainId)) { + if (isStellar) { payParams.toStellarAddress = config.recipientAddress; - } else if (isSolanaChain(config.chainId)) { + } else if (isSolana) { payParams.toSolanaAddress = config.recipientAddress; } } @@ -121,15 +120,11 @@ export default function DemoDeposit() { return; } - const isEvm = isEvmChain(parsedConfig.chainId); - const isSolana = isSolanaChain(parsedConfig.chainId); - const isStellar = isStellarChain(parsedConfig.chainId); - - // For EVM chains, use getAddress helper - // For non-EVM chains, use string directly - const addressCode = isEvm - ? `getAddress("${parsedConfig.recipientAddress}")` - : `"${parsedConfig.recipientAddress}"`; + const chain = getChainById(parsedConfig.chainId); + if (!chain) return; + const isEvm = chain.type === "evm"; + const isSolana = chain.type === "solana"; + const isStellar = chain.type === "stellar"; // First check if it's a native token (address is 0x0) if ( @@ -164,7 +159,11 @@ import { RozoPayButton } from "@rozoai/intent-pay"; `; @@ -201,7 +200,11 @@ import { RozoPayButton } from "@rozoai/intent-pay"; `; @@ -209,6 +212,16 @@ import { RozoPayButton } from "@rozoai/intent-pay"; } }, [parsedConfig, hasValidConfig]); + const isEvm = parsedConfig?.chainId + ? getChainById(parsedConfig.chainId)?.type === "evm" + : false; + const isSolana = parsedConfig?.chainId + ? getChainById(parsedConfig.chainId)?.type === "solana" + : false; + const isStellar = parsedConfig?.chainId + ? getChainById(parsedConfig.chainId)?.type === "stellar" + : false; + return ( @@ -223,12 +236,12 @@ import { RozoPayButton } from "@rozoai/intent-pay"; appId={APP_ID} toChain={parsedConfig.chainId} toAddress={ - isEvmChain(parsedConfig.chainId) + isEvm ? (getAddress(parsedConfig.recipientAddress) as Address) : parsedConfig.recipientAddress } toToken={ - isEvmChain(parsedConfig.chainId) + isEvm ? (getAddress(parsedConfig.tokenAddress) as Address) : parsedConfig.tokenAddress } @@ -283,7 +296,7 @@ import { RozoPayButton } from "@rozoai/intent-pay"; defaultRecipientAddress={config.recipientAddress} /> - {parsedConfig && isStellarChain(parsedConfig.chainId) && ( + {parsedConfig && isStellar && (

ℹ️ Stellar Deposit Configuration @@ -306,7 +319,7 @@ import { RozoPayButton } from "@rozoai/intent-pay";

)} - {parsedConfig && isSolanaChain(parsedConfig.chainId) && ( + {parsedConfig && isSolana && (

ℹ️ Solana Deposit Configuration diff --git a/package.json b/package.json index 68daf93eb..31edfe53c 100644 --- a/package.json +++ b/package.json @@ -5,18 +5,20 @@ "description": "", "main": "index.js", "scripts": { - "build": "pnpm --filter @rozoai/intent-common build && pnpm --filter @rozoai/intent-pay build", - "dev": "pnpm -r --parallel --filter '@rozoai/*' dev", - "dev:common": "pnpm --filter @rozoai/intent-common build --watch", - "dev:pay": "pnpm --filter @rozoai/intent-pay dev", - "dev:example": "pnpm --filter @rozoai/pay-nextjs-app-example dev", - "install:local": "pnpm install", + "build": "bun run build:common && bun run build:pay", + "build:common": "cd packages/pay-common && bun run build", + "build:pay": "cd packages/connectkit && bun run build", + "dev": "bun run dev:common & bun run dev:pay & bun run dev:example", + "dev:common": "cd packages/pay-common && bun run dev", + "dev:pay": "cd packages/connectkit && bun run dev", + "dev:example": "cd examples/nextjs-app && bun run dev", + "install:local": "bun install", "prepare": "husky", - "release": "pnpm build && pnpm --filter @rozoai/intent-common publish && pnpm --filter @rozoai/intent-pay release", - "clean": "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +", - "clean:full": "pnpm clean && rm -f bun.lock", - "clean:deps": "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' + && find . -name 'dist' -type d -prune -exec rm -rf '{}' + && find . -name 'build' -type d -prune -exec rm -rf '{}' + && find . -name '.next' -type d -prune -exec rm -rf '{}' +", - "clean:all": "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' + && find . -name 'dist' -type d -prune -exec rm -rf '{}' + && find . -name 'build' -type d -prune -exec rm -rf '{}' + && find . -name '.next' -type d -prune -exec rm -rf '{}' + && rm -f pnpm-lock.yaml" + "release": "bun run build && cd packages/pay-common && bun publish && cd ../../packages/connectkit && bun run release", + "clean": "bun x rimraf **/node_modules", + "clean:full": "bun run clean && rm -f bun.lock", + "clean:deps": "bun x rimraf **/node_modules **/dist **/build **/.next", + "clean:all": "bun run clean:deps && rm -f bun.lock && rm -f pnpm-lock.yaml" }, "lint-staged": { "packages/connectkit/**/*.{js,jsx,ts,tsx}": [ @@ -39,5 +41,25 @@ "packages/*", "examples/nextjs-app" ], + "pnpm": { + "overrides": { + "zod": "^3.25.76", + "@stellar/stellar-sdk": "^14.4.3", + "rollup": "^3.29.5", + "bs58": "^6.0.0", + "@solana/sysvars": "^3.0.3" + }, + "peerDependencyRules": { + "allowedVersions": { + "@stellar/stellar-sdk": "14", + "rollup": "3", + "react": "18", + "@types/react": "18" + }, + "ignoreMissing": [ + "react-native" + ] + } + }, "packageManager": "pnpm@10.26.0+sha512.3b3f6c725ebe712506c0ab1ad4133cf86b1f4b687effce62a9b38b4d72e3954242e643190fc51fa1642949c735f403debd44f5cb0edd657abe63a8b6a7e1e402" } diff --git a/packages/connectkit/PAYMENT_FLOW.md b/packages/connectkit/PAYMENT_FLOW.md new file mode 100644 index 000000000..98337d33f --- /dev/null +++ b/packages/connectkit/PAYMENT_FLOW.md @@ -0,0 +1,355 @@ +# Payment Flow Documentation + +## Overview + +This document describes the payment flow architecture in the ConnectKit payment system, including state management, payment methods, and cross-chain payment handling. + +--- + +## Payment States + +The system manages payment state through a centralized state machine: + +```mermaid +stateDiagram-v2 + [*] --> preview: createPreviewOrder() + preview --> payment_unpaid: setPaymentUnpaid() + payment_unpaid --> payment_started: setPaymentStarted() + payment_started --> payment_completed: setPaymentCompleted() + payment_started --> error: Payment fails + payment_started --> payment_unpaid: Cancel/Reset + error --> payment_unpaid: Retry + payment_completed --> [*] + + note right of preview + Order created but not initiated + User can modify parameters + end note + + note right of payment_unpaid + Payment ID assigned + Ready to start payment + end note + + note right of payment_started + Payment in progress + Transaction being processed + end note +``` + +--- + +## Main Payment Flow + +### 1. Initial Setup + +```mermaid +flowchart TD + Start([User Initiates Payment]) --> CheckParams{Valid PayParams?} + CheckParams -->|No| Error[Show Error] + CheckParams -->|Yes| CreatePreview[createPreviewOrder] + CreatePreview --> RouteSelect[Route to SELECT_METHOD] + RouteSelect --> SelectMethod[User Selects Payment Method] + + SelectMethod --> CheckChain{Payment Chain?} + CheckChain -->|EVM Chain| TokenFlow[Token Payment Flow] + CheckChain -->|Stellar| StellarFlow[Stellar Payment Flow] + CheckChain -->|Solana| SolanaFlow[Solana Payment Flow] +``` + +### 2. Token Payment Flow (EVM Chains) + +```mermaid +flowchart TD + Start([PayWithToken Component]) --> SelectToken[User Selects Token Option] + SelectToken --> CheckCrossChain{Cross-chain
payment?} + + CheckCrossChain -->|No| DirectPayment[Direct Payment] + CheckCrossChain -->|Yes| CreateRozoPayment[Create Rozo Payment] + + CreateRozoPayment --> HydrateOrder[Hydrate Order] + DirectPayment --> HydrateOrder + + HydrateOrder --> StateTransition{Current State?} + StateTransition -->|preview| SetUnpaid[setPaymentUnpaid] + StateTransition -->|payment_unpaid| SetStarted[setPaymentStarted] + StateTransition -->|payment_started| CheckSwitch{Chain switch?} + + CheckSwitch -->|Yes| ResetOld[setPaymentUnpaid old] + CheckSwitch -->|No| SetStarted + ResetOld --> SetStarted + + SetUnpaid --> SetStarted + SetStarted --> WaitWallet[Wait for Wallet Confirmation] + WaitWallet --> Complete[setPaymentCompleted] + Complete --> ConfirmRoute[Route to CONFIRMATION] +``` + +### 3. Stellar Payment Flow + +```mermaid +flowchart TD + Start([PayWithStellarToken Component]) --> ValidateParams{Valid payParams?} + ValidateParams -->|No| Skip[Skip transfer - stale state] + ValidateParams -->|Yes| ValidateDest{Valid destination
address?} + + ValidateDest -->|No| ErrorMsg[Throw Error: Address Required] + ValidateDest -->|Yes| CheckOrder{Order
initialized?} + + CheckOrder -->|No| ErrorMsg2[Throw Error: Order Not Init] + CheckOrder -->|Yes| CheckCrossChain{Cross-chain
payment?} + + CheckCrossChain -->|Yes| CreatePayment[createPayment] + CheckCrossChain -->|No| CheckState{Current
State?} + + CreatePayment --> FormatOrder[formatPaymentResponseToHydratedOrder] + CheckState -->|payment_unpaid or
payment_started| UseExisting[Use Existing Order] + CheckState -->|Other| HydrateExisting[hydrateOrder] + + FormatOrder --> ValidateHydrated{Hydrated
Order Valid?} + UseExisting --> ValidateHydrated + HydrateExisting --> ValidateHydrated + + ValidateHydrated -->|No| ErrorMsg3[Throw Error: Payment Not Found] + ValidateHydrated -->|Yes| SetPaymentId[setRozoPaymentId] + + SetPaymentId --> StateCheck{Check Store State} + StateCheck -->|payment_started
+ new payment| TransitionUnpaid[setPaymentUnpaid old] + StateCheck -->|preview| PreviewToUnpaid[setPaymentUnpaid → setPaymentStarted] + StateCheck -->|payment_unpaid| DirectStart[setPaymentStarted] + StateCheck -->|Already started| Continue[Continue] + + TransitionUnpaid --> StartNew[setPaymentStarted new] + PreviewToUnpaid --> RequestPayment + DirectStart --> RequestPayment + StartNew --> RequestPayment + Continue --> RequestPayment + + RequestPayment[Request Payment: payWithStellarToken] --> SignTx[Sign Transaction] + SignTx --> WaitConfirm[Wait for User Confirmation] + WaitConfirm --> Submit[Submit to Stellar Network] + Submit --> CheckSuccess{Transaction
Successful?} + + CheckSuccess -->|Yes| SetCompleted[setPaymentCompleted] + CheckSuccess -->|No| HandleError[Handle Error] + + HandleError --> CheckOrderExists{Order & Payment ID
exist?} + CheckOrderExists -->|Yes| ResetToUnpaid[setPaymentUnpaid with order] + CheckOrderExists -->|No| LogError[Log: Cannot set unpaid] + + ResetToUnpaid --> CheckRejected{User
Rejected?} + LogError --> CheckRejected + CheckRejected -->|Yes| StateCancelled[State: RequestCancelled] + CheckRejected -->|No| StateFailed[State: RequestFailed] + + SetCompleted --> RouteConfirm[Route to CONFIRMATION] +``` + +### 4. Solana Payment Flow + +The Solana payment flow is identical to the Stellar flow, with these differences: +- Uses `PayWithSolanaToken` component +- Uses `payWithSolanaToken` instead of `payWithStellarToken` +- Validates Solana addresses instead of Stellar addresses +- Submits to Solana network instead of Stellar + +--- + +## Reset Payment Flow + +```mermaid +flowchart TD + Start([resetPayment Called]) --> MergeParams[Merge New PayParams with Current] + MergeParams --> ClearState[Clear Old State] + + ClearState --> ClearOptions[setSelectedStellarTokenOption undefined
setSelectedSolanaTokenOption undefined
setSelectedTokenOption undefined] + ClearOptions --> ClearWallet[setSelectedWallet undefined
setSelectedWalletDeepLink undefined] + ClearWallet --> ResetPay[pay.reset] + + ResetPay --> CheckNewParams{New PayParams
Provided?} + CheckNewParams -->|No| RouteSelect[Route to SELECT_METHOD] + CheckNewParams -->|Yes| ConvertSymbols[Convert preferredSymbol to preferredTokens] + + ConvertSymbols --> CheckChain{Destination
Chain?} + CheckChain -->|Stellar| SetStellarAddr[toStellarAddress = toAddress
toAddress = 0x0
toSolanaAddress = undefined] + CheckChain -->|Solana| SetSolanaAddr[toSolanaAddress = toAddress
toAddress = 0x0
toStellarAddress = undefined] + CheckChain -->|EVM| ClearSpecial[toStellarAddress = undefined
toSolanaAddress = undefined] + + SetStellarAddr --> CreatePreview[createPreviewOrder] + SetSolanaAddr --> CreatePreview + ClearSpecial --> CreatePreview + + CreatePreview --> UpdateParams[setCurrPayParams] + UpdateParams --> RouteSelect +``` + +--- + +## State Transition Rules + +### From Preview State +- Can transition to: `payment_unpaid` +- Requires: Order data +- Action: `setPaymentUnpaid(paymentId, order)` + +### From Payment Unpaid State +- Can transition to: `payment_started` +- Requires: Payment ID and order +- Action: `setPaymentStarted(paymentId, hydratedOrder)` + +### From Payment Started State +- Can transition to: + - `payment_completed` (success) + - `payment_unpaid` (cancel/reset) + - `error` (failure) +- Special case: Cross-chain switch requires transition to `payment_unpaid` first + +### From Error State +- Cannot call `setPaymentUnpaid` without providing order +- Must provide both `paymentId` and `order` parameters + +--- + +## Cross-Chain Payment Handling + +```mermaid +flowchart TD + Start([User Selects Token]) --> CheckPreferred{Order has
preferredChainId?} + CheckPreferred -->|No| DirectPayment[Direct Payment on Selected Chain] + CheckPreferred -->|Yes| CheckMatch{preferredChainId ==
selectedToken.chainId?} + + CheckMatch -->|Yes| DirectPayment + CheckMatch -->|No| CrossChain[Cross-Chain Payment Required] + + CrossChain --> CreateRozo[createPayment via Rozo] + CreateRozo --> GetNewId[Get New Payment ID] + GetNewId --> CheckCurrentState{Current State?} + + CheckCurrentState -->|payment_started| HandleSwitch[Handle Chain Switch] + CheckCurrentState -->|Other| NormalFlow[Normal Flow] + + HandleSwitch --> UnpaidOld[setPaymentUnpaid old payment] + UnpaidOld --> StartNew[setPaymentStarted new payment] + + NormalFlow --> StartNew + DirectPayment --> UseExisting[Use Existing Order] +``` + +--- + +## Key Components + +### usePaymentState Hook +- **Location**: `packages/connectkit/src/hooks/usePaymentState.ts` +- **Responsibilities**: + - Manages `currPayParams` state + - Handles `resetOrder` logic + - Clears selected options on reset + - Routes to appropriate payment flow + +### useStellarDestination Hook +- **Location**: `packages/connectkit/src/hooks/useStellarDestination.ts` +- **Responsibilities**: + - Derives destination address from `payParams` + - Determines payment direction (Stellar → Base, Base → Stellar, etc.) + - Returns memoized values based on `payParams` + +### Payment Components +1. **PayWithToken** - EVM chain payments +2. **PayWithStellarToken** - Stellar network payments +3. **PayWithSolanaToken** - Solana network payments + +--- + +## Common Issues & Solutions + +### Issue: Stale Destination Address After Reset + +**Symptom**: After `resetPayment`, component uses old destination address from previous payment attempt. + +**Root Cause**: +- `destinationAddress` from `useStellarDestination(payParams)` is memoized +- React batches state updates, so component may not re-render with new `payParams` before `useEffect` triggers + +**Solution**: +1. Validate `payParams` exists before processing transfer +2. Add logging to track destination address and chain info +3. Check for stale state and skip processing if detected + +```typescript +// Validate we have current payParams - if not, component has stale state +if (!payParams) { + log?.("[Component] No payParams available, skipping transfer"); + setIsLoading(false); + return; +} +``` + +### Issue: setPaymentUnpaid Error When State is "error" + +**Symptom**: `Error: Cannot set payment unpaid: Order must be provided when state is error` + +**Root Cause**: +- Error handler calls `setPaymentUnpaid(paymentId)` without order parameter +- When state is "error", order must be provided + +**Solution**: +```typescript +if (rozoPaymentId && order && 'org' in order) { + try { + await setPaymentUnpaid(rozoPaymentId, order as any); + } catch (e) { + console.error("Failed to set payment unpaid:", e); + } +} else { + log?.(`Cannot set payment unpaid - missing requirements`); +} +``` + +--- + +## Payment State Validation + +### Required Checks Before Payment +1. ✅ `payParams` exists and is current +2. ✅ `destinationAddress` is valid for target chain +3. ✅ `order` is initialized +4. ✅ Selected token option matches payment parameters +5. ✅ Wallet is connected (for blockchain payments) + +### State Transition Validation +1. ✅ Cannot go from `error` to `payment_unpaid` without order +2. ✅ Cannot go from `preview` to `payment_started` without going through `payment_unpaid` +3. ✅ Cross-chain switch requires resetting old payment before starting new one + +--- + +## Debugging Tips + +### Enable Logging +Look for log statements in the components: +```typescript +log?.(`[PayWithStellarToken] Payment setup - destAddress: ${finalDestAddress}, toChain: ${payParams.toChain}, token chain: ${option.required.token.chainId}`); +``` + +### Check State Transitions +Monitor the payment state in Redux DevTools or logs: +```typescript +const currentState = store.getState().type; +console.log('Current payment state:', currentState); +``` + +### Validate payParams +Check if `payParams` are being updated correctly: +```typescript +console.log('payParams:', JSON.stringify(payParams, null, 2)); +``` + +### Check Destination Address +Verify destination address derivation: +```typescript +const { destinationAddress } = useStellarDestination(payParams); +console.log('Destination:', destinationAddress); +console.log('ToChain:', payParams?.toChain); +console.log('ToStellarAddress:', payParams?.toStellarAddress); +``` diff --git a/packages/connectkit/bundle-analysis.html b/packages/connectkit/bundle-analysis.html index be33d2e77..759f31475 100644 --- a/packages/connectkit/bundle-analysis.html +++ b/packages/connectkit/bundle-analysis.html @@ -4929,7 +4929,7 @@