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
new file mode 100644
index 0000000000..af4000a48a
--- /dev/null
+++ b/account-kit/universal-account/README.md
@@ -0,0 +1,677 @@
+# @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`.
+
+## 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)
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
new file mode 100644
index 0000000000..5226837a17
--- /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..09c4c24390
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/app/components/UniversalAccountDemo.tsx
@@ -0,0 +1,353 @@
+"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
+ // 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
+ 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 && (
+
+ )}
+
+ {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 0000000000..718d6fea48
Binary files /dev/null and b/account-kit/universal-account/examples/next-example/app/favicon.ico differ
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 ? (
+
+ ) : 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..667dd25b9b
--- /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.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.12",
+ "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..624fd05a36
--- /dev/null
+++ b/account-kit/universal-account/package.json
@@ -0,0 +1,72 @@
+{
+ "name": "@account-kit/universal-account",
+ "version": "4.80.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.80.0",
+ "@account-kit/signer": "^4.80.0",
+ "@particle-network/universal-account-sdk": "^1.0.12"
+ },
+ "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/__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/src/client.ts b/account-kit/universal-account/src/client.ts
new file mode 100644
index 0000000000..ebef299085
--- /dev/null
+++ b/account-kit/universal-account/src/client.ts
@@ -0,0 +1,456 @@
+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;
+
+ /**
+ * 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;
+ 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..976d14c5ef
--- /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..401116f1f5
--- /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..df3d024904
--- /dev/null
+++ b/account-kit/universal-account/tsconfig.build.json
@@ -0,0 +1,16 @@
+{
+ "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/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 f58f3913ae..0aaced699f 160000
--- a/docs-site
+++ b/docs-site
@@ -1 +1 @@
-Subproject commit f58f3913ae157575f5925961d6c6fdb38a1462a8
+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 96a82bb31c..c27a2a4e24 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.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"
+ "@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"