@atom-circuit/embed-sdk 1.2.1

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.
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Atom Circuit Embed SDK - wire protocol contracts.
3
+ *
4
+ * Every message passed between host page and iframe must match one of the
5
+ * discriminated unions exported here. Strict origin and shape checks rely on
6
+ * these types both at compile time and at runtime (see assertion helpers).
7
+ */
8
+ /**
9
+ * Wire-protocol major. SDK sends this in the URL (`?v=`) and during the
10
+ * handshake. The iframe must honor at least the last 2 majors or 18 months
11
+ * of SDK versions, whichever is longer. Bumped on every
12
+ * breaking wire change; independent of the npm package version.
13
+ */
14
+ declare const PROTOCOL_VERSION = "1.0.0";
15
+ /**
16
+ * Capabilities advertised in the handshake. Names are stable strings; new
17
+ * capabilities may be added without breaking older SDKs (they simply ignore
18
+ * unknown entries).
19
+ */
20
+ type Capability = 'swap.submit' | 'swap.status' | 'resize.report' | 'events.stream';
21
+ type Capabilities = ReadonlyArray<Capability | string>;
22
+ /**
23
+ * Handshake payload exchanged once on connect. The iframe is expected to
24
+ * reply with its own handshake describing its protocol version + capability
25
+ * set; the SDK warns (does not throw) when the major versions diverge.
26
+ */
27
+ interface HandshakeMessage {
28
+ readonly type: 'handshake';
29
+ readonly protocolVersion: string;
30
+ readonly capabilities: Capabilities;
31
+ }
32
+ interface ReadyPayload {
33
+ readonly protocolVersion: string;
34
+ }
35
+ interface SwapSubmittedPayload {
36
+ readonly txHash: string;
37
+ readonly route?: SwapRouteSummary;
38
+ }
39
+ interface SwapSuccessPayload {
40
+ readonly txHash: string;
41
+ }
42
+ interface SwapErrorPayload {
43
+ readonly code: string;
44
+ readonly message: string;
45
+ }
46
+ interface SwapRouteSummary {
47
+ readonly sourceChainId: string;
48
+ readonly destChainId: string;
49
+ readonly sourceDenom: string;
50
+ readonly destDenom: string;
51
+ readonly amountIn: string;
52
+ readonly amountOut?: string;
53
+ }
54
+ /**
55
+ * Discriminated union of all valid widget events. Use this when typing event
56
+ * subscribers on the host side.
57
+ */
58
+ type WidgetEvent = {
59
+ readonly name: 'ready';
60
+ readonly payload: ReadyPayload;
61
+ } | {
62
+ readonly name: 'swap:submitted';
63
+ readonly payload: SwapSubmittedPayload;
64
+ } | {
65
+ readonly name: 'swap:success';
66
+ readonly payload: SwapSuccessPayload;
67
+ } | {
68
+ readonly name: 'swap:error';
69
+ readonly payload: SwapErrorPayload;
70
+ };
71
+ /**
72
+ * Optional theming contract passed from host to iframe via the `?theme=` URL
73
+ * parameter (base64-encoded compact JSON of this object).
74
+ *
75
+ * Contract:
76
+ * - The SDK validates every field against the rules documented per-field. If
77
+ * ANY field fails validation, the entire theme is dropped and the iframe
78
+ * falls back to its default appearance. Validation is intentionally strict
79
+ * so a malformed theme cannot break the embed or be used as an injection
80
+ * vector against the iframe's CSS surface.
81
+ * - Fields are optional; the iframe must accept partial themes and apply only
82
+ * the keys present.
83
+ * - Color values are CSS hex strings (`#RGB` or `#RRGGBB`). Other CSS color
84
+ * notations (rgb(), named colors) are rejected to keep the wire surface
85
+ * trivial to validate and to avoid CSS-injection footguns via string
86
+ * interpolation on the iframe side.
87
+ * - `radius` and `fontSize` are plain pixel numbers in tight bounds.
88
+ * - `fontFamily` is a CSS-safe subset: letters, digits, spaces, hyphens,
89
+ * commas, single/double quotes, dots. Anything containing `<`, `>`, `;`,
90
+ * `{`, `}`, `=`, `(`, `)`, newlines, or tabs is rejected. Max 200 chars.
91
+ * - The iframe applies these as CSS custom properties on its embed root;
92
+ * see the dapp side for the variable mapping.
93
+ *
94
+ * Omitting the `theme` field on MountOptions omits the `?theme=` param
95
+ * from the iframe URL entirely.
96
+ */
97
+ interface ThemeOptions {
98
+ /** Light/dark/auto mode hint. Auto follows the host system preference. */
99
+ readonly mode?: 'light' | 'dark' | 'auto';
100
+ /** Brand accent color used for primary buttons and highlights. Hex only. */
101
+ readonly accentColor?: string;
102
+ /** Page background color. Hex only. */
103
+ readonly background?: string;
104
+ /** Primary text/foreground color. Hex only. */
105
+ readonly foreground?: string;
106
+ /** Border color for inputs, cards, dividers. Hex only. */
107
+ readonly border?: string;
108
+ /** Corner radius in pixels. Range: 0-64 inclusive. */
109
+ readonly radius?: number;
110
+ /** Base font size in pixels. Range: 8-32 inclusive. */
111
+ readonly fontSize?: number;
112
+ /** CSS font-family value. CSS-safe subset only; see ThemeOptions doc. */
113
+ readonly fontFamily?: string;
114
+ }
115
+ /**
116
+ * Toggles for the visual chrome surfaces rendered by the embed page. Each
117
+ * surface defaults to ON (true) so an embed dropped in with no chrome
118
+ * option shows the full chrome. Setting a flag to false hides the
119
+ * corresponding surface.
120
+ *
121
+ * Encoded into the iframe URL as part of the `?theme=` base64-JSON
122
+ * payload under the `chrome` key. Validation is strict: each present field
123
+ * must be a boolean, otherwise the entire chrome bundle is rejected.
124
+ *
125
+ * Omitting the `chrome` field, or omitting individual flags, leaves the
126
+ * default-on behaviour in place.
127
+ */
128
+ interface ChromeOptions {
129
+ /** Show the Atom Circuit logo in the top bar. Default true. */
130
+ readonly logo?: boolean;
131
+ /** Show the wallet connect / disconnect button in the top bar. Default true. */
132
+ readonly wallet?: boolean;
133
+ /** Show the "Fees stake with <moniker>" validator badge. Default true. */
134
+ readonly validator?: boolean;
135
+ /** Show the "Powered by Atom Circuit" footer. Default true. */
136
+ readonly footer?: boolean;
137
+ }
138
+ /**
139
+ * Stable error codes surfaced via the `onError` callback. Consumers should
140
+ * treat unknown codes as opaque diagnostics rather than control-flow
141
+ * signals.
142
+ */
143
+ type MountErrorCode = 'handshake_failed' | 'iframe_load_failed' | 'origin_mismatch' | 'protocol_incompatible' | 'unknown';
144
+ /**
145
+ * Shape of the error passed to `onError`. `cause` carries the original error
146
+ * (if any) for diagnostic logging; it is typed as `unknown` so consumers
147
+ * narrow it explicitly before use.
148
+ */
149
+ interface MountError {
150
+ readonly code: MountErrorCode;
151
+ readonly message: string;
152
+ readonly cause?: unknown;
153
+ }
154
+ /**
155
+ * Options accepted by both `mount(...)` (vanilla) and `<AtomCircuitSwap />`
156
+ * (React). Required fields are kept to the absolute minimum so the embed
157
+ * stays a one-liner for the validator.
158
+ */
159
+ interface MountOptions {
160
+ /**
161
+ * Validator-supplied affiliate identifier. Forwarded to the widget via the
162
+ * iframe URL so fees route correctly.
163
+ */
164
+ /**
165
+ * Validator referralId. Optional. When omitted (or empty / whitespace),
166
+ * the SDK defaults to the literal string `'general'`, which fans the
167
+ * affiliate fee across all participating Atom Circuit validators at
168
+ * sweep time. Hosts that want fees to stake to a specific validator
169
+ * pass that validator's 8-character hex referralId (or a registered
170
+ * vanity slug).
171
+ */
172
+ referralId?: string;
173
+ /**
174
+ * Override the widget origin. Default `https://atomcircuit.net`. Used by
175
+ * the test suite and local development only.
176
+ */
177
+ origin?: string;
178
+ /**
179
+ * Override the widget path. Default `/embed/swap`.
180
+ */
181
+ path?: string;
182
+ /**
183
+ * Minimum height applied to the iframe before any resize messages arrive.
184
+ * Default `480px`.
185
+ */
186
+ minHeight?: string;
187
+ /**
188
+ * Optional additional CSS class applied to the iframe element.
189
+ */
190
+ className?: string;
191
+ /**
192
+ * Optional inline style merge. `height` and `width` are managed by the SDK
193
+ * and ignored if supplied.
194
+ */
195
+ style?: Partial<CSSStyleDeclaration>;
196
+ /**
197
+ * Fires once the iframe has loaded and the handshake completes.
198
+ */
199
+ onReady?: (payload: ReadyPayload) => void;
200
+ /**
201
+ * Fires on every measured content-height change.
202
+ */
203
+ onResize?: (info: {
204
+ height: number;
205
+ }) => void;
206
+ /**
207
+ * Fires when the user submits a swap (tx broadcast).
208
+ */
209
+ onSwapSubmitted?: (payload: SwapSubmittedPayload) => void;
210
+ /**
211
+ * Fires when a submitted swap confirms on chain.
212
+ */
213
+ onSwapSuccess?: (payload: SwapSuccessPayload) => void;
214
+ /**
215
+ * Fires when a swap fails or is rejected by the wallet.
216
+ */
217
+ onSwapError?: (payload: SwapErrorPayload) => void;
218
+ /**
219
+ * Fires on SDK-level failures (iframe load failure, handshake timeout,
220
+ * origin mismatch, etc). When not supplied the SDK emits a single warning
221
+ * via the injected warn sink and returns. This is distinct from
222
+ * `onSwapError`, which reports widget-level (in-iframe) swap failures.
223
+ */
224
+ onError?: (error: MountError) => void;
225
+ /**
226
+ * Optional theme. See {@link ThemeOptions} for the validated contract.
227
+ * Validation failure silently drops the theme; the iframe falls back to
228
+ * defaults.
229
+ */
230
+ readonly theme?: ThemeOptions;
231
+ /**
232
+ * Optional chrome toggles. See {@link ChromeOptions} for the validated
233
+ * contract. Validation failure silently drops the chrome bundle; the
234
+ * iframe falls back to all-chrome-on defaults. Encoded alongside `theme`
235
+ * in the iframe URL.
236
+ */
237
+ readonly chrome?: ChromeOptions;
238
+ /**
239
+ * CSS `width` applied to the iframe. Default `'100%'` when omitted.
240
+ */
241
+ readonly width?: string;
242
+ /**
243
+ * CSS `max-width` applied to the iframe. Default unset (no cap) when
244
+ * omitted.
245
+ */
246
+ readonly maxWidth?: string;
247
+ /**
248
+ * CSS `padding` applied to the wrapper element around the iframe (NOT to
249
+ * the iframe element itself, since padding on iframes does not behave
250
+ * intuitively across browsers). Default `'0'` when omitted.
251
+ */
252
+ readonly padding?: string;
253
+ }
254
+
255
+ /**
256
+ * Host-side client that wraps Penpal v7 for typed RPC plus a custom
257
+ * event emitter for streamed widget events (resize, ready, swap:*).
258
+ *
259
+ * Strict origin validation is enforced two ways:
260
+ * 1. Penpal's `WindowMessenger({ allowedOrigins })` filters at the messenger layer.
261
+ * 2. A second `MessageEvent` listener double-checks `event.origin` before
262
+ * acknowledging stream events. Two-tier check is intentional: it ensures
263
+ * we never trust a payload coming through a future Penpal change that
264
+ * relaxes origin handling.
265
+ */
266
+
267
+ /**
268
+ * Per-event handler signatures. Keeps the public surface free of `any`.
269
+ */
270
+ interface EventHandlers {
271
+ ready: (payload: ReadyPayload) => void;
272
+ resize: (info: {
273
+ height: number;
274
+ }) => void;
275
+ 'swap:submitted': (payload: SwapSubmittedPayload) => void;
276
+ 'swap:success': (payload: SwapSuccessPayload) => void;
277
+ 'swap:error': (payload: SwapErrorPayload) => void;
278
+ }
279
+ type EventName = keyof EventHandlers;
280
+ interface IframeClientOptions {
281
+ /**
282
+ * The iframe element. Must already be appended to the DOM and have its
283
+ * `src` set so `iframe.contentWindow` is non-null by the time `init()`
284
+ * resolves.
285
+ */
286
+ iframe: HTMLIFrameElement;
287
+ /**
288
+ * Origin to trust for postMessage. Defaults to {@link WIDGET_ORIGIN}.
289
+ */
290
+ allowedOrigin?: string;
291
+ /**
292
+ * Connection timeout in milliseconds. Default 15000.
293
+ */
294
+ timeoutMs?: number;
295
+ /**
296
+ * Optional warning sink. Defaults to a no-op (the SDK refuses to write to
297
+ * console.log per project policy).
298
+ */
299
+ warn?: (message: string) => void;
300
+ }
301
+ /**
302
+ * Typed Penpal RPC client + stream-event hub. One instance per iframe.
303
+ */
304
+ declare class IframeClient {
305
+ private readonly iframe;
306
+ private readonly allowedOrigin;
307
+ private readonly timeoutMs;
308
+ private readonly warn;
309
+ private connection;
310
+ private rawListener;
311
+ private destroyed;
312
+ private handshakeReceived;
313
+ private handshakeResolvers;
314
+ private readonly handlers;
315
+ constructor(opts: IframeClientOptions);
316
+ /**
317
+ * Opens the Penpal connection and starts listening for stream events.
318
+ * Resolves once the remote handshake has been received.
319
+ */
320
+ init(): Promise<HandshakeMessage>;
321
+ /**
322
+ * Subscribe to a named stream event. Returns an unsubscribe function.
323
+ */
324
+ on<K extends EventName>(name: K, handler: EventHandlers[K]): () => void;
325
+ /**
326
+ * Remove a registered handler.
327
+ */
328
+ off<K extends EventName>(name: K, handler: EventHandlers[K]): void;
329
+ /**
330
+ * Tears down the Penpal connection, removes the raw listener, and clears
331
+ * every event subscriber. Safe to call multiple times.
332
+ */
333
+ destroy(): void;
334
+ /**
335
+ * Returns the handshake payload received from the iframe, or null if no
336
+ * handshake has been observed yet.
337
+ */
338
+ getHandshake(): HandshakeMessage | null;
339
+ /**
340
+ * Returns true if the iframe advertised the given capability in its
341
+ * handshake. Returns false when no handshake has been received yet or
342
+ * when the capability is not present.
343
+ *
344
+ * Callers wrapping a method that requires a capability should gate on
345
+ * `client.has('cap')` before invoking it. Calls to capabilities the
346
+ * iframe does not advertise are silent no-ops at the call-site (or
347
+ * resolve to `null`); the iframe is the source of truth for what it
348
+ * implements.
349
+ */
350
+ has(capability: string): boolean;
351
+ /**
352
+ * Test-only seam. Allows unit tests to invoke the message handler without
353
+ * dispatching real `MessageEvent`s through the JSDOM bus.
354
+ */
355
+ _handleMessageForTest(event: MessageEvent): void;
356
+ private handleRawMessage;
357
+ private recordHandshake;
358
+ private waitForHandshake;
359
+ private emitResize;
360
+ private dispatchWidgetEvent;
361
+ private emitReady;
362
+ private emitTyped;
363
+ }
364
+
365
+ /**
366
+ * Vanilla mount factory. Builds the iframe element, applies sandbox attrs,
367
+ * wires up the IframeClient + resize handler, and returns a destroy handle.
368
+ */
369
+
370
+ interface MountResult {
371
+ iframe: HTMLIFrameElement;
372
+ /**
373
+ * Wrapper div the iframe is appended to. Always present in v1.0.0 and
374
+ * later. Hosts that previously relied on `container > iframe` should use
375
+ * `container iframe` or `[data-atom-circuit-embed] iframe` instead. The
376
+ * wrapper carries a position:relative anchor so the pre-handshake
377
+ * loading overlay (the spinner) can absolutely-position over the iframe
378
+ * without leaking into surrounding host layout. Padding (when supplied)
379
+ * is applied to this wrapper, never to the iframe element itself.
380
+ */
381
+ wrapper: HTMLDivElement;
382
+ client: IframeClient;
383
+ destroy(): void;
384
+ }
385
+ /**
386
+ * Creates an iframe, appends it to the container, and connects to the widget.
387
+ * Returns a handle for cleanup.
388
+ */
389
+ declare function mount(container: HTMLElement, opts?: MountOptions): MountResult;
390
+
391
+ export { type ChromeOptions, type MountError, type MountErrorCode, type MountOptions, type MountResult, PROTOCOL_VERSION, type ReadyPayload, type SwapErrorPayload, type SwapRouteSummary, type SwapSubmittedPayload, type SwapSuccessPayload, type ThemeOptions, type WidgetEvent, mount };