From 18a2cf47a02275ebf6d2e230540bf0625d527fa6 Mon Sep 17 00:00:00 2001 From: stanleyyuen <102275989+stanleyyconsensys@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:40:23 +0800 Subject: [PATCH 1/6] chore: add chain rpc controller --- .../abstract/chain-rpc-controller.test.ts | 48 +++++++++++++++++++ .../src/rpcs/abstract/chain-rpc-controller.ts | 48 +++++++++++++++++++ .../src/state/__tests__/helper.ts | 2 +- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 packages/starknet-snap/src/rpcs/abstract/chain-rpc-controller.test.ts create mode 100644 packages/starknet-snap/src/rpcs/abstract/chain-rpc-controller.ts diff --git a/packages/starknet-snap/src/rpcs/abstract/chain-rpc-controller.test.ts b/packages/starknet-snap/src/rpcs/abstract/chain-rpc-controller.test.ts new file mode 100644 index 00000000..b4a4d505 --- /dev/null +++ b/packages/starknet-snap/src/rpcs/abstract/chain-rpc-controller.test.ts @@ -0,0 +1,48 @@ +import { string } from 'superstruct'; + +import { mockNetworkStateManager } from '../../state/__tests__/helper'; +import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../../utils/constants'; +import { InvalidNetworkError } from '../../utils/exceptions'; +import { BaseRequestStruct } from '../../utils/superstruct'; +import { ChainRpcController } from './chain-rpc-controller'; + +describe('ChainRpcController', () => { + type Request = { chainId: string }; + class MockRpc extends ChainRpcController { + protected requestStruct = BaseRequestStruct; + + protected responseStruct = string(); + + // Set it to public to be able to spy on it + async handleRequest(params: Request) { + return `tested with ${params.chainId}`; + } + } + + it('executes request', async () => { + const network = STARKNET_SEPOLIA_TESTNET_NETWORK; + const { getNetworkSpy } = mockNetworkStateManager(network); + const { chainId } = network; + + const rpc = new MockRpc(); + const result = await rpc.execute({ + chainId, + }); + + expect(getNetworkSpy).toHaveBeenCalledWith({ chainId }); + expect(result).toBe(`tested with ${chainId}`); + }); + + it('throws `InvalidNetworkError` error if the given chainId not found.', async () => { + const network = STARKNET_SEPOLIA_TESTNET_NETWORK; + mockNetworkStateManager(null); + const { chainId } = network; + + const rpc = new MockRpc(); + await expect( + rpc.execute({ + chainId, + }), + ).rejects.toThrow(InvalidNetworkError); + }); +}); diff --git a/packages/starknet-snap/src/rpcs/abstract/chain-rpc-controller.ts b/packages/starknet-snap/src/rpcs/abstract/chain-rpc-controller.ts new file mode 100644 index 00000000..1042f8f3 --- /dev/null +++ b/packages/starknet-snap/src/rpcs/abstract/chain-rpc-controller.ts @@ -0,0 +1,48 @@ +import type { Json } from '@metamask/snaps-sdk'; + +import { NetworkStateManager } from '../../state/network-state-manager'; +import type { Network } from '../../types/snapState'; +import { InvalidNetworkError } from '../../utils/exceptions'; +import { RpcController } from '../../utils/rpc'; + +/** + * A base class for all RPC controllers that require a chainId to be provided in the request parameters. + * + * @template Request - The expected structure of the request parameters that contains the chainId property. + * @template Response - The expected structure of the response. + * @augments RpcController - The base class for all RPC controllers. + * @class ChainRpcController + */ +export abstract class ChainRpcController< + Request extends { + chainId: string; + }, + Response extends Json, +> extends RpcController { + protected network: Network; + + protected networkStateMgr: NetworkStateManager; + + constructor() { + super(); + this.networkStateMgr = new NetworkStateManager(); + } + + protected async getNetwork(chainId: string): Promise { + const network = await this.networkStateMgr.getNetwork({ chainId }); + // if the network is not in the list of networks that we support, we throw an error + if (!network) { + throw new InvalidNetworkError() as unknown as Error; + } + + return network; + } + + protected async preExecute(params: Request): Promise { + await super.preExecute(params); + + const { chainId } = params; + + this.network = await this.getNetwork(chainId); + } +} diff --git a/packages/starknet-snap/src/state/__tests__/helper.ts b/packages/starknet-snap/src/state/__tests__/helper.ts index 6f31e09c..05044039 100644 --- a/packages/starknet-snap/src/state/__tests__/helper.ts +++ b/packages/starknet-snap/src/state/__tests__/helper.ts @@ -95,7 +95,7 @@ export const mockTransactionRequestStateManager = () => { }; }; -export const mockNetworkStateManager = (network: Network) => { +export const mockNetworkStateManager = (network: Network | null) => { const getNetworkSpy = jest.spyOn(NetworkStateManager.prototype, 'getNetwork'); getNetworkSpy.mockResolvedValue(network); return { From 1810707aafe6ef9f8515ba6ffccf6931fe193de5 Mon Sep 17 00:00:00 2001 From: stanleyyuen <102275989+stanleyyconsensys@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:01:08 +0800 Subject: [PATCH 2/6] chore: relocate base rpc controller --- .../abstract/account-rpc-controller.test.ts | 126 +++++++++++++++ .../rpcs/abstract/account-rpc-controller.ts | 86 ++++++++++ .../rpcs/abstract/base-rpc-controller.test.ts | 26 +++ .../src/rpcs/abstract/base-rpc-controller.ts | 51 ++++++ .../src/rpcs/abstract/chain-rpc-controller.ts | 2 +- .../src/rpcs/declare-contract.ts | 2 +- .../src/rpcs/display-private-key.ts | 7 +- .../starknet-snap/src/rpcs/estimate-fee.ts | 2 +- .../starknet-snap/src/rpcs/execute-txn.ts | 4 +- .../src/rpcs/get-deployment-data.ts | 8 +- .../src/rpcs/sign-declare-transaction.ts | 2 +- .../starknet-snap/src/rpcs/sign-message.ts | 2 +- .../src/rpcs/sign-transaction.ts | 2 +- .../starknet-snap/src/rpcs/switch-network.ts | 3 +- .../src/rpcs/verify-signature.ts | 8 +- .../starknet-snap/src/rpcs/watch-asset.ts | 2 +- packages/starknet-snap/src/utils/rpc.test.ts | 152 +----------------- packages/starknet-snap/src/utils/rpc.ts | 119 -------------- 18 files changed, 309 insertions(+), 295 deletions(-) create mode 100644 packages/starknet-snap/src/rpcs/abstract/account-rpc-controller.test.ts create mode 100644 packages/starknet-snap/src/rpcs/abstract/account-rpc-controller.ts create mode 100644 packages/starknet-snap/src/rpcs/abstract/base-rpc-controller.test.ts create mode 100644 packages/starknet-snap/src/rpcs/abstract/base-rpc-controller.ts diff --git a/packages/starknet-snap/src/rpcs/abstract/account-rpc-controller.test.ts b/packages/starknet-snap/src/rpcs/abstract/account-rpc-controller.test.ts new file mode 100644 index 00000000..6a64ca91 --- /dev/null +++ b/packages/starknet-snap/src/rpcs/abstract/account-rpc-controller.test.ts @@ -0,0 +1,126 @@ +import { constants } from 'starknet'; +import { object, string } from 'superstruct'; +import type { Infer } from 'superstruct'; + +import type { StarknetAccount } from '../../__tests__/helper'; +import { generateAccounts } from '../../__tests__/helper'; +import type { SnapState } from '../../types/snapState'; +import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../../utils/constants'; +import * as snapHelper from '../../utils/snap'; +import * as snapUtils from '../../utils/snapUtils'; +import * as starknetUtils from '../../utils/starknetUtils'; +import { AccountRpcController } from './account-rpc-controller'; + +jest.mock('../../utils/snap'); +jest.mock('../../utils/logger'); + +describe('AccountRpcController', () => { + const state: SnapState = { + accContracts: [], + erc20Tokens: [], + networks: [STARKNET_SEPOLIA_TESTNET_NETWORK], + transactions: [], + }; + + const RequestStruct = object({ + address: string(), + chainId: string(), + }); + + type Request = Infer; + + class MockAccountRpc extends AccountRpcController { + protected requestStruct = RequestStruct; + + protected responseStruct = string(); + + // Set it to public to be able to spy on it + async handleRequest(param: Request) { + return `done ${param.address} and ${param.chainId}`; + } + } + + const mockAccount = async (network: constants.StarknetChainId) => { + const accounts = await generateAccounts(network, 1); + return accounts[0]; + }; + + const prepareExecute = async (account: StarknetAccount) => { + const verifyIfAccountNeedUpgradeOrDeploySpy = jest.spyOn( + snapUtils, + 'verifyIfAccountNeedUpgradeOrDeploy', + ); + + const getKeysFromAddressSpy = jest.spyOn( + starknetUtils, + 'getKeysFromAddress', + ); + + const getStateDataSpy = jest.spyOn(snapHelper, 'getStateData'); + + getStateDataSpy.mockResolvedValue(state); + + getKeysFromAddressSpy.mockResolvedValue({ + privateKey: account.privateKey, + publicKey: account.publicKey, + addressIndex: account.addressIndex, + derivationPath: account.derivationPath as unknown as any, + }); + + verifyIfAccountNeedUpgradeOrDeploySpy.mockReturnThis(); + + return { + getKeysFromAddressSpy, + getStateDataSpy, + verifyIfAccountNeedUpgradeOrDeploySpy, + }; + }; + + it('executes request', async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const account = await mockAccount(chainId); + await prepareExecute(account); + const rpc = new MockAccountRpc(); + + const result = await rpc.execute({ + address: account.address, + chainId, + }); + + expect(result).toBe(`done ${account.address} and ${chainId}`); + }); + + it('fetchs account before execute', async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const account = await mockAccount(chainId); + const { getKeysFromAddressSpy } = await prepareExecute(account); + const rpc = new MockAccountRpc(); + + await rpc.execute({ address: account.address, chainId }); + + expect(getKeysFromAddressSpy).toHaveBeenCalled(); + }); + + it.each([true, false])( + `assign verifyIfAccountNeedUpgradeOrDeploy's argument "showAlert" to %s if the constructor option 'showInvalidAccountAlert' is set to %s`, + async (showInvalidAccountAlert: boolean) => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const account = await mockAccount(chainId); + const { verifyIfAccountNeedUpgradeOrDeploySpy } = await prepareExecute( + account, + ); + const rpc = new MockAccountRpc({ + showInvalidAccountAlert, + }); + + await rpc.execute({ address: account.address, chainId }); + + expect(verifyIfAccountNeedUpgradeOrDeploySpy).toHaveBeenCalledWith( + expect.any(Object), + account.address, + account.publicKey, + showInvalidAccountAlert, + ); + }, + ); +}); diff --git a/packages/starknet-snap/src/rpcs/abstract/account-rpc-controller.ts b/packages/starknet-snap/src/rpcs/abstract/account-rpc-controller.ts new file mode 100644 index 00000000..d02ed372 --- /dev/null +++ b/packages/starknet-snap/src/rpcs/abstract/account-rpc-controller.ts @@ -0,0 +1,86 @@ +import type { getBIP44ChangePathString } from '@metamask/key-tree/dist/types/utils'; +import type { Json } from '@metamask/snaps-sdk'; + +import type { Network, SnapState } from '../../types/snapState'; +import { getBip44Deriver, getStateData } from '../../utils'; +import { + getNetworkFromChainId, + verifyIfAccountNeedUpgradeOrDeploy, +} from '../../utils/snapUtils'; +import { getKeysFromAddress } from '../../utils/starknetUtils'; +import { RpcController } from './base-rpc-controller'; + +export type AccountRpcParams = { + chainId: string; + address: string; +}; + +// TODO: the Account object should move into a account manager for generate account +export type Account = { + privateKey: string; + publicKey: string; + addressIndex: number; + // This is the derivation path of the address, it is used in `getNextAddressIndex` to find the account in state where matching the same derivation path + derivationPath: ReturnType; +}; + +export type AccountRpcControllerOptions = { + showInvalidAccountAlert: boolean; +}; + +/** + * A base class for rpc controllers that require account discovery. + * + * @template Request - The expected structure of the request parameters. + * @template Response - The expected structure of the response. + * @class AccountRpcController + */ +export abstract class AccountRpcController< + Request extends AccountRpcParams, + Response extends Json, +> extends RpcController { + protected account: Account; + + protected network: Network; + + protected options: AccountRpcControllerOptions; + + protected defaultOptions: AccountRpcControllerOptions = { + showInvalidAccountAlert: true, + }; + + constructor(options?: AccountRpcControllerOptions) { + super(); + this.options = Object.assign({}, this.defaultOptions, options); + } + + protected async preExecute(params: Request): Promise { + await super.preExecute(params); + + const { chainId, address } = params; + const { showInvalidAccountAlert } = this.options; + + const deriver = await getBip44Deriver(); + // TODO: Instead of getting the state directly, we should implement state management to consolidate the state fetching + const state = await getStateData(); + + // TODO: getNetworkFromChainId from state is still needed, due to it is supporting in get-starknet at this moment + this.network = getNetworkFromChainId(state, chainId); + + // TODO: This method should be refactored to get the account from an account manager + this.account = await getKeysFromAddress( + deriver, + this.network, + state, + address, + ); + + // TODO: rename this method to verifyAccount + await verifyIfAccountNeedUpgradeOrDeploy( + this.network, + address, + this.account.publicKey, + showInvalidAccountAlert, + ); + } +} diff --git a/packages/starknet-snap/src/rpcs/abstract/base-rpc-controller.test.ts b/packages/starknet-snap/src/rpcs/abstract/base-rpc-controller.test.ts new file mode 100644 index 00000000..4646af23 --- /dev/null +++ b/packages/starknet-snap/src/rpcs/abstract/base-rpc-controller.test.ts @@ -0,0 +1,26 @@ +import { string } from 'superstruct'; + +import { RpcController } from './base-rpc-controller'; + +jest.mock('../../utils/logger'); + +describe('RpcController', () => { + class MockRpc extends RpcController { + protected requestStruct = string(); + + protected responseStruct = string(); + + // Set it to public to be able to spy on it + async handleRequest(params: string) { + return `done ${params}`; + } + } + + it('executes request', async () => { + const rpc = new MockRpc(); + + const result = await rpc.execute('test'); + + expect(result).toBe('done test'); + }); +}); diff --git a/packages/starknet-snap/src/rpcs/abstract/base-rpc-controller.ts b/packages/starknet-snap/src/rpcs/abstract/base-rpc-controller.ts new file mode 100644 index 00000000..1a4c7914 --- /dev/null +++ b/packages/starknet-snap/src/rpcs/abstract/base-rpc-controller.ts @@ -0,0 +1,51 @@ +import type { Json } from '@metamask/snaps-sdk'; +import type { Struct } from 'superstruct'; + +import { logger, validateRequest, validateResponse } from '../../utils'; + +/** + * A base class for rpc controllers. + * + * @template Request - The expected structure of the request parameters. + * @template Response - The expected structure of the response. + * @class RpcController + */ +export abstract class RpcController< + Request extends Json, + Response extends Json, +> { + /** + * Superstruct for the request. + */ + protected abstract requestStruct: Struct; + + /** + * Superstruct for the response. + */ + protected abstract responseStruct: Struct; + + protected abstract handleRequest(params: Request): Promise; + + protected async preExecute(params: Request): Promise { + logger.info(`Request: ${JSON.stringify(params)}`); + validateRequest(params, this.requestStruct); + } + + protected async postExecute(response: Response): Promise { + logger.info(`Response: ${JSON.stringify(response)}`); + validateResponse(response, this.responseStruct); + } + + /** + * A method to execute the rpc method. + * + * @param params - An struct contains the require parameter for the request. + * @returns A promise that resolves to an json. + */ + async execute(params: Request): Promise { + await this.preExecute(params); + const resp = await this.handleRequest(params); + await this.postExecute(resp); + return resp; + } +} diff --git a/packages/starknet-snap/src/rpcs/abstract/chain-rpc-controller.ts b/packages/starknet-snap/src/rpcs/abstract/chain-rpc-controller.ts index 1042f8f3..c873591d 100644 --- a/packages/starknet-snap/src/rpcs/abstract/chain-rpc-controller.ts +++ b/packages/starknet-snap/src/rpcs/abstract/chain-rpc-controller.ts @@ -3,7 +3,7 @@ import type { Json } from '@metamask/snaps-sdk'; import { NetworkStateManager } from '../../state/network-state-manager'; import type { Network } from '../../types/snapState'; import { InvalidNetworkError } from '../../utils/exceptions'; -import { RpcController } from '../../utils/rpc'; +import { RpcController } from './base-rpc-controller'; /** * A base class for all RPC controllers that require a chainId to be provided in the request parameters. diff --git a/packages/starknet-snap/src/rpcs/declare-contract.ts b/packages/starknet-snap/src/rpcs/declare-contract.ts index 03d06bba..c6a7b48d 100644 --- a/packages/starknet-snap/src/rpcs/declare-contract.ts +++ b/packages/starknet-snap/src/rpcs/declare-contract.ts @@ -10,7 +10,6 @@ import { mapDeprecatedParams, UniversalDetailsStruct, confirmDialog, - AccountRpcController, signerUI, networkUI, rowUI, @@ -19,6 +18,7 @@ import { } from '../utils'; import { UserRejectedOpError } from '../utils/exceptions'; import { declareContract as declareContractUtil } from '../utils/starknetUtils'; +import { AccountRpcController } from './abstract/account-rpc-controller'; // Define the DeclareContractRequestStruct export const DeclareContractRequestStruct = assign( diff --git a/packages/starknet-snap/src/rpcs/display-private-key.ts b/packages/starknet-snap/src/rpcs/display-private-key.ts index bef32db8..3a625a6e 100644 --- a/packages/starknet-snap/src/rpcs/display-private-key.ts +++ b/packages/starknet-snap/src/rpcs/display-private-key.ts @@ -4,12 +4,9 @@ import { renderDisplayPrivateKeyAlertUI, renderDisplayPrivateKeyConfirmUI, } from '../ui/utils'; -import { - AccountRpcController, - AddressStruct, - BaseRequestStruct, -} from '../utils'; +import { AddressStruct, BaseRequestStruct } from '../utils'; import { UserRejectedOpError } from '../utils/exceptions'; +import { AccountRpcController } from './abstract/account-rpc-controller'; export const DisplayPrivateKeyRequestStruct = assign( object({ diff --git a/packages/starknet-snap/src/rpcs/estimate-fee.ts b/packages/starknet-snap/src/rpcs/estimate-fee.ts index e36df47c..b7217d14 100644 --- a/packages/starknet-snap/src/rpcs/estimate-fee.ts +++ b/packages/starknet-snap/src/rpcs/estimate-fee.ts @@ -6,11 +6,11 @@ import { FeeTokenUnit } from '../types/snapApi'; import { AddressStruct, BaseRequestStruct, - AccountRpcController, UniversalDetailsStruct, InvocationsStruct, } from '../utils'; import { getEstimatedFees } from '../utils/starknetUtils'; +import { AccountRpcController } from './abstract/account-rpc-controller'; export const EstimateFeeRequestStruct = assign( object({ diff --git a/packages/starknet-snap/src/rpcs/execute-txn.ts b/packages/starknet-snap/src/rpcs/execute-txn.ts index db31752c..75a6eeab 100644 --- a/packages/starknet-snap/src/rpcs/execute-txn.ts +++ b/packages/starknet-snap/src/rpcs/execute-txn.ts @@ -13,11 +13,9 @@ import { FeeToken } from '../types/snapApi'; import type { TransactionRequest } from '../types/snapState'; import { VoyagerTransactionType, type Transaction } from '../types/snapState'; import { generateExecuteTxnFlow } from '../ui/utils'; -import type { AccountRpcControllerOptions } from '../utils'; import { AddressStruct, BaseRequestStruct, - AccountRpcController, UniversalDetailsStruct, CallsStruct, mapDeprecatedParams, @@ -30,6 +28,8 @@ import { executeTxn as executeTxnUtil, getEstimatedFees, } from '../utils/starknetUtils'; +import type { AccountRpcControllerOptions } from './abstract/account-rpc-controller'; +import { AccountRpcController } from './abstract/account-rpc-controller'; export const ExecuteTxnRequestStruct = assign( object({ diff --git a/packages/starknet-snap/src/rpcs/get-deployment-data.ts b/packages/starknet-snap/src/rpcs/get-deployment-data.ts index 8206db31..ac8fcc29 100644 --- a/packages/starknet-snap/src/rpcs/get-deployment-data.ts +++ b/packages/starknet-snap/src/rpcs/get-deployment-data.ts @@ -1,18 +1,14 @@ import type { Infer } from 'superstruct'; import { object, string, assign, array } from 'superstruct'; -import { - AddressStruct, - BaseRequestStruct, - AccountRpcController, - CairoVersionStruct, -} from '../utils'; +import { AddressStruct, BaseRequestStruct, CairoVersionStruct } from '../utils'; import { ACCOUNT_CLASS_HASH, CAIRO_VERSION } from '../utils/constants'; import { AccountAlreadyDeployedError } from '../utils/exceptions'; import { getDeployAccountCallData, isAccountDeployed, } from '../utils/starknetUtils'; +import { AccountRpcController } from './abstract/account-rpc-controller'; export const GetDeploymentDataRequestStruct = assign( object({ diff --git a/packages/starknet-snap/src/rpcs/sign-declare-transaction.ts b/packages/starknet-snap/src/rpcs/sign-declare-transaction.ts index aa1fbf15..cc5d6cfe 100644 --- a/packages/starknet-snap/src/rpcs/sign-declare-transaction.ts +++ b/packages/starknet-snap/src/rpcs/sign-declare-transaction.ts @@ -6,12 +6,12 @@ import { renderSignDeclareTransactionUI } from '../ui/utils'; import { AddressStruct, BaseRequestStruct, - AccountRpcController, DeclareSignDetailsStruct, mapDeprecatedParams, } from '../utils'; import { UserRejectedOpError } from '../utils/exceptions'; import { signDeclareTransaction as signDeclareTransactionUtil } from '../utils/starknetUtils'; +import { AccountRpcController } from './abstract/account-rpc-controller'; export const SignDeclareTransactionRequestStruct = assign( object({ diff --git a/packages/starknet-snap/src/rpcs/sign-message.ts b/packages/starknet-snap/src/rpcs/sign-message.ts index 6b4387cb..794634ab 100644 --- a/packages/starknet-snap/src/rpcs/sign-message.ts +++ b/packages/starknet-snap/src/rpcs/sign-message.ts @@ -7,11 +7,11 @@ import { TypeDataStruct, AuthorizableStruct, BaseRequestStruct, - AccountRpcController, mapDeprecatedParams, } from '../utils'; import { UserRejectedOpError } from '../utils/exceptions'; import { signMessage as signMessageUtil } from '../utils/starknetUtils'; +import { AccountRpcController } from './abstract/account-rpc-controller'; export const SignMessageRequestStruct = assign( object({ diff --git a/packages/starknet-snap/src/rpcs/sign-transaction.ts b/packages/starknet-snap/src/rpcs/sign-transaction.ts index 24b152e9..d9954e3b 100644 --- a/packages/starknet-snap/src/rpcs/sign-transaction.ts +++ b/packages/starknet-snap/src/rpcs/sign-transaction.ts @@ -7,12 +7,12 @@ import { AddressStruct, AuthorizableStruct, BaseRequestStruct, - AccountRpcController, CallDataStruct, mapDeprecatedParams, } from '../utils'; import { UserRejectedOpError } from '../utils/exceptions'; import { signTransactions } from '../utils/starknetUtils'; +import { AccountRpcController } from './abstract/account-rpc-controller'; export const SignTransactionRequestStruct = assign( object({ diff --git a/packages/starknet-snap/src/rpcs/switch-network.ts b/packages/starknet-snap/src/rpcs/switch-network.ts index ae94048e..bb039885 100644 --- a/packages/starknet-snap/src/rpcs/switch-network.ts +++ b/packages/starknet-snap/src/rpcs/switch-network.ts @@ -3,8 +3,9 @@ import { assign, boolean } from 'superstruct'; import { NetworkStateManager } from '../state/network-state-manager'; import { renderSwitchNetworkUI } from '../ui/utils'; -import { AuthorizableStruct, BaseRequestStruct, RpcController } from '../utils'; +import { AuthorizableStruct, BaseRequestStruct } from '../utils'; import { InvalidNetworkError, UserRejectedOpError } from '../utils/exceptions'; +import { RpcController } from './abstract/base-rpc-controller'; export const SwitchNetworkRequestStruct = assign( AuthorizableStruct, diff --git a/packages/starknet-snap/src/rpcs/verify-signature.ts b/packages/starknet-snap/src/rpcs/verify-signature.ts index 5ce845e9..0eefd6fa 100644 --- a/packages/starknet-snap/src/rpcs/verify-signature.ts +++ b/packages/starknet-snap/src/rpcs/verify-signature.ts @@ -2,13 +2,9 @@ import { HexStruct } from '@metamask/utils'; import type { Infer } from 'superstruct'; import { object, assign, boolean, array } from 'superstruct'; -import { - AddressStruct, - TypeDataStruct, - BaseRequestStruct, - AccountRpcController, -} from '../utils'; +import { AddressStruct, TypeDataStruct, BaseRequestStruct } from '../utils'; import { verifyTypedDataMessageSignature } from '../utils/starknetUtils'; +import { AccountRpcController } from './abstract/account-rpc-controller'; export const VerifySignatureRequestStruct = assign( object({ diff --git a/packages/starknet-snap/src/rpcs/watch-asset.ts b/packages/starknet-snap/src/rpcs/watch-asset.ts index aa825d4e..d3582e11 100644 --- a/packages/starknet-snap/src/rpcs/watch-asset.ts +++ b/packages/starknet-snap/src/rpcs/watch-asset.ts @@ -7,7 +7,6 @@ import type { Erc20Token, Network } from '../types/snapState'; import { renderWatchAssetUI } from '../ui/utils'; import { BaseRequestStruct, - RpcController, AddressStruct, TokenNameStruct, TokenSymbolStruct, @@ -20,6 +19,7 @@ import { UserRejectedOpError, } from '../utils/exceptions'; import { getValidNumber } from '../utils/snapUtils'; +import { RpcController } from './abstract/base-rpc-controller'; export const WatchAssetRequestStruct = assign( object({ diff --git a/packages/starknet-snap/src/utils/rpc.test.ts b/packages/starknet-snap/src/utils/rpc.test.ts index d8f01ad7..1880bb72 100644 --- a/packages/starknet-snap/src/utils/rpc.test.ts +++ b/packages/starknet-snap/src/utils/rpc.test.ts @@ -1,24 +1,10 @@ -import { constants } from 'starknet'; -import { object, string } from 'superstruct'; -import type { Struct, Infer } from 'superstruct'; +import { object } from 'superstruct'; +import type { Struct } from 'superstruct'; -import type { StarknetAccount } from '../__tests__/helper'; -import { generateAccounts } from '../__tests__/helper'; -import type { SnapState } from '../types/snapState'; -import { STARKNET_SEPOLIA_TESTNET_NETWORK } from './constants'; import { InvalidRequestParamsError, UnknownError } from './exceptions'; -import { - AccountRpcController, - RpcController, - validateRequest, - validateResponse, -} from './rpc'; -import * as snapHelper from './snap'; -import * as snapUtils from './snapUtils'; -import * as starknetUtils from './starknetUtils'; +import { validateRequest, validateResponse } from './rpc'; import { AddressStruct } from './superstruct'; -jest.mock('./snap'); jest.mock('./logger'); const validateStruct = object({ @@ -65,135 +51,3 @@ describe('validateResponse', () => { ).toThrow(new UnknownError('Invalid Response')); }); }); - -describe('RpcController', () => { - class MockRpc extends RpcController { - protected requestStruct = string(); - - protected responseStruct = string(); - - // Set it to public to be able to spy on it - async handleRequest(params: string) { - return `done ${params}`; - } - } - - it('executes request', async () => { - const rpc = new MockRpc(); - - const result = await rpc.execute('test'); - - expect(result).toBe('done test'); - }); -}); - -describe('AccountRpcController', () => { - const state: SnapState = { - accContracts: [], - erc20Tokens: [], - networks: [STARKNET_SEPOLIA_TESTNET_NETWORK], - transactions: [], - }; - - const RequestStruct = object({ - address: string(), - chainId: string(), - }); - - type Request = Infer; - - class MockAccountRpc extends AccountRpcController { - protected requestStruct = RequestStruct; - - protected responseStruct = string(); - - // Set it to public to be able to spy on it - async handleRequest(param: Request) { - return `done ${param.address} and ${param.chainId}`; - } - } - - const mockAccount = async (network: constants.StarknetChainId) => { - const accounts = await generateAccounts(network, 1); - return accounts[0]; - }; - - const prepareExecute = async (account: StarknetAccount) => { - const verifyIfAccountNeedUpgradeOrDeploySpy = jest.spyOn( - snapUtils, - 'verifyIfAccountNeedUpgradeOrDeploy', - ); - - const getKeysFromAddressSpy = jest.spyOn( - starknetUtils, - 'getKeysFromAddress', - ); - - const getStateDataSpy = jest.spyOn(snapHelper, 'getStateData'); - - getStateDataSpy.mockResolvedValue(state); - - getKeysFromAddressSpy.mockResolvedValue({ - privateKey: account.privateKey, - publicKey: account.publicKey, - addressIndex: account.addressIndex, - derivationPath: account.derivationPath as unknown as any, - }); - - verifyIfAccountNeedUpgradeOrDeploySpy.mockReturnThis(); - - return { - getKeysFromAddressSpy, - getStateDataSpy, - verifyIfAccountNeedUpgradeOrDeploySpy, - }; - }; - - it('executes request', async () => { - const chainId = constants.StarknetChainId.SN_SEPOLIA; - const account = await mockAccount(chainId); - await prepareExecute(account); - const rpc = new MockAccountRpc(); - - const result = await rpc.execute({ - address: account.address, - chainId, - }); - - expect(result).toBe(`done ${account.address} and ${chainId}`); - }); - - it('fetchs account before execute', async () => { - const chainId = constants.StarknetChainId.SN_SEPOLIA; - const account = await mockAccount(chainId); - const { getKeysFromAddressSpy } = await prepareExecute(account); - const rpc = new MockAccountRpc(); - - await rpc.execute({ address: account.address, chainId }); - - expect(getKeysFromAddressSpy).toHaveBeenCalled(); - }); - - it.each([true, false])( - `assign verifyIfAccountNeedUpgradeOrDeploy's argument "showAlert" to %s if the constructor option 'showInvalidAccountAlert' is set to %s`, - async (showInvalidAccountAlert: boolean) => { - const chainId = constants.StarknetChainId.SN_SEPOLIA; - const account = await mockAccount(chainId); - const { verifyIfAccountNeedUpgradeOrDeploySpy } = await prepareExecute( - account, - ); - const rpc = new MockAccountRpc({ - showInvalidAccountAlert, - }); - - await rpc.execute({ address: account.address, chainId }); - - expect(verifyIfAccountNeedUpgradeOrDeploySpy).toHaveBeenCalledWith( - expect.any(Object), - account.address, - account.publicKey, - showInvalidAccountAlert, - ); - }, - ); -}); diff --git a/packages/starknet-snap/src/utils/rpc.ts b/packages/starknet-snap/src/utils/rpc.ts index 4d88e347..518b102a 100644 --- a/packages/starknet-snap/src/utils/rpc.ts +++ b/packages/starknet-snap/src/utils/rpc.ts @@ -1,17 +1,7 @@ -import type { getBIP44ChangePathString } from '@metamask/key-tree/dist/types/utils'; -import type { Json } from '@metamask/snaps-sdk'; import type { Struct } from 'superstruct'; import { assert } from 'superstruct'; -import type { Network, SnapState } from '../types/snapState'; import { InvalidRequestParamsError, UnknownError } from './exceptions'; -import { logger } from './logger'; -import { getBip44Deriver, getStateData } from './snap'; -import { - getNetworkFromChainId, - verifyIfAccountNeedUpgradeOrDeploy, -} from './snapUtils'; -import { getKeysFromAddress } from './starknetUtils'; /** * Validates that the request parameters conform to the expected structure defined by the provided struct. @@ -44,112 +34,3 @@ export function validateResponse(response: Params, struct: Struct) { throw new UnknownError('Invalid Response') as unknown as Error; } } - -export abstract class RpcController< - Request extends Json, - Response extends Json, -> { - /** - * Superstruct for the request. - */ - protected abstract requestStruct: Struct; - - /** - * Superstruct for the response. - */ - protected abstract responseStruct: Struct; - - protected abstract handleRequest(params: Request): Promise; - - protected async preExecute(params: Request): Promise { - logger.info(`Request: ${JSON.stringify(params)}`); - validateRequest(params, this.requestStruct); - } - - protected async postExecute(response: Response): Promise { - logger.info(`Response: ${JSON.stringify(response)}`); - validateResponse(response, this.responseStruct); - } - - /** - * A method to execute the rpc method. - * - * @param params - An struct contains the require parameter for the request. - * @returns A promise that resolves to an json. - */ - async execute(params: Request): Promise { - await this.preExecute(params); - const resp = await this.handleRequest(params); - await this.postExecute(resp); - return resp; - } -} - -// TODO: the Type should be moved to a common place -export type AccountRpcParams = { - chainId: string; - address: string; -}; - -// TODO: the Account object should move into a account manager for generate account -export type Account = { - privateKey: string; - publicKey: string; - addressIndex: number; - // This is the derivation path of the address, it is used in `getNextAddressIndex` to find the account in state where matching the same derivation path - derivationPath: ReturnType; -}; - -export type AccountRpcControllerOptions = { - showInvalidAccountAlert: boolean; -}; - -export abstract class AccountRpcController< - Request extends AccountRpcParams, - Response extends Json, -> extends RpcController { - protected account: Account; - - protected network: Network; - - protected options: AccountRpcControllerOptions; - - protected defaultOptions: AccountRpcControllerOptions = { - showInvalidAccountAlert: true, - }; - - constructor(options?: AccountRpcControllerOptions) { - super(); - this.options = Object.assign({}, this.defaultOptions, options); - } - - protected async preExecute(params: Request): Promise { - await super.preExecute(params); - - const { chainId, address } = params; - const { showInvalidAccountAlert } = this.options; - - const deriver = await getBip44Deriver(); - // TODO: Instead of getting the state directly, we should implement state management to consolidate the state fetching - const state = await getStateData(); - - // TODO: getNetworkFromChainId from state is still needed, due to it is supporting in get-starknet at this moment - this.network = getNetworkFromChainId(state, chainId); - - // TODO: This method should be refactored to get the account from an account manager - this.account = await getKeysFromAddress( - deriver, - this.network, - state, - address, - ); - - // TODO: rename this method to verifyAccount - await verifyIfAccountNeedUpgradeOrDeploy( - this.network, - address, - this.account.publicKey, - showInvalidAccountAlert, - ); - } -} From bab6f8b76cb6ec169b23242021352367669b5341 Mon Sep 17 00:00:00 2001 From: stanleyyuen <102275989+stanleyyconsensys@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:32:52 +0800 Subject: [PATCH 3/6] chore: revamp getTransactionStatus API --- .../starknet-snap/src/getTransactionStatus.ts | 35 ---------- .../src/rpcs/get-transaction-status.test.ts | 68 +++++++++++++++++++ .../src/rpcs/get-transaction-status.ts | 65 ++++++++++++++++++ packages/starknet-snap/src/types/snapApi.ts | 6 +- 4 files changed, 137 insertions(+), 37 deletions(-) delete mode 100644 packages/starknet-snap/src/getTransactionStatus.ts create mode 100644 packages/starknet-snap/src/rpcs/get-transaction-status.test.ts create mode 100644 packages/starknet-snap/src/rpcs/get-transaction-status.ts diff --git a/packages/starknet-snap/src/getTransactionStatus.ts b/packages/starknet-snap/src/getTransactionStatus.ts deleted file mode 100644 index 998a1165..00000000 --- a/packages/starknet-snap/src/getTransactionStatus.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { - ApiParams, - GetTransactionStatusRequestParams, -} from './types/snapApi'; -import { logger } from './utils/logger'; -import { toJson } from './utils/serializer'; -import { getNetworkFromChainId } from './utils/snapUtils'; -import * as utils from './utils/starknetUtils'; - -/** - * - * @param params - */ -export async function getTransactionStatus(params: ApiParams) { - try { - const { state, requestParams } = params; - const requestParamsObj = requestParams as GetTransactionStatusRequestParams; - - const { transactionHash } = requestParamsObj; - const network = getNetworkFromChainId(state, requestParamsObj.chainId); - - const getTxnStatusResp = await utils.getTransactionStatus( - transactionHash, - network, - ); - logger.log( - `getTransactionStatus:\ngetTxnStatusResp: ${toJson(getTxnStatusResp)}`, - ); - - return getTxnStatusResp; - } catch (error) { - logger.error(`Problem found:`, error); - throw error; - } -} diff --git a/packages/starknet-snap/src/rpcs/get-transaction-status.test.ts b/packages/starknet-snap/src/rpcs/get-transaction-status.test.ts new file mode 100644 index 00000000..07308f42 --- /dev/null +++ b/packages/starknet-snap/src/rpcs/get-transaction-status.test.ts @@ -0,0 +1,68 @@ +import { constants, TransactionExecutionStatus, TransactionFinalityStatus } from 'starknet'; + +import { Config } from '../config'; +import { NetworkStateManager } from '../state/network-state-manager'; +import { TokenStateManager } from '../state/token-state-manager'; +import type { Network } from '../types/snapState'; +import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../utils/constants'; +import { + InvalidRequestParamsError, + TokenIsPreloadedError, + InvalidNetworkError, + UserRejectedOpError, +} from '../utils/exceptions'; +import { prepareRenderWatchAssetUI } from './__tests__/helper'; +import type { GetTransactionStatusParams } from './get-transaction-status'; +import { getTransactionStatus } from './get-transaction-status'; +import { mockNetworkStateManager } from '../state/__tests__/helper'; + +import * as starknetUtils from '../utils/starknetUtils'; + +jest.mock('../utils/snap'); +jest.mock('../utils/logger'); + +describe('GetTransactionStatusRpc', () => { + + const prepareWatchAssetTest = ({ + network = STARKNET_SEPOLIA_TESTNET_NETWORK, + }: { + network?: Network; + }) => { + const { getNetworkSpy } = mockNetworkStateManager( + network, + ); + + const getTransactionStatusSpy = jest.spyOn(starknetUtils, 'getTransactionStatus') + + getTransactionStatusSpy.mockResolvedValue({ + finalityStatus: TransactionFinalityStatus.ACCEPTED_ON_L1, + executionStatus: TransactionExecutionStatus.SUCCEEDED + }) + + return { + getTransactionStatusSpy, + getNetworkSpy + }; + }; + + it('returns transaction status', async () => { + const network = STARKNET_SEPOLIA_TESTNET_NETWORK; + const { getNetworkSpy, getTransactionStatusSpy } = prepareWatchAssetTest({network}); + + const expectedResult = true; + + const result = await getTransactionStatus.execute({ + chainId: network.chainId as unknown as constants.StarknetChainId, + + }); + + expect(result).toStrictEqual(expectedResult); + }); + + + it('throws `InvalidRequestParamsError` when request parameter is not correct', async () => { + await expect( + watchAsset.execute({} as unknown as WatchAssetParams), + ).rejects.toThrow(InvalidRequestParamsError); + }); +}); diff --git a/packages/starknet-snap/src/rpcs/get-transaction-status.ts b/packages/starknet-snap/src/rpcs/get-transaction-status.ts new file mode 100644 index 00000000..24b2c769 --- /dev/null +++ b/packages/starknet-snap/src/rpcs/get-transaction-status.ts @@ -0,0 +1,65 @@ +import type { Infer } from 'superstruct'; +import { assign, nonempty, object, enums, optional } from 'superstruct'; + +import { + BaseRequestStruct, +} from '../utils'; +import { getTransactionStatus as getTransactionStatusFn } from '../utils/starknetUtils'; +import { ChainRpcController } from './abstract/chain-rpc-controller'; +import { HexStruct } from '@metamask/utils'; +import { TransactionExecutionStatus, TransactionFinalityStatus } from 'starknet'; + +export const GetTransactionStatusRequestStruct = assign( + object({ + transactionHash: nonempty(HexStruct), + }), + BaseRequestStruct, +); + +export const GetTransactionStatusResponseStruct =object({ + executionStatus: optional(enums(Object.values(TransactionExecutionStatus))), + finalityStatus: optional(enums(Object.values(TransactionFinalityStatus))), +}) + +export type GetTransactionStatusParams = Infer; + +export type GetTransactionStatusResponse = Infer; + +/** + * The RPC handler to get a transaction status by the given transaction hash. + */ +export class GetTransactionStatusRpc extends ChainRpcController< + GetTransactionStatusParams, + GetTransactionStatusResponse +> { + protected requestStruct = GetTransactionStatusRequestStruct; + + protected responseStruct = GetTransactionStatusResponseStruct; + + /** + * Execute the get transaction request handler. + * + * @param params - The parameters of the request. + * @param params.transactionHash - The transaction hash to enquire the transaction status. + * @param params.chainId - The chain id of the transaction. + * @returns A promise that resolves to a GetTransactionStatusResponse object. + */ + async execute(params: GetTransactionStatusParams): Promise { + return super.execute(params); + } + + protected async handleRequest( + params: GetTransactionStatusParams, + ): Promise { + const { transactionHash } = params; + + const resp = await getTransactionStatusFn( + transactionHash, + this.network, + ); + + return resp; + } +} + +export const getTransactionStatus = new GetTransactionStatusRpc(); diff --git a/packages/starknet-snap/src/types/snapApi.ts b/packages/starknet-snap/src/types/snapApi.ts index 639952de..45cb569d 100644 --- a/packages/starknet-snap/src/types/snapApi.ts +++ b/packages/starknet-snap/src/types/snapApi.ts @@ -7,6 +7,8 @@ import type { EstimateFeeDetails, DeployAccountSignerDetails, constants, + TransactionExecutionStatus, + TransactionFinalityStatus, } from 'starknet'; import type { SnapState, VoyagerTransactionType } from './snapState'; @@ -152,8 +154,8 @@ export type DeclareContractRequestParams = { } & BaseRequestParams; export type RpcV4GetTransactionReceiptResponse = { - execution_status?: string; - finality_status?: string; + execution_status?: TransactionExecutionStatus; + finality_status?: TransactionFinalityStatus; }; export type Authorizable = { From 9a9d50c1365aca0cab7e08009d1fabe7f95d5c77 Mon Sep 17 00:00:00 2001 From: stanleyyuen <102275989+stanleyyconsensys@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:34:47 +0800 Subject: [PATCH 4/6] chore: update get transaction status code --- packages/starknet-snap/src/index.tsx | 7 +- .../src/rpcs/get-transaction-status.test.ts | 72 ++++++++++--------- .../src/rpcs/get-transaction-status.ts | 44 ++++++------ packages/starknet-snap/src/rpcs/index.ts | 1 + 4 files changed, 69 insertions(+), 55 deletions(-) diff --git a/packages/starknet-snap/src/index.tsx b/packages/starknet-snap/src/index.tsx index 0d8f4585..0350a17a 100644 --- a/packages/starknet-snap/src/index.tsx +++ b/packages/starknet-snap/src/index.tsx @@ -24,7 +24,6 @@ import { getStoredNetworks } from './getStoredNetworks'; import { getStoredTransactions } from './getStoredTransactions'; import { getStoredUserAccounts } from './getStoredUserAccounts'; import { getTransactions } from './getTransactions'; -import { getTransactionStatus } from './getTransactionStatus'; import { getValue } from './getValue'; import { homePageController } from './on-home-page'; import { recoverAccounts } from './recoverAccounts'; @@ -40,6 +39,7 @@ import type { GetDeploymentDataParams, DeclareContractParams, WatchAssetParams, + GetTransactionStatusParams, } from './rpcs'; import { displayPrivateKey, @@ -53,6 +53,7 @@ import { switchNetwork, getDeploymentData, watchAsset, + getTransactionStatus, } from './rpcs'; import { sendTransaction } from './sendTransaction'; import { signDeployAccountTransaction } from './signDeployAccountTransaction'; @@ -203,7 +204,9 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { return await getErc20TokenBalance(apiParams); case 'starkNet_getTransactionStatus': - return await getTransactionStatus(apiParams); + return await getTransactionStatus.execute( + apiParams.requestParams as unknown as GetTransactionStatusParams, + ); case 'starkNet_sendTransaction': apiParams.keyDeriver = await getAddressKeyDeriver(snap); diff --git a/packages/starknet-snap/src/rpcs/get-transaction-status.test.ts b/packages/starknet-snap/src/rpcs/get-transaction-status.test.ts index 07308f42..07e76d46 100644 --- a/packages/starknet-snap/src/rpcs/get-transaction-status.test.ts +++ b/packages/starknet-snap/src/rpcs/get-transaction-status.test.ts @@ -1,68 +1,74 @@ -import { constants, TransactionExecutionStatus, TransactionFinalityStatus } from 'starknet'; +import type { constants } from 'starknet'; +import { + TransactionExecutionStatus, + TransactionFinalityStatus, +} from 'starknet'; -import { Config } from '../config'; -import { NetworkStateManager } from '../state/network-state-manager'; -import { TokenStateManager } from '../state/token-state-manager'; +import { mockNetworkStateManager } from '../state/__tests__/helper'; import type { Network } from '../types/snapState'; import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../utils/constants'; -import { - InvalidRequestParamsError, - TokenIsPreloadedError, - InvalidNetworkError, - UserRejectedOpError, -} from '../utils/exceptions'; -import { prepareRenderWatchAssetUI } from './__tests__/helper'; +import { InvalidRequestParamsError } from '../utils/exceptions'; +import * as starknetUtils from '../utils/starknetUtils'; import type { GetTransactionStatusParams } from './get-transaction-status'; import { getTransactionStatus } from './get-transaction-status'; -import { mockNetworkStateManager } from '../state/__tests__/helper'; - -import * as starknetUtils from '../utils/starknetUtils'; jest.mock('../utils/snap'); jest.mock('../utils/logger'); describe('GetTransactionStatusRpc', () => { - - const prepareWatchAssetTest = ({ - network = STARKNET_SEPOLIA_TESTNET_NETWORK, + const prepareGetTransactionStatusTest = ({ + network, + status, }: { - network?: Network; + network: Network; + status: { + finalityStatus: TransactionFinalityStatus; + executionStatus: TransactionExecutionStatus; + }; }) => { - const { getNetworkSpy } = mockNetworkStateManager( - network, + const { getNetworkSpy } = mockNetworkStateManager(network); + + const getTransactionStatusSpy = jest.spyOn( + starknetUtils, + 'getTransactionStatus', ); - const getTransactionStatusSpy = jest.spyOn(starknetUtils, 'getTransactionStatus') - - getTransactionStatusSpy.mockResolvedValue({ - finalityStatus: TransactionFinalityStatus.ACCEPTED_ON_L1, - executionStatus: TransactionExecutionStatus.SUCCEEDED - }) + getTransactionStatusSpy.mockResolvedValue(status); return { getTransactionStatusSpy, - getNetworkSpy + getNetworkSpy, }; }; it('returns transaction status', async () => { const network = STARKNET_SEPOLIA_TESTNET_NETWORK; - const { getNetworkSpy, getTransactionStatusSpy } = prepareWatchAssetTest({network}); - - const expectedResult = true; + const transactionHash = + '0x06385d46da9fbed4a5798298b17df069ac5f786e4c9f8f6b81c665540aea245a'; + const expectedResult = { + finalityStatus: TransactionFinalityStatus.ACCEPTED_ON_L1, + executionStatus: TransactionExecutionStatus.SUCCEEDED, + }; + const { getTransactionStatusSpy } = prepareGetTransactionStatusTest({ + network, + status: expectedResult, + }); const result = await getTransactionStatus.execute({ chainId: network.chainId as unknown as constants.StarknetChainId, - + transactionHash, }); expect(result).toStrictEqual(expectedResult); + expect(getTransactionStatusSpy).toHaveBeenCalledWith( + transactionHash, + network, + ); }); - it('throws `InvalidRequestParamsError` when request parameter is not correct', async () => { await expect( - watchAsset.execute({} as unknown as WatchAssetParams), + getTransactionStatus.execute({} as unknown as GetTransactionStatusParams), ).rejects.toThrow(InvalidRequestParamsError); }); }); diff --git a/packages/starknet-snap/src/rpcs/get-transaction-status.ts b/packages/starknet-snap/src/rpcs/get-transaction-status.ts index 24b2c769..7f8ed16f 100644 --- a/packages/starknet-snap/src/rpcs/get-transaction-status.ts +++ b/packages/starknet-snap/src/rpcs/get-transaction-status.ts @@ -1,13 +1,14 @@ -import type { Infer } from 'superstruct'; -import { assign, nonempty, object, enums, optional } from 'superstruct'; - +import { HexStruct } from '@metamask/utils'; import { - BaseRequestStruct, -} from '../utils'; + TransactionExecutionStatus, + TransactionFinalityStatus, +} from 'starknet'; +import type { Infer } from 'superstruct'; +import { assign, nonempty, object, enums, optional } from 'superstruct'; + +import { BaseRequestStruct } from '../utils'; import { getTransactionStatus as getTransactionStatusFn } from '../utils/starknetUtils'; import { ChainRpcController } from './abstract/chain-rpc-controller'; -import { HexStruct } from '@metamask/utils'; -import { TransactionExecutionStatus, TransactionFinalityStatus } from 'starknet'; export const GetTransactionStatusRequestStruct = assign( object({ @@ -16,14 +17,18 @@ export const GetTransactionStatusRequestStruct = assign( BaseRequestStruct, ); -export const GetTransactionStatusResponseStruct =object({ - executionStatus: optional(enums(Object.values(TransactionExecutionStatus))), - finalityStatus: optional(enums(Object.values(TransactionFinalityStatus))), -}) +export const GetTransactionStatusResponseStruct = object({ + executionStatus: optional(enums(Object.values(TransactionExecutionStatus))), + finalityStatus: optional(enums(Object.values(TransactionFinalityStatus))), +}); -export type GetTransactionStatusParams = Infer; +export type GetTransactionStatusParams = Infer< + typeof GetTransactionStatusRequestStruct +>; -export type GetTransactionStatusResponse = Infer; +export type GetTransactionStatusResponse = Infer< + typeof GetTransactionStatusResponseStruct +>; /** * The RPC handler to get a transaction status by the given transaction hash. @@ -42,9 +47,11 @@ export class GetTransactionStatusRpc extends ChainRpcController< * @param params - The parameters of the request. * @param params.transactionHash - The transaction hash to enquire the transaction status. * @param params.chainId - The chain id of the transaction. - * @returns A promise that resolves to a GetTransactionStatusResponse object. + * @returns A promise that resolves to a GetTransactionStatusResponse object that contains executionStatus and finalityStatus. */ - async execute(params: GetTransactionStatusParams): Promise { + async execute( + params: GetTransactionStatusParams, + ): Promise { return super.execute(params); } @@ -53,11 +60,8 @@ export class GetTransactionStatusRpc extends ChainRpcController< ): Promise { const { transactionHash } = params; - const resp = await getTransactionStatusFn( - transactionHash, - this.network, - ); - + const resp = await getTransactionStatusFn(transactionHash, this.network); + return resp; } } diff --git a/packages/starknet-snap/src/rpcs/index.ts b/packages/starknet-snap/src/rpcs/index.ts index 09e65cf3..609edb5d 100644 --- a/packages/starknet-snap/src/rpcs/index.ts +++ b/packages/starknet-snap/src/rpcs/index.ts @@ -9,3 +9,4 @@ export * from './verify-signature'; export * from './switch-network'; export * from './get-deployment-data'; export * from './watch-asset'; +export * from './get-transaction-status'; From 88bf530bed2b746f3e7dd62809057c015baf1ddc Mon Sep 17 00:00:00 2001 From: stanleyyuen <102275989+stanleyyconsensys@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:37:44 +0800 Subject: [PATCH 5/6] chore: remove mocha test --- .../test/src/getTransactionStatus.test.ts | 69 ------------------- 1 file changed, 69 deletions(-) delete mode 100644 packages/starknet-snap/test/src/getTransactionStatus.test.ts diff --git a/packages/starknet-snap/test/src/getTransactionStatus.test.ts b/packages/starknet-snap/test/src/getTransactionStatus.test.ts deleted file mode 100644 index cc3b67fc..00000000 --- a/packages/starknet-snap/test/src/getTransactionStatus.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import chai, { expect } from 'chai'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import { WalletMock } from '../wallet.mock.test'; -import * as utils from '../../src/utils/starknetUtils'; -import { getTransactionStatus } from '../../src/getTransactionStatus'; -import { SnapState } from '../../src/types/snapState'; -import { STARKNET_MAINNET_NETWORK } from '../../src/utils/constants'; -import { getTxnStatusResp } from '../constants.test'; -import { Mutex } from 'async-mutex'; -import { - ApiParams, - GetTransactionStatusRequestParams, -} from '../../src/types/snapApi'; - -chai.use(sinonChai); -const sandbox = sinon.createSandbox(); - -describe('Test function: getTransactionStatus', function () { - const walletStub = new WalletMock(); - const state: SnapState = { - accContracts: [], - erc20Tokens: [], - networks: [STARKNET_MAINNET_NETWORK], - transactions: [], - }; - const apiParams: ApiParams = { - state, - requestParams: {}, - wallet: walletStub, - saveMutex: new Mutex(), - }; - - afterEach(function () { - walletStub.reset(); - sandbox.restore(); - }); - - it('should get the transaction status correctly', async function () { - sandbox.stub(utils, 'getTransactionStatus').callsFake(async () => { - return getTxnStatusResp; - }); - const requestObject: GetTransactionStatusRequestParams = { - transactionHash: - '0x27f204588cadd08a7914f6a9808b34de0cbfc4cb53aa053663e7fd3a34dbc26', - }; - apiParams.requestParams = requestObject; - const result = await getTransactionStatus(apiParams); - expect(result).to.be.eq(getTxnStatusResp); - }); - - it('should throw error if getTransactionStatus failed', async function () { - sandbox.stub(utils, 'getTransactionStatus').throws(new Error()); - const requestObject: GetTransactionStatusRequestParams = { - transactionHash: - '0x27f204588cadd08a7914f6a9808b34de0cbfc4cb53aa053663e7fd3a34dbc26', - }; - apiParams.requestParams = requestObject; - - let result; - try { - await getTransactionStatus(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); -}); From 7c6de30bd64efcf0ce000c2d54233d4f15ffec4c Mon Sep 17 00:00:00 2001 From: stanleyyuen <102275989+stanleyyconsensys@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:52:42 +0800 Subject: [PATCH 6/6] chore: remove get transactions test --- .../test/src/getTransactions.test.ts | 276 ------------------ 1 file changed, 276 deletions(-) delete mode 100644 packages/starknet-snap/test/src/getTransactions.test.ts diff --git a/packages/starknet-snap/test/src/getTransactions.test.ts b/packages/starknet-snap/test/src/getTransactions.test.ts deleted file mode 100644 index 5688c26c..00000000 --- a/packages/starknet-snap/test/src/getTransactions.test.ts +++ /dev/null @@ -1,276 +0,0 @@ -import chai, { expect } from 'chai'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import { WalletMock } from '../wallet.mock.test'; -import * as utils from '../../src/utils/starknetUtils'; -import * as snapUtils from '../../src/utils/snapUtils'; -import { SnapState, Transaction } from '../../src/types/snapState'; -import { - STARKNET_SEPOLIA_TESTNET_NETWORK, - STARKNET_MAINNET_NETWORK, -} from '../../src/utils/constants'; -import { - createAccountProxyTxn, - expectedMassagedTxn4, - expectedMassagedTxn5, - expectedMassagedTxns, - getTxnFromSequencerResp1, - getTxnFromSequencerResp2, - getTxnStatusAcceptL2Resp, - getTxnStatusResp, - getTxnsFromVoyagerResp, - unsettedTransactionInMassagedTxn, - initAccountTxn, - txn1, - txn2, - txn3, - txn4, - txn5, - mainnetTxn1, -} from '../constants.test'; -import { getTransactions, updateStatus } from '../../src/getTransactions'; -import { Mutex } from 'async-mutex'; -import { - ApiParams, - GetTransactionsRequestParams, -} from '../../src/types/snapApi'; -import { GetTransactionResponse, num } from 'starknet'; -import { VoyagerTransactions } from '../../src/types/voyager'; -import { TransactionStatuses } from '../../src/types/starknet'; - -chai.use(sinonChai); -const sandbox = sinon.createSandbox(); -describe('Test function: getTransactions', function () { - const walletStub = new WalletMock(); - let getTransactionStatusStub: sinon.SinonStub; - const state: SnapState = { - accContracts: [], - erc20Tokens: [], - networks: [STARKNET_SEPOLIA_TESTNET_NETWORK, STARKNET_MAINNET_NETWORK], - transactions: [ - { ...unsettedTransactionInMassagedTxn }, - { ...txn1 }, - { ...txn2 }, - { ...txn3 }, - { ...txn4 }, - { ...txn5 }, - { ...mainnetTxn1 }, - { ...createAccountProxyTxn }, - { ...initAccountTxn }, - ], - }; - const apiParams: ApiParams = { - state, - requestParams: {}, - wallet: walletStub, - saveMutex: new Mutex(), - }; - - beforeEach(function () { - sandbox.useFakeTimers(1653553083147); - sandbox.stub(utils, 'getTransactionsFromVoyager').callsFake(async () => { - return getTxnsFromVoyagerResp as unknown as VoyagerTransactions; - }); - sandbox.stub(utils, 'getTransaction').callsFake(async (...args) => { - if (args?.[0] === getTxnsFromVoyagerResp.items[0].hash) { - return getTxnFromSequencerResp1 as unknown as GetTransactionResponse; - } else if (args?.[0] === getTxnsFromVoyagerResp.items[1].hash) { - return getTxnFromSequencerResp2 as unknown as GetTransactionResponse; - } else { - return null as unknown as GetTransactionResponse; - } - }); - getTransactionStatusStub = sandbox - .stub(utils, 'getTransactionStatus') - .callsFake(async (...args) => { - if (args?.[0] === getTxnsFromVoyagerResp.items[0].hash) { - return getTxnStatusResp as unknown as TransactionStatuses; - } else if (args?.[0] === getTxnsFromVoyagerResp.items[1].hash) { - return getTxnStatusResp as unknown as TransactionStatuses; - } else if (args?.[0] === expectedMassagedTxn5.txnHash) { - return undefined as unknown as TransactionStatuses; - } - return getTxnStatusAcceptL2Resp as unknown as TransactionStatuses; - }); - walletStub.rpcStubs.snap_manageState.resolves(state); - }); - - afterEach(function () { - walletStub.reset(); - sandbox.restore(); - }); - - it('should get the transactions from Voyager of testnet correctly', async function () { - const requestObject: GetTransactionsRequestParams = { - senderAddress: txn4.senderAddress, - pageSize: '10', - chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, - }; - apiParams.requestParams = requestObject; - - const result = await getTransactions(apiParams); - - expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; - expect(result.length).to.be.eq(4); - expect(result).to.be.eql(expectedMassagedTxns); - }); - - it('should merge the transactions stored in snap state correctly', async function () { - const requestObject: GetTransactionsRequestParams = { - senderAddress: txn4.senderAddress, - pageSize: '10', - chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, - }; - apiParams.requestParams = requestObject; - - const result = await getTransactions(apiParams); - const mergeTxn = result.find( - (e) => - num.toBigInt(e.txnHash) === - num.toBigInt(unsettedTransactionInMassagedTxn.txnHash), - ); - expect(getTransactionStatusStub.callCount).to.be.eq(4); - expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; - expect(mergeTxn).not.to.be.undefined; - if (mergeTxn !== undefined) { - expect(mergeTxn.status).to.be.eq(''); - expect(mergeTxn.finalityStatus).to.be.eq(getTxnStatusResp.finalityStatus); - expect(mergeTxn.executionStatus).to.be.eq( - getTxnStatusResp.executionStatus, - ); - } - expect(result.length).to.be.eq(4); - expect(result).to.be.eql(expectedMassagedTxns); - }); - - it('should get the transactions of testnet stored in snap state correctly', async function () { - const requestObject: GetTransactionsRequestParams = { - senderAddress: txn4.senderAddress, - pageSize: '10', - onlyFromState: true, - chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, - }; - apiParams.requestParams = requestObject; - const result = await getTransactions(apiParams); - expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; - expect(result.length).to.be.eq(2); - expect(result).to.be.eql([expectedMassagedTxn5, expectedMassagedTxn4]); - }); - - it('should get the transactions with deploy txn from Voyager of testnet correctly', async function () { - const requestObject: GetTransactionsRequestParams = { - senderAddress: txn4.senderAddress, - pageSize: '10', - withDeployTxn: true, - chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, - }; - apiParams.requestParams = requestObject; - const result = await getTransactions(apiParams); - expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; - expect(result.length).to.be.eq(4); - expect(result).to.be.eql(expectedMassagedTxns); - }); - - it('should throw error if upsertTransactions failed', async function () { - sandbox.stub(snapUtils, 'upsertTransactions').throws(new Error()); - const requestObject: GetTransactionsRequestParams = { - senderAddress: txn4.senderAddress, - pageSize: '10', - withDeployTxn: true, - chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, - }; - apiParams.requestParams = requestObject; - - let result; - try { - await getTransactions(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - - it('should throw an error if the sender address is an invalid address', async function () { - const requestObject: GetTransactionsRequestParams = { - senderAddress: 'wrongAddress', - pageSize: '10', - withDeployTxn: true, - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await getTransactions(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - - it('should throw an error if the contract address is an invalid address', async function () { - const requestObject: GetTransactionsRequestParams = { - senderAddress: txn4.senderAddress, - pageSize: '10', - withDeployTxn: true, - contractAddress: 'wrongAddress', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await getTransactions(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); -}); - -describe('Test function: getTransactions.updateStatus', function () { - let getTransactionStatusStub: sinon.SinonStub; - let txns: Transaction[] = []; - beforeEach(function () { - txns = [{ ...unsettedTransactionInMassagedTxn }]; - getTransactionStatusStub = sandbox - .stub(utils, 'getTransactionStatus') - .callsFake(async () => { - return getTxnStatusAcceptL2Resp; - }); - }); - - afterEach(function () { - sandbox.restore(); - }); - - it('should update status correctly', async function () { - await updateStatus(txns[0], STARKNET_SEPOLIA_TESTNET_NETWORK); - expect(getTransactionStatusStub.callCount).to.be.eq(1); - expect(txns[0].finalityStatus).to.be.eq( - getTxnStatusAcceptL2Resp.finalityStatus, - ); - expect(txns[0].executionStatus).to.be.eq( - getTxnStatusAcceptL2Resp.executionStatus, - ); - expect(txns[0].status).to.be.eq(''); - }); - - describe('when getTransactionStatus throw error', function () { - beforeEach(function () { - sandbox.restore(); - getTransactionStatusStub = sandbox - .stub(utils, 'getTransactionStatus') - .throws(new Error()); - }); - it('should not throw error', async function () { - await updateStatus(txns[0], STARKNET_SEPOLIA_TESTNET_NETWORK); - expect(txns[0].finalityStatus).to.be.eq( - unsettedTransactionInMassagedTxn.finalityStatus, - ); - expect(txns[0].executionStatus).to.be.eq( - unsettedTransactionInMassagedTxn.executionStatus, - ); - expect(txns[0].status).to.be.eq(unsettedTransactionInMassagedTxn.status); - }); - }); -});