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:
EarnWidgetSwapWidget
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-sdkNotes:
- Use only the production npm package:
deframe-sdk(npm). - No local/workspace/file dependency references are required for host integration.
- Keep
reactandreact-dominstalled in the host app. deframe-sdkloads 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-widgetas 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
DeframeProviderconfig.themeas 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_keyCanonical env forms for host integrations:
- public client:
NEXT_PUBLIC_DEFRAME_API_URLNEXT_PUBLIC_DEFRAME_API_KEY
- server/internal:
DEFRAME_API_URLDEFRAME_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/historywallet->/dashboardswap->/dashboard/swapearn->/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_ACKSIGNATURE_PROMPTEDTX_SUBMITTEDTX_CONFIRMEDTX_FINALIZED- include
txHashwhen available onTX_SUBMITTEDandTX_CONFIRMEDif you have it
- include
Required error statuses
SIGNATURE_DECLINEDSIGNATURE_ERRORTX_REVERTEDTX_FAILED(supported)
Optional advanced statuses
TX_REPLACEDTX_DROPPEDSWAP_CROSSCHAIN_DESTINATION_CONFIRMED
When SIGNATURE_PROMPTED is emitted, you typically call the wallet signer next.
Map wallet refusal codes/messages to:
SIGNATURE_DECLINEDfor user cancellation (4001,ACTION_REJECTED).SIGNATURE_ERRORfor other thrown errors before submission.
SDK default fallback (no processBytecode)
- If you run without a
processBytecodeprop, 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 equivalentsProvider 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 equivalentsProvider 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, andwalletAddress.
- Verify
- Tx flow stuck in processing:
- Ensure host emits
TX_SUBMITTED, thenTX_CONFIRMED, thenTX_FINALIZED.
- Ensure host emits
- Signature rejected path not handled:
- Map wallet rejection errors (
4001/ACTION_REJECTED) toSIGNATURE_DECLINED.
- Map wallet rejection errors (
- Cross-route actions should be handled through
onRouteChange. - Widget appears with unexpected light cards/surfaces in dark mode:
- Inspect
.deframe-widgetcomputed variables and verify:--color-bg-default--color-bg-raised--color-text-primary
- If
--color-bg-raisedresolves 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.
- Inspect
Machine Companion
For AI/editor ingestion, use llms-full.txt.
