npm.io
0.2.6 • Published 1h ago

@surf_liquid/surf-widget

Licence
Version
0.2.6
Deps
2
Size
717 kB
Vulns
0
Weekly
624

@surf_liquid/surf-widget

Embeddable React widget for Surfliquid — drop DeFi yield into any app in minutes.

npm license


What it does

@surf_liquid/surf-widget gives your users a fully self-custodial yield account — backed by Surfliquid's on-chain strategy engine — without leaving your app. Users deposit USDC, the protocol deploys a personal smart contract vault for them, and the agent rebalances across the best yields automatically.

The widget handles everything:

  • Smart contract vault deployment (first-time deposit)
  • ERC-20 approval + deposit in a single, guided flow
  • Real-time APY, balance, and earnings display
  • Instant withdrawal
  • On-chain activity feed with agent messages

View the demo app →


How it fits together

Your App
  └── @surf_liquid/core-sdk   ← owns wallet connection & all on-chain calls
        └── @surf_liquid/surf-widget  ← consumes the client, renders the UI

Important: All wallet connections must go through @surf_liquid/core-sdk. Do not pass a raw ethers signer or wagmi connector directly to the widget. The SurfClient from the core SDK wraps your wallet and exposes the interface the widget depends on.


Installation

# 1. Install the core SDK (required — handles wallet + chain calls)
npm install @surf_liquid/core-sdk

# 2. Install the widget
npm install @surf_liquid/surf-widget

Peer dependencies (install if not already in your project):

npm install react react-dom

Quick Start

Step 1 — Get your App ID

Register your project at sdk.surfliquid.com to get an appId. This ties your integration to your project and unlocks access to the Surfliquid API.

Step 2 — Initialise and authenticate via core-sdk
import { SurfClient } from '@surf_liquid/core-sdk';

const client = SurfClient.create({
  projectName: 'my-app',   // identifies your integration in logs
  appId: 'YOUR_APP_ID',    // from https://sdk.surfliquid.com/
  chainId: 8453,           // 1 = Ethereum, 8453 = Base (default), 137 = Polygon
  autoApprove: true,       // handles ERC-20 approval automatically
});

await client.verifyApp();           // validate appId — throws INVALID_APP_ID if rejected
await client.connectWallet('metamask');   // or 'walletconnect', 'coinbase', etc.
await client.authenticate();        // establishes cookie session (token is always null — gate on auth.authenticated)
Step 3 — Drop the widget into your React tree
import { SurfWidget } from '@surf_liquid/surf-widget';

export default function App() {
  return (
    <SurfWidget
      appId="YOUR_APP_ID"
      client={client}
      walletAddress="0xYourAddress"
      chainId={8453}
      onSuccess={(action, txHash) => console.log(`${action} confirmed:`, txHash)}
      onError={(action, err)    => console.error(`${action} failed:`,    err)}
    />
  );
}

That's it. No extra providers, no manual CSS imports — styles are fully scoped inside the widget.


SurfWidget Props

Prop Type Default Description
appId string required App ID from sdk.surfliquid.com
client ISurfClient required SurfClient instance from @surf_liquid/core-sdk
walletAddress string required Connected wallet address
chainId number 8453 Chain to operate on (1 Ethereum, 8453 Base, 137 Polygon)
theme SurfTheme Override colors, radius, and font family
className string Extra CSS class on the root element
minDeposit number 0.1 Minimum deposit amount in USDC
onSuccess (action, txHash) => void Fired after a confirmed deposit or withdrawal
onError (action, error) => void Fired when a deposit or withdrawal fails

Theming

Override any design token via the theme prop. All values are optional — unset tokens fall back to the default Surfliquid palette.

<SurfWidget
  client={client}
  walletAddress={address}
  theme={{
    colors: {
      primary:        '#6366f1',   // buttons, active states, progress lines
      primaryText:    '#ffffff',   // text on primary-colored surfaces
      background:     '#0f0f11',   // widget background
      cardBackground: '#1a1a1f',   // card/modal background
      text:           '#f4f4f5',   // primary text
      textSecondary:  '#a1a1aa',   // labels, descriptions
      apy:            '#22c55e',   // APY percentage highlight
      border:         '#27272a',   // dividers and outlines
      success:        '#22c55e',   // success states
    },
    borderRadius: '16px',
    fontFamily:   'Inter, sans-serif',
  }}
/>

All tokens are injected as CSS custom properties scoped to [data-surf-widget], so they never affect the rest of your app.

CSS Class Overrides

For deeper customisation, target any .surf-* class in your own stylesheet. Every rule is already scoped to [data-surf-widget], so your overrides need the same scope to take effect:

[data-surf-widget] .surf-card {
  border-radius: 20px;
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
}

[data-surf-widget] .surf-btn--primary {
  background: linear-gradient(135deg, #6366f1, #8b5cf6);
}

[data-surf-widget] .surf-balance-amount {
  font-size: 28px;
}

Key class groups:

Group Classes
Card .surf-card, .surf-card--selected
Buttons .surf-btn, .surf-btn--primary, .surf-btn--outline, .surf-btn--soft, .surf-btn--ghost
Modal .surf-modal, .surf-modal-overlay, .surf-modal-body
Amount input .surf-amount-box, .surf-amount-input, .surf-amount-hint
Step progress .surf-step, .surf-step--active, .surf-step-icon, .surf-step-title, .surf-step-desc, .surf-step-separator
Tabs .surf-tabs, .surf-tab, .surf-tab--active
Stats .surf-stat-value, .surf-stat-value--apy, .surf-balance-amount
Success screen .surf-success, .surf-success-title, .surf-success-desc
Activity feed .surf-activity-item, .surf-activity-icon

Minimum Deposit

Control the minimum deposit amount with the minDeposit prop (default: 0.1 USDC):

<SurfWidget
  client={client}
  walletAddress={address}
  minDeposit={10}   // users must deposit at least 10 USDC
/>

The widget enforces this in two ways:

  • The deposit button stays disabled until the entered amount meets the minimum
  • A hint label below the input shows Min 10 USDC

Supported Chains

Chain Chain ID USDC Address
Ethereum 1 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
Base 8453 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Polygon 137 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359

The widget automatically enforces the correct network. If the connected wallet is on a different chain, the card shows a "Switch to [Chain]" button that calls wallet_switchEthereumChain (and wallet_addEthereumChain if the chain isn't in the wallet yet). No configuration needed — just pass the desired chainId.


Using the Provider directly

For tighter layout control — e.g. rendering the vault card in one place and the deposit button somewhere else — wrap your tree with SurfProvider and consume context with useSurf anywhere inside it:

import { SurfProvider, useSurf } from '@surf_liquid/surf-widget';

function App() {
  const containerRef = useRef<HTMLDivElement | null>(null);
  return (
    <div ref={containerRef}>
      <SurfProvider
        config={{ appId: 'YOUR_APP_ID', client, walletAddress: address, chainId: 8453, theme: myTheme }}
        containerRef={containerRef}
      >
        <Dashboard />
      </SurfProvider>
    </div>
  );
}

function Dashboard() {
  const { walletAddress, chainId } = useSurf();
  // ...
}

Headless Usage — Hooks

Import individual hooks to build a fully custom UI while relying on Surfliquid's state management and on-chain logic.

All hooks must be called inside a <SurfProvider>.

useVault

Fetches the connected user's vault state.

import { useVault } from '@surf_liquid/surf-widget';

const { vault, isLoading, refetch } = useVault();

vault?.balance          // "1234.56" — deposited USDC
vault?.earnings         // "12.34"   — total yield earned
vault?.apy              // 8.7       — current APY (number)
vault?.apy7d            // 8.957-day windowed APY (number | null)
vault?.apy14d           // 8.8014-day windowed APY (number | null)
vault?.apy30d           // 8.6030-day windowed APY (number | null)
vault?.availableBalance // withdrawable amount
useDeposit

Drives the full deposit flow: vault deploy → ERC-20 approve → deposit.

import { useDeposit } from '@surf_liquid/surf-widget';

const { step, isProcessing, execute, reset } = useDeposit();

// Kick off a deposit
await execute('100', vault);

// step.id progression:
// 'idle' → 'creating_contract' → 'approving' → 'depositing' → 'success' | 'error'
useWithdraw
import { useWithdraw } from '@surf_liquid/surf-widget';

const { step, isProcessing, execute, reset } = useWithdraw();

await execute('100', vault);
// step.id: 'idle' | 'closing_position' | 'success' | 'error'
useDepositBalance

Returns the wallet's USDC balance formatted for display.

import { useDepositBalance } from '@surf_liquid/surf-widget';

const { balance, isLoading } = useDepositBalance(vault);
// balance: "1500.00"
useWithdrawableBalance

Returns the amount currently available to withdraw from the vault.

import { useWithdrawableBalance } from '@surf_liquid/surf-widget';

const { balance, isLoading } = useWithdrawableBalance(vault);
useChainGuard

Detects whether the connected wallet is on the correct chain and provides a one-call switch.

import { useChainGuard } from '@surf_liquid/surf-widget';

const { walletChainId, isWrongChain, isSwitching, switchChain } = useChainGuard();

// walletChainId  — the chain the wallet is currently on (null if no wallet detected)
// isWrongChain   — true when walletChainId !== the chainId passed to SurfWidget
// isSwitching    — true while wallet_switchEthereumChain is in flight
// switchChain()  — prompts the wallet to switch; adds the chain if not present yet

<SurfWidget> calls this internally — the "Switch to [Chain]" UI on the card is automatic. Use this hook only if you need the chain-guard state in your own custom UI.

useAgentMessages

Fetches paginated agent activity for the connected wallet.

import { useAgentMessages } from '@surf_liquid/surf-widget';

const { activities, isLoading, refetch } = useAgentMessages();

// activities[0].description     — human-readable description
// activities[0].type            — "deposit" | "withdraw"
// activities[0].protocol        — e.g. "Surf Agent v2"
// activities[0].timestamp       — Unix ms
// activities[0].txHash          — on-chain transaction hash
// activities[0].chainId         — chain where the tx occurred
// activities[0].amount          — number | null
// activities[0].token           — token symbol | null
// activities[0].fromVault       — VaultRef | null  ({ name, address, apy })
// activities[0].toVault         — VaultRef | null
// activities[0].apyBefore       — APY before the action | null
// activities[0].apyAfter        — APY after the action | null
// activities[0].signal          — strategy signal | null

Headless Usage — Components

Use individual pre-built components inside your own layout:

import {
  VaultCard,
  DepositModal,
  WithdrawModal,
  VaultActivityModal,
  ManageDropdown,
} from '@surf_liquid/surf-widget';
UI Primitives
import {
  Button,
  Modal,
  Tabs,
  StepProgress,
  TokenIcon,
} from '@surf_liquid/surf-widget';
StepProgress

Renders a vertical step tracker — useful for any multi-step transaction flow.

import { StepProgress } from '@surf_liquid/surf-widget';

<StepProgress
  baseExplorerUrl="https://basescan.org/tx/"
  steps={[
    {
      id: 'create',
      title: 'Surf Account Creation',
      description: 'Deploy your self-custodial smart contract',
      status: 'completed',
      txHash: '0xabc...',
    },
    {
      id: 'deposit',
      title: 'Deposit USDC',
      description: 'Fund your Surf Account',
      status: 'active',
    },
  ]}
/>

Step status values: 'pending' | 'active' | 'completed' | 'error'


TypeScript

All types are exported from the package root:

import type {
  // Config & theming
  SurfWidgetProps,
  SurfConfig,
  SurfTheme,

  // Domain models
  VaultInfo,
  VaultActivity,
  VaultDeposit,
  TokenInfo,
  ChainInfo,
  SurfVaultInfo,
  SurfVaultAsset,
  SurfSupportedAsset,
  VaultRef,
  VaultChainAddress,

  // State machines
  DepositStep,
  WithdrawStep,

  // Agent messages
  AgentMessage,
  AgentMessagesResult,

  // Client interface — implement this to mock or extend
  ISurfClient,
} from '@surf_liquid/surf-widget';
ISurfClient

The widget depends on this interface, not on a specific SDK version. You can implement it yourself for testing:

import type { ISurfClient } from '@surf_liquid/surf-widget';

const mockClient: ISurfClient = {
  getVault:              async () => ({ exists: false, userVaultAddress: null }),
  deployVault:           async () => ({ vaultAddress: '0x...', transactionHash: '0x...', salt: '0x...' }),
  deposit:               async () => ({ hash: '0x...', wait: async () => {} }),
  withdraw:              async () => ({ hash: '0x...', wait: async () => {} }),
  getSupportedAssets:    async () => [],
  getTokenBalance:       async () => 0n,
  getWithdrawableAmount: async () => 0n,
  getAgentMessages:      async () => ({ page: 1, limit: 10, total: 0, pages: 0, messages: [] }),
  verifyApp:             async () => {},
  refreshSession:        async () => ({ expiresAt: new Date().toISOString() }),
  on:  () => {},
  off: () => {},
};

Demo App

A fully working reference integration is available at:

https://github.com/Dev-zkCross/demo-sdk-surfliquid

It demonstrates:

  • Connecting MetaMask via @surf_liquid/core-sdk
  • Passing the client to <SurfWidget>
  • Handling onSuccess / onError callbacks
  • Custom theming

Wallet Support

The widget is wallet-agnostic — it works with any wallet you connect through @surf_liquid/core-sdk:

Wallet Support
MetaMask
WalletConnect
Coinbase Wallet
Any EIP-1193 injected wallet
Safe (Gnosis)

Reminder: Always initialise SurfClient from @surf_liquid/core-sdk first, then pass the resulting client instance to the widget. Never bypass the core SDK by passing a raw signer.


License

MIT Surfliquid

Keywords