From b3c9e9f8a40351676e81016d5f2dc6f7acd77bf5 Mon Sep 17 00:00:00 2001 From: Soos3D <99700157+soos3d@users.noreply.github.com> Date: Wed, 3 Dec 2025 21:17:58 -0300 Subject: [PATCH 1/4] feat: add @account-kit/universal-account package with Particle Network integration - Add Universal Account provider and hooks (useUniversalAccount, useUnifiedBalance, useSendTransaction) - Support all transaction types: transfer, universal, buy, sell, convert - Add CHAIN_ID and TOKEN_TYPE constants for supported chains - Include Next.js demo app with comprehensive documentation - Add step-by-step comments explaining the integration --- account-kit/universal-account/README.md | 608 ++++++++++++++++++ .../examples/next-example/.gitignore | 42 ++ .../examples/next-example/README.md | 281 ++++++++ .../app/components/UniversalAccountDemo.tsx | 351 ++++++++++ .../examples/next-example/app/favicon.ico | Bin 0 -> 25931 bytes .../examples/next-example/app/globals.css | 27 + .../examples/next-example/app/layout.tsx | 43 ++ .../examples/next-example/app/page.tsx | 119 ++++ .../examples/next-example/app/providers.tsx | 46 ++ .../examples/next-example/config.ts | 69 ++ .../examples/next-example/eslint.config.mjs | 25 + .../examples/next-example/next.config.ts | 7 + .../examples/next-example/package.json | 33 + .../examples/next-example/postcss.config.mjs | 5 + .../examples/next-example/public/file.svg | 1 + .../examples/next-example/public/globe.svg | 1 + .../examples/next-example/public/next.svg | 1 + .../examples/next-example/public/vercel.svg | 1 + .../examples/next-example/public/window.svg | 1 + .../examples/next-example/tailwind.config.ts | 14 + .../examples/next-example/tsconfig.json | 27 + account-kit/universal-account/package.json | 72 +++ account-kit/universal-account/src/client.ts | 450 +++++++++++++ .../universal-account/src/constants.ts | 86 +++ account-kit/universal-account/src/index.ts | 49 ++ .../universal-account/src/particle-sdk.d.ts | 144 +++++ .../universal-account/src/provider.tsx | 552 ++++++++++++++++ account-kit/universal-account/src/types.ts | 284 ++++++++ .../universal-account/tsconfig.build.json | 10 + account-kit/universal-account/tsconfig.json | 10 + yarn.lock | 214 ++++-- 31 files changed, 3526 insertions(+), 47 deletions(-) create mode 100644 account-kit/universal-account/README.md create mode 100644 account-kit/universal-account/examples/next-example/.gitignore create mode 100644 account-kit/universal-account/examples/next-example/README.md create mode 100644 account-kit/universal-account/examples/next-example/app/components/UniversalAccountDemo.tsx create mode 100644 account-kit/universal-account/examples/next-example/app/favicon.ico create mode 100644 account-kit/universal-account/examples/next-example/app/globals.css create mode 100644 account-kit/universal-account/examples/next-example/app/layout.tsx create mode 100644 account-kit/universal-account/examples/next-example/app/page.tsx create mode 100644 account-kit/universal-account/examples/next-example/app/providers.tsx create mode 100644 account-kit/universal-account/examples/next-example/config.ts create mode 100644 account-kit/universal-account/examples/next-example/eslint.config.mjs create mode 100644 account-kit/universal-account/examples/next-example/next.config.ts create mode 100644 account-kit/universal-account/examples/next-example/package.json create mode 100644 account-kit/universal-account/examples/next-example/postcss.config.mjs create mode 100644 account-kit/universal-account/examples/next-example/public/file.svg create mode 100644 account-kit/universal-account/examples/next-example/public/globe.svg create mode 100644 account-kit/universal-account/examples/next-example/public/next.svg create mode 100644 account-kit/universal-account/examples/next-example/public/vercel.svg create mode 100644 account-kit/universal-account/examples/next-example/public/window.svg create mode 100644 account-kit/universal-account/examples/next-example/tailwind.config.ts create mode 100644 account-kit/universal-account/examples/next-example/tsconfig.json create mode 100644 account-kit/universal-account/package.json create mode 100644 account-kit/universal-account/src/client.ts create mode 100644 account-kit/universal-account/src/constants.ts create mode 100644 account-kit/universal-account/src/index.ts create mode 100644 account-kit/universal-account/src/particle-sdk.d.ts create mode 100644 account-kit/universal-account/src/provider.tsx create mode 100644 account-kit/universal-account/src/types.ts create mode 100644 account-kit/universal-account/tsconfig.build.json create mode 100644 account-kit/universal-account/tsconfig.json diff --git a/account-kit/universal-account/README.md b/account-kit/universal-account/README.md new file mode 100644 index 0000000000..c915a6a375 --- /dev/null +++ b/account-kit/universal-account/README.md @@ -0,0 +1,608 @@ +# @account-kit/universal-account + +Universal Account integration for Alchemy Account Kit, enabling chain abstraction with [Particle Network's Universal Accounts](https://developers.particle.network/universal-accounts/cha/overview). + +## Overview + +Universal Accounts provide users with a single account, balance, and interaction point across all supported chains (EVM + Solana). This package seamlessly integrates Universal Accounts into Alchemy Account Kit. + +### Key Features + +- **Seamless Integration**: Works naturally with Account Kit's authentication +- **Unified Balance**: View and use assets across all chains as a single balance +- **Cross-Chain Transactions**: Send transactions to any chain without manual bridging +- **Universal Gas**: Pay gas fees with any supported token + +## Installation + +```bash +yarn add @account-kit/universal-account + +# or with npm +npm install @account-kit/universal-account +``` + +## Prerequisites + +You'll need credentials from both dashboards: + +**Alchemy** (for authentication): +- Get your API key from [Alchemy Dashboard](https://dashboard.alchemy.com) + +**Particle Network** (for Universal Accounts): +1. Sign up at [Particle Dashboard](https://dashboard.particle.network/) +2. Create a project and web application +3. Copy your **Project ID**, **Client Key**, and **App ID** + +## Understanding the Architecture + +When integrating Alchemy Account Kit with Universal Accounts, it's important to understand the different account types: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Alchemy Account Kit │ +├─────────────────────────────────────────────────────────────────┤ +│ useUser() → user.address = EOA (Externally Owned Account) │ +│ useAccount() → address = SCA (Smart Contract Account) │ +│ useSigner() → Signs messages with the EOA │ +└─────────────────────────────────────────────────────────────────┘ + │ + │ EOA address (user.address) + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Universal Accounts │ +├─────────────────────────────────────────────────────────────────┤ +│ Owner: EOA from Alchemy (user.address) │ +│ Creates: Multi-chain smart accounts (EVM + Solana) │ +│ Provides: Unified balance, cross-chain transactions │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Key Concept**: Alchemy's SCA and Universal Account's smart accounts are **different**! +- Use Alchemy for authentication and getting the EOA +- Use Universal Accounts for cross-chain operations + +## Quick Start + +### 1. Set Up Providers + +Wrap your app with both `AlchemyAccountProvider` and `UniversalAccountProvider`: + +```tsx +// providers.tsx +"use client"; + +import { AlchemyAccountProvider } from "@account-kit/react"; +import { UniversalAccountProvider } from "@account-kit/universal-account"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { config, queryClient, universalAccountConfig } from "./config"; + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); +} +``` + +```tsx +// config.ts +import { createConfig, cookieStorage } from "@account-kit/react"; +import { mainnet, alchemy } from "@account-kit/infra"; +import { QueryClient } from "@tanstack/react-query"; + +export const config = createConfig({ + transport: alchemy({ apiKey: process.env.NEXT_PUBLIC_ALCHEMY_API_KEY! }), + chain: mainnet, + ssr: true, + storage: cookieStorage, +}); + +export const queryClient = new QueryClient(); + +export const universalAccountConfig = { + projectId: process.env.NEXT_PUBLIC_PARTICLE_PROJECT_ID!, + clientKey: process.env.NEXT_PUBLIC_PARTICLE_CLIENT_KEY!, + appId: process.env.NEXT_PUBLIC_PARTICLE_APP_ID!, +}; +``` + +### 2. Get the EOA Address + +**Important**: Use `user.address` (EOA) from `useUser()`, not the SCA from `useAccount()`: + +```tsx +import { useUser, useSigner } from "@account-kit/react"; + +function MyComponent() { + const user = useUser(); + const signer = useSigner(); + + // ✅ CORRECT: Use EOA for Universal Accounts + const eoaAddress = user?.address as `0x${string}` | undefined; + + // ❌ WRONG: Don't use SCA for Universal Accounts + // const { address } = useAccount({ type: "LightAccount" }); + + return ; +} +``` + +### 3. Initialize Universal Account + +The `useUniversalAccount` hook auto-initializes when you pass the EOA address: + +```tsx +import { useUser, useSigner } from "@account-kit/react"; +import { useUniversalAccount, useUnifiedBalance } from "@account-kit/universal-account"; + +function Dashboard() { + const user = useUser(); + const eoaAddress = user?.address as `0x${string}` | undefined; + + // Universal Account auto-initializes with the EOA address + const { + address, // Universal Account EVM address + solanaAddress, // Universal Account Solana address + isReady, + isInitializing, + error + } = useUniversalAccount(eoaAddress); + + // Get unified balance across all chains + const { totalBalanceUSD, assets, isLoading, refetch } = useUnifiedBalance(); + + if (isInitializing) return
Initializing Universal Account...
; + if (error) return
Error: {error.message}
; + if (!isReady) return null; + + return ( +
+

Universal Account

+

EVM Address: {address}

+

Solana Address: {solanaAddress}

+ +

Unified Balance: ${totalBalanceUSD?.toFixed(2)}

+ {assets?.map((asset) => ( +
+ {asset.tokenType}: {asset.amount} (${asset.amountInUSD.toFixed(2)}) +
+ ))} + + +
+ ); +} +``` + +### 4. Send Transactions + +Use `useSendTransaction` to send cross-chain transactions: + +```tsx +import { useUser, useSigner } from "@account-kit/react"; +import { useSendTransaction } from "@account-kit/universal-account"; +import { toBytes, encodeFunctionData } from "viem"; + +function MintNFT() { + const signer = useSigner(); + + const { sendUniversal, isLoading, error, lastResult } = useSendTransaction({ + signMessage: async (message: string) => { + if (!signer) throw new Error("Signer not available"); + // Sign the raw hash bytes + return await signer.signMessage({ raw: toBytes(message) }); + }, + }); + + const handleMint = async () => { + const NFT_CONTRACT = "0xdea7bF60E53CD578e3526F36eC431795f7EEbFe6"; + const AVALANCHE_CHAIN_ID = 43114; + + const mintData = encodeFunctionData({ + abi: [{ type: "function", name: "mint", inputs: [], outputs: [] }], + functionName: "mint", + }); + + const result = await sendUniversal({ + chainId: AVALANCHE_CHAIN_ID, + expectTokens: [], // No tokens needed for free mint + transactions: [ + { + to: NFT_CONTRACT, + data: mintData, + }, + ], + }); + + console.log("Transaction ID:", result.transactionId); + console.log("View on UniversalX:", `https://universalx.app/activity/details?id=${result.transactionId}`); + }; + + return ( +
+ + {lastResult && ( + + View Transaction + + )} + {error &&

Error: {error.message}

} +
+ ); +} +``` + +--- + +## API Reference + +### Constants + +The package exports helpful constants for chain IDs and token types: + +```typescript +import { CHAIN_ID, TOKEN_TYPE, NATIVE_TOKEN_ADDRESS } from "@account-kit/universal-account"; + +// Use chain IDs +const tx = await sendUniversal({ + chainId: CHAIN_ID.AVALANCHE, // 43114 + // ... +}); + +// Available chains: +CHAIN_ID.ETHEREUM // 1 +CHAIN_ID.BNB_CHAIN // 56 +CHAIN_ID.BASE // 8453 +CHAIN_ID.ARBITRUM // 42161 +CHAIN_ID.AVALANCHE // 43114 +CHAIN_ID.OPTIMISM // 10 +CHAIN_ID.POLYGON // 137 +CHAIN_ID.LINEA // 59144 +CHAIN_ID.BERACHAIN // 80094 +CHAIN_ID.SOLANA // 101 +// ... and more + +// Token types for expectTokens +TOKEN_TYPE.ETH +TOKEN_TYPE.USDC +TOKEN_TYPE.USDT +TOKEN_TYPE.SOL + +// Native token address (for ETH, AVAX, etc.) +NATIVE_TOKEN_ADDRESS // "0x0000000000000000000000000000000000000000" +``` + +--- + +### Provider + +#### `UniversalAccountProvider` + +Wrap your app to enable Universal Account functionality. Must be nested inside `AlchemyAccountProvider`. + +```tsx + + {children} + +``` + +--- + +### Hooks + +#### `useUniversalAccount(ownerAddress?)` + +Initialize and manage a Universal Account. Auto-initializes when `ownerAddress` is provided. + +**Parameters:** +| Parameter | Type | Description | +|-----------|------|-------------| +| `ownerAddress` | `Address \| undefined` | The EOA address from `useUser().address` | + +**Returns:** +| Property | Type | Description | +|----------|------|-------------| +| `client` | `UniversalAccountClient \| null` | The UA client instance | +| `address` | `Address \| null` | Universal Account EVM address | +| `solanaAddress` | `string \| null` | Universal Account Solana address | +| `isReady` | `boolean` | True when UA is initialized and ready | +| `isInitializing` | `boolean` | True during initialization | +| `error` | `Error \| null` | Any initialization error | +| `initialize` | `(ownerAddress: Address) => Promise` | Manual initialization | +| `disconnect` | `() => void` | Reset the Universal Account | + +**Example:** +```tsx +const user = useUser(); +const { address, solanaAddress, isReady, error } = useUniversalAccount( + user?.address as `0x${string}` +); +``` + +--- + +#### `useUnifiedBalance(options?)` + +Fetch the unified balance across all chains. Automatically fetches when the Universal Account is ready. + +**Parameters:** +| Option | Type | Description | +|--------|------|-------------| +| `refetchInterval` | `number` | Auto-refresh interval in milliseconds | + +**Returns:** +| Property | Type | Description | +|----------|------|-------------| +| `balance` | `PrimaryAssets \| null` | Full balance object | +| `totalBalanceUSD` | `number \| null` | Total balance in USD | +| `assets` | `AssetInfo[] \| null` | Array of individual assets | +| `isLoading` | `boolean` | True while fetching | +| `error` | `Error \| null` | Any fetch error | +| `refetch` | `() => void` | Manually refresh balance | + +**Asset Structure:** +```typescript +interface AssetInfo { + tokenType: string; // e.g., "USDT", "ETH" + price: number; // Current price in USD + amount: string; // Total amount across chains + amountInUSD: number; // Total value in USD + chainAggregation: { // Breakdown by chain + chainId: number; + address: string; + amount: string; + amountInUSD: number; + }[]; +} +``` + +**Example:** +```tsx +const { totalBalanceUSD, assets, refetch, isLoading } = useUnifiedBalance({ + refetchInterval: 30000, // Refresh every 30 seconds +}); +``` + +--- + +#### `useSendTransaction(options)` + +Send Universal Account transactions with automatic signing flow. Supports all transaction types: + +**Parameters:** +| Option | Type | Description | +|--------|------|-------------| +| `signMessage` | `(message: string) => Promise` | Function to sign the transaction hash | + +**Returns:** +| Property | Type | Description | +|----------|------|-------------| +| `sendTransfer` | `(params) => Promise` | Send a token transfer | +| `sendUniversal` | `(params) => Promise` | Send a custom contract interaction | +| `sendBuy` | `(params) => Promise` | Buy/swap into a target token | +| `sendSell` | `(params) => Promise` | Sell a token back to primary assets | +| `sendConvert` | `(params) => Promise` | Convert between primary assets | +| `isLoading` | `boolean` | True while transaction is pending | +| `error` | `Error \| null` | Any transaction error | +| `lastResult` | `TransactionResult \| null` | Result of last transaction | +| `isReady` | `boolean` | True when ready to send | + +**Transaction Types:** + +##### `sendTransfer` - Token Transfer +Send tokens to any address across chains. +```typescript +await sendTransfer({ + token: { + chainId: 42161, // Arbitrum + address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT + }, + amount: "10", // Human-readable amount + receiver: "0x...", // Recipient address +}); +``` + +##### `sendUniversal` - Custom Contract Interaction +Execute any contract call with automatic liquidity routing. +```typescript +await sendUniversal({ + chainId: 8453, // Base + expectTokens: [ // Tokens needed (for payable functions) + { type: "ETH", amount: "0.0001" }, + ], + transactions: [{ + to: "0x...", + data: encodeFunctionData({ ... }), + value: "0x...", // Optional: for payable functions + }], +}); +``` + +##### `sendBuy` - Buy/Swap Token +Buy a token using USD value from your primary assets. +```typescript +await sendBuy({ + token: { + chainId: 42161, // Arbitrum + address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT + }, + amountInUSD: "10", // Spend $10 worth of primary assets +}); +``` + +##### `sendSell` - Sell Token +Sell a token back into primary assets. +```typescript +await sendSell({ + token: { + chainId: 42161, // Arbitrum + address: "0x912CE59144191C1204E64559FE8253a0e49E6548", // ARB + }, + amount: "0.1", // Sell 0.1 ARB +}); +``` + +##### `sendConvert` - Convert Primary Assets +Convert between primary assets on a specific chain. +```typescript +await sendConvert({ + expectToken: { type: "USDC", amount: "1" }, + chainId: 42161, // Arbitrum +}); +``` + +**Solana Support:** +All transaction types work with Solana. Use chain ID for Solana mainnet and the appropriate token addresses: +```typescript +// Buy SOL or Solana tokens +await sendBuy({ + token: { + chainId: 1399811149, // Solana mainnet + address: "0x0000000000000000000000000000000000000000", // Native SOL + }, + amountInUSD: "1", +}); +``` + +**Full Example:** +```tsx +const signer = useSigner(); + +const { sendTransfer, sendBuy, sendUniversal, isLoading } = useSendTransaction({ + signMessage: async (message) => { + return await signer!.signMessage({ raw: toBytes(message) }); + }, +}); + +// Mint NFT on Avalanche +await sendUniversal({ + chainId: 43114, + expectTokens: [], + transactions: [{ + to: "0xdea7bF60E53CD578e3526F36eC431795f7EEbFe6", + data: encodeFunctionData({ + abi: [{ type: "function", name: "mint", inputs: [], outputs: [] }], + functionName: "mint", + }), + }], +}); +``` + +--- + +#### `useUniversalAccountContext()` + +Access the raw Universal Account context. Useful for advanced use cases. + +**Returns:** +| Property | Type | Description | +|----------|------|-------------| +| `client` | `UniversalAccountClient \| null` | The UA client instance | +| `config` | `UniversalAccountProviderConfig` | Provider configuration | +| `isReady` | `boolean` | True when initialized | +| `isInitializing` | `boolean` | True during initialization | +| `error` | `Error \| null` | Any error | +| `address` | `Address \| null` | EVM address | +| `solanaAddress` | `string \| null` | Solana address | +| `initialize` | `(ownerAddress: Address) => Promise` | Initialize UA | +| `disconnect` | `() => void` | Reset UA | + +--- + +### Client (Advanced) + +For manual control without React hooks, use `createUniversalAccountClient`: + +```typescript +import { createUniversalAccountClient } from "@account-kit/universal-account"; + +const client = await createUniversalAccountClient({ + ownerAddress: "0x...", + config: { + projectId: "...", + projectClientKey: "...", + projectAppUuid: "...", + }, +}); + +// Get addresses +const evmAddress = await client.getAddress(); +const solanaAddress = await client.getSolanaAddress(); + +// Get balance +const balance = await client.getPrimaryAssets(); +console.log("Total USD:", balance.totalAmountInUSD); + +// Create and send transaction +const tx = await client.createUniversalTransaction({ + chainId: 43114, + expectTokens: [], + transactions: [{ to: "0x...", data: "0x..." }], +}); + +const signature = await signer.signMessage({ raw: toBytes(tx.rootHash) }); +const result = await client.sendTransaction(tx, signature); + +console.log("Explorer:", client.getExplorerUrl(result.transactionId)); +``` + +--- + +## How Universal Accounts Work + +1. **Single Owner**: A Universal Account is controlled by a single EOA (your Alchemy Signer) +2. **Multiple Addresses**: Each UA has both an EVM address and a Solana address +3. **Unified Balance**: Assets across all chains are aggregated into a single balance view +4. **Automatic Routing**: When you send a transaction, the SDK automatically: + - Finds the optimal source of funds across your chains + - Routes liquidity through Particle's Universal Liquidity + - Handles all bridging and gas abstraction + +## Supported Chains + +Universal Accounts support 15+ EVM chains and Solana: +- Ethereum, Base, Arbitrum, Optimism, Polygon +- Avalanche, BNB Chain, Fantom, Gnosis +- And more... + +See the [full list of supported chains](https://developers.particle.network/universal-accounts/cha/chains). + +## Fees + +Universal Account transactions may include: +- **Gas fees**: Standard network fees on the destination chain +- **LP fee**: 0.2% for cross-chain transactions +- **Service fee**: 1% on transaction volume + +Fees are automatically calculated and shown in the transaction preview via `feeQuotes`. + +## Resources + +- [Particle Network Documentation](https://developers.particle.network/universal-accounts/cha/overview) +- [Universal Accounts SDK Reference](https://developers.particle.network/universal-accounts/ua-reference/desktop/web) +- [Supported Chains & Primary Assets](https://developers.particle.network/universal-accounts/cha/chains) +- [UniversalX Explorer](https://universalx.app) + +## License + +MIT diff --git a/account-kit/universal-account/examples/next-example/.gitignore b/account-kit/universal-account/examples/next-example/.gitignore new file mode 100644 index 0000000000..86fd299e8c --- /dev/null +++ b/account-kit/universal-account/examples/next-example/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +package-lock.json +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/account-kit/universal-account/examples/next-example/README.md b/account-kit/universal-account/examples/next-example/README.md new file mode 100644 index 0000000000..0a1e42758c --- /dev/null +++ b/account-kit/universal-account/examples/next-example/README.md @@ -0,0 +1,281 @@ +# Universal Account Demo + +A demo app showcasing the seamless integration of **Alchemy Account Kit** with **Particle Network Universal Accounts**. + +## What This Demo Shows + +- **Alchemy Account Kit Authentication**: Email, passkey, social login, and external wallet support +- **Universal Account Integration**: Automatic initialization from Alchemy's EOA +- **Unified Balance**: View aggregated balance across 15+ EVM chains and Solana +- **Cross-Chain Transactions**: Mint an NFT on Avalanche without holding AVAX + +--- + +## 📖 Understanding the Code + +To understand how this integration works, read the files in this order: + +### 1. `config.ts` - Configuration +Start here to see how both Alchemy Account Kit and Particle Universal Accounts are configured. +- Sets up Alchemy for authentication (email, passkey, social, wallet) +- Sets up Particle credentials for Universal Accounts + +### 2. `app/providers.tsx` - Provider Hierarchy +See how the providers are nested (order matters!): +- `QueryClientProvider` → React Query +- `AlchemyAccountProvider` → Authentication & signing +- `UniversalAccountProvider` → Chain abstraction (must be inside Alchemy provider) + +### 3. `app/page.tsx` - Main Page +Learn the key concept of EOA vs SCA: +- `useUser().address` → EOA (what Universal Accounts needs) +- `useAccount().address` → SCA (Alchemy's smart account, NOT used here) +- How to pass the EOA to the Universal Account component + +### 4. `app/components/UniversalAccountDemo.tsx` - The Integration +The main component showing all the hooks in action: +- **Step 1**: Get Alchemy signer with `useSigner()` +- **Step 2**: Initialize Universal Account with `useUniversalAccount(ownerAddress)` +- **Step 3**: Get unified balance with `useUnifiedBalance()` +- **Step 4**: Setup transactions with `useSendTransaction()` +- **Step 5**: Example transaction (NFT mint on Avalanche) + +--- + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Alchemy Account Kit │ +├─────────────────────────────────────────────────────────────────┤ +│ useUser() → user.address = EOA (Externally Owned Account) │ +│ useAccount() → address = SCA (Smart Contract Account) │ +│ useSigner() → Signs messages with the EOA │ +└─────────────────────────────────────────────────────────────────┘ + │ + │ EOA address (user.address) + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Universal Accounts │ +├─────────────────────────────────────────────────────────────────┤ +│ Owner: EOA from Alchemy (user.address) │ +│ Creates: Multi-chain smart accounts (EVM + Solana) │ +│ Provides: Unified balance, cross-chain transactions │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Key Concept**: We use Alchemy for authentication (getting the EOA), then Universal Accounts for cross-chain operations. The EOA from `useUser().address` is what controls the Universal Account. + +--- + +## Setup + +### 1. Get Alchemy Credentials + +1. Go to [Alchemy Dashboard](https://dashboard.alchemy.com) +2. Create an app and copy the API Key +3. Go to [Smart Wallets Configuration](https://dashboard.alchemy.com/services/smart-wallets/configuration) and enable login methods (email, passkey, social) +4. (Optional) Create a [Gas Manager Policy](https://dashboard.alchemy.com/services/gas-manager/configuration) for sponsored transactions + +### 2. Get Particle Network Credentials + +1. Go to [Particle Dashboard](https://dashboard.particle.network) +2. Create a project and web application +3. Copy your **Project ID**, **Client Key**, and **App ID** + +### 3. Configure Environment + +```bash +cp .env.example .env.local +``` + +Edit `.env.local` with your credentials: + +```env +# Alchemy Account Kit +NEXT_PUBLIC_ALCHEMY_API_KEY=your_alchemy_api_key +NEXT_PUBLIC_ALCHEMY_POLICY_ID=your_gas_policy_id # Optional + +# Particle Network Universal Accounts +NEXT_PUBLIC_PARTICLE_PROJECT_ID=your_particle_project_id +NEXT_PUBLIC_PARTICLE_CLIENT_KEY=your_particle_client_key +NEXT_PUBLIC_PARTICLE_APP_ID=your_particle_app_id +``` + +### 4. Install & Run + +```bash +npm install +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) to see the demo. + +--- + +## Code Walkthrough + +### Provider Setup (`providers.tsx`) + +The app wraps with both Alchemy and Universal Account providers: + +```tsx +import { AlchemyAccountProvider } from "@account-kit/react"; +import { UniversalAccountProvider } from "@account-kit/universal-account"; + +export function Providers({ children }) { + return ( + + + + {children} + + + + ); +} +``` + +### Getting the EOA Address (`page.tsx`) + +**Important**: Use `user.address` (EOA), not the SCA from `useAccount()`: + +```tsx +import { useUser, useAccount } from "@account-kit/react"; + +function Home() { + const user = useUser(); + + // ✅ CORRECT: EOA address for Universal Accounts + const eoaAddress = user?.address as `0x${string}`; + + // ❌ WRONG: This is Alchemy's Smart Contract Account + // const { address } = useAccount({ type: "LightAccount" }); + + return ; +} +``` + +### Using Universal Account Hooks (`UniversalAccountDemo.tsx`) + +```tsx +import { + useUniversalAccount, + useUnifiedBalance, + useSendTransaction +} from "@account-kit/universal-account"; +import { useSigner } from "@account-kit/react"; +import { toBytes, encodeFunctionData } from "viem"; + +function UniversalAccountDemo({ eoaAddress }) { + const signer = useSigner(); + + // 1. Initialize Universal Account with EOA + const { address, solanaAddress, isReady, error } = useUniversalAccount(eoaAddress); + + // 2. Get unified balance across all chains + const { totalBalanceUSD, assets, refetch } = useUnifiedBalance(); + + // 3. Set up transaction sending + const { sendUniversal, isLoading } = useSendTransaction({ + signMessage: async (message) => { + // Sign raw bytes of the transaction hash + return await signer!.signMessage({ raw: toBytes(message) }); + }, + }); + + // 4. Send a cross-chain transaction + const handleMint = async () => { + const result = await sendUniversal({ + chainId: 43114, // Avalanche + expectTokens: [], + transactions: [{ + to: "0xdea7bF60E53CD578e3526F36eC431795f7EEbFe6", + data: encodeFunctionData({ + abi: [{ type: "function", name: "mint", inputs: [], outputs: [] }], + functionName: "mint", + }), + }], + }); + + console.log("View TX:", `https://universalx.app/activity/details?id=${result.transactionId}`); + }; + + return ( +
+

EVM Address: {address}

+

Solana Address: {solanaAddress}

+

Balance: ${totalBalanceUSD?.toFixed(2)}

+ +
+ ); +} +``` + +--- + +## Available Hooks + +| Hook | Purpose | +|------|---------| +| `useUniversalAccount(ownerAddress)` | Initialize UA with EOA, get addresses | +| `useUnifiedBalance()` | Get aggregated balance across all chains | +| `useSendTransaction({ signMessage })` | Send cross-chain transactions | +| `useUniversalAccountContext()` | Access raw context for advanced use | + +### Transaction Types + +The `useSendTransaction` hook provides methods for all Universal Account transaction types: + +| Method | Description | +|--------|-------------| +| `sendTransfer` | Send tokens to any address across chains | +| `sendUniversal` | Execute custom contract interactions | +| `sendBuy` | Buy/swap into a target token using USD value | +| `sendSell` | Sell a token back into primary assets | +| `sendConvert` | Convert between primary assets on a chain | + +```tsx +const { sendTransfer, sendBuy, sendSell, sendConvert, sendUniversal } = useSendTransaction({ + signMessage: async (msg) => signer!.signMessage({ raw: toBytes(msg) }), +}); + +// Transfer tokens +await sendTransfer({ token: { chainId: 42161, address: "0x..." }, amount: "10", receiver: "0x..." }); + +// Buy $10 worth of a token +await sendBuy({ token: { chainId: 42161, address: "0x..." }, amountInUSD: "10" }); + +// Sell tokens +await sendSell({ token: { chainId: 42161, address: "0x..." }, amount: "0.1" }); + +// Convert to USDC on Arbitrum +await sendConvert({ expectToken: { type: "USDC", amount: "1" }, chainId: 42161 }); + +// Custom contract call +await sendUniversal({ chainId: 43114, expectTokens: [], transactions: [{ to: "0x...", data: "0x..." }] }); +``` + +See the [SDK README](../../README.md) for full API documentation. + +--- + +## How It Works + +1. **Sign In**: User authenticates with Alchemy Account Kit (email, passkey, social, or wallet) +2. **Get EOA**: Alchemy provides the EOA address via `useUser().address` +3. **Initialize UA**: The EOA is passed to `useUniversalAccount()` which creates the Universal Account +4. **Unified Balance**: `useUnifiedBalance()` fetches aggregated assets across all chains +5. **Transactions**: `useSendTransaction()` creates and signs cross-chain transactions + +--- + +## Resources + +- [SDK Documentation](../../README.md) - Full API reference +- [Alchemy Account Kit Docs](https://www.alchemy.com/docs/wallets) +- [Particle Network Universal Accounts](https://developers.particle.network/universal-accounts/cha/overview) +- [Supported Chains](https://developers.particle.network/universal-accounts/cha/chains) +- [UniversalX Explorer](https://universalx.app) diff --git a/account-kit/universal-account/examples/next-example/app/components/UniversalAccountDemo.tsx b/account-kit/universal-account/examples/next-example/app/components/UniversalAccountDemo.tsx new file mode 100644 index 0000000000..ea79b7eb69 --- /dev/null +++ b/account-kit/universal-account/examples/next-example/app/components/UniversalAccountDemo.tsx @@ -0,0 +1,351 @@ +"use client"; + +/** + * Universal Account Demo Component + * + * This component demonstrates how to use Universal Accounts with Alchemy Account Kit. + * It shows: + * 1. Initializing a Universal Account with an EOA owner + * 2. Fetching unified balance across all chains + * 3. Sending a cross-chain transaction (minting an NFT) + */ + +import { useState } from "react"; +import { + // Core hooks from @account-kit/universal-account + useUniversalAccount, // Initialize UA with owner address + useUnifiedBalance, // Get aggregated balance across chains + useSendTransaction, // Send transactions (transfer, buy, sell, etc.) + CHAIN_ID, // Chain ID constants for supported chains +} from "@account-kit/universal-account"; +import { useSigner } from "@account-kit/react"; +import { type Address, encodeFunctionData, toBytes } from "viem"; + +// ============================================================================= +// EXAMPLE: NFT Contract on Avalanche +// ============================================================================= +// This is a free mint NFT contract for demo purposes. +// Replace with your own contract address and ABI for your use case. +const NFT_CONTRACT = "0xdea7bF60E53CD578e3526F36eC431795f7EEbFe6" as const; + +interface UniversalAccountDemoProps { + /** + * The EOA (Externally Owned Account) address that controls the Universal Account. + * + * WHERE TO GET THIS: + * In your parent component, use Alchemy's useUser() hook: + * + * ```tsx + * import { useUser } from "@account-kit/react"; + * + * const user = useUser(); + * const eoaAddress = user?.address; // ← Pass this as the prop + * ``` + * + * See app/page.tsx for the full example. + */ + eoaAddress: Address; +} + +export function UniversalAccountDemo({ + eoaAddress, +}: UniversalAccountDemoProps) { + // ========================================================================== + // STEP 1: Get the Alchemy signer for transaction signing + // ========================================================================== + // The signer is used to sign Universal Account transactions. + // It comes from Alchemy Account Kit's authentication. + const signer = useSigner(); + + // Local state for transaction feedback + const [txResult, setTxResult] = useState(null); + const [txError, setTxError] = useState(null); + + // ========================================================================== + // STEP 2: Initialize Universal Account + // ========================================================================== + // Pass the EOA address to create/connect to the Universal Account. + // This automatically derives the UA's EVM and Solana addresses. + const { address, solanaAddress, isReady, isInitializing, error } = + useUniversalAccount(eoaAddress); + + // ========================================================================== + // STEP 3: Get Unified Balance + // ========================================================================== + // Fetches aggregated balance across ALL supported chains. + // No need to query each chain individually! + const { + totalBalanceUSD, // Total balance in USD + assets, // Array of assets with per-chain breakdown + isLoading: isLoadingBalance, + refetch, // Call to refresh balance + } = useUnifiedBalance(); + + // ========================================================================== + // STEP 4: Setup Transaction Hook + // ========================================================================== + // useSendTransaction provides methods for all transaction types: + // - sendTransfer: Send tokens to any address + // - sendUniversal: Execute custom contract calls + // - sendBuy: Buy/swap into a token + // - sendSell: Sell a token + // - sendConvert: Convert between primary assets + const { + sendUniversal, // We use this for the NFT mint + // sendTransfer, // For token transfers + // sendBuy, // For buying tokens + // sendSell, // For selling tokens + // sendConvert, // For converting assets + isLoading: isSending, + } = useSendTransaction({ + // This function signs the transaction hash with the Alchemy signer + signMessage: async (message: string) => { + if (!signer) throw new Error("Signer not available"); + // IMPORTANT: Sign the raw bytes, not a string message + // The message is a hex-encoded hash from Universal Account + return await signer.signMessage({ raw: toBytes(message) }); + }, + }); + + // ========================================================================== + // STEP 5: Example Transaction - Mint NFT on Avalanche + // ========================================================================== + // This demonstrates sendUniversal for custom contract interactions. + // The Universal Account will automatically: + // - Source funds from any chain where you have balance + // - Handle bridging and gas payment + // - Execute the transaction on the target chain + const handleMintNFT = async () => { + setTxResult(null); + setTxError(null); + + try { + // Encode the contract call data using viem + const mintData = encodeFunctionData({ + abi: [{ type: "function", name: "mint", inputs: [], outputs: [] }], + functionName: "mint", + }); + + // Send the transaction using Universal Account + const result = await sendUniversal({ + // Target chain - use CHAIN_ID constants for type safety + chainId: CHAIN_ID.AVALANCHE, + + // Tokens needed on the target chain for the transaction + // Empty array = no tokens needed (free mint) + // For payable functions: [{ type: "AVAX", amount: "0.1" }] + expectTokens: [], + + // Array of contract calls to execute + transactions: [ + { + to: NFT_CONTRACT, + data: mintData, + // value: "0x..." // For payable functions + }, + ], + }); + + // Transaction submitted! View on UniversalX explorer + setTxResult( + `https://universalx.app/activity/details?id=${result.transactionId}`, + ); + } catch (err) { + console.error("Mint failed:", err); + setTxError(err instanceof Error ? err.message : "Transaction failed"); + } + }; + + if (isInitializing) { + return ( +
+
+
+
+
+
+
+ ); + } + + if (error) { + return ( +
+

+ Error +

+

+ {error.message} +

+

+ Make sure you have set the following environment variables: +

+
    +
  • NEXT_PUBLIC_PARTICLE_PROJECT_ID
  • +
  • NEXT_PUBLIC_PARTICLE_CLIENT_KEY
  • +
  • NEXT_PUBLIC_PARTICLE_APP_ID
  • +
+
+ ); + } + + if (!isReady) { + return null; + } + + return ( +
+ {/* Universal Account Addresses */} +
+

+ Universal Account +

+
+
+

+ EVM Address +

+

+ {address} +

+
+ {solanaAddress && ( +
+

+ Solana Address +

+

+ {solanaAddress} +

+
+ )} +
+
+ + {/* Unified Balance */} +
+
+

+ Unified Balance +

+ +
+ +
+
+ ${totalBalanceUSD?.toFixed(2) ?? "0.00"} +
+ + {assets && assets.length > 0 ? ( +
+ {assets.map((asset) => ( +
+
+

+ {asset.tokenType} +

+

+ {asset.amount} +

+
+
+

+ ${asset.amountInUSD.toFixed(2)} +

+

+ {asset.chainAggregation.length} chain + {asset.chainAggregation.length !== 1 ? "s" : ""} +

+
+
+ ))} +
+ ) : ( +

+ No assets found. Fund your Universal Account to get started. +

+ )} +
+
+ + {/* Mint NFT Demo */} +
+

+ Mint NFT on Avalanche +

+

+ Mint a free NFT on Avalanche using your Universal Account. Gas will be + paid from any of your available balances. +

+ + + + {txResult && ( +
+

+ Transaction Successful! +

+ + View on UniversalX + +
+ )} + + {txError && ( +
+

+ Transaction Failed +

+

{txError}

+
+ )} +
+ + {/* Info Card */} +
+

+ How it works +

+
    +
  • + • Your Universal Account aggregates balances across all supported + chains +
  • +
  • • Send transactions to any chain without manual bridging
  • +
  • • Pay gas fees with any supported token
  • +
  • + • View your transactions on{" "} + + UniversalX + +
  • +
+
+
+ ); +} diff --git a/account-kit/universal-account/examples/next-example/app/favicon.ico b/account-kit/universal-account/examples/next-example/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/account-kit/universal-account/examples/next-example/app/globals.css b/account-kit/universal-account/examples/next-example/app/globals.css new file mode 100644 index 0000000000..ae574d66ae --- /dev/null +++ b/account-kit/universal-account/examples/next-example/app/globals.css @@ -0,0 +1,27 @@ +@import "tailwindcss"; +@config "../tailwind.config.ts"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/account-kit/universal-account/examples/next-example/app/layout.tsx b/account-kit/universal-account/examples/next-example/app/layout.tsx new file mode 100644 index 0000000000..274fa2441a --- /dev/null +++ b/account-kit/universal-account/examples/next-example/app/layout.tsx @@ -0,0 +1,43 @@ +import { config } from "@/config"; +import { cookieToInitialState } from "@account-kit/core"; +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import { headers } from "next/headers"; +import "./globals.css"; +import { Providers } from "./providers"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Universal Account Demo", + description: "Alchemy Account Kit + Particle Universal Accounts", +}; + +export default async function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const initialState = cookieToInitialState( + config, + (await headers()).get("cookie") ?? undefined, + ); + + return ( + + + {children} + + + ); +} diff --git a/account-kit/universal-account/examples/next-example/app/page.tsx b/account-kit/universal-account/examples/next-example/app/page.tsx new file mode 100644 index 0000000000..317509cd53 --- /dev/null +++ b/account-kit/universal-account/examples/next-example/app/page.tsx @@ -0,0 +1,119 @@ +"use client"; + +/** + * Main Page - Alchemy Account Kit + Particle Universal Accounts Demo + * + * This page demonstrates: + * 1. User authentication with Alchemy Account Kit (AuthCard) + * 2. Getting the user's EOA address for Universal Account initialization + * 3. Rendering the Universal Account demo component + */ + +import { + useLogout, + useSignerStatus, + useUser, + useAccount, + AuthCard, +} from "@account-kit/react"; +import { UniversalAccountDemo } from "./components/UniversalAccountDemo"; + +export default function Home() { + // ========================================================================== + // ALCHEMY ACCOUNT KIT HOOKS + // ========================================================================== + + // Get the authenticated user (contains the EOA address we need) + const user = useUser(); + + // Check if the signer is still initializing + const signerStatus = useSignerStatus(); + + // Logout function + const { logout } = useLogout(); + + // Get Alchemy's Smart Contract Account (SCA) - for display only + // Note: We don't use this for Universal Accounts, just showing it for reference + const { address: scaAddress } = useAccount({ type: "LightAccount" }); + + // ========================================================================== + // IMPORTANT: EOA vs SCA + // ========================================================================== + // Alchemy Account Kit provides TWO types of addresses: + // + // 1. EOA (Externally Owned Account) - user.address + // - This is the user's actual wallet address + // - This is what Universal Accounts needs as the "owner" + // - Used to sign transactions + // + // 2. SCA (Smart Contract Account) - from useAccount() + // - This is Alchemy's smart account for gasless transactions + // - NOT used for Universal Accounts + // + // We pass the EOA to Universal Accounts because it controls the UA. + const eoaAddress = user?.address as `0x${string}` | undefined; + + return ( +
+
+

+ Universal Account Demo +

+

+ Alchemy Account Kit + Particle Network Universal Accounts +

+ + {signerStatus.isInitializing ? ( +
+
Loading...
+
+ ) : user ? ( +
+ {/* User Info Card */} +
+
+
+

+ Logged in as +

+

+ {user.email ?? "Anonymous"} +

+ {eoaAddress && ( +

+ EOA: {eoaAddress.slice(0, 6)}...{eoaAddress.slice(-4)} +

+ )} + {scaAddress && ( +

+ SCA: {scaAddress.slice(0, 6)}...{scaAddress.slice(-4)} +

+ )} +
+ +
+
+ + {/* Universal Account Demo - pass the EOA address */} + {eoaAddress && } +
+ ) : ( +
+

+ Sign in to access your Universal Account +

+ {/* Auth Card - embedded login form */} +
+ +
+
+ )} +
+
+ ); +} diff --git a/account-kit/universal-account/examples/next-example/app/providers.tsx b/account-kit/universal-account/examples/next-example/app/providers.tsx new file mode 100644 index 0000000000..35cccc6c21 --- /dev/null +++ b/account-kit/universal-account/examples/next-example/app/providers.tsx @@ -0,0 +1,46 @@ +"use client"; + +/** + * Provider Setup for Alchemy Account Kit + Particle Universal Accounts + * + * PROVIDER HIERARCHY (order matters!): + * 1. QueryClientProvider - React Query for data fetching + * 2. AlchemyAccountProvider - Authentication & signing + * 3. UniversalAccountProvider - Chain abstraction + * + * The UniversalAccountProvider must be INSIDE AlchemyAccountProvider + * because it uses Alchemy's signer for transaction signing. + */ + +import { config, queryClient, universalAccountConfig } from "@/config"; +import { + AlchemyAccountProvider, + AlchemyAccountsProviderProps, +} from "@account-kit/react"; +import { UniversalAccountProvider } from "@account-kit/universal-account"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { PropsWithChildren } from "react"; + +export const Providers = ( + props: PropsWithChildren<{ + initialState?: AlchemyAccountsProviderProps["initialState"]; + }>, +) => { + return ( + // Step 1: React Query for data fetching/caching + + {/* Step 2: Alchemy Account Kit - handles authentication */} + + {/* Step 3: Universal Accounts - enables chain abstraction */} + {/* Must be inside AlchemyAccountProvider to access the signer */} + + {props.children} + + + + ); +}; diff --git a/account-kit/universal-account/examples/next-example/config.ts b/account-kit/universal-account/examples/next-example/config.ts new file mode 100644 index 0000000000..04406a4f77 --- /dev/null +++ b/account-kit/universal-account/examples/next-example/config.ts @@ -0,0 +1,69 @@ +/** + * Configuration for Alchemy Account Kit + Particle Universal Accounts + * + * This file sets up both: + * 1. Alchemy Account Kit - for authentication (email, passkey, social, wallet) + * 2. Particle Universal Accounts - for chain abstraction + */ + +import { createConfig, cookieStorage } from "@account-kit/react"; +import { mainnet, alchemy } from "@account-kit/infra"; +import { QueryClient } from "@tanstack/react-query"; +import type { UniversalAccountProviderConfig } from "@account-kit/universal-account"; + +// ============================================================================= +// STEP 1: Alchemy Account Kit Configuration +// ============================================================================= +// This handles user authentication and provides the EOA (signer) that will +// control the Universal Account. +// +// This stays the standard configuration for Alchemy Account Kit. +// +// Get your Alchemy API key from: https://dashboard.alchemy.com/ +export const config = createConfig( + { + // Alchemy RPC transport - used for blockchain interactions + transport: alchemy({ apiKey: process.env.NEXT_PUBLIC_ALCHEMY_API_KEY! }), + + // Default chain for Alchemy Account Kit (can be any EVM chain) + // Note: Universal Accounts work across ALL chains regardless of this setting + chain: mainnet, + ssr: true, + storage: cookieStorage, + enablePopupOauth: true, + }, + { + // Authentication options - customize which login methods to show + auth: { + sections: [ + [{ type: "email" }], + [ + { type: "passkey" }, + { type: "social", authProviderId: "google", mode: "popup" }, + ], + [{ type: "external_wallets" }], + ], + addPasskeyOnSignup: true, + }, + }, +); + +export const queryClient = new QueryClient(); + +// ============================================================================= +// STEP 2: Particle Universal Account Configuration +// ============================================================================= +// This enables chain abstraction - unified balance and cross-chain transactions. +// +// Get your Particle credentials from: https://dashboard.particle.network/ +export const universalAccountConfig: UniversalAccountProviderConfig = { + projectId: process.env.NEXT_PUBLIC_PARTICLE_PROJECT_ID!, + clientKey: process.env.NEXT_PUBLIC_PARTICLE_CLIENT_KEY!, + appId: process.env.NEXT_PUBLIC_PARTICLE_APP_ID!, + + // Optional: Trade configuration + // tradeConfig: { + // slippageBps: 100, // 1% slippage tolerance + // universalGas: true, // Use PARTI token for gas fees + // }, +}; diff --git a/account-kit/universal-account/examples/next-example/eslint.config.mjs b/account-kit/universal-account/examples/next-example/eslint.config.mjs new file mode 100644 index 0000000000..719cea2b59 --- /dev/null +++ b/account-kit/universal-account/examples/next-example/eslint.config.mjs @@ -0,0 +1,25 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), + { + ignores: [ + "node_modules/**", + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ], + }, +]; + +export default eslintConfig; diff --git a/account-kit/universal-account/examples/next-example/next.config.ts b/account-kit/universal-account/examples/next-example/next.config.ts new file mode 100644 index 0000000000..e9ffa3083a --- /dev/null +++ b/account-kit/universal-account/examples/next-example/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/account-kit/universal-account/examples/next-example/package.json b/account-kit/universal-account/examples/next-example/package.json new file mode 100644 index 0000000000..24f33f8d76 --- /dev/null +++ b/account-kit/universal-account/examples/next-example/package.json @@ -0,0 +1,33 @@ +{ + "name": "next-example", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "react": "19.1.0", + "react-dom": "19.1.0", + "next": "15.5.2", + "@account-kit/react": "^4.79.0", + "@account-kit/infra": "^4.79.0", + "@account-kit/core": "^4.79.0", + "@tanstack/react-query": "^5.62.0", + "@particle-network/universal-account-sdk": "^1.0.10", + "viem": "^2.29.2" + }, + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "@tailwindcss/postcss": "^4", + "tailwindcss": "^4", + "eslint": "^9", + "eslint-config-next": "15.5.2", + "@eslint/eslintrc": "^3" + } +} diff --git a/account-kit/universal-account/examples/next-example/postcss.config.mjs b/account-kit/universal-account/examples/next-example/postcss.config.mjs new file mode 100644 index 0000000000..c7bcb4b1ee --- /dev/null +++ b/account-kit/universal-account/examples/next-example/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/account-kit/universal-account/examples/next-example/public/file.svg b/account-kit/universal-account/examples/next-example/public/file.svg new file mode 100644 index 0000000000..004145cddf --- /dev/null +++ b/account-kit/universal-account/examples/next-example/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/account-kit/universal-account/examples/next-example/public/globe.svg b/account-kit/universal-account/examples/next-example/public/globe.svg new file mode 100644 index 0000000000..567f17b0d7 --- /dev/null +++ b/account-kit/universal-account/examples/next-example/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/account-kit/universal-account/examples/next-example/public/next.svg b/account-kit/universal-account/examples/next-example/public/next.svg new file mode 100644 index 0000000000..5174b28c56 --- /dev/null +++ b/account-kit/universal-account/examples/next-example/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/account-kit/universal-account/examples/next-example/public/vercel.svg b/account-kit/universal-account/examples/next-example/public/vercel.svg new file mode 100644 index 0000000000..7705396033 --- /dev/null +++ b/account-kit/universal-account/examples/next-example/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/account-kit/universal-account/examples/next-example/public/window.svg b/account-kit/universal-account/examples/next-example/public/window.svg new file mode 100644 index 0000000000..b2b2a44f6e --- /dev/null +++ b/account-kit/universal-account/examples/next-example/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/account-kit/universal-account/examples/next-example/tailwind.config.ts b/account-kit/universal-account/examples/next-example/tailwind.config.ts new file mode 100644 index 0000000000..06c244bb56 --- /dev/null +++ b/account-kit/universal-account/examples/next-example/tailwind.config.ts @@ -0,0 +1,14 @@ +import { withAccountKitUi } from "@account-kit/react/tailwind"; + +export default withAccountKitUi( + { + // Existing Tailwind config + content: [ + "./app/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + ], + }, + { + // AccountKit UI theme customizations (optional) + } +); diff --git a/account-kit/universal-account/examples/next-example/tsconfig.json b/account-kit/universal-account/examples/next-example/tsconfig.json new file mode 100644 index 0000000000..d8b93235f2 --- /dev/null +++ b/account-kit/universal-account/examples/next-example/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/account-kit/universal-account/package.json b/account-kit/universal-account/package.json new file mode 100644 index 0000000000..fabfe1de81 --- /dev/null +++ b/account-kit/universal-account/package.json @@ -0,0 +1,72 @@ +{ + "name": "@account-kit/universal-account", + "version": "4.79.0", + "description": "Universal Account integration for Account Kit - enabling chain abstraction with Particle Network", + "author": "Alchemy", + "license": "MIT", + "private": false, + "type": "module", + "main": "./dist/esm/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "typings": "./dist/types/index.d.ts", + "sideEffects": false, + "files": [ + "dist", + "src/**/*.ts", + "src/**/*.tsx", + "!dist/**/*.tsbuildinfo", + "!vitest.config.ts", + "!.env", + "!src/**/*.test.ts", + "!src/**/*.test-d.ts", + "!src/__tests__/**/*" + ], + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/esm/index.js", + "default": "./dist/esm/index.js" + }, + "./package.json": "./package.json" + }, + "scripts": { + "build": "yarn clean && yarn build:esm && yarn build:types", + "build:esm": "tsc --project tsconfig.build.json --outDir ./dist/esm", + "build:types": "tsc --project tsconfig.build.json --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap", + "clean": "rm -rf ./dist", + "test": "vitest --passWithNoTests", + "test:run": "vitest run --passWithNoTests" + }, + "dependencies": { + "@account-kit/infra": "^4.79.0", + "@account-kit/signer": "^4.79.0", + "@particle-network/universal-account-sdk": "^1.0.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "viem": "^2.29.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": false + } + }, + "devDependencies": { + "@types/react": "^18.2.0", + "react": "^18.2.0", + "typescript-template": "*" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/alchemyplatform/aa-sdk.git" + }, + "bugs": { + "url": "https://github.com/alchemyplatform/aa-sdk/issues" + }, + "homepage": "https://github.com/alchemyplatform/aa-sdk#readme" +} diff --git a/account-kit/universal-account/src/client.ts b/account-kit/universal-account/src/client.ts new file mode 100644 index 0000000000..95b79e2bf0 --- /dev/null +++ b/account-kit/universal-account/src/client.ts @@ -0,0 +1,450 @@ +import type { Address } from "viem"; +import type { + UniversalAccountConfig, + SmartAccountOptions, + PrimaryAssets, + TransferTransactionParams, + UniversalTransactionParams, + BuyTransactionParams, + SellTransactionParams, + ConvertTransactionParams, + UniversalTransaction, + TransactionResult, + IUniversalAccount, +} from "./types.js"; + +export interface CreateUniversalAccountClientParams { + /** Owner EOA address that controls the Universal Account */ + ownerAddress: Address; + /** Universal Account configuration */ + config: UniversalAccountConfig; +} + +/** + * Universal Account Client + * + * Wraps Particle Network's Universal Account SDK to provide + * chain abstraction capabilities within Account Kit. + * + * @example + * ```ts + * import { createUniversalAccountClient } from "@account-kit/universal-account"; + * + * const client = await createUniversalAccountClient({ + * ownerAddress: "0x...", + * config: { + * projectId: "your-project-id", + * projectClientKey: "your-client-key", + * projectAppUuid: "your-app-uuid", + * }, + * }); + * + * // Get unified balance across all chains + * const balance = await client.getPrimaryAssets(); + * console.log("Total USD:", balance.totalAmountInUSD); + * ``` + */ +export class UniversalAccountClient implements IUniversalAccount { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private ua: any; + private _ownerAddress: Address; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(ua: any, ownerAddress: Address) { + this.ua = ua; + this._ownerAddress = ownerAddress; + } + + /** + * Get the owner EOA address + * + * @returns {Address} The owner EOA address + */ + getOwnerAddress(): Address { + return this._ownerAddress; + } + + /** + * Get smart account options including all addresses + * + * @returns {Promise} Smart account options with EVM and Solana addresses + */ + async getSmartAccountOptions(): Promise { + const options = await this.ua.getSmartAccountOptions(); + return { + name: options.name, + version: options.version, + ownerAddress: options.ownerAddress as Address, + smartAccountAddress: options.smartAccountAddress as Address, + solanaSmartAccountAddress: options.solanaSmartAccountAddress, + senderAddress: options.senderAddress as Address, + senderSolanaAddress: options.senderSolanaAddress, + }; + } + + /** + * Get the EVM Universal Account address + * + * @returns {Promise
} The EVM smart account address + */ + async getAddress(): Promise
{ + const options = await this.getSmartAccountOptions(); + return options.smartAccountAddress; + } + + /** + * Get the Solana Universal Account address + * + * @returns {Promise} The Solana smart account address, if available + */ + async getSolanaAddress(): Promise { + const options = await this.getSmartAccountOptions(); + return options.solanaSmartAccountAddress; + } + + /** + * Get primary assets (unified balance across all chains) + * + * @returns {Promise} Primary assets with total USD value + * + * @example + * ```ts + * const assets = await client.getPrimaryAssets(); + * console.log("Total balance:", assets.totalAmountInUSD); + * + * // Iterate through assets + * for (const asset of assets.assets) { + * console.log(`${asset.tokenType}: $${asset.amountInUSD}`); + * } + * ``` + */ + async getPrimaryAssets(): Promise { + const assets = await this.ua.getPrimaryAssets(); + return { + assets: assets.assets.map((asset: any) => ({ + tokenType: asset.tokenType, + price: asset.price, + amount: asset.amount, + amountInUSD: asset.amountInUSD, + chainAggregation: asset.chainAggregation?.map((chain: any) => ({ + chainId: chain.token?.chainId ?? chain.chainId, + address: chain.token?.address ?? chain.address, + amount: chain.amount, + amountInUSD: chain.amountInUSD, + rawAmount: chain.rawAmount, + decimals: chain.token?.decimals ?? chain.decimals, + })) ?? [], + })), + totalAmountInUSD: assets.totalAmountInUSD, + }; + } + + /** + * Create a transfer transaction + * + * @param {TransferTransactionParams} params Transfer parameters + * @returns {Promise} Universal transaction ready to be signed + * + * @example + * ```ts + * const tx = await client.createTransferTransaction({ + * token: { + * chainId: 42161, // Arbitrum + * address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT + * }, + * amount: "10", + * receiver: "0x...", + * }); + * + * // Sign the rootHash with your signer + * const signature = await signer.signMessage(tx.rootHash); + * + * // Send the transaction + * const result = await client.sendTransaction(tx, signature); + * ``` + */ + async createTransferTransaction( + params: TransferTransactionParams + ): Promise { + const tx = await this.ua.createTransferTransaction({ + token: { + chainId: params.token.chainId, + address: params.token.address, + }, + amount: params.amount, + receiver: params.receiver, + }); + + return this.mapTransaction(tx); + } + + /** + * Create a universal transaction for contract interactions + * + * @param {UniversalTransactionParams} params Universal transaction parameters + * @returns {Promise} Universal transaction ready to be signed + * + * @example + * ```ts + * const tx = await client.createUniversalTransaction({ + * chainId: 8453, // Base + * expectTokens: [ + * { type: "ETH", amount: "0.01" }, + * ], + * transactions: [ + * { + * to: "0x...", + * data: "0x...", + * value: "0x...", + * }, + * ], + * }); + * ``` + */ + async createUniversalTransaction( + params: UniversalTransactionParams + ): Promise { + const tx = await this.ua.createUniversalTransaction({ + chainId: params.chainId, + expectTokens: params.expectTokens.map((token) => ({ + type: token.type, + amount: token.amount, + })), + transactions: params.transactions.map((txn) => ({ + to: txn.to, + data: txn.data, + value: txn.value, + })), + }); + + return this.mapTransaction(tx); + } + + /** + * Create a buy/swap transaction + * + * Converts USD value from your primary assets into a target token. + * The SDK will automatically route liquidity from your holdings. + * + * @param {BuyTransactionParams} params Buy transaction parameters + * @returns {Promise} Universal transaction ready to be signed + * + * @example + * ```ts + * const tx = await client.createBuyTransaction({ + * token: { + * chainId: 42161, // Arbitrum + * address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT + * }, + * amountInUSD: "10", // Buy $10 worth of USDT + * }); + * ``` + */ + async createBuyTransaction( + params: BuyTransactionParams + ): Promise { + const tx = await this.ua.createBuyTransaction({ + token: { + chainId: params.token.chainId, + address: params.token.address, + }, + amountInUSD: params.amountInUSD, + }); + + return this.mapTransaction(tx); + } + + /** + * Create a sell transaction + * + * Sells a token back into primary assets. Ensure the Universal Account + * has enough balance of the token before calling. + * + * @param {SellTransactionParams} params Sell transaction parameters + * @returns {Promise} Universal transaction ready to be signed + * + * @example + * ```ts + * const tx = await client.createSellTransaction({ + * token: { + * chainId: 42161, // Arbitrum + * address: "0x912CE59144191C1204E64559FE8253a0e49E6548", // ARB + * }, + * amount: "0.1", // Sell 0.1 ARB + * }); + * ``` + */ + async createSellTransaction( + params: SellTransactionParams + ): Promise { + const tx = await this.ua.createSellTransaction({ + token: { + chainId: params.token.chainId, + address: params.token.address, + }, + amount: params.amount, + }); + + return this.mapTransaction(tx); + } + + /** + * Create a convert transaction + * + * Converts between primary assets on a specific chain. + * Useful for converting assets directly to the target chain. + * + * @param {ConvertTransactionParams} params Convert transaction parameters + * @returns {Promise} Universal transaction ready to be signed + * + * @example + * ```ts + * const tx = await client.createConvertTransaction({ + * expectToken: { type: "USDC", amount: "1" }, + * chainId: 42161, // Arbitrum + * }); + * ``` + */ + async createConvertTransaction( + params: ConvertTransactionParams + ): Promise { + const tx = await this.ua.createConvertTransaction({ + expectToken: { + type: params.expectToken.type, + amount: params.expectToken.amount, + }, + chainId: params.chainId, + }); + + return this.mapTransaction(tx); + } + + /** + * Send a signed transaction + * + * @param {UniversalTransaction} transaction The transaction to send + * @param {string} signature The signature from signing the rootHash + * @returns {Promise} Transaction result with ID and status + * + * @example + * ```ts + * const result = await client.sendTransaction(tx, signature); + * console.log("Transaction ID:", result.transactionId); + * console.log("Explorer:", `https://universalx.app/activity/details?id=${result.transactionId}`); + * ``` + */ + async sendTransaction( + transaction: UniversalTransaction, + signature: string + ): Promise { + // We need to pass the original transaction object from the SDK + // The transaction parameter here is our mapped type, but we stored + // the original in a way that sendTransaction can use it + const result = await this.ua.sendTransaction( + transaction as any, + signature + ); + + return { + transactionId: result.transactionId, + status: result.status, + mode: result.mode as "mainnet" | "testnet", + sender: result.sender as Address, + receiver: result.receiver as Address, + tag: result.tag, + created_at: result.created_at, + updated_at: result.updated_at, + }; + } + + /** + * Get the explorer URL for a transaction + * + * @param {string} transactionId The transaction ID + * @returns {string} UniversalX explorer URL + */ + getExplorerUrl(transactionId: string): string { + return `https://universalx.app/activity/details?id=${transactionId}`; + } + + /** + * Get the underlying Particle Universal Account instance. + * Use this for advanced operations not covered by this wrapper. + * + * @returns {any} The underlying Particle SDK UniversalAccount instance + */ + getUnderlyingAccount(): typeof this.ua { + return this.ua; + } + + private mapTransaction(tx: any): UniversalTransaction { + return { + ...tx, + type: tx.type ?? "universal", + mode: tx.mode ?? "mainnet", + sender: tx.sender as Address, + receiver: tx.receiver as Address, + transactionId: tx.transactionId, + rootHash: tx.rootHash as `0x${string}`, + smartAccountOptions: { + name: tx.smartAccountOptions?.name ?? "", + version: tx.smartAccountOptions?.version ?? "", + ownerAddress: tx.smartAccountOptions?.ownerAddress as Address, + smartAccountAddress: tx.smartAccountOptions?.smartAccountAddress as Address, + solanaSmartAccountAddress: tx.smartAccountOptions?.solanaSmartAccountAddress, + senderAddress: tx.smartAccountOptions?.senderAddress as Address, + senderSolanaAddress: tx.smartAccountOptions?.senderSolanaAddress, + }, + feeQuotes: tx.feeQuotes ?? [], + }; + } +} + +/** + * Create a Universal Account client + * + * @param {CreateUniversalAccountClientParams} params Parameters for creating the client + * @returns {Promise} A configured Universal Account client + * + * @example + * ```ts + * import { createUniversalAccountClient } from "@account-kit/universal-account"; + * + * const client = await createUniversalAccountClient({ + * ownerAddress: userAddress, + * config: { + * projectId: process.env.PARTICLE_PROJECT_ID!, + * projectClientKey: process.env.PARTICLE_CLIENT_KEY!, + * projectAppUuid: process.env.PARTICLE_APP_UUID!, + * tradeConfig: { + * slippageBps: 100, // 1% slippage + * universalGas: true, // Use PARTI for gas + * }, + * }, + * }); + * ``` + */ +export async function createUniversalAccountClient( + params: CreateUniversalAccountClientParams +): Promise { + // Dynamic import to avoid bundling issues - users must install the peer dependency + const { UniversalAccount } = await import( + "@particle-network/universal-account-sdk" + ); + + const ua = new UniversalAccount({ + projectId: params.config.projectId, + projectClientKey: params.config.projectClientKey, + projectAppUuid: params.config.projectAppUuid, + ownerAddress: params.ownerAddress, + tradeConfig: params.config.tradeConfig + ? { + slippageBps: params.config.tradeConfig.slippageBps, + universalGas: params.config.tradeConfig.universalGas, + usePrimaryTokens: params.config.tradeConfig.usePrimaryTokens, + } + : undefined, + }); + + return new UniversalAccountClient(ua, params.ownerAddress); +} diff --git a/account-kit/universal-account/src/constants.ts b/account-kit/universal-account/src/constants.ts new file mode 100644 index 0000000000..03f1461a32 --- /dev/null +++ b/account-kit/universal-account/src/constants.ts @@ -0,0 +1,86 @@ +/** + * Chain IDs for Universal Account supported chains + * + * These match the chain IDs used by Particle Network's Universal Accounts. + * You can also import CHAIN_ID directly from @particle-network/universal-account-sdk + * + * @see https://developers.particle.network/universal-accounts/cha/chains + */ +export const CHAIN_ID = { + // EVM Chains + /** Ethereum Mainnet */ + ETHEREUM: 1, + /** BNB Chain (BSC) */ + BNB_CHAIN: 56, + /** Mantle */ + MANTLE: 5000, + /** Monad */ + MONAD: 143, + /** Plasma */ + PLASMA: 9745, + /** X Layer */ + X_LAYER: 196, + /** Base */ + BASE: 8453, + /** Arbitrum One */ + ARBITRUM: 42161, + /** Avalanche C-Chain */ + AVALANCHE: 43114, + /** Optimism */ + OPTIMISM: 10, + /** Polygon */ + POLYGON: 137, + /** HyperEVM */ + HYPER_EVM: 999, + /** Berachain */ + BERACHAIN: 80094, + /** Linea */ + LINEA: 59144, + /** Sonic */ + SONIC: 146, + /** Merlin */ + MERLIN: 4200, + + // Non-EVM Chains + /** Solana Mainnet */ + SOLANA: 101, +} as const; + +/** + * Type for chain IDs + */ +export type ChainId = (typeof CHAIN_ID)[keyof typeof CHAIN_ID]; + +/** + * Supported token types for Universal Accounts + * + * These are the primary asset types that can be used across chains. + */ +export const TOKEN_TYPE = { + /** Ethereum */ + ETH: "ETH", + /** USD Coin */ + USDC: "USDC", + /** Tether USD */ + USDT: "USDT", + /** Solana */ + SOL: "SOL", + /** Bitcoin (wrapped) */ + BTC: "BTC", + /** BNB */ + BNB: "BNB", + /** Mantle Native token */ + MNT: "MNT", +} as const; + +/** + * Type for token types + */ +export type TokenType = (typeof TOKEN_TYPE)[keyof typeof TOKEN_TYPE]; + +/** + * Native token address (zero address) + * Use this for native tokens like ETH, AVAX, MATIC, etc. + */ +export const NATIVE_TOKEN_ADDRESS = + "0x0000000000000000000000000000000000000000" as const; diff --git a/account-kit/universal-account/src/index.ts b/account-kit/universal-account/src/index.ts new file mode 100644 index 0000000000..8f3e22cceb --- /dev/null +++ b/account-kit/universal-account/src/index.ts @@ -0,0 +1,49 @@ +// Provider & Hooks (recommended - seamless integration) +export { + UniversalAccountProvider, + useUniversalAccount, + useUnifiedBalance, + useSendTransaction, + useUniversalAccountContext, + type UniversalAccountProviderConfig, + type UniversalAccountProviderProps, + type UniversalAccountContextValue, +} from "./provider.js"; + +// Client (for advanced/manual usage) +export { + UniversalAccountClient, + createUniversalAccountClient, + type CreateUniversalAccountClientParams, +} from "./client.js"; + +// Constants +export { + CHAIN_ID, + TOKEN_TYPE, + NATIVE_TOKEN_ADDRESS, + type ChainId, + type TokenType, +} from "./constants.js"; + +// Types +export type { + UniversalAccountConfig, + TradeConfig, + SmartAccountOptions, + AssetInfo, + ChainAssetInfo, + PrimaryAssets, + TokenIdentifier, + ExpectToken, + TransferTransactionParams, + UniversalTransactionParams, + BuyTransactionParams, + SellTransactionParams, + ConvertTransactionParams, + TransactionRequest, + UniversalTransaction, + FeeQuote, + TransactionResult, + IUniversalAccount, +} from "./types.js"; diff --git a/account-kit/universal-account/src/particle-sdk.d.ts b/account-kit/universal-account/src/particle-sdk.d.ts new file mode 100644 index 0000000000..47968fff78 --- /dev/null +++ b/account-kit/universal-account/src/particle-sdk.d.ts @@ -0,0 +1,144 @@ +/** + * Type declarations for @particle-network/universal-account-sdk + * This file helps TypeScript resolve the module when the SDK's package.json exports + * don't properly expose the type declarations. + */ +declare module "@particle-network/universal-account-sdk" { + export interface UniversalAccountConfig { + projectId: string; + projectClientKey?: string; + projectAppUuid?: string; + ownerAddress: string; + tradeConfig?: { + slippageBps?: number; + universalGas?: boolean; + usePrimaryTokens?: string[]; + }; + } + + export interface SmartAccountOptions { + name: string; + version: string; + ownerAddress: string; + smartAccountAddress: string; + solanaSmartAccountAddress?: string; + senderAddress: string; + senderSolanaAddress?: string; + } + + export interface PrimaryAssets { + assets: Array<{ + tokenType: string; + price: number; + amount: string; + amountInUSD: number; + chainAggregation?: Array<{ + token?: { + chainId: number; + address: string; + decimals: number; + }; + chainId?: number; + address?: string; + amount: string; + amountInUSD: number; + rawAmount: string; + decimals?: number; + }>; + }>; + totalAmountInUSD: number; + } + + export interface TransferTransactionParams { + token: { + chainId: number; + address: string; + }; + amount: string; + receiver: string; + } + + export interface UniversalTransactionParams { + chainId: number; + expectTokens: Array<{ + type: string; + amount: string; + }>; + transactions: Array<{ + to: string; + data: string; + value?: string; + }>; + } + + export interface UniversalTransaction { + type: string; + mode: string; + sender: string; + receiver: string; + transactionId: string; + rootHash: string; + smartAccountOptions: SmartAccountOptions; + feeQuotes: Array<{ + fees: { + totals: { + feeTokenAmountInUSD: string; + gasFeeTokenAmountInUSD: string; + transactionServiceFeeTokenAmountInUSD: string; + transactionLPFeeTokenAmountInUSD: string; + }; + freeGasFee: boolean; + freeServiceFee: boolean; + }; + }>; + } + + export interface TransactionResult { + transactionId: string; + status: string; + mode: string; + sender: string; + receiver: string; + tag: string; + created_at: string; + updated_at: string; + } + + export class UniversalAccount { + constructor(config: UniversalAccountConfig); + getSmartAccountOptions(): Promise; + getPrimaryAssets(): Promise; + createTransferTransaction( + params: TransferTransactionParams + ): Promise; + createUniversalTransaction( + params: UniversalTransactionParams + ): Promise; + sendTransaction( + transaction: UniversalTransaction, + signature: string + ): Promise; + } + + export const CHAIN_ID: { + ETHEREUM_MAINNET: number; + ARBITRUM_MAINNET_ONE: number; + BASE_MAINNET: number; + BSC_MAINNET: number; + POLYGON_MAINNET: number; + OPTIMISM_MAINNET: number; + AVALANCHE_MAINNET: number; + [key: string]: number; + }; + + export const SUPPORTED_TOKEN_TYPE: { + ETH: string; + USDT: string; + USDC: string; + BNB: string; + SOL: string; + MATIC: string; + AVAX: string; + [key: string]: string; + }; +} diff --git a/account-kit/universal-account/src/provider.tsx b/account-kit/universal-account/src/provider.tsx new file mode 100644 index 0000000000..cf35d7612a --- /dev/null +++ b/account-kit/universal-account/src/provider.tsx @@ -0,0 +1,552 @@ +"use client"; + +import { + createContext, + useContext, + useEffect, + useState, + useCallback, + useMemo, + type ReactNode, +} from "react"; +import type { Address } from "viem"; +import { + UniversalAccountClient, + createUniversalAccountClient, +} from "./client.js"; +import type { + UniversalAccountConfig, + PrimaryAssets, + TransferTransactionParams, + UniversalTransactionParams, + BuyTransactionParams, + SellTransactionParams, + ConvertTransactionParams, + UniversalTransaction, + TransactionResult, +} from "./types.js"; + +/** + * Configuration for Universal Accounts - simplified for seamless integration + */ +export interface UniversalAccountProviderConfig { + /** Particle Network project ID */ + projectId: string; + /** Particle Network client key */ + clientKey: string; + /** Particle Network app ID */ + appId: string; + /** Optional trade configuration */ + tradeConfig?: { + /** Slippage tolerance in basis points (100 = 1%) */ + slippageBps?: number; + /** Use PARTI token for gas fees */ + universalGas?: boolean; + }; +} + +/** + * Props for UniversalAccountProvider + */ +export interface UniversalAccountProviderProps { + children: ReactNode; + /** Universal Account configuration */ + config: UniversalAccountProviderConfig; +} + +/** + * Context value for Universal Account + */ +export interface UniversalAccountContextValue { + /** The Universal Account client instance */ + client: UniversalAccountClient | null; + /** Whether the client is currently initializing */ + isInitializing: boolean; + /** Whether the client is ready to use */ + isReady: boolean; + /** Error if initialization failed */ + error: Error | null; + /** EVM Universal Account address */ + address: Address | null; + /** Solana Universal Account address */ + solanaAddress: string | null; + /** Initialize the UA with an owner address */ + initialize: (ownerAddress: Address) => Promise; + /** Reset/disconnect the UA */ + disconnect: () => void; +} + +const UniversalAccountContext = + createContext(null); + +/** + * Provider component for Universal Account integration + * + * Wrap your app with this provider to enable Universal Account functionality. + * The UA will auto-initialize when you call `initialize` with the owner address. + * + * @param {UniversalAccountProviderProps} props - Provider props + * @param {ReactNode} props.children - Child components + * @param {UniversalAccountProviderConfig} props.config - UA configuration + * @returns {JSX.Element} Provider component + * + * @example + * ```tsx + * import { UniversalAccountProvider } from "@account-kit/universal-account"; + * + * function App() { + * return ( + * + * + * + * ); + * } + * ``` + */ +export function UniversalAccountProvider({ + children, + config, +}: UniversalAccountProviderProps): JSX.Element { + const [client, setClient] = useState(null); + const [isInitializing, setIsInitializing] = useState(false); + const [error, setError] = useState(null); + const [address, setAddress] = useState
(null); + const [solanaAddress, setSolanaAddress] = useState(null); + + const initialize = useCallback( + async (ownerAddress: Address) => { + if (!ownerAddress) return; + + setIsInitializing(true); + setError(null); + + try { + const uaConfig: UniversalAccountConfig = { + projectId: config.projectId, + projectClientKey: config.clientKey, + projectAppUuid: config.appId, + tradeConfig: config.tradeConfig, + }; + + const uaClient = await createUniversalAccountClient({ + ownerAddress, + config: uaConfig, + }); + + // Fetch addresses + const [evmAddr, solAddr] = await Promise.all([ + uaClient.getAddress(), + uaClient.getSolanaAddress(), + ]); + + setClient(uaClient); + setAddress(evmAddr); + setSolanaAddress(solAddr ?? null); + } catch (err) { + const error = + err instanceof Error + ? err + : new Error("Failed to initialize Universal Account"); + setError(error); + console.error("Universal Account initialization failed:", err); + } finally { + setIsInitializing(false); + } + }, + [config], + ); + + const disconnect = useCallback(() => { + setClient(null); + setAddress(null); + setSolanaAddress(null); + setError(null); + }, []); + + const value = useMemo( + () => ({ + client, + isInitializing, + isReady: client !== null && !isInitializing, + error, + address, + solanaAddress, + initialize, + disconnect, + }), + [ + client, + isInitializing, + error, + address, + solanaAddress, + initialize, + disconnect, + ], + ); + + return ( + + {children} + + ); +} + +/** + * Hook to access Universal Account context + * + * @returns {UniversalAccountContextValue} The Universal Account context + * @throws Error if used outside of UniversalAccountProvider + */ +export function useUniversalAccountContext(): UniversalAccountContextValue { + const context = useContext(UniversalAccountContext); + if (!context) { + throw new Error( + "useUniversalAccountContext must be used within a UniversalAccountProvider", + ); + } + return context; +} + +/** + * Hook to get the Universal Account client and auto-initialize with Account Kit + * + * This hook automatically initializes the Universal Account when the user + * is authenticated with Account Kit. Just provide the owner address from + * Account Kit's useAccount or useSigner hooks. + * + * @param {Address | null} [ownerAddress] - The EOA address that owns the Universal Account + * @returns {UniversalAccountContextValue} The Universal Account context with client and state + * + * @example + * ```tsx + * import { useUniversalAccount } from "@account-kit/universal-account"; + * import { useAccount } from "@account-kit/react"; + * + * function MyComponent() { + * const { address: ownerAddress } = useAccount({ type: "LightAccount" }); + * const { client, address, isReady, error } = useUniversalAccount(ownerAddress); + * + * if (!isReady) return
Loading...
; + * if (error) return
Error: {error.message}
; + * + * return
UA Address: {address}
; + * } + * ``` + */ +export function useUniversalAccount( + ownerAddress?: Address | null, +): UniversalAccountContextValue { + const context = useUniversalAccountContext(); + const { initialize, client, isInitializing } = context; + + // Auto-initialize when owner address is available + useEffect(() => { + if (ownerAddress && !client && !isInitializing) { + initialize(ownerAddress); + } + }, [ownerAddress, client, isInitializing, initialize]); + + // Disconnect when owner address is removed + useEffect(() => { + if (!ownerAddress && client) { + context.disconnect(); + } + }, [ownerAddress, client, context]); + + return context; +} + +/** + * Hook to get unified balance across all chains + * + * @param {object} [options] - Options for the hook + * @param {number} [options.refetchInterval] - Auto-refresh interval in milliseconds + * @returns {object} Balance state and refetch function + * + * @example + * ```tsx + * import { useUnifiedBalance } from "@account-kit/universal-account"; + * + * function BalanceDisplay() { + * const { totalBalanceUSD, assets, isLoading, refetch } = useUnifiedBalance(); + * + * return ( + *
+ *

Total: ${totalBalanceUSD?.toFixed(2)}

+ * + *
+ * ); + * } + * ``` + */ +export function useUnifiedBalance(options?: { refetchInterval?: number }): { + balance: PrimaryAssets | null; + totalBalanceUSD: number | null; + assets: any; + isLoading: boolean; + error: Error | null; + refetch: () => void; +} { + const { client, isReady } = useUniversalAccountContext(); + const [balance, setBalance] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchBalance = useCallback(async () => { + if (!client) return; + + setIsLoading(true); + setError(null); + + try { + const assets = await client.getPrimaryAssets(); + setBalance(assets); + } catch (err) { + setError( + err instanceof Error ? err : new Error("Failed to fetch balance"), + ); + } finally { + setIsLoading(false); + } + }, [client]); + + // Initial fetch when client is ready + useEffect(() => { + if (isReady && client) { + fetchBalance(); + } + }, [isReady, client, fetchBalance]); + + // Optional auto-refresh + useEffect(() => { + if (!options?.refetchInterval || !isReady) return; + + const interval = setInterval(fetchBalance, options.refetchInterval); + return () => clearInterval(interval); + }, [options?.refetchInterval, isReady, fetchBalance]); + + return { + balance, + totalBalanceUSD: balance?.totalAmountInUSD ?? null, + assets: balance?.assets ?? null, + isLoading, + error, + refetch: fetchBalance, + }; +} + +/** + * Options for transaction callbacks + */ +interface TransactionCallbackOptions { + /** Called when the transaction is created (before signing) */ + onTransactionCreated?: (tx: UniversalTransaction) => void; +} + +/** + * Hook to send Universal Account transactions + * + * Provides methods for all transaction types supported by Universal Accounts: + * - `sendTransfer` - Send tokens to any address + * - `sendUniversal` - Execute custom contract interactions + * - `sendBuy` - Buy/swap into a target token using USD value + * - `sendSell` - Sell a token back into primary assets + * - `sendConvert` - Convert between primary assets on a chain + * + * @param {object} options - Options for the hook + * @param {Function} options.signMessage - Function to sign messages with the owner wallet + * @returns {object} Transaction functions and state + * + * @example + * ```tsx + * import { useSendTransaction } from "@account-kit/universal-account"; + * import { useSigner } from "@account-kit/react"; + * import { toBytes } from "viem"; + * + * function TransactionButtons() { + * const signer = useSigner(); + * const { + * sendTransfer, + * sendUniversal, + * sendBuy, + * sendSell, + * sendConvert, + * isLoading + * } = useSendTransaction({ + * signMessage: (msg) => signer!.signMessage({ raw: toBytes(msg) }), + * }); + * + * // Transfer tokens + * const handleTransfer = () => sendTransfer({ + * token: { chainId: 42161, address: "0x..." }, + * amount: "10", + * receiver: "0x...", + * }); + * + * // Buy $10 worth of a token + * const handleBuy = () => sendBuy({ + * token: { chainId: 42161, address: "0x..." }, + * amountInUSD: "10", + * }); + * + * // Sell tokens + * const handleSell = () => sendSell({ + * token: { chainId: 42161, address: "0x..." }, + * amount: "0.1", + * }); + * + * // Convert to USDC on Arbitrum + * const handleConvert = () => sendConvert({ + * expectToken: { type: "USDC", amount: "1" }, + * chainId: 42161, + * }); + * } + * ``` + */ +export function useSendTransaction(options: { + signMessage: (message: string) => Promise; +}) { + const { client, isReady } = useUniversalAccountContext(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [lastResult, setLastResult] = useState(null); + + /** + * Helper to execute a transaction with signing + */ + const executeTransaction = useCallback( + async ( + createTx: () => Promise, + callbacks?: TransactionCallbackOptions, + ): Promise => { + if (!client) { + throw new Error("Universal Account not initialized"); + } + + setIsLoading(true); + setError(null); + + try { + const tx = await createTx(); + callbacks?.onTransactionCreated?.(tx); + + const signature = await options.signMessage(tx.rootHash); + const result = await client.sendTransaction(tx, signature); + + setLastResult(result); + return result; + } catch (err) { + const txError = + err instanceof Error ? err : new Error("Transaction failed"); + setError(txError); + throw txError; + } finally { + setIsLoading(false); + } + }, + [client, options], + ); + + /** + * Send a token transfer + */ + const sendTransfer = useCallback( + async ( + params: TransferTransactionParams & TransactionCallbackOptions, + ): Promise => { + return executeTransaction( + () => client!.createTransferTransaction(params), + params, + ); + }, + [client, executeTransaction], + ); + + /** + * Send a custom universal transaction (contract interactions) + */ + const sendUniversal = useCallback( + async ( + params: UniversalTransactionParams & TransactionCallbackOptions, + ): Promise => { + return executeTransaction( + () => client!.createUniversalTransaction(params), + params, + ); + }, + [client, executeTransaction], + ); + + /** + * Buy/swap into a target token using USD value from primary assets + */ + const sendBuy = useCallback( + async ( + params: BuyTransactionParams & TransactionCallbackOptions, + ): Promise => { + return executeTransaction( + () => client!.createBuyTransaction(params), + params, + ); + }, + [client, executeTransaction], + ); + + /** + * Sell a token back into primary assets + */ + const sendSell = useCallback( + async ( + params: SellTransactionParams & TransactionCallbackOptions, + ): Promise => { + return executeTransaction( + () => client!.createSellTransaction(params), + params, + ); + }, + [client, executeTransaction], + ); + + /** + * Convert between primary assets on a specific chain + */ + const sendConvert = useCallback( + async ( + params: ConvertTransactionParams & TransactionCallbackOptions, + ): Promise => { + return executeTransaction( + () => client!.createConvertTransaction(params), + params, + ); + }, + [client, executeTransaction], + ); + + return { + /** Send a token transfer */ + sendTransfer, + /** Send a custom contract interaction */ + sendUniversal, + /** Buy/swap into a target token */ + sendBuy, + /** Sell a token back into primary assets */ + sendSell, + /** Convert between primary assets */ + sendConvert, + /** Whether a transaction is in progress */ + isLoading, + /** Last error, if any */ + error, + /** Result of the last transaction */ + lastResult, + /** Whether the hook is ready to send transactions */ + isReady, + }; +} diff --git a/account-kit/universal-account/src/types.ts b/account-kit/universal-account/src/types.ts new file mode 100644 index 0000000000..c6d1294853 --- /dev/null +++ b/account-kit/universal-account/src/types.ts @@ -0,0 +1,284 @@ +/** + * Type definitions for @account-kit/universal-account + * + * These types define the interface for working with Particle Network's + * Universal Accounts within Alchemy Account Kit. + * + * @see https://developers.particle.network/universal-accounts/cha/overview + */ + +import type { Address } from "viem"; + +/** + * Configuration for initializing a Universal Account + */ +export interface UniversalAccountConfig { + /** Particle Network project ID from dashboard */ + projectId: string; + /** Particle Network client key from dashboard */ + projectClientKey: string; + /** Particle Network app UUID from dashboard */ + projectAppUuid: string; + /** Trade configuration for swaps and transactions */ + tradeConfig?: TradeConfig; +} + +/** + * Trade configuration options + */ +export interface TradeConfig { + /** Slippage tolerance in basis points (100 = 1%) */ + slippageBps?: number; + /** Use PARTI token to pay for gas fees */ + universalGas?: boolean; + /** Specify which primary tokens to use as source for swaps */ + usePrimaryTokens?: string[]; +} + +/** + * Smart account options returned from Universal Account + */ +export interface SmartAccountOptions { + /** Name of the smart account implementation */ + name: string; + /** Version of the smart account */ + version: string; + /** EOA address that owns the Universal Account */ + ownerAddress: Address; + /** EVM Universal Account address */ + smartAccountAddress: Address; + /** Solana Universal Account address */ + solanaSmartAccountAddress?: string; + /** Sender address for transactions */ + senderAddress: Address; + /** Sender Solana address */ + senderSolanaAddress?: string; +} + +/** + * Asset information for a specific token + */ +export interface AssetInfo { + /** Token type identifier */ + tokenType: string; + /** Current price in USD */ + price: number; + /** Amount held */ + amount: string; + /** Amount in USD */ + amountInUSD: number; + /** Breakdown by chain */ + chainAggregation: ChainAssetInfo[]; +} + +/** + * Asset information per chain + */ +export interface ChainAssetInfo { + /** Chain ID */ + chainId: number; + /** Token contract address */ + address: Address; + /** Amount on this chain */ + amount: string; + /** Amount in USD */ + amountInUSD: number; + /** Raw amount (with decimals) */ + rawAmount: string; + /** Token decimals */ + decimals: number; +} + +/** + * Primary assets response + */ +export interface PrimaryAssets { + /** List of assets */ + assets: AssetInfo[]; + /** Total balance in USD */ + totalAmountInUSD: number; +} + +/** + * Token identifier for transactions + */ +export interface TokenIdentifier { + /** Chain ID where the token exists */ + chainId: number; + /** Token contract address (use 0x0...0 for native token) */ + address: Address; +} + +/** + * Expected token for universal transactions + */ +export interface ExpectToken { + /** Token type (e.g., "ETH", "USDT") */ + type: string; + /** Amount to expect */ + amount: string; +} + +/** + * Parameters for creating a transfer transaction + */ +export interface TransferTransactionParams { + /** Token to transfer */ + token: TokenIdentifier; + /** Amount to transfer (human-readable) */ + amount: string; + /** Receiver address */ + receiver: Address; +} + +/** + * Parameters for creating a universal transaction + */ +export interface UniversalTransactionParams { + /** Destination chain ID */ + chainId: number; + /** Expected tokens on destination */ + expectTokens: ExpectToken[]; + /** Transactions to execute */ + transactions: TransactionRequest[]; +} + +/** + * Parameters for creating a buy/swap transaction + * Converts USD value from primary assets into a target token + */ +export interface BuyTransactionParams { + /** Target token to buy */ + token: TokenIdentifier; + /** Amount in USD to spend */ + amountInUSD: string; +} + +/** + * Parameters for creating a sell transaction + * Sells a token back into primary assets + */ +export interface SellTransactionParams { + /** Token to sell */ + token: TokenIdentifier; + /** Amount of token to sell (human-readable) */ + amount: string; +} + +/** + * Parameters for creating a convert transaction + * Converts between primary assets on a specific chain + */ +export interface ConvertTransactionParams { + /** Target token to convert to */ + expectToken: ExpectToken; + /** Destination chain ID */ + chainId: number; +} + +/** + * Transaction request for universal transactions + */ +export interface TransactionRequest { + /** Target contract address */ + to: Address; + /** Encoded function data */ + data: `0x${string}`; + /** Value to send (in wei, hex encoded) */ + value?: `0x${string}`; +} + +/** + * Universal transaction object returned from create methods + */ +export interface UniversalTransaction { + /** Transaction type */ + type: "universal"; + /** Network mode */ + mode: "mainnet" | "testnet"; + /** Sender address */ + sender: Address; + /** Receiver address */ + receiver: Address; + /** Transaction ID */ + transactionId: string; + /** Root hash to sign */ + rootHash: `0x${string}`; + /** Smart account options */ + smartAccountOptions: SmartAccountOptions; + /** Fee quotes */ + feeQuotes: FeeQuote[]; +} + +/** + * Fee quote for a transaction + */ +export interface FeeQuote { + fees: { + totals: { + feeTokenAmountInUSD: string; + gasFeeTokenAmountInUSD: string; + transactionServiceFeeTokenAmountInUSD: string; + transactionLPFeeTokenAmountInUSD: string; + }; + freeGasFee: boolean; + freeServiceFee: boolean; + }; +} + +/** + * Result from sending a transaction + */ +export interface TransactionResult { + /** Unique transaction ID */ + transactionId: string; + /** Transaction status */ + status: string; + /** Transaction mode */ + mode: "mainnet" | "testnet"; + /** Sender address */ + sender: Address; + /** Receiver address */ + receiver: Address; + /** Transaction tag (buy, swap, transfer, etc.) */ + tag: string; + /** Creation timestamp */ + created_at: string; + /** Last update timestamp */ + updated_at: string; +} + +/** + * Universal Account instance interface + */ +export interface IUniversalAccount { + /** Get smart account options/addresses */ + getSmartAccountOptions(): Promise; + /** Get primary assets (unified balance) */ + getPrimaryAssets(): Promise; + /** Create a transfer transaction */ + createTransferTransaction( + params: TransferTransactionParams + ): Promise; + /** Create a universal transaction */ + createUniversalTransaction( + params: UniversalTransactionParams + ): Promise; + /** Create a buy/swap transaction */ + createBuyTransaction( + params: BuyTransactionParams + ): Promise; + /** Create a sell transaction */ + createSellTransaction( + params: SellTransactionParams + ): Promise; + /** Create a convert transaction */ + createConvertTransaction( + params: ConvertTransactionParams + ): Promise; + /** Send a signed transaction */ + sendTransaction( + transaction: UniversalTransaction, + signature: string + ): Promise; +} diff --git a/account-kit/universal-account/tsconfig.build.json b/account-kit/universal-account/tsconfig.build.json new file mode 100644 index 0000000000..f13d852dc2 --- /dev/null +++ b/account-kit/universal-account/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test-d.ts", "src/__tests__/**/*"] +} diff --git a/account-kit/universal-account/tsconfig.json b/account-kit/universal-account/tsconfig.json new file mode 100644 index 0000000000..e5603d861e --- /dev/null +++ b/account-kit/universal-account/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "typescript-template/base.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/yarn.lock b/yarn.lock index 96a82bb31c..28a7b28de2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1239,20 +1239,7 @@ "@babel/parser" "^7.27.2" "@babel/types" "^7.27.1" -"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3": - version "7.27.1" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz" - integrity sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg== - dependencies: - "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.27.1" - "@babel/parser" "^7.27.1" - "@babel/template" "^7.27.1" - "@babel/types" "^7.27.1" - debug "^4.3.1" - globals "^11.1.0" - -"@babel/traverse@^7.18.9", "@babel/traverse@^7.20.0", "@babel/traverse@^7.25.3", "@babel/traverse@^7.27.1": +"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3", "@babel/traverse@^7.18.9", "@babel/traverse@^7.20.0", "@babel/traverse@^7.25.3", "@babel/traverse@^7.27.1": version "7.27.1" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz" integrity sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg== @@ -1507,6 +1494,40 @@ dependencies: chalk "^4.1.0" +"@coral-xyz/anchor-errors@^0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz#bdfd3a353131345244546876eb4afc0e125bec30" + integrity sha512-9Mkradf5yS5xiLWrl9WrpjqOrAV+/W2RQHDlbnAZBivoGpOs1ECjoDCkVk4aRG8ZdiFiB8zQEVlxf+8fKkmSfQ== + +"@coral-xyz/anchor@^0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.30.1.tgz#17f3e9134c28cd0ea83574c6bab4e410bcecec5d" + integrity sha512-gDXFoF5oHgpriXAaLpxyWBHdCs8Awgf/gLHIo6crv7Aqm937CNdY+x+6hoj7QR5vaJV7MxWSQ0NGFzL3kPbWEQ== + dependencies: + "@coral-xyz/anchor-errors" "^0.30.1" + "@coral-xyz/borsh" "^0.30.1" + "@noble/hashes" "^1.3.1" + "@solana/web3.js" "^1.68.0" + bn.js "^5.1.2" + bs58 "^4.0.1" + buffer-layout "^1.2.2" + camelcase "^6.3.0" + cross-fetch "^3.1.5" + crypto-hash "^1.3.0" + eventemitter3 "^4.0.7" + pako "^2.0.3" + snake-case "^3.0.4" + superstruct "^0.15.4" + toml "^3.0.0" + +"@coral-xyz/borsh@^0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.30.1.tgz#869d8833abe65685c72e9199b8688477a4f6b0e3" + integrity sha512-aaxswpPrCFKl8vZTbxLssA2RvwX2zmKLlRCIktJOwW+VpVwYtXRtlWiIP+c2pPRKneiTiWCN2GEMSH9j1zTlWQ== + dependencies: + bn.js "^5.1.2" + buffer-layout "^1.2.0" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" @@ -4854,6 +4875,23 @@ dependencies: "@particle-network/auth" "^1.3.1" +"@particle-network/universal-account-sdk@^1.0.0": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@particle-network/universal-account-sdk/-/universal-account-sdk-1.0.10.tgz#205ec4760fb0ebf87fca3cd3b07a249e10621cc9" + integrity sha512-xd1yaEBpeMl9QWRWq1h2yxwKFEae4pYPVsomnpLqZmY1XSqKRRCspdmVeGFgKHz6SaoowWTrmqkh+daP6SjAGQ== + dependencies: + "@coral-xyz/anchor" "^0.30.1" + "@noble/hashes" "^1.7.1" + "@solana/spl-token" "^0.4.9" + "@solana/web3.js" "^1.98.0" + axios "^1.8.4" + borsh "^2.0.0" + fast-json-stable-stringify "^2.1.0" + merkletreejs "^0.5.1" + ts-enum-util "^4.1.0" + uuid "^11.1.0" + viem "^2.24.3" + "@paulmillr/qr@^0.2.1": version "0.2.1" resolved "https://registry.npmjs.org/@paulmillr/qr/-/qr-0.2.1.tgz" @@ -7265,6 +7303,17 @@ "@solana/spl-token-metadata" "^0.1.6" buffer "^6.0.3" +"@solana/spl-token@^0.4.9": + version "0.4.14" + resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.4.14.tgz#b86bc8a17f50e9680137b585eca5f5eb9d55c025" + integrity sha512-u09zr96UBpX4U685MnvQsNzlvw9TiY005hk1vJmJr7gMJldoPG1eYU5/wNEyOA5lkMLiR/gOi9SFD4MefOYEsA== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/buffer-layout-utils" "^0.2.0" + "@solana/spl-token-group" "^0.0.7" + "@solana/spl-token-metadata" "^0.1.6" + buffer "^6.0.3" + "@solana/subscribable@2.3.0": version "2.3.0" resolved "https://registry.npmjs.org/@solana/subscribable/-/subscribable-2.3.0.tgz" @@ -7763,6 +7812,27 @@ rpc-websockets "^9.0.2" superstruct "^2.0.2" +"@solana/web3.js@^1.68.0": + version "1.98.4" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.98.4.tgz#df51d78be9d865181ec5138b4e699d48e6895bbe" + integrity sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw== + dependencies: + "@babel/runtime" "^7.25.0" + "@noble/curves" "^1.4.2" + "@noble/hashes" "^1.4.0" + "@solana/buffer-layout" "^4.0.1" + "@solana/codecs-numbers" "^2.1.0" + agentkeepalive "^4.5.0" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.1" + node-fetch "^2.7.0" + rpc-websockets "^9.0.2" + superstruct "^2.0.2" + "@solflare-wallet/metamask-sdk@^1.0.3": version "1.0.3" resolved "https://registry.npmjs.org/@solflare-wallet/metamask-sdk/-/metamask-sdk-1.0.3.tgz" @@ -12234,7 +12304,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.8, bn.js@^4.11.9: resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz" integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw== -bn.js@^5.2.0, bn.js@^5.2.1, bn.js@^5.2.2: +bn.js@^5.1.2, bn.js@^5.2.0, bn.js@^5.2.1, bn.js@^5.2.2: version "5.2.2" resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz" integrity sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw== @@ -12262,7 +12332,7 @@ boring-avatars@^1.11.2: resolved "https://registry.npmjs.org/boring-avatars/-/boring-avatars-1.11.2.tgz" integrity sha512-3+wkwPeObwS4R37FGXMYViqc4iTrIRj5yzfX9Qy4mnpZ26sX41dGMhsAgmKks1r/uufY1pl4vpgzMWHYfJRb2A== -borsh@2.0.0: +borsh@2.0.0, borsh@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/borsh/-/borsh-2.0.0.tgz" integrity sha512-kc9+BgR3zz9+cjbwM8ODoUB4fs3X3I5A/HtX7LZKxCLaMrEeDFoBpnhZY//DTS1VZBSs6S5v46RZRbZjRFspEg== @@ -12495,6 +12565,16 @@ buffer-from@^1.0.0: resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer-layout@^1.2.0, buffer-layout@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/buffer-layout/-/buffer-layout-1.2.2.tgz#b9814e7c7235783085f9ca4966a0cfff112259d5" + integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA== + +buffer-reverse@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-reverse/-/buffer-reverse-1.0.1.tgz#49283c8efa6f901bc01fa3304d06027971ae2f60" + integrity sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg== + buffer-xor@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz" @@ -13660,7 +13740,12 @@ crypto-browserify@^3.12.1: randombytes "^2.1.0" randomfill "^1.0.4" -crypto-js@^4.1.1: +crypto-hash@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247" + integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg== + +crypto-js@^4.1.1, crypto-js@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz" integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== @@ -14180,6 +14265,14 @@ domain-browser@^1.1.1: resolved "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + dot-prop@^5.1.0: version "5.3.0" resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz" @@ -19328,6 +19421,13 @@ loupe@^3.1.0, loupe@^3.1.1, loupe@^3.1.2: resolved "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz" integrity sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug== +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + lowlight@^1.17.0: version "1.20.0" resolved "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz" @@ -19875,6 +19975,15 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +merkletreejs@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/merkletreejs/-/merkletreejs-0.5.2.tgz#12321ff6121aa60ef237f4323a9fda193a69148f" + integrity sha512-MHqclSWRSQQbYciUMALC3PZmE23NPf5IIYo+Z7qAz5jVcqgCB95L1T9jGcr+FtOj2Pa2/X26uG2Xzxs7FJccUg== + dependencies: + buffer-reverse "^1.0.1" + crypto-js "^4.2.0" + treeify "^1.1.0" + methods@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" @@ -21388,6 +21497,14 @@ nice-try@^1.0.4: resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + nocache@^3.0.1: version "3.0.4" resolved "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz" @@ -22265,6 +22382,11 @@ pacote@^18.0.0, pacote@^18.0.6: ssri "^10.0.0" tar "^6.1.11" +pako@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + pako@~1.0.5: version "1.0.11" resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" @@ -24849,6 +24971,14 @@ smart-buffer@^4.2.0: resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + socket.io-client@^4.5.1, socket.io-client@^4.7.5: version "4.8.1" resolved "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz" @@ -25181,16 +25311,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -25316,7 +25437,7 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -25330,13 +25451,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" @@ -25479,6 +25593,11 @@ sudo-prompt@^9.0.0: resolved "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz" integrity sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw== +superstruct@^0.15.4: + version "0.15.5" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.15.5.tgz#0f0a8d3ce31313f0d84c6096cd4fa1bfdedc9dab" + integrity sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ== + superstruct@^1.0.3: version "1.0.4" resolved "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz" @@ -25914,6 +26033,11 @@ tr46@~0.0.3: resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +treeify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" + integrity sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A== + treeverse@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/treeverse/-/treeverse-3.0.0.tgz" @@ -25954,6 +26078,11 @@ ts-dedent@^2.0.0, ts-dedent@^2.2.0: resolved "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz" integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== +ts-enum-util@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ts-enum-util/-/ts-enum-util-4.1.0.tgz#d9d87f730a5c0bf72bad409e3ac431a9b0b6a878" + integrity sha512-kIs48itmNehkzLk0YJW/LfI2+VFYlyscGsY+oDNCnxrDfkex/OfYUV1ip7L7YIN7ppSqj2VmOOssiW81Rno9QA== + ts-interface-checker@^0.1.9: version "0.1.13" resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" @@ -26012,7 +26141,7 @@ tslib@2.6.2: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.6.0, tslib@^2.6.2, tslib@^2.8.0, tslib@^2.8.1: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.6.0, tslib@^2.6.2, tslib@^2.8.0, tslib@^2.8.1: version "2.8.1" resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -27011,7 +27140,7 @@ vfile@^6.0.0, vfile@^6.0.3: "@types/unist" "^3.0.0" vfile-message "^4.0.0" -viem@2.23.2, viem@2.29.2, viem@2.31.0, viem@2.33.3, viem@>=2.23.11, viem@>=2.29.0, viem@^2, viem@^2.1.1, viem@^2.21.35, viem@^2.21.40, viem@^2.29.2, viem@^2.31.7, viem@^2.32.0: +viem@2.23.2, viem@2.29.2, viem@2.31.0, viem@2.33.3, viem@>=2.23.11, viem@>=2.29.0, viem@^2, viem@^2.1.1, viem@^2.21.35, viem@^2.21.40, viem@^2.24.3, viem@^2.29.2, viem@^2.31.7, viem@^2.32.0: version "2.33.3" resolved "https://registry.yarnpkg.com/viem/-/viem-2.33.3.tgz#b69d7ff9edf649d1b7d9218e0225bcadc83a8caa" integrity sha512-aWDr6i6r3OfNCs0h9IieHFhn7xQJJ8YsuA49+9T5JRyGGAkWhLgcbLq2YMecgwM7HdUZpx1vPugZjsShqNi7Gw== @@ -27360,7 +27489,7 @@ wordwrap@^1.0.0: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -27378,15 +27507,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" From e61fc74c957a361fa0bbef47a81e280092efd9cb Mon Sep 17 00:00:00 2001 From: Soos3D <99700157+soos3d@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:36:31 -0300 Subject: [PATCH 2/4] feat: add universal account integration with Particle Network --- .eslintignore | 2 + README.md | 1 + account-kit/universal-account/CHANGELOG.md | 10 ++ account-kit/universal-account/README.md | 139 +++++++++++------- .../examples/next-example/.env.example | 10 ++ .../examples/next-example/.gitignore | 2 +- account-kit/universal-account/package.json | 6 +- account-kit/universal-account/src/client.ts | 48 +++--- .../universal-account/src/particle-sdk.d.ts | 6 +- account-kit/universal-account/src/types.ts | 12 +- .../universal-account/tsconfig.build.json | 8 +- docs-site | 2 +- 12 files changed, 158 insertions(+), 88 deletions(-) create mode 100644 account-kit/universal-account/CHANGELOG.md create mode 100644 account-kit/universal-account/examples/next-example/.env.example diff --git a/.eslintignore b/.eslintignore index 13e737468e..e91100e875 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,6 +9,8 @@ site/.vitepress/cache/**/* /examples/ui-demo/contracts /examples/ui-demo/.next/* +account-kit/universal-account/examples/* + **/.turbo/* account-kit/rn-signer/lib/* account-kit/java/* diff --git a/README.md b/README.md index d38bd8ffaa..a7e4897e5a 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Account Kit packages are all prefixed with `@account-kit` are broken down into t 1. [`@account-kit/signer`](https://github.com/alchemyplatform/aa-sdk/tree/main/account-kit/signer) 1. [`@account-kit/smart-contracts`](https://github.com/alchemyplatform/aa-sdk/tree/main/account-kit/smart-contracts) 1. [`@account-kit/privy-integration`](https://github.com/alchemyplatform/aa-sdk/tree/main/account-kit/privy-integration) +1. [`@account-kit/universal-account`](https://github.com/alchemyplatform/aa-sdk/tree/main/account-kit/universal-account) ## @aa-sdk/\* diff --git a/account-kit/universal-account/CHANGELOG.md b/account-kit/universal-account/CHANGELOG.md new file mode 100644 index 0000000000..23e53106b5 --- /dev/null +++ b/account-kit/universal-account/CHANGELOG.md @@ -0,0 +1,10 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 4.80.0 (2025-12-04) + +### Features + +- add Universal Account integration with Particle Network for chain abstraction diff --git a/account-kit/universal-account/README.md b/account-kit/universal-account/README.md index c915a6a375..390f0c8104 100644 --- a/account-kit/universal-account/README.md +++ b/account-kit/universal-account/README.md @@ -27,9 +27,11 @@ npm install @account-kit/universal-account You'll need credentials from both dashboards: **Alchemy** (for authentication): + - Get your API key from [Alchemy Dashboard](https://dashboard.alchemy.com) **Particle Network** (for Universal Accounts): + 1. Sign up at [Particle Dashboard](https://dashboard.particle.network/) 2. Create a project and web application 3. Copy your **Project ID**, **Client Key**, and **App ID** @@ -59,6 +61,7 @@ When integrating Alchemy Account Kit with Universal Accounts, it's important to ``` **Key Concept**: Alchemy's SCA and Universal Account's smart accounts are **different**! + - Use Alchemy for authentication and getting the EOA - Use Universal Accounts for cross-chain operations @@ -122,13 +125,13 @@ import { useUser, useSigner } from "@account-kit/react"; function MyComponent() { const user = useUser(); const signer = useSigner(); - + // ✅ CORRECT: Use EOA for Universal Accounts const eoaAddress = user?.address as `0x${string}` | undefined; - + // ❌ WRONG: Don't use SCA for Universal Accounts // const { address } = useAccount({ type: "LightAccount" }); - + return ; } ``` @@ -139,19 +142,22 @@ The `useUniversalAccount` hook auto-initializes when you pass the EOA address: ```tsx import { useUser, useSigner } from "@account-kit/react"; -import { useUniversalAccount, useUnifiedBalance } from "@account-kit/universal-account"; +import { + useUniversalAccount, + useUnifiedBalance, +} from "@account-kit/universal-account"; function Dashboard() { const user = useUser(); const eoaAddress = user?.address as `0x${string}` | undefined; // Universal Account auto-initializes with the EOA address - const { - address, // Universal Account EVM address - solanaAddress, // Universal Account Solana address - isReady, + const { + address, // Universal Account EVM address + solanaAddress, // Universal Account Solana address + isReady, isInitializing, - error + error, } = useUniversalAccount(eoaAddress); // Get unified balance across all chains @@ -166,14 +172,14 @@ function Dashboard() {

Universal Account

EVM Address: {address}

Solana Address: {solanaAddress}

- +

Unified Balance: ${totalBalanceUSD?.toFixed(2)}

{assets?.map((asset) => (
{asset.tokenType}: {asset.amount} (${asset.amountInUSD.toFixed(2)})
))} - + @@ -193,7 +199,7 @@ import { toBytes, encodeFunctionData } from "viem"; function MintNFT() { const signer = useSigner(); - + const { sendUniversal, isLoading, error, lastResult } = useSendTransaction({ signMessage: async (message: string) => { if (!signer) throw new Error("Signer not available"); @@ -223,7 +229,10 @@ function MintNFT() { }); console.log("Transaction ID:", result.transactionId); - console.log("View on UniversalX:", `https://universalx.app/activity/details?id=${result.transactionId}`); + console.log( + "View on UniversalX:", + `https://universalx.app/activity/details?id=${result.transactionId}`, + ); }; return ( @@ -232,7 +241,9 @@ function MintNFT() { {isLoading ? "Minting..." : "Mint NFT on Avalanche"} {lastResult && ( - + View Transaction )} @@ -251,35 +262,39 @@ function MintNFT() { The package exports helpful constants for chain IDs and token types: ```typescript -import { CHAIN_ID, TOKEN_TYPE, NATIVE_TOKEN_ADDRESS } from "@account-kit/universal-account"; +import { + CHAIN_ID, + TOKEN_TYPE, + NATIVE_TOKEN_ADDRESS, +} from "@account-kit/universal-account"; // Use chain IDs const tx = await sendUniversal({ - chainId: CHAIN_ID.AVALANCHE, // 43114 + chainId: CHAIN_ID.AVALANCHE, // 43114 // ... }); // Available chains: -CHAIN_ID.ETHEREUM // 1 -CHAIN_ID.BNB_CHAIN // 56 -CHAIN_ID.BASE // 8453 -CHAIN_ID.ARBITRUM // 42161 -CHAIN_ID.AVALANCHE // 43114 -CHAIN_ID.OPTIMISM // 10 -CHAIN_ID.POLYGON // 137 -CHAIN_ID.LINEA // 59144 -CHAIN_ID.BERACHAIN // 80094 -CHAIN_ID.SOLANA // 101 +CHAIN_ID.ETHEREUM; // 1 +CHAIN_ID.BNB_CHAIN; // 56 +CHAIN_ID.BASE; // 8453 +CHAIN_ID.ARBITRUM; // 42161 +CHAIN_ID.AVALANCHE; // 43114 +CHAIN_ID.OPTIMISM; // 10 +CHAIN_ID.POLYGON; // 137 +CHAIN_ID.LINEA; // 59144 +CHAIN_ID.BERACHAIN; // 80094 +CHAIN_ID.SOLANA; // 101 // ... and more // Token types for expectTokens -TOKEN_TYPE.ETH -TOKEN_TYPE.USDC -TOKEN_TYPE.USDT -TOKEN_TYPE.SOL +TOKEN_TYPE.ETH; +TOKEN_TYPE.USDC; +TOKEN_TYPE.USDT; +TOKEN_TYPE.SOL; // Native token address (for ETH, AVAX, etc.) -NATIVE_TOKEN_ADDRESS // "0x0000000000000000000000000000000000000000" +NATIVE_TOKEN_ADDRESS; // "0x0000000000000000000000000000000000000000" ``` --- @@ -332,10 +347,11 @@ Initialize and manage a Universal Account. Auto-initializes when `ownerAddress` | `disconnect` | `() => void` | Reset the Universal Account | **Example:** + ```tsx const user = useUser(); const { address, solanaAddress, isReady, error } = useUniversalAccount( - user?.address as `0x${string}` + user?.address as `0x${string}`, ); ``` @@ -361,13 +377,15 @@ Fetch the unified balance across all chains. Automatically fetches when the Univ | `refetch` | `() => void` | Manually refresh balance | **Asset Structure:** + ```typescript interface AssetInfo { - tokenType: string; // e.g., "USDT", "ETH" - price: number; // Current price in USD - amount: string; // Total amount across chains - amountInUSD: number; // Total value in USD - chainAggregation: { // Breakdown by chain + tokenType: string; // e.g., "USDT", "ETH" + price: number; // Current price in USD + amount: string; // Total amount across chains + amountInUSD: number; // Total value in USD + chainAggregation: { + // Breakdown by chain chainId: number; address: string; amount: string; @@ -377,6 +395,7 @@ interface AssetInfo { ``` **Example:** + ```tsx const { totalBalanceUSD, assets, refetch, isLoading } = useUnifiedBalance({ refetchInterval: 30000, // Refresh every 30 seconds @@ -410,20 +429,24 @@ Send Universal Account transactions with automatic signing flow. Supports all tr **Transaction Types:** ##### `sendTransfer` - Token Transfer + Send tokens to any address across chains. + ```typescript await sendTransfer({ token: { - chainId: 42161, // Arbitrum + chainId: 42161, // Arbitrum address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT }, - amount: "10", // Human-readable amount - receiver: "0x...", // Recipient address + amount: "10", // Human-readable amount + receiver: "0x...", // Recipient address }); ``` ##### `sendUniversal` - Custom Contract Interaction + Execute any contract call with automatic liquidity routing. + ```typescript await sendUniversal({ chainId: 8453, // Base @@ -439,40 +462,47 @@ await sendUniversal({ ``` ##### `sendBuy` - Buy/Swap Token + Buy a token using USD value from your primary assets. + ```typescript await sendBuy({ token: { - chainId: 42161, // Arbitrum + chainId: 42161, // Arbitrum address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT }, - amountInUSD: "10", // Spend $10 worth of primary assets + amountInUSD: "10", // Spend $10 worth of primary assets }); ``` ##### `sendSell` - Sell Token + Sell a token back into primary assets. + ```typescript await sendSell({ token: { - chainId: 42161, // Arbitrum + chainId: 42161, // Arbitrum address: "0x912CE59144191C1204E64559FE8253a0e49E6548", // ARB }, - amount: "0.1", // Sell 0.1 ARB + amount: "0.1", // Sell 0.1 ARB }); ``` ##### `sendConvert` - Convert Primary Assets + Convert between primary assets on a specific chain. + ```typescript await sendConvert({ expectToken: { type: "USDC", amount: "1" }, - chainId: 42161, // Arbitrum + chainId: 42161, // Arbitrum }); ``` **Solana Support:** All transaction types work with Solana. Use chain ID for Solana mainnet and the appropriate token addresses: + ```typescript // Buy SOL or Solana tokens await sendBuy({ @@ -485,6 +515,7 @@ await sendBuy({ ``` **Full Example:** + ```tsx const signer = useSigner(); @@ -498,13 +529,15 @@ const { sendTransfer, sendBuy, sendUniversal, isLoading } = useSendTransaction({ await sendUniversal({ chainId: 43114, expectTokens: [], - transactions: [{ - to: "0xdea7bF60E53CD578e3526F36eC431795f7EEbFe6", - data: encodeFunctionData({ - abi: [{ type: "function", name: "mint", inputs: [], outputs: [] }], - functionName: "mint", - }), - }], + transactions: [ + { + to: "0xdea7bF60E53CD578e3526F36eC431795f7EEbFe6", + data: encodeFunctionData({ + abi: [{ type: "function", name: "mint", inputs: [], outputs: [] }], + functionName: "mint", + }), + }, + ], }); ``` @@ -581,6 +614,7 @@ console.log("Explorer:", client.getExplorerUrl(result.transactionId)); ## Supported Chains Universal Accounts support 15+ EVM chains and Solana: + - Ethereum, Base, Arbitrum, Optimism, Polygon - Avalanche, BNB Chain, Fantom, Gnosis - And more... @@ -590,6 +624,7 @@ See the [full list of supported chains](https://developers.particle.network/univ ## Fees Universal Account transactions may include: + - **Gas fees**: Standard network fees on the destination chain - **LP fee**: 0.2% for cross-chain transactions - **Service fee**: 1% on transaction volume diff --git a/account-kit/universal-account/examples/next-example/.env.example b/account-kit/universal-account/examples/next-example/.env.example new file mode 100644 index 0000000000..19252ad9b0 --- /dev/null +++ b/account-kit/universal-account/examples/next-example/.env.example @@ -0,0 +1,10 @@ +# Alchemy Account Kit +# Get these from https://dashboard.alchemy.com +NEXT_PUBLIC_ALCHEMY_API_KEY=your_alchemy_api_key +NEXT_PUBLIC_ALCHEMY_POLICY_ID=your_gas_policy_id + +# Particle Network Universal Accounts +# Get these from https://dashboard.particle.network +NEXT_PUBLIC_PARTICLE_PROJECT_ID=your_particle_project_id +NEXT_PUBLIC_PARTICLE_CLIENT_KEY=your_particle_client_key +NEXT_PUBLIC_PARTICLE_APP_ID=your_particle_app_id diff --git a/account-kit/universal-account/examples/next-example/.gitignore b/account-kit/universal-account/examples/next-example/.gitignore index 86fd299e8c..5226837a17 100644 --- a/account-kit/universal-account/examples/next-example/.gitignore +++ b/account-kit/universal-account/examples/next-example/.gitignore @@ -32,7 +32,7 @@ yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) -.env* +.env # vercel .vercel diff --git a/account-kit/universal-account/package.json b/account-kit/universal-account/package.json index fabfe1de81..5f0779888e 100644 --- a/account-kit/universal-account/package.json +++ b/account-kit/universal-account/package.json @@ -1,6 +1,6 @@ { "name": "@account-kit/universal-account", - "version": "4.79.0", + "version": "4.80.0", "description": "Universal Account integration for Account Kit - enabling chain abstraction with Particle Network", "author": "Alchemy", "license": "MIT", @@ -39,8 +39,8 @@ "test:run": "vitest run --passWithNoTests" }, "dependencies": { - "@account-kit/infra": "^4.79.0", - "@account-kit/signer": "^4.79.0", + "@account-kit/infra": "^4.80.0", + "@account-kit/signer": "^4.80.0", "@particle-network/universal-account-sdk": "^1.0.0" }, "peerDependencies": { diff --git a/account-kit/universal-account/src/client.ts b/account-kit/universal-account/src/client.ts index 95b79e2bf0..ebef299085 100644 --- a/account-kit/universal-account/src/client.ts +++ b/account-kit/universal-account/src/client.ts @@ -49,6 +49,12 @@ export class UniversalAccountClient implements IUniversalAccount { private ua: any; private _ownerAddress: Address; + /** + * Creates a new UniversalAccountClient instance + * + * @param {any} ua - The underlying Particle Universal Account instance + * @param {Address} ownerAddress - The EOA address that owns this Universal Account + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(ua: any, ownerAddress: Address) { this.ua = ua; @@ -126,14 +132,15 @@ export class UniversalAccountClient implements IUniversalAccount { price: asset.price, amount: asset.amount, amountInUSD: asset.amountInUSD, - chainAggregation: asset.chainAggregation?.map((chain: any) => ({ - chainId: chain.token?.chainId ?? chain.chainId, - address: chain.token?.address ?? chain.address, - amount: chain.amount, - amountInUSD: chain.amountInUSD, - rawAmount: chain.rawAmount, - decimals: chain.token?.decimals ?? chain.decimals, - })) ?? [], + chainAggregation: + asset.chainAggregation?.map((chain: any) => ({ + chainId: chain.token?.chainId ?? chain.chainId, + address: chain.token?.address ?? chain.address, + amount: chain.amount, + amountInUSD: chain.amountInUSD, + rawAmount: chain.rawAmount, + decimals: chain.token?.decimals ?? chain.decimals, + })) ?? [], })), totalAmountInUSD: assets.totalAmountInUSD, }; @@ -164,7 +171,7 @@ export class UniversalAccountClient implements IUniversalAccount { * ``` */ async createTransferTransaction( - params: TransferTransactionParams + params: TransferTransactionParams, ): Promise { const tx = await this.ua.createTransferTransaction({ token: { @@ -202,7 +209,7 @@ export class UniversalAccountClient implements IUniversalAccount { * ``` */ async createUniversalTransaction( - params: UniversalTransactionParams + params: UniversalTransactionParams, ): Promise { const tx = await this.ua.createUniversalTransaction({ chainId: params.chainId, @@ -241,7 +248,7 @@ export class UniversalAccountClient implements IUniversalAccount { * ``` */ async createBuyTransaction( - params: BuyTransactionParams + params: BuyTransactionParams, ): Promise { const tx = await this.ua.createBuyTransaction({ token: { @@ -275,7 +282,7 @@ export class UniversalAccountClient implements IUniversalAccount { * ``` */ async createSellTransaction( - params: SellTransactionParams + params: SellTransactionParams, ): Promise { const tx = await this.ua.createSellTransaction({ token: { @@ -306,7 +313,7 @@ export class UniversalAccountClient implements IUniversalAccount { * ``` */ async createConvertTransaction( - params: ConvertTransactionParams + params: ConvertTransactionParams, ): Promise { const tx = await this.ua.createConvertTransaction({ expectToken: { @@ -335,15 +342,12 @@ export class UniversalAccountClient implements IUniversalAccount { */ async sendTransaction( transaction: UniversalTransaction, - signature: string + signature: string, ): Promise { // We need to pass the original transaction object from the SDK // The transaction parameter here is our mapped type, but we stored // the original in a way that sendTransaction can use it - const result = await this.ua.sendTransaction( - transaction as any, - signature - ); + const result = await this.ua.sendTransaction(transaction as any, signature); return { transactionId: result.transactionId, @@ -390,8 +394,10 @@ export class UniversalAccountClient implements IUniversalAccount { name: tx.smartAccountOptions?.name ?? "", version: tx.smartAccountOptions?.version ?? "", ownerAddress: tx.smartAccountOptions?.ownerAddress as Address, - smartAccountAddress: tx.smartAccountOptions?.smartAccountAddress as Address, - solanaSmartAccountAddress: tx.smartAccountOptions?.solanaSmartAccountAddress, + smartAccountAddress: tx.smartAccountOptions + ?.smartAccountAddress as Address, + solanaSmartAccountAddress: + tx.smartAccountOptions?.solanaSmartAccountAddress, senderAddress: tx.smartAccountOptions?.senderAddress as Address, senderSolanaAddress: tx.smartAccountOptions?.senderSolanaAddress, }, @@ -425,7 +431,7 @@ export class UniversalAccountClient implements IUniversalAccount { * ``` */ export async function createUniversalAccountClient( - params: CreateUniversalAccountClientParams + params: CreateUniversalAccountClientParams, ): Promise { // Dynamic import to avoid bundling issues - users must install the peer dependency const { UniversalAccount } = await import( diff --git a/account-kit/universal-account/src/particle-sdk.d.ts b/account-kit/universal-account/src/particle-sdk.d.ts index 47968fff78..976d14c5ef 100644 --- a/account-kit/universal-account/src/particle-sdk.d.ts +++ b/account-kit/universal-account/src/particle-sdk.d.ts @@ -109,14 +109,14 @@ declare module "@particle-network/universal-account-sdk" { getSmartAccountOptions(): Promise; getPrimaryAssets(): Promise; createTransferTransaction( - params: TransferTransactionParams + params: TransferTransactionParams, ): Promise; createUniversalTransaction( - params: UniversalTransactionParams + params: UniversalTransactionParams, ): Promise; sendTransaction( transaction: UniversalTransaction, - signature: string + signature: string, ): Promise; } diff --git a/account-kit/universal-account/src/types.ts b/account-kit/universal-account/src/types.ts index c6d1294853..401116f1f5 100644 --- a/account-kit/universal-account/src/types.ts +++ b/account-kit/universal-account/src/types.ts @@ -258,27 +258,27 @@ export interface IUniversalAccount { getPrimaryAssets(): Promise; /** Create a transfer transaction */ createTransferTransaction( - params: TransferTransactionParams + params: TransferTransactionParams, ): Promise; /** Create a universal transaction */ createUniversalTransaction( - params: UniversalTransactionParams + params: UniversalTransactionParams, ): Promise; /** Create a buy/swap transaction */ createBuyTransaction( - params: BuyTransactionParams + params: BuyTransactionParams, ): Promise; /** Create a sell transaction */ createSellTransaction( - params: SellTransactionParams + params: SellTransactionParams, ): Promise; /** Create a convert transaction */ createConvertTransaction( - params: ConvertTransactionParams + params: ConvertTransactionParams, ): Promise; /** Send a signed transaction */ sendTransaction( transaction: UniversalTransaction, - signature: string + signature: string, ): Promise; } diff --git a/account-kit/universal-account/tsconfig.build.json b/account-kit/universal-account/tsconfig.build.json index f13d852dc2..df3d024904 100644 --- a/account-kit/universal-account/tsconfig.build.json +++ b/account-kit/universal-account/tsconfig.build.json @@ -6,5 +6,11 @@ "declarationMap": true, "sourceMap": true }, - "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test-d.ts", "src/__tests__/**/*"] + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.test-d.ts", + "src/__tests__/**/*" + ] } diff --git a/docs-site b/docs-site index f58f3913ae..256822886b 160000 --- a/docs-site +++ b/docs-site @@ -1 +1 @@ -Subproject commit f58f3913ae157575f5925961d6c6fdb38a1462a8 +Subproject commit 256822886b4c07e8bac4bb97a5ed9c0b97d49ced From 0b33fc213d769cc1bdf5264e8b120c6aa656e708 Mon Sep 17 00:00:00 2001 From: Soos3D <99700157+soos3d@users.noreply.github.com> Date: Fri, 5 Dec 2025 10:50:45 -0300 Subject: [PATCH 3/4] feat: add tests and documentation for universal-account package --- account-kit/universal-account/README.md | 43 +- .../examples/next-example/package.json | 8 +- account-kit/universal-account/package.json | 2 +- .../src/__tests__/client.test.ts | 384 ++++++++++++++++++ .../src/__tests__/constants.test.ts | 61 +++ .../universal-account/vitest.config.ts | 9 + docs-site | 2 +- docs/docs.yml | 2 + docs/pages/third-party/universal-accounts.mdx | 279 +++++++++++++ yarn.lock | 8 +- 10 files changed, 784 insertions(+), 14 deletions(-) create mode 100644 account-kit/universal-account/src/__tests__/client.test.ts create mode 100644 account-kit/universal-account/src/__tests__/constants.test.ts create mode 100644 account-kit/universal-account/vitest.config.ts create mode 100644 docs/pages/third-party/universal-accounts.mdx diff --git a/account-kit/universal-account/README.md b/account-kit/universal-account/README.md index 390f0c8104..193078ba7a 100644 --- a/account-kit/universal-account/README.md +++ b/account-kit/universal-account/README.md @@ -631,13 +631,48 @@ Universal Account transactions may include: Fees are automatically calculated and shown in the transaction preview via `feeQuotes`. +## Testing + +### Run Tests + +```bash +# Run all tests +yarn test + +# Run tests once (CI mode) +yarn test:run +``` + +### Test Coverage + +The package includes two types of testing: + +**Unit Tests** (`src/__tests__/`) + +- `constants.test.ts` - Verifies exported chain IDs, token types, and constants +- `client.test.ts` - Tests the `UniversalAccountClient` wrapper logic using mocks + +Unit tests verify that: + +- Parameters are correctly passed to the underlying Particle SDK +- Responses are correctly mapped to our TypeScript types +- The client API behaves as expected + +**Integration Testing** (`examples/next-example/`) + +For full integration testing with real SDK calls, use the demo app: + +```bash +cd examples/next-example +yarn install +yarn dev +``` + +This tests the complete flow: authentication → Universal Account initialization → balance fetching → transaction signing. + ## Resources - [Particle Network Documentation](https://developers.particle.network/universal-accounts/cha/overview) - [Universal Accounts SDK Reference](https://developers.particle.network/universal-accounts/ua-reference/desktop/web) - [Supported Chains & Primary Assets](https://developers.particle.network/universal-accounts/cha/chains) -- [UniversalX Explorer](https://universalx.app) - -## License -MIT diff --git a/account-kit/universal-account/examples/next-example/package.json b/account-kit/universal-account/examples/next-example/package.json index 24f33f8d76..667dd25b9b 100644 --- a/account-kit/universal-account/examples/next-example/package.json +++ b/account-kit/universal-account/examples/next-example/package.json @@ -12,11 +12,11 @@ "react": "19.1.0", "react-dom": "19.1.0", "next": "15.5.2", - "@account-kit/react": "^4.79.0", - "@account-kit/infra": "^4.79.0", - "@account-kit/core": "^4.79.0", + "@account-kit/react": "^4.80.0", + "@account-kit/infra": "^4.80.0", + "@account-kit/core": "^4.80.0", "@tanstack/react-query": "^5.62.0", - "@particle-network/universal-account-sdk": "^1.0.10", + "@particle-network/universal-account-sdk": "^1.0.12", "viem": "^2.29.2" }, "devDependencies": { diff --git a/account-kit/universal-account/package.json b/account-kit/universal-account/package.json index 5f0779888e..624fd05a36 100644 --- a/account-kit/universal-account/package.json +++ b/account-kit/universal-account/package.json @@ -41,7 +41,7 @@ "dependencies": { "@account-kit/infra": "^4.80.0", "@account-kit/signer": "^4.80.0", - "@particle-network/universal-account-sdk": "^1.0.0" + "@particle-network/universal-account-sdk": "^1.0.12" }, "peerDependencies": { "react": ">=18.0.0", diff --git a/account-kit/universal-account/src/__tests__/client.test.ts b/account-kit/universal-account/src/__tests__/client.test.ts new file mode 100644 index 0000000000..d6299d720b --- /dev/null +++ b/account-kit/universal-account/src/__tests__/client.test.ts @@ -0,0 +1,384 @@ +/** + * Unit tests for UniversalAccountClient + * + * These tests verify the wrapper logic around the Particle Network SDK. + * They use mocks to test that: + * - Parameters are correctly passed to the underlying SDK + * - Responses are correctly mapped to our types + * - The client API behaves as expected + * + * NOTE: These are unit tests, not integration tests. They do NOT verify + * actual Particle SDK behavior or network calls. For integration testing, + * use the demo app in examples/next-example which tests the full flow + * with real SDK calls. + */ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { UniversalAccountClient } from "../client.js"; +import type { Address } from "viem"; + +// Mock the Particle SDK - we test our wrapper logic, not the SDK itself +vi.mock("@particle-network/universal-account-sdk", () => ({ + UniversalAccount: vi.fn(), +})); + +describe("UniversalAccountClient", () => { + const mockOwnerAddress: Address = + "0x1234567890123456789012345678901234567890"; + const mockSmartAccountAddress: Address = + "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; + const mockSolanaAddress = "SoLaNaAddReSs123456789012345678901234567890123"; + + const mockSmartAccountOptions = { + name: "UniversalAccount", + version: "1.0.0", + ownerAddress: mockOwnerAddress, + smartAccountAddress: mockSmartAccountAddress, + solanaSmartAccountAddress: mockSolanaAddress, + senderAddress: mockSmartAccountAddress, + senderSolanaAddress: mockSolanaAddress, + }; + + const mockPrimaryAssets = { + assets: [ + { + tokenType: "ETH", + price: 2000, + amount: "1.5", + amountInUSD: 3000, + chainAggregation: [ + { + token: { + chainId: 1, + address: "0x0000000000000000000000000000000000000000", + decimals: 18, + }, + amount: "1.0", + amountInUSD: 2000, + rawAmount: "1000000000000000000", + }, + { + token: { + chainId: 42161, + address: "0x0000000000000000000000000000000000000000", + decimals: 18, + }, + amount: "0.5", + amountInUSD: 1000, + rawAmount: "500000000000000000", + }, + ], + }, + ], + totalAmountInUSD: 3000, + }; + + let mockUa: any; + let client: UniversalAccountClient; + + beforeEach(() => { + mockUa = { + getSmartAccountOptions: vi + .fn() + .mockResolvedValue(mockSmartAccountOptions), + getPrimaryAssets: vi.fn().mockResolvedValue(mockPrimaryAssets), + createTransferTransaction: vi.fn(), + createUniversalTransaction: vi.fn(), + createBuyTransaction: vi.fn(), + createSellTransaction: vi.fn(), + createConvertTransaction: vi.fn(), + sendTransaction: vi.fn(), + }; + + client = new UniversalAccountClient(mockUa, mockOwnerAddress); + }); + + describe("getOwnerAddress", () => { + it("returns the owner address", () => { + expect(client.getOwnerAddress()).toBe(mockOwnerAddress); + }); + }); + + describe("getSmartAccountOptions", () => { + it("returns smart account options with correct types", async () => { + const options = await client.getSmartAccountOptions(); + + expect(options.name).toBe("UniversalAccount"); + expect(options.version).toBe("1.0.0"); + expect(options.ownerAddress).toBe(mockOwnerAddress); + expect(options.smartAccountAddress).toBe(mockSmartAccountAddress); + expect(options.solanaSmartAccountAddress).toBe(mockSolanaAddress); + }); + }); + + describe("getAddress", () => { + it("returns the EVM smart account address", async () => { + const address = await client.getAddress(); + expect(address).toBe(mockSmartAccountAddress); + }); + }); + + describe("getSolanaAddress", () => { + it("returns the Solana smart account address", async () => { + const address = await client.getSolanaAddress(); + expect(address).toBe(mockSolanaAddress); + }); + }); + + describe("getPrimaryAssets", () => { + it("returns formatted primary assets", async () => { + const assets = await client.getPrimaryAssets(); + + expect(assets.totalAmountInUSD).toBe(3000); + expect(assets.assets).toHaveLength(1); + expect(assets.assets[0].tokenType).toBe("ETH"); + expect(assets.assets[0].chainAggregation).toHaveLength(2); + }); + + it("correctly maps chain aggregation data", async () => { + const assets = await client.getPrimaryAssets(); + const ethAsset = assets.assets[0]; + + expect(ethAsset.chainAggregation[0].chainId).toBe(1); + expect(ethAsset.chainAggregation[0].amount).toBe("1.0"); + expect(ethAsset.chainAggregation[1].chainId).toBe(42161); + }); + }); + + describe("createTransferTransaction", () => { + it("calls underlying SDK with correct params", async () => { + const mockTx = { + type: "universal", + mode: "mainnet", + sender: mockSmartAccountAddress, + receiver: "0x9999999999999999999999999999999999999999", + transactionId: "tx-123", + rootHash: "0xabcd1234", + smartAccountOptions: mockSmartAccountOptions, + feeQuotes: [], + }; + mockUa.createTransferTransaction.mockResolvedValue(mockTx); + + const params = { + token: { + chainId: 42161, + address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" as Address, + }, + amount: "10", + receiver: "0x9999999999999999999999999999999999999999" as Address, + }; + + const tx = await client.createTransferTransaction(params); + + expect(mockUa.createTransferTransaction).toHaveBeenCalledWith({ + token: { chainId: 42161, address: params.token.address }, + amount: "10", + receiver: params.receiver, + }); + expect(tx.transactionId).toBe("tx-123"); + expect(tx.rootHash).toBe("0xabcd1234"); + }); + }); + + describe("createUniversalTransaction", () => { + it("calls underlying SDK with correct params", async () => { + const mockTx = { + type: "universal", + mode: "mainnet", + sender: mockSmartAccountAddress, + receiver: mockSmartAccountAddress, + transactionId: "tx-456", + rootHash: "0xdef456", + smartAccountOptions: mockSmartAccountOptions, + feeQuotes: [], + }; + mockUa.createUniversalTransaction.mockResolvedValue(mockTx); + + const params = { + chainId: 8453, + expectTokens: [{ type: "ETH", amount: "0.01" }], + transactions: [ + { + to: "0x1111111111111111111111111111111111111111" as Address, + data: "0x1234" as `0x${string}`, + }, + ], + }; + + const tx = await client.createUniversalTransaction(params); + + expect(mockUa.createUniversalTransaction).toHaveBeenCalledWith({ + chainId: 8453, + expectTokens: [{ type: "ETH", amount: "0.01" }], + transactions: [ + { to: params.transactions[0].to, data: "0x1234", value: undefined }, + ], + }); + expect(tx.transactionId).toBe("tx-456"); + }); + }); + + describe("sendTransaction", () => { + it("sends transaction with signature and returns result", async () => { + const mockResult = { + transactionId: "tx-789", + status: "pending", + mode: "mainnet", + sender: mockSmartAccountAddress, + receiver: "0x9999999999999999999999999999999999999999", + tag: "transfer", + created_at: "2024-01-01T00:00:00Z", + updated_at: "2024-01-01T00:00:00Z", + }; + mockUa.sendTransaction.mockResolvedValue(mockResult); + + const mockTx = { rootHash: "0xabc" } as any; + const signature = "0xsignature"; + + const result = await client.sendTransaction(mockTx, signature); + + expect(mockUa.sendTransaction).toHaveBeenCalledWith(mockTx, signature); + expect(result.transactionId).toBe("tx-789"); + expect(result.status).toBe("pending"); + }); + }); + + describe("getExplorerUrl", () => { + it("returns correct UniversalX explorer URL", () => { + const url = client.getExplorerUrl("tx-123"); + expect(url).toBe("https://universalx.app/activity/details?id=tx-123"); + }); + }); + + describe("getUnderlyingAccount", () => { + it("returns the underlying Particle UA instance", () => { + const ua = client.getUnderlyingAccount(); + expect(ua).toBe(mockUa); + }); + }); + + describe("createBuyTransaction", () => { + it("calls underlying SDK with correct params", async () => { + const mockTx = { + type: "universal", + mode: "mainnet", + sender: mockSmartAccountAddress, + receiver: mockSmartAccountAddress, + transactionId: "tx-buy-123", + rootHash: "0xbuy123", + smartAccountOptions: mockSmartAccountOptions, + feeQuotes: [], + }; + mockUa.createBuyTransaction.mockResolvedValue(mockTx); + + const params = { + token: { + chainId: 42161, + address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" as Address, + }, + amountInUSD: "10", + }; + + const tx = await client.createBuyTransaction(params); + + expect(mockUa.createBuyTransaction).toHaveBeenCalledWith({ + token: { chainId: 42161, address: params.token.address }, + amountInUSD: "10", + }); + expect(tx.transactionId).toBe("tx-buy-123"); + }); + }); + + describe("createSellTransaction", () => { + it("calls underlying SDK with correct params", async () => { + const mockTx = { + type: "universal", + mode: "mainnet", + sender: mockSmartAccountAddress, + receiver: mockSmartAccountAddress, + transactionId: "tx-sell-123", + rootHash: "0xsell123", + smartAccountOptions: mockSmartAccountOptions, + feeQuotes: [], + }; + mockUa.createSellTransaction.mockResolvedValue(mockTx); + + const params = { + token: { + chainId: 42161, + address: "0x912CE59144191C1204E64559FE8253a0e49E6548" as Address, + }, + amount: "0.1", + }; + + const tx = await client.createSellTransaction(params); + + expect(mockUa.createSellTransaction).toHaveBeenCalledWith({ + token: { chainId: 42161, address: params.token.address }, + amount: "0.1", + }); + expect(tx.transactionId).toBe("tx-sell-123"); + }); + }); + + describe("createConvertTransaction", () => { + it("calls underlying SDK with correct params", async () => { + const mockTx = { + type: "universal", + mode: "mainnet", + sender: mockSmartAccountAddress, + receiver: mockSmartAccountAddress, + transactionId: "tx-convert-123", + rootHash: "0xconvert123", + smartAccountOptions: mockSmartAccountOptions, + feeQuotes: [], + }; + mockUa.createConvertTransaction.mockResolvedValue(mockTx); + + const params = { + expectToken: { type: "USDC", amount: "1" }, + chainId: 42161, + }; + + const tx = await client.createConvertTransaction(params); + + expect(mockUa.createConvertTransaction).toHaveBeenCalledWith({ + expectToken: { type: "USDC", amount: "1" }, + chainId: 42161, + }); + expect(tx.transactionId).toBe("tx-convert-123"); + }); + }); +}); + +describe("createUniversalAccountClient", () => { + it("creates a client with the Particle SDK", async () => { + const { UniversalAccount } = await import( + "@particle-network/universal-account-sdk" + ); + + const mockUaInstance = { + getSmartAccountOptions: vi.fn(), + }; + (UniversalAccount as any).mockImplementation(() => mockUaInstance); + + const { createUniversalAccountClient } = await import("../client.js"); + + const client = await createUniversalAccountClient({ + ownerAddress: "0x1234567890123456789012345678901234567890", + config: { + projectId: "test-project-id", + projectClientKey: "test-client-key", + projectAppUuid: "test-app-uuid", + }, + }); + + expect(UniversalAccount).toHaveBeenCalledWith({ + projectId: "test-project-id", + projectClientKey: "test-client-key", + projectAppUuid: "test-app-uuid", + ownerAddress: "0x1234567890123456789012345678901234567890", + tradeConfig: undefined, + }); + expect(client).toBeInstanceOf(UniversalAccountClient); + }); +}); diff --git a/account-kit/universal-account/src/__tests__/constants.test.ts b/account-kit/universal-account/src/__tests__/constants.test.ts new file mode 100644 index 0000000000..201c8f3925 --- /dev/null +++ b/account-kit/universal-account/src/__tests__/constants.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from "vitest"; +import { CHAIN_ID, TOKEN_TYPE, NATIVE_TOKEN_ADDRESS } from "../constants.js"; + +describe("constants", () => { + describe("CHAIN_ID", () => { + it("exports all EVM chain IDs with correct values", () => { + expect(CHAIN_ID.ETHEREUM).toBe(1); + expect(CHAIN_ID.BNB_CHAIN).toBe(56); + expect(CHAIN_ID.AVALANCHE).toBe(43114); + expect(CHAIN_ID.POLYGON).toBe(137); + + expect(CHAIN_ID.BASE).toBe(8453); + expect(CHAIN_ID.ARBITRUM).toBe(42161); + expect(CHAIN_ID.OPTIMISM).toBe(10); + expect(CHAIN_ID.LINEA).toBe(59144); + + expect(CHAIN_ID.MANTLE).toBe(5000); + expect(CHAIN_ID.MONAD).toBe(143); + expect(CHAIN_ID.PLASMA).toBe(9745); + expect(CHAIN_ID.X_LAYER).toBe(196); + expect(CHAIN_ID.HYPER_EVM).toBe(999); + expect(CHAIN_ID.BERACHAIN).toBe(80094); + expect(CHAIN_ID.SONIC).toBe(146); + expect(CHAIN_ID.MERLIN).toBe(4200); + }); + + it("exports correct non-EVM chain IDs", () => { + expect(CHAIN_ID.SOLANA).toBe(101); + }); + + it("exports exactly 17 chain IDs", () => { + // This ensures we don't accidentally add/remove chains without updating tests + expect(Object.keys(CHAIN_ID)).toHaveLength(17); + }); + }); + + describe("TOKEN_TYPE", () => { + it("exports correct token types", () => { + expect(TOKEN_TYPE.ETH).toBe("ETH"); + expect(TOKEN_TYPE.USDC).toBe("USDC"); + expect(TOKEN_TYPE.USDT).toBe("USDT"); + expect(TOKEN_TYPE.SOL).toBe("SOL"); + expect(TOKEN_TYPE.BTC).toBe("BTC"); + expect(TOKEN_TYPE.BNB).toBe("BNB"); + expect(TOKEN_TYPE.MNT).toBe("MNT"); + }); + }); + + describe("NATIVE_TOKEN_ADDRESS", () => { + it("is the zero address", () => { + expect(NATIVE_TOKEN_ADDRESS).toBe( + "0x0000000000000000000000000000000000000000", + ); + }); + + it("has correct length for an Ethereum address", () => { + expect(NATIVE_TOKEN_ADDRESS).toHaveLength(42); + expect(NATIVE_TOKEN_ADDRESS.startsWith("0x")).toBe(true); + }); + }); +}); diff --git a/account-kit/universal-account/vitest.config.ts b/account-kit/universal-account/vitest.config.ts new file mode 100644 index 0000000000..0ce18d0b6a --- /dev/null +++ b/account-kit/universal-account/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + name: "account-kit/universal-account", + globals: true, + environment: "node", + }, +}); diff --git a/docs-site b/docs-site index 256822886b..0aaced699f 160000 --- a/docs-site +++ b/docs-site @@ -1 +1 @@ -Subproject commit 256822886b4c07e8bac4bb97a5ed9c0b97d49ced +Subproject commit 0aaced699f1436c7072e3284bb353a7a7033cee1 diff --git a/docs/docs.yml b/docs/docs.yml index 7216334518..cf53cf1a93 100644 --- a/docs/docs.yml +++ b/docs/docs.yml @@ -124,6 +124,8 @@ navigation: path: wallets/pages/transactions/swap-tokens/index.mdx - page: "[NEW] Cross-chain swaps" path: wallets/pages/transactions/cross-chain-swap-tokens/index.mdx + - page: "[NEW] Universal Accounts" + path: wallets/pages/third-party/universal-accounts.mdx - page: Send parallel transactions path: wallets/pages/transactions/send-parallel-transactions/index.mdx - page: Retry transactions diff --git a/docs/pages/third-party/universal-accounts.mdx b/docs/pages/third-party/universal-accounts.mdx new file mode 100644 index 0000000000..78695a6cc5 --- /dev/null +++ b/docs/pages/third-party/universal-accounts.mdx @@ -0,0 +1,279 @@ +--- +title: Universal Accounts +description: Enable chain abstraction with Particle Network Universal Accounts +slug: wallets/third-party/universal-accounts +--- + +Universal Accounts provide a single account, balance, and interaction point across all supported chains (EVM + Solana). This integration brings [Particle Network's Universal Accounts](https://developers.particle.network/universal-accounts/cha/overview) into Smart Wallets. + +## Key features + +- **Unified balance**: View and use assets across all chains as a single balance +- **Cross-chain transactions**: Send transactions to any chain without manual bridging +- **Universal gas**: Pay gas fees with any supported token +- **Solana support**: Interact with both EVM chains and Solana + +## Prerequisites + +You need credentials from both dashboards: + +**Alchemy** (for authentication): + +- Get your API key from [Alchemy Dashboard](https://dashboard.alchemy.com) + +**Particle Network** (for Universal Accounts): + +1. Sign up at [Particle Dashboard](https://dashboard.particle.network/) +2. Create a project and web application +3. Copy your **Project ID**, **Client Key**, and **App ID** + +## Installation + +```bash +npm install @account-kit/universal-account +# or +yarn add @account-kit/universal-account +``` + +## Architecture + +When integrating Smart Wallets with Universal Accounts, understand the different account types: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Smart Wallets (Alchemy) │ +├─────────────────────────────────────────────────────────────────┤ +│ useUser() → user.address = EOA (Externally Owned Account) │ +│ useAccount() → address = SCA (Smart Contract Account) │ +│ useSigner() → Signs messages with the EOA │ +└─────────────────────────────────────────────────────────────────┘ + │ + │ EOA address (user.address) + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Universal Accounts │ +├─────────────────────────────────────────────────────────────────┤ +│ Owner: EOA from Alchemy (user.address) │ +│ Creates: Multi-chain smart accounts (EVM + Solana) │ +│ Provides: Unified balance, cross-chain transactions │ +└─────────────────────────────────────────────────────────────────┘ +``` + + + Use the EOA address from `useUser().address` for Universal Accounts, not the + smart account address from `useAccount()`. These are different account types. + + +## Quick start + +Find a full demo app in the [examples](https://github.com/alchemyplatform/aa-sdk/tree/main/account-kit/universal-account/examples) directory. + +### 1. Set up providers + +Wrap your app with both `AlchemyAccountProvider` and `UniversalAccountProvider`: + +```tsx twoslash +// @noErrors +// providers.tsx +"use client"; + +import { AlchemyAccountProvider } from "@account-kit/react"; +import { UniversalAccountProvider } from "@account-kit/universal-account"; +import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; + +const queryClient = new QueryClient(); + +// Your Alchemy config +const config = { + // ... your alchemy config +}; + +const universalAccountConfig = { + projectId: process.env.NEXT_PUBLIC_PARTICLE_PROJECT_ID!, + clientKey: process.env.NEXT_PUBLIC_PARTICLE_CLIENT_KEY!, + appId: process.env.NEXT_PUBLIC_PARTICLE_APP_ID!, +}; + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); +} +``` + +### 2. Initialize Universal Account + +Use the `useUniversalAccount` hook with the EOA address: + +```tsx twoslash +// @noErrors +import { useUser } from "@account-kit/react"; +import { + useUniversalAccount, + useUnifiedBalance, +} from "@account-kit/universal-account"; + +function Dashboard() { + const user = useUser(); + const eoaAddress = user?.address as `0x${string}` | undefined; + + // Universal Account auto-initializes with the EOA address + const { + address, // Universal Account EVM address + solanaAddress, // Universal Account Solana address + isReady, + isInitializing, + error, + } = useUniversalAccount(eoaAddress); + + // Get unified balance across all chains + const { totalBalanceUSD, assets, isLoading, refetch } = useUnifiedBalance(); + + if (isInitializing) return
Initializing Universal Account...
; + if (error) return
Error: {error.message}
; + if (!isReady) return null; + + return ( +
+

Universal Account

+

EVM Address: {address}

+

Solana Address: {solanaAddress}

+

Unified Balance: ${totalBalanceUSD?.toFixed(2)}

+
+ ); +} +``` + +### 3. Send transactions + +Use `useSendTransaction` to send cross-chain transactions: + +```tsx twoslash +// @noErrors +import { useSigner } from "@account-kit/react"; +import { useSendTransaction } from "@account-kit/universal-account"; +import { toBytes, encodeFunctionData } from "viem"; + +function MintNFT() { + const signer = useSigner(); + + const { sendUniversal, isLoading, error, lastResult } = useSendTransaction({ + signMessage: async (message: string) => { + if (!signer) throw new Error("Signer not available"); + return await signer.signMessage({ raw: toBytes(message) }); + }, + }); + + const handleMint = async () => { + const NFT_CONTRACT = "0xdea7bF60E53CD578e3526F36eC431795f7EEbFe6"; + const AVALANCHE_CHAIN_ID = 43114; + + const mintData = encodeFunctionData({ + abi: [{ type: "function", name: "mint", inputs: [], outputs: [] }], + functionName: "mint", + }); + + const result = await sendUniversal({ + chainId: AVALANCHE_CHAIN_ID, + expectTokens: [], + transactions: [{ to: NFT_CONTRACT, data: mintData }], + }); + + console.log("Transaction ID:", result.transactionId); + }; + + return ( + + ); +} +``` + +## Transaction types + +The `useSendTransaction` hook provides methods for different transaction types: + +### Transfer tokens + +```tsx twoslash +// @noErrors +const { sendTransfer } = useSendTransaction({ signMessage }); + +await sendTransfer({ + token: { + chainId: 42161, // Arbitrum + address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT + }, + amount: "10", + receiver: "0x...", +}); +``` + +### Buy tokens + +Convert USD value from your primary assets into a target token: + +```tsx twoslash +// @noErrors +const { sendBuy } = useSendTransaction({ signMessage }); + +await sendBuy({ + token: { + chainId: 42161, + address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + }, + amountInUSD: "10", // Buy $10 worth +}); +``` + +### Sell tokens + +Sell a token back into primary assets: + +```tsx twoslash +// @noErrors +const { sendSell } = useSendTransaction({ signMessage }); + +await sendSell({ + token: { + chainId: 42161, + address: "0x912CE59144191C1204E64559FE8253a0e49E6548", // ARB + }, + amount: "0.1", +}); +``` + +## Supported chains + +Universal Accounts support 15+ EVM chains and Solana: + +- Ethereum, Base, Arbitrum, Optimism, Polygon +- Avalanche, BNB Chain, Linea, Berachain +- Solana +- And more... + +See the [full list of supported chains](https://developers.particle.network/universal-accounts/cha/chains). + +## Fees + +Universal Account transactions may include: + +- **Gas fees**: Standard network fees on the destination chain +- **LP fee**: 0.2% for cross-chain transactions +- **Service fee**: 1% on transaction volume + +Fees are automatically calculated and shown in the transaction preview. + +## Resources + +- [Particle Network Documentation](https://developers.particle.network/universal-accounts/cha/overview) +- [Universal Accounts SDK Reference](https://developers.particle.network/universal-accounts/ua-reference/desktop/web) +- [Supported Chains & Primary Assets](https://developers.particle.network/universal-accounts/cha/chains) +- [UniversalX Explorer](https://universalx.app) diff --git a/yarn.lock b/yarn.lock index 28a7b28de2..c27a2a4e24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4875,10 +4875,10 @@ dependencies: "@particle-network/auth" "^1.3.1" -"@particle-network/universal-account-sdk@^1.0.0": - version "1.0.10" - resolved "https://registry.yarnpkg.com/@particle-network/universal-account-sdk/-/universal-account-sdk-1.0.10.tgz#205ec4760fb0ebf87fca3cd3b07a249e10621cc9" - integrity sha512-xd1yaEBpeMl9QWRWq1h2yxwKFEae4pYPVsomnpLqZmY1XSqKRRCspdmVeGFgKHz6SaoowWTrmqkh+daP6SjAGQ== +"@particle-network/universal-account-sdk@^1.0.12": + version "1.0.12" + resolved "https://registry.yarnpkg.com/@particle-network/universal-account-sdk/-/universal-account-sdk-1.0.12.tgz#fbb1173629477ec34456624d53cd2095c86f7503" + integrity sha512-H9ws7mjpXx6K0MxbmQ8OK+j9Gn836Ns5ExQP9Bt8k6cmPKIgQM6IK6aR5bifKZaMUTthHFZpWU3jGRwa4wypyg== dependencies: "@coral-xyz/anchor" "^0.30.1" "@noble/hashes" "^1.7.1" From 79c941339837986cba23dfbd76fad1470ecbda87 Mon Sep 17 00:00:00 2001 From: Soos3D <99700157+soos3d@users.noreply.github.com> Date: Fri, 5 Dec 2025 10:50:45 -0300 Subject: [PATCH 4/4] feat: add tests and documentation for universal-account package --- account-kit/universal-account/README.md | 44 +- .../app/components/UniversalAccountDemo.tsx | 2 + .../examples/next-example/package.json | 8 +- account-kit/universal-account/package.json | 2 +- .../src/__tests__/client.test.ts | 384 ++++++++++++++++++ .../src/__tests__/constants.test.ts | 61 +++ .../universal-account/vitest.config.ts | 9 + docs-site | 2 +- docs/docs.yml | 2 + docs/pages/third-party/universal-accounts.mdx | 279 +++++++++++++ yarn.lock | 8 +- 11 files changed, 786 insertions(+), 15 deletions(-) create mode 100644 account-kit/universal-account/src/__tests__/client.test.ts create mode 100644 account-kit/universal-account/src/__tests__/constants.test.ts create mode 100644 account-kit/universal-account/vitest.config.ts create mode 100644 docs/pages/third-party/universal-accounts.mdx diff --git a/account-kit/universal-account/README.md b/account-kit/universal-account/README.md index 390f0c8104..af4000a48a 100644 --- a/account-kit/universal-account/README.md +++ b/account-kit/universal-account/README.md @@ -631,13 +631,47 @@ Universal Account transactions may include: Fees are automatically calculated and shown in the transaction preview via `feeQuotes`. +## Testing + +### Run Tests + +```bash +# Run all tests +yarn test + +# Run tests once (CI mode) +yarn test:run +``` + +### Test Coverage + +The package includes two types of testing: + +**Unit Tests** (`src/__tests__/`) + +- `constants.test.ts` - Verifies exported chain IDs, token types, and constants +- `client.test.ts` - Tests the `UniversalAccountClient` wrapper logic using mocks + +Unit tests verify that: + +- Parameters are correctly passed to the underlying Particle SDK +- Responses are correctly mapped to our TypeScript types +- The client API behaves as expected + +**Integration Testing** (`examples/next-example/`) + +For full integration testing with real SDK calls, use the demo app: + +```bash +cd examples/next-example +yarn install +yarn dev +``` + +This tests the complete flow: authentication → Universal Account initialization → balance fetching → transaction signing. + ## Resources - [Particle Network Documentation](https://developers.particle.network/universal-accounts/cha/overview) - [Universal Accounts SDK Reference](https://developers.particle.network/universal-accounts/ua-reference/desktop/web) - [Supported Chains & Primary Assets](https://developers.particle.network/universal-accounts/cha/chains) -- [UniversalX Explorer](https://universalx.app) - -## License - -MIT diff --git a/account-kit/universal-account/examples/next-example/app/components/UniversalAccountDemo.tsx b/account-kit/universal-account/examples/next-example/app/components/UniversalAccountDemo.tsx index ea79b7eb69..09c4c24390 100644 --- a/account-kit/universal-account/examples/next-example/app/components/UniversalAccountDemo.tsx +++ b/account-kit/universal-account/examples/next-example/app/components/UniversalAccountDemo.tsx @@ -96,6 +96,8 @@ export function UniversalAccountDemo({ // sendBuy, // For buying tokens // sendSell, // For selling tokens // sendConvert, // For converting assets + // Check Particle docs for more details + // https://developers.particle.network/universal-accounts/ua-reference/desktop/web#sending-a-transfer-transaction isLoading: isSending, } = useSendTransaction({ // This function signs the transaction hash with the Alchemy signer diff --git a/account-kit/universal-account/examples/next-example/package.json b/account-kit/universal-account/examples/next-example/package.json index 24f33f8d76..667dd25b9b 100644 --- a/account-kit/universal-account/examples/next-example/package.json +++ b/account-kit/universal-account/examples/next-example/package.json @@ -12,11 +12,11 @@ "react": "19.1.0", "react-dom": "19.1.0", "next": "15.5.2", - "@account-kit/react": "^4.79.0", - "@account-kit/infra": "^4.79.0", - "@account-kit/core": "^4.79.0", + "@account-kit/react": "^4.80.0", + "@account-kit/infra": "^4.80.0", + "@account-kit/core": "^4.80.0", "@tanstack/react-query": "^5.62.0", - "@particle-network/universal-account-sdk": "^1.0.10", + "@particle-network/universal-account-sdk": "^1.0.12", "viem": "^2.29.2" }, "devDependencies": { diff --git a/account-kit/universal-account/package.json b/account-kit/universal-account/package.json index 5f0779888e..624fd05a36 100644 --- a/account-kit/universal-account/package.json +++ b/account-kit/universal-account/package.json @@ -41,7 +41,7 @@ "dependencies": { "@account-kit/infra": "^4.80.0", "@account-kit/signer": "^4.80.0", - "@particle-network/universal-account-sdk": "^1.0.0" + "@particle-network/universal-account-sdk": "^1.0.12" }, "peerDependencies": { "react": ">=18.0.0", diff --git a/account-kit/universal-account/src/__tests__/client.test.ts b/account-kit/universal-account/src/__tests__/client.test.ts new file mode 100644 index 0000000000..d6299d720b --- /dev/null +++ b/account-kit/universal-account/src/__tests__/client.test.ts @@ -0,0 +1,384 @@ +/** + * Unit tests for UniversalAccountClient + * + * These tests verify the wrapper logic around the Particle Network SDK. + * They use mocks to test that: + * - Parameters are correctly passed to the underlying SDK + * - Responses are correctly mapped to our types + * - The client API behaves as expected + * + * NOTE: These are unit tests, not integration tests. They do NOT verify + * actual Particle SDK behavior or network calls. For integration testing, + * use the demo app in examples/next-example which tests the full flow + * with real SDK calls. + */ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { UniversalAccountClient } from "../client.js"; +import type { Address } from "viem"; + +// Mock the Particle SDK - we test our wrapper logic, not the SDK itself +vi.mock("@particle-network/universal-account-sdk", () => ({ + UniversalAccount: vi.fn(), +})); + +describe("UniversalAccountClient", () => { + const mockOwnerAddress: Address = + "0x1234567890123456789012345678901234567890"; + const mockSmartAccountAddress: Address = + "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; + const mockSolanaAddress = "SoLaNaAddReSs123456789012345678901234567890123"; + + const mockSmartAccountOptions = { + name: "UniversalAccount", + version: "1.0.0", + ownerAddress: mockOwnerAddress, + smartAccountAddress: mockSmartAccountAddress, + solanaSmartAccountAddress: mockSolanaAddress, + senderAddress: mockSmartAccountAddress, + senderSolanaAddress: mockSolanaAddress, + }; + + const mockPrimaryAssets = { + assets: [ + { + tokenType: "ETH", + price: 2000, + amount: "1.5", + amountInUSD: 3000, + chainAggregation: [ + { + token: { + chainId: 1, + address: "0x0000000000000000000000000000000000000000", + decimals: 18, + }, + amount: "1.0", + amountInUSD: 2000, + rawAmount: "1000000000000000000", + }, + { + token: { + chainId: 42161, + address: "0x0000000000000000000000000000000000000000", + decimals: 18, + }, + amount: "0.5", + amountInUSD: 1000, + rawAmount: "500000000000000000", + }, + ], + }, + ], + totalAmountInUSD: 3000, + }; + + let mockUa: any; + let client: UniversalAccountClient; + + beforeEach(() => { + mockUa = { + getSmartAccountOptions: vi + .fn() + .mockResolvedValue(mockSmartAccountOptions), + getPrimaryAssets: vi.fn().mockResolvedValue(mockPrimaryAssets), + createTransferTransaction: vi.fn(), + createUniversalTransaction: vi.fn(), + createBuyTransaction: vi.fn(), + createSellTransaction: vi.fn(), + createConvertTransaction: vi.fn(), + sendTransaction: vi.fn(), + }; + + client = new UniversalAccountClient(mockUa, mockOwnerAddress); + }); + + describe("getOwnerAddress", () => { + it("returns the owner address", () => { + expect(client.getOwnerAddress()).toBe(mockOwnerAddress); + }); + }); + + describe("getSmartAccountOptions", () => { + it("returns smart account options with correct types", async () => { + const options = await client.getSmartAccountOptions(); + + expect(options.name).toBe("UniversalAccount"); + expect(options.version).toBe("1.0.0"); + expect(options.ownerAddress).toBe(mockOwnerAddress); + expect(options.smartAccountAddress).toBe(mockSmartAccountAddress); + expect(options.solanaSmartAccountAddress).toBe(mockSolanaAddress); + }); + }); + + describe("getAddress", () => { + it("returns the EVM smart account address", async () => { + const address = await client.getAddress(); + expect(address).toBe(mockSmartAccountAddress); + }); + }); + + describe("getSolanaAddress", () => { + it("returns the Solana smart account address", async () => { + const address = await client.getSolanaAddress(); + expect(address).toBe(mockSolanaAddress); + }); + }); + + describe("getPrimaryAssets", () => { + it("returns formatted primary assets", async () => { + const assets = await client.getPrimaryAssets(); + + expect(assets.totalAmountInUSD).toBe(3000); + expect(assets.assets).toHaveLength(1); + expect(assets.assets[0].tokenType).toBe("ETH"); + expect(assets.assets[0].chainAggregation).toHaveLength(2); + }); + + it("correctly maps chain aggregation data", async () => { + const assets = await client.getPrimaryAssets(); + const ethAsset = assets.assets[0]; + + expect(ethAsset.chainAggregation[0].chainId).toBe(1); + expect(ethAsset.chainAggregation[0].amount).toBe("1.0"); + expect(ethAsset.chainAggregation[1].chainId).toBe(42161); + }); + }); + + describe("createTransferTransaction", () => { + it("calls underlying SDK with correct params", async () => { + const mockTx = { + type: "universal", + mode: "mainnet", + sender: mockSmartAccountAddress, + receiver: "0x9999999999999999999999999999999999999999", + transactionId: "tx-123", + rootHash: "0xabcd1234", + smartAccountOptions: mockSmartAccountOptions, + feeQuotes: [], + }; + mockUa.createTransferTransaction.mockResolvedValue(mockTx); + + const params = { + token: { + chainId: 42161, + address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" as Address, + }, + amount: "10", + receiver: "0x9999999999999999999999999999999999999999" as Address, + }; + + const tx = await client.createTransferTransaction(params); + + expect(mockUa.createTransferTransaction).toHaveBeenCalledWith({ + token: { chainId: 42161, address: params.token.address }, + amount: "10", + receiver: params.receiver, + }); + expect(tx.transactionId).toBe("tx-123"); + expect(tx.rootHash).toBe("0xabcd1234"); + }); + }); + + describe("createUniversalTransaction", () => { + it("calls underlying SDK with correct params", async () => { + const mockTx = { + type: "universal", + mode: "mainnet", + sender: mockSmartAccountAddress, + receiver: mockSmartAccountAddress, + transactionId: "tx-456", + rootHash: "0xdef456", + smartAccountOptions: mockSmartAccountOptions, + feeQuotes: [], + }; + mockUa.createUniversalTransaction.mockResolvedValue(mockTx); + + const params = { + chainId: 8453, + expectTokens: [{ type: "ETH", amount: "0.01" }], + transactions: [ + { + to: "0x1111111111111111111111111111111111111111" as Address, + data: "0x1234" as `0x${string}`, + }, + ], + }; + + const tx = await client.createUniversalTransaction(params); + + expect(mockUa.createUniversalTransaction).toHaveBeenCalledWith({ + chainId: 8453, + expectTokens: [{ type: "ETH", amount: "0.01" }], + transactions: [ + { to: params.transactions[0].to, data: "0x1234", value: undefined }, + ], + }); + expect(tx.transactionId).toBe("tx-456"); + }); + }); + + describe("sendTransaction", () => { + it("sends transaction with signature and returns result", async () => { + const mockResult = { + transactionId: "tx-789", + status: "pending", + mode: "mainnet", + sender: mockSmartAccountAddress, + receiver: "0x9999999999999999999999999999999999999999", + tag: "transfer", + created_at: "2024-01-01T00:00:00Z", + updated_at: "2024-01-01T00:00:00Z", + }; + mockUa.sendTransaction.mockResolvedValue(mockResult); + + const mockTx = { rootHash: "0xabc" } as any; + const signature = "0xsignature"; + + const result = await client.sendTransaction(mockTx, signature); + + expect(mockUa.sendTransaction).toHaveBeenCalledWith(mockTx, signature); + expect(result.transactionId).toBe("tx-789"); + expect(result.status).toBe("pending"); + }); + }); + + describe("getExplorerUrl", () => { + it("returns correct UniversalX explorer URL", () => { + const url = client.getExplorerUrl("tx-123"); + expect(url).toBe("https://universalx.app/activity/details?id=tx-123"); + }); + }); + + describe("getUnderlyingAccount", () => { + it("returns the underlying Particle UA instance", () => { + const ua = client.getUnderlyingAccount(); + expect(ua).toBe(mockUa); + }); + }); + + describe("createBuyTransaction", () => { + it("calls underlying SDK with correct params", async () => { + const mockTx = { + type: "universal", + mode: "mainnet", + sender: mockSmartAccountAddress, + receiver: mockSmartAccountAddress, + transactionId: "tx-buy-123", + rootHash: "0xbuy123", + smartAccountOptions: mockSmartAccountOptions, + feeQuotes: [], + }; + mockUa.createBuyTransaction.mockResolvedValue(mockTx); + + const params = { + token: { + chainId: 42161, + address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" as Address, + }, + amountInUSD: "10", + }; + + const tx = await client.createBuyTransaction(params); + + expect(mockUa.createBuyTransaction).toHaveBeenCalledWith({ + token: { chainId: 42161, address: params.token.address }, + amountInUSD: "10", + }); + expect(tx.transactionId).toBe("tx-buy-123"); + }); + }); + + describe("createSellTransaction", () => { + it("calls underlying SDK with correct params", async () => { + const mockTx = { + type: "universal", + mode: "mainnet", + sender: mockSmartAccountAddress, + receiver: mockSmartAccountAddress, + transactionId: "tx-sell-123", + rootHash: "0xsell123", + smartAccountOptions: mockSmartAccountOptions, + feeQuotes: [], + }; + mockUa.createSellTransaction.mockResolvedValue(mockTx); + + const params = { + token: { + chainId: 42161, + address: "0x912CE59144191C1204E64559FE8253a0e49E6548" as Address, + }, + amount: "0.1", + }; + + const tx = await client.createSellTransaction(params); + + expect(mockUa.createSellTransaction).toHaveBeenCalledWith({ + token: { chainId: 42161, address: params.token.address }, + amount: "0.1", + }); + expect(tx.transactionId).toBe("tx-sell-123"); + }); + }); + + describe("createConvertTransaction", () => { + it("calls underlying SDK with correct params", async () => { + const mockTx = { + type: "universal", + mode: "mainnet", + sender: mockSmartAccountAddress, + receiver: mockSmartAccountAddress, + transactionId: "tx-convert-123", + rootHash: "0xconvert123", + smartAccountOptions: mockSmartAccountOptions, + feeQuotes: [], + }; + mockUa.createConvertTransaction.mockResolvedValue(mockTx); + + const params = { + expectToken: { type: "USDC", amount: "1" }, + chainId: 42161, + }; + + const tx = await client.createConvertTransaction(params); + + expect(mockUa.createConvertTransaction).toHaveBeenCalledWith({ + expectToken: { type: "USDC", amount: "1" }, + chainId: 42161, + }); + expect(tx.transactionId).toBe("tx-convert-123"); + }); + }); +}); + +describe("createUniversalAccountClient", () => { + it("creates a client with the Particle SDK", async () => { + const { UniversalAccount } = await import( + "@particle-network/universal-account-sdk" + ); + + const mockUaInstance = { + getSmartAccountOptions: vi.fn(), + }; + (UniversalAccount as any).mockImplementation(() => mockUaInstance); + + const { createUniversalAccountClient } = await import("../client.js"); + + const client = await createUniversalAccountClient({ + ownerAddress: "0x1234567890123456789012345678901234567890", + config: { + projectId: "test-project-id", + projectClientKey: "test-client-key", + projectAppUuid: "test-app-uuid", + }, + }); + + expect(UniversalAccount).toHaveBeenCalledWith({ + projectId: "test-project-id", + projectClientKey: "test-client-key", + projectAppUuid: "test-app-uuid", + ownerAddress: "0x1234567890123456789012345678901234567890", + tradeConfig: undefined, + }); + expect(client).toBeInstanceOf(UniversalAccountClient); + }); +}); diff --git a/account-kit/universal-account/src/__tests__/constants.test.ts b/account-kit/universal-account/src/__tests__/constants.test.ts new file mode 100644 index 0000000000..201c8f3925 --- /dev/null +++ b/account-kit/universal-account/src/__tests__/constants.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from "vitest"; +import { CHAIN_ID, TOKEN_TYPE, NATIVE_TOKEN_ADDRESS } from "../constants.js"; + +describe("constants", () => { + describe("CHAIN_ID", () => { + it("exports all EVM chain IDs with correct values", () => { + expect(CHAIN_ID.ETHEREUM).toBe(1); + expect(CHAIN_ID.BNB_CHAIN).toBe(56); + expect(CHAIN_ID.AVALANCHE).toBe(43114); + expect(CHAIN_ID.POLYGON).toBe(137); + + expect(CHAIN_ID.BASE).toBe(8453); + expect(CHAIN_ID.ARBITRUM).toBe(42161); + expect(CHAIN_ID.OPTIMISM).toBe(10); + expect(CHAIN_ID.LINEA).toBe(59144); + + expect(CHAIN_ID.MANTLE).toBe(5000); + expect(CHAIN_ID.MONAD).toBe(143); + expect(CHAIN_ID.PLASMA).toBe(9745); + expect(CHAIN_ID.X_LAYER).toBe(196); + expect(CHAIN_ID.HYPER_EVM).toBe(999); + expect(CHAIN_ID.BERACHAIN).toBe(80094); + expect(CHAIN_ID.SONIC).toBe(146); + expect(CHAIN_ID.MERLIN).toBe(4200); + }); + + it("exports correct non-EVM chain IDs", () => { + expect(CHAIN_ID.SOLANA).toBe(101); + }); + + it("exports exactly 17 chain IDs", () => { + // This ensures we don't accidentally add/remove chains without updating tests + expect(Object.keys(CHAIN_ID)).toHaveLength(17); + }); + }); + + describe("TOKEN_TYPE", () => { + it("exports correct token types", () => { + expect(TOKEN_TYPE.ETH).toBe("ETH"); + expect(TOKEN_TYPE.USDC).toBe("USDC"); + expect(TOKEN_TYPE.USDT).toBe("USDT"); + expect(TOKEN_TYPE.SOL).toBe("SOL"); + expect(TOKEN_TYPE.BTC).toBe("BTC"); + expect(TOKEN_TYPE.BNB).toBe("BNB"); + expect(TOKEN_TYPE.MNT).toBe("MNT"); + }); + }); + + describe("NATIVE_TOKEN_ADDRESS", () => { + it("is the zero address", () => { + expect(NATIVE_TOKEN_ADDRESS).toBe( + "0x0000000000000000000000000000000000000000", + ); + }); + + it("has correct length for an Ethereum address", () => { + expect(NATIVE_TOKEN_ADDRESS).toHaveLength(42); + expect(NATIVE_TOKEN_ADDRESS.startsWith("0x")).toBe(true); + }); + }); +}); diff --git a/account-kit/universal-account/vitest.config.ts b/account-kit/universal-account/vitest.config.ts new file mode 100644 index 0000000000..0ce18d0b6a --- /dev/null +++ b/account-kit/universal-account/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + name: "account-kit/universal-account", + globals: true, + environment: "node", + }, +}); diff --git a/docs-site b/docs-site index 256822886b..0aaced699f 160000 --- a/docs-site +++ b/docs-site @@ -1 +1 @@ -Subproject commit 256822886b4c07e8bac4bb97a5ed9c0b97d49ced +Subproject commit 0aaced699f1436c7072e3284bb353a7a7033cee1 diff --git a/docs/docs.yml b/docs/docs.yml index 7216334518..cf53cf1a93 100644 --- a/docs/docs.yml +++ b/docs/docs.yml @@ -124,6 +124,8 @@ navigation: path: wallets/pages/transactions/swap-tokens/index.mdx - page: "[NEW] Cross-chain swaps" path: wallets/pages/transactions/cross-chain-swap-tokens/index.mdx + - page: "[NEW] Universal Accounts" + path: wallets/pages/third-party/universal-accounts.mdx - page: Send parallel transactions path: wallets/pages/transactions/send-parallel-transactions/index.mdx - page: Retry transactions diff --git a/docs/pages/third-party/universal-accounts.mdx b/docs/pages/third-party/universal-accounts.mdx new file mode 100644 index 0000000000..78695a6cc5 --- /dev/null +++ b/docs/pages/third-party/universal-accounts.mdx @@ -0,0 +1,279 @@ +--- +title: Universal Accounts +description: Enable chain abstraction with Particle Network Universal Accounts +slug: wallets/third-party/universal-accounts +--- + +Universal Accounts provide a single account, balance, and interaction point across all supported chains (EVM + Solana). This integration brings [Particle Network's Universal Accounts](https://developers.particle.network/universal-accounts/cha/overview) into Smart Wallets. + +## Key features + +- **Unified balance**: View and use assets across all chains as a single balance +- **Cross-chain transactions**: Send transactions to any chain without manual bridging +- **Universal gas**: Pay gas fees with any supported token +- **Solana support**: Interact with both EVM chains and Solana + +## Prerequisites + +You need credentials from both dashboards: + +**Alchemy** (for authentication): + +- Get your API key from [Alchemy Dashboard](https://dashboard.alchemy.com) + +**Particle Network** (for Universal Accounts): + +1. Sign up at [Particle Dashboard](https://dashboard.particle.network/) +2. Create a project and web application +3. Copy your **Project ID**, **Client Key**, and **App ID** + +## Installation + +```bash +npm install @account-kit/universal-account +# or +yarn add @account-kit/universal-account +``` + +## Architecture + +When integrating Smart Wallets with Universal Accounts, understand the different account types: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Smart Wallets (Alchemy) │ +├─────────────────────────────────────────────────────────────────┤ +│ useUser() → user.address = EOA (Externally Owned Account) │ +│ useAccount() → address = SCA (Smart Contract Account) │ +│ useSigner() → Signs messages with the EOA │ +└─────────────────────────────────────────────────────────────────┘ + │ + │ EOA address (user.address) + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Universal Accounts │ +├─────────────────────────────────────────────────────────────────┤ +│ Owner: EOA from Alchemy (user.address) │ +│ Creates: Multi-chain smart accounts (EVM + Solana) │ +│ Provides: Unified balance, cross-chain transactions │ +└─────────────────────────────────────────────────────────────────┘ +``` + + + Use the EOA address from `useUser().address` for Universal Accounts, not the + smart account address from `useAccount()`. These are different account types. + + +## Quick start + +Find a full demo app in the [examples](https://github.com/alchemyplatform/aa-sdk/tree/main/account-kit/universal-account/examples) directory. + +### 1. Set up providers + +Wrap your app with both `AlchemyAccountProvider` and `UniversalAccountProvider`: + +```tsx twoslash +// @noErrors +// providers.tsx +"use client"; + +import { AlchemyAccountProvider } from "@account-kit/react"; +import { UniversalAccountProvider } from "@account-kit/universal-account"; +import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; + +const queryClient = new QueryClient(); + +// Your Alchemy config +const config = { + // ... your alchemy config +}; + +const universalAccountConfig = { + projectId: process.env.NEXT_PUBLIC_PARTICLE_PROJECT_ID!, + clientKey: process.env.NEXT_PUBLIC_PARTICLE_CLIENT_KEY!, + appId: process.env.NEXT_PUBLIC_PARTICLE_APP_ID!, +}; + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); +} +``` + +### 2. Initialize Universal Account + +Use the `useUniversalAccount` hook with the EOA address: + +```tsx twoslash +// @noErrors +import { useUser } from "@account-kit/react"; +import { + useUniversalAccount, + useUnifiedBalance, +} from "@account-kit/universal-account"; + +function Dashboard() { + const user = useUser(); + const eoaAddress = user?.address as `0x${string}` | undefined; + + // Universal Account auto-initializes with the EOA address + const { + address, // Universal Account EVM address + solanaAddress, // Universal Account Solana address + isReady, + isInitializing, + error, + } = useUniversalAccount(eoaAddress); + + // Get unified balance across all chains + const { totalBalanceUSD, assets, isLoading, refetch } = useUnifiedBalance(); + + if (isInitializing) return
Initializing Universal Account...
; + if (error) return
Error: {error.message}
; + if (!isReady) return null; + + return ( +
+

Universal Account

+

EVM Address: {address}

+

Solana Address: {solanaAddress}

+

Unified Balance: ${totalBalanceUSD?.toFixed(2)}

+
+ ); +} +``` + +### 3. Send transactions + +Use `useSendTransaction` to send cross-chain transactions: + +```tsx twoslash +// @noErrors +import { useSigner } from "@account-kit/react"; +import { useSendTransaction } from "@account-kit/universal-account"; +import { toBytes, encodeFunctionData } from "viem"; + +function MintNFT() { + const signer = useSigner(); + + const { sendUniversal, isLoading, error, lastResult } = useSendTransaction({ + signMessage: async (message: string) => { + if (!signer) throw new Error("Signer not available"); + return await signer.signMessage({ raw: toBytes(message) }); + }, + }); + + const handleMint = async () => { + const NFT_CONTRACT = "0xdea7bF60E53CD578e3526F36eC431795f7EEbFe6"; + const AVALANCHE_CHAIN_ID = 43114; + + const mintData = encodeFunctionData({ + abi: [{ type: "function", name: "mint", inputs: [], outputs: [] }], + functionName: "mint", + }); + + const result = await sendUniversal({ + chainId: AVALANCHE_CHAIN_ID, + expectTokens: [], + transactions: [{ to: NFT_CONTRACT, data: mintData }], + }); + + console.log("Transaction ID:", result.transactionId); + }; + + return ( + + ); +} +``` + +## Transaction types + +The `useSendTransaction` hook provides methods for different transaction types: + +### Transfer tokens + +```tsx twoslash +// @noErrors +const { sendTransfer } = useSendTransaction({ signMessage }); + +await sendTransfer({ + token: { + chainId: 42161, // Arbitrum + address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT + }, + amount: "10", + receiver: "0x...", +}); +``` + +### Buy tokens + +Convert USD value from your primary assets into a target token: + +```tsx twoslash +// @noErrors +const { sendBuy } = useSendTransaction({ signMessage }); + +await sendBuy({ + token: { + chainId: 42161, + address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + }, + amountInUSD: "10", // Buy $10 worth +}); +``` + +### Sell tokens + +Sell a token back into primary assets: + +```tsx twoslash +// @noErrors +const { sendSell } = useSendTransaction({ signMessage }); + +await sendSell({ + token: { + chainId: 42161, + address: "0x912CE59144191C1204E64559FE8253a0e49E6548", // ARB + }, + amount: "0.1", +}); +``` + +## Supported chains + +Universal Accounts support 15+ EVM chains and Solana: + +- Ethereum, Base, Arbitrum, Optimism, Polygon +- Avalanche, BNB Chain, Linea, Berachain +- Solana +- And more... + +See the [full list of supported chains](https://developers.particle.network/universal-accounts/cha/chains). + +## Fees + +Universal Account transactions may include: + +- **Gas fees**: Standard network fees on the destination chain +- **LP fee**: 0.2% for cross-chain transactions +- **Service fee**: 1% on transaction volume + +Fees are automatically calculated and shown in the transaction preview. + +## Resources + +- [Particle Network Documentation](https://developers.particle.network/universal-accounts/cha/overview) +- [Universal Accounts SDK Reference](https://developers.particle.network/universal-accounts/ua-reference/desktop/web) +- [Supported Chains & Primary Assets](https://developers.particle.network/universal-accounts/cha/chains) +- [UniversalX Explorer](https://universalx.app) diff --git a/yarn.lock b/yarn.lock index 28a7b28de2..c27a2a4e24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4875,10 +4875,10 @@ dependencies: "@particle-network/auth" "^1.3.1" -"@particle-network/universal-account-sdk@^1.0.0": - version "1.0.10" - resolved "https://registry.yarnpkg.com/@particle-network/universal-account-sdk/-/universal-account-sdk-1.0.10.tgz#205ec4760fb0ebf87fca3cd3b07a249e10621cc9" - integrity sha512-xd1yaEBpeMl9QWRWq1h2yxwKFEae4pYPVsomnpLqZmY1XSqKRRCspdmVeGFgKHz6SaoowWTrmqkh+daP6SjAGQ== +"@particle-network/universal-account-sdk@^1.0.12": + version "1.0.12" + resolved "https://registry.yarnpkg.com/@particle-network/universal-account-sdk/-/universal-account-sdk-1.0.12.tgz#fbb1173629477ec34456624d53cd2095c86f7503" + integrity sha512-H9ws7mjpXx6K0MxbmQ8OK+j9Gn836Ns5ExQP9Bt8k6cmPKIgQM6IK6aR5bifKZaMUTthHFZpWU3jGRwa4wypyg== dependencies: "@coral-xyz/anchor" "^0.30.1" "@noble/hashes" "^1.7.1"