@caatinga/cli 2.0.2 → 2.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.
Files changed (23) hide show
  1. package/README.md +12 -6
  2. package/dist/index.js +226 -17
  3. package/package.json +3 -3
  4. package/templates/marketplace-with-token/caatinga.template.json +1 -1
  5. package/templates/marketplace-with-token/package.json +3 -3
  6. package/templates/react-vite-counter/caatinga.template.json +1 -1
  7. package/templates/react-vite-counter/package.json +7 -7
  8. package/templates/react-vite-counter/pnpm-workspace.yaml +4 -10
  9. package/templates/react-vite-counter/src/App.tsx +17 -3
  10. package/templates/react-vite-counter/src/components/ContractNotDeployed.tsx +27 -0
  11. package/templates/react-vite-counter/src/components/CounterCard.tsx +2 -10
  12. package/templates/react-vite-counter/src/components/WalletButton.tsx +8 -7
  13. package/templates/react-vite-counter/src/components/WalletModal.tsx +248 -0
  14. package/templates/react-vite-counter/src/contracts/generated/counter/src/index.ts +23 -38
  15. package/templates/react-vite-counter/src/stubs/empty-wallet-dep/index.cjs +3 -0
  16. package/templates/react-vite-counter/src/stubs/empty-wallet-dep/package.json +6 -0
  17. package/templates/react-vite-counter/src/stubs/hot-wallet-sdk/index.cjs +7 -0
  18. package/templates/react-vite-counter/src/stubs/hot-wallet-sdk/package.json +6 -0
  19. package/templates/react-vite-counter/src/styles.css +261 -0
  20. package/templates/react-vite-counter/src/wallet-modal-controller.ts +73 -0
  21. package/templates/react-vite-counter/src/wallet.ts +9 -1
  22. package/templates/react-vite-counter/vite.config.ts +17 -1
  23. package/templates/react-vite-counter/src/context/WalletContext.tsx +0 -64
@@ -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
+ }
@@ -1,37 +1,22 @@
1
- type TransactionResult = {
2
- txHash: string;
3
- result?: unknown;
4
- };
5
-
6
- type SignTransaction = (
7
- xdr: string,
8
- opts?: { networkPassphrase?: string; address?: string }
9
- ) => Promise<{ signedTxXdr: string }> | { signedTxXdr: string };
10
-
11
- class ExampleTransaction {
12
- constructor(
13
- private readonly method: string,
14
- private readonly result?: unknown
15
- ) {}
16
-
17
- toXDR(): string {
18
- return `example-${this.method}-xdr`;
19
- }
20
-
21
- async prepare(): Promise<ExampleTransaction> {
22
- return this;
23
- }
24
-
25
- async signAndSend(input?: { signTransaction?: SignTransaction }): Promise<TransactionResult> {
26
- const signed = input?.signTransaction
27
- ? await input.signTransaction(this.toXDR())
28
- : { signedTxXdr: this.toXDR() };
29
-
30
- return {
31
- txHash: `example-transaction-hash:${signed.signedTxXdr}`,
32
- result: this.result
33
- };
34
- }
1
+ // Placeholder bindings. This file exists so the template type-checks before you
2
+ // run `caatinga generate`. It does NOT talk to the chain — every method throws a
3
+ // clear, actionable error. `caatinga generate counter` overwrites this file with
4
+ // real Stellar CLI bindings.
5
+ import { CaatingaError, CaatingaErrorCode } from "@caatinga/core/browser";
6
+
7
+ // Marker the client checks to detect that real bindings haven't been generated
8
+ // yet. Real Stellar CLI bindings never export this.
9
+ export const __caatingaPlaceholder = true;
10
+
11
+ const GENERATE_HINT =
12
+ "Run `npx caatinga generate counter --network testnet`, then restart the dev server.";
13
+
14
+ function placeholderBinding(method: string): never {
15
+ throw new CaatingaError(
16
+ `Placeholder bindings are still in use for "counter.${method}".`,
17
+ CaatingaErrorCode.PLACEHOLDER_BINDING,
18
+ GENERATE_HINT
19
+ );
35
20
  }
36
21
 
37
22
  export class Client {
@@ -44,12 +29,12 @@ export class Client {
44
29
  }
45
30
  ) {}
46
31
 
47
- increment(): ExampleTransaction {
48
- return new ExampleTransaction("increment", 1);
32
+ increment(): never {
33
+ return placeholderBinding("increment");
49
34
  }
50
35
 
51
- get(): ExampleTransaction {
52
- return new ExampleTransaction("get", 1);
36
+ get(): never {
37
+ return placeholderBinding("get");
53
38
  }
54
39
 
55
40
  describe(): string {
@@ -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
+ }