Repository: hirosystems/clarinet (language: rust)

Issue:
Include tx cost in transaction receipts
One handy feature in Clarinet v1 was the inclusion of costs in the return type of function calls (both public and read-only). It would be great if this was included in the Clarinet v2. Specifically, I'm referring to writing unit tests and the JS return type. I wouldn't consider this a "must have", so if it's a huge effort, it's probably not worth prioritization. But if it's relatively easy to add, I'd appreciate considering it as a feature.


Discussion / hints:
Definitely easy to add. 
The interface could be 
```js
const { result, events, costs } = simnet.callReadOnlyFn(...)

console.log(costs)
/*
{
    total: {
      write_length: 19,
      write_count: 2,
      read_length: 1137,
      read_count: 8,
      runtime: 8882
    },
    limit: {
      write_length: 15000000,
      write_count: 15000,
      read_length: 100000000,
      read_count: 15000,
      runtime: 5000000000
    },
    memory: 393,
    memory_limit: 100000000
  }
*/
```

Is that what you have in mind?
Very late response, but yeah!

Fix patch:
diff --git a/components/clarinet-sdk-wasm/src/core.rs b/components/clarinet-sdk-wasm/src/core.rs
index 07a48821c..66868e91f 100644
--- a/components/clarinet-sdk-wasm/src/core.rs
+++ b/components/clarinet-sdk-wasm/src/core.rs
@@ -35,7 +35,6 @@ use std::{panic, path::PathBuf};
 use wasm_bindgen::prelude::*;
 use wasm_bindgen::JsValue;
 
-use crate::utils::costs::SerializableCostsReport;
 use crate::utils::events::serialize_event;
 
 #[wasm_bindgen]
@@ -217,6 +216,7 @@ pub struct TxArgs {
 pub struct TransactionRes {
     pub result: String,
     pub events: String,
+    pub costs: String,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -248,6 +248,7 @@ pub fn execution_result_to_transaction_res(execution: &ExecutionResult) -> Trans
     TransactionRes {
         result,
         events: json!(events_as_strings).to_string(),
+        costs: json!(execution.cost).to_string(),
     }
 }
 
@@ -1076,13 +1077,8 @@ impl SDK {
 
         let coverage = session.collect_lcov_content(&asts, &contract_paths);
 
-        let mut costs_reports = Vec::new();
-        costs_reports.append(&mut self.costs_reports);
-        let costs_reports: Vec<SerializableCostsReport> = costs_reports
-            .iter()
-            .map(SerializableCostsReport::from_vm_costs_report)
-            .collect();
-        let costs = serde_json::to_string(&costs_reports).map_err(|e| e.to_string())?;
+        let costs = serde_json::to_string(&self.costs_reports).map_err(|e| e.to_string())?;
+        self.costs_reports.clear();
 
         Ok(SessionReport { coverage, costs })
     }
diff --git a/components/clarinet-sdk-wasm/src/utils/costs.rs b/components/clarinet-sdk-wasm/src/utils/costs.rs
deleted file mode 100644
index b0c0798a9..000000000
--- a/components/clarinet-sdk-wasm/src/utils/costs.rs
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
-  Implements a custom CostReport struct because the clarity-vm CostSynthesis struct
-  does not have the serde::Serialize macro. This is a fix to avoid modifying the VM code.
-  Let's try to update it in the upcoming vm version (with epoch 3)
-*/
-
-use clarity_repl::{clarity::costs::ExecutionCost, repl::session::CostsReport};
-use serde::Serialize;
-
-#[derive(Clone, Debug, Serialize)]
-pub struct CostSynthesis {
-    pub total: ExecutionCost,
-    pub limit: ExecutionCost,
-    pub memory: u64,
-    pub memory_limit: u64,
-}
-
-#[derive(Clone, Debug, Serialize)]
-pub struct SerializableCostsReport {
-    pub test_name: String,
-    pub contract_id: String,
-    pub method: String,
-    pub args: Vec<String>,
-    pub cost_result: CostSynthesis,
-}
-
-impl SerializableCostsReport {
-    pub fn from_vm_costs_report(costs_report: &CostsReport) -> Self {
-        let cost_result = CostSynthesis {
-            total: costs_report.cost_result.total.clone(),
-            limit: costs_report.cost_result.limit.clone(),
-            memory: costs_report.cost_result.memory,
-            memory_limit: costs_report.cost_result.memory_limit,
-        };
-
-        SerializableCostsReport {
-            test_name: costs_report.test_name.clone(),
-            contract_id: costs_report.contract_id.clone(),
-            method: costs_report.method.clone(),
-            args: costs_report.args.clone(),
-            cost_result,
-        }
-    }
-}
diff --git a/components/clarinet-sdk-wasm/src/utils/mod.rs b/components/clarinet-sdk-wasm/src/utils/mod.rs
index 539658aaf..a9970c28f 100644
--- a/components/clarinet-sdk-wasm/src/utils/mod.rs
+++ b/components/clarinet-sdk-wasm/src/utils/mod.rs
@@ -1,2 +1,1 @@
-pub mod costs;
 pub mod events;
diff --git a/components/clarinet-sdk/browser/src/sdkProxy.ts b/components/clarinet-sdk/browser/src/sdkProxy.ts
index 67bb68b05..8cce4149b 100644
--- a/components/clarinet-sdk/browser/src/sdkProxy.ts
+++ b/components/clarinet-sdk/browser/src/sdkProxy.ts
@@ -18,6 +18,7 @@ import {
   type ParsedTransactionResult,
   type Execute,
   type TransferSTX,
+  parseCosts,
 } from "../../common/src/sdkProxyHelpers.js";
 
 /** @deprecated use `simnet.execute(command)` instead */
@@ -48,6 +49,7 @@ function parseTxResponse(response: TransactionRes): ParsedTransactionResult {
   return {
     result: Cl.deserialize(response.result),
     events: parseEvents(response.events),
+    costs: parseCosts(response.costs),
   };
 }
 
diff --git a/components/clarinet-sdk/common/src/sdkProxyHelpers.ts b/components/clarinet-sdk/common/src/sdkProxyHelpers.ts
index 022edabb9..10b918944 100644
--- a/components/clarinet-sdk/common/src/sdkProxyHelpers.ts
+++ b/components/clarinet-sdk/common/src/sdkProxyHelpers.ts
@@ -5,9 +5,25 @@ export type ClarityEvent = {
   data: { raw_value?: string; value?: ClarityValue; [key: string]: any };
 };
 
+export type ExecutionCost = {
+  writeLength: number;
+  writeCount: number;
+  readLength: number;
+  readCount: number;
+  runtime: number;
+};
+
+export type ClarityCosts = {
+  total: ExecutionCost;
+  limit: ExecutionCost;
+  memory: number;
+  memory_limit: number;
+};
+
 export type ParsedTransactionResult = {
   result: ClarityValue;
   events: ClarityEvent[];
+  costs: ClarityCosts | null;
 };
 
 export type CallFn = (
@@ -113,6 +129,32 @@ export function parseEvents(events: string): ClarityEvent[] {
   }
 }
 
+export function parseCosts(costs: string): ClarityCosts | null {
+  try {
+    let { memory, memory_limit, total, limit } = JSON.parse(costs);
+    return {
+      memory: memory,
+      memory_limit: memory_limit,
+      total: {
+        writeLength: total.write_length,
+        writeCount: total.write_count,
+        readLength: total.read_length,
+        readCount: total.read_count,
+        runtime: total.runtime,
+      },
+      limit: {
+        writeLength: limit.write_length,
+        writeCount: limit.write_count,
+        readLength: limit.read_length,
+        readCount: limit.read_count,
+        runtime: limit.runtime,
+      },
+    };
+  } catch (_e) {
+    return null;
+  }
+}
+
 export type MineBlock = (txs: Array<Tx>) => ParsedTransactionResult[];
 export type Execute = (snippet: string) => ParsedTransactionResult;
 export type GetDataVar = (contract: string, dataVar: string) => ClarityValue;
diff --git a/components/clarinet-sdk/node/src/sdkProxy.ts b/components/clarinet-sdk/node/src/sdkProxy.ts
index 32198eff7..8a8c2326d 100644
--- a/components/clarinet-sdk/node/src/sdkProxy.ts
+++ b/components/clarinet-sdk/node/src/sdkProxy.ts
@@ -18,6 +18,7 @@ import {
   type ParsedTransactionResult,
   type Execute,
   type TransferSTX,
+  parseCosts,
 } from "../../common/src/sdkProxyHelpers.js";
 
 /** @deprecated use `simnet.execute(command)` instead */
@@ -48,6 +49,7 @@ function parseTxResponse(response: TransactionRes): ParsedTransactionResult {
   return {
     result: Cl.deserialize(response.result),
     events: parseEvents(response.events),
+    costs: parseCosts(response.costs),
   };
 }
 
diff --git a/components/clarity-repl/src/repl/session.rs b/components/clarity-repl/src/repl/session.rs
index dd672ccbe..6f82d7f5d 100644
--- a/components/clarity-repl/src/repl/session.rs
+++ b/components/clarity-repl/src/repl/session.rs
@@ -84,7 +84,7 @@ lazy_static! {
     };
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize)]
 pub struct CostsReport {
     pub test_name: String,
     pub contract_id: String,


Test patch:
diff --git a/components/clarinet-sdk/node/tests/simnet-usage.test.ts b/components/clarinet-sdk/node/tests/simnet-usage.test.ts
index 258866fa3..4ccb8e5d7 100644
--- a/components/clarinet-sdk/node/tests/simnet-usage.test.ts
+++ b/components/clarinet-sdk/node/tests/simnet-usage.test.ts
@@ -28,7 +28,10 @@ function deleteExistingDeploymentPlan() {
 
 beforeEach(async () => {
   deleteExistingDeploymentPlan();
-  simnet = await initSimnet("tests/fixtures/Clarinet.toml");
+  simnet = await initSimnet("tests/fixtures/Clarinet.toml", false, {
+    trackCosts: true,
+    trackCoverage: false,
+  });
 });
 
 afterEach(() => {
@@ -146,6 +149,30 @@ describe("simnet can call contracts function", () => {
     expect(printEvent.data.value).toStrictEqual(Cl.stringAscii("call increment"));
   });
 
+  it("reports costs", () => {
+    const res = simnet.callPublicFn("counter", "increment", [], address1);
+
+    expect(res).toHaveProperty("costs");
+    expect(res.costs).toStrictEqual({
+      memory: 417,
+      memory_limit: 100000000,
+      total: {
+        writeLength: 44,
+        writeCount: 3,
+        readLength: 1466,
+        readCount: 8,
+        runtime: 15630,
+      },
+      limit: {
+        writeLength: 15000000,
+        writeCount: 15000,
+        readLength: 100000000,
+        readCount: 15000,
+        runtime: 5000000000,
+      },
+    });
+  });
+
   it("can call public functions with arguments", () => {
     const res = simnet.callPublicFn("counter", "add", [Cl.uint(2)], address1);