@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
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @caatinga/cli
2
2
 
3
- Developer toolkit for Stellar / Soroban dApps — `init`, `build`, `deploy`, `generate`, and `invoke`.
3
+ Developer toolkit for Stellar / Soroban dApps — `init`, `build`, `deploy`, `generate`, `status`, and `invoke`.
4
4
 
5
5
  ## Install
6
6
 
@@ -35,11 +35,11 @@ npm install
35
35
 
36
36
  npx caatinga build counter
37
37
  npx caatinga deploy counter --network testnet --source alice
38
- npx caatinga generate counter --network testnet
38
+ npx caatinga status --network testnet
39
39
  npx caatinga invoke counter.increment --network testnet --source alice
40
40
  ```
41
41
 
42
- `build` only compiles the WASM file. `deploy` writes contract IDs to `caatinga.artifacts.json`, and the frontend/client flow needs those IDs before it can resolve a contract. `generate` creates TypeScript bindings under the path configured in `caatinga.config.ts` (templates default to `contracts/generated/`).
42
+ `build` only compiles the WASM file. `deploy` writes contract IDs to `caatinga.artifacts.json` and then generates TypeScript bindings automatically under the path configured in `caatinga.config.ts` (templates default to `contracts/generated/`); pass `--no-generate` to skip. `status` shows what's deployed per network and whether bindings are fresh.
43
43
 
44
44
  ## Commands
45
45
 
@@ -49,10 +49,11 @@ npx caatinga invoke counter.increment --network testnet --source alice
49
49
  | `caatinga doctor [--network <network>] [--source <identity>]` | Check local Node, Stellar CLI, Rust, config, artifacts, network, and source identity setup |
50
50
  | `caatinga build [contract]` | Compile contract WASM through Stellar CLI (default contract: `counter`) |
51
51
  | `caatinga deploy [contract]` | Deploy one contract or the full configured graph; record IDs in artifacts |
52
- | `caatinga generate <contract>` | Generate TypeScript bindings from a deployed contract ID |
52
+ | `caatinga generate [contract]` | (Re)generate TypeScript bindings; omit the name to generate for all deployed contracts |
53
+ | `caatinga status [--network <name>] [--json]` | Show deployed contracts and binding freshness per network |
53
54
  | `caatinga invoke <contract.method>` | Invoke a deployed contract method; extra args forward to Stellar CLI |
54
55
 
55
- The supported CLI flow is `init -> build -> deploy -> generate -> invoke`.
56
+ The supported CLI flow is `init -> build -> deploy (bindings auto-generate) -> invoke`.
56
57
 
57
58
  ### `init`
58
59
 
@@ -78,12 +79,17 @@ The supported CLI flow is `init -> build -> deploy -> generate -> invoke`.
78
79
  - `-s, --source <identity>` is required; must be a Stellar CLI identity alias that can sign (for example `alice`)
79
80
  - `--force` redeploys even when artifacts already store a contract ID
80
81
  - `--no-deps` skips dependency deployment for a single named contract (`--no-deps` requires `[contract]`)
82
+ - `--no-generate` skips the automatic bindings generation after deploy
81
83
 
82
84
  Dependencies listed in `dependsOn` deploy first unless `--no-deps` is set. Deploy args may reference `${contracts.<name>.contractId}` placeholders resolved from artifacts.
83
85
 
84
- ### `generate` and `invoke`
86
+ After a successful deploy, bindings generate automatically for the deployed contracts. A generation failure never fails the deploy — the CLI prints a warning plus the recovery command (`npx caatinga generate --network <network>`).
87
+
88
+ ### `generate`, `status`, and `invoke`
85
89
 
86
90
  - `-n, --network <network>` selects the network used to resolve deployed contract IDs
91
+ - `generate` prints binding freshness per contract before regenerating in all-contracts mode
92
+ - `status` prints a per-network table (contract ID, WASM hash, deployed, binding freshness, dependencies); `--json` emits the machine-readable structure
87
93
  - `invoke` expects `<contract.method>` (for example `counter.increment`) and forwards `[args...]` to the underlying Stellar invocation
88
94
 
89
95
  `caatinga dev` is reserved, hidden in pre-v1 builds, and not part of the stability promise. Use your frontend dev server (for example Vite) alongside the commands above.
package/dist/index.js CHANGED
@@ -161,9 +161,16 @@ async function warnIfDefaultNetworkNeedsDeploy(config) {
161
161
  }
162
162
 
163
163
  // src/commands/deploy.command.ts
164
- import { deployContractGraph, CaatingaError as CaatingaError2, CaatingaErrorCode as CaatingaErrorCode2, loadConfig as loadConfig3 } from "@caatinga/core";
164
+ import {
165
+ deployContractGraph,
166
+ generateBindingsGraph,
167
+ toCaatingaError as toCaatingaError2,
168
+ CaatingaError as CaatingaError2,
169
+ CaatingaErrorCode as CaatingaErrorCode2,
170
+ loadConfig as loadConfig3
171
+ } from "@caatinga/core";
165
172
  function registerDeployCommand(program2) {
166
- program2.command("deploy").description("Deploy one or all configured Soroban contracts").argument("[contract]", "Contract name").option("-n, --network <network>", "Configured network name").requiredOption("-s, --source <source>", "Stellar CLI identity alias that can sign (for example alice)").option("--force", "Redeploy contracts even if artifacts already contain contract IDs").option("--no-deps", "Do not deploy missing dependencies for a selected contract").option("--no-stale-check", "Do not warn when WASM may be older than contract sources").option("--verify-deps", "Verify dependency contract IDs exist on-chain before deploy").action((contractName, options) => runCliAction(async () => {
173
+ program2.command("deploy").description("Deploy one or all configured Soroban contracts").argument("[contract]", "Contract name").option("-n, --network <network>", "Configured network name").requiredOption("-s, --source <source>", "Stellar CLI identity alias that can sign (for example alice)").option("--force", "Redeploy contracts even if artifacts already contain contract IDs").option("--no-deps", "Do not deploy missing dependencies for a selected contract").option("--no-stale-check", "Do not warn when WASM may be older than contract sources").option("--verify-deps", "Verify dependency contract IDs exist on-chain before deploy").option("--no-generate", "Skip TypeScript bindings generation after deploy").action((contractName, options) => runCliAction(async () => {
167
174
  if (options.deps === false && !contractName) {
168
175
  throw new CaatingaError2(
169
176
  "`--no-deps` requires a contract name.",
@@ -197,6 +204,45 @@ function registerDeployCommand(program2) {
197
204
  logger.info(` Contract ID: ${contract.contractId}`);
198
205
  }
199
206
  logger.info("Artifacts updated: caatinga.artifacts.json");
207
+ if (result.deployedContracts.length === 0) {
208
+ return;
209
+ }
210
+ if (options.generate === false) {
211
+ logger.info("");
212
+ logger.info("Bindings generation skipped (--no-generate).");
213
+ logger.info("Next:");
214
+ for (const contract of result.deployedContracts) {
215
+ logger.info(` npx caatinga generate ${contract.name} --network ${result.network.name}`);
216
+ }
217
+ logger.info(" npm run dev");
218
+ return;
219
+ }
220
+ try {
221
+ const generated = await generateBindingsGraph({
222
+ config,
223
+ contractNames: result.deployedContracts.map((contract) => contract.name),
224
+ networkName: result.network.name
225
+ });
226
+ logger.info("");
227
+ logger.success("Bindings generated");
228
+ for (const binding of generated.results) {
229
+ logger.info(` ${binding.contractName} \u2192 ${binding.importPath}`);
230
+ }
231
+ logger.info("");
232
+ logger.info("Next:");
233
+ logger.info(" npm run dev");
234
+ } catch (error) {
235
+ const caatingaError = toCaatingaError2(error);
236
+ logger.info("");
237
+ logger.warn("Deploy succeeded, but bindings generation failed.");
238
+ logger.warn(` ${caatingaError.message} (${caatingaError.code})`);
239
+ if (caatingaError.hint) {
240
+ logger.warn(` Hint: ${caatingaError.hint}`);
241
+ }
242
+ logger.info("");
243
+ logger.info("Recover with:");
244
+ logger.info(` npx caatinga generate --network ${result.network.name}`);
245
+ }
200
246
  }));
201
247
  }
202
248
 
@@ -388,6 +434,33 @@ function printFixes(diagnostics) {
388
434
  }
389
435
  }
390
436
 
437
+ // src/commands/doctor-bindings.ts
438
+ import {
439
+ evaluateBindingsFreshness,
440
+ loadConfig as loadConfig5,
441
+ readArtifacts as readArtifacts3,
442
+ resolveNetwork as resolveNetwork3
443
+ } from "@caatinga/core";
444
+ async function evaluateBindingCoverage(options) {
445
+ const cwd = options.cwd;
446
+ const config = await loadConfig5({ cwd });
447
+ const network = resolveNetwork3(config, options.networkName);
448
+ const artifacts = await readArtifacts3(cwd);
449
+ const freshness = await evaluateBindingsFreshness({
450
+ config,
451
+ artifacts,
452
+ networkName: network.name,
453
+ cwd
454
+ });
455
+ const lines = freshness.map((entry) => ({
456
+ name: entry.contractName,
457
+ status: entry.status,
458
+ reason: entry.reason,
459
+ ...entry.status === "fresh" ? {} : { fix: `Run: caatinga generate ${entry.contractName} --network ${network.name}` }
460
+ }));
461
+ return { lines, allFresh: lines.every((line) => line.status === "fresh") };
462
+ }
463
+
391
464
  // src/commands/doctor.command.ts
392
465
  function printDeployCoverageLine(line) {
393
466
  if (line.ok) {
@@ -414,6 +487,25 @@ async function reportDeployCoverage(networkName) {
414
487
  }
415
488
  return true;
416
489
  }
490
+ function printBindingCoverageLine(line) {
491
+ if (line.status === "fresh") {
492
+ logger.info(`\u2713 ${line.name} \u2014 bindings fresh`);
493
+ return;
494
+ }
495
+ logger.info(`\u2717 ${line.name} \u2014 bindings ${line.status}${line.reason ? ` (${line.reason})` : ""}`);
496
+ if (line.fix) logger.info(` ${line.fix}`);
497
+ }
498
+ async function reportBindingCoverage(networkName) {
499
+ const coverage = await evaluateBindingCoverage({ networkName });
500
+ if (coverage.lines.length === 0) {
501
+ return;
502
+ }
503
+ logger.info("");
504
+ logger.info(`Bindings (${networkName}):`);
505
+ for (const line of coverage.lines) {
506
+ printBindingCoverageLine(line);
507
+ }
508
+ }
417
509
  function registerDoctorCommand(program2) {
418
510
  program2.command("doctor").description("Check local Caatinga, Stellar CLI, Rust, config, and source identity setup").option("-n, --network <network>", "Configured network name to validate").option("-s, --source <source>", "Stellar CLI identity alias to validate").action((options) => runCliAction(async () => {
419
511
  logger.info("Caatinga Doctor");
@@ -431,6 +523,7 @@ function registerDoctorCommand(program2) {
431
523
  ready = false;
432
524
  throw error;
433
525
  }
526
+ await reportBindingCoverage(options.network);
434
527
  }
435
528
  logger.info("");
436
529
  logger.info(`Status: ${ready ? "ready" : "blocked"}`);
@@ -441,23 +534,56 @@ function registerDoctorCommand(program2) {
441
534
  }
442
535
 
443
536
  // src/commands/generate.command.ts
444
- import { generateBindings, loadConfig as loadConfig5 } from "@caatinga/core";
537
+ import {
538
+ evaluateBindingsFreshness as evaluateBindingsFreshness2,
539
+ generateBindingsGraph as generateBindingsGraph2,
540
+ loadConfig as loadConfig6,
541
+ readArtifacts as readArtifacts4,
542
+ resolveNetwork as resolveNetwork4
543
+ } from "@caatinga/core";
544
+ async function printFreshnessPreState(config, networkName) {
545
+ try {
546
+ const network = resolveNetwork4(config, networkName);
547
+ const artifacts = await readArtifacts4();
548
+ const freshness = await evaluateBindingsFreshness2({
549
+ config,
550
+ artifacts,
551
+ networkName: network.name
552
+ });
553
+ if (freshness.length === 0) {
554
+ return;
555
+ }
556
+ logger.info("Current bindings:");
557
+ for (const entry of freshness) {
558
+ const reason = entry.reason ? ` \u2014 ${entry.reason}` : "";
559
+ logger.info(` [${entry.status}] ${entry.contractName}${reason}`);
560
+ }
561
+ logger.info("");
562
+ } catch {
563
+ }
564
+ }
445
565
  function registerGenerateCommand(program2) {
446
- program2.command("generate").description("Generate TypeScript bindings for a deployed contract").argument("<contract>", "Contract name").option("-n, --network <network>", "Configured network name").action((contractName, options) => runCliAction(async () => {
447
- const config = await loadConfig5();
448
- const result = await generateBindings({
566
+ program2.command("generate").description("Generate TypeScript bindings for deployed contracts").argument("[contract]", "Contract name (defaults to all deployed contracts)").option("-n, --network <network>", "Configured network name").action((contractName, options) => runCliAction(async () => {
567
+ const config = await loadConfig6();
568
+ if (!contractName) {
569
+ await printFreshnessPreState(config, options.network);
570
+ }
571
+ const { network, results } = await generateBindingsGraph2({
449
572
  config,
450
573
  contractName,
451
574
  networkName: options.network
452
575
  });
453
576
  logger.success("Client generated");
454
577
  logger.info("");
455
- logger.info(`Contract: ${result.contractName}`);
456
- logger.info(`Network: ${result.network.name}`);
457
- logger.info(`Output: ${result.outputDir}`);
458
- logger.info(`Import path: ${result.importPath}`);
459
- if (result.legacyStubRemoved) {
460
- logger.info(`Removed legacy stub: ${config.frontend.bindingsOutput}/${result.contractName}.ts`);
578
+ logger.info(`Network: ${network.name}`);
579
+ for (const result of results) {
580
+ logger.info("");
581
+ logger.info(`Contract: ${result.contractName}`);
582
+ logger.info(`Output: ${result.outputDir}`);
583
+ logger.info(`Import path: ${result.importPath}`);
584
+ if (result.legacyStubRemoved) {
585
+ logger.info(`Removed legacy stub: ${config.frontend.bindingsOutput}/${result.contractName}.ts`);
586
+ }
461
587
  }
462
588
  logger.info("");
463
589
  logger.info("Next: import bindings from the import path above, then run npm run dev");
@@ -540,21 +666,34 @@ function registerInitCommand(program2) {
540
666
  logger.info(`Template: ${result.template.name}@${result.template.version}`);
541
667
  logger.info(`Path: ${targetDir}`);
542
668
  logger.info("");
669
+ const defaultContract = result.template.contracts.default;
543
670
  logger.info("Next steps:");
544
671
  logger.info(` cd ${projectDirectory}`);
545
672
  logger.info(" npm install");
546
- const defaultContract = result.template.contracts.default;
673
+ if (defaultContract) {
674
+ logger.info(` npx caatinga build ${defaultContract}`);
675
+ logger.info(
676
+ ` npx caatinga deploy ${defaultContract} --network testnet --source <identity>`
677
+ );
678
+ } else {
679
+ logger.info(" npx caatinga build");
680
+ logger.info(" npx caatinga deploy --network testnet --source <identity>");
681
+ }
682
+ logger.info(" npm run dev");
683
+ logger.info("");
547
684
  logger.info(
548
- defaultContract ? ` npx caatinga build ${defaultContract}` : " npx caatinga build"
685
+ "Note: deploy generates TypeScript bindings automatically (--no-generate to skip) \u2014"
549
686
  );
687
+ logger.info("the dApp reads the contract ID from caatinga.artifacts.json.");
688
+ logger.info("If generation fails, recover with: npx caatinga generate --network testnet");
550
689
  }));
551
690
  }
552
691
 
553
692
  // src/commands/invoke.command.ts
554
- import { invokeContract, loadConfig as loadConfig6 } from "@caatinga/core";
693
+ import { invokeContract, loadConfig as loadConfig7 } from "@caatinga/core";
555
694
  function registerInvokeCommand(program2) {
556
695
  program2.command("invoke").description("Invoke a deployed contract function").argument("<target>", "Invoke target in contract.method format").argument("[args...]", "Arguments forwarded to Stellar CLI after the method name").option("-n, --network <network>", "Configured network name").requiredOption("-s, --source <source>", "Stellar CLI identity alias that can sign (for example alice)").allowUnknownOption(true).allowExcessArguments(true).action((target, args, options) => runCliAction(async () => {
557
- const config = await loadConfig6();
696
+ const config = await loadConfig7();
558
697
  const result = await invokeContract({
559
698
  config,
560
699
  target,
@@ -574,8 +713,77 @@ function registerInvokeCommand(program2) {
574
713
  }));
575
714
  }
576
715
 
716
+ // src/commands/status.command.ts
717
+ import { collectProjectStatus, loadConfig as loadConfig8 } from "@caatinga/core";
718
+
719
+ // src/utils/table.ts
720
+ function renderTable(headers, rows) {
721
+ const widths = headers.map(
722
+ (header, column) => Math.max(header.length, ...rows.map((row) => (row[column] ?? "").length))
723
+ );
724
+ const renderRow = (cells) => cells.map((cell, column) => (cell ?? "").padEnd(widths[column])).join(" ").trimEnd();
725
+ return [
726
+ renderRow(headers),
727
+ widths.map((width) => "\u2500".repeat(width)).join(" "),
728
+ ...rows.map((row) => renderRow(row))
729
+ ];
730
+ }
731
+
732
+ // src/commands/status.command.ts
733
+ function shortId(value) {
734
+ if (!value) return "\u2014";
735
+ return value.length > 12 ? `${value.slice(0, 5)}\u2026${value.slice(-4)}` : value;
736
+ }
737
+ function shortHash(value) {
738
+ if (!value) return "\u2014";
739
+ return value.slice(0, 8);
740
+ }
741
+ function toRow(entry) {
742
+ return [
743
+ entry.name,
744
+ shortId(entry.contractId),
745
+ shortHash(entry.wasmHash),
746
+ entry.deployed ? "\u2713" : "\u2717",
747
+ entry.bindings.status,
748
+ entry.dependencies.length > 0 ? entry.dependencies.join(", ") : "\u2014"
749
+ ];
750
+ }
751
+ function registerStatusCommand(program2) {
752
+ program2.command("status").description("Show deployed contracts and binding freshness per network").option("-n, --network <network>", "Configured network name").option("--json", "Print machine-readable JSON instead of the table").action((options) => runCliAction(async () => {
753
+ const config = await loadConfig8();
754
+ const status = await collectProjectStatus({
755
+ config,
756
+ networkName: options.network
757
+ });
758
+ if (options.json) {
759
+ console.log(JSON.stringify(status, null, 2));
760
+ return;
761
+ }
762
+ logger.success(`Project: ${status.project}`);
763
+ for (const network of status.networks) {
764
+ logger.info("");
765
+ logger.info(`Network: ${network.network}`);
766
+ const lines = renderTable(
767
+ ["CONTRACT", "CONTRACT ID", "WASM HASH", "DEPLOYED", "BINDINGS", "DEPS"],
768
+ network.contracts.map(toRow)
769
+ );
770
+ for (const line of lines) {
771
+ logger.info(line);
772
+ }
773
+ const needsAttention = network.contracts.filter(
774
+ (entry) => entry.deployed && entry.bindings.status !== "fresh"
775
+ );
776
+ for (const entry of needsAttention) {
777
+ logger.warn(
778
+ `Bindings ${entry.bindings.status} for ${entry.name}${entry.bindings.reason ? ` (${entry.bindings.reason})` : ""} \u2014 run: npx caatinga generate ${entry.name} --network ${network.network}`
779
+ );
780
+ }
781
+ }
782
+ }));
783
+ }
784
+
577
785
  // src/version.ts
578
- var CAATINGA_CLI_VERSION = "2.0.2";
786
+ var CAATINGA_CLI_VERSION = "2.2.0";
579
787
 
580
788
  // src/program.ts
581
789
  function createProgram() {
@@ -588,6 +796,7 @@ function createProgram() {
588
796
  registerDeployCommand(program2);
589
797
  registerGenerateCommand(program2);
590
798
  registerInvokeCommand(program2);
799
+ registerStatusCommand(program2);
591
800
  return program2;
592
801
  }
593
802
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caatinga/cli",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
4
4
  "description": "Caatinga CLI for building dApps on Stellar/Soroban",
5
5
  "keywords": [
6
6
  "stellar",
@@ -43,7 +43,7 @@
43
43
  "LICENSE"
44
44
  ],
45
45
  "dependencies": {
46
- "@caatinga/core": "^2.0.2",
46
+ "@caatinga/core": "^2.2.0",
47
47
  "chalk": "^5.4.1",
48
48
  "commander": "^12.1.0"
49
49
  },
@@ -54,7 +54,7 @@
54
54
  "vitest": "^2.1.8"
55
55
  },
56
56
  "scripts": {
57
- "build": "tsup src/index.ts --format esm --dts --clean && rm -rf ./templates && cp -r ../templates ./templates",
57
+ "build": "tsup src/index.ts --format esm --dts --clean && rm -rf ./templates && mkdir ./templates && (cd ../templates && tar --exclude=node_modules --exclude=dist -cf - .) | (cd ./templates && tar -xf -)",
58
58
  "predev": "pnpm --filter @caatinga/core build",
59
59
  "dev": "tsx src/index.ts",
60
60
  "test": "vitest run --pool=threads",
@@ -3,7 +3,7 @@
3
3
  "version": "0.1.0",
4
4
  "description": "Experimental multi-contract Soroban template with token dependency injection.",
5
5
  "caatinga": {
6
- "compatibleCore": "^2.0.2",
6
+ "compatibleCore": "^2.2.0",
7
7
  "templateVersion": 1
8
8
  },
9
9
  "frontend": {
@@ -12,15 +12,15 @@
12
12
  "caatinga:generate": "caatinga generate token && caatinga generate marketplace"
13
13
  },
14
14
  "dependencies": {
15
- "@caatinga/client": "^2.0.2",
16
- "@caatinga/core": "^2.0.2",
15
+ "@caatinga/client": "^2.2.0",
16
+ "@caatinga/core": "^2.2.0",
17
17
  "@vitejs/plugin-react": "^4.3.4",
18
18
  "react": "^18.3.1",
19
19
  "react-dom": "^18.3.1",
20
20
  "vite": "^6.0.6"
21
21
  },
22
22
  "devDependencies": {
23
- "@caatinga/cli": "^2.0.2",
23
+ "@caatinga/cli": "^2.2.0",
24
24
  "@types/react": "^18.3.18",
25
25
  "@types/react-dom": "^18.3.5",
26
26
  "typescript": "^5.7.2"
@@ -3,7 +3,7 @@
3
3
  "version": "0.1.0",
4
4
  "description": "Minimal Vite + React + Soroban counter dApp.",
5
5
  "caatinga": {
6
- "compatibleCore": "^2.0.2",
6
+ "compatibleCore": "^2.2.0",
7
7
  "templateVersion": 1
8
8
  },
9
9
  "frontend": {
@@ -7,14 +7,14 @@
7
7
  "dev": "vite",
8
8
  "build": "tsc && vite build",
9
9
  "preview": "vite preview",
10
- "caatinga:build": "caatinga build counter",
11
- "caatinga:deploy": "caatinga deploy counter",
12
- "caatinga:generate": "caatinga generate counter"
10
+ "caatinga:build": "npx caatinga build counter",
11
+ "caatinga:deploy": "npx caatinga deploy counter --network testnet --source ${CAATINGA_SOURCE:-alice}",
12
+ "caatinga:generate": "npx caatinga generate counter --network testnet"
13
13
  },
14
14
  "dependencies": {
15
- "@caatinga/client": "^2.0.2",
16
- "@caatinga/core": "^2.0.2",
17
- "@creit.tech/stellar-wallets-kit": "^1.9.5",
15
+ "@caatinga/client": "^2.2.0",
16
+ "@caatinga/core": "^2.2.0",
17
+ "@creit.tech/stellar-wallets-kit": "^2.3.0",
18
18
  "@stellar/stellar-sdk": "^14.5.0",
19
19
  "buffer": "^6.0.3",
20
20
  "@vitejs/plugin-react": "^4.3.4",
@@ -23,7 +23,7 @@
23
23
  "vite": "^6.0.6"
24
24
  },
25
25
  "devDependencies": {
26
- "@caatinga/cli": "^2.0.2",
26
+ "@caatinga/cli": "^2.2.0",
27
27
  "@types/react": "^18.3.18",
28
28
  "@types/react-dom": "^18.3.5",
29
29
  "typescript": "^5.7.2"
@@ -1,12 +1,6 @@
1
- # pnpm 10.26+ / 11.x block lifecycle scripts by default; vite depends on esbuild.
1
+ # pnpm-only file (npm / yarn / bun ignore it). pnpm 10.26+/11.x block lifecycle
2
+ # scripts by default; vite depends on esbuild's, so allow it. All dependency
3
+ # stubbing now happens at the build layer (see vite.config.ts), so no pnpm-only
4
+ # `overrides` are needed and this template installs identically on every PM.
2
5
  allowBuilds:
3
6
  esbuild: true
4
-
5
- # stellar-wallets-kit@0.0.7 is unpublished from npm and is therefore pinned
6
- # to a github: URL in package.json. Its own dependencies also reference
7
- # @creit.tech/xbull-wallet-connect via a github: URL, and pnpm 10.26+ (and
8
- # 11.x) defaults blockExoticSubdeps to true, which would refuse to install
9
- # that exotic subdep. Allow it explicitly. This is a targeted opt-out for
10
- # this one transitive dep; direct dependencies in package.json still must
11
- # come from a trusted source.
12
- blockExoticSubdeps: false
@@ -1,6 +1,15 @@
1
+ import { WalletProvider, useWallet } from "@caatinga/client/react";
2
+ import type { CaatingaArtifacts } from "@caatinga/core/browser";
3
+ import artifactsJson from "../caatinga.artifacts.json";
4
+ import { ContractNotDeployed } from "./components/ContractNotDeployed";
1
5
  import { CounterCard } from "./components/CounterCard";
2
6
  import { WalletButton } from "./components/WalletButton";
3
- import { WalletProvider, useWallet } from "./context/WalletContext";
7
+ import { WalletModal } from "./components/WalletModal";
8
+ import { stellarWalletAdapter } from "./wallet.js";
9
+
10
+ const artifacts = artifactsJson as CaatingaArtifacts;
11
+ const counterContractId = artifacts.networks?.testnet?.contracts?.counter?.contractId;
12
+ const isDeployed = Boolean(counterContractId);
4
13
 
5
14
  function AppBody() {
6
15
  const { publicKey } = useWallet();
@@ -15,7 +24,9 @@ function AppBody() {
15
24
  <WalletButton />
16
25
  </header>
17
26
 
18
- {publicKey ? (
27
+ {!isDeployed ? (
28
+ <ContractNotDeployed />
29
+ ) : publicKey ? (
19
30
  <CounterCard />
20
31
  ) : (
21
32
  <section className="counter-panel" aria-labelledby="connect-title">
@@ -35,8 +46,11 @@ function AppBody() {
35
46
 
36
47
  export default function App() {
37
48
  return (
38
- <WalletProvider>
49
+ // persist keeps the session across reloads; the provider silently
50
+ // reconnects on mount (autoConnect defaults to true when persisting).
51
+ <WalletProvider adapter={stellarWalletAdapter} options={{ persist: true }}>
39
52
  <AppBody />
53
+ <WalletModal />
40
54
  </WalletProvider>
41
55
  );
42
56
  }
@@ -0,0 +1,27 @@
1
+ export function ContractNotDeployed() {
2
+ return (
3
+ <section className="counter-panel" aria-labelledby="not-deployed-title">
4
+ <div className="counter-panel__header">
5
+ <div>
6
+ <p className="eyebrow">Get started</p>
7
+ <h2 id="not-deployed-title">Contract not deployed</h2>
8
+ </div>
9
+ <span className="network-pill">testnet</span>
10
+ </div>
11
+ <p>
12
+ The counter contract has no on-chain ID yet, so the frontend can&apos;t read or update it.
13
+ Build and deploy first — the dApp reads the contract ID from{" "}
14
+ <code>caatinga.artifacts.json</code>. Deploy also generates TypeScript bindings
15
+ automatically.
16
+ </p>
17
+ <pre className="counter-error" role="note">
18
+ {`npx caatinga build counter
19
+ npx caatinga deploy counter --network testnet --source <identity>
20
+ npm run dev
21
+
22
+ # If bindings generation failed after deploy:
23
+ npx caatinga generate counter --network testnet`}
24
+ </pre>
25
+ </section>
26
+ );
27
+ }
@@ -1,17 +1,9 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import { caatingaClient } from "../caatinga.js";
3
- import { CaatingaError } from "@caatinga/core/browser";
4
- import { useWallet } from "../context/WalletContext.js";
3
+ import { formatCaatingaError } from "@caatinga/core/browser";
4
+ import { useWallet } from "@caatinga/client/react";
5
5
  import { LoadingModal } from "./LoadingModal.js";
6
6
 
7
- function formatCaatingaError(error: unknown): string {
8
- if (error instanceof CaatingaError) {
9
- return `[${error.code}] ${error.message}\n\n${error.hint}`;
10
- }
11
-
12
- return error instanceof Error ? error.message : String(error);
13
- }
14
-
15
7
  export function CounterCard() {
16
8
  const { publicKey } = useWallet();
17
9
  const [count, setCount] = useState<number | null>(null);
@@ -1,4 +1,5 @@
1
- import { useWallet } from "../context/WalletContext.js";
1
+ import { useWallet } from "@caatinga/client/react";
2
+ import { WALLET_SELECTION_CLOSED_ERROR } from "../wallet-modal-controller.js";
2
3
 
3
4
  function shortenAddress(address: string): string {
4
5
  if (address.length <= 12) {
@@ -9,23 +10,23 @@ function shortenAddress(address: string): string {
9
10
  }
10
11
 
11
12
  export function WalletButton() {
12
- const { publicKey, loading, error, connect, disconnect } = useWallet();
13
+ const { publicKey, connecting, error, connect, disconnect } = useWallet();
13
14
 
14
15
  return (
15
16
  <div className="wallet-shell">
16
17
  <button
17
18
  className="wallet-button"
18
19
  type="button"
19
- onClick={publicKey ? () => void disconnect() : () => void connect()}
20
- disabled={loading}
20
+ onClick={publicKey ? () => void disconnect() : () => void connect().catch(() => {})}
21
+ disabled={connecting}
21
22
  aria-live="polite"
22
23
  >
23
24
  <span className={publicKey ? "status-dot status-dot--on" : "status-dot"} />
24
- {loading ? "Connecting..." : publicKey ? shortenAddress(publicKey) : "Connect"}
25
+ {connecting ? "Connecting..." : publicKey ? shortenAddress(publicKey) : "Connect"}
25
26
  </button>
26
- {error ? (
27
+ {error && error.name !== WALLET_SELECTION_CLOSED_ERROR ? (
27
28
  <p className="wallet-error" role="alert">
28
- {error}
29
+ {error.message}
29
30
  </p>
30
31
  ) : null}
31
32
  </div>