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
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;
});
});
26 changes: 26 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,30 @@ 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