Repository: 0xs34n/starknet.js (ts)

Issue:
Refactor `simulateTransaction` to Account
Regarding:
https://github.com/0xs34n/starknet.js/pull/466

`simulateTransaction` should be defined on Account as signature is mandatory for simulating a transaction, just like `estimateFee`

We can throw error for RPC just like estimateFeeBulk

`
throw Error(“Simulate Transaction is not enabled on RPC for now”)
`


Pull request description:
Refactor: simulateTransaction to Account
## Motivation and Resolution

Closes #478 
Refactor `simulateTransaction` to `Account` as signature is mandatory for simulating a transaction.

## Usage related changes

Move `simulateTransaction` to `Account` class. Function will throw an error for RPC. 

## Checklist:

- [x] Performed a self-review of the code
- [x] Rebased to the last commit of the target branch (or merged it into my branch)
- [x] Linked the issues which this PR resolves
- [x] Documented the changes
- [x] Updated the docs (www)
- [x] Updated the tests
- [x] All tests are passing


Patch:
diff --git a/src/account/default.ts b/src/account/default.ts
index 3dfe4232..ac178567 100644
--- a/src/account/default.ts
+++ b/src/account/default.ts
@@ -31,7 +31,7 @@ import {
   TransactionBulk,
   UniversalDeployerContractPayload,
 } from '../types';
-import { EstimateFeeBulk } from '../types/account';
+import { EstimateFeeBulk, TransactionSimulation } from '../types/account';
 import { parseUDCEvent } from '../utils/events';
 import {
   calculateContractAddressFromHash,
@@ -637,4 +637,39 @@ export class Account extends Provider implements AccountInterface {
     });
     return calls;
   }
+
+  public async simulateTransaction(
+    calls: AllowArray<Call>,
+    { nonce: providedNonce, blockIdentifier }: EstimateFeeDetails = {}
+  ): Promise<TransactionSimulation> {
+    const transactions = Array.isArray(calls) ? calls : [calls];
+    const nonce = toBN(providedNonce ?? (await this.getNonce()));
+    const version = toBN(feeTransactionVersion);
+    const chainId = await this.getChainId();
+
+    const signerDetails: InvocationsSignerDetails = {
+      walletAddress: this.address,
+      nonce,
+      maxFee: ZERO,
+      version,
+      chainId,
+    };
+
+    const invocation = await this.buildInvocation(transactions, signerDetails);
+    const response = await super.getSimulateTransaction(
+      invocation,
+      { version, nonce },
+      blockIdentifier
+    );
+
+    const suggestedMaxFee = estimatedFeeToMaxFee(response.fee_estimation.overall_fee);
+
+    return {
+      ...response,
+      fee_estimation: {
+        ...response.fee_estimation,
+        suggestedMaxFee,
+      },
+    };
+  }
 }
diff --git a/src/account/interface.ts b/src/account/interface.ts
index 9bf4ea54..278eb20e 100644
--- a/src/account/interface.ts
+++ b/src/account/interface.ts
@@ -21,6 +21,7 @@ import {
   MultiDeployContractResponse,
   Signature,
   TransactionBulk,
+  TransactionSimulation,
   UniversalDeployerContractPayload,
 } from '../types';
 import { BigNumberish } from '../utils/number';
@@ -325,4 +326,19 @@ export abstract class AccountInterface extends ProviderInterface {
     estimateFeeAction: EstimateFeeAction,
     details: EstimateFeeDetails
   ): Promise<BigNumberish>;
+
+  /**
+   * Simulates the transaction and returns the transaction trace and estimated fee.
+   *
+   * @param calls the invocation object containing:
+   * - contractAddress - the address of the contract
+   * - entrypoint - the entrypoint of the contract
+   * - calldata - (defaults to []) the calldata
+   *
+   * @returns response from estimate_fee
+   */
+  public abstract simulateTransaction(
+    calls: AllowArray<Call>,
+    estimateFeeDetails?: EstimateFeeDetails
+  ): Promise<TransactionSimulation>;
 }
diff --git a/src/provider/default.ts b/src/provider/default.ts
index e5d77afb..84b59188 100644
--- a/src/provider/default.ts
+++ b/src/provider/default.ts
@@ -18,6 +18,7 @@ import {
   InvocationsDetailsWithNonce,
   InvokeFunctionResponse,
   Status,
+  TransactionSimulationResponse,
 } from '../types';
 import { BigNumberish } from '../utils/number';
 import { ProviderInterface } from './interface';
@@ -193,4 +194,12 @@ export class Provider implements ProviderInterface {
   ): Promise<GetTransactionReceiptResponse> {
     return this.provider.waitForTransaction(txHash, retryInterval, successStates);
   }
+
+  public async getSimulateTransaction(
+    invocation: Invocation,
+    invocationDetails: InvocationsDetailsWithNonce,
+    blockIdentifier?: BlockIdentifier
+  ): Promise<TransactionSimulationResponse> {
+    return this.provider.getSimulateTransaction(invocation, invocationDetails, blockIdentifier);
+  }
 }
diff --git a/src/provider/interface.ts b/src/provider/interface.ts
index 42d851c0..7e7cc9dd 100644
--- a/src/provider/interface.ts
+++ b/src/provider/interface.ts
@@ -19,6 +19,7 @@ import type {
   InvocationsDetailsWithNonce,
   InvokeFunctionResponse,
   Status,
+  TransactionSimulationResponse,
 } from '../types';
 import type { BigNumberish } from '../utils/number';
 import { BlockIdentifier } from './utils';
@@ -297,4 +298,24 @@ export abstract class ProviderInterface {
     retryInterval?: number,
     successStates?: Array<Status>
   ): Promise<GetTransactionReceiptResponse>;
+
+  /**
+   * Simulates the transaction and returns the transaction trace and estimated fee.
+   *
+   * @param invocation the invocation object containing:
+   * - contractAddress - the address of the contract
+   * - entrypoint - the entrypoint of the contract
+   * - calldata - (defaults to []) the calldata
+   * - signature - (defaults to []) the signature
+   * @param details - optional details containing:
+   * - nonce - optional nonce
+   * - version - optional version
+   * @param blockIdentifier - block identifier
+   * @returns the transaction trace and estimated fee
+   */
+  public abstract getSimulateTransaction(
+    invocation: Invocation,
+    invocationDetails: InvocationsDetailsWithNonce,
+    blockIdentifier?: BlockIdentifier
+  ): Promise<TransactionSimulationResponse>;
 }
diff --git a/src/provider/rpc.ts b/src/provider/rpc.ts
index efb5f700..255f5ca3 100644
--- a/src/provider/rpc.ts
+++ b/src/provider/rpc.ts
@@ -16,6 +16,7 @@ import {
   InvocationsDetailsWithNonce,
   InvokeFunctionResponse,
   RPC,
+  TransactionSimulationResponse,
 } from '../types';
 import fetch from '../utils/fetchPonyfill';
 import { getSelectorFromName } from '../utils/hash';
@@ -491,4 +492,12 @@ export class RpcProvider implements ProviderInterface {
   public async getEvents(eventFilter: RPC.EventFilter): Promise<RPC.GetEventsResponse> {
     return this.fetchEndpoint('starknet_getEvents', { filter: eventFilter });
   }
+
+  public async getSimulateTransaction(
+    _invocation: Invocation,
+    _invocationDetails: InvocationsDetailsWithNonce,
+    _blockIdentifier?: BlockIdentifier
+  ): Promise<TransactionSimulationResponse> {
+    throw new Error('RPC does not implement simulateTransaction function');
+  }
 }
diff --git a/src/provider/sequencer.ts b/src/provider/sequencer.ts
index 1fe4d655..fd463160 100644
--- a/src/provider/sequencer.ts
+++ b/src/provider/sequencer.ts
@@ -22,6 +22,7 @@ import {
   InvocationsDetailsWithNonce,
   InvokeFunctionResponse,
   Sequencer,
+  TransactionSimulationResponse,
   TransactionTraceResponse,
 } from '../types';
 import fetch from '../utils/fetchPonyfill';
@@ -568,11 +569,11 @@ export class SequencerProvider implements ProviderInterface {
     return this.fetchEndpoint('estimate_message_fee', { blockIdentifier }, validCallL1Handler);
   }
 
-  public async simulateTransaction(
+  public async getSimulateTransaction(
     invocation: Invocation,
     invocationDetails: InvocationsDetailsWithNonce,
     blockIdentifier: BlockIdentifier = this.blockIdentifier
-  ): Promise<Sequencer.TransactionSimulationResponse> {
+  ): Promise<TransactionSimulationResponse> {
     return this.fetchEndpoint(
       'simulate_transaction',
       { blockIdentifier },
@@ -584,6 +585,6 @@ export class SequencerProvider implements ProviderInterface {
         version: toHex(toBN(invocationDetails?.version || 1)),
         nonce: toHex(toBN(invocationDetails.nonce)),
       }
-    );
+    ).then(this.responseParser.parseFeeSimulateTransactionResponse);
   }
 }
diff --git a/src/types/account.ts b/src/types/account.ts
index 5d72156b..81658a9c 100644
--- a/src/types/account.ts
+++ b/src/types/account.ts
@@ -2,7 +2,11 @@ import BN from 'bn.js';
 
 import { BlockIdentifier } from '../provider/utils';
 import { BigNumberish } from '../utils/number';
-import { DeclareTransactionReceiptResponse, EstimateFeeResponse } from './provider';
+import {
+  DeclareTransactionReceiptResponse,
+  EstimateFeeResponse,
+  TransactionSimulationResponse,
+} from './provider';
 
 export interface EstimateFee extends EstimateFeeResponse {
   suggestedMaxFee: BN;
@@ -43,3 +47,7 @@ export type DeclareDeployUDCResponse = {
   } & DeclareTransactionReceiptResponse;
   deploy: DeployContractUDCResponse;
 };
+
+export interface TransactionSimulation extends TransactionSimulationResponse {
+  fee_estimation: EstimateFee;
+}
diff --git a/src/types/provider.ts b/src/types/provider.ts
index ffdd7fac..3a8ea005 100644
--- a/src/types/provider.ts
+++ b/src/types/provider.ts
@@ -4,6 +4,7 @@
  */
 import BN from 'bn.js';
 
+import { TransactionTraceResponse } from './api/sequencer';
 import {
   AllowArray,
   Call,
@@ -134,3 +135,8 @@ export type EstimateFeeAction =
     };
 
 export type EstimateFeeResponseBulk = Array<EstimateFeeResponse>;
+
+export interface TransactionSimulationResponse {
+  trace: TransactionTraceResponse;
+  fee_estimation: EstimateFeeResponse;
+}
diff --git a/src/utils/responseParser/sequencer.ts b/src/utils/responseParser/sequencer.ts
index d6e77422..572a00bd 100644
--- a/src/utils/responseParser/sequencer.ts
+++ b/src/utils/responseParser/sequencer.ts
@@ -13,6 +13,7 @@ import {
   GetTransactionResponse,
   InvokeFunctionResponse,
   Sequencer,
+  TransactionSimulationResponse,
 } from '../../types';
 import { toBN } from '../number';
 import { ResponseParser } from '.';
@@ -129,6 +130,38 @@ export class SequencerAPIResponseParser extends ResponseParser {
     });
   }
 
+  public parseFeeSimulateTransactionResponse(
+    res: Sequencer.TransactionSimulationResponse
+  ): TransactionSimulationResponse {
+    if ('overall_fee' in res.fee_estimation) {
+      let gasInfo = {};
+
+      try {
+        gasInfo = {
+          gas_consumed: toBN(res.fee_estimation.gas_usage),
+          gas_price: toBN(res.fee_estimation.gas_price),
+        };
+      } catch {
+        // do nothing
+      }
+
+      return {
+        trace: res.trace,
+        fee_estimation: {
+          ...gasInfo,
+          overall_fee: toBN(res.fee_estimation.overall_fee),
+        },
+      };
+    }
+
+    return {
+      trace: res.trace,
+      fee_estimation: {
+        overall_fee: toBN(res.fee_estimation.amount),
+      },
+    };
+  }
+
   public parseCallContractResponse(res: Sequencer.CallContractResponse): CallContractResponse {
     return {
       result: res.result,
diff --git a/www/docs/API/Provider/sequencerProvider.md b/www/docs/API/Provider/sequencerProvider.md
index 381372f1..e754ee3f 100644
--- a/www/docs/API/Provider/sequencerProvider.md
+++ b/www/docs/API/Provider/sequencerProvider.md
@@ -156,20 +156,3 @@ Gets the transaction trace from a tx hash.
   };
 }
 ```
-
----
-
-### simulateTransaction()
-
-provider.**simulateTransaction**(invocation, invocationDetails, blockIdentifier) => _Promise < TransactionSimulationResponse >_
-
-Simulates the transaction and returns the transaction trace and estimated fee.
-
-###### _TransactionSimulationResponse_
-
-```typescript
-{
-  trace: TransactionTraceResponse;
-  fee_estimation: EstimateFeeResponse;
-}
-```
diff --git a/www/docs/API/account.md b/www/docs/API/account.md
index c83060b3..1b9baebe 100644
--- a/www/docs/API/account.md
+++ b/www/docs/API/account.md
@@ -489,3 +489,20 @@ Gets account address with the starknet id stark name.
 The _StarknetIdContract_ argument can be undefined, if it is, the function will automatically use official starknet id contracts of your network (It currently supports TESTNET 1 only).
 
 Returns directly the address in a string (Example: `0xff...34`).
+
+---
+
+### simulateTransaction()
+
+account.**simulateTransaction**(calls [ , estimateFeeDetails ]) => _Promise < TransactionSimulationResponse >_
+
+Simulates the transaction and returns the transaction trace and estimated fee.
+
+###### _TransactionSimulationResponse_
+
+```typescript
+{
+  trace: TransactionTraceResponse;
+  fee_estimation: EstimateFee;
+}
+```


Test patch:
diff --git a/__tests__/account.test.ts b/__tests__/account.test.ts
index fe63c578..e3a76292 100644
--- a/__tests__/account.test.ts
+++ b/__tests__/account.test.ts
@@ -102,6 +102,22 @@ describe('deploy and test Wallet', () => {
     });
   });
 
+  describeIfDevnetSequencer('Simulate transaction on Sequencer', () => {
+    test('simulate transaction', async () => {
+      const innerInvokeEstFeeSpy = jest.spyOn(account.signer, 'signTransaction');
+      const res = await account.simulateTransaction({
+        contractAddress: erc20Address,
+        entrypoint: 'transfer',
+        calldata: [erc20.address, '10', '0'],
+      });
+      expect(res).toHaveProperty('fee_estimation');
+      expect(res.fee_estimation).toHaveProperty('suggestedMaxFee');
+      expect(res).toHaveProperty('trace');
+      expect(innerInvokeEstFeeSpy.mock.calls[0][1].version).toBe(feeTransactionVersion);
+      innerInvokeEstFeeSpy.mockClear();
+    });
+  });
+
   test('read balance of wallet', async () => {
     const x = await erc20.balanceOf(account.address);
 
diff --git a/__tests__/sequencerProvider.test.ts b/__tests__/sequencerProvider.test.ts
index 62a91bd1..96739c8d 100644
--- a/__tests__/sequencerProvider.test.ts
+++ b/__tests__/sequencerProvider.test.ts
@@ -1,9 +1,6 @@
 import { Contract, Provider, SequencerProvider, stark } from '../src';
-import { ZERO } from '../src/constants';
-import { feeTransactionVersion } from '../src/utils/hash';
 import { toBN } from '../src/utils/number';
 import { encodeShortString } from '../src/utils/shortString';
-import { fromCallsToExecuteCalldata } from '../src/utils/transaction';
 import {
   compiledErc20,
   compiledL1L2,
@@ -50,36 +47,6 @@ describeIfSequencer('SequencerProvider', () => {
       expect(transactionTrace).toHaveProperty('signature');
     });
 
-    test('simulate transaction', async () => {
-      const call = {
-        contractAddress: exampleContractAddress,
-        entrypoint: 'transfer',
-        calldata: [exampleContractAddress, '10', '0'],
-      };
-      const calldata = fromCallsToExecuteCalldata([call]);
-      const nonce = toBN(await account.getNonce());
-      const version = toBN(feeTransactionVersion);
-      const chainId = await account.getChainId();
-      const signature = await account.signer.signTransaction([call], {
-        walletAddress: account.address,
-        nonce,
-        version,
-        maxFee: ZERO,
-        chainId,
-      });
-
-      const res = await sequencerProvider.simulateTransaction(
-        {
-          contractAddress: account.address,
-          calldata,
-          signature,
-        },
-        { version, nonce }
-      );
-      expect(res).toHaveProperty('trace');
-      expect(res).toHaveProperty('fee_estimation');
-    });
-
     test('getCode() -> { bytecode }', async () => {
       const code = await sequencerProvider.getCode(exampleContractAddress);
       return expect(Array.isArray(code.bytecode)).toBe(true);