Skip to content
Open
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
105 changes: 105 additions & 0 deletions packages/core/src/artifacts/artifact.schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { describe, expect, it } from "vitest";
import {
ContractArtifactHistoryEntrySchema,
ContractArtifactSchema,
} from "./artifact.schema.js";

/** A well-formed contract strkey: `C` + 55 base32 (A-Z2-7) characters. */
const VALID_CONTRACT_ID = `C${"A".repeat(55)}`;
/** A well-formed wasm hash: 64 lowercase hex characters. */
const VALID_WASM_HASH = "a".repeat(64);

function baseArtifact(overrides: Record<string, unknown> = {}) {
return {
contractId: VALID_CONTRACT_ID,
wasmHash: VALID_WASM_HASH,
deployedAt: "2026-05-12T00:00:00.000Z",
sourcePath: "./contracts/token",
wasmPath: "./contracts/token.wasm",
...overrides,
};
}

function baseHistoryEntry(overrides: Record<string, unknown> = {}) {
return {
contractId: VALID_CONTRACT_ID,
wasmHash: VALID_WASM_HASH,
deployedAt: "2026-05-12T00:00:00.000Z",
supersededAt: "2026-05-13T00:00:00.000Z",
...overrides,
};
}

describe("ContractArtifactSchema contractId/wasmHash validation", () => {
it("accepts a well-formed contract strkey and lowercase-hex wasm hash", () => {
const parsed = ContractArtifactSchema.parse(baseArtifact());
expect(parsed.contractId).toBe(VALID_CONTRACT_ID);
expect(parsed.wasmHash).toBe(VALID_WASM_HASH);
});

it("accepts a realistic base32 contract strkey", () => {
const parsed = ContractArtifactSchema.parse(
baseArtifact({ contractId: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" })
);
expect(parsed.contractId).toBe("CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM");
});

it("rejects a contractId with the wrong prefix (G instead of C)", () => {
expect(() => ContractArtifactSchema.parse(baseArtifact({ contractId: `G${"A".repeat(55)}` }))).toThrow();
});

it("rejects a contractId of the wrong length", () => {
expect(() => ContractArtifactSchema.parse(baseArtifact({ contractId: `C${"A".repeat(54)}` }))).toThrow();
expect(() => ContractArtifactSchema.parse(baseArtifact({ contractId: "C123" }))).toThrow();
});

it("rejects a lowercase contractId", () => {
expect(() =>
ContractArtifactSchema.parse(baseArtifact({ contractId: `C${"a".repeat(55)}` }))
).toThrow();
});

it("rejects a contractId containing base32-invalid digits 0/1/8/9", () => {
for (const digit of ["0", "1", "8", "9"]) {
const bad = `C${digit}${"A".repeat(54)}`;
expect(() => ContractArtifactSchema.parse(baseArtifact({ contractId: bad }))).toThrow();
}
});

it("rejects an uppercase wasmHash", () => {
expect(() =>
ContractArtifactSchema.parse(baseArtifact({ wasmHash: "A".repeat(64) }))
).toThrow();
});

it("rejects a wasmHash of the wrong length", () => {
expect(() => ContractArtifactSchema.parse(baseArtifact({ wasmHash: "a".repeat(63) }))).toThrow();
expect(() => ContractArtifactSchema.parse(baseArtifact({ wasmHash: "abc" }))).toThrow();
});

it("rejects a non-hex wasmHash", () => {
expect(() =>
ContractArtifactSchema.parse(baseArtifact({ wasmHash: `g${"a".repeat(63)}` }))
).toThrow();
});
});

describe("ContractArtifactHistoryEntrySchema contractId/wasmHash validation", () => {
it("accepts a well-formed history entry", () => {
const parsed = ContractArtifactHistoryEntrySchema.parse(baseHistoryEntry());
expect(parsed.contractId).toBe(VALID_CONTRACT_ID);
expect(parsed.wasmHash).toBe(VALID_WASM_HASH);
});

it("rejects a malformed contractId in the history entry", () => {
expect(() =>
ContractArtifactHistoryEntrySchema.parse(baseHistoryEntry({ contractId: "C123" }))
).toThrow();
});

it("rejects a malformed wasmHash in the history entry", () => {
expect(() =>
ContractArtifactHistoryEntrySchema.parse(baseHistoryEntry({ wasmHash: "A".repeat(64) }))
).toThrow();
});
});
13 changes: 9 additions & 4 deletions packages/core/src/artifacts/artifact.schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { z } from "zod";
import { CONTRACT_ID_REGEX, WASM_HASH_REGEX } from "../stellar-cli/strkey.js";

const CONTRACT_ID_MESSAGE =
"contractId must be a Stellar contract strkey: a `C` prefix followed by 55 base32 characters (A-Z2-7).";
const WASM_HASH_MESSAGE = "wasmHash must be 64 lowercase hex characters.";

export const ArtifactSupersedeReasonSchema = z.enum(["upgrade", "rollback", "force-redeploy"]);

Expand All @@ -24,8 +29,8 @@ export const ContractMetadataSchema = z.object({
export type ContractMetadata = z.infer<typeof ContractMetadataSchema>;

export const ContractArtifactHistoryEntrySchema = z.object({
contractId: z.string().min(1),
wasmHash: z.string().min(1),
contractId: z.string().regex(CONTRACT_ID_REGEX, CONTRACT_ID_MESSAGE),
wasmHash: z.string().regex(WASM_HASH_REGEX, WASM_HASH_MESSAGE),
deployedAt: z.string().datetime(),
supersededAt: z.string().datetime(),
reason: ArtifactSupersedeReasonSchema.optional(),
Expand All @@ -36,8 +41,8 @@ export const ContractArtifactHistoryEntrySchema = z.object({
export type ContractArtifactHistoryEntry = z.infer<typeof ContractArtifactHistoryEntrySchema>;

export const ContractArtifactSchema = z.object({
contractId: z.string().min(1),
wasmHash: z.string().min(1),
contractId: z.string().regex(CONTRACT_ID_REGEX, CONTRACT_ID_MESSAGE),
wasmHash: z.string().regex(WASM_HASH_REGEX, WASM_HASH_MESSAGE),
deployedAt: z.string().datetime(),
sourcePath: z.string().min(1),
wasmPath: z.string().min(1),
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/artifacts/artifacts-lock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ afterEach(async () => {
function contractRecord(contractId: string) {
return {
contractId,
wasmHash: "hash",
wasmHash: "a".repeat(64),
deployedAt: "2026-06-25T00:00:00.000Z",
sourcePath: "./contracts/x",
wasmPath: "./target/x.wasm",
Expand All @@ -43,11 +43,13 @@ describe("withArtifactsLock", () => {
return writeArtifacts(next, cwd);
});

await Promise.all([deploy("alpha", "CALPHA"), deploy("beta", "CBETA")]);
const alphaId = "C".padEnd(56, "A");
const betaId = "C".padEnd(56, "B");
await Promise.all([deploy("alpha", alphaId), deploy("beta", betaId)]);

const artifacts = await readArtifacts(cwd);
expect(artifacts.networks.testnet?.contracts.alpha?.contractId).toBe("CALPHA");
expect(artifacts.networks.testnet?.contracts.beta?.contractId).toBe("CBETA");
expect(artifacts.networks.testnet?.contracts.alpha?.contractId).toBe(alphaId);
expect(artifacts.networks.testnet?.contracts.beta?.contractId).toBe(betaId);
});

it("releases the lock when the callback throws", async () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/artifacts/project-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async function writeDeployedCounter(tmpDir: string): Promise<void> {
contracts: {
counter: {
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-06-11T12:00:00.000Z",
sourcePath: "./contracts/counter",
wasmPath: "./rel/counter.wasm",
Expand Down Expand Up @@ -87,7 +87,7 @@ describe("collectProjectStatus", () => {
expect(counter).toMatchObject({
deployed: true,
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
});

const token = testnet.contracts.find((entry) => entry.name === "token");
Expand All @@ -108,7 +108,7 @@ describe("collectProjectStatus", () => {
await writeBindingMarker(outputDir, {
version: 1,
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
network: "testnet",
generatedAt: "2026-06-11T12:00:00.000Z",
});
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/artifacts/read-write-artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,15 @@ describe("writeArtifacts and readArtifacts", () => {
contracts: {
token: {
contractId: "C".padEnd(56, "A"),
wasmHash: "hash-token",
wasmHash: "a".repeat(64),
deployedAt: "2026-05-12T00:00:00.000Z",
sourcePath: "./contracts/token",
wasmPath: "./contracts/token.wasm",
dependencies: [],
},
marketplace: {
contractId: "C".padEnd(56, "B"),
wasmHash: "hash-marketplace",
wasmHash: "b".repeat(64),
deployedAt: "2026-05-12T00:00:00.000Z",
sourcePath: "./contracts/marketplace",
wasmPath: "./contracts/marketplace.wasm",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/compat/fixtures/artifacts-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"contracts": {
"counter": {
"contractId": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
"wasmHash": "abc123hash",
"wasmHash": "3f786850e387550fdab836ed7e6dc881de23001b3f786850e387550fdab836ed",
"deployedAt": "2025-06-01T12:00:00.000Z",
"sourcePath": "./contracts/counter",
"wasmPath": "./contracts/counter.wasm",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"contracts": {
"token": {
"contractId": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4",
"wasmHash": "hash-token-v2",
"wasmHash": "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90",
"deployedAt": "2026-05-12T00:00:00.000Z",
"sourcePath": "./contracts/token",
"wasmPath": "./contracts/token.wasm",
Expand All @@ -22,7 +22,7 @@
},
"vault": {
"contractId": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC5",
"wasmHash": "hash-vault-v2",
"wasmHash": "0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0",
"deployedAt": "2026-05-12T00:01:00.000Z",
"sourcePath": "./contracts/vault",
"wasmPath": "./contracts/vault.wasm",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/contracts/estimate-deploy-cost.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe("estimateDeployCost", () => {
const artifacts = createInitialArtifacts("app", { networks: ["testnet"] });
artifacts.networks.testnet!.contracts.counter = {
contractId: `C${"A".repeat(55)}`,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-06-21T00:00:00.000Z",
sourcePath: "./contracts/counter",
wasmPath: "./contracts/counter/target/wasm32v1-none/release/counter.wasm",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const baseConfig: CaatingaConfig = {
function deployedArtifact(contractId: string) {
return {
contractId,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-05-11T12:00:00.000Z",
sourcePath: "./contracts/x",
wasmPath: "./rel/x.wasm",
Expand Down
12 changes: 6 additions & 6 deletions packages/core/src/contracts/generate-bindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe("generateBindings", () => {
contracts: {
counter: {
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-05-11T12:00:00.000Z",
sourcePath: "./contracts/counter",
wasmPath: "./rel/counter.wasm",
Expand All @@ -126,7 +126,7 @@ describe("generateBindings", () => {
expect(result.marker).toMatchObject({
version: 1,
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
network: "testnet",
});
await expect(readBindingMarker(result.outputDir)).resolves.toEqual(result.marker);
Expand Down Expand Up @@ -166,7 +166,7 @@ describe("generateBindings", () => {
contracts: {
counter: {
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-05-11T12:00:00.000Z",
sourcePath: "./contracts/counter",
wasmPath: "./rel/counter.wasm",
Expand Down Expand Up @@ -215,7 +215,7 @@ describe("generateBindings", () => {
contracts: {
counter: {
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-05-11T12:00:00.000Z",
sourcePath: "./contracts/counter",
wasmPath: "./rel/counter.wasm",
Expand Down Expand Up @@ -251,7 +251,7 @@ describe("generateBindings", () => {
contracts: {
counter: {
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-05-11T12:00:00.000Z",
sourcePath: "./contracts/counter",
wasmPath: "./rel/counter.wasm",
Expand Down Expand Up @@ -294,7 +294,7 @@ describe("generateBindings", () => {
contracts: {
counter: {
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-05-11T12:00:00.000Z",
sourcePath: "./contracts/counter",
wasmPath: "./rel/counter.wasm",
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/contracts/invoke-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe("invokeContract", () => {
contracts: {
counter: {
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-05-11T12:00:00.000Z",
sourcePath: "./contracts/counter",
wasmPath: "./rel/counter.wasm",
Expand Down Expand Up @@ -131,7 +131,7 @@ describe("invokeContract", () => {
contracts: {
counter: {
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-05-11T12:00:00.000Z",
sourcePath: "./contracts/counter",
wasmPath: "./rel/counter.wasm",
Expand Down Expand Up @@ -173,7 +173,7 @@ describe("invokeContract", () => {
contracts: {
counter: {
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-05-11T12:00:00.000Z",
sourcePath: "./contracts/counter",
wasmPath: "./rel/counter.wasm",
Expand Down Expand Up @@ -218,7 +218,7 @@ describe("invokeContract", () => {
contracts: {
counter: {
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-05-11T12:00:00.000Z",
sourcePath: "./contracts/counter",
wasmPath: "./rel/counter.wasm",
Expand Down Expand Up @@ -264,7 +264,7 @@ describe("invokeContract", () => {
contracts: {
counter: {
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-05-11T12:00:00.000Z",
sourcePath: "./contracts/counter",
wasmPath: "./rel/counter.wasm",
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/contracts/read-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe("readContract", () => {
contracts: {
app: {
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-05-11T12:00:00.000Z",
sourcePath: "./contracts/app",
wasmPath: "./rel/app.wasm",
Expand Down Expand Up @@ -104,7 +104,7 @@ describe("readContract", () => {
contracts: {
app: {
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-05-11T12:00:00.000Z",
sourcePath: "./contracts/app",
wasmPath: "./rel/app.wasm",
Expand Down Expand Up @@ -147,7 +147,7 @@ describe("readContract", () => {
contracts: {
app: {
contractId: CONTRACT_ID,
wasmHash: "abc",
wasmHash: "a".repeat(64),
deployedAt: "2026-05-11T12:00:00.000Z",
sourcePath: "./contracts/app",
wasmPath: "./rel/app.wasm",
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/contracts/resolve-method-args.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { CaatingaError, CaatingaErrorCode } from "../errors/CaatingaError.js";
import { STELLAR_ADDRESS_REGEX } from "../stellar-cli/strkey.js";
import { formatNamedCliArgs } from "./format-cli-args.js";
import { resolveSourceAddress } from "./resolve-source-address.js";
import type { DeployArgValue } from "./resolve-deploy-args.js";

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

export type ResolveMethodArgsOptions = {
args: Record<string, DeployArgValue>;
source?: string;
Expand Down
Loading