@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.
- package/CHANGELOG.md +106 -0
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/SECURITY.md +88 -0
- package/dist/atom-circuit.iife.js +2 -0
- package/dist/atom-circuit.iife.js.map +1 -0
- package/dist/index.cjs +694 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +391 -0
- package/dist/index.d.ts +391 -0
- package/dist/index.mjs +691 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react.cjs +744 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +337 -0
- package/dist/react.d.ts +337 -0
- package/dist/react.mjs +742 -0
- package/dist/react.mjs.map +1 -0
- package/package.json +125 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file. The format
|
|
4
|
+
follows Keep a Changelog (https://keepachangelog.com/en/1.1.0/), and this
|
|
5
|
+
project adheres to Semantic Versioning (https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [1.2.1] - 2026-05-29
|
|
8
|
+
|
|
9
|
+
- Re-release of 1.2.0 with provenance attached to a stable commit (the
|
|
10
|
+
prior 1.2.0 release was tagged from a commit that was later rewritten
|
|
11
|
+
during a docs cleanup; the npm tarball itself is unchanged). Bytes
|
|
12
|
+
byte-identical to 1.2.0.
|
|
13
|
+
|
|
14
|
+
## [1.2.0] - 2026-05-29
|
|
15
|
+
|
|
16
|
+
- Docs: consolidate design notes into README.
|
|
17
|
+
|
|
18
|
+
## [1.1.1] - 2026-05-28
|
|
19
|
+
|
|
20
|
+
- Republish; no behavior change. IIFE bundle bytes byte-identical to 1.1.0.
|
|
21
|
+
|
|
22
|
+
## [1.1.0] - 2026-05-28
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `referralId` is now optional on both `mount()` and
|
|
27
|
+
`<AtomCircuitSwap />`. When omitted, undefined, or an empty /
|
|
28
|
+
whitespace-only string, the SDK defaults to `'general'` and the
|
|
29
|
+
affiliate fee fans across all participating Atom Circuit
|
|
30
|
+
validators at sweep time. Existing integrations that pass an
|
|
31
|
+
explicit referralId continue to work without change.
|
|
32
|
+
|
|
33
|
+
## [1.0.1] - 2026-05-28
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Dev-dependency refresh: vitest 4, typescript 6, jsdom 29, size-limit
|
|
38
|
+
12, @types/node 25, and actions/checkout + actions/setup-node v6 in
|
|
39
|
+
CI workflows. No runtime impact; the published IIFE bundle bytes
|
|
40
|
+
are byte-identical to 1.0.0 and the SRI hash is unchanged.
|
|
41
|
+
|
|
42
|
+
## [1.0.0] - 2026-05-28
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- Pre-handshake loading overlay: centered spinner rendered inside the
|
|
47
|
+
wrapper while the iframe is fetching the dapp bundle. Fades out on the
|
|
48
|
+
first `ready` event and on `onError` so a permanent handshake failure
|
|
49
|
+
never leaves a forever-spinning state.
|
|
50
|
+
- `referralId: 'general'` documented as a first-class option for hosts
|
|
51
|
+
that do not represent a single validator. The affiliate fee is split
|
|
52
|
+
across all participating validators (registered Atom Circuit validators
|
|
53
|
+
with at least one prior swap attribution).
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- The iframe is now ALWAYS wrapped in `<div data-atom-circuit-embed>`
|
|
58
|
+
regardless of whether `padding` is supplied. The wrapper carries
|
|
59
|
+
`position: relative` so the loading overlay can absolutely-position
|
|
60
|
+
over the iframe. Hosts using the `container > iframe` direct-child
|
|
61
|
+
selector should switch to `container iframe` or
|
|
62
|
+
`[data-atom-circuit-embed] iframe`.
|
|
63
|
+
- `MountResult.wrapper` is no longer nullable. Existing code that
|
|
64
|
+
checked `if (handle.wrapper)` continues to compile and behave
|
|
65
|
+
correctly; the check is now always truthy.
|
|
66
|
+
|
|
67
|
+
## [0.1.0] - 2026-05-27
|
|
68
|
+
|
|
69
|
+
### Initial release
|
|
70
|
+
|
|
71
|
+
- Iframe-based swap widget embeddable on any website via vanilla JS or
|
|
72
|
+
React, carrying a referralId so swap fees route to the host validator.
|
|
73
|
+
- Theming surface: optional `theme` object with `mode`, `accentColor`,
|
|
74
|
+
`background`, `foreground`, `border`, `radius`, `fontSize`, `fontFamily`.
|
|
75
|
+
Strict validation; invalid themes are silently dropped and the embed
|
|
76
|
+
renders with defaults.
|
|
77
|
+
- Sizing surface: `width`, `maxWidth`, `padding` applied to the wrapper
|
|
78
|
+
element around the iframe.
|
|
79
|
+
- Chrome toggles: `chrome.logo`, `chrome.wallet`, `chrome.validator`,
|
|
80
|
+
`chrome.footer` flags to hide the corresponding embed surfaces.
|
|
81
|
+
- Stable `onError` callback for SDK-level failures (handshake timeout,
|
|
82
|
+
iframe load failure, origin mismatch, protocol incompatibility) plus
|
|
83
|
+
swap-level callbacks (`onSwapSubmitted`, `onSwapSuccess`, `onSwapError`).
|
|
84
|
+
- Penpal v7.x typed RPC layer plus a custom event stream for `ready`,
|
|
85
|
+
`swap:submitted`, `swap:success`, `swap:error`, and `resize`.
|
|
86
|
+
- Hand-rolled ResizeObserver + MutationObserver auto-resize (MIT-licensed;
|
|
87
|
+
no iframe-resizer dependency).
|
|
88
|
+
- Strict `https://atomcircuit.net` origin equality check on every inbound
|
|
89
|
+
postMessage; sandboxed iframe with no `allow-top-navigation`.
|
|
90
|
+
- Cross-origin Playwright integration test covering ready,
|
|
91
|
+
`swap:submitted`, `swap:success`, and destroy lifecycle across two
|
|
92
|
+
127.0.0.1 ports.
|
|
93
|
+
- Vitest + jsdom unit suite covering protocol message validation,
|
|
94
|
+
iframe-client raw-postMessage path, resize behaviour, theme validation,
|
|
95
|
+
error reporting, capability gating, and React wrapper lifecycle.
|
|
96
|
+
- IIFE bundle at `dist/atom-circuit.iife.js` for `<script>`-tag drop-in on
|
|
97
|
+
static sites.
|
|
98
|
+
- PostMessage protocol versioned independently of npm semver.
|
|
99
|
+
|
|
100
|
+
[1.2.1]: https://github.com/cosmosrescue/atom-circuit-embed-sdk/releases/tag/v1.2.1
|
|
101
|
+
[1.2.0]: https://github.com/cosmosrescue/atom-circuit-embed-sdk/releases/tag/v1.2.0
|
|
102
|
+
[1.1.1]: https://github.com/cosmosrescue/atom-circuit-embed-sdk/releases/tag/v1.1.1
|
|
103
|
+
[1.1.0]: https://github.com/cosmosrescue/atom-circuit-embed-sdk/releases/tag/v1.1.0
|
|
104
|
+
[1.0.1]: https://github.com/cosmosrescue/atom-circuit-embed-sdk/releases/tag/v1.0.1
|
|
105
|
+
[1.0.0]: https://github.com/cosmosrescue/atom-circuit-embed-sdk/releases/tag/v1.0.0
|
|
106
|
+
[0.1.0]: https://github.com/cosmosrescue/atom-circuit-embed-sdk/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Atom Circuit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# @atom-circuit/embed-sdk
|
|
2
|
+
|
|
3
|
+
Embed the [Atom Circuit](https://atomcircuit.net) swap widget on any website. Every swap routed through the widget carries a `referralId` so the 0.5% affiliate fee is converted to ATOM and staked to a Cosmos Hub validator chosen by the host site.
|
|
4
|
+
|
|
5
|
+
- License: MIT
|
|
6
|
+
- Bundles: ESM, CJS, IIFE
|
|
7
|
+
- Release notes: [CHANGELOG.md](./CHANGELOG.md).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
For React, Next.js, or any bundled project:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install @atom-circuit/embed-sdk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
For static sites that do not bundle, load the IIFE from a CDN with a pinned Subresource Integrity hash:
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<script
|
|
21
|
+
src="https://unpkg.com/@atom-circuit/embed-sdk@1.2.1/dist/atom-circuit.iife.js"
|
|
22
|
+
integrity="sha384-e0EM289L42Rs5yaVi2w+xv5Pwr6rAK9tLh5caDpIW5ADmulSQ97R3CXxC7T/R7D/"
|
|
23
|
+
crossorigin="anonymous"
|
|
24
|
+
></script>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Each release publishes its hash on the [GitHub release page](https://github.com/cosmosrescue/atom-circuit-embed-sdk/releases). Bump the version pin and the `integrity` value together. Recipe for computing the hash yourself is under [Security](#security).
|
|
28
|
+
|
|
29
|
+
## Getting your referral ID
|
|
30
|
+
|
|
31
|
+
Open your validator page on [atomcircuit.net](https://atomcircuit.net). The referral ID is shown next to your referral link with a Copy button. Either the raw referral ID or your registered validator slug works as `referralId`; both resolve to the same on-chain validator.
|
|
32
|
+
|
|
33
|
+
If you do not represent a validator (community sites, ecosystem aggregators, content creators, podcast hosts) you can still embed the widget. Use `referralId: 'general'` to split the affiliate fee equally across all participating validators (registered Atom Circuit validators that have received at least one swap attribution). The `referralId` option is **optional** since v1.1.0 - omitting it defaults to `'general'`, so the minimal install is literally one `mount()` call with no options.
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
Pick the stack you ship with. Replace `YOUR_REFERRAL_ID` with the value from your validator profile (or the literal string `general`). Every other field is optional.
|
|
38
|
+
|
|
39
|
+
### Vanilla HTML
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<div id="atom-circuit-widget"></div>
|
|
43
|
+
<script src="https://unpkg.com/@atom-circuit/embed-sdk@1.2.1/dist/atom-circuit.iife.js"></script>
|
|
44
|
+
<script>
|
|
45
|
+
AtomCircuit.mount(document.getElementById('atom-circuit-widget'), {
|
|
46
|
+
referralId: 'YOUR_REFERRAL_ID',
|
|
47
|
+
});
|
|
48
|
+
</script>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For production, use the SRI-pinned form from [Install](#install).
|
|
52
|
+
|
|
53
|
+
### React
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { AtomCircuitSwap } from '@atom-circuit/embed-sdk/react';
|
|
57
|
+
|
|
58
|
+
export function SwapPanel() {
|
|
59
|
+
return <AtomCircuitSwap referralId="YOUR_REFERRAL_ID" />;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Next.js (App Router)
|
|
64
|
+
|
|
65
|
+
The SDK is iframe-only at runtime; skip the server bundle with `next/dynamic`:
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
'use client';
|
|
69
|
+
import dynamic from 'next/dynamic';
|
|
70
|
+
|
|
71
|
+
const AtomCircuitSwap = dynamic(
|
|
72
|
+
() => import('@atom-circuit/embed-sdk/react').then((m) => m.AtomCircuitSwap),
|
|
73
|
+
{ ssr: false }
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
export default function Page() {
|
|
77
|
+
return <AtomCircuitSwap referralId="YOUR_REFERRAL_ID" />;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
That is the entire integration. The rest of this README documents what is configurable and how the trust boundary works. Each stack has a fully-wired example under [`examples/`](./examples/) that shows every option and every callback.
|
|
82
|
+
|
|
83
|
+
## Full examples
|
|
84
|
+
|
|
85
|
+
Each stack has a fully-wired example that demonstrates every option and every callback:
|
|
86
|
+
|
|
87
|
+
- Vanilla HTML: [examples/vanilla-html/full.html](./examples/vanilla-html/full.html)
|
|
88
|
+
- React: [examples/react/full.tsx](./examples/react/full.tsx)
|
|
89
|
+
- Next.js: [examples/nextjs/full.tsx](./examples/nextjs/full.tsx)
|
|
90
|
+
|
|
91
|
+
## API surface
|
|
92
|
+
|
|
93
|
+
`mount` (vanilla) and `<AtomCircuitSwap />` (React) expose the same options.
|
|
94
|
+
|
|
95
|
+
### `mount(container, options)`
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const { iframe, wrapper, client, destroy } = AtomCircuit.mount(container, {
|
|
99
|
+
referralId: 'YOUR_REFERRAL_ID',
|
|
100
|
+
// sizing - all optional
|
|
101
|
+
width: '100%',
|
|
102
|
+
maxWidth: '480px',
|
|
103
|
+
minHeight: '520px',
|
|
104
|
+
padding: '16px',
|
|
105
|
+
// appearance - all optional
|
|
106
|
+
theme: { mode: 'dark', accentColor: '#7b61ff', radius: 12 },
|
|
107
|
+
chrome: { logo: true, wallet: true, validator: true, footer: true },
|
|
108
|
+
// callbacks - all optional
|
|
109
|
+
onReady: ({ protocolVersion }) => {},
|
|
110
|
+
onResize: ({ height }) => {},
|
|
111
|
+
onSwapSubmitted: ({ txHash, route }) => {},
|
|
112
|
+
onSwapSuccess: ({ txHash }) => {},
|
|
113
|
+
onSwapError: ({ code, message }) => {},
|
|
114
|
+
onError: ({ code, message }) => {},
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Call `destroy()` when the host removes the widget from the DOM. The returned `wrapper` is the always-present `<div data-atom-circuit-embed>` containing the iframe and the loading overlay.
|
|
119
|
+
|
|
120
|
+
### `<AtomCircuitSwap />`
|
|
121
|
+
|
|
122
|
+
Same options as `mount`, expressed as React props. Re-mounts the iframe only when `referralId`, `origin`, or `path` change; changing `theme`, `chrome`, `width`, `maxWidth`, `padding`, or `minHeight` after the initial mount is a silent no-op so a stylistic tweak does not drop the user's wallet session. To force a re-mount (and accept the wallet session drop), bump a `key=` on the component.
|
|
123
|
+
|
|
124
|
+
## Theming
|
|
125
|
+
|
|
126
|
+
The host SDK is the trust boundary. The theme passes through strict validation, is serialised as compact JSON, and is forwarded to the iframe as a base64-encoded `?theme=` URL parameter. The iframe decodes the validated subset and applies it as CSS custom properties.
|
|
127
|
+
|
|
128
|
+
| Key | Type | Range / format |
|
|
129
|
+
| -------------- | ----------------------------- | -------------------------------------------------------- |
|
|
130
|
+
| `mode` | `'light' \| 'dark' \| 'auto'` | - |
|
|
131
|
+
| `accentColor` | hex string | `#abc` or `#aabbcc` |
|
|
132
|
+
| `background` | hex string | as above |
|
|
133
|
+
| `foreground` | hex string | as above |
|
|
134
|
+
| `border` | hex string | as above |
|
|
135
|
+
| `radius` | number | px, 0-64 inclusive |
|
|
136
|
+
| `fontSize` | number | px, 8-32 inclusive |
|
|
137
|
+
| `fontFamily` | string | CSS-safe subset, no `<>;{}=()`, no newlines, max 200ch |
|
|
138
|
+
|
|
139
|
+
Every field is optional. If any single field fails validation the entire theme is dropped and the widget renders with its defaults; the SDK emits one `console.warn` describing the failure. The widget does not download fonts, so use a `fontFamily` already loaded on the host page.
|
|
140
|
+
|
|
141
|
+
Source: [`src/theme.ts`](./src/theme.ts) for validation, [`src/protocol.ts`](./src/protocol.ts) for the `ThemeOptions` type.
|
|
142
|
+
|
|
143
|
+
### Chrome toggles
|
|
144
|
+
|
|
145
|
+
Hide individual surfaces inside the embed without restyling:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
chrome: {
|
|
149
|
+
logo: false, // Atom Circuit logo (top-left)
|
|
150
|
+
wallet: false, // Connect Wallet button (top-right)
|
|
151
|
+
validator: false, // "Fees stake with <moniker>" badge row
|
|
152
|
+
footer: false, // bottom links / help footer
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Each flag defaults to `true`. A non-boolean drops the entire `chrome` bundle.
|
|
157
|
+
|
|
158
|
+
### Sizing
|
|
159
|
+
|
|
160
|
+
- `width`: any CSS width. Default `'100%'`.
|
|
161
|
+
- `maxWidth`: any CSS max-width. Default unset.
|
|
162
|
+
- `padding`: applied to the wrapper, not the iframe. Default `'0'`.
|
|
163
|
+
- `minHeight`: starting iframe height before the widget reports its content size. Default `'480px'`.
|
|
164
|
+
|
|
165
|
+
The runtime iframe height is managed by the SDK's resize handler and cannot be overridden.
|
|
166
|
+
|
|
167
|
+
## Callbacks
|
|
168
|
+
|
|
169
|
+
| Event | Fires when | Payload |
|
|
170
|
+
| ----------------- | ------------------------------------------------------------------------------------- | -------------------------------- |
|
|
171
|
+
| `onReady` | iframe handshake completes; from here the widget is interactive | `{ protocolVersion }` |
|
|
172
|
+
| `onResize` | iframe content height changes | `{ height }` in px |
|
|
173
|
+
| `onSwapSubmitted` | user signed and the source-chain tx broadcast | `{ txHash, route }` |
|
|
174
|
+
| `onSwapSuccess` | cross-chain delivery confirmed by the indexer | `{ txHash }` (source-chain hash) |
|
|
175
|
+
| `onSwapError` | swap failed inside the iframe or the wallet rejected the signature | `{ code, message }` |
|
|
176
|
+
| `onError` | SDK-level failure: handshake timeout, iframe load failure, origin mismatch, protocol | `{ code, message, cause }` |
|
|
177
|
+
|
|
178
|
+
`onError` covers widget bring-up failures; `onSwapError` covers in-flow swap failures. They are separate so a host can wire different UI for each.
|
|
179
|
+
|
|
180
|
+
`onError` codes are stable strings: `handshake_failed`, `iframe_load_failed`, `origin_mismatch`, `protocol_incompatible`, `unknown`. If `onError` is not supplied, the SDK logs a single `console.warn` and continues. Nothing is thrown.
|
|
181
|
+
|
|
182
|
+
### Capability negotiation
|
|
183
|
+
|
|
184
|
+
The iframe advertises capabilities during the handshake. Probe before relying on one:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const result = AtomCircuit.mount(container, {
|
|
188
|
+
referralId: 'YOUR_REFERRAL_ID',
|
|
189
|
+
onReady: () => {
|
|
190
|
+
if (result.client.has('swap.submit')) {
|
|
191
|
+
// safe to use programmatic submit
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`client.has(name)` returns `false` before the handshake completes and for any capability the iframe did not advertise. Names are case-sensitive.
|
|
198
|
+
|
|
199
|
+
## Persisting across route changes
|
|
200
|
+
|
|
201
|
+
React Router and most SPA routers unmount route-level components when the visitor navigates away. The default behavior is: visitor lands on `/swap`, the widget mounts, the loading spinner runs, the handshake completes. They navigate to `/about`, the widget unmounts (iframe destroyed). They return to `/swap`, the widget remounts from scratch with a fresh spinner. Their wallet session survives via iframe-side browser storage, but in-progress swap state (selected tokens, typed amounts, fetched route) is lost.
|
|
202
|
+
|
|
203
|
+
Three patterns to handle this:
|
|
204
|
+
|
|
205
|
+
### Pattern 1 - React layout hoist (recommended for React SPAs)
|
|
206
|
+
|
|
207
|
+
Mount `<AtomCircuitSwap />` once in a top-level layout that does not unmount across route changes. Toggle CSS visibility per route:
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
'use client';
|
|
211
|
+
import { AtomCircuitSwap } from '@atom-circuit/embed-sdk/react';
|
|
212
|
+
import { usePathname } from 'next/navigation';
|
|
213
|
+
|
|
214
|
+
export function PersistentSwap() {
|
|
215
|
+
const pathname = usePathname();
|
|
216
|
+
return (
|
|
217
|
+
<div style={{ display: pathname === '/swap' ? 'block' : 'none' }}>
|
|
218
|
+
<AtomCircuitSwap referralId="YOUR_REFERRAL_ID" />
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The widget stays mounted across navigations; only `display` toggles. Wallet AND form state preserved. Trade-off: the iframe + dapp instance stays in memory on every page.
|
|
225
|
+
|
|
226
|
+
### Pattern 2 - imperative mount once
|
|
227
|
+
|
|
228
|
+
Use `AtomCircuit.mount()` directly into a persistent DOM container outside the router-managed area. Show or hide via CSS:
|
|
229
|
+
|
|
230
|
+
```html
|
|
231
|
+
<div id="atom-circuit-widget" style="display: none;"></div>
|
|
232
|
+
<script src="https://unpkg.com/@atom-circuit/embed-sdk@1.2.1/dist/atom-circuit.iife.js"></script>
|
|
233
|
+
<script>
|
|
234
|
+
AtomCircuit.mount(document.getElementById('atom-circuit-widget'), {
|
|
235
|
+
referralId: 'YOUR_REFERRAL_ID',
|
|
236
|
+
});
|
|
237
|
+
function showSwap() {
|
|
238
|
+
document.getElementById('atom-circuit-widget').style.display = 'block';
|
|
239
|
+
}
|
|
240
|
+
function hideSwap() {
|
|
241
|
+
document.getElementById('atom-circuit-widget').style.display = 'none';
|
|
242
|
+
}
|
|
243
|
+
</script>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The vanilla `mount()` lifecycle is not tied to React. Same trade-off as Pattern 1: persistent memory cost in exchange for state preservation.
|
|
247
|
+
|
|
248
|
+
### Pattern 3 - accept the reload
|
|
249
|
+
|
|
250
|
+
Zero extra code. Re-handshake on every visit takes 1-3 seconds with the loading spinner. Appropriate when the swap page is the destination rather than a sidebar - which is how Stripe Elements, Mapbox demos, and most embedded widget previews work.
|
|
251
|
+
|
|
252
|
+
## Loading state
|
|
253
|
+
|
|
254
|
+
The wrapper renders a centered spinner overlay during the iframe handshake (typically 1-3s on a warm cache). The overlay fades out on the first `ready` event and is also dismissed if `onError` fires, so a permanent handshake failure never leaves a forever-spinning state. No flash of blank container while the iframe is fetching the dapp bundle.
|
|
255
|
+
|
|
256
|
+
## Security
|
|
257
|
+
|
|
258
|
+
The widget runs inside a sandboxed iframe served from `atomcircuit.net`. It cannot read or write the host page's DOM, cookies, or storage. All host/iframe traffic goes over `postMessage` with origin validation on both sides.
|
|
259
|
+
|
|
260
|
+
### Subresource Integrity for CDN consumers
|
|
261
|
+
|
|
262
|
+
Current SRI hash for `1.1.1`:
|
|
263
|
+
|
|
264
|
+
```html
|
|
265
|
+
<script
|
|
266
|
+
src="https://unpkg.com/@atom-circuit/embed-sdk@1.2.1/dist/atom-circuit.iife.js"
|
|
267
|
+
integrity="sha384-e0EM289L42Rs5yaVi2w+xv5Pwr6rAK9tLh5caDpIW5ADmulSQ97R3CXxC7T/R7D/"
|
|
268
|
+
crossorigin="anonymous"
|
|
269
|
+
></script>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Verify the hash yourself:
|
|
273
|
+
|
|
274
|
+
```sh
|
|
275
|
+
curl -sL https://unpkg.com/@atom-circuit/embed-sdk@1.2.1/dist/atom-circuit.iife.js \
|
|
276
|
+
| openssl dgst -sha384 -binary | openssl base64 -A
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Each release publishes a fresh hash on the [GitHub release page](https://github.com/cosmosrescue/atom-circuit-embed-sdk/releases); bump the version pin and the `integrity` value together. See [SECURITY.md](./SECURITY.md) for the disclosure channel and the full trust boundary.
|
|
280
|
+
|
|
281
|
+
### Sandbox attributes
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms"
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`allow-same-origin` is required so Keplr can inject `window.keplr`. `allow-popups` and its escape variant let wallet popups (Keplr, Leap, Cosmostation) open. `allow-top-navigation` is intentionally omitted to limit clickjacking surface.
|
|
288
|
+
|
|
289
|
+
### Chrome storage partitioning (115+)
|
|
290
|
+
|
|
291
|
+
Chromium 115+ partitions iframe storage by `(iframe origin, top-level site)`. A user who connected their wallet on `validatorA.com` will need to reconnect on `validatorB.com`; each host gets its own isolated wallet session inside the widget.
|
|
292
|
+
|
|
293
|
+
## Versioning
|
|
294
|
+
|
|
295
|
+
- The npm package follows semver. Major bumps signal a breaking change to `mount()` or `<AtomCircuitSwap />`.
|
|
296
|
+
- The iframe wire protocol version (`PROTOCOL_VERSION`, currently `1.0.0`) is independent of the npm package version. SDK and iframe negotiate at handshake time; a major mismatch surfaces as `onError` with `code: 'protocol_incompatible'`.
|
|
297
|
+
|
|
298
|
+
## Compatibility
|
|
299
|
+
|
|
300
|
+
- React: `>=17 <20` (peer dependency, optional).
|
|
301
|
+
- Modern evergreen browsers, ES2020 baseline. Tested: Chromium 115+, Firefox 115+, Safari 16+.
|
|
302
|
+
- Desktop browser extensions (Keplr, Leap, Cosmostation) are the primary wallet path. Mobile WalletConnect inside an iframe has documented iOS Safari issues.
|
|
303
|
+
- Node.js `>=20` for development of this package.
|
|
304
|
+
- No GPL or other non-permissive runtime dependencies.
|
|
305
|
+
|
|
306
|
+
## Cosmiframe coexistence
|
|
307
|
+
|
|
308
|
+
The Atom Circuit dapp loads [Cosmiframe](https://github.com/DA0-DA0/cosmiframe) for an unrelated integration. When the embedded widget runs on a host page, Cosmiframe logs `Failed to detect Cosmiframe parent of allowed origin` to the browser console. This is non-blocking noise from the dapp side; the swap widget itself functions normally and your `onReady` / `onSwap*` callbacks fire as expected.
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
MIT. See [LICENSE](./LICENSE).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Trust model
|
|
4
|
+
|
|
5
|
+
`@atom-circuit/embed-sdk` ships an iframe served from `https://atomcircuit.net` into a host page. The iframe is treated as an independent security principal: the SDK never reads anything from the iframe's DOM and never exposes host-page state to the iframe beyond the `referralId` it places in the URL.
|
|
6
|
+
|
|
7
|
+
## Origin validation
|
|
8
|
+
|
|
9
|
+
The SDK enforces strict origin equality on every postMessage:
|
|
10
|
+
|
|
11
|
+
- Penpal's `WindowMessenger` is configured with `allowedOrigins: ['https://atomcircuit.net']`.
|
|
12
|
+
- A second `MessageEvent` listener double-checks `event.origin === 'https://atomcircuit.net'` AND `event.source === iframe.contentWindow` before dispatching any stream event to host-side subscribers.
|
|
13
|
+
|
|
14
|
+
No wildcard origin (`*`) is ever used. Hosts that need to point at a staging build during local development can override the origin via the `origin` option; this also retargets the postMessage allow-list, so the trust boundary stays explicit.
|
|
15
|
+
|
|
16
|
+
## Sandbox
|
|
17
|
+
|
|
18
|
+
The iframe is created with:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`allow-top-navigation` is intentionally omitted. The iframe cannot navigate the host page, which removes the most common clickjacking attack vector. `allow-same-origin` is required so the host's browser extension wallets (Keplr, Leap, Cosmostation) can inject `window.keplr` and similar into the iframe; the SDK does not relax this to grant additional capability.
|
|
25
|
+
|
|
26
|
+
## Clickjacking trust boundary
|
|
27
|
+
|
|
28
|
+
The host page is fully responsible for protecting the iframe from clickjacking. Concretely, the host should:
|
|
29
|
+
|
|
30
|
+
- Set `Content-Security-Policy: frame-ancestors` on its own origin to whatever ancestry policy makes sense for the host site (the SDK cannot do this on the host's behalf).
|
|
31
|
+
- Avoid overlaying transparent UI on top of the widget. The SDK does not detect such overlays.
|
|
32
|
+
- Consider serving the embedding page over HTTPS only; mixed-content rules will already block the widget on HTTP pages.
|
|
33
|
+
|
|
34
|
+
The iframe origin (`atomcircuit.net`) sets its own `Content-Security-Policy` to control what ancestors may embed it, and uses `X-Frame-Options` plus `Permissions-Policy` to constrain the widget's surface.
|
|
35
|
+
|
|
36
|
+
## CDN integrity (Subresource Integrity)
|
|
37
|
+
|
|
38
|
+
When loading the IIFE bundle from a public CDN (such as unpkg) instead of installing via npm, pin the file with a SHA-384 Subresource Integrity hash and the `crossorigin="anonymous"` attribute. This way a CDN compromise cannot ship a different SDK to your visitors.
|
|
39
|
+
|
|
40
|
+
Compute the hash against the artifact that was published:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
openssl dgst -sha384 -binary dist/atom-circuit.iife.js | openssl base64 -A
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Use the output in the `<script>` tag:
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<script
|
|
50
|
+
src="https://unpkg.com/@atom-circuit/embed-sdk@1.2.1/dist/atom-circuit.iife.js"
|
|
51
|
+
integrity="sha384-e0EM289L42Rs5yaVi2w+xv5Pwr6rAK9tLh5caDpIW5ADmulSQ97R3CXxC7T/R7D/"
|
|
52
|
+
crossorigin="anonymous"
|
|
53
|
+
></script>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If the bytes do not match, the browser refuses to execute the script. Operators rotating a release update the hash on every `<script>` tag they control.
|
|
57
|
+
|
|
58
|
+
## Bundled dependencies
|
|
59
|
+
|
|
60
|
+
The only runtime dependency is `penpal` (MIT), pinned to the `7.0.x` range. No GPL or other copyleft dependency ships in the published artifacts.
|
|
61
|
+
|
|
62
|
+
## Scope
|
|
63
|
+
|
|
64
|
+
In scope for this policy:
|
|
65
|
+
|
|
66
|
+
- The SDK source under `src/` and the published artifacts under `dist/` (npm + IIFE).
|
|
67
|
+
- The postMessage protocol surface (handshake, resize, widget event stream).
|
|
68
|
+
- Origin and source-window validation in `IframeClient`.
|
|
69
|
+
- Theme + chrome validation in `src/theme.ts`.
|
|
70
|
+
- Sandbox attributes applied by `mount()`.
|
|
71
|
+
|
|
72
|
+
Out of scope:
|
|
73
|
+
|
|
74
|
+
- The Atom Circuit dapp that runs inside the iframe (report to that repository's security policy).
|
|
75
|
+
- Wallet extensions injected via `allow-same-origin` (report to the wallet vendor).
|
|
76
|
+
- Misconfiguration on the host site (CSP, `frame-ancestors`, transport security).
|
|
77
|
+
- Build-time tooling and test fixtures (`tsup`, `vitest`, `playwright`).
|
|
78
|
+
|
|
79
|
+
## Reporting
|
|
80
|
+
|
|
81
|
+
Two channels:
|
|
82
|
+
|
|
83
|
+
1. **GitHub Security Advisories** (preferred): open a private advisory under the repository's Security tab via "Report a vulnerability". This gives us a private, auditable conversation without exposing the issue to the public.
|
|
84
|
+
2. **Email**: send a report to `security@atomcircuit.net`. If you would like the report encrypted, request the maintainer's PGP key in your first message and attach a key of your own; the project's PGP fingerprint is `<TO_BE_INSERTED_BY_OPERATOR>` (operator to insert real fingerprint).
|
|
85
|
+
|
|
86
|
+
Include enough detail to reproduce the issue. We will acknowledge within 72 hours and aim to ship a fix or mitigation within 14 days for high-severity issues.
|
|
87
|
+
|
|
88
|
+
Do NOT open a public GitHub issue for an unpatched vulnerability.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var AtomCircuit=(function(exports){'use strict';var $e=Object.defineProperty;var ue=e=>{throw TypeError(e)};var Ge=(e,t,n)=>t in e?$e(e,t,{enumerable:true,configurable:true,writable:true,value:n}):e[t]=n;var R=(e,t,n)=>Ge(e,typeof t!="symbol"?t+"":t,n),fe=(e,t,n)=>t.has(e)||ue("Cannot "+n);var d=(e,t,n)=>(fe(e,t,"read from private field"),n?n.call(e):t.get(e)),g=(e,t,n)=>t.has(e)?ue("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),k=(e,t,n,r)=>(fe(e,t,"write to private field"),t.set(e,n),n);var Ke=class extends Error{constructor(t,n){super(n);R(this,"code");this.name="PenpalError",this.code=t;}},v=Ke,Ue=e=>({name:e.name,message:e.message,stack:e.stack,penpalCode:e instanceof v?e.code:void 0}),Ye=({name:e,message:t,stack:n,penpalCode:r})=>{let i=r?new v(r,t):new Error(t);return i.name=e,i.stack=n,i},Be=Symbol("Reply"),te,ge,Xe=(ge=class{constructor(e,t){R(this,"value");R(this,"transferables");g(this,te,Be);this.value=e,this.transferables=t?.transferables;}},te=new WeakMap,ge),qe=Xe,S="penpal",X=e=>typeof e=="object"&&e!==null,Me=e=>typeof e=="function",Je=e=>X(e)&&e.namespace===S,Y=e=>e.type==="SYN",re=e=>e.type==="ACK1",Q=e=>e.type==="ACK2",Ee=e=>e.type==="CALL",Se=e=>e.type==="REPLY",Ze=e=>e.type==="DESTROY",be=(e,t=[])=>{let n=[];for(let r of Object.keys(e)){let i=e[r];Me(i)?n.push([...t,r]):X(i)&&n.push(...be(i,[...t,r]));}return n},Qe=(e,t)=>{let n=e.reduce((r,i)=>X(r)?r[i]:void 0,t);return Me(n)?n:void 0},_=e=>e.join("."),he=(e,t,n)=>({namespace:S,channel:e,type:"REPLY",callId:t,isError:true,...n instanceof Error?{value:Ue(n),isSerializedErrorInstance:true}:{value:n}}),et=(e,t,n,r)=>{let i=false,a=async u=>{if(i||!Ee(u))return;r?.(`Received ${_(u.methodPath)}() call`,u);let{methodPath:l,args:o,id:s}=u,c,p;try{let h=Qe(l,t);if(!h)throw new v("METHOD_NOT_FOUND",`Method \`${_(l)}\` is not found.`);let m=await h(...o);m instanceof qe&&(p=m.transferables,m=await m.value),c={namespace:S,channel:n,type:"REPLY",callId:s,value:m};}catch(h){c=he(n,s,h);}if(!i)try{r?.(`Sending ${_(l)}() reply`,c),e.sendMessage(c,p);}catch(h){throw h.name==="DataCloneError"&&(c=he(n,s,h),r?.(`Sending ${_(l)}() reply`,c),e.sendMessage(c)),h}};return e.addMessageHandler(a),()=>{i=true,e.removeMessageHandler(a);}},tt=et,Ie=crypto.randomUUID?.bind(crypto)??(()=>new Array(4).fill(0).map(()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16)).join("-")),nt=Symbol("CallOptions"),ne,ve,rt=(ve=class{constructor(e){R(this,"transferables");R(this,"timeout");g(this,ne,nt);this.transferables=e?.transferables,this.timeout=e?.timeout;}},ne=new WeakMap,ve),it=rt,ot=new Set(["apply","call","bind"]),Re=(e,t,n=[])=>new Proxy(n.length?()=>{}:Object.create(null),{get(r,i){if(i!=="then")return n.length&&ot.has(i)?Reflect.get(r,i):Re(e,t,[...n,i])},apply(r,i,a){return e(n,a)}}),pe=e=>new v("CONNECTION_DESTROYED",`Method call ${_(e)}() failed due to destroyed connection`),at=(e,t,n)=>{let r=false,i=new Map,a=o=>{if(!Se(o))return;let{callId:s,value:c,isError:p,isSerializedErrorInstance:h}=o,m=i.get(s);m&&(i.delete(s),n?.(`Received ${_(m.methodPath)}() call`,o),p?m.reject(h?Ye(c):c):m.resolve(c));};return e.addMessageHandler(a),{remoteProxy:Re((o,s)=>{if(r)throw pe(o);let c=Ie(),p=s[s.length-1],h=p instanceof it,{timeout:m,transferables:x}=h?p:{},w=h?s.slice(0,-1):s;return new Promise((W,b)=>{let I=m!==void 0?window.setTimeout(()=>{i.delete(c),b(new v("METHOD_CALL_TIMEOUT",`Method call ${_(o)}() timed out after ${m}ms`));},m):void 0;i.set(c,{methodPath:o,resolve:W,reject:b,timeoutId:I});try{let C={namespace:S,channel:t,type:"CALL",id:c,methodPath:o,args:w};n?.(`Sending ${_(o)}() call`,C),e.sendMessage(C,x);}catch(C){b(new v("TRANSMISSION_FAILED",C.message));}})},n),destroy:()=>{r=true,e.removeMessageHandler(a);for(let{methodPath:o,reject:s,timeoutId:c}of i.values())clearTimeout(c),s(pe(o));i.clear();}}},st=at,dt=()=>{let e,t;return {promise:new Promise((r,i)=>{e=r,t=i;}),resolve:e,reject:t}},lt=dt,ee="deprecated-penpal",ct=e=>X(e)&&"penpal"in e,ut=e=>e.split("."),ye=e=>e.join("."),ft=e=>{try{return JSON.stringify(e)}catch{return String(e)}},ke=e=>new v("TRANSMISSION_FAILED",`Unexpected message to translate: ${ft(e)}`),ht=e=>{if(e.penpal==="syn")return {namespace:S,channel:void 0,type:"SYN",participantId:ee};if(e.penpal==="ack")return {namespace:S,channel:void 0,type:"ACK2"};if(e.penpal==="call")return {namespace:S,channel:void 0,type:"CALL",id:e.id,methodPath:ut(e.methodName),args:e.args};if(e.penpal==="reply")return e.resolution==="fulfilled"?{namespace:S,channel:void 0,type:"REPLY",callId:e.id,value:e.returnValue}:{namespace:S,channel:void 0,type:"REPLY",callId:e.id,isError:true,...e.returnValueIsError?{value:e.returnValue,isSerializedErrorInstance:true}:{value:e.returnValue}};throw ke(e)},pt=e=>{if(re(e))return {penpal:"synAck",methodNames:e.methodPaths.map(ye)};if(Ee(e))return {penpal:"call",id:e.id,methodName:ye(e.methodPath),args:e.args};if(Se(e))return e.isError?{penpal:"reply",id:e.callId,resolution:"rejected",...e.isSerializedErrorInstance?{returnValue:e.value,returnValueIsError:true}:{returnValue:e.value}}:{penpal:"reply",id:e.callId,resolution:"fulfilled",returnValue:e.value};throw ke(e)},yt=({messenger:e,methods:t,timeout:n,channel:r,log:i})=>{let a=Ie(),u,l=[],o=false,s=be(t),{promise:c,resolve:p,reject:h}=lt(),m=n!==void 0?setTimeout(()=>{h(new v("CONNECTION_TIMEOUT",`Connection timed out after ${n}ms`));},n):void 0,x=()=>{for(let y of l)y();},w=()=>{if(o)return;l.push(tt(e,t,r,i));let{remoteProxy:y,destroy:O}=st(e,r,i);l.push(O),clearTimeout(m),o=true,p({remoteProxy:y,destroy:x});},W=()=>{let y={namespace:S,type:"SYN",channel:r,participantId:a};i?.("Sending handshake SYN",y);try{e.sendMessage(y);}catch(O){h(new v("TRANSMISSION_FAILED",O.message));}},b=y=>{if(i?.("Received handshake SYN",y),y.participantId===u&&u!==ee||(u=y.participantId,W(),!(a>u||u===ee)))return;let T={namespace:S,channel:r,type:"ACK1",methodPaths:s};i?.("Sending handshake ACK1",T);try{e.sendMessage(T);}catch(ce){h(new v("TRANSMISSION_FAILED",ce.message));return}},I=y=>{i?.("Received handshake ACK1",y);let O={namespace:S,channel:r,type:"ACK2"};i?.("Sending handshake ACK2",O);try{e.sendMessage(O);}catch(T){h(new v("TRANSMISSION_FAILED",T.message));return}w();},C=y=>{i?.("Received handshake ACK2",y),w();},j=y=>{Y(y)&&b(y),re(y)&&I(y),Q(y)&&C(y);};return e.addMessageHandler(j),l.push(()=>e.removeMessageHandler(j)),W(),c},mt=yt,gt=e=>{let t=false,n;return (...r)=>(t||(t=true,n=e(...r)),n)},vt=gt,me=new WeakSet,wt=({messenger:e,methods:t={},timeout:n,channel:r,log:i})=>{if(!e)throw new v("INVALID_ARGUMENT","messenger must be defined");if(me.has(e))throw new v("INVALID_ARGUMENT","A messenger can only be used for a single connection");me.add(e);let a=[e.destroy],u=vt(s=>{if(s){let c={namespace:S,channel:r,type:"DESTROY"};try{e.sendMessage(c);}catch{}}for(let c of a)c();i?.("Connection destroyed");}),l=s=>Je(s)&&s.channel===r;return {promise:(async()=>{try{e.initialize({log:i,validateReceivedMessage:l}),e.addMessageHandler(p=>{Ze(p)&&u(!1);});let{remoteProxy:s,destroy:c}=await mt({messenger:e,methods:t,timeout:n,channel:r,log:i});return a.push(c),s}catch(s){throw u(true),s}})(),destroy:()=>{u(true);}}},Ce=wt,A,H,P,V,L,N,E,D,B,$,K,U,G,we,Mt=(we=class{constructor({remoteWindow:e,allowedOrigins:t}){g(this,A);g(this,H);g(this,P);g(this,V);g(this,L);g(this,N,new Set);g(this,E);g(this,D,false);R(this,"initialize",({log:e,validateReceivedMessage:t})=>{k(this,P,e),k(this,V,t),window.addEventListener("message",d(this,U));});R(this,"sendMessage",(e,t)=>{if(Y(e)){let n=d(this,$).call(this,e);d(this,A).postMessage(e,{targetOrigin:n,transfer:t});return}if(re(e)||d(this,D)){let n=d(this,D)?pt(e):e,r=d(this,$).call(this,e);d(this,A).postMessage(n,{targetOrigin:r,transfer:t});return}if(Q(e)){let{port1:n,port2:r}=new MessageChannel;k(this,E,n),n.addEventListener("message",d(this,G)),n.start();let i=[r,...t||[]],a=d(this,$).call(this,e);d(this,A).postMessage(e,{targetOrigin:a,transfer:i});return}if(d(this,E)){d(this,E).postMessage(e,{transfer:t});return}throw new v("TRANSMISSION_FAILED","Cannot send message because the MessagePort is not connected")});R(this,"addMessageHandler",e=>{d(this,N).add(e);});R(this,"removeMessageHandler",e=>{d(this,N).delete(e);});R(this,"destroy",()=>{window.removeEventListener("message",d(this,U)),d(this,K).call(this),d(this,N).clear();});g(this,B,e=>d(this,H).some(t=>t instanceof RegExp?t.test(e):t===e||t==="*"));g(this,$,e=>{if(Y(e))return "*";if(!d(this,L))throw new v("TRANSMISSION_FAILED","Cannot send message because the remote origin is not established");return d(this,L)==="null"&&d(this,H).includes("*")?"*":d(this,L)});g(this,K,()=>{d(this,E)?.removeEventListener("message",d(this,G)),d(this,E)?.close(),k(this,E,void 0);});g(this,U,({source:e,origin:t,ports:n,data:r})=>{var i,a,u,l,o;if(e===d(this,A)){if(ct(r)){(i=d(this,P))==null||i.call(this,"Please upgrade the child window to the latest version of Penpal."),k(this,D,true);try{r=ht(r);}catch(s){(a=d(this,P))==null||a.call(this,`Failed to translate deprecated message: ${s.message}`);return}}if((u=d(this,V))!=null&&u.call(this,r)){if(!d(this,B).call(this,t)){(l=d(this,P))==null||l.call(this,`Received a message from origin \`${t}\` which did not match allowed origins \`[${d(this,H).join(", ")}]\``);return}if(Y(r)&&(d(this,K).call(this),k(this,L,t)),Q(r)&&!d(this,D)){if(k(this,E,n[0]),!d(this,E)){(o=d(this,P))==null||o.call(this,"Ignoring ACK2 because it did not include a MessagePort");return}d(this,E).addEventListener("message",d(this,G)),d(this,E).start();}for(let s of d(this,N))s(r);}}});g(this,G,({data:e})=>{var t;if((t=d(this,V))!=null&&t.call(this,e))for(let n of d(this,N))n(e);});if(!e)throw new v("INVALID_ARGUMENT","remoteWindow must be defined");k(this,A,e),k(this,H,t?.length?t:[window.origin]);}},A=new WeakMap,H=new WeakMap,P=new WeakMap,V=new WeakMap,L=new WeakMap,N=new WeakMap,E=new WeakMap,D=new WeakMap,B=new WeakMap,$=new WeakMap,K=new WeakMap,U=new WeakMap,G=new WeakMap,we),Oe=Mt;var F="1.0.0",z="https://atomcircuit.net",Te="/embed/swap",oe=e=>typeof e=="object"&&e!==null,ie=e=>typeof e=="string",Et=e=>typeof e=="number"&&Number.isFinite(e);function St(e){return !oe(e)||e.type!=="handshake"||!ie(e.protocolVersion)||!Array.isArray(e.capabilities)?false:e.capabilities.every(ie)}function ae(e){return !oe(e)||e.type!=="atomcircuit:resize"?false:Et(e.height)&&e.height>=0}var bt=new Set(["ready","swap:submitted","swap:success","swap:error"]);function se(e){return !oe(e)||e.type!=="atomcircuit:event"||!ie(e.name)?false:bt.has(e.name)}function Ae(e){return St(e)||ae(e)||se(e)}function Pe(e,t){let n=e.split(".")[0],r=t.split(".")[0];return n!==void 0&&n===r}var It=e=>{},q=class{constructor(t){this.connection=null;this.rawListener=null;this.destroyed=false;this.handshakeReceived=null;this.handshakeResolvers=[];this.handlers={ready:new Set,resize:new Set,"swap:submitted":new Set,"swap:success":new Set,"swap:error":new Set};this.iframe=t.iframe,this.allowedOrigin=t.allowedOrigin??z,this.timeoutMs=t.timeoutMs??15e3,this.warn=t.warn??It;}async init(){if(this.destroyed)throw new Error("IframeClient: cannot init after destroy()");if(this.connection)throw new Error("IframeClient: already initialised");let t=this.iframe.contentWindow;if(!t)throw new Error("IframeClient: iframe.contentWindow is null");this.rawListener=a=>{this.handleRawMessage(a);},window.addEventListener("message",this.rawListener);let n=new Oe({remoteWindow:t,allowedOrigins:[this.allowedOrigin]}),r={handshake:a=>{this.recordHandshake(a);}},i=Ce;return this.connection=i({messenger:n,methods:r,timeout:this.timeoutMs}),await this.connection.promise,this.waitForHandshake()}on(t,n){return this.handlers[t].add(n),()=>this.off(t,n)}off(t,n){this.handlers[t].delete(n);}destroy(){if(!this.destroyed){this.destroyed=true,this.rawListener&&(window.removeEventListener("message",this.rawListener),this.rawListener=null),this.connection&&(this.connection.destroy(),this.connection=null);for(let t of Object.values(this.handlers))t.clear();this.handshakeResolvers=[];}}getHandshake(){return this.handshakeReceived}has(t){let n=this.handshakeReceived?.capabilities;if(!n)return false;for(let r of n)if(r===t)return true;return false}_handleMessageForTest(t){this.handleRawMessage(t);}handleRawMessage(t){if(t.origin!==this.allowedOrigin||t.source!==this.iframe.contentWindow)return;let n=t.data;if(Ae(n)){if(n.type==="handshake"){this.recordHandshake(n);return}if(ae(n)){this.emitResize(n.height);return}se(n)&&this.dispatchWidgetEvent(n);}}recordHandshake(t){this.handshakeReceived=t,Pe(F,t.protocolVersion)||this.warn(`Atom Circuit embed: protocol mismatch (sdk=${F}, iframe=${t.protocolVersion}). Some features may be unavailable.`);let n=this.handshakeResolvers;this.handshakeResolvers=[];for(let r of n)r(t);}waitForHandshake(){return this.handshakeReceived?Promise.resolve(this.handshakeReceived):new Promise((t,n)=>{let r=setTimeout(()=>{let a=this.handshakeResolvers.indexOf(i);a>=0&&this.handshakeResolvers.splice(a,1),n(new Error("IframeClient: handshake timeout"));},this.timeoutMs),i=a=>{clearTimeout(r),t(a);};this.handshakeResolvers.push(i);})}emitResize(t){for(let n of this.handlers.resize)n({height:t});}dispatchWidgetEvent(t){switch(t.name){case "ready":this.emitReady(t.payload??{protocolVersion:this.handshakeReceived?.protocolVersion??"unknown"});return;case "swap:submitted":this.emitTyped("swap:submitted",t.payload);return;case "swap:success":this.emitTyped("swap:success",t.payload);return;case "swap:error":this.emitTyped("swap:error",t.payload);return;default:return}}emitReady(t){for(let n of this.handlers.ready)n(t);}emitTyped(t,n){for(let r of this.handlers[t])r(n);}};var Rt="480px";function kt(e){let n=e.trim().match(/^([0-9]+(?:\.[0-9]+)?)(px)?$/i);if(!n)return 0;let r=n[1];if(r===void 0)return 0;let i=Number.parseFloat(r);return Number.isFinite(i)?i:0}function Ne(e){let{iframe:t,client:n}=e,r=e.minHeight??Rt,i=kt(r);t.style.minHeight=r,t.style.height||(t.style.height=r);let a=null,u=-1,l=false,o=p=>{if(l)return;let h=Math.max(p,i);h!==u&&(t.style.height=`${h}px`,u=h);},s=p=>{if(!l){if(a!==null&&cancelAnimationFrame(a),typeof window>"u"||typeof window.requestAnimationFrame!="function"){o(p);return}a=window.requestAnimationFrame(()=>{a=null,o(p);});}},c=n.on("resize",({height:p})=>{s(p);});return {destroy(){l||(l=true,a!==null&&typeof window<"u"&&typeof window.cancelAnimationFrame=="function"&&window.cancelAnimationFrame(a),a=null,c());}}}var Ct=/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/,Ot=/^[a-zA-Z0-9 ,'".\-]+$/;var Tt=new Set(["light","dark","auto"]),xe=e=>typeof e=="object"&&e!==null,de=e=>typeof e=="string",_e=e=>typeof e=="number"&&Number.isFinite(e);function J(e){return de(e)&&Ct.test(e)}function At(e){return de(e)&&Tt.has(e)}function Pt(e){return !de(e)||e.length===0||e.length>200?false:Ot.test(e)}function He(e){if(!xe(e))return null;let t={};if(e.mode!==void 0){if(!At(e.mode))return null;t.mode=e.mode;}if(e.accentColor!==void 0){if(!J(e.accentColor))return null;t.accentColor=e.accentColor;}if(e.background!==void 0){if(!J(e.background))return null;t.background=e.background;}if(e.foreground!==void 0){if(!J(e.foreground))return null;t.foreground=e.foreground;}if(e.border!==void 0){if(!J(e.border))return null;t.border=e.border;}if(e.radius!==void 0){let n=e.radius;if(!_e(n)||n<0||n>64)return null;t.radius=n;}if(e.fontSize!==void 0){let n=e.fontSize;if(!_e(n)||n<8||n>32)return null;t.fontSize=n;}if(e.fontFamily!==void 0){if(!Pt(e.fontFamily))return null;t.fontFamily=e.fontFamily;}return t}var Z=e=>typeof e=="boolean";function Le(e){if(!xe(e))return null;let t={};if(e.logo!==void 0){if(!Z(e.logo))return null;t.logo=e.logo;}if(e.wallet!==void 0){if(!Z(e.wallet))return null;t.wallet=e.wallet;}if(e.validator!==void 0){if(!Z(e.validator))return null;t.validator=e.validator;}if(e.footer!==void 0){if(!Z(e.footer))return null;t.footer=e.footer;}return t}function De(e,t){let n={};for(let[i,a]of Object.entries(e))a!=null&&(n[i]=a);if(t&&Object.keys(t).length>0){let i={};for(let[a,u]of Object.entries(t))u!=null&&(i[a]=u);Object.keys(i).length>0&&(n.chrome=i);}let r=JSON.stringify(n);return typeof btoa=="function"?btoa(r):Buffer.from(r,"utf-8").toString("base64")}var Fe=15e3,le="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms",Nt='<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><circle cx="16" cy="16" r="13" stroke="#7B61FF" stroke-width="2.5" stroke-opacity="0.28"/><path d="M29 16a13 13 0 0 0-13-13" stroke="#33D6FF" stroke-width="2.5" stroke-linecap="round"/></svg>';function ze(e){let t=(e.origin??z).replace(/\/$/,""),n=e.path??Te,r=new URLSearchParams;r.set("ref",e.referralId),r.set("v",F);let i=e.theme!==void 0?He(e.theme):null;e.theme!==void 0&&i===null&&e.warn&&e.warn("Atom Circuit embed: theme validation failed, falling back to defaults");let a=e.chrome!==void 0?Le(e.chrome):null;e.chrome!==void 0&&a===null&&e.warn&&e.warn("Atom Circuit embed: chrome validation failed, falling back to defaults"),(i!==null||a!==null)&&r.set("theme",De(i??{},a??void 0));let u=n.includes("?")?"&":"?";return `${t}${n}${u}${r.toString()}`}function _t(e,t){if(t)for(let n of Object.keys(t)){if(n==="height"||n==="width")continue;let r=t[n];typeof r=="string"&&(e.style[n]=r);}}function xt(e,t,n){n.width!==void 0&&(e.style.width=n.width),n.maxWidth!==void 0&&(e.style.maxWidth=n.maxWidth),n.padding!==void 0&&(t.style.padding=n.padding);}function We(e,t={}){if(!e||typeof e.appendChild!="function")throw new TypeError("mount: container must be an HTMLElement");let n=typeof t.referralId=="string"?t.referralId.trim():"",r=n.length>0?n:"general",i=typeof console<"u"&&typeof console.warn=="function"?f=>{console.warn(f);}:()=>{},a=()=>{},u=f=>{if(a(),t.onError){t.onError(f);return}i(`Atom Circuit embed: ${f.code}: ${f.message}`);},l=document.createElement("div");l.setAttribute("data-atom-circuit-embed",""),l.style.position="relative",l.style.width="100%",l.style.display="block";let o=document.createElement("iframe");o.src=ze({referralId:r,...t.origin!==void 0?{origin:t.origin}:{},...t.path!==void 0?{path:t.path}:{},...t.theme!==void 0?{theme:t.theme}:{},...t.chrome!==void 0?{chrome:t.chrome}:{},warn:i}),o.setAttribute("sandbox",le),o.setAttribute("allow","clipboard-write; clipboard-read"),o.setAttribute("title","Atom Circuit swap widget"),o.setAttribute("loading","lazy"),o.setAttribute("referrerpolicy","strict-origin-when-cross-origin"),o.style.width="100%",o.style.border="0",o.style.display="block",o.style.colorScheme="normal",o.style.position="relative",o.style.zIndex="1",_t(o,t.style),xt(o,l,t),t.className&&(o.className=t.className);let s=document.createElement("div");s.setAttribute("data-atom-circuit-loader",""),s.setAttribute("aria-hidden","true"),s.style.position="absolute",s.style.inset="0",s.style.display="flex",s.style.alignItems="center",s.style.justifyContent="center",s.style.pointerEvents="none",s.style.color="#888888",s.style.transition="opacity 0.08s ease-out",s.style.opacity="1",s.style.zIndex="2",s.innerHTML=Nt;let c=s.querySelector("svg"),p=null;c&&typeof c.animate=="function"&&(c.style.transformOrigin="center",p=c.animate([{transform:"rotate(0deg)"},{transform:"rotate(360deg)"}],{duration:900,iterations:1/0,easing:"linear"}));let h=false;a=()=>{if(!h){if(h=true,s.style.opacity="0",p){try{p.cancel();}catch{}p=null;}setTimeout(()=>{s.parentNode&&s.parentNode.removeChild(s);},100);}};let m=f=>{u({code:"iframe_load_failed",message:"Iframe failed to load the widget URL",cause:f});};o.addEventListener("error",m);let x=()=>{a();};o.addEventListener("load",x);try{l.appendChild(o),l.appendChild(s),e.appendChild(l);}catch(f){throw o.removeEventListener("error",m),o.removeEventListener("load",x),f}let w=new q({iframe:o,allowedOrigin:t.origin??z}),W=Ne({iframe:o,client:w,...t.minHeight!==void 0?{minHeight:t.minHeight}:{}}),b=[];if(t.onReady){let f=t.onReady;b.push(w.on("ready",M=>f(M)));}if(t.onResize){let f=t.onResize;b.push(w.on("resize",M=>f(M)));}if(t.onSwapSubmitted){let f=t.onSwapSubmitted;b.push(w.on("swap:submitted",M=>f(M)));}if(t.onSwapSuccess){let f=t.onSwapSuccess;b.push(w.on("swap:success",M=>f(M)));}if(t.onSwapError){let f=t.onSwapError;b.push(w.on("swap:error",M=>f(M)));}let I=null,C=false,j=()=>{I!==null&&(clearTimeout(I),I=null);},y=w.on("ready",()=>{C=true,j(),a(),y();}),O=new Promise((f,M)=>{I=setTimeout(()=>{M(new Error(`Iframe handshake timed out after ${Fe}ms`));},Fe);});Promise.race([w.init(),O]).then(()=>{j();}).catch(f=>{if(j(),C||T)return;let M=f instanceof Error?f.message:String(f),Ve=Ht(M);u({code:Ve,message:M,cause:f});});let T=false;return {iframe:o,wrapper:l,client:w,destroy:()=>{if(!T){T=true,I!==null&&(clearTimeout(I),I=null),o.removeEventListener("error",m),o.removeEventListener("load",x);for(let f of b)f();W.destroy(),w.destroy(),a(),l.parentNode?l.parentNode.removeChild(l):o.parentNode&&o.parentNode.removeChild(o);}}}}function Ht(e){let t=e.toLowerCase();return t.includes("handshake")||t.includes("timed out")||t.includes("timeout")?"handshake_failed":t.includes("protocol mismatch")||t.includes("protocolversion")?"protocol_incompatible":t.includes("origin mismatch")||t.includes("allowed origin")?"origin_mismatch":t.includes("contentwindow")||t.includes("iframe failed")||t.includes("load")?"iframe_load_failed":"unknown"}function je(e){let t=typeof e.referralId=="string"?e.referralId.trim():"",n=t.length>0?t:"general";return ze({...e,referralId:n})}var Lt=Object.freeze({mount:We,buildIframeSrc:je,PROTOCOL_VERSION:F,WIDGET_ORIGIN:z,SANDBOX_ATTR:le});if(typeof window<"u"){let e=window;e.AtomCircuit||(e.AtomCircuit=Lt);}
|
|
2
|
+
exports.PROTOCOL_VERSION=F;exports.SANDBOX_ATTR=le;exports.WIDGET_ORIGIN=z;exports.buildIframeSrc=je;exports.mount=We;return exports;})({});//# sourceMappingURL=atom-circuit.iife.js.map
|