Zlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i
zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7
zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG
z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S
zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr
z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S
zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er
zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa
zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc-
zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V
zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I
zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc
z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E(
zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef
LrJugUA?W`A8`#=m
literal 0
HcmV?d00001
diff --git a/account-kit/universal-account/examples/next-example/app/globals.css b/account-kit/universal-account/examples/next-example/app/globals.css
new file mode 100644
index 0000000000..ae574d66ae
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/app/globals.css
@@ -0,0 +1,27 @@
+@import "tailwindcss";
+@config "../tailwind.config.ts";
+
+:root {
+ --background: #ffffff;
+ --foreground: #171717;
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --background: #0a0a0a;
+ --foreground: #ededed;
+ }
+}
+
+body {
+ background: var(--background);
+ color: var(--foreground);
+ font-family: Arial, Helvetica, sans-serif;
+}
diff --git a/account-kit/universal-account/examples/next-example/app/layout.tsx b/account-kit/universal-account/examples/next-example/app/layout.tsx
new file mode 100644
index 0000000000..274fa2441a
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/app/layout.tsx
@@ -0,0 +1,43 @@
+import { config } from "@/config";
+import { cookieToInitialState } from "@account-kit/core";
+import type { Metadata } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
+import { headers } from "next/headers";
+import "./globals.css";
+import { Providers } from "./providers";
+
+const geistSans = Geist({
+ variable: "--font-geist-sans",
+ subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+ variable: "--font-geist-mono",
+ subsets: ["latin"],
+});
+
+export const metadata: Metadata = {
+ title: "Universal Account Demo",
+ description: "Alchemy Account Kit + Particle Universal Accounts",
+};
+
+export default async function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ const initialState = cookieToInitialState(
+ config,
+ (await headers()).get("cookie") ?? undefined,
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/account-kit/universal-account/examples/next-example/app/page.tsx b/account-kit/universal-account/examples/next-example/app/page.tsx
new file mode 100644
index 0000000000..317509cd53
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/app/page.tsx
@@ -0,0 +1,119 @@
+"use client";
+
+/**
+ * Main Page - Alchemy Account Kit + Particle Universal Accounts Demo
+ *
+ * This page demonstrates:
+ * 1. User authentication with Alchemy Account Kit (AuthCard)
+ * 2. Getting the user's EOA address for Universal Account initialization
+ * 3. Rendering the Universal Account demo component
+ */
+
+import {
+ useLogout,
+ useSignerStatus,
+ useUser,
+ useAccount,
+ AuthCard,
+} from "@account-kit/react";
+import { UniversalAccountDemo } from "./components/UniversalAccountDemo";
+
+export default function Home() {
+ // ==========================================================================
+ // ALCHEMY ACCOUNT KIT HOOKS
+ // ==========================================================================
+
+ // Get the authenticated user (contains the EOA address we need)
+ const user = useUser();
+
+ // Check if the signer is still initializing
+ const signerStatus = useSignerStatus();
+
+ // Logout function
+ const { logout } = useLogout();
+
+ // Get Alchemy's Smart Contract Account (SCA) - for display only
+ // Note: We don't use this for Universal Accounts, just showing it for reference
+ const { address: scaAddress } = useAccount({ type: "LightAccount" });
+
+ // ==========================================================================
+ // IMPORTANT: EOA vs SCA
+ // ==========================================================================
+ // Alchemy Account Kit provides TWO types of addresses:
+ //
+ // 1. EOA (Externally Owned Account) - user.address
+ // - This is the user's actual wallet address
+ // - This is what Universal Accounts needs as the "owner"
+ // - Used to sign transactions
+ //
+ // 2. SCA (Smart Contract Account) - from useAccount()
+ // - This is Alchemy's smart account for gasless transactions
+ // - NOT used for Universal Accounts
+ //
+ // We pass the EOA to Universal Accounts because it controls the UA.
+ const eoaAddress = user?.address as `0x${string}` | undefined;
+
+ return (
+
+
+
+ Universal Account Demo
+
+
+ Alchemy Account Kit + Particle Network Universal Accounts
+
+
+ {signerStatus.isInitializing ? (
+
+ ) : user ? (
+
+ {/* User Info Card */}
+
+
+
+
+ Logged in as
+
+
+ {user.email ?? "Anonymous"}
+
+ {eoaAddress && (
+
+ EOA: {eoaAddress.slice(0, 6)}...{eoaAddress.slice(-4)}
+
+ )}
+ {scaAddress && (
+
+ SCA: {scaAddress.slice(0, 6)}...{scaAddress.slice(-4)}
+
+ )}
+
+
+
+
+
+ {/* Universal Account Demo - pass the EOA address */}
+ {eoaAddress && }
+
+ ) : (
+
+
+ Sign in to access your Universal Account
+
+ {/* Auth Card - embedded login form */}
+
+
+ )}
+
+
+ );
+}
diff --git a/account-kit/universal-account/examples/next-example/app/providers.tsx b/account-kit/universal-account/examples/next-example/app/providers.tsx
new file mode 100644
index 0000000000..35cccc6c21
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/app/providers.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+/**
+ * Provider Setup for Alchemy Account Kit + Particle Universal Accounts
+ *
+ * PROVIDER HIERARCHY (order matters!):
+ * 1. QueryClientProvider - React Query for data fetching
+ * 2. AlchemyAccountProvider - Authentication & signing
+ * 3. UniversalAccountProvider - Chain abstraction
+ *
+ * The UniversalAccountProvider must be INSIDE AlchemyAccountProvider
+ * because it uses Alchemy's signer for transaction signing.
+ */
+
+import { config, queryClient, universalAccountConfig } from "@/config";
+import {
+ AlchemyAccountProvider,
+ AlchemyAccountsProviderProps,
+} from "@account-kit/react";
+import { UniversalAccountProvider } from "@account-kit/universal-account";
+import { QueryClientProvider } from "@tanstack/react-query";
+import { PropsWithChildren } from "react";
+
+export const Providers = (
+ props: PropsWithChildren<{
+ initialState?: AlchemyAccountsProviderProps["initialState"];
+ }>,
+) => {
+ return (
+ // Step 1: React Query for data fetching/caching
+
+ {/* Step 2: Alchemy Account Kit - handles authentication */}
+
+ {/* Step 3: Universal Accounts - enables chain abstraction */}
+ {/* Must be inside AlchemyAccountProvider to access the signer */}
+
+ {props.children}
+
+
+
+ );
+};
diff --git a/account-kit/universal-account/examples/next-example/config.ts b/account-kit/universal-account/examples/next-example/config.ts
new file mode 100644
index 0000000000..04406a4f77
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/config.ts
@@ -0,0 +1,69 @@
+/**
+ * Configuration for Alchemy Account Kit + Particle Universal Accounts
+ *
+ * This file sets up both:
+ * 1. Alchemy Account Kit - for authentication (email, passkey, social, wallet)
+ * 2. Particle Universal Accounts - for chain abstraction
+ */
+
+import { createConfig, cookieStorage } from "@account-kit/react";
+import { mainnet, alchemy } from "@account-kit/infra";
+import { QueryClient } from "@tanstack/react-query";
+import type { UniversalAccountProviderConfig } from "@account-kit/universal-account";
+
+// =============================================================================
+// STEP 1: Alchemy Account Kit Configuration
+// =============================================================================
+// This handles user authentication and provides the EOA (signer) that will
+// control the Universal Account.
+//
+// This stays the standard configuration for Alchemy Account Kit.
+//
+// Get your Alchemy API key from: https://dashboard.alchemy.com/
+export const config = createConfig(
+ {
+ // Alchemy RPC transport - used for blockchain interactions
+ transport: alchemy({ apiKey: process.env.NEXT_PUBLIC_ALCHEMY_API_KEY! }),
+
+ // Default chain for Alchemy Account Kit (can be any EVM chain)
+ // Note: Universal Accounts work across ALL chains regardless of this setting
+ chain: mainnet,
+ ssr: true,
+ storage: cookieStorage,
+ enablePopupOauth: true,
+ },
+ {
+ // Authentication options - customize which login methods to show
+ auth: {
+ sections: [
+ [{ type: "email" }],
+ [
+ { type: "passkey" },
+ { type: "social", authProviderId: "google", mode: "popup" },
+ ],
+ [{ type: "external_wallets" }],
+ ],
+ addPasskeyOnSignup: true,
+ },
+ },
+);
+
+export const queryClient = new QueryClient();
+
+// =============================================================================
+// STEP 2: Particle Universal Account Configuration
+// =============================================================================
+// This enables chain abstraction - unified balance and cross-chain transactions.
+//
+// Get your Particle credentials from: https://dashboard.particle.network/
+export const universalAccountConfig: UniversalAccountProviderConfig = {
+ projectId: process.env.NEXT_PUBLIC_PARTICLE_PROJECT_ID!,
+ clientKey: process.env.NEXT_PUBLIC_PARTICLE_CLIENT_KEY!,
+ appId: process.env.NEXT_PUBLIC_PARTICLE_APP_ID!,
+
+ // Optional: Trade configuration
+ // tradeConfig: {
+ // slippageBps: 100, // 1% slippage tolerance
+ // universalGas: true, // Use PARTI token for gas fees
+ // },
+};
diff --git a/account-kit/universal-account/examples/next-example/eslint.config.mjs b/account-kit/universal-account/examples/next-example/eslint.config.mjs
new file mode 100644
index 0000000000..719cea2b59
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/eslint.config.mjs
@@ -0,0 +1,25 @@
+import { dirname } from "path";
+import { fileURLToPath } from "url";
+import { FlatCompat } from "@eslint/eslintrc";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const compat = new FlatCompat({
+ baseDirectory: __dirname,
+});
+
+const eslintConfig = [
+ ...compat.extends("next/core-web-vitals", "next/typescript"),
+ {
+ ignores: [
+ "node_modules/**",
+ ".next/**",
+ "out/**",
+ "build/**",
+ "next-env.d.ts",
+ ],
+ },
+];
+
+export default eslintConfig;
diff --git a/account-kit/universal-account/examples/next-example/next.config.ts b/account-kit/universal-account/examples/next-example/next.config.ts
new file mode 100644
index 0000000000..e9ffa3083a
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/next.config.ts
@@ -0,0 +1,7 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ /* config options here */
+};
+
+export default nextConfig;
diff --git a/account-kit/universal-account/examples/next-example/package.json b/account-kit/universal-account/examples/next-example/package.json
new file mode 100644
index 0000000000..24f33f8d76
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "next-example",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "eslint"
+ },
+ "dependencies": {
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
+ "next": "15.5.2",
+ "@account-kit/react": "^4.79.0",
+ "@account-kit/infra": "^4.79.0",
+ "@account-kit/core": "^4.79.0",
+ "@tanstack/react-query": "^5.62.0",
+ "@particle-network/universal-account-sdk": "^1.0.10",
+ "viem": "^2.29.2"
+ },
+ "devDependencies": {
+ "typescript": "^5",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "@tailwindcss/postcss": "^4",
+ "tailwindcss": "^4",
+ "eslint": "^9",
+ "eslint-config-next": "15.5.2",
+ "@eslint/eslintrc": "^3"
+ }
+}
diff --git a/account-kit/universal-account/examples/next-example/postcss.config.mjs b/account-kit/universal-account/examples/next-example/postcss.config.mjs
new file mode 100644
index 0000000000..c7bcb4b1ee
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/postcss.config.mjs
@@ -0,0 +1,5 @@
+const config = {
+ plugins: ["@tailwindcss/postcss"],
+};
+
+export default config;
diff --git a/account-kit/universal-account/examples/next-example/public/file.svg b/account-kit/universal-account/examples/next-example/public/file.svg
new file mode 100644
index 0000000000..004145cddf
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/public/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/account-kit/universal-account/examples/next-example/public/globe.svg b/account-kit/universal-account/examples/next-example/public/globe.svg
new file mode 100644
index 0000000000..567f17b0d7
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/public/globe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/account-kit/universal-account/examples/next-example/public/next.svg b/account-kit/universal-account/examples/next-example/public/next.svg
new file mode 100644
index 0000000000..5174b28c56
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/account-kit/universal-account/examples/next-example/public/vercel.svg b/account-kit/universal-account/examples/next-example/public/vercel.svg
new file mode 100644
index 0000000000..7705396033
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/account-kit/universal-account/examples/next-example/public/window.svg b/account-kit/universal-account/examples/next-example/public/window.svg
new file mode 100644
index 0000000000..b2b2a44f6e
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/public/window.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/account-kit/universal-account/examples/next-example/tailwind.config.ts b/account-kit/universal-account/examples/next-example/tailwind.config.ts
new file mode 100644
index 0000000000..06c244bb56
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/tailwind.config.ts
@@ -0,0 +1,14 @@
+import { withAccountKitUi } from "@account-kit/react/tailwind";
+
+export default withAccountKitUi(
+ {
+ // Existing Tailwind config
+ content: [
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
+ ],
+ },
+ {
+ // AccountKit UI theme customizations (optional)
+ }
+);
diff --git a/account-kit/universal-account/examples/next-example/tsconfig.json b/account-kit/universal-account/examples/next-example/tsconfig.json
new file mode 100644
index 0000000000..d8b93235f2
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/account-kit/universal-account/package.json b/account-kit/universal-account/package.json
new file mode 100644
index 0000000000..fabfe1de81
--- /dev/null
+++ b/account-kit/universal-account/package.json
@@ -0,0 +1,72 @@
+{
+ "name": "@account-kit/universal-account",
+ "version": "4.79.0",
+ "description": "Universal Account integration for Account Kit - enabling chain abstraction with Particle Network",
+ "author": "Alchemy",
+ "license": "MIT",
+ "private": false,
+ "type": "module",
+ "main": "./dist/esm/index.js",
+ "module": "./dist/esm/index.js",
+ "types": "./dist/types/index.d.ts",
+ "typings": "./dist/types/index.d.ts",
+ "sideEffects": false,
+ "files": [
+ "dist",
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "!dist/**/*.tsbuildinfo",
+ "!vitest.config.ts",
+ "!.env",
+ "!src/**/*.test.ts",
+ "!src/**/*.test-d.ts",
+ "!src/__tests__/**/*"
+ ],
+ "exports": {
+ ".": {
+ "types": "./dist/types/index.d.ts",
+ "import": "./dist/esm/index.js",
+ "default": "./dist/esm/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "build": "yarn clean && yarn build:esm && yarn build:types",
+ "build:esm": "tsc --project tsconfig.build.json --outDir ./dist/esm",
+ "build:types": "tsc --project tsconfig.build.json --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap",
+ "clean": "rm -rf ./dist",
+ "test": "vitest --passWithNoTests",
+ "test:run": "vitest run --passWithNoTests"
+ },
+ "dependencies": {
+ "@account-kit/infra": "^4.79.0",
+ "@account-kit/signer": "^4.79.0",
+ "@particle-network/universal-account-sdk": "^1.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18.0.0",
+ "viem": "^2.29.2"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": false
+ }
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.0",
+ "react": "^18.2.0",
+ "typescript-template": "*"
+ },
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org/"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/alchemyplatform/aa-sdk.git"
+ },
+ "bugs": {
+ "url": "https://github.com/alchemyplatform/aa-sdk/issues"
+ },
+ "homepage": "https://github.com/alchemyplatform/aa-sdk#readme"
+}
diff --git a/account-kit/universal-account/src/client.ts b/account-kit/universal-account/src/client.ts
new file mode 100644
index 0000000000..95b79e2bf0
--- /dev/null
+++ b/account-kit/universal-account/src/client.ts
@@ -0,0 +1,450 @@
+import type { Address } from "viem";
+import type {
+ UniversalAccountConfig,
+ SmartAccountOptions,
+ PrimaryAssets,
+ TransferTransactionParams,
+ UniversalTransactionParams,
+ BuyTransactionParams,
+ SellTransactionParams,
+ ConvertTransactionParams,
+ UniversalTransaction,
+ TransactionResult,
+ IUniversalAccount,
+} from "./types.js";
+
+export interface CreateUniversalAccountClientParams {
+ /** Owner EOA address that controls the Universal Account */
+ ownerAddress: Address;
+ /** Universal Account configuration */
+ config: UniversalAccountConfig;
+}
+
+/**
+ * Universal Account Client
+ *
+ * Wraps Particle Network's Universal Account SDK to provide
+ * chain abstraction capabilities within Account Kit.
+ *
+ * @example
+ * ```ts
+ * import { createUniversalAccountClient } from "@account-kit/universal-account";
+ *
+ * const client = await createUniversalAccountClient({
+ * ownerAddress: "0x...",
+ * config: {
+ * projectId: "your-project-id",
+ * projectClientKey: "your-client-key",
+ * projectAppUuid: "your-app-uuid",
+ * },
+ * });
+ *
+ * // Get unified balance across all chains
+ * const balance = await client.getPrimaryAssets();
+ * console.log("Total USD:", balance.totalAmountInUSD);
+ * ```
+ */
+export class UniversalAccountClient implements IUniversalAccount {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ private ua: any;
+ private _ownerAddress: Address;
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ constructor(ua: any, ownerAddress: Address) {
+ this.ua = ua;
+ this._ownerAddress = ownerAddress;
+ }
+
+ /**
+ * Get the owner EOA address
+ *
+ * @returns {Address} The owner EOA address
+ */
+ getOwnerAddress(): Address {
+ return this._ownerAddress;
+ }
+
+ /**
+ * Get smart account options including all addresses
+ *
+ * @returns {Promise} Smart account options with EVM and Solana addresses
+ */
+ async getSmartAccountOptions(): Promise {
+ const options = await this.ua.getSmartAccountOptions();
+ return {
+ name: options.name,
+ version: options.version,
+ ownerAddress: options.ownerAddress as Address,
+ smartAccountAddress: options.smartAccountAddress as Address,
+ solanaSmartAccountAddress: options.solanaSmartAccountAddress,
+ senderAddress: options.senderAddress as Address,
+ senderSolanaAddress: options.senderSolanaAddress,
+ };
+ }
+
+ /**
+ * Get the EVM Universal Account address
+ *
+ * @returns {Promise} The EVM smart account address
+ */
+ async getAddress(): Promise {
+ const options = await this.getSmartAccountOptions();
+ return options.smartAccountAddress;
+ }
+
+ /**
+ * Get the Solana Universal Account address
+ *
+ * @returns {Promise} The Solana smart account address, if available
+ */
+ async getSolanaAddress(): Promise {
+ const options = await this.getSmartAccountOptions();
+ return options.solanaSmartAccountAddress;
+ }
+
+ /**
+ * Get primary assets (unified balance across all chains)
+ *
+ * @returns {Promise} Primary assets with total USD value
+ *
+ * @example
+ * ```ts
+ * const assets = await client.getPrimaryAssets();
+ * console.log("Total balance:", assets.totalAmountInUSD);
+ *
+ * // Iterate through assets
+ * for (const asset of assets.assets) {
+ * console.log(`${asset.tokenType}: $${asset.amountInUSD}`);
+ * }
+ * ```
+ */
+ async getPrimaryAssets(): Promise {
+ const assets = await this.ua.getPrimaryAssets();
+ return {
+ assets: assets.assets.map((asset: any) => ({
+ tokenType: asset.tokenType,
+ price: asset.price,
+ amount: asset.amount,
+ amountInUSD: asset.amountInUSD,
+ chainAggregation: asset.chainAggregation?.map((chain: any) => ({
+ chainId: chain.token?.chainId ?? chain.chainId,
+ address: chain.token?.address ?? chain.address,
+ amount: chain.amount,
+ amountInUSD: chain.amountInUSD,
+ rawAmount: chain.rawAmount,
+ decimals: chain.token?.decimals ?? chain.decimals,
+ })) ?? [],
+ })),
+ totalAmountInUSD: assets.totalAmountInUSD,
+ };
+ }
+
+ /**
+ * Create a transfer transaction
+ *
+ * @param {TransferTransactionParams} params Transfer parameters
+ * @returns {Promise} Universal transaction ready to be signed
+ *
+ * @example
+ * ```ts
+ * const tx = await client.createTransferTransaction({
+ * token: {
+ * chainId: 42161, // Arbitrum
+ * address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT
+ * },
+ * amount: "10",
+ * receiver: "0x...",
+ * });
+ *
+ * // Sign the rootHash with your signer
+ * const signature = await signer.signMessage(tx.rootHash);
+ *
+ * // Send the transaction
+ * const result = await client.sendTransaction(tx, signature);
+ * ```
+ */
+ async createTransferTransaction(
+ params: TransferTransactionParams
+ ): Promise {
+ const tx = await this.ua.createTransferTransaction({
+ token: {
+ chainId: params.token.chainId,
+ address: params.token.address,
+ },
+ amount: params.amount,
+ receiver: params.receiver,
+ });
+
+ return this.mapTransaction(tx);
+ }
+
+ /**
+ * Create a universal transaction for contract interactions
+ *
+ * @param {UniversalTransactionParams} params Universal transaction parameters
+ * @returns {Promise} Universal transaction ready to be signed
+ *
+ * @example
+ * ```ts
+ * const tx = await client.createUniversalTransaction({
+ * chainId: 8453, // Base
+ * expectTokens: [
+ * { type: "ETH", amount: "0.01" },
+ * ],
+ * transactions: [
+ * {
+ * to: "0x...",
+ * data: "0x...",
+ * value: "0x...",
+ * },
+ * ],
+ * });
+ * ```
+ */
+ async createUniversalTransaction(
+ params: UniversalTransactionParams
+ ): Promise {
+ const tx = await this.ua.createUniversalTransaction({
+ chainId: params.chainId,
+ expectTokens: params.expectTokens.map((token) => ({
+ type: token.type,
+ amount: token.amount,
+ })),
+ transactions: params.transactions.map((txn) => ({
+ to: txn.to,
+ data: txn.data,
+ value: txn.value,
+ })),
+ });
+
+ return this.mapTransaction(tx);
+ }
+
+ /**
+ * Create a buy/swap transaction
+ *
+ * Converts USD value from your primary assets into a target token.
+ * The SDK will automatically route liquidity from your holdings.
+ *
+ * @param {BuyTransactionParams} params Buy transaction parameters
+ * @returns {Promise} Universal transaction ready to be signed
+ *
+ * @example
+ * ```ts
+ * const tx = await client.createBuyTransaction({
+ * token: {
+ * chainId: 42161, // Arbitrum
+ * address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT
+ * },
+ * amountInUSD: "10", // Buy $10 worth of USDT
+ * });
+ * ```
+ */
+ async createBuyTransaction(
+ params: BuyTransactionParams
+ ): Promise {
+ const tx = await this.ua.createBuyTransaction({
+ token: {
+ chainId: params.token.chainId,
+ address: params.token.address,
+ },
+ amountInUSD: params.amountInUSD,
+ });
+
+ return this.mapTransaction(tx);
+ }
+
+ /**
+ * Create a sell transaction
+ *
+ * Sells a token back into primary assets. Ensure the Universal Account
+ * has enough balance of the token before calling.
+ *
+ * @param {SellTransactionParams} params Sell transaction parameters
+ * @returns {Promise} Universal transaction ready to be signed
+ *
+ * @example
+ * ```ts
+ * const tx = await client.createSellTransaction({
+ * token: {
+ * chainId: 42161, // Arbitrum
+ * address: "0x912CE59144191C1204E64559FE8253a0e49E6548", // ARB
+ * },
+ * amount: "0.1", // Sell 0.1 ARB
+ * });
+ * ```
+ */
+ async createSellTransaction(
+ params: SellTransactionParams
+ ): Promise {
+ const tx = await this.ua.createSellTransaction({
+ token: {
+ chainId: params.token.chainId,
+ address: params.token.address,
+ },
+ amount: params.amount,
+ });
+
+ return this.mapTransaction(tx);
+ }
+
+ /**
+ * Create a convert transaction
+ *
+ * Converts between primary assets on a specific chain.
+ * Useful for converting assets directly to the target chain.
+ *
+ * @param {ConvertTransactionParams} params Convert transaction parameters
+ * @returns {Promise} Universal transaction ready to be signed
+ *
+ * @example
+ * ```ts
+ * const tx = await client.createConvertTransaction({
+ * expectToken: { type: "USDC", amount: "1" },
+ * chainId: 42161, // Arbitrum
+ * });
+ * ```
+ */
+ async createConvertTransaction(
+ params: ConvertTransactionParams
+ ): Promise {
+ const tx = await this.ua.createConvertTransaction({
+ expectToken: {
+ type: params.expectToken.type,
+ amount: params.expectToken.amount,
+ },
+ chainId: params.chainId,
+ });
+
+ return this.mapTransaction(tx);
+ }
+
+ /**
+ * Send a signed transaction
+ *
+ * @param {UniversalTransaction} transaction The transaction to send
+ * @param {string} signature The signature from signing the rootHash
+ * @returns {Promise} Transaction result with ID and status
+ *
+ * @example
+ * ```ts
+ * const result = await client.sendTransaction(tx, signature);
+ * console.log("Transaction ID:", result.transactionId);
+ * console.log("Explorer:", `https://universalx.app/activity/details?id=${result.transactionId}`);
+ * ```
+ */
+ async sendTransaction(
+ transaction: UniversalTransaction,
+ signature: string
+ ): Promise {
+ // We need to pass the original transaction object from the SDK
+ // The transaction parameter here is our mapped type, but we stored
+ // the original in a way that sendTransaction can use it
+ const result = await this.ua.sendTransaction(
+ transaction as any,
+ signature
+ );
+
+ return {
+ transactionId: result.transactionId,
+ status: result.status,
+ mode: result.mode as "mainnet" | "testnet",
+ sender: result.sender as Address,
+ receiver: result.receiver as Address,
+ tag: result.tag,
+ created_at: result.created_at,
+ updated_at: result.updated_at,
+ };
+ }
+
+ /**
+ * Get the explorer URL for a transaction
+ *
+ * @param {string} transactionId The transaction ID
+ * @returns {string} UniversalX explorer URL
+ */
+ getExplorerUrl(transactionId: string): string {
+ return `https://universalx.app/activity/details?id=${transactionId}`;
+ }
+
+ /**
+ * Get the underlying Particle Universal Account instance.
+ * Use this for advanced operations not covered by this wrapper.
+ *
+ * @returns {any} The underlying Particle SDK UniversalAccount instance
+ */
+ getUnderlyingAccount(): typeof this.ua {
+ return this.ua;
+ }
+
+ private mapTransaction(tx: any): UniversalTransaction {
+ return {
+ ...tx,
+ type: tx.type ?? "universal",
+ mode: tx.mode ?? "mainnet",
+ sender: tx.sender as Address,
+ receiver: tx.receiver as Address,
+ transactionId: tx.transactionId,
+ rootHash: tx.rootHash as `0x${string}`,
+ smartAccountOptions: {
+ name: tx.smartAccountOptions?.name ?? "",
+ version: tx.smartAccountOptions?.version ?? "",
+ ownerAddress: tx.smartAccountOptions?.ownerAddress as Address,
+ smartAccountAddress: tx.smartAccountOptions?.smartAccountAddress as Address,
+ solanaSmartAccountAddress: tx.smartAccountOptions?.solanaSmartAccountAddress,
+ senderAddress: tx.smartAccountOptions?.senderAddress as Address,
+ senderSolanaAddress: tx.smartAccountOptions?.senderSolanaAddress,
+ },
+ feeQuotes: tx.feeQuotes ?? [],
+ };
+ }
+}
+
+/**
+ * Create a Universal Account client
+ *
+ * @param {CreateUniversalAccountClientParams} params Parameters for creating the client
+ * @returns {Promise} A configured Universal Account client
+ *
+ * @example
+ * ```ts
+ * import { createUniversalAccountClient } from "@account-kit/universal-account";
+ *
+ * const client = await createUniversalAccountClient({
+ * ownerAddress: userAddress,
+ * config: {
+ * projectId: process.env.PARTICLE_PROJECT_ID!,
+ * projectClientKey: process.env.PARTICLE_CLIENT_KEY!,
+ * projectAppUuid: process.env.PARTICLE_APP_UUID!,
+ * tradeConfig: {
+ * slippageBps: 100, // 1% slippage
+ * universalGas: true, // Use PARTI for gas
+ * },
+ * },
+ * });
+ * ```
+ */
+export async function createUniversalAccountClient(
+ params: CreateUniversalAccountClientParams
+): Promise {
+ // Dynamic import to avoid bundling issues - users must install the peer dependency
+ const { UniversalAccount } = await import(
+ "@particle-network/universal-account-sdk"
+ );
+
+ const ua = new UniversalAccount({
+ projectId: params.config.projectId,
+ projectClientKey: params.config.projectClientKey,
+ projectAppUuid: params.config.projectAppUuid,
+ ownerAddress: params.ownerAddress,
+ tradeConfig: params.config.tradeConfig
+ ? {
+ slippageBps: params.config.tradeConfig.slippageBps,
+ universalGas: params.config.tradeConfig.universalGas,
+ usePrimaryTokens: params.config.tradeConfig.usePrimaryTokens,
+ }
+ : undefined,
+ });
+
+ return new UniversalAccountClient(ua, params.ownerAddress);
+}
diff --git a/account-kit/universal-account/src/constants.ts b/account-kit/universal-account/src/constants.ts
new file mode 100644
index 0000000000..03f1461a32
--- /dev/null
+++ b/account-kit/universal-account/src/constants.ts
@@ -0,0 +1,86 @@
+/**
+ * Chain IDs for Universal Account supported chains
+ *
+ * These match the chain IDs used by Particle Network's Universal Accounts.
+ * You can also import CHAIN_ID directly from @particle-network/universal-account-sdk
+ *
+ * @see https://developers.particle.network/universal-accounts/cha/chains
+ */
+export const CHAIN_ID = {
+ // EVM Chains
+ /** Ethereum Mainnet */
+ ETHEREUM: 1,
+ /** BNB Chain (BSC) */
+ BNB_CHAIN: 56,
+ /** Mantle */
+ MANTLE: 5000,
+ /** Monad */
+ MONAD: 143,
+ /** Plasma */
+ PLASMA: 9745,
+ /** X Layer */
+ X_LAYER: 196,
+ /** Base */
+ BASE: 8453,
+ /** Arbitrum One */
+ ARBITRUM: 42161,
+ /** Avalanche C-Chain */
+ AVALANCHE: 43114,
+ /** Optimism */
+ OPTIMISM: 10,
+ /** Polygon */
+ POLYGON: 137,
+ /** HyperEVM */
+ HYPER_EVM: 999,
+ /** Berachain */
+ BERACHAIN: 80094,
+ /** Linea */
+ LINEA: 59144,
+ /** Sonic */
+ SONIC: 146,
+ /** Merlin */
+ MERLIN: 4200,
+
+ // Non-EVM Chains
+ /** Solana Mainnet */
+ SOLANA: 101,
+} as const;
+
+/**
+ * Type for chain IDs
+ */
+export type ChainId = (typeof CHAIN_ID)[keyof typeof CHAIN_ID];
+
+/**
+ * Supported token types for Universal Accounts
+ *
+ * These are the primary asset types that can be used across chains.
+ */
+export const TOKEN_TYPE = {
+ /** Ethereum */
+ ETH: "ETH",
+ /** USD Coin */
+ USDC: "USDC",
+ /** Tether USD */
+ USDT: "USDT",
+ /** Solana */
+ SOL: "SOL",
+ /** Bitcoin (wrapped) */
+ BTC: "BTC",
+ /** BNB */
+ BNB: "BNB",
+ /** Mantle Native token */
+ MNT: "MNT",
+} as const;
+
+/**
+ * Type for token types
+ */
+export type TokenType = (typeof TOKEN_TYPE)[keyof typeof TOKEN_TYPE];
+
+/**
+ * Native token address (zero address)
+ * Use this for native tokens like ETH, AVAX, MATIC, etc.
+ */
+export const NATIVE_TOKEN_ADDRESS =
+ "0x0000000000000000000000000000000000000000" as const;
diff --git a/account-kit/universal-account/src/index.ts b/account-kit/universal-account/src/index.ts
new file mode 100644
index 0000000000..8f3e22cceb
--- /dev/null
+++ b/account-kit/universal-account/src/index.ts
@@ -0,0 +1,49 @@
+// Provider & Hooks (recommended - seamless integration)
+export {
+ UniversalAccountProvider,
+ useUniversalAccount,
+ useUnifiedBalance,
+ useSendTransaction,
+ useUniversalAccountContext,
+ type UniversalAccountProviderConfig,
+ type UniversalAccountProviderProps,
+ type UniversalAccountContextValue,
+} from "./provider.js";
+
+// Client (for advanced/manual usage)
+export {
+ UniversalAccountClient,
+ createUniversalAccountClient,
+ type CreateUniversalAccountClientParams,
+} from "./client.js";
+
+// Constants
+export {
+ CHAIN_ID,
+ TOKEN_TYPE,
+ NATIVE_TOKEN_ADDRESS,
+ type ChainId,
+ type TokenType,
+} from "./constants.js";
+
+// Types
+export type {
+ UniversalAccountConfig,
+ TradeConfig,
+ SmartAccountOptions,
+ AssetInfo,
+ ChainAssetInfo,
+ PrimaryAssets,
+ TokenIdentifier,
+ ExpectToken,
+ TransferTransactionParams,
+ UniversalTransactionParams,
+ BuyTransactionParams,
+ SellTransactionParams,
+ ConvertTransactionParams,
+ TransactionRequest,
+ UniversalTransaction,
+ FeeQuote,
+ TransactionResult,
+ IUniversalAccount,
+} from "./types.js";
diff --git a/account-kit/universal-account/src/particle-sdk.d.ts b/account-kit/universal-account/src/particle-sdk.d.ts
new file mode 100644
index 0000000000..47968fff78
--- /dev/null
+++ b/account-kit/universal-account/src/particle-sdk.d.ts
@@ -0,0 +1,144 @@
+/**
+ * Type declarations for @particle-network/universal-account-sdk
+ * This file helps TypeScript resolve the module when the SDK's package.json exports
+ * don't properly expose the type declarations.
+ */
+declare module "@particle-network/universal-account-sdk" {
+ export interface UniversalAccountConfig {
+ projectId: string;
+ projectClientKey?: string;
+ projectAppUuid?: string;
+ ownerAddress: string;
+ tradeConfig?: {
+ slippageBps?: number;
+ universalGas?: boolean;
+ usePrimaryTokens?: string[];
+ };
+ }
+
+ export interface SmartAccountOptions {
+ name: string;
+ version: string;
+ ownerAddress: string;
+ smartAccountAddress: string;
+ solanaSmartAccountAddress?: string;
+ senderAddress: string;
+ senderSolanaAddress?: string;
+ }
+
+ export interface PrimaryAssets {
+ assets: Array<{
+ tokenType: string;
+ price: number;
+ amount: string;
+ amountInUSD: number;
+ chainAggregation?: Array<{
+ token?: {
+ chainId: number;
+ address: string;
+ decimals: number;
+ };
+ chainId?: number;
+ address?: string;
+ amount: string;
+ amountInUSD: number;
+ rawAmount: string;
+ decimals?: number;
+ }>;
+ }>;
+ totalAmountInUSD: number;
+ }
+
+ export interface TransferTransactionParams {
+ token: {
+ chainId: number;
+ address: string;
+ };
+ amount: string;
+ receiver: string;
+ }
+
+ export interface UniversalTransactionParams {
+ chainId: number;
+ expectTokens: Array<{
+ type: string;
+ amount: string;
+ }>;
+ transactions: Array<{
+ to: string;
+ data: string;
+ value?: string;
+ }>;
+ }
+
+ export interface UniversalTransaction {
+ type: string;
+ mode: string;
+ sender: string;
+ receiver: string;
+ transactionId: string;
+ rootHash: string;
+ smartAccountOptions: SmartAccountOptions;
+ feeQuotes: Array<{
+ fees: {
+ totals: {
+ feeTokenAmountInUSD: string;
+ gasFeeTokenAmountInUSD: string;
+ transactionServiceFeeTokenAmountInUSD: string;
+ transactionLPFeeTokenAmountInUSD: string;
+ };
+ freeGasFee: boolean;
+ freeServiceFee: boolean;
+ };
+ }>;
+ }
+
+ export interface TransactionResult {
+ transactionId: string;
+ status: string;
+ mode: string;
+ sender: string;
+ receiver: string;
+ tag: string;
+ created_at: string;
+ updated_at: string;
+ }
+
+ export class UniversalAccount {
+ constructor(config: UniversalAccountConfig);
+ getSmartAccountOptions(): Promise;
+ getPrimaryAssets(): Promise;
+ createTransferTransaction(
+ params: TransferTransactionParams
+ ): Promise;
+ createUniversalTransaction(
+ params: UniversalTransactionParams
+ ): Promise;
+ sendTransaction(
+ transaction: UniversalTransaction,
+ signature: string
+ ): Promise;
+ }
+
+ export const CHAIN_ID: {
+ ETHEREUM_MAINNET: number;
+ ARBITRUM_MAINNET_ONE: number;
+ BASE_MAINNET: number;
+ BSC_MAINNET: number;
+ POLYGON_MAINNET: number;
+ OPTIMISM_MAINNET: number;
+ AVALANCHE_MAINNET: number;
+ [key: string]: number;
+ };
+
+ export const SUPPORTED_TOKEN_TYPE: {
+ ETH: string;
+ USDT: string;
+ USDC: string;
+ BNB: string;
+ SOL: string;
+ MATIC: string;
+ AVAX: string;
+ [key: string]: string;
+ };
+}
diff --git a/account-kit/universal-account/src/provider.tsx b/account-kit/universal-account/src/provider.tsx
new file mode 100644
index 0000000000..cf35d7612a
--- /dev/null
+++ b/account-kit/universal-account/src/provider.tsx
@@ -0,0 +1,552 @@
+"use client";
+
+import {
+ createContext,
+ useContext,
+ useEffect,
+ useState,
+ useCallback,
+ useMemo,
+ type ReactNode,
+} from "react";
+import type { Address } from "viem";
+import {
+ UniversalAccountClient,
+ createUniversalAccountClient,
+} from "./client.js";
+import type {
+ UniversalAccountConfig,
+ PrimaryAssets,
+ TransferTransactionParams,
+ UniversalTransactionParams,
+ BuyTransactionParams,
+ SellTransactionParams,
+ ConvertTransactionParams,
+ UniversalTransaction,
+ TransactionResult,
+} from "./types.js";
+
+/**
+ * Configuration for Universal Accounts - simplified for seamless integration
+ */
+export interface UniversalAccountProviderConfig {
+ /** Particle Network project ID */
+ projectId: string;
+ /** Particle Network client key */
+ clientKey: string;
+ /** Particle Network app ID */
+ appId: string;
+ /** Optional trade configuration */
+ tradeConfig?: {
+ /** Slippage tolerance in basis points (100 = 1%) */
+ slippageBps?: number;
+ /** Use PARTI token for gas fees */
+ universalGas?: boolean;
+ };
+}
+
+/**
+ * Props for UniversalAccountProvider
+ */
+export interface UniversalAccountProviderProps {
+ children: ReactNode;
+ /** Universal Account configuration */
+ config: UniversalAccountProviderConfig;
+}
+
+/**
+ * Context value for Universal Account
+ */
+export interface UniversalAccountContextValue {
+ /** The Universal Account client instance */
+ client: UniversalAccountClient | null;
+ /** Whether the client is currently initializing */
+ isInitializing: boolean;
+ /** Whether the client is ready to use */
+ isReady: boolean;
+ /** Error if initialization failed */
+ error: Error | null;
+ /** EVM Universal Account address */
+ address: Address | null;
+ /** Solana Universal Account address */
+ solanaAddress: string | null;
+ /** Initialize the UA with an owner address */
+ initialize: (ownerAddress: Address) => Promise;
+ /** Reset/disconnect the UA */
+ disconnect: () => void;
+}
+
+const UniversalAccountContext =
+ createContext(null);
+
+/**
+ * Provider component for Universal Account integration
+ *
+ * Wrap your app with this provider to enable Universal Account functionality.
+ * The UA will auto-initialize when you call `initialize` with the owner address.
+ *
+ * @param {UniversalAccountProviderProps} props - Provider props
+ * @param {ReactNode} props.children - Child components
+ * @param {UniversalAccountProviderConfig} props.config - UA configuration
+ * @returns {JSX.Element} Provider component
+ *
+ * @example
+ * ```tsx
+ * import { UniversalAccountProvider } from "@account-kit/universal-account";
+ *
+ * function App() {
+ * return (
+ *
+ *
+ *
+ * );
+ * }
+ * ```
+ */
+export function UniversalAccountProvider({
+ children,
+ config,
+}: UniversalAccountProviderProps): JSX.Element {
+ const [client, setClient] = useState(null);
+ const [isInitializing, setIsInitializing] = useState(false);
+ const [error, setError] = useState(null);
+ const [address, setAddress] = useState(null);
+ const [solanaAddress, setSolanaAddress] = useState(null);
+
+ const initialize = useCallback(
+ async (ownerAddress: Address) => {
+ if (!ownerAddress) return;
+
+ setIsInitializing(true);
+ setError(null);
+
+ try {
+ const uaConfig: UniversalAccountConfig = {
+ projectId: config.projectId,
+ projectClientKey: config.clientKey,
+ projectAppUuid: config.appId,
+ tradeConfig: config.tradeConfig,
+ };
+
+ const uaClient = await createUniversalAccountClient({
+ ownerAddress,
+ config: uaConfig,
+ });
+
+ // Fetch addresses
+ const [evmAddr, solAddr] = await Promise.all([
+ uaClient.getAddress(),
+ uaClient.getSolanaAddress(),
+ ]);
+
+ setClient(uaClient);
+ setAddress(evmAddr);
+ setSolanaAddress(solAddr ?? null);
+ } catch (err) {
+ const error =
+ err instanceof Error
+ ? err
+ : new Error("Failed to initialize Universal Account");
+ setError(error);
+ console.error("Universal Account initialization failed:", err);
+ } finally {
+ setIsInitializing(false);
+ }
+ },
+ [config],
+ );
+
+ const disconnect = useCallback(() => {
+ setClient(null);
+ setAddress(null);
+ setSolanaAddress(null);
+ setError(null);
+ }, []);
+
+ const value = useMemo(
+ () => ({
+ client,
+ isInitializing,
+ isReady: client !== null && !isInitializing,
+ error,
+ address,
+ solanaAddress,
+ initialize,
+ disconnect,
+ }),
+ [
+ client,
+ isInitializing,
+ error,
+ address,
+ solanaAddress,
+ initialize,
+ disconnect,
+ ],
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to access Universal Account context
+ *
+ * @returns {UniversalAccountContextValue} The Universal Account context
+ * @throws Error if used outside of UniversalAccountProvider
+ */
+export function useUniversalAccountContext(): UniversalAccountContextValue {
+ const context = useContext(UniversalAccountContext);
+ if (!context) {
+ throw new Error(
+ "useUniversalAccountContext must be used within a UniversalAccountProvider",
+ );
+ }
+ return context;
+}
+
+/**
+ * Hook to get the Universal Account client and auto-initialize with Account Kit
+ *
+ * This hook automatically initializes the Universal Account when the user
+ * is authenticated with Account Kit. Just provide the owner address from
+ * Account Kit's useAccount or useSigner hooks.
+ *
+ * @param {Address | null} [ownerAddress] - The EOA address that owns the Universal Account
+ * @returns {UniversalAccountContextValue} The Universal Account context with client and state
+ *
+ * @example
+ * ```tsx
+ * import { useUniversalAccount } from "@account-kit/universal-account";
+ * import { useAccount } from "@account-kit/react";
+ *
+ * function MyComponent() {
+ * const { address: ownerAddress } = useAccount({ type: "LightAccount" });
+ * const { client, address, isReady, error } = useUniversalAccount(ownerAddress);
+ *
+ * if (!isReady) return Loading... ;
+ * if (error) return Error: {error.message} ;
+ *
+ * return UA Address: {address} ;
+ * }
+ * ```
+ */
+export function useUniversalAccount(
+ ownerAddress?: Address | null,
+): UniversalAccountContextValue {
+ const context = useUniversalAccountContext();
+ const { initialize, client, isInitializing } = context;
+
+ // Auto-initialize when owner address is available
+ useEffect(() => {
+ if (ownerAddress && !client && !isInitializing) {
+ initialize(ownerAddress);
+ }
+ }, [ownerAddress, client, isInitializing, initialize]);
+
+ // Disconnect when owner address is removed
+ useEffect(() => {
+ if (!ownerAddress && client) {
+ context.disconnect();
+ }
+ }, [ownerAddress, client, context]);
+
+ return context;
+}
+
+/**
+ * Hook to get unified balance across all chains
+ *
+ * @param {object} [options] - Options for the hook
+ * @param {number} [options.refetchInterval] - Auto-refresh interval in milliseconds
+ * @returns {object} Balance state and refetch function
+ *
+ * @example
+ * ```tsx
+ * import { useUnifiedBalance } from "@account-kit/universal-account";
+ *
+ * function BalanceDisplay() {
+ * const { totalBalanceUSD, assets, isLoading, refetch } = useUnifiedBalance();
+ *
+ * return (
+ *
+ * Total: ${totalBalanceUSD?.toFixed(2)}
+ *
+ *
+ * );
+ * }
+ * ```
+ */
+export function useUnifiedBalance(options?: { refetchInterval?: number }): {
+ balance: PrimaryAssets | null;
+ totalBalanceUSD: number | null;
+ assets: any;
+ isLoading: boolean;
+ error: Error | null;
+ refetch: () => void;
+} {
+ const { client, isReady } = useUniversalAccountContext();
+ const [balance, setBalance] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const fetchBalance = useCallback(async () => {
+ if (!client) return;
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const assets = await client.getPrimaryAssets();
+ setBalance(assets);
+ } catch (err) {
+ setError(
+ err instanceof Error ? err : new Error("Failed to fetch balance"),
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ }, [client]);
+
+ // Initial fetch when client is ready
+ useEffect(() => {
+ if (isReady && client) {
+ fetchBalance();
+ }
+ }, [isReady, client, fetchBalance]);
+
+ // Optional auto-refresh
+ useEffect(() => {
+ if (!options?.refetchInterval || !isReady) return;
+
+ const interval = setInterval(fetchBalance, options.refetchInterval);
+ return () => clearInterval(interval);
+ }, [options?.refetchInterval, isReady, fetchBalance]);
+
+ return {
+ balance,
+ totalBalanceUSD: balance?.totalAmountInUSD ?? null,
+ assets: balance?.assets ?? null,
+ isLoading,
+ error,
+ refetch: fetchBalance,
+ };
+}
+
+/**
+ * Options for transaction callbacks
+ */
+interface TransactionCallbackOptions {
+ /** Called when the transaction is created (before signing) */
+ onTransactionCreated?: (tx: UniversalTransaction) => void;
+}
+
+/**
+ * Hook to send Universal Account transactions
+ *
+ * Provides methods for all transaction types supported by Universal Accounts:
+ * - `sendTransfer` - Send tokens to any address
+ * - `sendUniversal` - Execute custom contract interactions
+ * - `sendBuy` - Buy/swap into a target token using USD value
+ * - `sendSell` - Sell a token back into primary assets
+ * - `sendConvert` - Convert between primary assets on a chain
+ *
+ * @param {object} options - Options for the hook
+ * @param {Function} options.signMessage - Function to sign messages with the owner wallet
+ * @returns {object} Transaction functions and state
+ *
+ * @example
+ * ```tsx
+ * import { useSendTransaction } from "@account-kit/universal-account";
+ * import { useSigner } from "@account-kit/react";
+ * import { toBytes } from "viem";
+ *
+ * function TransactionButtons() {
+ * const signer = useSigner();
+ * const {
+ * sendTransfer,
+ * sendUniversal,
+ * sendBuy,
+ * sendSell,
+ * sendConvert,
+ * isLoading
+ * } = useSendTransaction({
+ * signMessage: (msg) => signer!.signMessage({ raw: toBytes(msg) }),
+ * });
+ *
+ * // Transfer tokens
+ * const handleTransfer = () => sendTransfer({
+ * token: { chainId: 42161, address: "0x..." },
+ * amount: "10",
+ * receiver: "0x...",
+ * });
+ *
+ * // Buy $10 worth of a token
+ * const handleBuy = () => sendBuy({
+ * token: { chainId: 42161, address: "0x..." },
+ * amountInUSD: "10",
+ * });
+ *
+ * // Sell tokens
+ * const handleSell = () => sendSell({
+ * token: { chainId: 42161, address: "0x..." },
+ * amount: "0.1",
+ * });
+ *
+ * // Convert to USDC on Arbitrum
+ * const handleConvert = () => sendConvert({
+ * expectToken: { type: "USDC", amount: "1" },
+ * chainId: 42161,
+ * });
+ * }
+ * ```
+ */
+export function useSendTransaction(options: {
+ signMessage: (message: string) => Promise;
+}) {
+ const { client, isReady } = useUniversalAccountContext();
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [lastResult, setLastResult] = useState(null);
+
+ /**
+ * Helper to execute a transaction with signing
+ */
+ const executeTransaction = useCallback(
+ async (
+ createTx: () => Promise,
+ callbacks?: TransactionCallbackOptions,
+ ): Promise => {
+ if (!client) {
+ throw new Error("Universal Account not initialized");
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const tx = await createTx();
+ callbacks?.onTransactionCreated?.(tx);
+
+ const signature = await options.signMessage(tx.rootHash);
+ const result = await client.sendTransaction(tx, signature);
+
+ setLastResult(result);
+ return result;
+ } catch (err) {
+ const txError =
+ err instanceof Error ? err : new Error("Transaction failed");
+ setError(txError);
+ throw txError;
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ [client, options],
+ );
+
+ /**
+ * Send a token transfer
+ */
+ const sendTransfer = useCallback(
+ async (
+ params: TransferTransactionParams & TransactionCallbackOptions,
+ ): Promise => {
+ return executeTransaction(
+ () => client!.createTransferTransaction(params),
+ params,
+ );
+ },
+ [client, executeTransaction],
+ );
+
+ /**
+ * Send a custom universal transaction (contract interactions)
+ */
+ const sendUniversal = useCallback(
+ async (
+ params: UniversalTransactionParams & TransactionCallbackOptions,
+ ): Promise => {
+ return executeTransaction(
+ () => client!.createUniversalTransaction(params),
+ params,
+ );
+ },
+ [client, executeTransaction],
+ );
+
+ /**
+ * Buy/swap into a target token using USD value from primary assets
+ */
+ const sendBuy = useCallback(
+ async (
+ params: BuyTransactionParams & TransactionCallbackOptions,
+ ): Promise => {
+ return executeTransaction(
+ () => client!.createBuyTransaction(params),
+ params,
+ );
+ },
+ [client, executeTransaction],
+ );
+
+ /**
+ * Sell a token back into primary assets
+ */
+ const sendSell = useCallback(
+ async (
+ params: SellTransactionParams & TransactionCallbackOptions,
+ ): Promise => {
+ return executeTransaction(
+ () => client!.createSellTransaction(params),
+ params,
+ );
+ },
+ [client, executeTransaction],
+ );
+
+ /**
+ * Convert between primary assets on a specific chain
+ */
+ const sendConvert = useCallback(
+ async (
+ params: ConvertTransactionParams & TransactionCallbackOptions,
+ ): Promise => {
+ return executeTransaction(
+ () => client!.createConvertTransaction(params),
+ params,
+ );
+ },
+ [client, executeTransaction],
+ );
+
+ return {
+ /** Send a token transfer */
+ sendTransfer,
+ /** Send a custom contract interaction */
+ sendUniversal,
+ /** Buy/swap into a target token */
+ sendBuy,
+ /** Sell a token back into primary assets */
+ sendSell,
+ /** Convert between primary assets */
+ sendConvert,
+ /** Whether a transaction is in progress */
+ isLoading,
+ /** Last error, if any */
+ error,
+ /** Result of the last transaction */
+ lastResult,
+ /** Whether the hook is ready to send transactions */
+ isReady,
+ };
+}
diff --git a/account-kit/universal-account/src/types.ts b/account-kit/universal-account/src/types.ts
new file mode 100644
index 0000000000..c6d1294853
--- /dev/null
+++ b/account-kit/universal-account/src/types.ts
@@ -0,0 +1,284 @@
+/**
+ * Type definitions for @account-kit/universal-account
+ *
+ * These types define the interface for working with Particle Network's
+ * Universal Accounts within Alchemy Account Kit.
+ *
+ * @see https://developers.particle.network/universal-accounts/cha/overview
+ */
+
+import type { Address } from "viem";
+
+/**
+ * Configuration for initializing a Universal Account
+ */
+export interface UniversalAccountConfig {
+ /** Particle Network project ID from dashboard */
+ projectId: string;
+ /** Particle Network client key from dashboard */
+ projectClientKey: string;
+ /** Particle Network app UUID from dashboard */
+ projectAppUuid: string;
+ /** Trade configuration for swaps and transactions */
+ tradeConfig?: TradeConfig;
+}
+
+/**
+ * Trade configuration options
+ */
+export interface TradeConfig {
+ /** Slippage tolerance in basis points (100 = 1%) */
+ slippageBps?: number;
+ /** Use PARTI token to pay for gas fees */
+ universalGas?: boolean;
+ /** Specify which primary tokens to use as source for swaps */
+ usePrimaryTokens?: string[];
+}
+
+/**
+ * Smart account options returned from Universal Account
+ */
+export interface SmartAccountOptions {
+ /** Name of the smart account implementation */
+ name: string;
+ /** Version of the smart account */
+ version: string;
+ /** EOA address that owns the Universal Account */
+ ownerAddress: Address;
+ /** EVM Universal Account address */
+ smartAccountAddress: Address;
+ /** Solana Universal Account address */
+ solanaSmartAccountAddress?: string;
+ /** Sender address for transactions */
+ senderAddress: Address;
+ /** Sender Solana address */
+ senderSolanaAddress?: string;
+}
+
+/**
+ * Asset information for a specific token
+ */
+export interface AssetInfo {
+ /** Token type identifier */
+ tokenType: string;
+ /** Current price in USD */
+ price: number;
+ /** Amount held */
+ amount: string;
+ /** Amount in USD */
+ amountInUSD: number;
+ /** Breakdown by chain */
+ chainAggregation: ChainAssetInfo[];
+}
+
+/**
+ * Asset information per chain
+ */
+export interface ChainAssetInfo {
+ /** Chain ID */
+ chainId: number;
+ /** Token contract address */
+ address: Address;
+ /** Amount on this chain */
+ amount: string;
+ /** Amount in USD */
+ amountInUSD: number;
+ /** Raw amount (with decimals) */
+ rawAmount: string;
+ /** Token decimals */
+ decimals: number;
+}
+
+/**
+ * Primary assets response
+ */
+export interface PrimaryAssets {
+ /** List of assets */
+ assets: AssetInfo[];
+ /** Total balance in USD */
+ totalAmountInUSD: number;
+}
+
+/**
+ * Token identifier for transactions
+ */
+export interface TokenIdentifier {
+ /** Chain ID where the token exists */
+ chainId: number;
+ /** Token contract address (use 0x0...0 for native token) */
+ address: Address;
+}
+
+/**
+ * Expected token for universal transactions
+ */
+export interface ExpectToken {
+ /** Token type (e.g., "ETH", "USDT") */
+ type: string;
+ /** Amount to expect */
+ amount: string;
+}
+
+/**
+ * Parameters for creating a transfer transaction
+ */
+export interface TransferTransactionParams {
+ /** Token to transfer */
+ token: TokenIdentifier;
+ /** Amount to transfer (human-readable) */
+ amount: string;
+ /** Receiver address */
+ receiver: Address;
+}
+
+/**
+ * Parameters for creating a universal transaction
+ */
+export interface UniversalTransactionParams {
+ /** Destination chain ID */
+ chainId: number;
+ /** Expected tokens on destination */
+ expectTokens: ExpectToken[];
+ /** Transactions to execute */
+ transactions: TransactionRequest[];
+}
+
+/**
+ * Parameters for creating a buy/swap transaction
+ * Converts USD value from primary assets into a target token
+ */
+export interface BuyTransactionParams {
+ /** Target token to buy */
+ token: TokenIdentifier;
+ /** Amount in USD to spend */
+ amountInUSD: string;
+}
+
+/**
+ * Parameters for creating a sell transaction
+ * Sells a token back into primary assets
+ */
+export interface SellTransactionParams {
+ /** Token to sell */
+ token: TokenIdentifier;
+ /** Amount of token to sell (human-readable) */
+ amount: string;
+}
+
+/**
+ * Parameters for creating a convert transaction
+ * Converts between primary assets on a specific chain
+ */
+export interface ConvertTransactionParams {
+ /** Target token to convert to */
+ expectToken: ExpectToken;
+ /** Destination chain ID */
+ chainId: number;
+}
+
+/**
+ * Transaction request for universal transactions
+ */
+export interface TransactionRequest {
+ /** Target contract address */
+ to: Address;
+ /** Encoded function data */
+ data: `0x${string}`;
+ /** Value to send (in wei, hex encoded) */
+ value?: `0x${string}`;
+}
+
+/**
+ * Universal transaction object returned from create methods
+ */
+export interface UniversalTransaction {
+ /** Transaction type */
+ type: "universal";
+ /** Network mode */
+ mode: "mainnet" | "testnet";
+ /** Sender address */
+ sender: Address;
+ /** Receiver address */
+ receiver: Address;
+ /** Transaction ID */
+ transactionId: string;
+ /** Root hash to sign */
+ rootHash: `0x${string}`;
+ /** Smart account options */
+ smartAccountOptions: SmartAccountOptions;
+ /** Fee quotes */
+ feeQuotes: FeeQuote[];
+}
+
+/**
+ * Fee quote for a transaction
+ */
+export interface FeeQuote {
+ fees: {
+ totals: {
+ feeTokenAmountInUSD: string;
+ gasFeeTokenAmountInUSD: string;
+ transactionServiceFeeTokenAmountInUSD: string;
+ transactionLPFeeTokenAmountInUSD: string;
+ };
+ freeGasFee: boolean;
+ freeServiceFee: boolean;
+ };
+}
+
+/**
+ * Result from sending a transaction
+ */
+export interface TransactionResult {
+ /** Unique transaction ID */
+ transactionId: string;
+ /** Transaction status */
+ status: string;
+ /** Transaction mode */
+ mode: "mainnet" | "testnet";
+ /** Sender address */
+ sender: Address;
+ /** Receiver address */
+ receiver: Address;
+ /** Transaction tag (buy, swap, transfer, etc.) */
+ tag: string;
+ /** Creation timestamp */
+ created_at: string;
+ /** Last update timestamp */
+ updated_at: string;
+}
+
+/**
+ * Universal Account instance interface
+ */
+export interface IUniversalAccount {
+ /** Get smart account options/addresses */
+ getSmartAccountOptions(): Promise;
+ /** Get primary assets (unified balance) */
+ getPrimaryAssets(): Promise;
+ /** Create a transfer transaction */
+ createTransferTransaction(
+ params: TransferTransactionParams
+ ): Promise;
+ /** Create a universal transaction */
+ createUniversalTransaction(
+ params: UniversalTransactionParams
+ ): Promise;
+ /** Create a buy/swap transaction */
+ createBuyTransaction(
+ params: BuyTransactionParams
+ ): Promise;
+ /** Create a sell transaction */
+ createSellTransaction(
+ params: SellTransactionParams
+ ): Promise;
+ /** Create a convert transaction */
+ createConvertTransaction(
+ params: ConvertTransactionParams
+ ): Promise;
+ /** Send a signed transaction */
+ sendTransaction(
+ transaction: UniversalTransaction,
+ signature: string
+ ): Promise;
+}
diff --git a/account-kit/universal-account/tsconfig.build.json b/account-kit/universal-account/tsconfig.build.json
new file mode 100644
index 0000000000..f13d852dc2
--- /dev/null
+++ b/account-kit/universal-account/tsconfig.build.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "composite": false,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ },
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test-d.ts", "src/__tests__/**/*"]
+}
diff --git a/account-kit/universal-account/tsconfig.json b/account-kit/universal-account/tsconfig.json
new file mode 100644
index 0000000000..e5603d861e
--- /dev/null
+++ b/account-kit/universal-account/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "typescript-template/base.json",
+ "compilerOptions": {
+ "composite": true,
+ "rootDir": "./src",
+ "jsx": "react-jsx"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/yarn.lock b/yarn.lock
index 96a82bb31c..28a7b28de2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1239,20 +1239,7 @@
"@babel/parser" "^7.27.2"
"@babel/types" "^7.27.1"
-"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3":
- version "7.27.1"
- resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz"
- integrity sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==
- dependencies:
- "@babel/code-frame" "^7.27.1"
- "@babel/generator" "^7.27.1"
- "@babel/parser" "^7.27.1"
- "@babel/template" "^7.27.1"
- "@babel/types" "^7.27.1"
- debug "^4.3.1"
- globals "^11.1.0"
-
-"@babel/traverse@^7.18.9", "@babel/traverse@^7.20.0", "@babel/traverse@^7.25.3", "@babel/traverse@^7.27.1":
+"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3", "@babel/traverse@^7.18.9", "@babel/traverse@^7.20.0", "@babel/traverse@^7.25.3", "@babel/traverse@^7.27.1":
version "7.27.1"
resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz"
integrity sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==
@@ -1507,6 +1494,40 @@
dependencies:
chalk "^4.1.0"
+"@coral-xyz/anchor-errors@^0.30.1":
+ version "0.30.1"
+ resolved "https://registry.yarnpkg.com/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz#bdfd3a353131345244546876eb4afc0e125bec30"
+ integrity sha512-9Mkradf5yS5xiLWrl9WrpjqOrAV+/W2RQHDlbnAZBivoGpOs1ECjoDCkVk4aRG8ZdiFiB8zQEVlxf+8fKkmSfQ==
+
+"@coral-xyz/anchor@^0.30.1":
+ version "0.30.1"
+ resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.30.1.tgz#17f3e9134c28cd0ea83574c6bab4e410bcecec5d"
+ integrity sha512-gDXFoF5oHgpriXAaLpxyWBHdCs8Awgf/gLHIo6crv7Aqm937CNdY+x+6hoj7QR5vaJV7MxWSQ0NGFzL3kPbWEQ==
+ dependencies:
+ "@coral-xyz/anchor-errors" "^0.30.1"
+ "@coral-xyz/borsh" "^0.30.1"
+ "@noble/hashes" "^1.3.1"
+ "@solana/web3.js" "^1.68.0"
+ bn.js "^5.1.2"
+ bs58 "^4.0.1"
+ buffer-layout "^1.2.2"
+ camelcase "^6.3.0"
+ cross-fetch "^3.1.5"
+ crypto-hash "^1.3.0"
+ eventemitter3 "^4.0.7"
+ pako "^2.0.3"
+ snake-case "^3.0.4"
+ superstruct "^0.15.4"
+ toml "^3.0.0"
+
+"@coral-xyz/borsh@^0.30.1":
+ version "0.30.1"
+ resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.30.1.tgz#869d8833abe65685c72e9199b8688477a4f6b0e3"
+ integrity sha512-aaxswpPrCFKl8vZTbxLssA2RvwX2zmKLlRCIktJOwW+VpVwYtXRtlWiIP+c2pPRKneiTiWCN2GEMSH9j1zTlWQ==
+ dependencies:
+ bn.js "^5.1.2"
+ buffer-layout "^1.2.0"
+
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz"
@@ -4854,6 +4875,23 @@
dependencies:
"@particle-network/auth" "^1.3.1"
+"@particle-network/universal-account-sdk@^1.0.0":
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/@particle-network/universal-account-sdk/-/universal-account-sdk-1.0.10.tgz#205ec4760fb0ebf87fca3cd3b07a249e10621cc9"
+ integrity sha512-xd1yaEBpeMl9QWRWq1h2yxwKFEae4pYPVsomnpLqZmY1XSqKRRCspdmVeGFgKHz6SaoowWTrmqkh+daP6SjAGQ==
+ dependencies:
+ "@coral-xyz/anchor" "^0.30.1"
+ "@noble/hashes" "^1.7.1"
+ "@solana/spl-token" "^0.4.9"
+ "@solana/web3.js" "^1.98.0"
+ axios "^1.8.4"
+ borsh "^2.0.0"
+ fast-json-stable-stringify "^2.1.0"
+ merkletreejs "^0.5.1"
+ ts-enum-util "^4.1.0"
+ uuid "^11.1.0"
+ viem "^2.24.3"
+
"@paulmillr/qr@^0.2.1":
version "0.2.1"
resolved "https://registry.npmjs.org/@paulmillr/qr/-/qr-0.2.1.tgz"
@@ -7265,6 +7303,17 @@
"@solana/spl-token-metadata" "^0.1.6"
buffer "^6.0.3"
+"@solana/spl-token@^0.4.9":
+ version "0.4.14"
+ resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.4.14.tgz#b86bc8a17f50e9680137b585eca5f5eb9d55c025"
+ integrity sha512-u09zr96UBpX4U685MnvQsNzlvw9TiY005hk1vJmJr7gMJldoPG1eYU5/wNEyOA5lkMLiR/gOi9SFD4MefOYEsA==
+ dependencies:
+ "@solana/buffer-layout" "^4.0.0"
+ "@solana/buffer-layout-utils" "^0.2.0"
+ "@solana/spl-token-group" "^0.0.7"
+ "@solana/spl-token-metadata" "^0.1.6"
+ buffer "^6.0.3"
+
"@solana/subscribable@2.3.0":
version "2.3.0"
resolved "https://registry.npmjs.org/@solana/subscribable/-/subscribable-2.3.0.tgz"
@@ -7763,6 +7812,27 @@
rpc-websockets "^9.0.2"
superstruct "^2.0.2"
+"@solana/web3.js@^1.68.0":
+ version "1.98.4"
+ resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.98.4.tgz#df51d78be9d865181ec5138b4e699d48e6895bbe"
+ integrity sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==
+ dependencies:
+ "@babel/runtime" "^7.25.0"
+ "@noble/curves" "^1.4.2"
+ "@noble/hashes" "^1.4.0"
+ "@solana/buffer-layout" "^4.0.1"
+ "@solana/codecs-numbers" "^2.1.0"
+ agentkeepalive "^4.5.0"
+ bn.js "^5.2.1"
+ borsh "^0.7.0"
+ bs58 "^4.0.1"
+ buffer "6.0.3"
+ fast-stable-stringify "^1.0.0"
+ jayson "^4.1.1"
+ node-fetch "^2.7.0"
+ rpc-websockets "^9.0.2"
+ superstruct "^2.0.2"
+
"@solflare-wallet/metamask-sdk@^1.0.3":
version "1.0.3"
resolved "https://registry.npmjs.org/@solflare-wallet/metamask-sdk/-/metamask-sdk-1.0.3.tgz"
@@ -12234,7 +12304,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.8, bn.js@^4.11.9:
resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz"
integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==
-bn.js@^5.2.0, bn.js@^5.2.1, bn.js@^5.2.2:
+bn.js@^5.1.2, bn.js@^5.2.0, bn.js@^5.2.1, bn.js@^5.2.2:
version "5.2.2"
resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz"
integrity sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==
@@ -12262,7 +12332,7 @@ boring-avatars@^1.11.2:
resolved "https://registry.npmjs.org/boring-avatars/-/boring-avatars-1.11.2.tgz"
integrity sha512-3+wkwPeObwS4R37FGXMYViqc4iTrIRj5yzfX9Qy4mnpZ26sX41dGMhsAgmKks1r/uufY1pl4vpgzMWHYfJRb2A==
-borsh@2.0.0:
+borsh@2.0.0, borsh@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/borsh/-/borsh-2.0.0.tgz"
integrity sha512-kc9+BgR3zz9+cjbwM8ODoUB4fs3X3I5A/HtX7LZKxCLaMrEeDFoBpnhZY//DTS1VZBSs6S5v46RZRbZjRFspEg==
@@ -12495,6 +12565,16 @@ buffer-from@^1.0.0:
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+buffer-layout@^1.2.0, buffer-layout@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/buffer-layout/-/buffer-layout-1.2.2.tgz#b9814e7c7235783085f9ca4966a0cfff112259d5"
+ integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==
+
+buffer-reverse@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/buffer-reverse/-/buffer-reverse-1.0.1.tgz#49283c8efa6f901bc01fa3304d06027971ae2f60"
+ integrity sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg==
+
buffer-xor@^1.0.3:
version "1.0.3"
resolved "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz"
@@ -13660,7 +13740,12 @@ crypto-browserify@^3.12.1:
randombytes "^2.1.0"
randomfill "^1.0.4"
-crypto-js@^4.1.1:
+crypto-hash@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247"
+ integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==
+
+crypto-js@^4.1.1, crypto-js@^4.2.0:
version "4.2.0"
resolved "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz"
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
@@ -14180,6 +14265,14 @@ domain-browser@^1.1.1:
resolved "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
+dot-case@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
+ integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==
+ dependencies:
+ no-case "^3.0.4"
+ tslib "^2.0.3"
+
dot-prop@^5.1.0:
version "5.3.0"
resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz"
@@ -19328,6 +19421,13 @@ loupe@^3.1.0, loupe@^3.1.1, loupe@^3.1.2:
resolved "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz"
integrity sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==
+lower-case@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
+ integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
+ dependencies:
+ tslib "^2.0.3"
+
lowlight@^1.17.0:
version "1.20.0"
resolved "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz"
@@ -19875,6 +19975,15 @@ merge2@^1.3.0, merge2@^1.4.1:
resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+merkletreejs@^0.5.1:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/merkletreejs/-/merkletreejs-0.5.2.tgz#12321ff6121aa60ef237f4323a9fda193a69148f"
+ integrity sha512-MHqclSWRSQQbYciUMALC3PZmE23NPf5IIYo+Z7qAz5jVcqgCB95L1T9jGcr+FtOj2Pa2/X26uG2Xzxs7FJccUg==
+ dependencies:
+ buffer-reverse "^1.0.1"
+ crypto-js "^4.2.0"
+ treeify "^1.1.0"
+
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz"
@@ -21388,6 +21497,14 @@ nice-try@^1.0.4:
resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+no-case@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
+ integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
+ dependencies:
+ lower-case "^2.0.2"
+ tslib "^2.0.3"
+
nocache@^3.0.1:
version "3.0.4"
resolved "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz"
@@ -22265,6 +22382,11 @@ pacote@^18.0.0, pacote@^18.0.6:
ssri "^10.0.0"
tar "^6.1.11"
+pako@^2.0.3:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
+ integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
+
pako@~1.0.5:
version "1.0.11"
resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz"
@@ -24849,6 +24971,14 @@ smart-buffer@^4.2.0:
resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz"
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
+snake-case@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"
+ integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==
+ dependencies:
+ dot-case "^3.0.4"
+ tslib "^2.0.3"
+
socket.io-client@^4.5.1, socket.io-client@^4.7.5:
version "4.8.1"
resolved "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz"
@@ -25181,16 +25311,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
-"string-width-cjs@npm:string-width@^4.2.0":
- version "4.2.3"
- resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -25316,7 +25437,7 @@ stringify-entities@^4.0.0:
character-entities-html4 "^2.0.0"
character-entities-legacy "^3.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -25330,13 +25451,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.0"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz"
@@ -25479,6 +25593,11 @@ sudo-prompt@^9.0.0:
resolved "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz"
integrity sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==
+superstruct@^0.15.4:
+ version "0.15.5"
+ resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.15.5.tgz#0f0a8d3ce31313f0d84c6096cd4fa1bfdedc9dab"
+ integrity sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==
+
superstruct@^1.0.3:
version "1.0.4"
resolved "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz"
@@ -25914,6 +26033,11 @@ tr46@~0.0.3:
resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
+treeify@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8"
+ integrity sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==
+
treeverse@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/treeverse/-/treeverse-3.0.0.tgz"
@@ -25954,6 +26078,11 @@ ts-dedent@^2.0.0, ts-dedent@^2.2.0:
resolved "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz"
integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==
+ts-enum-util@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/ts-enum-util/-/ts-enum-util-4.1.0.tgz#d9d87f730a5c0bf72bad409e3ac431a9b0b6a878"
+ integrity sha512-kIs48itmNehkzLk0YJW/LfI2+VFYlyscGsY+oDNCnxrDfkex/OfYUV1ip7L7YIN7ppSqj2VmOOssiW81Rno9QA==
+
ts-interface-checker@^0.1.9:
version "0.1.13"
resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz"
@@ -26012,7 +26141,7 @@ tslib@2.6.2:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
-tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.6.0, tslib@^2.6.2, tslib@^2.8.0, tslib@^2.8.1:
+tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.6.0, tslib@^2.6.2, tslib@^2.8.0, tslib@^2.8.1:
version "2.8.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
@@ -27011,7 +27140,7 @@ vfile@^6.0.0, vfile@^6.0.3:
"@types/unist" "^3.0.0"
vfile-message "^4.0.0"
-viem@2.23.2, viem@2.29.2, viem@2.31.0, viem@2.33.3, viem@>=2.23.11, viem@>=2.29.0, viem@^2, viem@^2.1.1, viem@^2.21.35, viem@^2.21.40, viem@^2.29.2, viem@^2.31.7, viem@^2.32.0:
+viem@2.23.2, viem@2.29.2, viem@2.31.0, viem@2.33.3, viem@>=2.23.11, viem@>=2.29.0, viem@^2, viem@^2.1.1, viem@^2.21.35, viem@^2.21.40, viem@^2.24.3, viem@^2.29.2, viem@^2.31.7, viem@^2.32.0:
version "2.33.3"
resolved "https://registry.yarnpkg.com/viem/-/viem-2.33.3.tgz#b69d7ff9edf649d1b7d9218e0225bcadc83a8caa"
integrity sha512-aWDr6i6r3OfNCs0h9IieHFhn7xQJJ8YsuA49+9T5JRyGGAkWhLgcbLq2YMecgwM7HdUZpx1vPugZjsShqNi7Gw==
@@ -27360,7 +27489,7 @@ wordwrap@^1.0.0:
resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -27378,15 +27507,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
From e61fc74c957a361fa0bbef47a81e280092efd9cb Mon Sep 17 00:00:00 2001
From: Soos3D <99700157+soos3d@users.noreply.github.com>
Date: Thu, 4 Dec 2025 11:36:31 -0300
Subject: [PATCH 2/4] feat: add universal account integration with Particle
Network
---
.eslintignore | 2 +
README.md | 1 +
account-kit/universal-account/CHANGELOG.md | 10 ++
account-kit/universal-account/README.md | 139 +++++++++++-------
.../examples/next-example/.env.example | 10 ++
.../examples/next-example/.gitignore | 2 +-
account-kit/universal-account/package.json | 6 +-
account-kit/universal-account/src/client.ts | 48 +++---
.../universal-account/src/particle-sdk.d.ts | 6 +-
account-kit/universal-account/src/types.ts | 12 +-
.../universal-account/tsconfig.build.json | 8 +-
docs-site | 2 +-
12 files changed, 158 insertions(+), 88 deletions(-)
create mode 100644 account-kit/universal-account/CHANGELOG.md
create mode 100644 account-kit/universal-account/examples/next-example/.env.example
diff --git a/.eslintignore b/.eslintignore
index 13e737468e..e91100e875 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -9,6 +9,8 @@ site/.vitepress/cache/**/*
/examples/ui-demo/contracts
/examples/ui-demo/.next/*
+account-kit/universal-account/examples/*
+
**/.turbo/*
account-kit/rn-signer/lib/*
account-kit/java/*
diff --git a/README.md b/README.md
index d38bd8ffaa..a7e4897e5a 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@ Account Kit packages are all prefixed with `@account-kit` are broken down into t
1. [`@account-kit/signer`](https://github.com/alchemyplatform/aa-sdk/tree/main/account-kit/signer)
1. [`@account-kit/smart-contracts`](https://github.com/alchemyplatform/aa-sdk/tree/main/account-kit/smart-contracts)
1. [`@account-kit/privy-integration`](https://github.com/alchemyplatform/aa-sdk/tree/main/account-kit/privy-integration)
+1. [`@account-kit/universal-account`](https://github.com/alchemyplatform/aa-sdk/tree/main/account-kit/universal-account)
## @aa-sdk/\*
diff --git a/account-kit/universal-account/CHANGELOG.md b/account-kit/universal-account/CHANGELOG.md
new file mode 100644
index 0000000000..23e53106b5
--- /dev/null
+++ b/account-kit/universal-account/CHANGELOG.md
@@ -0,0 +1,10 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# 4.80.0 (2025-12-04)
+
+### Features
+
+- add Universal Account integration with Particle Network for chain abstraction
diff --git a/account-kit/universal-account/README.md b/account-kit/universal-account/README.md
index c915a6a375..390f0c8104 100644
--- a/account-kit/universal-account/README.md
+++ b/account-kit/universal-account/README.md
@@ -27,9 +27,11 @@ npm install @account-kit/universal-account
You'll need credentials from both dashboards:
**Alchemy** (for authentication):
+
- Get your API key from [Alchemy Dashboard](https://dashboard.alchemy.com)
**Particle Network** (for Universal Accounts):
+
1. Sign up at [Particle Dashboard](https://dashboard.particle.network/)
2. Create a project and web application
3. Copy your **Project ID**, **Client Key**, and **App ID**
@@ -59,6 +61,7 @@ When integrating Alchemy Account Kit with Universal Accounts, it's important to
```
**Key Concept**: Alchemy's SCA and Universal Account's smart accounts are **different**!
+
- Use Alchemy for authentication and getting the EOA
- Use Universal Accounts for cross-chain operations
@@ -122,13 +125,13 @@ import { useUser, useSigner } from "@account-kit/react";
function MyComponent() {
const user = useUser();
const signer = useSigner();
-
+
// ✅ CORRECT: Use EOA for Universal Accounts
const eoaAddress = user?.address as `0x${string}` | undefined;
-
+
// ❌ WRONG: Don't use SCA for Universal Accounts
// const { address } = useAccount({ type: "LightAccount" });
-
+
return ;
}
```
@@ -139,19 +142,22 @@ The `useUniversalAccount` hook auto-initializes when you pass the EOA address:
```tsx
import { useUser, useSigner } from "@account-kit/react";
-import { useUniversalAccount, useUnifiedBalance } from "@account-kit/universal-account";
+import {
+ useUniversalAccount,
+ useUnifiedBalance,
+} from "@account-kit/universal-account";
function Dashboard() {
const user = useUser();
const eoaAddress = user?.address as `0x${string}` | undefined;
// Universal Account auto-initializes with the EOA address
- const {
- address, // Universal Account EVM address
- solanaAddress, // Universal Account Solana address
- isReady,
+ const {
+ address, // Universal Account EVM address
+ solanaAddress, // Universal Account Solana address
+ isReady,
isInitializing,
- error
+ error,
} = useUniversalAccount(eoaAddress);
// Get unified balance across all chains
@@ -166,14 +172,14 @@ function Dashboard() {
Universal Account
EVM Address: {address}
Solana Address: {solanaAddress}
-
+
Unified Balance: ${totalBalanceUSD?.toFixed(2)}
{assets?.map((asset) => (
{asset.tokenType}: {asset.amount} (${asset.amountInUSD.toFixed(2)})
))}
-
+
@@ -193,7 +199,7 @@ import { toBytes, encodeFunctionData } from "viem";
function MintNFT() {
const signer = useSigner();
-
+
const { sendUniversal, isLoading, error, lastResult } = useSendTransaction({
signMessage: async (message: string) => {
if (!signer) throw new Error("Signer not available");
@@ -223,7 +229,10 @@ function MintNFT() {
});
console.log("Transaction ID:", result.transactionId);
- console.log("View on UniversalX:", `https://universalx.app/activity/details?id=${result.transactionId}`);
+ console.log(
+ "View on UniversalX:",
+ `https://universalx.app/activity/details?id=${result.transactionId}`,
+ );
};
return (
@@ -232,7 +241,9 @@ function MintNFT() {
{isLoading ? "Minting..." : "Mint NFT on Avalanche"}
{lastResult && (
-
+
View Transaction
)}
@@ -251,35 +262,39 @@ function MintNFT() {
The package exports helpful constants for chain IDs and token types:
```typescript
-import { CHAIN_ID, TOKEN_TYPE, NATIVE_TOKEN_ADDRESS } from "@account-kit/universal-account";
+import {
+ CHAIN_ID,
+ TOKEN_TYPE,
+ NATIVE_TOKEN_ADDRESS,
+} from "@account-kit/universal-account";
// Use chain IDs
const tx = await sendUniversal({
- chainId: CHAIN_ID.AVALANCHE, // 43114
+ chainId: CHAIN_ID.AVALANCHE, // 43114
// ...
});
// Available chains:
-CHAIN_ID.ETHEREUM // 1
-CHAIN_ID.BNB_CHAIN // 56
-CHAIN_ID.BASE // 8453
-CHAIN_ID.ARBITRUM // 42161
-CHAIN_ID.AVALANCHE // 43114
-CHAIN_ID.OPTIMISM // 10
-CHAIN_ID.POLYGON // 137
-CHAIN_ID.LINEA // 59144
-CHAIN_ID.BERACHAIN // 80094
-CHAIN_ID.SOLANA // 101
+CHAIN_ID.ETHEREUM; // 1
+CHAIN_ID.BNB_CHAIN; // 56
+CHAIN_ID.BASE; // 8453
+CHAIN_ID.ARBITRUM; // 42161
+CHAIN_ID.AVALANCHE; // 43114
+CHAIN_ID.OPTIMISM; // 10
+CHAIN_ID.POLYGON; // 137
+CHAIN_ID.LINEA; // 59144
+CHAIN_ID.BERACHAIN; // 80094
+CHAIN_ID.SOLANA; // 101
// ... and more
// Token types for expectTokens
-TOKEN_TYPE.ETH
-TOKEN_TYPE.USDC
-TOKEN_TYPE.USDT
-TOKEN_TYPE.SOL
+TOKEN_TYPE.ETH;
+TOKEN_TYPE.USDC;
+TOKEN_TYPE.USDT;
+TOKEN_TYPE.SOL;
// Native token address (for ETH, AVAX, etc.)
-NATIVE_TOKEN_ADDRESS // "0x0000000000000000000000000000000000000000"
+NATIVE_TOKEN_ADDRESS; // "0x0000000000000000000000000000000000000000"
```
---
@@ -332,10 +347,11 @@ Initialize and manage a Universal Account. Auto-initializes when `ownerAddress`
| `disconnect` | `() => void` | Reset the Universal Account |
**Example:**
+
```tsx
const user = useUser();
const { address, solanaAddress, isReady, error } = useUniversalAccount(
- user?.address as `0x${string}`
+ user?.address as `0x${string}`,
);
```
@@ -361,13 +377,15 @@ Fetch the unified balance across all chains. Automatically fetches when the Univ
| `refetch` | `() => void` | Manually refresh balance |
**Asset Structure:**
+
```typescript
interface AssetInfo {
- tokenType: string; // e.g., "USDT", "ETH"
- price: number; // Current price in USD
- amount: string; // Total amount across chains
- amountInUSD: number; // Total value in USD
- chainAggregation: { // Breakdown by chain
+ tokenType: string; // e.g., "USDT", "ETH"
+ price: number; // Current price in USD
+ amount: string; // Total amount across chains
+ amountInUSD: number; // Total value in USD
+ chainAggregation: {
+ // Breakdown by chain
chainId: number;
address: string;
amount: string;
@@ -377,6 +395,7 @@ interface AssetInfo {
```
**Example:**
+
```tsx
const { totalBalanceUSD, assets, refetch, isLoading } = useUnifiedBalance({
refetchInterval: 30000, // Refresh every 30 seconds
@@ -410,20 +429,24 @@ Send Universal Account transactions with automatic signing flow. Supports all tr
**Transaction Types:**
##### `sendTransfer` - Token Transfer
+
Send tokens to any address across chains.
+
```typescript
await sendTransfer({
token: {
- chainId: 42161, // Arbitrum
+ chainId: 42161, // Arbitrum
address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT
},
- amount: "10", // Human-readable amount
- receiver: "0x...", // Recipient address
+ amount: "10", // Human-readable amount
+ receiver: "0x...", // Recipient address
});
```
##### `sendUniversal` - Custom Contract Interaction
+
Execute any contract call with automatic liquidity routing.
+
```typescript
await sendUniversal({
chainId: 8453, // Base
@@ -439,40 +462,47 @@ await sendUniversal({
```
##### `sendBuy` - Buy/Swap Token
+
Buy a token using USD value from your primary assets.
+
```typescript
await sendBuy({
token: {
- chainId: 42161, // Arbitrum
+ chainId: 42161, // Arbitrum
address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT
},
- amountInUSD: "10", // Spend $10 worth of primary assets
+ amountInUSD: "10", // Spend $10 worth of primary assets
});
```
##### `sendSell` - Sell Token
+
Sell a token back into primary assets.
+
```typescript
await sendSell({
token: {
- chainId: 42161, // Arbitrum
+ chainId: 42161, // Arbitrum
address: "0x912CE59144191C1204E64559FE8253a0e49E6548", // ARB
},
- amount: "0.1", // Sell 0.1 ARB
+ amount: "0.1", // Sell 0.1 ARB
});
```
##### `sendConvert` - Convert Primary Assets
+
Convert between primary assets on a specific chain.
+
```typescript
await sendConvert({
expectToken: { type: "USDC", amount: "1" },
- chainId: 42161, // Arbitrum
+ chainId: 42161, // Arbitrum
});
```
**Solana Support:**
All transaction types work with Solana. Use chain ID for Solana mainnet and the appropriate token addresses:
+
```typescript
// Buy SOL or Solana tokens
await sendBuy({
@@ -485,6 +515,7 @@ await sendBuy({
```
**Full Example:**
+
```tsx
const signer = useSigner();
@@ -498,13 +529,15 @@ const { sendTransfer, sendBuy, sendUniversal, isLoading } = useSendTransaction({
await sendUniversal({
chainId: 43114,
expectTokens: [],
- transactions: [{
- to: "0xdea7bF60E53CD578e3526F36eC431795f7EEbFe6",
- data: encodeFunctionData({
- abi: [{ type: "function", name: "mint", inputs: [], outputs: [] }],
- functionName: "mint",
- }),
- }],
+ transactions: [
+ {
+ to: "0xdea7bF60E53CD578e3526F36eC431795f7EEbFe6",
+ data: encodeFunctionData({
+ abi: [{ type: "function", name: "mint", inputs: [], outputs: [] }],
+ functionName: "mint",
+ }),
+ },
+ ],
});
```
@@ -581,6 +614,7 @@ console.log("Explorer:", client.getExplorerUrl(result.transactionId));
## Supported Chains
Universal Accounts support 15+ EVM chains and Solana:
+
- Ethereum, Base, Arbitrum, Optimism, Polygon
- Avalanche, BNB Chain, Fantom, Gnosis
- And more...
@@ -590,6 +624,7 @@ See the [full list of supported chains](https://developers.particle.network/univ
## Fees
Universal Account transactions may include:
+
- **Gas fees**: Standard network fees on the destination chain
- **LP fee**: 0.2% for cross-chain transactions
- **Service fee**: 1% on transaction volume
diff --git a/account-kit/universal-account/examples/next-example/.env.example b/account-kit/universal-account/examples/next-example/.env.example
new file mode 100644
index 0000000000..19252ad9b0
--- /dev/null
+++ b/account-kit/universal-account/examples/next-example/.env.example
@@ -0,0 +1,10 @@
+# Alchemy Account Kit
+# Get these from https://dashboard.alchemy.com
+NEXT_PUBLIC_ALCHEMY_API_KEY=your_alchemy_api_key
+NEXT_PUBLIC_ALCHEMY_POLICY_ID=your_gas_policy_id
+
+# Particle Network Universal Accounts
+# Get these from https://dashboard.particle.network
+NEXT_PUBLIC_PARTICLE_PROJECT_ID=your_particle_project_id
+NEXT_PUBLIC_PARTICLE_CLIENT_KEY=your_particle_client_key
+NEXT_PUBLIC_PARTICLE_APP_ID=your_particle_app_id
diff --git a/account-kit/universal-account/examples/next-example/.gitignore b/account-kit/universal-account/examples/next-example/.gitignore
index 86fd299e8c..5226837a17 100644
--- a/account-kit/universal-account/examples/next-example/.gitignore
+++ b/account-kit/universal-account/examples/next-example/.gitignore
@@ -32,7 +32,7 @@ yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
-.env*
+.env
# vercel
.vercel
diff --git a/account-kit/universal-account/package.json b/account-kit/universal-account/package.json
index fabfe1de81..5f0779888e 100644
--- a/account-kit/universal-account/package.json
+++ b/account-kit/universal-account/package.json
@@ -1,6 +1,6 @@
{
"name": "@account-kit/universal-account",
- "version": "4.79.0",
+ "version": "4.80.0",
"description": "Universal Account integration for Account Kit - enabling chain abstraction with Particle Network",
"author": "Alchemy",
"license": "MIT",
@@ -39,8 +39,8 @@
"test:run": "vitest run --passWithNoTests"
},
"dependencies": {
- "@account-kit/infra": "^4.79.0",
- "@account-kit/signer": "^4.79.0",
+ "@account-kit/infra": "^4.80.0",
+ "@account-kit/signer": "^4.80.0",
"@particle-network/universal-account-sdk": "^1.0.0"
},
"peerDependencies": {
diff --git a/account-kit/universal-account/src/client.ts b/account-kit/universal-account/src/client.ts
index 95b79e2bf0..ebef299085 100644
--- a/account-kit/universal-account/src/client.ts
+++ b/account-kit/universal-account/src/client.ts
@@ -49,6 +49,12 @@ export class UniversalAccountClient implements IUniversalAccount {
private ua: any;
private _ownerAddress: Address;
+ /**
+ * Creates a new UniversalAccountClient instance
+ *
+ * @param {any} ua - The underlying Particle Universal Account instance
+ * @param {Address} ownerAddress - The EOA address that owns this Universal Account
+ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(ua: any, ownerAddress: Address) {
this.ua = ua;
@@ -126,14 +132,15 @@ export class UniversalAccountClient implements IUniversalAccount {
price: asset.price,
amount: asset.amount,
amountInUSD: asset.amountInUSD,
- chainAggregation: asset.chainAggregation?.map((chain: any) => ({
- chainId: chain.token?.chainId ?? chain.chainId,
- address: chain.token?.address ?? chain.address,
- amount: chain.amount,
- amountInUSD: chain.amountInUSD,
- rawAmount: chain.rawAmount,
- decimals: chain.token?.decimals ?? chain.decimals,
- })) ?? [],
+ chainAggregation:
+ asset.chainAggregation?.map((chain: any) => ({
+ chainId: chain.token?.chainId ?? chain.chainId,
+ address: chain.token?.address ?? chain.address,
+ amount: chain.amount,
+ amountInUSD: chain.amountInUSD,
+ rawAmount: chain.rawAmount,
+ decimals: chain.token?.decimals ?? chain.decimals,
+ })) ?? [],
})),
totalAmountInUSD: assets.totalAmountInUSD,
};
@@ -164,7 +171,7 @@ export class UniversalAccountClient implements IUniversalAccount {
* ```
*/
async createTransferTransaction(
- params: TransferTransactionParams
+ params: TransferTransactionParams,
): Promise {
const tx = await this.ua.createTransferTransaction({
token: {
@@ -202,7 +209,7 @@ export class UniversalAccountClient implements IUniversalAccount {
* ```
*/
async createUniversalTransaction(
- params: UniversalTransactionParams
+ params: UniversalTransactionParams,
): Promise {
const tx = await this.ua.createUniversalTransaction({
chainId: params.chainId,
@@ -241,7 +248,7 @@ export class UniversalAccountClient implements IUniversalAccount {
* ```
*/
async createBuyTransaction(
- params: BuyTransactionParams
+ params: BuyTransactionParams,
): Promise {
const tx = await this.ua.createBuyTransaction({
token: {
@@ -275,7 +282,7 @@ export class UniversalAccountClient implements IUniversalAccount {
* ```
*/
async createSellTransaction(
- params: SellTransactionParams
+ params: SellTransactionParams,
): Promise {
const tx = await this.ua.createSellTransaction({
token: {
@@ -306,7 +313,7 @@ export class UniversalAccountClient implements IUniversalAccount {
* ```
*/
async createConvertTransaction(
- params: ConvertTransactionParams
+ params: ConvertTransactionParams,
): Promise {
const tx = await this.ua.createConvertTransaction({
expectToken: {
@@ -335,15 +342,12 @@ export class UniversalAccountClient implements IUniversalAccount {
*/
async sendTransaction(
transaction: UniversalTransaction,
- signature: string
+ signature: string,
): Promise {
// We need to pass the original transaction object from the SDK
// The transaction parameter here is our mapped type, but we stored
// the original in a way that sendTransaction can use it
- const result = await this.ua.sendTransaction(
- transaction as any,
- signature
- );
+ const result = await this.ua.sendTransaction(transaction as any, signature);
return {
transactionId: result.transactionId,
@@ -390,8 +394,10 @@ export class UniversalAccountClient implements IUniversalAccount {
name: tx.smartAccountOptions?.name ?? "",
version: tx.smartAccountOptions?.version ?? "",
ownerAddress: tx.smartAccountOptions?.ownerAddress as Address,
- smartAccountAddress: tx.smartAccountOptions?.smartAccountAddress as Address,
- solanaSmartAccountAddress: tx.smartAccountOptions?.solanaSmartAccountAddress,
+ smartAccountAddress: tx.smartAccountOptions
+ ?.smartAccountAddress as Address,
+ solanaSmartAccountAddress:
+ tx.smartAccountOptions?.solanaSmartAccountAddress,
senderAddress: tx.smartAccountOptions?.senderAddress as Address,
senderSolanaAddress: tx.smartAccountOptions?.senderSolanaAddress,
},
@@ -425,7 +431,7 @@ export class UniversalAccountClient implements IUniversalAccount {
* ```
*/
export async function createUniversalAccountClient(
- params: CreateUniversalAccountClientParams
+ params: CreateUniversalAccountClientParams,
): Promise {
// Dynamic import to avoid bundling issues - users must install the peer dependency
const { UniversalAccount } = await import(
diff --git a/account-kit/universal-account/src/particle-sdk.d.ts b/account-kit/universal-account/src/particle-sdk.d.ts
index 47968fff78..976d14c5ef 100644
--- a/account-kit/universal-account/src/particle-sdk.d.ts
+++ b/account-kit/universal-account/src/particle-sdk.d.ts
@@ -109,14 +109,14 @@ declare module "@particle-network/universal-account-sdk" {
getSmartAccountOptions(): Promise;
getPrimaryAssets(): Promise;
createTransferTransaction(
- params: TransferTransactionParams
+ params: TransferTransactionParams,
): Promise;
createUniversalTransaction(
- params: UniversalTransactionParams
+ params: UniversalTransactionParams,
): Promise;
sendTransaction(
transaction: UniversalTransaction,
- signature: string
+ signature: string,
): Promise;
}
diff --git a/account-kit/universal-account/src/types.ts b/account-kit/universal-account/src/types.ts
index c6d1294853..401116f1f5 100644
--- a/account-kit/universal-account/src/types.ts
+++ b/account-kit/universal-account/src/types.ts
@@ -258,27 +258,27 @@ export interface IUniversalAccount {
getPrimaryAssets(): Promise;
/** Create a transfer transaction */
createTransferTransaction(
- params: TransferTransactionParams
+ params: TransferTransactionParams,
): Promise;
/** Create a universal transaction */
createUniversalTransaction(
- params: UniversalTransactionParams
+ params: UniversalTransactionParams,
): Promise;
/** Create a buy/swap transaction */
createBuyTransaction(
- params: BuyTransactionParams
+ params: BuyTransactionParams,
): Promise;
/** Create a sell transaction */
createSellTransaction(
- params: SellTransactionParams
+ params: SellTransactionParams,
): Promise;
/** Create a convert transaction */
createConvertTransaction(
- params: ConvertTransactionParams
+ params: ConvertTransactionParams,
): Promise;
/** Send a signed transaction */
sendTransaction(
transaction: UniversalTransaction,
- signature: string
+ signature: string,
): Promise;
}
diff --git a/account-kit/universal-account/tsconfig.build.json b/account-kit/universal-account/tsconfig.build.json
index f13d852dc2..df3d024904 100644
--- a/account-kit/universal-account/tsconfig.build.json
+++ b/account-kit/universal-account/tsconfig.build.json
@@ -6,5 +6,11 @@
"declarationMap": true,
"sourceMap": true
},
- "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test-d.ts", "src/__tests__/**/*"]
+ "exclude": [
+ "node_modules",
+ "dist",
+ "**/*.test.ts",
+ "**/*.test-d.ts",
+ "src/__tests__/**/*"
+ ]
}
diff --git a/docs-site b/docs-site
index f58f3913ae..256822886b 160000
--- a/docs-site
+++ b/docs-site
@@ -1 +1 @@
-Subproject commit f58f3913ae157575f5925961d6c6fdb38a1462a8
+Subproject commit 256822886b4c07e8bac4bb97a5ed9c0b97d49ced
From 0b33fc213d769cc1bdf5264e8b120c6aa656e708 Mon Sep 17 00:00:00 2001
From: Soos3D <99700157+soos3d@users.noreply.github.com>
Date: Fri, 5 Dec 2025 10:50:45 -0300
Subject: [PATCH 3/4] feat: add tests and documentation for universal-account
package
---
account-kit/universal-account/README.md | 43 +-
.../examples/next-example/package.json | 8 +-
account-kit/universal-account/package.json | 2 +-
.../src/__tests__/client.test.ts | 384 ++++++++++++++++++
.../src/__tests__/constants.test.ts | 61 +++
.../universal-account/vitest.config.ts | 9 +
docs-site | 2 +-
docs/docs.yml | 2 +
docs/pages/third-party/universal-accounts.mdx | 279 +++++++++++++
yarn.lock | 8 +-
10 files changed, 784 insertions(+), 14 deletions(-)
create mode 100644 account-kit/universal-account/src/__tests__/client.test.ts
create mode 100644 account-kit/universal-account/src/__tests__/constants.test.ts
create mode 100644 account-kit/universal-account/vitest.config.ts
create mode 100644 docs/pages/third-party/universal-accounts.mdx
diff --git a/account-kit/universal-account/README.md b/account-kit/universal-account/README.md
index 390f0c8104..193078ba7a 100644
--- a/account-kit/universal-account/README.md
+++ b/account-kit/universal-account/README.md
@@ -631,13 +631,48 @@ Universal Account transactions may include:
Fees are automatically calculated and shown in the transaction preview via `feeQuotes`.
+## Testing
+
+### Run Tests
+
+```bash
+# Run all tests
+yarn test
+
+# Run tests once (CI mode)
+yarn test:run
+```
+
+### Test Coverage
+
+The package includes two types of testing:
+
+**Unit Tests** (`src/__tests__/`)
+
+- `constants.test.ts` - Verifies exported chain IDs, token types, and constants
+- `client.test.ts` - Tests the `UniversalAccountClient` wrapper logic using mocks
+
+Unit tests verify that:
+
+- Parameters are correctly passed to the underlying Particle SDK
+- Responses are correctly mapped to our TypeScript types
+- The client API behaves as expected
+
+**Integration Testing** (`examples/next-example/`)
+
+For full integration testing with real SDK calls, use the demo app:
+
+```bash
+cd examples/next-example
+yarn install
+yarn dev
+```
+
+This tests the complete flow: authentication → Universal Account initialization → balance fetching → transaction signing.
+
## Resources
- [Particle Network Documentation](https://developers.particle.network/universal-accounts/cha/overview)
- [Universal Accounts SDK Reference](https://developers.particle.network/universal-accounts/ua-reference/desktop/web)
- [Supported Chains & Primary Assets](https://developers.particle.network/universal-accounts/cha/chains)
-- [UniversalX Explorer](https://universalx.app)
-
-## License
-MIT
diff --git a/account-kit/universal-account/examples/next-example/package.json b/account-kit/universal-account/examples/next-example/package.json
index 24f33f8d76..667dd25b9b 100644
--- a/account-kit/universal-account/examples/next-example/package.json
+++ b/account-kit/universal-account/examples/next-example/package.json
@@ -12,11 +12,11 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"next": "15.5.2",
- "@account-kit/react": "^4.79.0",
- "@account-kit/infra": "^4.79.0",
- "@account-kit/core": "^4.79.0",
+ "@account-kit/react": "^4.80.0",
+ "@account-kit/infra": "^4.80.0",
+ "@account-kit/core": "^4.80.0",
"@tanstack/react-query": "^5.62.0",
- "@particle-network/universal-account-sdk": "^1.0.10",
+ "@particle-network/universal-account-sdk": "^1.0.12",
"viem": "^2.29.2"
},
"devDependencies": {
diff --git a/account-kit/universal-account/package.json b/account-kit/universal-account/package.json
index 5f0779888e..624fd05a36 100644
--- a/account-kit/universal-account/package.json
+++ b/account-kit/universal-account/package.json
@@ -41,7 +41,7 @@
"dependencies": {
"@account-kit/infra": "^4.80.0",
"@account-kit/signer": "^4.80.0",
- "@particle-network/universal-account-sdk": "^1.0.0"
+ "@particle-network/universal-account-sdk": "^1.0.12"
},
"peerDependencies": {
"react": ">=18.0.0",
diff --git a/account-kit/universal-account/src/__tests__/client.test.ts b/account-kit/universal-account/src/__tests__/client.test.ts
new file mode 100644
index 0000000000..d6299d720b
--- /dev/null
+++ b/account-kit/universal-account/src/__tests__/client.test.ts
@@ -0,0 +1,384 @@
+/**
+ * Unit tests for UniversalAccountClient
+ *
+ * These tests verify the wrapper logic around the Particle Network SDK.
+ * They use mocks to test that:
+ * - Parameters are correctly passed to the underlying SDK
+ * - Responses are correctly mapped to our types
+ * - The client API behaves as expected
+ *
+ * NOTE: These are unit tests, not integration tests. They do NOT verify
+ * actual Particle SDK behavior or network calls. For integration testing,
+ * use the demo app in examples/next-example which tests the full flow
+ * with real SDK calls.
+ */
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { UniversalAccountClient } from "../client.js";
+import type { Address } from "viem";
+
+// Mock the Particle SDK - we test our wrapper logic, not the SDK itself
+vi.mock("@particle-network/universal-account-sdk", () => ({
+ UniversalAccount: vi.fn(),
+}));
+
+describe("UniversalAccountClient", () => {
+ const mockOwnerAddress: Address =
+ "0x1234567890123456789012345678901234567890";
+ const mockSmartAccountAddress: Address =
+ "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd";
+ const mockSolanaAddress = "SoLaNaAddReSs123456789012345678901234567890123";
+
+ const mockSmartAccountOptions = {
+ name: "UniversalAccount",
+ version: "1.0.0",
+ ownerAddress: mockOwnerAddress,
+ smartAccountAddress: mockSmartAccountAddress,
+ solanaSmartAccountAddress: mockSolanaAddress,
+ senderAddress: mockSmartAccountAddress,
+ senderSolanaAddress: mockSolanaAddress,
+ };
+
+ const mockPrimaryAssets = {
+ assets: [
+ {
+ tokenType: "ETH",
+ price: 2000,
+ amount: "1.5",
+ amountInUSD: 3000,
+ chainAggregation: [
+ {
+ token: {
+ chainId: 1,
+ address: "0x0000000000000000000000000000000000000000",
+ decimals: 18,
+ },
+ amount: "1.0",
+ amountInUSD: 2000,
+ rawAmount: "1000000000000000000",
+ },
+ {
+ token: {
+ chainId: 42161,
+ address: "0x0000000000000000000000000000000000000000",
+ decimals: 18,
+ },
+ amount: "0.5",
+ amountInUSD: 1000,
+ rawAmount: "500000000000000000",
+ },
+ ],
+ },
+ ],
+ totalAmountInUSD: 3000,
+ };
+
+ let mockUa: any;
+ let client: UniversalAccountClient;
+
+ beforeEach(() => {
+ mockUa = {
+ getSmartAccountOptions: vi
+ .fn()
+ .mockResolvedValue(mockSmartAccountOptions),
+ getPrimaryAssets: vi.fn().mockResolvedValue(mockPrimaryAssets),
+ createTransferTransaction: vi.fn(),
+ createUniversalTransaction: vi.fn(),
+ createBuyTransaction: vi.fn(),
+ createSellTransaction: vi.fn(),
+ createConvertTransaction: vi.fn(),
+ sendTransaction: vi.fn(),
+ };
+
+ client = new UniversalAccountClient(mockUa, mockOwnerAddress);
+ });
+
+ describe("getOwnerAddress", () => {
+ it("returns the owner address", () => {
+ expect(client.getOwnerAddress()).toBe(mockOwnerAddress);
+ });
+ });
+
+ describe("getSmartAccountOptions", () => {
+ it("returns smart account options with correct types", async () => {
+ const options = await client.getSmartAccountOptions();
+
+ expect(options.name).toBe("UniversalAccount");
+ expect(options.version).toBe("1.0.0");
+ expect(options.ownerAddress).toBe(mockOwnerAddress);
+ expect(options.smartAccountAddress).toBe(mockSmartAccountAddress);
+ expect(options.solanaSmartAccountAddress).toBe(mockSolanaAddress);
+ });
+ });
+
+ describe("getAddress", () => {
+ it("returns the EVM smart account address", async () => {
+ const address = await client.getAddress();
+ expect(address).toBe(mockSmartAccountAddress);
+ });
+ });
+
+ describe("getSolanaAddress", () => {
+ it("returns the Solana smart account address", async () => {
+ const address = await client.getSolanaAddress();
+ expect(address).toBe(mockSolanaAddress);
+ });
+ });
+
+ describe("getPrimaryAssets", () => {
+ it("returns formatted primary assets", async () => {
+ const assets = await client.getPrimaryAssets();
+
+ expect(assets.totalAmountInUSD).toBe(3000);
+ expect(assets.assets).toHaveLength(1);
+ expect(assets.assets[0].tokenType).toBe("ETH");
+ expect(assets.assets[0].chainAggregation).toHaveLength(2);
+ });
+
+ it("correctly maps chain aggregation data", async () => {
+ const assets = await client.getPrimaryAssets();
+ const ethAsset = assets.assets[0];
+
+ expect(ethAsset.chainAggregation[0].chainId).toBe(1);
+ expect(ethAsset.chainAggregation[0].amount).toBe("1.0");
+ expect(ethAsset.chainAggregation[1].chainId).toBe(42161);
+ });
+ });
+
+ describe("createTransferTransaction", () => {
+ it("calls underlying SDK with correct params", async () => {
+ const mockTx = {
+ type: "universal",
+ mode: "mainnet",
+ sender: mockSmartAccountAddress,
+ receiver: "0x9999999999999999999999999999999999999999",
+ transactionId: "tx-123",
+ rootHash: "0xabcd1234",
+ smartAccountOptions: mockSmartAccountOptions,
+ feeQuotes: [],
+ };
+ mockUa.createTransferTransaction.mockResolvedValue(mockTx);
+
+ const params = {
+ token: {
+ chainId: 42161,
+ address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" as Address,
+ },
+ amount: "10",
+ receiver: "0x9999999999999999999999999999999999999999" as Address,
+ };
+
+ const tx = await client.createTransferTransaction(params);
+
+ expect(mockUa.createTransferTransaction).toHaveBeenCalledWith({
+ token: { chainId: 42161, address: params.token.address },
+ amount: "10",
+ receiver: params.receiver,
+ });
+ expect(tx.transactionId).toBe("tx-123");
+ expect(tx.rootHash).toBe("0xabcd1234");
+ });
+ });
+
+ describe("createUniversalTransaction", () => {
+ it("calls underlying SDK with correct params", async () => {
+ const mockTx = {
+ type: "universal",
+ mode: "mainnet",
+ sender: mockSmartAccountAddress,
+ receiver: mockSmartAccountAddress,
+ transactionId: "tx-456",
+ rootHash: "0xdef456",
+ smartAccountOptions: mockSmartAccountOptions,
+ feeQuotes: [],
+ };
+ mockUa.createUniversalTransaction.mockResolvedValue(mockTx);
+
+ const params = {
+ chainId: 8453,
+ expectTokens: [{ type: "ETH", amount: "0.01" }],
+ transactions: [
+ {
+ to: "0x1111111111111111111111111111111111111111" as Address,
+ data: "0x1234" as `0x${string}`,
+ },
+ ],
+ };
+
+ const tx = await client.createUniversalTransaction(params);
+
+ expect(mockUa.createUniversalTransaction).toHaveBeenCalledWith({
+ chainId: 8453,
+ expectTokens: [{ type: "ETH", amount: "0.01" }],
+ transactions: [
+ { to: params.transactions[0].to, data: "0x1234", value: undefined },
+ ],
+ });
+ expect(tx.transactionId).toBe("tx-456");
+ });
+ });
+
+ describe("sendTransaction", () => {
+ it("sends transaction with signature and returns result", async () => {
+ const mockResult = {
+ transactionId: "tx-789",
+ status: "pending",
+ mode: "mainnet",
+ sender: mockSmartAccountAddress,
+ receiver: "0x9999999999999999999999999999999999999999",
+ tag: "transfer",
+ created_at: "2024-01-01T00:00:00Z",
+ updated_at: "2024-01-01T00:00:00Z",
+ };
+ mockUa.sendTransaction.mockResolvedValue(mockResult);
+
+ const mockTx = { rootHash: "0xabc" } as any;
+ const signature = "0xsignature";
+
+ const result = await client.sendTransaction(mockTx, signature);
+
+ expect(mockUa.sendTransaction).toHaveBeenCalledWith(mockTx, signature);
+ expect(result.transactionId).toBe("tx-789");
+ expect(result.status).toBe("pending");
+ });
+ });
+
+ describe("getExplorerUrl", () => {
+ it("returns correct UniversalX explorer URL", () => {
+ const url = client.getExplorerUrl("tx-123");
+ expect(url).toBe("https://universalx.app/activity/details?id=tx-123");
+ });
+ });
+
+ describe("getUnderlyingAccount", () => {
+ it("returns the underlying Particle UA instance", () => {
+ const ua = client.getUnderlyingAccount();
+ expect(ua).toBe(mockUa);
+ });
+ });
+
+ describe("createBuyTransaction", () => {
+ it("calls underlying SDK with correct params", async () => {
+ const mockTx = {
+ type: "universal",
+ mode: "mainnet",
+ sender: mockSmartAccountAddress,
+ receiver: mockSmartAccountAddress,
+ transactionId: "tx-buy-123",
+ rootHash: "0xbuy123",
+ smartAccountOptions: mockSmartAccountOptions,
+ feeQuotes: [],
+ };
+ mockUa.createBuyTransaction.mockResolvedValue(mockTx);
+
+ const params = {
+ token: {
+ chainId: 42161,
+ address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" as Address,
+ },
+ amountInUSD: "10",
+ };
+
+ const tx = await client.createBuyTransaction(params);
+
+ expect(mockUa.createBuyTransaction).toHaveBeenCalledWith({
+ token: { chainId: 42161, address: params.token.address },
+ amountInUSD: "10",
+ });
+ expect(tx.transactionId).toBe("tx-buy-123");
+ });
+ });
+
+ describe("createSellTransaction", () => {
+ it("calls underlying SDK with correct params", async () => {
+ const mockTx = {
+ type: "universal",
+ mode: "mainnet",
+ sender: mockSmartAccountAddress,
+ receiver: mockSmartAccountAddress,
+ transactionId: "tx-sell-123",
+ rootHash: "0xsell123",
+ smartAccountOptions: mockSmartAccountOptions,
+ feeQuotes: [],
+ };
+ mockUa.createSellTransaction.mockResolvedValue(mockTx);
+
+ const params = {
+ token: {
+ chainId: 42161,
+ address: "0x912CE59144191C1204E64559FE8253a0e49E6548" as Address,
+ },
+ amount: "0.1",
+ };
+
+ const tx = await client.createSellTransaction(params);
+
+ expect(mockUa.createSellTransaction).toHaveBeenCalledWith({
+ token: { chainId: 42161, address: params.token.address },
+ amount: "0.1",
+ });
+ expect(tx.transactionId).toBe("tx-sell-123");
+ });
+ });
+
+ describe("createConvertTransaction", () => {
+ it("calls underlying SDK with correct params", async () => {
+ const mockTx = {
+ type: "universal",
+ mode: "mainnet",
+ sender: mockSmartAccountAddress,
+ receiver: mockSmartAccountAddress,
+ transactionId: "tx-convert-123",
+ rootHash: "0xconvert123",
+ smartAccountOptions: mockSmartAccountOptions,
+ feeQuotes: [],
+ };
+ mockUa.createConvertTransaction.mockResolvedValue(mockTx);
+
+ const params = {
+ expectToken: { type: "USDC", amount: "1" },
+ chainId: 42161,
+ };
+
+ const tx = await client.createConvertTransaction(params);
+
+ expect(mockUa.createConvertTransaction).toHaveBeenCalledWith({
+ expectToken: { type: "USDC", amount: "1" },
+ chainId: 42161,
+ });
+ expect(tx.transactionId).toBe("tx-convert-123");
+ });
+ });
+});
+
+describe("createUniversalAccountClient", () => {
+ it("creates a client with the Particle SDK", async () => {
+ const { UniversalAccount } = await import(
+ "@particle-network/universal-account-sdk"
+ );
+
+ const mockUaInstance = {
+ getSmartAccountOptions: vi.fn(),
+ };
+ (UniversalAccount as any).mockImplementation(() => mockUaInstance);
+
+ const { createUniversalAccountClient } = await import("../client.js");
+
+ const client = await createUniversalAccountClient({
+ ownerAddress: "0x1234567890123456789012345678901234567890",
+ config: {
+ projectId: "test-project-id",
+ projectClientKey: "test-client-key",
+ projectAppUuid: "test-app-uuid",
+ },
+ });
+
+ expect(UniversalAccount).toHaveBeenCalledWith({
+ projectId: "test-project-id",
+ projectClientKey: "test-client-key",
+ projectAppUuid: "test-app-uuid",
+ ownerAddress: "0x1234567890123456789012345678901234567890",
+ tradeConfig: undefined,
+ });
+ expect(client).toBeInstanceOf(UniversalAccountClient);
+ });
+});
diff --git a/account-kit/universal-account/src/__tests__/constants.test.ts b/account-kit/universal-account/src/__tests__/constants.test.ts
new file mode 100644
index 0000000000..201c8f3925
--- /dev/null
+++ b/account-kit/universal-account/src/__tests__/constants.test.ts
@@ -0,0 +1,61 @@
+import { describe, it, expect } from "vitest";
+import { CHAIN_ID, TOKEN_TYPE, NATIVE_TOKEN_ADDRESS } from "../constants.js";
+
+describe("constants", () => {
+ describe("CHAIN_ID", () => {
+ it("exports all EVM chain IDs with correct values", () => {
+ expect(CHAIN_ID.ETHEREUM).toBe(1);
+ expect(CHAIN_ID.BNB_CHAIN).toBe(56);
+ expect(CHAIN_ID.AVALANCHE).toBe(43114);
+ expect(CHAIN_ID.POLYGON).toBe(137);
+
+ expect(CHAIN_ID.BASE).toBe(8453);
+ expect(CHAIN_ID.ARBITRUM).toBe(42161);
+ expect(CHAIN_ID.OPTIMISM).toBe(10);
+ expect(CHAIN_ID.LINEA).toBe(59144);
+
+ expect(CHAIN_ID.MANTLE).toBe(5000);
+ expect(CHAIN_ID.MONAD).toBe(143);
+ expect(CHAIN_ID.PLASMA).toBe(9745);
+ expect(CHAIN_ID.X_LAYER).toBe(196);
+ expect(CHAIN_ID.HYPER_EVM).toBe(999);
+ expect(CHAIN_ID.BERACHAIN).toBe(80094);
+ expect(CHAIN_ID.SONIC).toBe(146);
+ expect(CHAIN_ID.MERLIN).toBe(4200);
+ });
+
+ it("exports correct non-EVM chain IDs", () => {
+ expect(CHAIN_ID.SOLANA).toBe(101);
+ });
+
+ it("exports exactly 17 chain IDs", () => {
+ // This ensures we don't accidentally add/remove chains without updating tests
+ expect(Object.keys(CHAIN_ID)).toHaveLength(17);
+ });
+ });
+
+ describe("TOKEN_TYPE", () => {
+ it("exports correct token types", () => {
+ expect(TOKEN_TYPE.ETH).toBe("ETH");
+ expect(TOKEN_TYPE.USDC).toBe("USDC");
+ expect(TOKEN_TYPE.USDT).toBe("USDT");
+ expect(TOKEN_TYPE.SOL).toBe("SOL");
+ expect(TOKEN_TYPE.BTC).toBe("BTC");
+ expect(TOKEN_TYPE.BNB).toBe("BNB");
+ expect(TOKEN_TYPE.MNT).toBe("MNT");
+ });
+ });
+
+ describe("NATIVE_TOKEN_ADDRESS", () => {
+ it("is the zero address", () => {
+ expect(NATIVE_TOKEN_ADDRESS).toBe(
+ "0x0000000000000000000000000000000000000000",
+ );
+ });
+
+ it("has correct length for an Ethereum address", () => {
+ expect(NATIVE_TOKEN_ADDRESS).toHaveLength(42);
+ expect(NATIVE_TOKEN_ADDRESS.startsWith("0x")).toBe(true);
+ });
+ });
+});
diff --git a/account-kit/universal-account/vitest.config.ts b/account-kit/universal-account/vitest.config.ts
new file mode 100644
index 0000000000..0ce18d0b6a
--- /dev/null
+++ b/account-kit/universal-account/vitest.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ name: "account-kit/universal-account",
+ globals: true,
+ environment: "node",
+ },
+});
diff --git a/docs-site b/docs-site
index 256822886b..0aaced699f 160000
--- a/docs-site
+++ b/docs-site
@@ -1 +1 @@
-Subproject commit 256822886b4c07e8bac4bb97a5ed9c0b97d49ced
+Subproject commit 0aaced699f1436c7072e3284bb353a7a7033cee1
diff --git a/docs/docs.yml b/docs/docs.yml
index 7216334518..cf53cf1a93 100644
--- a/docs/docs.yml
+++ b/docs/docs.yml
@@ -124,6 +124,8 @@ navigation:
path: wallets/pages/transactions/swap-tokens/index.mdx
- page: "[NEW] Cross-chain swaps"
path: wallets/pages/transactions/cross-chain-swap-tokens/index.mdx
+ - page: "[NEW] Universal Accounts"
+ path: wallets/pages/third-party/universal-accounts.mdx
- page: Send parallel transactions
path: wallets/pages/transactions/send-parallel-transactions/index.mdx
- page: Retry transactions
diff --git a/docs/pages/third-party/universal-accounts.mdx b/docs/pages/third-party/universal-accounts.mdx
new file mode 100644
index 0000000000..78695a6cc5
--- /dev/null
+++ b/docs/pages/third-party/universal-accounts.mdx
@@ -0,0 +1,279 @@
+---
+title: Universal Accounts
+description: Enable chain abstraction with Particle Network Universal Accounts
+slug: wallets/third-party/universal-accounts
+---
+
+Universal Accounts provide a single account, balance, and interaction point across all supported chains (EVM + Solana). This integration brings [Particle Network's Universal Accounts](https://developers.particle.network/universal-accounts/cha/overview) into Smart Wallets.
+
+## Key features
+
+- **Unified balance**: View and use assets across all chains as a single balance
+- **Cross-chain transactions**: Send transactions to any chain without manual bridging
+- **Universal gas**: Pay gas fees with any supported token
+- **Solana support**: Interact with both EVM chains and Solana
+
+## Prerequisites
+
+You need credentials from both dashboards:
+
+**Alchemy** (for authentication):
+
+- Get your API key from [Alchemy Dashboard](https://dashboard.alchemy.com)
+
+**Particle Network** (for Universal Accounts):
+
+1. Sign up at [Particle Dashboard](https://dashboard.particle.network/)
+2. Create a project and web application
+3. Copy your **Project ID**, **Client Key**, and **App ID**
+
+## Installation
+
+```bash
+npm install @account-kit/universal-account
+# or
+yarn add @account-kit/universal-account
+```
+
+## Architecture
+
+When integrating Smart Wallets with Universal Accounts, understand the different account types:
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ Smart Wallets (Alchemy) │
+├─────────────────────────────────────────────────────────────────┤
+│ useUser() → user.address = EOA (Externally Owned Account) │
+│ useAccount() → address = SCA (Smart Contract Account) │
+│ useSigner() → Signs messages with the EOA │
+└─────────────────────────────────────────────────────────────────┘
+ │
+ │ EOA address (user.address)
+ ▼
+┌─────────────────────────────────────────────────────────────────┐
+│ Universal Accounts │
+├─────────────────────────────────────────────────────────────────┤
+│ Owner: EOA from Alchemy (user.address) │
+│ Creates: Multi-chain smart accounts (EVM + Solana) │
+│ Provides: Unified balance, cross-chain transactions │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+
+ Use the EOA address from `useUser().address` for Universal Accounts, not the
+ smart account address from `useAccount()`. These are different account types.
+
+
+## Quick start
+
+Find a full demo app in the [examples](https://github.com/alchemyplatform/aa-sdk/tree/main/account-kit/universal-account/examples) directory.
+
+### 1. Set up providers
+
+Wrap your app with both `AlchemyAccountProvider` and `UniversalAccountProvider`:
+
+```tsx twoslash
+// @noErrors
+// providers.tsx
+"use client";
+
+import { AlchemyAccountProvider } from "@account-kit/react";
+import { UniversalAccountProvider } from "@account-kit/universal-account";
+import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
+
+const queryClient = new QueryClient();
+
+// Your Alchemy config
+const config = {
+ // ... your alchemy config
+};
+
+const universalAccountConfig = {
+ projectId: process.env.NEXT_PUBLIC_PARTICLE_PROJECT_ID!,
+ clientKey: process.env.NEXT_PUBLIC_PARTICLE_CLIENT_KEY!,
+ appId: process.env.NEXT_PUBLIC_PARTICLE_APP_ID!,
+};
+
+export function Providers({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+```
+
+### 2. Initialize Universal Account
+
+Use the `useUniversalAccount` hook with the EOA address:
+
+```tsx twoslash
+// @noErrors
+import { useUser } from "@account-kit/react";
+import {
+ useUniversalAccount,
+ useUnifiedBalance,
+} from "@account-kit/universal-account";
+
+function Dashboard() {
+ const user = useUser();
+ const eoaAddress = user?.address as `0x${string}` | undefined;
+
+ // Universal Account auto-initializes with the EOA address
+ const {
+ address, // Universal Account EVM address
+ solanaAddress, // Universal Account Solana address
+ isReady,
+ isInitializing,
+ error,
+ } = useUniversalAccount(eoaAddress);
+
+ // Get unified balance across all chains
+ const { totalBalanceUSD, assets, isLoading, refetch } = useUnifiedBalance();
+
+ if (isInitializing) return Initializing Universal Account... ;
+ if (error) return Error: {error.message} ;
+ if (!isReady) return null;
+
+ return (
+
+ Universal Account
+ EVM Address: {address}
+ Solana Address: {solanaAddress}
+ Unified Balance: ${totalBalanceUSD?.toFixed(2)}
+
+ );
+}
+```
+
+### 3. Send transactions
+
+Use `useSendTransaction` to send cross-chain transactions:
+
+```tsx twoslash
+// @noErrors
+import { useSigner } from "@account-kit/react";
+import { useSendTransaction } from "@account-kit/universal-account";
+import { toBytes, encodeFunctionData } from "viem";
+
+function MintNFT() {
+ const signer = useSigner();
+
+ const { sendUniversal, isLoading, error, lastResult } = useSendTransaction({
+ signMessage: async (message: string) => {
+ if (!signer) throw new Error("Signer not available");
+ return await signer.signMessage({ raw: toBytes(message) });
+ },
+ });
+
+ const handleMint = async () => {
+ const NFT_CONTRACT = "0xdea7bF60E53CD578e3526F36eC431795f7EEbFe6";
+ const AVALANCHE_CHAIN_ID = 43114;
+
+ const mintData = encodeFunctionData({
+ abi: [{ type: "function", name: "mint", inputs: [], outputs: [] }],
+ functionName: "mint",
+ });
+
+ const result = await sendUniversal({
+ chainId: AVALANCHE_CHAIN_ID,
+ expectTokens: [],
+ transactions: [{ to: NFT_CONTRACT, data: mintData }],
+ });
+
+ console.log("Transaction ID:", result.transactionId);
+ };
+
+ return (
+
+ );
+}
+```
+
+## Transaction types
+
+The `useSendTransaction` hook provides methods for different transaction types:
+
+### Transfer tokens
+
+```tsx twoslash
+// @noErrors
+const { sendTransfer } = useSendTransaction({ signMessage });
+
+await sendTransfer({
+ token: {
+ chainId: 42161, // Arbitrum
+ address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT
+ },
+ amount: "10",
+ receiver: "0x...",
+});
+```
+
+### Buy tokens
+
+Convert USD value from your primary assets into a target token:
+
+```tsx twoslash
+// @noErrors
+const { sendBuy } = useSendTransaction({ signMessage });
+
+await sendBuy({
+ token: {
+ chainId: 42161,
+ address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
+ },
+ amountInUSD: "10", // Buy $10 worth
+});
+```
+
+### Sell tokens
+
+Sell a token back into primary assets:
+
+```tsx twoslash
+// @noErrors
+const { sendSell } = useSendTransaction({ signMessage });
+
+await sendSell({
+ token: {
+ chainId: 42161,
+ address: "0x912CE59144191C1204E64559FE8253a0e49E6548", // ARB
+ },
+ amount: "0.1",
+});
+```
+
+## Supported chains
+
+Universal Accounts support 15+ EVM chains and Solana:
+
+- Ethereum, Base, Arbitrum, Optimism, Polygon
+- Avalanche, BNB Chain, Linea, Berachain
+- Solana
+- And more...
+
+See the [full list of supported chains](https://developers.particle.network/universal-accounts/cha/chains).
+
+## Fees
+
+Universal Account transactions may include:
+
+- **Gas fees**: Standard network fees on the destination chain
+- **LP fee**: 0.2% for cross-chain transactions
+- **Service fee**: 1% on transaction volume
+
+Fees are automatically calculated and shown in the transaction preview.
+
+## Resources
+
+- [Particle Network Documentation](https://developers.particle.network/universal-accounts/cha/overview)
+- [Universal Accounts SDK Reference](https://developers.particle.network/universal-accounts/ua-reference/desktop/web)
+- [Supported Chains & Primary Assets](https://developers.particle.network/universal-accounts/cha/chains)
+- [UniversalX Explorer](https://universalx.app)
diff --git a/yarn.lock b/yarn.lock
index 28a7b28de2..c27a2a4e24 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4875,10 +4875,10 @@
dependencies:
"@particle-network/auth" "^1.3.1"
-"@particle-network/universal-account-sdk@^1.0.0":
- version "1.0.10"
- resolved "https://registry.yarnpkg.com/@particle-network/universal-account-sdk/-/universal-account-sdk-1.0.10.tgz#205ec4760fb0ebf87fca3cd3b07a249e10621cc9"
- integrity sha512-xd1yaEBpeMl9QWRWq1h2yxwKFEae4pYPVsomnpLqZmY1XSqKRRCspdmVeGFgKHz6SaoowWTrmqkh+daP6SjAGQ==
+"@particle-network/universal-account-sdk@^1.0.12":
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/@particle-network/universal-account-sdk/-/universal-account-sdk-1.0.12.tgz#fbb1173629477ec34456624d53cd2095c86f7503"
+ integrity sha512-H9ws7mjpXx6K0MxbmQ8OK+j9Gn836Ns5ExQP9Bt8k6cmPKIgQM6IK6aR5bifKZaMUTthHFZpWU3jGRwa4wypyg==
dependencies:
"@coral-xyz/anchor" "^0.30.1"
"@noble/hashes" "^1.7.1"
From 79c941339837986cba23dfbd76fad1470ecbda87 Mon Sep 17 00:00:00 2001
From: Soos3D <99700157+soos3d@users.noreply.github.com>
Date: Fri, 5 Dec 2025 10:50:45 -0300
Subject: [PATCH 4/4] feat: add tests and documentation for universal-account
package
---
account-kit/universal-account/README.md | 44 +-
.../app/components/UniversalAccountDemo.tsx | 2 +
.../examples/next-example/package.json | 8 +-
account-kit/universal-account/package.json | 2 +-
.../src/__tests__/client.test.ts | 384 ++++++++++++++++++
.../src/__tests__/constants.test.ts | 61 +++
.../universal-account/vitest.config.ts | 9 +
docs-site | 2 +-
docs/docs.yml | 2 +
docs/pages/third-party/universal-accounts.mdx | 279 +++++++++++++
yarn.lock | 8 +-
11 files changed, 786 insertions(+), 15 deletions(-)
create mode 100644 account-kit/universal-account/src/__tests__/client.test.ts
create mode 100644 account-kit/universal-account/src/__tests__/constants.test.ts
create mode 100644 account-kit/universal-account/vitest.config.ts
create mode 100644 docs/pages/third-party/universal-accounts.mdx
diff --git a/account-kit/universal-account/README.md b/account-kit/universal-account/README.md
index 390f0c8104..af4000a48a 100644
--- a/account-kit/universal-account/README.md
+++ b/account-kit/universal-account/README.md
@@ -631,13 +631,47 @@ Universal Account transactions may include:
Fees are automatically calculated and shown in the transaction preview via `feeQuotes`.
+## Testing
+
+### Run Tests
+
+```bash
+# Run all tests
+yarn test
+
+# Run tests once (CI mode)
+yarn test:run
+```
+
+### Test Coverage
+
+The package includes two types of testing:
+
+**Unit Tests** (`src/__tests__/`)
+
+- `constants.test.ts` - Verifies exported chain IDs, token types, and constants
+- `client.test.ts` - Tests the `UniversalAccountClient` wrapper logic using mocks
+
+Unit tests verify that:
+
+- Parameters are correctly passed to the underlying Particle SDK
+- Responses are correctly mapped to our TypeScript types
+- The client API behaves as expected
+
+**Integration Testing** (`examples/next-example/`)
+
+For full integration testing with real SDK calls, use the demo app:
+
+```bash
+cd examples/next-example
+yarn install
+yarn dev
+```
+
+This tests the complete flow: authentication → Universal Account initialization → balance fetching → transaction signing.
+
## Resources
- [Particle Network Documentation](https://developers.particle.network/universal-accounts/cha/overview)
- [Universal Accounts SDK Reference](https://developers.particle.network/universal-accounts/ua-reference/desktop/web)
- [Supported Chains & Primary Assets](https://developers.particle.network/universal-accounts/cha/chains)
-- [UniversalX Explorer](https://universalx.app)
-
-## License
-
-MIT
diff --git a/account-kit/universal-account/examples/next-example/app/components/UniversalAccountDemo.tsx b/account-kit/universal-account/examples/next-example/app/components/UniversalAccountDemo.tsx
index ea79b7eb69..09c4c24390 100644
--- a/account-kit/universal-account/examples/next-example/app/components/UniversalAccountDemo.tsx
+++ b/account-kit/universal-account/examples/next-example/app/components/UniversalAccountDemo.tsx
@@ -96,6 +96,8 @@ export function UniversalAccountDemo({
// sendBuy, // For buying tokens
// sendSell, // For selling tokens
// sendConvert, // For converting assets
+ // Check Particle docs for more details
+ // https://developers.particle.network/universal-accounts/ua-reference/desktop/web#sending-a-transfer-transaction
isLoading: isSending,
} = useSendTransaction({
// This function signs the transaction hash with the Alchemy signer
diff --git a/account-kit/universal-account/examples/next-example/package.json b/account-kit/universal-account/examples/next-example/package.json
index 24f33f8d76..667dd25b9b 100644
--- a/account-kit/universal-account/examples/next-example/package.json
+++ b/account-kit/universal-account/examples/next-example/package.json
@@ -12,11 +12,11 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"next": "15.5.2",
- "@account-kit/react": "^4.79.0",
- "@account-kit/infra": "^4.79.0",
- "@account-kit/core": "^4.79.0",
+ "@account-kit/react": "^4.80.0",
+ "@account-kit/infra": "^4.80.0",
+ "@account-kit/core": "^4.80.0",
"@tanstack/react-query": "^5.62.0",
- "@particle-network/universal-account-sdk": "^1.0.10",
+ "@particle-network/universal-account-sdk": "^1.0.12",
"viem": "^2.29.2"
},
"devDependencies": {
diff --git a/account-kit/universal-account/package.json b/account-kit/universal-account/package.json
index 5f0779888e..624fd05a36 100644
--- a/account-kit/universal-account/package.json
+++ b/account-kit/universal-account/package.json
@@ -41,7 +41,7 @@
"dependencies": {
"@account-kit/infra": "^4.80.0",
"@account-kit/signer": "^4.80.0",
- "@particle-network/universal-account-sdk": "^1.0.0"
+ "@particle-network/universal-account-sdk": "^1.0.12"
},
"peerDependencies": {
"react": ">=18.0.0",
diff --git a/account-kit/universal-account/src/__tests__/client.test.ts b/account-kit/universal-account/src/__tests__/client.test.ts
new file mode 100644
index 0000000000..d6299d720b
--- /dev/null
+++ b/account-kit/universal-account/src/__tests__/client.test.ts
@@ -0,0 +1,384 @@
+/**
+ * Unit tests for UniversalAccountClient
+ *
+ * These tests verify the wrapper logic around the Particle Network SDK.
+ * They use mocks to test that:
+ * - Parameters are correctly passed to the underlying SDK
+ * - Responses are correctly mapped to our types
+ * - The client API behaves as expected
+ *
+ * NOTE: These are unit tests, not integration tests. They do NOT verify
+ * actual Particle SDK behavior or network calls. For integration testing,
+ * use the demo app in examples/next-example which tests the full flow
+ * with real SDK calls.
+ */
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { UniversalAccountClient } from "../client.js";
+import type { Address } from "viem";
+
+// Mock the Particle SDK - we test our wrapper logic, not the SDK itself
+vi.mock("@particle-network/universal-account-sdk", () => ({
+ UniversalAccount: vi.fn(),
+}));
+
+describe("UniversalAccountClient", () => {
+ const mockOwnerAddress: Address =
+ "0x1234567890123456789012345678901234567890";
+ const mockSmartAccountAddress: Address =
+ "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd";
+ const mockSolanaAddress = "SoLaNaAddReSs123456789012345678901234567890123";
+
+ const mockSmartAccountOptions = {
+ name: "UniversalAccount",
+ version: "1.0.0",
+ ownerAddress: mockOwnerAddress,
+ smartAccountAddress: mockSmartAccountAddress,
+ solanaSmartAccountAddress: mockSolanaAddress,
+ senderAddress: mockSmartAccountAddress,
+ senderSolanaAddress: mockSolanaAddress,
+ };
+
+ const mockPrimaryAssets = {
+ assets: [
+ {
+ tokenType: "ETH",
+ price: 2000,
+ amount: "1.5",
+ amountInUSD: 3000,
+ chainAggregation: [
+ {
+ token: {
+ chainId: 1,
+ address: "0x0000000000000000000000000000000000000000",
+ decimals: 18,
+ },
+ amount: "1.0",
+ amountInUSD: 2000,
+ rawAmount: "1000000000000000000",
+ },
+ {
+ token: {
+ chainId: 42161,
+ address: "0x0000000000000000000000000000000000000000",
+ decimals: 18,
+ },
+ amount: "0.5",
+ amountInUSD: 1000,
+ rawAmount: "500000000000000000",
+ },
+ ],
+ },
+ ],
+ totalAmountInUSD: 3000,
+ };
+
+ let mockUa: any;
+ let client: UniversalAccountClient;
+
+ beforeEach(() => {
+ mockUa = {
+ getSmartAccountOptions: vi
+ .fn()
+ .mockResolvedValue(mockSmartAccountOptions),
+ getPrimaryAssets: vi.fn().mockResolvedValue(mockPrimaryAssets),
+ createTransferTransaction: vi.fn(),
+ createUniversalTransaction: vi.fn(),
+ createBuyTransaction: vi.fn(),
+ createSellTransaction: vi.fn(),
+ createConvertTransaction: vi.fn(),
+ sendTransaction: vi.fn(),
+ };
+
+ client = new UniversalAccountClient(mockUa, mockOwnerAddress);
+ });
+
+ describe("getOwnerAddress", () => {
+ it("returns the owner address", () => {
+ expect(client.getOwnerAddress()).toBe(mockOwnerAddress);
+ });
+ });
+
+ describe("getSmartAccountOptions", () => {
+ it("returns smart account options with correct types", async () => {
+ const options = await client.getSmartAccountOptions();
+
+ expect(options.name).toBe("UniversalAccount");
+ expect(options.version).toBe("1.0.0");
+ expect(options.ownerAddress).toBe(mockOwnerAddress);
+ expect(options.smartAccountAddress).toBe(mockSmartAccountAddress);
+ expect(options.solanaSmartAccountAddress).toBe(mockSolanaAddress);
+ });
+ });
+
+ describe("getAddress", () => {
+ it("returns the EVM smart account address", async () => {
+ const address = await client.getAddress();
+ expect(address).toBe(mockSmartAccountAddress);
+ });
+ });
+
+ describe("getSolanaAddress", () => {
+ it("returns the Solana smart account address", async () => {
+ const address = await client.getSolanaAddress();
+ expect(address).toBe(mockSolanaAddress);
+ });
+ });
+
+ describe("getPrimaryAssets", () => {
+ it("returns formatted primary assets", async () => {
+ const assets = await client.getPrimaryAssets();
+
+ expect(assets.totalAmountInUSD).toBe(3000);
+ expect(assets.assets).toHaveLength(1);
+ expect(assets.assets[0].tokenType).toBe("ETH");
+ expect(assets.assets[0].chainAggregation).toHaveLength(2);
+ });
+
+ it("correctly maps chain aggregation data", async () => {
+ const assets = await client.getPrimaryAssets();
+ const ethAsset = assets.assets[0];
+
+ expect(ethAsset.chainAggregation[0].chainId).toBe(1);
+ expect(ethAsset.chainAggregation[0].amount).toBe("1.0");
+ expect(ethAsset.chainAggregation[1].chainId).toBe(42161);
+ });
+ });
+
+ describe("createTransferTransaction", () => {
+ it("calls underlying SDK with correct params", async () => {
+ const mockTx = {
+ type: "universal",
+ mode: "mainnet",
+ sender: mockSmartAccountAddress,
+ receiver: "0x9999999999999999999999999999999999999999",
+ transactionId: "tx-123",
+ rootHash: "0xabcd1234",
+ smartAccountOptions: mockSmartAccountOptions,
+ feeQuotes: [],
+ };
+ mockUa.createTransferTransaction.mockResolvedValue(mockTx);
+
+ const params = {
+ token: {
+ chainId: 42161,
+ address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" as Address,
+ },
+ amount: "10",
+ receiver: "0x9999999999999999999999999999999999999999" as Address,
+ };
+
+ const tx = await client.createTransferTransaction(params);
+
+ expect(mockUa.createTransferTransaction).toHaveBeenCalledWith({
+ token: { chainId: 42161, address: params.token.address },
+ amount: "10",
+ receiver: params.receiver,
+ });
+ expect(tx.transactionId).toBe("tx-123");
+ expect(tx.rootHash).toBe("0xabcd1234");
+ });
+ });
+
+ describe("createUniversalTransaction", () => {
+ it("calls underlying SDK with correct params", async () => {
+ const mockTx = {
+ type: "universal",
+ mode: "mainnet",
+ sender: mockSmartAccountAddress,
+ receiver: mockSmartAccountAddress,
+ transactionId: "tx-456",
+ rootHash: "0xdef456",
+ smartAccountOptions: mockSmartAccountOptions,
+ feeQuotes: [],
+ };
+ mockUa.createUniversalTransaction.mockResolvedValue(mockTx);
+
+ const params = {
+ chainId: 8453,
+ expectTokens: [{ type: "ETH", amount: "0.01" }],
+ transactions: [
+ {
+ to: "0x1111111111111111111111111111111111111111" as Address,
+ data: "0x1234" as `0x${string}`,
+ },
+ ],
+ };
+
+ const tx = await client.createUniversalTransaction(params);
+
+ expect(mockUa.createUniversalTransaction).toHaveBeenCalledWith({
+ chainId: 8453,
+ expectTokens: [{ type: "ETH", amount: "0.01" }],
+ transactions: [
+ { to: params.transactions[0].to, data: "0x1234", value: undefined },
+ ],
+ });
+ expect(tx.transactionId).toBe("tx-456");
+ });
+ });
+
+ describe("sendTransaction", () => {
+ it("sends transaction with signature and returns result", async () => {
+ const mockResult = {
+ transactionId: "tx-789",
+ status: "pending",
+ mode: "mainnet",
+ sender: mockSmartAccountAddress,
+ receiver: "0x9999999999999999999999999999999999999999",
+ tag: "transfer",
+ created_at: "2024-01-01T00:00:00Z",
+ updated_at: "2024-01-01T00:00:00Z",
+ };
+ mockUa.sendTransaction.mockResolvedValue(mockResult);
+
+ const mockTx = { rootHash: "0xabc" } as any;
+ const signature = "0xsignature";
+
+ const result = await client.sendTransaction(mockTx, signature);
+
+ expect(mockUa.sendTransaction).toHaveBeenCalledWith(mockTx, signature);
+ expect(result.transactionId).toBe("tx-789");
+ expect(result.status).toBe("pending");
+ });
+ });
+
+ describe("getExplorerUrl", () => {
+ it("returns correct UniversalX explorer URL", () => {
+ const url = client.getExplorerUrl("tx-123");
+ expect(url).toBe("https://universalx.app/activity/details?id=tx-123");
+ });
+ });
+
+ describe("getUnderlyingAccount", () => {
+ it("returns the underlying Particle UA instance", () => {
+ const ua = client.getUnderlyingAccount();
+ expect(ua).toBe(mockUa);
+ });
+ });
+
+ describe("createBuyTransaction", () => {
+ it("calls underlying SDK with correct params", async () => {
+ const mockTx = {
+ type: "universal",
+ mode: "mainnet",
+ sender: mockSmartAccountAddress,
+ receiver: mockSmartAccountAddress,
+ transactionId: "tx-buy-123",
+ rootHash: "0xbuy123",
+ smartAccountOptions: mockSmartAccountOptions,
+ feeQuotes: [],
+ };
+ mockUa.createBuyTransaction.mockResolvedValue(mockTx);
+
+ const params = {
+ token: {
+ chainId: 42161,
+ address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" as Address,
+ },
+ amountInUSD: "10",
+ };
+
+ const tx = await client.createBuyTransaction(params);
+
+ expect(mockUa.createBuyTransaction).toHaveBeenCalledWith({
+ token: { chainId: 42161, address: params.token.address },
+ amountInUSD: "10",
+ });
+ expect(tx.transactionId).toBe("tx-buy-123");
+ });
+ });
+
+ describe("createSellTransaction", () => {
+ it("calls underlying SDK with correct params", async () => {
+ const mockTx = {
+ type: "universal",
+ mode: "mainnet",
+ sender: mockSmartAccountAddress,
+ receiver: mockSmartAccountAddress,
+ transactionId: "tx-sell-123",
+ rootHash: "0xsell123",
+ smartAccountOptions: mockSmartAccountOptions,
+ feeQuotes: [],
+ };
+ mockUa.createSellTransaction.mockResolvedValue(mockTx);
+
+ const params = {
+ token: {
+ chainId: 42161,
+ address: "0x912CE59144191C1204E64559FE8253a0e49E6548" as Address,
+ },
+ amount: "0.1",
+ };
+
+ const tx = await client.createSellTransaction(params);
+
+ expect(mockUa.createSellTransaction).toHaveBeenCalledWith({
+ token: { chainId: 42161, address: params.token.address },
+ amount: "0.1",
+ });
+ expect(tx.transactionId).toBe("tx-sell-123");
+ });
+ });
+
+ describe("createConvertTransaction", () => {
+ it("calls underlying SDK with correct params", async () => {
+ const mockTx = {
+ type: "universal",
+ mode: "mainnet",
+ sender: mockSmartAccountAddress,
+ receiver: mockSmartAccountAddress,
+ transactionId: "tx-convert-123",
+ rootHash: "0xconvert123",
+ smartAccountOptions: mockSmartAccountOptions,
+ feeQuotes: [],
+ };
+ mockUa.createConvertTransaction.mockResolvedValue(mockTx);
+
+ const params = {
+ expectToken: { type: "USDC", amount: "1" },
+ chainId: 42161,
+ };
+
+ const tx = await client.createConvertTransaction(params);
+
+ expect(mockUa.createConvertTransaction).toHaveBeenCalledWith({
+ expectToken: { type: "USDC", amount: "1" },
+ chainId: 42161,
+ });
+ expect(tx.transactionId).toBe("tx-convert-123");
+ });
+ });
+});
+
+describe("createUniversalAccountClient", () => {
+ it("creates a client with the Particle SDK", async () => {
+ const { UniversalAccount } = await import(
+ "@particle-network/universal-account-sdk"
+ );
+
+ const mockUaInstance = {
+ getSmartAccountOptions: vi.fn(),
+ };
+ (UniversalAccount as any).mockImplementation(() => mockUaInstance);
+
+ const { createUniversalAccountClient } = await import("../client.js");
+
+ const client = await createUniversalAccountClient({
+ ownerAddress: "0x1234567890123456789012345678901234567890",
+ config: {
+ projectId: "test-project-id",
+ projectClientKey: "test-client-key",
+ projectAppUuid: "test-app-uuid",
+ },
+ });
+
+ expect(UniversalAccount).toHaveBeenCalledWith({
+ projectId: "test-project-id",
+ projectClientKey: "test-client-key",
+ projectAppUuid: "test-app-uuid",
+ ownerAddress: "0x1234567890123456789012345678901234567890",
+ tradeConfig: undefined,
+ });
+ expect(client).toBeInstanceOf(UniversalAccountClient);
+ });
+});
diff --git a/account-kit/universal-account/src/__tests__/constants.test.ts b/account-kit/universal-account/src/__tests__/constants.test.ts
new file mode 100644
index 0000000000..201c8f3925
--- /dev/null
+++ b/account-kit/universal-account/src/__tests__/constants.test.ts
@@ -0,0 +1,61 @@
+import { describe, it, expect } from "vitest";
+import { CHAIN_ID, TOKEN_TYPE, NATIVE_TOKEN_ADDRESS } from "../constants.js";
+
+describe("constants", () => {
+ describe("CHAIN_ID", () => {
+ it("exports all EVM chain IDs with correct values", () => {
+ expect(CHAIN_ID.ETHEREUM).toBe(1);
+ expect(CHAIN_ID.BNB_CHAIN).toBe(56);
+ expect(CHAIN_ID.AVALANCHE).toBe(43114);
+ expect(CHAIN_ID.POLYGON).toBe(137);
+
+ expect(CHAIN_ID.BASE).toBe(8453);
+ expect(CHAIN_ID.ARBITRUM).toBe(42161);
+ expect(CHAIN_ID.OPTIMISM).toBe(10);
+ expect(CHAIN_ID.LINEA).toBe(59144);
+
+ expect(CHAIN_ID.MANTLE).toBe(5000);
+ expect(CHAIN_ID.MONAD).toBe(143);
+ expect(CHAIN_ID.PLASMA).toBe(9745);
+ expect(CHAIN_ID.X_LAYER).toBe(196);
+ expect(CHAIN_ID.HYPER_EVM).toBe(999);
+ expect(CHAIN_ID.BERACHAIN).toBe(80094);
+ expect(CHAIN_ID.SONIC).toBe(146);
+ expect(CHAIN_ID.MERLIN).toBe(4200);
+ });
+
+ it("exports correct non-EVM chain IDs", () => {
+ expect(CHAIN_ID.SOLANA).toBe(101);
+ });
+
+ it("exports exactly 17 chain IDs", () => {
+ // This ensures we don't accidentally add/remove chains without updating tests
+ expect(Object.keys(CHAIN_ID)).toHaveLength(17);
+ });
+ });
+
+ describe("TOKEN_TYPE", () => {
+ it("exports correct token types", () => {
+ expect(TOKEN_TYPE.ETH).toBe("ETH");
+ expect(TOKEN_TYPE.USDC).toBe("USDC");
+ expect(TOKEN_TYPE.USDT).toBe("USDT");
+ expect(TOKEN_TYPE.SOL).toBe("SOL");
+ expect(TOKEN_TYPE.BTC).toBe("BTC");
+ expect(TOKEN_TYPE.BNB).toBe("BNB");
+ expect(TOKEN_TYPE.MNT).toBe("MNT");
+ });
+ });
+
+ describe("NATIVE_TOKEN_ADDRESS", () => {
+ it("is the zero address", () => {
+ expect(NATIVE_TOKEN_ADDRESS).toBe(
+ "0x0000000000000000000000000000000000000000",
+ );
+ });
+
+ it("has correct length for an Ethereum address", () => {
+ expect(NATIVE_TOKEN_ADDRESS).toHaveLength(42);
+ expect(NATIVE_TOKEN_ADDRESS.startsWith("0x")).toBe(true);
+ });
+ });
+});
diff --git a/account-kit/universal-account/vitest.config.ts b/account-kit/universal-account/vitest.config.ts
new file mode 100644
index 0000000000..0ce18d0b6a
--- /dev/null
+++ b/account-kit/universal-account/vitest.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ name: "account-kit/universal-account",
+ globals: true,
+ environment: "node",
+ },
+});
diff --git a/docs-site b/docs-site
index 256822886b..0aaced699f 160000
--- a/docs-site
+++ b/docs-site
@@ -1 +1 @@
-Subproject commit 256822886b4c07e8bac4bb97a5ed9c0b97d49ced
+Subproject commit 0aaced699f1436c7072e3284bb353a7a7033cee1
diff --git a/docs/docs.yml b/docs/docs.yml
index 7216334518..cf53cf1a93 100644
--- a/docs/docs.yml
+++ b/docs/docs.yml
@@ -124,6 +124,8 @@ navigation:
path: wallets/pages/transactions/swap-tokens/index.mdx
- page: "[NEW] Cross-chain swaps"
path: wallets/pages/transactions/cross-chain-swap-tokens/index.mdx
+ - page: "[NEW] Universal Accounts"
+ path: wallets/pages/third-party/universal-accounts.mdx
- page: Send parallel transactions
path: wallets/pages/transactions/send-parallel-transactions/index.mdx
- page: Retry transactions
diff --git a/docs/pages/third-party/universal-accounts.mdx b/docs/pages/third-party/universal-accounts.mdx
new file mode 100644
index 0000000000..78695a6cc5
--- /dev/null
+++ b/docs/pages/third-party/universal-accounts.mdx
@@ -0,0 +1,279 @@
+---
+title: Universal Accounts
+description: Enable chain abstraction with Particle Network Universal Accounts
+slug: wallets/third-party/universal-accounts
+---
+
+Universal Accounts provide a single account, balance, and interaction point across all supported chains (EVM + Solana). This integration brings [Particle Network's Universal Accounts](https://developers.particle.network/universal-accounts/cha/overview) into Smart Wallets.
+
+## Key features
+
+- **Unified balance**: View and use assets across all chains as a single balance
+- **Cross-chain transactions**: Send transactions to any chain without manual bridging
+- **Universal gas**: Pay gas fees with any supported token
+- **Solana support**: Interact with both EVM chains and Solana
+
+## Prerequisites
+
+You need credentials from both dashboards:
+
+**Alchemy** (for authentication):
+
+- Get your API key from [Alchemy Dashboard](https://dashboard.alchemy.com)
+
+**Particle Network** (for Universal Accounts):
+
+1. Sign up at [Particle Dashboard](https://dashboard.particle.network/)
+2. Create a project and web application
+3. Copy your **Project ID**, **Client Key**, and **App ID**
+
+## Installation
+
+```bash
+npm install @account-kit/universal-account
+# or
+yarn add @account-kit/universal-account
+```
+
+## Architecture
+
+When integrating Smart Wallets with Universal Accounts, understand the different account types:
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ Smart Wallets (Alchemy) │
+├─────────────────────────────────────────────────────────────────┤
+│ useUser() → user.address = EOA (Externally Owned Account) │
+│ useAccount() → address = SCA (Smart Contract Account) │
+│ useSigner() → Signs messages with the EOA │
+└─────────────────────────────────────────────────────────────────┘
+ │
+ │ EOA address (user.address)
+ ▼
+┌─────────────────────────────────────────────────────────────────┐
+│ Universal Accounts │
+├─────────────────────────────────────────────────────────────────┤
+│ Owner: EOA from Alchemy (user.address) │
+│ Creates: Multi-chain smart accounts (EVM + Solana) │
+│ Provides: Unified balance, cross-chain transactions │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+
+ Use the EOA address from `useUser().address` for Universal Accounts, not the
+ smart account address from `useAccount()`. These are different account types.
+
+
+## Quick start
+
+Find a full demo app in the [examples](https://github.com/alchemyplatform/aa-sdk/tree/main/account-kit/universal-account/examples) directory.
+
+### 1. Set up providers
+
+Wrap your app with both `AlchemyAccountProvider` and `UniversalAccountProvider`:
+
+```tsx twoslash
+// @noErrors
+// providers.tsx
+"use client";
+
+import { AlchemyAccountProvider } from "@account-kit/react";
+import { UniversalAccountProvider } from "@account-kit/universal-account";
+import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
+
+const queryClient = new QueryClient();
+
+// Your Alchemy config
+const config = {
+ // ... your alchemy config
+};
+
+const universalAccountConfig = {
+ projectId: process.env.NEXT_PUBLIC_PARTICLE_PROJECT_ID!,
+ clientKey: process.env.NEXT_PUBLIC_PARTICLE_CLIENT_KEY!,
+ appId: process.env.NEXT_PUBLIC_PARTICLE_APP_ID!,
+};
+
+export function Providers({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+```
+
+### 2. Initialize Universal Account
+
+Use the `useUniversalAccount` hook with the EOA address:
+
+```tsx twoslash
+// @noErrors
+import { useUser } from "@account-kit/react";
+import {
+ useUniversalAccount,
+ useUnifiedBalance,
+} from "@account-kit/universal-account";
+
+function Dashboard() {
+ const user = useUser();
+ const eoaAddress = user?.address as `0x${string}` | undefined;
+
+ // Universal Account auto-initializes with the EOA address
+ const {
+ address, // Universal Account EVM address
+ solanaAddress, // Universal Account Solana address
+ isReady,
+ isInitializing,
+ error,
+ } = useUniversalAccount(eoaAddress);
+
+ // Get unified balance across all chains
+ const { totalBalanceUSD, assets, isLoading, refetch } = useUnifiedBalance();
+
+ if (isInitializing) return Initializing Universal Account... ;
+ if (error) return Error: {error.message} ;
+ if (!isReady) return null;
+
+ return (
+
+ Universal Account
+ EVM Address: {address}
+ Solana Address: {solanaAddress}
+ Unified Balance: ${totalBalanceUSD?.toFixed(2)}
+
+ );
+}
+```
+
+### 3. Send transactions
+
+Use `useSendTransaction` to send cross-chain transactions:
+
+```tsx twoslash
+// @noErrors
+import { useSigner } from "@account-kit/react";
+import { useSendTransaction } from "@account-kit/universal-account";
+import { toBytes, encodeFunctionData } from "viem";
+
+function MintNFT() {
+ const signer = useSigner();
+
+ const { sendUniversal, isLoading, error, lastResult } = useSendTransaction({
+ signMessage: async (message: string) => {
+ if (!signer) throw new Error("Signer not available");
+ return await signer.signMessage({ raw: toBytes(message) });
+ },
+ });
+
+ const handleMint = async () => {
+ const NFT_CONTRACT = "0xdea7bF60E53CD578e3526F36eC431795f7EEbFe6";
+ const AVALANCHE_CHAIN_ID = 43114;
+
+ const mintData = encodeFunctionData({
+ abi: [{ type: "function", name: "mint", inputs: [], outputs: [] }],
+ functionName: "mint",
+ });
+
+ const result = await sendUniversal({
+ chainId: AVALANCHE_CHAIN_ID,
+ expectTokens: [],
+ transactions: [{ to: NFT_CONTRACT, data: mintData }],
+ });
+
+ console.log("Transaction ID:", result.transactionId);
+ };
+
+ return (
+
+ );
+}
+```
+
+## Transaction types
+
+The `useSendTransaction` hook provides methods for different transaction types:
+
+### Transfer tokens
+
+```tsx twoslash
+// @noErrors
+const { sendTransfer } = useSendTransaction({ signMessage });
+
+await sendTransfer({
+ token: {
+ chainId: 42161, // Arbitrum
+ address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT
+ },
+ amount: "10",
+ receiver: "0x...",
+});
+```
+
+### Buy tokens
+
+Convert USD value from your primary assets into a target token:
+
+```tsx twoslash
+// @noErrors
+const { sendBuy } = useSendTransaction({ signMessage });
+
+await sendBuy({
+ token: {
+ chainId: 42161,
+ address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
+ },
+ amountInUSD: "10", // Buy $10 worth
+});
+```
+
+### Sell tokens
+
+Sell a token back into primary assets:
+
+```tsx twoslash
+// @noErrors
+const { sendSell } = useSendTransaction({ signMessage });
+
+await sendSell({
+ token: {
+ chainId: 42161,
+ address: "0x912CE59144191C1204E64559FE8253a0e49E6548", // ARB
+ },
+ amount: "0.1",
+});
+```
+
+## Supported chains
+
+Universal Accounts support 15+ EVM chains and Solana:
+
+- Ethereum, Base, Arbitrum, Optimism, Polygon
+- Avalanche, BNB Chain, Linea, Berachain
+- Solana
+- And more...
+
+See the [full list of supported chains](https://developers.particle.network/universal-accounts/cha/chains).
+
+## Fees
+
+Universal Account transactions may include:
+
+- **Gas fees**: Standard network fees on the destination chain
+- **LP fee**: 0.2% for cross-chain transactions
+- **Service fee**: 1% on transaction volume
+
+Fees are automatically calculated and shown in the transaction preview.
+
+## Resources
+
+- [Particle Network Documentation](https://developers.particle.network/universal-accounts/cha/overview)
+- [Universal Accounts SDK Reference](https://developers.particle.network/universal-accounts/ua-reference/desktop/web)
+- [Supported Chains & Primary Assets](https://developers.particle.network/universal-accounts/cha/chains)
+- [UniversalX Explorer](https://universalx.app)
diff --git a/yarn.lock b/yarn.lock
index 28a7b28de2..c27a2a4e24 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4875,10 +4875,10 @@
dependencies:
"@particle-network/auth" "^1.3.1"
-"@particle-network/universal-account-sdk@^1.0.0":
- version "1.0.10"
- resolved "https://registry.yarnpkg.com/@particle-network/universal-account-sdk/-/universal-account-sdk-1.0.10.tgz#205ec4760fb0ebf87fca3cd3b07a249e10621cc9"
- integrity sha512-xd1yaEBpeMl9QWRWq1h2yxwKFEae4pYPVsomnpLqZmY1XSqKRRCspdmVeGFgKHz6SaoowWTrmqkh+daP6SjAGQ==
+"@particle-network/universal-account-sdk@^1.0.12":
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/@particle-network/universal-account-sdk/-/universal-account-sdk-1.0.12.tgz#fbb1173629477ec34456624d53cd2095c86f7503"
+ integrity sha512-H9ws7mjpXx6K0MxbmQ8OK+j9Gn836Ns5ExQP9Bt8k6cmPKIgQM6IK6aR5bifKZaMUTthHFZpWU3jGRwa4wypyg==
dependencies:
"@coral-xyz/anchor" "^0.30.1"
"@noble/hashes" "^1.7.1"
|