From cdda3441394736d749f5d63f6f4152fb959c17b9 Mon Sep 17 00:00:00 2001 From: Eugene Chybisov Date: Mon, 2 Mar 2026 15:46:05 +0100 Subject: [PATCH 01/14] feat: add widget light draft --- examples/nft-checkout/.env | 3 + examples/nft-checkout/README.md | 35 ++ examples/nft-checkout/index.html | 23 + examples/nft-checkout/package.json | 61 +++ examples/nft-checkout/public/favicon.ico | Bin 0 -> 15086 bytes examples/nft-checkout/public/favicon.png | Bin 0 -> 980 bytes examples/nft-checkout/public/logo192.png | Bin 0 -> 5347 bytes examples/nft-checkout/public/logo512.png | Bin 0 -> 9664 bytes examples/nft-checkout/public/manifest.json | 25 + examples/nft-checkout/public/robots.txt | 3 + examples/nft-checkout/src/App.tsx | 59 ++ .../src/components/NFTOpenSea/NFTOpenSea.tsx | 0 .../NFTOpenSea/NFTOpenSeaSecondary.tsx | 0 .../components/NFTOpenSea/getEthersSigner.ts | 0 .../src/components/NFTOpenSea/index.ts | 0 .../src/components/NFTOpenSea/types.ts | 0 .../NFTOpenSea/useOpenSeaFulfillment.tsx | 0 .../components/NFTOpenSea/useOpenSeaOrder.tsx | 0 .../src/components/NFTOpenSea/utils.ts | 0 .../nft-checkout}/src/config.ts | 0 .../nft-checkout}/src/index.css | 0 .../nft-checkout}/src/index.tsx | 1 - examples/nft-checkout/src/reportWebVitals.ts | 13 + examples/nft-checkout/src/vite-env.d.ts | 1 + examples/nft-checkout/tsconfig.json | 10 + examples/nft-checkout/tsconfig.node.json | 10 + examples/nft-checkout/vite.config.ts | 32 ++ examples/vite-iframe/.env.example | 5 + examples/vite-iframe/index.html | 12 + examples/vite-iframe/package.json | 32 ++ examples/vite-iframe/src/App.tsx | 42 ++ .../src/components/WalletHeader.tsx | 63 +++ examples/vite-iframe/src/hooks/useChains.ts | 80 +++ examples/vite-iframe/src/main.tsx | 17 + .../src/providers/HostWalletProvider.tsx | 67 +++ examples/vite-iframe/src/vite-env.d.ts | 1 + examples/vite-iframe/src/widgetConfig.ts | 15 + examples/vite-iframe/tsconfig.json | 22 + examples/vite-iframe/tsconfig.node.json | 9 + examples/vite-iframe/vite.config.ts | 15 + packages/widget-embedded/.env | 1 - packages/widget-embedded/index.html | 4 +- packages/widget-embedded/package.json | 32 +- packages/widget-embedded/src/App.tsx | 89 ++- packages/widget-embedded/src/main.tsx | 54 ++ .../src/providers/WalletProvider.tsx | 48 ++ .../src/providers/WidgetConfigProvider.tsx | 44 ++ packages/widget-embedded/vite.config.ts | 16 - packages/widget-light/package.json | 69 +++ .../widget-light/src/guest/IframeProvider.ts | 346 ++++++++++++ .../widget-light/src/guest/iframeConnector.ts | 231 ++++++++ .../widget-light/src/host/WidgetLight.tsx | 55 ++ packages/widget-light/src/host/rpcHandler.ts | 272 +++++++++ .../src/host/useWidgetLightHost.ts | 204 +++++++ packages/widget-light/src/index.ts | 22 + packages/widget-light/src/shared/protocol.ts | 137 +++++ packages/widget-light/tsconfig.json | 20 + .../widget-provider-ethereum/package.json | 1 + packages/widget/package.json | 4 +- packages/widget/src/types/widget.ts | 6 +- pnpm-lock.yaml | 518 ++++++++---------- 61 files changed, 2443 insertions(+), 386 deletions(-) create mode 100644 examples/nft-checkout/.env create mode 100644 examples/nft-checkout/README.md create mode 100644 examples/nft-checkout/index.html create mode 100644 examples/nft-checkout/package.json create mode 100644 examples/nft-checkout/public/favicon.ico create mode 100644 examples/nft-checkout/public/favicon.png create mode 100644 examples/nft-checkout/public/logo192.png create mode 100644 examples/nft-checkout/public/logo512.png create mode 100644 examples/nft-checkout/public/manifest.json create mode 100644 examples/nft-checkout/public/robots.txt create mode 100644 examples/nft-checkout/src/App.tsx rename {packages/widget-embedded => examples/nft-checkout}/src/components/NFTOpenSea/NFTOpenSea.tsx (100%) rename {packages/widget-embedded => examples/nft-checkout}/src/components/NFTOpenSea/NFTOpenSeaSecondary.tsx (100%) rename {packages/widget-embedded => examples/nft-checkout}/src/components/NFTOpenSea/getEthersSigner.ts (100%) rename {packages/widget-embedded => examples/nft-checkout}/src/components/NFTOpenSea/index.ts (100%) rename {packages/widget-embedded => examples/nft-checkout}/src/components/NFTOpenSea/types.ts (100%) rename {packages/widget-embedded => examples/nft-checkout}/src/components/NFTOpenSea/useOpenSeaFulfillment.tsx (100%) rename {packages/widget-embedded => examples/nft-checkout}/src/components/NFTOpenSea/useOpenSeaOrder.tsx (100%) rename {packages/widget-embedded => examples/nft-checkout}/src/components/NFTOpenSea/utils.ts (100%) rename {packages/widget-embedded => examples/nft-checkout}/src/config.ts (100%) rename {packages/widget-embedded => examples/nft-checkout}/src/index.css (100%) rename {packages/widget-embedded => examples/nft-checkout}/src/index.tsx (95%) create mode 100644 examples/nft-checkout/src/reportWebVitals.ts create mode 100644 examples/nft-checkout/src/vite-env.d.ts create mode 100644 examples/nft-checkout/tsconfig.json create mode 100644 examples/nft-checkout/tsconfig.node.json create mode 100644 examples/nft-checkout/vite.config.ts create mode 100644 examples/vite-iframe/.env.example create mode 100644 examples/vite-iframe/index.html create mode 100644 examples/vite-iframe/package.json create mode 100644 examples/vite-iframe/src/App.tsx create mode 100644 examples/vite-iframe/src/components/WalletHeader.tsx create mode 100644 examples/vite-iframe/src/hooks/useChains.ts create mode 100644 examples/vite-iframe/src/main.tsx create mode 100644 examples/vite-iframe/src/providers/HostWalletProvider.tsx create mode 100644 examples/vite-iframe/src/vite-env.d.ts create mode 100644 examples/vite-iframe/src/widgetConfig.ts create mode 100644 examples/vite-iframe/tsconfig.json create mode 100644 examples/vite-iframe/tsconfig.node.json create mode 100644 examples/vite-iframe/vite.config.ts create mode 100644 packages/widget-embedded/src/main.tsx create mode 100644 packages/widget-embedded/src/providers/WalletProvider.tsx create mode 100644 packages/widget-embedded/src/providers/WidgetConfigProvider.tsx create mode 100644 packages/widget-light/package.json create mode 100644 packages/widget-light/src/guest/IframeProvider.ts create mode 100644 packages/widget-light/src/guest/iframeConnector.ts create mode 100644 packages/widget-light/src/host/WidgetLight.tsx create mode 100644 packages/widget-light/src/host/rpcHandler.ts create mode 100644 packages/widget-light/src/host/useWidgetLightHost.ts create mode 100644 packages/widget-light/src/index.ts create mode 100644 packages/widget-light/src/shared/protocol.ts create mode 100644 packages/widget-light/tsconfig.json diff --git a/examples/nft-checkout/.env b/examples/nft-checkout/.env new file mode 100644 index 000000000..15cb40c49 --- /dev/null +++ b/examples/nft-checkout/.env @@ -0,0 +1,3 @@ +VITE_OPENSEA_API_KEY=ee7460014fda4f58804f25c29a27df35 +VITE_WALLET_CONNECT=5432e3507d41270bee46b7b85bbc2ef8 + diff --git a/examples/nft-checkout/README.md b/examples/nft-checkout/README.md new file mode 100644 index 000000000..51e546b23 --- /dev/null +++ b/examples/nft-checkout/README.md @@ -0,0 +1,35 @@ +# LI.FI Widget NFT Checkout + +The demo of the LI.FI Widget NFT Checkout based on the OpenSea API. + +### How to run? + +``` +pnpm dev +``` + +### How to test? + +1. Find an NFT on the [OpenSea](https://opensea.io/). Please make sure it has an active listing and the test wallet has enough tokens to buy it. While we will be able to pay with any token in the process, the OpenSea SDK checks for the token in which the NFT is listed to generate transaction data. +2. Let's say we found this NFT https://opensea.io/assets/base/0x9e81df5258908dbeef4f841d0ab3816b10850426/2578 +3. We need to replace the `opensea.io/assets` part with `localhost:3000` or `widget.li.fi`, depending on the testing environment, so the final URL should look like this +http://localhost:3000/base/0x9e81df5258908dbeef4f841d0ab3816b10850426/2578 or this https://widget.li.fi/base/0x9e81df5258908dbeef4f841d0ab3816b10850426/2578 +4. Open the URL and make sure the test wallet is switched to the chain the NFT is on so OpenSea SDK can generate transaction data. +5. Select any token on any chain and pay for NFT. + +### Live Demo + +https://github.com/lifinance/widget/assets/18644653/af360181-3856-4276-b309-f923f476f40b + +#### Demo Transactions + +https://optimistic.etherscan.io/tx/0xa9f4e4304822cfe01808555b66e047761361c9e54b2387f93e23e9ffb92ba151 +https://polygonscan.com/tx/0x370682cbbc544e0ea258da774220b529a086c4b22941b924587cd2e0105579f6 + +### What does it look like? + + + +### Questions? + +Please don't hesitate to open an issue or contact us if you have any questions. diff --git a/examples/nft-checkout/index.html b/examples/nft-checkout/index.html new file mode 100644 index 000000000..043627a50 --- /dev/null +++ b/examples/nft-checkout/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + LI.FI Widget + + + + +
+ + + diff --git a/examples/nft-checkout/package.json b/examples/nft-checkout/package.json new file mode 100644 index 000000000..20b2db64d --- /dev/null +++ b/examples/nft-checkout/package.json @@ -0,0 +1,61 @@ +{ + "name": "@lifi/widget-embedded", + "version": "1.0.0", + "type": "module", + "scripts": { + "analyze": "source-map-explorer 'dist/assets/*.js' --no-border-checks", + "dev": "vite", + "build": "tsc && vite build", + "check:types": "tsc --noEmit", + "clean": "rm -rf dist tsconfig.tsbuildinfo", + "preview": "vite preview", + "pre-push:validate": "pnpm check:types" + }, + "author": "Eugene Chybisov ", + "dependencies": { + "@lifi/sdk": "^4.0.0-alpha.13", + "@lifi/wallet-management": "workspace:*", + "@lifi/widget": "workspace:*", + "@lifi/widget-provider-bitcoin": "workspace:*", + "@lifi/widget-provider-ethereum": "workspace:*", + "@lifi/widget-provider-solana": "workspace:*", + "@lifi/widget-provider-sui": "workspace:*", + "@mui/icons-material": "^7.3.6", + "@mui/material": "^7.3.6", + "@mui/system": "^7.3.6", + "@opensea/seaport-js": "4.0.6", + "@tanstack/react-query": "^5.90.20", + "bignumber.js": "^9.3.0", + "ethers": "^6.16.0", + "events": "^3.3.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router-dom": "^7.13.0", + "viem": "^2.45.1", + "wagmi": "^3.1.0" + }, + "devDependencies": { + "@esbuild-plugins/node-globals-polyfill": "^0.2.3", + "@vitejs/plugin-react-swc": "^4.2.3", + "source-map-explorer": "^2.5.3", + "typescript": "^5.9.3", + "vite": "^7.3.0", + "vite-plugin-node-polyfills": "0.25.0", + "web-vitals": "^5.1.0" + }, + "browserslist": { + "production": [ + "> 1%", + "last 2 versions", + "not dead", + "not IE > 0", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "private": true +} diff --git a/examples/nft-checkout/public/favicon.ico b/examples/nft-checkout/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..073f01ec86da23a9b70eb6d013d684cb60c366a8 GIT binary patch literal 15086 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN literal 0 HcmV?d00001 diff --git a/examples/nft-checkout/public/manifest.json b/examples/nft-checkout/public/manifest.json new file mode 100644 index 000000000..41431693d --- /dev/null +++ b/examples/nft-checkout/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "LI.FI Widget", + "name": "LI.FI Widget", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/nft-checkout/public/robots.txt b/examples/nft-checkout/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/examples/nft-checkout/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/nft-checkout/src/App.tsx b/examples/nft-checkout/src/App.tsx new file mode 100644 index 000000000..dafe26d38 --- /dev/null +++ b/examples/nft-checkout/src/App.tsx @@ -0,0 +1,59 @@ +import { LiFiWidget } from '@lifi/widget' +import { Box, CssBaseline } from '@mui/material' +import type { NFTNetwork } from './components/NFTOpenSea/index.js' +import { + NFTOpenSea, + NFTOpenSeaSecondary, + openSeaContractTool, +} from './components/NFTOpenSea/index.js' +import { widgetConfig } from './config.js' +import './index.css' + +export const App = () => { + const pathnameParams = window.location.pathname.substring(1).split('/') + + return ( + + + + + } + contractSecondaryComponent={ + + } + contractCompactComponent={ + + } + contractTool={openSeaContractTool} + config={widgetConfig} + integrator={widgetConfig.integrator} + open + /> + + + ) +} diff --git a/packages/widget-embedded/src/components/NFTOpenSea/NFTOpenSea.tsx b/examples/nft-checkout/src/components/NFTOpenSea/NFTOpenSea.tsx similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/NFTOpenSea.tsx rename to examples/nft-checkout/src/components/NFTOpenSea/NFTOpenSea.tsx diff --git a/packages/widget-embedded/src/components/NFTOpenSea/NFTOpenSeaSecondary.tsx b/examples/nft-checkout/src/components/NFTOpenSea/NFTOpenSeaSecondary.tsx similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/NFTOpenSeaSecondary.tsx rename to examples/nft-checkout/src/components/NFTOpenSea/NFTOpenSeaSecondary.tsx diff --git a/packages/widget-embedded/src/components/NFTOpenSea/getEthersSigner.ts b/examples/nft-checkout/src/components/NFTOpenSea/getEthersSigner.ts similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/getEthersSigner.ts rename to examples/nft-checkout/src/components/NFTOpenSea/getEthersSigner.ts diff --git a/packages/widget-embedded/src/components/NFTOpenSea/index.ts b/examples/nft-checkout/src/components/NFTOpenSea/index.ts similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/index.ts rename to examples/nft-checkout/src/components/NFTOpenSea/index.ts diff --git a/packages/widget-embedded/src/components/NFTOpenSea/types.ts b/examples/nft-checkout/src/components/NFTOpenSea/types.ts similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/types.ts rename to examples/nft-checkout/src/components/NFTOpenSea/types.ts diff --git a/packages/widget-embedded/src/components/NFTOpenSea/useOpenSeaFulfillment.tsx b/examples/nft-checkout/src/components/NFTOpenSea/useOpenSeaFulfillment.tsx similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/useOpenSeaFulfillment.tsx rename to examples/nft-checkout/src/components/NFTOpenSea/useOpenSeaFulfillment.tsx diff --git a/packages/widget-embedded/src/components/NFTOpenSea/useOpenSeaOrder.tsx b/examples/nft-checkout/src/components/NFTOpenSea/useOpenSeaOrder.tsx similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/useOpenSeaOrder.tsx rename to examples/nft-checkout/src/components/NFTOpenSea/useOpenSeaOrder.tsx diff --git a/packages/widget-embedded/src/components/NFTOpenSea/utils.ts b/examples/nft-checkout/src/components/NFTOpenSea/utils.ts similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/utils.ts rename to examples/nft-checkout/src/components/NFTOpenSea/utils.ts diff --git a/packages/widget-embedded/src/config.ts b/examples/nft-checkout/src/config.ts similarity index 100% rename from packages/widget-embedded/src/config.ts rename to examples/nft-checkout/src/config.ts diff --git a/packages/widget-embedded/src/index.css b/examples/nft-checkout/src/index.css similarity index 100% rename from packages/widget-embedded/src/index.css rename to examples/nft-checkout/src/index.css diff --git a/packages/widget-embedded/src/index.tsx b/examples/nft-checkout/src/index.tsx similarity index 95% rename from packages/widget-embedded/src/index.tsx rename to examples/nft-checkout/src/index.tsx index 34da85d6c..60611f65a 100644 --- a/packages/widget-embedded/src/index.tsx +++ b/examples/nft-checkout/src/index.tsx @@ -45,6 +45,5 @@ root.render( // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals if (import.meta.env.DEV) { - // biome-ignore lint/suspicious/noConsole: allowed in dev reportWebVitals(console.log) } diff --git a/examples/nft-checkout/src/reportWebVitals.ts b/examples/nft-checkout/src/reportWebVitals.ts new file mode 100644 index 000000000..0cf1bb720 --- /dev/null +++ b/examples/nft-checkout/src/reportWebVitals.ts @@ -0,0 +1,13 @@ +import type { MetricType } from 'web-vitals' + +export const reportWebVitals = (onPerfEntry?: (metric: MetricType) => void) => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => { + onCLS(onPerfEntry) + onINP(onPerfEntry) + onFCP(onPerfEntry) + onLCP(onPerfEntry) + onTTFB(onPerfEntry) + }) + } +} diff --git a/examples/nft-checkout/src/vite-env.d.ts b/examples/nft-checkout/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/examples/nft-checkout/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/nft-checkout/tsconfig.json b/examples/nft-checkout/tsconfig.json new file mode 100644 index 000000000..bd4ab8766 --- /dev/null +++ b/examples/nft-checkout/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", + "outDir": "dist", + "rootDir": "./src", + "composite": true + }, + "include": ["src"] +} diff --git a/examples/nft-checkout/tsconfig.node.json b/examples/nft-checkout/tsconfig.node.json new file mode 100644 index 000000000..9617166fd --- /dev/null +++ b/examples/nft-checkout/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "composite": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/nft-checkout/vite.config.ts b/examples/nft-checkout/vite.config.ts new file mode 100644 index 000000000..1dfef3f6a --- /dev/null +++ b/examples/nft-checkout/vite.config.ts @@ -0,0 +1,32 @@ +import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill' +import react from '@vitejs/plugin-react-swc' +import { defineConfig } from 'vite' +import { nodePolyfills } from 'vite-plugin-node-polyfills' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [nodePolyfills(), react()], + esbuild: { + target: 'esnext', + }, + build: { + sourcemap: true, + }, + optimizeDeps: { + esbuildOptions: { + define: { + global: 'globalThis', + }, + plugins: [ + NodeGlobalsPolyfillPlugin({ + process: true, + buffer: true, + }), + ], + }, + }, + server: { + port: 3000, + open: true, + }, +}) diff --git a/examples/vite-iframe/.env.example b/examples/vite-iframe/.env.example new file mode 100644 index 000000000..a670cc765 --- /dev/null +++ b/examples/vite-iframe/.env.example @@ -0,0 +1,5 @@ +# Copy this file to .env and fill in your values. + +# WalletConnect project ID (optional — injected wallet works without it) +# Get one at https://cloud.walletconnect.com/ +VITE_WALLET_CONNECT_PROJECT_ID= diff --git a/examples/vite-iframe/index.html b/examples/vite-iframe/index.html new file mode 100644 index 000000000..b0ef8f513 --- /dev/null +++ b/examples/vite-iframe/index.html @@ -0,0 +1,12 @@ + + + + + + Widget Light — Host + + +
+ + + diff --git a/examples/vite-iframe/package.json b/examples/vite-iframe/package.json new file mode 100644 index 000000000..cb544c2a9 --- /dev/null +++ b/examples/vite-iframe/package.json @@ -0,0 +1,32 @@ +{ + "name": "vite-iframe", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "analyze": "source-map-explorer 'dist/assets/*.js' --no-border-checks", + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@lifi/widget-light": "workspace:*", + "@lifi/widget-provider-ethereum": "workspace:*", + "@mui/material": "^7.3.6", + "@tanstack/react-query": "^5.90.20", + "@wagmi/connectors": "^7.1.6", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "viem": "^2.45.1", + "wagmi": "^3.4.2" + }, + "devDependencies": { + "@types/node": "^25.2.1", + "@types/react": "^19.2.13", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.3", + "typescript": "^5.9.3", + "vite": "^7.3.0", + "vite-plugin-node-polyfills": "^0.25.0" + } +} diff --git a/examples/vite-iframe/src/App.tsx b/examples/vite-iframe/src/App.tsx new file mode 100644 index 000000000..16d0f3855 --- /dev/null +++ b/examples/vite-iframe/src/App.tsx @@ -0,0 +1,42 @@ +import { LiFiWidgetLight } from '@lifi/widget-light' +import { Box, Typography } from '@mui/material' +import { useConnection } from 'wagmi' +import { WalletHeader } from './components/WalletHeader' +import { widgetConfig } from './widgetConfig' + +const WIDGET_ORIGIN = 'http://localhost:3000' +const WIDGET_URL = WIDGET_ORIGIN + +export function HostApp() { + // wagmi v3: useAccount → useConnection + const { isConnected } = useConnection() + + return ( + + + + + {!isConnected && ( + + Connect your wallet above — all widget transactions will be signed + through your connected wallet. + + )} + + + + + ) +} diff --git a/examples/vite-iframe/src/components/WalletHeader.tsx b/examples/vite-iframe/src/components/WalletHeader.tsx new file mode 100644 index 000000000..6ac0f925c --- /dev/null +++ b/examples/vite-iframe/src/components/WalletHeader.tsx @@ -0,0 +1,63 @@ +import { Box, Button, Chip, Typography } from '@mui/material' +import { useConnect, useConnection, useConnectors, useDisconnect } from 'wagmi' + +export function WalletHeader() { + // wagmi v3: useAccount → useConnection + const { address, isConnected, chainId } = useConnection() + + // wagmi v3: mutateAsync replaces connectAsync; connectors removed from useConnect + const connect = useConnect() + + // wagmi v3: connectors moved to dedicated useConnectors hook + const connectors = useConnectors() + + // wagmi v3: mutate replaces disconnect + const { mutate: disconnect } = useDisconnect() + + return ( + + + Widget Light — Host + + + + {isConnected && chainId && ( + + )} + {address && ( + + {`${address.slice(0, 6)}…${address.slice(-4)}`} + + )} + {!isConnected ? ( + + {connectors.map((connector) => ( + + ))} + + ) : ( + + )} + + + ) +} diff --git a/examples/vite-iframe/src/hooks/useChains.ts b/examples/vite-iframe/src/hooks/useChains.ts new file mode 100644 index 000000000..d18c8a88a --- /dev/null +++ b/examples/vite-iframe/src/hooks/useChains.ts @@ -0,0 +1,80 @@ +import { useQuery } from '@tanstack/react-query' +import type { Chain } from 'viem' +import { mainnet } from 'viem/chains' + +const LIFI_API_URL = 'https://li.quest/v1' + +interface LiFiChain { + id: number + chainType: string + metamask: { + chainId: string + chainName: string + rpcUrls: string[] + blockExplorerUrls: string[] + nativeCurrency: { + name: string + symbol: string + decimals: number + } + } + multicallAddress?: string +} + +interface LiFiChainsResponse { + chains: LiFiChain[] +} + +function toViemChain(chain: LiFiChain): Chain { + const viemChain: Chain = { + id: chain.id, + name: chain.metamask.chainName, + nativeCurrency: chain.metamask.nativeCurrency, + rpcUrls: { + default: { http: chain.metamask.rpcUrls }, + }, + blockExplorers: chain.metamask.blockExplorerUrls.length + ? { + default: { + name: chain.metamask.blockExplorerUrls[0], + url: chain.metamask.blockExplorerUrls[0], + }, + } + : undefined, + contracts: chain.multicallAddress + ? { multicall3: { address: chain.multicallAddress as `0x${string}` } } + : undefined, + } + + // Preserve mainnet ENS contracts + if (chain.id === mainnet.id) { + viemChain.contracts = { ...mainnet.contracts, ...viemChain.contracts } + } + + return viemChain +} + +/** + * Fetches EVM chains from the LI.FI API and converts them to viem `Chain` + * objects. No dependency on `@lifi/sdk` or `@lifi/widget`. + */ +export function useChains(): { + chains: Chain[] | undefined + isLoading: boolean +} { + const { data, isLoading } = useQuery({ + queryKey: ['lifi-chains'], + queryFn: async (): Promise => { + const res = await fetch(`${LIFI_API_URL}/chains?chainTypes=EVM`) + if (!res.ok) { + throw new Error(`Failed to fetch chains: ${res.status}`) + } + const json: LiFiChainsResponse = await res.json() + return json.chains.map(toViemChain) + }, + refetchInterval: 300_000, + staleTime: 300_000, + }) + + return { chains: data, isLoading } +} diff --git a/examples/vite-iframe/src/main.tsx b/examples/vite-iframe/src/main.tsx new file mode 100644 index 000000000..5282916ce --- /dev/null +++ b/examples/vite-iframe/src/main.tsx @@ -0,0 +1,17 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import React from 'react' +import ReactDOM from 'react-dom/client' +import { HostApp } from './App' +import { HostWalletProvider } from './providers/HostWalletProvider' + +const queryClient = new QueryClient() + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + +) diff --git a/examples/vite-iframe/src/providers/HostWalletProvider.tsx b/examples/vite-iframe/src/providers/HostWalletProvider.tsx new file mode 100644 index 000000000..066a51fba --- /dev/null +++ b/examples/vite-iframe/src/providers/HostWalletProvider.tsx @@ -0,0 +1,67 @@ +import { injected, walletConnect } from '@wagmi/connectors' +import { type FC, type PropsWithChildren, useEffect, useRef } from 'react' +import { createClient, http } from 'viem' +import { mainnet } from 'viem/chains' +import type { Config } from 'wagmi' +import { createConfig, WagmiProvider } from 'wagmi' +import { reconnect } from 'wagmi/actions' +import { useChains } from '../hooks/useChains' + +const projectId = import.meta.env.VITE_WALLET_CONNECT_PROJECT_ID as + | string + | undefined + +const connectors = [ + injected(), + ...(projectId ? [walletConnect({ projectId })] : []), +] + +/** + * Host-side WalletProvider. + * + * Uses standard injected + WalletConnect connectors — the parent window owns + * the real wallet. EVM chains are fetched directly from the LI.FI API so that + * wallet_switchEthereumChain requests forwarded by the guest can succeed. + */ +export const HostWalletProvider: FC = ({ children }) => { + const { chains } = useChains() + const wagmi = useRef(null) + + if (!wagmi.current) { + wagmi.current = createConfig({ + chains: [mainnet], + client({ chain }) { + return createClient({ chain, transport: http() }) + }, + multiInjectedProviderDiscovery: true, + ssr: false, + }) + } + + useEffect(() => { + if (!chains?.length || !wagmi.current) { + return + } + const typed = chains as unknown as readonly [ + (typeof chains)[0], + ...(typeof chains)[number][], + ] + wagmi.current._internal.chains.setState(typed) + wagmi.current._internal.connectors.setState(() => + [ + ...connectors, + ...(wagmi + .current!._internal.mipd?.getProviders() + .map(wagmi.current!._internal.connectors.providerDetailToConnector) ?? + []), + ].map(wagmi.current!._internal.connectors.setup) + ) + reconnect(wagmi.current) + }, [chains]) + + return ( + + {children} + + ) +} diff --git a/examples/vite-iframe/src/vite-env.d.ts b/examples/vite-iframe/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/examples/vite-iframe/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/vite-iframe/src/widgetConfig.ts b/examples/vite-iframe/src/widgetConfig.ts new file mode 100644 index 000000000..c795a552d --- /dev/null +++ b/examples/vite-iframe/src/widgetConfig.ts @@ -0,0 +1,15 @@ +import type { WidgetLightConfig } from '@lifi/widget-light' + +/** + * Widget configuration sent to the iframe. + * Must be JSON-serialisable — no React nodes or callback functions. + */ +export const widgetConfig: WidgetLightConfig = { + integrator: 'vite-iframe-example', + theme: { + container: { + border: '1px solid rgb(234, 234, 234)', + borderRadius: '16px', + }, + }, +} diff --git a/examples/vite-iframe/tsconfig.json b/examples/vite-iframe/tsconfig.json new file mode 100644 index 000000000..bb0095e8e --- /dev/null +++ b/examples/vite-iframe/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "allowJs": false, + "allowSyntheticDefaultImports": true, + "esModuleInterop": false, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "types": ["vite/client"], + "useDefineForClassFields": true + }, + "include": ["src", "vite.config.ts"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/vite-iframe/tsconfig.node.json b/examples/vite-iframe/tsconfig.node.json new file mode 100644 index 000000000..16dfedc6a --- /dev/null +++ b/examples/vite-iframe/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/vite-iframe/vite.config.ts b/examples/vite-iframe/vite.config.ts new file mode 100644 index 000000000..65d5aa6d3 --- /dev/null +++ b/examples/vite-iframe/vite.config.ts @@ -0,0 +1,15 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +// Host-only Vite app. +// The guest (iframe) page is served by @lifi/widget-embedded (port 3000). +export default defineConfig({ + plugins: [react()], + esbuild: { + target: 'esnext', + }, + server: { + port: 4000, + open: true, + }, +}) diff --git a/packages/widget-embedded/.env b/packages/widget-embedded/.env index 15cb40c49..285f81c83 100644 --- a/packages/widget-embedded/.env +++ b/packages/widget-embedded/.env @@ -1,3 +1,2 @@ -VITE_OPENSEA_API_KEY=ee7460014fda4f58804f25c29a27df35 VITE_WALLET_CONNECT=5432e3507d41270bee46b7b85bbc2ef8 diff --git a/packages/widget-embedded/index.html b/packages/widget-embedded/index.html index 043627a50..40409751a 100644 --- a/packages/widget-embedded/index.html +++ b/packages/widget-embedded/index.html @@ -1,4 +1,4 @@ - + @@ -18,6 +18,6 @@
- + diff --git a/packages/widget-embedded/package.json b/packages/widget-embedded/package.json index 20b2db64d..47a4fa86e 100644 --- a/packages/widget-embedded/package.json +++ b/packages/widget-embedded/package.json @@ -3,7 +3,6 @@ "version": "1.0.0", "type": "module", "scripts": { - "analyze": "source-map-explorer 'dist/assets/*.js' --no-border-checks", "dev": "vite", "build": "tsc && vite build", "check:types": "tsc --noEmit", @@ -16,46 +15,25 @@ "@lifi/sdk": "^4.0.0-alpha.13", "@lifi/wallet-management": "workspace:*", "@lifi/widget": "workspace:*", + "@lifi/widget-light": "workspace:*", "@lifi/widget-provider-bitcoin": "workspace:*", "@lifi/widget-provider-ethereum": "workspace:*", "@lifi/widget-provider-solana": "workspace:*", "@lifi/widget-provider-sui": "workspace:*", - "@mui/icons-material": "^7.3.6", - "@mui/material": "^7.3.6", - "@mui/system": "^7.3.6", - "@opensea/seaport-js": "4.0.6", "@tanstack/react-query": "^5.90.20", - "bignumber.js": "^9.3.0", - "ethers": "^6.16.0", - "events": "^3.3.0", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-router-dom": "^7.13.0", "viem": "^2.45.1", - "wagmi": "^3.1.0" + "wagmi": "^3.4.2" }, "devDependencies": { - "@esbuild-plugins/node-globals-polyfill": "^0.2.3", + "@types/react": "^19.2.13", + "@types/react-dom": "^19.2.3", "@vitejs/plugin-react-swc": "^4.2.3", - "source-map-explorer": "^2.5.3", "typescript": "^5.9.3", "vite": "^7.3.0", - "vite-plugin-node-polyfills": "0.25.0", + "vite-plugin-node-polyfills": "^0.25.0", "web-vitals": "^5.1.0" }, - "browserslist": { - "production": [ - "> 1%", - "last 2 versions", - "not dead", - "not IE > 0", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, "private": true } diff --git a/packages/widget-embedded/src/App.tsx b/packages/widget-embedded/src/App.tsx index dafe26d38..6ecb33f16 100644 --- a/packages/widget-embedded/src/App.tsx +++ b/packages/widget-embedded/src/App.tsx @@ -1,59 +1,42 @@ import { LiFiWidget } from '@lifi/widget' -import { Box, CssBaseline } from '@mui/material' -import type { NFTNetwork } from './components/NFTOpenSea/index.js' -import { - NFTOpenSea, - NFTOpenSeaSecondary, - openSeaContractTool, -} from './components/NFTOpenSea/index.js' -import { widgetConfig } from './config.js' -import './index.css' +import { EthereumProvider } from '@lifi/widget-provider-ethereum' +import { useEmbeddedWidgetConfig } from './providers/WidgetConfigProvider.js' -export const App = () => { - const pathnameParams = window.location.pathname.substring(1).split('/') +/** + * Providers injected locally — these are React component factories that cannot + * be serialised over postMessage. Only EthereumProvider is included because the + * host bridge (`useWidgetLightHost`) is EVM-only (wagmi WalletClient / PublicClient). + * + * EthereumProvider detects the existing WagmiContext supplied by WalletProvider + * and uses it as an external context, so the widgetLightIframe connector is + * picked up automatically without creating a second wagmi instance. + */ +const IFRAME_PROVIDERS = [EthereumProvider()] + +/** + * Guest (iframe) app. + * + * Connection lifecycle: + * 1. WidgetConfigProvider listens for INIT and stores the widget config. + * 2. WalletProvider creates a wagmi config with widgetLightIframe() and + * syncs chains from the LI.FI API via useSyncWagmiConfig. + * 3. The connector's setup() creates the IframeProvider which sends READY + * to the host and awaits INIT with accounts + chainId. + * 4. When INIT arrives the connector emits wagmi 'connect' and the widget + * config context updates, rendering this component. + */ +export function App() { + const widgetConfig = useEmbeddedWidgetConfig() + + if (!widgetConfig) { + return null + } return ( - - - - - } - contractSecondaryComponent={ - - } - contractCompactComponent={ - - } - contractTool={openSeaContractTool} - config={widgetConfig} - integrator={widgetConfig.integrator} - open - /> - - + ) } diff --git a/packages/widget-embedded/src/main.tsx b/packages/widget-embedded/src/main.tsx new file mode 100644 index 000000000..3394d51b0 --- /dev/null +++ b/packages/widget-embedded/src/main.tsx @@ -0,0 +1,54 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { App } from './App.js' +import { WalletProvider } from './providers/WalletProvider.js' +import { WidgetConfigProvider } from './providers/WidgetConfigProvider.js' +import { reportWebVitals } from './reportWebVitals.js' + +const rootElement = document.getElementById('root') +if (!rootElement) { + throw new Error('Failed to find the root element.') +} + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + enabled: true, + refetchInterval: false, + refetchIntervalInBackground: false, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + refetchOnMount: true, + retryOnMount: true, + // suspense: true, + }, + mutations: { + onError: (_error) => { + // + }, + }, + }, +}) + +const root = createRoot(rootElement) + +root.render( + + + + + + + + + +) + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +if (import.meta.env.DEV) { + // biome-ignore lint/suspicious/noConsole: allowed in dev + reportWebVitals(console.log) +} diff --git a/packages/widget-embedded/src/providers/WalletProvider.tsx b/packages/widget-embedded/src/providers/WalletProvider.tsx new file mode 100644 index 000000000..d7f6eea6f --- /dev/null +++ b/packages/widget-embedded/src/providers/WalletProvider.tsx @@ -0,0 +1,48 @@ +import type { WidgetConfig } from '@lifi/widget' +import { useWidgetChains } from '@lifi/widget' +import { widgetLightIframe } from '@lifi/widget-light' +import { useSyncWagmiConfig } from '@lifi/widget-provider-ethereum' +import { type FC, type PropsWithChildren, useMemo, useRef } from 'react' +import { createClient, http } from 'viem' +import { mainnet } from 'viem/chains' +import type { Config, CreateConnectorFn } from 'wagmi' +import { createConfig, WagmiProvider } from 'wagmi' +import { useEmbeddedWidgetConfig } from './WidgetConfigProvider.js' + +const FALLBACK_CONFIG: Partial = { + integrator: 'widget-embedded', +} + +export const WalletProvider: FC = ({ children }) => { + const widgetConfig = useEmbeddedWidgetConfig() + const { chains } = useWidgetChains( + (widgetConfig ?? FALLBACK_CONFIG) as WidgetConfig + ) + + const iframeConnectorFn = useRef(null) + if (!iframeConnectorFn.current) { + iframeConnectorFn.current = widgetLightIframe() + } + const connectors = useMemo(() => [iframeConnectorFn.current!], []) + + const wagmi = useRef(null) + if (!wagmi.current) { + wagmi.current = createConfig({ + chains: [mainnet], + client({ chain }) { + return createClient({ chain, transport: http() }) + }, + connectors: [iframeConnectorFn.current], + multiInjectedProviderDiscovery: false, + ssr: false, + }) + } + + useSyncWagmiConfig(wagmi.current, connectors, chains) + + return ( + + {children} + + ) +} diff --git a/packages/widget-embedded/src/providers/WidgetConfigProvider.tsx b/packages/widget-embedded/src/providers/WidgetConfigProvider.tsx new file mode 100644 index 000000000..2097e333e --- /dev/null +++ b/packages/widget-embedded/src/providers/WidgetConfigProvider.tsx @@ -0,0 +1,44 @@ +import type { WidgetConfig } from '@lifi/widget' +import { WIDGET_LIGHT_SOURCE } from '@lifi/widget-light' +import { + createContext, + type FC, + type PropsWithChildren, + useContext, + useEffect, + useState, +} from 'react' + +const DEV_CONFIG: Partial = { + integrator: 'widget-embedded-dev', +} + +const WidgetConfigContext = createContext | null>(null) + +export const useEmbeddedWidgetConfig = () => useContext(WidgetConfigContext) + +export const WidgetConfigProvider: FC = ({ children }) => { + const [config, setConfig] = useState | null>( + import.meta.env.DEV ? DEV_CONFIG : null + ) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const msg = event.data + if (!msg || msg.source !== WIDGET_LIGHT_SOURCE) { + return + } + if (msg.type === 'INIT' && msg.config) { + setConfig(msg.config as unknown as Partial) + } + } + window.addEventListener('message', handleMessage) + return () => window.removeEventListener('message', handleMessage) + }, []) + + return ( + + {children} + + ) +} diff --git a/packages/widget-embedded/vite.config.ts b/packages/widget-embedded/vite.config.ts index 1dfef3f6a..e15efcdd1 100644 --- a/packages/widget-embedded/vite.config.ts +++ b/packages/widget-embedded/vite.config.ts @@ -1,9 +1,7 @@ -import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill' import react from '@vitejs/plugin-react-swc' import { defineConfig } from 'vite' import { nodePolyfills } from 'vite-plugin-node-polyfills' -// https://vitejs.dev/config/ export default defineConfig({ plugins: [nodePolyfills(), react()], esbuild: { @@ -12,21 +10,7 @@ export default defineConfig({ build: { sourcemap: true, }, - optimizeDeps: { - esbuildOptions: { - define: { - global: 'globalThis', - }, - plugins: [ - NodeGlobalsPolyfillPlugin({ - process: true, - buffer: true, - }), - ], - }, - }, server: { port: 3000, - open: true, }, }) diff --git a/packages/widget-light/package.json b/packages/widget-light/package.json new file mode 100644 index 000000000..8608bad24 --- /dev/null +++ b/packages/widget-light/package.json @@ -0,0 +1,69 @@ +{ + "name": "@lifi/widget-light", + "version": "4.0.0-alpha.0", + "description": "LI.FI Widget Light - a lightweight version of the LI.FI Widget for cross-chain bridging and swapping.", + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.d.ts", + "sideEffects": false, + "scripts": { + "watch": "tsc -w -p ./tsconfig.json", + "build": "pnpm clean && pnpm build:version && pnpm build:esm && pnpm build:clean", + "build:esm": "tsc --build", + "build:prerelease": "node ../../scripts/prerelease.js && cpy '../../*.md' .", + "build:postrelease": "node ../../scripts/postrelease.js && rm -rf *.md", + "build:version": "node ../../scripts/version.js", + "build:clean": "rm -rf tsconfig.tsbuildinfo ./dist/tsconfig.tsbuildinfo", + "release:build": "pnpm build", + "clean": "pnpm build:clean && rm -rf dist", + "check:types": "tsc --noEmit", + "check:circular-deps": "madge --circular $(find ./src -name '*.ts' -o -name '*.tsx')", + "check:circular-deps-graph": "madge --circular $(find ./src -name '*.ts' -o -name '*.tsx') --image graph.svg", + "test": "vitest run" + }, + "author": "Eugene Chybisov ", + "homepage": "https://github.com/lifinance/widget", + "repository": { + "type": "git", + "url": "https://github.com/lifinance/widget.git", + "directory": "packages/widget" + }, + "bugs": { + "url": "https://github.com/lifinance/widget/issues" + }, + "license": "Apache-2.0", + "keywords": [ + "widget", + "lifi-widget", + "bridge", + "swap", + "cross-chain", + "multi-chain", + "ethereum", + "solana", + "bitcoin", + "sui", + "web3", + "iframe", + "lifi" + ], + "dependencies": {}, + "devDependencies": { + "madge": "^8.0.0", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + }, + "peerDependencies": { + "react": ">=18", + "wagmi": "^3", + "@wagmi/core": "^3", + "viem": "^2" + }, + "peerDependenciesMeta": {}, + "files": [ + "dist/**", + "src/**", + "!tsconfig.json", + "!*.tmp" + ] +} diff --git a/packages/widget-light/src/guest/IframeProvider.ts b/packages/widget-light/src/guest/IframeProvider.ts new file mode 100644 index 000000000..6b9169c6d --- /dev/null +++ b/packages/widget-light/src/guest/IframeProvider.ts @@ -0,0 +1,346 @@ +import type { + HostMessage, + RpcError, + WidgetLightConfig, +} from '../shared/protocol.js' +import { WIDGET_LIGHT_SOURCE } from '../shared/protocol.js' + +// --------------------------------------------------------------------------- +// Minimal EventEmitter — avoids any dependency on Node.js `events` or +// `eventemitter3`. Satisfies the EIP-1193 provider event interface. +// --------------------------------------------------------------------------- +type Listener = (...args: unknown[]) => void + +class MinimalEventEmitter { + private readonly _listeners = new Map>() + + on(event: string, listener: Listener): this { + let set = this._listeners.get(event) + if (!set) { + set = new Set() + this._listeners.set(event, set) + } + set.add(listener) + return this + } + + removeListener(event: string, listener: Listener): this { + this._listeners.get(event)?.delete(listener) + return this + } + + emit(event: string, ...args: unknown[]): boolean { + const set = this._listeners.get(event) + if (!set || set.size === 0) { + return false + } + for (const listener of set) { + listener(...args) + } + return true + } + + removeAllListeners(event?: string): this { + if (event) { + this._listeners.delete(event) + } else { + this._listeners.clear() + } + return this + } +} + +interface PendingRequest { + resolve: (value: unknown) => void + reject: (reason: Error) => void +} + +/** + * EIP-1193 compatible provider that bridges the iframe guest to the parent + * window via postMessage. + * + * Lifecycle: + * 1. Constructor sends READY to the parent. + * 2. Parent responds with INIT (config + initial accounts/chainId). + * 3. All subsequent request() calls are either handled locally (eth_accounts, + * eth_chainId, net_version) or forwarded to the parent as RPC_REQUEST. + * 4. Parent pushes EVENT messages for EIP-1193 events (accountsChanged, etc.). + * + * EIP-5792 methods (wallet_sendCalls, wallet_getCallsStatus, etc.) are + * forwarded transparently to the parent just like any other RPC method. + */ +export class IframeProvider extends MinimalEventEmitter { + private readonly pendingRequests = new Map() + + /** Resolves once the INIT message arrives from the parent. */ + private readonly initPromise: Promise + private initResolve!: () => void + + /** Cached from INIT / EVENT messages — avoids a round-trip for common reads. */ + private accounts: string[] = [] + private _chainIdHex: `0x${string}` = '0x1' + + /** Current chain ID as a hex string, kept in sync via EVENT messages. */ + get chainIdHex(): `0x${string}` { + return this._chainIdHex + } + + /** + * The origin we accept further messages from. + * Set to event.origin of the first INIT message — prevents spoofing. + */ + private trustedOrigin = '*' + + /** Config received from the parent during INIT. */ + private _config: WidgetLightConfig | null = null + + constructor() { + super() + + this.initPromise = new Promise((resolve) => { + this.initResolve = resolve + }) + + window.addEventListener('message', this.handleMessage) + this.sendReadyWithRetry() + + // Safety valve: if INIT never arrives (e.g. host not running), resolve + // with empty state after 5 s so wagmi's reconnect doesn't block forever. + setTimeout(() => { + if (this._config === null) { + this.initResolve() + } + }, 5_000) + } + + /** + * Sends READY to the parent and retries every 250 ms until INIT arrives. + * + * This handles the race where the iframe's JavaScript initialises faster + * than the host's React useEffect sets up its message handler. Without + * retries, the single READY could be sent into the void and the iframe + * would wait forever for INIT. + */ + private sendReadyWithRetry(): void { + const sendReady = () => { + window.parent.postMessage( + { source: WIDGET_LIGHT_SOURCE, type: 'READY' }, + '*' + ) + } + + sendReady() + + const interval = setInterval(() => { + if (this._config !== null) { + clearInterval(interval) + return + } + sendReady() + }, 250) + + // Give up after 30 s — at that point the user will have to reload anyway. + setTimeout(() => clearInterval(interval), 30_000) + } + + /** Returns the config received from the parent, or null before INIT. */ + get config(): WidgetLightConfig | null { + return this._config + } + + /** Waits until the INIT message has been received. */ + waitForInit(): Promise { + return this.initPromise + } + + /** + * EIP-1193 `request()` implementation. + * + * Common read methods (eth_accounts, eth_chainId, net_version) are served + * from local cache to avoid latency. Everything else (including EIP-5792 + * wallet_sendCalls / wallet_getCallsStatus / wallet_getCapabilities) is + * forwarded to the parent as a RPC_REQUEST and awaits its RPC_RESPONSE. + */ + async request({ + method, + params, + }: { + method: string + params?: unknown[] + }): Promise { + // Block until INIT arrives so we have accounts/chainId + await this.initPromise + + switch (method) { + case 'eth_accounts': + case 'eth_requestAccounts': + return this.accounts + + case 'eth_chainId': + return this.chainIdHex + + case 'net_version': + return String(parseInt(this.chainIdHex, 16)) + + default: + return this.forwardToParent(method, params) + } + } + + // --------------------------------------------------------------------------- + // Private + // --------------------------------------------------------------------------- + + private forwardToParent( + method: string, + params?: unknown[] + ): Promise { + return new Promise((resolve, reject) => { + const id = crypto.randomUUID() + this.pendingRequests.set(id, { resolve, reject }) + window.parent.postMessage( + { + source: WIDGET_LIGHT_SOURCE, + type: 'RPC_REQUEST', + id, + method, + params, + }, + this.trustedOrigin + ) + }) + } + + private readonly handleMessage = (event: MessageEvent): void => { + // After INIT we only accept messages from the trusted origin + if (this.trustedOrigin !== '*' && event.origin !== this.trustedOrigin) { + return + } + // Ignore messages that don't come from the parent frame + if (event.source !== window.parent) { + return + } + + const msg = event.data as HostMessage + if (!msg || msg.source !== WIDGET_LIGHT_SOURCE) { + return + } + + switch (msg.type) { + case 'INIT': { + // Pin the trusted origin on first contact + this.trustedOrigin = event.origin + this._config = msg.config + this.accounts = msg.accounts + this._chainIdHex = `0x${msg.chainId.toString(16)}` + + // Resolve pending init so request() calls can proceed + this.initResolve() + + // Start reporting content size to the host now that we have a trusted + // origin. The host uses these to auto-resize the