@tangle-network/blueprint-ui 0.5.1 → 0.5.2
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 → chunk-TM5ROMDV.js} +1 -1
- package/dist/chunk-TM5ROMDV.js.map +1 -0
- package/dist/iframe/index.d.ts +5 -4
- package/dist/iframe/index.js +32 -4
- package/dist/iframe/index.js.map +1 -1
- package/dist/iframe/testing-index.d.ts +2 -2
- package/dist/iframe/testing-index.js +13 -0
- package/dist/iframe/testing-index.js.map +1 -1
- package/dist/{parentBridgeProtocol-BS2zbIvX.d.ts → parentBridgeProtocol-BSgLXg9g.d.ts} +12 -2
- package/dist/{tangleIframeClient-CAyUr99p.d.ts → tangleIframeClient-C7NFG_Dw.d.ts} +11 -1
- package/dist/wallet/index.d.ts +6 -1
- package/dist/wallet/index.js +25 -4
- package/dist/wallet/index.js.map +1 -1
- package/package.json +1 -1
- package/src/iframe/hooks.ts +6 -0
- package/src/iframe/tangleIframeClient.test.ts +37 -0
- package/src/iframe/tangleIframeClient.ts +30 -3
- package/src/iframe/testing.tsx +19 -0
- package/src/wallet/parentBridgeProtocol.ts +15 -0
- package/src/wallet/parentBridgeProvider.ts +39 -4
- package/dist/chunk-ZKICSKZH.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/wallet/detectParentOrigin.ts","../src/wallet/parentBridgeProtocol.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","// 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\n// Ask the parent to ensure a wallet is connected — opening its connect modal\n// if none is. A sandboxed iframe can't reach a wallet extension itself, so\n// this is the *only* way an iframe can initiate a connection: it delegates to\n// the parent, which owns the wallet. Resolves once the parent has an account.\nexport type RequestConnectRequest = {\n kind: 'tangle.app.requestConnect';\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// EIP-712 typed-data signing for publishers that need to sign custom message\n// shapes — operator envelopes, off-chain attestations, claim proofs, etc.\n// The parent renders the typed-data fields in its approval modal so the user\n// can audit what they're signing. Iframes never see the wallet's signing key\n// or private state.\n//\n// Shape mirrors viem's `signTypedData` argument: `domain` + `types` (without\n// the EIP712Domain entry — viem injects it) + `primaryType` + `message`.\n// Validation on the parent side rejects payloads that are obviously\n// malformed (missing primaryType, types map empty, etc.) but does NOT\n// re-shape the message — the user is the one who decides whether to sign.\nexport type SignTypedDataRequest = {\n kind: 'tangle.app.signTypedData';\n correlationId: string;\n chainId: number;\n domain: Readonly<{\n name?: string;\n version?: string;\n chainId?: number;\n verifyingContract?: Address;\n salt?: Hex;\n }>;\n /** EIP-712 types map; do NOT include the EIP712Domain entry (the parent\n * injects it derived from `domain`). */\n types: Readonly<Record<string, ReadonlyArray<{ name: string; type: string }>>>;\n /** Top-level type name in `types` whose values appear in `message`. */\n primaryType: string;\n /** The actual typed-data values. Shape matches `types[primaryType]`. */\n message: Readonly<Record<string, unknown>>;\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 ConnectResult = {\n kind: 'tangle.app.connectResult';\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 SignTypedDataResult = {\n kind: 'tangle.app.signTypedDataResult';\n} & ResultEnvelope<{ signature: 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\n// ─── Service context (parent → iframe) ──────────────────────────────────────\n//\n// Iframe blueprints embedded by Tangle Cloud need to know which service +\n// blueprint they're rendering for, plus which operators are quoted. The\n// parent broadcasts this on mount and on every change (mode picker swap,\n// new service activation, operator delta). The iframe just reads — it\n// doesn't query the chain itself.\n//\n// The thin-iframe SDK exposes this as `useTangleService()`. Iframes that\n// use the full wagmi connector path can still listen to `serviceContext`\n// for routing convenience.\n\nexport type ServiceContextOperator = {\n readonly address: Address;\n readonly rpcAddress: string | undefined;\n readonly status: 'active' | 'inactive' | 'unknown';\n};\n\nexport type ServiceContextJob = {\n readonly index: number;\n readonly name: string;\n readonly inputSchema?: unknown;\n};\n\n/**\n * Chain configuration the parent broadcasts to the iframe along with\n * service context. Iframes use this to build a `viem` public client for\n * READ-ONLY queries (`useTanglePublicClient` is the convenience hook).\n *\n * Iframes can ignore this and roll their own RPC config — particularly\n * when they need to read from chains OTHER than the active one (e.g. a\n * trading dapp pulling oracle data from mainnet while the active service\n * lives on Base Sepolia). The injected client is a hint, not a constraint.\n *\n * `rpcUrl` is the public RPC the parent uses, NOT a wallet RPC. Iframes\n * cannot sign or submit with this URL; signing always routes upstream via\n * the bridge.\n */\nexport type ChainContext = {\n readonly id: number;\n readonly name: string;\n readonly rpcUrl: string;\n /** Block-explorer base URL — useful for rendering tx links. */\n readonly blockExplorerUrl?: string;\n /** Native currency metadata for cost displays. */\n readonly nativeCurrency?: { readonly name: string; readonly symbol: string; readonly decimals: number };\n};\n\nexport type ServiceContextBroadcast = {\n kind: 'tangle.app.serviceContext';\n readonly blueprintId: string;\n readonly serviceId: string | null;\n readonly operators: readonly ServiceContextOperator[];\n readonly jobs: readonly ServiceContextJob[];\n readonly mode: string | null;\n /** Active chain the parent is connected to; iframes can build a viem\n * publicClient against this for convenience. Optional for backwards\n * compatibility with parents that haven't been upgraded yet. */\n readonly chain?: ChainContext;\n};\n\n// ─── Job invocation (iframe ↔ parent) ────────────────────────────────────────\n//\n// Instead of the iframe wiring up its own EIP-712 quote / sign / submit\n// flow, it sends a single CallJob request upstream. The parent does the\n// whole dance (fetch RFQ quote, build typed data, request user signature,\n// submit on-chain) and streams results back. The iframe never touches\n// chain logic.\n\nexport type JobInputs = Readonly<Record<string, unknown>>;\n\nexport type CallJobRequest = {\n kind: 'tangle.app.callJob';\n correlationId: string;\n /** Job index within the blueprint, e.g. 0 for the primary entry-point. */\n jobIndex: number;\n /** Free-form inputs validated by the parent against the on-chain ABI. */\n inputs: JobInputs;\n /**\n * Whether the publisher wants intermediate progress (streaming chunks)\n * or just the terminal result. Streaming jobs (LLM generation, video\n * encode) opt in; one-shots (embeddings, classifications) don't.\n */\n stream?: boolean;\n};\n\nexport type JobResultStatus = 'pending' | 'streaming' | 'success' | 'error';\n\nexport type JobResultEvent = {\n kind: 'tangle.app.jobResult';\n correlationId: string;\n status: JobResultStatus;\n /** Present on `streaming` and `success`. Shape is publisher-defined. */\n data?: unknown;\n /** Present on `streaming` only — incremental chunk for live UI. */\n chunk?: unknown;\n /** Present on `error`. Human-readable. */\n error?: string;\n /** Optional progress metadata (e.g. `{ percent: 0.42, eta_ms: 8000 }`). */\n progress?: { readonly percent?: number; readonly eta_ms?: number };\n};\n\nexport type ParentMessage =\n | HandshakeAck\n | ReadAccountResult\n | ConnectResult\n | SwitchChainResult\n | SignMessageResult\n | SignTransactionResult\n | SignTypedDataResult\n | AccountChanged\n | ChainChanged\n | ServiceContextBroadcast\n | JobResultEvent;\n\nexport type IframeRequest =\n | HandshakeRequest\n | ReadAccountRequest\n | RequestConnectRequest\n | SwitchChainRequest\n | SignMessageRequest\n | SignTransactionRequest\n | SignTypedDataRequest\n | CallJobRequest;\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"],"mappings":";AAiBO,IAAM,+BAA+B,OAAO,OAAO;AAAA,EACxD;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AACF,CAAU;AAEV,SAAS,qBAAoC;AAC3C,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,MAAM,SAAS;AACrB,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACF,WAAO,IAAI,IAAI,GAAG,EAAE;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAiBO,SAAS,8BACd,UAAgD,CAAC,GAClC;AACf,MAAI,OAAO,WAAW,eAAe,OAAO,WAAW,QAAQ;AAC7D,WAAO;AAAA,EACT;AACA,QAAM,YAAY,oBAAI,IAAY;AAAA,IAChC,GAAG;AAAA,IACH,GAAI,QAAQ,gBAAgB,CAAC;AAAA,EAC/B,CAAC;AACD,QAAM,iBAAiB,mBAAmB;AAC1C,MAAI,kBAAkB,UAAU,IAAI,cAAc,GAAG;AACnD,WAAO;AAAA,EACT;AACA,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AACxC,UAAM,WAAW,IAAI,aAAa,IAAI,QAAQ;AAC9C,QAAI,YAAY,UAAU,IAAI,QAAQ,EAAG,QAAO;AAAA,EAClD,QAAQ;AAAA,EAER;AACA,SAAO;AACT;;;ACnEO,IAAM,iCAAiC;AACvC,IAAM,gCAAgC;AA4PtC,IAAM,oBAAoB;AAO1B,SAAS,kBAAkB,QAAwB;AACxD,QAAM,SACJ,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,aAC1D,OAAO,WAAW,IAClB,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,SAAS,EAAE;AAClE,SAAO,GAAG,MAAM,IAAI,MAAM;AAC5B;","names":[]}
|
package/dist/iframe/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { T as TangleIframeClient, W as WalletSnapshot, S as ServiceSnapshot, J as JobInvocation } from '../tangleIframeClient-
|
|
4
|
-
export { C as ClientEventMap, a as TangleIframeClientOptions } from '../tangleIframeClient-
|
|
3
|
+
import { T as TangleIframeClient, W as WalletSnapshot, S as ServiceSnapshot, J as JobInvocation } from '../tangleIframeClient-C7NFG_Dw.js';
|
|
4
|
+
export { C as ClientEventMap, a as TangleIframeClientOptions } from '../tangleIframeClient-C7NFG_Dw.js';
|
|
5
5
|
import { Address, PublicClient, Hex } from 'viem';
|
|
6
|
-
import { J as JobInputs, b as ChainContext, m as SignTypedDataRequest } from '../parentBridgeProtocol-
|
|
7
|
-
export { C as CallJobRequest, d as JobResultEvent, e as JobResultStatus, S as ServiceContextBroadcast, g as ServiceContextJob, h as ServiceContextOperator, n as SignTypedDataResult } from '../parentBridgeProtocol-
|
|
6
|
+
import { J as JobInputs, b as ChainContext, m as SignTypedDataRequest } from '../parentBridgeProtocol-BSgLXg9g.js';
|
|
7
|
+
export { C as CallJobRequest, d as JobResultEvent, e as JobResultStatus, S as ServiceContextBroadcast, g as ServiceContextJob, h as ServiceContextOperator, n as SignTypedDataResult } from '../parentBridgeProtocol-BSgLXg9g.js';
|
|
8
8
|
export { T as TANGLE_CLOUD_ORIGINS_DEFAULT } from '../detectParentOrigin-BYruoIdc.js';
|
|
9
9
|
|
|
10
10
|
type Props = {
|
|
@@ -59,6 +59,7 @@ declare function useTangleIframeContext(): ContextValue;
|
|
|
59
59
|
* dapp's wagmi config + ConnectKit modal.
|
|
60
60
|
*/
|
|
61
61
|
declare function useTangleWallet(): WalletSnapshot & {
|
|
62
|
+
connect: () => Promise<Address | null>;
|
|
62
63
|
signMessage: (message: string) => Promise<Hex>;
|
|
63
64
|
sendTransaction: (tx: {
|
|
64
65
|
to: Address;
|
package/dist/iframe/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
TANGLE_IFRAME_PROTOCOL_VERSION,
|
|
5
5
|
detectTangleCloudParentOrigin,
|
|
6
6
|
makeCorrelationId
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-TM5ROMDV.js";
|
|
8
8
|
|
|
9
9
|
// src/iframe/TangleIframeProvider.tsx
|
|
10
10
|
import {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
|
|
19
19
|
// src/iframe/tangleIframeClient.ts
|
|
20
20
|
var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
|
|
21
|
+
var CONNECT_REQUEST_TIMEOUT_MS = 3e5;
|
|
21
22
|
var HANDSHAKE_RETRY_MS = 250;
|
|
22
23
|
var HANDSHAKE_RETRY_BUDGET_MS = 1e4;
|
|
23
24
|
var NULL_WALLET = {
|
|
@@ -94,6 +95,27 @@ var TangleIframeClient = class {
|
|
|
94
95
|
};
|
|
95
96
|
}
|
|
96
97
|
// ── Wallet operations ───────────────────────────────────────────────────
|
|
98
|
+
/**
|
|
99
|
+
* Ask the parent dapp to connect a wallet — opening its connect modal if
|
|
100
|
+
* none is connected. The iframe is sandboxed and cannot reach a wallet
|
|
101
|
+
* itself, so connection is always delegated to the parent. Resolves with the
|
|
102
|
+
* connected address (or `null` if the user dismissed without connecting).
|
|
103
|
+
*
|
|
104
|
+
* Uses a long timeout (the user is interacting with a modal). Already-
|
|
105
|
+
* connected parents resolve immediately.
|
|
106
|
+
*/
|
|
107
|
+
async connect() {
|
|
108
|
+
await this.ensureBootstrapped();
|
|
109
|
+
const data = await this.dispatchWallet(
|
|
110
|
+
"tangle.app.requestConnect",
|
|
111
|
+
{},
|
|
112
|
+
CONNECT_REQUEST_TIMEOUT_MS
|
|
113
|
+
);
|
|
114
|
+
const { account, chainId } = data;
|
|
115
|
+
const address = account === NO_WALLET_ADDRESS ? null : account;
|
|
116
|
+
this.updateWallet({ address, chainId, isConnected: address !== null });
|
|
117
|
+
return address;
|
|
118
|
+
}
|
|
97
119
|
async signMessage(message) {
|
|
98
120
|
await this.ensureBootstrapped();
|
|
99
121
|
return this.dispatchWallet("tangle.app.signMessage", {
|
|
@@ -246,15 +268,16 @@ var TangleIframeClient = class {
|
|
|
246
268
|
return;
|
|
247
269
|
}
|
|
248
270
|
};
|
|
249
|
-
async dispatchWallet(kind, payload) {
|
|
271
|
+
async dispatchWallet(kind, payload, timeoutMs) {
|
|
250
272
|
return new Promise((resolve, reject) => {
|
|
251
273
|
const correlationId = makeCorrelationId(kind);
|
|
252
|
-
const timeout = this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
274
|
+
const timeout = timeoutMs ?? this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
253
275
|
const expectedKind = {
|
|
254
276
|
"tangle.app.signMessage": "tangle.app.signMessageResult",
|
|
255
277
|
"tangle.app.signTransaction": "tangle.app.signTransactionResult",
|
|
256
278
|
"tangle.app.signTypedData": "tangle.app.signTypedDataResult",
|
|
257
|
-
"tangle.app.switchChain": "tangle.app.switchChainResult"
|
|
279
|
+
"tangle.app.switchChain": "tangle.app.switchChainResult",
|
|
280
|
+
"tangle.app.requestConnect": "tangle.app.connectResult"
|
|
258
281
|
}[kind];
|
|
259
282
|
const timer = setTimeout(() => {
|
|
260
283
|
window.removeEventListener("message", listener);
|
|
@@ -449,6 +472,10 @@ import {
|
|
|
449
472
|
} from "viem";
|
|
450
473
|
function useTangleWallet() {
|
|
451
474
|
const { client, wallet } = useTangleIframeContext();
|
|
475
|
+
const connect = useCallback(() => {
|
|
476
|
+
if (!client) throw new Error("Wallet not available in dev mode.");
|
|
477
|
+
return client.connect();
|
|
478
|
+
}, [client]);
|
|
452
479
|
const signMessage = useCallback(
|
|
453
480
|
(message) => {
|
|
454
481
|
if (!client) throw new Error("Wallet not available in dev mode.");
|
|
@@ -479,6 +506,7 @@ function useTangleWallet() {
|
|
|
479
506
|
);
|
|
480
507
|
return {
|
|
481
508
|
...wallet,
|
|
509
|
+
connect,
|
|
482
510
|
signMessage,
|
|
483
511
|
sendTransaction,
|
|
484
512
|
signTypedData,
|
package/dist/iframe/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/iframe/TangleIframeProvider.tsx","../../src/iframe/tangleIframeClient.ts","../../src/iframe/hooks.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\n\nimport {\n TangleIframeClient,\n type ServiceSnapshot,\n type TangleIframeClientOptions,\n type WalletSnapshot,\n} from './tangleIframeClient';\nimport {\n detectTangleCloudParentOrigin,\n TANGLE_CLOUD_ORIGINS_DEFAULT,\n} from '../wallet/detectParentOrigin';\n\ntype Props = {\n appId: string;\n /** Override the detected parent origin (e.g. dev/staging deploys). */\n parentOrigin?: string;\n /** Extra trusted origins for `detectTangleCloudParentOrigin`. */\n extraOrigins?: readonly string[];\n /**\n * Override the bootstrap behavior. When `'auto'` (default), the SDK\n * sniffs the embed context: real parent → install the bridge, top-frame\n * → drop into dev mode. `'bridge'` forces real-parent mode and throws\n * if no parent is detected. `'dev'` forces dev mode even when embedded\n * — useful for component-level tests.\n */\n mode?: 'auto' | 'bridge' | 'dev';\n children: ReactNode;\n};\n\ntype ContextValue = {\n readonly client: TangleIframeClient | null;\n readonly wallet: WalletSnapshot;\n readonly service: ServiceSnapshot;\n readonly mode: 'bridge' | 'dev';\n readonly isReady: boolean;\n};\n\nconst TangleIframeContext = createContext<ContextValue | null>(null);\n\nconst NULL_WALLET: WalletSnapshot = {\n address: null,\n chainId: null,\n isConnected: false,\n};\nconst NULL_SERVICE: ServiceSnapshot = {\n blueprintId: null,\n serviceId: null,\n operators: [],\n jobs: [],\n mode: null,\n chain: null,\n};\n\n/**\n * Iframe-blueprint root provider. Wrap your app once at the entry point.\n *\n * In `auto` mode (default) the SDK detects whether the app is embedded by a\n * trusted Tangle Cloud parent. If yes → installs the postMessage bridge.\n * If no (running standalone at `localhost:5173` etc.) → enters **dev mode**\n * with an in-memory state machine that the developer can drive via the\n * exported debug controls. Dev mode keeps the hook surface identical to\n * production so component code never branches on embed-vs-not.\n *\n * Three lifecycle stages:\n *\n * 1. Mount — `client` is created, mode is decided.\n * 2. Bootstrap — handshake (bridge) or first-paint setup (dev). The\n * `isReady` flag flips to true.\n * 3. Active — wallet + service snapshots flow in via subscriptions.\n */\nexport function TangleIframeProvider({\n appId,\n parentOrigin: explicitOrigin,\n extraOrigins,\n mode: requestedMode = 'auto',\n children,\n}: Props) {\n // Resolve the effective mode once at mount. Switching modes mid-session\n // would tear down the bridge / dev state inconsistently; restart instead.\n const resolution = useMemo(() => {\n if (requestedMode === 'dev') {\n return { mode: 'dev' as const, parentOrigin: null };\n }\n const detected =\n explicitOrigin ?? detectTangleCloudParentOrigin({ extraOrigins });\n if (requestedMode === 'bridge') {\n if (!detected) {\n // eslint-disable-next-line no-console\n console.error(\n '[TangleIframeProvider] mode=\"bridge\" but no trusted parent was detected. Falling back to dev mode.',\n );\n return { mode: 'dev' as const, parentOrigin: null };\n }\n return { mode: 'bridge' as const, parentOrigin: detected };\n }\n // auto: bridge when detected, dev otherwise.\n return detected\n ? { mode: 'bridge' as const, parentOrigin: detected }\n : { mode: 'dev' as const, parentOrigin: null };\n }, [requestedMode, explicitOrigin, extraOrigins]);\n\n const clientRef = useRef<TangleIframeClient | null>(null);\n const [wallet, setWallet] = useState<WalletSnapshot>(NULL_WALLET);\n const [service, setService] = useState<ServiceSnapshot>(NULL_SERVICE);\n const [isReady, setIsReady] = useState(false);\n\n useEffect(() => {\n if (resolution.mode === 'dev') {\n // Dev mode: no bridge. The DevHarness component (or a test) seeds\n // wallet + service via `setDevWallet` / `setDevService` on the\n // returned context. Mark ready immediately so app code unblocks.\n setIsReady(true);\n return undefined;\n }\n // Bridge mode\n const options: TangleIframeClientOptions = {\n parentOrigin: resolution.parentOrigin,\n appId,\n };\n const client = new TangleIframeClient(options);\n clientRef.current = client;\n const unsubWallet = client.subscribe('wallet', setWallet);\n const unsubService = client.subscribe('service', setService);\n client.install();\n setIsReady(true);\n return () => {\n unsubWallet();\n unsubService();\n client.uninstall();\n clientRef.current = null;\n setIsReady(false);\n };\n }, [resolution, appId]);\n\n const value = useMemo<ContextValue>(\n () => ({\n client: clientRef.current,\n wallet,\n service,\n mode: resolution.mode,\n isReady,\n }),\n [wallet, service, resolution.mode, isReady],\n );\n\n return (\n <TangleIframeContext.Provider value={value}>\n {children}\n </TangleIframeContext.Provider>\n );\n}\n\nexport function useTangleIframeContext(): ContextValue {\n const ctx = useContext(TangleIframeContext);\n if (!ctx) {\n throw new Error(\n 'useTangleIframeContext must be used inside <TangleIframeProvider>.',\n );\n }\n return ctx;\n}\n\nexport { TANGLE_CLOUD_ORIGINS_DEFAULT };\n","// Thin-iframe SDK client — the framework-agnostic state machine that talks\n// to a Tangle Cloud parent dapp over postMessage. React hooks (below) are\n// thin wrappers around an instance of this class.\n//\n// Why a class, not a bag of functions: the iframe lifecycle is stateful —\n// handshake, account changes, service-context broadcasts, in-flight job\n// requests. The class owns that state once; hooks subscribe via listeners.\n// Testing the protocol shape doesn't require React.\n\nimport type { Address, Hex } from 'viem';\n\nimport {\n makeCorrelationId,\n NO_WALLET_ADDRESS,\n TANGLE_IFRAME_PROTOCOL_VERSION,\n type CallJobRequest,\n type ChainContext,\n type JobInputs,\n type JobResultEvent,\n type JobResultStatus,\n type ParentMessage,\n type ServiceContextBroadcast,\n type ServiceContextJob,\n type ServiceContextOperator,\n type SignTypedDataRequest,\n} from '../wallet/parentBridgeProtocol';\n\nexport type WalletSnapshot = {\n readonly address: Address | null;\n readonly chainId: number | null;\n readonly isConnected: boolean;\n};\n\nexport type ServiceSnapshot = {\n readonly blueprintId: string | null;\n readonly serviceId: string | null;\n readonly operators: readonly ServiceContextOperator[];\n readonly jobs: readonly ServiceContextJob[];\n readonly mode: string | null;\n /** Chain context broadcast by the parent — drives `useTanglePublicClient`.\n * `null` when the parent hasn't sent one (older parent or dev mode). */\n readonly chain: ChainContext | null;\n};\n\nexport type JobInvocation = {\n readonly correlationId: string;\n readonly status: JobResultStatus;\n readonly data?: unknown;\n readonly chunks: readonly unknown[];\n readonly error?: string;\n readonly progress?: { readonly percent?: number; readonly eta_ms?: number };\n};\n\nexport type ClientEventMap = {\n wallet: WalletSnapshot;\n service: ServiceSnapshot;\n job: JobInvocation;\n};\n\ntype Listener<K extends keyof ClientEventMap> = (\n value: ClientEventMap[K],\n) => void;\n\nexport type TangleIframeClientOptions = {\n /**\n * Origin of the parent dapp. The client posts every message with this\n * exact `targetOrigin` and rejects inbound messages from any other origin.\n * Pass `'*'` only in dev — production must pin to the real parent\n * (`https://cloud.tangle.tools` etc.).\n */\n parentOrigin: string;\n /**\n * Stable identifier for this iframe app. The parent surfaces it in\n * handshake logs + uses it for permission scoping.\n */\n appId: string;\n /**\n * Per-request timeout. Defaults to 60s — long enough for a user to\n * read + approve a signing prompt in the parent. Long-running jobs\n * stream progress events; the request \"completes\" only on terminal\n * status, so the timeout protects against parents that drop replies\n * entirely.\n */\n requestTimeoutMs?: number;\n};\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 60_000;\nconst HANDSHAKE_RETRY_MS = 250;\nconst HANDSHAKE_RETRY_BUDGET_MS = 10_000;\nconst NULL_WALLET: WalletSnapshot = {\n address: null,\n chainId: null,\n isConnected: false,\n};\nconst NULL_SERVICE: ServiceSnapshot = {\n blueprintId: null,\n serviceId: null,\n operators: [],\n jobs: [],\n mode: null,\n chain: null,\n};\n\ntype PendingJob = {\n resolve: (value: JobInvocation) => void;\n reject: (reason: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n invocation: JobInvocation;\n};\n\nexport class TangleIframeClient {\n private wallet: WalletSnapshot = NULL_WALLET;\n private service: ServiceSnapshot = NULL_SERVICE;\n private handshakeAcked = false;\n private handshakeWaiters: Array<() => void> = [];\n private installed = false;\n private handshakeRetry: ReturnType<typeof setInterval> | null = null;\n private listeners: {\n [K in keyof ClientEventMap]: Set<Listener<K>>;\n } = {\n wallet: new Set(),\n service: new Set(),\n job: new Set(),\n };\n private pendingJobs = new Map<string, PendingJob>();\n\n constructor(private readonly options: TangleIframeClientOptions) {}\n\n /** Wire the global message listener + initial handshake. Idempotent. */\n install(): void {\n if (this.installed || typeof window === 'undefined') return;\n this.installed = true;\n window.addEventListener('message', this.handleParentMessage);\n this.postHandshake();\n // Stand up a bounded retry. The parent may attach its listener slightly\n // after the iframe loads (React mounts child effects before parent\n // effects; a real parent may create the frame before its handler is\n // ready), so a single handshake can be dropped. Retry until acked.\n if (this.handshakeRetry === null) {\n let elapsed = 0;\n this.handshakeRetry = setInterval(() => {\n elapsed += HANDSHAKE_RETRY_MS;\n if (this.handshakeAcked || elapsed >= HANDSHAKE_RETRY_BUDGET_MS) {\n this.clearHandshakeRetry();\n return;\n }\n this.postHandshake();\n }, HANDSHAKE_RETRY_MS);\n }\n }\n\n uninstall(): void {\n if (!this.installed || typeof window === 'undefined') return;\n this.installed = false;\n this.clearHandshakeRetry();\n window.removeEventListener('message', this.handleParentMessage);\n for (const [, pending] of this.pendingJobs) {\n clearTimeout(pending.timer);\n pending.reject(new Error('Tangle iframe client uninstalled'));\n }\n this.pendingJobs.clear();\n }\n\n // ── State accessors ─────────────────────────────────────────────────────\n\n getWallet(): WalletSnapshot {\n return this.wallet;\n }\n getService(): ServiceSnapshot {\n return this.service;\n }\n\n // ── Subscription API (used by React hooks) ──────────────────────────────\n\n subscribe<K extends keyof ClientEventMap>(\n event: K,\n listener: Listener<K>,\n ): () => void {\n this.listeners[event].add(listener);\n return () => {\n this.listeners[event].delete(listener);\n };\n }\n\n // ── Wallet operations ───────────────────────────────────────────────────\n\n async signMessage(message: string): Promise<Hex> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.signMessage', {\n chainId: this.wallet.chainId ?? 0,\n message,\n }).then((data) => (data as { signature: Hex }).signature);\n }\n\n async sendTransaction(tx: {\n to: Address;\n data: Hex;\n value?: bigint;\n }): Promise<Hex> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.signTransaction', {\n chainId: this.wallet.chainId ?? 0,\n to: tx.to,\n data: tx.data,\n ...(tx.value !== undefined ? { value: tx.value.toString(10) } : {}),\n }).then((data) => (data as { txHash: Hex }).txHash);\n }\n\n async switchChain(chainId: number): Promise<number> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.switchChain', { chainId }).then(\n (data) => (data as { chainId: number }).chainId,\n );\n }\n\n /**\n * EIP-712 typed-data signing. The parent renders the typed-data fields in\n * its approval modal; the user audits what they're signing. Use for\n * operator envelopes, off-chain attestations, anything that needs a\n * signature outside the standard blueprint-job RFQ flow.\n *\n * Shape mirrors viem's `signTypedData` argument. Do not include the\n * EIP712Domain entry in `types` — the parent injects it from `domain`.\n */\n async signTypedData(args: {\n domain: SignTypedDataRequest['domain'];\n types: SignTypedDataRequest['types'];\n primaryType: string;\n message: Readonly<Record<string, unknown>>;\n }): Promise<Hex> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.signTypedData', {\n chainId: this.wallet.chainId ?? 0,\n domain: args.domain,\n types: args.types,\n primaryType: args.primaryType,\n message: args.message,\n }).then((data) => (data as { signature: Hex }).signature);\n }\n\n // ── Job invocation ──────────────────────────────────────────────────────\n\n /**\n * Invoke a blueprint job. Returns a Promise that resolves on terminal\n * status (`success` or `error`); subscribe to the `job` event for\n * intermediate streaming chunks.\n *\n * Streaming opt-in: pass `stream: true` if the publisher's job emits\n * chunks (LLM generation, video encoding). One-shot jobs (embeddings,\n * classifications) skip the streaming machinery.\n */\n async callJob(args: {\n jobIndex: number;\n inputs: JobInputs;\n stream?: boolean;\n }): Promise<JobInvocation> {\n await this.ensureBootstrapped();\n const correlationId = makeCorrelationId('tangle.app.callJob');\n const timeout =\n this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n return new Promise<JobInvocation>((resolve, reject) => {\n const invocation: JobInvocation = {\n correlationId,\n status: 'pending',\n chunks: [],\n };\n const timer = setTimeout(() => {\n this.pendingJobs.delete(correlationId);\n reject(\n bridgeError(4900, `Job did not respond within ${timeout}ms`),\n );\n }, timeout);\n this.pendingJobs.set(correlationId, {\n resolve,\n reject,\n timer,\n invocation,\n });\n const message: CallJobRequest = {\n kind: 'tangle.app.callJob',\n correlationId,\n jobIndex: args.jobIndex,\n inputs: args.inputs,\n ...(args.stream !== undefined ? { stream: args.stream } : {}),\n };\n this.postToParent(message);\n // Emit pending immediately so consumer UIs can show optimistic state.\n this.emit('job', invocation);\n });\n }\n\n // ── Internals ───────────────────────────────────────────────────────────\n\n private clearHandshakeRetry(): void {\n if (this.handshakeRetry !== null) {\n clearInterval(this.handshakeRetry);\n this.handshakeRetry = null;\n }\n }\n\n private postHandshake(): void {\n this.postToParent({\n kind: 'tangle.app.handshake',\n appId: this.options.appId,\n version: TANGLE_IFRAME_PROTOCOL_VERSION,\n });\n }\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; defensive only — postMessage shouldn't throw.\n }\n }\n\n private handleParentMessage = (event: MessageEvent): void => {\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 this.clearHandshakeRetry();\n for (const resolve of this.handshakeWaiters) resolve();\n this.handshakeWaiters = [];\n return;\n case 'tangle.app.readAccountResult':\n if (message.ok) {\n this.updateWallet({\n address:\n message.data.account === NO_WALLET_ADDRESS\n ? null\n : message.data.account,\n chainId: message.data.chainId,\n isConnected: message.data.account !== NO_WALLET_ADDRESS,\n });\n }\n return;\n case 'tangle.app.accountChanged':\n this.updateWallet({\n address: message.account,\n chainId: this.wallet.chainId,\n isConnected: message.account !== null,\n });\n return;\n case 'tangle.app.chainChanged':\n this.updateWallet({\n address: this.wallet.address,\n chainId: message.chainId,\n isConnected: this.wallet.isConnected,\n });\n return;\n case 'tangle.app.serviceContext':\n this.updateService(message);\n return;\n case 'tangle.app.jobResult':\n this.handleJobResult(message);\n return;\n // Wallet-shape responses (signMessageResult etc.) are routed by\n // dispatchWallet's promise resolver, not here.\n default:\n return;\n }\n };\n\n private async dispatchWallet(\n kind:\n | 'tangle.app.signMessage'\n | 'tangle.app.signTransaction'\n | 'tangle.app.signTypedData'\n | 'tangle.app.switchChain',\n payload: Record<string, unknown>,\n ): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const correlationId = makeCorrelationId(kind);\n const timeout =\n this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const expectedKind = (\n {\n 'tangle.app.signMessage': 'tangle.app.signMessageResult',\n 'tangle.app.signTransaction': 'tangle.app.signTransactionResult',\n 'tangle.app.signTypedData': 'tangle.app.signTypedDataResult',\n 'tangle.app.switchChain': 'tangle.app.switchChainResult',\n } as const\n )[kind];\n const timer = setTimeout(() => {\n window.removeEventListener('message', listener);\n reject(bridgeError(4900, `Parent did not respond to ${kind} in ${timeout}ms`));\n }, timeout);\n const listener = (event: MessageEvent) => {\n if (event.origin !== this.options.parentOrigin) return;\n const data = event.data;\n if (typeof data !== 'object' || data === null) return;\n const msg = data as ParentMessage;\n if (\n msg.kind !== expectedKind ||\n !('correlationId' in msg) ||\n msg.correlationId !== correlationId\n ) {\n return;\n }\n clearTimeout(timer);\n window.removeEventListener('message', listener);\n // Narrow the type — expectedKind is the wallet-shape `{ok, data|error}` envelope\n const env = msg as {\n ok: boolean;\n data?: unknown;\n error?: string;\n };\n if (env.ok) {\n resolve(env.data);\n } else {\n reject(bridgeError(4001, env.error ?? 'Parent rejected request'));\n }\n };\n window.addEventListener('message', listener);\n this.postToParent({ kind, correlationId, ...payload });\n });\n }\n\n private handleJobResult(message: JobResultEvent): void {\n const pending = this.pendingJobs.get(message.correlationId);\n if (!pending) return;\n const updated: JobInvocation = {\n correlationId: message.correlationId,\n status: message.status,\n chunks:\n message.chunk !== undefined\n ? [...pending.invocation.chunks, message.chunk]\n : pending.invocation.chunks,\n ...(message.data !== undefined ? { data: message.data } : {}),\n ...(message.error !== undefined ? { error: message.error } : {}),\n ...(message.progress !== undefined ? { progress: message.progress } : {}),\n };\n pending.invocation = updated;\n this.emit('job', updated);\n if (message.status === 'success' || message.status === 'error') {\n clearTimeout(pending.timer);\n this.pendingJobs.delete(message.correlationId);\n if (message.status === 'success') {\n pending.resolve(updated);\n } else {\n pending.reject(bridgeError(4001, message.error ?? 'Job failed'));\n }\n }\n }\n\n private updateWallet(next: WalletSnapshot): void {\n if (\n this.wallet.address === next.address &&\n this.wallet.chainId === next.chainId &&\n this.wallet.isConnected === next.isConnected\n ) {\n return;\n }\n this.wallet = next;\n this.emit('wallet', next);\n }\n\n private updateService(broadcast: ServiceContextBroadcast): void {\n const next: ServiceSnapshot = {\n blueprintId: broadcast.blueprintId,\n serviceId: broadcast.serviceId,\n operators: broadcast.operators,\n jobs: broadcast.jobs,\n mode: broadcast.mode,\n chain: broadcast.chain ?? null,\n };\n this.service = next;\n this.emit('service', next);\n }\n\n private emit<K extends keyof ClientEventMap>(\n event: K,\n value: ClientEventMap[K],\n ): void {\n for (const listener of [...this.listeners[event]]) {\n try {\n (listener as Listener<K>)(value);\n } catch {\n // Listener bugs shouldn't break the bridge.\n }\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 const retry = setInterval(() => {\n if (this.handshakeAcked) {\n clearInterval(retry);\n return;\n }\n this.postHandshake();\n }, 500);\n setTimeout(() => clearInterval(retry), 10_000);\n });\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","import { useCallback, useEffect, useMemo, useState } from 'react';\nimport {\n createPublicClient,\n http,\n type Address,\n type Chain,\n type Hex,\n type PublicClient,\n} from 'viem';\n\nimport { useTangleIframeContext } from './TangleIframeProvider';\nimport type {\n JobInvocation,\n ServiceSnapshot,\n WalletSnapshot,\n} from './tangleIframeClient';\nimport type {\n ChainContext,\n JobInputs,\n SignTypedDataRequest,\n} from '../wallet/parentBridgeProtocol';\n\n/**\n * Read-only view of the connected wallet, plus the operations the iframe\n * can request the parent to perform.\n *\n * The iframe never holds a private key, never sees `window.ethereum`, never\n * imports wagmi. All wallet work happens upstream in the Tangle Cloud\n * dapp's wagmi config + ConnectKit modal.\n */\nexport function useTangleWallet(): WalletSnapshot & {\n signMessage: (message: string) => Promise<Hex>;\n sendTransaction: (tx: {\n to: Address;\n data: Hex;\n value?: bigint;\n }) => Promise<Hex>;\n signTypedData: (args: {\n domain: SignTypedDataRequest['domain'];\n types: SignTypedDataRequest['types'];\n primaryType: string;\n message: Readonly<Record<string, unknown>>;\n }) => Promise<Hex>;\n switchChain: (chainId: number) => Promise<number>;\n} {\n const { client, wallet } = useTangleIframeContext();\n const signMessage = useCallback(\n (message: string) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.signMessage(message);\n },\n [client],\n );\n const sendTransaction = useCallback(\n (tx: { to: Address; data: Hex; value?: bigint }) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.sendTransaction(tx);\n },\n [client],\n );\n const signTypedData = useCallback(\n (args: {\n domain: SignTypedDataRequest['domain'];\n types: SignTypedDataRequest['types'];\n primaryType: string;\n message: Readonly<Record<string, unknown>>;\n }) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.signTypedData(args);\n },\n [client],\n );\n const switchChain = useCallback(\n (chainId: number) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.switchChain(chainId);\n },\n [client],\n );\n return {\n ...wallet,\n signMessage,\n sendTransaction,\n signTypedData,\n switchChain,\n };\n}\n\n/**\n * Chain configuration broadcast by the parent: chain id, name, RPC URL,\n * block explorer, native currency. Returns `null` until the parent has\n * sent its first `serviceContext` broadcast (or in dev mode without a\n * seeded harness).\n *\n * Use this when you want to display chain-aware info (block explorer\n * links, native currency labels) or when you want to build your own viem\n * client with the parent's RPC URL. For a pre-built read-only client,\n * see `useTanglePublicClient()`.\n */\nexport function useChainContext(): ChainContext | null {\n return useTangleIframeContext().service.chain;\n}\n\n/**\n * Read-only viem `PublicClient` pinned to the chain the parent dapp is\n * connected to. Useful for `readContract`, `getBalance`, `multicall`, etc.\n *\n * Returns `null` until the parent broadcasts a chain context. Iframes that\n * need to read from chains *other* than the active one should bring their\n * own client — this hook is a convenience for the common case, not a\n * constraint. Multi-chain dashboards just create additional clients\n * directly via `createPublicClient`.\n *\n * Memoized per chain id + RPC URL, so consumers get a stable identity\n * across re-renders.\n */\nexport function useTanglePublicClient(): PublicClient | null {\n const chain = useChainContext();\n return useMemo(() => {\n if (!chain) return null;\n const chainConfig: Chain = {\n id: chain.id,\n name: chain.name,\n nativeCurrency:\n chain.nativeCurrency !== undefined\n ? { ...chain.nativeCurrency }\n : { name: 'Ether', symbol: 'ETH', decimals: 18 },\n rpcUrls: {\n default: { http: [chain.rpcUrl] },\n },\n ...(chain.blockExplorerUrl\n ? {\n blockExplorers: {\n default: { name: 'Explorer', url: chain.blockExplorerUrl },\n },\n }\n : {}),\n } as Chain;\n return createPublicClient({\n chain: chainConfig,\n transport: http(chain.rpcUrl),\n });\n }, [chain]);\n}\n\n/**\n * The service the iframe is currently rendering for. Broadcast by the\n * parent dapp on mount + every time the service/mode changes — the iframe\n * never queries the chain or the indexer itself.\n *\n * `serviceId === null` means the operator hasn't deployed an instance yet;\n * the iframe should render its deploy-ready / configuration surface.\n */\nexport function useTangleService(): ServiceSnapshot {\n return useTangleIframeContext().service;\n}\n\n/**\n * Invoke a blueprint job. Returns a callable + a snapshot of the most\n * recent invocation (or null if none yet).\n *\n * Streaming jobs (LLM, video, audio) opt in via `stream: true`. The hook's\n * `invocation.chunks` accumulates each streaming chunk so the UI can render\n * progressive output. For one-shot jobs (embeddings, classification), use\n * the `invocation.data` once `status === 'success'`.\n *\n * Multiple in-flight invocations are supported — each `call()` returns its\n * own correlationId. The hook tracks only the *latest* invocation in its\n * state; consumers that need all history can subscribe to the client's\n * `job` event directly.\n */\nexport function useCallJob() {\n const { client } = useTangleIframeContext();\n const [invocation, setInvocation] = useState<JobInvocation | null>(null);\n const [latestId, setLatestId] = useState<string | null>(null);\n\n useEffect(() => {\n if (!client) return undefined;\n return client.subscribe('job', (next) => {\n // Only update if this is the latest invocation, or no latest tracked.\n setLatestId((prevLatest) => {\n if (prevLatest === null || prevLatest === next.correlationId) {\n setInvocation(next);\n return next.correlationId;\n }\n return prevLatest;\n });\n });\n }, [client]);\n\n const call = useCallback(\n async (args: { jobIndex: number; inputs: JobInputs; stream?: boolean }) => {\n if (!client) {\n throw new Error(\n 'Job invocation not available in dev mode without a configured stub. See `setDevJobHandler` in the testing harness.',\n );\n }\n // Clear prior invocation state when starting a new call.\n setInvocation(null);\n const result = await client.callJob(args);\n setLatestId(result.correlationId);\n return result;\n },\n [client],\n );\n\n const reset = useCallback(() => {\n setInvocation(null);\n setLatestId(null);\n }, []);\n\n return useMemo(\n () => ({ call, invocation, reset, isPending: invocation?.status === 'pending' || invocation?.status === 'streaming' }),\n [call, invocation, reset],\n );\n}\n\n/**\n * Convenience: returns just the address when connected, or `null`. Most\n * iframe components only care about the address.\n */\nexport function useTangleAddress(): Address | null {\n return useTangleIframeContext().wallet.address;\n}\n\n/** Whether the iframe has completed its parent-handshake (or is in dev mode). */\nexport function useTangleReady(): boolean {\n return useTangleIframeContext().isReady;\n}\n\n/** Resolved mode — `'bridge'` (real parent) or `'dev'` (standalone). */\nexport function useTangleMode(): 'bridge' | 'dev' {\n return useTangleIframeContext().mode;\n}\n"],"mappings":";;;;;;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;;;AC8EP,IAAM,6BAA6B;AACnC,IAAM,qBAAqB;AAC3B,IAAM,4BAA4B;AAClC,IAAM,cAA8B;AAAA,EAClC,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AACA,IAAM,eAAgC;AAAA,EACpC,aAAa;AAAA,EACb,WAAW;AAAA,EACX,WAAW,CAAC;AAAA,EACZ,MAAM,CAAC;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AACT;AASO,IAAM,qBAAN,MAAyB;AAAA,EAgB9B,YAA6B,SAAoC;AAApC;AAAA,EAAqC;AAAA,EAArC;AAAA,EAfrB,SAAyB;AAAA,EACzB,UAA2B;AAAA,EAC3B,iBAAiB;AAAA,EACjB,mBAAsC,CAAC;AAAA,EACvC,YAAY;AAAA,EACZ,iBAAwD;AAAA,EACxD,YAEJ;AAAA,IACF,QAAQ,oBAAI,IAAI;AAAA,IAChB,SAAS,oBAAI,IAAI;AAAA,IACjB,KAAK,oBAAI,IAAI;AAAA,EACf;AAAA,EACQ,cAAc,oBAAI,IAAwB;AAAA;AAAA,EAKlD,UAAgB;AACd,QAAI,KAAK,aAAa,OAAO,WAAW,YAAa;AACrD,SAAK,YAAY;AACjB,WAAO,iBAAiB,WAAW,KAAK,mBAAmB;AAC3D,SAAK,cAAc;AAKnB,QAAI,KAAK,mBAAmB,MAAM;AAChC,UAAI,UAAU;AACd,WAAK,iBAAiB,YAAY,MAAM;AACtC,mBAAW;AACX,YAAI,KAAK,kBAAkB,WAAW,2BAA2B;AAC/D,eAAK,oBAAoB;AACzB;AAAA,QACF;AACA,aAAK,cAAc;AAAA,MACrB,GAAG,kBAAkB;AAAA,IACvB;AAAA,EACF;AAAA,EAEA,YAAkB;AAChB,QAAI,CAAC,KAAK,aAAa,OAAO,WAAW,YAAa;AACtD,SAAK,YAAY;AACjB,SAAK,oBAAoB;AACzB,WAAO,oBAAoB,WAAW,KAAK,mBAAmB;AAC9D,eAAW,CAAC,EAAE,OAAO,KAAK,KAAK,aAAa;AAC1C,mBAAa,QAAQ,KAAK;AAC1B,cAAQ,OAAO,IAAI,MAAM,kCAAkC,CAAC;AAAA,IAC9D;AACA,SAAK,YAAY,MAAM;AAAA,EACzB;AAAA;AAAA,EAIA,YAA4B;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA,EACA,aAA8B;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAIA,UACE,OACA,UACY;AACZ,SAAK,UAAU,KAAK,EAAE,IAAI,QAAQ;AAClC,WAAO,MAAM;AACX,WAAK,UAAU,KAAK,EAAE,OAAO,QAAQ;AAAA,IACvC;AAAA,EACF;AAAA;AAAA,EAIA,MAAM,YAAY,SAA+B;AAC/C,UAAM,KAAK,mBAAmB;AAC9B,WAAO,KAAK,eAAe,0BAA0B;AAAA,MACnD,SAAS,KAAK,OAAO,WAAW;AAAA,MAChC;AAAA,IACF,CAAC,EAAE,KAAK,CAAC,SAAU,KAA4B,SAAS;AAAA,EAC1D;AAAA,EAEA,MAAM,gBAAgB,IAIL;AACf,UAAM,KAAK,mBAAmB;AAC9B,WAAO,KAAK,eAAe,8BAA8B;AAAA,MACvD,SAAS,KAAK,OAAO,WAAW;AAAA,MAChC,IAAI,GAAG;AAAA,MACP,MAAM,GAAG;AAAA,MACT,GAAI,GAAG,UAAU,SAAY,EAAE,OAAO,GAAG,MAAM,SAAS,EAAE,EAAE,IAAI,CAAC;AAAA,IACnE,CAAC,EAAE,KAAK,CAAC,SAAU,KAAyB,MAAM;AAAA,EACpD;AAAA,EAEA,MAAM,YAAY,SAAkC;AAClD,UAAM,KAAK,mBAAmB;AAC9B,WAAO,KAAK,eAAe,0BAA0B,EAAE,QAAQ,CAAC,EAAE;AAAA,MAChE,CAAC,SAAU,KAA6B;AAAA,IAC1C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,cAAc,MAKH;AACf,UAAM,KAAK,mBAAmB;AAC9B,WAAO,KAAK,eAAe,4BAA4B;AAAA,MACrD,SAAS,KAAK,OAAO,WAAW;AAAA,MAChC,QAAQ,KAAK;AAAA,MACb,OAAO,KAAK;AAAA,MACZ,aAAa,KAAK;AAAA,MAClB,SAAS,KAAK;AAAA,IAChB,CAAC,EAAE,KAAK,CAAC,SAAU,KAA4B,SAAS;AAAA,EAC1D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,QAAQ,MAIa;AACzB,UAAM,KAAK,mBAAmB;AAC9B,UAAM,gBAAgB,kBAAkB,oBAAoB;AAC5D,UAAM,UACJ,KAAK,QAAQ,oBAAoB;AACnC,WAAO,IAAI,QAAuB,CAAC,SAAS,WAAW;AACrD,YAAM,aAA4B;AAAA,QAChC;AAAA,QACA,QAAQ;AAAA,QACR,QAAQ,CAAC;AAAA,MACX;AACA,YAAM,QAAQ,WAAW,MAAM;AAC7B,aAAK,YAAY,OAAO,aAAa;AACrC;AAAA,UACE,YAAY,MAAM,8BAA8B,OAAO,IAAI;AAAA,QAC7D;AAAA,MACF,GAAG,OAAO;AACV,WAAK,YAAY,IAAI,eAAe;AAAA,QAClC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AACD,YAAM,UAA0B;AAAA,QAC9B,MAAM;AAAA,QACN;AAAA,QACA,UAAU,KAAK;AAAA,QACf,QAAQ,KAAK;AAAA,QACb,GAAI,KAAK,WAAW,SAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;AAAA,MAC7D;AACA,WAAK,aAAa,OAAO;AAEzB,WAAK,KAAK,OAAO,UAAU;AAAA,IAC7B,CAAC;AAAA,EACH;AAAA;AAAA,EAIQ,sBAA4B;AAClC,QAAI,KAAK,mBAAmB,MAAM;AAChC,oBAAc,KAAK,cAAc;AACjC,WAAK,iBAAiB;AAAA,IACxB;AAAA,EACF;AAAA,EAEQ,gBAAsB;AAC5B,SAAK,aAAa;AAAA,MAChB,MAAM;AAAA,MACN,OAAO,KAAK,QAAQ;AAAA,MACpB,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AAAA,EAEQ,aAAa,SAAuB;AAC1C,QAAI,OAAO,WAAW,YAAa;AACnC,QAAI;AACF,aAAO,OAAO,YAAY,SAAS,KAAK,QAAQ,YAAY;AAAA,IAC9D,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,sBAAsB,CAAC,UAA8B;AAC3D,QAAI,MAAM,WAAW,KAAK,QAAQ,aAAc;AAChD,UAAM,OAAO,MAAM;AACnB,QAAI,OAAO,SAAS,YAAY,SAAS,KAAM;AAC/C,UAAM,UAAU;AAChB,YAAQ,QAAQ,MAAM;AAAA,MACpB,KAAK;AACH,aAAK,iBAAiB;AACtB,aAAK,oBAAoB;AACzB,mBAAW,WAAW,KAAK,iBAAkB,SAAQ;AACrD,aAAK,mBAAmB,CAAC;AACzB;AAAA,MACF,KAAK;AACH,YAAI,QAAQ,IAAI;AACd,eAAK,aAAa;AAAA,YAChB,SACE,QAAQ,KAAK,YAAY,oBACrB,OACA,QAAQ,KAAK;AAAA,YACnB,SAAS,QAAQ,KAAK;AAAA,YACtB,aAAa,QAAQ,KAAK,YAAY;AAAA,UACxC,CAAC;AAAA,QACH;AACA;AAAA,MACF,KAAK;AACH,aAAK,aAAa;AAAA,UAChB,SAAS,QAAQ;AAAA,UACjB,SAAS,KAAK,OAAO;AAAA,UACrB,aAAa,QAAQ,YAAY;AAAA,QACnC,CAAC;AACD;AAAA,MACF,KAAK;AACH,aAAK,aAAa;AAAA,UAChB,SAAS,KAAK,OAAO;AAAA,UACrB,SAAS,QAAQ;AAAA,UACjB,aAAa,KAAK,OAAO;AAAA,QAC3B,CAAC;AACD;AAAA,MACF,KAAK;AACH,aAAK,cAAc,OAAO;AAC1B;AAAA,MACF,KAAK;AACH,aAAK,gBAAgB,OAAO;AAC5B;AAAA;AAAA;AAAA,MAGF;AACE;AAAA,IACJ;AAAA,EACF;AAAA,EAEA,MAAc,eACZ,MAKA,SACkB;AAClB,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,gBAAgB,kBAAkB,IAAI;AAC5C,YAAM,UACJ,KAAK,QAAQ,oBAAoB;AACnC,YAAM,eACJ;AAAA,QACE,0BAA0B;AAAA,QAC1B,8BAA8B;AAAA,QAC9B,4BAA4B;AAAA,QAC5B,0BAA0B;AAAA,MAC5B,EACA,IAAI;AACN,YAAM,QAAQ,WAAW,MAAM;AAC7B,eAAO,oBAAoB,WAAW,QAAQ;AAC9C,eAAO,YAAY,MAAM,6BAA6B,IAAI,OAAO,OAAO,IAAI,CAAC;AAAA,MAC/E,GAAG,OAAO;AACV,YAAM,WAAW,CAAC,UAAwB;AACxC,YAAI,MAAM,WAAW,KAAK,QAAQ,aAAc;AAChD,cAAM,OAAO,MAAM;AACnB,YAAI,OAAO,SAAS,YAAY,SAAS,KAAM;AAC/C,cAAM,MAAM;AACZ,YACE,IAAI,SAAS,gBACb,EAAE,mBAAmB,QACrB,IAAI,kBAAkB,eACtB;AACA;AAAA,QACF;AACA,qBAAa,KAAK;AAClB,eAAO,oBAAoB,WAAW,QAAQ;AAE9C,cAAM,MAAM;AAKZ,YAAI,IAAI,IAAI;AACV,kBAAQ,IAAI,IAAI;AAAA,QAClB,OAAO;AACL,iBAAO,YAAY,MAAM,IAAI,SAAS,yBAAyB,CAAC;AAAA,QAClE;AAAA,MACF;AACA,aAAO,iBAAiB,WAAW,QAAQ;AAC3C,WAAK,aAAa,EAAE,MAAM,eAAe,GAAG,QAAQ,CAAC;AAAA,IACvD,CAAC;AAAA,EACH;AAAA,EAEQ,gBAAgB,SAA+B;AACrD,UAAM,UAAU,KAAK,YAAY,IAAI,QAAQ,aAAa;AAC1D,QAAI,CAAC,QAAS;AACd,UAAM,UAAyB;AAAA,MAC7B,eAAe,QAAQ;AAAA,MACvB,QAAQ,QAAQ;AAAA,MAChB,QACE,QAAQ,UAAU,SACd,CAAC,GAAG,QAAQ,WAAW,QAAQ,QAAQ,KAAK,IAC5C,QAAQ,WAAW;AAAA,MACzB,GAAI,QAAQ,SAAS,SAAY,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,MAC3D,GAAI,QAAQ,UAAU,SAAY,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;AAAA,MAC9D,GAAI,QAAQ,aAAa,SAAY,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,IACzE;AACA,YAAQ,aAAa;AACrB,SAAK,KAAK,OAAO,OAAO;AACxB,QAAI,QAAQ,WAAW,aAAa,QAAQ,WAAW,SAAS;AAC9D,mBAAa,QAAQ,KAAK;AAC1B,WAAK,YAAY,OAAO,QAAQ,aAAa;AAC7C,UAAI,QAAQ,WAAW,WAAW;AAChC,gBAAQ,QAAQ,OAAO;AAAA,MACzB,OAAO;AACL,gBAAQ,OAAO,YAAY,MAAM,QAAQ,SAAS,YAAY,CAAC;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,aAAa,MAA4B;AAC/C,QACE,KAAK,OAAO,YAAY,KAAK,WAC7B,KAAK,OAAO,YAAY,KAAK,WAC7B,KAAK,OAAO,gBAAgB,KAAK,aACjC;AACA;AAAA,IACF;AACA,SAAK,SAAS;AACd,SAAK,KAAK,UAAU,IAAI;AAAA,EAC1B;AAAA,EAEQ,cAAc,WAA0C;AAC9D,UAAM,OAAwB;AAAA,MAC5B,aAAa,UAAU;AAAA,MACvB,WAAW,UAAU;AAAA,MACrB,WAAW,UAAU;AAAA,MACrB,MAAM,UAAU;AAAA,MAChB,MAAM,UAAU;AAAA,MAChB,OAAO,UAAU,SAAS;AAAA,IAC5B;AACA,SAAK,UAAU;AACf,SAAK,KAAK,WAAW,IAAI;AAAA,EAC3B;AAAA,EAEQ,KACN,OACA,OACM;AACN,eAAW,YAAY,CAAC,GAAG,KAAK,UAAU,KAAK,CAAC,GAAG;AACjD,UAAI;AACF,QAAC,SAAyB,KAAK;AAAA,MACjC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,qBAAoC;AAChD,QAAI,KAAK,eAAgB;AACzB,SAAK,QAAQ;AACb,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,WAAK,iBAAiB,KAAK,OAAO;AAClC,YAAM,QAAQ,YAAY,MAAM;AAC9B,YAAI,KAAK,gBAAgB;AACvB,wBAAc,KAAK;AACnB;AAAA,QACF;AACA,aAAK,cAAc;AAAA,MACrB,GAAG,GAAG;AACN,iBAAW,MAAM,cAAc,KAAK,GAAG,GAAM;AAAA,IAC/C,CAAC;AAAA,EACH;AACF;AAEA,SAAS,YAAY,MAAc,SAAwB;AACzD,QAAM,MAAM,IAAI,MAAM,OAAO;AAC7B,MAAI,OAAO;AACX,SAAO;AACT;;;ADlWI;AA7GJ,IAAM,sBAAsB,cAAmC,IAAI;AAEnE,IAAMA,eAA8B;AAAA,EAClC,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AACA,IAAMC,gBAAgC;AAAA,EACpC,aAAa;AAAA,EACb,WAAW;AAAA,EACX,WAAW,CAAC;AAAA,EACZ,MAAM,CAAC;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AACT;AAmBO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA,MAAM,gBAAgB;AAAA,EACtB;AACF,GAAU;AAGR,QAAM,aAAa,QAAQ,MAAM;AAC/B,QAAI,kBAAkB,OAAO;AAC3B,aAAO,EAAE,MAAM,OAAgB,cAAc,KAAK;AAAA,IACpD;AACA,UAAM,WACJ,kBAAkB,8BAA8B,EAAE,aAAa,CAAC;AAClE,QAAI,kBAAkB,UAAU;AAC9B,UAAI,CAAC,UAAU;AAEb,gBAAQ;AAAA,UACN;AAAA,QACF;AACA,eAAO,EAAE,MAAM,OAAgB,cAAc,KAAK;AAAA,MACpD;AACA,aAAO,EAAE,MAAM,UAAmB,cAAc,SAAS;AAAA,IAC3D;AAEA,WAAO,WACH,EAAE,MAAM,UAAmB,cAAc,SAAS,IAClD,EAAE,MAAM,OAAgB,cAAc,KAAK;AAAA,EACjD,GAAG,CAAC,eAAe,gBAAgB,YAAY,CAAC;AAEhD,QAAM,YAAY,OAAkC,IAAI;AACxD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAyBD,YAAW;AAChE,QAAM,CAAC,SAAS,UAAU,IAAI,SAA0BC,aAAY;AACpE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAE5C,YAAU,MAAM;AACd,QAAI,WAAW,SAAS,OAAO;AAI7B,iBAAW,IAAI;AACf,aAAO;AAAA,IACT;AAEA,UAAM,UAAqC;AAAA,MACzC,cAAc,WAAW;AAAA,MACzB;AAAA,IACF;AACA,UAAM,SAAS,IAAI,mBAAmB,OAAO;AAC7C,cAAU,UAAU;AACpB,UAAM,cAAc,OAAO,UAAU,UAAU,SAAS;AACxD,UAAM,eAAe,OAAO,UAAU,WAAW,UAAU;AAC3D,WAAO,QAAQ;AACf,eAAW,IAAI;AACf,WAAO,MAAM;AACX,kBAAY;AACZ,mBAAa;AACb,aAAO,UAAU;AACjB,gBAAU,UAAU;AACpB,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,YAAY,KAAK,CAAC;AAEtB,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,MACL,QAAQ,UAAU;AAAA,MAClB;AAAA,MACA;AAAA,MACA,MAAM,WAAW;AAAA,MACjB;AAAA,IACF;AAAA,IACA,CAAC,QAAQ,SAAS,WAAW,MAAM,OAAO;AAAA,EAC5C;AAEA,SACE,oBAAC,oBAAoB,UAApB,EAA6B,OAC3B,UACH;AAEJ;AAEO,SAAS,yBAAuC;AACrD,QAAM,MAAM,WAAW,mBAAmB;AAC1C,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;;;AEzKA,SAAS,aAAa,aAAAC,YAAW,WAAAC,UAAS,YAAAC,iBAAgB;AAC1D;AAAA,EACE;AAAA,EACA;AAAA,OAKK;AAsBA,SAAS,kBAcd;AACA,QAAM,EAAE,QAAQ,OAAO,IAAI,uBAAuB;AAClD,QAAM,cAAc;AAAA,IAClB,CAAC,YAAoB;AACnB,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAChE,aAAO,OAAO,YAAY,OAAO;AAAA,IACnC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,QAAM,kBAAkB;AAAA,IACtB,CAAC,OAAmD;AAClD,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAChE,aAAO,OAAO,gBAAgB,EAAE;AAAA,IAClC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,QAAM,gBAAgB;AAAA,IACpB,CAAC,SAKK;AACJ,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAChE,aAAO,OAAO,cAAc,IAAI;AAAA,IAClC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,QAAM,cAAc;AAAA,IAClB,CAAC,YAAoB;AACnB,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAChE,aAAO,OAAO,YAAY,OAAO;AAAA,IACnC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAaO,SAAS,kBAAuC;AACrD,SAAO,uBAAuB,EAAE,QAAQ;AAC1C;AAeO,SAAS,wBAA6C;AAC3D,QAAM,QAAQ,gBAAgB;AAC9B,SAAOC,SAAQ,MAAM;AACnB,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,cAAqB;AAAA,MACzB,IAAI,MAAM;AAAA,MACV,MAAM,MAAM;AAAA,MACZ,gBACE,MAAM,mBAAmB,SACrB,EAAE,GAAG,MAAM,eAAe,IAC1B,EAAE,MAAM,SAAS,QAAQ,OAAO,UAAU,GAAG;AAAA,MACnD,SAAS;AAAA,QACP,SAAS,EAAE,MAAM,CAAC,MAAM,MAAM,EAAE;AAAA,MAClC;AAAA,MACA,GAAI,MAAM,mBACN;AAAA,QACE,gBAAgB;AAAA,UACd,SAAS,EAAE,MAAM,YAAY,KAAK,MAAM,iBAAiB;AAAA,QAC3D;AAAA,MACF,IACA,CAAC;AAAA,IACP;AACA,WAAO,mBAAmB;AAAA,MACxB,OAAO;AAAA,MACP,WAAW,KAAK,MAAM,MAAM;AAAA,IAC9B,CAAC;AAAA,EACH,GAAG,CAAC,KAAK,CAAC;AACZ;AAUO,SAAS,mBAAoC;AAClD,SAAO,uBAAuB,EAAE;AAClC;AAgBO,SAAS,aAAa;AAC3B,QAAM,EAAE,OAAO,IAAI,uBAAuB;AAC1C,QAAM,CAAC,YAAY,aAAa,IAAIC,UAA+B,IAAI;AACvE,QAAM,CAAC,UAAU,WAAW,IAAIA,UAAwB,IAAI;AAE5D,EAAAC,WAAU,MAAM;AACd,QAAI,CAAC,OAAQ,QAAO;AACpB,WAAO,OAAO,UAAU,OAAO,CAAC,SAAS;AAEvC,kBAAY,CAAC,eAAe;AAC1B,YAAI,eAAe,QAAQ,eAAe,KAAK,eAAe;AAC5D,wBAAc,IAAI;AAClB,iBAAO,KAAK;AAAA,QACd;AACA,eAAO;AAAA,MACT,CAAC;AAAA,IACH,CAAC;AAAA,EACH,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,OAAO;AAAA,IACX,OAAO,SAAoE;AACzE,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AAEA,oBAAc,IAAI;AAClB,YAAM,SAAS,MAAM,OAAO,QAAQ,IAAI;AACxC,kBAAY,OAAO,aAAa;AAChC,aAAO;AAAA,IACT;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,QAAM,QAAQ,YAAY,MAAM;AAC9B,kBAAc,IAAI;AAClB,gBAAY,IAAI;AAAA,EAClB,GAAG,CAAC,CAAC;AAEL,SAAOF;AAAA,IACL,OAAO,EAAE,MAAM,YAAY,OAAO,WAAW,YAAY,WAAW,aAAa,YAAY,WAAW,YAAY;AAAA,IACpH,CAAC,MAAM,YAAY,KAAK;AAAA,EAC1B;AACF;AAMO,SAAS,mBAAmC;AACjD,SAAO,uBAAuB,EAAE,OAAO;AACzC;AAGO,SAAS,iBAA0B;AACxC,SAAO,uBAAuB,EAAE;AAClC;AAGO,SAAS,gBAAkC;AAChD,SAAO,uBAAuB,EAAE;AAClC;","names":["NULL_WALLET","NULL_SERVICE","useEffect","useMemo","useState","useMemo","useState","useEffect"]}
|
|
1
|
+
{"version":3,"sources":["../../src/iframe/TangleIframeProvider.tsx","../../src/iframe/tangleIframeClient.ts","../../src/iframe/hooks.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\n\nimport {\n TangleIframeClient,\n type ServiceSnapshot,\n type TangleIframeClientOptions,\n type WalletSnapshot,\n} from './tangleIframeClient';\nimport {\n detectTangleCloudParentOrigin,\n TANGLE_CLOUD_ORIGINS_DEFAULT,\n} from '../wallet/detectParentOrigin';\n\ntype Props = {\n appId: string;\n /** Override the detected parent origin (e.g. dev/staging deploys). */\n parentOrigin?: string;\n /** Extra trusted origins for `detectTangleCloudParentOrigin`. */\n extraOrigins?: readonly string[];\n /**\n * Override the bootstrap behavior. When `'auto'` (default), the SDK\n * sniffs the embed context: real parent → install the bridge, top-frame\n * → drop into dev mode. `'bridge'` forces real-parent mode and throws\n * if no parent is detected. `'dev'` forces dev mode even when embedded\n * — useful for component-level tests.\n */\n mode?: 'auto' | 'bridge' | 'dev';\n children: ReactNode;\n};\n\ntype ContextValue = {\n readonly client: TangleIframeClient | null;\n readonly wallet: WalletSnapshot;\n readonly service: ServiceSnapshot;\n readonly mode: 'bridge' | 'dev';\n readonly isReady: boolean;\n};\n\nconst TangleIframeContext = createContext<ContextValue | null>(null);\n\nconst NULL_WALLET: WalletSnapshot = {\n address: null,\n chainId: null,\n isConnected: false,\n};\nconst NULL_SERVICE: ServiceSnapshot = {\n blueprintId: null,\n serviceId: null,\n operators: [],\n jobs: [],\n mode: null,\n chain: null,\n};\n\n/**\n * Iframe-blueprint root provider. Wrap your app once at the entry point.\n *\n * In `auto` mode (default) the SDK detects whether the app is embedded by a\n * trusted Tangle Cloud parent. If yes → installs the postMessage bridge.\n * If no (running standalone at `localhost:5173` etc.) → enters **dev mode**\n * with an in-memory state machine that the developer can drive via the\n * exported debug controls. Dev mode keeps the hook surface identical to\n * production so component code never branches on embed-vs-not.\n *\n * Three lifecycle stages:\n *\n * 1. Mount — `client` is created, mode is decided.\n * 2. Bootstrap — handshake (bridge) or first-paint setup (dev). The\n * `isReady` flag flips to true.\n * 3. Active — wallet + service snapshots flow in via subscriptions.\n */\nexport function TangleIframeProvider({\n appId,\n parentOrigin: explicitOrigin,\n extraOrigins,\n mode: requestedMode = 'auto',\n children,\n}: Props) {\n // Resolve the effective mode once at mount. Switching modes mid-session\n // would tear down the bridge / dev state inconsistently; restart instead.\n const resolution = useMemo(() => {\n if (requestedMode === 'dev') {\n return { mode: 'dev' as const, parentOrigin: null };\n }\n const detected =\n explicitOrigin ?? detectTangleCloudParentOrigin({ extraOrigins });\n if (requestedMode === 'bridge') {\n if (!detected) {\n // eslint-disable-next-line no-console\n console.error(\n '[TangleIframeProvider] mode=\"bridge\" but no trusted parent was detected. Falling back to dev mode.',\n );\n return { mode: 'dev' as const, parentOrigin: null };\n }\n return { mode: 'bridge' as const, parentOrigin: detected };\n }\n // auto: bridge when detected, dev otherwise.\n return detected\n ? { mode: 'bridge' as const, parentOrigin: detected }\n : { mode: 'dev' as const, parentOrigin: null };\n }, [requestedMode, explicitOrigin, extraOrigins]);\n\n const clientRef = useRef<TangleIframeClient | null>(null);\n const [wallet, setWallet] = useState<WalletSnapshot>(NULL_WALLET);\n const [service, setService] = useState<ServiceSnapshot>(NULL_SERVICE);\n const [isReady, setIsReady] = useState(false);\n\n useEffect(() => {\n if (resolution.mode === 'dev') {\n // Dev mode: no bridge. The DevHarness component (or a test) seeds\n // wallet + service via `setDevWallet` / `setDevService` on the\n // returned context. Mark ready immediately so app code unblocks.\n setIsReady(true);\n return undefined;\n }\n // Bridge mode\n const options: TangleIframeClientOptions = {\n parentOrigin: resolution.parentOrigin,\n appId,\n };\n const client = new TangleIframeClient(options);\n clientRef.current = client;\n const unsubWallet = client.subscribe('wallet', setWallet);\n const unsubService = client.subscribe('service', setService);\n client.install();\n setIsReady(true);\n return () => {\n unsubWallet();\n unsubService();\n client.uninstall();\n clientRef.current = null;\n setIsReady(false);\n };\n }, [resolution, appId]);\n\n const value = useMemo<ContextValue>(\n () => ({\n client: clientRef.current,\n wallet,\n service,\n mode: resolution.mode,\n isReady,\n }),\n [wallet, service, resolution.mode, isReady],\n );\n\n return (\n <TangleIframeContext.Provider value={value}>\n {children}\n </TangleIframeContext.Provider>\n );\n}\n\nexport function useTangleIframeContext(): ContextValue {\n const ctx = useContext(TangleIframeContext);\n if (!ctx) {\n throw new Error(\n 'useTangleIframeContext must be used inside <TangleIframeProvider>.',\n );\n }\n return ctx;\n}\n\nexport { TANGLE_CLOUD_ORIGINS_DEFAULT };\n","// Thin-iframe SDK client — the framework-agnostic state machine that talks\n// to a Tangle Cloud parent dapp over postMessage. React hooks (below) are\n// thin wrappers around an instance of this class.\n//\n// Why a class, not a bag of functions: the iframe lifecycle is stateful —\n// handshake, account changes, service-context broadcasts, in-flight job\n// requests. The class owns that state once; hooks subscribe via listeners.\n// Testing the protocol shape doesn't require React.\n\nimport type { Address, Hex } from 'viem';\n\nimport {\n makeCorrelationId,\n NO_WALLET_ADDRESS,\n TANGLE_IFRAME_PROTOCOL_VERSION,\n type CallJobRequest,\n type ChainContext,\n type JobInputs,\n type JobResultEvent,\n type JobResultStatus,\n type ParentMessage,\n type ServiceContextBroadcast,\n type ServiceContextJob,\n type ServiceContextOperator,\n type SignTypedDataRequest,\n} from '../wallet/parentBridgeProtocol';\n\nexport type WalletSnapshot = {\n readonly address: Address | null;\n readonly chainId: number | null;\n readonly isConnected: boolean;\n};\n\nexport type ServiceSnapshot = {\n readonly blueprintId: string | null;\n readonly serviceId: string | null;\n readonly operators: readonly ServiceContextOperator[];\n readonly jobs: readonly ServiceContextJob[];\n readonly mode: string | null;\n /** Chain context broadcast by the parent — drives `useTanglePublicClient`.\n * `null` when the parent hasn't sent one (older parent or dev mode). */\n readonly chain: ChainContext | null;\n};\n\nexport type JobInvocation = {\n readonly correlationId: string;\n readonly status: JobResultStatus;\n readonly data?: unknown;\n readonly chunks: readonly unknown[];\n readonly error?: string;\n readonly progress?: { readonly percent?: number; readonly eta_ms?: number };\n};\n\nexport type ClientEventMap = {\n wallet: WalletSnapshot;\n service: ServiceSnapshot;\n job: JobInvocation;\n};\n\ntype Listener<K extends keyof ClientEventMap> = (\n value: ClientEventMap[K],\n) => void;\n\nexport type TangleIframeClientOptions = {\n /**\n * Origin of the parent dapp. The client posts every message with this\n * exact `targetOrigin` and rejects inbound messages from any other origin.\n * Pass `'*'` only in dev — production must pin to the real parent\n * (`https://cloud.tangle.tools` etc.).\n */\n parentOrigin: string;\n /**\n * Stable identifier for this iframe app. The parent surfaces it in\n * handshake logs + uses it for permission scoping.\n */\n appId: string;\n /**\n * Per-request timeout. Defaults to 60s — long enough for a user to\n * read + approve a signing prompt in the parent. Long-running jobs\n * stream progress events; the request \"completes\" only on terminal\n * status, so the timeout protects against parents that drop replies\n * entirely.\n */\n requestTimeoutMs?: number;\n};\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 60_000;\n// Connecting is gated on the user picking + approving a wallet in the parent's\n// modal — give it a generous window rather than the standard request timeout.\nconst CONNECT_REQUEST_TIMEOUT_MS = 300_000;\nconst HANDSHAKE_RETRY_MS = 250;\nconst HANDSHAKE_RETRY_BUDGET_MS = 10_000;\nconst NULL_WALLET: WalletSnapshot = {\n address: null,\n chainId: null,\n isConnected: false,\n};\nconst NULL_SERVICE: ServiceSnapshot = {\n blueprintId: null,\n serviceId: null,\n operators: [],\n jobs: [],\n mode: null,\n chain: null,\n};\n\ntype PendingJob = {\n resolve: (value: JobInvocation) => void;\n reject: (reason: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n invocation: JobInvocation;\n};\n\nexport class TangleIframeClient {\n private wallet: WalletSnapshot = NULL_WALLET;\n private service: ServiceSnapshot = NULL_SERVICE;\n private handshakeAcked = false;\n private handshakeWaiters: Array<() => void> = [];\n private installed = false;\n private handshakeRetry: ReturnType<typeof setInterval> | null = null;\n private listeners: {\n [K in keyof ClientEventMap]: Set<Listener<K>>;\n } = {\n wallet: new Set(),\n service: new Set(),\n job: new Set(),\n };\n private pendingJobs = new Map<string, PendingJob>();\n\n constructor(private readonly options: TangleIframeClientOptions) {}\n\n /** Wire the global message listener + initial handshake. Idempotent. */\n install(): void {\n if (this.installed || typeof window === 'undefined') return;\n this.installed = true;\n window.addEventListener('message', this.handleParentMessage);\n this.postHandshake();\n // Stand up a bounded retry. The parent may attach its listener slightly\n // after the iframe loads (React mounts child effects before parent\n // effects; a real parent may create the frame before its handler is\n // ready), so a single handshake can be dropped. Retry until acked.\n if (this.handshakeRetry === null) {\n let elapsed = 0;\n this.handshakeRetry = setInterval(() => {\n elapsed += HANDSHAKE_RETRY_MS;\n if (this.handshakeAcked || elapsed >= HANDSHAKE_RETRY_BUDGET_MS) {\n this.clearHandshakeRetry();\n return;\n }\n this.postHandshake();\n }, HANDSHAKE_RETRY_MS);\n }\n }\n\n uninstall(): void {\n if (!this.installed || typeof window === 'undefined') return;\n this.installed = false;\n this.clearHandshakeRetry();\n window.removeEventListener('message', this.handleParentMessage);\n for (const [, pending] of this.pendingJobs) {\n clearTimeout(pending.timer);\n pending.reject(new Error('Tangle iframe client uninstalled'));\n }\n this.pendingJobs.clear();\n }\n\n // ── State accessors ─────────────────────────────────────────────────────\n\n getWallet(): WalletSnapshot {\n return this.wallet;\n }\n getService(): ServiceSnapshot {\n return this.service;\n }\n\n // ── Subscription API (used by React hooks) ──────────────────────────────\n\n subscribe<K extends keyof ClientEventMap>(\n event: K,\n listener: Listener<K>,\n ): () => void {\n this.listeners[event].add(listener);\n return () => {\n this.listeners[event].delete(listener);\n };\n }\n\n // ── Wallet operations ───────────────────────────────────────────────────\n\n /**\n * Ask the parent dapp to connect a wallet — opening its connect modal if\n * none is connected. The iframe is sandboxed and cannot reach a wallet\n * itself, so connection is always delegated to the parent. Resolves with the\n * connected address (or `null` if the user dismissed without connecting).\n *\n * Uses a long timeout (the user is interacting with a modal). Already-\n * connected parents resolve immediately.\n */\n async connect(): Promise<Address | null> {\n await this.ensureBootstrapped();\n const data = await this.dispatchWallet(\n 'tangle.app.requestConnect',\n {},\n CONNECT_REQUEST_TIMEOUT_MS,\n );\n const { account, chainId } = data as { account: Address; chainId: number };\n const address = account === NO_WALLET_ADDRESS ? null : account;\n this.updateWallet({ address, chainId, isConnected: address !== null });\n return address;\n }\n\n async signMessage(message: string): Promise<Hex> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.signMessage', {\n chainId: this.wallet.chainId ?? 0,\n message,\n }).then((data) => (data as { signature: Hex }).signature);\n }\n\n async sendTransaction(tx: {\n to: Address;\n data: Hex;\n value?: bigint;\n }): Promise<Hex> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.signTransaction', {\n chainId: this.wallet.chainId ?? 0,\n to: tx.to,\n data: tx.data,\n ...(tx.value !== undefined ? { value: tx.value.toString(10) } : {}),\n }).then((data) => (data as { txHash: Hex }).txHash);\n }\n\n async switchChain(chainId: number): Promise<number> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.switchChain', { chainId }).then(\n (data) => (data as { chainId: number }).chainId,\n );\n }\n\n /**\n * EIP-712 typed-data signing. The parent renders the typed-data fields in\n * its approval modal; the user audits what they're signing. Use for\n * operator envelopes, off-chain attestations, anything that needs a\n * signature outside the standard blueprint-job RFQ flow.\n *\n * Shape mirrors viem's `signTypedData` argument. Do not include the\n * EIP712Domain entry in `types` — the parent injects it from `domain`.\n */\n async signTypedData(args: {\n domain: SignTypedDataRequest['domain'];\n types: SignTypedDataRequest['types'];\n primaryType: string;\n message: Readonly<Record<string, unknown>>;\n }): Promise<Hex> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.signTypedData', {\n chainId: this.wallet.chainId ?? 0,\n domain: args.domain,\n types: args.types,\n primaryType: args.primaryType,\n message: args.message,\n }).then((data) => (data as { signature: Hex }).signature);\n }\n\n // ── Job invocation ──────────────────────────────────────────────────────\n\n /**\n * Invoke a blueprint job. Returns a Promise that resolves on terminal\n * status (`success` or `error`); subscribe to the `job` event for\n * intermediate streaming chunks.\n *\n * Streaming opt-in: pass `stream: true` if the publisher's job emits\n * chunks (LLM generation, video encoding). One-shot jobs (embeddings,\n * classifications) skip the streaming machinery.\n */\n async callJob(args: {\n jobIndex: number;\n inputs: JobInputs;\n stream?: boolean;\n }): Promise<JobInvocation> {\n await this.ensureBootstrapped();\n const correlationId = makeCorrelationId('tangle.app.callJob');\n const timeout =\n this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n return new Promise<JobInvocation>((resolve, reject) => {\n const invocation: JobInvocation = {\n correlationId,\n status: 'pending',\n chunks: [],\n };\n const timer = setTimeout(() => {\n this.pendingJobs.delete(correlationId);\n reject(\n bridgeError(4900, `Job did not respond within ${timeout}ms`),\n );\n }, timeout);\n this.pendingJobs.set(correlationId, {\n resolve,\n reject,\n timer,\n invocation,\n });\n const message: CallJobRequest = {\n kind: 'tangle.app.callJob',\n correlationId,\n jobIndex: args.jobIndex,\n inputs: args.inputs,\n ...(args.stream !== undefined ? { stream: args.stream } : {}),\n };\n this.postToParent(message);\n // Emit pending immediately so consumer UIs can show optimistic state.\n this.emit('job', invocation);\n });\n }\n\n // ── Internals ───────────────────────────────────────────────────────────\n\n private clearHandshakeRetry(): void {\n if (this.handshakeRetry !== null) {\n clearInterval(this.handshakeRetry);\n this.handshakeRetry = null;\n }\n }\n\n private postHandshake(): void {\n this.postToParent({\n kind: 'tangle.app.handshake',\n appId: this.options.appId,\n version: TANGLE_IFRAME_PROTOCOL_VERSION,\n });\n }\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; defensive only — postMessage shouldn't throw.\n }\n }\n\n private handleParentMessage = (event: MessageEvent): void => {\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 this.clearHandshakeRetry();\n for (const resolve of this.handshakeWaiters) resolve();\n this.handshakeWaiters = [];\n return;\n case 'tangle.app.readAccountResult':\n if (message.ok) {\n this.updateWallet({\n address:\n message.data.account === NO_WALLET_ADDRESS\n ? null\n : message.data.account,\n chainId: message.data.chainId,\n isConnected: message.data.account !== NO_WALLET_ADDRESS,\n });\n }\n return;\n case 'tangle.app.accountChanged':\n this.updateWallet({\n address: message.account,\n chainId: this.wallet.chainId,\n isConnected: message.account !== null,\n });\n return;\n case 'tangle.app.chainChanged':\n this.updateWallet({\n address: this.wallet.address,\n chainId: message.chainId,\n isConnected: this.wallet.isConnected,\n });\n return;\n case 'tangle.app.serviceContext':\n this.updateService(message);\n return;\n case 'tangle.app.jobResult':\n this.handleJobResult(message);\n return;\n // Wallet-shape responses (signMessageResult etc.) are routed by\n // dispatchWallet's promise resolver, not here.\n default:\n return;\n }\n };\n\n private async dispatchWallet(\n kind:\n | 'tangle.app.signMessage'\n | 'tangle.app.signTransaction'\n | 'tangle.app.signTypedData'\n | 'tangle.app.switchChain'\n | 'tangle.app.requestConnect',\n payload: Record<string, unknown>,\n timeoutMs?: number,\n ): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const correlationId = makeCorrelationId(kind);\n const timeout = timeoutMs ?? this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const expectedKind = (\n {\n 'tangle.app.signMessage': 'tangle.app.signMessageResult',\n 'tangle.app.signTransaction': 'tangle.app.signTransactionResult',\n 'tangle.app.signTypedData': 'tangle.app.signTypedDataResult',\n 'tangle.app.switchChain': 'tangle.app.switchChainResult',\n 'tangle.app.requestConnect': 'tangle.app.connectResult',\n } as const\n )[kind];\n const timer = setTimeout(() => {\n window.removeEventListener('message', listener);\n reject(bridgeError(4900, `Parent did not respond to ${kind} in ${timeout}ms`));\n }, timeout);\n const listener = (event: MessageEvent) => {\n if (event.origin !== this.options.parentOrigin) return;\n const data = event.data;\n if (typeof data !== 'object' || data === null) return;\n const msg = data as ParentMessage;\n if (\n msg.kind !== expectedKind ||\n !('correlationId' in msg) ||\n msg.correlationId !== correlationId\n ) {\n return;\n }\n clearTimeout(timer);\n window.removeEventListener('message', listener);\n // Narrow the type — expectedKind is the wallet-shape `{ok, data|error}` envelope\n const env = msg as {\n ok: boolean;\n data?: unknown;\n error?: string;\n };\n if (env.ok) {\n resolve(env.data);\n } else {\n reject(bridgeError(4001, env.error ?? 'Parent rejected request'));\n }\n };\n window.addEventListener('message', listener);\n this.postToParent({ kind, correlationId, ...payload });\n });\n }\n\n private handleJobResult(message: JobResultEvent): void {\n const pending = this.pendingJobs.get(message.correlationId);\n if (!pending) return;\n const updated: JobInvocation = {\n correlationId: message.correlationId,\n status: message.status,\n chunks:\n message.chunk !== undefined\n ? [...pending.invocation.chunks, message.chunk]\n : pending.invocation.chunks,\n ...(message.data !== undefined ? { data: message.data } : {}),\n ...(message.error !== undefined ? { error: message.error } : {}),\n ...(message.progress !== undefined ? { progress: message.progress } : {}),\n };\n pending.invocation = updated;\n this.emit('job', updated);\n if (message.status === 'success' || message.status === 'error') {\n clearTimeout(pending.timer);\n this.pendingJobs.delete(message.correlationId);\n if (message.status === 'success') {\n pending.resolve(updated);\n } else {\n pending.reject(bridgeError(4001, message.error ?? 'Job failed'));\n }\n }\n }\n\n private updateWallet(next: WalletSnapshot): void {\n if (\n this.wallet.address === next.address &&\n this.wallet.chainId === next.chainId &&\n this.wallet.isConnected === next.isConnected\n ) {\n return;\n }\n this.wallet = next;\n this.emit('wallet', next);\n }\n\n private updateService(broadcast: ServiceContextBroadcast): void {\n const next: ServiceSnapshot = {\n blueprintId: broadcast.blueprintId,\n serviceId: broadcast.serviceId,\n operators: broadcast.operators,\n jobs: broadcast.jobs,\n mode: broadcast.mode,\n chain: broadcast.chain ?? null,\n };\n this.service = next;\n this.emit('service', next);\n }\n\n private emit<K extends keyof ClientEventMap>(\n event: K,\n value: ClientEventMap[K],\n ): void {\n for (const listener of [...this.listeners[event]]) {\n try {\n (listener as Listener<K>)(value);\n } catch {\n // Listener bugs shouldn't break the bridge.\n }\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 const retry = setInterval(() => {\n if (this.handshakeAcked) {\n clearInterval(retry);\n return;\n }\n this.postHandshake();\n }, 500);\n setTimeout(() => clearInterval(retry), 10_000);\n });\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","import { useCallback, useEffect, useMemo, useState } from 'react';\nimport {\n createPublicClient,\n http,\n type Address,\n type Chain,\n type Hex,\n type PublicClient,\n} from 'viem';\n\nimport { useTangleIframeContext } from './TangleIframeProvider';\nimport type {\n JobInvocation,\n ServiceSnapshot,\n WalletSnapshot,\n} from './tangleIframeClient';\nimport type {\n ChainContext,\n JobInputs,\n SignTypedDataRequest,\n} from '../wallet/parentBridgeProtocol';\n\n/**\n * Read-only view of the connected wallet, plus the operations the iframe\n * can request the parent to perform.\n *\n * The iframe never holds a private key, never sees `window.ethereum`, never\n * imports wagmi. All wallet work happens upstream in the Tangle Cloud\n * dapp's wagmi config + ConnectKit modal.\n */\nexport function useTangleWallet(): WalletSnapshot & {\n connect: () => Promise<Address | null>;\n signMessage: (message: string) => Promise<Hex>;\n sendTransaction: (tx: {\n to: Address;\n data: Hex;\n value?: bigint;\n }) => Promise<Hex>;\n signTypedData: (args: {\n domain: SignTypedDataRequest['domain'];\n types: SignTypedDataRequest['types'];\n primaryType: string;\n message: Readonly<Record<string, unknown>>;\n }) => Promise<Hex>;\n switchChain: (chainId: number) => Promise<number>;\n} {\n const { client, wallet } = useTangleIframeContext();\n const connect = useCallback(() => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.connect();\n }, [client]);\n const signMessage = useCallback(\n (message: string) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.signMessage(message);\n },\n [client],\n );\n const sendTransaction = useCallback(\n (tx: { to: Address; data: Hex; value?: bigint }) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.sendTransaction(tx);\n },\n [client],\n );\n const signTypedData = useCallback(\n (args: {\n domain: SignTypedDataRequest['domain'];\n types: SignTypedDataRequest['types'];\n primaryType: string;\n message: Readonly<Record<string, unknown>>;\n }) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.signTypedData(args);\n },\n [client],\n );\n const switchChain = useCallback(\n (chainId: number) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.switchChain(chainId);\n },\n [client],\n );\n return {\n ...wallet,\n connect,\n signMessage,\n sendTransaction,\n signTypedData,\n switchChain,\n };\n}\n\n/**\n * Chain configuration broadcast by the parent: chain id, name, RPC URL,\n * block explorer, native currency. Returns `null` until the parent has\n * sent its first `serviceContext` broadcast (or in dev mode without a\n * seeded harness).\n *\n * Use this when you want to display chain-aware info (block explorer\n * links, native currency labels) or when you want to build your own viem\n * client with the parent's RPC URL. For a pre-built read-only client,\n * see `useTanglePublicClient()`.\n */\nexport function useChainContext(): ChainContext | null {\n return useTangleIframeContext().service.chain;\n}\n\n/**\n * Read-only viem `PublicClient` pinned to the chain the parent dapp is\n * connected to. Useful for `readContract`, `getBalance`, `multicall`, etc.\n *\n * Returns `null` until the parent broadcasts a chain context. Iframes that\n * need to read from chains *other* than the active one should bring their\n * own client — this hook is a convenience for the common case, not a\n * constraint. Multi-chain dashboards just create additional clients\n * directly via `createPublicClient`.\n *\n * Memoized per chain id + RPC URL, so consumers get a stable identity\n * across re-renders.\n */\nexport function useTanglePublicClient(): PublicClient | null {\n const chain = useChainContext();\n return useMemo(() => {\n if (!chain) return null;\n const chainConfig: Chain = {\n id: chain.id,\n name: chain.name,\n nativeCurrency:\n chain.nativeCurrency !== undefined\n ? { ...chain.nativeCurrency }\n : { name: 'Ether', symbol: 'ETH', decimals: 18 },\n rpcUrls: {\n default: { http: [chain.rpcUrl] },\n },\n ...(chain.blockExplorerUrl\n ? {\n blockExplorers: {\n default: { name: 'Explorer', url: chain.blockExplorerUrl },\n },\n }\n : {}),\n } as Chain;\n return createPublicClient({\n chain: chainConfig,\n transport: http(chain.rpcUrl),\n });\n }, [chain]);\n}\n\n/**\n * The service the iframe is currently rendering for. Broadcast by the\n * parent dapp on mount + every time the service/mode changes — the iframe\n * never queries the chain or the indexer itself.\n *\n * `serviceId === null` means the operator hasn't deployed an instance yet;\n * the iframe should render its deploy-ready / configuration surface.\n */\nexport function useTangleService(): ServiceSnapshot {\n return useTangleIframeContext().service;\n}\n\n/**\n * Invoke a blueprint job. Returns a callable + a snapshot of the most\n * recent invocation (or null if none yet).\n *\n * Streaming jobs (LLM, video, audio) opt in via `stream: true`. The hook's\n * `invocation.chunks` accumulates each streaming chunk so the UI can render\n * progressive output. For one-shot jobs (embeddings, classification), use\n * the `invocation.data` once `status === 'success'`.\n *\n * Multiple in-flight invocations are supported — each `call()` returns its\n * own correlationId. The hook tracks only the *latest* invocation in its\n * state; consumers that need all history can subscribe to the client's\n * `job` event directly.\n */\nexport function useCallJob() {\n const { client } = useTangleIframeContext();\n const [invocation, setInvocation] = useState<JobInvocation | null>(null);\n const [latestId, setLatestId] = useState<string | null>(null);\n\n useEffect(() => {\n if (!client) return undefined;\n return client.subscribe('job', (next) => {\n // Only update if this is the latest invocation, or no latest tracked.\n setLatestId((prevLatest) => {\n if (prevLatest === null || prevLatest === next.correlationId) {\n setInvocation(next);\n return next.correlationId;\n }\n return prevLatest;\n });\n });\n }, [client]);\n\n const call = useCallback(\n async (args: { jobIndex: number; inputs: JobInputs; stream?: boolean }) => {\n if (!client) {\n throw new Error(\n 'Job invocation not available in dev mode without a configured stub. See `setDevJobHandler` in the testing harness.',\n );\n }\n // Clear prior invocation state when starting a new call.\n setInvocation(null);\n const result = await client.callJob(args);\n setLatestId(result.correlationId);\n return result;\n },\n [client],\n );\n\n const reset = useCallback(() => {\n setInvocation(null);\n setLatestId(null);\n }, []);\n\n return useMemo(\n () => ({ call, invocation, reset, isPending: invocation?.status === 'pending' || invocation?.status === 'streaming' }),\n [call, invocation, reset],\n );\n}\n\n/**\n * Convenience: returns just the address when connected, or `null`. Most\n * iframe components only care about the address.\n */\nexport function useTangleAddress(): Address | null {\n return useTangleIframeContext().wallet.address;\n}\n\n/** Whether the iframe has completed its parent-handshake (or is in dev mode). */\nexport function useTangleReady(): boolean {\n return useTangleIframeContext().isReady;\n}\n\n/** Resolved mode — `'bridge'` (real parent) or `'dev'` (standalone). */\nexport function useTangleMode(): 'bridge' | 'dev' {\n return useTangleIframeContext().mode;\n}\n"],"mappings":";;;;;;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;;;AC8EP,IAAM,6BAA6B;AAGnC,IAAM,6BAA6B;AACnC,IAAM,qBAAqB;AAC3B,IAAM,4BAA4B;AAClC,IAAM,cAA8B;AAAA,EAClC,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AACA,IAAM,eAAgC;AAAA,EACpC,aAAa;AAAA,EACb,WAAW;AAAA,EACX,WAAW,CAAC;AAAA,EACZ,MAAM,CAAC;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AACT;AASO,IAAM,qBAAN,MAAyB;AAAA,EAgB9B,YAA6B,SAAoC;AAApC;AAAA,EAAqC;AAAA,EAArC;AAAA,EAfrB,SAAyB;AAAA,EACzB,UAA2B;AAAA,EAC3B,iBAAiB;AAAA,EACjB,mBAAsC,CAAC;AAAA,EACvC,YAAY;AAAA,EACZ,iBAAwD;AAAA,EACxD,YAEJ;AAAA,IACF,QAAQ,oBAAI,IAAI;AAAA,IAChB,SAAS,oBAAI,IAAI;AAAA,IACjB,KAAK,oBAAI,IAAI;AAAA,EACf;AAAA,EACQ,cAAc,oBAAI,IAAwB;AAAA;AAAA,EAKlD,UAAgB;AACd,QAAI,KAAK,aAAa,OAAO,WAAW,YAAa;AACrD,SAAK,YAAY;AACjB,WAAO,iBAAiB,WAAW,KAAK,mBAAmB;AAC3D,SAAK,cAAc;AAKnB,QAAI,KAAK,mBAAmB,MAAM;AAChC,UAAI,UAAU;AACd,WAAK,iBAAiB,YAAY,MAAM;AACtC,mBAAW;AACX,YAAI,KAAK,kBAAkB,WAAW,2BAA2B;AAC/D,eAAK,oBAAoB;AACzB;AAAA,QACF;AACA,aAAK,cAAc;AAAA,MACrB,GAAG,kBAAkB;AAAA,IACvB;AAAA,EACF;AAAA,EAEA,YAAkB;AAChB,QAAI,CAAC,KAAK,aAAa,OAAO,WAAW,YAAa;AACtD,SAAK,YAAY;AACjB,SAAK,oBAAoB;AACzB,WAAO,oBAAoB,WAAW,KAAK,mBAAmB;AAC9D,eAAW,CAAC,EAAE,OAAO,KAAK,KAAK,aAAa;AAC1C,mBAAa,QAAQ,KAAK;AAC1B,cAAQ,OAAO,IAAI,MAAM,kCAAkC,CAAC;AAAA,IAC9D;AACA,SAAK,YAAY,MAAM;AAAA,EACzB;AAAA;AAAA,EAIA,YAA4B;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA,EACA,aAA8B;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAIA,UACE,OACA,UACY;AACZ,SAAK,UAAU,KAAK,EAAE,IAAI,QAAQ;AAClC,WAAO,MAAM;AACX,WAAK,UAAU,KAAK,EAAE,OAAO,QAAQ;AAAA,IACvC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,UAAmC;AACvC,UAAM,KAAK,mBAAmB;AAC9B,UAAM,OAAO,MAAM,KAAK;AAAA,MACtB;AAAA,MACA,CAAC;AAAA,MACD;AAAA,IACF;AACA,UAAM,EAAE,SAAS,QAAQ,IAAI;AAC7B,UAAM,UAAU,YAAY,oBAAoB,OAAO;AACvD,SAAK,aAAa,EAAE,SAAS,SAAS,aAAa,YAAY,KAAK,CAAC;AACrE,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,YAAY,SAA+B;AAC/C,UAAM,KAAK,mBAAmB;AAC9B,WAAO,KAAK,eAAe,0BAA0B;AAAA,MACnD,SAAS,KAAK,OAAO,WAAW;AAAA,MAChC;AAAA,IACF,CAAC,EAAE,KAAK,CAAC,SAAU,KAA4B,SAAS;AAAA,EAC1D;AAAA,EAEA,MAAM,gBAAgB,IAIL;AACf,UAAM,KAAK,mBAAmB;AAC9B,WAAO,KAAK,eAAe,8BAA8B;AAAA,MACvD,SAAS,KAAK,OAAO,WAAW;AAAA,MAChC,IAAI,GAAG;AAAA,MACP,MAAM,GAAG;AAAA,MACT,GAAI,GAAG,UAAU,SAAY,EAAE,OAAO,GAAG,MAAM,SAAS,EAAE,EAAE,IAAI,CAAC;AAAA,IACnE,CAAC,EAAE,KAAK,CAAC,SAAU,KAAyB,MAAM;AAAA,EACpD;AAAA,EAEA,MAAM,YAAY,SAAkC;AAClD,UAAM,KAAK,mBAAmB;AAC9B,WAAO,KAAK,eAAe,0BAA0B,EAAE,QAAQ,CAAC,EAAE;AAAA,MAChE,CAAC,SAAU,KAA6B;AAAA,IAC1C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,cAAc,MAKH;AACf,UAAM,KAAK,mBAAmB;AAC9B,WAAO,KAAK,eAAe,4BAA4B;AAAA,MACrD,SAAS,KAAK,OAAO,WAAW;AAAA,MAChC,QAAQ,KAAK;AAAA,MACb,OAAO,KAAK;AAAA,MACZ,aAAa,KAAK;AAAA,MAClB,SAAS,KAAK;AAAA,IAChB,CAAC,EAAE,KAAK,CAAC,SAAU,KAA4B,SAAS;AAAA,EAC1D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,QAAQ,MAIa;AACzB,UAAM,KAAK,mBAAmB;AAC9B,UAAM,gBAAgB,kBAAkB,oBAAoB;AAC5D,UAAM,UACJ,KAAK,QAAQ,oBAAoB;AACnC,WAAO,IAAI,QAAuB,CAAC,SAAS,WAAW;AACrD,YAAM,aAA4B;AAAA,QAChC;AAAA,QACA,QAAQ;AAAA,QACR,QAAQ,CAAC;AAAA,MACX;AACA,YAAM,QAAQ,WAAW,MAAM;AAC7B,aAAK,YAAY,OAAO,aAAa;AACrC;AAAA,UACE,YAAY,MAAM,8BAA8B,OAAO,IAAI;AAAA,QAC7D;AAAA,MACF,GAAG,OAAO;AACV,WAAK,YAAY,IAAI,eAAe;AAAA,QAClC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AACD,YAAM,UAA0B;AAAA,QAC9B,MAAM;AAAA,QACN;AAAA,QACA,UAAU,KAAK;AAAA,QACf,QAAQ,KAAK;AAAA,QACb,GAAI,KAAK,WAAW,SAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;AAAA,MAC7D;AACA,WAAK,aAAa,OAAO;AAEzB,WAAK,KAAK,OAAO,UAAU;AAAA,IAC7B,CAAC;AAAA,EACH;AAAA;AAAA,EAIQ,sBAA4B;AAClC,QAAI,KAAK,mBAAmB,MAAM;AAChC,oBAAc,KAAK,cAAc;AACjC,WAAK,iBAAiB;AAAA,IACxB;AAAA,EACF;AAAA,EAEQ,gBAAsB;AAC5B,SAAK,aAAa;AAAA,MAChB,MAAM;AAAA,MACN,OAAO,KAAK,QAAQ;AAAA,MACpB,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AAAA,EAEQ,aAAa,SAAuB;AAC1C,QAAI,OAAO,WAAW,YAAa;AACnC,QAAI;AACF,aAAO,OAAO,YAAY,SAAS,KAAK,QAAQ,YAAY;AAAA,IAC9D,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,sBAAsB,CAAC,UAA8B;AAC3D,QAAI,MAAM,WAAW,KAAK,QAAQ,aAAc;AAChD,UAAM,OAAO,MAAM;AACnB,QAAI,OAAO,SAAS,YAAY,SAAS,KAAM;AAC/C,UAAM,UAAU;AAChB,YAAQ,QAAQ,MAAM;AAAA,MACpB,KAAK;AACH,aAAK,iBAAiB;AACtB,aAAK,oBAAoB;AACzB,mBAAW,WAAW,KAAK,iBAAkB,SAAQ;AACrD,aAAK,mBAAmB,CAAC;AACzB;AAAA,MACF,KAAK;AACH,YAAI,QAAQ,IAAI;AACd,eAAK,aAAa;AAAA,YAChB,SACE,QAAQ,KAAK,YAAY,oBACrB,OACA,QAAQ,KAAK;AAAA,YACnB,SAAS,QAAQ,KAAK;AAAA,YACtB,aAAa,QAAQ,KAAK,YAAY;AAAA,UACxC,CAAC;AAAA,QACH;AACA;AAAA,MACF,KAAK;AACH,aAAK,aAAa;AAAA,UAChB,SAAS,QAAQ;AAAA,UACjB,SAAS,KAAK,OAAO;AAAA,UACrB,aAAa,QAAQ,YAAY;AAAA,QACnC,CAAC;AACD;AAAA,MACF,KAAK;AACH,aAAK,aAAa;AAAA,UAChB,SAAS,KAAK,OAAO;AAAA,UACrB,SAAS,QAAQ;AAAA,UACjB,aAAa,KAAK,OAAO;AAAA,QAC3B,CAAC;AACD;AAAA,MACF,KAAK;AACH,aAAK,cAAc,OAAO;AAC1B;AAAA,MACF,KAAK;AACH,aAAK,gBAAgB,OAAO;AAC5B;AAAA;AAAA;AAAA,MAGF;AACE;AAAA,IACJ;AAAA,EACF;AAAA,EAEA,MAAc,eACZ,MAMA,SACA,WACkB;AAClB,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,gBAAgB,kBAAkB,IAAI;AAC5C,YAAM,UAAU,aAAa,KAAK,QAAQ,oBAAoB;AAC9D,YAAM,eACJ;AAAA,QACE,0BAA0B;AAAA,QAC1B,8BAA8B;AAAA,QAC9B,4BAA4B;AAAA,QAC5B,0BAA0B;AAAA,QAC1B,6BAA6B;AAAA,MAC/B,EACA,IAAI;AACN,YAAM,QAAQ,WAAW,MAAM;AAC7B,eAAO,oBAAoB,WAAW,QAAQ;AAC9C,eAAO,YAAY,MAAM,6BAA6B,IAAI,OAAO,OAAO,IAAI,CAAC;AAAA,MAC/E,GAAG,OAAO;AACV,YAAM,WAAW,CAAC,UAAwB;AACxC,YAAI,MAAM,WAAW,KAAK,QAAQ,aAAc;AAChD,cAAM,OAAO,MAAM;AACnB,YAAI,OAAO,SAAS,YAAY,SAAS,KAAM;AAC/C,cAAM,MAAM;AACZ,YACE,IAAI,SAAS,gBACb,EAAE,mBAAmB,QACrB,IAAI,kBAAkB,eACtB;AACA;AAAA,QACF;AACA,qBAAa,KAAK;AAClB,eAAO,oBAAoB,WAAW,QAAQ;AAE9C,cAAM,MAAM;AAKZ,YAAI,IAAI,IAAI;AACV,kBAAQ,IAAI,IAAI;AAAA,QAClB,OAAO;AACL,iBAAO,YAAY,MAAM,IAAI,SAAS,yBAAyB,CAAC;AAAA,QAClE;AAAA,MACF;AACA,aAAO,iBAAiB,WAAW,QAAQ;AAC3C,WAAK,aAAa,EAAE,MAAM,eAAe,GAAG,QAAQ,CAAC;AAAA,IACvD,CAAC;AAAA,EACH;AAAA,EAEQ,gBAAgB,SAA+B;AACrD,UAAM,UAAU,KAAK,YAAY,IAAI,QAAQ,aAAa;AAC1D,QAAI,CAAC,QAAS;AACd,UAAM,UAAyB;AAAA,MAC7B,eAAe,QAAQ;AAAA,MACvB,QAAQ,QAAQ;AAAA,MAChB,QACE,QAAQ,UAAU,SACd,CAAC,GAAG,QAAQ,WAAW,QAAQ,QAAQ,KAAK,IAC5C,QAAQ,WAAW;AAAA,MACzB,GAAI,QAAQ,SAAS,SAAY,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,MAC3D,GAAI,QAAQ,UAAU,SAAY,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;AAAA,MAC9D,GAAI,QAAQ,aAAa,SAAY,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,IACzE;AACA,YAAQ,aAAa;AACrB,SAAK,KAAK,OAAO,OAAO;AACxB,QAAI,QAAQ,WAAW,aAAa,QAAQ,WAAW,SAAS;AAC9D,mBAAa,QAAQ,KAAK;AAC1B,WAAK,YAAY,OAAO,QAAQ,aAAa;AAC7C,UAAI,QAAQ,WAAW,WAAW;AAChC,gBAAQ,QAAQ,OAAO;AAAA,MACzB,OAAO;AACL,gBAAQ,OAAO,YAAY,MAAM,QAAQ,SAAS,YAAY,CAAC;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,aAAa,MAA4B;AAC/C,QACE,KAAK,OAAO,YAAY,KAAK,WAC7B,KAAK,OAAO,YAAY,KAAK,WAC7B,KAAK,OAAO,gBAAgB,KAAK,aACjC;AACA;AAAA,IACF;AACA,SAAK,SAAS;AACd,SAAK,KAAK,UAAU,IAAI;AAAA,EAC1B;AAAA,EAEQ,cAAc,WAA0C;AAC9D,UAAM,OAAwB;AAAA,MAC5B,aAAa,UAAU;AAAA,MACvB,WAAW,UAAU;AAAA,MACrB,WAAW,UAAU;AAAA,MACrB,MAAM,UAAU;AAAA,MAChB,MAAM,UAAU;AAAA,MAChB,OAAO,UAAU,SAAS;AAAA,IAC5B;AACA,SAAK,UAAU;AACf,SAAK,KAAK,WAAW,IAAI;AAAA,EAC3B;AAAA,EAEQ,KACN,OACA,OACM;AACN,eAAW,YAAY,CAAC,GAAG,KAAK,UAAU,KAAK,CAAC,GAAG;AACjD,UAAI;AACF,QAAC,SAAyB,KAAK;AAAA,MACjC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,qBAAoC;AAChD,QAAI,KAAK,eAAgB;AACzB,SAAK,QAAQ;AACb,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,WAAK,iBAAiB,KAAK,OAAO;AAClC,YAAM,QAAQ,YAAY,MAAM;AAC9B,YAAI,KAAK,gBAAgB;AACvB,wBAAc,KAAK;AACnB;AAAA,QACF;AACA,aAAK,cAAc;AAAA,MACrB,GAAG,GAAG;AACN,iBAAW,MAAM,cAAc,KAAK,GAAG,GAAM;AAAA,IAC/C,CAAC;AAAA,EACH;AACF;AAEA,SAAS,YAAY,MAAc,SAAwB;AACzD,QAAM,MAAM,IAAI,MAAM,OAAO;AAC7B,MAAI,OAAO;AACX,SAAO;AACT;;;AD7XI;AA7GJ,IAAM,sBAAsB,cAAmC,IAAI;AAEnE,IAAMA,eAA8B;AAAA,EAClC,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AACA,IAAMC,gBAAgC;AAAA,EACpC,aAAa;AAAA,EACb,WAAW;AAAA,EACX,WAAW,CAAC;AAAA,EACZ,MAAM,CAAC;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AACT;AAmBO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA,MAAM,gBAAgB;AAAA,EACtB;AACF,GAAU;AAGR,QAAM,aAAa,QAAQ,MAAM;AAC/B,QAAI,kBAAkB,OAAO;AAC3B,aAAO,EAAE,MAAM,OAAgB,cAAc,KAAK;AAAA,IACpD;AACA,UAAM,WACJ,kBAAkB,8BAA8B,EAAE,aAAa,CAAC;AAClE,QAAI,kBAAkB,UAAU;AAC9B,UAAI,CAAC,UAAU;AAEb,gBAAQ;AAAA,UACN;AAAA,QACF;AACA,eAAO,EAAE,MAAM,OAAgB,cAAc,KAAK;AAAA,MACpD;AACA,aAAO,EAAE,MAAM,UAAmB,cAAc,SAAS;AAAA,IAC3D;AAEA,WAAO,WACH,EAAE,MAAM,UAAmB,cAAc,SAAS,IAClD,EAAE,MAAM,OAAgB,cAAc,KAAK;AAAA,EACjD,GAAG,CAAC,eAAe,gBAAgB,YAAY,CAAC;AAEhD,QAAM,YAAY,OAAkC,IAAI;AACxD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAyBD,YAAW;AAChE,QAAM,CAAC,SAAS,UAAU,IAAI,SAA0BC,aAAY;AACpE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAE5C,YAAU,MAAM;AACd,QAAI,WAAW,SAAS,OAAO;AAI7B,iBAAW,IAAI;AACf,aAAO;AAAA,IACT;AAEA,UAAM,UAAqC;AAAA,MACzC,cAAc,WAAW;AAAA,MACzB;AAAA,IACF;AACA,UAAM,SAAS,IAAI,mBAAmB,OAAO;AAC7C,cAAU,UAAU;AACpB,UAAM,cAAc,OAAO,UAAU,UAAU,SAAS;AACxD,UAAM,eAAe,OAAO,UAAU,WAAW,UAAU;AAC3D,WAAO,QAAQ;AACf,eAAW,IAAI;AACf,WAAO,MAAM;AACX,kBAAY;AACZ,mBAAa;AACb,aAAO,UAAU;AACjB,gBAAU,UAAU;AACpB,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,YAAY,KAAK,CAAC;AAEtB,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,MACL,QAAQ,UAAU;AAAA,MAClB;AAAA,MACA;AAAA,MACA,MAAM,WAAW;AAAA,MACjB;AAAA,IACF;AAAA,IACA,CAAC,QAAQ,SAAS,WAAW,MAAM,OAAO;AAAA,EAC5C;AAEA,SACE,oBAAC,oBAAoB,UAApB,EAA6B,OAC3B,UACH;AAEJ;AAEO,SAAS,yBAAuC;AACrD,QAAM,MAAM,WAAW,mBAAmB;AAC1C,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;;;AEzKA,SAAS,aAAa,aAAAC,YAAW,WAAAC,UAAS,YAAAC,iBAAgB;AAC1D;AAAA,EACE;AAAA,EACA;AAAA,OAKK;AAsBA,SAAS,kBAed;AACA,QAAM,EAAE,QAAQ,OAAO,IAAI,uBAAuB;AAClD,QAAM,UAAU,YAAY,MAAM;AAChC,QAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAChE,WAAO,OAAO,QAAQ;AAAA,EACxB,GAAG,CAAC,MAAM,CAAC;AACX,QAAM,cAAc;AAAA,IAClB,CAAC,YAAoB;AACnB,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAChE,aAAO,OAAO,YAAY,OAAO;AAAA,IACnC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,QAAM,kBAAkB;AAAA,IACtB,CAAC,OAAmD;AAClD,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAChE,aAAO,OAAO,gBAAgB,EAAE;AAAA,IAClC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,QAAM,gBAAgB;AAAA,IACpB,CAAC,SAKK;AACJ,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAChE,aAAO,OAAO,cAAc,IAAI;AAAA,IAClC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,QAAM,cAAc;AAAA,IAClB,CAAC,YAAoB;AACnB,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAChE,aAAO,OAAO,YAAY,OAAO;AAAA,IACnC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAaO,SAAS,kBAAuC;AACrD,SAAO,uBAAuB,EAAE,QAAQ;AAC1C;AAeO,SAAS,wBAA6C;AAC3D,QAAM,QAAQ,gBAAgB;AAC9B,SAAOC,SAAQ,MAAM;AACnB,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,cAAqB;AAAA,MACzB,IAAI,MAAM;AAAA,MACV,MAAM,MAAM;AAAA,MACZ,gBACE,MAAM,mBAAmB,SACrB,EAAE,GAAG,MAAM,eAAe,IAC1B,EAAE,MAAM,SAAS,QAAQ,OAAO,UAAU,GAAG;AAAA,MACnD,SAAS;AAAA,QACP,SAAS,EAAE,MAAM,CAAC,MAAM,MAAM,EAAE;AAAA,MAClC;AAAA,MACA,GAAI,MAAM,mBACN;AAAA,QACE,gBAAgB;AAAA,UACd,SAAS,EAAE,MAAM,YAAY,KAAK,MAAM,iBAAiB;AAAA,QAC3D;AAAA,MACF,IACA,CAAC;AAAA,IACP;AACA,WAAO,mBAAmB;AAAA,MACxB,OAAO;AAAA,MACP,WAAW,KAAK,MAAM,MAAM;AAAA,IAC9B,CAAC;AAAA,EACH,GAAG,CAAC,KAAK,CAAC;AACZ;AAUO,SAAS,mBAAoC;AAClD,SAAO,uBAAuB,EAAE;AAClC;AAgBO,SAAS,aAAa;AAC3B,QAAM,EAAE,OAAO,IAAI,uBAAuB;AAC1C,QAAM,CAAC,YAAY,aAAa,IAAIC,UAA+B,IAAI;AACvE,QAAM,CAAC,UAAU,WAAW,IAAIA,UAAwB,IAAI;AAE5D,EAAAC,WAAU,MAAM;AACd,QAAI,CAAC,OAAQ,QAAO;AACpB,WAAO,OAAO,UAAU,OAAO,CAAC,SAAS;AAEvC,kBAAY,CAAC,eAAe;AAC1B,YAAI,eAAe,QAAQ,eAAe,KAAK,eAAe;AAC5D,wBAAc,IAAI;AAClB,iBAAO,KAAK;AAAA,QACd;AACA,eAAO;AAAA,MACT,CAAC;AAAA,IACH,CAAC;AAAA,EACH,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,OAAO;AAAA,IACX,OAAO,SAAoE;AACzE,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AAEA,oBAAc,IAAI;AAClB,YAAM,SAAS,MAAM,OAAO,QAAQ,IAAI;AACxC,kBAAY,OAAO,aAAa;AAChC,aAAO;AAAA,IACT;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,QAAM,QAAQ,YAAY,MAAM;AAC9B,kBAAc,IAAI;AAClB,gBAAY,IAAI;AAAA,EAClB,GAAG,CAAC,CAAC;AAEL,SAAOF;AAAA,IACL,OAAO,EAAE,MAAM,YAAY,OAAO,WAAW,YAAY,WAAW,aAAa,YAAY,WAAW,YAAY;AAAA,IACpH,CAAC,MAAM,YAAY,KAAK;AAAA,EAC1B;AACF;AAMO,SAAS,mBAAmC;AACjD,SAAO,uBAAuB,EAAE,OAAO;AACzC;AAGO,SAAS,iBAA0B;AACxC,SAAO,uBAAuB,EAAE;AAClC;AAGO,SAAS,gBAAkC;AAChD,SAAO,uBAAuB,EAAE;AAClC;","names":["NULL_WALLET","NULL_SERVICE","useEffect","useMemo","useState","useMemo","useState","useEffect"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { C as CallJobRequest, h as ServiceContextOperator, g as ServiceContextJob, b as ChainContext } from '../parentBridgeProtocol-
|
|
1
|
+
import { C as CallJobRequest, h as ServiceContextOperator, g as ServiceContextJob, b as ChainContext } from '../parentBridgeProtocol-BSgLXg9g.js';
|
|
2
2
|
import { FC, ReactNode } from 'react';
|
|
3
3
|
import { Address } from 'viem';
|
|
4
|
-
import { W as WalletSnapshot, S as ServiceSnapshot } from '../tangleIframeClient-
|
|
4
|
+
import { W as WalletSnapshot, S as ServiceSnapshot } from '../tangleIframeClient-C7NFG_Dw.js';
|
|
5
5
|
|
|
6
6
|
type MockWalletInput = Partial<{
|
|
7
7
|
address: Address | null;
|
|
@@ -112,6 +112,19 @@ var TangleParentHarness = ({
|
|
|
112
112
|
});
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
115
|
+
case "tangle.app.requestConnect": {
|
|
116
|
+
if (typeof message.correlationId !== "string") return;
|
|
117
|
+
reply({
|
|
118
|
+
kind: "tangle.app.connectResult",
|
|
119
|
+
correlationId: message.correlationId,
|
|
120
|
+
ok: true,
|
|
121
|
+
data: {
|
|
122
|
+
account: currentWallet.address ?? "0x0000000000000000000000000000000000000000",
|
|
123
|
+
chainId: currentWallet.chainId ?? 0
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
115
128
|
case "tangle.app.callJob": {
|
|
116
129
|
if (typeof message.correlationId !== "string") return;
|
|
117
130
|
const request = message;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/iframe/testing.tsx"],"sourcesContent":["// Testing harness for iframe blueprints. The promise of the SDK is that\n// publishers can iterate on their UI without running the Tangle Cloud dapp\n// — these utilities are what makes that true.\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport type { Address } from 'viem';\n\nimport type {\n ServiceSnapshot,\n WalletSnapshot,\n} from './tangleIframeClient';\nimport type {\n CallJobRequest,\n JobInputs,\n JobResultEvent,\n ParentMessage,\n ServiceContextBroadcast,\n ServiceContextJob,\n ServiceContextOperator,\n} from '../wallet/parentBridgeProtocol';\n\nexport type MockWalletInput = Partial<{\n address: Address | null;\n chainId: number;\n isConnected: boolean;\n}>;\n\nexport type MockServiceInput = Partial<{\n blueprintId: string;\n serviceId: string | null;\n operators: readonly ServiceContextOperator[];\n jobs: readonly ServiceContextJob[];\n mode: string | null;\n chain: import('../wallet/parentBridgeProtocol').ChainContext | null;\n}>;\n\n/**\n * Construct a deterministic wallet snapshot for tests. Defaults:\n * connected, vitalik.eth's address, Base Sepolia (84532).\n */\nexport function mockWallet(input: MockWalletInput = {}): WalletSnapshot {\n return {\n address:\n input.address === undefined\n ? '0xd8da6bf26964af9d7eed9e03e53415d37aa96045'\n : input.address,\n chainId: input.chainId ?? 84532,\n isConnected: input.isConnected ?? input.address !== null,\n };\n}\n\n/**\n * Construct a deterministic service snapshot for tests. Defaults: blueprint\n * id `0`, no service deployed yet (serviceId null), single mock operator on\n * the canonical local sidecar URL.\n */\nexport function mockServiceContext(\n input: MockServiceInput = {},\n): ServiceSnapshot {\n return {\n blueprintId: input.blueprintId ?? '0',\n serviceId: input.serviceId === undefined ? null : input.serviceId,\n operators:\n input.operators ?? [\n {\n address: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',\n rpcAddress: 'http://localhost:8545',\n status: 'active',\n },\n ],\n jobs:\n input.jobs ?? [\n { index: 0, name: 'invoke' },\n ],\n mode: input.mode ?? null,\n chain:\n input.chain === undefined\n ? {\n id: 84532,\n name: 'Base Sepolia',\n rpcUrl: 'https://sepolia.base.org',\n blockExplorerUrl: 'https://sepolia.basescan.org',\n nativeCurrency: { name: 'Sepolia Ether', symbol: 'ETH', decimals: 18 },\n }\n : input.chain,\n };\n}\n\nexport type CallJobHandler = (\n request: CallJobRequest,\n) => Promise<{\n status: 'success' | 'error';\n data?: unknown;\n error?: string;\n /** Streaming chunks emitted in order before the terminal status. */\n chunks?: readonly unknown[];\n}>;\n\ntype HarnessProps = {\n appId?: string;\n wallet?: WalletSnapshot;\n service?: ServiceSnapshot;\n /** Override callJob behavior. Default: returns a static `{ ok: true }`. */\n onCallJob?: CallJobHandler;\n /** Surface a floating debug panel that lets the developer flip state at runtime. */\n showDebugPanel?: boolean;\n children: ReactNode;\n};\n\n/**\n * Drop-in parent simulator for tests + storybook + standalone dev. Wraps\n * children in a fake parent that:\n *\n * - Acks the iframe's handshake immediately\n * - Broadcasts the configured wallet + service context on mount\n * - Intercepts `callJob` requests and routes them through `onCallJob`\n * - (Optional) Mounts a floating debug panel so the developer can\n * mutate state at runtime: change account, switch chain, set\n * serviceId, fire a custom job\n *\n * The harness runs in the same JS context as the iframe app — there's no\n * cross-frame postMessage, just same-window event dispatch. That keeps it\n * fully synchronous + assertable, but the messages still flow through the\n * exact same protocol surface the production bridge uses.\n *\n * Usage:\n *\n * <TangleParentHarness wallet={mockWallet()} service={mockServiceContext()}>\n * <TangleIframeProvider appId=\"my-app\" mode=\"bridge\" parentOrigin=\"harness://\">\n * <App />\n * </TangleIframeProvider>\n * </TangleParentHarness>\n *\n * Set `mode=\"bridge\"` + `parentOrigin=\"harness://\"` on the provider so it\n * matches the harness's synthetic origin. In production, use `mode=\"auto\"`\n * (the default).\n */\nexport const TangleParentHarness: FC<HarnessProps> = ({\n appId = 'harness',\n wallet = mockWallet(),\n service = mockServiceContext(),\n onCallJob,\n showDebugPanel = false,\n children,\n}) => {\n const [currentWallet, setCurrentWallet] = useState<WalletSnapshot>(wallet);\n const [currentService, setCurrentService] =\n useState<ServiceSnapshot>(service);\n const [callLog, setCallLog] = useState<CallJobRequest[]>([]);\n const callJobHandler = useRef<CallJobHandler | undefined>(onCallJob);\n callJobHandler.current = onCallJob;\n const seenHandshake = useRef(false);\n\n // Bridge the iframe → \"parent\" channel. The SDK posts via\n // `window.parent.postMessage(msg, parentOrigin)`. In a real iframe that\n // crosses a window boundary; in the harness both live in one window, where\n // a same-window `postMessage` with a synthetic targetOrigin is *not*\n // delivered (the origin won't match the document's real origin in jsdom /\n // happy-dom / a real browser). So we intercept `window.parent.postMessage`\n // directly — identical to how production parents receive frames, minus the\n // window hop. Replies still travel back as dispatched `message` events\n // tagged with HARNESS_ORIGIN, which the SDK's listener filters for.\n useEffect(() => {\n const reply = (message: ParentMessage) => {\n window.dispatchEvent(\n new MessageEvent('message', {\n data: message,\n origin: HARNESS_ORIGIN,\n }),\n );\n };\n\n const broadcast = () => {\n const broadcastMsg: ServiceContextBroadcast = {\n kind: 'tangle.app.serviceContext',\n blueprintId: currentService.blueprintId ?? '0',\n serviceId: currentService.serviceId,\n operators: currentService.operators,\n jobs: currentService.jobs,\n mode: currentService.mode,\n ...(currentService.chain !== null\n ? { chain: currentService.chain }\n : {}),\n };\n reply(broadcastMsg);\n // Also broadcast wallet — combined into accountChanged + chainChanged.\n reply({\n kind: 'tangle.app.accountChanged',\n account: currentWallet.address,\n });\n if (currentWallet.chainId !== null) {\n reply({\n kind: 'tangle.app.chainChanged',\n chainId: currentWallet.chainId,\n });\n }\n };\n\n const handleInbound = async (data: unknown) => {\n if (typeof data !== 'object' || data === null) return;\n const message = data as { kind?: string; correlationId?: string };\n\n switch (message.kind) {\n case 'tangle.app.handshake': {\n if (!seenHandshake.current) {\n seenHandshake.current = true;\n reply({\n kind: 'tangle.app.handshakeAck',\n appId,\n protocolVersion: '1',\n });\n broadcast();\n }\n return;\n }\n case 'tangle.app.readAccount': {\n if (typeof message.correlationId !== 'string') return;\n reply({\n kind: 'tangle.app.readAccountResult',\n correlationId: message.correlationId,\n ok: true,\n data: {\n account:\n currentWallet.address ??\n ('0x0000000000000000000000000000000000000000' as Address),\n chainId: currentWallet.chainId ?? 0,\n },\n });\n return;\n }\n case 'tangle.app.callJob': {\n if (typeof message.correlationId !== 'string') return;\n const request = message as unknown as CallJobRequest;\n setCallLog((prev) => [...prev, request]);\n // Default behavior when no handler: emit a single `success` with\n // a echo of the inputs so UIs render *something* in dev mode.\n const handler = callJobHandler.current;\n if (!handler) {\n const result: JobResultEvent = {\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: 'success',\n data: { echo: request.inputs },\n };\n reply(result);\n return;\n }\n try {\n const outcome = await handler(request);\n for (const chunk of outcome.chunks ?? []) {\n reply({\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: 'streaming',\n chunk,\n });\n }\n reply({\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: outcome.status,\n ...(outcome.data !== undefined ? { data: outcome.data } : {}),\n ...(outcome.error !== undefined ? { error: outcome.error } : {}),\n });\n } catch (err) {\n reply({\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: 'error',\n error: err instanceof Error ? err.message : String(err),\n });\n }\n return;\n }\n // Wallet ops respond optimistically — tests that want to assert\n // specific signatures should pre-set them via the dev handler.\n case 'tangle.app.signMessage': {\n if (typeof message.correlationId !== 'string') return;\n reply({\n kind: 'tangle.app.signMessageResult',\n correlationId: message.correlationId,\n ok: true,\n data: { signature: '0xdeadbeef' as `0x${string}` },\n });\n return;\n }\n case 'tangle.app.signTypedData': {\n if (typeof message.correlationId !== 'string') return;\n // The harness signs deterministically — production parents show\n // an approval modal first. Tests that need to assert the\n // typed-data payload should inspect callLog (extend later if\n // needed) or pass a custom onSignTypedData handler.\n reply({\n kind: 'tangle.app.signTypedDataResult',\n correlationId: message.correlationId,\n ok: true,\n data: {\n signature: ('0x' + '11'.repeat(65)) as `0x${string}`,\n },\n });\n return;\n }\n case 'tangle.app.signTransaction': {\n if (typeof message.correlationId !== 'string') return;\n reply({\n kind: 'tangle.app.signTransactionResult',\n correlationId: message.correlationId,\n ok: true,\n data: { txHash: ('0x' + '00'.repeat(32)) as `0x${string}` },\n });\n return;\n }\n case 'tangle.app.switchChain': {\n if (\n typeof message.correlationId !== 'string' ||\n typeof (message as unknown as { chainId?: number }).chainId !== 'number'\n ) {\n return;\n }\n const chainId = (message as unknown as { chainId: number }).chainId;\n setCurrentWallet((w) => ({ ...w, chainId }));\n reply({\n kind: 'tangle.app.switchChainResult',\n correlationId: message.correlationId,\n ok: true,\n data: { chainId },\n });\n return;\n }\n }\n };\n\n // Route the iframe's outbound posts straight into the handler. We expose\n // only `postMessage` — the SDK never touches other `window.parent`\n // members — and restore the original on teardown.\n const originalParent = window.parent;\n const proxyParent = {\n postMessage: (message: unknown) => {\n void handleInbound(message);\n },\n } as unknown as Window;\n Object.defineProperty(window, 'parent', {\n configurable: true,\n get: () => proxyParent,\n });\n return () => {\n Object.defineProperty(window, 'parent', {\n configurable: true,\n value: originalParent,\n });\n };\n }, [appId, currentWallet, currentService]);\n\n // Re-broadcast when state changes.\n useEffect(() => {\n if (!seenHandshake.current) return;\n window.dispatchEvent(\n new MessageEvent('message', {\n data: {\n kind: 'tangle.app.accountChanged',\n account: currentWallet.address,\n },\n origin: HARNESS_ORIGIN,\n }),\n );\n }, [currentWallet.address]);\n\n useEffect(() => {\n if (!seenHandshake.current || currentWallet.chainId === null) return;\n window.dispatchEvent(\n new MessageEvent('message', {\n data: {\n kind: 'tangle.app.chainChanged',\n chainId: currentWallet.chainId,\n },\n origin: HARNESS_ORIGIN,\n }),\n );\n }, [currentWallet.chainId]);\n\n useEffect(() => {\n if (!seenHandshake.current) return;\n window.dispatchEvent(\n new MessageEvent('message', {\n data: {\n kind: 'tangle.app.serviceContext',\n blueprintId: currentService.blueprintId ?? '0',\n serviceId: currentService.serviceId,\n operators: currentService.operators,\n jobs: currentService.jobs,\n mode: currentService.mode,\n ...(currentService.chain !== null\n ? { chain: currentService.chain }\n : {}),\n },\n origin: HARNESS_ORIGIN,\n }),\n );\n }, [currentService]);\n\n const debugApi = useMemo(\n () => ({\n setWallet: setCurrentWallet,\n setService: setCurrentService,\n callLog,\n }),\n [callLog],\n );\n\n return (\n <>\n {children}\n {showDebugPanel && <DebugPanel api={debugApi} />}\n </>\n );\n};\n\n/**\n * Synthetic origin every harness instance uses. Stable across tests so the\n * iframe SDK + the harness can pin to the same string.\n */\nexport const HARNESS_ORIGIN = 'harness://tangle.local';\n\n// ── Debug panel ──────────────────────────────────────────────────────────────\n\nconst DebugPanel: FC<{\n api: {\n setWallet: (w: WalletSnapshot | ((prev: WalletSnapshot) => WalletSnapshot)) => void;\n setService: (\n s: ServiceSnapshot | ((prev: ServiceSnapshot) => ServiceSnapshot),\n ) => void;\n callLog: readonly CallJobRequest[];\n };\n}> = ({ api }) => {\n const [open, setOpen] = useState(true);\n const [tab, setTab] = useState<'wallet' | 'service' | 'log'>('wallet');\n if (!open) {\n return (\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n style={debugStyles.collapsedTrigger}\n >\n Debug\n </button>\n );\n }\n return (\n <div style={debugStyles.panel}>\n <header style={debugStyles.header}>\n <strong style={{ fontSize: 11 }}>TANGLE DEV HARNESS</strong>\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n style={debugStyles.closeButton}\n aria-label=\"Close debug panel\"\n >\n ×\n </button>\n </header>\n <nav style={debugStyles.tabs}>\n {(['wallet', 'service', 'log'] as const).map((t) => (\n <button\n key={t}\n type=\"button\"\n onClick={() => setTab(t)}\n style={{\n ...debugStyles.tab,\n ...(tab === t ? debugStyles.tabActive : {}),\n }}\n >\n {t}\n </button>\n ))}\n </nav>\n <div style={debugStyles.body}>\n {tab === 'wallet' && <WalletTab api={api} />}\n {tab === 'service' && <ServiceTab api={api} />}\n {tab === 'log' && <CallLogTab callLog={api.callLog} />}\n </div>\n </div>\n );\n};\n\nconst WalletTab: FC<{\n api: { setWallet: (w: WalletSnapshot | ((prev: WalletSnapshot) => WalletSnapshot)) => void };\n}> = ({ api }) => {\n const [address, setAddressInput] = useState(\n '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',\n );\n const [chainId, setChainIdInput] = useState('84532');\n const applyConnect = useCallback(() => {\n api.setWallet({\n address: address as Address,\n chainId: Number(chainId) || null,\n isConnected: true,\n });\n }, [address, chainId, api]);\n const disconnect = useCallback(() => {\n api.setWallet({ address: null, chainId: null, isConnected: false });\n }, [api]);\n return (\n <div>\n <label style={debugStyles.label}>address</label>\n <input\n value={address}\n onChange={(e) => setAddressInput(e.target.value)}\n style={debugStyles.input}\n />\n <label style={debugStyles.label}>chain id</label>\n <input\n value={chainId}\n onChange={(e) => setChainIdInput(e.target.value)}\n style={debugStyles.input}\n />\n <div style={debugStyles.buttonRow}>\n <button type=\"button\" onClick={applyConnect} style={debugStyles.primary}>\n Set connected\n </button>\n <button type=\"button\" onClick={disconnect} style={debugStyles.secondary}>\n Disconnect\n </button>\n </div>\n </div>\n );\n};\n\nconst ServiceTab: FC<{\n api: {\n setService: (\n s: ServiceSnapshot | ((prev: ServiceSnapshot) => ServiceSnapshot),\n ) => void;\n };\n}> = ({ api }) => {\n const [serviceId, setServiceIdInput] = useState('1');\n const [blueprintId, setBlueprintIdInput] = useState('0');\n const apply = useCallback(() => {\n api.setService((prev) => ({\n ...prev,\n serviceId: serviceId || null,\n blueprintId,\n }));\n }, [api, serviceId, blueprintId]);\n const clearService = useCallback(() => {\n api.setService((prev) => ({ ...prev, serviceId: null }));\n }, [api]);\n return (\n <div>\n <label style={debugStyles.label}>blueprint id</label>\n <input\n value={blueprintId}\n onChange={(e) => setBlueprintIdInput(e.target.value)}\n style={debugStyles.input}\n />\n <label style={debugStyles.label}>service id (empty = not deployed)</label>\n <input\n value={serviceId}\n onChange={(e) => setServiceIdInput(e.target.value)}\n style={debugStyles.input}\n />\n <div style={debugStyles.buttonRow}>\n <button type=\"button\" onClick={apply} style={debugStyles.primary}>\n Apply\n </button>\n <button type=\"button\" onClick={clearService} style={debugStyles.secondary}>\n Clear service\n </button>\n </div>\n </div>\n );\n};\n\nconst CallLogTab: FC<{ callLog: readonly CallJobRequest[] }> = ({ callLog }) => {\n if (callLog.length === 0) {\n return <p style={debugStyles.empty}>No callJob requests yet.</p>;\n }\n return (\n <ol style={debugStyles.log}>\n {callLog.map((entry) => (\n <li key={entry.correlationId} style={debugStyles.logEntry}>\n <strong>job {entry.jobIndex}</strong>\n <pre style={debugStyles.pre}>\n {JSON.stringify(entry.inputs, null, 2)}\n </pre>\n </li>\n ))}\n </ol>\n );\n};\n\n// Inline styles keep the harness style-system-agnostic — consumers may not\n// ship Tailwind, and the panel shouldn't add a dependency.\nconst debugStyles = {\n panel: {\n position: 'fixed' as const,\n right: 12,\n top: 12,\n width: 280,\n zIndex: 99999,\n background: '#0b0b14',\n color: '#fff',\n border: '1px solid #3a3a52',\n borderRadius: 10,\n boxShadow: '0 14px 32px rgba(0,0,0,0.4)',\n fontFamily:\n 'ui-monospace, SFMono-Regular, Menlo, Monaco, \"Cascadia Code\", monospace',\n fontSize: 12,\n },\n header: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '8px 10px',\n borderBottom: '1px solid #2a2a3e',\n },\n closeButton: {\n background: 'none',\n border: 'none',\n color: '#fff',\n fontSize: 18,\n cursor: 'pointer',\n lineHeight: 1,\n },\n tabs: {\n display: 'flex',\n borderBottom: '1px solid #2a2a3e',\n },\n tab: {\n flex: 1,\n background: 'none',\n border: 'none',\n color: '#a0a0c0',\n padding: '6px 8px',\n cursor: 'pointer',\n fontSize: 11,\n textTransform: 'uppercase' as const,\n },\n tabActive: {\n color: '#fff',\n borderBottom: '2px solid #818cf8',\n },\n body: {\n padding: 10,\n maxHeight: 320,\n overflow: 'auto' as const,\n },\n label: {\n display: 'block',\n color: '#a0a0c0',\n fontSize: 10,\n marginBottom: 4,\n marginTop: 6,\n textTransform: 'uppercase' as const,\n },\n input: {\n width: '100%',\n background: '#15152a',\n border: '1px solid #2a2a3e',\n color: '#fff',\n padding: '6px 8px',\n borderRadius: 4,\n fontFamily: 'inherit',\n fontSize: 11,\n boxSizing: 'border-box' as const,\n },\n buttonRow: { display: 'flex', gap: 6, marginTop: 8 },\n primary: {\n flex: 1,\n background: '#4f46e5',\n color: '#fff',\n border: 'none',\n padding: '6px 8px',\n borderRadius: 4,\n cursor: 'pointer',\n fontSize: 11,\n fontFamily: 'inherit',\n },\n secondary: {\n flex: 1,\n background: 'transparent',\n color: '#a0a0c0',\n border: '1px solid #3a3a52',\n padding: '6px 8px',\n borderRadius: 4,\n cursor: 'pointer',\n fontSize: 11,\n fontFamily: 'inherit',\n },\n collapsedTrigger: {\n position: 'fixed' as const,\n right: 12,\n top: 12,\n zIndex: 99999,\n padding: '6px 10px',\n background: '#0b0b14',\n border: '1px solid #3a3a52',\n color: '#fff',\n borderRadius: 6,\n fontFamily: 'inherit',\n fontSize: 11,\n cursor: 'pointer',\n },\n log: { listStyle: 'none', padding: 0, margin: 0 },\n logEntry: {\n padding: 6,\n borderBottom: '1px solid #2a2a3e',\n fontSize: 11,\n },\n pre: {\n margin: '4px 0 0',\n color: '#a0a0c0',\n fontSize: 10,\n whiteSpace: 'pre-wrap' as const,\n wordBreak: 'break-word' as const,\n },\n empty: { color: '#a0a0c0', fontSize: 11, margin: 0 },\n} as const;\n\nexport type { JobInputs };\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/iframe/testing.tsx"],"sourcesContent":["// Testing harness for iframe blueprints. The promise of the SDK is that\n// publishers can iterate on their UI without running the Tangle Cloud dapp\n// — these utilities are what makes that true.\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport type { Address } from 'viem';\n\nimport type {\n ServiceSnapshot,\n WalletSnapshot,\n} from './tangleIframeClient';\nimport type {\n CallJobRequest,\n JobInputs,\n JobResultEvent,\n ParentMessage,\n ServiceContextBroadcast,\n ServiceContextJob,\n ServiceContextOperator,\n} from '../wallet/parentBridgeProtocol';\n\nexport type MockWalletInput = Partial<{\n address: Address | null;\n chainId: number;\n isConnected: boolean;\n}>;\n\nexport type MockServiceInput = Partial<{\n blueprintId: string;\n serviceId: string | null;\n operators: readonly ServiceContextOperator[];\n jobs: readonly ServiceContextJob[];\n mode: string | null;\n chain: import('../wallet/parentBridgeProtocol').ChainContext | null;\n}>;\n\n/**\n * Construct a deterministic wallet snapshot for tests. Defaults:\n * connected, vitalik.eth's address, Base Sepolia (84532).\n */\nexport function mockWallet(input: MockWalletInput = {}): WalletSnapshot {\n return {\n address:\n input.address === undefined\n ? '0xd8da6bf26964af9d7eed9e03e53415d37aa96045'\n : input.address,\n chainId: input.chainId ?? 84532,\n isConnected: input.isConnected ?? input.address !== null,\n };\n}\n\n/**\n * Construct a deterministic service snapshot for tests. Defaults: blueprint\n * id `0`, no service deployed yet (serviceId null), single mock operator on\n * the canonical local sidecar URL.\n */\nexport function mockServiceContext(\n input: MockServiceInput = {},\n): ServiceSnapshot {\n return {\n blueprintId: input.blueprintId ?? '0',\n serviceId: input.serviceId === undefined ? null : input.serviceId,\n operators:\n input.operators ?? [\n {\n address: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',\n rpcAddress: 'http://localhost:8545',\n status: 'active',\n },\n ],\n jobs:\n input.jobs ?? [\n { index: 0, name: 'invoke' },\n ],\n mode: input.mode ?? null,\n chain:\n input.chain === undefined\n ? {\n id: 84532,\n name: 'Base Sepolia',\n rpcUrl: 'https://sepolia.base.org',\n blockExplorerUrl: 'https://sepolia.basescan.org',\n nativeCurrency: { name: 'Sepolia Ether', symbol: 'ETH', decimals: 18 },\n }\n : input.chain,\n };\n}\n\nexport type CallJobHandler = (\n request: CallJobRequest,\n) => Promise<{\n status: 'success' | 'error';\n data?: unknown;\n error?: string;\n /** Streaming chunks emitted in order before the terminal status. */\n chunks?: readonly unknown[];\n}>;\n\ntype HarnessProps = {\n appId?: string;\n wallet?: WalletSnapshot;\n service?: ServiceSnapshot;\n /** Override callJob behavior. Default: returns a static `{ ok: true }`. */\n onCallJob?: CallJobHandler;\n /** Surface a floating debug panel that lets the developer flip state at runtime. */\n showDebugPanel?: boolean;\n children: ReactNode;\n};\n\n/**\n * Drop-in parent simulator for tests + storybook + standalone dev. Wraps\n * children in a fake parent that:\n *\n * - Acks the iframe's handshake immediately\n * - Broadcasts the configured wallet + service context on mount\n * - Intercepts `callJob` requests and routes them through `onCallJob`\n * - (Optional) Mounts a floating debug panel so the developer can\n * mutate state at runtime: change account, switch chain, set\n * serviceId, fire a custom job\n *\n * The harness runs in the same JS context as the iframe app — there's no\n * cross-frame postMessage, just same-window event dispatch. That keeps it\n * fully synchronous + assertable, but the messages still flow through the\n * exact same protocol surface the production bridge uses.\n *\n * Usage:\n *\n * <TangleParentHarness wallet={mockWallet()} service={mockServiceContext()}>\n * <TangleIframeProvider appId=\"my-app\" mode=\"bridge\" parentOrigin=\"harness://\">\n * <App />\n * </TangleIframeProvider>\n * </TangleParentHarness>\n *\n * Set `mode=\"bridge\"` + `parentOrigin=\"harness://\"` on the provider so it\n * matches the harness's synthetic origin. In production, use `mode=\"auto\"`\n * (the default).\n */\nexport const TangleParentHarness: FC<HarnessProps> = ({\n appId = 'harness',\n wallet = mockWallet(),\n service = mockServiceContext(),\n onCallJob,\n showDebugPanel = false,\n children,\n}) => {\n const [currentWallet, setCurrentWallet] = useState<WalletSnapshot>(wallet);\n const [currentService, setCurrentService] =\n useState<ServiceSnapshot>(service);\n const [callLog, setCallLog] = useState<CallJobRequest[]>([]);\n const callJobHandler = useRef<CallJobHandler | undefined>(onCallJob);\n callJobHandler.current = onCallJob;\n const seenHandshake = useRef(false);\n\n // Bridge the iframe → \"parent\" channel. The SDK posts via\n // `window.parent.postMessage(msg, parentOrigin)`. In a real iframe that\n // crosses a window boundary; in the harness both live in one window, where\n // a same-window `postMessage` with a synthetic targetOrigin is *not*\n // delivered (the origin won't match the document's real origin in jsdom /\n // happy-dom / a real browser). So we intercept `window.parent.postMessage`\n // directly — identical to how production parents receive frames, minus the\n // window hop. Replies still travel back as dispatched `message` events\n // tagged with HARNESS_ORIGIN, which the SDK's listener filters for.\n useEffect(() => {\n const reply = (message: ParentMessage) => {\n window.dispatchEvent(\n new MessageEvent('message', {\n data: message,\n origin: HARNESS_ORIGIN,\n }),\n );\n };\n\n const broadcast = () => {\n const broadcastMsg: ServiceContextBroadcast = {\n kind: 'tangle.app.serviceContext',\n blueprintId: currentService.blueprintId ?? '0',\n serviceId: currentService.serviceId,\n operators: currentService.operators,\n jobs: currentService.jobs,\n mode: currentService.mode,\n ...(currentService.chain !== null\n ? { chain: currentService.chain }\n : {}),\n };\n reply(broadcastMsg);\n // Also broadcast wallet — combined into accountChanged + chainChanged.\n reply({\n kind: 'tangle.app.accountChanged',\n account: currentWallet.address,\n });\n if (currentWallet.chainId !== null) {\n reply({\n kind: 'tangle.app.chainChanged',\n chainId: currentWallet.chainId,\n });\n }\n };\n\n const handleInbound = async (data: unknown) => {\n if (typeof data !== 'object' || data === null) return;\n const message = data as { kind?: string; correlationId?: string };\n\n switch (message.kind) {\n case 'tangle.app.handshake': {\n if (!seenHandshake.current) {\n seenHandshake.current = true;\n reply({\n kind: 'tangle.app.handshakeAck',\n appId,\n protocolVersion: '1',\n });\n broadcast();\n }\n return;\n }\n case 'tangle.app.readAccount': {\n if (typeof message.correlationId !== 'string') return;\n reply({\n kind: 'tangle.app.readAccountResult',\n correlationId: message.correlationId,\n ok: true,\n data: {\n account:\n currentWallet.address ??\n ('0x0000000000000000000000000000000000000000' as Address),\n chainId: currentWallet.chainId ?? 0,\n },\n });\n return;\n }\n case 'tangle.app.requestConnect': {\n // The harness \"connects\" immediately to the mocked wallet — a real\n // parent would open its connect modal and resolve once the user\n // picks a wallet. Tests that need the disconnected path pass a\n // wallet with a null address.\n if (typeof message.correlationId !== 'string') return;\n reply({\n kind: 'tangle.app.connectResult',\n correlationId: message.correlationId,\n ok: true,\n data: {\n account:\n currentWallet.address ??\n ('0x0000000000000000000000000000000000000000' as Address),\n chainId: currentWallet.chainId ?? 0,\n },\n });\n return;\n }\n case 'tangle.app.callJob': {\n if (typeof message.correlationId !== 'string') return;\n const request = message as unknown as CallJobRequest;\n setCallLog((prev) => [...prev, request]);\n // Default behavior when no handler: emit a single `success` with\n // a echo of the inputs so UIs render *something* in dev mode.\n const handler = callJobHandler.current;\n if (!handler) {\n const result: JobResultEvent = {\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: 'success',\n data: { echo: request.inputs },\n };\n reply(result);\n return;\n }\n try {\n const outcome = await handler(request);\n for (const chunk of outcome.chunks ?? []) {\n reply({\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: 'streaming',\n chunk,\n });\n }\n reply({\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: outcome.status,\n ...(outcome.data !== undefined ? { data: outcome.data } : {}),\n ...(outcome.error !== undefined ? { error: outcome.error } : {}),\n });\n } catch (err) {\n reply({\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: 'error',\n error: err instanceof Error ? err.message : String(err),\n });\n }\n return;\n }\n // Wallet ops respond optimistically — tests that want to assert\n // specific signatures should pre-set them via the dev handler.\n case 'tangle.app.signMessage': {\n if (typeof message.correlationId !== 'string') return;\n reply({\n kind: 'tangle.app.signMessageResult',\n correlationId: message.correlationId,\n ok: true,\n data: { signature: '0xdeadbeef' as `0x${string}` },\n });\n return;\n }\n case 'tangle.app.signTypedData': {\n if (typeof message.correlationId !== 'string') return;\n // The harness signs deterministically — production parents show\n // an approval modal first. Tests that need to assert the\n // typed-data payload should inspect callLog (extend later if\n // needed) or pass a custom onSignTypedData handler.\n reply({\n kind: 'tangle.app.signTypedDataResult',\n correlationId: message.correlationId,\n ok: true,\n data: {\n signature: ('0x' + '11'.repeat(65)) as `0x${string}`,\n },\n });\n return;\n }\n case 'tangle.app.signTransaction': {\n if (typeof message.correlationId !== 'string') return;\n reply({\n kind: 'tangle.app.signTransactionResult',\n correlationId: message.correlationId,\n ok: true,\n data: { txHash: ('0x' + '00'.repeat(32)) as `0x${string}` },\n });\n return;\n }\n case 'tangle.app.switchChain': {\n if (\n typeof message.correlationId !== 'string' ||\n typeof (message as unknown as { chainId?: number }).chainId !== 'number'\n ) {\n return;\n }\n const chainId = (message as unknown as { chainId: number }).chainId;\n setCurrentWallet((w) => ({ ...w, chainId }));\n reply({\n kind: 'tangle.app.switchChainResult',\n correlationId: message.correlationId,\n ok: true,\n data: { chainId },\n });\n return;\n }\n }\n };\n\n // Route the iframe's outbound posts straight into the handler. We expose\n // only `postMessage` — the SDK never touches other `window.parent`\n // members — and restore the original on teardown.\n const originalParent = window.parent;\n const proxyParent = {\n postMessage: (message: unknown) => {\n void handleInbound(message);\n },\n } as unknown as Window;\n Object.defineProperty(window, 'parent', {\n configurable: true,\n get: () => proxyParent,\n });\n return () => {\n Object.defineProperty(window, 'parent', {\n configurable: true,\n value: originalParent,\n });\n };\n }, [appId, currentWallet, currentService]);\n\n // Re-broadcast when state changes.\n useEffect(() => {\n if (!seenHandshake.current) return;\n window.dispatchEvent(\n new MessageEvent('message', {\n data: {\n kind: 'tangle.app.accountChanged',\n account: currentWallet.address,\n },\n origin: HARNESS_ORIGIN,\n }),\n );\n }, [currentWallet.address]);\n\n useEffect(() => {\n if (!seenHandshake.current || currentWallet.chainId === null) return;\n window.dispatchEvent(\n new MessageEvent('message', {\n data: {\n kind: 'tangle.app.chainChanged',\n chainId: currentWallet.chainId,\n },\n origin: HARNESS_ORIGIN,\n }),\n );\n }, [currentWallet.chainId]);\n\n useEffect(() => {\n if (!seenHandshake.current) return;\n window.dispatchEvent(\n new MessageEvent('message', {\n data: {\n kind: 'tangle.app.serviceContext',\n blueprintId: currentService.blueprintId ?? '0',\n serviceId: currentService.serviceId,\n operators: currentService.operators,\n jobs: currentService.jobs,\n mode: currentService.mode,\n ...(currentService.chain !== null\n ? { chain: currentService.chain }\n : {}),\n },\n origin: HARNESS_ORIGIN,\n }),\n );\n }, [currentService]);\n\n const debugApi = useMemo(\n () => ({\n setWallet: setCurrentWallet,\n setService: setCurrentService,\n callLog,\n }),\n [callLog],\n );\n\n return (\n <>\n {children}\n {showDebugPanel && <DebugPanel api={debugApi} />}\n </>\n );\n};\n\n/**\n * Synthetic origin every harness instance uses. Stable across tests so the\n * iframe SDK + the harness can pin to the same string.\n */\nexport const HARNESS_ORIGIN = 'harness://tangle.local';\n\n// ── Debug panel ──────────────────────────────────────────────────────────────\n\nconst DebugPanel: FC<{\n api: {\n setWallet: (w: WalletSnapshot | ((prev: WalletSnapshot) => WalletSnapshot)) => void;\n setService: (\n s: ServiceSnapshot | ((prev: ServiceSnapshot) => ServiceSnapshot),\n ) => void;\n callLog: readonly CallJobRequest[];\n };\n}> = ({ api }) => {\n const [open, setOpen] = useState(true);\n const [tab, setTab] = useState<'wallet' | 'service' | 'log'>('wallet');\n if (!open) {\n return (\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n style={debugStyles.collapsedTrigger}\n >\n Debug\n </button>\n );\n }\n return (\n <div style={debugStyles.panel}>\n <header style={debugStyles.header}>\n <strong style={{ fontSize: 11 }}>TANGLE DEV HARNESS</strong>\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n style={debugStyles.closeButton}\n aria-label=\"Close debug panel\"\n >\n ×\n </button>\n </header>\n <nav style={debugStyles.tabs}>\n {(['wallet', 'service', 'log'] as const).map((t) => (\n <button\n key={t}\n type=\"button\"\n onClick={() => setTab(t)}\n style={{\n ...debugStyles.tab,\n ...(tab === t ? debugStyles.tabActive : {}),\n }}\n >\n {t}\n </button>\n ))}\n </nav>\n <div style={debugStyles.body}>\n {tab === 'wallet' && <WalletTab api={api} />}\n {tab === 'service' && <ServiceTab api={api} />}\n {tab === 'log' && <CallLogTab callLog={api.callLog} />}\n </div>\n </div>\n );\n};\n\nconst WalletTab: FC<{\n api: { setWallet: (w: WalletSnapshot | ((prev: WalletSnapshot) => WalletSnapshot)) => void };\n}> = ({ api }) => {\n const [address, setAddressInput] = useState(\n '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',\n );\n const [chainId, setChainIdInput] = useState('84532');\n const applyConnect = useCallback(() => {\n api.setWallet({\n address: address as Address,\n chainId: Number(chainId) || null,\n isConnected: true,\n });\n }, [address, chainId, api]);\n const disconnect = useCallback(() => {\n api.setWallet({ address: null, chainId: null, isConnected: false });\n }, [api]);\n return (\n <div>\n <label style={debugStyles.label}>address</label>\n <input\n value={address}\n onChange={(e) => setAddressInput(e.target.value)}\n style={debugStyles.input}\n />\n <label style={debugStyles.label}>chain id</label>\n <input\n value={chainId}\n onChange={(e) => setChainIdInput(e.target.value)}\n style={debugStyles.input}\n />\n <div style={debugStyles.buttonRow}>\n <button type=\"button\" onClick={applyConnect} style={debugStyles.primary}>\n Set connected\n </button>\n <button type=\"button\" onClick={disconnect} style={debugStyles.secondary}>\n Disconnect\n </button>\n </div>\n </div>\n );\n};\n\nconst ServiceTab: FC<{\n api: {\n setService: (\n s: ServiceSnapshot | ((prev: ServiceSnapshot) => ServiceSnapshot),\n ) => void;\n };\n}> = ({ api }) => {\n const [serviceId, setServiceIdInput] = useState('1');\n const [blueprintId, setBlueprintIdInput] = useState('0');\n const apply = useCallback(() => {\n api.setService((prev) => ({\n ...prev,\n serviceId: serviceId || null,\n blueprintId,\n }));\n }, [api, serviceId, blueprintId]);\n const clearService = useCallback(() => {\n api.setService((prev) => ({ ...prev, serviceId: null }));\n }, [api]);\n return (\n <div>\n <label style={debugStyles.label}>blueprint id</label>\n <input\n value={blueprintId}\n onChange={(e) => setBlueprintIdInput(e.target.value)}\n style={debugStyles.input}\n />\n <label style={debugStyles.label}>service id (empty = not deployed)</label>\n <input\n value={serviceId}\n onChange={(e) => setServiceIdInput(e.target.value)}\n style={debugStyles.input}\n />\n <div style={debugStyles.buttonRow}>\n <button type=\"button\" onClick={apply} style={debugStyles.primary}>\n Apply\n </button>\n <button type=\"button\" onClick={clearService} style={debugStyles.secondary}>\n Clear service\n </button>\n </div>\n </div>\n );\n};\n\nconst CallLogTab: FC<{ callLog: readonly CallJobRequest[] }> = ({ callLog }) => {\n if (callLog.length === 0) {\n return <p style={debugStyles.empty}>No callJob requests yet.</p>;\n }\n return (\n <ol style={debugStyles.log}>\n {callLog.map((entry) => (\n <li key={entry.correlationId} style={debugStyles.logEntry}>\n <strong>job {entry.jobIndex}</strong>\n <pre style={debugStyles.pre}>\n {JSON.stringify(entry.inputs, null, 2)}\n </pre>\n </li>\n ))}\n </ol>\n );\n};\n\n// Inline styles keep the harness style-system-agnostic — consumers may not\n// ship Tailwind, and the panel shouldn't add a dependency.\nconst debugStyles = {\n panel: {\n position: 'fixed' as const,\n right: 12,\n top: 12,\n width: 280,\n zIndex: 99999,\n background: '#0b0b14',\n color: '#fff',\n border: '1px solid #3a3a52',\n borderRadius: 10,\n boxShadow: '0 14px 32px rgba(0,0,0,0.4)',\n fontFamily:\n 'ui-monospace, SFMono-Regular, Menlo, Monaco, \"Cascadia Code\", monospace',\n fontSize: 12,\n },\n header: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '8px 10px',\n borderBottom: '1px solid #2a2a3e',\n },\n closeButton: {\n background: 'none',\n border: 'none',\n color: '#fff',\n fontSize: 18,\n cursor: 'pointer',\n lineHeight: 1,\n },\n tabs: {\n display: 'flex',\n borderBottom: '1px solid #2a2a3e',\n },\n tab: {\n flex: 1,\n background: 'none',\n border: 'none',\n color: '#a0a0c0',\n padding: '6px 8px',\n cursor: 'pointer',\n fontSize: 11,\n textTransform: 'uppercase' as const,\n },\n tabActive: {\n color: '#fff',\n borderBottom: '2px solid #818cf8',\n },\n body: {\n padding: 10,\n maxHeight: 320,\n overflow: 'auto' as const,\n },\n label: {\n display: 'block',\n color: '#a0a0c0',\n fontSize: 10,\n marginBottom: 4,\n marginTop: 6,\n textTransform: 'uppercase' as const,\n },\n input: {\n width: '100%',\n background: '#15152a',\n border: '1px solid #2a2a3e',\n color: '#fff',\n padding: '6px 8px',\n borderRadius: 4,\n fontFamily: 'inherit',\n fontSize: 11,\n boxSizing: 'border-box' as const,\n },\n buttonRow: { display: 'flex', gap: 6, marginTop: 8 },\n primary: {\n flex: 1,\n background: '#4f46e5',\n color: '#fff',\n border: 'none',\n padding: '6px 8px',\n borderRadius: 4,\n cursor: 'pointer',\n fontSize: 11,\n fontFamily: 'inherit',\n },\n secondary: {\n flex: 1,\n background: 'transparent',\n color: '#a0a0c0',\n border: '1px solid #3a3a52',\n padding: '6px 8px',\n borderRadius: 4,\n cursor: 'pointer',\n fontSize: 11,\n fontFamily: 'inherit',\n },\n collapsedTrigger: {\n position: 'fixed' as const,\n right: 12,\n top: 12,\n zIndex: 99999,\n padding: '6px 10px',\n background: '#0b0b14',\n border: '1px solid #3a3a52',\n color: '#fff',\n borderRadius: 6,\n fontFamily: 'inherit',\n fontSize: 11,\n cursor: 'pointer',\n },\n log: { listStyle: 'none', padding: 0, margin: 0 },\n logEntry: {\n padding: 6,\n borderBottom: '1px solid #2a2a3e',\n fontSize: 11,\n },\n pre: {\n margin: '4px 0 0',\n color: '#a0a0c0',\n fontSize: 10,\n whiteSpace: 'pre-wrap' as const,\n wordBreak: 'break-word' as const,\n },\n empty: { color: '#a0a0c0', fontSize: 11, margin: 0 },\n} as const;\n\nexport type { JobInputs };\n"],"mappings":"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","names":[]}
|
|
@@ -11,6 +11,10 @@ type ReadAccountRequest = {
|
|
|
11
11
|
kind: 'tangle.app.readAccount';
|
|
12
12
|
correlationId: string;
|
|
13
13
|
};
|
|
14
|
+
type RequestConnectRequest = {
|
|
15
|
+
kind: 'tangle.app.requestConnect';
|
|
16
|
+
correlationId: string;
|
|
17
|
+
};
|
|
14
18
|
type SwitchChainRequest = {
|
|
15
19
|
kind: 'tangle.app.switchChain';
|
|
16
20
|
correlationId: string;
|
|
@@ -72,6 +76,12 @@ type ReadAccountResult = {
|
|
|
72
76
|
account: Address;
|
|
73
77
|
chainId: number;
|
|
74
78
|
}>;
|
|
79
|
+
type ConnectResult = {
|
|
80
|
+
kind: 'tangle.app.connectResult';
|
|
81
|
+
} & ResultEnvelope<{
|
|
82
|
+
account: Address;
|
|
83
|
+
chainId: number;
|
|
84
|
+
}>;
|
|
75
85
|
type SwitchChainResult = {
|
|
76
86
|
kind: 'tangle.app.switchChainResult';
|
|
77
87
|
} & ResultEnvelope<{
|
|
@@ -181,8 +191,8 @@ type JobResultEvent = {
|
|
|
181
191
|
readonly eta_ms?: number;
|
|
182
192
|
};
|
|
183
193
|
};
|
|
184
|
-
type ParentMessage = HandshakeAck | ReadAccountResult | SwitchChainResult | SignMessageResult | SignTransactionResult | SignTypedDataResult | AccountChanged | ChainChanged | ServiceContextBroadcast | JobResultEvent;
|
|
185
|
-
type IframeRequest = HandshakeRequest | ReadAccountRequest | SwitchChainRequest | SignMessageRequest | SignTransactionRequest | SignTypedDataRequest | CallJobRequest;
|
|
194
|
+
type ParentMessage = HandshakeAck | ReadAccountResult | ConnectResult | SwitchChainResult | SignMessageResult | SignTransactionResult | SignTypedDataResult | AccountChanged | ChainChanged | ServiceContextBroadcast | JobResultEvent;
|
|
195
|
+
type IframeRequest = HandshakeRequest | ReadAccountRequest | RequestConnectRequest | SwitchChainRequest | SignMessageRequest | SignTransactionRequest | SignTypedDataRequest | CallJobRequest;
|
|
186
196
|
declare const NO_WALLET_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
187
197
|
/**
|
|
188
198
|
* Cryptographically-random ASCII correlation id matching the parent's
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Address, Hex } from 'viem';
|
|
2
|
-
import { h as ServiceContextOperator, g as ServiceContextJob, b as ChainContext, e as JobResultStatus, m as SignTypedDataRequest, J as JobInputs } from './parentBridgeProtocol-
|
|
2
|
+
import { h as ServiceContextOperator, g as ServiceContextJob, b as ChainContext, e as JobResultStatus, m as SignTypedDataRequest, J as JobInputs } from './parentBridgeProtocol-BSgLXg9g.js';
|
|
3
3
|
|
|
4
4
|
type WalletSnapshot = {
|
|
5
5
|
readonly address: Address | null;
|
|
@@ -72,6 +72,16 @@ declare class TangleIframeClient {
|
|
|
72
72
|
getWallet(): WalletSnapshot;
|
|
73
73
|
getService(): ServiceSnapshot;
|
|
74
74
|
subscribe<K extends keyof ClientEventMap>(event: K, listener: Listener<K>): () => void;
|
|
75
|
+
/**
|
|
76
|
+
* Ask the parent dapp to connect a wallet — opening its connect modal if
|
|
77
|
+
* none is connected. The iframe is sandboxed and cannot reach a wallet
|
|
78
|
+
* itself, so connection is always delegated to the parent. Resolves with the
|
|
79
|
+
* connected address (or `null` if the user dismissed without connecting).
|
|
80
|
+
*
|
|
81
|
+
* Uses a long timeout (the user is interacting with a modal). Already-
|
|
82
|
+
* connected parents resolve immediately.
|
|
83
|
+
*/
|
|
84
|
+
connect(): Promise<Address | null>;
|
|
75
85
|
signMessage(message: string): Promise<Hex>;
|
|
76
86
|
sendTransaction(tx: {
|
|
77
87
|
to: Address;
|
package/dist/wallet/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { T as TANGLE_CLOUD_ORIGINS_DEFAULT, d as detectTangleCloudParentOrigin } from '../detectParentOrigin-BYruoIdc.js';
|
|
2
2
|
import * as wagmi from 'wagmi';
|
|
3
3
|
import { Address } from 'viem';
|
|
4
|
-
export { A as AccountChanged, C as CallJobRequest, a as ChainChanged, b as ChainContext, H as HandshakeAck, c as HandshakeRequest, I as IframeRequest, J as JobInputs, d as JobResultEvent, e as JobResultStatus, N as NO_WALLET_ADDRESS, P as ParentMessage, R as ReadAccountRequest, f as ReadAccountResult, S as ServiceContextBroadcast, g as ServiceContextJob, h as ServiceContextOperator, i as SignMessageRequest, j as SignMessageResult, k as SignTransactionRequest, l as SignTransactionResult, m as SignTypedDataRequest, n as SignTypedDataResult, o as SwitchChainRequest, p as SwitchChainResult, T as TANGLE_IFRAME_PROTOCOL_PREFIX, q as TANGLE_IFRAME_PROTOCOL_VERSION, r as makeCorrelationId } from '../parentBridgeProtocol-
|
|
4
|
+
export { A as AccountChanged, C as CallJobRequest, a as ChainChanged, b as ChainContext, H as HandshakeAck, c as HandshakeRequest, I as IframeRequest, J as JobInputs, d as JobResultEvent, e as JobResultStatus, N as NO_WALLET_ADDRESS, P as ParentMessage, R as ReadAccountRequest, f as ReadAccountResult, S as ServiceContextBroadcast, g as ServiceContextJob, h as ServiceContextOperator, i as SignMessageRequest, j as SignMessageResult, k as SignTransactionRequest, l as SignTransactionResult, m as SignTypedDataRequest, n as SignTypedDataResult, o as SwitchChainRequest, p as SwitchChainResult, T as TANGLE_IFRAME_PROTOCOL_PREFIX, q as TANGLE_IFRAME_PROTOCOL_VERSION, r as makeCorrelationId } from '../parentBridgeProtocol-BSgLXg9g.js';
|
|
5
5
|
|
|
6
6
|
type EventName = 'accountsChanged' | 'chainChanged' | 'connect' | 'disconnect' | 'message';
|
|
7
7
|
type Listener = (...args: unknown[]) => void;
|
|
@@ -62,6 +62,11 @@ declare class ParentBridgeProvider {
|
|
|
62
62
|
private postToParent;
|
|
63
63
|
private handleParentMessage;
|
|
64
64
|
private sendReadAccount;
|
|
65
|
+
/**
|
|
66
|
+
* Ask the parent to connect a wallet (opening its modal if needed) and wait
|
|
67
|
+
* for the result. Long timeout — the user is interacting with the modal.
|
|
68
|
+
*/
|
|
69
|
+
private requestConnect;
|
|
65
70
|
private requestSignMessage;
|
|
66
71
|
private requestSignTransaction;
|
|
67
72
|
private requestSwitchChain;
|
package/dist/wallet/index.js
CHANGED
|
@@ -5,13 +5,14 @@ import {
|
|
|
5
5
|
TANGLE_IFRAME_PROTOCOL_VERSION,
|
|
6
6
|
detectTangleCloudParentOrigin,
|
|
7
7
|
makeCorrelationId
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-TM5ROMDV.js";
|
|
9
9
|
|
|
10
10
|
// src/wallet/parentBridgeConnector.ts
|
|
11
11
|
import { createConnector } from "wagmi";
|
|
12
12
|
|
|
13
13
|
// src/wallet/parentBridgeProvider.ts
|
|
14
14
|
var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
|
|
15
|
+
var CONNECT_REQUEST_TIMEOUT_MS = 3e5;
|
|
15
16
|
function isRunningInIframe() {
|
|
16
17
|
if (typeof window === "undefined") return false;
|
|
17
18
|
try {
|
|
@@ -64,11 +65,16 @@ var ParentBridgeProvider = class {
|
|
|
64
65
|
await this.ensureBootstrapped();
|
|
65
66
|
return this.cachedChainId !== null ? `0x${this.cachedChainId.toString(16)}` : "0x0";
|
|
66
67
|
}
|
|
67
|
-
case "eth_accounts":
|
|
68
|
-
case "eth_requestAccounts": {
|
|
68
|
+
case "eth_accounts": {
|
|
69
69
|
await this.ensureBootstrapped();
|
|
70
70
|
return this.cachedAccount !== null ? [this.cachedAccount] : [];
|
|
71
71
|
}
|
|
72
|
+
case "eth_requestAccounts": {
|
|
73
|
+
await this.ensureBootstrapped();
|
|
74
|
+
if (this.cachedAccount !== null) return [this.cachedAccount];
|
|
75
|
+
const account = await this.requestConnect();
|
|
76
|
+
return account !== null ? [account] : [];
|
|
77
|
+
}
|
|
72
78
|
case "personal_sign": {
|
|
73
79
|
const [message, _signer] = params;
|
|
74
80
|
return this.requestSignMessage(message);
|
|
@@ -135,6 +141,7 @@ var ParentBridgeProvider = class {
|
|
|
135
141
|
});
|
|
136
142
|
return;
|
|
137
143
|
case "tangle.app.readAccountResult":
|
|
144
|
+
case "tangle.app.connectResult":
|
|
138
145
|
this.resolvePending(message);
|
|
139
146
|
if (message.ok) {
|
|
140
147
|
this.updateAccount(
|
|
@@ -165,6 +172,20 @@ var ParentBridgeProvider = class {
|
|
|
165
172
|
expectedKind: "tangle.app.readAccountResult"
|
|
166
173
|
});
|
|
167
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Ask the parent to connect a wallet (opening its modal if needed) and wait
|
|
177
|
+
* for the result. Long timeout — the user is interacting with the modal.
|
|
178
|
+
*/
|
|
179
|
+
requestConnect() {
|
|
180
|
+
return this.dispatch({
|
|
181
|
+
kind: "tangle.app.requestConnect",
|
|
182
|
+
expectedKind: "tangle.app.connectResult",
|
|
183
|
+
timeoutMs: CONNECT_REQUEST_TIMEOUT_MS
|
|
184
|
+
}).then((data) => {
|
|
185
|
+
const { account } = data;
|
|
186
|
+
return account === NO_WALLET_ADDRESS ? null : account;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
168
189
|
requestSignMessage(message) {
|
|
169
190
|
const chainId = this.cachedChainId ?? 1;
|
|
170
191
|
return this.dispatch({
|
|
@@ -197,7 +218,7 @@ var ParentBridgeProvider = class {
|
|
|
197
218
|
async dispatch(req) {
|
|
198
219
|
await this.ensureBootstrapped();
|
|
199
220
|
const correlationId = makeCorrelationId(req.kind);
|
|
200
|
-
const timeout = this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
221
|
+
const timeout = req.timeoutMs ?? this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
201
222
|
return new Promise((resolve, reject) => {
|
|
202
223
|
const timer = window.setTimeout(() => {
|
|
203
224
|
this.pending.delete(correlationId);
|
package/dist/wallet/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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":[]}
|
|
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 ConnectResult,\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// Connecting is gated on the user interacting with the parent's modal — allow\n// a generous window rather than the standard per-request timeout.\nconst CONNECT_REQUEST_TIMEOUT_MS = 300_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 // Passive read — never opens a modal.\n await this.ensureBootstrapped();\n return this.cachedAccount !== null ? [this.cachedAccount] : [];\n }\n case 'eth_requestAccounts': {\n // Active connect (the wallet \"Connect\" button). If the parent has no\n // wallet yet, delegate to it to open its connect modal and wait.\n await this.ensureBootstrapped();\n if (this.cachedAccount !== null) return [this.cachedAccount];\n const account = await this.requestConnect();\n return account !== null ? [account] : [];\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 case 'tangle.app.connectResult':\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 /**\n * Ask the parent to connect a wallet (opening its modal if needed) and wait\n * for the result. Long timeout — the user is interacting with the modal.\n */\n private requestConnect(): Promise<Address | null> {\n return this.dispatch({\n kind: 'tangle.app.requestConnect',\n expectedKind: 'tangle.app.connectResult',\n timeoutMs: CONNECT_REQUEST_TIMEOUT_MS,\n }).then((data) => {\n const { account } = data as { account: Address; chainId: number };\n return account === NO_WALLET_ADDRESS ? null : account;\n });\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:\n | 'tangle.app.readAccount'\n | 'tangle.app.switchChain'\n | 'tangle.app.signMessage'\n | 'tangle.app.signTransaction'\n | 'tangle.app.requestConnect';\n expectedKind: ParentMessage['kind'];\n payload?: Record<string, unknown>;\n timeoutMs?: number;\n }): Promise<unknown> {\n await this.ensureBootstrapped();\n const correlationId = makeCorrelationId(req.kind);\n const timeout = req.timeoutMs ?? 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 | ConnectResult\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":";;;;;;;;;;AAYA,SAAS,uBAAuB;;;ACyChC,IAAM,6BAA6B;AAGnC,IAAM,6BAA6B;AAU5B,SAAS,oBAA6B;AAC3C,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,WAAO,OAAO,WAAW,UAAa,OAAO,WAAW;AAAA,EAC1D,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAMO,IAAM,uBAAN,MAA2B;AAAA,EAShC,YAA6B,SAA8B;AAA9B;AAAA,EAA+B;AAAA,EAA/B;AAAA,EARrB,YAAY,oBAAI,IAA8B;AAAA,EAC9C,UAAU,oBAAI,IAAqC;AAAA,EACnD,gBAAgC;AAAA,EAChC,gBAA+B;AAAA,EAC/B,iBAAiB;AAAA,EACjB,mBAAsC,CAAC;AAAA,EACvC,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA,EAQpB,UAAgB;AACd,QAAI,KAAK,aAAa,OAAO,WAAW,YAAa;AACrD,SAAK,YAAY;AACjB,WAAO,iBAAiB,WAAW,KAAK,mBAAmB;AAC3D,SAAK,aAAa;AAAA,MAChB,MAAM;AAAA,MACN,OAAO,KAAK,QAAQ;AAAA,MACpB,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AAAA,EAEA,YAAkB;AAChB,QAAI,CAAC,KAAK,aAAa,OAAO,WAAW,YAAa;AACtD,SAAK,YAAY;AACjB,WAAO,oBAAoB,WAAW,KAAK,mBAAmB;AAE9D,eAAW,CAAC,EAAE,OAAO,KAAK,KAAK,SAAS;AACtC,cAAQ,OAAO,IAAI,MAAM,2BAA2B,CAAC;AAAA,IACvD;AACA,SAAK,QAAQ,MAAM;AAAA,EACrB;AAAA;AAAA,EAIA,MAAM,QAAQ,KAA+D;AAC3E,UAAM,SAAS,IAAI;AACnB,UAAM,SAAU,IAAI,UAAU,CAAC;AAC/B,YAAQ,QAAQ;AAAA,MACd,KAAK,eAAe;AAClB,cAAM,KAAK,mBAAmB;AAC9B,eAAO,KAAK,kBAAkB,OAAO,KAAK,KAAK,cAAc,SAAS,EAAE,CAAC,KAAK;AAAA,MAChF;AAAA,MACA,KAAK,gBAAgB;AAEnB,cAAM,KAAK,mBAAmB;AAC9B,eAAO,KAAK,kBAAkB,OAAO,CAAC,KAAK,aAAa,IAAI,CAAC;AAAA,MAC/D;AAAA,MACA,KAAK,uBAAuB;AAG1B,cAAM,KAAK,mBAAmB;AAC9B,YAAI,KAAK,kBAAkB,KAAM,QAAO,CAAC,KAAK,aAAa;AAC3D,cAAM,UAAU,MAAM,KAAK,eAAe;AAC1C,eAAO,YAAY,OAAO,CAAC,OAAO,IAAI,CAAC;AAAA,MACzC;AAAA,MACA,KAAK,iBAAiB;AACpB,cAAM,CAAC,SAAS,OAAO,IAAI;AAC3B,eAAO,KAAK,mBAAmB,OAAO;AAAA,MACxC;AAAA,MACA,KAAK,wBAAwB;AAI3B,cAAM;AAAA,UACJ;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,MACA,KAAK,uBAAuB;AAC1B,cAAM,CAAC,EAAE,IAAI;AAGb,YAAI,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM;AACvB,gBAAM,YAAY,QAAQ,+CAA+C;AAAA,QAC3E;AACA,eAAO,KAAK,uBAAuB,EAAE;AAAA,MACvC;AAAA,MACA,KAAK,8BAA8B;AACjC,cAAM,CAAC,EAAE,QAAQ,CAAC,IAAI;AACtB,cAAM,UAAU,OAAO,SAAS,SAAS,EAAE;AAC3C,YAAI,CAAC,OAAO,SAAS,OAAO,KAAK,WAAW,GAAG;AAC7C,gBAAM,YAAY,QAAQ,oBAAoB,OAAO,EAAE;AAAA,QACzD;AACA,cAAM,KAAK,mBAAmB,OAAO;AACrC,eAAO;AAAA,MACT;AAAA,MACA,KAAK,2BAA2B;AAG9B,cAAM;AAAA,UACJ;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,MACA;AACE,cAAM,YAAY,MAAM,UAAU,MAAM,kCAAkC;AAAA,IAC9E;AAAA,EACF;AAAA,EAEA,GAAG,OAAkB,UAA0B;AAC7C,UAAM,MAAM,KAAK,UAAU,IAAI,KAAK,KAAK,oBAAI,IAAI;AACjD,QAAI,IAAI,QAAQ;AAChB,SAAK,UAAU,IAAI,OAAO,GAAG;AAAA,EAC/B;AAAA,EAEA,eAAe,OAAkB,UAA0B;AACzD,SAAK,UAAU,IAAI,KAAK,GAAG,OAAO,QAAQ;AAAA,EAC5C;AAAA;AAAA,EAIQ,aAAa,SAAuB;AAC1C,QAAI,OAAO,WAAW,YAAa;AACnC,QAAI;AACF,aAAO,OAAO,YAAY,SAAS,KAAK,QAAQ,YAAY;AAAA,IAC9D,QAAQ;AAAA,IAGR;AAAA,EACF;AAAA,EAEQ,sBAAsB,CAAC,UAA8B;AAE3D,QAAI,MAAM,WAAW,KAAK,QAAQ,aAAc;AAChD,UAAM,OAAO,MAAM;AACnB,QAAI,OAAO,SAAS,YAAY,SAAS,KAAM;AAC/C,UAAM,UAAU;AAChB,YAAQ,QAAQ,MAAM;AAAA,MACpB,KAAK;AACH,aAAK,iBAAiB;AACtB,mBAAW,WAAW,KAAK,iBAAkB,SAAQ;AACrD,aAAK,mBAAmB,CAAC;AAIzB,aAAK,gBAAgB,EAAE,MAAM,MAAM;AAAA,QAInC,CAAC;AACD;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,aAAK,eAAe,OAAO;AAC3B,YAAI,QAAQ,IAAI;AACd,eAAK;AAAA,YACH,QAAQ,KAAK,YAAY,oBACrB,OACA,QAAQ,KAAK;AAAA,UACnB;AACA,eAAK,cAAc,QAAQ,KAAK,OAAO;AAAA,QACzC;AACA;AAAA,MACF,KAAK;AACH,aAAK,eAAe,OAAO;AAC3B,YAAI,QAAQ,GAAI,MAAK,cAAc,QAAQ,KAAK,OAAO;AACvD;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,aAAK,eAAe,OAAO;AAC3B;AAAA,MACF,KAAK;AACH,aAAK,cAAc,QAAQ,OAAO;AAClC;AAAA,MACF,KAAK;AACH,aAAK,cAAc,QAAQ,OAAO;AAClC;AAAA,IACJ;AAAA,EACF;AAAA,EAEQ,kBAAkE;AACxE,WAAO,KAAK,SAAS;AAAA,MACnB,MAAM;AAAA,MACN,cAAc;AAAA,IAChB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,iBAA0C;AAChD,WAAO,KAAK,SAAS;AAAA,MACnB,MAAM;AAAA,MACN,cAAc;AAAA,MACd,WAAW;AAAA,IACb,CAAC,EAAE,KAAK,CAAC,SAAS;AAChB,YAAM,EAAE,QAAQ,IAAI;AACpB,aAAO,YAAY,oBAAoB,OAAO;AAAA,IAChD,CAAC;AAAA,EACH;AAAA,EAEQ,mBAAmB,SAA+B;AACxD,UAAM,UAAU,KAAK,iBAAiB;AACtC,WAAO,KAAK,SAAS;AAAA,MACnB,MAAM;AAAA,MACN,cAAc;AAAA,MACd,SAAS,EAAE,SAAS,QAAQ;AAAA,IAC9B,CAAC,EAAE,KAAK,CAAC,SAAU,KAA4B,SAAS;AAAA,EAC1D;AAAA,EAEQ,uBAAuB,IAId;AACf,UAAM,UAAU,KAAK,iBAAiB;AACtC,UAAM,QACJ,OAAO,GAAG,UAAU,YAAY,GAAG,MAAM,WAAW,IAAI,IACpD,OAAO,GAAG,KAAK,EAAE,SAAS,EAAE,IAC5B,OAAO,GAAG,UAAU,WAClB,GAAG,QACH;AACR,WAAO,KAAK,SAAS;AAAA,MACnB,MAAM;AAAA,MACN,cAAc;AAAA,MACd,SAAS;AAAA,QACP;AAAA,QACA,IAAI,GAAG;AAAA,QACP,MAAM,GAAG;AAAA,QACT,GAAI,UAAU,SAAY,EAAE,MAAM,IAAI,CAAC;AAAA,MACzC;AAAA,IACF,CAAC,EAAE,KAAK,CAAC,SAAU,KAAyB,MAAM;AAAA,EACpD;AAAA,EAEQ,mBAAmB,SAAkC;AAC3D,WAAO,KAAK,SAAS;AAAA,MACnB,MAAM;AAAA,MACN,cAAc;AAAA,MACd,SAAS,EAAE,QAAQ;AAAA,IACrB,CAAC,EAAE,KAAK,CAAC,SAAU,KAA6B,OAAO;AAAA,EACzD;AAAA,EAEA,MAAc,SAAS,KAUF;AACnB,UAAM,KAAK,mBAAmB;AAC9B,UAAM,gBAAgB,kBAAkB,IAAI,IAAI;AAChD,UAAM,UAAU,IAAI,aAAa,KAAK,QAAQ,oBAAoB;AAClE,WAAO,IAAI,QAAiB,CAAC,SAAS,WAAW;AAC/C,YAAM,QAAQ,OAAO,WAAW,MAAM;AACpC,aAAK,QAAQ,OAAO,aAAa;AACjC,eAAO,YAAY,MAAM,6BAA6B,IAAI,IAAI,WAAW,OAAO,IAAI,CAAC;AAAA,MACvF,GAAG,OAAO;AACV,WAAK,QAAQ,IAAI,eAAe;AAAA,QAC9B,SAAS,CAAC,MAAM;AACd,iBAAO,aAAa,KAAK;AACzB,kBAAQ,CAAC;AAAA,QACX;AAAA,QACA,QAAQ,CAAC,MAAM;AACb,iBAAO,aAAa,KAAK;AACzB,iBAAO,CAAC;AAAA,QACV;AAAA,QACA,cAAc,IAAI;AAAA,MACpB,CAAC;AACD,WAAK,aAAa;AAAA,QAChB,MAAM,IAAI;AAAA,QACV;AAAA,QACA,GAAI,IAAI,WAAW,CAAC;AAAA,MACtB,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,eACN,SAMM;AACN,UAAM,QAAQ,KAAK,QAAQ,IAAI,QAAQ,aAAa;AACpD,QAAI,CAAC,MAAO;AACZ,SAAK,QAAQ,OAAO,QAAQ,aAAa;AACzC,QAAI,MAAM,iBAAiB,QAAQ,MAAM;AACvC,YAAM;AAAA,QACJ;AAAA,UACE;AAAA,UACA,uBAAuB,QAAQ,IAAI,QAAQ,MAAM,YAAY;AAAA,QAC/D;AAAA,MACF;AACA;AAAA,IACF;AACA,QAAI,QAAQ,IAAI;AACd,YAAM,QAAQ,QAAQ,IAAI;AAAA,IAC5B,OAAO;AACL,YAAM,OAAO,YAAY,MAAM,QAAQ,KAAK,CAAC;AAAA,IAC/C;AAAA,EACF;AAAA,EAEA,MAAc,qBAAoC;AAChD,QAAI,KAAK,eAAgB;AACzB,SAAK,QAAQ;AACb,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,WAAK,iBAAiB,KAAK,OAAO;AAGlC,YAAM,QAAQ,OAAO,YAAY,MAAM;AACrC,YAAI,KAAK,gBAAgB;AACvB,iBAAO,cAAc,KAAK;AAC1B;AAAA,QACF;AACA,aAAK,aAAa;AAAA,UAChB,MAAM;AAAA,UACN,OAAO,KAAK,QAAQ;AAAA,UACpB,SAAS;AAAA,QACX,CAAC;AAAA,MACH,GAAG,GAAG;AAEN,aAAO,WAAW,MAAM,OAAO,cAAc,KAAK,GAAG,GAAM;AAAA,IAC7D,CAAC;AAAA,EACH;AAAA,EAEQ,cAAc,MAA4B;AAChD,QAAI,KAAK,kBAAkB,KAAM;AACjC,UAAM,OAAO,KAAK;AAClB,SAAK,gBAAgB;AACrB,QAAI,SAAS,QAAQ,SAAS,MAAM;AAClC,WAAK,KAAK,YAAY;AACtB,WAAK,KAAK,mBAAmB,CAAC,CAAC;AAAA,IACjC,WAAW,SAAS,MAAM;AACxB,WAAK,KAAK,mBAAmB,CAAC,IAAI,CAAC;AACnC,UAAI,SAAS,MAAM;AACjB,aAAK,KAAK,WAAW,EAAE,SAAS,KAAK,iBAAiB,EAAE,CAAC;AAAA,MAC3D;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,cAAc,MAAoB;AACxC,QAAI,KAAK,kBAAkB,KAAM;AACjC,SAAK,gBAAgB;AACrB,SAAK,KAAK,gBAAgB,KAAK,KAAK,SAAS,EAAE,CAAC,EAAE;AAAA,EACpD;AAAA,EAEQ,KAAK,UAAqB,MAAuB;AACvD,UAAM,MAAM,KAAK,UAAU,IAAI,KAAK;AACpC,QAAI,CAAC,IAAK;AACV,eAAW,YAAY,CAAC,GAAG,GAAG,GAAG;AAC/B,UAAI;AACF,iBAAS,GAAG,IAAI;AAAA,MAClB,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA,EAKA,mBAAmC;AACjC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAEA,mBAAkC;AAChC,WAAO,KAAK;AAAA,EACd;AACF;AAEA,SAAS,YAAY,MAAc,SAAwB;AACzD,QAAM,MAAM,IAAI,MAAM,OAAO;AAC7B,MAAI,OAAO;AACX,SAAO;AACT;;;AD3bO,SAAS,sBAAsB,SAAuC;AAC3E,MAAI;AACJ,MAAI,YAAY;AAEhB,SAAO,gBAAsC,CAAC,WAAW;AACvD,UAAM,iBAAiB,MAA4B;AACjD,UAAI,CAAC,SAAU,YAAW,IAAI,qBAAqB,OAAO;AAC1D,UAAI,CAAC,WAAW;AACd,iBAAS,QAAQ;AACjB,oBAAY;AAIZ,iBAAS,GAAG,mBAAmB,CAAC,aAAa;AAC3C,iBAAO,QAAQ,KAAK,UAAU;AAAA,YAC5B,UAAU,MAAM,QAAQ,QAAQ,IAC3B,WACA,CAAC;AAAA,UACR,CAAC;AAAA,QACH,CAAC;AACD,iBAAS,GAAG,gBAAgB,CAAC,eAAe;AAC1C,gBAAM,UACJ,OAAO,eAAe,WAClB,OAAO,SAAS,YAAY,EAAE,IAC9B,OAAO,UAAU;AACvB,cAAI,OAAO,SAAS,OAAO,GAAG;AAC5B,mBAAO,QAAQ,KAAK,UAAU,EAAE,QAAQ,CAAC;AAAA,UAC3C;AAAA,QACF,CAAC;AACD,iBAAS,GAAG,cAAc,MAAM;AAC9B,iBAAO,QAAQ,KAAK,YAAY;AAAA,QAClC,CAAC;AAAA,MACH;AACA,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,MAAM;AAAA;AAAA,MAGN,MAAM,UAAwB;AAI5B,cAAM,IAAI,eAAe;AACzB,cAAM,iBAAkB,MAAM,EAAE,QAAQ;AAAA,UACtC,QAAQ;AAAA,QACV,CAAC;AACD,cAAM,aAAc,MAAM,EAAE,QAAQ,EAAE,QAAQ,cAAc,CAAC;AAC7D,cAAM,UAAU,OAAO,SAAS,YAAY,EAAE;AAC9C,eAAO;AAAA,UACL,UAAU;AAAA,UACV,SAAS,OAAO,SAAS,OAAO,IAAI,UAAU;AAAA,QAChD;AAAA,MACF;AAAA,MAEA,MAAM,aAAa;AAMjB,YAAI,SAAU,UAAS,UAAU;AACjC,oBAAY;AACZ,mBAAW;AAAA,MACb;AAAA,MAEA,MAAM,cAAc;AAClB,cAAM,IAAI,eAAe;AACzB,cAAM,SAAS,EAAE,iBAAiB;AAClC,YAAI,OAAQ,QAAO,CAAC,MAAM;AAC1B,cAAM,WAAY,MAAM,EAAE,QAAQ;AAAA,UAChC,QAAQ;AAAA,QACV,CAAC;AACD,eAAO;AAAA,MACT;AAAA,MAEA,MAAM,aAAa;AACjB,cAAM,IAAI,eAAe;AACzB,cAAM,SAAS,EAAE,iBAAiB;AAClC,YAAI,WAAW,KAAM,QAAO;AAC5B,cAAM,aAAc,MAAM,EAAE,QAAQ,EAAE,QAAQ,cAAc,CAAC;AAC7D,cAAM,UAAU,OAAO,SAAS,YAAY,EAAE;AAC9C,eAAO,OAAO,SAAS,OAAO,IAAI,UAAU;AAAA,MAC9C;AAAA,MAEA,MAAM,cAAc;AAClB,eAAO,eAAe;AAAA,MACxB;AAAA,MAEA,MAAM,eAAe;AAKnB,YAAI;AACF,gBAAM,IAAI,eAAe;AACzB,gBAAM,WAAY,MAAM,EAAE,QAAQ;AAAA,YAChC,QAAQ;AAAA,UACV,CAAC;AACD,iBAAO,SAAS,SAAS;AAAA,QAC3B,QAAQ;AACN,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,MAEA,MAAM,YAAY,EAAE,QAAQ,GAAmB;AAC7C,cAAM,IAAI,eAAe;AACzB,cAAM,EAAE,QAAQ;AAAA,UACd,QAAQ;AAAA,UACR,QAAQ,CAAC,EAAE,SAAS,KAAK,QAAQ,SAAS,EAAE,CAAC,GAAG,CAAC;AAAA,QACnD,CAAC;AACD,cAAM,QAAQ,OAAO,OAAO,KAAK,CAAC,MAAM,EAAE,OAAO,OAAO;AACxD,YAAI,CAAC,OAAO;AACV,gBAAM,IAAI,MAAM,SAAS,OAAO,8BAA8B;AAAA,QAChE;AACA,eAAO;AAAA,MACT;AAAA,MAEA,kBAAkB,UAAU;AAC1B,eAAO,QAAQ,KAAK,UAAU;AAAA,UAC5B;AAAA,QACF,CAAC;AAAA,MACH;AAAA,MACA,eAAe,YAAY;AACzB,cAAM,UAAU,OAAO,SAAS,YAAY,EAAE;AAC9C,YAAI,OAAO,SAAS,OAAO,GAAG;AAC5B,iBAAO,QAAQ,KAAK,UAAU,EAAE,QAAQ,CAAC;AAAA,QAC3C;AAAA,MACF;AAAA,MACA,eAAe;AACb,eAAO,QAAQ,KAAK,YAAY;AAAA,MAClC;AAAA,IACF;AAAA,EACF,CAAC;AACH;","names":[]}
|
package/package.json
CHANGED
package/src/iframe/hooks.ts
CHANGED
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
* dapp's wagmi config + ConnectKit modal.
|
|
30
30
|
*/
|
|
31
31
|
export function useTangleWallet(): WalletSnapshot & {
|
|
32
|
+
connect: () => Promise<Address | null>;
|
|
32
33
|
signMessage: (message: string) => Promise<Hex>;
|
|
33
34
|
sendTransaction: (tx: {
|
|
34
35
|
to: Address;
|
|
@@ -44,6 +45,10 @@ export function useTangleWallet(): WalletSnapshot & {
|
|
|
44
45
|
switchChain: (chainId: number) => Promise<number>;
|
|
45
46
|
} {
|
|
46
47
|
const { client, wallet } = useTangleIframeContext();
|
|
48
|
+
const connect = useCallback(() => {
|
|
49
|
+
if (!client) throw new Error('Wallet not available in dev mode.');
|
|
50
|
+
return client.connect();
|
|
51
|
+
}, [client]);
|
|
47
52
|
const signMessage = useCallback(
|
|
48
53
|
(message: string) => {
|
|
49
54
|
if (!client) throw new Error('Wallet not available in dev mode.');
|
|
@@ -79,6 +84,7 @@ export function useTangleWallet(): WalletSnapshot & {
|
|
|
79
84
|
);
|
|
80
85
|
return {
|
|
81
86
|
...wallet,
|
|
87
|
+
connect,
|
|
82
88
|
signMessage,
|
|
83
89
|
sendTransaction,
|
|
84
90
|
signTypedData,
|
|
@@ -77,6 +77,43 @@ describe('TangleIframeClient', () => {
|
|
|
77
77
|
]);
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
it('connect() asks the parent to connect and resolves with the address', async () => {
|
|
81
|
+
client.install();
|
|
82
|
+
fake.sendFromParent({
|
|
83
|
+
kind: 'tangle.app.handshakeAck',
|
|
84
|
+
appId: 'test-app',
|
|
85
|
+
protocolVersion: '1',
|
|
86
|
+
});
|
|
87
|
+
const promise = client.connect();
|
|
88
|
+
await vi.waitFor(() =>
|
|
89
|
+
expect(
|
|
90
|
+
fake.captured.some(
|
|
91
|
+
(m) => (m as { kind?: string }).kind === 'tangle.app.requestConnect',
|
|
92
|
+
),
|
|
93
|
+
).toBe(true),
|
|
94
|
+
);
|
|
95
|
+
const req = fake.captured.find(
|
|
96
|
+
(m) => (m as { kind?: string }).kind === 'tangle.app.requestConnect',
|
|
97
|
+
) as { correlationId: string };
|
|
98
|
+
fake.sendFromParent({
|
|
99
|
+
kind: 'tangle.app.connectResult',
|
|
100
|
+
correlationId: req.correlationId,
|
|
101
|
+
ok: true,
|
|
102
|
+
data: {
|
|
103
|
+
account: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
|
|
104
|
+
chainId: 84532,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
await expect(promise).resolves.toBe(
|
|
108
|
+
'0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
|
|
109
|
+
);
|
|
110
|
+
expect(client.getWallet()).toMatchObject({
|
|
111
|
+
address: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
|
|
112
|
+
chainId: 84532,
|
|
113
|
+
isConnected: true,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
80
117
|
it('emits a service snapshot when the parent broadcasts serviceContext', () => {
|
|
81
118
|
client.install();
|
|
82
119
|
const seen: unknown[] = [];
|
|
@@ -85,6 +85,9 @@ export type TangleIframeClientOptions = {
|
|
|
85
85
|
};
|
|
86
86
|
|
|
87
87
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
88
|
+
// Connecting is gated on the user picking + approving a wallet in the parent's
|
|
89
|
+
// modal — give it a generous window rather than the standard request timeout.
|
|
90
|
+
const CONNECT_REQUEST_TIMEOUT_MS = 300_000;
|
|
88
91
|
const HANDSHAKE_RETRY_MS = 250;
|
|
89
92
|
const HANDSHAKE_RETRY_BUDGET_MS = 10_000;
|
|
90
93
|
const NULL_WALLET: WalletSnapshot = {
|
|
@@ -184,6 +187,28 @@ export class TangleIframeClient {
|
|
|
184
187
|
|
|
185
188
|
// ── Wallet operations ───────────────────────────────────────────────────
|
|
186
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Ask the parent dapp to connect a wallet — opening its connect modal if
|
|
192
|
+
* none is connected. The iframe is sandboxed and cannot reach a wallet
|
|
193
|
+
* itself, so connection is always delegated to the parent. Resolves with the
|
|
194
|
+
* connected address (or `null` if the user dismissed without connecting).
|
|
195
|
+
*
|
|
196
|
+
* Uses a long timeout (the user is interacting with a modal). Already-
|
|
197
|
+
* connected parents resolve immediately.
|
|
198
|
+
*/
|
|
199
|
+
async connect(): Promise<Address | null> {
|
|
200
|
+
await this.ensureBootstrapped();
|
|
201
|
+
const data = await this.dispatchWallet(
|
|
202
|
+
'tangle.app.requestConnect',
|
|
203
|
+
{},
|
|
204
|
+
CONNECT_REQUEST_TIMEOUT_MS,
|
|
205
|
+
);
|
|
206
|
+
const { account, chainId } = data as { account: Address; chainId: number };
|
|
207
|
+
const address = account === NO_WALLET_ADDRESS ? null : account;
|
|
208
|
+
this.updateWallet({ address, chainId, isConnected: address !== null });
|
|
209
|
+
return address;
|
|
210
|
+
}
|
|
211
|
+
|
|
187
212
|
async signMessage(message: string): Promise<Hex> {
|
|
188
213
|
await this.ensureBootstrapped();
|
|
189
214
|
return this.dispatchWallet('tangle.app.signMessage', {
|
|
@@ -371,19 +396,21 @@ export class TangleIframeClient {
|
|
|
371
396
|
| 'tangle.app.signMessage'
|
|
372
397
|
| 'tangle.app.signTransaction'
|
|
373
398
|
| 'tangle.app.signTypedData'
|
|
374
|
-
| 'tangle.app.switchChain'
|
|
399
|
+
| 'tangle.app.switchChain'
|
|
400
|
+
| 'tangle.app.requestConnect',
|
|
375
401
|
payload: Record<string, unknown>,
|
|
402
|
+
timeoutMs?: number,
|
|
376
403
|
): Promise<unknown> {
|
|
377
404
|
return new Promise((resolve, reject) => {
|
|
378
405
|
const correlationId = makeCorrelationId(kind);
|
|
379
|
-
const timeout =
|
|
380
|
-
this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
406
|
+
const timeout = timeoutMs ?? this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
381
407
|
const expectedKind = (
|
|
382
408
|
{
|
|
383
409
|
'tangle.app.signMessage': 'tangle.app.signMessageResult',
|
|
384
410
|
'tangle.app.signTransaction': 'tangle.app.signTransactionResult',
|
|
385
411
|
'tangle.app.signTypedData': 'tangle.app.signTypedDataResult',
|
|
386
412
|
'tangle.app.switchChain': 'tangle.app.switchChainResult',
|
|
413
|
+
'tangle.app.requestConnect': 'tangle.app.connectResult',
|
|
387
414
|
} as const
|
|
388
415
|
)[kind];
|
|
389
416
|
const timer = setTimeout(() => {
|
package/src/iframe/testing.tsx
CHANGED
|
@@ -236,6 +236,25 @@ export const TangleParentHarness: FC<HarnessProps> = ({
|
|
|
236
236
|
});
|
|
237
237
|
return;
|
|
238
238
|
}
|
|
239
|
+
case 'tangle.app.requestConnect': {
|
|
240
|
+
// The harness "connects" immediately to the mocked wallet — a real
|
|
241
|
+
// parent would open its connect modal and resolve once the user
|
|
242
|
+
// picks a wallet. Tests that need the disconnected path pass a
|
|
243
|
+
// wallet with a null address.
|
|
244
|
+
if (typeof message.correlationId !== 'string') return;
|
|
245
|
+
reply({
|
|
246
|
+
kind: 'tangle.app.connectResult',
|
|
247
|
+
correlationId: message.correlationId,
|
|
248
|
+
ok: true,
|
|
249
|
+
data: {
|
|
250
|
+
account:
|
|
251
|
+
currentWallet.address ??
|
|
252
|
+
('0x0000000000000000000000000000000000000000' as Address),
|
|
253
|
+
chainId: currentWallet.chainId ?? 0,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
239
258
|
case 'tangle.app.callJob': {
|
|
240
259
|
if (typeof message.correlationId !== 'string') return;
|
|
241
260
|
const request = message as unknown as CallJobRequest;
|
|
@@ -20,6 +20,15 @@ export type ReadAccountRequest = {
|
|
|
20
20
|
correlationId: string;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
// Ask the parent to ensure a wallet is connected — opening its connect modal
|
|
24
|
+
// if none is. A sandboxed iframe can't reach a wallet extension itself, so
|
|
25
|
+
// this is the *only* way an iframe can initiate a connection: it delegates to
|
|
26
|
+
// the parent, which owns the wallet. Resolves once the parent has an account.
|
|
27
|
+
export type RequestConnectRequest = {
|
|
28
|
+
kind: 'tangle.app.requestConnect';
|
|
29
|
+
correlationId: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
23
32
|
export type SwitchChainRequest = {
|
|
24
33
|
kind: 'tangle.app.switchChain';
|
|
25
34
|
correlationId: string;
|
|
@@ -90,6 +99,10 @@ export type ReadAccountResult = {
|
|
|
90
99
|
kind: 'tangle.app.readAccountResult';
|
|
91
100
|
} & ResultEnvelope<{ account: Address; chainId: number }>;
|
|
92
101
|
|
|
102
|
+
export type ConnectResult = {
|
|
103
|
+
kind: 'tangle.app.connectResult';
|
|
104
|
+
} & ResultEnvelope<{ account: Address; chainId: number }>;
|
|
105
|
+
|
|
93
106
|
export type SwitchChainResult = {
|
|
94
107
|
kind: 'tangle.app.switchChainResult';
|
|
95
108
|
} & ResultEnvelope<{ chainId: number }>;
|
|
@@ -221,6 +234,7 @@ export type JobResultEvent = {
|
|
|
221
234
|
export type ParentMessage =
|
|
222
235
|
| HandshakeAck
|
|
223
236
|
| ReadAccountResult
|
|
237
|
+
| ConnectResult
|
|
224
238
|
| SwitchChainResult
|
|
225
239
|
| SignMessageResult
|
|
226
240
|
| SignTransactionResult
|
|
@@ -233,6 +247,7 @@ export type ParentMessage =
|
|
|
233
247
|
export type IframeRequest =
|
|
234
248
|
| HandshakeRequest
|
|
235
249
|
| ReadAccountRequest
|
|
250
|
+
| RequestConnectRequest
|
|
236
251
|
| SwitchChainRequest
|
|
237
252
|
| SignMessageRequest
|
|
238
253
|
| SignTransactionRequest
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
makeCorrelationId,
|
|
15
15
|
NO_WALLET_ADDRESS,
|
|
16
16
|
TANGLE_IFRAME_PROTOCOL_VERSION,
|
|
17
|
+
type ConnectResult,
|
|
17
18
|
type ParentMessage,
|
|
18
19
|
type ReadAccountResult,
|
|
19
20
|
type SignMessageResult,
|
|
@@ -51,6 +52,9 @@ export type ParentBridgeOptions = {
|
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
55
|
+
// Connecting is gated on the user interacting with the parent's modal — allow
|
|
56
|
+
// a generous window rather than the standard per-request timeout.
|
|
57
|
+
const CONNECT_REQUEST_TIMEOUT_MS = 300_000;
|
|
54
58
|
|
|
55
59
|
/**
|
|
56
60
|
* Detect iframe execution context. When this returns `false` the bridge
|
|
@@ -121,11 +125,19 @@ export class ParentBridgeProvider {
|
|
|
121
125
|
await this.ensureBootstrapped();
|
|
122
126
|
return this.cachedChainId !== null ? `0x${this.cachedChainId.toString(16)}` : '0x0';
|
|
123
127
|
}
|
|
124
|
-
case 'eth_accounts':
|
|
125
|
-
|
|
128
|
+
case 'eth_accounts': {
|
|
129
|
+
// Passive read — never opens a modal.
|
|
126
130
|
await this.ensureBootstrapped();
|
|
127
131
|
return this.cachedAccount !== null ? [this.cachedAccount] : [];
|
|
128
132
|
}
|
|
133
|
+
case 'eth_requestAccounts': {
|
|
134
|
+
// Active connect (the wallet "Connect" button). If the parent has no
|
|
135
|
+
// wallet yet, delegate to it to open its connect modal and wait.
|
|
136
|
+
await this.ensureBootstrapped();
|
|
137
|
+
if (this.cachedAccount !== null) return [this.cachedAccount];
|
|
138
|
+
const account = await this.requestConnect();
|
|
139
|
+
return account !== null ? [account] : [];
|
|
140
|
+
}
|
|
129
141
|
case 'personal_sign': {
|
|
130
142
|
const [message, _signer] = params as [string, Address];
|
|
131
143
|
return this.requestSignMessage(message);
|
|
@@ -213,6 +225,7 @@ export class ParentBridgeProvider {
|
|
|
213
225
|
});
|
|
214
226
|
return;
|
|
215
227
|
case 'tangle.app.readAccountResult':
|
|
228
|
+
case 'tangle.app.connectResult':
|
|
216
229
|
this.resolvePending(message);
|
|
217
230
|
if (message.ok) {
|
|
218
231
|
this.updateAccount(
|
|
@@ -247,6 +260,21 @@ export class ParentBridgeProvider {
|
|
|
247
260
|
}) as Promise<{ account: Address; chainId: number }>;
|
|
248
261
|
}
|
|
249
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Ask the parent to connect a wallet (opening its modal if needed) and wait
|
|
265
|
+
* for the result. Long timeout — the user is interacting with the modal.
|
|
266
|
+
*/
|
|
267
|
+
private requestConnect(): Promise<Address | null> {
|
|
268
|
+
return this.dispatch({
|
|
269
|
+
kind: 'tangle.app.requestConnect',
|
|
270
|
+
expectedKind: 'tangle.app.connectResult',
|
|
271
|
+
timeoutMs: CONNECT_REQUEST_TIMEOUT_MS,
|
|
272
|
+
}).then((data) => {
|
|
273
|
+
const { account } = data as { account: Address; chainId: number };
|
|
274
|
+
return account === NO_WALLET_ADDRESS ? null : account;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
250
278
|
private requestSignMessage(message: string): Promise<Hex> {
|
|
251
279
|
const chainId = this.cachedChainId ?? 1;
|
|
252
280
|
return this.dispatch({
|
|
@@ -289,13 +317,19 @@ export class ParentBridgeProvider {
|
|
|
289
317
|
}
|
|
290
318
|
|
|
291
319
|
private async dispatch(req: {
|
|
292
|
-
kind:
|
|
320
|
+
kind:
|
|
321
|
+
| 'tangle.app.readAccount'
|
|
322
|
+
| 'tangle.app.switchChain'
|
|
323
|
+
| 'tangle.app.signMessage'
|
|
324
|
+
| 'tangle.app.signTransaction'
|
|
325
|
+
| 'tangle.app.requestConnect';
|
|
293
326
|
expectedKind: ParentMessage['kind'];
|
|
294
327
|
payload?: Record<string, unknown>;
|
|
328
|
+
timeoutMs?: number;
|
|
295
329
|
}): Promise<unknown> {
|
|
296
330
|
await this.ensureBootstrapped();
|
|
297
331
|
const correlationId = makeCorrelationId(req.kind);
|
|
298
|
-
const timeout = this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
332
|
+
const timeout = req.timeoutMs ?? this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
299
333
|
return new Promise<unknown>((resolve, reject) => {
|
|
300
334
|
const timer = window.setTimeout(() => {
|
|
301
335
|
this.pending.delete(correlationId);
|
|
@@ -329,6 +363,7 @@ export class ParentBridgeProvider {
|
|
|
329
363
|
private resolvePending(
|
|
330
364
|
message:
|
|
331
365
|
| ReadAccountResult
|
|
366
|
+
| ConnectResult
|
|
332
367
|
| SwitchChainResult
|
|
333
368
|
| SignMessageResult
|
|
334
369
|
| SignTransactionResult,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/wallet/detectParentOrigin.ts","../src/wallet/parentBridgeProtocol.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","// 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// EIP-712 typed-data signing for publishers that need to sign custom message\n// shapes — operator envelopes, off-chain attestations, claim proofs, etc.\n// The parent renders the typed-data fields in its approval modal so the user\n// can audit what they're signing. Iframes never see the wallet's signing key\n// or private state.\n//\n// Shape mirrors viem's `signTypedData` argument: `domain` + `types` (without\n// the EIP712Domain entry — viem injects it) + `primaryType` + `message`.\n// Validation on the parent side rejects payloads that are obviously\n// malformed (missing primaryType, types map empty, etc.) but does NOT\n// re-shape the message — the user is the one who decides whether to sign.\nexport type SignTypedDataRequest = {\n kind: 'tangle.app.signTypedData';\n correlationId: string;\n chainId: number;\n domain: Readonly<{\n name?: string;\n version?: string;\n chainId?: number;\n verifyingContract?: Address;\n salt?: Hex;\n }>;\n /** EIP-712 types map; do NOT include the EIP712Domain entry (the parent\n * injects it derived from `domain`). */\n types: Readonly<Record<string, ReadonlyArray<{ name: string; type: string }>>>;\n /** Top-level type name in `types` whose values appear in `message`. */\n primaryType: string;\n /** The actual typed-data values. Shape matches `types[primaryType]`. */\n message: Readonly<Record<string, unknown>>;\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 SignTypedDataResult = {\n kind: 'tangle.app.signTypedDataResult';\n} & ResultEnvelope<{ signature: 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\n// ─── Service context (parent → iframe) ──────────────────────────────────────\n//\n// Iframe blueprints embedded by Tangle Cloud need to know which service +\n// blueprint they're rendering for, plus which operators are quoted. The\n// parent broadcasts this on mount and on every change (mode picker swap,\n// new service activation, operator delta). The iframe just reads — it\n// doesn't query the chain itself.\n//\n// The thin-iframe SDK exposes this as `useTangleService()`. Iframes that\n// use the full wagmi connector path can still listen to `serviceContext`\n// for routing convenience.\n\nexport type ServiceContextOperator = {\n readonly address: Address;\n readonly rpcAddress: string | undefined;\n readonly status: 'active' | 'inactive' | 'unknown';\n};\n\nexport type ServiceContextJob = {\n readonly index: number;\n readonly name: string;\n readonly inputSchema?: unknown;\n};\n\n/**\n * Chain configuration the parent broadcasts to the iframe along with\n * service context. Iframes use this to build a `viem` public client for\n * READ-ONLY queries (`useTanglePublicClient` is the convenience hook).\n *\n * Iframes can ignore this and roll their own RPC config — particularly\n * when they need to read from chains OTHER than the active one (e.g. a\n * trading dapp pulling oracle data from mainnet while the active service\n * lives on Base Sepolia). The injected client is a hint, not a constraint.\n *\n * `rpcUrl` is the public RPC the parent uses, NOT a wallet RPC. Iframes\n * cannot sign or submit with this URL; signing always routes upstream via\n * the bridge.\n */\nexport type ChainContext = {\n readonly id: number;\n readonly name: string;\n readonly rpcUrl: string;\n /** Block-explorer base URL — useful for rendering tx links. */\n readonly blockExplorerUrl?: string;\n /** Native currency metadata for cost displays. */\n readonly nativeCurrency?: { readonly name: string; readonly symbol: string; readonly decimals: number };\n};\n\nexport type ServiceContextBroadcast = {\n kind: 'tangle.app.serviceContext';\n readonly blueprintId: string;\n readonly serviceId: string | null;\n readonly operators: readonly ServiceContextOperator[];\n readonly jobs: readonly ServiceContextJob[];\n readonly mode: string | null;\n /** Active chain the parent is connected to; iframes can build a viem\n * publicClient against this for convenience. Optional for backwards\n * compatibility with parents that haven't been upgraded yet. */\n readonly chain?: ChainContext;\n};\n\n// ─── Job invocation (iframe ↔ parent) ────────────────────────────────────────\n//\n// Instead of the iframe wiring up its own EIP-712 quote / sign / submit\n// flow, it sends a single CallJob request upstream. The parent does the\n// whole dance (fetch RFQ quote, build typed data, request user signature,\n// submit on-chain) and streams results back. The iframe never touches\n// chain logic.\n\nexport type JobInputs = Readonly<Record<string, unknown>>;\n\nexport type CallJobRequest = {\n kind: 'tangle.app.callJob';\n correlationId: string;\n /** Job index within the blueprint, e.g. 0 for the primary entry-point. */\n jobIndex: number;\n /** Free-form inputs validated by the parent against the on-chain ABI. */\n inputs: JobInputs;\n /**\n * Whether the publisher wants intermediate progress (streaming chunks)\n * or just the terminal result. Streaming jobs (LLM generation, video\n * encode) opt in; one-shots (embeddings, classifications) don't.\n */\n stream?: boolean;\n};\n\nexport type JobResultStatus = 'pending' | 'streaming' | 'success' | 'error';\n\nexport type JobResultEvent = {\n kind: 'tangle.app.jobResult';\n correlationId: string;\n status: JobResultStatus;\n /** Present on `streaming` and `success`. Shape is publisher-defined. */\n data?: unknown;\n /** Present on `streaming` only — incremental chunk for live UI. */\n chunk?: unknown;\n /** Present on `error`. Human-readable. */\n error?: string;\n /** Optional progress metadata (e.g. `{ percent: 0.42, eta_ms: 8000 }`). */\n progress?: { readonly percent?: number; readonly eta_ms?: number };\n};\n\nexport type ParentMessage =\n | HandshakeAck\n | ReadAccountResult\n | SwitchChainResult\n | SignMessageResult\n | SignTransactionResult\n | SignTypedDataResult\n | AccountChanged\n | ChainChanged\n | ServiceContextBroadcast\n | JobResultEvent;\n\nexport type IframeRequest =\n | HandshakeRequest\n | ReadAccountRequest\n | SwitchChainRequest\n | SignMessageRequest\n | SignTransactionRequest\n | SignTypedDataRequest\n | CallJobRequest;\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"],"mappings":";AAiBO,IAAM,+BAA+B,OAAO,OAAO;AAAA,EACxD;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AACF,CAAU;AAEV,SAAS,qBAAoC;AAC3C,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,MAAM,SAAS;AACrB,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACF,WAAO,IAAI,IAAI,GAAG,EAAE;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAiBO,SAAS,8BACd,UAAgD,CAAC,GAClC;AACf,MAAI,OAAO,WAAW,eAAe,OAAO,WAAW,QAAQ;AAC7D,WAAO;AAAA,EACT;AACA,QAAM,YAAY,oBAAI,IAAY;AAAA,IAChC,GAAG;AAAA,IACH,GAAI,QAAQ,gBAAgB,CAAC;AAAA,EAC/B,CAAC;AACD,QAAM,iBAAiB,mBAAmB;AAC1C,MAAI,kBAAkB,UAAU,IAAI,cAAc,GAAG;AACnD,WAAO;AAAA,EACT;AACA,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AACxC,UAAM,WAAW,IAAI,aAAa,IAAI,QAAQ;AAC9C,QAAI,YAAY,UAAU,IAAI,QAAQ,EAAG,QAAO;AAAA,EAClD,QAAQ;AAAA,EAER;AACA,SAAO;AACT;;;ACnEO,IAAM,iCAAiC;AACvC,IAAM,gCAAgC;AA6OtC,IAAM,oBAAoB;AAO1B,SAAS,kBAAkB,QAAwB;AACxD,QAAM,SACJ,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,aAC1D,OAAO,WAAW,IAClB,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,SAAS,EAAE;AAClE,SAAO,GAAG,MAAM,IAAI,MAAM;AAC5B;","names":[]}
|