@caatinga/core 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,72 +1,11 @@
1
- // src/errors/CaatingaErrorCode.ts
2
- var CaatingaErrorCode = {
3
- CONFIG_NOT_FOUND: "CAATINGA_CONFIG_NOT_FOUND",
4
- INVALID_CONFIG: "CAATINGA_INVALID_CONFIG",
5
- COMMAND_FAILED: "CAATINGA_COMMAND_FAILED",
6
- UNEXPECTED_ERROR: "CAATINGA_UNEXPECTED_ERROR",
7
- STELLAR_CLI_NOT_FOUND: "CAATINGA_STELLAR_CLI_NOT_FOUND",
8
- STELLAR_CLI_VERSION_PARSE_FAILED: "CAATINGA_STELLAR_CLI_VERSION_PARSE_FAILED",
9
- UNSUPPORTED_CLI_VERSION: "CAATINGA_UNSUPPORTED_CLI_VERSION",
10
- UNTESTED_CLI_VERSION: "CAATINGA_UNTESTED_CLI_VERSION",
11
- RUST_NOT_FOUND: "CAATINGA_RUST_NOT_FOUND",
12
- RUST_TARGET_NOT_FOUND: "CAATINGA_RUST_TARGET_NOT_FOUND",
13
- DEPLOY_FAILED: "CAATINGA_DEPLOY_FAILED",
14
- BUILD_FAILED: "CAATINGA_BUILD_FAILED",
15
- BINDINGS_FAILED: "CAATINGA_BINDINGS_FAILED",
16
- INVOKE_FAILED: "CAATINGA_INVOKE_FAILED",
17
- CONTRACT_NOT_FOUND: "CAATINGA_CONTRACT_NOT_FOUND",
18
- NETWORK_NOT_FOUND: "CAATINGA_NETWORK_NOT_FOUND",
19
- ARTIFACT_NOT_FOUND: "CAATINGA_ARTIFACT_NOT_FOUND",
20
- ARTIFACT_INVALID: "CAATINGA_ARTIFACT_INVALID",
21
- CONTRACT_ID_NOT_FOUND: "CAATINGA_CONTRACT_ID_NOT_FOUND",
22
- CONTRACT_ARTIFACT_NOT_FOUND: "CAATINGA_CONTRACT_ARTIFACT_NOT_FOUND",
23
- CONTRACT_DEPENDENCY_NOT_FOUND: "CAATINGA_CONTRACT_DEPENDENCY_NOT_FOUND",
24
- CONTRACT_DEPENDENCY_CYCLE: "CAATINGA_CONTRACT_DEPENDENCY_CYCLE",
25
- CONTRACT_DEPENDENCY_ARTIFACT_NOT_FOUND: "CAATINGA_CONTRACT_DEPENDENCY_ARTIFACT_NOT_FOUND",
26
- DEPLOY_ARG_PLACEHOLDER_INVALID: "CAATINGA_DEPLOY_ARG_PLACEHOLDER_INVALID",
27
- DEPLOY_ARG_PLACEHOLDER_UNRESOLVED: "CAATINGA_DEPLOY_ARG_PLACEHOLDER_UNRESOLVED",
28
- BINDING_CLIENT_NOT_FOUND: "CAATINGA_BINDING_CLIENT_NOT_FOUND",
29
- BINDING_METHOD_NOT_FOUND: "CAATINGA_BINDING_METHOD_NOT_FOUND",
30
- XDR_BUILD_FAILED: "CAATINGA_XDR_BUILD_FAILED",
31
- XDR_PREPARE_FAILED: "CAATINGA_XDR_PREPARE_FAILED",
32
- XDR_SIGN_FAILED: "CAATINGA_XDR_SIGN_FAILED",
33
- XDR_SUBMIT_FAILED: "CAATINGA_XDR_SUBMIT_FAILED",
34
- XDR_RESULT_FAILED: "CAATINGA_XDR_RESULT_FAILED",
35
- WALLET_NOT_CONNECTED: "CAATINGA_WALLET_NOT_CONNECTED",
36
- SOURCE_ACCOUNT_REQUIRED: "CAATINGA_SOURCE_ACCOUNT_REQUIRED",
37
- UNSAFE_SOURCE_ACCOUNT: "CAATINGA_UNSAFE_SOURCE_ACCOUNT",
38
- INVOKE_TARGET_INVALID: "CAATINGA_INVOKE_TARGET_INVALID",
39
- TEMPLATE_NOT_FOUND: "CAATINGA_TEMPLATE_NOT_FOUND",
40
- INVALID_TEMPLATE_MANIFEST: "CAATINGA_INVALID_TEMPLATE_MANIFEST",
41
- TEMPLATE_MANIFEST_NOT_FOUND: "CAATINGA_TEMPLATE_MANIFEST_NOT_FOUND",
42
- TEMPLATE_INCOMPATIBLE: "CAATINGA_TEMPLATE_INCOMPATIBLE"
43
- };
44
-
45
- // src/errors/CaatingaError.ts
46
- var CaatingaError = class extends Error {
47
- constructor(message, code, hint, cause) {
48
- super(message);
49
- this.code = code;
50
- this.hint = hint;
51
- this.cause = cause;
52
- this.name = "CaatingaError";
53
- }
54
- code;
55
- hint;
56
- cause;
57
- };
58
- function toCaatingaError(error) {
59
- if (error instanceof CaatingaError) {
60
- return error;
61
- }
62
- if (error instanceof Error) {
63
- return new CaatingaError(error.message, CaatingaErrorCode.UNEXPECTED_ERROR, void 0, error);
64
- }
65
- return new CaatingaError("An unexpected error occurred.", CaatingaErrorCode.UNEXPECTED_ERROR);
66
- }
1
+ import {
2
+ CaatingaError,
3
+ CaatingaErrorCode,
4
+ toCaatingaError
5
+ } from "./chunk-GMABXVEY.js";
67
6
 
68
7
  // src/version.ts
69
- var CAATINGA_CORE_VERSION = "0.2.0";
8
+ var CAATINGA_CORE_VERSION = "0.2.2";
70
9
 
71
10
  // src/config/config.schema.ts
72
11
  import { z } from "zod";
@@ -253,7 +192,7 @@ import { execa } from "execa";
253
192
 
254
193
  // src/stellar-cli/version.ts
255
194
  import semver from "semver";
256
- var STELLAR_CLI_MIN_VERSION = "22.0.0";
195
+ var STELLAR_CLI_MIN_VERSION = "23.0.0";
257
196
  var STELLAR_CLI_TESTED_MAX_VERSION = "25.2.0";
258
197
  var STELLAR_CLI_SEMVER_REGEX = /\b(\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?)\b/;
259
198
  function parseStellarCliVersion(output) {
@@ -386,6 +325,146 @@ function parseContractId(output) {
386
325
  return match[0];
387
326
  }
388
327
 
328
+ // src/stellar-cli/build-stellar-network-args.ts
329
+ function matchesWellKnownNetwork(name, config) {
330
+ const known = WELL_KNOWN_NETWORKS[name];
331
+ if (!known) {
332
+ return false;
333
+ }
334
+ return known.rpcUrl === config.rpcUrl && known.networkPassphrase === config.networkPassphrase;
335
+ }
336
+ function buildRpcNetworkArgs(config) {
337
+ return [
338
+ "--rpc-url",
339
+ config.rpcUrl,
340
+ "--network-passphrase",
341
+ config.networkPassphrase
342
+ ];
343
+ }
344
+ function buildStellarNetworkArgsFromConfig(config) {
345
+ for (const [name, known] of Object.entries(WELL_KNOWN_NETWORKS)) {
346
+ if (known.rpcUrl === config.rpcUrl && known.networkPassphrase === config.networkPassphrase) {
347
+ return ["--network", name];
348
+ }
349
+ }
350
+ return buildRpcNetworkArgs(config);
351
+ }
352
+ function buildStellarNetworkArgs(network) {
353
+ if (matchesWellKnownNetwork(network.name, network.config)) {
354
+ return ["--network", network.name];
355
+ }
356
+ return buildStellarNetworkArgsFromConfig(network.config);
357
+ }
358
+
359
+ // src/stellar-cli/recover-deploy-contract-id.ts
360
+ var TX_HASH_REGEX = /Transaction hash is ([a-f0-9]{64})/i;
361
+ var DEPLOY_SIGNING_FAILURE_REGEX = /xdr processing error: xdr value invalid/i;
362
+ var HORIZON_URL_BY_PASSPHRASE = {
363
+ "Test SDF Network ; September 2015": "https://horizon-testnet.stellar.org",
364
+ "Public Global Stellar Network ; September 2015": "https://horizon.stellar.org"
365
+ };
366
+ function isLikelyPublicKeySource(source) {
367
+ return /^G[A-Z2-7]{55}$/.test(source);
368
+ }
369
+ function decimalSaltToHex(salt) {
370
+ return BigInt(salt).toString(16).padStart(64, "0");
371
+ }
372
+ function resolveHorizonUrl(network) {
373
+ const horizonUrl = HORIZON_URL_BY_PASSPHRASE[network.networkPassphrase];
374
+ if (!horizonUrl) {
375
+ throw new CaatingaError(
376
+ `No Horizon URL mapping for network passphrase "${network.networkPassphrase}".`,
377
+ CaatingaErrorCode.NETWORK_NOT_FOUND,
378
+ "Use testnet or mainnet, or extend Caatinga network metadata."
379
+ );
380
+ }
381
+ return horizonUrl;
382
+ }
383
+ async function fetchCreateContractSalt(horizonUrl, transactionHash, fetchImpl = fetch) {
384
+ const response = await fetchImpl(`${horizonUrl}/transactions/${transactionHash}/operations`);
385
+ if (!response.ok) {
386
+ return null;
387
+ }
388
+ const body = await response.json();
389
+ const operation = body._embedded?.records?.find(
390
+ (record) => record.transaction_successful === true && record.type === "invoke_host_function" && record.function === "HostFunctionTypeHostFunctionTypeCreateContract" && typeof record.salt === "string"
391
+ );
392
+ return operation?.salt ?? null;
393
+ }
394
+ async function resolveContractIdFromDeploySalt(options) {
395
+ const saltHex = decimalSaltToHex(options.salt);
396
+ const result = await runCommand("stellar", [
397
+ "contract",
398
+ "id",
399
+ "wasm",
400
+ "--salt",
401
+ saltHex,
402
+ "--source-account",
403
+ options.source,
404
+ ...buildStellarNetworkArgsFromConfig(options.network)
405
+ ], {
406
+ cwd: options.cwd,
407
+ allowUntestedStellarCli: options.allowUntestedStellarCli,
408
+ skipStellarVersionCheck: true
409
+ });
410
+ return parseContractId(result.all || `${result.stdout}
411
+ ${result.stderr}`);
412
+ }
413
+ async function tryRecoverContractIdFromDeployFailure(options) {
414
+ if (!DEPLOY_SIGNING_FAILURE_REGEX.test(options.output)) {
415
+ return null;
416
+ }
417
+ const hashMatch = options.output.match(TX_HASH_REGEX);
418
+ if (!hashMatch) {
419
+ return null;
420
+ }
421
+ const horizonUrl = resolveHorizonUrl(options.network);
422
+ const salt = await fetchCreateContractSalt(horizonUrl, hashMatch[1], options.fetchImpl);
423
+ if (!salt) {
424
+ return null;
425
+ }
426
+ return resolveContractIdFromDeploySalt({
427
+ salt,
428
+ source: options.source,
429
+ network: options.network,
430
+ cwd: options.cwd,
431
+ allowUntestedStellarCli: options.allowUntestedStellarCli
432
+ });
433
+ }
434
+
435
+ // src/contracts/validate-source-shape.ts
436
+ function validateSourceShape(source) {
437
+ if (source.startsWith("S")) {
438
+ return new CaatingaError(
439
+ "Refusing to accept a Stellar secret key as --source.",
440
+ CaatingaErrorCode.SOURCE_IS_SECRET_KEY,
441
+ "Use a Stellar CLI identity alias instead, for example: --source alice"
442
+ );
443
+ }
444
+ if (source.trim().includes(" ")) {
445
+ return new CaatingaError(
446
+ "Refusing to accept a seed phrase as --source.",
447
+ CaatingaErrorCode.SOURCE_IS_SEED_PHRASE,
448
+ "Use a Stellar CLI identity alias instead, for example: --source alice"
449
+ );
450
+ }
451
+ if (isLikelyPublicKeySource(source)) {
452
+ return new CaatingaError(
453
+ `Public account address cannot sign transactions: ${source}`,
454
+ CaatingaErrorCode.SOURCE_IS_PUBLIC_KEY,
455
+ "Use a Stellar CLI identity with a secret key. Example: stellar keys generate alice --fund --network testnet, then --source alice"
456
+ );
457
+ }
458
+ if (source.startsWith("G")) {
459
+ return new CaatingaError(
460
+ "Refusing to accept a public account address as --source.",
461
+ CaatingaErrorCode.UNSAFE_SOURCE_ACCOUNT,
462
+ "Use a Stellar CLI identity alias, not a public address. Example: --source alice"
463
+ );
464
+ }
465
+ return void 0;
466
+ }
467
+
389
468
  // src/contracts/resolve-contract.ts
390
469
  import path4 from "path";
391
470
  function resolveContract(config, contractName, cwd = process.cwd()) {
@@ -407,25 +486,99 @@ function resolveContract(config, contractName, cwd = process.cwd()) {
407
486
 
408
487
  // src/contracts/wasm.ts
409
488
  import { createHash } from "crypto";
410
- import { access as access2, readFile as readFile2 } from "fs/promises";
411
- async function assertWasmExists(wasmPath) {
489
+ import { access as access2, readdir, readFile as readFile2, stat } from "fs/promises";
490
+ import path5 from "path";
491
+ var LEGACY_RUST_WASM_TARGET = "wasm32-unknown-unknown";
492
+ var CURRENT_RUST_WASM_TARGET = "wasm32v1-none";
493
+ function toCurrentWasmTargetPath(wasmPath) {
494
+ if (!wasmPath.includes(LEGACY_RUST_WASM_TARGET)) {
495
+ return wasmPath;
496
+ }
497
+ return wasmPath.replaceAll(LEGACY_RUST_WASM_TARGET, CURRENT_RUST_WASM_TARGET);
498
+ }
499
+ function wasmNotFoundError(configuredWasmPath, options) {
500
+ const migratedPath = options?.migratedPath;
501
+ const hint = migratedPath === void 0 ? "Run caatinga build before deploy or generate." : [
502
+ "Run caatinga build before deploy or generate.",
503
+ `Soroban builds use the "${CURRENT_RUST_WASM_TARGET}" target.`,
504
+ `Update wasm in caatinga.config.ts to "${toConfigRelativeWasmPath(migratedPath)}" or an equivalent path under target/${CURRENT_RUST_WASM_TARGET}/release/.`
505
+ ].join(" ");
506
+ return new CaatingaError(
507
+ `WASM output was not found at ${configuredWasmPath}.`,
508
+ CaatingaErrorCode.ARTIFACT_NOT_FOUND,
509
+ hint
510
+ );
511
+ }
512
+ function toConfigRelativeWasmPath(absoluteWasmPath) {
513
+ const relative = path5.relative(process.cwd(), absoluteWasmPath);
514
+ return relative.startsWith("..") ? absoluteWasmPath : `./${relative.split(path5.sep).join("/")}`;
515
+ }
516
+ async function resolveWasmArtifactPath(configuredWasmPath) {
412
517
  try {
413
- await access2(wasmPath);
518
+ await access2(configuredWasmPath);
519
+ return configuredWasmPath;
414
520
  } catch {
415
- throw new CaatingaError(
416
- `WASM output was not found at ${wasmPath}.`,
417
- CaatingaErrorCode.ARTIFACT_NOT_FOUND,
418
- "Run caatinga build before deploy or generate."
419
- );
521
+ const currentTargetPath = toCurrentWasmTargetPath(configuredWasmPath);
522
+ if (currentTargetPath === configuredWasmPath) {
523
+ throw wasmNotFoundError(configuredWasmPath);
524
+ }
525
+ try {
526
+ await access2(currentTargetPath);
527
+ return currentTargetPath;
528
+ } catch {
529
+ throw wasmNotFoundError(configuredWasmPath, { migratedPath: currentTargetPath });
530
+ }
420
531
  }
421
532
  }
422
533
  async function hashWasm(wasmPath) {
423
534
  const bytes = await readFile2(wasmPath);
424
535
  return createHash("sha256").update(bytes).digest("hex");
425
536
  }
537
+ async function getNewestMtimeInDirectory(directory) {
538
+ try {
539
+ await access2(directory);
540
+ } catch {
541
+ return void 0;
542
+ }
543
+ let newest = 0;
544
+ async function walk(dir) {
545
+ const entries = await readdir(dir, { withFileTypes: true });
546
+ for (const entry of entries) {
547
+ const entryPath = path5.join(dir, entry.name);
548
+ if (entry.isDirectory()) {
549
+ await walk(entryPath);
550
+ continue;
551
+ }
552
+ if (!entry.isFile()) {
553
+ continue;
554
+ }
555
+ const fileStat = await stat(entryPath);
556
+ newest = Math.max(newest, fileStat.mtimeMs);
557
+ }
558
+ }
559
+ await walk(directory);
560
+ return newest > 0 ? newest : void 0;
561
+ }
562
+ async function isWasmOlderThanSources(input) {
563
+ const srcDir = path5.join(input.contractPath, "src");
564
+ const newestSourceMtime = await getNewestMtimeInDirectory(srcDir);
565
+ if (newestSourceMtime === void 0) {
566
+ return false;
567
+ }
568
+ let wasmStat;
569
+ try {
570
+ wasmStat = await stat(input.wasmPath);
571
+ } catch {
572
+ return false;
573
+ }
574
+ return wasmStat.mtimeMs < newestSourceMtime;
575
+ }
576
+ function formatStaleWasmWarning(contractName) {
577
+ return `WASM for "${contractName}" may be stale: contract sources under src/ are newer than the WASM file. Run \`caatinga build\` before deploy.`;
578
+ }
426
579
 
427
580
  // src/contracts/build-contract.ts
428
- var RUST_WASM_TARGET = "wasm32-unknown-unknown";
581
+ var RUST_WASM_TARGET = "wasm32v1-none";
429
582
  var MISSING_WASM_TARGET_HINT_SUBSTRINGS = [
430
583
  "not installed",
431
584
  "not found",
@@ -467,21 +620,24 @@ async function buildContract(options) {
467
620
  throw new CaatingaError(
468
621
  `Required Rust wasm target "${RUST_WASM_TARGET}" is missing.`,
469
622
  CaatingaErrorCode.RUST_TARGET_NOT_FOUND,
470
- `Run \`rustup target add ${RUST_WASM_TARGET}\` and retry the build.`,
623
+ `Run \`rustup target add ${RUST_WASM_TARGET}\` and retry.`,
471
624
  error
472
625
  );
473
626
  }
474
627
  throw error;
475
628
  }
476
- await assertWasmExists(contract.wasmPath);
629
+ const wasmPath = await resolveWasmArtifactPath(contract.wasmPath);
477
630
  return {
478
- contract,
631
+ contract: {
632
+ ...contract,
633
+ wasmPath
634
+ },
479
635
  output: result.all || result.stdout
480
636
  };
481
637
  }
482
638
 
483
639
  // src/contracts/deploy-contract.ts
484
- import path5 from "path";
640
+ import path6 from "path";
485
641
 
486
642
  // src/contracts/dependency-graph.ts
487
643
  function buildDependencyGraph(contracts) {
@@ -529,15 +685,12 @@ function assertSafeSourceAccount(source) {
529
685
  throw new CaatingaError(
530
686
  "A source account or Stellar CLI identity is required.",
531
687
  CaatingaErrorCode.SOURCE_ACCOUNT_REQUIRED,
532
- "Pass --source alice or --source G...; do not pass secret keys or seed phrases."
688
+ "Pass a Stellar CLI identity alias, for example: --source alice"
533
689
  );
534
690
  }
535
- if (source.startsWith("S") || source.trim().includes(" ")) {
536
- throw new CaatingaError(
537
- "Refusing to accept a likely secret key or seed phrase as --source.",
538
- CaatingaErrorCode.UNSAFE_SOURCE_ACCOUNT,
539
- "Use a Stellar CLI identity alias or public account address instead."
540
- );
691
+ const unsafeSource = validateSourceShape(source);
692
+ if (unsafeSource) {
693
+ throw unsafeSource;
541
694
  }
542
695
  return source;
543
696
  }
@@ -565,17 +718,32 @@ async function deployContract(options) {
565
718
  await checkBinary("stellar", "Install Stellar CLI before running caatinga deploy.", {
566
719
  allowUntestedStellarCli: options.allowUntestedStellarCli
567
720
  });
568
- await assertWasmExists(contract.wasmPath);
721
+ const wasmPath = await resolveWasmArtifactPath(contract.wasmPath);
722
+ const contractWithWasm = {
723
+ ...contract,
724
+ wasmPath
725
+ };
726
+ let staleWasmWarning;
727
+ if (options.checkStaleWasm !== false) {
728
+ const stale = await isWasmOlderThanSources({
729
+ wasmPath,
730
+ contractPath: contract.sourcePath
731
+ });
732
+ if (stale) {
733
+ staleWasmWarning = formatStaleWasmWarning(contract.name);
734
+ }
735
+ }
569
736
  const artifactsBefore = await readArtifacts(cwd);
570
737
  const existing = artifactsBefore.networks[network.name]?.contracts[contract.name];
571
738
  if (existing?.contractId && !options.force) {
572
739
  return {
573
- contract,
740
+ contract: contractWithWasm,
574
741
  network,
575
742
  contractId: existing.contractId,
576
- artifactsPath: path5.resolve(cwd, "caatinga.artifacts.json"),
743
+ artifactsPath: path6.resolve(cwd, "caatinga.artifacts.json"),
577
744
  output: "",
578
- skipped: true
745
+ skipped: true,
746
+ staleWasmWarning
579
747
  };
580
748
  }
581
749
  const rawDeployArgs = contract.config.deployArgs;
@@ -606,24 +774,46 @@ async function deployContract(options) {
606
774
  "contract",
607
775
  "deploy",
608
776
  "--wasm",
609
- contract.wasmPath,
777
+ wasmPath,
610
778
  "--source-account",
611
779
  source,
612
- "--rpc-url",
613
- network.config.rpcUrl,
614
- "--network-passphrase",
615
- network.config.networkPassphrase,
780
+ ...buildStellarNetworkArgs(network),
616
781
  ...constructorArgs
617
782
  ];
618
- const result = await runCommand("stellar", stellarArgs, {
619
- cwd,
620
- allowUntestedStellarCli: options.allowUntestedStellarCli,
621
- failureCode: CaatingaErrorCode.DEPLOY_FAILED
622
- });
623
- const output = result.all || `${result.stdout}
783
+ let output = "";
784
+ let contractId;
785
+ try {
786
+ const result = await runCommand("stellar", stellarArgs, {
787
+ cwd,
788
+ allowUntestedStellarCli: options.allowUntestedStellarCli,
789
+ failureCode: CaatingaErrorCode.DEPLOY_FAILED
790
+ });
791
+ output = result.all || `${result.stdout}
624
792
  ${result.stderr}`;
625
- const contractId = parseContractId(output);
626
- const wasmHash = await hashWasm(contract.wasmPath);
793
+ contractId = parseContractId(output);
794
+ } catch (error) {
795
+ if (!(error instanceof CaatingaError) || error.code !== CaatingaErrorCode.DEPLOY_FAILED) {
796
+ throw error;
797
+ }
798
+ const recoveredContractId = await tryRecoverContractIdFromDeployFailure({
799
+ output: `${error.message}
800
+ ${error.hint ?? ""}`,
801
+ source,
802
+ network: network.config,
803
+ cwd,
804
+ allowUntestedStellarCli: options.allowUntestedStellarCli
805
+ });
806
+ if (!recoveredContractId) {
807
+ throw error;
808
+ }
809
+ contractId = recoveredContractId;
810
+ output = [
811
+ error.hint ?? "",
812
+ "Caatinga recovered the contract ID from the on-chain deploy transaction.",
813
+ `Contract ID: ${contractId}`
814
+ ].filter(Boolean).join("\n");
815
+ }
816
+ const wasmHash = await hashWasm(wasmPath);
627
817
  const dependencyGraph = buildDependencyGraph(options.config.contracts);
628
818
  const dependencies = options.dependencies ?? contract.config.dependsOn;
629
819
  const nextArtifacts = updateArtifact(
@@ -643,12 +833,13 @@ ${result.stderr}`;
643
833
  );
644
834
  const artifactsPath = await writeArtifacts(nextArtifacts, cwd);
645
835
  return {
646
- contract,
836
+ contract: contractWithWasm,
647
837
  network,
648
838
  contractId,
649
839
  artifactsPath,
650
840
  output,
651
- skipped: false
841
+ skipped: false,
842
+ staleWasmWarning
652
843
  };
653
844
  }
654
845
 
@@ -703,6 +894,53 @@ function resolveDeployOrder(input) {
703
894
  }
704
895
  }
705
896
 
897
+ // src/contracts/verify-dependency-contract.ts
898
+ async function verifyDependencyContract(options) {
899
+ try {
900
+ await runCommand("stellar", [
901
+ "contract",
902
+ "info",
903
+ "interface",
904
+ "--contract-id",
905
+ options.contractId,
906
+ ...buildStellarNetworkArgs(options.network)
907
+ ], {
908
+ cwd: options.cwd,
909
+ allowUntestedStellarCli: options.allowUntestedStellarCli,
910
+ failureCode: CaatingaErrorCode.DEPENDENCY_CONTRACT_NOT_FOUND
911
+ });
912
+ } catch (error) {
913
+ if (error instanceof CaatingaError && error.code === CaatingaErrorCode.DEPENDENCY_CONTRACT_NOT_FOUND) {
914
+ throw new CaatingaError(
915
+ `Dependency "${options.dependencyName}" is not deployed on "${options.network.name}" (contract ID ${options.contractId}).`,
916
+ CaatingaErrorCode.DEPENDENCY_CONTRACT_NOT_FOUND,
917
+ "Deploy the dependency on this network, fix caatinga.artifacts.json, or omit --verify-deps.",
918
+ error.cause
919
+ );
920
+ }
921
+ throw error;
922
+ }
923
+ }
924
+ async function verifyDependencyContracts(options) {
925
+ for (const dependencyName of options.dependencies) {
926
+ const contractArtifact = options.artifacts.networks[options.network.name]?.contracts[dependencyName];
927
+ if (!contractArtifact?.contractId) {
928
+ throw new CaatingaError(
929
+ `No dependency artifact found for "${dependencyName}" on "${options.network.name}".`,
930
+ CaatingaErrorCode.CONTRACT_DEPENDENCY_ARTIFACT_NOT_FOUND,
931
+ "Deploy the dependency first or run deploy without --no-deps."
932
+ );
933
+ }
934
+ await verifyDependencyContract({
935
+ dependencyName,
936
+ contractId: contractArtifact.contractId,
937
+ network: options.network,
938
+ cwd: options.cwd,
939
+ allowUntestedStellarCli: options.allowUntestedStellarCli
940
+ });
941
+ }
942
+ }
943
+
706
944
  // src/contracts/deploy-contract-graph.ts
707
945
  async function deployContractGraph(options) {
708
946
  const cwd = options.cwd ?? process.cwd();
@@ -713,17 +951,33 @@ async function deployContractGraph(options) {
713
951
  includeDependencies: options.includeDependencies
714
952
  });
715
953
  const deployedContracts = [];
954
+ const skippedContracts = [];
955
+ const staleWasmWarnings = [];
716
956
  for (const contractName of order) {
717
957
  const artifacts = await readArtifacts(cwd);
718
958
  const existing = artifacts.networks[network.name]?.contracts[contractName];
719
959
  const contractConfig = options.config.contracts[contractName];
960
+ if (options.verifyDeps && contractConfig.dependsOn.length > 0) {
961
+ await verifyDependencyContracts({
962
+ dependencies: contractConfig.dependsOn,
963
+ artifacts,
964
+ network,
965
+ cwd,
966
+ allowUntestedStellarCli: options.allowUntestedStellarCli
967
+ });
968
+ }
720
969
  const resolvedDeployArgs = resolveDeployArgs({
721
970
  deployArgs: contractConfig.deployArgs,
722
971
  artifacts,
723
972
  network: network.name
724
973
  });
725
974
  if (existing?.contractId && !options.force) {
726
- deployedContracts.push({ name: contractName, contractId: existing.contractId });
975
+ skippedContracts.push({
976
+ name: contractName,
977
+ contractId: existing.contractId,
978
+ network: network.name,
979
+ reason: "already-deployed"
980
+ });
727
981
  continue;
728
982
  }
729
983
  const result = await deployContract({
@@ -734,20 +988,38 @@ async function deployContractGraph(options) {
734
988
  cwd,
735
989
  allowUntestedStellarCli: options.allowUntestedStellarCli,
736
990
  force: options.force,
991
+ checkStaleWasm: options.checkStaleWasm,
737
992
  resolvedDeployArgs,
738
993
  dependencies: contractConfig.dependsOn
739
994
  });
740
- deployedContracts.push({ name: contractName, contractId: result.contractId });
995
+ if (result.staleWasmWarning) {
996
+ staleWasmWarnings.push({
997
+ contract: contractName,
998
+ message: result.staleWasmWarning
999
+ });
1000
+ }
1001
+ if (result.skipped) {
1002
+ skippedContracts.push({
1003
+ name: contractName,
1004
+ contractId: result.contractId,
1005
+ network: network.name,
1006
+ reason: "already-deployed"
1007
+ });
1008
+ } else {
1009
+ deployedContracts.push({ name: contractName, contractId: result.contractId });
1010
+ }
741
1011
  }
742
1012
  return {
743
1013
  network,
744
- deployedContracts
1014
+ deployedContracts,
1015
+ skippedContracts,
1016
+ staleWasmWarnings
745
1017
  };
746
1018
  }
747
1019
 
748
1020
  // src/contracts/generate-bindings.ts
749
1021
  import { mkdir as mkdir2 } from "fs/promises";
750
- import path6 from "path";
1022
+ import path7 from "path";
751
1023
  async function generateBindings(options) {
752
1024
  const cwd = options.cwd ?? process.cwd();
753
1025
  const network = resolveNetwork(options.config, options.networkName);
@@ -763,7 +1035,7 @@ async function generateBindings(options) {
763
1035
  await checkBinary("stellar", "Install Stellar CLI before running caatinga generate.", {
764
1036
  allowUntestedStellarCli: options.allowUntestedStellarCli
765
1037
  });
766
- const outputDir = path6.resolve(cwd, options.config.frontend.bindingsOutput, options.contractName);
1038
+ const outputDir = path7.resolve(cwd, options.config.frontend.bindingsOutput, options.contractName);
767
1039
  await mkdir2(outputDir, { recursive: true });
768
1040
  const result = await runCommand("stellar", [
769
1041
  "contract",
@@ -774,10 +1046,7 @@ async function generateBindings(options) {
774
1046
  "--output-dir",
775
1047
  outputDir,
776
1048
  "--overwrite",
777
- "--rpc-url",
778
- network.config.rpcUrl,
779
- "--network-passphrase",
780
- network.config.networkPassphrase
1049
+ ...buildStellarNetworkArgs(network)
781
1050
  ], {
782
1051
  cwd,
783
1052
  allowUntestedStellarCli: options.allowUntestedStellarCli,
@@ -792,6 +1061,7 @@ async function generateBindings(options) {
792
1061
  }
793
1062
 
794
1063
  // src/contracts/invoke-contract.ts
1064
+ var INVOKE_SIGNING_FAILURE_REGEX = /xdr processing error: xdr value invalid/i;
795
1065
  function parseInvokeTarget(target) {
796
1066
  const [contractName, method, extra] = target.split(".");
797
1067
  if (!contractName || !method || extra) {
@@ -820,25 +1090,43 @@ async function invokeContract(options) {
820
1090
  await checkBinary("stellar", "Install Stellar CLI before running caatinga invoke.", {
821
1091
  allowUntestedStellarCli: options.allowUntestedStellarCli
822
1092
  });
823
- const result = await runCommand("stellar", [
824
- "contract",
825
- "invoke",
826
- "--id",
827
- contractArtifact.contractId,
828
- "--source-account",
829
- source,
830
- "--rpc-url",
831
- network.config.rpcUrl,
832
- "--network-passphrase",
833
- network.config.networkPassphrase,
834
- "--",
835
- target.method,
836
- ...options.args ?? []
837
- ], {
838
- cwd,
839
- allowUntestedStellarCli: options.allowUntestedStellarCli,
840
- failureCode: CaatingaErrorCode.INVOKE_FAILED
841
- });
1093
+ let result;
1094
+ try {
1095
+ result = await runCommand("stellar", [
1096
+ "contract",
1097
+ "invoke",
1098
+ "--id",
1099
+ contractArtifact.contractId,
1100
+ "--source-account",
1101
+ source,
1102
+ ...buildStellarNetworkArgs(network),
1103
+ "--",
1104
+ target.method,
1105
+ ...options.args ?? []
1106
+ ], {
1107
+ cwd,
1108
+ allowUntestedStellarCli: options.allowUntestedStellarCli,
1109
+ failureCode: CaatingaErrorCode.INVOKE_FAILED
1110
+ });
1111
+ } catch (error) {
1112
+ if (error instanceof CaatingaError && error.code === CaatingaErrorCode.INVOKE_FAILED && INVOKE_SIGNING_FAILURE_REGEX.test(`${error.message}
1113
+ ${error.hint ?? ""}`)) {
1114
+ throw new CaatingaError(
1115
+ error.message,
1116
+ error.code,
1117
+ [
1118
+ "Stellar CLI could not sign the invoke transaction (xdr value invalid).",
1119
+ "Stellar CLI 22.x has a known invoke signing bug; upgrade to 23.0.0 or newer (25.2.0 recommended).",
1120
+ " stellar --version",
1121
+ "Then retry with a funded identity, for example:",
1122
+ " stellar keys generate alice --fund --network testnet",
1123
+ " npx caatinga invoke counter.increment --network testnet --source alice"
1124
+ ].join("\n"),
1125
+ error
1126
+ );
1127
+ }
1128
+ throw error;
1129
+ }
842
1130
  return {
843
1131
  target,
844
1132
  network,
@@ -847,8 +1135,8 @@ async function invokeContract(options) {
847
1135
  }
848
1136
 
849
1137
  // src/templates/create-project-from-template.ts
850
- import { cp, mkdir as mkdir3, readFile as readFile3, readdir, stat, writeFile as writeFile2 } from "fs/promises";
851
- import path7 from "path";
1138
+ import { cp, mkdir as mkdir3, readFile as readFile3, readdir as readdir2, stat as stat2, writeFile as writeFile2 } from "fs/promises";
1139
+ import path8 from "path";
852
1140
  import { z as z6 } from "zod";
853
1141
 
854
1142
  // src/templates/template-manifest.schema.ts
@@ -876,16 +1164,52 @@ var TemplateManifestSchema = z5.object({
876
1164
  artifacts: z5.string().default("caatinga.artifacts.json")
877
1165
  })
878
1166
  });
1167
+ function defaultCompatibleCoreRange(coreVersion = CAATINGA_CORE_VERSION) {
1168
+ const version = semver2.valid(semver2.coerce(coreVersion));
1169
+ if (!version) {
1170
+ throw new Error(`Invalid core version: ${coreVersion}`);
1171
+ }
1172
+ return `^${version}`;
1173
+ }
879
1174
  function isCoreVersionCompatible(range, coreVersion = CAATINGA_CORE_VERSION) {
880
1175
  return semver2.satisfies(coreVersion, range);
881
1176
  }
1177
+ function getTemplateCompatibilityIssue(manifest, coreVersion = CAATINGA_CORE_VERSION) {
1178
+ if (manifest.caatinga.templateVersion !== CURRENT_TEMPLATE_VERSION) {
1179
+ return {
1180
+ kind: "template-version",
1181
+ expected: CURRENT_TEMPLATE_VERSION,
1182
+ actual: manifest.caatinga.templateVersion
1183
+ };
1184
+ }
1185
+ if (!isCoreVersionCompatible(manifest.caatinga.compatibleCore, coreVersion)) {
1186
+ return {
1187
+ kind: "core-range",
1188
+ requiredRange: manifest.caatinga.compatibleCore,
1189
+ runningVersion: coreVersion
1190
+ };
1191
+ }
1192
+ return null;
1193
+ }
1194
+ function formatTemplateCompatibilityMessage(issue) {
1195
+ if (issue.kind === "template-version") {
1196
+ return `Template manifest version ${issue.actual} is not supported; Caatinga requires templateVersion ${issue.expected}.`;
1197
+ }
1198
+ return `Template requires @caatinga/core ${issue.requiredRange} but running ${issue.runningVersion}.`;
1199
+ }
1200
+ function formatTemplateCompatibilityHint(issue) {
1201
+ if (issue.kind === "template-version") {
1202
+ return "Use a template built for this Caatinga release or upgrade Caatinga.";
1203
+ }
1204
+ return `Use a template with compatibleCore ${defaultCompatibleCoreRange(issue.runningVersion)} or install a matching Caatinga version.`;
1205
+ }
882
1206
 
883
1207
  // src/templates/create-project-from-template.ts
884
1208
  async function createProjectFromTemplate(options) {
885
- const targetDir = path7.resolve(options.targetDir);
886
- const templateDir = path7.resolve(options.templateDir);
1209
+ const targetDir = path8.resolve(options.targetDir);
1210
+ const templateDir = path8.resolve(options.templateDir);
887
1211
  try {
888
- await stat(templateDir);
1212
+ await stat2(templateDir);
889
1213
  } catch {
890
1214
  throw new CaatingaError(
891
1215
  `Template directory was not found: ${templateDir}`,
@@ -905,22 +1229,16 @@ async function createProjectFromTemplate(options) {
905
1229
  return { targetDir, template: manifest };
906
1230
  }
907
1231
  async function readTemplateManifest(templateDir) {
908
- const manifestPath = path7.join(templateDir, "caatinga.template.json");
1232
+ const manifestPath = path8.join(templateDir, "caatinga.template.json");
909
1233
  try {
910
1234
  const rawManifest = await readFile3(manifestPath, "utf8");
911
1235
  const manifest = TemplateManifestSchema.parse(JSON.parse(rawManifest));
912
- if (manifest.caatinga.templateVersion !== CURRENT_TEMPLATE_VERSION) {
913
- throw new CaatingaError(
914
- "Template is not compatible with this Caatinga version.",
915
- CaatingaErrorCode.TEMPLATE_INCOMPATIBLE,
916
- "Use a compatible template version or upgrade Caatinga."
917
- );
918
- }
919
- if (!isCoreVersionCompatible(manifest.caatinga.compatibleCore)) {
1236
+ const compatibilityIssue = getTemplateCompatibilityIssue(manifest);
1237
+ if (compatibilityIssue) {
920
1238
  throw new CaatingaError(
921
- "Template is not compatible with this Caatinga version.",
1239
+ formatTemplateCompatibilityMessage(compatibilityIssue),
922
1240
  CaatingaErrorCode.TEMPLATE_INCOMPATIBLE,
923
- "Use a compatible template version or upgrade Caatinga."
1241
+ formatTemplateCompatibilityHint(compatibilityIssue)
924
1242
  );
925
1243
  }
926
1244
  return manifest;
@@ -946,10 +1264,10 @@ async function readTemplateManifest(templateDir) {
946
1264
  }
947
1265
  }
948
1266
  async function replaceTemplateVariables(dir, projectName) {
949
- const entries = await readdir(dir);
1267
+ const entries = await readdir2(dir);
950
1268
  await Promise.all(entries.map(async (entry) => {
951
- const entryPath = path7.join(dir, entry);
952
- const entryStat = await stat(entryPath);
1269
+ const entryPath = path8.join(dir, entry);
1270
+ const entryStat = await stat2(entryPath);
953
1271
  if (entryStat.isDirectory()) {
954
1272
  await replaceTemplateVariables(entryPath, projectName);
955
1273
  return;
@@ -971,7 +1289,7 @@ function isTextTemplateFile(filePath) {
971
1289
  ".tsx",
972
1290
  ".css",
973
1291
  ".html"
974
- ].includes(path7.extname(filePath));
1292
+ ].includes(path8.extname(filePath));
975
1293
  }
976
1294
 
977
1295
  // src/ci/is-transient-testnet-smoke-failure.ts
@@ -1029,5 +1347,6 @@ export {
1029
1347
  runCommand,
1030
1348
  toCaatingaError,
1031
1349
  updateArtifact,
1350
+ validateSourceShape,
1032
1351
  writeArtifacts
1033
1352
  };