@vulcx/widget 0.2.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.
@@ -0,0 +1,1404 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.VulcxWidget = {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ class VulcxError extends Error {
8
+ constructor(message, statusCode, body) {
9
+ super(message);
10
+ this.statusCode = statusCode;
11
+ this.body = body;
12
+ this.name = "VulcxError";
13
+ }
14
+ }
15
+ class RateLimitError extends VulcxError {
16
+ constructor(body) {
17
+ super("Rate limit exceeded", 429, body);
18
+ this.name = "RateLimitError";
19
+ }
20
+ }
21
+ class NoRouteError extends VulcxError {
22
+ constructor(body) {
23
+ super("No route found", 404, body);
24
+ this.name = "NoRouteError";
25
+ }
26
+ }
27
+ class BadRequestError extends VulcxError {
28
+ constructor(message, body) {
29
+ super(message, 400, body);
30
+ this.name = "BadRequestError";
31
+ }
32
+ }
33
+ class AuthError extends VulcxError {
34
+ constructor(body) {
35
+ super("Invalid or missing API key", 401, body);
36
+ this.name = "AuthError";
37
+ }
38
+ }
39
+ class ServerError extends VulcxError {
40
+ constructor(message, body) {
41
+ super(message, 500, body);
42
+ this.name = "ServerError";
43
+ }
44
+ }
45
+
46
+ const DEFAULT_BASE_URL = "https://api.vulcx.xyz";
47
+ const DEFAULT_TIMEOUT = 30000;
48
+ const DEFAULT_RETRIES = 2;
49
+ class VulcxSDK {
50
+ constructor(config) {
51
+ if (!config.apiKey)
52
+ throw new Error("apiKey is required");
53
+ this.apiKey = config.apiKey;
54
+ this.chain = config.chain ?? "solana";
55
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
56
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
57
+ this.retries = config.retries ?? DEFAULT_RETRIES;
58
+ }
59
+ async quote(params) {
60
+ const qs = new URLSearchParams({
61
+ inputMint: params.inputMint,
62
+ outputMint: params.outputMint,
63
+ amount: params.amount,
64
+ swapMode: params.swapMode,
65
+ });
66
+ if (params.slippageBps !== undefined) {
67
+ qs.set("slippageBps", String(params.slippageBps));
68
+ }
69
+ return this.request("GET", `/api/v1/quote?${qs}`);
70
+ }
71
+ async swap(params) {
72
+ return this.request("POST", "/api/v1/swap", params);
73
+ }
74
+ async instructions(params) {
75
+ return this.request("POST", "/api/v1/instructions", params);
76
+ }
77
+ async request(method, path, body) {
78
+ const separator = path.includes("?") ? "&" : "?";
79
+ const url = `${this.baseUrl}${path}${separator}chain=${this.chain}`;
80
+ const headers = {
81
+ Authorization: `Bearer ${this.apiKey}`,
82
+ Accept: "application/json",
83
+ };
84
+ if (body) {
85
+ headers["Content-Type"] = "application/json";
86
+ }
87
+ let lastError;
88
+ for (let attempt = 0; attempt <= this.retries; attempt++) {
89
+ if (attempt > 0) {
90
+ await sleep(Math.min(1000 * 2 ** (attempt - 1), 8000));
91
+ }
92
+ try {
93
+ const controller = new AbortController();
94
+ const timer = setTimeout(() => controller.abort(), this.timeout);
95
+ const res = await fetch(url, {
96
+ method,
97
+ headers,
98
+ body: body ? JSON.stringify(body) : undefined,
99
+ signal: controller.signal,
100
+ });
101
+ clearTimeout(timer);
102
+ if (res.ok) {
103
+ const parsed = (await res.json());
104
+ // API responses wrap the payload in {success, data}; hand back the
105
+ // payload the way the method signatures promise.
106
+ if (parsed && typeof parsed === "object" && "success" in parsed) {
107
+ if (!parsed.success) {
108
+ throw new VulcxError(parsed.error ?? "request failed", res.status, parsed);
109
+ }
110
+ return parsed.data;
111
+ }
112
+ return parsed;
113
+ }
114
+ const errBody = await res.json().catch(() => ({}));
115
+ const errMsg = errBody?.error ?? res.statusText;
116
+ switch (res.status) {
117
+ case 401:
118
+ case 403:
119
+ throw new AuthError(errBody);
120
+ case 400:
121
+ throw new BadRequestError(errMsg, errBody);
122
+ case 404:
123
+ throw new NoRouteError(errBody);
124
+ case 429:
125
+ lastError = new RateLimitError(errBody);
126
+ continue;
127
+ default:
128
+ if (res.status >= 500) {
129
+ lastError = new ServerError(errMsg, errBody);
130
+ continue;
131
+ }
132
+ throw new VulcxError(errMsg, res.status, errBody);
133
+ }
134
+ }
135
+ catch (err) {
136
+ if (err instanceof AuthError ||
137
+ err instanceof BadRequestError ||
138
+ err instanceof NoRouteError ||
139
+ err instanceof VulcxError) {
140
+ throw err;
141
+ }
142
+ lastError = err;
143
+ }
144
+ }
145
+ throw lastError ?? new Error("request failed");
146
+ }
147
+ }
148
+ function sleep(ms) {
149
+ return new Promise((r) => setTimeout(r, ms));
150
+ }
151
+
152
+ const CSS_VARS = {
153
+ "--vulcx-bg": "#0c0c10",
154
+ "--vulcx-surface": "#151519",
155
+ "--vulcx-surface-hover": "#1e1e25",
156
+ "--vulcx-border": "#252530",
157
+ "--vulcx-text": "#e8e8ed",
158
+ "--vulcx-text-secondary": "#7a7a8a",
159
+ "--vulcx-text-dim": "#555565",
160
+ "--vulcx-accent": "#c8ff00",
161
+ "--vulcx-accent-hover": "#d4ff33",
162
+ "--vulcx-error": "#ff4d6a",
163
+ "--vulcx-warning": "#ffaa00",
164
+ "--vulcx-success": "#00d68f",
165
+ "--vulcx-radius": "20px",
166
+ "--vulcx-radius-sm": "14px",
167
+ "--vulcx-badge-bg": "#0a0a0e",
168
+ "--vulcx-font": "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
169
+ };
170
+
171
+ const defaultVars = Object.entries(CSS_VARS)
172
+ .map(([k, v]) => `${k}: ${v};`)
173
+ .join("\n ");
174
+ const WIDGET_CSS = `
175
+ :host {
176
+ ${defaultVars}
177
+ display: block;
178
+ font-family: var(--vulcx-font);
179
+ color: var(--vulcx-text);
180
+ box-sizing: border-box;
181
+ }
182
+
183
+ :host([theme="light"]) {
184
+ --vulcx-bg: #ffffff;
185
+ --vulcx-surface: #f5f5f7;
186
+ --vulcx-surface-hover: #ebebef;
187
+ --vulcx-border: #d4d4dc;
188
+ --vulcx-text: #1a1a2e;
189
+ --vulcx-text-secondary: #6b6b7a;
190
+ --vulcx-text-dim: #9a9aaa;
191
+ --vulcx-accent: #1a1a2e;
192
+ --vulcx-accent-hover: #2d2d45;
193
+ --vulcx-badge-bg: #e8e8ed;
194
+ }
195
+
196
+ *, *::before, *::after {
197
+ box-sizing: border-box;
198
+ margin: 0;
199
+ padding: 0;
200
+ }
201
+
202
+ .swap-container {
203
+ background: var(--vulcx-bg);
204
+ border-radius: var(--vulcx-radius);
205
+ padding: 12px;
206
+ max-width: 460px;
207
+ width: 100%;
208
+ }
209
+
210
+ /* ── Token Panel ── */
211
+ .token-panel {
212
+ background: var(--vulcx-surface);
213
+ border-radius: var(--vulcx-radius-sm);
214
+ padding: 18px 18px 14px;
215
+ margin-bottom: 4px;
216
+ }
217
+
218
+ .token-panel-label {
219
+ font-size: 13px;
220
+ color: var(--vulcx-text-secondary);
221
+ margin-bottom: 10px;
222
+ }
223
+
224
+ .token-row {
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: space-between;
228
+ gap: 12px;
229
+ }
230
+
231
+ .amount-input {
232
+ flex: 1;
233
+ background: transparent;
234
+ border: none;
235
+ outline: none;
236
+ font-size: 36px;
237
+ font-weight: 500;
238
+ color: var(--vulcx-text);
239
+ font-family: var(--vulcx-font);
240
+ min-width: 0;
241
+ }
242
+
243
+ .amount-input::placeholder {
244
+ color: var(--vulcx-text-dim);
245
+ font-weight: 400;
246
+ }
247
+
248
+ .amount-input:disabled {
249
+ opacity: 0.6;
250
+ }
251
+
252
+ /* ── Token Badge ── */
253
+ .token-badge {
254
+ display: flex;
255
+ align-items: center;
256
+ gap: 8px;
257
+ background: var(--vulcx-badge-bg);
258
+ border: 1px solid var(--vulcx-border);
259
+ border-radius: 24px;
260
+ padding: 6px 14px 6px 6px;
261
+ cursor: pointer;
262
+ font-size: 17px;
263
+ font-weight: 700;
264
+ color: var(--vulcx-text);
265
+ white-space: nowrap;
266
+ transition: background 0.15s;
267
+ height: 44px;
268
+ }
269
+
270
+ .token-badge:hover {
271
+ background: var(--vulcx-surface-hover);
272
+ }
273
+
274
+ .token-icon {
275
+ width: 30px;
276
+ height: 30px;
277
+ border-radius: 50%;
278
+ object-fit: cover;
279
+ flex-shrink: 0;
280
+ }
281
+
282
+ .token-icon-placeholder {
283
+ width: 30px;
284
+ height: 30px;
285
+ border-radius: 50%;
286
+ background: var(--vulcx-border);
287
+ flex-shrink: 0;
288
+ }
289
+
290
+ .badge-chevron {
291
+ width: 12px;
292
+ height: 12px;
293
+ opacity: 0.5;
294
+ flex-shrink: 0;
295
+ }
296
+
297
+ /* ── USD Value ── */
298
+ .usd-value {
299
+ font-size: 13px;
300
+ color: var(--vulcx-text-secondary);
301
+ margin-top: 6px;
302
+ font-family: 'SF Mono', 'Fira Code', monospace;
303
+ }
304
+
305
+ /* ── Balance Row ── */
306
+ .balance-row {
307
+ display: flex;
308
+ justify-content: space-between;
309
+ align-items: center;
310
+ margin-top: 10px;
311
+ padding-top: 10px;
312
+ border-top: 1px solid var(--vulcx-border);
313
+ }
314
+
315
+ .balance-label {
316
+ display: flex;
317
+ align-items: center;
318
+ gap: 6px;
319
+ font-size: 13px;
320
+ color: var(--vulcx-text-secondary);
321
+ }
322
+
323
+ .balance-label svg {
324
+ width: 16px;
325
+ height: 16px;
326
+ opacity: 0.5;
327
+ }
328
+
329
+ .balance-actions {
330
+ display: flex;
331
+ gap: 6px;
332
+ }
333
+
334
+ .half-max-btn {
335
+ font-size: 12px;
336
+ font-weight: 600;
337
+ padding: 4px 12px;
338
+ border-radius: 8px;
339
+ border: 1px solid var(--vulcx-border);
340
+ background: var(--vulcx-surface-hover);
341
+ color: var(--vulcx-text-secondary);
342
+ cursor: pointer;
343
+ font-family: var(--vulcx-font);
344
+ transition: background 0.15s, color 0.15s;
345
+ }
346
+
347
+ .half-max-btn:hover {
348
+ background: var(--vulcx-border);
349
+ color: var(--vulcx-text);
350
+ }
351
+
352
+ /* ── Swap Direction ── */
353
+ .swap-direction {
354
+ display: flex;
355
+ justify-content: center;
356
+ margin: -10px 0;
357
+ position: relative;
358
+ z-index: 1;
359
+ }
360
+
361
+ .swap-direction-btn {
362
+ width: 48px;
363
+ height: 48px;
364
+ border-radius: 50%;
365
+ border: 4px solid var(--vulcx-bg);
366
+ background: var(--vulcx-surface);
367
+ color: var(--vulcx-text);
368
+ cursor: pointer;
369
+ display: flex;
370
+ align-items: center;
371
+ justify-content: center;
372
+ transition: background 0.15s, transform 0.2s;
373
+ }
374
+
375
+ .swap-direction-btn:hover {
376
+ background: var(--vulcx-surface-hover);
377
+ transform: rotate(180deg);
378
+ }
379
+
380
+ .swap-direction-btn svg {
381
+ width: 22px;
382
+ height: 22px;
383
+ }
384
+
385
+ /* ── Swap Button ── */
386
+ .swap-btn {
387
+ width: 100%;
388
+ padding: 18px;
389
+ border: none;
390
+ border-radius: var(--vulcx-radius-sm);
391
+ background: var(--vulcx-accent);
392
+ color: #0a0a0f;
393
+ font-size: 17px;
394
+ font-weight: 700;
395
+ cursor: pointer;
396
+ margin-top: 10px;
397
+ font-family: var(--vulcx-font);
398
+ transition: background 0.15s, opacity 0.15s;
399
+ letter-spacing: -0.2px;
400
+ }
401
+
402
+ .swap-btn:hover:not(:disabled) {
403
+ background: var(--vulcx-accent-hover);
404
+ }
405
+
406
+ .swap-btn:disabled {
407
+ opacity: 0.5;
408
+ cursor: not-allowed;
409
+ }
410
+
411
+ /* ── Quote Bar ── */
412
+ .quote-bar {
413
+ display: flex;
414
+ align-items: center;
415
+ gap: 10px;
416
+ margin-top: 10px;
417
+ padding: 10px 14px;
418
+ background: var(--vulcx-surface);
419
+ border-radius: var(--vulcx-radius-sm);
420
+ font-size: 13px;
421
+ color: var(--vulcx-text-secondary);
422
+ cursor: pointer;
423
+ transition: background 0.15s;
424
+ position: relative;
425
+ }
426
+
427
+ .quote-bar:hover {
428
+ background: var(--vulcx-surface-hover);
429
+ }
430
+
431
+ .quote-bar-tag {
432
+ display: flex;
433
+ align-items: center;
434
+ gap: 6px;
435
+ color: var(--vulcx-accent);
436
+ font-weight: 600;
437
+ flex-shrink: 0;
438
+ }
439
+
440
+ .quote-bar-tag svg {
441
+ width: 16px;
442
+ height: 16px;
443
+ }
444
+
445
+ .quote-bar-fee {
446
+ padding: 2px 8px;
447
+ border-radius: 6px;
448
+ background: var(--vulcx-badge-bg);
449
+ border: 1px solid var(--vulcx-border);
450
+ font-size: 12px;
451
+ font-weight: 600;
452
+ color: var(--vulcx-text);
453
+ flex-shrink: 0;
454
+ }
455
+
456
+ .quote-bar-route {
457
+ flex: 1;
458
+ min-width: 0;
459
+ white-space: nowrap;
460
+ overflow: hidden;
461
+ text-overflow: ellipsis;
462
+ font-size: 12px;
463
+ color: var(--vulcx-text-secondary);
464
+ }
465
+
466
+ .quote-bar-info {
467
+ width: 20px;
468
+ height: 20px;
469
+ border-radius: 50%;
470
+ border: 1.5px solid var(--vulcx-border);
471
+ display: flex;
472
+ align-items: center;
473
+ justify-content: center;
474
+ flex-shrink: 0;
475
+ color: var(--vulcx-text-dim);
476
+ font-size: 12px;
477
+ font-weight: 700;
478
+ transition: border-color 0.15s, color 0.15s;
479
+ }
480
+
481
+ .quote-bar:hover .quote-bar-info {
482
+ border-color: var(--vulcx-text-secondary);
483
+ color: var(--vulcx-text);
484
+ }
485
+
486
+ /* ── Quote Detail Panel ── */
487
+ .quote-detail {
488
+ margin-top: 6px;
489
+ padding: 12px 14px;
490
+ background: var(--vulcx-surface);
491
+ border-radius: var(--vulcx-radius-sm);
492
+ font-size: 12px;
493
+ color: var(--vulcx-text-secondary);
494
+ }
495
+
496
+ .quote-detail-row {
497
+ display: flex;
498
+ justify-content: space-between;
499
+ padding: 5px 0;
500
+ }
501
+
502
+ .quote-detail-label {
503
+ opacity: 0.7;
504
+ }
505
+
506
+ .impact-warning {
507
+ color: var(--vulcx-warning);
508
+ }
509
+
510
+ .impact-high {
511
+ color: var(--vulcx-error);
512
+ }
513
+
514
+ /* ── Status Messages ── */
515
+ .error-msg {
516
+ margin-top: 8px;
517
+ padding: 10px 14px;
518
+ background: rgba(255, 77, 106, 0.08);
519
+ border: 1px solid rgba(255, 77, 106, 0.15);
520
+ border-radius: var(--vulcx-radius-sm);
521
+ color: var(--vulcx-error);
522
+ font-size: 13px;
523
+ }
524
+
525
+ .success-msg {
526
+ margin-top: 8px;
527
+ padding: 10px 14px;
528
+ background: rgba(0, 214, 143, 0.08);
529
+ border: 1px solid rgba(0, 214, 143, 0.15);
530
+ border-radius: var(--vulcx-radius-sm);
531
+ color: var(--vulcx-success);
532
+ font-size: 13px;
533
+ }
534
+
535
+ .loading-spinner {
536
+ display: inline-block;
537
+ width: 16px;
538
+ height: 16px;
539
+ border: 2px solid var(--vulcx-text-secondary);
540
+ border-top-color: transparent;
541
+ border-radius: 50%;
542
+ animation: spin 0.6s linear infinite;
543
+ vertical-align: middle;
544
+ margin-right: 6px;
545
+ }
546
+
547
+ @keyframes spin {
548
+ to { transform: rotate(360deg); }
549
+ }
550
+
551
+ /* ── Token Selector Modal ── */
552
+ .modal-overlay {
553
+ position: fixed;
554
+ inset: 0;
555
+ background: rgba(0,0,0,0.65);
556
+ z-index: 100;
557
+ display: flex;
558
+ align-items: center;
559
+ justify-content: center;
560
+ backdrop-filter: blur(4px);
561
+ }
562
+
563
+ .modal-panel {
564
+ background: var(--vulcx-surface);
565
+ border: 1px solid var(--vulcx-border);
566
+ border-radius: var(--vulcx-radius);
567
+ width: 420px;
568
+ max-width: 95vw;
569
+ max-height: 520px;
570
+ display: flex;
571
+ flex-direction: column;
572
+ overflow: hidden;
573
+ position: relative;
574
+ }
575
+
576
+ .modal-close {
577
+ position: absolute;
578
+ top: 14px;
579
+ right: 14px;
580
+ width: 28px;
581
+ height: 28px;
582
+ border: none;
583
+ background: transparent;
584
+ color: var(--vulcx-text-secondary);
585
+ cursor: pointer;
586
+ font-size: 20px;
587
+ display: flex;
588
+ align-items: center;
589
+ justify-content: center;
590
+ border-radius: 6px;
591
+ transition: background 0.15s;
592
+ }
593
+
594
+ .modal-close:hover {
595
+ background: var(--vulcx-surface-hover);
596
+ color: var(--vulcx-text);
597
+ }
598
+
599
+ .modal-search {
600
+ margin: 16px 16px 0;
601
+ padding: 14px 16px;
602
+ background: var(--vulcx-bg);
603
+ border: 1px solid var(--vulcx-border);
604
+ border-radius: var(--vulcx-radius-sm);
605
+ color: var(--vulcx-text);
606
+ font-size: 15px;
607
+ font-family: var(--vulcx-font);
608
+ outline: none;
609
+ transition: border-color 0.15s;
610
+ }
611
+
612
+ .modal-search::placeholder {
613
+ color: var(--vulcx-text-dim);
614
+ }
615
+
616
+ .modal-search:focus {
617
+ border-color: var(--vulcx-text-secondary);
618
+ }
619
+
620
+ .popular-chips {
621
+ display: flex;
622
+ gap: 8px;
623
+ padding: 12px 16px;
624
+ flex-wrap: wrap;
625
+ }
626
+
627
+ .popular-chip {
628
+ display: flex;
629
+ align-items: center;
630
+ gap: 6px;
631
+ padding: 6px 14px 6px 6px;
632
+ border-radius: 20px;
633
+ border: 1px solid var(--vulcx-border);
634
+ background: var(--vulcx-bg);
635
+ color: var(--vulcx-text);
636
+ font-size: 14px;
637
+ font-weight: 600;
638
+ font-family: var(--vulcx-font);
639
+ cursor: pointer;
640
+ transition: background 0.15s, border-color 0.15s;
641
+ white-space: nowrap;
642
+ }
643
+
644
+ .popular-chip:hover {
645
+ background: var(--vulcx-surface-hover);
646
+ border-color: var(--vulcx-text-secondary);
647
+ }
648
+
649
+ .popular-chip img {
650
+ width: 24px;
651
+ height: 24px;
652
+ border-radius: 50%;
653
+ }
654
+
655
+ .token-list {
656
+ flex: 1;
657
+ overflow-y: auto;
658
+ padding: 0 8px 8px;
659
+ }
660
+
661
+ .token-list::-webkit-scrollbar {
662
+ width: 4px;
663
+ }
664
+
665
+ .token-list::-webkit-scrollbar-track {
666
+ background: transparent;
667
+ }
668
+
669
+ .token-list::-webkit-scrollbar-thumb {
670
+ background: var(--vulcx-border);
671
+ border-radius: 2px;
672
+ }
673
+
674
+ .token-list-item {
675
+ display: flex;
676
+ align-items: center;
677
+ gap: 12px;
678
+ padding: 12px 10px;
679
+ border-radius: 12px;
680
+ cursor: pointer;
681
+ transition: background 0.12s;
682
+ }
683
+
684
+ .token-list-item:hover {
685
+ background: var(--vulcx-surface-hover);
686
+ }
687
+
688
+ .tli-icon {
689
+ width: 40px;
690
+ height: 40px;
691
+ border-radius: 50%;
692
+ object-fit: cover;
693
+ flex-shrink: 0;
694
+ }
695
+
696
+ .tli-icon-placeholder {
697
+ width: 40px;
698
+ height: 40px;
699
+ border-radius: 50%;
700
+ background: var(--vulcx-border);
701
+ flex-shrink: 0;
702
+ }
703
+
704
+ .tli-info {
705
+ flex: 1;
706
+ min-width: 0;
707
+ }
708
+
709
+ .tli-symbol {
710
+ font-size: 16px;
711
+ font-weight: 700;
712
+ color: var(--vulcx-text);
713
+ }
714
+
715
+ .tli-name {
716
+ font-size: 12px;
717
+ color: var(--vulcx-text-secondary);
718
+ margin-top: 1px;
719
+ }
720
+
721
+ .tli-address {
722
+ display: flex;
723
+ align-items: center;
724
+ gap: 4px;
725
+ font-size: 11px;
726
+ color: var(--vulcx-text-dim);
727
+ margin-top: 2px;
728
+ }
729
+
730
+ .tli-copy {
731
+ width: 12px;
732
+ height: 12px;
733
+ opacity: 0.4;
734
+ cursor: pointer;
735
+ }
736
+
737
+ .tli-balance {
738
+ text-align: right;
739
+ flex-shrink: 0;
740
+ }
741
+
742
+ .tli-balance-amount {
743
+ font-size: 15px;
744
+ font-weight: 600;
745
+ color: var(--vulcx-text);
746
+ }
747
+
748
+ .tli-balance-usd {
749
+ font-size: 12px;
750
+ color: var(--vulcx-text-secondary);
751
+ margin-top: 1px;
752
+ }
753
+
754
+ .token-list-empty {
755
+ padding: 24px;
756
+ text-align: center;
757
+ color: var(--vulcx-text-dim);
758
+ font-size: 14px;
759
+ }
760
+ `;
761
+
762
+ function createInitialState(inputMint, outputMint) {
763
+ return {
764
+ inputMint,
765
+ outputMint,
766
+ inputAmount: "",
767
+ outputAmount: "",
768
+ slippageBps: 50,
769
+ quote: null,
770
+ status: "idle",
771
+ error: "",
772
+ walletAddress: "",
773
+ inputBalance: "0",
774
+ outputBalance: "0",
775
+ inputDecimals: 9,
776
+ outputDecimals: 9,
777
+ tokenMap: new Map(),
778
+ allBalances: new Map(),
779
+ nativeBalance: "0",
780
+ selectorOpen: null,
781
+ selectorSearch: "",
782
+ };
783
+ }
784
+ class Store {
785
+ constructor(initial) {
786
+ this.listeners = new Set();
787
+ this.state = { ...initial };
788
+ }
789
+ getState() {
790
+ return this.state;
791
+ }
792
+ setState(partial) {
793
+ this.state = { ...this.state, ...partial };
794
+ this.listeners.forEach((fn) => fn(this.state));
795
+ }
796
+ subscribe(fn) {
797
+ this.listeners.add(fn);
798
+ return () => this.listeners.delete(fn);
799
+ }
800
+ }
801
+
802
+ function formatAmount(raw, decimals) {
803
+ if (!raw || raw === "0")
804
+ return "0";
805
+ const padded = raw.padStart(decimals + 1, "0");
806
+ const intPart = padded.slice(0, padded.length - decimals) || "0";
807
+ const fracPart = padded.slice(padded.length - decimals);
808
+ const trimmed = fracPart.replace(/0+$/, "");
809
+ return trimmed ? `${intPart}.${trimmed}` : intPart;
810
+ }
811
+ function shortenAddress(addr, chars = 4) {
812
+ if (addr.length <= chars * 2 + 3)
813
+ return addr;
814
+ return `${addr.slice(0, chars)}...${addr.slice(-chars)}`;
815
+ }
816
+
817
+ const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
818
+ async function rpcCall(url, method, params) {
819
+ const res = await fetch(url, {
820
+ method: "POST",
821
+ headers: { "Content-Type": "application/json" },
822
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
823
+ });
824
+ const json = (await res.json());
825
+ if (json.error)
826
+ throw new Error(json.error.message);
827
+ return json.result;
828
+ }
829
+ async function getNativeBalance(rpcUrl, wallet) {
830
+ const result = await rpcCall(rpcUrl, "getBalance", [wallet]);
831
+ return String(result.value);
832
+ }
833
+ async function getTokenBalances(rpcUrl, wallet) {
834
+ const result = await rpcCall(rpcUrl, "getTokenAccountsByOwner", [
835
+ wallet,
836
+ { programId: TOKEN_PROGRAM },
837
+ { encoding: "jsonParsed" },
838
+ ]);
839
+ const balances = new Map();
840
+ for (const acct of result.value) {
841
+ const info = acct.account.data.parsed.info;
842
+ balances.set(info.mint, {
843
+ amount: info.tokenAmount.amount,
844
+ decimals: info.tokenAmount.decimals,
845
+ });
846
+ }
847
+ return balances;
848
+ }
849
+ const NATIVE_SOL_MINT = "So11111111111111111111111111111111111111112";
850
+
851
+ const DEBOUNCE_MS = 400;
852
+ const DEFAULT_RPC = "https://mainnet.fogo.io/";
853
+ const SWAP_ICON_SVG = `<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4v16M7 20l-3-3m3 3l3-3M17 20V4m0 0l-3 3m3-3l3 3"/></svg>`;
854
+ const CHEVRON_SVG = `<svg class="badge-chevron" viewBox="0 0 12 8" fill="currentColor"><path d="M1.4 1.4L6 6l4.6-4.6"/></svg>`;
855
+ const WALLET_SVG = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="20" height="14" rx="2"/><path d="M2 10h20"/><circle cx="17" cy="14" r="1.5"/></svg>`;
856
+ const COPY_SVG = `<svg class="tli-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>`;
857
+ const CLOSE_SVG = `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>`;
858
+ const TAG_SVG = `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 4a2 2 0 012-2h5.586a2 2 0 011.414.586l9.414 9.414a2 2 0 010 2.828l-5.586 5.586a2 2 0 01-2.828 0L2.586 11A2 2 0 012 9.586V4zm4.5 3a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"/></svg>`;
859
+ const POPULAR_MINTS = [
860
+ "So11111111111111111111111111111111111111112",
861
+ "uSd2czE61Evaf76RNbq4KPpXnkiL3irdzgLFUMe3NoG",
862
+ ];
863
+ class VulcxSwapElement extends HTMLElement {
864
+ static get observedAttributes() {
865
+ return [
866
+ "api-key", "chain", "base-url",
867
+ "default-input-mint", "default-output-mint",
868
+ "theme", "rpc-url",
869
+ ];
870
+ }
871
+ constructor() {
872
+ super();
873
+ this.debounceTimer = null;
874
+ this.unsubscribe = null;
875
+ this.mounted = false;
876
+ this.rpcUrl = DEFAULT_RPC;
877
+ this.quoteDetailOpen = false;
878
+ this.shadow = this.attachShadow({ mode: "open" });
879
+ }
880
+ connectedCallback() {
881
+ const apiKey = this.getAttribute("api-key") || "";
882
+ const chain = this.getAttribute("chain") || "solana";
883
+ const baseUrl = this.getAttribute("base-url") || undefined;
884
+ const inputMint = this.getAttribute("default-input-mint") || "";
885
+ const outputMint = this.getAttribute("default-output-mint") || "";
886
+ this.rpcUrl = this.getAttribute("rpc-url") || DEFAULT_RPC;
887
+ this.sdk = new VulcxSDK({ apiKey, chain, baseUrl });
888
+ this.store = new Store(createInitialState(inputMint, outputMint));
889
+ this.unsubscribe = this.store.subscribe(() => this.update());
890
+ this.mount();
891
+ this.loadTokenList();
892
+ }
893
+ disconnectedCallback() {
894
+ this.unsubscribe?.();
895
+ if (this.debounceTimer)
896
+ clearTimeout(this.debounceTimer);
897
+ this.mounted = false;
898
+ }
899
+ attributeChangedCallback() {
900
+ if (!this.store)
901
+ return;
902
+ const apiKey = this.getAttribute("api-key") || "";
903
+ const chain = this.getAttribute("chain") || "solana";
904
+ const baseUrl = this.getAttribute("base-url") || undefined;
905
+ this.rpcUrl = this.getAttribute("rpc-url") || DEFAULT_RPC;
906
+ this.sdk = new VulcxSDK({ apiKey, chain, baseUrl });
907
+ }
908
+ emit(name, detail) {
909
+ this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
910
+ }
911
+ async loadTokenList() {
912
+ try {
913
+ const baseUrl = this.getAttribute("base-url") || "https://api.vulcx.xyz";
914
+ const res = await fetch(`${baseUrl}/api/v1/tokens`);
915
+ if (!res.ok)
916
+ return;
917
+ const data = await res.json();
918
+ const map = new Map();
919
+ for (const t of data.tokens || []) {
920
+ map.set(t.mint, t);
921
+ }
922
+ this.store.setState({ tokenMap: map });
923
+ }
924
+ catch { /* silent */ }
925
+ }
926
+ async loadBalances() {
927
+ const { walletAddress, inputMint, outputMint } = this.store.getState();
928
+ if (!walletAddress)
929
+ return;
930
+ try {
931
+ const [nativeBal, tokenBals] = await Promise.all([
932
+ getNativeBalance(this.rpcUrl, walletAddress),
933
+ getTokenBalances(this.rpcUrl, walletAddress),
934
+ ]);
935
+ let inputBal = "0";
936
+ let outputBal = "0";
937
+ let inputDec = 9;
938
+ let outputDec = 9;
939
+ const tokenMap = this.store.getState().tokenMap;
940
+ if (inputMint === NATIVE_SOL_MINT) {
941
+ inputBal = nativeBal;
942
+ inputDec = 9;
943
+ }
944
+ else if (tokenBals.has(inputMint)) {
945
+ const b = tokenBals.get(inputMint);
946
+ inputBal = b.amount;
947
+ inputDec = b.decimals;
948
+ }
949
+ if (tokenMap.has(inputMint))
950
+ inputDec = tokenMap.get(inputMint).decimals;
951
+ if (outputMint === NATIVE_SOL_MINT) {
952
+ outputBal = nativeBal;
953
+ outputDec = 9;
954
+ }
955
+ else if (tokenBals.has(outputMint)) {
956
+ const b = tokenBals.get(outputMint);
957
+ outputBal = b.amount;
958
+ outputDec = b.decimals;
959
+ }
960
+ if (tokenMap.has(outputMint))
961
+ outputDec = tokenMap.get(outputMint).decimals;
962
+ this.store.setState({
963
+ inputBalance: inputBal,
964
+ outputBalance: outputBal,
965
+ inputDecimals: inputDec,
966
+ outputDecimals: outputDec,
967
+ allBalances: tokenBals,
968
+ nativeBalance: nativeBal,
969
+ });
970
+ }
971
+ catch { /* silent */ }
972
+ }
973
+ async fetchQuote() {
974
+ const { inputMint, outputMint, inputAmount, slippageBps } = this.store.getState();
975
+ if (!inputMint || !outputMint || !inputAmount || inputAmount === "0") {
976
+ this.store.setState({ quote: null, outputAmount: "", status: "idle" });
977
+ return;
978
+ }
979
+ this.store.setState({ status: "quoting", error: "" });
980
+ try {
981
+ const quote = await this.sdk.quote({
982
+ inputMint, outputMint, amount: inputAmount,
983
+ swapMode: "ExactIn", slippageBps,
984
+ });
985
+ this.store.setState({ quote, outputAmount: quote.amountOut, status: "idle" });
986
+ this.emit("quote-update", quote);
987
+ }
988
+ catch (err) {
989
+ const msg = err instanceof Error ? err.message : "Quote failed";
990
+ this.store.setState({ status: "error", error: msg, quote: null, outputAmount: "" });
991
+ }
992
+ }
993
+ debouncedQuote() {
994
+ if (this.debounceTimer)
995
+ clearTimeout(this.debounceTimer);
996
+ this.debounceTimer = setTimeout(() => this.fetchQuote(), DEBOUNCE_MS);
997
+ }
998
+ async executeSwap() {
999
+ const { inputMint, outputMint, inputAmount, slippageBps, walletAddress } = this.store.getState();
1000
+ if (!walletAddress) {
1001
+ this.store.setState({ error: "Connect wallet first" });
1002
+ return;
1003
+ }
1004
+ this.store.setState({ status: "swapping", error: "" });
1005
+ this.emit("swap-initiated", { inputMint, outputMint, amount: inputAmount });
1006
+ try {
1007
+ const result = await this.sdk.swap({
1008
+ userWallet: walletAddress, inputMint, outputMint,
1009
+ amount: inputAmount, swapMode: "ExactIn", slippageBps,
1010
+ });
1011
+ this.store.setState({ status: "success" });
1012
+ this.emit("swap-complete", result);
1013
+ }
1014
+ catch (err) {
1015
+ const msg = err instanceof Error ? err.message : "Swap failed";
1016
+ this.store.setState({ status: "error", error: msg });
1017
+ this.emit("swap-error", { error: msg });
1018
+ }
1019
+ }
1020
+ handleSwapDirection() {
1021
+ const { inputMint, outputMint, inputBalance, outputBalance, inputDecimals, outputDecimals } = this.store.getState();
1022
+ this.store.setState({
1023
+ inputMint: outputMint, outputMint: inputMint,
1024
+ inputAmount: "", outputAmount: "", quote: null, status: "idle",
1025
+ inputBalance: outputBalance, outputBalance: inputBalance,
1026
+ inputDecimals: outputDecimals, outputDecimals: inputDecimals,
1027
+ });
1028
+ }
1029
+ setWallet(address) {
1030
+ this.store.setState({ walletAddress: address });
1031
+ if (address)
1032
+ this.loadBalances();
1033
+ }
1034
+ getTokenInfo(mint) {
1035
+ return this.store.getState().tokenMap.get(mint);
1036
+ }
1037
+ renderBadge(mint) {
1038
+ const info = this.getTokenInfo(mint);
1039
+ if (info) {
1040
+ return `<img class="token-icon" src="${info.logoURI}" alt="${info.symbol}" onerror="this.style.display='none'"/><span>${info.symbol}</span>${CHEVRON_SVG}`;
1041
+ }
1042
+ if (!mint)
1043
+ return `<span>Select</span>${CHEVRON_SVG}`;
1044
+ const short = mint.length > 8 ? mint.slice(0, 4) + "..." + mint.slice(-4) : mint;
1045
+ return `<div class="token-icon-placeholder"></div><span>${short}</span>${CHEVRON_SVG}`;
1046
+ }
1047
+ getButtonText(state) {
1048
+ if (!state.walletAddress)
1049
+ return "Connect";
1050
+ if (state.status === "quoting")
1051
+ return "Fetching Quote...";
1052
+ if (state.status === "swapping")
1053
+ return "Swapping...";
1054
+ if (!state.inputAmount)
1055
+ return "Enter Amount";
1056
+ if (!state.inputMint || !state.outputMint)
1057
+ return "Select Tokens";
1058
+ return "Swap";
1059
+ }
1060
+ isButtonDisabled(state) {
1061
+ if (!state.walletAddress)
1062
+ return false;
1063
+ return state.status === "quoting" || state.status === "swapping" || !state.inputAmount || !state.inputMint || !state.outputMint;
1064
+ }
1065
+ renderQuoteInfoHTML(quote) {
1066
+ const fee = (quote.feeBps / 100).toFixed(quote.feeBps % 100 === 0 ? 0 : 1);
1067
+ const pools = quote.routes.map(r => r.poolType);
1068
+ const uniquePools = [...new Set(pools)];
1069
+ const moreCount = uniquePools.length > 2 ? uniquePools.length - 2 : 0;
1070
+ const shown = uniquePools.slice(0, 2).join(", ");
1071
+ const routeText = moreCount > 0 ? `via ${shown} & ${moreCount} more` : `via ${shown}`;
1072
+ const impactClass = (quote.priceImpactSeverity === "high" || quote.priceImpactSeverity === "extreme")
1073
+ ? "impact-high" : quote.priceImpactSeverity === "moderate" ? "impact-warning" : "";
1074
+ let html = `
1075
+ <div class="quote-bar" id="quote-bar">
1076
+ <span class="quote-bar-tag">${TAG_SVG} Quotes</span>
1077
+ <span class="quote-bar-fee">${fee} %</span>
1078
+ <span class="quote-bar-route">${routeText}</span>
1079
+ <span class="quote-bar-info">i</span>
1080
+ </div>`;
1081
+ if (this.quoteDetailOpen) {
1082
+ html += `
1083
+ <div class="quote-detail">
1084
+ <div class="quote-detail-row"><span class="quote-detail-label">Price Impact</span><span class="${impactClass}">${quote.priceImpactPercent}</span></div>
1085
+ <div class="quote-detail-row"><span class="quote-detail-label">Route</span><span>${quote.hopCount} hop${quote.hopCount > 1 ? "s" : ""} via ${pools.join(" → ")}</span></div>
1086
+ <div class="quote-detail-row"><span class="quote-detail-label">Fee</span><span>${fee}%</span></div>
1087
+ <div class="quote-detail-row"><span class="quote-detail-label">Min Received</span><span>${quote.otherAmountThreshold}</span></div>
1088
+ </div>`;
1089
+ }
1090
+ return html;
1091
+ }
1092
+ formatBalance(raw, decimals, symbol) {
1093
+ const formatted = formatAmount(raw, decimals);
1094
+ return `${formatted} ${symbol}`;
1095
+ }
1096
+ getTokenBalance(mint) {
1097
+ const state = this.store.getState();
1098
+ if (mint === NATIVE_SOL_MINT)
1099
+ return { amount: state.nativeBalance, decimals: 9 };
1100
+ const bal = state.allBalances.get(mint);
1101
+ if (bal)
1102
+ return bal;
1103
+ const info = state.tokenMap.get(mint);
1104
+ return { amount: "0", decimals: info?.decimals ?? 9 };
1105
+ }
1106
+ getFilteredTokens() {
1107
+ const state = this.store.getState();
1108
+ const search = state.selectorSearch.toLowerCase().trim();
1109
+ const tokens = Array.from(state.tokenMap.values());
1110
+ if (!search)
1111
+ return tokens;
1112
+ return tokens.filter(t => t.symbol.toLowerCase().includes(search) ||
1113
+ t.name.toLowerCase().includes(search) ||
1114
+ t.mint.toLowerCase().includes(search));
1115
+ }
1116
+ openSelector(target) {
1117
+ this.store.setState({ selectorOpen: target, selectorSearch: "" });
1118
+ this.renderModal();
1119
+ }
1120
+ closeSelector() {
1121
+ this.store.setState({ selectorOpen: null, selectorSearch: "" });
1122
+ const overlay = this.shadow.querySelector("#token-modal");
1123
+ if (overlay)
1124
+ overlay.remove();
1125
+ }
1126
+ selectToken(mint) {
1127
+ const target = this.store.getState().selectorOpen;
1128
+ if (!target)
1129
+ return;
1130
+ const bal = this.getTokenBalance(mint);
1131
+ const info = this.store.getState().tokenMap.get(mint);
1132
+ const decimals = info?.decimals ?? bal.decimals;
1133
+ if (target === "input") {
1134
+ this.store.setState({
1135
+ inputMint: mint,
1136
+ inputBalance: bal.amount,
1137
+ inputDecimals: decimals,
1138
+ inputAmount: "",
1139
+ outputAmount: "",
1140
+ quote: null,
1141
+ });
1142
+ }
1143
+ else {
1144
+ this.store.setState({
1145
+ outputMint: mint,
1146
+ outputBalance: bal.amount,
1147
+ outputDecimals: decimals,
1148
+ outputAmount: "",
1149
+ quote: null,
1150
+ });
1151
+ }
1152
+ this.closeSelector();
1153
+ }
1154
+ renderModal() {
1155
+ let overlay = this.shadow.querySelector("#token-modal");
1156
+ if (!overlay) {
1157
+ overlay = document.createElement("div");
1158
+ overlay.id = "token-modal";
1159
+ overlay.className = "modal-overlay";
1160
+ overlay.addEventListener("click", (e) => {
1161
+ if (e.target === overlay)
1162
+ this.closeSelector();
1163
+ });
1164
+ this.shadow.appendChild(overlay);
1165
+ }
1166
+ const state = this.store.getState();
1167
+ const filtered = this.getFilteredTokens();
1168
+ const popularChips = POPULAR_MINTS
1169
+ .map(m => state.tokenMap.get(m))
1170
+ .filter((t) => !!t);
1171
+ const allPopular = Array.from(state.tokenMap.values())
1172
+ .filter(t => !POPULAR_MINTS.includes(t.mint))
1173
+ .slice(0, 3);
1174
+ const chips = [...popularChips, ...allPopular];
1175
+ const chipsHTML = chips.map(t => `<button class="popular-chip" data-mint="${t.mint}"><img src="${t.logoURI}" alt="${t.symbol}" onerror="this.style.display='none'"/>${t.symbol}</button>`).join("");
1176
+ const listHTML = filtered.length === 0
1177
+ ? `<div class="token-list-empty">No tokens found</div>`
1178
+ : filtered.map(t => {
1179
+ const bal = this.getTokenBalance(t.mint);
1180
+ const balDisplay = formatAmount(bal.amount, bal.decimals);
1181
+ const addr = shortenAddress(t.mint);
1182
+ const icon = t.logoURI
1183
+ ? `<img class="tli-icon" src="${t.logoURI}" alt="${t.symbol}" onerror="this.className='tli-icon-placeholder'"/>`
1184
+ : `<div class="tli-icon-placeholder"></div>`;
1185
+ return `<div class="token-list-item" data-mint="${t.mint}">
1186
+ ${icon}
1187
+ <div class="tli-info">
1188
+ <div class="tli-symbol">${t.symbol}</div>
1189
+ <div class="tli-name">${t.name}</div>
1190
+ <div class="tli-address"><span>${addr}</span> <span class="tli-copy-btn" data-addr="${t.mint}">${COPY_SVG}</span></div>
1191
+ </div>
1192
+ <div class="tli-balance">
1193
+ <div class="tli-balance-amount">${balDisplay}</div>
1194
+ <div class="tli-balance-usd">$0.00</div>
1195
+ </div>
1196
+ </div>`;
1197
+ }).join("");
1198
+ overlay.innerHTML = `
1199
+ <div class="modal-panel">
1200
+ <button class="modal-close" id="modal-close">${CLOSE_SVG}</button>
1201
+ <input class="modal-search" type="text" placeholder="Search for a token or address" id="modal-search" value="${state.selectorSearch}" />
1202
+ <div class="popular-chips" id="popular-chips">${chipsHTML}</div>
1203
+ <div class="token-list" id="token-list">${listHTML}</div>
1204
+ </div>
1205
+ `;
1206
+ this.bindModalEvents(overlay);
1207
+ const searchInput = overlay.querySelector("#modal-search");
1208
+ if (searchInput)
1209
+ searchInput.focus();
1210
+ }
1211
+ bindModalEvents(overlay) {
1212
+ const closeBtn = overlay.querySelector("#modal-close");
1213
+ if (closeBtn)
1214
+ closeBtn.addEventListener("click", () => this.closeSelector());
1215
+ const searchInput = overlay.querySelector("#modal-search");
1216
+ if (searchInput) {
1217
+ searchInput.addEventListener("input", () => {
1218
+ this.store.setState({ selectorSearch: searchInput.value });
1219
+ this.renderModal();
1220
+ });
1221
+ }
1222
+ overlay.querySelectorAll(".popular-chip").forEach(chip => {
1223
+ chip.addEventListener("click", () => {
1224
+ const mint = chip.dataset.mint;
1225
+ if (mint)
1226
+ this.selectToken(mint);
1227
+ });
1228
+ });
1229
+ overlay.querySelectorAll(".token-list-item").forEach(item => {
1230
+ item.addEventListener("click", (e) => {
1231
+ if (e.target.closest(".tli-copy-btn"))
1232
+ return;
1233
+ const mint = item.dataset.mint;
1234
+ if (mint)
1235
+ this.selectToken(mint);
1236
+ });
1237
+ });
1238
+ overlay.querySelectorAll(".tli-copy-btn").forEach(btn => {
1239
+ btn.addEventListener("click", (e) => {
1240
+ e.stopPropagation();
1241
+ const addr = btn.dataset.addr;
1242
+ if (addr)
1243
+ navigator.clipboard?.writeText(addr);
1244
+ });
1245
+ });
1246
+ }
1247
+ mount() {
1248
+ const state = this.store.getState();
1249
+ const inputInfo = this.getTokenInfo(state.inputMint);
1250
+ const outputInfo = this.getTokenInfo(state.outputMint);
1251
+ const inputSymbol = inputInfo?.symbol || "Token";
1252
+ const outputSymbol = outputInfo?.symbol || "Token";
1253
+ this.shadow.innerHTML = `
1254
+ <style>${WIDGET_CSS}</style>
1255
+ <div class="swap-container">
1256
+
1257
+ <div class="token-panel">
1258
+ <div class="token-panel-label">You're selling</div>
1259
+ <div class="token-row">
1260
+ <input class="amount-input" type="text" inputmode="decimal" placeholder="0.00" value="${state.inputAmount}" id="input-amount" />
1261
+ <div class="token-badge" id="select-input">${this.renderBadge(state.inputMint)}</div>
1262
+ </div>
1263
+ <div class="usd-value" id="input-usd">$0.00</div>
1264
+ <div class="balance-row">
1265
+ <span class="balance-label">${WALLET_SVG}<span id="input-balance-label">${this.formatBalance(state.inputBalance, state.inputDecimals, inputSymbol)}</span></span>
1266
+ <div class="balance-actions">
1267
+ <button class="half-max-btn" id="btn-half">HALF</button>
1268
+ <button class="half-max-btn" id="btn-max">MAX</button>
1269
+ </div>
1270
+ </div>
1271
+ </div>
1272
+
1273
+ <div class="swap-direction">
1274
+ <button class="swap-direction-btn" id="swap-direction">${SWAP_ICON_SVG}</button>
1275
+ </div>
1276
+
1277
+ <div class="token-panel">
1278
+ <div class="token-panel-label">To buy</div>
1279
+ <div class="token-row">
1280
+ <input class="amount-input" type="text" placeholder="0.00" value="${state.outputAmount}" disabled id="output-amount" />
1281
+ <div class="token-badge" id="select-output">${this.renderBadge(state.outputMint)}</div>
1282
+ </div>
1283
+ <div class="usd-value" id="output-usd">$0.00</div>
1284
+ <div class="balance-row">
1285
+ <span class="balance-label">${WALLET_SVG}<span id="output-balance-label">${this.formatBalance(state.outputBalance, state.outputDecimals, outputSymbol)}</span></span>
1286
+ <div class="balance-actions">
1287
+ <button class="half-max-btn" id="btn-half-out">HALF</button>
1288
+ <button class="half-max-btn" id="btn-max-out">MAX</button>
1289
+ </div>
1290
+ </div>
1291
+ </div>
1292
+
1293
+ <button class="swap-btn" id="swap-btn">${this.getButtonText(state)}</button>
1294
+ <div id="quote-info"></div>
1295
+ <div id="status-msg"></div>
1296
+ </div>
1297
+ `;
1298
+ this.bindEvents();
1299
+ this.mounted = true;
1300
+ }
1301
+ bindEvents() {
1302
+ this.$("#input-amount", (el) => {
1303
+ el.oninput = () => {
1304
+ const val = el.value.replace(/[^0-9.]/g, "");
1305
+ if (val !== el.value)
1306
+ el.value = val;
1307
+ this.store.setState({ inputAmount: val });
1308
+ this.debouncedQuote();
1309
+ };
1310
+ });
1311
+ this.$("#select-input", el => { el.onclick = () => this.openSelector("input"); });
1312
+ this.$("#select-output", el => { el.onclick = () => this.openSelector("output"); });
1313
+ this.$("#swap-direction", el => { el.onclick = () => this.handleSwapDirection(); });
1314
+ this.$("#swap-btn", el => {
1315
+ el.onclick = () => {
1316
+ const s = this.store.getState();
1317
+ if (!s.walletAddress)
1318
+ this.emit("connect-wallet", {});
1319
+ else
1320
+ this.executeSwap();
1321
+ };
1322
+ });
1323
+ this.$("#btn-half", el => {
1324
+ el.onclick = () => {
1325
+ const { inputBalance, inputDecimals } = this.store.getState();
1326
+ const half = (BigInt(inputBalance || "0") / 2n).toString();
1327
+ const display = formatAmount(half, inputDecimals);
1328
+ this.store.setState({ inputAmount: half });
1329
+ this.$("#input-amount", inp => { inp.value = display; });
1330
+ this.debouncedQuote();
1331
+ };
1332
+ });
1333
+ this.$("#btn-max", el => {
1334
+ el.onclick = () => {
1335
+ const { inputBalance, inputDecimals } = this.store.getState();
1336
+ const display = formatAmount(inputBalance, inputDecimals);
1337
+ this.store.setState({ inputAmount: inputBalance });
1338
+ this.$("#input-amount", inp => { inp.value = display; });
1339
+ this.debouncedQuote();
1340
+ };
1341
+ });
1342
+ }
1343
+ update() {
1344
+ if (!this.mounted)
1345
+ return;
1346
+ const state = this.store.getState();
1347
+ const inputInfo = this.getTokenInfo(state.inputMint);
1348
+ const outputInfo = this.getTokenInfo(state.outputMint);
1349
+ const inputSymbol = inputInfo?.symbol || "Token";
1350
+ const outputSymbol = outputInfo?.symbol || "Token";
1351
+ this.$("#input-amount", el => {
1352
+ if (this.shadow.activeElement !== el)
1353
+ el.value = state.inputAmount;
1354
+ });
1355
+ this.$("#output-amount", el => { el.value = state.outputAmount; });
1356
+ this.$("#select-input", el => { el.innerHTML = this.renderBadge(state.inputMint); });
1357
+ this.$("#select-output", el => { el.innerHTML = this.renderBadge(state.outputMint); });
1358
+ this.$("#input-balance-label", el => {
1359
+ el.textContent = this.formatBalance(state.inputBalance, state.inputDecimals, inputSymbol);
1360
+ });
1361
+ this.$("#output-balance-label", el => {
1362
+ el.textContent = this.formatBalance(state.outputBalance, state.outputDecimals, outputSymbol);
1363
+ });
1364
+ const btnText = this.getButtonText(state);
1365
+ const btnDisabled = this.isButtonDisabled(state);
1366
+ const showSpinner = state.status === "quoting" || state.status === "swapping";
1367
+ this.$("#swap-btn", el => {
1368
+ el.innerHTML = showSpinner ? `<span class="loading-spinner"></span>${btnText}` : btnText;
1369
+ el.disabled = btnDisabled;
1370
+ });
1371
+ this.$("#quote-info", el => {
1372
+ el.innerHTML = state.quote ? this.renderQuoteInfoHTML(state.quote) : "";
1373
+ const bar = el.querySelector("#quote-bar");
1374
+ if (bar) {
1375
+ bar.addEventListener("click", () => {
1376
+ this.quoteDetailOpen = !this.quoteDetailOpen;
1377
+ this.update();
1378
+ });
1379
+ }
1380
+ });
1381
+ this.$("#status-msg", el => {
1382
+ if (state.status === "error")
1383
+ el.innerHTML = `<div class="error-msg">${state.error}</div>`;
1384
+ else if (state.status === "success")
1385
+ el.innerHTML = `<div class="success-msg">Swap successful!</div>`;
1386
+ else
1387
+ el.innerHTML = "";
1388
+ });
1389
+ }
1390
+ $(sel, fn) {
1391
+ const el = this.shadow.querySelector(sel);
1392
+ if (el)
1393
+ fn(el);
1394
+ }
1395
+ }
1396
+
1397
+ if (typeof customElements !== "undefined" && !customElements.get("vulcx-swap")) {
1398
+ customElements.define("vulcx-swap", VulcxSwapElement);
1399
+ }
1400
+
1401
+ exports.VulcxSwapElement = VulcxSwapElement;
1402
+
1403
+ }));
1404
+ //# sourceMappingURL=vulcx-widget.umd.js.map