# Client-side SDKs

The two L1-authorized actions in the [two-key model](/docs/recipes/lighter#the-two-key-model) — funding and registering an API key — both work fine from an embedded wallet on-device, in React (via `@openfort/react` + wagmi) or React Native (`@openfort/react-native`). Order signing does not: it needs Lighter's Go-based signer, and that can't run in the client.

## Login model: ephemeral guests, persistent email

The demo app requires an explicit login on every cold launch — it never auto-restores a previous session — and offers two paths with deliberately different identity semantics:

* **Guest** — a brand-new anonymous user and a fresh embedded wallet every time. Guests are unrecoverable once signed out, which makes them perfect for the free testnet flow: each run onboards a fresh Lighter account in a few seconds.
* **Email OTP** — the persistent path. The same email recovers the same embedded wallet (via Shield automatic recovery), so a returning user lands back on their existing Lighter account without re-onboarding.

:::warning
If you build your own create-vs-reconnect logic on `useEmbeddedEthereumWallet`, don't trust `status === "disconnected" && wallets.length === 0` as proof that no wallet exists — the SDK briefly reports exactly that during session restore, and creating a wallet then mints a **new** one on every reload, orphaning the previous account. Gate creation on `embeddedState` (via `useOpenfortContext`) having left its pre-restore value, as the recipe's `UserScreen.tsx` does.
:::

## Deposit from the embedded wallet

On testnet there's no deposit at all — a single faucet call creates and funds the account with no transaction (see [Configuration](/docs/recipes/lighter/configuration#funding-routes)). The flow below is the mainnet path.

Depositing is a plain EVM transaction (`approve` then `deposit`), sent through the embedded wallet's EIP-1193 provider:

```tsx
import { useEmbeddedEthereumWallet } from '@openfort/react-native'
import { encodeFunctionData, erc20Abi, parseUnits } from 'viem'

const LIGHTER_DEPOSIT_CONTRACT = '0x3B4D794a66304F130a4Db8F2551B0070dfCf5ca7'
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
const USDC_ASSET_INDEX = 3
const ROUTE_TYPE_PERPS = 0

function useDepositUsdc() {
  const ethereum = useEmbeddedEthereumWallet()

  return async (amountHuman: string) => {
    if (ethereum.status !== 'connected') return
    const from = ethereum.activeWallet.address
    const amount = parseUnits(amountHuman, 6)

    const approveData = encodeFunctionData({
      abi: erc20Abi,
      functionName: 'approve',
      args: [LIGHTER_DEPOSIT_CONTRACT, amount],
    })
    await ethereum.provider.request({
      method: 'eth_sendTransaction',
      params: [{ from, to: USDC, data: approveData, value: '0x0' }],
    })

    const depositData = encodeFunctionData({
      abi: [
        {
          type: 'function',
          name: 'deposit',
          stateMutability: 'payable',
          inputs: [
            { name: '_to', type: 'address' },
            { name: '_assetIndex', type: 'uint16' },
            { name: '_routeType', type: 'uint8' },
            { name: '_amount', type: 'uint256' },
          ],
          outputs: [],
        },
      ],
      functionName: 'deposit',
      args: [from, USDC_ASSET_INDEX, ROUTE_TYPE_PERPS, amount],
    })
    await ethereum.provider.request({
      method: 'eth_sendTransaction',
      params: [{ from, to: LIGHTER_DEPOSIT_CONTRACT, data: depositData, value: '0x0' }],
    })
  }
}
```

:::info
Openfort's [documented send-transaction pattern](/docs/products/embedded-wallet/react-native/wallet/actions/send-transaction/ethereum) uses `wallet_sendCalls`, which batches `approve` + `deposit` and picks up gas sponsorship automatically. The recipe uses sequential `eth_sendTransaction` calls only because `depositFlow.ts` shares a generic EIP-1193 interface with the backend-wallet path — either works.
:::

### Deposit from other chains

Mainnet Lighter also accepts USDC from **Arbitrum, Base, and Avalanche C-Chain** via CCTP (minimum 5 USDC, ~15–20 minutes to credit): request a deposit intent address from Lighter's `createIntentAddress` endpoint (source `chain_id`, `from_addr`, `amount`), then send a plain USDC transfer to it — no Lighter contract call on the source chain. The recipe implements only the direct Ethereum route; [Configuration](/docs/recipes/lighter/configuration#funding-routes) compares all routes.

## Register an API key

The server builds the `ChangePubKey` message (see [API keys](/docs/recipes/lighter/api-keys)); the wallet only signs it with a plain `personal_sign`:

:::code-group
```tsx [React Native]
import { useEmbeddedEthereumWallet } from '@openfort/react-native'
import { toHex } from 'viem'

function useRegisterApiKey() {
  const ethereum = useEmbeddedEthereumWallet()

  return async (accountIndex: number) => {
    if (ethereum.status !== 'connected') return
    const address = ethereum.activeWallet.address

    const { messageToSign } = await requestChangePubKeyMessage(accountIndex)

    const l1Sig = await ethereum.provider.request({
      method: 'personal_sign',
      params: [toHex(messageToSign), address],
    })

    return submitChangePubKey(accountIndex, l1Sig)
  }
}
```

```tsx [React (wagmi)]
import { useSignMessage } from 'wagmi'

// With @openfort/react's wagmi integration, wagmi hooks drive the embedded
// wallet directly — signMessageAsync produces the same EIP-191 personal_sign.
function useRegisterApiKey() {
  const { signMessageAsync } = useSignMessage()

  return async (accountIndex: number) => {
    const { messageToSign } = await requestChangePubKeyMessage(accountIndex)

    const l1Sig = await signMessageAsync({ message: messageToSign })

    return submitChangePubKey(accountIndex, l1Sig)
  }
}
```
:::

:::info
Openfort's provider accepts either encoding — a `0x`-hex message is decoded to bytes, anything else is hashed as UTF-8, and both produce the same EIP-191 digest. Other providers may accept only one convention; a wrong encoding fails at `ChangePubKey` submission with `21120 invalid signature`.
:::

:::warning
Embedded wallets sign `personal_sign` **without a native confirmation prompt**, and every `ChangePubKey` submit [rotates the key](/docs/recipes/lighter/api-keys#rotation--every-submit-replaces-the-key) — give any re-authorize action a confirmation dialog and a double-fire guard, as the recipe does.
:::

## Why orders route through the server

There's no official TypeScript SDK for Lighter, and the recipe's signer — a WASM build of `lighter-go` — needs the `WebAssembly` global, which React Native's Hermes engine doesn't implement. Every order, cancel, and auth-token request goes to the recipe's server instead (`POST /api/lighter/order`, `/api/lighter/order/cancel`, `GET /api/lighter/orders`), which holds the registered API key and runs the signer in Node.

```text
App (embedded wallet)              Recipe server (Node, WASM signer)
  │                                       │
  ├─ deposit / approve  ─────────────────▶│  (not needed — sent directly to Lighter)
  ├─ personal_sign(ChangePubKey) ────────▶│  builds message, submits signed tx
  └─ POST /api/lighter/order ────────────▶│  signs + submits with the API key
```

## Withdraw back to the wallet

Lighter has two withdrawal paths: **secure withdrawals** (any supported asset, minimum 1 USDC or equivalent) and **fast withdrawals** (USDC only, minimum 4 USDC). Account-to-account transfers also exist, with the same 1 USDC minimum.

The recipe wires the secure path, which needs no wallet interaction: the transaction is signed by the server-held API key and carries **no destination field** — the protocol pays out only to the registered L1 owner, i.e. the embedded wallet that deposited:

```ts
await fetch(`${serverBaseUrl}/api/lighter/withdraw`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ amountUsdcRaw: 5_000_000 }), // 5 USDC at 6 decimals
})
```

Fast withdrawals and transfers additionally need a signature from the owner's Ethereum key; the recipe doesn't implement either — see [Lighter's docs](https://apidocs.lighter.xyz/docs/deposits-transfers-and-withdrawals) before wiring them up.

## Demo app

The full onboarding flow — faucet or deposit funding, then registration with automatic server key adoption — is implemented in `hooks/useLighterOnboarding.ts` and `components/onboarding/` in the recipe app:

:::info
See [Trade on Lighter](/docs/recipes/lighter#demo-app) for the complete source and setup instructions.
:::

## Best practices

* **Poll server readiness with identity, not just existence** — the recipe requires `GET /api/lighter/config` to report `serverWalletConfigured: true` *and* a matching `accountIndex` before trading (the server also 409s mismatched orders).
* **Show fills from the server's confirmation, not from balance diffs** — see [Confirming fills](/docs/recipes/lighter/trading#confirming-fills--sendtx-wont-tell-you).
