# @surf_liquid/core-sdk

Framework-agnostic TypeScript SDK for Surfliquid — wallet authentication, vault deployment, deposits, withdrawals, portfolio queries, and activity feeds.

> **Integration examples & AI skill:** runnable sample apps and an integration `SKILL.md` live in the **[surfliquid-sdk-core-integration](https://github.com/Dev-zkCross/surfliquid-sdk-core-integration)** repo.

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Initialization](#initialization)
- [Gas on Ethereum mainnet](#gas-on-ethereum-mainnet-low-fee-defaults)
- [Step-by-Step Integration](#step-by-step-integration)
- [API Reference](#api-reference)
- [Events](#events)
- [Types](#types)
- [Error Handling](#error-handling)
- [Examples](#examples)

---

## Installation

```bash
npm install @surf_liquid/core-sdk ethers
```

> **ethers v6** is a required peer dependency.

---

## Quick Start

```ts
import { SurfClient } from "@surf_liquid/core-sdk";

const client = SurfClient.create({
  projectName: "my-app",
  appId: "your-app-id",
  autoApprove: true,
});

await client.verifyApp();          // validate your appId (throws on a bad/missing appId)
await client.connectWallet("metamask");
await client.authenticate();

const vault = await client.getVault();
if (!vault.exists) await client.deployVault();

const tx = await client.deposit({
  asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
  amount: "10.0",
});
await tx.wait();
```

---

## Initialization

### `SurfClient.create(config)` — recommended

```ts
const client = SurfClient.create({
  projectName: "my-app",       // Required — sent as X-Surf-Project-Name header
  appId: "your-app-id",        // Required — sent as X-App-ID header on first user login
  environment: "mainnet",      // "mainnet" | "testnet" — default: "mainnet"
  chainId: 8453,               // Default: Base (8453) on mainnet
  autoApprove: true,           // Auto-approve ERC20 spend before deposit — default: false
  apiBaseUrl: "https://api.surfliquid.com", // Optional — uses default if omitted
});
```

### `SurfClient.builder()` — fluent API

Use this when you need to register custom chains, tokens, or wallet adapters at build time.

```ts
const client = SurfClient.builder()
  .setProject("my-app", "your-app-id")
  .setEnvironment("mainnet")
  .setChain(8453)
  .setAutoApprove(true)
  .build();
```

### Config options

| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| `projectName` | `string` | Yes | — | Your project name |
| `appId` | `string` | Yes | — | Your app / project ID |
| `environment` | `"mainnet" \| "testnet"` | No | `"mainnet"` | Network environment |
| `chainId` | `number` | No | `8453` | Active chain ID |
| `autoApprove` | `boolean` | No | `false` | Auto-approve ERC20 allowance before deposits |
| `rpcUrl` | `string` | No | Chain default | Custom RPC endpoint |
| `apiBaseUrl` | `string` | No | `https://api.surfliquid.com` | Custom API base URL |
| `factoryAddress` | `` `0x${string}` `` | No | Chain default | Override vault factory address |

### Supported chains

| Chain | Chain ID | Environment |
|-------|----------|-------------|
| Base | `8453` | mainnet |
| Ethereum | `1` | mainnet |
| Polygon | `137` | mainnet |
| Base Sepolia | `84532` | testnet |

---

## Gas on Ethereum mainnet (low-fee defaults)

On **Ethereum mainnet (`chainId: 1`) only**, the SDK attaches explicit EIP-1559 fee overrides to every transaction it sends (vault deploy, deposit, withdraw, and the ERC-20 approval / WETH-wrap steps) so transactions cost as little as possible. On every other chain (Base, Polygon, Base Sepolia, and any custom chain) the SDK supplies **no** fee overrides and lets the wallet/provider pick fees as usual — this behavior is Ethereum-specific and there is **no integration change required** on any chain.

**What the SDK sets on Ethereum:**

- `type: 2` (EIP-1559) is set **explicitly**. This is deliberate: without an explicit `type: 2`, wallets such as MetaMask ignore a dapp's supplied 1559 fees and substitute their own "market" estimate, which is higher.
- `maxPriorityFeePerGas` (the tip) is derived from `eth_feeHistory` over the last ~20 blocks: the SDK takes the per-block 10th-percentile priority fees, uses their median, and floors the result at **0.1 gwei** (`100_000_000` wei) so the tx stays includable.
- `maxFeePerGas` is `baseFee * 1.25 + tip` — i.e. only **1.25x** base-fee headroom, **not** the 2x "survive 6 blocks" headroom that ethers/EIP-1559 defaults to.
- If `eth_feeHistory` is unavailable, it falls back to the latest block's `baseFeePerGas` plus the 0.1 gwei floor tip.

**"Site suggested" vs the wallet's own "Low" preset.** These values are tuned to line up with a wallet's **"Low"** fee tier. The reason the 2x headroom was dropped to 1.25x is that MetaMask was surfacing the 2x figure as a scarily high **"max"** on site-suggested (dapp-supplied) transactions, even though the effective cost is only ~`baseFee + tip`. With the 1.25x cap, the SDK's **"Site suggested"** fee no longer reads higher than the wallet's own lowest (Low) option. The trade-off is the same one the wallet's Low tier makes: a sharp base-fee spike between submission and inclusion can delay mining. Users who want faster inclusion can still override the fee in their wallet at confirmation time.

> Effective cost is still approximately `baseFee + tip`; the headroom only caps the worst-case ceiling — you are not charged the max.

### Robust write-gas handling (Ethereum mainnet)

To make first deposits and back-to-back deposit/withdraw flows reliable, the SDK **pre-estimates the gas limit for write transactions itself** (via the read RPC) and passes it to the wallet as an explicit `gasLimit`, instead of letting the wallet re-estimate. This is scoped to **Ethereum mainnet (`chainId: 1`)** — the chain where the failures below were observed and where the explicit low-fee overrides already apply — and covers `deployVault`, `deposit` (both the `initialDeposit` and `userDeposit` paths), and `withdraw`. A small **20% buffer** is added to the estimate so a slight state shift between estimate and mining can't cause out-of-gas (you still only pay for gas actually used).

The estimate is done against the SDK's own read provider with the connected wallet as `from`. If that estimate hits a **genuine contract revert**, the error is re-thrown immediately (retrying wouldn't help); if it fails for a **transient/non-revert reason**, the SDK returns no gas limit and falls back to letting the wallet estimate — so robustness never makes a call strictly worse.

**Deposit allowance-settle wait.** When `autoApprove` sends an approval before a deposit, the SDK then **polls the allowance** (up to 8 times, ~1s apart) via the read provider until the new allowance is visible before submitting the deposit. If the budget elapses it proceeds anyway (a genuinely-insufficient allowance surfaces on the deposit itself).

**Which `estimateGas` failures this fixes:**

- **Post-approval first-deposit revert.** The deposit's gas estimation can race the approval: if the estimating node hasn't yet observed the freshly-set allowance, the deposit's `transferFrom` reverts during `estimateGas` with `require(false)` / no revert data. The allowance-settle wait plus SDK-side pre-estimation removes this race.
- **Stale-state re-estimation on deposit/withdraw.** Submitting a withdraw (or a second deposit) right after a deposit could make the wallet re-estimate against stale state and fail with `missing revert data` / `could not decode result data`. Pre-estimating against the read provider and passing an explicit `gasLimit` avoids the wallet's faulty re-estimate.

All of this is internal — **no integration change is required**; deposits/withdrawals/deploys are simply more reliable on Ethereum.

---

## Step-by-Step Integration

### 1. Create the client

```ts
const client = SurfClient.create({
  projectName: "my-app",
  appId: "your-app-id",
  autoApprove: true,
});
```

### 2. Register event listeners

Set up listeners before connecting so no events are missed.

```ts
client.on("wallet:connected", ({ address, chainId }) => {
  console.log("Connected:", address, "on chain", chainId);
});
client.on("auth:authenticated", ({ address, authenticated }) => {
  console.log("Authenticated:", authenticated, "as", address);
});
client.on("vault:deployed", ({ vaultAddress }) => {
  console.log("Vault at:", vaultAddress);
});
client.on("deposit:completed", ({ asset, amount, txHash }) => {
  console.log(`Deposited ${amount} of ${asset} — tx: ${txHash}`);
});
client.on("error", ({ code, message }) => {
  console.error(`[${code}] ${message}`);
});
```

### 3. Connect wallet

```ts
// Supported: "metamask" | "trust" | "coinbase" | "rabby" | "phantom" | "walletconnect" | "injected"
const state = await client.connectWallet("metamask");
console.log(state.address);  // "0x..."
console.log(state.chainId);  // 8453
```

### 4. Authenticate

```ts
const auth = await client.authenticate();
console.log(auth.authenticated); // true
// Auth is cookie-based: the session token lives in an httpOnly cookie set by the
// backend, so `auth.token` is always null. Gate on `auth.authenticated` instead.
console.log(auth.token);         // null
```

### 5. Check or deploy vault

```ts
const vault = await client.getVault();

if (!vault.exists) {
  const result = await client.deployVault();
  console.log("Deployed at:", result.vaultAddress);
} else {
  console.log("Vault:", vault.userVaultAddress);
  console.log("Total value: $" + vault.totalValueUSD);
  console.log("APY:", vault.apyBreakdown?.currentAPY + "%");
}
```

### 6. Deposit

```ts
const tx = await client.deposit({
  asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
  amount: "10.0", // human-readable, not wei
});
await tx.wait();
```

### 7. Check portfolio

```ts
const portfolio = await client.getPortfolioSummary();
console.log("Active assets:", portfolio.activeCount);
portfolio.assets.forEach((addr, i) => {
  console.log(addr, "→ profit:", portfolio.profits[i].toString());
});
```

### 8. Withdraw

```ts
// Partial
const tx = await client.withdraw({
  asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  amount: "5.0",
});
await tx.wait();

// Full (omit amount)
const tx = await client.withdraw({
  asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
});
await tx.wait();
```

---

## API Reference

### Setup

#### `verifyApp()`
Validates your `appId` against the backend (`GET /api/sdk/public/overview` with the `x-app-id` header). Call it right after `create()` to fail fast on a bad app ID. Throws `MISSING_PROJECT_ID` if no `appId` was provided, or `INVALID_APP_ID` if the backend rejects it.
```ts
await client.verifyApp(); // resolves if the appId is valid; throws otherwise
```

### Wallet

#### `connectWallet(walletName)`
Connects a wallet and switches it to the configured chain.

```ts
const state = await client.connectWallet("metamask");
// state: { address, chainId, connected }
```

#### `disconnectWallet()`
```ts
await client.disconnectWallet();
```

#### `getWalletState()`
Returns current wallet state or `null` if not connected.
```ts
const state = client.getWalletState(); // WalletState | null
```

#### `switchChain(chainId)`
Re-points the **whole SDK** at `chainId`: it updates `config.chainId`, `rpcUrl`, `factoryAddress`, and the read provider from the chain registry, and switches the connected wallet too (works read-only without a wallet). Throws `UNSUPPORTED_CHAIN` for a chain not registered for the active environment. Your auth (the cookie session) is preserved — it's per-wallet, not per-chain. Any custom `rpcUrl` / `factoryAddress` you set at creation are replaced by the target chain's registry values.
```ts
await client.switchChain(137); // SDK now operates on Polygon
```

#### `registerWalletAdapter(name, adapter)`
Register a custom wallet adapter at runtime.
```ts
client.registerWalletAdapter("my-wallet", new MyAdapter());
```

---

### Authentication

#### `authenticate()`
Signs a nonce with the connected wallet and logs in. Auth is **cookie-based**: on login the backend sets an httpOnly session cookie (`SameSite=None; Secure`) — the token is never returned in the response body, so `auth.token` is always `null`. Gate on `auth.authenticated` / `auth.user`, never on `auth.token`. Flow: `getNonce` → `signMessage` → `login`.

```ts
const auth = await client.authenticate();
// auth: { token, address, authenticated, user } — token is always null (lives in the cookie)
```

> Requires wallet connected.
>
> **Cross-origin:** the SDK sends every request with `credentials: "include"` (no `Authorization: Bearer` header). For the cookie to work cross-origin the API must return a **specific** `Access-Control-Allow-Origin` (never `*`) plus `Access-Control-Allow-Credentials: true`, and your web origin must be allowlisted.

#### `refreshSession()`
Extends the session using the existing httpOnly cookie — no wallet signature and no body. Calls `POST /api/auth/refresh`; the backend rotates the cookie (new 7-day token) and invalidates the old one immediately. Returns `{ expiresAt }` (ISO 8601). Call it proactively before expiry (e.g. when less than 24h remain) to keep the session alive without re-signing.

```ts
const { expiresAt } = await client.refreshSession();
console.log("Session valid until", expiresAt);
```

> Throws `SurfError` (`API_ERROR`, status 401) if the cookie is missing, expired, or revoked.

#### `getAuthState()`
```ts
const auth = client.getAuthState(); // AuthState (synchronous, no request)
```

#### `logout()`
```ts
await client.logout();
```

---

### Vault

#### `getVault(walletAddress?)`
Fetches vault info. Falls back to connected wallet if no address passed. **Public — no auth required.**

```ts
const vault = await client.getVault();
// or: await client.getVault("0xAnyAddress");

vault.exists             // boolean
vault.userVaultAddress   // "0x..." | null
vault.homeChainId        // 8453
vault.vaultVersion       // "v4"
vault.isActive           // boolean
vault.totalValueUSD      // 13.03
vault.totalDepositedUSD  // 13.03
vault.apyBreakdown       // { currentAPY, nativeAPY, merklAPY, leagueAPY, totalAPY?, apy7d?, apy14d?, apy30d? } — windowed APYs are portfolio-weighted, null until enough history
vault.earned             // { totalEarningsUSD, nativeEarningsUSD, merklRewardsUSD, leagueEarnedUSD }
vault.league             // { rank, totalXP, estimatedSURF, estimatedSURFUSD, joinDate } | null
vault.assets             // VaultAsset[] — per-chain asset breakdown
```

#### `deployVault()`
Deploys a new vault. Flow: `prepare` → on-chain `deployVault` → `confirm`.

```ts
const result = await client.deployVault();
result.vaultAddress      // "0x..."
result.transactionHash   // "0x..."
result.salt              // "0x..."
```

> Requires a connected wallet (the SDK enforces a wallet, not auth — the backend still expects an authenticated session). Throws `VAULT_ALREADY_EXISTS` if a vault already exists.

#### `getSupportedAssets(chainId?)`
Returns supported assets with live APY data. **Public — no wallet or auth required.**

```ts
const assets = await client.getSupportedAssets();         // all chains
const assets = await client.getSupportedAssets(8453);     // Base only
const assets = await client.getSupportedAssets(137);      // Polygon only

assets[0].assetSymbol   // "USDC"
assets[0].assetAddress  // "0x..."
assets[0].chainId       // 8453
assets[0].chainStatus   // "ACTIVE" | "STAKING"
assets[0].currentAPY    // 15.99
assets[0].nativeAPY     // 13.08
assets[0].merklAPY      // 0.05
assets[0].leagueAPY     // 2.86
assets[0].apy7d         // 14.80 — trailing 7-day APY (null until 7 days of history)
assets[0].apy14d        // 15.10 — trailing 14-day APY (null until 14 days)
assets[0].apy30d        // 15.40 — trailing 30-day APY (null until 30 days)
```

#### `getAgentMessages(walletAddress?, page?, limit?, from?, to?)`
Returns paginated activity feed — deposits, withdrawals, rebalances, bridges, Merkl claims. **Public.** `from`/`to` are optional ISO-8601 strings; when provided, results are filtered by `timestamp` inclusive on both ends.

```ts
const result = await client.getAgentMessages();           // connected wallet, page 1, limit 20
const result = await client.getAgentMessages("0x...", 2, 10);

// Filter by a date range (ISO 8601, inclusive on both ends)
const result = await client.getAgentMessages(
  "0x...", 1, 20,
  "2026-01-01T00:00:00.000Z", // from
  "2026-03-31T23:59:59.999Z", // to
);

result.total    // 42
result.pages    // 3
result.messages // AgentMessage[]

result.messages[0].message          // human-readable description
result.messages[0].transactionType  // "USER_DEPOSIT" | "REBALANCE_COMPLETED" | "MERKL_CLAIM" | ...
result.messages[0].executedBy       // "USER" | "AGENT"
result.messages[0].chainId          // 8453
result.messages[0].txHash           // "0x..."
result.messages[0].timestamp        // "2026-04-02T12:21:41.000Z"

// Structured fields — present only when applicable to the event type:
result.messages[0].amount     // 10.0 | null
result.messages[0].token      // "USDC" | null
result.messages[0].fromVault  // { name, address, apy } | null
result.messages[0].toVault    // { name, address, apy } | null
result.messages[0].apyBefore  // 13.2 | null
result.messages[0].apyAfter   // 15.4 | null
result.messages[0].signal     // string | null
```

#### `getOwnerVaults(owner?)` / `getOwnerVaultCount(owner?)`
```ts
const vaults = await client.getOwnerVaults();   // string[]
const count  = await client.getOwnerVaultCount(); // number
```

#### `isVaultFromFactory(vaultAddress)`
```ts
const valid = await client.isVaultFromFactory("0x..."); // boolean
```

#### `getAllowedAssets(vaultAddress?)`
```ts
const assets = await client.getAllowedAssets(); // string[]
```

#### `getFeeInfo(vaultAddress?)`
```ts
const fees = await client.getFeeInfo();
fees.feePercentage            // bigint
fees.rebalanceFeePercentage   // bigint
fees.merklClaimFeePercentage  // bigint
```

---

### Deposits & Withdrawals

#### `deposit(params)`

```ts
const tx = await client.deposit({
  asset: "0x...",     // token address
  amount: "10.0",     // human-readable (not wei)
  wrapEth: false,     // wrap native ETH → WETH first
  bestVault: "0x...", // optional: override Morpho vault
});
await tx.wait();
```

> If `autoApprove: true`, the SDK automatically approves the token spend if needed.

#### `withdraw(params)`

```ts
const tx = await client.withdraw({
  asset: "0x...",
  amount: "5.0",  // omit for full withdrawal
});
await tx.wait();
```

#### `getWithdrawableAmount(asset, vaultAddress?)`
```ts
const wei = await client.getWithdrawableAmount("0x..."); // bigint
```

#### `getBestVault(assetSymbol)`
```ts
const options = await client.getBestVault("USDC");
// [{ chainId: 8453, vaultAddress: "0x..." }, ...]
```

#### `hasInitialDeposit(asset, vaultAddress?)`
```ts
const done = await client.hasInitialDeposit("0x..."); // boolean
```

---

### Portfolio

#### `getPortfolioSummary(vaultAddress?)`
```ts
const p = await client.getPortfolioSummary();
p.activeCount    // number
p.assets         // string[]  — asset addresses
p.deposited      // bigint[]  — deposited amounts in wei
p.currentValues  // bigint[]  — current values in wei
p.profits        // bigint[]  — profits in wei
```

#### `getAssetProfit(asset, vaultAddress?)`
```ts
const profit = await client.getAssetProfit("0x..."); // bigint (wei)
```

#### `getAssetProfitPercentage(asset, vaultAddress?)`
```ts
const pct = await client.getAssetProfitPercentage("0x..."); // bigint
```

---

### Token Utilities

#### `getTokenBalance(token, owner?)`
```ts
const balance = await client.getTokenBalance("0xusdcAddress");          // connected wallet
const balance = await client.getTokenBalance("0xusdcAddress", "0x..."); // any address
// returns bigint (wei)
```

#### `getSupportedTokens()`
Returns tokens registered in the SDK for the active chain (synchronous).
```ts
const tokens = client.getSupportedTokens();
// [{ address, symbol, decimals, vaultAddresses }, ...]
```

#### `getConfig()`
```ts
const config = client.getConfig();
config.chainId      // 8453
config.environment  // "mainnet"
config.apiBaseUrl   // "https://api.surfliquid.com"
```

---

## Events

```ts
client.on("event:name", handler);
client.off("event:name", handler);
```

| Event | Payload | When |
|-------|---------|------|
| `wallet:connected` | `WalletState` | Wallet connects |
| `wallet:disconnected` | `void` | Wallet disconnects |
| `wallet:accountChanged` | `{ oldAddress, newAddress }` | Account switch |
| `wallet:chainChanged` | `{ chainId }` | Chain switch |
| `auth:authenticated` | `AuthState` | Login complete |
| `auth:logout` | `void` | Logout called |
| `vault:deployed` | `DeployVaultResult` | Vault deployed |
| `deposit:started` | `{ asset, amount }` | Deposit begins |
| `deposit:approved` | `{ asset, txHash }` | ERC20 approval sent |
| `deposit:completed` | `{ asset, amount, txHash }` | Deposit confirmed |
| `withdraw:started` | `{ asset, amount }` | Withdrawal begins |
| `withdraw:completed` | `{ asset, amount, txHash }` | Withdrawal confirmed |
| `error` | `{ code, message }` | Any SDK error |

---

## Types

```ts
interface WalletState {
  address: string;
  chainId: number;
  connected: boolean;
}

interface AuthState {
  token: string | null; // Always null under cookie auth — the token lives in an httpOnly cookie, not here
  address: string | null;
  authenticated: boolean;
  user: UserProfile | null;
}

interface RefreshResult {
  expiresAt: string; // ISO 8601 expiry of the freshly issued session token
}

interface VaultInfo {
  userVaultAddress: string | null;
  deploymentSalt: string | null;
  exists: boolean;
  homeChainId?: number | null;
  vaultVersion?: string | null;
  isActive?: boolean;
  totalValueUSD?: number | null;
  totalDepositedUSD?: number | null;
  earned?: VaultEarned | null;
  apyBreakdown?: VaultApyBreakdown | null;
  league?: VaultLeague | null;
  assets?: VaultAsset[];
  // Per-chain vault addresses. A vault can be deployed at a different address per
  // chain (e.g. Ethereum), so this can differ from userVaultAddress.
  chainAddresses?: VaultChainAddress[];
}

interface VaultChainAddress {
  chainId: number;
  vaultAddress: string;
}

interface VaultApyBreakdown {
  currentAPY: number;
  nativeAPY: number;
  merklAPY: number;
  leagueAPY: number;
  totalAPY?: number;
  // Trailing-window APYs (portfolio-weighted). Null until enough history exists.
  apy7d?: number | null;
  apy14d?: number | null;
  apy30d?: number | null;
}

interface VaultAsset {
  assetAddress: string;
  assetSymbol: string;
  assetDecimals: number;
  chainId: number;
  chainStatus: string;
  currentAPY: number;
  nativeAPY: number;
  merklAPY: number;
  leagueAPY: number;
  // Trailing-window APYs for this asset. Null until enough history exists.
  apy7d?: number | null;
  apy14d?: number | null;
  apy30d?: number | null;
  // ...balances, earnings, and vault metadata
}

interface SupportedAsset {
  assetAddress: string;
  assetSymbol: string;
  assetDecimals: number;
  chainId: number;
  chainStatus: string;
  currentAPY: number;
  nativeAPY: number;
  merklAPY: number;
  leagueAPY: number;
  // Trailing-window APYs. Null until enough history exists (e.g. apy7d is null with < 7 days).
  apy7d?: number | null;
  apy14d?: number | null;
  apy30d?: number | null;
}

interface VaultRef {
  name: string;
  address: string;
  apy: number;
}

interface AgentMessage {
  message: string;
  txHash: string;
  timestamp: string;
  transactionType:
    | "INITIAL_DEPOSIT" | "USER_DEPOSIT" | "USER_WITHDRAWAL" | "REBALANCE" | "CROSS_CHAIN_REBALANCE" | "MIGRATE"
    | "DEPOSIT" | "WITHDRAWAL" | "REBALANCE_COMPLETED" | "REBALANCE_CANCELLED" | "MERKL_CLAIM"
    | "ASSET_ADDED" | "ASSET_REMOVED" | "ASSET_SWAPPED"
    | string;
  executedBy: "USER" | "AGENT";
  vaultVersion: string;
  chainId: number;
  // Optional structured fields — omitted when not applicable to the event type:
  amount?: number | null;
  token?: string | null;
  fromVault?: VaultRef | null;
  toVault?: VaultRef | null;
  apyBefore?: number | null;
  apyAfter?: number | null;
  signal?: string | null;
}

interface DepositParams {
  asset: string;
  amount: string;
  vaultAddress?: string;
  bestVault?: string;
  wrapEth?: boolean;
}

interface WithdrawParams {
  asset: string;
  amount?: string;
  vaultAddress?: string;
}

interface TransactionResult {
  hash: string;
  wait: () => Promise<any>;
}
```

---

## Error Handling

All errors are instances of `SurfError` with a typed `code` and `message`.

```ts
import { SurfError, SurfErrorCode } from "@surf_liquid/core-sdk";

try {
  await client.deposit({ asset: "0x...", amount: "10" });
} catch (err) {
  if (err instanceof SurfError) {
    switch (err.code) {
      case SurfErrorCode.WALLET_NOT_CONNECTED: break;
      case SurfErrorCode.INSUFFICIENT_BALANCE: break;
      case SurfErrorCode.DEPOSIT_FAILED: break;
    }
  }
}
```

| Code | Cause |
|------|-------|
| `INVALID_CONFIG` | Missing or invalid configuration |
| `MISSING_PROJECT_ID` | `appId` not provided |
| `INVALID_APP_ID` | `appId` rejected by the backend (`verifyApp()`) |
| `UNSUPPORTED_CHAIN` | `chainId` not registered for environment |
| `ENVIRONMENT_CHAIN_MISMATCH` | `chainId` doesn't belong to the configured environment |
| `WALLET_NOT_INSTALLED` | Wallet extension not found |
| `WALLET_NOT_CONNECTED` | Operation requires connected wallet |
| `WALLET_REJECTED` | User rejected the wallet request |
| `WRONG_CHAIN` | Wallet is on wrong chain |
| `AUTH_FAILED` | Login request failed |
| `SIGNATURE_REJECTED` | User rejected message signing |
| `VAULT_NOT_FOUND` | No vault exists for this address |
| `VAULT_ALREADY_EXISTS` | Vault already deployed |
| `VAULT_DEPLOY_FAILED` | On-chain deployment failed |
| `INSUFFICIENT_BALANCE` | Token balance too low |
| `APPROVE_FAILED` | ERC20 approval failed |
| `DEPOSIT_FAILED` | Deposit transaction failed |
| `WITHDRAW_FAILED` | Withdrawal transaction failed |
| `NO_BEST_VAULT` | No Morpho vault found for asset |
| `API_ERROR` | Backend API error |
| `RPC_ERROR` | RPC/blockchain call failed |
| `TRANSACTION_FAILED` | On-chain transaction failed |

---

## Examples

> **Full runnable examples + AI integration guide:** [surfliquid-sdk-core-integration](https://github.com/Dev-zkCross/surfliquid-sdk-core-integration) — a React + Vite sample app and a `SKILL.md` an AI agent can use to integrate this SDK. The snippets below are quick references.

### React

```tsx
import { useEffect, useState } from "react";
import { SurfClient } from "@surf_liquid/core-sdk";

const client = SurfClient.create({
  projectName: "my-app",
  appId: "your-app-id",
  autoApprove: true,
});

export function App() {
  const [address, setAddress] = useState<string | null>(null);
  const [vault, setVault] = useState<string | null>(null);

  useEffect(() => {
    client.on("wallet:connected", (s) => setAddress(s.address));
    client.on("wallet:disconnected", () => setAddress(null));
  }, []);

  async function setup() {
    await client.connectWallet("metamask");
    await client.authenticate();
    const info = await client.getVault();
    if (!info.exists) {
      const r = await client.deployVault();
      setVault(r.vaultAddress);
    } else {
      setVault(info.userVaultAddress);
    }
  }

  async function deposit() {
    const tx = await client.deposit({
      asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      amount: "10.0",
    });
    await tx.wait();
  }

  return (
    <div>
      <button onClick={setup}>Connect & Setup</button>
      {address && <p>Wallet: {address}</p>}
      {vault && <button onClick={deposit}>Deposit 10 USDC</button>}
    </div>
  );
}
```

### Vanilla JS

```html
<script type="module">
  import { SurfClient } from "/node_modules/@surf_liquid/core-sdk/dist/index.js";

  const client = SurfClient.create({
    projectName: "my-app",
    appId: "your-app-id",
    autoApprove: true,
  });

  // No wallet needed — fetch supported assets
  const assets = await client.getSupportedAssets(8453);
  console.table(assets.map(a => ({
    symbol: a.assetSymbol,
    APY: a.currentAPY + "%",
    address: a.assetAddress,
  })));

  // Full flow
  await client.connectWallet("metamask");
  await client.authenticate();

  const vault = await client.getVault();
  if (!vault.exists) await client.deployVault();

  const tx = await client.deposit({
    asset: assets[0].assetAddress,
    amount: "10.0",
  });
  await tx.wait();
</script>
```

### Node.js (read-only)

```js
import { SurfClient } from "@surf_liquid/core-sdk";

const client = SurfClient.create({
  projectName: "dashboard",
  appId: "your-app-id",
});

// No wallet needed for public endpoints
const vault = await client.getVault("0xUserWalletAddress");
console.log("Vault:", vault.userVaultAddress);
console.log("Value: $" + vault.totalValueUSD);

const assets = await client.getSupportedAssets();
console.log("Supported:", assets.map(a => a.assetSymbol));

const { messages } = await client.getAgentMessages("0xUserWalletAddress");
messages.forEach(m => console.log(`[${m.executedBy}] ${m.message}`));
```

### Custom wallet adapter

```ts
import { SurfClient, IWalletAdapter } from "@surf_liquid/core-sdk";
import type { Signer } from "ethers";

class MyWalletAdapter implements IWalletAdapter {
  async connect(chainId: number) { /* return WalletState */ }
  async disconnect() {}
  async switchChain(chainId: number) {}
  async getSigner(): Promise<Signer> { /* return ethers Signer */ }
  async signMessage(message: string): Promise<string> { /* return sig */ }
  onAccountsChanged(cb: (accounts: string[]) => void) {}
  onChainChanged(cb: (chainId: number) => void) {}
  onDisconnect(cb: () => void) {}
}

const client = SurfClient.builder()
  .setProject("my-app", "your-app-id")
  .registerWalletAdapter("my-wallet", new MyWalletAdapter())
  .build();

await client.connectWallet("my-wallet");
```

### WalletConnect

```ts
import { SurfClient, WalletConnectAdapter } from "@surf_liquid/core-sdk";
import { EthereumProvider } from "@walletconnect/ethereum-provider";

const client = SurfClient.create({ projectName: "my-app", appId: "your-app-id" });

const wcProvider = await EthereumProvider.init({
  projectId: "your-walletconnect-project-id", // from cloud.walletconnect.com
  chains: [8453],
  showQrModal: true,
});

client.registerWalletAdapter("walletconnect", new WalletConnectAdapter(() => wcProvider));
await client.connectWallet("walletconnect");
```

### Register custom chain / token

```ts
const client = SurfClient.builder()
  .setProject("my-app", "your-app-id")
  .registerChain("mainnet", {
    chainId: 42161,
    rpcUrl: "https://arb1.arbitrum.io/rpc",
    factoryAddress: "0x...",
    wethAddress: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1",
    tokens: [],
  })
  .registerToken("mainnet", 42161, {
    address: "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
    symbol: "USDC",
    decimals: 6,
    vaultAddresses: [],
  })
  .setChain(42161)
  .build();
```

---

## Notes

- `projectName` and `appId` are required. `projectId` is accepted as a backwards-compatible alias for `appId`.
- Default environment is `mainnet`; default mainnet chain is Base (`8453`).
- Amounts passed to `deposit()` and `withdraw()` are human-readable strings (e.g. `"10.5"`), not wei.
- Withdraw amounts are encoded using the token's on-chain `decimals()` value.
- `getVault()`, `getSupportedAssets()`, and `getAgentMessages()` are public — no wallet or auth required when passing an explicit wallet address.
- Set `autoApprove: true` on the client to skip manual ERC20 approval before deposits.
- **Auth is cookie-based.** Login sets an httpOnly session cookie (`SameSite=None; Secure`); the token is never in the response body, so `authState.token` is always `null`. Gate on `authState.authenticated` / `authState.user`. The SDK sends every request with `credentials: "include"` (no `Authorization: Bearer` header), so the API must return a **specific** `Access-Control-Allow-Origin` (never `*`) plus `Access-Control-Allow-Credentials: true`, and your web origin must be allowlisted. Use `refreshSession()` to rotate the cookie before it expires without re-signing.
- **A vault can have a different address per chain.** `VaultInfo.chainAddresses` lists per-chain vault addresses; on some chains (e.g. Ethereum) the address differs from `userVaultAddress` (the home-chain address). The SDK resolves the correct per-chain address automatically **when you omit the `vaultAddress` argument** on `deposit`/`withdraw`/`getPortfolioSummary`/`getWithdrawableAmount`/etc. Don't pass `userVaultAddress` explicitly on a non-home chain — it points at an address with no vault contract there.
