@yo-protocol/widget-sdk
v0.1.2
Published
Embed the YO Protocol widget in any website. Host-side loader + postMessage bridge.
Readme
@yo-protocol/widget-sdk
Embed the YO Protocol widget in any website. The widget renders inside an
iframe loaded from sdk-widget.vercel.app; your page owns the wallet and
signs transactions the widget prepares, over a versioned postMessage bridge.
npm install @yo-protocol/widget-sdkLLM-friendly: this README contains everything needed to ship a working integration in one pass — props, events, adapters, protocol, security, embed modes. Paste it verbatim into an LLM along with "integrate the YO widget into my app using wagmi + RainbowKit" and it should produce working code on the first try.
Table of contents
- Two embed modes
- 60-second React quickstart (wagmi + RainbowKit)
- Vanilla JS quickstart
<YoWidget />props (inline)<YoWidgetLauncher />props (floating)- Events — optional
- Signer adapters
- RPC configuration
- Theming
- Security model
- Protocol reference
- Troubleshooting
- One-shot LLM prompt template
Peer dependencies
Install whatever matches your stack — peers are optional:
react@>=18,react-dom@>=18— for<YoWidget />and<YoWidgetLauncher />wagmi@>=2+@wagmi/core@>=2.14— forcreateWagmiAdapterviem@>=2— forcreateViemAdapter
No peers required if you use vanilla mount().
Two embed modes
Pick the one that fits your layout. Both share the same signer + SDK plumbing — switching is a one-component swap.
| Component | Use when | How it looks |
|---|---|---|
| <YoWidget /> | Widget is a permanent, prominent feature of a page (e.g. a dashboard section, an "Earn" tab). | Inline iframe that grows/shrinks with content. |
| <YoWidgetLauncher /> | Widget is ambient / secondary (e.g. bottom-right helper, always-available entry point). | Floating round "YO" bubble anchored to a page corner; click to open a panel containing the widget, click × or outside to minimize. |
Both mount the same iframe. The launcher keeps it mounted across open/close toggles so the handshake, signer state, and cached data survive minimizing.
60-second React quickstart (wagmi + RainbowKit)
Inline embed
'use client'
import { ConnectButton, useConnectModal } from '@rainbow-me/rainbowkit'
import { YoWidget } from '@yo-protocol/widget-sdk'
import { createWagmiAdapter } from '@yo-protocol/widget-sdk/adapters/wagmi'
import { useMemo } from 'react'
import { useConfig } from 'wagmi'
export default function Page() {
const config = useConfig()
const { openConnectModal } = useConnectModal()
const signer = useMemo(
() => createWagmiAdapter({ config, onConnectRequest: openConnectModal }),
[config, openConnectModal],
)
return (
<>
<ConnectButton />
<YoWidget signer={signer} />
</>
)
}Floating launcher
import { YoWidgetLauncher } from '@yo-protocol/widget-sdk'
// … same signer as above …
<YoWidgetLauncher
signer={signer}
position="bottom-right"
/>That's it. The widget picks up the connected address from wagmi, renders
vaults and positions, and prompts the user's wallet for deposits/withdraws
via the adapter. You do not need onEvent unless you want analytics —
see Events.
Vanilla JS quickstart (any framework or plain HTML)
<div id="yo-widget"></div>
<script type="module">
import { mount } from 'https://esm.sh/@yo-protocol/widget-sdk'
import { createViemAdapter } from 'https://esm.sh/@yo-protocol/widget-sdk/adapters/viem'
const adapter = createViemAdapter({
getWalletClient: () => window.yourViemWalletClient,
subscribe: (notify) => {
const unsub = yourWallet.on('change', (state) =>
notify({ address: state.address, chainId: state.chainId }),
)
return unsub
},
onConnectRequest: () => yourWallet.openModal(),
})
mount(document.getElementById('yo-widget'), { signer: adapter })
</script>mount() produces the inline embed. For a launcher in vanilla JS, wrap
your own floating button around the iframe container — or open a PR for a
mountLauncher() helper.
<YoWidget /> props (inline)
| Prop | Type | Default | Description |
|---|---|---|---|
| signer | SignerAdapter | required | Bridges your wallet to the widget. See Signer adapters. |
| partnerId | number | 9999 | Attribution id assigned by the YO team. |
| chains | number[] | [1, 8453, 42161] | Restrict vaults to these chains. |
| rpcUrls | Record<number, string \| string[]> | — | Optional host-provided RPCs per chain. Tried first; SDK's curated public fallbacks cover any gaps. Use this to pass Alchemy/Infura keys. |
| widgetOrigin | string | https://sdk-widget.vercel.app | Where to load the iframe from. Override for staging/local. |
| theme | { primary?, background?, card?, radius? } | dark palette | Narrow colour overrides forwarded via URL query. See Theming. |
| locale | string | — | Reserved for i18n (v1 is English). |
| onEvent | (e: WidgetEvent) => void | — | Optional. See Events. |
| className / style / width / minHeight | — | — | Pass-through to the iframe element. |
Vanilla mount(el, options) accepts the same option set minus the styling
props; it returns { iframe, destroy() }.
<YoWidgetLauncher /> props (floating)
Extends all <YoWidget /> props (signer, partnerId, chains, rpcUrls,
widgetOrigin, theme, locale, onEvent) plus:
| Prop | Type | Default | Description |
|---|---|---|---|
| position | 'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left' | 'bottom-right' | Corner to anchor the bubble + panel. |
| label | string | 'Open YO' | Aria-label / tooltip for the bubble. |
| panelWidth | number (px) | 420 | Panel width when open. |
| panelHeight | number (px) | 640 | Panel height when open. |
| defaultOpen | boolean | false | Start open. |
| dismissOnOutsideClick | boolean | true | Close when the user clicks outside the panel or hits Escape. |
| bubbleStyle | CSSProperties | — | Override the floating button's inline style (size, color, shadow, etc.). |
| bubbleContent | ReactNode | 'YO' | Replace the bubble's text with a custom node (emoji, SVG, icon). |
The panel automatically clamps to viewport bounds
(max-width: calc(100vw - 32px), max-height: calc(100vh - 120px)) so it
never overflows on mobile.
Events — optional
You do not need to handle events to connect the widget to your wallet. The bridge is established by the
signeradapter alone. Events are a one-way telemetry stream from the widget to your host page — useful for analytics, toast notifications, or post-deposit redirects, but entirely optional. Skip theonEventprop if you don't need them.
Subscribe via onEvent={(e) => ...}. e.name is one of the strings below;
e.data is typed per-event.
| name | data | When |
|---|---|---|
| session-ready | { address?, chainId?, partnerId? } | First successful handshake; only fires when address/chain actually change. |
| deposit-started | { vault, asset } | User clicked Deposit, widget is preparing the txs. |
| approve-submitted | { vault, asset?, hash } | ERC-20 approval tx broadcast. Only fires when allowance was insufficient. |
| deposit-submitted | { vault, asset, hash } | Deposit tx broadcast. |
| deposit-confirmed | { vault, asset, hash } | Deposit tx mined successfully. |
| redeem-started | { vault } | User clicked Withdraw, widget is preparing. |
| redeem-submitted | { vault, hash } | Redeem tx broadcast. |
| redeem-confirmed | { vault, hash } | Redeem tx mined successfully. |
| error | { kind: 'deposit' \| 'redeem', vault, message } | Any user-visible error after reset. |
vault is a short label like yoETH, yoUSD. hash is a 0x… tx hash.
<YoWidget
signer={signer}
onEvent={(e) => {
if (e.name === 'deposit-confirmed') {
analytics.track('yo_deposit', e.data)
toast.success(`Deposited into ${e.data.vault}`)
}
}}
/>Signer adapters
The SDK talks to your wallet through a SignerAdapter. This is the only
required wiring between your app and the widget. Pre-built adapters cover
the common stacks:
createWagmiAdapter({ config, onConnectRequest })
Best if your app already uses wagmi v2.
import { createWagmiAdapter } from '@yo-protocol/widget-sdk/adapters/wagmi'
const signer = createWagmiAdapter({
config: myWagmiConfig, // same Config passed to <WagmiProvider>
onConnectRequest: openConnectModal, // e.g. from useConnectModal()
})createViemAdapter({ getWalletClient, subscribe, onConnectRequest })
For apps using raw viem (no wagmi).
import { createViemAdapter } from '@yo-protocol/widget-sdk/adapters/viem'
const signer = createViemAdapter({
getWalletClient: () => currentWalletClient, // viem WalletClient | undefined
subscribe: (notify) => yourStore.subscribe(() => notify({ address, chainId })),
onConnectRequest: () => openWalletModal(),
})Custom adapter
import type { SignerAdapter } from '@yo-protocol/widget-sdk'
const signer: SignerAdapter = {
getAccount: () => '0x…', // current address or undefined
getChainId: () => 8453, // current chainId or undefined
sendTransaction: async (tx, meta) => {
// tx = { to, data, value: string (decimal), chainId }
// meta = { kind: 'approve'|'deposit'|'redeem', vault?, amount?, asset? }
return await wallet.send({
to: tx.to,
data: tx.data,
value: BigInt(tx.value),
chainId: tx.chainId,
}) as `0x${string}`
},
switchChain: async (chainId) => wallet.switchChain(chainId),
subscribe: (notify) => {
const unsub = wallet.on('change', (s) =>
notify({ address: s.address, chainId: s.chainId }),
)
return unsub // cleanup
},
onConnectRequest: () => wallet.openModal(),
}Important: memoize adapters with useMemo so they don't re-create on
every render — otherwise the widget bridge reattaches each time.
RPC configuration
The widget reads on-chain state (balances, vault stats) from its own PublicClients — it never uses your wallet's RPC. Defaults:
- Ethereum: llamarpc → publicnode → cloudflare-eth
- Base: mainnet.base.org → publicnode → llamarpc
- Arbitrum: arb1.arbitrum.io → publicnode → llamarpc
All wrapped in viem's fallback() so one failing endpoint doesn't zero out
reads (critical for multicall — errors on a chain silently return 0 shares).
Host override:
<YoWidget
signer={signer}
rpcUrls={{
1: 'https://eth-mainnet.g.alchemy.com/v2/<key>',
8453: [
'https://base-mainnet.g.alchemy.com/v2/<key>',
'https://base-mainnet.infura.io/v3/<key>',
],
}}
/>Host RPCs are stacked in front of the SDK defaults, not instead of them — so partner endpoints are tried first, public RPCs remain as safety nets.
Theming
The widget is cross-origin so host CSS can't reach inside. We expose a narrow, validated theme contract forwarded via the iframe URL:
<YoWidget
theme={{
primary: '#ff00aa', // CTA color (default #ccff00 lime)
background: '#07070a', // widget body bg (default #000)
card: '#151520', // card bg (default #0d0d0d)
radius: 1.5, // rem, used for card/button rounding
}}
/>Rules:
- Colors must match
/^#(RGB|RRGGBB|RGBA|RRGGBBAA)$/— rejected otherwise (CSS-injection safe). - Radius must be a number; passed as
rem. - Unrecognized keys are ignored.
What IS overridable
| Layer | How |
|---|---|
| Iframe wrapper, launcher bubble, panel shell | Host CSS / className / style / bubbleStyle |
| Accent color, background, card bg, corner radius | theme prop |
What IS NOT overridable (by design)
- Widget internal fonts, spacing, button shape, deposit-flow layout
- Per-vault or per-screen styling
This is intentional: the iframe boundary is the security + update-control
boundary. If you need a deeper hook (e.g. brand font, secondary color, compact
mode), open an issue — these become new theme fields in a few lines.
Security model
- Origin check — every postMessage must come from
widgetOrigin. Anything else is silently dropped. - Contract allowlist — the host-side adapter refuses to sign any tx whose
tois not a known YO contract (vaults, gateway, redeemer, or whitelisted underlying tokens). ReturnsTO_NOT_ALLOWLISTED. - Chain match — widget must request the target chain via
switch-chain-requestbefore the adapter will sign on it. Mismatch →WRONG_CHAIN. - Sandboxed iframe —
sandbox="allow-scripts allow-same-origin allow-forms allow-popups". Cannot navigate the host, read its cookies, or touch its DOM. - Protocol versioning — host + widget both assert
PROTOCOL_VERSION. Mismatch shows an in-iframe upgrade message. - 10-min tx timeout — any pending sign request resolves or times out within 10 minutes; the widget resets and lets the user retry.
Protocol reference
For adapter authors and debugging. Every message:
{ v: 1, id, type, payload }.
Host → Widget
| type | payload | Meaning |
|---|---|---|
| init | { chains, address?, chainId?, partnerId?, locale?, rpcUrls? } | Sent once on widget ready. |
| account-changed | { address? } | Wallet connected/disconnected/switched account. |
| chain-changed | { chainId? } | Wallet switched networks. |
| tx-result | { id, hash? \| error? } | Response to send-transaction-request. |
| switch-chain-result | { id, ok, error? } | Response to switch-chain-request. |
Widget → Host
| type | payload | Meaning |
|---|---|---|
| ready | { version } | Widget booted. |
| connect-request | {} | User clicked connect; host opens its wallet modal. |
| switch-chain-request | { id, chainId } | Before signing on a different chain. |
| send-transaction-request | { id, tx, meta } | Sign + send. tx = { to, data, value (string), chainId }. |
| resize | { height } | Host adjusts iframe height. |
| event | { name, data? } | Telemetry (see Events). |
| navigate | { href } | Open external link in new tab (iframe can't). |
The pre-built adapters handle every message above. You only touch the protocol directly if you're writing a custom adapter for a non-EVM stack or debugging the bridge.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Widget stuck loading / black iframe | Bridge handshake didn't complete | Confirm widgetOrigin matches the URL your iframe actually loads. Check devtools for postMessage drops. |
| "Out of date" banner inside widget | Host-side SDK is older than the deployed widget protocol | npm update @yo-protocol/widget-sdk |
| "Refusing to sign" wallet error | Tx to not on the YO allowlist | Upgrade @yo-protocol/widget-sdk — allowlist ships with the package. |
| yoGOLD / Ethereum-only vaults invisible in positions | Default public Ethereum RPC rate-limited | Pass your own rpcUrls={{ 1: 'https://eth-mainnet.g.alchemy.com/v2/<key>' }} |
| Events fire multiple times in dev | React StrictMode double-mount | Harmless — session-ready dedupes internally; ignore in production. |
| Deposit tx rejected with WRONG_CHAIN | Wallet on different chain than vault | Widget will auto-request a chain switch before retrying; accept the wallet prompt. |
| Launcher panel stretches off screen on mobile | Viewport too small | The panel is already clamped to calc(100vw - 32px) / calc(100vh - 120px). For tiny screens, pass smaller panelWidth / panelHeight. |
One-shot LLM prompt template
Paste this to an LLM along with your app's framework + wallet stack and it should produce a working integration:
You are integrating the YO Protocol widget into a {FRAMEWORK} app that
uses {WALLET_STACK} for wallet connection.
Goals:
- Install @yo-protocol/widget-sdk
- Render either <YoWidget /> (inline) or <YoWidgetLauncher /> (floating)
with a signer adapter matching our wallet stack. Choose based on whether
the widget is a permanent page feature or ambient helper.
- Wire the connect button to openConnectModal (or equivalent)
- Pass our Alchemy RPC URLs via the rpcUrls prop
- (Optional) Log lifecycle events via onEvent for analytics — events are
not required for the bridge to work.
Constraints:
- The widget runs inside an iframe; we never import anything from the
widget app itself, only from @yo-protocol/widget-sdk.
- The SignerAdapter returned by createWagmiAdapter / createViemAdapter
must be stable across renders (wrap in useMemo).
- rpcUrls is stacked in front of the SDK's fallback list, not instead
of it — we only need to provide chains we care to speed up.
- Use the `theme` prop (primary, background, card, radius) if we need
to match our brand; deeper style overrides are not possible because
the iframe is cross-origin.
Produce:
- The provider file (wagmi/query setup) if needed
- The page/component that renders <YoWidget /> or <YoWidgetLauncher />
- (Optional) A small events handler
Reference docs follow:
<paste the rest of this README here>License
MIT
