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
11 changes: 7 additions & 4 deletions src/commands/contracts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,7 @@ export class DeployAction extends BaseAction {
this.startSpinner(`Transpiling TypeScript file: ${filePath}`);
try {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "genlayer-deploy-"));
const outFile = path.join(
tempDir,
path.basename(filePath).replace(/\.ts$/, ".compiled.js"),
);
const outFile = path.join(tempDir, path.basename(filePath).replace(/\.ts$/, ".compiled.js"));
buildSync({
entryPoints: [filePath],
outfile: outFile,
Expand Down Expand Up @@ -188,6 +185,12 @@ export class DeployAction extends BaseAction {
// testnet
(result.txDataDecoded as any)?.contractAddress;

if (!contractAddress) {
throw new Error(
`Deployment transaction ${String(hash)} reached consensus (${transactionConsensusStatus(result)}), but no contract address was returned. The contract may have failed to initialize.`,
);
}

this.succeedSpinner("Contract deployed successfully.", {
"Transaction Hash": hash,
"Contract Address": contractAddress,
Expand Down
62 changes: 44 additions & 18 deletions src/commands/contracts/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ function normalizeExecutionResult(value: unknown): ExecutionResult | undefined {
if (normalized === ExecutionResult.DETERMINISTIC_VIOLATION) {
return ExecutionResult.DETERMINISTIC_VIOLATION;
}
if (normalized === "NONDET_DISAGREE" || normalized === "NONDET_DISAGREEMENT") return ExecutionResult.NONDET_DISAGREE;
if (normalized === "NONDET_DISAGREE" || normalized === "NONDET_DISAGREEMENT")
return ExecutionResult.NONDET_DISAGREE;
if (normalized === "SUCCESS") return ExecutionResult.FINISHED_WITH_RETURN;
if (normalized === "ERROR" || normalized === "FAILURE") return ExecutionResult.FINISHED_WITH_ERROR;
if (/^\d+$/.test(normalized)) return normalizeExecutionResult(Number(normalized));
Expand Down Expand Up @@ -58,9 +59,10 @@ function receiptData(receipt: unknown): {
firstLeaderReceipt: unknown;
genvmResult: unknown;
} {
const record = receipt && typeof receipt === "object" && !Array.isArray(receipt)
? receipt as Record<string, unknown>
: {};
const record =
receipt && typeof receipt === "object" && !Array.isArray(receipt)
? (receipt as Record<string, unknown>)
: {};
const data = directField(record, ["data"]);
const consensusData =
directField(data, ["consensus_data", "consensusData"]) ??
Expand Down Expand Up @@ -113,8 +115,10 @@ function consensusDiagnosis(status: TransactionStatus | undefined): string {
}

function executionDiagnosis(result: ExecutionResult | undefined): string {
if (result === ExecutionResult.TIMEOUT) return "TIMEOUT (leader timed out during execution)";
if (result === ExecutionResult.NONDET_DISAGREE) {
if (result === "TIMEOUT" || result === (ExecutionResult as any).TIMEOUT) {
return "TIMEOUT (leader timed out during execution)";
}
if (result === "NONDET_DISAGREE" || result === (ExecutionResult as any).NONDET_DISAGREE) {
return "NONDET_DISAGREE (validators disagreed on non-deterministic output)";
}
if (result === ExecutionResult.DETERMINISTIC_VIOLATION) {
Expand All @@ -123,30 +127,52 @@ function executionDiagnosis(result: ExecutionResult | undefined): string {
return result ?? "UNKNOWN";
}

export function assertSuccessfulExecution(
operation: string,
hash: unknown,
receipt: unknown,
): void {
export function extractExecutionErrorPayload(receipt: unknown): string | undefined {
const {firstLeaderReceipt, genvmResult} = receiptData(receipt);
const rawStderr = directField(genvmResult, ["stderr"]);
if (typeof rawStderr === "string" && rawStderr.trim()) {
return rawStderr.trim();
}
const rawResult = directField(firstLeaderReceipt, ["result", "genvm_result"]);
if (typeof rawResult === "string" && rawResult.trim()) {
try {
const decoded = Buffer.from(rawResult, "base64").toString("utf-8");
const cleaned = decoded.replace(/^[\x00-\x1F]+/, "").trim();
if (cleaned) return cleaned;
} catch {
return rawResult.trim();
}
}
return undefined;
}

export function assertSuccessfulExecution(operation: string, hash: unknown, receipt: unknown): void {
const status = transactionConsensusStatus(receipt);
const result = transactionExecutionResult(receipt);
if (isSuccessful(receipt as any) || isSuccessful({
...(receipt && typeof receipt === "object" && !Array.isArray(receipt) ? receipt as Record<string, unknown> : {}),
statusName: status,
txExecutionResultName: result,
} as any)) {
if (
isSuccessful(receipt as any) ||
isSuccessful({
...(receipt && typeof receipt === "object" && !Array.isArray(receipt)
? (receipt as Record<string, unknown>)
: {}),
statusName: status,
txExecutionResultName: result,
} as any)
) {
return;
}

const decidedAs = consensusDiagnosis(status);
const errorPayload = extractExecutionErrorPayload(receipt);
const payloadSuffix = errorPayload ? ` (payload: ${errorPayload})` : "";

if (result === undefined) {
throw new Error(
`${operation} ${String(hash)} transaction was decided as ${decidedAs}; leader execution result: UNKNOWN.`,
`${operation} ${String(hash)} transaction was decided as ${decidedAs}; leader execution result: UNKNOWN${payloadSuffix}.`,
);
}

throw new Error(
`${operation} ${String(hash)} transaction was decided as ${decidedAs}; leader execution result: ${executionDiagnosis(result)}.`,
`${operation} ${String(hash)} transaction was decided as ${decidedAs}; leader execution result: ${executionDiagnosis(result)}${payloadSuffix}.`,
);
}
82 changes: 73 additions & 9 deletions tests/actions/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,14 @@ describe("DeployAction", () => {
leaderTimeunitsAllocation: "10",
rotations: ["0"],
},
messageAllocations: [{
messageType: 1,
recipient: "0x0000000000000000000000000000000000000001",
callKey: DEPLOY_CALL_KEY,
budget: "5",
}],
messageAllocations: [
{
messageType: 1,
recipient: "0x0000000000000000000000000000000000000001",
callKey: DEPLOY_CALL_KEY,
budget: "5",
},
],
feeValue: "123",
},
validUntil: "999",
Expand Down Expand Up @@ -266,6 +268,70 @@ describe("DeployAction", () => {
);
});

test("fails when deployment consensus succeeds but execution returns error with invalid_contract payload", async () => {
const options: DeployOptions = {
contract: "/mocked/contract/path",
args: ["hello"],
};

vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue("contract code");
vi.mocked(mockClient.deployContract).mockResolvedValue(
"0x10520771987b0cf77665542282b80bbf9362f7cc86154b927fd45d8e806d9ae6",
);
// Studio receipt shaped like in issue #430: status FINALIZED, execution_result ERROR, payload base64 invalid_contract
vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue({
status: "FINALIZED",
statusName: "FINALIZED",
data: {
consensus_data: {
leader_receipt: [
{
execution_result: "ERROR",
result: "AmludmFsaWRfY29udHJhY3Q=",
genvm_result: {stdout: "", stderr: ""},
},
],
},
},
});

await deployer.deploy(options);

expect(deployer["failSpinner"]).toHaveBeenCalledWith(
"Error deploying contract",
expect.objectContaining({
message: expect.stringContaining(
"leader execution result: FINISHED_WITH_ERROR (payload: invalid_contract)",
),
}),
);
});

test("fails when deployment completes without contract address", async () => {
const options: DeployOptions = {
contract: "/mocked/contract/path",
};

vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue("contract code");
vi.mocked(mockClient.deployContract).mockResolvedValue("mocked_tx_hash");
vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue({
statusName: "ACCEPTED",
txExecutionResultName: "FINISHED_WITH_RETURN",
data: {}, // no contract_address!
});

await deployer.deploy(options);

expect(deployer["failSpinner"]).toHaveBeenCalledWith(
"Error deploying contract",
expect.objectContaining({
message: expect.stringContaining("no contract address was returned"),
}),
);
});

test("fails when deployment is undetermined despite leader return", async () => {
const options: DeployOptions = {
contract: "/mocked/contract/path",
Expand Down Expand Up @@ -737,9 +803,7 @@ describe("DeployAction", () => {
};
// Lane B: getClient (BaseAction) builds the client itself. Stub the browser
// session opener so the real getClient runs, then assert the wiring.
const getBrowserSessionSpy = vi
.spyOn(deployer as any, "getBrowserSession")
.mockResolvedValue(session);
const getBrowserSessionSpy = vi.spyOn(deployer as any, "getBrowserSession").mockResolvedValue(session);
const getAccountSpy = vi.spyOn(deployer as any, "getAccount");

vi.mocked(fs.existsSync).mockReturnValue(true);
Expand Down