@caatinga/core 0.2.1 → 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.1";
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,100 +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);
555
-
556
- // src/stellar-cli/recover-deploy-contract-id.ts
557
- var TX_HASH_REGEX = /Transaction hash is ([a-f0-9]{64})/i;
558
- var DEPLOY_SIGNING_FAILURE_REGEX = /xdr processing error: xdr value invalid/i;
559
- var HORIZON_URL_BY_PASSPHRASE = {
560
- "Test SDF Network ; September 2015": "https://horizon-testnet.stellar.org",
561
- "Public Global Stellar Network ; September 2015": "https://horizon.stellar.org"
562
- };
563
- function isLikelyPublicKeySource(source) {
564
- return /^G[A-Z2-7]{55}$/.test(source);
565
- }
566
- function decimalSaltToHex(salt) {
567
- return BigInt(salt).toString(16).padStart(64, "0");
568
- }
569
- function resolveHorizonUrl(network) {
570
- const horizonUrl = HORIZON_URL_BY_PASSPHRASE[network.networkPassphrase];
571
- if (!horizonUrl) {
572
- throw new CaatingaError(
573
- `No Horizon URL mapping for network passphrase "${network.networkPassphrase}".`,
574
- CaatingaErrorCode.NETWORK_NOT_FOUND,
575
- "Use testnet or mainnet, or extend Caatinga network metadata."
576
- );
577
- }
578
- return horizonUrl;
579
- }
580
- async function fetchCreateContractSalt(horizonUrl, transactionHash, fetchImpl = fetch) {
581
- const response = await fetchImpl(`${horizonUrl}/transactions/${transactionHash}/operations`);
582
- if (!response.ok) {
583
- return null;
584
- }
585
- const body = await response.json();
586
- const operation = body._embedded?.records?.find(
587
- (record) => record.transaction_successful === true && record.type === "invoke_host_function" && record.function === "HostFunctionTypeHostFunctionTypeCreateContract" && typeof record.salt === "string"
588
- );
589
- return operation?.salt ?? null;
590
- }
591
- async function resolveContractIdFromDeploySalt(options) {
592
- const saltHex = decimalSaltToHex(options.salt);
593
- const result = await runCommand("stellar", [
594
- "contract",
595
- "id",
596
- "wasm",
597
- "--salt",
598
- saltHex,
599
- "--source-account",
600
- options.source,
601
- "--rpc-url",
602
- options.network.rpcUrl,
603
- "--network-passphrase",
604
- options.network.networkPassphrase
605
- ], {
606
- cwd: options.cwd,
607
- allowUntestedStellarCli: options.allowUntestedStellarCli,
608
- skipStellarVersionCheck: true
609
- });
610
- return parseContractId(result.all || `${result.stdout}
611
- ${result.stderr}`);
612
- }
613
- async function tryRecoverContractIdFromDeployFailure(options) {
614
- if (!DEPLOY_SIGNING_FAILURE_REGEX.test(options.output)) {
615
- return null;
616
- }
617
- const hashMatch = options.output.match(TX_HASH_REGEX);
618
- if (!hashMatch) {
619
- return null;
620
- }
621
- const horizonUrl = resolveHorizonUrl(options.network);
622
- const salt = await fetchCreateContractSalt(horizonUrl, hashMatch[1], options.fetchImpl);
623
- if (!salt) {
624
- return null;
625
- }
626
- return resolveContractIdFromDeploySalt({
627
- salt,
628
- source: options.source,
629
- network: options.network,
630
- cwd: options.cwd,
631
- allowUntestedStellarCli: options.allowUntestedStellarCli
632
- });
633
- }
778
+ var import_node_path6 = __toESM(require("path"), 1);
634
779
 
635
780
  // src/contracts/dependency-graph.ts
636
781
  function buildDependencyGraph(contracts) {
@@ -681,19 +826,9 @@ function assertSafeSourceAccount(source) {
681
826
  "Pass a Stellar CLI identity alias, for example: --source alice"
682
827
  );
683
828
  }
684
- if (source.startsWith("S") || source.trim().includes(" ")) {
685
- throw new CaatingaError(
686
- "Refusing to accept a likely secret key or seed phrase as --source.",
687
- CaatingaErrorCode.UNSAFE_SOURCE_ACCOUNT,
688
- "Use a Stellar CLI identity alias instead, for example: --source alice"
689
- );
690
- }
691
- if (isLikelyPublicKeySource(source)) {
692
- throw new CaatingaError(
693
- `Public account address cannot sign transactions: ${source}`,
694
- CaatingaErrorCode.UNSAFE_SOURCE_ACCOUNT,
695
- "Use a Stellar CLI identity with a secret key. Example: stellar keys generate alice --fund --network testnet, then --source alice"
696
- );
829
+ const unsafeSource = validateSourceShape(source);
830
+ if (unsafeSource) {
831
+ throw unsafeSource;
697
832
  }
698
833
  return source;
699
834
  }
@@ -721,17 +856,32 @@ async function deployContract(options) {
721
856
  await checkBinary("stellar", "Install Stellar CLI before running caatinga deploy.", {
722
857
  allowUntestedStellarCli: options.allowUntestedStellarCli
723
858
  });
724
- 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
+ }
725
874
  const artifactsBefore = await readArtifacts(cwd);
726
875
  const existing = artifactsBefore.networks[network.name]?.contracts[contract.name];
727
876
  if (existing?.contractId && !options.force) {
728
877
  return {
729
- contract,
878
+ contract: contractWithWasm,
730
879
  network,
731
880
  contractId: existing.contractId,
732
- artifactsPath: import_node_path5.default.resolve(cwd, "caatinga.artifacts.json"),
881
+ artifactsPath: import_node_path6.default.resolve(cwd, "caatinga.artifacts.json"),
733
882
  output: "",
734
- skipped: true
883
+ skipped: true,
884
+ staleWasmWarning
735
885
  };
736
886
  }
737
887
  const rawDeployArgs = contract.config.deployArgs;
@@ -762,13 +912,10 @@ async function deployContract(options) {
762
912
  "contract",
763
913
  "deploy",
764
914
  "--wasm",
765
- contract.wasmPath,
915
+ wasmPath,
766
916
  "--source-account",
767
917
  source,
768
- "--rpc-url",
769
- network.config.rpcUrl,
770
- "--network-passphrase",
771
- network.config.networkPassphrase,
918
+ ...buildStellarNetworkArgs(network),
772
919
  ...constructorArgs
773
920
  ];
774
921
  let output = "";
@@ -804,7 +951,7 @@ ${error.hint ?? ""}`,
804
951
  `Contract ID: ${contractId}`
805
952
  ].filter(Boolean).join("\n");
806
953
  }
807
- const wasmHash = await hashWasm(contract.wasmPath);
954
+ const wasmHash = await hashWasm(wasmPath);
808
955
  const dependencyGraph = buildDependencyGraph(options.config.contracts);
809
956
  const dependencies = options.dependencies ?? contract.config.dependsOn;
810
957
  const nextArtifacts = updateArtifact(
@@ -824,12 +971,13 @@ ${error.hint ?? ""}`,
824
971
  );
825
972
  const artifactsPath = await writeArtifacts(nextArtifacts, cwd);
826
973
  return {
827
- contract,
974
+ contract: contractWithWasm,
828
975
  network,
829
976
  contractId,
830
977
  artifactsPath,
831
978
  output,
832
- skipped: false
979
+ skipped: false,
980
+ staleWasmWarning
833
981
  };
834
982
  }
835
983
 
@@ -884,6 +1032,53 @@ function resolveDeployOrder(input) {
884
1032
  }
885
1033
  }
886
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
+
887
1082
  // src/contracts/deploy-contract-graph.ts
888
1083
  async function deployContractGraph(options) {
889
1084
  const cwd = options.cwd ?? process.cwd();
@@ -894,17 +1089,33 @@ async function deployContractGraph(options) {
894
1089
  includeDependencies: options.includeDependencies
895
1090
  });
896
1091
  const deployedContracts = [];
1092
+ const skippedContracts = [];
1093
+ const staleWasmWarnings = [];
897
1094
  for (const contractName of order) {
898
1095
  const artifacts = await readArtifacts(cwd);
899
1096
  const existing = artifacts.networks[network.name]?.contracts[contractName];
900
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
+ }
901
1107
  const resolvedDeployArgs = resolveDeployArgs({
902
1108
  deployArgs: contractConfig.deployArgs,
903
1109
  artifacts,
904
1110
  network: network.name
905
1111
  });
906
1112
  if (existing?.contractId && !options.force) {
907
- 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
+ });
908
1119
  continue;
909
1120
  }
910
1121
  const result = await deployContract({
@@ -915,20 +1126,38 @@ async function deployContractGraph(options) {
915
1126
  cwd,
916
1127
  allowUntestedStellarCli: options.allowUntestedStellarCli,
917
1128
  force: options.force,
1129
+ checkStaleWasm: options.checkStaleWasm,
918
1130
  resolvedDeployArgs,
919
1131
  dependencies: contractConfig.dependsOn
920
1132
  });
921
- 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
+ }
922
1149
  }
923
1150
  return {
924
1151
  network,
925
- deployedContracts
1152
+ deployedContracts,
1153
+ skippedContracts,
1154
+ staleWasmWarnings
926
1155
  };
927
1156
  }
928
1157
 
929
1158
  // src/contracts/generate-bindings.ts
930
1159
  var import_promises5 = require("fs/promises");
931
- var import_node_path6 = __toESM(require("path"), 1);
1160
+ var import_node_path7 = __toESM(require("path"), 1);
932
1161
  async function generateBindings(options) {
933
1162
  const cwd = options.cwd ?? process.cwd();
934
1163
  const network = resolveNetwork(options.config, options.networkName);
@@ -944,7 +1173,7 @@ async function generateBindings(options) {
944
1173
  await checkBinary("stellar", "Install Stellar CLI before running caatinga generate.", {
945
1174
  allowUntestedStellarCli: options.allowUntestedStellarCli
946
1175
  });
947
- 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);
948
1177
  await (0, import_promises5.mkdir)(outputDir, { recursive: true });
949
1178
  const result = await runCommand("stellar", [
950
1179
  "contract",
@@ -955,10 +1184,7 @@ async function generateBindings(options) {
955
1184
  "--output-dir",
956
1185
  outputDir,
957
1186
  "--overwrite",
958
- "--rpc-url",
959
- network.config.rpcUrl,
960
- "--network-passphrase",
961
- network.config.networkPassphrase
1187
+ ...buildStellarNetworkArgs(network)
962
1188
  ], {
963
1189
  cwd,
964
1190
  allowUntestedStellarCli: options.allowUntestedStellarCli,
@@ -973,6 +1199,7 @@ async function generateBindings(options) {
973
1199
  }
974
1200
 
975
1201
  // src/contracts/invoke-contract.ts
1202
+ var INVOKE_SIGNING_FAILURE_REGEX = /xdr processing error: xdr value invalid/i;
976
1203
  function parseInvokeTarget(target) {
977
1204
  const [contractName, method, extra] = target.split(".");
978
1205
  if (!contractName || !method || extra) {
@@ -1001,25 +1228,43 @@ async function invokeContract(options) {
1001
1228
  await checkBinary("stellar", "Install Stellar CLI before running caatinga invoke.", {
1002
1229
  allowUntestedStellarCli: options.allowUntestedStellarCli
1003
1230
  });
1004
- const result = await runCommand("stellar", [
1005
- "contract",
1006
- "invoke",
1007
- "--id",
1008
- contractArtifact.contractId,
1009
- "--source-account",
1010
- source,
1011
- "--rpc-url",
1012
- network.config.rpcUrl,
1013
- "--network-passphrase",
1014
- network.config.networkPassphrase,
1015
- "--",
1016
- target.method,
1017
- ...options.args ?? []
1018
- ], {
1019
- cwd,
1020
- allowUntestedStellarCli: options.allowUntestedStellarCli,
1021
- failureCode: CaatingaErrorCode.INVOKE_FAILED
1022
- });
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
+ }
1023
1268
  return {
1024
1269
  target,
1025
1270
  network,
@@ -1029,7 +1274,7 @@ async function invokeContract(options) {
1029
1274
 
1030
1275
  // src/templates/create-project-from-template.ts
1031
1276
  var import_promises6 = require("fs/promises");
1032
- var import_node_path7 = __toESM(require("path"), 1);
1277
+ var import_node_path8 = __toESM(require("path"), 1);
1033
1278
  var import_zod6 = require("zod");
1034
1279
 
1035
1280
  // src/templates/template-manifest.schema.ts
@@ -1099,8 +1344,8 @@ function formatTemplateCompatibilityHint(issue) {
1099
1344
 
1100
1345
  // src/templates/create-project-from-template.ts
1101
1346
  async function createProjectFromTemplate(options) {
1102
- const targetDir = import_node_path7.default.resolve(options.targetDir);
1103
- 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);
1104
1349
  try {
1105
1350
  await (0, import_promises6.stat)(templateDir);
1106
1351
  } catch {
@@ -1122,7 +1367,7 @@ async function createProjectFromTemplate(options) {
1122
1367
  return { targetDir, template: manifest };
1123
1368
  }
1124
1369
  async function readTemplateManifest(templateDir) {
1125
- const manifestPath = import_node_path7.default.join(templateDir, "caatinga.template.json");
1370
+ const manifestPath = import_node_path8.default.join(templateDir, "caatinga.template.json");
1126
1371
  try {
1127
1372
  const rawManifest = await (0, import_promises6.readFile)(manifestPath, "utf8");
1128
1373
  const manifest = TemplateManifestSchema.parse(JSON.parse(rawManifest));
@@ -1159,7 +1404,7 @@ async function readTemplateManifest(templateDir) {
1159
1404
  async function replaceTemplateVariables(dir, projectName) {
1160
1405
  const entries = await (0, import_promises6.readdir)(dir);
1161
1406
  await Promise.all(entries.map(async (entry) => {
1162
- const entryPath = import_node_path7.default.join(dir, entry);
1407
+ const entryPath = import_node_path8.default.join(dir, entry);
1163
1408
  const entryStat = await (0, import_promises6.stat)(entryPath);
1164
1409
  if (entryStat.isDirectory()) {
1165
1410
  await replaceTemplateVariables(entryPath, projectName);
@@ -1182,7 +1427,7 @@ function isTextTemplateFile(filePath) {
1182
1427
  ".tsx",
1183
1428
  ".css",
1184
1429
  ".html"
1185
- ].includes(import_node_path7.default.extname(filePath));
1430
+ ].includes(import_node_path8.default.extname(filePath));
1186
1431
  }
1187
1432
 
1188
1433
  // src/ci/is-transient-testnet-smoke-failure.ts
@@ -1241,5 +1486,6 @@ function isTransientTestnetSmokeFailure(logText) {
1241
1486
  runCommand,
1242
1487
  toCaatingaError,
1243
1488
  updateArtifact,
1489
+ validateSourceShape,
1244
1490
  writeArtifacts
1245
1491
  });