@cofhe/hardhat-3-plugin 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/gas.ts ADDED
@@ -0,0 +1,340 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { parseAbiItem, toEventSelector, toFunctionSelector, toFunctionSignature, type PublicClient } from 'viem';
5
+ import { MOCKS_TASK_MANAGER_ADDRESS } from '@cofhe/sdk';
6
+ import type { ArtifactManager } from 'hardhat/types/artifacts';
7
+
8
+ /// Topic of MockCoFHE's `MockGasConsumed(uint256)` event, emitted once per block of
9
+ /// mock-only work (op replication, decrypt-task storage, log building) with the gas it
10
+ /// consumed. Summing these per transaction gives the mock overhead of that transaction.
11
+ export const MOCK_GAS_CONSUMED_EVENT = parseAbiItem('event MockGasConsumed(uint256 gas)');
12
+ export const MOCK_GAS_CONSUMED_TOPIC = toEventSelector(MOCK_GAS_CONSUMED_EVENT);
13
+
14
+ /// Minimal receipt shape - compatible with viem and ethers v6 receipts.
15
+ export type AdjustableGasReceipt = {
16
+ gasUsed: bigint | number | string;
17
+ logs?: ReadonlyArray<{ address: string; topics: ReadonlyArray<string>; data: string }>;
18
+ };
19
+
20
+ export type AdjustedGasBreakdown = {
21
+ /** Raw gas used by the transaction (includes mock overhead). */
22
+ gasUsed: bigint;
23
+ /** Gas consumed by mock-only work (FHE op replication, decrypt-task storage, mock logging). */
24
+ mockGas: bigint;
25
+ /** gasUsed - mockGas: an estimate of what the transaction would cost on a real CoFHE network. */
26
+ adjustedGasUsed: bigint;
27
+ /** Number of MockGasConsumed events - roughly the number of mock-only blocks executed. */
28
+ mockGasEvents: number;
29
+ };
30
+
31
+ /// Computes the gas breakdown of a transaction receipt by summing the MockGasConsumed
32
+ /// events emitted by the mock task manager. Pure function of the receipt - no RPC calls.
33
+ /// On a real network (no mock events in the logs) `adjustedGasUsed` equals `gasUsed`.
34
+ /// Caveat: `mockGas` is measured pre-refund while `gasUsed` is post-refund, so transactions
35
+ /// that earn large gas refunds (storage clearing) can slightly overstate the mock share;
36
+ /// `adjustedGasUsed` is clamped at 0 for the pathological case.
37
+ export const mock_getAdjustedGasBreakdown = (receipt: AdjustableGasReceipt): AdjustedGasBreakdown => {
38
+ let mockGas = 0n;
39
+ let mockGasEvents = 0;
40
+
41
+ for (const log of receipt.logs ?? []) {
42
+ if (
43
+ log.address.toLowerCase() === MOCKS_TASK_MANAGER_ADDRESS.toLowerCase() &&
44
+ log.topics[0]?.toLowerCase() === MOCK_GAS_CONSUMED_TOPIC
45
+ ) {
46
+ mockGas += BigInt(log.data);
47
+ mockGasEvents += 1;
48
+ }
49
+ }
50
+
51
+ const gasUsed = BigInt(receipt.gasUsed);
52
+ return { gasUsed, mockGas, adjustedGasUsed: mockGas > gasUsed ? 0n : gasUsed - mockGas, mockGasEvents };
53
+ };
54
+
55
+ /// Returns the receipt's gas usage excluding mock overhead - an estimate of what the
56
+ /// transaction would cost on a real CoFHE network. See mock_getAdjustedGasBreakdown.
57
+ export const mock_getAdjustedGasUsed = (receipt: AdjustableGasReceipt): bigint =>
58
+ mock_getAdjustedGasBreakdown(receipt).adjustedGasUsed;
59
+
60
+ // =====================
61
+ // END-OF-RUN SUMMARY
62
+ // =====================
63
+ //
64
+ // Hardhat 3 runs node:test files in worker subprocesses, and every network.connect()
65
+ // creates its own in-process chain - so a single query at the end of the `test` task
66
+ // (which runs in the parent process) can't see any of the transactions. Instead:
67
+ // 1. Each connection is registered here (from the plugin's newConnection hook) when
68
+ // `cofhe.gasSummary` is enabled.
69
+ // 2. On worker `beforeExit`, the gas rows of every registered connection are collected
70
+ // (eth_getLogs + receipts) and appended to a JSON file in the hardhat cache dir.
71
+ // 3. The parent's `test` task override merges all row files and prints one table.
72
+
73
+ const SUMMARY_DIR_NAME = 'cofhe-gas-summary';
74
+
75
+ /// Serializable per-method aggregate (bigints as strings) passed from workers to the parent.
76
+ export type MockGasSummaryRow = {
77
+ contract: string;
78
+ method: string;
79
+ calls: number;
80
+ totalGasUsed: string;
81
+ totalMockGas: string;
82
+ };
83
+
84
+ type RegisteredConnection = {
85
+ publicClient: PublicClient;
86
+ artifacts: ArtifactManager;
87
+ /** Block at which the mocks were deployed on this connection's chain - the summary only
88
+ * scans from here, so a long-lived or forked chain isn't queried from genesis. */
89
+ fromBlock: bigint;
90
+ };
91
+
92
+ const registeredConnections: RegisteredConnection[] = [];
93
+ let summaryDir: string | undefined;
94
+ let exitHookInstalled = false;
95
+ let exitHookRan = false;
96
+
97
+ const shortAddress = (address: string) => `${address.slice(0, 6)}..${address.slice(-4)}`;
98
+
99
+ /// Builds a selector -> "signature" map from every compiled artifact so summary rows can
100
+ /// show method names instead of raw selectors.
101
+ const buildSelectorNameMap = async (artifacts: ArtifactManager): Promise<Map<string, string>> => {
102
+ const selectorNames = new Map<string, string>();
103
+ for (const fqn of await artifacts.getAllFullyQualifiedNames()) {
104
+ try {
105
+ const artifact = await artifacts.readArtifact(fqn);
106
+ for (const item of artifact.abi) {
107
+ if (item.type !== 'function') continue;
108
+ const selector = toFunctionSelector(item);
109
+ if (!selectorNames.has(selector)) selectorNames.set(selector, toFunctionSignature(item));
110
+ }
111
+ } catch {
112
+ // Artifacts without a usable ABI are skipped
113
+ }
114
+ }
115
+ return selectorNames;
116
+ };
117
+
118
+ /// Resolves deployed addresses to contract names by matching on-chain code against
119
+ /// artifact deployedBytecode. Best effort: contracts with immutables won't match and
120
+ /// fall back to a shortened address.
121
+ const buildAddressNameMap = async (
122
+ publicClient: PublicClient,
123
+ artifacts: ArtifactManager,
124
+ addresses: `0x${string}`[]
125
+ ): Promise<Map<string, string>> => {
126
+ const codeToName = new Map<string, string>();
127
+ for (const fqn of await artifacts.getAllFullyQualifiedNames()) {
128
+ try {
129
+ const artifact = await artifacts.readArtifact(fqn);
130
+ if (artifact.deployedBytecode && artifact.deployedBytecode !== '0x') {
131
+ codeToName.set(artifact.deployedBytecode.toLowerCase(), artifact.contractName);
132
+ }
133
+ } catch {
134
+ // skip
135
+ }
136
+ }
137
+
138
+ const addressNames = new Map<string, string>();
139
+ for (const address of addresses) {
140
+ try {
141
+ const code = (await publicClient.getCode({ address })) ?? '0x';
142
+ addressNames.set(address, codeToName.get(code.toLowerCase()) ?? shortAddress(address));
143
+ } catch {
144
+ addressNames.set(address, shortAddress(address));
145
+ }
146
+ }
147
+ return addressNames;
148
+ };
149
+
150
+ /// Collects per-method gas aggregates from one connection's chain.
151
+ export const collectMockGasRows = async (
152
+ publicClient: PublicClient,
153
+ artifacts: ArtifactManager,
154
+ fromBlock: bigint = 0n
155
+ ): Promise<MockGasSummaryRow[]> => {
156
+ let logs;
157
+ try {
158
+ logs = await publicClient.getLogs({
159
+ address: MOCKS_TASK_MANAGER_ADDRESS,
160
+ event: MOCK_GAS_CONSUMED_EVENT,
161
+ fromBlock,
162
+ toBlock: 'latest',
163
+ });
164
+ } catch {
165
+ return [];
166
+ }
167
+ if (logs.length === 0) return [];
168
+
169
+ // Sum mock gas per transaction
170
+ const mockGasPerTx = new Map<`0x${string}`, bigint>();
171
+ for (const log of logs) {
172
+ if (log.transactionHash === null) continue;
173
+ mockGasPerTx.set(log.transactionHash, (mockGasPerTx.get(log.transactionHash) ?? 0n) + (log.args.gas ?? 0n));
174
+ }
175
+
176
+ // Aggregate per called contract + method
177
+ type Aggregate = { to: `0x${string}`; selector: string; calls: number; totalGasUsed: bigint; totalMockGas: bigint };
178
+ const aggregates = new Map<string, Aggregate>();
179
+ // Deployments (FHE ops in constructors) are attributed to the created contract under a
180
+ // '(deployment)' method label.
181
+ for (const [txHash, mockGas] of mockGasPerTx) {
182
+ try {
183
+ const receipt = await publicClient.getTransactionReceipt({ hash: txHash });
184
+ const tx = await publicClient.getTransaction({ hash: txHash });
185
+ const target = tx.to ?? receipt.contractAddress;
186
+ if (!target) continue;
187
+
188
+ const selector = tx.to ? (tx.input ?? '0x').slice(0, 10) : '(deployment)';
189
+ const key = `${target}:${selector}`;
190
+ const aggregate = aggregates.get(key) ?? { to: target, selector, calls: 0, totalGasUsed: 0n, totalMockGas: 0n };
191
+ aggregate.calls += 1;
192
+ aggregate.totalGasUsed += receipt.gasUsed;
193
+ aggregate.totalMockGas += mockGas;
194
+ aggregates.set(key, aggregate);
195
+ } catch {
196
+ // Skip transactions that can't be resolved
197
+ }
198
+ }
199
+ if (aggregates.size === 0) return [];
200
+
201
+ const selectorNames = await buildSelectorNameMap(artifacts);
202
+ const addressNames = await buildAddressNameMap(publicClient, artifacts, [
203
+ ...new Set([...aggregates.values()].map((a) => a.to)),
204
+ ]);
205
+
206
+ return [...aggregates.values()].map((a) => ({
207
+ contract: addressNames.get(a.to) ?? shortAddress(a.to),
208
+ method: selectorNames.get(a.selector) ?? a.selector,
209
+ calls: a.calls,
210
+ totalGasUsed: a.totalGasUsed.toString(),
211
+ totalMockGas: a.totalMockGas.toString(),
212
+ }));
213
+ };
214
+
215
+ /// Registers a connection for end-of-run gas summary collection, installing (once) a
216
+ /// process beforeExit hook that dumps the aggregated rows into the hardhat cache dir
217
+ /// for the parent `test` task to merge and print.
218
+ export const registerGasSummaryConnection = (
219
+ publicClient: PublicClient,
220
+ artifacts: ArtifactManager,
221
+ cacheDir: string,
222
+ fromBlock: bigint
223
+ ) => {
224
+ registeredConnections.push({ publicClient, artifacts, fromBlock });
225
+ summaryDir = path.join(cacheDir, SUMMARY_DIR_NAME);
226
+
227
+ if (exitHookInstalled) return;
228
+ exitHookInstalled = true;
229
+
230
+ process.on('beforeExit', () => {
231
+ if (exitHookRan) return;
232
+ exitHookRan = true;
233
+
234
+ // Async work here keeps the event loop alive until it settles; the guard above
235
+ // prevents re-entry when beforeExit fires again afterwards.
236
+ void (async () => {
237
+ const rows: MockGasSummaryRow[] = [];
238
+ for (const { publicClient: client, artifacts: arts, fromBlock } of registeredConnections) {
239
+ try {
240
+ rows.push(...(await collectMockGasRows(client, arts, fromBlock)));
241
+ } catch {
242
+ // A closed or unusable connection contributes nothing
243
+ }
244
+ }
245
+ if (rows.length === 0 || summaryDir === undefined) return;
246
+
247
+ try {
248
+ fs.mkdirSync(summaryDir, { recursive: true });
249
+ const file = path.join(summaryDir, `rows-${process.pid}-${Date.now()}.json`);
250
+ fs.writeFileSync(file, JSON.stringify(rows), 'utf8');
251
+ } catch {
252
+ // Summary is best-effort; never fail the test run over it
253
+ }
254
+ })();
255
+ });
256
+ };
257
+
258
+ /// Removes any leftover row files (from previous or non-test runs).
259
+ export const cleanGasSummaryDir = (cacheDir: string) => {
260
+ try {
261
+ fs.rmSync(path.join(cacheDir, SUMMARY_DIR_NAME), { recursive: true, force: true });
262
+ } catch {
263
+ // best-effort
264
+ }
265
+ };
266
+
267
+ /// Reads all worker row files, merges them by contract + method, prints the table, and
268
+ /// cleans up. Called from the parent `test` task after the run completes.
269
+ export const printMockGasSummaryFromDir = (cacheDir: string) => {
270
+ const dir = path.join(cacheDir, SUMMARY_DIR_NAME);
271
+ let files: string[];
272
+ try {
273
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
274
+ } catch {
275
+ return;
276
+ }
277
+
278
+ const merged = new Map<
279
+ string,
280
+ { contract: string; method: string; calls: number; gasUsed: bigint; mockGas: bigint }
281
+ >();
282
+ for (const file of files) {
283
+ try {
284
+ const rows: MockGasSummaryRow[] = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
285
+ for (const r of rows) {
286
+ const key = `${r.contract}:${r.method}`;
287
+ const row = merged.get(key) ?? { contract: r.contract, method: r.method, calls: 0, gasUsed: 0n, mockGas: 0n };
288
+ row.calls += r.calls;
289
+ row.gasUsed += BigInt(r.totalGasUsed);
290
+ row.mockGas += BigInt(r.totalMockGas);
291
+ merged.set(key, row);
292
+ }
293
+ } catch {
294
+ // skip unreadable files
295
+ }
296
+ }
297
+ cleanGasSummaryDir(cacheDir);
298
+ if (merged.size === 0) return;
299
+
300
+ const rows = [...merged.values()]
301
+ .map((r) => {
302
+ const avgGasUsed = r.gasUsed / BigInt(r.calls);
303
+ const avgMockGas = r.mockGas / BigInt(r.calls);
304
+ return {
305
+ contract: r.contract,
306
+ method: r.method,
307
+ calls: r.calls,
308
+ avgGasUsed,
309
+ avgAdjusted: avgMockGas > avgGasUsed ? 0n : avgGasUsed - avgMockGas,
310
+ overheadPct: avgGasUsed === 0n ? 0 : Math.min(100, Number((avgMockGas * 100n) / avgGasUsed)),
311
+ };
312
+ })
313
+ .sort((a, b) => a.contract.localeCompare(b.contract) || a.method.localeCompare(b.method));
314
+
315
+ const headers = ['Contract', 'Method', 'Calls', 'Avg gas (mocks)', 'Avg gas (adjusted)', 'Mock overhead'];
316
+ const cells = rows.map((r) => [
317
+ r.contract,
318
+ r.method,
319
+ String(r.calls),
320
+ r.avgGasUsed.toLocaleString(),
321
+ r.avgAdjusted.toLocaleString(),
322
+ `${r.overheadPct}%`,
323
+ ]);
324
+ const widths = headers.map((h, i) => Math.max(h.length, ...cells.map((row) => row[i]!.length)));
325
+ const line = (row: string[]) => `│ ${row.map((cell, i) => cell.padEnd(widths[i]!)).join(' │ ')} │`;
326
+ const divider = (l: string, m: string, r: string) => `${l}${widths.map((w) => '─'.repeat(w + 2)).join(m)}${r}`;
327
+
328
+ console.log('');
329
+ console.log(chalk.bold('[COFHE-MOCKS] Gas summary') + chalk.dim(' — adjusted ≈ cost excluding mock-only overhead'));
330
+ console.log(divider('┌', '┬', '┐'));
331
+ console.log(line(headers));
332
+ console.log(divider('├', '┼', '┤'));
333
+ for (const row of cells) console.log(line(row));
334
+ console.log(divider('└', '┴', '┘'));
335
+ console.log(
336
+ chalk.dim(
337
+ 'Raw numbers include on-chain replication of off-chain CoFHE work; adjusted numbers estimate real-network cost. estimateGas remains unadjusted.'
338
+ )
339
+ );
340
+ };
package/src/hooks/hre.ts CHANGED
@@ -7,6 +7,7 @@ import { privateKeyToAccount } from 'viem/accounts';
7
7
  import type { ArtifactManager } from 'hardhat/types/artifacts';
8
8
 
9
9
  import {
10
+ MockCoFHEAddressBookArtifact,
10
11
  MockTaskManagerArtifact,
11
12
  MockACLArtifact,
12
13
  MockZkVerifierArtifact,
@@ -19,6 +20,7 @@ import { hardhat as hardhatChain } from '@cofhe/sdk/chains';
19
20
  import { deployMocks, type DeployMocksArgs, type DeployedMockContracts } from '../deploy.js';
20
21
  import { mock_setLoggingEnabled, mock_withLogs } from '../logging.js';
21
22
  import { mock_getPlaintext, mock_expectPlaintext, getMockContractsNpmPaths } from '../utils.js';
23
+ import { mock_getAdjustedGasBreakdown, mock_getAdjustedGasUsed, registerGasSummaryConnection } from '../gas.js';
22
24
  import type { CofheConnection } from '../type-extensions.js';
23
25
 
24
26
  // ─── Per-connection cofhe object factory ─────────────────────────────────────
@@ -55,6 +57,9 @@ function createCofheConnection(
55
57
  return createCofheClient(config);
56
58
  },
57
59
 
60
+ getAdjustedGasUsed: mock_getAdjustedGasUsed,
61
+ getAdjustedGasBreakdown: mock_getAdjustedGasBreakdown,
62
+
58
63
  async createClientWithBatteries(signerWalletClient?: WalletClient) {
59
64
  let signerClient: WalletClient;
60
65
  let address: `0x${string}`;
@@ -105,6 +110,10 @@ function createCofheConnection(
105
110
  await mock_expectPlaintext(publicClient, ctHash, expectedValue);
106
111
  },
107
112
 
113
+ MockCoFHEAddressBook: {
114
+ address: deployedMockContracts.MockCoFHEAddressBook,
115
+ abi: MockCoFHEAddressBookArtifact.abi,
116
+ },
108
117
  MockTaskManager: { address: deployedMockContracts.MockTaskManager, abi: MockTaskManagerArtifact.abi },
109
118
  MockACL: { address: deployedMockContracts.MockACL, abi: MockACLArtifact.abi },
110
119
  MockZkVerifier: { address: deployedMockContracts.MockZkVerifier, abi: MockZkVerifierArtifact.abi },
@@ -176,6 +185,20 @@ const hreHooks: Partial<HardhatRuntimeEnvironmentHooks> = {
176
185
  }
177
186
  );
178
187
 
188
+ // With gasSummary enabled, track this connection so its adjusted-gas rows are
189
+ // dumped at process exit for the `test` task to merge and print. Connections
190
+ // (and their chains) live in node:test worker processes, so collection has to
191
+ // happen here rather than in the test task itself.
192
+ // Only connections where mocks were actually deployed are tracked (deployMocks
193
+ // skips non-hardhat networks and returns an empty result) - registering external
194
+ // RPCs would trigger pointless log scans at exit and double-count shared chains.
195
+ // The current block is recorded so the scan never reaches back before the deploy
196
+ // (relevant for forks and long-lived chains).
197
+ if (hre.config.cofhe.gasSummary && deployedMockContracts.MockTaskManager !== undefined) {
198
+ const fromBlock = await publicClient.getBlockNumber();
199
+ registerGasSummaryConnection(publicClient, hre.artifacts, hre.config.paths.cache, fromBlock);
200
+ }
201
+
179
202
  (conn as any).cofhe = createCofheConnection(
180
203
  publicClient,
181
204
  walletClient,
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { HardhatPlugin } from 'hardhat/types/plugins';
2
- import { task } from 'hardhat/config';
2
+ import { overrideTask, task } from 'hardhat/config';
3
3
  import { ArgumentType } from 'hardhat/types/arguments';
4
4
 
5
5
  import { TASK_COFHE_USE_FAUCET, TASK_COFHE_SET_LOG_OPS } from './consts.js';
@@ -29,6 +29,11 @@ const plugin: HardhatPlugin = {
29
29
  .addFlag({ name: 'enable', description: 'Whether to enable logging' })
30
30
  .setAction(async () => import('./tasks/set-log-ops.js'))
31
31
  .build(),
32
+
33
+ // Prints the adjusted-gas summary after `hardhat test` when cofhe.gasSummary is enabled.
34
+ overrideTask('test')
35
+ .setAction(async () => import('./tasks/test-gas-summary.js'))
36
+ .build(),
32
37
  ],
33
38
  };
34
39
 
@@ -37,5 +42,6 @@ export default plugin;
37
42
  export * from './consts.js';
38
43
  export * from './deploy.js';
39
44
  export * from './fund.js';
45
+ export * from './gas.js';
40
46
  export * from './logging.js';
41
47
  export * from './utils.js';
package/src/logging.ts CHANGED
@@ -2,11 +2,11 @@ import type { PublicClient, WalletClient } from 'viem';
2
2
  import chalk from 'chalk';
3
3
 
4
4
  import { MockTaskManagerArtifact } from '@cofhe/mock-contracts';
5
- import { TASK_MANAGER_ADDRESS } from '@cofhe/sdk';
5
+ import { MOCKS_TASK_MANAGER_ADDRESS } from '@cofhe/sdk';
6
6
 
7
7
  async function getLoggingEnabled(publicClient: PublicClient): Promise<boolean> {
8
8
  return publicClient.readContract({
9
- address: TASK_MANAGER_ADDRESS,
9
+ address: MOCKS_TASK_MANAGER_ADDRESS,
10
10
  abi: MockTaskManagerArtifact.abi,
11
11
  functionName: 'logOps',
12
12
  args: [],
@@ -20,7 +20,7 @@ async function setLoggingEnabled(
20
20
  ): Promise<void> {
21
21
  const [account] = await walletClient.getAddresses();
22
22
  const hash = await walletClient.writeContract({
23
- address: TASK_MANAGER_ADDRESS,
23
+ address: MOCKS_TASK_MANAGER_ADDRESS,
24
24
  abi: MockTaskManagerArtifact.abi,
25
25
  functionName: 'setLogOps',
26
26
  args: [enabled],
@@ -0,0 +1,22 @@
1
+ import type { TaskOverrideActionFunction } from 'hardhat/types/tasks';
2
+
3
+ import { cleanGasSummaryDir, printMockGasSummaryFromDir } from '../gas.js';
4
+
5
+ /// Wraps the `test` task to print the adjusted-gas summary when `cofhe.gasSummary` is
6
+ /// enabled. Tests run in node:test worker processes, each with its own chains; workers
7
+ /// dump their per-method gas rows into the hardhat cache dir at exit (see gas.ts), and
8
+ /// this override merges and prints them once the run completes.
9
+ const action: TaskOverrideActionFunction = async (args, hre, runSuper) => {
10
+ if (!hre.config.cofhe.gasSummary) {
11
+ return runSuper(args);
12
+ }
13
+
14
+ // Drop leftovers from previous or aborted runs so the table only reflects this run.
15
+ cleanGasSummaryDir(hre.config.paths.cache);
16
+
17
+ const result = await runSuper(args);
18
+ printMockGasSummaryFromDir(hre.config.paths.cache);
19
+ return result;
20
+ };
21
+
22
+ export default action;
@@ -1,7 +1,9 @@
1
1
  import type { WalletClient } from 'viem';
2
2
  import type { CofheClient, CofheConfig, CofheInputConfig } from '@cofhe/sdk';
3
3
  import type { DeployMocksArgs, LogMocksDeploy } from './deploy.js';
4
+ import type { AdjustableGasReceipt, AdjustedGasBreakdown } from './gas.js';
4
5
  import type {
6
+ MockCoFHEAddressBookArtifact,
5
7
  MockTaskManagerArtifact,
6
8
  MockACLArtifact,
7
9
  MockZkVerifierArtifact,
@@ -29,6 +31,22 @@ export interface CofheConnection {
29
31
  */
30
32
  createClientWithBatteries(walletClient?: WalletClient): Promise<CofheClient>;
31
33
 
34
+ /**
35
+ * Returns a transaction's gas usage excluding mock-only overhead (FHE op replication,
36
+ * decrypt-task storage, mock logging) - an estimate of what the transaction would cost
37
+ * on a real CoFHE network. Pure function of the receipt (sums the mock task manager's
38
+ * MockGasConsumed events from `receipt.logs`); on a real network the receipt carries no
39
+ * such events and the raw `gasUsed` is returned unchanged.
40
+ */
41
+ getAdjustedGasUsed(receipt: AdjustableGasReceipt): bigint;
42
+
43
+ /**
44
+ * Full gas breakdown of a transaction receipt: raw `gasUsed`, the mock-only `mockGas`,
45
+ * the `adjustedGasUsed` (raw minus mock), and the number of mock gas events.
46
+ * See getAdjustedGasUsed.
47
+ */
48
+ getAdjustedGasBreakdown(receipt: AdjustableGasReceipt): AdjustedGasBreakdown;
49
+
32
50
  mocks: {
33
51
  /** Deploy (or re-deploy) the mock contracts. */
34
52
  deployMocks(options?: DeployMocksArgs): Promise<void>;
@@ -48,6 +66,9 @@ export interface CofheConnection {
48
66
  /** Assert that a ciphertext hash represents the expected plaintext value. */
49
67
  expectPlaintext(ctHash: bigint | string, expectedValue: bigint): Promise<void>;
50
68
 
69
+ /** MockCoFHEAddressBook contract descriptor — spread into readContract / writeContract */
70
+ MockCoFHEAddressBook: { address: `0x${string}`; abi: typeof MockCoFHEAddressBookArtifact.abi };
71
+
51
72
  /** MockTaskManager contract descriptor — spread into readContract / writeContract */
52
73
  MockTaskManager: { address: `0x${string}`; abi: typeof MockTaskManagerArtifact.abi };
53
74
 
@@ -71,6 +92,14 @@ declare module 'hardhat/types/config' {
71
92
  logMocks?: boolean;
72
93
  /** Whether to show gas usage warnings for mock operations (default: true) */
73
94
  gasWarning?: boolean;
95
+ /**
96
+ * Print a per-method gas summary after `hardhat test`, showing raw gas next to
97
+ * adjusted gas (mock-only overhead excluded - an estimate of real-network cost).
98
+ * Collected per test worker from chain history: transactions rolled back by
99
+ * snapshots won't appear, and the table is best-effort after forced exits -
100
+ * use getAdjustedGasUsed(receipt) for exact numbers. (default: false)
101
+ */
102
+ gasSummary?: boolean;
74
103
  /**
75
104
  * Controls deploy-mocks console output.
76
105
  * - `''` — silent, no output
@@ -85,6 +114,8 @@ declare module 'hardhat/types/config' {
85
114
  cofhe: {
86
115
  logMocks: boolean;
87
116
  gasWarning: boolean;
117
+ /** Print a per-method adjusted-gas summary after `hardhat test` (default: false) */
118
+ gasSummary: boolean;
88
119
  mocksDeployVerbosity: LogMocksDeploy;
89
120
  };
90
121
  }
package/src/utils.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { PublicClient } from 'viem';
2
- import { TASK_MANAGER_ADDRESS, MOCKS_ZK_VERIFIER_ADDRESS } from '@cofhe/sdk';
2
+ import { MOCKS_TASK_MANAGER_ADDRESS, MOCKS_ZK_VERIFIER_ADDRESS } from '@cofhe/sdk';
3
3
  import path from 'node:path';
4
4
  import fs from 'node:fs';
5
5
  import { createRequire } from 'node:module';
@@ -57,7 +57,7 @@ export async function mock_getPlaintext(
57
57
  }
58
58
 
59
59
  return publicClient.readContract({
60
- address: TASK_MANAGER_ADDRESS,
60
+ address: MOCKS_TASK_MANAGER_ADDRESS,
61
61
  abi: [
62
62
  {
63
63
  name: 'mockStorage',
@@ -86,7 +86,7 @@ export async function mock_expectPlaintext(
86
86
  }
87
87
 
88
88
  const exists = (await publicClient.readContract({
89
- address: TASK_MANAGER_ADDRESS,
89
+ address: MOCKS_TASK_MANAGER_ADDRESS,
90
90
  abi: [
91
91
  {
92
92
  name: 'inMockStorage',