@tangle-network/blueprint-ui 0.5.0 → 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.
@@ -54,4 +54,4 @@ export {
54
54
  NO_WALLET_ADDRESS,
55
55
  makeCorrelationId
56
56
  };
57
- //# sourceMappingURL=chunk-ZKICSKZH.js.map
57
+ //# sourceMappingURL=chunk-TM5ROMDV.js.map
@@ -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":[]}
@@ -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-DES8FDF0.js';
4
- export { C as ClientEventMap, a as TangleIframeClientOptions } from '../tangleIframeClient-DES8FDF0.js';
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-BS2zbIvX.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-BS2zbIvX.js';
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;
@@ -4,7 +4,7 @@ import {
4
4
  TANGLE_IFRAME_PROTOCOL_VERSION,
5
5
  detectTangleCloudParentOrigin,
6
6
  makeCorrelationId
7
- } from "../chunk-ZKICSKZH.js";
7
+ } from "../chunk-TM5ROMDV.js";
8
8
 
9
9
  // src/iframe/TangleIframeProvider.tsx
10
10
  import {
@@ -18,6 +18,9 @@ import {
18
18
 
19
19
  // src/iframe/tangleIframeClient.ts
20
20
  var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
21
+ var CONNECT_REQUEST_TIMEOUT_MS = 3e5;
22
+ var HANDSHAKE_RETRY_MS = 250;
23
+ var HANDSHAKE_RETRY_BUDGET_MS = 1e4;
21
24
  var NULL_WALLET = {
22
25
  address: null,
23
26
  chainId: null,
@@ -41,6 +44,7 @@ var TangleIframeClient = class {
41
44
  handshakeAcked = false;
42
45
  handshakeWaiters = [];
43
46
  installed = false;
47
+ handshakeRetry = null;
44
48
  listeners = {
45
49
  wallet: /* @__PURE__ */ new Set(),
46
50
  service: /* @__PURE__ */ new Set(),
@@ -53,10 +57,22 @@ var TangleIframeClient = class {
53
57
  this.installed = true;
54
58
  window.addEventListener("message", this.handleParentMessage);
55
59
  this.postHandshake();
60
+ if (this.handshakeRetry === null) {
61
+ let elapsed = 0;
62
+ this.handshakeRetry = setInterval(() => {
63
+ elapsed += HANDSHAKE_RETRY_MS;
64
+ if (this.handshakeAcked || elapsed >= HANDSHAKE_RETRY_BUDGET_MS) {
65
+ this.clearHandshakeRetry();
66
+ return;
67
+ }
68
+ this.postHandshake();
69
+ }, HANDSHAKE_RETRY_MS);
70
+ }
56
71
  }
57
72
  uninstall() {
58
73
  if (!this.installed || typeof window === "undefined") return;
59
74
  this.installed = false;
75
+ this.clearHandshakeRetry();
60
76
  window.removeEventListener("message", this.handleParentMessage);
61
77
  for (const [, pending] of this.pendingJobs) {
62
78
  clearTimeout(pending.timer);
@@ -79,6 +95,27 @@ var TangleIframeClient = class {
79
95
  };
80
96
  }
81
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
+ }
82
119
  async signMessage(message) {
83
120
  await this.ensureBootstrapped();
84
121
  return this.dispatchWallet("tangle.app.signMessage", {
@@ -164,6 +201,12 @@ var TangleIframeClient = class {
164
201
  });
165
202
  }
166
203
  // ── Internals ───────────────────────────────────────────────────────────
204
+ clearHandshakeRetry() {
205
+ if (this.handshakeRetry !== null) {
206
+ clearInterval(this.handshakeRetry);
207
+ this.handshakeRetry = null;
208
+ }
209
+ }
167
210
  postHandshake() {
168
211
  this.postToParent({
169
212
  kind: "tangle.app.handshake",
@@ -186,6 +229,7 @@ var TangleIframeClient = class {
186
229
  switch (message.kind) {
187
230
  case "tangle.app.handshakeAck":
188
231
  this.handshakeAcked = true;
232
+ this.clearHandshakeRetry();
189
233
  for (const resolve of this.handshakeWaiters) resolve();
190
234
  this.handshakeWaiters = [];
191
235
  return;
@@ -224,15 +268,16 @@ var TangleIframeClient = class {
224
268
  return;
225
269
  }
226
270
  };
227
- async dispatchWallet(kind, payload) {
271
+ async dispatchWallet(kind, payload, timeoutMs) {
228
272
  return new Promise((resolve, reject) => {
229
273
  const correlationId = makeCorrelationId(kind);
230
- const timeout = this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
274
+ const timeout = timeoutMs ?? this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
231
275
  const expectedKind = {
232
276
  "tangle.app.signMessage": "tangle.app.signMessageResult",
233
277
  "tangle.app.signTransaction": "tangle.app.signTransactionResult",
234
278
  "tangle.app.signTypedData": "tangle.app.signTypedDataResult",
235
- "tangle.app.switchChain": "tangle.app.switchChainResult"
279
+ "tangle.app.switchChain": "tangle.app.switchChainResult",
280
+ "tangle.app.requestConnect": "tangle.app.connectResult"
236
281
  }[kind];
237
282
  const timer = setTimeout(() => {
238
283
  window.removeEventListener("message", listener);
@@ -427,6 +472,10 @@ import {
427
472
  } from "viem";
428
473
  function useTangleWallet() {
429
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]);
430
479
  const signMessage = useCallback(
431
480
  (message) => {
432
481
  if (!client) throw new Error("Wallet not available in dev mode.");
@@ -457,6 +506,7 @@ function useTangleWallet() {
457
506
  );
458
507
  return {
459
508
  ...wallet,
509
+ connect,
460
510
  signMessage,
461
511
  sendTransaction,
462
512
  signTypedData,
@@ -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 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 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 }\n\n uninstall(): void {\n if (!this.installed || typeof window === 'undefined') return;\n this.installed = false;\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 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 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":"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","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":"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","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-BS2zbIvX.js';
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-DES8FDF0.js';
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;
@@ -83,9 +83,7 @@ var TangleParentHarness = ({
83
83
  });
84
84
  }
85
85
  };
86
- const handler = async (event) => {
87
- if (event.origin === HARNESS_ORIGIN) return;
88
- const data = event.data;
86
+ const handleInbound = async (data) => {
89
87
  if (typeof data !== "object" || data === null) return;
90
88
  const message = data;
91
89
  switch (message.kind) {
@@ -114,12 +112,25 @@ var TangleParentHarness = ({
114
112
  });
115
113
  return;
116
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
+ }
117
128
  case "tangle.app.callJob": {
118
129
  if (typeof message.correlationId !== "string") return;
119
130
  const request = message;
120
131
  setCallLog((prev) => [...prev, request]);
121
- const handler2 = callJobHandler.current;
122
- if (!handler2) {
132
+ const handler = callJobHandler.current;
133
+ if (!handler) {
123
134
  const result = {
124
135
  kind: "tangle.app.jobResult",
125
136
  correlationId: request.correlationId,
@@ -130,7 +141,7 @@ var TangleParentHarness = ({
130
141
  return;
131
142
  }
132
143
  try {
133
- const outcome = await handler2(request);
144
+ const outcome = await handler(request);
134
145
  for (const chunk of outcome.chunks ?? []) {
135
146
  reply({
136
147
  kind: "tangle.app.jobResult",
@@ -206,8 +217,22 @@ var TangleParentHarness = ({
206
217
  }
207
218
  }
208
219
  };
209
- window.addEventListener("message", handler);
210
- return () => window.removeEventListener("message", handler);
220
+ const originalParent = window.parent;
221
+ const proxyParent = {
222
+ postMessage: (message) => {
223
+ void handleInbound(message);
224
+ }
225
+ };
226
+ Object.defineProperty(window, "parent", {
227
+ configurable: true,
228
+ get: () => proxyParent
229
+ });
230
+ return () => {
231
+ Object.defineProperty(window, "parent", {
232
+ configurable: true,
233
+ value: originalParent
234
+ });
235
+ };
211
236
  }, [appId, currentWallet, currentService]);
212
237
  useEffect(() => {
213
238
  if (!seenHandshake.current) return;