@tangle-network/blueprint-ui 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-ZKICSKZH.js +57 -0
- package/dist/chunk-ZKICSKZH.js.map +1 -0
- package/dist/detectParentOrigin-BYruoIdc.d.ts +26 -0
- package/dist/iframe/index.d.ts +145 -0
- package/dist/iframe/index.js +557 -0
- package/dist/iframe/index.js.map +1 -0
- package/dist/iframe/testing-index.d.ts +82 -0
- package/dist/iframe/testing-index.js +535 -0
- package/dist/iframe/testing-index.js.map +1 -0
- package/dist/parentBridgeProtocol-BS2zbIvX.d.ts +194 -0
- package/dist/styles.css +3 -0
- package/dist/tangleIframeClient-DES8FDF0.d.ts +121 -0
- package/dist/wallet/index.d.ts +10 -109
- package/dist/wallet/index.js +14 -47
- package/dist/wallet/index.js.map +1 -1
- package/package.json +11 -1
- package/src/iframe/TangleIframeProvider.tsx +172 -0
- package/src/iframe/hooks.ts +234 -0
- package/src/iframe/index.ts +77 -0
- package/src/iframe/tangleIframeClient.test.ts +317 -0
- package/src/iframe/tangleIframeClient.ts +483 -0
- package/src/iframe/testing-index.ts +15 -0
- package/src/iframe/testing.tsx +710 -0
- package/src/wallet/index.ts +11 -0
- package/src/wallet/parentBridgeProtocol.ts +150 -1
- package/src/wallet/parentBridgeProvider.ts +17 -1
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
// Thin-iframe SDK client — the framework-agnostic state machine that talks
|
|
2
|
+
// to a Tangle Cloud parent dapp over postMessage. React hooks (below) are
|
|
3
|
+
// thin wrappers around an instance of this class.
|
|
4
|
+
//
|
|
5
|
+
// Why a class, not a bag of functions: the iframe lifecycle is stateful —
|
|
6
|
+
// handshake, account changes, service-context broadcasts, in-flight job
|
|
7
|
+
// requests. The class owns that state once; hooks subscribe via listeners.
|
|
8
|
+
// Testing the protocol shape doesn't require React.
|
|
9
|
+
|
|
10
|
+
import type { Address, Hex } from 'viem';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
makeCorrelationId,
|
|
14
|
+
NO_WALLET_ADDRESS,
|
|
15
|
+
TANGLE_IFRAME_PROTOCOL_VERSION,
|
|
16
|
+
type CallJobRequest,
|
|
17
|
+
type ChainContext,
|
|
18
|
+
type JobInputs,
|
|
19
|
+
type JobResultEvent,
|
|
20
|
+
type JobResultStatus,
|
|
21
|
+
type ParentMessage,
|
|
22
|
+
type ServiceContextBroadcast,
|
|
23
|
+
type ServiceContextJob,
|
|
24
|
+
type ServiceContextOperator,
|
|
25
|
+
type SignTypedDataRequest,
|
|
26
|
+
} from '../wallet/parentBridgeProtocol';
|
|
27
|
+
|
|
28
|
+
export type WalletSnapshot = {
|
|
29
|
+
readonly address: Address | null;
|
|
30
|
+
readonly chainId: number | null;
|
|
31
|
+
readonly isConnected: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ServiceSnapshot = {
|
|
35
|
+
readonly blueprintId: string | null;
|
|
36
|
+
readonly serviceId: string | null;
|
|
37
|
+
readonly operators: readonly ServiceContextOperator[];
|
|
38
|
+
readonly jobs: readonly ServiceContextJob[];
|
|
39
|
+
readonly mode: string | null;
|
|
40
|
+
/** Chain context broadcast by the parent — drives `useTanglePublicClient`.
|
|
41
|
+
* `null` when the parent hasn't sent one (older parent or dev mode). */
|
|
42
|
+
readonly chain: ChainContext | null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type JobInvocation = {
|
|
46
|
+
readonly correlationId: string;
|
|
47
|
+
readonly status: JobResultStatus;
|
|
48
|
+
readonly data?: unknown;
|
|
49
|
+
readonly chunks: readonly unknown[];
|
|
50
|
+
readonly error?: string;
|
|
51
|
+
readonly progress?: { readonly percent?: number; readonly eta_ms?: number };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type ClientEventMap = {
|
|
55
|
+
wallet: WalletSnapshot;
|
|
56
|
+
service: ServiceSnapshot;
|
|
57
|
+
job: JobInvocation;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type Listener<K extends keyof ClientEventMap> = (
|
|
61
|
+
value: ClientEventMap[K],
|
|
62
|
+
) => void;
|
|
63
|
+
|
|
64
|
+
export type TangleIframeClientOptions = {
|
|
65
|
+
/**
|
|
66
|
+
* Origin of the parent dapp. The client posts every message with this
|
|
67
|
+
* exact `targetOrigin` and rejects inbound messages from any other origin.
|
|
68
|
+
* Pass `'*'` only in dev — production must pin to the real parent
|
|
69
|
+
* (`https://cloud.tangle.tools` etc.).
|
|
70
|
+
*/
|
|
71
|
+
parentOrigin: string;
|
|
72
|
+
/**
|
|
73
|
+
* Stable identifier for this iframe app. The parent surfaces it in
|
|
74
|
+
* handshake logs + uses it for permission scoping.
|
|
75
|
+
*/
|
|
76
|
+
appId: string;
|
|
77
|
+
/**
|
|
78
|
+
* Per-request timeout. Defaults to 60s — long enough for a user to
|
|
79
|
+
* read + approve a signing prompt in the parent. Long-running jobs
|
|
80
|
+
* stream progress events; the request "completes" only on terminal
|
|
81
|
+
* status, so the timeout protects against parents that drop replies
|
|
82
|
+
* entirely.
|
|
83
|
+
*/
|
|
84
|
+
requestTimeoutMs?: number;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
88
|
+
const NULL_WALLET: WalletSnapshot = {
|
|
89
|
+
address: null,
|
|
90
|
+
chainId: null,
|
|
91
|
+
isConnected: false,
|
|
92
|
+
};
|
|
93
|
+
const NULL_SERVICE: ServiceSnapshot = {
|
|
94
|
+
blueprintId: null,
|
|
95
|
+
serviceId: null,
|
|
96
|
+
operators: [],
|
|
97
|
+
jobs: [],
|
|
98
|
+
mode: null,
|
|
99
|
+
chain: null,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
type PendingJob = {
|
|
103
|
+
resolve: (value: JobInvocation) => void;
|
|
104
|
+
reject: (reason: Error) => void;
|
|
105
|
+
timer: ReturnType<typeof setTimeout>;
|
|
106
|
+
invocation: JobInvocation;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export class TangleIframeClient {
|
|
110
|
+
private wallet: WalletSnapshot = NULL_WALLET;
|
|
111
|
+
private service: ServiceSnapshot = NULL_SERVICE;
|
|
112
|
+
private handshakeAcked = false;
|
|
113
|
+
private handshakeWaiters: Array<() => void> = [];
|
|
114
|
+
private installed = false;
|
|
115
|
+
private listeners: {
|
|
116
|
+
[K in keyof ClientEventMap]: Set<Listener<K>>;
|
|
117
|
+
} = {
|
|
118
|
+
wallet: new Set(),
|
|
119
|
+
service: new Set(),
|
|
120
|
+
job: new Set(),
|
|
121
|
+
};
|
|
122
|
+
private pendingJobs = new Map<string, PendingJob>();
|
|
123
|
+
|
|
124
|
+
constructor(private readonly options: TangleIframeClientOptions) {}
|
|
125
|
+
|
|
126
|
+
/** Wire the global message listener + initial handshake. Idempotent. */
|
|
127
|
+
install(): void {
|
|
128
|
+
if (this.installed || typeof window === 'undefined') return;
|
|
129
|
+
this.installed = true;
|
|
130
|
+
window.addEventListener('message', this.handleParentMessage);
|
|
131
|
+
this.postHandshake();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
uninstall(): void {
|
|
135
|
+
if (!this.installed || typeof window === 'undefined') return;
|
|
136
|
+
this.installed = false;
|
|
137
|
+
window.removeEventListener('message', this.handleParentMessage);
|
|
138
|
+
for (const [, pending] of this.pendingJobs) {
|
|
139
|
+
clearTimeout(pending.timer);
|
|
140
|
+
pending.reject(new Error('Tangle iframe client uninstalled'));
|
|
141
|
+
}
|
|
142
|
+
this.pendingJobs.clear();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── State accessors ─────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
getWallet(): WalletSnapshot {
|
|
148
|
+
return this.wallet;
|
|
149
|
+
}
|
|
150
|
+
getService(): ServiceSnapshot {
|
|
151
|
+
return this.service;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Subscription API (used by React hooks) ──────────────────────────────
|
|
155
|
+
|
|
156
|
+
subscribe<K extends keyof ClientEventMap>(
|
|
157
|
+
event: K,
|
|
158
|
+
listener: Listener<K>,
|
|
159
|
+
): () => void {
|
|
160
|
+
this.listeners[event].add(listener);
|
|
161
|
+
return () => {
|
|
162
|
+
this.listeners[event].delete(listener);
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Wallet operations ───────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
async signMessage(message: string): Promise<Hex> {
|
|
169
|
+
await this.ensureBootstrapped();
|
|
170
|
+
return this.dispatchWallet('tangle.app.signMessage', {
|
|
171
|
+
chainId: this.wallet.chainId ?? 0,
|
|
172
|
+
message,
|
|
173
|
+
}).then((data) => (data as { signature: Hex }).signature);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async sendTransaction(tx: {
|
|
177
|
+
to: Address;
|
|
178
|
+
data: Hex;
|
|
179
|
+
value?: bigint;
|
|
180
|
+
}): Promise<Hex> {
|
|
181
|
+
await this.ensureBootstrapped();
|
|
182
|
+
return this.dispatchWallet('tangle.app.signTransaction', {
|
|
183
|
+
chainId: this.wallet.chainId ?? 0,
|
|
184
|
+
to: tx.to,
|
|
185
|
+
data: tx.data,
|
|
186
|
+
...(tx.value !== undefined ? { value: tx.value.toString(10) } : {}),
|
|
187
|
+
}).then((data) => (data as { txHash: Hex }).txHash);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async switchChain(chainId: number): Promise<number> {
|
|
191
|
+
await this.ensureBootstrapped();
|
|
192
|
+
return this.dispatchWallet('tangle.app.switchChain', { chainId }).then(
|
|
193
|
+
(data) => (data as { chainId: number }).chainId,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* EIP-712 typed-data signing. The parent renders the typed-data fields in
|
|
199
|
+
* its approval modal; the user audits what they're signing. Use for
|
|
200
|
+
* operator envelopes, off-chain attestations, anything that needs a
|
|
201
|
+
* signature outside the standard blueprint-job RFQ flow.
|
|
202
|
+
*
|
|
203
|
+
* Shape mirrors viem's `signTypedData` argument. Do not include the
|
|
204
|
+
* EIP712Domain entry in `types` — the parent injects it from `domain`.
|
|
205
|
+
*/
|
|
206
|
+
async signTypedData(args: {
|
|
207
|
+
domain: SignTypedDataRequest['domain'];
|
|
208
|
+
types: SignTypedDataRequest['types'];
|
|
209
|
+
primaryType: string;
|
|
210
|
+
message: Readonly<Record<string, unknown>>;
|
|
211
|
+
}): Promise<Hex> {
|
|
212
|
+
await this.ensureBootstrapped();
|
|
213
|
+
return this.dispatchWallet('tangle.app.signTypedData', {
|
|
214
|
+
chainId: this.wallet.chainId ?? 0,
|
|
215
|
+
domain: args.domain,
|
|
216
|
+
types: args.types,
|
|
217
|
+
primaryType: args.primaryType,
|
|
218
|
+
message: args.message,
|
|
219
|
+
}).then((data) => (data as { signature: Hex }).signature);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Job invocation ──────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Invoke a blueprint job. Returns a Promise that resolves on terminal
|
|
226
|
+
* status (`success` or `error`); subscribe to the `job` event for
|
|
227
|
+
* intermediate streaming chunks.
|
|
228
|
+
*
|
|
229
|
+
* Streaming opt-in: pass `stream: true` if the publisher's job emits
|
|
230
|
+
* chunks (LLM generation, video encoding). One-shot jobs (embeddings,
|
|
231
|
+
* classifications) skip the streaming machinery.
|
|
232
|
+
*/
|
|
233
|
+
async callJob(args: {
|
|
234
|
+
jobIndex: number;
|
|
235
|
+
inputs: JobInputs;
|
|
236
|
+
stream?: boolean;
|
|
237
|
+
}): Promise<JobInvocation> {
|
|
238
|
+
await this.ensureBootstrapped();
|
|
239
|
+
const correlationId = makeCorrelationId('tangle.app.callJob');
|
|
240
|
+
const timeout =
|
|
241
|
+
this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
242
|
+
return new Promise<JobInvocation>((resolve, reject) => {
|
|
243
|
+
const invocation: JobInvocation = {
|
|
244
|
+
correlationId,
|
|
245
|
+
status: 'pending',
|
|
246
|
+
chunks: [],
|
|
247
|
+
};
|
|
248
|
+
const timer = setTimeout(() => {
|
|
249
|
+
this.pendingJobs.delete(correlationId);
|
|
250
|
+
reject(
|
|
251
|
+
bridgeError(4900, `Job did not respond within ${timeout}ms`),
|
|
252
|
+
);
|
|
253
|
+
}, timeout);
|
|
254
|
+
this.pendingJobs.set(correlationId, {
|
|
255
|
+
resolve,
|
|
256
|
+
reject,
|
|
257
|
+
timer,
|
|
258
|
+
invocation,
|
|
259
|
+
});
|
|
260
|
+
const message: CallJobRequest = {
|
|
261
|
+
kind: 'tangle.app.callJob',
|
|
262
|
+
correlationId,
|
|
263
|
+
jobIndex: args.jobIndex,
|
|
264
|
+
inputs: args.inputs,
|
|
265
|
+
...(args.stream !== undefined ? { stream: args.stream } : {}),
|
|
266
|
+
};
|
|
267
|
+
this.postToParent(message);
|
|
268
|
+
// Emit pending immediately so consumer UIs can show optimistic state.
|
|
269
|
+
this.emit('job', invocation);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Internals ───────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
private postHandshake(): void {
|
|
276
|
+
this.postToParent({
|
|
277
|
+
kind: 'tangle.app.handshake',
|
|
278
|
+
appId: this.options.appId,
|
|
279
|
+
version: TANGLE_IFRAME_PROTOCOL_VERSION,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private postToParent(message: object): void {
|
|
284
|
+
if (typeof window === 'undefined') return;
|
|
285
|
+
try {
|
|
286
|
+
window.parent.postMessage(message, this.options.parentOrigin);
|
|
287
|
+
} catch {
|
|
288
|
+
// Cross-origin / sandboxed; defensive only — postMessage shouldn't throw.
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private handleParentMessage = (event: MessageEvent): void => {
|
|
293
|
+
if (event.origin !== this.options.parentOrigin) return;
|
|
294
|
+
const data = event.data;
|
|
295
|
+
if (typeof data !== 'object' || data === null) return;
|
|
296
|
+
const message = data as ParentMessage;
|
|
297
|
+
switch (message.kind) {
|
|
298
|
+
case 'tangle.app.handshakeAck':
|
|
299
|
+
this.handshakeAcked = true;
|
|
300
|
+
for (const resolve of this.handshakeWaiters) resolve();
|
|
301
|
+
this.handshakeWaiters = [];
|
|
302
|
+
return;
|
|
303
|
+
case 'tangle.app.readAccountResult':
|
|
304
|
+
if (message.ok) {
|
|
305
|
+
this.updateWallet({
|
|
306
|
+
address:
|
|
307
|
+
message.data.account === NO_WALLET_ADDRESS
|
|
308
|
+
? null
|
|
309
|
+
: message.data.account,
|
|
310
|
+
chainId: message.data.chainId,
|
|
311
|
+
isConnected: message.data.account !== NO_WALLET_ADDRESS,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
case 'tangle.app.accountChanged':
|
|
316
|
+
this.updateWallet({
|
|
317
|
+
address: message.account,
|
|
318
|
+
chainId: this.wallet.chainId,
|
|
319
|
+
isConnected: message.account !== null,
|
|
320
|
+
});
|
|
321
|
+
return;
|
|
322
|
+
case 'tangle.app.chainChanged':
|
|
323
|
+
this.updateWallet({
|
|
324
|
+
address: this.wallet.address,
|
|
325
|
+
chainId: message.chainId,
|
|
326
|
+
isConnected: this.wallet.isConnected,
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
case 'tangle.app.serviceContext':
|
|
330
|
+
this.updateService(message);
|
|
331
|
+
return;
|
|
332
|
+
case 'tangle.app.jobResult':
|
|
333
|
+
this.handleJobResult(message);
|
|
334
|
+
return;
|
|
335
|
+
// Wallet-shape responses (signMessageResult etc.) are routed by
|
|
336
|
+
// dispatchWallet's promise resolver, not here.
|
|
337
|
+
default:
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
private async dispatchWallet(
|
|
343
|
+
kind:
|
|
344
|
+
| 'tangle.app.signMessage'
|
|
345
|
+
| 'tangle.app.signTransaction'
|
|
346
|
+
| 'tangle.app.signTypedData'
|
|
347
|
+
| 'tangle.app.switchChain',
|
|
348
|
+
payload: Record<string, unknown>,
|
|
349
|
+
): Promise<unknown> {
|
|
350
|
+
return new Promise((resolve, reject) => {
|
|
351
|
+
const correlationId = makeCorrelationId(kind);
|
|
352
|
+
const timeout =
|
|
353
|
+
this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
354
|
+
const expectedKind = (
|
|
355
|
+
{
|
|
356
|
+
'tangle.app.signMessage': 'tangle.app.signMessageResult',
|
|
357
|
+
'tangle.app.signTransaction': 'tangle.app.signTransactionResult',
|
|
358
|
+
'tangle.app.signTypedData': 'tangle.app.signTypedDataResult',
|
|
359
|
+
'tangle.app.switchChain': 'tangle.app.switchChainResult',
|
|
360
|
+
} as const
|
|
361
|
+
)[kind];
|
|
362
|
+
const timer = setTimeout(() => {
|
|
363
|
+
window.removeEventListener('message', listener);
|
|
364
|
+
reject(bridgeError(4900, `Parent did not respond to ${kind} in ${timeout}ms`));
|
|
365
|
+
}, timeout);
|
|
366
|
+
const listener = (event: MessageEvent) => {
|
|
367
|
+
if (event.origin !== this.options.parentOrigin) return;
|
|
368
|
+
const data = event.data;
|
|
369
|
+
if (typeof data !== 'object' || data === null) return;
|
|
370
|
+
const msg = data as ParentMessage;
|
|
371
|
+
if (
|
|
372
|
+
msg.kind !== expectedKind ||
|
|
373
|
+
!('correlationId' in msg) ||
|
|
374
|
+
msg.correlationId !== correlationId
|
|
375
|
+
) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
clearTimeout(timer);
|
|
379
|
+
window.removeEventListener('message', listener);
|
|
380
|
+
// Narrow the type — expectedKind is the wallet-shape `{ok, data|error}` envelope
|
|
381
|
+
const env = msg as {
|
|
382
|
+
ok: boolean;
|
|
383
|
+
data?: unknown;
|
|
384
|
+
error?: string;
|
|
385
|
+
};
|
|
386
|
+
if (env.ok) {
|
|
387
|
+
resolve(env.data);
|
|
388
|
+
} else {
|
|
389
|
+
reject(bridgeError(4001, env.error ?? 'Parent rejected request'));
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
window.addEventListener('message', listener);
|
|
393
|
+
this.postToParent({ kind, correlationId, ...payload });
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private handleJobResult(message: JobResultEvent): void {
|
|
398
|
+
const pending = this.pendingJobs.get(message.correlationId);
|
|
399
|
+
if (!pending) return;
|
|
400
|
+
const updated: JobInvocation = {
|
|
401
|
+
correlationId: message.correlationId,
|
|
402
|
+
status: message.status,
|
|
403
|
+
chunks:
|
|
404
|
+
message.chunk !== undefined
|
|
405
|
+
? [...pending.invocation.chunks, message.chunk]
|
|
406
|
+
: pending.invocation.chunks,
|
|
407
|
+
...(message.data !== undefined ? { data: message.data } : {}),
|
|
408
|
+
...(message.error !== undefined ? { error: message.error } : {}),
|
|
409
|
+
...(message.progress !== undefined ? { progress: message.progress } : {}),
|
|
410
|
+
};
|
|
411
|
+
pending.invocation = updated;
|
|
412
|
+
this.emit('job', updated);
|
|
413
|
+
if (message.status === 'success' || message.status === 'error') {
|
|
414
|
+
clearTimeout(pending.timer);
|
|
415
|
+
this.pendingJobs.delete(message.correlationId);
|
|
416
|
+
if (message.status === 'success') {
|
|
417
|
+
pending.resolve(updated);
|
|
418
|
+
} else {
|
|
419
|
+
pending.reject(bridgeError(4001, message.error ?? 'Job failed'));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private updateWallet(next: WalletSnapshot): void {
|
|
425
|
+
if (
|
|
426
|
+
this.wallet.address === next.address &&
|
|
427
|
+
this.wallet.chainId === next.chainId &&
|
|
428
|
+
this.wallet.isConnected === next.isConnected
|
|
429
|
+
) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
this.wallet = next;
|
|
433
|
+
this.emit('wallet', next);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private updateService(broadcast: ServiceContextBroadcast): void {
|
|
437
|
+
const next: ServiceSnapshot = {
|
|
438
|
+
blueprintId: broadcast.blueprintId,
|
|
439
|
+
serviceId: broadcast.serviceId,
|
|
440
|
+
operators: broadcast.operators,
|
|
441
|
+
jobs: broadcast.jobs,
|
|
442
|
+
mode: broadcast.mode,
|
|
443
|
+
chain: broadcast.chain ?? null,
|
|
444
|
+
};
|
|
445
|
+
this.service = next;
|
|
446
|
+
this.emit('service', next);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private emit<K extends keyof ClientEventMap>(
|
|
450
|
+
event: K,
|
|
451
|
+
value: ClientEventMap[K],
|
|
452
|
+
): void {
|
|
453
|
+
for (const listener of [...this.listeners[event]]) {
|
|
454
|
+
try {
|
|
455
|
+
(listener as Listener<K>)(value);
|
|
456
|
+
} catch {
|
|
457
|
+
// Listener bugs shouldn't break the bridge.
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private async ensureBootstrapped(): Promise<void> {
|
|
463
|
+
if (this.handshakeAcked) return;
|
|
464
|
+
this.install();
|
|
465
|
+
await new Promise<void>((resolve) => {
|
|
466
|
+
this.handshakeWaiters.push(resolve);
|
|
467
|
+
const retry = setInterval(() => {
|
|
468
|
+
if (this.handshakeAcked) {
|
|
469
|
+
clearInterval(retry);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
this.postHandshake();
|
|
473
|
+
}, 500);
|
|
474
|
+
setTimeout(() => clearInterval(retry), 10_000);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function bridgeError(code: number, message: string): Error {
|
|
480
|
+
const err = new Error(message) as Error & { code?: number };
|
|
481
|
+
err.code = code;
|
|
482
|
+
return err;
|
|
483
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing-only utilities for the iframe SDK. Imported from
|
|
3
|
+
* `@tangle-network/blueprint-ui/iframe/testing` so production bundles don't
|
|
4
|
+
* pull in the debug panel + mock factories.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
TangleParentHarness,
|
|
9
|
+
HARNESS_ORIGIN,
|
|
10
|
+
mockWallet,
|
|
11
|
+
mockServiceContext,
|
|
12
|
+
type CallJobHandler,
|
|
13
|
+
type MockServiceInput,
|
|
14
|
+
type MockWalletInput,
|
|
15
|
+
} from './testing';
|