From 5617ccf85af58943313ef81bf3a03deed0c4eb0f Mon Sep 17 00:00:00 2001 From: khanti42 Date: Tue, 22 Oct 2024 11:02:40 +0200 Subject: [PATCH] refactor(DeclareContract): revamp RPC starkNet_declareContract (#398) * feat: refactor RPC starkNet_declareContract to have superstruct validation * Update packages/starknet-snap/src/rpcs/declare-contract.ts Co-authored-by: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com> * Update packages/starknet-snap/src/rpcs/declare-contract.ts Co-authored-by: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com> * Update packages/starknet-snap/src/rpcs/declare-contract.ts Co-authored-by: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com> * Update packages/starknet-snap/src/rpcs/declare-contract.ts Co-authored-by: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com> * Update packages/starknet-snap/src/rpcs/declare-contract.ts Co-authored-by: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com> * Update packages/starknet-snap/src/rpcs/declare-contract.ts Co-authored-by: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com> * chore: fix comments * Update packages/starknet-snap/src/rpcs/declare-contract.test.ts Co-authored-by: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com> * Update packages/starknet-snap/src/rpcs/declare-contract.test.ts Co-authored-by: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com> * Update packages/starknet-snap/src/rpcs/declare-contract.test.ts Co-authored-by: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com> * Update packages/starknet-snap/src/rpcs/declare-contract.test.ts Co-authored-by: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com> * chore: fix comments on tests * chore: add generateRandomFee to standardize fee generation in tests * chore: use network from prepareMockDeclareContract * chore: use existing generateRandomValue helper --------- Co-authored-by: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com> --- packages/starknet-snap/src/declareContract.ts | 75 ------ packages/starknet-snap/src/index.ts | 8 +- .../src/rpcs/__tests__/helper.ts | 26 +- .../src/rpcs/declare-contract.test.ts | 229 ++++++++++++++++++ .../src/rpcs/declare-contract.ts | 208 ++++++++++++++++ .../src/rpcs/execute-txn.test.ts | 5 +- packages/starknet-snap/src/rpcs/index.ts | 1 + .../starknet-snap/src/utils/superstruct.ts | 2 +- .../test/src/declareContract.test.ts | 146 ----------- 9 files changed, 471 insertions(+), 229 deletions(-) delete mode 100644 packages/starknet-snap/src/declareContract.ts create mode 100644 packages/starknet-snap/src/rpcs/declare-contract.test.ts create mode 100644 packages/starknet-snap/src/rpcs/declare-contract.ts delete mode 100644 packages/starknet-snap/test/src/declareContract.test.ts diff --git a/packages/starknet-snap/src/declareContract.ts b/packages/starknet-snap/src/declareContract.ts deleted file mode 100644 index 583e244e..00000000 --- a/packages/starknet-snap/src/declareContract.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { heading, panel, DialogType } from '@metamask/snaps-sdk'; - -import type { - ApiParamsWithKeyDeriver, - DeclareContractRequestParams, -} from './types/snapApi'; -import { logger } from './utils/logger'; -import { toJson } from './utils/serializer'; -import { - getNetworkFromChainId, - getDeclareSnapTxt, - verifyIfAccountNeedUpgradeOrDeploy, -} from './utils/snapUtils'; -import { - getKeysFromAddress, - declareContract as declareContractUtil, -} from './utils/starknetUtils'; - -/** - * - * @param params - */ -export async function declareContract(params: ApiParamsWithKeyDeriver) { - try { - const { state, keyDeriver, requestParams, wallet } = params; - - const requestParamsObj = requestParams as DeclareContractRequestParams; - - logger.log(`executeTxn params: ${toJson(requestParamsObj, 2)}}`); - - const { senderAddress } = requestParamsObj; - const network = getNetworkFromChainId(state, requestParamsObj.chainId); - const { privateKey, publicKey } = await getKeysFromAddress( - keyDeriver, - network, - state, - senderAddress, - ); - - await verifyIfAccountNeedUpgradeOrDeploy(network, senderAddress, publicKey); - - const snapComponents = getDeclareSnapTxt( - senderAddress, - network, - requestParamsObj.contractPayload, - requestParamsObj.invocationsDetails, - ); - - const response = await wallet.request({ - method: 'snap_dialog', - params: { - type: DialogType.Confirmation, - content: panel([ - heading('Do you want to sign this transaction?'), - ...snapComponents, - ]), - }, - }); - - if (!response) { - return false; - } - - return await declareContractUtil( - network, - senderAddress, - privateKey, - requestParamsObj.contractPayload, - requestParamsObj.invocationsDetails, - ); - } catch (error) { - logger.error(`Problem found:`, error); - throw error; - } -} diff --git a/packages/starknet-snap/src/index.ts b/packages/starknet-snap/src/index.ts index 80a5349c..6c71914b 100644 --- a/packages/starknet-snap/src/index.ts +++ b/packages/starknet-snap/src/index.ts @@ -9,7 +9,6 @@ import { panel, text, MethodNotFoundError } from '@metamask/snaps-sdk'; import { addNetwork } from './addNetwork'; import { Config } from './config'; import { createAccount } from './createAccount'; -import { declareContract } from './declareContract'; import { estimateAccDeployFee } from './estimateAccountDeployFee'; import { estimateFees } from './estimateFees'; import { extractPublicKey } from './extractPublicKey'; @@ -35,12 +34,14 @@ import type { VerifySignatureParams, SwitchNetworkParams, GetDeploymentDataParams, + DeclareContractParams, WatchAssetParams, } from './rpcs'; import { displayPrivateKey, estimateFee, executeTxn, + declareContract, signMessage, signTransaction, signDeclareTransaction, @@ -276,9 +277,8 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { ); case 'starkNet_declareContract': - apiParams.keyDeriver = await getAddressKeyDeriver(snap); - return await declareContract( - apiParams as unknown as ApiParamsWithKeyDeriver, + return await declareContract.execute( + apiParams as unknown as DeclareContractParams, ); case 'starkNet_getStarkName': diff --git a/packages/starknet-snap/src/rpcs/__tests__/helper.ts b/packages/starknet-snap/src/rpcs/__tests__/helper.ts index 06734f23..70c08cd8 100644 --- a/packages/starknet-snap/src/rpcs/__tests__/helper.ts +++ b/packages/starknet-snap/src/rpcs/__tests__/helper.ts @@ -1,7 +1,8 @@ +import { BigNumber } from 'ethers'; import type { constants } from 'starknet'; import type { StarknetAccount } from '../../__tests__/helper'; -import { generateAccounts } from '../../__tests__/helper'; +import { generateAccounts, generateRandomValue } from '../../__tests__/helper'; import type { SnapState } from '../../types/snapState'; import * as snapHelper from '../../utils/snap'; import * as snapUtils from '../../utils/snapUtils'; @@ -82,3 +83,26 @@ export const buildDividerComponent = () => { type: 'divider', }; }; + +/** + * + * @param min + * @param max + * @param useBigInt + */ +export function generateRandomFee( + min = '100000000000000', + max = '1000000000000000', + useBigInt = false, +) { + const minFee = BigInt(min); + const maxFee = BigInt(max); + const randomFactor = generateRandomValue(); + const randomFee = BigInt( + Math.max(Number(minFee), Math.floor(randomFactor * Number(maxFee))), + ); + + return useBigInt + ? randomFee.toString(10) + : BigNumber.from(randomFee).toString(); +} diff --git a/packages/starknet-snap/src/rpcs/declare-contract.test.ts b/packages/starknet-snap/src/rpcs/declare-contract.test.ts new file mode 100644 index 00000000..4f916a9b --- /dev/null +++ b/packages/starknet-snap/src/rpcs/declare-contract.test.ts @@ -0,0 +1,229 @@ +import { utils } from 'ethers'; +import type { Abi, UniversalDetails } from 'starknet'; +import { constants } from 'starknet'; +import type { Infer } from 'superstruct'; + +import { toJson, type DeclareContractPayloadStruct } from '../utils'; +import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../utils/constants'; +import { + UserRejectedOpError, + InvalidRequestParamsError, + UnknownError, +} from '../utils/exceptions'; +import * as starknetUtils from '../utils/starknetUtils'; +import { + buildDividerComponent, + buildRowComponent, + generateRandomFee, + mockAccount, + prepareConfirmDialog, + prepareMockAccount, +} from './__tests__/helper'; +import { declareContract } from './declare-contract'; +import type { + DeclareContractParams, + DeclareContractResponse, +} from './declare-contract'; + +jest.mock('../utils/snap'); +jest.mock('../utils/logger'); + +type DeclareContractPayload = Infer; + +// Helper function to generate the expected DeclareContractPayload +const generateExpectedDeclareTransactionPayload = + (): DeclareContractPayload => ({ + compiledClassHash: '0xcompiledClassHash', + classHash: '0xclassHash', + contract: { + // eslint-disable-next-line @typescript-eslint/naming-convention + sierra_program: ['0x1', '0x2'], + // eslint-disable-next-line @typescript-eslint/naming-convention + contract_class_version: '1.0.0', + // eslint-disable-next-line @typescript-eslint/naming-convention + entry_points_by_type: { + // eslint-disable-next-line @typescript-eslint/naming-convention + CONSTRUCTOR: [{ selector: '0xconstructorSelector', function_idx: 0 }], + // eslint-disable-next-line @typescript-eslint/naming-convention + EXTERNAL: [{ selector: '0xexternalSelector', function_idx: 1 }], + // eslint-disable-next-line @typescript-eslint/naming-convention + L1_HANDLER: [{ selector: '0xhandlerSelector', function_idx: 2 }], + }, + abi: '[{"type":"function","name":"transfer"}]' as unknown as Abi, + }, + }); + +const prepareMockDeclareContract = async ( + transactionHash: string, + payload: DeclareContractPayload, + details: UniversalDetails, +) => { + const state = { + accContracts: [], + erc20Tokens: [], + networks: [STARKNET_SEPOLIA_TESTNET_NETWORK], + transactions: [], + }; + const { confirmDialogSpy } = prepareConfirmDialog(); + + const account = await mockAccount(constants.StarknetChainId.SN_SEPOLIA); + prepareMockAccount(account, state); + + const request = { + chainId: state.networks[0].chainId as unknown as constants.StarknetChainId, + address: account.address, + payload, + details, + }; + + const declareContractRespMock: DeclareContractResponse = { + // eslint-disable-next-line @typescript-eslint/naming-convention + transaction_hash: transactionHash, + // eslint-disable-next-line @typescript-eslint/naming-convention + class_hash: '0x123456789abcdef', + }; + + const declareContractUtilSpy = jest.spyOn(starknetUtils, 'declareContract'); + declareContractUtilSpy.mockResolvedValue(declareContractRespMock); + + return { + network: state.networks[0], + account, + request, + confirmDialogSpy, + declareContractRespMock, + declareContractUtilSpy, + }; +}; + +describe('DeclareContractRpc', () => { + it('declares a contract correctly if user confirms the dialog', async () => { + const payload = generateExpectedDeclareTransactionPayload(); + const details = { + maxFee: generateRandomFee('1000000000000000', '2000000000000000'), + }; + const transactionHash = '0x123'; + + const { + account, + request, + network, + declareContractRespMock, + confirmDialogSpy, + declareContractUtilSpy, + } = await prepareMockDeclareContract(transactionHash, payload, details); + + confirmDialogSpy.mockResolvedValue(true); + + const result = await declareContract.execute(request); + + expect(result).toStrictEqual(declareContractRespMock); + expect(declareContractUtilSpy).toHaveBeenCalledWith( + network, + account.address, + account.privateKey, + request.payload, + request.details, + ); + }); + + it('throws UserRejectedOpError if user cancels the dialog', async () => { + const payload = generateExpectedDeclareTransactionPayload(); + const details = { + maxFee: generateRandomFee('1000000000000000', '2000000000000000'), + }; + const transactionHash = + '0x07f901c023bac6c874691244c4c2332c6825b916fb68d240c807c6156db84fd3'; + + const { request, confirmDialogSpy } = await prepareMockDeclareContract( + transactionHash, + payload, + details, + ); + confirmDialogSpy.mockResolvedValue(false); + + await expect(declareContract.execute(request)).rejects.toThrow( + UserRejectedOpError, + ); + }); + + it('throws `InvalidRequestParamsError` when request parameter is not correct', async () => { + await expect( + declareContract.execute({} as unknown as DeclareContractParams), + ).rejects.toThrow(InvalidRequestParamsError); + }); + + it.each([ + { + testCase: 'class_hash is missing', + declareContractRespMock: { + // eslint-disable-next-line @typescript-eslint/naming-convention + transaction_hash: '0x123', + }, + }, + { + testCase: 'transaction_hash is missing', + declareContractRespMock: { + // eslint-disable-next-line @typescript-eslint/naming-convention + class_hash: '0x123456789abcdef', + }, + }, + { + testCase: 'empty object is returned', + declareContractRespMock: {}, + }, + ])( + 'throws `Unknown Error` when $testCase', + async ({ declareContractRespMock }) => { + const payload = generateExpectedDeclareTransactionPayload(); + const details = { + maxFee: generateRandomFee('1000000000000000', '2000000000000000'), + }; + const transactionHash = '0x123'; + + const { request, declareContractUtilSpy } = + await prepareMockDeclareContract(transactionHash, payload, details); + + declareContractUtilSpy.mockResolvedValue( + declareContractRespMock as unknown as DeclareContractResponse, + ); + + await expect(declareContract.execute(request)).rejects.toThrow( + UnknownError, + ); + }, + ); + + it('renders confirmation dialog', async () => { + const payload = generateExpectedDeclareTransactionPayload(); + const details = { + maxFee: generateRandomFee('1000000000000000', '2000000000000000'), + }; + // Convert maxFee to ETH from Wei + const maxFeeInEth = utils.formatUnits(details.maxFee, 'ether'); + const transactionHash = '0x123'; + + const { request, network, confirmDialogSpy, account } = + await prepareMockDeclareContract(transactionHash, payload, details); + + await declareContract.execute(request); + + expect(confirmDialogSpy).toHaveBeenCalledWith([ + { + type: 'heading', + value: 'Do you want to sign this transaction?', + }, + buildRowComponent('Signer Address', account.address), + buildDividerComponent(), + buildRowComponent('Network', network.name), + buildDividerComponent(), + buildRowComponent('Contract', toJson(payload.contract)), + buildDividerComponent(), + buildRowComponent('Compiled Class Hash', payload.compiledClassHash ?? ''), + buildDividerComponent(), + buildRowComponent('Class Hash', payload.classHash ?? ''), + buildDividerComponent(), + buildRowComponent('Max Fee (ETH)', maxFeeInEth), + ]); + }); +}); diff --git a/packages/starknet-snap/src/rpcs/declare-contract.ts b/packages/starknet-snap/src/rpcs/declare-contract.ts new file mode 100644 index 00000000..533549cb --- /dev/null +++ b/packages/starknet-snap/src/rpcs/declare-contract.ts @@ -0,0 +1,208 @@ +import type { Component } from '@metamask/snaps-sdk'; +import { heading, divider, row, text } from '@metamask/snaps-sdk'; +import convert from 'ethereum-unit-converter'; +import type { Infer } from 'superstruct'; +import { assign, object, optional, string } from 'superstruct'; + +import { + AddressStruct, + BaseRequestStruct, + DeclareContractPayloadStruct, + mapDeprecatedParams, + UniversalDetailsStruct, + confirmDialog, + AccountRpcController, + toJson, +} from '../utils'; +import { UserRejectedOpError } from '../utils/exceptions'; +import { declareContract as declareContractUtil } from '../utils/starknetUtils'; + +// Define the DeclareContractRequestStruct +export const DeclareContractRequestStruct = assign( + object({ + address: AddressStruct, // Sender address + payload: DeclareContractPayloadStruct, // Contract payload structure + details: optional(UniversalDetailsStruct), // Optional invocation details + }), + BaseRequestStruct, // Base request struct, could include chainId, etc. +); + +export const DeclareContractResponseStruct = object({ + // eslint-disable-next-line @typescript-eslint/naming-convention + transaction_hash: string(), + // eslint-disable-next-line @typescript-eslint/naming-convention + class_hash: string(), +}); + +export type DeclareContractParams = Infer; +export type DeclareContractResponse = Infer< + typeof DeclareContractResponseStruct +>; + +/** + * The RPC handler to declare a contract. + */ +export class DeclareContractRpc extends AccountRpcController< + DeclareContractParams, + DeclareContractResponse +> { + protected requestStruct = DeclareContractRequestStruct; + + protected responseStruct = DeclareContractResponseStruct; + + protected async preExecute(params: DeclareContractParams): Promise { + // Define mappings to ensure backward compatibility with previous versions of the API. + // These mappings replace deprecated parameter names with the updated equivalents, + // allowing older integrations to function without changes + const paramMappings: Record = { + senderAddress: 'address', + invocationsDetails: 'details', + contractPayload: 'payload', + }; + + // Apply the mappings to params + mapDeprecatedParams(params, paramMappings); + await super.preExecute(params); + } + + /** + * Execute the declare contract request handler. + * It will show a confirmation dialog to the user before signing the contract declaration. + * + * @param params - The parameters of the request. + * @param params.address - The address of the request account. + * @param params.payload - The contract payload of the declare transaction. + * @param [params.details] - The declare transaction details. + * @param params.chainId - The chain id of the network. + * @returns A Promise that resolve the `DeclareContractResponse` object. + */ + async execute( + params: DeclareContractParams, + ): Promise { + return super.execute(params); + } + + protected async handleRequest( + params: DeclareContractParams, + ): Promise { + const { payload, details, address } = params; + + if (!(await this.getDeclareContractConsensus(params))) { + throw new UserRejectedOpError() as unknown as Error; + } + + return (await declareContractUtil( + this.network, + address, + this.account.privateKey, + payload, + details, + )) as DeclareContractResponse; + } + + protected async getDeclareContractConsensus(params: DeclareContractParams) { + const { payload, details, address } = params; + const components: Component[] = []; + components.push(heading('Do you want to sign this transaction?')); + + components.push( + row( + 'Signer Address', + text({ + value: address, + markdown: false, + }), + ), + ); + + components.push(divider()); + + components.push( + row( + 'Network', + text({ + value: this.network.name, + markdown: false, + }), + ), + ); + + if (payload.contract) { + components.push(divider()); + const contractDetails = + typeof payload.contract === 'string' + ? payload.contract + : toJson(payload.contract); + components.push( + row( + 'Contract', + text({ + value: contractDetails, + markdown: false, + }), + ), + ); + } + + if (payload.compiledClassHash) { + components.push(divider()); + components.push( + row( + 'Compiled Class Hash', + text({ + value: payload.compiledClassHash, + markdown: false, + }), + ), + ); + } + + if (payload.classHash) { + components.push(divider()); + components.push( + row( + 'Class Hash', + text({ + value: payload.classHash, + markdown: false, + }), + ), + ); + } + + if (payload.casm) { + const casmDetails = toJson(payload.casm); + components.push(divider()); + components.push( + row( + 'Casm', + text({ + value: casmDetails, + markdown: false, + }), + ), + ); + } + + if (details?.maxFee) { + const maxFeeInEth = convert(details.maxFee, 'wei', 'ether'); + components.push(divider()); + components.push( + row( + 'Max Fee (ETH)', + text({ + value: maxFeeInEth, + markdown: false, + }), + ), + ); + } + + // Return the confirmation dialog with all the components + return await confirmDialog(components); + } +} + +export const declareContract = new DeclareContractRpc({ + showInvalidAccountAlert: true, +}); diff --git a/packages/starknet-snap/src/rpcs/execute-txn.test.ts b/packages/starknet-snap/src/rpcs/execute-txn.test.ts index 9dc6e58b..f9498055 100644 --- a/packages/starknet-snap/src/rpcs/execute-txn.test.ts +++ b/packages/starknet-snap/src/rpcs/execute-txn.test.ts @@ -12,6 +12,7 @@ import { import * as starknetUtils from '../utils/starknetUtils'; import { executeTxn as executeTxnUtil } from '../utils/starknetUtils'; import { + generateRandomFee, mockAccount, prepareConfirmDialog, prepareMockAccount, @@ -54,8 +55,8 @@ const prepareMockExecuteTxn = async ( const estimateResults = getEstimateFees(); const getEstimatedFeesRepsMock = { - suggestedMaxFee: BigInt(1000000000000000).toString(10), - overallFee: BigInt(1000000000000000).toString(10), + suggestedMaxFee: generateRandomFee('1000000000000000', '2000000000000000'), + overallFee: generateRandomFee('1000000000000000', '2000000000000000'), includeDeploy: !accountDeployed, unit: 'wei' as FeeTokenUnit, estimateResults, diff --git a/packages/starknet-snap/src/rpcs/index.ts b/packages/starknet-snap/src/rpcs/index.ts index d4bd560e..09e65cf3 100644 --- a/packages/starknet-snap/src/rpcs/index.ts +++ b/packages/starknet-snap/src/rpcs/index.ts @@ -1,6 +1,7 @@ export * from './display-private-key'; export * from './estimate-fee'; export * from './execute-txn'; +export * from './declare-contract'; export * from './sign-message'; export * from './sign-transaction'; export * from './sign-declare-transaction'; diff --git a/packages/starknet-snap/src/utils/superstruct.ts b/packages/starknet-snap/src/utils/superstruct.ts index 94082863..505d7f94 100644 --- a/packages/starknet-snap/src/utils/superstruct.ts +++ b/packages/starknet-snap/src/utils/superstruct.ts @@ -192,7 +192,7 @@ export const DeclareSignDetailsStruct = assign( /* eslint-disable */ export const SierraContractEntryPointFieldsStruct = object({ selector: string(), - function_idx: string(), + function_idx: number(), }); export const ContractEntryPointFieldsStruct = object({ diff --git a/packages/starknet-snap/test/src/declareContract.test.ts b/packages/starknet-snap/test/src/declareContract.test.ts deleted file mode 100644 index 137aaae2..00000000 --- a/packages/starknet-snap/test/src/declareContract.test.ts +++ /dev/null @@ -1,146 +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 snapsUtil from '../../src/utils/snapUtils'; -import { declareContract } from '../../src/declareContract'; -import { SnapState } from '../../src/types/snapState'; -import { - STARKNET_MAINNET_NETWORK, - STARKNET_SEPOLIA_TESTNET_NETWORK, -} from '../../src/utils/constants'; -import { - createAccountProxyTxn, - getBip44EntropyStub, - account1, -} from '../constants.test'; -import { getAddressKeyDeriver } from '../../src/utils/keyPair'; -import { Mutex } from 'async-mutex'; -import { - ApiParamsWithKeyDeriver, - DeclareContractRequestParams, -} from '../../src/types/snapApi'; -import { - DeployRequiredError, - UpgradeRequiredError, -} from '../../src/utils/exceptions'; - -chai.use(sinonChai); -const sandbox = sinon.createSandbox(); - -describe('Test function: declareContract', function () { - this.timeout(10000); - const walletStub = new WalletMock(); - const state: SnapState = { - accContracts: [account1], - erc20Tokens: [], - networks: [STARKNET_MAINNET_NETWORK, STARKNET_SEPOLIA_TESTNET_NETWORK], - transactions: [], - }; - let apiParams: ApiParamsWithKeyDeriver; - - const requestObject: DeclareContractRequestParams = { - chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, - senderAddress: account1.address, - contractPayload: { - contract: 'TestContract', - }, - invocationsDetails: { - maxFee: 100, - }, - }; - - beforeEach(async function () { - walletStub.rpcStubs.snap_getBip44Entropy.callsFake(getBip44EntropyStub); - - apiParams = { - state, - requestParams: requestObject, - wallet: walletStub, - saveMutex: new Mutex(), - keyDeriver: await getAddressKeyDeriver(walletStub), - }; - sandbox.useFakeTimers(createAccountProxyTxn.timestamp); - walletStub.rpcStubs.snap_dialog.resolves(true); - walletStub.rpcStubs.snap_manageState.resolves(state); - }); - - afterEach(function () { - walletStub.reset(); - sandbox.restore(); - }); - - it('should declareContract correctly', async function () { - sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); - const declareContractStub = sandbox - .stub(utils, 'declareContract') - .resolves({ - transaction_hash: 'transaction_hash', - class_hash: 'class_hash', - }); - const result = await declareContract(apiParams); - const { privateKey } = await utils.getKeysFromAddress( - apiParams.keyDeriver, - STARKNET_SEPOLIA_TESTNET_NETWORK, - state, - account1.address, - ); - - expect(result).to.eql({ - transaction_hash: 'transaction_hash', - class_hash: 'class_hash', - }); - expect(declareContractStub).to.have.been.calledOnce; - expect(declareContractStub).to.have.been.calledWith( - STARKNET_SEPOLIA_TESTNET_NETWORK, - account1.address, - privateKey, - { contract: 'TestContract' }, - { maxFee: 100 }, - ); - }); - - it('should throw error if declareContract fail', async function () { - sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); - const declareContractStub = sandbox - .stub(utils, 'declareContract') - .rejects('error'); - const { privateKey } = await utils.getKeysFromAddress( - apiParams.keyDeriver, - STARKNET_SEPOLIA_TESTNET_NETWORK, - state, - account1.address, - ); - let result; - try { - await declareContract(apiParams); - } catch (e) { - result = e; - } finally { - expect(result).to.be.an('Error'); - expect(declareContractStub).to.have.been.calledOnce; - expect(declareContractStub).to.have.been.calledWith( - STARKNET_SEPOLIA_TESTNET_NETWORK, - account1.address, - privateKey, - { contract: 'TestContract' }, - { maxFee: 100 }, - ); - } - }); - - it('should return false if user rejected to sign the transaction', async function () { - sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); - walletStub.rpcStubs.snap_dialog.resolves(false); - const declareContractStub = sandbox - .stub(utils, 'declareContract') - .resolves({ - transaction_hash: 'transaction_hash', - class_hash: 'class_hash', - }); - const result = await declareContract(apiParams); - expect(result).to.equal(false); - expect(declareContractStub).to.have.been.not.called; - }); -});