Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 packages/mock-chain/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./execution";
export * from "./mockChain";
export * from "./party";
export * from "./objectMocking";
export * from "./transactionChecks";
86 changes: 86 additions & 0 deletions packages/mock-chain/src/mockChain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,3 +554,89 @@ describe("Contract execution and chain mocking", () => {
expect(chain.timestamp - params.timestamp).to.be.equal(0);
});
});

describe("Transaction validation checks", () => {
it("Should validate minimum nanoergs per box", () => {
const chain = new MockChain();
const alice = chain.newParty("Alice");
alice.addBalance({ nanoergs: 10000000n });

const unsignedTransaction = new TransactionBuilder(chain.height)
.from(alice.utxos)
.to(new OutputBuilder("100", alice.address)) // Too small value
.sendChangeTo(alice.address)
.build();

// Should fail with validation error
expect(() => chain.execute(unsignedTransaction, { signers: [alice] }))
.to.throw("Transaction validation failed");
});

it("Should validate miner fee", () => {
const chain = new MockChain();
const alice = chain.newParty("Alice");
alice.addBalance({ nanoergs: 10000000n });

// Transaction without fee
const unsignedTransaction = new TransactionBuilder(chain.height)
.from(alice.utxos)
.to(new OutputBuilder(SAFE_MIN_BOX_VALUE, alice.address))
.sendChangeTo(alice.address)
.build();

// Should pass but with warning (logged if log: true)
const consoleMock = vi.spyOn(console, "log").mockImplementationOnce(() => {});

expect(
chain.execute(unsignedTransaction, { signers: [alice], log: true })
).to.be.true;

expect(consoleMock).toHaveBeenCalledWith(
expect.stringContaining("No miner fee box found")
);

consoleMock.mockRestore();
});

it("Should allow disabling checks", () => {
const chain = new MockChain();
const alice = chain.newParty("Alice");
alice.addBalance({ nanoergs: 10000000n });

const unsignedTransaction = new TransactionBuilder(chain.height)
.from(alice.utxos)
.to(new OutputBuilder("100", alice.address)) // Too small value
.sendChangeTo(alice.address)
.build();

// Should pass when checks are disabled
expect(
chain.execute(unsignedTransaction, { signers: [alice], checks: false })
).to.be.true;
});

it("Should support custom fee trees", () => {
const chain = new MockChain();
const alice = chain.newParty("Alice");
const bob = chain.newParty("Bob"); // Create another party to use their ErgoTree
alice.addBalance({ nanoergs: 10000000n });

// Use Bob's ErgoTree as a custom fee contract
const customFeeTree = bob.ergoTree;

const unsignedTransaction = new TransactionBuilder(chain.height)
.from(alice.utxos)
.to(new OutputBuilder(SAFE_MIN_BOX_VALUE, alice.address))
.to(new OutputBuilder(RECOMMENDED_MIN_FEE_VALUE, customFeeTree)) // Custom fee box
.sendChangeTo(alice.address)
.build();

// Should pass with custom fee tree
expect(
chain.execute(unsignedTransaction, {
signers: [alice],
checks: { customFeeErgoTrees: [customFeeTree] }
})
).to.be.true;
});
});
32 changes: 32 additions & 0 deletions packages/mock-chain/src/mockChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { printDiff } from "./balancePrinting";
import { BLOCKCHAIN_PARAMETERS, execute } from "./execution";
import { mockBlockchainStateContext } from "./objectMocking";
import { KeyedMockChainParty, type MockChainParty, NonKeyedMockChainParty } from "./party";
import { type TransactionCheckOptions, validateTransaction } from "./transactionChecks";

const BLOCK_TIME_MS = 120000;
const DEFAULT_HEIGHT = 1;
Expand All @@ -36,6 +37,7 @@ export type TransactionExecutionOptions = {
signers?: KeyedMockChainParty[];
throw?: boolean;
log?: boolean;
checks?: TransactionCheckOptions | false;
};

export type MockChainOptions = {
Expand Down Expand Up @@ -178,6 +180,36 @@ export class MockChain {
? unsignedTransaction.toEIP12Object()
: unsignedTransaction;

// Perform transaction validation checks if enabled (default is to run checks unless explicitly disabled)
if (options?.checks !== false) {
const validationResult = validateTransaction(
txObject,
this.#tip.parameters,
options?.checks
);

if (options?.log) {
// Log warnings
for (const warning of validationResult.warnings) {
log(pc.yellow(`${pc.bgYellow(pc.bold(" Warning "))} ${warning}`));
}

// Log errors
for (const error of validationResult.errors) {
log(pc.red(`${pc.bgRed(pc.bold(" Check Error "))} ${error}`));
}
}

if (!validationResult.success) {
if (options?.throw !== false) {
throw new Error(
`Transaction validation failed:\n${validationResult.errors.join("\n")}`
);
}
return false;
}
}

const result = execute(txObject, keys, {
context,
baseCost,
Expand Down
Loading
Loading