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