@tangle-network/blueprint-ui 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-ZKICSKZH.js +57 -0
- package/dist/chunk-ZKICSKZH.js.map +1 -0
- package/dist/detectParentOrigin-BYruoIdc.d.ts +26 -0
- package/dist/iframe/index.d.ts +145 -0
- package/dist/iframe/index.js +557 -0
- package/dist/iframe/index.js.map +1 -0
- package/dist/iframe/testing-index.d.ts +82 -0
- package/dist/iframe/testing-index.js +535 -0
- package/dist/iframe/testing-index.js.map +1 -0
- package/dist/parentBridgeProtocol-BS2zbIvX.d.ts +194 -0
- package/dist/styles.css +3 -0
- package/dist/tangleIframeClient-DES8FDF0.d.ts +121 -0
- package/dist/wallet/index.d.ts +10 -109
- package/dist/wallet/index.js +14 -47
- package/dist/wallet/index.js.map +1 -1
- package/package.json +11 -1
- package/src/iframe/TangleIframeProvider.tsx +172 -0
- package/src/iframe/hooks.ts +234 -0
- package/src/iframe/index.ts +77 -0
- package/src/iframe/tangleIframeClient.test.ts +317 -0
- package/src/iframe/tangleIframeClient.ts +483 -0
- package/src/iframe/testing-index.ts +15 -0
- package/src/iframe/testing.tsx +710 -0
- package/src/wallet/index.ts +11 -0
- package/src/wallet/parentBridgeProtocol.ts +150 -1
- package/src/wallet/parentBridgeProvider.ts +17 -1
package/dist/wallet/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/wallet/detectParentOrigin.ts","../../src/wallet/parentBridgeConnector.ts","../../src/wallet/parentBridgeProtocol.ts","../../src/wallet/parentBridgeProvider.ts"],"sourcesContent":["// Determine which origin to trust as the parent dapp.\n//\n// `document.referrer` is the *initial* embedder — it's set when the iframe is\n// first loaded and survives reloads (though it can be cleared by `referrerpolicy`\n// or by the embedder). The Tangle Cloud iframe wrapper deliberately omits\n// `referrerpolicy=\"no-referrer\"` so we get the embedder's origin here.\n//\n// We compare it against an allowlist of known Tangle Cloud origins. If it\n// matches, that's the parent. Otherwise the iframe is being loaded directly\n// (standalone domain visit, dev server, untrusted embedder) and the bridge\n// stays disabled — the app falls back to its normal injected/WC wallet path.\n\n/**\n * Default Tangle Cloud origins. Consumers (agent-sandbox UI,\n * trading-arena, future iframe blueprints) pass app-specific additions\n * via `extraOrigins` rather than mutating this list.\n */\nexport const TANGLE_CLOUD_ORIGINS_DEFAULT = Object.freeze([\n 'https://cloud.tangle.tools',\n 'https://develop.cloud.tangle.tools',\n // Local dev (Vite default port for tangle-cloud + Netlify dev preview).\n 'http://localhost:4300',\n 'http://localhost:8888',\n] as const);\n\nfunction originFromReferrer(): string | null {\n if (typeof document === 'undefined') return null;\n const ref = document.referrer;\n if (!ref) return null;\n try {\n return new URL(ref).origin;\n } catch {\n return null;\n }\n}\n\n/**\n * Returns the parent origin to bridge to, or null when no trusted parent is\n * detected. Caller should skip installing the bridge connector when this\n * returns null.\n *\n * `extraOrigins` is the application's escape hatch for staging or dev\n * deploys not covered by the default list. The library deliberately does\n * not read environment variables itself (consumers may bundle for non-Vite\n * runtimes); the consuming app threads `import.meta.env.VITE_*` or\n * `process.env.*` in itself.\n *\n * Falls back to a `?parent=<origin>` query parameter when no referrer is\n * present (some browsers strip referrer from cross-origin loads). Useful\n * for dev embedding flows.\n */\nexport function detectTangleCloudParentOrigin(\n options: { extraOrigins?: readonly string[] } = {},\n): string | null {\n if (typeof window === 'undefined' || window.parent === window) {\n return null;\n }\n const allowlist = new Set<string>([\n ...TANGLE_CLOUD_ORIGINS_DEFAULT,\n ...(options.extraOrigins ?? []),\n ]);\n const referrerOrigin = originFromReferrer();\n if (referrerOrigin && allowlist.has(referrerOrigin)) {\n return referrerOrigin;\n }\n try {\n const url = new URL(window.location.href);\n const explicit = url.searchParams.get('parent');\n if (explicit && allowlist.has(explicit)) return explicit;\n } catch {\n // ignore\n }\n return null;\n}\n","// Wagmi connector that proxies wallet operations to the Tangle Cloud parent\n// dapp via the iframe postMessage bridge. Becomes the autoConnect target\n// when this app is loaded inside an iframe sandbox without a window.ethereum\n// — i.e. always, when embedded by cloud.tangle.tools.\n//\n// Architecture: the connector owns one `ParentBridgeProvider` (singleton),\n// forwards every wagmi method to it, and reflects the provider's EIP-1193\n// events back to wagmi's emitter so the rest of the dapp (ConnectKit's\n// account chip, hooks like useAccount/useChainId) reacts to parent-state\n// changes without polling.\n\nimport type { Address, Chain } from 'viem';\nimport { createConnector } from 'wagmi';\n\nimport { ParentBridgeProvider, type ParentBridgeOptions } from './parentBridgeProvider';\n\nexport type ParentBridgeConnectorOptions = ParentBridgeOptions;\n\nexport function parentBridgeConnector(options: ParentBridgeConnectorOptions) {\n let provider: ParentBridgeProvider | undefined;\n let installed = false;\n\n return createConnector<ParentBridgeProvider>((config) => {\n const ensureProvider = (): ParentBridgeProvider => {\n if (!provider) provider = new ParentBridgeProvider(options);\n if (!installed) {\n provider.install();\n installed = true;\n // Wire the provider's EIP-1193 events to wagmi's emitter so\n // ConnectKit and useAccount/useChainId reflect parent-state changes\n // without polling.\n provider.on('accountsChanged', (accounts) => {\n config.emitter.emit('change', {\n accounts: Array.isArray(accounts)\n ? (accounts as readonly Address[])\n : ([] as readonly Address[]),\n });\n });\n provider.on('chainChanged', (chainIdHex) => {\n const chainId =\n typeof chainIdHex === 'string'\n ? Number.parseInt(chainIdHex, 16)\n : Number(chainIdHex);\n if (Number.isFinite(chainId)) {\n config.emitter.emit('change', { chainId });\n }\n });\n provider.on('disconnect', () => {\n config.emitter.emit('disconnect');\n });\n }\n return provider;\n };\n\n return {\n id: 'tangleParentBridge',\n name: 'Tangle Cloud',\n type: 'parentBridge',\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async connect(): Promise<any> {\n // wagmi v3's connect() return type is a conditional based on\n // `withCapabilities`. We always return plain addresses; cast through\n // `any` rather than re-implementing the type predicate.\n const p = ensureProvider();\n const accountsResult = (await p.request({\n method: 'eth_requestAccounts',\n })) as readonly Address[];\n const chainIdHex = (await p.request({ method: 'eth_chainId' })) as string;\n const chainId = Number.parseInt(chainIdHex, 16);\n return {\n accounts: accountsResult,\n chainId: Number.isFinite(chainId) ? chainId : 0,\n };\n },\n\n async disconnect() {\n // Disconnect from the iframe's perspective is a local-only state\n // reset — we can't ask the parent dapp to disconnect its wallet on\n // our behalf, and a real disconnect should be initiated from the\n // parent's UI. Tear down listeners + the message bridge so a future\n // reconnect re-handshakes cleanly.\n if (provider) provider.uninstall();\n installed = false;\n provider = undefined;\n },\n\n async getAccounts() {\n const p = ensureProvider();\n const cached = p.getCachedAccount();\n if (cached) return [cached];\n const accounts = (await p.request({\n method: 'eth_accounts',\n })) as readonly Address[];\n return accounts;\n },\n\n async getChainId() {\n const p = ensureProvider();\n const cached = p.getCachedChainId();\n if (cached !== null) return cached;\n const chainIdHex = (await p.request({ method: 'eth_chainId' })) as string;\n const chainId = Number.parseInt(chainIdHex, 16);\n return Number.isFinite(chainId) ? chainId : 0;\n },\n\n async getProvider() {\n return ensureProvider();\n },\n\n async isAuthorized() {\n // Always authorized when in iframe mode — the parent dapp has\n // already gated access by being the embedder. Returning `true`\n // makes wagmi auto-reconnect on every page load, which is the\n // right UX (iframe → parent wallet is always-on).\n try {\n const p = ensureProvider();\n const accounts = (await p.request({\n method: 'eth_accounts',\n })) as readonly Address[];\n return accounts.length > 0;\n } catch {\n return false;\n }\n },\n\n async switchChain({ chainId }): Promise<Chain> {\n const p = ensureProvider();\n await p.request({\n method: 'wallet_switchEthereumChain',\n params: [{ chainId: `0x${chainId.toString(16)}` }],\n });\n const chain = config.chains.find((c) => c.id === chainId);\n if (!chain) {\n throw new Error(`Chain ${chainId} not configured for this app`);\n }\n return chain;\n },\n\n onAccountsChanged(accounts) {\n config.emitter.emit('change', {\n accounts: accounts as readonly Address[],\n });\n },\n onChainChanged(chainIdHex) {\n const chainId = Number.parseInt(chainIdHex, 16);\n if (Number.isFinite(chainId)) {\n config.emitter.emit('change', { chainId });\n }\n },\n onDisconnect() {\n config.emitter.emit('disconnect');\n },\n };\n });\n}\n","// Tangle Cloud iframe ↔ parent dapp protocol — must mirror the parent's\n// spec at `apps/tangle-cloud/src/blueprintApps/iframe/protocol.ts`. Bump the\n// version constant in lockstep when either side adds a request kind.\n\nimport type { Address, Hex } from 'viem';\n\nexport const TANGLE_IFRAME_PROTOCOL_VERSION = '1' as const;\nexport const TANGLE_IFRAME_PROTOCOL_PREFIX = 'tangle.app.';\n\n// ─── Iframe → Parent requests ────────────────────────────────────────────────\n\nexport type HandshakeRequest = {\n kind: 'tangle.app.handshake';\n appId: string;\n version: typeof TANGLE_IFRAME_PROTOCOL_VERSION;\n};\n\nexport type ReadAccountRequest = {\n kind: 'tangle.app.readAccount';\n correlationId: string;\n};\n\nexport type SwitchChainRequest = {\n kind: 'tangle.app.switchChain';\n correlationId: string;\n chainId: number;\n};\n\nexport type SignMessageRequest = {\n kind: 'tangle.app.signMessage';\n correlationId: string;\n chainId: number;\n message: string;\n};\n\nexport type SignTransactionRequest = {\n kind: 'tangle.app.signTransaction';\n correlationId: string;\n chainId: number;\n to: Address;\n data: Hex;\n value?: string;\n};\n\n// ─── Parent → Iframe messages ────────────────────────────────────────────────\n\nexport type HandshakeAck = {\n kind: 'tangle.app.handshakeAck';\n appId: string;\n protocolVersion: typeof TANGLE_IFRAME_PROTOCOL_VERSION;\n};\n\nexport type ResultEnvelope<T> = { correlationId: string } & (\n | { ok: true; data: T }\n | { ok: false; error: string }\n);\n\nexport type ReadAccountResult = {\n kind: 'tangle.app.readAccountResult';\n} & ResultEnvelope<{ account: Address; chainId: number }>;\n\nexport type SwitchChainResult = {\n kind: 'tangle.app.switchChainResult';\n} & ResultEnvelope<{ chainId: number }>;\n\nexport type SignMessageResult = {\n kind: 'tangle.app.signMessageResult';\n} & ResultEnvelope<{ signature: Hex }>;\n\nexport type SignTransactionResult = {\n kind: 'tangle.app.signTransactionResult';\n} & ResultEnvelope<{ txHash: Hex }>;\n\nexport type AccountChanged = {\n kind: 'tangle.app.accountChanged';\n account: Address | null;\n};\n\nexport type ChainChanged = {\n kind: 'tangle.app.chainChanged';\n chainId: number;\n};\n\nexport type ParentMessage =\n | HandshakeAck\n | ReadAccountResult\n | SwitchChainResult\n | SignMessageResult\n | SignTransactionResult\n | AccountChanged\n | ChainChanged;\n\n// The zero address used by the parent when no wallet is connected. The parent\n// always responds to readAccount with an address; this sentinel means \"no\n// wallet\" without making the response type a union of result shapes.\nexport const NO_WALLET_ADDRESS = '0x0000000000000000000000000000000000000000';\n\n/**\n * Cryptographically-random ASCII correlation id matching the parent's\n * validator regex (`/^[\\w.\\-:]+$/`, max length 128). The connector keeps a\n * Map<correlationId, Resolver> so each request resolves independently.\n */\nexport function makeCorrelationId(prefix: string): string {\n const random =\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID()\n : Math.random().toString(36).slice(2) + Date.now().toString(36);\n return `${prefix}.${random}`;\n}\n","// EIP-1193 provider implementation that proxies wallet calls to the parent\n// dapp via window.postMessage. The iframe doesn't talk to a wallet directly\n// — it inherits the parent's connected account + chain, and forwards signing\n// requests through the existing tangle.app.* protocol.\n//\n// This is the lowest layer of the parent-bridge stack. Wagmi sees this as a\n// regular Ethereum provider and routes `eth_accounts`, `eth_chainId`,\n// `personal_sign`, `eth_sendTransaction`, `wallet_switchEthereumChain`, etc.\n// through it.\n\nimport type { Address, Hex } from 'viem';\n\nimport {\n makeCorrelationId,\n NO_WALLET_ADDRESS,\n TANGLE_IFRAME_PROTOCOL_VERSION,\n type ParentMessage,\n} from './parentBridgeProtocol';\n\ntype EventName = 'accountsChanged' | 'chainChanged' | 'connect' | 'disconnect' | 'message';\ntype Listener = (...args: unknown[]) => void;\n\ntype PendingRequest<T> = {\n resolve: (value: T) => void;\n reject: (reason: Error) => void;\n expectedKind: ParentMessage['kind'];\n};\n\nexport type ParentBridgeOptions = {\n /**\n * Origin of the parent dapp that hosts this iframe. The provider posts to\n * `window.parent` with this exact origin and rejects inbound messages from\n * any other origin. Pass `'*'` only in development; production must pin to\n * the real parent (`https://cloud.tangle.tools` or its develop equivalent).\n */\n parentOrigin: string;\n /**\n * Stable identifier for this iframe app. The parent includes this in the\n * handshake ack so dev tooling can correlate logs across the two windows.\n */\n appId: string;\n /**\n * Optional ms timeout for each bridged request. Defaults to 60 seconds —\n * long enough for a user to read + approve a signing prompt in the parent.\n */\n requestTimeoutMs?: number;\n};\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 60_000;\n\n/**\n * Detect iframe execution context. When this returns `false` the bridge\n * connector should not be installed and the host app should fall back to its\n * normal wallet config (ConnectKit + injected/walletConnect).\n *\n * `window.parent !== window` is the most reliable signal that works across\n * sandbox-iframe contexts where direct property access to parent throws.\n */\nexport function isRunningInIframe(): boolean {\n if (typeof window === 'undefined') return false;\n try {\n return window.parent !== undefined && window.parent !== window;\n } catch {\n // Cross-origin read of `window.parent` shouldn't throw, but be defensive.\n return true;\n }\n}\n\n/**\n * EIP-1193 provider backed by the Tangle Cloud iframe protocol. One instance\n * lives per iframe app; the wagmi connector owns the singleton.\n */\nexport class ParentBridgeProvider {\n private listeners = new Map<EventName, Set<Listener>>();\n private pending = new Map<string, PendingRequest<unknown>>();\n private cachedAccount: Address | null = null;\n private cachedChainId: number | null = null;\n private handshakeAcked = false;\n private handshakeWaiters: Array<() => void> = [];\n private installed = false;\n\n constructor(private readonly options: ParentBridgeOptions) {}\n\n /**\n * Wire up the global message listener and send the initial handshake.\n * Idempotent — safe to call repeatedly during reconnect attempts.\n */\n install(): void {\n if (this.installed || typeof window === 'undefined') return;\n this.installed = true;\n window.addEventListener('message', this.handleParentMessage);\n this.postToParent({\n kind: 'tangle.app.handshake',\n appId: this.options.appId,\n version: TANGLE_IFRAME_PROTOCOL_VERSION,\n });\n }\n\n uninstall(): void {\n if (!this.installed || typeof window === 'undefined') return;\n this.installed = false;\n window.removeEventListener('message', this.handleParentMessage);\n // Reject every pending request so callers don't hang forever.\n for (const [, pending] of this.pending) {\n pending.reject(new Error('Parent bridge uninstalled'));\n }\n this.pending.clear();\n }\n\n // ── EIP-1193 surface ────────────────────────────────────────────────────\n\n async request(req: { method: string; params?: unknown[] }): Promise<unknown> {\n const method = req.method;\n const params = (req.params ?? []) as unknown[];\n switch (method) {\n case 'eth_chainId': {\n await this.ensureBootstrapped();\n return this.cachedChainId !== null ? `0x${this.cachedChainId.toString(16)}` : '0x0';\n }\n case 'eth_accounts':\n case 'eth_requestAccounts': {\n await this.ensureBootstrapped();\n return this.cachedAccount !== null ? [this.cachedAccount] : [];\n }\n case 'personal_sign': {\n const [message, _signer] = params as [string, Address];\n return this.requestSignMessage(message);\n }\n case 'eth_signTypedData_v4': {\n // The current protocol doesn't carry typed-data — surface a clear\n // error rather than silently producing a personal_sign. Publishers\n // that need typed-data signing should upgrade the protocol.\n throw bridgeError(\n 4200,\n 'eth_signTypedData_v4 is not supported by the parent-bridge protocol yet.',\n );\n }\n case 'eth_sendTransaction': {\n const [tx] = params as [\n { to?: Address; data?: Hex; value?: Hex | string; chainId?: Hex | number },\n ];\n if (!tx?.to || !tx.data) {\n throw bridgeError(-32602, 'eth_sendTransaction requires `to` and `data`.');\n }\n return this.requestSignTransaction(tx);\n }\n case 'wallet_switchEthereumChain': {\n const [{ chainId }] = params as [{ chainId: Hex }];\n const numeric = Number.parseInt(chainId, 16);\n if (!Number.isFinite(numeric) || numeric <= 0) {\n throw bridgeError(-32602, `Invalid chainId: ${chainId}`);\n }\n await this.requestSwitchChain(numeric);\n return null;\n }\n case 'wallet_addEthereumChain': {\n // The parent owns the chain registry; iframes can't add chains the\n // dapp doesn't already know about.\n throw bridgeError(\n 4200,\n 'wallet_addEthereumChain is not supported through the parent bridge.',\n );\n }\n default:\n throw bridgeError(4200, `Method ${method} not supported by parent bridge.`);\n }\n }\n\n on(event: EventName, listener: Listener): void {\n const set = this.listeners.get(event) ?? new Set();\n set.add(listener);\n this.listeners.set(event, set);\n }\n\n removeListener(event: EventName, listener: Listener): void {\n this.listeners.get(event)?.delete(listener);\n }\n\n // ── Internal: dispatch + book-keeping ───────────────────────────────────\n\n private postToParent(message: object): void {\n if (typeof window === 'undefined') return;\n try {\n window.parent.postMessage(message, this.options.parentOrigin);\n } catch {\n // Cross-origin / sandboxed; parent.postMessage shouldn't actually throw\n // but be defensive against future browser changes.\n }\n }\n\n private handleParentMessage = (event: MessageEvent): void => {\n // Origin gate first; never parse untrusted payloads.\n if (event.origin !== this.options.parentOrigin) return;\n const data = event.data;\n if (typeof data !== 'object' || data === null) return;\n const message = data as ParentMessage;\n switch (message.kind) {\n case 'tangle.app.handshakeAck':\n this.handshakeAcked = true;\n for (const resolve of this.handshakeWaiters) resolve();\n this.handshakeWaiters = [];\n // After ack, ask for the current account so cached state reflects\n // reality before any consumer queries. Fire-and-forget — explicit\n // calls (`eth_accounts`, etc.) await their own request.\n this.sendReadAccount().catch(() => {\n // The first read commonly races with bridge teardown in tests\n // and isn't user-facing; swallow rather than producing unhandled\n // rejections. Subsequent `eth_accounts` calls retry on demand.\n });\n return;\n case 'tangle.app.readAccountResult':\n this.resolvePending(message);\n if (message.ok) {\n this.updateAccount(\n message.data.account === NO_WALLET_ADDRESS\n ? null\n : message.data.account,\n );\n this.updateChainId(message.data.chainId);\n }\n return;\n case 'tangle.app.switchChainResult':\n this.resolvePending(message);\n if (message.ok) this.updateChainId(message.data.chainId);\n return;\n case 'tangle.app.signMessageResult':\n case 'tangle.app.signTransactionResult':\n this.resolvePending(message);\n return;\n case 'tangle.app.accountChanged':\n this.updateAccount(message.account);\n return;\n case 'tangle.app.chainChanged':\n this.updateChainId(message.chainId);\n return;\n }\n };\n\n private sendReadAccount(): Promise<{ account: Address; chainId: number }> {\n return this.dispatch({\n kind: 'tangle.app.readAccount',\n expectedKind: 'tangle.app.readAccountResult',\n }) as Promise<{ account: Address; chainId: number }>;\n }\n\n private requestSignMessage(message: string): Promise<Hex> {\n const chainId = this.cachedChainId ?? 1;\n return this.dispatch({\n kind: 'tangle.app.signMessage',\n expectedKind: 'tangle.app.signMessageResult',\n payload: { chainId, message },\n }).then((data) => (data as { signature: Hex }).signature);\n }\n\n private requestSignTransaction(tx: {\n to?: Address;\n data?: Hex;\n value?: Hex | string;\n }): Promise<Hex> {\n const chainId = this.cachedChainId ?? 1;\n const value =\n typeof tx.value === 'string' && tx.value.startsWith('0x')\n ? BigInt(tx.value).toString(10)\n : typeof tx.value === 'string'\n ? tx.value\n : undefined;\n return this.dispatch({\n kind: 'tangle.app.signTransaction',\n expectedKind: 'tangle.app.signTransactionResult',\n payload: {\n chainId,\n to: tx.to as Address,\n data: tx.data as Hex,\n ...(value !== undefined ? { value } : {}),\n },\n }).then((data) => (data as { txHash: Hex }).txHash);\n }\n\n private requestSwitchChain(chainId: number): Promise<number> {\n return this.dispatch({\n kind: 'tangle.app.switchChain',\n expectedKind: 'tangle.app.switchChainResult',\n payload: { chainId },\n }).then((data) => (data as { chainId: number }).chainId);\n }\n\n private async dispatch(req: {\n kind: 'tangle.app.readAccount' | 'tangle.app.switchChain' | 'tangle.app.signMessage' | 'tangle.app.signTransaction';\n expectedKind: ParentMessage['kind'];\n payload?: Record<string, unknown>;\n }): Promise<unknown> {\n await this.ensureBootstrapped();\n const correlationId = makeCorrelationId(req.kind);\n const timeout = this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n return new Promise<unknown>((resolve, reject) => {\n const timer = window.setTimeout(() => {\n this.pending.delete(correlationId);\n reject(bridgeError(4900, `Parent did not respond to ${req.kind} within ${timeout}ms`));\n }, timeout);\n this.pending.set(correlationId, {\n resolve: (v) => {\n window.clearTimeout(timer);\n resolve(v);\n },\n reject: (e) => {\n window.clearTimeout(timer);\n reject(e);\n },\n expectedKind: req.expectedKind,\n });\n this.postToParent({\n kind: req.kind,\n correlationId,\n ...(req.payload ?? {}),\n });\n });\n }\n\n private resolvePending(message: Extract<ParentMessage, { correlationId: string }>): void {\n const entry = this.pending.get(message.correlationId);\n if (!entry) return;\n this.pending.delete(message.correlationId);\n if (entry.expectedKind !== message.kind) {\n entry.reject(\n bridgeError(\n -32000,\n `Parent replied with ${message.kind} but ${entry.expectedKind} was expected`,\n ),\n );\n return;\n }\n if (message.ok) {\n entry.resolve(message.data);\n } else {\n entry.reject(bridgeError(4001, message.error));\n }\n }\n\n private async ensureBootstrapped(): Promise<void> {\n if (this.handshakeAcked) return;\n this.install();\n await new Promise<void>((resolve) => {\n this.handshakeWaiters.push(resolve);\n // Re-send handshake every 500ms while we wait — covers a parent that\n // mounted after the iframe and missed the initial post.\n const retry = window.setInterval(() => {\n if (this.handshakeAcked) {\n window.clearInterval(retry);\n return;\n }\n this.postToParent({\n kind: 'tangle.app.handshake',\n appId: this.options.appId,\n version: TANGLE_IFRAME_PROTOCOL_VERSION,\n });\n }, 500);\n // Safety stop — handshake won't be re-attempted indefinitely.\n window.setTimeout(() => window.clearInterval(retry), 10_000);\n });\n }\n\n private updateAccount(next: Address | null): void {\n if (this.cachedAccount === next) return;\n const prev = this.cachedAccount;\n this.cachedAccount = next;\n if (next === null && prev !== null) {\n this.emit('disconnect');\n this.emit('accountsChanged', []);\n } else if (next !== null) {\n this.emit('accountsChanged', [next]);\n if (prev === null) {\n this.emit('connect', { chainId: this.cachedChainId ?? 0 });\n }\n }\n }\n\n private updateChainId(next: number): void {\n if (this.cachedChainId === next) return;\n this.cachedChainId = next;\n this.emit('chainChanged', `0x${next.toString(16)}`);\n }\n\n private emit(event: EventName, ...args: unknown[]): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const listener of [...set]) {\n try {\n listener(...args);\n } catch {\n // Listener bugs shouldn't break the bridge.\n }\n }\n }\n\n // ── Test seams ──────────────────────────────────────────────────────────\n\n /** Visible for tests + the connector's `getAccounts()` shortcut. */\n getCachedAccount(): Address | null {\n return this.cachedAccount;\n }\n /** Visible for tests + the connector's `getChainId()` shortcut. */\n getCachedChainId(): number | null {\n return this.cachedChainId;\n }\n}\n\nfunction bridgeError(code: number, message: string): Error {\n const err = new Error(message) as Error & { code?: number };\n err.code = code;\n return err;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/wallet/parentBridgeConnector.ts","../../src/wallet/parentBridgeProvider.ts"],"sourcesContent":["// Wagmi connector that proxies wallet operations to the Tangle Cloud parent\n// dapp via the iframe postMessage bridge. Becomes the autoConnect target\n// when this app is loaded inside an iframe sandbox without a window.ethereum\n// — i.e. always, when embedded by cloud.tangle.tools.\n//\n// Architecture: the connector owns one `ParentBridgeProvider` (singleton),\n// forwards every wagmi method to it, and reflects the provider's EIP-1193\n// events back to wagmi's emitter so the rest of the dapp (ConnectKit's\n// account chip, hooks like useAccount/useChainId) reacts to parent-state\n// changes without polling.\n\nimport type { Address, Chain } from 'viem';\nimport { createConnector } from 'wagmi';\n\nimport { ParentBridgeProvider, type ParentBridgeOptions } from './parentBridgeProvider';\n\nexport type ParentBridgeConnectorOptions = ParentBridgeOptions;\n\nexport function parentBridgeConnector(options: ParentBridgeConnectorOptions) {\n let provider: ParentBridgeProvider | undefined;\n let installed = false;\n\n return createConnector<ParentBridgeProvider>((config) => {\n const ensureProvider = (): ParentBridgeProvider => {\n if (!provider) provider = new ParentBridgeProvider(options);\n if (!installed) {\n provider.install();\n installed = true;\n // Wire the provider's EIP-1193 events to wagmi's emitter so\n // ConnectKit and useAccount/useChainId reflect parent-state changes\n // without polling.\n provider.on('accountsChanged', (accounts) => {\n config.emitter.emit('change', {\n accounts: Array.isArray(accounts)\n ? (accounts as readonly Address[])\n : ([] as readonly Address[]),\n });\n });\n provider.on('chainChanged', (chainIdHex) => {\n const chainId =\n typeof chainIdHex === 'string'\n ? Number.parseInt(chainIdHex, 16)\n : Number(chainIdHex);\n if (Number.isFinite(chainId)) {\n config.emitter.emit('change', { chainId });\n }\n });\n provider.on('disconnect', () => {\n config.emitter.emit('disconnect');\n });\n }\n return provider;\n };\n\n return {\n id: 'tangleParentBridge',\n name: 'Tangle Cloud',\n type: 'parentBridge',\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async connect(): Promise<any> {\n // wagmi v3's connect() return type is a conditional based on\n // `withCapabilities`. We always return plain addresses; cast through\n // `any` rather than re-implementing the type predicate.\n const p = ensureProvider();\n const accountsResult = (await p.request({\n method: 'eth_requestAccounts',\n })) as readonly Address[];\n const chainIdHex = (await p.request({ method: 'eth_chainId' })) as string;\n const chainId = Number.parseInt(chainIdHex, 16);\n return {\n accounts: accountsResult,\n chainId: Number.isFinite(chainId) ? chainId : 0,\n };\n },\n\n async disconnect() {\n // Disconnect from the iframe's perspective is a local-only state\n // reset — we can't ask the parent dapp to disconnect its wallet on\n // our behalf, and a real disconnect should be initiated from the\n // parent's UI. Tear down listeners + the message bridge so a future\n // reconnect re-handshakes cleanly.\n if (provider) provider.uninstall();\n installed = false;\n provider = undefined;\n },\n\n async getAccounts() {\n const p = ensureProvider();\n const cached = p.getCachedAccount();\n if (cached) return [cached];\n const accounts = (await p.request({\n method: 'eth_accounts',\n })) as readonly Address[];\n return accounts;\n },\n\n async getChainId() {\n const p = ensureProvider();\n const cached = p.getCachedChainId();\n if (cached !== null) return cached;\n const chainIdHex = (await p.request({ method: 'eth_chainId' })) as string;\n const chainId = Number.parseInt(chainIdHex, 16);\n return Number.isFinite(chainId) ? chainId : 0;\n },\n\n async getProvider() {\n return ensureProvider();\n },\n\n async isAuthorized() {\n // Always authorized when in iframe mode — the parent dapp has\n // already gated access by being the embedder. Returning `true`\n // makes wagmi auto-reconnect on every page load, which is the\n // right UX (iframe → parent wallet is always-on).\n try {\n const p = ensureProvider();\n const accounts = (await p.request({\n method: 'eth_accounts',\n })) as readonly Address[];\n return accounts.length > 0;\n } catch {\n return false;\n }\n },\n\n async switchChain({ chainId }): Promise<Chain> {\n const p = ensureProvider();\n await p.request({\n method: 'wallet_switchEthereumChain',\n params: [{ chainId: `0x${chainId.toString(16)}` }],\n });\n const chain = config.chains.find((c) => c.id === chainId);\n if (!chain) {\n throw new Error(`Chain ${chainId} not configured for this app`);\n }\n return chain;\n },\n\n onAccountsChanged(accounts) {\n config.emitter.emit('change', {\n accounts: accounts as readonly Address[],\n });\n },\n onChainChanged(chainIdHex) {\n const chainId = Number.parseInt(chainIdHex, 16);\n if (Number.isFinite(chainId)) {\n config.emitter.emit('change', { chainId });\n }\n },\n onDisconnect() {\n config.emitter.emit('disconnect');\n },\n };\n });\n}\n","// EIP-1193 provider implementation that proxies wallet calls to the parent\n// dapp via window.postMessage. The iframe doesn't talk to a wallet directly\n// — it inherits the parent's connected account + chain, and forwards signing\n// requests through the existing tangle.app.* protocol.\n//\n// This is the lowest layer of the parent-bridge stack. Wagmi sees this as a\n// regular Ethereum provider and routes `eth_accounts`, `eth_chainId`,\n// `personal_sign`, `eth_sendTransaction`, `wallet_switchEthereumChain`, etc.\n// through it.\n\nimport type { Address, Hex } from 'viem';\n\nimport {\n makeCorrelationId,\n NO_WALLET_ADDRESS,\n TANGLE_IFRAME_PROTOCOL_VERSION,\n type ParentMessage,\n type ReadAccountResult,\n type SignMessageResult,\n type SignTransactionResult,\n type SwitchChainResult,\n} from './parentBridgeProtocol';\n\ntype EventName = 'accountsChanged' | 'chainChanged' | 'connect' | 'disconnect' | 'message';\ntype Listener = (...args: unknown[]) => void;\n\ntype PendingRequest<T> = {\n resolve: (value: T) => void;\n reject: (reason: Error) => void;\n expectedKind: ParentMessage['kind'];\n};\n\nexport type ParentBridgeOptions = {\n /**\n * Origin of the parent dapp that hosts this iframe. The provider posts to\n * `window.parent` with this exact origin and rejects inbound messages from\n * any other origin. Pass `'*'` only in development; production must pin to\n * the real parent (`https://cloud.tangle.tools` or its develop equivalent).\n */\n parentOrigin: string;\n /**\n * Stable identifier for this iframe app. The parent includes this in the\n * handshake ack so dev tooling can correlate logs across the two windows.\n */\n appId: string;\n /**\n * Optional ms timeout for each bridged request. Defaults to 60 seconds —\n * long enough for a user to read + approve a signing prompt in the parent.\n */\n requestTimeoutMs?: number;\n};\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 60_000;\n\n/**\n * Detect iframe execution context. When this returns `false` the bridge\n * connector should not be installed and the host app should fall back to its\n * normal wallet config (ConnectKit + injected/walletConnect).\n *\n * `window.parent !== window` is the most reliable signal that works across\n * sandbox-iframe contexts where direct property access to parent throws.\n */\nexport function isRunningInIframe(): boolean {\n if (typeof window === 'undefined') return false;\n try {\n return window.parent !== undefined && window.parent !== window;\n } catch {\n // Cross-origin read of `window.parent` shouldn't throw, but be defensive.\n return true;\n }\n}\n\n/**\n * EIP-1193 provider backed by the Tangle Cloud iframe protocol. One instance\n * lives per iframe app; the wagmi connector owns the singleton.\n */\nexport class ParentBridgeProvider {\n private listeners = new Map<EventName, Set<Listener>>();\n private pending = new Map<string, PendingRequest<unknown>>();\n private cachedAccount: Address | null = null;\n private cachedChainId: number | null = null;\n private handshakeAcked = false;\n private handshakeWaiters: Array<() => void> = [];\n private installed = false;\n\n constructor(private readonly options: ParentBridgeOptions) {}\n\n /**\n * Wire up the global message listener and send the initial handshake.\n * Idempotent — safe to call repeatedly during reconnect attempts.\n */\n install(): void {\n if (this.installed || typeof window === 'undefined') return;\n this.installed = true;\n window.addEventListener('message', this.handleParentMessage);\n this.postToParent({\n kind: 'tangle.app.handshake',\n appId: this.options.appId,\n version: TANGLE_IFRAME_PROTOCOL_VERSION,\n });\n }\n\n uninstall(): void {\n if (!this.installed || typeof window === 'undefined') return;\n this.installed = false;\n window.removeEventListener('message', this.handleParentMessage);\n // Reject every pending request so callers don't hang forever.\n for (const [, pending] of this.pending) {\n pending.reject(new Error('Parent bridge uninstalled'));\n }\n this.pending.clear();\n }\n\n // ── EIP-1193 surface ────────────────────────────────────────────────────\n\n async request(req: { method: string; params?: unknown[] }): Promise<unknown> {\n const method = req.method;\n const params = (req.params ?? []) as unknown[];\n switch (method) {\n case 'eth_chainId': {\n await this.ensureBootstrapped();\n return this.cachedChainId !== null ? `0x${this.cachedChainId.toString(16)}` : '0x0';\n }\n case 'eth_accounts':\n case 'eth_requestAccounts': {\n await this.ensureBootstrapped();\n return this.cachedAccount !== null ? [this.cachedAccount] : [];\n }\n case 'personal_sign': {\n const [message, _signer] = params as [string, Address];\n return this.requestSignMessage(message);\n }\n case 'eth_signTypedData_v4': {\n // The current protocol doesn't carry typed-data — surface a clear\n // error rather than silently producing a personal_sign. Publishers\n // that need typed-data signing should upgrade the protocol.\n throw bridgeError(\n 4200,\n 'eth_signTypedData_v4 is not supported by the parent-bridge protocol yet.',\n );\n }\n case 'eth_sendTransaction': {\n const [tx] = params as [\n { to?: Address; data?: Hex; value?: Hex | string; chainId?: Hex | number },\n ];\n if (!tx?.to || !tx.data) {\n throw bridgeError(-32602, 'eth_sendTransaction requires `to` and `data`.');\n }\n return this.requestSignTransaction(tx);\n }\n case 'wallet_switchEthereumChain': {\n const [{ chainId }] = params as [{ chainId: Hex }];\n const numeric = Number.parseInt(chainId, 16);\n if (!Number.isFinite(numeric) || numeric <= 0) {\n throw bridgeError(-32602, `Invalid chainId: ${chainId}`);\n }\n await this.requestSwitchChain(numeric);\n return null;\n }\n case 'wallet_addEthereumChain': {\n // The parent owns the chain registry; iframes can't add chains the\n // dapp doesn't already know about.\n throw bridgeError(\n 4200,\n 'wallet_addEthereumChain is not supported through the parent bridge.',\n );\n }\n default:\n throw bridgeError(4200, `Method ${method} not supported by parent bridge.`);\n }\n }\n\n on(event: EventName, listener: Listener): void {\n const set = this.listeners.get(event) ?? new Set();\n set.add(listener);\n this.listeners.set(event, set);\n }\n\n removeListener(event: EventName, listener: Listener): void {\n this.listeners.get(event)?.delete(listener);\n }\n\n // ── Internal: dispatch + book-keeping ───────────────────────────────────\n\n private postToParent(message: object): void {\n if (typeof window === 'undefined') return;\n try {\n window.parent.postMessage(message, this.options.parentOrigin);\n } catch {\n // Cross-origin / sandboxed; parent.postMessage shouldn't actually throw\n // but be defensive against future browser changes.\n }\n }\n\n private handleParentMessage = (event: MessageEvent): void => {\n // Origin gate first; never parse untrusted payloads.\n if (event.origin !== this.options.parentOrigin) return;\n const data = event.data;\n if (typeof data !== 'object' || data === null) return;\n const message = data as ParentMessage;\n switch (message.kind) {\n case 'tangle.app.handshakeAck':\n this.handshakeAcked = true;\n for (const resolve of this.handshakeWaiters) resolve();\n this.handshakeWaiters = [];\n // After ack, ask for the current account so cached state reflects\n // reality before any consumer queries. Fire-and-forget — explicit\n // calls (`eth_accounts`, etc.) await their own request.\n this.sendReadAccount().catch(() => {\n // The first read commonly races with bridge teardown in tests\n // and isn't user-facing; swallow rather than producing unhandled\n // rejections. Subsequent `eth_accounts` calls retry on demand.\n });\n return;\n case 'tangle.app.readAccountResult':\n this.resolvePending(message);\n if (message.ok) {\n this.updateAccount(\n message.data.account === NO_WALLET_ADDRESS\n ? null\n : message.data.account,\n );\n this.updateChainId(message.data.chainId);\n }\n return;\n case 'tangle.app.switchChainResult':\n this.resolvePending(message);\n if (message.ok) this.updateChainId(message.data.chainId);\n return;\n case 'tangle.app.signMessageResult':\n case 'tangle.app.signTransactionResult':\n this.resolvePending(message);\n return;\n case 'tangle.app.accountChanged':\n this.updateAccount(message.account);\n return;\n case 'tangle.app.chainChanged':\n this.updateChainId(message.chainId);\n return;\n }\n };\n\n private sendReadAccount(): Promise<{ account: Address; chainId: number }> {\n return this.dispatch({\n kind: 'tangle.app.readAccount',\n expectedKind: 'tangle.app.readAccountResult',\n }) as Promise<{ account: Address; chainId: number }>;\n }\n\n private requestSignMessage(message: string): Promise<Hex> {\n const chainId = this.cachedChainId ?? 1;\n return this.dispatch({\n kind: 'tangle.app.signMessage',\n expectedKind: 'tangle.app.signMessageResult',\n payload: { chainId, message },\n }).then((data) => (data as { signature: Hex }).signature);\n }\n\n private requestSignTransaction(tx: {\n to?: Address;\n data?: Hex;\n value?: Hex | string;\n }): Promise<Hex> {\n const chainId = this.cachedChainId ?? 1;\n const value =\n typeof tx.value === 'string' && tx.value.startsWith('0x')\n ? BigInt(tx.value).toString(10)\n : typeof tx.value === 'string'\n ? tx.value\n : undefined;\n return this.dispatch({\n kind: 'tangle.app.signTransaction',\n expectedKind: 'tangle.app.signTransactionResult',\n payload: {\n chainId,\n to: tx.to as Address,\n data: tx.data as Hex,\n ...(value !== undefined ? { value } : {}),\n },\n }).then((data) => (data as { txHash: Hex }).txHash);\n }\n\n private requestSwitchChain(chainId: number): Promise<number> {\n return this.dispatch({\n kind: 'tangle.app.switchChain',\n expectedKind: 'tangle.app.switchChainResult',\n payload: { chainId },\n }).then((data) => (data as { chainId: number }).chainId);\n }\n\n private async dispatch(req: {\n kind: 'tangle.app.readAccount' | 'tangle.app.switchChain' | 'tangle.app.signMessage' | 'tangle.app.signTransaction';\n expectedKind: ParentMessage['kind'];\n payload?: Record<string, unknown>;\n }): Promise<unknown> {\n await this.ensureBootstrapped();\n const correlationId = makeCorrelationId(req.kind);\n const timeout = this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n return new Promise<unknown>((resolve, reject) => {\n const timer = window.setTimeout(() => {\n this.pending.delete(correlationId);\n reject(bridgeError(4900, `Parent did not respond to ${req.kind} within ${timeout}ms`));\n }, timeout);\n this.pending.set(correlationId, {\n resolve: (v) => {\n window.clearTimeout(timer);\n resolve(v);\n },\n reject: (e) => {\n window.clearTimeout(timer);\n reject(e);\n },\n expectedKind: req.expectedKind,\n });\n this.postToParent({\n kind: req.kind,\n correlationId,\n ...(req.payload ?? {}),\n });\n });\n }\n\n /**\n * Resolves wallet-shape responses (`{ ok, data | error }`). Job results\n * use a different envelope (`{ status, data?, chunk?, error? }`) and are\n * routed through a separate listener registered by `useCallJob` / the SDK\n * — the provider doesn't double-handle them.\n */\n private resolvePending(\n message:\n | ReadAccountResult\n | SwitchChainResult\n | SignMessageResult\n | SignTransactionResult,\n ): void {\n const entry = this.pending.get(message.correlationId);\n if (!entry) return;\n this.pending.delete(message.correlationId);\n if (entry.expectedKind !== message.kind) {\n entry.reject(\n bridgeError(\n -32000,\n `Parent replied with ${message.kind} but ${entry.expectedKind} was expected`,\n ),\n );\n return;\n }\n if (message.ok) {\n entry.resolve(message.data);\n } else {\n entry.reject(bridgeError(4001, message.error));\n }\n }\n\n private async ensureBootstrapped(): Promise<void> {\n if (this.handshakeAcked) return;\n this.install();\n await new Promise<void>((resolve) => {\n this.handshakeWaiters.push(resolve);\n // Re-send handshake every 500ms while we wait — covers a parent that\n // mounted after the iframe and missed the initial post.\n const retry = window.setInterval(() => {\n if (this.handshakeAcked) {\n window.clearInterval(retry);\n return;\n }\n this.postToParent({\n kind: 'tangle.app.handshake',\n appId: this.options.appId,\n version: TANGLE_IFRAME_PROTOCOL_VERSION,\n });\n }, 500);\n // Safety stop — handshake won't be re-attempted indefinitely.\n window.setTimeout(() => window.clearInterval(retry), 10_000);\n });\n }\n\n private updateAccount(next: Address | null): void {\n if (this.cachedAccount === next) return;\n const prev = this.cachedAccount;\n this.cachedAccount = next;\n if (next === null && prev !== null) {\n this.emit('disconnect');\n this.emit('accountsChanged', []);\n } else if (next !== null) {\n this.emit('accountsChanged', [next]);\n if (prev === null) {\n this.emit('connect', { chainId: this.cachedChainId ?? 0 });\n }\n }\n }\n\n private updateChainId(next: number): void {\n if (this.cachedChainId === next) return;\n this.cachedChainId = next;\n this.emit('chainChanged', `0x${next.toString(16)}`);\n }\n\n private emit(event: EventName, ...args: unknown[]): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const listener of [...set]) {\n try {\n listener(...args);\n } catch {\n // Listener bugs shouldn't break the bridge.\n }\n }\n }\n\n // ── Test seams ──────────────────────────────────────────────────────────\n\n /** Visible for tests + the connector's `getAccounts()` shortcut. */\n getCachedAccount(): Address | null {\n return this.cachedAccount;\n }\n /** Visible for tests + the connector's `getChainId()` shortcut. */\n getCachedChainId(): number | null {\n return this.cachedChainId;\n }\n}\n\nfunction bridgeError(code: number, message: string): Error {\n const err = new Error(message) as Error & { code?: number };\n err.code = code;\n return err;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/blueprint-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Shared blueprint UI components, hooks, and contract utilities for Tangle Network apps",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -48,6 +48,16 @@
|
|
|
48
48
|
"types": "./dist/wallet/index.d.ts",
|
|
49
49
|
"default": "./dist/wallet/index.js"
|
|
50
50
|
},
|
|
51
|
+
"./iframe": {
|
|
52
|
+
"import": "./dist/iframe/index.js",
|
|
53
|
+
"types": "./dist/iframe/index.d.ts",
|
|
54
|
+
"default": "./dist/iframe/index.js"
|
|
55
|
+
},
|
|
56
|
+
"./iframe/testing": {
|
|
57
|
+
"import": "./dist/iframe/testing-index.js",
|
|
58
|
+
"types": "./dist/iframe/testing-index.d.ts",
|
|
59
|
+
"default": "./dist/iframe/testing-index.js"
|
|
60
|
+
},
|
|
51
61
|
"./styles.css": "./dist/styles.css"
|
|
52
62
|
},
|
|
53
63
|
"scripts": {
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from 'react';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
TangleIframeClient,
|
|
13
|
+
type ServiceSnapshot,
|
|
14
|
+
type TangleIframeClientOptions,
|
|
15
|
+
type WalletSnapshot,
|
|
16
|
+
} from './tangleIframeClient';
|
|
17
|
+
import {
|
|
18
|
+
detectTangleCloudParentOrigin,
|
|
19
|
+
TANGLE_CLOUD_ORIGINS_DEFAULT,
|
|
20
|
+
} from '../wallet/detectParentOrigin';
|
|
21
|
+
|
|
22
|
+
type Props = {
|
|
23
|
+
appId: string;
|
|
24
|
+
/** Override the detected parent origin (e.g. dev/staging deploys). */
|
|
25
|
+
parentOrigin?: string;
|
|
26
|
+
/** Extra trusted origins for `detectTangleCloudParentOrigin`. */
|
|
27
|
+
extraOrigins?: readonly string[];
|
|
28
|
+
/**
|
|
29
|
+
* Override the bootstrap behavior. When `'auto'` (default), the SDK
|
|
30
|
+
* sniffs the embed context: real parent → install the bridge, top-frame
|
|
31
|
+
* → drop into dev mode. `'bridge'` forces real-parent mode and throws
|
|
32
|
+
* if no parent is detected. `'dev'` forces dev mode even when embedded
|
|
33
|
+
* — useful for component-level tests.
|
|
34
|
+
*/
|
|
35
|
+
mode?: 'auto' | 'bridge' | 'dev';
|
|
36
|
+
children: ReactNode;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type ContextValue = {
|
|
40
|
+
readonly client: TangleIframeClient | null;
|
|
41
|
+
readonly wallet: WalletSnapshot;
|
|
42
|
+
readonly service: ServiceSnapshot;
|
|
43
|
+
readonly mode: 'bridge' | 'dev';
|
|
44
|
+
readonly isReady: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const TangleIframeContext = createContext<ContextValue | null>(null);
|
|
48
|
+
|
|
49
|
+
const NULL_WALLET: WalletSnapshot = {
|
|
50
|
+
address: null,
|
|
51
|
+
chainId: null,
|
|
52
|
+
isConnected: false,
|
|
53
|
+
};
|
|
54
|
+
const NULL_SERVICE: ServiceSnapshot = {
|
|
55
|
+
blueprintId: null,
|
|
56
|
+
serviceId: null,
|
|
57
|
+
operators: [],
|
|
58
|
+
jobs: [],
|
|
59
|
+
mode: null,
|
|
60
|
+
chain: null,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Iframe-blueprint root provider. Wrap your app once at the entry point.
|
|
65
|
+
*
|
|
66
|
+
* In `auto` mode (default) the SDK detects whether the app is embedded by a
|
|
67
|
+
* trusted Tangle Cloud parent. If yes → installs the postMessage bridge.
|
|
68
|
+
* If no (running standalone at `localhost:5173` etc.) → enters **dev mode**
|
|
69
|
+
* with an in-memory state machine that the developer can drive via the
|
|
70
|
+
* exported debug controls. Dev mode keeps the hook surface identical to
|
|
71
|
+
* production so component code never branches on embed-vs-not.
|
|
72
|
+
*
|
|
73
|
+
* Three lifecycle stages:
|
|
74
|
+
*
|
|
75
|
+
* 1. Mount — `client` is created, mode is decided.
|
|
76
|
+
* 2. Bootstrap — handshake (bridge) or first-paint setup (dev). The
|
|
77
|
+
* `isReady` flag flips to true.
|
|
78
|
+
* 3. Active — wallet + service snapshots flow in via subscriptions.
|
|
79
|
+
*/
|
|
80
|
+
export function TangleIframeProvider({
|
|
81
|
+
appId,
|
|
82
|
+
parentOrigin: explicitOrigin,
|
|
83
|
+
extraOrigins,
|
|
84
|
+
mode: requestedMode = 'auto',
|
|
85
|
+
children,
|
|
86
|
+
}: Props) {
|
|
87
|
+
// Resolve the effective mode once at mount. Switching modes mid-session
|
|
88
|
+
// would tear down the bridge / dev state inconsistently; restart instead.
|
|
89
|
+
const resolution = useMemo(() => {
|
|
90
|
+
if (requestedMode === 'dev') {
|
|
91
|
+
return { mode: 'dev' as const, parentOrigin: null };
|
|
92
|
+
}
|
|
93
|
+
const detected =
|
|
94
|
+
explicitOrigin ?? detectTangleCloudParentOrigin({ extraOrigins });
|
|
95
|
+
if (requestedMode === 'bridge') {
|
|
96
|
+
if (!detected) {
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
98
|
+
console.error(
|
|
99
|
+
'[TangleIframeProvider] mode="bridge" but no trusted parent was detected. Falling back to dev mode.',
|
|
100
|
+
);
|
|
101
|
+
return { mode: 'dev' as const, parentOrigin: null };
|
|
102
|
+
}
|
|
103
|
+
return { mode: 'bridge' as const, parentOrigin: detected };
|
|
104
|
+
}
|
|
105
|
+
// auto: bridge when detected, dev otherwise.
|
|
106
|
+
return detected
|
|
107
|
+
? { mode: 'bridge' as const, parentOrigin: detected }
|
|
108
|
+
: { mode: 'dev' as const, parentOrigin: null };
|
|
109
|
+
}, [requestedMode, explicitOrigin, extraOrigins]);
|
|
110
|
+
|
|
111
|
+
const clientRef = useRef<TangleIframeClient | null>(null);
|
|
112
|
+
const [wallet, setWallet] = useState<WalletSnapshot>(NULL_WALLET);
|
|
113
|
+
const [service, setService] = useState<ServiceSnapshot>(NULL_SERVICE);
|
|
114
|
+
const [isReady, setIsReady] = useState(false);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (resolution.mode === 'dev') {
|
|
118
|
+
// Dev mode: no bridge. The DevHarness component (or a test) seeds
|
|
119
|
+
// wallet + service via `setDevWallet` / `setDevService` on the
|
|
120
|
+
// returned context. Mark ready immediately so app code unblocks.
|
|
121
|
+
setIsReady(true);
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
// Bridge mode
|
|
125
|
+
const options: TangleIframeClientOptions = {
|
|
126
|
+
parentOrigin: resolution.parentOrigin,
|
|
127
|
+
appId,
|
|
128
|
+
};
|
|
129
|
+
const client = new TangleIframeClient(options);
|
|
130
|
+
clientRef.current = client;
|
|
131
|
+
const unsubWallet = client.subscribe('wallet', setWallet);
|
|
132
|
+
const unsubService = client.subscribe('service', setService);
|
|
133
|
+
client.install();
|
|
134
|
+
setIsReady(true);
|
|
135
|
+
return () => {
|
|
136
|
+
unsubWallet();
|
|
137
|
+
unsubService();
|
|
138
|
+
client.uninstall();
|
|
139
|
+
clientRef.current = null;
|
|
140
|
+
setIsReady(false);
|
|
141
|
+
};
|
|
142
|
+
}, [resolution, appId]);
|
|
143
|
+
|
|
144
|
+
const value = useMemo<ContextValue>(
|
|
145
|
+
() => ({
|
|
146
|
+
client: clientRef.current,
|
|
147
|
+
wallet,
|
|
148
|
+
service,
|
|
149
|
+
mode: resolution.mode,
|
|
150
|
+
isReady,
|
|
151
|
+
}),
|
|
152
|
+
[wallet, service, resolution.mode, isReady],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<TangleIframeContext.Provider value={value}>
|
|
157
|
+
{children}
|
|
158
|
+
</TangleIframeContext.Provider>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function useTangleIframeContext(): ContextValue {
|
|
163
|
+
const ctx = useContext(TangleIframeContext);
|
|
164
|
+
if (!ctx) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
'useTangleIframeContext must be used inside <TangleIframeProvider>.',
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return ctx;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export { TANGLE_CLOUD_ORIGINS_DEFAULT };
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
createPublicClient,
|
|
4
|
+
http,
|
|
5
|
+
type Address,
|
|
6
|
+
type Chain,
|
|
7
|
+
type Hex,
|
|
8
|
+
type PublicClient,
|
|
9
|
+
} from 'viem';
|
|
10
|
+
|
|
11
|
+
import { useTangleIframeContext } from './TangleIframeProvider';
|
|
12
|
+
import type {
|
|
13
|
+
JobInvocation,
|
|
14
|
+
ServiceSnapshot,
|
|
15
|
+
WalletSnapshot,
|
|
16
|
+
} from './tangleIframeClient';
|
|
17
|
+
import type {
|
|
18
|
+
ChainContext,
|
|
19
|
+
JobInputs,
|
|
20
|
+
SignTypedDataRequest,
|
|
21
|
+
} from '../wallet/parentBridgeProtocol';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read-only view of the connected wallet, plus the operations the iframe
|
|
25
|
+
* can request the parent to perform.
|
|
26
|
+
*
|
|
27
|
+
* The iframe never holds a private key, never sees `window.ethereum`, never
|
|
28
|
+
* imports wagmi. All wallet work happens upstream in the Tangle Cloud
|
|
29
|
+
* dapp's wagmi config + ConnectKit modal.
|
|
30
|
+
*/
|
|
31
|
+
export function useTangleWallet(): WalletSnapshot & {
|
|
32
|
+
signMessage: (message: string) => Promise<Hex>;
|
|
33
|
+
sendTransaction: (tx: {
|
|
34
|
+
to: Address;
|
|
35
|
+
data: Hex;
|
|
36
|
+
value?: bigint;
|
|
37
|
+
}) => Promise<Hex>;
|
|
38
|
+
signTypedData: (args: {
|
|
39
|
+
domain: SignTypedDataRequest['domain'];
|
|
40
|
+
types: SignTypedDataRequest['types'];
|
|
41
|
+
primaryType: string;
|
|
42
|
+
message: Readonly<Record<string, unknown>>;
|
|
43
|
+
}) => Promise<Hex>;
|
|
44
|
+
switchChain: (chainId: number) => Promise<number>;
|
|
45
|
+
} {
|
|
46
|
+
const { client, wallet } = useTangleIframeContext();
|
|
47
|
+
const signMessage = useCallback(
|
|
48
|
+
(message: string) => {
|
|
49
|
+
if (!client) throw new Error('Wallet not available in dev mode.');
|
|
50
|
+
return client.signMessage(message);
|
|
51
|
+
},
|
|
52
|
+
[client],
|
|
53
|
+
);
|
|
54
|
+
const sendTransaction = useCallback(
|
|
55
|
+
(tx: { to: Address; data: Hex; value?: bigint }) => {
|
|
56
|
+
if (!client) throw new Error('Wallet not available in dev mode.');
|
|
57
|
+
return client.sendTransaction(tx);
|
|
58
|
+
},
|
|
59
|
+
[client],
|
|
60
|
+
);
|
|
61
|
+
const signTypedData = useCallback(
|
|
62
|
+
(args: {
|
|
63
|
+
domain: SignTypedDataRequest['domain'];
|
|
64
|
+
types: SignTypedDataRequest['types'];
|
|
65
|
+
primaryType: string;
|
|
66
|
+
message: Readonly<Record<string, unknown>>;
|
|
67
|
+
}) => {
|
|
68
|
+
if (!client) throw new Error('Wallet not available in dev mode.');
|
|
69
|
+
return client.signTypedData(args);
|
|
70
|
+
},
|
|
71
|
+
[client],
|
|
72
|
+
);
|
|
73
|
+
const switchChain = useCallback(
|
|
74
|
+
(chainId: number) => {
|
|
75
|
+
if (!client) throw new Error('Wallet not available in dev mode.');
|
|
76
|
+
return client.switchChain(chainId);
|
|
77
|
+
},
|
|
78
|
+
[client],
|
|
79
|
+
);
|
|
80
|
+
return {
|
|
81
|
+
...wallet,
|
|
82
|
+
signMessage,
|
|
83
|
+
sendTransaction,
|
|
84
|
+
signTypedData,
|
|
85
|
+
switchChain,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Chain configuration broadcast by the parent: chain id, name, RPC URL,
|
|
91
|
+
* block explorer, native currency. Returns `null` until the parent has
|
|
92
|
+
* sent its first `serviceContext` broadcast (or in dev mode without a
|
|
93
|
+
* seeded harness).
|
|
94
|
+
*
|
|
95
|
+
* Use this when you want to display chain-aware info (block explorer
|
|
96
|
+
* links, native currency labels) or when you want to build your own viem
|
|
97
|
+
* client with the parent's RPC URL. For a pre-built read-only client,
|
|
98
|
+
* see `useTanglePublicClient()`.
|
|
99
|
+
*/
|
|
100
|
+
export function useChainContext(): ChainContext | null {
|
|
101
|
+
return useTangleIframeContext().service.chain;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Read-only viem `PublicClient` pinned to the chain the parent dapp is
|
|
106
|
+
* connected to. Useful for `readContract`, `getBalance`, `multicall`, etc.
|
|
107
|
+
*
|
|
108
|
+
* Returns `null` until the parent broadcasts a chain context. Iframes that
|
|
109
|
+
* need to read from chains *other* than the active one should bring their
|
|
110
|
+
* own client — this hook is a convenience for the common case, not a
|
|
111
|
+
* constraint. Multi-chain dashboards just create additional clients
|
|
112
|
+
* directly via `createPublicClient`.
|
|
113
|
+
*
|
|
114
|
+
* Memoized per chain id + RPC URL, so consumers get a stable identity
|
|
115
|
+
* across re-renders.
|
|
116
|
+
*/
|
|
117
|
+
export function useTanglePublicClient(): PublicClient | null {
|
|
118
|
+
const chain = useChainContext();
|
|
119
|
+
return useMemo(() => {
|
|
120
|
+
if (!chain) return null;
|
|
121
|
+
const chainConfig: Chain = {
|
|
122
|
+
id: chain.id,
|
|
123
|
+
name: chain.name,
|
|
124
|
+
nativeCurrency:
|
|
125
|
+
chain.nativeCurrency !== undefined
|
|
126
|
+
? { ...chain.nativeCurrency }
|
|
127
|
+
: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
|
128
|
+
rpcUrls: {
|
|
129
|
+
default: { http: [chain.rpcUrl] },
|
|
130
|
+
},
|
|
131
|
+
...(chain.blockExplorerUrl
|
|
132
|
+
? {
|
|
133
|
+
blockExplorers: {
|
|
134
|
+
default: { name: 'Explorer', url: chain.blockExplorerUrl },
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
: {}),
|
|
138
|
+
} as Chain;
|
|
139
|
+
return createPublicClient({
|
|
140
|
+
chain: chainConfig,
|
|
141
|
+
transport: http(chain.rpcUrl),
|
|
142
|
+
});
|
|
143
|
+
}, [chain]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The service the iframe is currently rendering for. Broadcast by the
|
|
148
|
+
* parent dapp on mount + every time the service/mode changes — the iframe
|
|
149
|
+
* never queries the chain or the indexer itself.
|
|
150
|
+
*
|
|
151
|
+
* `serviceId === null` means the operator hasn't deployed an instance yet;
|
|
152
|
+
* the iframe should render its deploy-ready / configuration surface.
|
|
153
|
+
*/
|
|
154
|
+
export function useTangleService(): ServiceSnapshot {
|
|
155
|
+
return useTangleIframeContext().service;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Invoke a blueprint job. Returns a callable + a snapshot of the most
|
|
160
|
+
* recent invocation (or null if none yet).
|
|
161
|
+
*
|
|
162
|
+
* Streaming jobs (LLM, video, audio) opt in via `stream: true`. The hook's
|
|
163
|
+
* `invocation.chunks` accumulates each streaming chunk so the UI can render
|
|
164
|
+
* progressive output. For one-shot jobs (embeddings, classification), use
|
|
165
|
+
* the `invocation.data` once `status === 'success'`.
|
|
166
|
+
*
|
|
167
|
+
* Multiple in-flight invocations are supported — each `call()` returns its
|
|
168
|
+
* own correlationId. The hook tracks only the *latest* invocation in its
|
|
169
|
+
* state; consumers that need all history can subscribe to the client's
|
|
170
|
+
* `job` event directly.
|
|
171
|
+
*/
|
|
172
|
+
export function useCallJob() {
|
|
173
|
+
const { client } = useTangleIframeContext();
|
|
174
|
+
const [invocation, setInvocation] = useState<JobInvocation | null>(null);
|
|
175
|
+
const [latestId, setLatestId] = useState<string | null>(null);
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (!client) return undefined;
|
|
179
|
+
return client.subscribe('job', (next) => {
|
|
180
|
+
// Only update if this is the latest invocation, or no latest tracked.
|
|
181
|
+
setLatestId((prevLatest) => {
|
|
182
|
+
if (prevLatest === null || prevLatest === next.correlationId) {
|
|
183
|
+
setInvocation(next);
|
|
184
|
+
return next.correlationId;
|
|
185
|
+
}
|
|
186
|
+
return prevLatest;
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}, [client]);
|
|
190
|
+
|
|
191
|
+
const call = useCallback(
|
|
192
|
+
async (args: { jobIndex: number; inputs: JobInputs; stream?: boolean }) => {
|
|
193
|
+
if (!client) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
'Job invocation not available in dev mode without a configured stub. See `setDevJobHandler` in the testing harness.',
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
// Clear prior invocation state when starting a new call.
|
|
199
|
+
setInvocation(null);
|
|
200
|
+
const result = await client.callJob(args);
|
|
201
|
+
setLatestId(result.correlationId);
|
|
202
|
+
return result;
|
|
203
|
+
},
|
|
204
|
+
[client],
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const reset = useCallback(() => {
|
|
208
|
+
setInvocation(null);
|
|
209
|
+
setLatestId(null);
|
|
210
|
+
}, []);
|
|
211
|
+
|
|
212
|
+
return useMemo(
|
|
213
|
+
() => ({ call, invocation, reset, isPending: invocation?.status === 'pending' || invocation?.status === 'streaming' }),
|
|
214
|
+
[call, invocation, reset],
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Convenience: returns just the address when connected, or `null`. Most
|
|
220
|
+
* iframe components only care about the address.
|
|
221
|
+
*/
|
|
222
|
+
export function useTangleAddress(): Address | null {
|
|
223
|
+
return useTangleIframeContext().wallet.address;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Whether the iframe has completed its parent-handshake (or is in dev mode). */
|
|
227
|
+
export function useTangleReady(): boolean {
|
|
228
|
+
return useTangleIframeContext().isReady;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Resolved mode — `'bridge'` (real parent) or `'dev'` (standalone). */
|
|
232
|
+
export function useTangleMode(): 'bridge' | 'dev' {
|
|
233
|
+
return useTangleIframeContext().mode;
|
|
234
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tangle Cloud iframe SDK — thin renderer for marketplace blueprints.
|
|
3
|
+
*
|
|
4
|
+
* Use this when building a blueprint UI that will be embedded by the
|
|
5
|
+
* Tangle Cloud dapp. The SDK ships wallet + service-context state
|
|
6
|
+
* subscriptions and a `callJob` helper, all driven by the parent dapp
|
|
7
|
+
* over postMessage. The iframe never imports wagmi, never holds a wallet,
|
|
8
|
+
* never touches the chain.
|
|
9
|
+
*
|
|
10
|
+
* Quick start:
|
|
11
|
+
*
|
|
12
|
+
* import { TangleIframeProvider, useTangleWallet, useCallJob }
|
|
13
|
+
* from '@tangle-network/blueprint-iframe-sdk';
|
|
14
|
+
*
|
|
15
|
+
* <TangleIframeProvider appId="llm-inference">
|
|
16
|
+
* <App />
|
|
17
|
+
* </TangleIframeProvider>
|
|
18
|
+
*
|
|
19
|
+
* function PromptBar() {
|
|
20
|
+
* const { address } = useTangleWallet();
|
|
21
|
+
* const { call, invocation } = useCallJob();
|
|
22
|
+
* return <button onClick={() => call({ jobIndex: 0, inputs: { prompt: '...' }, stream: true })} />;
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* Two execution modes auto-detected:
|
|
26
|
+
*
|
|
27
|
+
* - **bridge** (production): real Tangle Cloud parent. Wallet + service
|
|
28
|
+
* state flows in via postMessage. `callJob` is forwarded upstream and
|
|
29
|
+
* the parent handles RFQ + signing + submission.
|
|
30
|
+
* - **dev** (standalone): no parent detected. Hook surface is identical;
|
|
31
|
+
* drive state via the testing harness or a `<TangleParentHarness>`
|
|
32
|
+
* wrapped around the provider with `mode="bridge"` + the harness
|
|
33
|
+
* origin.
|
|
34
|
+
*
|
|
35
|
+
* The mode is decided once at mount and doesn't switch mid-session.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
TangleIframeProvider,
|
|
40
|
+
useTangleIframeContext,
|
|
41
|
+
TANGLE_CLOUD_ORIGINS_DEFAULT,
|
|
42
|
+
} from './TangleIframeProvider';
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
useTangleWallet,
|
|
46
|
+
useTangleService,
|
|
47
|
+
useCallJob,
|
|
48
|
+
useTangleAddress,
|
|
49
|
+
useTangleReady,
|
|
50
|
+
useTangleMode,
|
|
51
|
+
useChainContext,
|
|
52
|
+
useTanglePublicClient,
|
|
53
|
+
} from './hooks';
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
TangleIframeClient,
|
|
57
|
+
type ClientEventMap,
|
|
58
|
+
type JobInvocation,
|
|
59
|
+
type ServiceSnapshot,
|
|
60
|
+
type TangleIframeClientOptions,
|
|
61
|
+
type WalletSnapshot,
|
|
62
|
+
} from './tangleIframeClient';
|
|
63
|
+
|
|
64
|
+
// Re-export the protocol types so consumers can build their own clients
|
|
65
|
+
// against the same wire format if they want to skip the React layer.
|
|
66
|
+
export type {
|
|
67
|
+
CallJobRequest,
|
|
68
|
+
ChainContext,
|
|
69
|
+
JobInputs,
|
|
70
|
+
JobResultEvent,
|
|
71
|
+
JobResultStatus,
|
|
72
|
+
ServiceContextBroadcast,
|
|
73
|
+
ServiceContextJob,
|
|
74
|
+
ServiceContextOperator,
|
|
75
|
+
SignTypedDataRequest,
|
|
76
|
+
SignTypedDataResult,
|
|
77
|
+
} from '../wallet/parentBridgeProtocol';
|