npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

deframe-sdk

v0.2.53

Published

UI Components for Deframe integration on the frontend

Downloads

4,820

Readme

Deframe SDK

React widgets for DeFi earn and swap flows

This package ships two host-rendered widgets:

  • EarnWidget
  • SwapWidget

The host application owns wallet connection, signing, and transaction submission through processBytecode.

Installation

# pnpm
pnpm add deframe-sdk
# npm
npm install deframe-sdk
# yarn
yarn add deframe-sdk

Notes:

  • Use only the production npm package: deframe-sdk (npm).
  • No local/workspace/file dependency references are required for host integration.
  • Keep react and react-dom installed in the host app.
  • deframe-sdk loads widget styles from its entrypoint. No separate CSS import is required for the default setup.

Vite: Single React Instance (important for linked/local packages)

If your app consumes SDK packages via file:, workspace links, or symlinks, multiple React copies can be resolved at runtime. That can cause Invalid hook call errors (for example: Cannot read properties of null (reading 'useEffect')).

Use this Vite config to force one React instance across app + linked packages:

import path from 'node:path'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  resolve: {
    dedupe: ['react', 'react-dom'],
    alias: {
      react: path.resolve(__dirname, 'node_modules/react'),
      'react-dom': path.resolve(__dirname, 'node_modules/react-dom'),
      'react/jsx-runtime': path.resolve(__dirname, 'node_modules/react/jsx-runtime.js'),
      'react/jsx-dev-runtime': path.resolve(__dirname, 'node_modules/react/jsx-dev-runtime.js'),
    },
  },
  optimizeDeps: {
    // Avoid manual .vite cache cleanup in linked-package setups.
    force: true,
  },
})

With this config in place, you should not need manual rm -rf node_modules/.vite or manual restart instructions in your integration flow.

CSS Isolation (Host Shell vs Widget)

The SDK widgets are designed to be embedded into existing apps without taking over host styling.

Use this isolation model:

  • Keep host/shell styles scoped to your own container (example: .host-shell ...).
  • Treat .deframe-widget as a style boundary owned by the SDK.
  • Avoid global resets that can leak into third-party widgets:
    • * { ... }
    • div, button, input { ... }
    • global all: initial / aggressive normalize rules on app-wide containers.
  • Use DeframeProvider config.theme as the source of truth for widget theme (mode, preset, overrides).
  • Do not rely on ad-hoc utility-class overrides inside widget internals except as temporary debug workarounds.

Recommended host layout:

<main className="host-shell">
  {/* host UI */}
  <section className="widget-slot">
    <EarnWidget autoHeight />
  </section>
</main>
.host-shell {
  /* your app styles */
}

.widget-slot {
  /* spacing, border, layout only */
}

Reference widget-only variable block (generic dark/default preset, client-agnostic):

/* Scope to widget only. Do not apply globally. */
.host-shell .deframe-widget {
  --deframe-brand-primary: #405eff;
  --deframe-brand-secondary: #a9abf7;
  --deframe-bg-default: #121212;
  --deframe-bg-subtle: #1e1e1e;
  --deframe-bg-muted: #2c2c2c;
  --deframe-bg-raised: #232323;
  --deframe-bg-inverse: #ffffff;
  --deframe-text-primary: #ffffff;
  --deframe-text-secondary: #e3e4e8;
  --deframe-text-disabled: #898d95;
  --deframe-text-inverse: #252050;
  --deframe-state-success: #2ba176;
  --deframe-state-error: #ff4d4f;
  --deframe-state-warning: #f6a700;

  /* Important fallbacks to avoid light cards in dark mode */
  --color-bg-raised: var(--deframe-bg-raised, #232323);
  --color-brand-tint: #1e1e3f;
}

Generic preset recommendation:

theme: {
  mode: 'dark',
  preset: 'default',
}

Use preset: 'cryptocontrol' only when you intentionally want that brand style.

If you need hard guarantees in high-risk host environments, use one of:

  • Dedicated iframe container for the widget.
  • Shadow DOM boundary around the widget mount point.

Environment Variables

NEXT_PUBLIC_DEFRAME_API_URL=https://api.deframe.com
NEXT_PUBLIC_DEFRAME_API_KEY=your_deframe_api_key

Canonical env forms for host integrations:

  • public client:
    • NEXT_PUBLIC_DEFRAME_API_URL
    • NEXT_PUBLIC_DEFRAME_API_KEY
  • server/internal:
    • DEFRAME_API_URL
    • DEFRAME_API_KEY
  • if API URL is not set in some internal hosts, default to https://api.deframe.com (where applicable).

Core Integration

1) Wrap your widget tree with DeframeProvider

'use client'

import { ReactNode, useMemo } from 'react'
import { DeframeProvider, type UpdateTxStatus } from 'deframe-sdk'

type BytecodeTransaction = {
  chainId?: number
  to: string
  data: string
  value: string
  gasLimit?: string
}

type ProcessBytecode = (
  payload: { clientTxId: string; bytecodes: BytecodeTransaction[]; simulateError?: boolean },
  ctx: { updateTxStatus: UpdateTxStatus }
) => void | Promise<void>

export function DeframeHostProvider({
  children,
  walletAddress,
  userId,
  globalCurrency,
  globalCurrencyExchangeRate,
  processBytecode,
}: {
  children: ReactNode
  walletAddress: string
  userId?: string
  globalCurrency?: 'USD' | 'BRL'
  globalCurrencyExchangeRate?: number
  processBytecode: ProcessBytecode
}) {
const config = useMemo(() => ({
    DEFRAME_API_URL: process.env.NEXT_PUBLIC_DEFRAME_API_URL,
    DEFRAME_API_KEY: process.env.NEXT_PUBLIC_DEFRAME_API_KEY,
    walletAddress,
    userId,
    globalCurrency,
    globalCurrencyExchangeRate,
    strategiesLimit: 50,
    debug: process.env.NODE_ENV !== 'production',
    theme: {
      mode: 'dark' as const,
      preset: 'cryptocontrol' as const,
    },
  }), [walletAddress, userId, globalCurrency, globalCurrencyExchangeRate])

  return (
    <DeframeProvider config={config} processBytecode={processBytecode}>
      {children}
    </DeframeProvider>
  )
}

2) Render EarnWidget

'use client'

import { useState } from 'react'
import { EarnWidget } from 'deframe-sdk'

export function EarnPage() {
  const [routeName, setRouteName] = useState('overview')
  const isOverview = routeName === 'overview'

  return (
    <div className={`w-full mx-auto ${isOverview ? 'lg:p-12' : 'lg:p-6'}`}>
      <div className={`w-full mx-auto ${isOverview ? 'max-w-[1400px]' : 'max-w-[620px]'}`}>
        <EarnWidget autoHeight onRouteChange={setRouteName} />
      </div>
    </div>
  )
}

3) Render SwapWidget with optional prefilled params

'use client'

import { useMemo } from 'react'
import { useSearchParams } from 'next/navigation'
import { SwapWidget } from 'deframe-sdk'

export function SwapPage() {
  const searchParams = useSearchParams()

  const toTokenAddress = searchParams.get('toTokenAddress') ?? undefined
  const toChainId = searchParams.get('toChainId') ? Number(searchParams.get('toChainId')) : undefined
  const fromTokenAddress = searchParams.get('fromTokenAddress') ?? undefined
  const fromChainId = searchParams.get('fromChainId') ? Number(searchParams.get('fromChainId')) : undefined

  const widgetProps = useMemo(() => {
    const props: {
      height: string
      toTokenAddress?: string
      toChainId?: number
      fromTokenAddress?: string
      fromChainId?: number
    } = {
      height: '800px',
    }

    if (toTokenAddress) props.toTokenAddress = toTokenAddress
    if (toChainId) props.toChainId = toChainId
    if (fromTokenAddress) props.fromTokenAddress = fromTokenAddress
    if (fromChainId) props.fromChainId = fromChainId

    return props
  }, [toTokenAddress, toChainId, fromTokenAddress, fromChainId])

  const widgetKey = `${toTokenAddress}-${toChainId}-${fromTokenAddress}-${fromChainId}`

  return <SwapWidget key={widgetKey} {...widgetProps} />
}

Widget-to-Host Events

This README keeps the working route pattern loop-safe for existing Privy-based implementations:

Use EarnWidget onRouteChange prop to keep lightweight host routing state:

  • history -> /dashboard/history
  • wallet -> /dashboard
  • swap -> /dashboard/swap
  • earn -> /dashboard/earn

useDeframe().addEventListener route handlers are intentionally omitted here because they caused host loops in some integrations.

processBytecode contract

The host must execute bytecodes and report lifecycle updates with ctx.updateTxStatus(...).

Payload shape is:

type TxIntent = {
  clientTxId: string
  bytecodes: Array<{ chainId?: number; to: string; data: string; value: string; gasLimit?: string }>
  simulateError?: boolean
}

Required success path statuses

  • HOST_ACK
  • SIGNATURE_PROMPTED
  • TX_SUBMITTED
  • TX_CONFIRMED
  • TX_FINALIZED
    • include txHash when available on TX_SUBMITTED and TX_CONFIRMED if you have it

Required error statuses

  • SIGNATURE_DECLINED
  • SIGNATURE_ERROR
  • TX_REVERTED
  • TX_FAILED (supported)

Optional advanced statuses

  • TX_REPLACED
  • TX_DROPPED
  • SWAP_CROSSCHAIN_DESTINATION_CONFIRMED

When SIGNATURE_PROMPTED is emitted, you typically call the wallet signer next. Map wallet refusal codes/messages to:

  • SIGNATURE_DECLINED for user cancellation (4001, ACTION_REJECTED).
  • SIGNATURE_ERROR for other thrown errors before submission.

SDK default fallback (no processBytecode)

  • If you run without a processBytecode prop, Deframe falls back to:
    • window.parent.postMessage({ type: 'REQUEST_SIGNATURE', clientTxId, bytecodes })
  • Keep this as a compatibility path only if your host is already wired to that message bridge.

Minimal host implementation

export const processBytecode = async (
  payload: {
    clientTxId: string
    bytecodes: Array<{ chainId?: number; to: string; data: string; value: string; gasLimit?: string }>
  },
  ctx: {
    updateTxStatus: (event: any) => void
  }
) => {
  const { clientTxId, bytecodes } = payload

  try {
    ctx.updateTxStatus({ type: 'HOST_ACK', clientTxId })

    for (const tx of bytecodes) {
      ctx.updateTxStatus({ type: 'SIGNATURE_PROMPTED', clientTxId })

      // 1) sign + send transaction in your wallet stack
      // 2) get tx hash
      const txHash = await sendTransactionWithYourWallet(tx)

      ctx.updateTxStatus({ type: 'TX_SUBMITTED', clientTxId, chainId: tx.chainId, txHash })

      // Wait until mined/confirmed
      await waitForConfirmation(txHash)
      ctx.updateTxStatus({ type: 'TX_CONFIRMED', clientTxId, txHash })
    }

    ctx.updateTxStatus({ type: 'TX_FINALIZED', clientTxId })
  } catch (error: any) {
    const code = error?.code
    const message = error?.message || 'Transaction failed'

    if (code === 4001 || code === 'ACTION_REJECTED' || String(message).includes('rejected')) {
      ctx.updateTxStatus({ type: 'SIGNATURE_DECLINED', clientTxId })
      return
    }

    ctx.updateTxStatus({
      type: 'SIGNATURE_ERROR',
      clientTxId,
      code: String(code || 'UNKNOWN_ERROR'),
      message,
    })
  }
}

Dynamic Integration (React)

Install

pnpm add @dynamic-labs/sdk-react-core @dynamic-labs/ethereum viem
# or npm/yarn equivalents

Provider setup

'use client'

import { DynamicContextProvider } from '@dynamic-labs/sdk-react-core'
import { EthereumWalletConnectors } from '@dynamic-labs/ethereum'

export function DynamicProviders({ children }: { children: React.ReactNode }) {
  return (
    <DynamicContextProvider
      settings={{
        environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID!,
        walletConnectors: [EthereumWalletConnectors],
      }}
    >
      {children}
    </DynamicContextProvider>
  )
}

processBytecode using Dynamic primaryWallet

'use client'

import { useDynamicContext } from '@dynamic-labs/sdk-react-core'

type Bytecode = { chainId?: number; to: string; data: string; value: string }

export function useDynamicDeframeProcessBytecode() {
  const { primaryWallet } = useDynamicContext()

  return async (
    payload: { clientTxId: string; bytecodes: Bytecode[] },
    ctx: { updateTxStatus: (event: any) => void }
  ) => {
    const { clientTxId, bytecodes } = payload

    if (!primaryWallet) {
      ctx.updateTxStatus({
        type: 'SIGNATURE_ERROR',
        clientTxId,
        code: 'NO_WALLET',
        message: 'No connected Dynamic wallet',
      })
      return
    }

    try {
      ctx.updateTxStatus({ type: 'HOST_ACK', clientTxId })

      for (const tx of bytecodes) {
        if (tx.chainId && primaryWallet.connector.supportsNetworkSwitching?.()) {
          await primaryWallet.switchNetwork(tx.chainId)
        }

        ctx.updateTxStatus({ type: 'SIGNATURE_PROMPTED', clientTxId })

        const walletClient = await primaryWallet.connector.getWalletClient()
        const txHash = await walletClient.sendTransaction({
          account: primaryWallet.address as `0x${string}`,
          to: tx.to as `0x${string}`,
          data: tx.data as `0x${string}`,
          value: BigInt(tx.value || '0'),
        })

        ctx.updateTxStatus({
          type: 'TX_SUBMITTED',
          clientTxId,
          chainId: tx.chainId,
          txHash,
        })

        const publicClient = primaryWallet.connector.getPublicClient?.()
        if (publicClient) {
          await publicClient.waitForTransactionReceipt({ hash: txHash })
        }

        ctx.updateTxStatus({ type: 'TX_CONFIRMED', clientTxId, txHash })
      }

      ctx.updateTxStatus({ type: 'TX_FINALIZED', clientTxId })
    } catch (error: any) {
      if (error?.code === 4001 || error?.code === 'ACTION_REJECTED') {
        ctx.updateTxStatus({ type: 'SIGNATURE_DECLINED', clientTxId })
        return
      }

      ctx.updateTxStatus({
        type: 'SIGNATURE_ERROR',
        clientTxId,
        code: String(error?.code || 'UNKNOWN_ERROR'),
        message: String(error?.message || 'Dynamic transaction failed'),
      })
    }
  }
}

Privy Integration (React)

Install

pnpm add @privy-io/react-auth viem
# or npm/yarn equivalents

Provider setup

'use client'

import { PrivyProvider } from '@privy-io/react-auth'

export function PrivyProviders({ children }: { children: React.ReactNode }) {
  return (
    <PrivyProvider
      appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}
      clientId={process.env.NEXT_PUBLIC_PRIVY_CLIENT_ID!}
      config={{
        embeddedWallets: {
          createOnLogin: 'users-without-wallets',
        },
      }}
    >
      {children}
    </PrivyProvider>
  )
}

processBytecode using Privy connected wallets

'use client'

import { useWallets } from '@privy-io/react-auth'

type Bytecode = { chainId?: number; to: string; data: string; value: string }

async function waitForReceipt(
  provider: { request: (args: { method: string; params?: any[] }) => Promise<any> },
  txHash: string
) {
  for (;;) {
    const receipt = await provider.request({
      method: 'eth_getTransactionReceipt',
      params: [txHash],
    })

    if (receipt) return receipt
    await new Promise((resolve) => setTimeout(resolve, 1500))
  }
}

export function usePrivyDeframeProcessBytecode() {
  const { wallets, ready } = useWallets()

  return async (
    payload: { clientTxId: string; bytecodes: Bytecode[] },
    ctx: { updateTxStatus: (event: any) => void }
  ) => {
    const { clientTxId, bytecodes } = payload

    if (!ready || wallets.length === 0) {
      ctx.updateTxStatus({
        type: 'SIGNATURE_ERROR',
        clientTxId,
        code: 'NO_WALLET',
        message: 'No Privy wallet is connected',
      })
      return
    }

    const wallet = wallets[0]

    try {
      ctx.updateTxStatus({ type: 'HOST_ACK', clientTxId })

      for (const tx of bytecodes) {
        if (tx.chainId) {
          await wallet.switchChain(tx.chainId)
        }

        const provider = await wallet.getEthereumProvider()

        ctx.updateTxStatus({ type: 'SIGNATURE_PROMPTED', clientTxId })

        const txHash = await provider.request({
          method: 'eth_sendTransaction',
          params: [
            {
              from: wallet.address,
              to: tx.to,
              data: tx.data,
              value: `0x${BigInt(tx.value || '0').toString(16)}`,
            },
          ],
        })

        ctx.updateTxStatus({
          type: 'TX_SUBMITTED',
          clientTxId,
          chainId: tx.chainId,
          txHash,
        })

        await waitForReceipt(provider, txHash)
        ctx.updateTxStatus({ type: 'TX_CONFIRMED', clientTxId, txHash })
      }

      ctx.updateTxStatus({ type: 'TX_FINALIZED', clientTxId })
    } catch (error: any) {
      if (error?.code === 4001 || error?.code === 'ACTION_REJECTED') {
        ctx.updateTxStatus({ type: 'SIGNATURE_DECLINED', clientTxId })
        return
      }

      ctx.updateTxStatus({
        type: 'SIGNATURE_ERROR',
        clientTxId,
        code: String(error?.code || 'UNKNOWN_ERROR'),
        message: String(error?.message || 'Privy transaction failed'),
      })
    }
  }
}

API Reference

EarnWidget props

type EarnWidgetProps = {
  className?: string
  style?: React.CSSProperties
  height?: string | number
  enableScroll?: boolean
  autoHeight?: boolean
  onRouteChange?: (routeName: string) => void
}

SwapWidget props

type SwapWidgetProps = {
  className?: string
  style?: React.CSSProperties
  height?: string | number
  enableScroll?: boolean
  autoHeight?: boolean
  fromTokenAddress?: string
  fromChainId?: number
  toTokenAddress?: string
  toChainId?: number
}

DeframeConfig

type DeframeConfig = {
  DEFRAME_API_URL?: string
  DEFRAME_API_KEY?: string
  walletAddress?: string
  userId?: string
  strategiesLimit?: number
  globalCurrency?: 'USD' | 'BRL'
  globalCurrencyExchangeRate?: number
  theme?: {
    mode?: 'light' | 'dark' | 'auto'
    preset?: 'default' | 'cryptocontrol'
    overrides?: {
      light?: { colors?: Record<string, string> }
      dark?: { colors?: Record<string, string> }
    }
  }
  debug?: boolean
  components?: ComponentOverridesMap
}

Component Overrides

Hosts can replace specific SDK view components via config.components, without forking. For example:

import { ChooseAnAssetSwapViewCardGrid } from 'deframe-sdk'

const config = {
  ...baseConfig,
  components: {
    ChooseAnAssetSwapView: ChooseAnAssetSwapViewCardGrid
  }
}

If no override is set, the default component is used.

Troubleshooting

  • Widget renders with no data:
    • Verify DEFRAME_API_URL, DEFRAME_API_KEY, and walletAddress.
  • Tx flow stuck in processing:
    • Ensure host emits TX_SUBMITTED, then TX_CONFIRMED, then TX_FINALIZED.
  • Signature rejected path not handled:
    • Map wallet rejection errors (4001 / ACTION_REJECTED) to SIGNATURE_DECLINED.
  • Cross-route actions should be handled through onRouteChange.
  • Widget appears with unexpected light cards/surfaces in dark mode:
    • Inspect .deframe-widget computed variables and verify:
      • --color-bg-default
      • --color-bg-raised
      • --color-text-primary
    • If --color-bg-raised resolves to a light value while using dark theme, set widget-scoped fallbacks:
      • --color-bg-raised: var(--deframe-bg-raised, #232323)
      • --color-brand-tint: #1e1e3f
    • Keep these overrides scoped to .deframe-widget, never global.

Machine Companion

For AI/editor ingestion, use llms-full.txt.