@caatinga/cli 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,248 @@
1
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
2
+ import { formatCaatingaError } from "@caatinga/core/browser";
3
+ import type { ISupportedWallet } from "@creit.tech/stellar-wallets-kit/types";
4
+ import { stellarWalletAdapter } from "../wallet.js";
5
+ import {
6
+ cancelWalletSelection,
7
+ getWalletModalState,
8
+ resolveWalletSelection,
9
+ subscribeWalletModal
10
+ } from "../wallet-modal-controller.js";
11
+
12
+ const LEARN_MORE_URL = "https://developers.stellar.org/docs/build/apps/wallet/overview";
13
+
14
+ // SWK wallet modules reject with plain `{ code, message }` objects, not Errors.
15
+ function describeWalletError(caught: unknown): string {
16
+ if (caught && typeof caught === "object" && "message" in caught) {
17
+ const message = (caught as { message?: unknown }).message;
18
+ if (typeof message === "string" && message) {
19
+ return message;
20
+ }
21
+ }
22
+ return formatCaatingaError(caught);
23
+ }
24
+
25
+ function walletBadge(wallet: ISupportedWallet): string | null {
26
+ if (!wallet.isAvailable) {
27
+ return "Install ↗";
28
+ }
29
+ // Bridge wallets (WalletConnect) hand off to a QR code instead of an extension.
30
+ return wallet.type === "BRIDGE_WALLET" ? "QR code" : null;
31
+ }
32
+
33
+ interface WalletOptionProps {
34
+ wallet: ISupportedWallet;
35
+ connectingId: string | null;
36
+ onSelect(wallet: ISupportedWallet): void;
37
+ }
38
+
39
+ function WalletOption({ wallet, connectingId, onSelect }: WalletOptionProps) {
40
+ const connecting = connectingId === wallet.id;
41
+ const badge = walletBadge(wallet);
42
+
43
+ return (
44
+ <li>
45
+ <button
46
+ type="button"
47
+ className={connecting ? "wallet-option wallet-option--connecting" : "wallet-option"}
48
+ onClick={() => onSelect(wallet)}
49
+ disabled={connectingId !== null && !connecting}
50
+ >
51
+ <img className="wallet-option__icon" src={wallet.icon} alt="" />
52
+ <span className="wallet-option__name">
53
+ {connecting ? `Connecting to ${wallet.name}…` : wallet.name}
54
+ </span>
55
+ {connecting ? (
56
+ <span className="wallet-option__spinner" aria-hidden="true" />
57
+ ) : badge ? (
58
+ <span className="wallet-option__badge">{badge}</span>
59
+ ) : (
60
+ <span className="status-dot status-dot--on" aria-hidden="true" />
61
+ )}
62
+ </button>
63
+ </li>
64
+ );
65
+ }
66
+
67
+ export function WalletModal() {
68
+ const { open } = useSyncExternalStore(
69
+ subscribeWalletModal,
70
+ getWalletModalState,
71
+ getWalletModalState
72
+ );
73
+ const panelRef = useRef<HTMLDivElement>(null);
74
+ const [wallets, setWallets] = useState<ISupportedWallet[] | null>(null);
75
+ const [connectingId, setConnectingId] = useState<string | null>(null);
76
+ const [error, setError] = useState<string | null>(null);
77
+
78
+ useEffect(() => {
79
+ if (!open) {
80
+ return;
81
+ }
82
+
83
+ setWallets(null);
84
+ setConnectingId(null);
85
+ setError(null);
86
+
87
+ // Availability probes can take a moment; ignore the result if the modal
88
+ // closed (or reopened) before they resolve.
89
+ let cancelled = false;
90
+ stellarWalletAdapter
91
+ .getSupportedWallets()
92
+ .then((list) => {
93
+ if (!cancelled) {
94
+ setWallets(list);
95
+ }
96
+ })
97
+ .catch((caught: unknown) => {
98
+ if (!cancelled) {
99
+ setWallets([]);
100
+ setError(describeWalletError(caught));
101
+ }
102
+ });
103
+
104
+ panelRef.current?.focus();
105
+
106
+ function onKeyDown(event: KeyboardEvent) {
107
+ if (event.key === "Escape") {
108
+ cancelWalletSelection();
109
+ }
110
+ }
111
+ window.addEventListener("keydown", onKeyDown);
112
+
113
+ return () => {
114
+ cancelled = true;
115
+ window.removeEventListener("keydown", onKeyDown);
116
+ };
117
+ }, [open]);
118
+
119
+ if (!open) {
120
+ return null;
121
+ }
122
+
123
+ async function selectWallet(wallet: ISupportedWallet) {
124
+ if (connectingId) {
125
+ return;
126
+ }
127
+
128
+ if (!wallet.isAvailable) {
129
+ window.open(wallet.url, "_blank", "noopener");
130
+ return;
131
+ }
132
+
133
+ setError(null);
134
+ setConnectingId(wallet.id);
135
+ try {
136
+ stellarWalletAdapter.setWallet(wallet.id);
137
+ const address = await stellarWalletAdapter.getPublicKey();
138
+ resolveWalletSelection(address);
139
+ } catch (caught) {
140
+ setError(describeWalletError(caught));
141
+ } finally {
142
+ setConnectingId(null);
143
+ }
144
+ }
145
+
146
+ const detected = wallets?.filter((wallet) => wallet.isAvailable) ?? [];
147
+ const others = wallets?.filter((wallet) => !wallet.isAvailable) ?? [];
148
+
149
+ return (
150
+ <div
151
+ className="wallet-modal"
152
+ role="presentation"
153
+ onClick={(event) => {
154
+ if (event.target === event.currentTarget) {
155
+ cancelWalletSelection();
156
+ }
157
+ }}
158
+ >
159
+ <div
160
+ ref={panelRef}
161
+ className="wallet-modal__panel"
162
+ role="dialog"
163
+ aria-modal="true"
164
+ aria-labelledby="wallet-modal-title"
165
+ tabIndex={-1}
166
+ >
167
+ <header className="wallet-modal__header">
168
+ <h2 id="wallet-modal-title">Connect a wallet</h2>
169
+ <button
170
+ type="button"
171
+ className="wallet-modal__close"
172
+ onClick={() => cancelWalletSelection()}
173
+ aria-label="Close"
174
+ >
175
+
176
+ </button>
177
+ </header>
178
+
179
+ {error ? (
180
+ <p className="wallet-modal__error" role="alert">
181
+ {error}
182
+ </p>
183
+ ) : null}
184
+
185
+ <div className="wallet-modal__body">
186
+ {wallets === null ? (
187
+ <ul className="wallet-modal__list" aria-label="Loading wallets">
188
+ {[0, 1, 2].map((row) => (
189
+ <li key={row} className="wallet-option wallet-option--skeleton" aria-hidden="true">
190
+ <span className="wallet-skeleton__icon" />
191
+ <span className="wallet-skeleton__line" />
192
+ </li>
193
+ ))}
194
+ </ul>
195
+ ) : (
196
+ <>
197
+ {detected.length > 0 ? (
198
+ <>
199
+ <p className="eyebrow wallet-modal__section">Detected</p>
200
+ <ul className="wallet-modal__list">
201
+ {detected.map((wallet) => (
202
+ <WalletOption
203
+ key={wallet.id}
204
+ wallet={wallet}
205
+ connectingId={connectingId}
206
+ onSelect={(selected) => void selectWallet(selected)}
207
+ />
208
+ ))}
209
+ </ul>
210
+ </>
211
+ ) : null}
212
+
213
+ {others.length > 0 ? (
214
+ <>
215
+ <p className="eyebrow wallet-modal__section">More wallets</p>
216
+ <ul className="wallet-modal__list">
217
+ {others.map((wallet) => (
218
+ <WalletOption
219
+ key={wallet.id}
220
+ wallet={wallet}
221
+ connectingId={connectingId}
222
+ onSelect={(selected) => void selectWallet(selected)}
223
+ />
224
+ ))}
225
+ </ul>
226
+ </>
227
+ ) : null}
228
+
229
+ {detected.length === 0 && others.length === 0 && !error ? (
230
+ <p className="wallet-modal__empty">
231
+ No wallets available. Install a Stellar wallet to continue.
232
+ </p>
233
+ ) : null}
234
+ </>
235
+ )}
236
+
237
+ </div>
238
+
239
+ <footer className="wallet-modal__footer">
240
+ <span>New to Stellar wallets?</span>
241
+ <a href={LEARN_MORE_URL} target="_blank" rel="noreferrer">
242
+ Learn more ↗
243
+ </a>
244
+ </footer>
245
+ </div>
246
+ </div>
247
+ );
248
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+
3
+ module.exports = {};
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "empty-wallet-dep",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "main": "./index.cjs"
6
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+
3
+ exports.HOT = {
4
+ request() {
5
+ return Promise.reject(new Error("HOT Wallet is not supported in this build."));
6
+ }
7
+ };
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@hot-wallet/sdk",
3
+ "version": "1.0.11",
4
+ "private": true,
5
+ "main": "./index.cjs"
6
+ }
@@ -239,6 +239,232 @@ button:hover {
239
239
  background: #f4f2ec;
240
240
  }
241
241
 
242
+ .wallet-modal {
243
+ position: fixed;
244
+ inset: 0;
245
+ z-index: 20;
246
+ display: flex;
247
+ align-items: center;
248
+ justify-content: center;
249
+ padding: 16px;
250
+ background: rgba(32, 35, 42, 0.45);
251
+ backdrop-filter: blur(3px);
252
+ }
253
+
254
+ .wallet-modal__panel {
255
+ display: flex;
256
+ flex-direction: column;
257
+ width: min(400px, 100%);
258
+ max-height: min(600px, calc(100vh - 32px));
259
+ border: 1px solid #d9d5ca;
260
+ border-radius: 12px;
261
+ background: #fffdf7;
262
+ box-shadow: 0 18px 60px rgba(32, 35, 42, 0.22);
263
+ outline: none;
264
+ animation: wallet-modal-pop 0.18s ease;
265
+ }
266
+
267
+ @keyframes wallet-modal-pop {
268
+ from {
269
+ opacity: 0;
270
+ transform: translateY(8px) scale(0.98);
271
+ }
272
+ }
273
+
274
+ .wallet-modal__header {
275
+ display: flex;
276
+ align-items: center;
277
+ justify-content: space-between;
278
+ gap: 16px;
279
+ padding: 20px 20px 8px;
280
+ }
281
+
282
+ .wallet-modal__header h2 {
283
+ margin: 0;
284
+ color: #16181d;
285
+ font-size: 1.2rem;
286
+ }
287
+
288
+ .wallet-modal__close {
289
+ width: 32px;
290
+ min-height: 32px;
291
+ padding: 0;
292
+ border-radius: 8px;
293
+ background: transparent;
294
+ color: #697076;
295
+ font-size: 0.95rem;
296
+ font-weight: 400;
297
+ line-height: 1;
298
+ }
299
+
300
+ .wallet-modal__close:hover {
301
+ background: #f4f2ec;
302
+ color: #20232a;
303
+ }
304
+
305
+ .wallet-modal__body {
306
+ overflow-y: auto;
307
+ padding: 0 12px 16px;
308
+ }
309
+
310
+ .wallet-modal__section {
311
+ margin: 14px 10px 6px;
312
+ }
313
+
314
+ .wallet-modal__list {
315
+ display: grid;
316
+ gap: 4px;
317
+ margin: 0;
318
+ padding: 0;
319
+ list-style: none;
320
+ }
321
+
322
+ .wallet-option {
323
+ display: flex;
324
+ align-items: center;
325
+ gap: 12px;
326
+ width: 100%;
327
+ min-height: 56px;
328
+ padding: 8px 12px;
329
+ border: 1px solid transparent;
330
+ border-radius: 10px;
331
+ background: transparent;
332
+ color: #20232a;
333
+ font-weight: 700;
334
+ text-align: left;
335
+ transition:
336
+ background 0.15s ease,
337
+ border-color 0.15s ease;
338
+ }
339
+
340
+ .wallet-option:hover {
341
+ background: #f4f2ec;
342
+ border-color: #d9d5ca;
343
+ }
344
+
345
+ .wallet-option:disabled {
346
+ background: transparent;
347
+ border-color: transparent;
348
+ opacity: 0.5;
349
+ cursor: default;
350
+ }
351
+
352
+ .wallet-option--connecting,
353
+ .wallet-option--connecting:hover {
354
+ background: #f4f2ec;
355
+ border-color: #d9d5ca;
356
+ }
357
+
358
+ .wallet-option__icon {
359
+ width: 36px;
360
+ height: 36px;
361
+ border: 1px solid #eee9dd;
362
+ border-radius: 8px;
363
+ background: #ffffff;
364
+ object-fit: contain;
365
+ }
366
+
367
+ .wallet-option__name {
368
+ flex: 1;
369
+ min-width: 0;
370
+ overflow: hidden;
371
+ text-overflow: ellipsis;
372
+ white-space: nowrap;
373
+ }
374
+
375
+ .wallet-option__badge {
376
+ border: 1px solid #d9d5ca;
377
+ border-radius: 999px;
378
+ padding: 3px 10px;
379
+ background: #ffffff;
380
+ color: #45515a;
381
+ font-size: 0.7rem;
382
+ font-weight: 800;
383
+ letter-spacing: 0.04em;
384
+ text-transform: uppercase;
385
+ white-space: nowrap;
386
+ }
387
+
388
+ .wallet-option__spinner {
389
+ width: 16px;
390
+ height: 16px;
391
+ border: 3px solid #d9d5ca;
392
+ border-top-color: #1d6154;
393
+ border-radius: 50%;
394
+ animation: loading-spin 0.7s linear infinite;
395
+ }
396
+
397
+ .wallet-option--skeleton {
398
+ pointer-events: none;
399
+ }
400
+
401
+ .wallet-skeleton__icon,
402
+ .wallet-skeleton__line {
403
+ border-radius: 8px;
404
+ background: #ece8dc;
405
+ animation: wallet-skeleton-pulse 1.2s ease-in-out infinite;
406
+ }
407
+
408
+ .wallet-skeleton__icon {
409
+ flex: none;
410
+ width: 36px;
411
+ height: 36px;
412
+ }
413
+
414
+ .wallet-skeleton__line {
415
+ width: 45%;
416
+ height: 14px;
417
+ }
418
+
419
+ @keyframes wallet-skeleton-pulse {
420
+ 50% {
421
+ opacity: 0.55;
422
+ }
423
+ }
424
+
425
+ .wallet-modal__empty {
426
+ margin: 14px 10px;
427
+ color: #45515a;
428
+ font-size: 0.88rem;
429
+ line-height: 1.4;
430
+ }
431
+
432
+ .wallet-modal__error {
433
+ margin: 0 20px 4px;
434
+ max-height: 72px;
435
+ overflow-y: auto;
436
+ border: 1px solid #e7c8c8;
437
+ border-radius: 8px;
438
+ padding: 8px 10px;
439
+ background: #fbf1f1;
440
+ color: #8b1e1e;
441
+ font-size: 0.82rem;
442
+ line-height: 1.4;
443
+ white-space: pre-wrap;
444
+ }
445
+
446
+ .wallet-modal__footer {
447
+ display: flex;
448
+ align-items: center;
449
+ justify-content: space-between;
450
+ gap: 8px;
451
+ padding: 14px 20px;
452
+ border-top: 1px solid #eee9dd;
453
+ color: #697076;
454
+ font-size: 0.82rem;
455
+ }
456
+
457
+ .wallet-modal__footer a {
458
+ color: #1d6154;
459
+ font-weight: 700;
460
+ text-decoration: none;
461
+ white-space: nowrap;
462
+ }
463
+
464
+ .wallet-modal__footer a:hover {
465
+ text-decoration: underline;
466
+ }
467
+
242
468
  @media (max-width: 560px) {
243
469
  .topbar,
244
470
  .counter-panel__header {
@@ -254,4 +480,39 @@ button:hover {
254
480
  width: 100%;
255
481
  justify-content: center;
256
482
  }
483
+
484
+ .wallet-modal {
485
+ align-items: flex-end;
486
+ padding: 0;
487
+ }
488
+
489
+ .wallet-modal__panel {
490
+ width: 100%;
491
+ max-height: 80vh;
492
+ border-bottom: 0;
493
+ border-radius: 16px 16px 0 0;
494
+ animation: wallet-modal-rise 0.2s ease;
495
+ }
496
+ }
497
+
498
+ @keyframes wallet-modal-rise {
499
+ from {
500
+ opacity: 0;
501
+ transform: translateY(24px);
502
+ }
503
+ }
504
+
505
+ @media (prefers-reduced-motion: reduce) {
506
+ .wallet-modal__panel {
507
+ animation: none;
508
+ }
509
+
510
+ .wallet-option__spinner {
511
+ animation-duration: 1.6s;
512
+ }
513
+
514
+ .wallet-skeleton__icon,
515
+ .wallet-skeleton__line {
516
+ animation: none;
517
+ }
257
518
  }
@@ -0,0 +1,73 @@
1
+ // Promise bridge between the wallet adapter's `openModal()` capability and the
2
+ // <WalletModal> component. The adapter resolves the same promise the wallet
3
+ // session awaits, so connect/persist/restore flows stay untouched.
4
+
5
+ export interface WalletModalState {
6
+ open: boolean;
7
+ }
8
+
9
+ /**
10
+ * `error.name` used when the user closes the modal without picking a wallet.
11
+ * Closing on purpose is not a failure, so the UI can suppress this one.
12
+ */
13
+ export const WALLET_SELECTION_CLOSED_ERROR = "WalletSelectionClosedError";
14
+
15
+ interface PendingSelection {
16
+ resolve(address: string): void;
17
+ reject(error: Error): void;
18
+ }
19
+
20
+ let state: WalletModalState = { open: false };
21
+ let pending: PendingSelection | null = null;
22
+ const listeners = new Set<() => void>();
23
+
24
+ function setState(next: WalletModalState): void {
25
+ state = next;
26
+ for (const listener of listeners) {
27
+ listener();
28
+ }
29
+ }
30
+
31
+ export function getWalletModalState(): WalletModalState {
32
+ return state;
33
+ }
34
+
35
+ export function subscribeWalletModal(listener: () => void): () => void {
36
+ listeners.add(listener);
37
+ return () => {
38
+ listeners.delete(listener);
39
+ };
40
+ }
41
+
42
+ /** Opens the modal. Resolves with the connected address, rejects when closed. */
43
+ export function requestWalletSelection(): Promise<string> {
44
+ // A connect while the modal is already open supersedes the previous request.
45
+ cancelWalletSelection();
46
+
47
+ return new Promise<string>((resolve, reject) => {
48
+ pending = { resolve, reject };
49
+ setState({ open: true });
50
+ });
51
+ }
52
+
53
+ export function resolveWalletSelection(address: string): void {
54
+ const current = pending;
55
+ pending = null;
56
+ if (state.open) {
57
+ setState({ open: false });
58
+ }
59
+ current?.resolve(address);
60
+ }
61
+
62
+ export function cancelWalletSelection(): void {
63
+ const current = pending;
64
+ pending = null;
65
+ if (state.open) {
66
+ setState({ open: false });
67
+ }
68
+ if (current) {
69
+ const error = new Error("Wallet selection closed.");
70
+ error.name = WALLET_SELECTION_CLOSED_ERROR;
71
+ current.reject(error);
72
+ }
73
+ }
@@ -4,12 +4,20 @@ import {
4
4
  WalletNetwork,
5
5
  type StellarWalletsKitMetadata
6
6
  } from "@caatinga/client/stellar-wallets-kit";
7
+ import { requestWalletSelection } from "./wallet-modal-controller.js";
7
8
 
8
- export const stellarWalletAdapter = createStellarWalletsKitAdapter({
9
+ const baseWalletAdapter = createStellarWalletsKitAdapter({
9
10
  network: WalletNetwork.TESTNET,
10
11
  walletConnectMetadata: getWalletConnectMetadata()
11
12
  });
12
13
 
14
+ // Route connect() through the custom <WalletModal> instead of SWK's built-in
15
+ // authModal. Everything else (persistence, restore, signing) is untouched.
16
+ export const stellarWalletAdapter = {
17
+ ...baseWalletAdapter,
18
+ openModal: () => requestWalletSelection()
19
+ };
20
+
13
21
  export { WalletNetwork };
14
22
 
15
23
  function getWalletConnectMetadata(): StellarWalletsKitMetadata | undefined {
@@ -2,6 +2,15 @@ import { defineConfig } from "vite";
2
2
  import react from "@vitejs/plugin-react";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
+ // Empty CJS module. Aliasing browser-hostile wallet sub-deps here keeps them out
6
+ // of the bundle on yarn/bun (and as a safety net on npm/pnpm). Install-time
7
+ // overrides in package.json / pnpm-workspace.yaml block the same deps from
8
+ // being installed. CJS interop lets any named import resolve to `undefined`
9
+ // without an esbuild "not exported" error.
10
+ const emptyStub = fileURLToPath(
11
+ new URL("./src/stubs/empty-wallet-dep/index.cjs", import.meta.url)
12
+ );
13
+
5
14
  export default defineConfig({
6
15
  plugins: [react()],
7
16
  resolve: {
@@ -9,7 +18,14 @@ export default defineConfig({
9
18
  // Stellar Wallets Kit drags NEAR's @hot-wallet/sdk (Node-only crypto) into
10
19
  // the browser bundle. The adapter filters HOT Wallet out, so stub the SDK
11
20
  // to keep the NEAR chain out of the build. See src/stubs/hot-wallet.ts.
12
- "@hot-wallet/sdk": fileURLToPath(new URL("./src/stubs/hot-wallet.ts", import.meta.url))
21
+ "@hot-wallet/sdk": fileURLToPath(new URL("./src/stubs/hot-wallet.ts", import.meta.url)),
22
+ // SWK + Reown/WalletConnect pull Trezor Connect (Node-only) and Safe Global
23
+ // SDKs that none of the wallets Caatinga ships actually use.
24
+ "@trezor/connect-web": emptyStub,
25
+ "@trezor/connect-plugin-stellar": emptyStub,
26
+ "@safe-global/safe-apps-sdk": emptyStub,
27
+ "@safe-global/safe-apps-provider": emptyStub,
28
+ "@safe-global/safe-gateway-typescript-sdk": emptyStub
13
29
  }
14
30
  }
15
31
  });