@synfin/widget 0.1.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/LICENSE +21 -0
- package/README.md +145 -0
- package/dist/api.d.ts +23 -0
- package/dist/api.js +66 -0
- package/dist/assets.d.ts +25 -0
- package/dist/assets.js +64 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +80 -0
- package/dist/element.d.ts +65 -0
- package/dist/element.js +542 -0
- package/dist/events.d.ts +16 -0
- package/dist/events.js +17 -0
- package/dist/format.d.ts +36 -0
- package/dist/format.js +57 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +21 -0
- package/dist/react.d.ts +25 -0
- package/dist/react.js +60 -0
- package/dist/render.d.ts +18 -0
- package/dist/render.js +163 -0
- package/dist/state.d.ts +60 -0
- package/dist/state.js +55 -0
- package/dist/styles.d.ts +9 -0
- package/dist/styles.js +226 -0
- package/dist/synfin-widget.global.js +290 -0
- package/package.json +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cayvox Labs
|
|
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,145 @@
|
|
|
1
|
+
# @synfin/widget
|
|
2
|
+
|
|
3
|
+
An embeddable **quote-and-fee widget** for Canton, built on
|
|
4
|
+
[`@synfin/client`](https://www.npmjs.com/package/@synfin/client). Drop it into
|
|
5
|
+
any site to show a live best-execution quote across venues with your disclosed
|
|
6
|
+
fees. Non-custodial: it holds no keys and signs nothing.
|
|
7
|
+
|
|
8
|
+
- Docs: https://synfin.xyz/docs/sdk
|
|
9
|
+
- Status: **v0**, the surface may evolve with design partners. Pin a version.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Two paths, pick one. Both register the same `<synfin-widget>` custom element.
|
|
14
|
+
|
|
15
|
+
### 1. Script tag (no build step, no bundler)
|
|
16
|
+
|
|
17
|
+
For any site (plain HTML, Vue, Svelte, Angular, Rails, WordPress). One `<script>`
|
|
18
|
+
loads a **self-contained** bundle (`@synfin/client` is inlined, so there is no
|
|
19
|
+
import map and nothing else to fetch) and registers the element:
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<script src="https://unpkg.com/@synfin/widget"></script>
|
|
23
|
+
<synfin-widget
|
|
24
|
+
api-key="sk_live_..."
|
|
25
|
+
fee-bps="30"
|
|
26
|
+
fee-recipient="you::1220..."
|
|
27
|
+
from="CC" to="USDCx" amount="100"
|
|
28
|
+
></synfin-widget>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Pin a version in production (the CDN caches it and the API stays stable):
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<script src="https://unpkg.com/@synfin/widget@0.1.0"></script>
|
|
35
|
+
<!-- or jsDelivr -->
|
|
36
|
+
<script src="https://cdn.jsdelivr.net/npm/@synfin/widget@0.1.0"></script>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The bare `unpkg.com/@synfin/widget` URL resolves to the IIFE bundle
|
|
40
|
+
(`dist/synfin-widget.global.js`) through the package `unpkg`/`jsdelivr` fields,
|
|
41
|
+
so you never spell the path. The bundle is browser-only and minified.
|
|
42
|
+
|
|
43
|
+
### 2. npm (with a bundler: Vite, Next.js, Webpack)
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npm install @synfin/widget
|
|
47
|
+
```
|
|
48
|
+
```ts
|
|
49
|
+
import '@synfin/widget'; // registers <synfin-widget>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`@synfin/client` comes in as a normal dependency (the real range `^0.1.0`, not a
|
|
53
|
+
workspace reference), so the package installs cleanly outside this repo.
|
|
54
|
+
|
|
55
|
+
React:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { SynfinWidget } from '@synfin/widget/react';
|
|
59
|
+
|
|
60
|
+
<SynfinWidget apiKey="sk_live_..." feeBps={30} feeRecipient="you::1220..."
|
|
61
|
+
onQuote={(e) => console.log(e.detail)} />
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## What it does (v1)
|
|
65
|
+
|
|
66
|
+
A live quote across Canton venues for your pair and amount, ranked best net
|
|
67
|
+
receive first. We recommend the best, and mark it, but the user can select any
|
|
68
|
+
available venue (a radio group: click, tap, or arrow keys); the receive figure,
|
|
69
|
+
the fee breakdown, and the plan all follow their choice. The widget is shown to
|
|
70
|
+
your END USER, so by default it shows only
|
|
71
|
+
what they care about: what they pay, what they receive (net of all fees, read
|
|
72
|
+
verbatim from the live quote and never recomputed in the browser), and a single
|
|
73
|
+
neutral fee line for the total taken. The fee attribution (the Synfin service
|
|
74
|
+
fee, your integrator fee, and the partner / Synfin split) is partner
|
|
75
|
+
information, hidden from the end user by default; set `show-fee-breakdown` to
|
|
76
|
+
reveal it for your own integration testing. Every state is designed: loading
|
|
77
|
+
skeletons, an auto-refresh countdown, a venue shown honestly as unavailable
|
|
78
|
+
(never breaking the widget), no-route, and errors.
|
|
79
|
+
|
|
80
|
+
Creating a plan emits `synfin:plan` for **your** app to execute through the
|
|
81
|
+
`WalletAdapter` it owns. In-widget wallet execution is deferred until a standard
|
|
82
|
+
Canton browser wallet adapter exists (see docs/WIDGET.md); the widget never
|
|
83
|
+
holds keys.
|
|
84
|
+
|
|
85
|
+
## Configuration
|
|
86
|
+
|
|
87
|
+
| attribute / prop | meaning |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `api-key` / `apiKey` | your partner key (unlocks the fee breakdown) |
|
|
90
|
+
| `fee-bps` / `feeBps` | your integrator fee, 0 to the cap (needs a recipient) |
|
|
91
|
+
| `fee-recipient` / `feeRecipient` | the Canton party your fee settles to |
|
|
92
|
+
| `from` `to` `amount` | default pair and amount |
|
|
93
|
+
| `appearance` | `dark` (default) or `light` |
|
|
94
|
+
| `base-url` / `baseUrl` | API origin (default `https://synfin.xyz`) |
|
|
95
|
+
| `show-fee-breakdown` / `showFeeBreakdown` | show the full partner fee attribution (off by default; end users see the net and one neutral fee line) |
|
|
96
|
+
| `show-attribution` / `showAttribution` | the subtle "Powered by Synfin" footer link (on by default; set off to white-label) |
|
|
97
|
+
|
|
98
|
+
**On the API key:** in a browser widget the key is client-side visible by
|
|
99
|
+
nature. Synfin keys are free, rate-limited, and non-custodial: a leaked key can
|
|
100
|
+
only fetch quotes at that key's rate limit, and is revocable in one click. Use a
|
|
101
|
+
dedicated widget key. A per-key origin allowlist is a roadmap item.
|
|
102
|
+
|
|
103
|
+
## Theming
|
|
104
|
+
|
|
105
|
+
Override any of these CSS custom properties (they pierce the Shadow DOM) to match
|
|
106
|
+
your site; the Synfin cream/ember dark system is the default, `appearance="light"`
|
|
107
|
+
a warm off-white:
|
|
108
|
+
|
|
109
|
+
```css
|
|
110
|
+
synfin-widget {
|
|
111
|
+
--synfin-accent: #6c5cff;
|
|
112
|
+
--synfin-bg: #0c0c14;
|
|
113
|
+
--synfin-surface: #17172a;
|
|
114
|
+
--synfin-ink: #eef;
|
|
115
|
+
--synfin-radius: 20px;
|
|
116
|
+
--synfin-font: 'Inter', sans-serif;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Events
|
|
121
|
+
|
|
122
|
+
DOM `CustomEvent`s on the element (and `on*` props in the React wrapper):
|
|
123
|
+
|
|
124
|
+
- `synfin:quote` - a fresh `QuoteResponse` loaded
|
|
125
|
+
- `synfin:venue` - the user selected a venue (`{ venueId, net, isBest, pair }`)
|
|
126
|
+
- `synfin:plan` - an `ExecutionPlan` created (execute it from your wallet)
|
|
127
|
+
- `synfin:asset` - the pair changed
|
|
128
|
+
- `synfin:error` - a `SynfinApiError` surfaced
|
|
129
|
+
|
|
130
|
+
## Demo
|
|
131
|
+
|
|
132
|
+
- `example/index.html`: the full embed against live production
|
|
133
|
+
(`https://synfin.xyz`) by default, with a control panel, the event log, and
|
|
134
|
+
theming. Add your API key to unlock the fee breakdown; keyless shows the venue
|
|
135
|
+
net only.
|
|
136
|
+
- `example/cdn.html`: the script-tag path in isolation. It loads the
|
|
137
|
+
self-contained bundle with a plain `<script src>` (no import map), proving the
|
|
138
|
+
CDN embed a partner uses.
|
|
139
|
+
|
|
140
|
+
The widget reads the public API cross-origin, so the API sends permissive CORS
|
|
141
|
+
headers on `/api/*`; see the `/api/*` CORS middleware.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT. Copyright Cayvox Labs.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The widget's own quote fetch. It talks to the same public /api/quote surface
|
|
3
|
+
* as @synfin/client, but unlike the client (which binds a key and always sends
|
|
4
|
+
* it) this attaches the x-api-key header ONLY when a real key is present. A
|
|
5
|
+
* keyless embed therefore sends a clean anonymous request, which the endpoint
|
|
6
|
+
* answers 200 with ranked venues and no clientFees (add a key to see fees). It
|
|
7
|
+
* reuses the client's exported error and quote types so the widget stays built
|
|
8
|
+
* on @synfin/client; planning still goes through the client, which needs a key.
|
|
9
|
+
*/
|
|
10
|
+
import { type FetchLike, type GetQuoteParams, type QuoteResponse } from '@synfin/client';
|
|
11
|
+
export interface QuoteRequest {
|
|
12
|
+
readonly baseUrl: string;
|
|
13
|
+
/** The partner key, or null for a keyless (anonymous) request. */
|
|
14
|
+
readonly apiKey: string | null;
|
|
15
|
+
readonly fetch?: FetchLike;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Fetch a quote. Sends the x-api-key header only when {@link QuoteRequest.apiKey}
|
|
19
|
+
* is a non-empty string; a null/empty key produces a header-free anonymous
|
|
20
|
+
* request. Throws {@link SynfinApiError} on a non-2xx or non-JSON response.
|
|
21
|
+
*/
|
|
22
|
+
export declare function requestQuote(req: QuoteRequest, params: GetQuoteParams): Promise<QuoteResponse>;
|
|
23
|
+
//# sourceMappingURL=api.d.ts.map
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The widget's own quote fetch. It talks to the same public /api/quote surface
|
|
3
|
+
* as @synfin/client, but unlike the client (which binds a key and always sends
|
|
4
|
+
* it) this attaches the x-api-key header ONLY when a real key is present. A
|
|
5
|
+
* keyless embed therefore sends a clean anonymous request, which the endpoint
|
|
6
|
+
* answers 200 with ranked venues and no clientFees (add a key to see fees). It
|
|
7
|
+
* reuses the client's exported error and quote types so the widget stays built
|
|
8
|
+
* on @synfin/client; planning still goes through the client, which needs a key.
|
|
9
|
+
*/
|
|
10
|
+
import { SynfinApiError, } from '@synfin/client';
|
|
11
|
+
/** Encode a flat param set, skipping undefined values. */
|
|
12
|
+
function query(params) {
|
|
13
|
+
const parts = [];
|
|
14
|
+
for (const [k, v] of Object.entries(params)) {
|
|
15
|
+
if (v === undefined)
|
|
16
|
+
continue;
|
|
17
|
+
parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
18
|
+
}
|
|
19
|
+
return parts.length > 0 ? `?${parts.join('&')}` : '';
|
|
20
|
+
}
|
|
21
|
+
function resolveFetch(injected) {
|
|
22
|
+
if (injected !== undefined)
|
|
23
|
+
return injected;
|
|
24
|
+
const g = globalThis.fetch;
|
|
25
|
+
if (g === undefined) {
|
|
26
|
+
throw new SynfinApiError(0, 'network', 'no fetch available in this runtime');
|
|
27
|
+
}
|
|
28
|
+
return g;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Fetch a quote. Sends the x-api-key header only when {@link QuoteRequest.apiKey}
|
|
32
|
+
* is a non-empty string; a null/empty key produces a header-free anonymous
|
|
33
|
+
* request. Throws {@link SynfinApiError} on a non-2xx or non-JSON response.
|
|
34
|
+
*/
|
|
35
|
+
export async function requestQuote(req, params) {
|
|
36
|
+
const qs = query({
|
|
37
|
+
from: params.from,
|
|
38
|
+
to: params.to,
|
|
39
|
+
amount: String(params.amount),
|
|
40
|
+
slippageBps: params.slippageBps,
|
|
41
|
+
feeBps: params.feeBps,
|
|
42
|
+
feeRecipient: params.feeRecipient,
|
|
43
|
+
});
|
|
44
|
+
const headers = {};
|
|
45
|
+
if (req.apiKey !== null && req.apiKey !== '') {
|
|
46
|
+
headers['x-api-key'] = req.apiKey;
|
|
47
|
+
}
|
|
48
|
+
const res = await resolveFetch(req.fetch)(`${req.baseUrl}/api/quote${qs}`, {
|
|
49
|
+
method: 'GET',
|
|
50
|
+
headers,
|
|
51
|
+
});
|
|
52
|
+
let body;
|
|
53
|
+
try {
|
|
54
|
+
body = await res.json();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
const text = await res.text().catch(() => '');
|
|
58
|
+
throw new SynfinApiError(res.status, 'bad_response', text.length > 0 ? text : 'non-JSON response from the Synfin API');
|
|
59
|
+
}
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
const e = body;
|
|
62
|
+
throw new SynfinApiError(res.status, e.code ?? 'error', e.error ?? `HTTP ${res.status}`);
|
|
63
|
+
}
|
|
64
|
+
return body;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=api.js.map
|
package/dist/assets.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Canton asset catalog the selector offers. UI metadata only (symbol, name,
|
|
3
|
+
* decimals for display); the quote and its fees are the API's, never ours. Kept
|
|
4
|
+
* in sync with the tokens the hosted /api/quote resolves.
|
|
5
|
+
*/
|
|
6
|
+
export interface WidgetAsset {
|
|
7
|
+
readonly symbol: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly instrumentId: string;
|
|
10
|
+
readonly decimals: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const ASSETS: readonly WidgetAsset[];
|
|
13
|
+
export declare function findAsset(symbol: string): WidgetAsset | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Map a name from the API (which may be the internal canonical instrumentId,
|
|
16
|
+
* e.g. "Amulet") to the user-facing ticker (e.g. "CC"). The canonical name must
|
|
17
|
+
* never surface in user-facing text; the user only ever sees the ticker they
|
|
18
|
+
* chose. Unknown names pass through unchanged (honest, never a wrong label).
|
|
19
|
+
*/
|
|
20
|
+
export declare function displaySymbol(nameOrId: string): string;
|
|
21
|
+
/** A crisp, self-contained SVG monogram for a token (no remote image, no
|
|
22
|
+
* possibly-wrong logo). Deterministic per ticker, on-brand, theme-agnostic. */
|
|
23
|
+
export declare function assetIcon(symbol: string): string;
|
|
24
|
+
export declare function searchAssets(query: string): readonly WidgetAsset[];
|
|
25
|
+
//# sourceMappingURL=assets.d.ts.map
|
package/dist/assets.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const ASSETS = [
|
|
2
|
+
{ symbol: 'CC', name: 'Canton Coin', instrumentId: 'Amulet', decimals: 10 },
|
|
3
|
+
{
|
|
4
|
+
symbol: 'USDCx',
|
|
5
|
+
name: 'USDC (interchain)',
|
|
6
|
+
instrumentId: 'USDCx',
|
|
7
|
+
decimals: 6,
|
|
8
|
+
},
|
|
9
|
+
{ symbol: 'CBTC', name: 'Canton BTC', instrumentId: 'CBTC', decimals: 8 },
|
|
10
|
+
];
|
|
11
|
+
export function findAsset(symbol) {
|
|
12
|
+
const s = symbol.trim().toLowerCase();
|
|
13
|
+
return ASSETS.find((a) => a.symbol.toLowerCase() === s);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Map a name from the API (which may be the internal canonical instrumentId,
|
|
17
|
+
* e.g. "Amulet") to the user-facing ticker (e.g. "CC"). The canonical name must
|
|
18
|
+
* never surface in user-facing text; the user only ever sees the ticker they
|
|
19
|
+
* chose. Unknown names pass through unchanged (honest, never a wrong label).
|
|
20
|
+
*/
|
|
21
|
+
export function displaySymbol(nameOrId) {
|
|
22
|
+
const s = nameOrId.trim();
|
|
23
|
+
const l = s.toLowerCase();
|
|
24
|
+
const byId = ASSETS.find((a) => a.instrumentId.toLowerCase() === l);
|
|
25
|
+
if (byId)
|
|
26
|
+
return byId.symbol;
|
|
27
|
+
const bySym = ASSETS.find((a) => a.symbol.toLowerCase() === l);
|
|
28
|
+
return bySym ? bySym.symbol : s;
|
|
29
|
+
}
|
|
30
|
+
// Curated per-token monogram (label + hue) for the crisp fallback icon. Real
|
|
31
|
+
// Canton token logos are an OPTIONAL field on the CIP-0056 off-ledger token
|
|
32
|
+
// metadata API and are not reliably published for these tokens, so the widget
|
|
33
|
+
// ships a deterministic on-brand monogram rather than a possibly-wrong logo.
|
|
34
|
+
const ICON = {
|
|
35
|
+
CC: { label: 'CC', hue: 20 },
|
|
36
|
+
AMULET: { label: 'CC', hue: 20 },
|
|
37
|
+
USDCX: { label: 'USDC', hue: 212 },
|
|
38
|
+
CBTC: { label: 'BTC', hue: 33 },
|
|
39
|
+
CETH: { label: 'ETH', hue: 250 },
|
|
40
|
+
CUSD: { label: 'USD', hue: 150 },
|
|
41
|
+
};
|
|
42
|
+
function hashHue(s) {
|
|
43
|
+
let h = 0;
|
|
44
|
+
for (let i = 0; i < s.length; i++)
|
|
45
|
+
h = (h * 31 + s.charCodeAt(i)) >>> 0;
|
|
46
|
+
return h % 360;
|
|
47
|
+
}
|
|
48
|
+
/** A crisp, self-contained SVG monogram for a token (no remote image, no
|
|
49
|
+
* possibly-wrong logo). Deterministic per ticker, on-brand, theme-agnostic. */
|
|
50
|
+
export function assetIcon(symbol) {
|
|
51
|
+
const key = displaySymbol(symbol).toUpperCase();
|
|
52
|
+
const c = ICON[key] ?? { label: key.slice(0, 3), hue: hashHue(key) };
|
|
53
|
+
const h2 = (c.hue + 24) % 360;
|
|
54
|
+
const id = `sfic${c.hue}`;
|
|
55
|
+
const fs = c.label.length >= 4 ? 6.5 : c.label.length === 3 ? 8 : 9.5;
|
|
56
|
+
return `<svg class="asset__icon" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><defs><linearGradient id="${id}" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${c.hue} 78% 58%)"></stop><stop offset="1" stop-color="hsl(${h2} 72% 44%)"></stop></linearGradient></defs><circle cx="12" cy="12" r="11" fill="url(#${id})"></circle><text x="12" y="12" dy="0.35em" text-anchor="middle" font-size="${fs}" font-weight="700" fill="#fff">${c.label}</text></svg>`;
|
|
57
|
+
}
|
|
58
|
+
export function searchAssets(query) {
|
|
59
|
+
const q = query.trim().toLowerCase();
|
|
60
|
+
if (q === '')
|
|
61
|
+
return ASSETS;
|
|
62
|
+
return ASSETS.filter((a) => a.symbol.toLowerCase().includes(q) || a.name.toLowerCase().includes(q));
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=assets.js.map
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface WidgetConfig {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
feeBps?: number;
|
|
4
|
+
feeRecipient?: string;
|
|
5
|
+
from?: string;
|
|
6
|
+
to?: string;
|
|
7
|
+
amount?: string;
|
|
8
|
+
appearance?: 'dark' | 'light';
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
refreshMs?: number;
|
|
11
|
+
/** Partner-facing detail mode: show the full fee attribution (Synfin service,
|
|
12
|
+
* integrator fee, partner/Synfin split). OFF by default; the end-user default
|
|
13
|
+
* shows only the net receive and a single neutral total fee. */
|
|
14
|
+
showFeeBreakdown?: boolean;
|
|
15
|
+
/** The subtle "Powered by Synfin" footer link (a growth surface, LI.FI-style).
|
|
16
|
+
* ON by default; a white-label enterprise partner sets it off. */
|
|
17
|
+
showAttribution?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface NormalizedConfig {
|
|
20
|
+
apiKey: string | null;
|
|
21
|
+
feeBps: number | null;
|
|
22
|
+
feeRecipient: string | null;
|
|
23
|
+
from: string;
|
|
24
|
+
to: string;
|
|
25
|
+
amount: string;
|
|
26
|
+
appearance: 'dark' | 'light';
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
refreshMs: number;
|
|
29
|
+
showFeeBreakdown: boolean;
|
|
30
|
+
showAttribution: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface ConfigIssue {
|
|
33
|
+
field: string;
|
|
34
|
+
message: string;
|
|
35
|
+
}
|
|
36
|
+
/** The integrator fee cap for input validation (BUSINESS.md; the server is
|
|
37
|
+
* authoritative and re-checks against the applied schema). */
|
|
38
|
+
export declare const FEE_CAP_BPS = 100;
|
|
39
|
+
export declare function normalizeConfig(raw: WidgetConfig): {
|
|
40
|
+
config: NormalizedConfig;
|
|
41
|
+
issues: ConfigIssue[];
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widget configuration: the partner passes their key, their integrator fee,
|
|
3
|
+
* default pair/amount, theme, and API origin. Normalization applies defaults
|
|
4
|
+
* and collects honest issues (shown, never thrown), so a misconfigured embed
|
|
5
|
+
* degrades to a clear message instead of a blank box.
|
|
6
|
+
*/
|
|
7
|
+
import { findAsset } from './assets.js';
|
|
8
|
+
/** The integrator fee cap for input validation (BUSINESS.md; the server is
|
|
9
|
+
* authoritative and re-checks against the applied schema). */
|
|
10
|
+
export const FEE_CAP_BPS = 100;
|
|
11
|
+
const DEFAULT_REFRESH_MS = 15_000;
|
|
12
|
+
export function normalizeConfig(raw) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
const from = (raw.from ?? 'CC').trim();
|
|
15
|
+
const to = (raw.to ?? 'USDCx').trim();
|
|
16
|
+
if (findAsset(from) === undefined)
|
|
17
|
+
issues.push({ field: 'from', message: `unknown asset "${from}"` });
|
|
18
|
+
if (findAsset(to) === undefined)
|
|
19
|
+
issues.push({ field: 'to', message: `unknown asset "${to}"` });
|
|
20
|
+
if (from.toLowerCase() === to.toLowerCase())
|
|
21
|
+
issues.push({ field: 'to', message: 'from and to must differ' });
|
|
22
|
+
const amount = (raw.amount ?? '100').trim();
|
|
23
|
+
if (!/^\d+(\.\d+)?$/.test(amount) || Number(amount) <= 0)
|
|
24
|
+
issues.push({
|
|
25
|
+
field: 'amount',
|
|
26
|
+
message: 'amount must be a positive number',
|
|
27
|
+
});
|
|
28
|
+
let feeBps = null;
|
|
29
|
+
let feeRecipient = null;
|
|
30
|
+
const hasFee = raw.feeBps !== undefined || raw.feeRecipient !== undefined;
|
|
31
|
+
if (hasFee) {
|
|
32
|
+
if (raw.feeBps === undefined || raw.feeRecipient === undefined) {
|
|
33
|
+
issues.push({
|
|
34
|
+
field: 'fee',
|
|
35
|
+
message: 'feeBps and feeRecipient must be set together',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const n = Number(raw.feeBps);
|
|
40
|
+
if (!Number.isInteger(n) || n < 0 || n > FEE_CAP_BPS)
|
|
41
|
+
issues.push({
|
|
42
|
+
field: 'feeBps',
|
|
43
|
+
message: `feeBps must be an integer 0 to ${FEE_CAP_BPS}`,
|
|
44
|
+
});
|
|
45
|
+
else
|
|
46
|
+
feeBps = n;
|
|
47
|
+
const r = String(raw.feeRecipient).trim();
|
|
48
|
+
if (r === '')
|
|
49
|
+
issues.push({
|
|
50
|
+
field: 'feeRecipient',
|
|
51
|
+
message: 'feeRecipient required',
|
|
52
|
+
});
|
|
53
|
+
else
|
|
54
|
+
feeRecipient = r;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const appearance = raw.appearance === 'light' ? 'light' : 'dark';
|
|
58
|
+
const baseUrl = (raw.baseUrl ?? 'https://synfin.xyz').replace(/\/+$/, '');
|
|
59
|
+
const refreshMs = typeof raw.refreshMs === 'number' && raw.refreshMs >= 3000
|
|
60
|
+
? raw.refreshMs
|
|
61
|
+
: DEFAULT_REFRESH_MS;
|
|
62
|
+
const apiKeyTrimmed = (raw.apiKey ?? '').trim();
|
|
63
|
+
return {
|
|
64
|
+
config: {
|
|
65
|
+
apiKey: apiKeyTrimmed === '' ? null : apiKeyTrimmed,
|
|
66
|
+
feeBps,
|
|
67
|
+
feeRecipient,
|
|
68
|
+
from,
|
|
69
|
+
to,
|
|
70
|
+
amount,
|
|
71
|
+
appearance,
|
|
72
|
+
baseUrl,
|
|
73
|
+
refreshMs,
|
|
74
|
+
showFeeBreakdown: raw.showFeeBreakdown === true,
|
|
75
|
+
showAttribution: raw.showAttribution !== false,
|
|
76
|
+
},
|
|
77
|
+
issues,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <synfin-widget>: the custom element. Owns the persistent chrome (pair + amount
|
|
3
|
+
* fields, the asset selector popover, the refresh countdown) and drives the
|
|
4
|
+
* quote lifecycle through @synfin/client, swapping only the result region per
|
|
5
|
+
* quote. Non-custodial: it holds no keys and signs nothing; creating a plan
|
|
6
|
+
* emits synfin:plan for the host to execute. Shadow DOM isolates styles so it is
|
|
7
|
+
* robust inside any host page.
|
|
8
|
+
*/
|
|
9
|
+
import { type FetchLike } from '@synfin/client';
|
|
10
|
+
export declare class SynfinWidgetElement extends HTMLElement {
|
|
11
|
+
static get observedAttributes(): readonly string[];
|
|
12
|
+
/** Injectable fetch (tests and advanced hosts); defaults to global fetch. */
|
|
13
|
+
fetchImpl?: FetchLike;
|
|
14
|
+
private cfg;
|
|
15
|
+
private client;
|
|
16
|
+
private view;
|
|
17
|
+
private debounce;
|
|
18
|
+
private ticker;
|
|
19
|
+
private countdown;
|
|
20
|
+
private reqSeq;
|
|
21
|
+
private selectorOpen;
|
|
22
|
+
private built;
|
|
23
|
+
private syncing;
|
|
24
|
+
/** The venue the user has chosen (defaults to best); persists across refreshes
|
|
25
|
+
* while it stays available. */
|
|
26
|
+
private selectedVenue;
|
|
27
|
+
/** Transient Create-a-plan feedback, cleared on a fresh quote. */
|
|
28
|
+
private plan;
|
|
29
|
+
connectedCallback(): void;
|
|
30
|
+
disconnectedCallback(): void;
|
|
31
|
+
attributeChangedCallback(): void;
|
|
32
|
+
/** Set config programmatically (React wrapper / advanced hosts). */
|
|
33
|
+
configure(partial: Record<string, string | number | boolean | undefined | null>): void;
|
|
34
|
+
private reconfigure;
|
|
35
|
+
private root;
|
|
36
|
+
private $;
|
|
37
|
+
private build;
|
|
38
|
+
private assetBtn;
|
|
39
|
+
private paintChrome;
|
|
40
|
+
private onAmount;
|
|
41
|
+
/** Change the pair from an INTERNAL action (swap/select): update live state
|
|
42
|
+
* and re-quote directly, never through observed attributes (which would
|
|
43
|
+
* reset the typed amount and re-enter reconfigure). */
|
|
44
|
+
private setPair;
|
|
45
|
+
private swap;
|
|
46
|
+
private setView;
|
|
47
|
+
/** Re-render the result region from the current view, selected venue, and plan
|
|
48
|
+
* feedback. Called on a new quote and on any selection / plan change. */
|
|
49
|
+
private paintResult;
|
|
50
|
+
/** The "You receive" chrome follows the SELECTED venue, same formatting as the
|
|
51
|
+
* summary line so the two never disagree. */
|
|
52
|
+
private paintReceive;
|
|
53
|
+
/** Select a venue (click or keyboard). Repaints so the receive figure, fees,
|
|
54
|
+
* and plan all follow the choice, and emits synfin:venue for the host. */
|
|
55
|
+
private selectVenue;
|
|
56
|
+
private venueKeys;
|
|
57
|
+
private quote;
|
|
58
|
+
private startCountdown;
|
|
59
|
+
private stopCountdown;
|
|
60
|
+
private createPlan;
|
|
61
|
+
private openSelector;
|
|
62
|
+
private selectorKeys;
|
|
63
|
+
private closeSelector;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=element.d.ts.map
|