@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.cjs CHANGED
@@ -63,6 +63,7 @@ __export(index_exports, {
63
63
  runCommand: () => runCommand,
64
64
  toCaatingaError: () => toCaatingaError,
65
65
  updateArtifact: () => updateArtifact,
66
+ validateSourceShape: () => validateSourceShape,
66
67
  writeArtifacts: () => writeArtifacts
67
68
  });
68
69
  module.exports = __toCommonJS(index_exports);
@@ -92,6 +93,7 @@ var CaatingaErrorCode = {
92
93
  CONTRACT_DEPENDENCY_NOT_FOUND: "CAATINGA_CONTRACT_DEPENDENCY_NOT_FOUND",
93
94
  CONTRACT_DEPENDENCY_CYCLE: "CAATINGA_CONTRACT_DEPENDENCY_CYCLE",
94
95
  CONTRACT_DEPENDENCY_ARTIFACT_NOT_FOUND: "CAATINGA_CONTRACT_DEPENDENCY_ARTIFACT_NOT_FOUND",
96
+ DEPENDENCY_CONTRACT_NOT_FOUND: "CAATINGA_DEPENDENCY_CONTRACT_NOT_FOUND",
95
97
  DEPLOY_ARG_PLACEHOLDER_INVALID: "CAATINGA_DEPLOY_ARG_PLACEHOLDER_INVALID",
96
98
  DEPLOY_ARG_PLACEHOLDER_UNRESOLVED: "CAATINGA_DEPLOY_ARG_PLACEHOLDER_UNRESOLVED",
97
99
  BINDING_CLIENT_NOT_FOUND: "CAATINGA_BINDING_CLIENT_NOT_FOUND",
@@ -102,13 +104,18 @@ var CaatingaErrorCode = {
102
104
  XDR_SUBMIT_FAILED: "CAATINGA_XDR_SUBMIT_FAILED",
103
105
  XDR_RESULT_FAILED: "CAATINGA_XDR_RESULT_FAILED",
104
106
  WALLET_NOT_CONNECTED: "CAATINGA_WALLET_NOT_CONNECTED",
107
+ WALLET_TIMEOUT: "CAATINGA_WALLET_TIMEOUT",
105
108
  SOURCE_ACCOUNT_REQUIRED: "CAATINGA_SOURCE_ACCOUNT_REQUIRED",
109
+ SOURCE_IS_SECRET_KEY: "CAATINGA_SOURCE_IS_SECRET_KEY",
110
+ SOURCE_IS_SEED_PHRASE: "CAATINGA_SOURCE_IS_SEED_PHRASE",
111
+ SOURCE_IS_PUBLIC_KEY: "CAATINGA_SOURCE_IS_PUBLIC_KEY",
106
112
  UNSAFE_SOURCE_ACCOUNT: "CAATINGA_UNSAFE_SOURCE_ACCOUNT",
107
113
  INVOKE_TARGET_INVALID: "CAATINGA_INVOKE_TARGET_INVALID",
108
114
  TEMPLATE_NOT_FOUND: "CAATINGA_TEMPLATE_NOT_FOUND",
109
115
  INVALID_TEMPLATE_MANIFEST: "CAATINGA_INVALID_TEMPLATE_MANIFEST",
110
116
  TEMPLATE_MANIFEST_NOT_FOUND: "CAATINGA_TEMPLATE_MANIFEST_NOT_FOUND",
111
- TEMPLATE_INCOMPATIBLE: "CAATINGA_TEMPLATE_INCOMPATIBLE"
117
+ TEMPLATE_INCOMPATIBLE: "CAATINGA_TEMPLATE_INCOMPATIBLE",
118
+ DOCTOR_PARTIAL_DEPLOY: "CAATINGA_DOCTOR_PARTIAL_DEPLOY"
112
119
  };
113
120
 
114
121
  // src/errors/CaatingaError.ts
@@ -135,7 +142,7 @@ function toCaatingaError(error) {
135
142
  }
136
143
 
137
144
  // src/version.ts
138
- var CAATINGA_CORE_VERSION = "0.2.0";
145
+ var CAATINGA_CORE_VERSION = "0.2.2";
139
146
 
140
147
  // src/config/config.schema.ts
141
148
  var import_zod = require("zod");
@@ -323,7 +330,7 @@ var import_execa = require("execa");
323
330
 
324
331
  // src/stellar-cli/version.ts
325
332
  var import_semver = __toESM(require("semver"), 1);
326
- var STELLAR_CLI_MIN_VERSION = "22.0.0";
333
+ var STELLAR_CLI_MIN_VERSION = "23.0.0";
327
334
  var STELLAR_CLI_TESTED_MAX_VERSION = "25.2.0";
328
335
  var STELLAR_CLI_SEMVER_REGEX = /\b(\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?)\b/;
329
336
  function parseStellarCliVersion(output) {
@@ -456,6 +463,146 @@ function parseContractId(output) {
456
463
  return match[0];
457
464
  }
458
465
 
466
+ // src/stellar-cli/build-stellar-network-args.ts
467
+ function matchesWellKnownNetwork(name, config) {
468
+ const known = WELL_KNOWN_NETWORKS[name];
469
+ if (!known) {
470
+ return false;
471
+ }
472
+ return known.rpcUrl === config.rpcUrl && known.networkPassphrase === config.networkPassphrase;
473
+ }
474
+ function buildRpcNetworkArgs(config) {
475
+ return [
476
+ "--rpc-url",
477
+ config.rpcUrl,
478
+ "--network-passphrase",
479
+ config.networkPassphrase
480
+ ];
481
+ }
482
+ function buildStellarNetworkArgsFromConfig(config) {
483
+ for (const [name, known] of Object.entries(WELL_KNOWN_NETWORKS)) {
484
+ if (known.rpcUrl === config.rpcUrl && known.networkPassphrase === config.networkPassphrase) {
485
+ return ["--network", name];
486
+ }
487
+ }
488
+ return buildRpcNetworkArgs(config);
489
+ }
490
+ function buildStellarNetworkArgs(network) {
491
+ if (matchesWellKnownNetwork(network.name, network.config)) {
492
+ return ["--network", network.name];
493
+ }
494
+ return buildStellarNetworkArgsFromConfig(network.config);
495
+ }
496
+
497
+ // src/stellar-cli/recover-deploy-contract-id.ts
498
+ var TX_HASH_REGEX = /Transaction hash is ([a-f0-9]{64})/i;
499
+ var DEPLOY_SIGNING_FAILURE_REGEX = /xdr processing error: xdr value invalid/i;
500
+ var HORIZON_URL_BY_PASSPHRASE = {
501
+ "Test SDF Network ; September 2015": "https://horizon-testnet.stellar.org",
502
+ "Public Global Stellar Network ; September 2015": "https://horizon.stellar.org"
503
+ };
504
+ function isLikelyPublicKeySource(source) {
505
+ return /^G[A-Z2-7]{55}$/.test(source);
506
+ }
507
+ function decimalSaltToHex(salt) {
508
+ return BigInt(salt).toString(16).padStart(64, "0");
509
+ }
510
+ function resolveHorizonUrl(network) {
511
+ const horizonUrl = HORIZON_URL_BY_PASSPHRASE[network.networkPassphrase];
512
+ if (!horizonUrl) {
513
+ throw new CaatingaError(
514
+ `No Horizon URL mapping for network passphrase "${network.networkPassphrase}".`,
515
+ CaatingaErrorCode.NETWORK_NOT_FOUND,
516
+ "Use testnet or mainnet, or extend Caatinga network metadata."
517
+ );
518
+ }
519
+ return horizonUrl;
520
+ }
521
+ async function fetchCreateContractSalt(horizonUrl, transactionHash, fetchImpl = fetch) {
522
+ const response = await fetchImpl(`${horizonUrl}/transactions/${transactionHash}/operations`);
523
+ if (!response.ok) {
524
+ return null;
525
+ }
526
+ const body = await response.json();
527
+ const operation = body._embedded?.records?.find(
528
+ (record) => record.transaction_successful === true && record.type === "invoke_host_function" && record.function === "HostFunctionTypeHostFunctionTypeCreateContract" && typeof record.salt === "string"
529
+ );
530
+ return operation?.salt ?? null;
531
+ }
532
+ async function resolveContractIdFromDeploySalt(options) {
533
+ const saltHex = decimalSaltToHex(options.salt);
534
+ const result = await runCommand("stellar", [
535
+ "contract",
536
+ "id",
537
+ "wasm",
538
+ "--salt",
539
+ saltHex,
540
+ "--source-account",
541
+ options.source,
542
+ ...buildStellarNetworkArgsFromConfig(options.network)
543
+ ], {
544
+ cwd: options.cwd,
545
+ allowUntestedStellarCli: options.allowUntestedStellarCli,
546
+ skipStellarVersionCheck: true
547
+ });
548
+ return parseContractId(result.all || `${result.stdout}
549
+ ${result.stderr}`);
550
+ }
551
+ async function tryRecoverContractIdFromDeployFailure(options) {
552
+ if (!DEPLOY_SIGNING_FAILURE_REGEX.test(options.output)) {
553
+ return null;
554
+ }
555
+ const hashMatch = options.output.match(TX_HASH_REGEX);
556
+ if (!hashMatch) {
557
+ return null;
558
+ }
559
+ const horizonUrl = resolveHorizonUrl(options.network);
560
+ const salt = await fetchCreateContractSalt(horizonUrl, hashMatch[1], options.fetchImpl);
561
+ if (!salt) {
562
+ return null;
563
+ }
564
+ return resolveContractIdFromDeploySalt({
565
+ salt,
566
+ source: options.source,
567
+ network: options.network,
568
+ cwd: options.cwd,
569
+ allowUntestedStellarCli: options.allowUntestedStellarCli
570
+ });
571
+ }
572
+
573
+ // src/contracts/validate-source-shape.ts
574
+ function validateSourceShape(source) {
575
+ if (source.startsWith("S")) {
576
+ return new CaatingaError(
577
+ "Refusing to accept a Stellar secret key as --source.",
578
+ CaatingaErrorCode.SOURCE_IS_SECRET_KEY,
579
+ "Use a Stellar CLI identity alias instead, for example: --source alice"
580
+ );
581
+ }
582
+ if (source.trim().includes(" ")) {
583
+ return new CaatingaError(
584
+ "Refusing to accept a seed phrase as --source.",
585
+ CaatingaErrorCode.SOURCE_IS_SEED_PHRASE,
586
+ "Use a Stellar CLI identity alias instead, for example: --source alice"
587
+ );
588
+ }
589
+ if (isLikelyPublicKeySource(source)) {
590
+ return new CaatingaError(
591
+ `Public account address cannot sign transactions: ${source}`,
592
+ CaatingaErrorCode.SOURCE_IS_PUBLIC_KEY,
593
+ "Use a Stellar CLI identity with a secret key. Example: stellar keys generate alice --fund --network testnet, then --source alice"
594
+ );
595
+ }
596
+ if (source.startsWith("G")) {
597
+ return new CaatingaError(
598
+ "Refusing to accept a public account address as --source.",
599
+ CaatingaErrorCode.UNSAFE_SOURCE_ACCOUNT,
600
+ "Use a Stellar CLI identity alias, not a public address. Example: --source alice"
601
+ );
602
+ }
603
+ return void 0;
604
+ }
605
+
459
606
  // src/contracts/resolve-contract.ts
460
607
  var import_node_path4 = __toESM(require("path"), 1);
461
608
  function resolveContract(config, contractName, cwd = process.cwd()) {
@@ -478,24 +625,98 @@ function resolveContract(config, contractName, cwd = process.cwd()) {
478
625
  // src/contracts/wasm.ts
479
626
  var import_node_crypto = require("crypto");
480
627
  var import_promises4 = require("fs/promises");
481
- async function assertWasmExists(wasmPath) {
628
+ var import_node_path5 = __toESM(require("path"), 1);
629
+ var LEGACY_RUST_WASM_TARGET = "wasm32-unknown-unknown";
630
+ var CURRENT_RUST_WASM_TARGET = "wasm32v1-none";
631
+ function toCurrentWasmTargetPath(wasmPath) {
632
+ if (!wasmPath.includes(LEGACY_RUST_WASM_TARGET)) {
633
+ return wasmPath;
634
+ }
635
+ return wasmPath.replaceAll(LEGACY_RUST_WASM_TARGET, CURRENT_RUST_WASM_TARGET);
636
+ }
637
+ function wasmNotFoundError(configuredWasmPath, options) {
638
+ const migratedPath = options?.migratedPath;
639
+ const hint = migratedPath === void 0 ? "Run caatinga build before deploy or generate." : [
640
+ "Run caatinga build before deploy or generate.",
641
+ `Soroban builds use the "${CURRENT_RUST_WASM_TARGET}" target.`,
642
+ `Update wasm in caatinga.config.ts to "${toConfigRelativeWasmPath(migratedPath)}" or an equivalent path under target/${CURRENT_RUST_WASM_TARGET}/release/.`
643
+ ].join(" ");
644
+ return new CaatingaError(
645
+ `WASM output was not found at ${configuredWasmPath}.`,
646
+ CaatingaErrorCode.ARTIFACT_NOT_FOUND,
647
+ hint
648
+ );
649
+ }
650
+ function toConfigRelativeWasmPath(absoluteWasmPath) {
651
+ const relative = import_node_path5.default.relative(process.cwd(), absoluteWasmPath);
652
+ return relative.startsWith("..") ? absoluteWasmPath : `./${relative.split(import_node_path5.default.sep).join("/")}`;
653
+ }
654
+ async function resolveWasmArtifactPath(configuredWasmPath) {
482
655
  try {
483
- await (0, import_promises4.access)(wasmPath);
656
+ await (0, import_promises4.access)(configuredWasmPath);
657
+ return configuredWasmPath;
484
658
  } catch {
485
- throw new CaatingaError(
486
- `WASM output was not found at ${wasmPath}.`,
487
- CaatingaErrorCode.ARTIFACT_NOT_FOUND,
488
- "Run caatinga build before deploy or generate."
489
- );
659
+ const currentTargetPath = toCurrentWasmTargetPath(configuredWasmPath);
660
+ if (currentTargetPath === configuredWasmPath) {
661
+ throw wasmNotFoundError(configuredWasmPath);
662
+ }
663
+ try {
664
+ await (0, import_promises4.access)(currentTargetPath);
665
+ return currentTargetPath;
666
+ } catch {
667
+ throw wasmNotFoundError(configuredWasmPath, { migratedPath: currentTargetPath });
668
+ }
490
669
  }
491
670
  }
492
671
  async function hashWasm(wasmPath) {
493
672
  const bytes = await (0, import_promises4.readFile)(wasmPath);
494
673
  return (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
495
674
  }
675
+ async function getNewestMtimeInDirectory(directory) {
676
+ try {
677
+ await (0, import_promises4.access)(directory);
678
+ } catch {
679
+ return void 0;
680
+ }
681
+ let newest = 0;
682
+ async function walk(dir) {
683
+ const entries = await (0, import_promises4.readdir)(dir, { withFileTypes: true });
684
+ for (const entry of entries) {
685
+ const entryPath = import_node_path5.default.join(dir, entry.name);
686
+ if (entry.isDirectory()) {
687
+ await walk(entryPath);
688
+ continue;
689
+ }
690
+ if (!entry.isFile()) {
691
+ continue;
692
+ }
693
+ const fileStat = await (0, import_promises4.stat)(entryPath);
694
+ newest = Math.max(newest, fileStat.mtimeMs);
695
+ }
696
+ }
697
+ await walk(directory);
698
+ return newest > 0 ? newest : void 0;
699
+ }
700
+ async function isWasmOlderThanSources(input) {
701
+ const srcDir = import_node_path5.default.join(input.contractPath, "src");
702
+ const newestSourceMtime = await getNewestMtimeInDirectory(srcDir);
703
+ if (newestSourceMtime === void 0) {
704
+ return false;
705
+ }
706
+ let wasmStat;
707
+ try {
708
+ wasmStat = await (0, import_promises4.stat)(input.wasmPath);
709
+ } catch {
710
+ return false;
711
+ }
712
+ return wasmStat.mtimeMs < newestSourceMtime;
713
+ }
714
+ function formatStaleWasmWarning(contractName) {
715
+ return `WASM for "${contractName}" may be stale: contract sources under src/ are newer than the WASM file. Run \`caatinga build\` before deploy.`;
716
+ }
496
717
 
497
718
  // src/contracts/build-contract.ts
498
- var RUST_WASM_TARGET = "wasm32-unknown-unknown";
719
+ var RUST_WASM_TARGET = "wasm32v1-none";
499
720
  var MISSING_WASM_TARGET_HINT_SUBSTRINGS = [
500
721
  "not installed",
501
722
  "not found",
@@ -537,21 +758,24 @@ async function buildContract(options) {
537
758
  throw new CaatingaError(
538
759
  `Required Rust wasm target "${RUST_WASM_TARGET}" is missing.`,
539
760
  CaatingaErrorCode.RUST_TARGET_NOT_FOUND,
540
- `Run \`rustup target add ${RUST_WASM_TARGET}\` and retry the build.`,
761
+ `Run \`rustup target add ${RUST_WASM_TARGET}\` and retry.`,
541
762
  error
542
763
  );
543
764
  }
544
765
  throw error;
545
766
  }
546
- await assertWasmExists(contract.wasmPath);
767
+ const wasmPath = await resolveWasmArtifactPath(contract.wasmPath);
547
768
  return {
548
- contract,
769
+ contract: {
770
+ ...contract,
771
+ wasmPath
772
+ },
549
773
  output: result.all || result.stdout
550
774
  };
551
775
  }
552
776
 
553
777
  // src/contracts/deploy-contract.ts
554
- var import_node_path5 = __toESM(require("path"), 1);
778
+ var import_node_path6 = __toESM(require("path"), 1);
555
779
 
556
780
  // src/contracts/dependency-graph.ts
557
781
  function buildDependencyGraph(contracts) {
@@ -599,15 +823,12 @@ function assertSafeSourceAccount(source) {
599
823
  throw new CaatingaError(
600
824
  "A source account or Stellar CLI identity is required.",
601
825
  CaatingaErrorCode.SOURCE_ACCOUNT_REQUIRED,
602
- "Pass --source alice or --source G...; do not pass secret keys or seed phrases."
826
+ "Pass a Stellar CLI identity alias, for example: --source alice"
603
827
  );
604
828
  }
605
- if (source.startsWith("S") || source.trim().includes(" ")) {
606
- throw new CaatingaError(
607
- "Refusing to accept a likely secret key or seed phrase as --source.",
608
- CaatingaErrorCode.UNSAFE_SOURCE_ACCOUNT,
609
- "Use a Stellar CLI identity alias or public account address instead."
610
- );
829
+ const unsafeSource = validateSourceShape(source);
830
+ if (unsafeSource) {
831
+ throw unsafeSource;
611
832
  }
612
833
  return source;
613
834
  }
@@ -635,17 +856,32 @@ async function deployContract(options) {
635
856
  await checkBinary("stellar", "Install Stellar CLI before running caatinga deploy.", {
636
857
  allowUntestedStellarCli: options.allowUntestedStellarCli
637
858
  });
638
- await assertWasmExists(contract.wasmPath);
859
+ const wasmPath = await resolveWasmArtifactPath(contract.wasmPath);
860
+ const contractWithWasm = {
861
+ ...contract,
862
+ wasmPath
863
+ };
864
+ let staleWasmWarning;
865
+ if (options.checkStaleWasm !== false) {
866
+ const stale = await isWasmOlderThanSources({
867
+ wasmPath,
868
+ contractPath: contract.sourcePath
869
+ });
870
+ if (stale) {
871
+ staleWasmWarning = formatStaleWasmWarning(contract.name);
872
+ }
873
+ }
639
874
  const artifactsBefore = await readArtifacts(cwd);
640
875
  const existing = artifactsBefore.networks[network.name]?.contracts[contract.name];
641
876
  if (existing?.contractId && !options.force) {
642
877
  return {
643
- contract,
878
+ contract: contractWithWasm,
644
879
  network,
645
880
  contractId: existing.contractId,
646
- artifactsPath: import_node_path5.default.resolve(cwd, "caatinga.artifacts.json"),
881
+ artifactsPath: import_node_path6.default.resolve(cwd, "caatinga.artifacts.json"),
647
882
  output: "",
648
- skipped: true
883
+ skipped: true,
884
+ staleWasmWarning
649
885
  };
650
886
  }
651
887
  const rawDeployArgs = contract.config.deployArgs;
@@ -676,24 +912,46 @@ async function deployContract(options) {
676
912
  "contract",
677
913
  "deploy",
678
914
  "--wasm",
679
- contract.wasmPath,
915
+ wasmPath,
680
916
  "--source-account",
681
917
  source,
682
- "--rpc-url",
683
- network.config.rpcUrl,
684
- "--network-passphrase",
685
- network.config.networkPassphrase,
918
+ ...buildStellarNetworkArgs(network),
686
919
  ...constructorArgs
687
920
  ];
688
- const result = await runCommand("stellar", stellarArgs, {
689
- cwd,
690
- allowUntestedStellarCli: options.allowUntestedStellarCli,
691
- failureCode: CaatingaErrorCode.DEPLOY_FAILED
692
- });
693
- const output = result.all || `${result.stdout}
921
+ let output = "";
922
+ let contractId;
923
+ try {
924
+ const result = await runCommand("stellar", stellarArgs, {
925
+ cwd,
926
+ allowUntestedStellarCli: options.allowUntestedStellarCli,
927
+ failureCode: CaatingaErrorCode.DEPLOY_FAILED
928
+ });
929
+ output = result.all || `${result.stdout}
694
930
  ${result.stderr}`;
695
- const contractId = parseContractId(output);
696
- const wasmHash = await hashWasm(contract.wasmPath);
931
+ contractId = parseContractId(output);
932
+ } catch (error) {
933
+ if (!(error instanceof CaatingaError) || error.code !== CaatingaErrorCode.DEPLOY_FAILED) {
934
+ throw error;
935
+ }
936
+ const recoveredContractId = await tryRecoverContractIdFromDeployFailure({
937
+ output: `${error.message}
938
+ ${error.hint ?? ""}`,
939
+ source,
940
+ network: network.config,
941
+ cwd,
942
+ allowUntestedStellarCli: options.allowUntestedStellarCli
943
+ });
944
+ if (!recoveredContractId) {
945
+ throw error;
946
+ }
947
+ contractId = recoveredContractId;
948
+ output = [
949
+ error.hint ?? "",
950
+ "Caatinga recovered the contract ID from the on-chain deploy transaction.",
951
+ `Contract ID: ${contractId}`
952
+ ].filter(Boolean).join("\n");
953
+ }
954
+ const wasmHash = await hashWasm(wasmPath);
697
955
  const dependencyGraph = buildDependencyGraph(options.config.contracts);
698
956
  const dependencies = options.dependencies ?? contract.config.dependsOn;
699
957
  const nextArtifacts = updateArtifact(
@@ -713,12 +971,13 @@ ${result.stderr}`;
713
971
  );
714
972
  const artifactsPath = await writeArtifacts(nextArtifacts, cwd);
715
973
  return {
716
- contract,
974
+ contract: contractWithWasm,
717
975
  network,
718
976
  contractId,
719
977
  artifactsPath,
720
978
  output,
721
- skipped: false
979
+ skipped: false,
980
+ staleWasmWarning
722
981
  };
723
982
  }
724
983
 
@@ -773,6 +1032,53 @@ function resolveDeployOrder(input) {
773
1032
  }
774
1033
  }
775
1034
 
1035
+ // src/contracts/verify-dependency-contract.ts
1036
+ async function verifyDependencyContract(options) {
1037
+ try {
1038
+ await runCommand("stellar", [
1039
+ "contract",
1040
+ "info",
1041
+ "interface",
1042
+ "--contract-id",
1043
+ options.contractId,
1044
+ ...buildStellarNetworkArgs(options.network)
1045
+ ], {
1046
+ cwd: options.cwd,
1047
+ allowUntestedStellarCli: options.allowUntestedStellarCli,
1048
+ failureCode: CaatingaErrorCode.DEPENDENCY_CONTRACT_NOT_FOUND
1049
+ });
1050
+ } catch (error) {
1051
+ if (error instanceof CaatingaError && error.code === CaatingaErrorCode.DEPENDENCY_CONTRACT_NOT_FOUND) {
1052
+ throw new CaatingaError(
1053
+ `Dependency "${options.dependencyName}" is not deployed on "${options.network.name}" (contract ID ${options.contractId}).`,
1054
+ CaatingaErrorCode.DEPENDENCY_CONTRACT_NOT_FOUND,
1055
+ "Deploy the dependency on this network, fix caatinga.artifacts.json, or omit --verify-deps.",
1056
+ error.cause
1057
+ );
1058
+ }
1059
+ throw error;
1060
+ }
1061
+ }
1062
+ async function verifyDependencyContracts(options) {
1063
+ for (const dependencyName of options.dependencies) {
1064
+ const contractArtifact = options.artifacts.networks[options.network.name]?.contracts[dependencyName];
1065
+ if (!contractArtifact?.contractId) {
1066
+ throw new CaatingaError(
1067
+ `No dependency artifact found for "${dependencyName}" on "${options.network.name}".`,
1068
+ CaatingaErrorCode.CONTRACT_DEPENDENCY_ARTIFACT_NOT_FOUND,
1069
+ "Deploy the dependency first or run deploy without --no-deps."
1070
+ );
1071
+ }
1072
+ await verifyDependencyContract({
1073
+ dependencyName,
1074
+ contractId: contractArtifact.contractId,
1075
+ network: options.network,
1076
+ cwd: options.cwd,
1077
+ allowUntestedStellarCli: options.allowUntestedStellarCli
1078
+ });
1079
+ }
1080
+ }
1081
+
776
1082
  // src/contracts/deploy-contract-graph.ts
777
1083
  async function deployContractGraph(options) {
778
1084
  const cwd = options.cwd ?? process.cwd();
@@ -783,17 +1089,33 @@ async function deployContractGraph(options) {
783
1089
  includeDependencies: options.includeDependencies
784
1090
  });
785
1091
  const deployedContracts = [];
1092
+ const skippedContracts = [];
1093
+ const staleWasmWarnings = [];
786
1094
  for (const contractName of order) {
787
1095
  const artifacts = await readArtifacts(cwd);
788
1096
  const existing = artifacts.networks[network.name]?.contracts[contractName];
789
1097
  const contractConfig = options.config.contracts[contractName];
1098
+ if (options.verifyDeps && contractConfig.dependsOn.length > 0) {
1099
+ await verifyDependencyContracts({
1100
+ dependencies: contractConfig.dependsOn,
1101
+ artifacts,
1102
+ network,
1103
+ cwd,
1104
+ allowUntestedStellarCli: options.allowUntestedStellarCli
1105
+ });
1106
+ }
790
1107
  const resolvedDeployArgs = resolveDeployArgs({
791
1108
  deployArgs: contractConfig.deployArgs,
792
1109
  artifacts,
793
1110
  network: network.name
794
1111
  });
795
1112
  if (existing?.contractId && !options.force) {
796
- deployedContracts.push({ name: contractName, contractId: existing.contractId });
1113
+ skippedContracts.push({
1114
+ name: contractName,
1115
+ contractId: existing.contractId,
1116
+ network: network.name,
1117
+ reason: "already-deployed"
1118
+ });
797
1119
  continue;
798
1120
  }
799
1121
  const result = await deployContract({
@@ -804,20 +1126,38 @@ async function deployContractGraph(options) {
804
1126
  cwd,
805
1127
  allowUntestedStellarCli: options.allowUntestedStellarCli,
806
1128
  force: options.force,
1129
+ checkStaleWasm: options.checkStaleWasm,
807
1130
  resolvedDeployArgs,
808
1131
  dependencies: contractConfig.dependsOn
809
1132
  });
810
- deployedContracts.push({ name: contractName, contractId: result.contractId });
1133
+ if (result.staleWasmWarning) {
1134
+ staleWasmWarnings.push({
1135
+ contract: contractName,
1136
+ message: result.staleWasmWarning
1137
+ });
1138
+ }
1139
+ if (result.skipped) {
1140
+ skippedContracts.push({
1141
+ name: contractName,
1142
+ contractId: result.contractId,
1143
+ network: network.name,
1144
+ reason: "already-deployed"
1145
+ });
1146
+ } else {
1147
+ deployedContracts.push({ name: contractName, contractId: result.contractId });
1148
+ }
811
1149
  }
812
1150
  return {
813
1151
  network,
814
- deployedContracts
1152
+ deployedContracts,
1153
+ skippedContracts,
1154
+ staleWasmWarnings
815
1155
  };
816
1156
  }
817
1157
 
818
1158
  // src/contracts/generate-bindings.ts
819
1159
  var import_promises5 = require("fs/promises");
820
- var import_node_path6 = __toESM(require("path"), 1);
1160
+ var import_node_path7 = __toESM(require("path"), 1);
821
1161
  async function generateBindings(options) {
822
1162
  const cwd = options.cwd ?? process.cwd();
823
1163
  const network = resolveNetwork(options.config, options.networkName);
@@ -833,7 +1173,7 @@ async function generateBindings(options) {
833
1173
  await checkBinary("stellar", "Install Stellar CLI before running caatinga generate.", {
834
1174
  allowUntestedStellarCli: options.allowUntestedStellarCli
835
1175
  });
836
- const outputDir = import_node_path6.default.resolve(cwd, options.config.frontend.bindingsOutput, options.contractName);
1176
+ const outputDir = import_node_path7.default.resolve(cwd, options.config.frontend.bindingsOutput, options.contractName);
837
1177
  await (0, import_promises5.mkdir)(outputDir, { recursive: true });
838
1178
  const result = await runCommand("stellar", [
839
1179
  "contract",
@@ -844,10 +1184,7 @@ async function generateBindings(options) {
844
1184
  "--output-dir",
845
1185
  outputDir,
846
1186
  "--overwrite",
847
- "--rpc-url",
848
- network.config.rpcUrl,
849
- "--network-passphrase",
850
- network.config.networkPassphrase
1187
+ ...buildStellarNetworkArgs(network)
851
1188
  ], {
852
1189
  cwd,
853
1190
  allowUntestedStellarCli: options.allowUntestedStellarCli,
@@ -862,6 +1199,7 @@ async function generateBindings(options) {
862
1199
  }
863
1200
 
864
1201
  // src/contracts/invoke-contract.ts
1202
+ var INVOKE_SIGNING_FAILURE_REGEX = /xdr processing error: xdr value invalid/i;
865
1203
  function parseInvokeTarget(target) {
866
1204
  const [contractName, method, extra] = target.split(".");
867
1205
  if (!contractName || !method || extra) {
@@ -890,25 +1228,43 @@ async function invokeContract(options) {
890
1228
  await checkBinary("stellar", "Install Stellar CLI before running caatinga invoke.", {
891
1229
  allowUntestedStellarCli: options.allowUntestedStellarCli
892
1230
  });
893
- const result = await runCommand("stellar", [
894
- "contract",
895
- "invoke",
896
- "--id",
897
- contractArtifact.contractId,
898
- "--source-account",
899
- source,
900
- "--rpc-url",
901
- network.config.rpcUrl,
902
- "--network-passphrase",
903
- network.config.networkPassphrase,
904
- "--",
905
- target.method,
906
- ...options.args ?? []
907
- ], {
908
- cwd,
909
- allowUntestedStellarCli: options.allowUntestedStellarCli,
910
- failureCode: CaatingaErrorCode.INVOKE_FAILED
911
- });
1231
+ let result;
1232
+ try {
1233
+ result = await runCommand("stellar", [
1234
+ "contract",
1235
+ "invoke",
1236
+ "--id",
1237
+ contractArtifact.contractId,
1238
+ "--source-account",
1239
+ source,
1240
+ ...buildStellarNetworkArgs(network),
1241
+ "--",
1242
+ target.method,
1243
+ ...options.args ?? []
1244
+ ], {
1245
+ cwd,
1246
+ allowUntestedStellarCli: options.allowUntestedStellarCli,
1247
+ failureCode: CaatingaErrorCode.INVOKE_FAILED
1248
+ });
1249
+ } catch (error) {
1250
+ if (error instanceof CaatingaError && error.code === CaatingaErrorCode.INVOKE_FAILED && INVOKE_SIGNING_FAILURE_REGEX.test(`${error.message}
1251
+ ${error.hint ?? ""}`)) {
1252
+ throw new CaatingaError(
1253
+ error.message,
1254
+ error.code,
1255
+ [
1256
+ "Stellar CLI could not sign the invoke transaction (xdr value invalid).",
1257
+ "Stellar CLI 22.x has a known invoke signing bug; upgrade to 23.0.0 or newer (25.2.0 recommended).",
1258
+ " stellar --version",
1259
+ "Then retry with a funded identity, for example:",
1260
+ " stellar keys generate alice --fund --network testnet",
1261
+ " npx caatinga invoke counter.increment --network testnet --source alice"
1262
+ ].join("\n"),
1263
+ error
1264
+ );
1265
+ }
1266
+ throw error;
1267
+ }
912
1268
  return {
913
1269
  target,
914
1270
  network,
@@ -918,7 +1274,7 @@ async function invokeContract(options) {
918
1274
 
919
1275
  // src/templates/create-project-from-template.ts
920
1276
  var import_promises6 = require("fs/promises");
921
- var import_node_path7 = __toESM(require("path"), 1);
1277
+ var import_node_path8 = __toESM(require("path"), 1);
922
1278
  var import_zod6 = require("zod");
923
1279
 
924
1280
  // src/templates/template-manifest.schema.ts
@@ -946,14 +1302,50 @@ var TemplateManifestSchema = import_zod5.z.object({
946
1302
  artifacts: import_zod5.z.string().default("caatinga.artifacts.json")
947
1303
  })
948
1304
  });
1305
+ function defaultCompatibleCoreRange(coreVersion = CAATINGA_CORE_VERSION) {
1306
+ const version = import_semver2.default.valid(import_semver2.default.coerce(coreVersion));
1307
+ if (!version) {
1308
+ throw new Error(`Invalid core version: ${coreVersion}`);
1309
+ }
1310
+ return `^${version}`;
1311
+ }
949
1312
  function isCoreVersionCompatible(range, coreVersion = CAATINGA_CORE_VERSION) {
950
1313
  return import_semver2.default.satisfies(coreVersion, range);
951
1314
  }
1315
+ function getTemplateCompatibilityIssue(manifest, coreVersion = CAATINGA_CORE_VERSION) {
1316
+ if (manifest.caatinga.templateVersion !== CURRENT_TEMPLATE_VERSION) {
1317
+ return {
1318
+ kind: "template-version",
1319
+ expected: CURRENT_TEMPLATE_VERSION,
1320
+ actual: manifest.caatinga.templateVersion
1321
+ };
1322
+ }
1323
+ if (!isCoreVersionCompatible(manifest.caatinga.compatibleCore, coreVersion)) {
1324
+ return {
1325
+ kind: "core-range",
1326
+ requiredRange: manifest.caatinga.compatibleCore,
1327
+ runningVersion: coreVersion
1328
+ };
1329
+ }
1330
+ return null;
1331
+ }
1332
+ function formatTemplateCompatibilityMessage(issue) {
1333
+ if (issue.kind === "template-version") {
1334
+ return `Template manifest version ${issue.actual} is not supported; Caatinga requires templateVersion ${issue.expected}.`;
1335
+ }
1336
+ return `Template requires @caatinga/core ${issue.requiredRange} but running ${issue.runningVersion}.`;
1337
+ }
1338
+ function formatTemplateCompatibilityHint(issue) {
1339
+ if (issue.kind === "template-version") {
1340
+ return "Use a template built for this Caatinga release or upgrade Caatinga.";
1341
+ }
1342
+ return `Use a template with compatibleCore ${defaultCompatibleCoreRange(issue.runningVersion)} or install a matching Caatinga version.`;
1343
+ }
952
1344
 
953
1345
  // src/templates/create-project-from-template.ts
954
1346
  async function createProjectFromTemplate(options) {
955
- const targetDir = import_node_path7.default.resolve(options.targetDir);
956
- const templateDir = import_node_path7.default.resolve(options.templateDir);
1347
+ const targetDir = import_node_path8.default.resolve(options.targetDir);
1348
+ const templateDir = import_node_path8.default.resolve(options.templateDir);
957
1349
  try {
958
1350
  await (0, import_promises6.stat)(templateDir);
959
1351
  } catch {
@@ -975,22 +1367,16 @@ async function createProjectFromTemplate(options) {
975
1367
  return { targetDir, template: manifest };
976
1368
  }
977
1369
  async function readTemplateManifest(templateDir) {
978
- const manifestPath = import_node_path7.default.join(templateDir, "caatinga.template.json");
1370
+ const manifestPath = import_node_path8.default.join(templateDir, "caatinga.template.json");
979
1371
  try {
980
1372
  const rawManifest = await (0, import_promises6.readFile)(manifestPath, "utf8");
981
1373
  const manifest = TemplateManifestSchema.parse(JSON.parse(rawManifest));
982
- if (manifest.caatinga.templateVersion !== CURRENT_TEMPLATE_VERSION) {
983
- throw new CaatingaError(
984
- "Template is not compatible with this Caatinga version.",
985
- CaatingaErrorCode.TEMPLATE_INCOMPATIBLE,
986
- "Use a compatible template version or upgrade Caatinga."
987
- );
988
- }
989
- if (!isCoreVersionCompatible(manifest.caatinga.compatibleCore)) {
1374
+ const compatibilityIssue = getTemplateCompatibilityIssue(manifest);
1375
+ if (compatibilityIssue) {
990
1376
  throw new CaatingaError(
991
- "Template is not compatible with this Caatinga version.",
1377
+ formatTemplateCompatibilityMessage(compatibilityIssue),
992
1378
  CaatingaErrorCode.TEMPLATE_INCOMPATIBLE,
993
- "Use a compatible template version or upgrade Caatinga."
1379
+ formatTemplateCompatibilityHint(compatibilityIssue)
994
1380
  );
995
1381
  }
996
1382
  return manifest;
@@ -1018,7 +1404,7 @@ async function readTemplateManifest(templateDir) {
1018
1404
  async function replaceTemplateVariables(dir, projectName) {
1019
1405
  const entries = await (0, import_promises6.readdir)(dir);
1020
1406
  await Promise.all(entries.map(async (entry) => {
1021
- const entryPath = import_node_path7.default.join(dir, entry);
1407
+ const entryPath = import_node_path8.default.join(dir, entry);
1022
1408
  const entryStat = await (0, import_promises6.stat)(entryPath);
1023
1409
  if (entryStat.isDirectory()) {
1024
1410
  await replaceTemplateVariables(entryPath, projectName);
@@ -1041,7 +1427,7 @@ function isTextTemplateFile(filePath) {
1041
1427
  ".tsx",
1042
1428
  ".css",
1043
1429
  ".html"
1044
- ].includes(import_node_path7.default.extname(filePath));
1430
+ ].includes(import_node_path8.default.extname(filePath));
1045
1431
  }
1046
1432
 
1047
1433
  // src/ci/is-transient-testnet-smoke-failure.ts
@@ -1100,5 +1486,6 @@ function isTransientTestnetSmokeFailure(logText) {
1100
1486
  runCommand,
1101
1487
  toCaatingaError,
1102
1488
  updateArtifact,
1489
+ validateSourceShape,
1103
1490
  writeArtifacts
1104
1491
  });