Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CLI command to list transactions for a given deployment #821

Merged
merged 3 commits into from
Oct 24, 2024
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
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
176 changes: 176 additions & 0 deletions packages/core/src/list-transactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import type { ArtifactResolver } from "./types/artifact";

import findLastIndex from "lodash/findLastIndex";

import { IgnitionError } from "./errors";
import { FileDeploymentLoader } from "./internal/deployment-loader/file-deployment-loader";
import { ERRORS } from "./internal/errors-list";
import { 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 { JournalMessageType } from "./internal/execution/types/messages";
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 transactions: ListTransactionsResult = [];

for await (const message of deploymentLoader.readFromJournal()) {
if (message.type !== JournalMessageType.TRANSACTION_SEND) {
continue;
}

const exState = deploymentState.executionStates[message.futureId];

assertIgnitionInvariant(
doesSendTransactions(exState),
"Expected execution state to be a type that sends transactions"
);

const networkInteraction =
exState.networkInteractions[message.networkInteractionId - 1];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This -1 caught me by surprise tbh. Do we always have the network interaction ids be 1, 2, 3, ...

TBH I don't remember, and I couldn't find it easily in the code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I search the code, and we normally do

const onchainInteraction = executionState.networkInteractions.find(
    (interaction) => interaction.id === networkInteractionId
  );

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The -1 is because i'm indexing it directly, not using a find. As far as I know, we've always started the networkInteraction ID's at 1


assertIgnitionInvariant(
networkInteraction.type === "ONCHAIN_INTERACTION",
"Expected network interaction to be an onchain interaction"
);

// this seems redundant, but we use it later to determine pending vs dropped status
const lastTxIndex = findLastIndex(
networkInteraction.transactions,
(tx) => tx.hash === message.transaction.hash
);

const transaction = networkInteraction.transactions[lastTxIndex];

switch (exState.type) {
case ExecutionSateType.DEPLOYMENT_EXECUTION_STATE: {
transactions.push({
type: exState.type,
from: exState.from,
txHash: transaction.hash,
status: getTransactionStatus(
transaction,
lastTxIndex === networkInteraction.transactions.length - 1
),
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: {
const artifact = await deploymentLoader.loadArtifact(
exState.artifactId
);

transactions.push({
type: exState.type,
from: exState.from,
txHash: transaction.hash,
status: getTransactionStatus(
transaction,
lastTxIndex === networkInteraction.transactions.length - 1
),
name: `${artifact.contractName}#${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,
lastTxIndex === networkInteraction.transactions.length - 1
),
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,
isFinalTransaction: boolean
): TransactionStatus {
if (transaction.receipt === undefined) {
if (isFinalTransaction) {
return TransactionStatus.PENDING;
}

return TransactionStatus.DROPPED;
}

if (transaction.receipt.status === TransactionReceiptStatus.SUCCESS) {
return TransactionStatus.SUCCESS;
}

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

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

/**
* 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: "BasicContract#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
Loading