Skip to content

Commit

Permalink
add CLI command to list transactions for a given deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
zoeyTM committed Oct 8, 2024
1 parent 774c465 commit 5165e7d
Show file tree
Hide file tree
Showing 23 changed files with 7,318 additions and 1 deletion.
5 changes: 5 additions & 0 deletions examples/complete/contracts/BasicContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ pragma solidity ^0.8.9;

contract BasicContract {
uint public savedArg;
address public sender;

event BasicEvent(uint eventArg);

constructor(address _sender) {
sender = _sender;
}

receive() external payable {}

function basicFunction(uint funcArg) public {
Expand Down
4 changes: 3 additions & 1 deletion examples/complete/ignition/modules/CompleteModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ const withLibArtifact = require("../../libArtifacts/ContractWithLibrary.json");
const libArtifact = require("../../libArtifacts/BasicLibrary.json");

module.exports = buildModule("CompleteModule", (m) => {
const basic = m.contract("BasicContract");
const acct2 = m.getAccount(2);

const basic = m.contract("BasicContract", [acct2]);
const library = m.library("BasicLibrary");
const libFromArtifact = m.library("BasicLibrary", libArtifact, {
id: "BasicLibrary2",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ export * from "./errors";
export { IgnitionModuleSerializer } from "./ignition-module-serializer";
export { formatSolidityParameter } from "./internal/formatters";
export { listDeployments } from "./list-deployments";
export { listTransactions } from "./list-transactions";
export { status } from "./status";
export * from "./type-guards";
export * from "./types/artifact";
export * from "./types/deploy";
export * from "./types/errors";
export * from "./types/execution-events";
export * from "./types/list-transactions";
export * from "./types/module";
export * from "./types/module-builder";
export * from "./types/provider";
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/internal/errors-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ export const ERROR_RANGES: {
max: 1199,
title: "Strategy errors",
},
LIST_TRANSACTIONS: {
min: 1200,
max: 1299,
title: "List transactions errors",
},
};

/**
Expand Down Expand Up @@ -400,6 +405,13 @@ export const ERRORS = {
"Strategy configuration parameter '%paramName%' for the strategy '%strategyName%' is invalid: %reason%",
},
},
LIST_TRANSACTIONS: {
UNINITIALIZED_DEPLOYMENT: {
number: 1200,
message:
"Cannot list transactions for nonexistant deployment at %deploymentDir%",
},
},
};

/**
Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/internal/execution/deployment-state-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,20 @@ export function shouldBeJournaled(message: JournalMessage): boolean {

return true;
}

/**
* Reads the journal and returns an array of future IDs in the order they were executed.
*/
export async function getExecutionOrder(
deploymentLoader: DeploymentLoader
): Promise<string[]> {
const futureIds: string[] = [];

for await (const message of deploymentLoader.readFromJournal()) {
if ("futureId" in message && !futureIds.includes(message.futureId)) {
futureIds.push(message.futureId);
}
}

return futureIds;
}
144 changes: 144 additions & 0 deletions packages/core/src/list-transactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import type { ArtifactResolver } from "./types/artifact";

import { IgnitionError } from "./errors";
import { FileDeploymentLoader } from "./internal/deployment-loader/file-deployment-loader";
import { ERRORS } from "./internal/errors-list";
import {
getExecutionOrder,
loadDeploymentState,
} from "./internal/execution/deployment-state-helpers";
import { ExecutionResultType } from "./internal/execution/types/execution-result";
import {
ExecutionSateType,
type CallExecutionState,
type DeploymentExecutionState,
type ExecutionState,
type SendDataExecutionState,
} from "./internal/execution/types/execution-state";
import {
type Transaction,
TransactionReceiptStatus,
} from "./internal/execution/types/jsonrpc";
import { assertIgnitionInvariant } from "./internal/utils/assertions";
import {
type ListTransactionsResult,
TransactionStatus,
} from "./types/list-transactions";

/**
* Returns the transactions associated with a deployment.
*
* @param deploymentDir - the directory of the deployment to get the transactions of
* @param artifactResolver - the artifact resolver to use when loading artifacts
* for a future
*
* @beta
*/
export async function listTransactions(
deploymentDir: string,
_artifactResolver: Omit<ArtifactResolver, "getBuildInfo">
): Promise<ListTransactionsResult> {
const deploymentLoader = new FileDeploymentLoader(deploymentDir);

const deploymentState = await loadDeploymentState(deploymentLoader);

if (deploymentState === undefined) {
throw new IgnitionError(ERRORS.LIST_TRANSACTIONS.UNINITIALIZED_DEPLOYMENT, {
deploymentDir,
});
}

const executionOrder = await getExecutionOrder(deploymentLoader);
const transactions: ListTransactionsResult = [];

for (const futureId of executionOrder) {
const exState = deploymentState.executionStates[futureId];

if (!doesSendTransactions(exState)) {
continue;
}

for (const networkInteraction of exState.networkInteractions) {
assertIgnitionInvariant(
networkInteraction.type === "ONCHAIN_INTERACTION",
"Expected network interaction to be an onchain interaction"
);

for (const transaction of networkInteraction.transactions) {
switch (exState.type) {
case ExecutionSateType.DEPLOYMENT_EXECUTION_STATE: {
transactions.push({
type: exState.type,
from: exState.from,
txHash: transaction.hash,
status: getTransactionStatus(transaction),
name: exState.contractName,
address:
transaction.receipt?.status === TransactionReceiptStatus.SUCCESS
? exState.result?.type === ExecutionResultType.SUCCESS
? exState.result.address
: undefined
: undefined,
params: exState.constructorArgs,
value: networkInteraction.value,
});

break;
}
case ExecutionSateType.CALL_EXECUTION_STATE: {
transactions.push({
type: exState.type,
from: exState.from,
txHash: transaction.hash,
status: getTransactionStatus(transaction),
name: exState.functionName,
to: networkInteraction.to,
params: exState.args,
value: networkInteraction.value,
});

break;
}
case ExecutionSateType.SEND_DATA_EXECUTION_STATE: {
transactions.push({
type: exState.type,
from: exState.from,
txHash: transaction.hash,
status: getTransactionStatus(transaction),
to: networkInteraction.to,
value: networkInteraction.value,
});

break;
}
}
}
}
}

return transactions;
}

function doesSendTransactions(
exState: ExecutionState
): exState is
| DeploymentExecutionState
| CallExecutionState
| SendDataExecutionState {
return (
exState.type === ExecutionSateType.DEPLOYMENT_EXECUTION_STATE ||
exState.type === ExecutionSateType.CALL_EXECUTION_STATE ||
exState.type === ExecutionSateType.SEND_DATA_EXECUTION_STATE
);
}

function getTransactionStatus(transaction: Transaction): TransactionStatus {
const status =
transaction.receipt === undefined
? TransactionStatus.DROPPED
: transaction.receipt.status === TransactionReceiptStatus.SUCCESS
? TransactionStatus.SUCCESS
: TransactionStatus.FAILURE;

return status;
}
36 changes: 36 additions & 0 deletions packages/core/src/types/list-transactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { SolidityParameterType } from "./module";

/**
* The status of a transaction.
*
* @beta
*/
export enum TransactionStatus {
SUCCESS = "SUCCESS",
FAILURE = "FAILURE",
DROPPED = "DROPPED",
}

/**
* The information of a transaction.
*
* @beta
*/
export interface TransactionInfo {
type: string;
status: TransactionStatus;
txHash: string;
from: string;
to?: string;
name?: string; // can be contract name, function name, or undefined, depending on the type
address?: string;
params?: SolidityParameterType[];
value?: bigint;
}

/**
* An array of transaction information.
*
* @beta
*/
export type ListTransactionsResult = TransactionInfo[];
103 changes: 103 additions & 0 deletions packages/core/test/list-transactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { assert } from "chai";
import path from "path";

import {
listTransactions,
ListTransactionsResult,
TransactionStatus,
} from "../src";

import { setupMockArtifactResolver } from "./helpers";

describe("listTransactions", () => {
it("should return the transactions associated with a deployment", async () => {
const expected: ListTransactionsResult = [
{
type: "DEPLOYMENT_EXECUTION_STATE",
from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
txHash:
"0x65c7c0850d014fe44aced2249b3b3523c3a29e5e40b6388b6d84b28c0345b9e1",
status: TransactionStatus.SUCCESS,
name: "BasicContract",
address: "0x74e720c9B362ae3A65fF356ad62866511486BBBc",
params: ["0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"],
value: 0n,
},
{
type: "DEPLOYMENT_EXECUTION_STATE",
from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
txHash:
"0xee331a69f69646d8b551a1ee6514760763cb7b1c332dadb2f0d05c730e554a28",
status: TransactionStatus.SUCCESS,
name: "BasicLibrary",
address: "0x1c947344BA932fC7f3D622600dA0199520A67EFd",
params: [],
value: 0n,
},
{
type: "DEPLOYMENT_EXECUTION_STATE",
from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
txHash:
"0x6f06b87969f7543887e7cda4b0cf82426b6712a57c915593adf2dd6168f9f283",
status: TransactionStatus.SUCCESS,
name: "BasicLibrary",
address: "0xBdAce15b3211019E272418B8014971c1cefbC8f0",
params: [],
value: 0n,
},
{
type: "CALL_EXECUTION_STATE",
from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
txHash:
"0xb7b49d16087ab6351e26b2358ae211e5dac335441f323a28c6c26f0bc0c3a0a3",
status: TransactionStatus.SUCCESS,
name: "basicFunction",
to: "0x74e720c9B362ae3A65fF356ad62866511486BBBc",
params: [40],
value: 0n,
},
{
type: "DEPLOYMENT_EXECUTION_STATE",
from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
txHash:
"0x7542503401d0ad31f0c8de576c8d524535538c25050bd20f77562ecab25c4c8d",
status: TransactionStatus.SUCCESS,
name: "ContractWithLibrary",
address: "0xD369D9aB22D85C2A12bEabc0B581a419789E3755",
params: [],
value: 0n,
},
{
type: "SEND_DATA_EXECUTION_STATE",
from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
txHash:
"0x2870c7d9f84122caba3739be0dc2246343a87d1b216b57002654b3bd413fe8e2",
status: TransactionStatus.SUCCESS,
to: "0x74e720c9B362ae3A65fF356ad62866511486BBBc",
value: 123n,
},
];

const deploymentDir = path.join(
__dirname,
"mocks",
"listTransactions",
"success"
);

const artifactResolver = setupMockArtifactResolver();

const result = await listTransactions(deploymentDir, artifactResolver);

assert.deepEqual(result, expected);
});

it("should throw an error if the deployment is not initialized", async () => {
const artifactResolver = setupMockArtifactResolver();

await assert.isRejected(
listTransactions("fake", artifactResolver),
/IGN1200: Cannot list transactions for nonexistant deployment at fake/
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"_format": "hh-sol-dbg-1",
"buildInfo": "../build-info/0957b2d8bc1fe22551b79ef37cafc10a.json"
}
Loading

0 comments on commit 5165e7d

Please sign in to comment.