Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ error-code table because they are not errors.
| Code | Meaning | Common cause | User action | CI/release action | Versioning note |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `CAATINGA_COMMAND_FAILED` | An underlying command failed. | Stellar CLI, Cargo, or another tool returned non-zero. | Re-run the printed command directly for full diagnostics. | Fail CI and inspect the underlying command output. | Public code; adding a new code is minor, removal/rename/meaning change is major. |
| `CAATINGA_COMMAND_TIMEOUT` | A subprocess exceeded its timeout and was killed. | A network-facing or transaction command (npm view, npx generate, stellar deploy/invoke) exceeded its budget — stalled registry, wedged network, or hung process. | Check network and registry availability, then retry; the hint reports the limit that was exceeded. | Fail CI; usually a flaky registry or RPC — retry or investigate connectivity. | Public code; adding a new code is minor, removal/rename/meaning change is major. |
| `CAATINGA_UNEXPECTED_ERROR` | An unexpected non-Caatinga error was normalized. | A dependency or internal path threw an unknown error. | Re-run with the latest output and report the underlying message. | Fail CI and capture logs for triage. | Public code; adding a new code is minor, removal/rename/meaning change is major. |
| `CAATINGA_ROLLBACK_TARGET_NOT_FOUND` | Requested contract ID is not in artifact history. | `ctg rollback --to` target was never recorded in `history`. | Run `ctg inspect` or redeploy; use `ctg migrate artifacts` if on schema v1. | Fail automation when rollback target is invalid. | Public code; adding a new code is minor, removal/rename/meaning change is major. |
| `CAATINGA_ESTIMATE_FAILED` | Deploy fee estimate could not be built or simulated. | WASM missing, deploy args unresolved, or Stellar CLI simulate failed. | Run `ctg build`, fix deploy args, then retry `ctg estimate deploy`. | Advisory in CI; do not block deploy pipelines on estimate alone. | Public code; adding a new code is minor, removal/rename/meaning change is major. |
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/contracts/deploy-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
isWasmOlderThanSources,
resolveWasmArtifactPath,
} from "./wasm.js";
import { TRANSACTION_TIMEOUT_MS } from "../shell/command-timeouts.js";

export type DeployContractOptions = {
config: CaatingaConfig;
Expand Down Expand Up @@ -161,6 +162,7 @@ export async function deployContract(options: DeployContractOptions) {
const result = await runCommand("stellar", stellarArgs, {
cwd,
failureCode: CaatingaErrorCode.DEPLOY_FAILED,
timeout: TRANSACTION_TIMEOUT_MS,
});
const output = result.all || `${result.stdout}\n${result.stderr}`;
deployOutcome = {
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/contracts/estimate-deploy-cost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { resolveDeployArgs, type DeployArgValue } from "./resolve-deploy-args.js
import { assertSafeSourceAccount } from "./source-account.js";
import { resolveContract } from "./resolve-contract.js";
import { resolveWasmArtifactPath } from "./wasm.js";
import { TRANSACTION_TIMEOUT_MS } from "../shell/command-timeouts.js";

export type DeployCostEstimate = {
contractName: string;
Expand Down Expand Up @@ -95,6 +96,7 @@ export async function estimateDeployCost(
const buildResult = await runCommand("stellar", deployArgs, {
cwd,
failureCode: CaatingaErrorCode.ESTIMATE_FAILED,
timeout: TRANSACTION_TIMEOUT_MS,
});
buildOutput = (buildResult.stdout || buildResult.all).trim();
} catch (error) {
Expand All @@ -117,6 +119,7 @@ export async function estimateDeployCost(
const simulateResult = await runCommand("stellar", simulateArgs, {
cwd,
failureCode: CaatingaErrorCode.ESTIMATE_FAILED,
timeout: TRANSACTION_TIMEOUT_MS,
});
simulateOutput = simulateResult.all || `${simulateResult.stdout}\n${simulateResult.stderr}`;
} catch (error) {
Expand Down Expand Up @@ -148,10 +151,9 @@ export async function estimateDeployCost(
resourceFeeStroops,
totalFeeStroops,
simulation,
advisory:
simulation.ok
? "Advisory estimate only — actual fees may differ under network congestion or contract complexity."
: "Fee estimate unavailable — simulation did not produce a parseable inclusion fee.",
advisory: simulation.ok
? "Advisory estimate only — actual fees may differ under network congestion or contract complexity."
: "Fee estimate unavailable — simulation did not produce a parseable inclusion fee.",
rawOutput,
};
}
7 changes: 6 additions & 1 deletion packages/core/src/contracts/generate-bindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ vi.mock("../stellar-sdk/check-stellar-sdk-version.js", () => ({

import { generateBindings } from "./generate-bindings.js";
import { readBindingMarker } from "../bindings/binding-marker.js";
import { BINDINGS_TIMEOUT_MS } from "../shell/command-timeouts.js";

const CONTRACT_ID = `C${"2".repeat(55)}`;

Expand Down Expand Up @@ -154,7 +155,11 @@ describe("generateBindings", () => {
"--network",
"testnet",
]),
{ cwd: tmpDir, failureCode: CaatingaErrorCode.BINDINGS_FAILED }
{
cwd: tmpDir,
failureCode: CaatingaErrorCode.BINDINGS_FAILED,
timeout: BINDINGS_TIMEOUT_MS,
}
);
});

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/contracts/generate-bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { resolveNetwork } from "../networks/resolve-network.js";
import { runCommand } from "../shell/run-command.js";
import { checkStellarSdkVersion } from "../stellar-sdk/check-stellar-sdk-version.js";
import { buildGenerateNetworkArgs } from "./build-generate-network-args.js";
import { BINDINGS_TIMEOUT_MS } from "../shell/command-timeouts.js";

export type GenerateBindingsOptions = {
config: CaatingaConfig;
Expand Down Expand Up @@ -85,6 +86,7 @@ export async function generateBindings(options: GenerateBindingsOptions) {
{
cwd,
failureCode: CaatingaErrorCode.BINDINGS_FAILED,
timeout: BINDINGS_TIMEOUT_MS,
}
);

Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/contracts/invoke-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ vi.mock("../shell/run-command.js", () => ({
}));

import { invokeContract, parseInvokeTarget } from "./invoke-contract.js";
import { TRANSACTION_TIMEOUT_MS } from "../shell/command-timeouts.js";

const CONTRACT_ID = `C${"3".repeat(55)}`;

Expand Down Expand Up @@ -119,7 +120,11 @@ describe("invokeContract", () => {
"--arg1",
"x",
]),
{ cwd: tmpDir, failureCode: CaatingaErrorCode.INVOKE_FAILED }
{
cwd: tmpDir,
failureCode: CaatingaErrorCode.INVOKE_FAILED,
timeout: TRANSACTION_TIMEOUT_MS,
}
);
});

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/contracts/invoke-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { buildStellarNetworkArgs } from "../stellar-cli/build-stellar-network-ar
import { assertSafeSourceAccount } from "./source-account.js";
import { buildReadCallHint, isReadCallFailure, parseInvokeTarget } from "./invoke-target.js";
import { resolveCliMethodArgs } from "./resolve-method-args.js";
import { TRANSACTION_TIMEOUT_MS } from "../shell/command-timeouts.js";

const INVOKE_SIGNING_FAILURE_REGEX = /xdr processing error: xdr value invalid/i;

Expand Down Expand Up @@ -66,6 +67,7 @@ export async function invokeContract(options: InvokeContractOptions) {
{
cwd,
failureCode: CaatingaErrorCode.INVOKE_FAILED,
timeout: TRANSACTION_TIMEOUT_MS,
}
);
} catch (error) {
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/contracts/read-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ vi.mock("../shell/run-command.js", () => ({
}));

import { readContract } from "./read-contract.js";
import { TRANSACTION_TIMEOUT_MS } from "../shell/command-timeouts.js";

const CONTRACT_ID = `C${"4".repeat(55)}`;

Expand Down Expand Up @@ -91,7 +92,11 @@ describe("readContract", () => {
"--",
"version",
]),
{ cwd: tmpDir, failureCode: CaatingaErrorCode.INVOKE_FAILED }
{
cwd: tmpDir,
failureCode: CaatingaErrorCode.INVOKE_FAILED,
timeout: TRANSACTION_TIMEOUT_MS,
}
);
});

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/contracts/read-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { parseInvokeTarget } from "./invoke-target.js";
import { resolveDeployArgs } from "./resolve-deploy-args.js";
import { resolveCliMethodArgs, resolveMethodArgs } from "./resolve-method-args.js";
import { resolveCliSource } from "./source-account.js";
import { TRANSACTION_TIMEOUT_MS } from "../shell/command-timeouts.js";

export { buildReadCallHint, isReadCallFailure, READ_CALL_FAILURE_REGEX } from "./invoke-target.js";

Expand Down Expand Up @@ -76,6 +77,7 @@ export async function readContract(options: ReadContractOptions) {
const result = await runCommand("stellar", stellarArgs, {
cwd,
failureCode: CaatingaErrorCode.INVOKE_FAILED,
timeout: TRANSACTION_TIMEOUT_MS,
});

return {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/contracts/resolve-source-address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CaatingaError, CaatingaErrorCode } from "../errors/CaatingaError.js";
import { checkBinary } from "../shell/check-binary.js";
import { runCommand } from "../shell/run-command.js";
import { assertSafeSourceAccount } from "./source-account.js";
import { VERSION_PROBE_TIMEOUT_MS } from "../shell/command-timeouts.js";

const STELLAR_ADDRESS_REGEX = /^G[A-Z2-7]{55}$/;

Expand All @@ -19,6 +20,7 @@ export async function resolveSourceAddress(options: {
result = await runCommand("stellar", ["keys", "address", source], {
cwd,
failureCode: CaatingaErrorCode.SOURCE_ADDRESS_UNRESOLVED,
timeout: VERSION_PROBE_TIMEOUT_MS,
});
} catch (error) {
if (error instanceof CaatingaError) {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/contracts/run-post-deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { assertSafeSourceAccount } from "./source-account.js";
import { assertExpect } from "./verify-expect.js";
import { resolvePlaceholders } from "./placeholder-engine.js";
import { resolveSourceAddress } from "./resolve-source-address.js";
import { TRANSACTION_TIMEOUT_MS } from "../shell/command-timeouts.js";

export type RunPostDeployHooksOptions = {
config: CaatingaConfig;
Expand Down Expand Up @@ -206,6 +207,7 @@ export async function runPostDeployHooks(
{
cwd,
failureCode: CaatingaErrorCode.INVOKE_FAILED,
timeout: TRANSACTION_TIMEOUT_MS,
}
);
break;
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/contracts/upgrade-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { assertSafeSourceAccount } from "./source-account.js";
import { resolveContract } from "./resolve-contract.js";
import { uploadWasm } from "./upload-wasm.js";
import { hashWasm, resolveWasmArtifactPath } from "./wasm.js";
import { TRANSACTION_TIMEOUT_MS } from "../shell/command-timeouts.js";

export type UpgradeContractOptions = {
config: CaatingaConfig;
Expand Down Expand Up @@ -137,6 +138,7 @@ export async function upgradeContractInPlace(
{
cwd,
failureCode: CaatingaErrorCode.INVOKE_FAILED,
timeout: TRANSACTION_TIMEOUT_MS,
}
);
break;
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/contracts/upload-wasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { buildStellarNetworkArgs } from "../stellar-cli/build-stellar-network-ar
import { parseWasmHash } from "../stellar-cli/parse-wasm-hash.js";
import { assertSafeSourceAccount } from "./source-account.js";
import { hashWasm } from "./wasm.js";
import { TRANSACTION_TIMEOUT_MS } from "../shell/command-timeouts.js";

export type UploadWasmOptions = {
wasmPath: string;
Expand Down Expand Up @@ -49,6 +50,7 @@ export async function uploadWasm(options: UploadWasmOptions): Promise<UploadWasm
{
cwd,
failureCode: CaatingaErrorCode.UPLOAD_FAILED,
timeout: TRANSACTION_TIMEOUT_MS,
}
);

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/contracts/verify-dependency-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CaatingaError, CaatingaErrorCode } from "../errors/CaatingaError.js";
import type { ResolvedNetwork } from "../networks/resolve-network.js";
import { runCommand } from "../shell/run-command.js";
import { buildStellarNetworkArgs } from "../stellar-cli/build-stellar-network-args.js";
import { TRANSACTION_TIMEOUT_MS } from "../shell/command-timeouts.js";

export async function verifyDependencyContract(options: {
dependencyName: string;
Expand All @@ -24,6 +25,7 @@ export async function verifyDependencyContract(options: {
{
cwd: options.cwd,
failureCode: CaatingaErrorCode.DEPENDENCY_CONTRACT_NOT_FOUND,
timeout: TRANSACTION_TIMEOUT_MS,
}
);
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/errors/CaatingaErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const CaatingaErrorCode = {
DEPENDENCIES_NOT_INSTALLED: "CAATINGA_DEPENDENCIES_NOT_INSTALLED",
INVALID_CONFIG: "CAATINGA_INVALID_CONFIG",
COMMAND_FAILED: "CAATINGA_COMMAND_FAILED",
COMMAND_TIMEOUT: "CAATINGA_COMMAND_TIMEOUT",
UNEXPECTED_ERROR: "CAATINGA_UNEXPECTED_ERROR",
STELLAR_CLI_NOT_FOUND: "CAATINGA_STELLAR_CLI_NOT_FOUND",
STELLAR_CLI_VERSION_PARSE_FAILED: "CAATINGA_STELLAR_CLI_VERSION_PARSE_FAILED",
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/errors/error-surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ const productionTriggerTests: Record<CaatingaErrorCodeValue, { file: string; tri
file: "packages/core/src/shell/run-command.test.ts",
trigger: "runCommand(",
},
[CaatingaErrorCode.COMMAND_TIMEOUT]: {
file: "packages/core/src/shell/run-command.test.ts",
trigger: "runCommand(",
},
[CaatingaErrorCode.UNEXPECTED_ERROR]: {
file: "packages/core/src/errors/to-caatinga-error.test.ts",
trigger: "toCaatingaError(",
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ export { WELL_KNOWN_NETWORKS } from "./networks/networks.js";
export { resolveNetwork, type ResolvedNetwork } from "./networks/resolve-network.js";

export { runCommand, type RunCommandResult } from "./shell/run-command.js";
export {
VERSION_PROBE_TIMEOUT_MS,
REGISTRY_TIMEOUT_MS,
BINDINGS_TIMEOUT_MS,
TRANSACTION_TIMEOUT_MS,
} from "./shell/command-timeouts.js";
export { resolveSubprocessEnv, isCargoBinMissingFromPath } from "./shell/resolve-subprocess-env.js";
export { checkBinary } from "./shell/check-binary.js";
export { parseContractId } from "./stellar-cli/parse-contract-id.js";
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/shell/check-binary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ vi.mock("./run-command.js", () => ({
}));

import { checkBinary } from "./check-binary.js";
import { VERSION_PROBE_TIMEOUT_MS } from "./command-timeouts.js";

describe("checkBinary", () => {
it("skips the Stellar version gate because the real command validates it", async () => {
Expand All @@ -16,6 +17,7 @@ describe("checkBinary", () => {
await checkBinary("stellar", "hint");

expect(runCommand).toHaveBeenCalledWith("stellar", ["--version"], {
timeout: VERSION_PROBE_TIMEOUT_MS,
skipStellarVersionCheck: true,
});
});
Expand All @@ -27,6 +29,8 @@ describe("checkBinary", () => {
code: CaatingaErrorCode.RUST_NOT_FOUND,
});

expect(runCommand).toHaveBeenCalledWith("rustc", ["--version"], {});
expect(runCommand).toHaveBeenCalledWith("rustc", ["--version"], {
timeout: VERSION_PROBE_TIMEOUT_MS,
});
});
});
2 changes: 2 additions & 0 deletions packages/core/src/shell/check-binary.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CaatingaError, CaatingaErrorCode } from "../errors/CaatingaError.js";
import { runCommand } from "./run-command.js";
import { VERSION_PROBE_TIMEOUT_MS } from "./command-timeouts.js";

type CheckBinaryOptions = {
skipStellarVersionCheck?: boolean;
Expand All @@ -12,6 +13,7 @@ export async function checkBinary(
): Promise<void> {
try {
await runCommand(binary, ["--version"], {
timeout: VERSION_PROBE_TIMEOUT_MS,
...options,
...(binary === "stellar" ? { skipStellarVersionCheck: true } : {}),
});
Expand Down
31 changes: 31 additions & 0 deletions packages/core/src/shell/command-timeouts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Timeout budgets for subprocess and network-facing calls (#145).
*
* These are opt-in per call site rather than a single global default. Caatinga
* drives commands whose legitimate runtime spans four orders of magnitude —
* `stellar --version` returns in milliseconds, while a ZK powers-of-tau
* ceremony or a cold `cargo build` can legitimately run for many minutes. A
* global default tight enough to catch a hang would kill real work; one loose
* enough to be safe would not catch anything.
*
* Call sites left deliberately untimed: contract builds (`stellar contract
* build`, cold Cargo compiles) and every ZK circuit/ceremony command in
* `@caatinga/zk`. Bounding those needs a per-project budget, not a constant.
*/

/** Version and capability probes. These return immediately or not at all. */
export const VERSION_PROBE_TIMEOUT_MS = 30_000;

/** Registry metadata lookups (`npm view`). A wedged registry must not hang the CLI. */
export const REGISTRY_TIMEOUT_MS = 60_000;

/** `npx --yes @stellar/stellar-sdk generate` — downloads a package, then generates. */
export const BINDINGS_TIMEOUT_MS = 120_000;

/**
* Signed and simulated transactions driven through the Stellar CLI: deploy,
* upgrade, upload, invoke, read, simulate. Generous, since these wait on ledger
* close and RPC, but bounded — an irreversible mainnet operation must not hang
* forever with no output and no recourse.
*/
export const TRANSACTION_TIMEOUT_MS = 300_000;
Loading
Loading