@tangle-network/blueprint-ui 0.5.1 → 0.5.3

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.
@@ -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-BS2zbIvX.js';
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;
@@ -1,7 +1,47 @@
1
1
  export { T as TANGLE_CLOUD_ORIGINS_DEFAULT, d as detectTangleCloudParentOrigin } from '../detectParentOrigin-BYruoIdc.js';
2
2
  import * as wagmi from 'wagmi';
3
+ import { CreateConnectorFn } from 'wagmi';
3
4
  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-BS2zbIvX.js';
5
+ 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';
6
+
7
+ interface TangleBlueprintConnectorsOptions {
8
+ /** App id reported to the parent dapp on handshake (when embedded). */
9
+ appId: string;
10
+ /**
11
+ * Connectors used when running STANDALONE (the app's own domain) — e.g.
12
+ * `[injected(), walletConnect({ projectId })]`. Ignored when embedded.
13
+ */
14
+ standalone: CreateConnectorFn[];
15
+ /** Extra trusted parent origins (staging / preview deploys). */
16
+ extraOrigins?: readonly string[];
17
+ /** Override the bridged-request timeout (ms). */
18
+ requestTimeoutMs?: number;
19
+ }
20
+ /**
21
+ * One connector list, two deployment modes — so a blueprint app can ship a
22
+ * single build that runs both standalone and embedded in Tangle Cloud:
23
+ *
24
+ * - **Embedded** (a trusted Tangle Cloud parent is detected): the
25
+ * parent-bridge connector is the *only* connector. The sandboxed iframe
26
+ * can't inject a wallet extension, so it inherits / drives the parent's
27
+ * wallet over the postMessage bridge — surfacing injected/WalletConnect
28
+ * here would just dead-end.
29
+ * - **Standalone** (no trusted parent): the app's own `standalone`
30
+ * connectors, exactly as a normal dapp.
31
+ *
32
+ * The choice is made at runtime from the embedding context, so the same
33
+ * artifact works in both places with no build flags.
34
+ *
35
+ * createConfig({
36
+ * chains,
37
+ * transports,
38
+ * connectors: tangleBlueprintConnectors({
39
+ * appId: 'trading-arena',
40
+ * standalone: [injected(), walletConnect({ projectId })],
41
+ * }),
42
+ * })
43
+ */
44
+ declare function tangleBlueprintConnectors(options: TangleBlueprintConnectorsOptions): CreateConnectorFn[];
5
45
 
6
46
  type EventName = 'accountsChanged' | 'chainChanged' | 'connect' | 'disconnect' | 'message';
7
47
  type Listener = (...args: unknown[]) => void;
@@ -62,6 +102,11 @@ declare class ParentBridgeProvider {
62
102
  private postToParent;
63
103
  private handleParentMessage;
64
104
  private sendReadAccount;
105
+ /**
106
+ * Ask the parent to connect a wallet (opening its modal if needed) and wait
107
+ * for the result. Long timeout — the user is interacting with the modal.
108
+ */
109
+ private requestConnect;
65
110
  private requestSignMessage;
66
111
  private requestSignTransaction;
67
112
  private requestSwitchChain;
@@ -86,4 +131,4 @@ declare class ParentBridgeProvider {
86
131
  type ParentBridgeConnectorOptions = ParentBridgeOptions;
87
132
  declare function parentBridgeConnector(options: ParentBridgeConnectorOptions): wagmi.CreateConnectorFn<ParentBridgeProvider, Record<string, unknown>, Record<string, unknown>>;
88
133
 
89
- export { type ParentBridgeConnectorOptions, type ParentBridgeOptions, ParentBridgeProvider, isRunningInIframe, parentBridgeConnector };
134
+ export { type ParentBridgeConnectorOptions, type ParentBridgeOptions, ParentBridgeProvider, type TangleBlueprintConnectorsOptions, isRunningInIframe, parentBridgeConnector, tangleBlueprintConnectors };
@@ -5,13 +5,14 @@ import {
5
5
  TANGLE_IFRAME_PROTOCOL_VERSION,
6
6
  detectTangleCloudParentOrigin,
7
7
  makeCorrelationId
8
- } from "../chunk-ZKICSKZH.js";
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);
@@ -419,6 +440,23 @@ function parentBridgeConnector(options) {
419
440
  };
420
441
  });
421
442
  }
443
+
444
+ // src/wallet/tangleConnectors.ts
445
+ function tangleBlueprintConnectors(options) {
446
+ const parentOrigin = detectTangleCloudParentOrigin({
447
+ extraOrigins: options.extraOrigins
448
+ });
449
+ if (parentOrigin === null) {
450
+ return options.standalone;
451
+ }
452
+ return [
453
+ parentBridgeConnector({
454
+ parentOrigin,
455
+ appId: options.appId,
456
+ ...options.requestTimeoutMs !== void 0 ? { requestTimeoutMs: options.requestTimeoutMs } : {}
457
+ })
458
+ ];
459
+ }
422
460
  export {
423
461
  NO_WALLET_ADDRESS,
424
462
  ParentBridgeProvider,
@@ -428,6 +466,7 @@ export {
428
466
  detectTangleCloudParentOrigin,
429
467
  isRunningInIframe,
430
468
  makeCorrelationId,
431
- parentBridgeConnector
469
+ parentBridgeConnector,
470
+ tangleBlueprintConnectors
432
471
  };
433
472
  //# sourceMappingURL=index.js.map
@@ -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","../../src/wallet/tangleConnectors.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","import type { CreateConnectorFn } from 'wagmi';\n\nimport { detectTangleCloudParentOrigin } from './detectParentOrigin';\nimport { parentBridgeConnector } from './parentBridgeConnector';\n\nexport interface TangleBlueprintConnectorsOptions {\n /** App id reported to the parent dapp on handshake (when embedded). */\n appId: string;\n /**\n * Connectors used when running STANDALONE (the app's own domain) — e.g.\n * `[injected(), walletConnect({ projectId })]`. Ignored when embedded.\n */\n standalone: CreateConnectorFn[];\n /** Extra trusted parent origins (staging / preview deploys). */\n extraOrigins?: readonly string[];\n /** Override the bridged-request timeout (ms). */\n requestTimeoutMs?: number;\n}\n\n/**\n * One connector list, two deployment modes — so a blueprint app can ship a\n * single build that runs both standalone and embedded in Tangle Cloud:\n *\n * - **Embedded** (a trusted Tangle Cloud parent is detected): the\n * parent-bridge connector is the *only* connector. The sandboxed iframe\n * can't inject a wallet extension, so it inherits / drives the parent's\n * wallet over the postMessage bridge — surfacing injected/WalletConnect\n * here would just dead-end.\n * - **Standalone** (no trusted parent): the app's own `standalone`\n * connectors, exactly as a normal dapp.\n *\n * The choice is made at runtime from the embedding context, so the same\n * artifact works in both places with no build flags.\n *\n * createConfig({\n * chains,\n * transports,\n * connectors: tangleBlueprintConnectors({\n * appId: 'trading-arena',\n * standalone: [injected(), walletConnect({ projectId })],\n * }),\n * })\n */\nexport function tangleBlueprintConnectors(\n options: TangleBlueprintConnectorsOptions,\n): CreateConnectorFn[] {\n const parentOrigin = detectTangleCloudParentOrigin({\n extraOrigins: options.extraOrigins,\n });\n if (parentOrigin === null) {\n return options.standalone;\n }\n return [\n parentBridgeConnector({\n parentOrigin,\n appId: options.appId,\n ...(options.requestTimeoutMs !== undefined\n ? { requestTimeoutMs: options.requestTimeoutMs }\n : {}),\n }),\n ];\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/blueprint-ui",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Shared blueprint UI components, hooks, and contract utilities for Tangle Network apps",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "repository": {
@@ -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(() => {
@@ -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;
@@ -8,26 +8,27 @@
8
8
  * postMessage protocol, so the iframe inherits the parent's wallet without
9
9
  * its own picker.
10
10
  *
11
- * Usage in an iframe app's wagmi config:
11
+ * Build a blueprint app ONCE and ship it both standalone and embedded — the
12
+ * recommended entry point is `tangleBlueprintConnectors`, which picks the
13
+ * right connectors at runtime:
12
14
  *
13
- * import {
14
- * detectTangleCloudParentOrigin,
15
- * parentBridgeConnector,
16
- * } from '@tangle-network/blueprint-ui/wallet';
15
+ * import { tangleBlueprintConnectors } from '@tangle-network/blueprint-ui/wallet';
17
16
  *
18
- * const parent = detectTangleCloudParentOrigin();
19
- * const config = createConfig(
20
- * parent !== null
21
- * ? { ...getDefaultConfig({...}), connectors: [
22
- * parentBridgeConnector({ parentOrigin: parent, appId: 'my-app' }),
23
- * ] }
24
- * : getDefaultConfig({...}),
25
- * );
17
+ * createConfig({
18
+ * chains, transports,
19
+ * connectors: tangleBlueprintConnectors({
20
+ * appId: 'my-app',
21
+ * standalone: [injected(), walletConnect({ projectId })],
22
+ * }),
23
+ * });
26
24
  *
27
- * The bridge is intentionally the ONLY connector when running inside the
28
- * dapp surfacing injected / WalletConnect / Coinbase inside a sandboxed
29
- * iframe doesn't work (no popup, no extension injection) and would just
30
- * confuse operators.
25
+ * Embedded in Tangle Cloud the bridge connector inherits/drives the parent
26
+ * wallet. Standalone the app's own connectors. The bridge is intentionally
27
+ * the ONLY connector when embedded surfacing injected / WalletConnect inside
28
+ * a sandboxed iframe doesn't work (no popup, no extension injection).
29
+ *
30
+ * `detectTangleCloudParentOrigin` + `parentBridgeConnector` stay exported for
31
+ * apps that hand-roll the selection.
31
32
  */
32
33
 
33
34
  export {
@@ -35,6 +36,11 @@ export {
35
36
  TANGLE_CLOUD_ORIGINS_DEFAULT,
36
37
  } from './detectParentOrigin';
37
38
 
39
+ export {
40
+ tangleBlueprintConnectors,
41
+ type TangleBlueprintConnectorsOptions,
42
+ } from './tangleConnectors';
43
+
38
44
  export {
39
45
  parentBridgeConnector,
40
46
  type ParentBridgeConnectorOptions,
@@ -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