From d7708dafea7884be861265e5b3617f94a7715a1c Mon Sep 17 00:00:00 2001 From: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:04:20 +0800 Subject: [PATCH] refactor: revamp `starkNet_signDeclareTransaction` (#333) * refactor: revamp sign delcare transaction * chore: add superstruct test * chore: update get-starknet interface * chore: restrict tx version to v3 and v2 --- packages/get-starknet/src/snap.ts | 6 +- packages/starknet-snap/src/index.ts | 8 +- packages/starknet-snap/src/rpcs/index.ts | 1 + .../src/rpcs/sign-declare-transaction.test.ts | 134 ++++++++++++ .../src/rpcs/sign-declare-transaction.ts | 120 +++++++++++ .../src/signDeclareTransaction.ts | 80 -------- .../src/utils/superstruct.test.ts | 191 +++++++++++++++++- .../starknet-snap/src/utils/superstruct.ts | 59 +++++- .../test/src/signDeclareTransaction.test.ts | 138 ------------- 9 files changed, 509 insertions(+), 228 deletions(-) create mode 100644 packages/starknet-snap/src/rpcs/sign-declare-transaction.test.ts create mode 100644 packages/starknet-snap/src/rpcs/sign-declare-transaction.ts delete mode 100644 packages/starknet-snap/src/signDeclareTransaction.ts delete mode 100644 packages/starknet-snap/test/src/signDeclareTransaction.test.ts diff --git a/packages/get-starknet/src/snap.ts b/packages/get-starknet/src/snap.ts index f85e367a..722fc17c 100644 --- a/packages/get-starknet/src/snap.ts +++ b/packages/get-starknet/src/snap.ts @@ -88,7 +88,7 @@ export class MetaMaskSnap { })) as Signature; } - async signDeclareTransaction(signerAddress: string, transaction: DeclareSignerDetails): Promise { + async signDeclareTransaction(address: string, details: DeclareSignerDetails): Promise { return (await this.#provider.request({ method: 'wallet_invokeSnap', params: { @@ -96,8 +96,8 @@ export class MetaMaskSnap { request: { method: 'starkNet_signDeclareTransaction', params: this.removeUndefined({ - signerAddress, - transaction, + address, + details, ...(await this.#getSnapParams()), }), }, diff --git a/packages/starknet-snap/src/index.ts b/packages/starknet-snap/src/index.ts index 5d46b43e..e9482055 100644 --- a/packages/starknet-snap/src/index.ts +++ b/packages/starknet-snap/src/index.ts @@ -39,16 +39,17 @@ import type { DisplayPrivateKeyParams, SignMessageParams, SignTransactionParams, + SignDeclareTransactionParams, VerifySignatureParams, } from './rpcs'; import { displayPrivateKey, signMessage, signTransaction, + signDeclareTransaction, verifySignature, } from './rpcs'; import { sendTransaction } from './sendTransaction'; -import { signDeclareTransaction } from './signDeclareTransaction'; import { signDeployAccountTransaction } from './signDeployAccountTransaction'; import { switchNetwork } from './switchNetwork'; import type { @@ -192,9 +193,8 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { ); case 'starkNet_signDeclareTransaction': - apiParams.keyDeriver = await getAddressKeyDeriver(snap); - return await signDeclareTransaction( - apiParams as unknown as ApiParamsWithKeyDeriver, + return await signDeclareTransaction.execute( + apiParams as unknown as SignDeclareTransactionParams, ); case 'starkNet_signDeployAccountTransaction': diff --git a/packages/starknet-snap/src/rpcs/index.ts b/packages/starknet-snap/src/rpcs/index.ts index 989d4a3c..3733777a 100644 --- a/packages/starknet-snap/src/rpcs/index.ts +++ b/packages/starknet-snap/src/rpcs/index.ts @@ -1,4 +1,5 @@ export * from './signMessage'; export * from './displayPrivateKey'; export * from './signTransaction'; +export * from './sign-declare-transaction'; export * from './verify-signature'; diff --git a/packages/starknet-snap/src/rpcs/sign-declare-transaction.test.ts b/packages/starknet-snap/src/rpcs/sign-declare-transaction.test.ts new file mode 100644 index 00000000..5cb8f1ee --- /dev/null +++ b/packages/starknet-snap/src/rpcs/sign-declare-transaction.test.ts @@ -0,0 +1,134 @@ +import { + InvalidParamsError, + UserRejectedRequestError, +} from '@metamask/snaps-sdk'; +import type { DeclareSignerDetails } from 'starknet'; +import { constants } from 'starknet'; + +import type { SnapState } from '../types/snapState'; +import { toJson } from '../utils'; +import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../utils/constants'; +import * as starknetUtils from '../utils/starknetUtils'; +import { + mockAccount, + prepareMockAccount, + prepareConfirmDialog, +} from './__tests__/helper'; +import { signDeclareTransaction } from './sign-declare-transaction'; +import type { SignDeclareTransactionParams } from './sign-declare-transaction'; + +jest.mock('../utils/snap'); +jest.mock('../utils/logger'); + +describe('signDeclareTransaction', () => { + const state: SnapState = { + accContracts: [], + erc20Tokens: [], + networks: [STARKNET_SEPOLIA_TESTNET_NETWORK], + transactions: [], + }; + + const createRequest = ( + chainId: constants.StarknetChainId, + address: string, + ) => ({ + details: { + classHash: + '0x025ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918', + senderAddress: address, + chainId, + version: constants.TRANSACTION_VERSION.V2, + maxFee: 0, + nonce: 0, + }, + address, + chainId, + }); + + it('signs message correctly', async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const account = await mockAccount(chainId); + + prepareMockAccount(account, state); + prepareConfirmDialog(); + + const request = createRequest(chainId, account.address); + + const expectedResult = await starknetUtils.signDeclareTransaction( + account.privateKey, + request.details as unknown as DeclareSignerDetails, + ); + + const result = await signDeclareTransaction.execute(request); + + expect(result).toStrictEqual(expectedResult); + }); + + it('renders confirmation dialog', async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const account = await mockAccount(chainId); + + prepareMockAccount(account, state); + const { confirmDialogSpy } = prepareConfirmDialog(); + + const request = createRequest(chainId, account.address); + + await signDeclareTransaction.execute(request); + + const calls = confirmDialogSpy.mock.calls[0][0]; + expect(calls).toStrictEqual([ + { type: 'heading', value: 'Do you want to sign this transaction?' }, + { + type: 'row', + label: 'Network', + value: { + value: STARKNET_SEPOLIA_TESTNET_NETWORK.name, + markdown: false, + type: 'text', + }, + }, + { + type: 'row', + label: 'Signer Address', + value: { + value: account.address, + markdown: false, + type: 'text', + }, + }, + { + type: 'row', + label: 'Declare Transaction Details', + value: { + value: toJson(request.details), + markdown: false, + type: 'text', + }, + }, + ]); + }); + + it('throws `UserRejectedRequestError` if user denied the operation', async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const account = await mockAccount(chainId); + + prepareMockAccount(account, state); + const { confirmDialogSpy } = prepareConfirmDialog(); + + confirmDialogSpy.mockResolvedValue(false); + + const request = createRequest(chainId, account.address); + + await expect(signDeclareTransaction.execute(request)).rejects.toThrow( + UserRejectedRequestError, + ); + }); + + it('throws `InvalidParamsError` when request parameter is not correct', async () => { + await expect( + signDeclareTransaction.execute( + {} as unknown as SignDeclareTransactionParams, + ), + ).rejects.toThrow(InvalidParamsError); + }); +}); diff --git a/packages/starknet-snap/src/rpcs/sign-declare-transaction.ts b/packages/starknet-snap/src/rpcs/sign-declare-transaction.ts new file mode 100644 index 00000000..b6496553 --- /dev/null +++ b/packages/starknet-snap/src/rpcs/sign-declare-transaction.ts @@ -0,0 +1,120 @@ +import type { Component } from '@metamask/snaps-sdk'; +import { + heading, + row, + text, + UserRejectedRequestError, +} from '@metamask/snaps-sdk'; +import type { DeclareSignerDetails } from 'starknet'; +import type { Infer } from 'superstruct'; +import { array, object, string, assign } from 'superstruct'; + +import { + confirmDialog, + AddressStruct, + toJson, + BaseRequestStruct, + AccountRpcController, + DeclareSignDetailsStruct, +} from '../utils'; +import { signDeclareTransaction as signDeclareTransactionUtil } from '../utils/starknetUtils'; + +export const SignDeclareTransactionRequestStruct = assign( + object({ + address: AddressStruct, + details: DeclareSignDetailsStruct, + }), + BaseRequestStruct, +); + +export const SignDeclareTransactionResponseStruct = array(string()); + +export type SignDeclareTransactionParams = Infer< + typeof SignDeclareTransactionRequestStruct +>; + +export type SignDeclareTransactionResponse = Infer< + typeof SignDeclareTransactionResponseStruct +>; + +/** + * The RPC handler to sign a declare transaction. + */ +export class SignDeclareTransactionRpc extends AccountRpcController< + SignDeclareTransactionParams, + SignDeclareTransactionResponse +> { + protected requestStruct = SignDeclareTransactionRequestStruct; + + protected responseStruct = SignDeclareTransactionResponseStruct; + + /** + * Execute the sign declare transaction request handler. + * It will show a confirmation dialog to the user before signing the declare transaction. + * + * @param params - The parameters of the request. + * @param params.address - The address of the signer. + * @param params.details - The declare transaction details to sign. + * @param [params.enableAuthorize] - Optional, a flag to enable or display the confirmation dialog to the user. + * @param params.chainId - The chain id of the network. + * @returns the signature of the message in string array. + */ + async execute( + params: SignDeclareTransactionParams, + ): Promise { + return super.execute(params); + } + + protected async handleRequest( + params: SignDeclareTransactionParams, + ): Promise { + const { details } = params; + if (!(await this.getSignDeclareTransactionConsensus(details))) { + throw new UserRejectedRequestError() as unknown as Error; + } + + return (await signDeclareTransactionUtil( + this.account.privateKey, + details as unknown as DeclareSignerDetails, + )) as unknown as SignDeclareTransactionResponse; + } + + protected async getSignDeclareTransactionConsensus( + details: Infer, + ) { + const components: Component[] = []; + components.push(heading('Do you want to sign this transaction?')); + components.push( + row( + 'Network', + text({ + value: this.network.name, + markdown: false, + }), + ), + ); + components.push( + row( + 'Signer Address', + text({ + value: details.senderAddress, + markdown: false, + }), + ), + ); + + components.push( + row( + 'Declare Transaction Details', + text({ + value: toJson(details), + markdown: false, + }), + ), + ); + + return await confirmDialog(components); + } +} + +export const signDeclareTransaction = new SignDeclareTransactionRpc(); diff --git a/packages/starknet-snap/src/signDeclareTransaction.ts b/packages/starknet-snap/src/signDeclareTransaction.ts deleted file mode 100644 index 3c627d63..00000000 --- a/packages/starknet-snap/src/signDeclareTransaction.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { heading, panel, DialogType } from '@metamask/snaps-sdk'; -import type { Signature } from 'starknet'; - -import type { - ApiParamsWithKeyDeriver, - SignDeclareTransactionRequestParams, -} from './types/snapApi'; -import { logger } from './utils/logger'; -import { toJson } from './utils/serializer'; -import { - getNetworkFromChainId, - getSignTxnTxt, - verifyIfAccountNeedUpgradeOrDeploy, -} from './utils/snapUtils'; -import { - getKeysFromAddress, - signDeclareTransaction as signDeclareTransactionUtil, -} from './utils/starknetUtils'; - -/** - * - * @param params - */ -export async function signDeclareTransaction( - params: ApiParamsWithKeyDeriver, -): Promise { - try { - const { state, keyDeriver, requestParams, wallet } = params; - const requestParamsObj = - requestParams as SignDeclareTransactionRequestParams; - const { signerAddress } = requestParamsObj; - const network = getNetworkFromChainId(state, requestParamsObj.chainId); - const { privateKey, publicKey } = await getKeysFromAddress( - keyDeriver, - network, - state, - signerAddress, - ); - - await verifyIfAccountNeedUpgradeOrDeploy(network, signerAddress, publicKey); - - logger.log( - `signDeclareTransaction params: ${toJson( - requestParamsObj.transaction, - 2, - )}}`, - ); - - const snapComponents = getSignTxnTxt( - signerAddress, - network, - requestParamsObj.transaction, - ); - - if (requestParamsObj.enableAuthorize) { - 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 signDeclareTransactionUtil( - privateKey, - requestParamsObj.transaction, - ); - } catch (error) { - logger.error(`Problem found:`, error); - throw error; - } -} diff --git a/packages/starknet-snap/src/utils/superstruct.test.ts b/packages/starknet-snap/src/utils/superstruct.test.ts index 81f90f04..ea2c4027 100644 --- a/packages/starknet-snap/src/utils/superstruct.test.ts +++ b/packages/starknet-snap/src/utils/superstruct.test.ts @@ -11,9 +11,14 @@ import { CairoVersionStruct, CallDataStruct, ChainIdStruct, + DeclareSignDetailsStruct, + EDataModeStruct, + NumberStringStruct, + ResourceBoundMappingStruct, createStructWithAdditionalProperties, TxVersionStruct, TypeDataStruct, + V3TransactionDetailStruct, } from './superstruct'; describe('AddressStruct', () => { @@ -148,7 +153,7 @@ describe('CairoVersionStruct', () => { }); describe('TxVersionStruct', () => { - it.each(Object.values(constants.TRANSACTION_VERSION))( + it.each([constants.TRANSACTION_VERSION.V2, constants.TRANSACTION_VERSION.V3])( 'does not throw error if the tx version is %s', (version) => { expect(() => assert(version, TxVersionStruct)).not.toThrow(); @@ -176,6 +181,190 @@ describe('CallDataStruct', () => { }); }); +describe('NumberStringStruct', () => { + it.each([1, '0x1'])( + 'does not throw error if the input is %s', + (val: number | string) => { + expect(() => assert(val, NumberStringStruct)).not.toThrow(); + }, + ); + + it('throws error if the input is invalid', () => { + expect(() => assert('9sd0', NumberStringStruct)).toThrow(StructError); + }); +}); + +describe('EDataModeStruct', () => { + it.each(['L1', 'L2'])( + 'does not throw error if the data mode is %s', + (val: string) => { + expect(() => assert(val, EDataModeStruct)).not.toThrow(); + }, + ); + + it('throws error if the data mode is invalid', () => { + expect(() => assert('L3', EDataModeStruct)).toThrow(StructError); + }); +}); + +// this test also cover the test for ResourceBoundStruct +describe('ResourceBoundMappingStruct', () => { + it.each([ + { + // eslint-disable-next-line @typescript-eslint/naming-convention + l1_gas: { + // eslint-disable-next-line @typescript-eslint/naming-convention + max_amount: '100', + // eslint-disable-next-line @typescript-eslint/naming-convention + max_price_per_unit: '100', + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + l2_gas: { + // eslint-disable-next-line @typescript-eslint/naming-convention + max_amount: '100', + // eslint-disable-next-line @typescript-eslint/naming-convention + max_price_per_unit: '100', + }, + }, + { + // eslint-disable-next-line @typescript-eslint/naming-convention + l1_gas: { + // eslint-disable-next-line @typescript-eslint/naming-convention + max_amount: '100', + // eslint-disable-next-line @typescript-eslint/naming-convention + max_price_per_unit: '100', + }, + }, + { + // eslint-disable-next-line @typescript-eslint/naming-convention + l2_gas: { + // eslint-disable-next-line @typescript-eslint/naming-convention + max_amount: '100', + // eslint-disable-next-line @typescript-eslint/naming-convention + max_price_per_unit: '100', + }, + }, + ])('does not throw error if the data mode is correct', (request: unknown) => { + expect(() => assert(request, ResourceBoundMappingStruct)).not.toThrow(); + }); + + it('throws error if the data mode is invalid', () => { + expect(() => + assert( + { + // eslint-disable-next-line @typescript-eslint/naming-convention + l3_gas: { + // eslint-disable-next-line @typescript-eslint/naming-convention + max_amount: '100', + // eslint-disable-next-line @typescript-eslint/naming-convention + max_price_per_unit: '100', + }, + }, + ResourceBoundMappingStruct, + ), + ).toThrow(StructError); + }); +}); + +describe('V3TransactionDetailStruct', () => { + it.each([ + { + nonce: '1', + version: constants.TRANSACTION_VERSION.V3, + resourceBounds: { + // eslint-disable-next-line @typescript-eslint/naming-convention + l1_gas: { + // eslint-disable-next-line @typescript-eslint/naming-convention + max_amount: '100', + // eslint-disable-next-line @typescript-eslint/naming-convention + max_price_per_unit: '100', + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + l2_gas: { + // eslint-disable-next-line @typescript-eslint/naming-convention + max_amount: '100', + // eslint-disable-next-line @typescript-eslint/naming-convention + max_price_per_unit: '100', + }, + }, + tip: '1', + paymasterData: ['1'], + accountDeploymentData: ['1'], + nonceDataAvailabilityMode: 'L1', + feeDataAvailabilityMode: 'L2', + }, + { + nonce: '1', + }, + ])('does not throw error if the data mode is correct', (request: unknown) => { + expect(() => assert(request, V3TransactionDetailStruct)).not.toThrow(); + }); + + it('does not throw error if the tx detail is empty', () => { + expect(() => assert({}, V3TransactionDetailStruct)).not.toThrow(); + }); + + it('throws error if the data mode is invalid', () => { + expect(() => + assert( + { + nonce: 'ae', + }, + ResourceBoundMappingStruct, + ), + ).toThrow(StructError); + }); +}); + +describe('DeclareSignDetailsStruct', () => { + it.each([ + { + classHash: + '0x025ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918', + senderAddress: + '0x025ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918', + chainId: constants.StarknetChainId.SN_SEPOLIA, + version: constants.TRANSACTION_VERSION.V2, + maxFee: '0', + nonce: '0', + }, + { + classHash: + '0x025ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918', + senderAddress: + '0x025ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918', + chainId: constants.StarknetChainId.SN_SEPOLIA, + version: constants.TRANSACTION_VERSION.V3, + nonce: '0x1', + resourceBounds: { + // eslint-disable-next-line @typescript-eslint/naming-convention + l1_gas: { + // eslint-disable-next-line @typescript-eslint/naming-convention + max_amount: '100', + // eslint-disable-next-line @typescript-eslint/naming-convention + max_price_per_unit: '100', + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + l2_gas: { + // eslint-disable-next-line @typescript-eslint/naming-convention + max_amount: '100', + // eslint-disable-next-line @typescript-eslint/naming-convention + max_price_per_unit: '100', + }, + }, + }, + ])( + 'does not throw error if the declare sign details is correct', + (request: unknown) => { + expect(() => assert(request, DeclareSignDetailsStruct)).not.toThrow(); + }, + ); + + it('throws error if the declare sign details is invalid', () => { + expect(() => assert({}, DeclareSignDetailsStruct)).toThrow(StructError); + }); +}); + describe('createStructWithAdditionalProperties', () => { const predefinedProperties = object({ name: string(), diff --git a/packages/starknet-snap/src/utils/superstruct.ts b/packages/starknet-snap/src/utils/superstruct.ts index b4659c01..2afc0301 100644 --- a/packages/starknet-snap/src/utils/superstruct.ts +++ b/packages/starknet-snap/src/utils/superstruct.ts @@ -1,4 +1,5 @@ import { union } from '@metamask/snaps-sdk'; +import { HexStruct } from '@metamask/utils'; import { constants, validateAndParseAddress } from 'starknet'; import type { Struct } from 'superstruct'; import { @@ -12,6 +13,7 @@ import { any, number, array, + nonempty, dynamic, assign, } from 'superstruct'; @@ -89,10 +91,63 @@ export const CallDataStruct = object({ calldata: union([array(string()), record(string(), any())]), // TODO: refine this to calldata }); +export const NumberStringStruct = union([number(), HexStruct]); + export const CairoVersionStruct = enums([CAIRO_VERSION, CAIRO_VERSION_LEGACY]); -export const TxVersionStruct = enums( - Object.values(constants.TRANSACTION_VERSION), +export const TxVersionStruct = enums([ + constants.TRANSACTION_VERSION.V2, + constants.TRANSACTION_VERSION.V3, +]); + +export const V2TxVersionStruct = enums([constants.TRANSACTION_VERSION.V2]); + +export const V3TxVersionStruct = enums([constants.TRANSACTION_VERSION.V3]); + +export const EDataModeStruct = enums(['L1', 'L2']); + +export const ResourceBoundStruct = object({ + // eslint-disable-next-line @typescript-eslint/naming-convention + max_amount: optional(string()), + // eslint-disable-next-line @typescript-eslint/naming-convention + max_price_per_unit: optional(string()), +}); + +export const ResourceBoundMappingStruct = object({ + // eslint-disable-next-line @typescript-eslint/naming-convention + l1_gas: optional(ResourceBoundStruct), + // eslint-disable-next-line @typescript-eslint/naming-convention + l2_gas: optional(ResourceBoundStruct), +}); + +export const V3TransactionDetailStruct = object({ + nonce: optional(NumberStringStruct), + version: optional(V3TxVersionStruct), + resourceBounds: optional(ResourceBoundMappingStruct), + tip: optional(NumberStringStruct), + paymasterData: optional(array(NumberStringStruct)), + accountDeploymentData: optional(array(NumberStringStruct)), + nonceDataAvailabilityMode: optional(EDataModeStruct), + feeDataAvailabilityMode: optional(EDataModeStruct), +}); + +// The declare transaction details struct does not using union to have more relax validation as starknet.js +export const DeclareSignDetailsStruct = assign( + object({ + nonce: optional(NumberStringStruct), + maxFee: optional(NumberStringStruct), + }), + V3TransactionDetailStruct, + // Only restrict some required parameters for the declare transaction + object({ + // TODO: classHash should be a hex string + classHash: nonempty(string()), + // TODO: compiledClassHash should be a hex string + compiledClassHash: optional(string()), + senderAddress: AddressStruct, + chainId: ChainIdStruct, + version: TxVersionStruct, + }), ); /** diff --git a/packages/starknet-snap/test/src/signDeclareTransaction.test.ts b/packages/starknet-snap/test/src/signDeclareTransaction.test.ts deleted file mode 100644 index e7d0689e..00000000 --- a/packages/starknet-snap/test/src/signDeclareTransaction.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import chai, { expect } from 'chai'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import { WalletMock } from '../wallet.mock.test'; -import { signDeclareTransaction } from '../../src/signDeclareTransaction'; -import { SnapState } from '../../src/types/snapState'; -import { - STARKNET_MAINNET_NETWORK, - STARKNET_SEPOLIA_TESTNET_NETWORK, -} from '../../src/utils/constants'; -import { - createAccountProxyTxn, - getBip44EntropyStub, - account1, - signature3, -} from '../constants.test'; -import { getAddressKeyDeriver } from '../../src/utils/keyPair'; -import { Mutex } from 'async-mutex'; -import { - ApiParamsWithKeyDeriver, - SignDeclareTransactionRequestParams, -} from '../../src/types/snapApi'; -import { DeclareSignerDetails, constants } from 'starknet'; -import * as utils from '../../src/utils/starknetUtils'; -import * as snapsUtil from '../../src/utils/snapUtils'; -import { - DeployRequiredError, - UpgradeRequiredError, -} from '../../src/utils/exceptions'; - -chai.use(sinonChai); -const sandbox = sinon.createSandbox(); - -describe('Test function: signDeclareTransaction', function () { - this.timeout(10000); - const walletStub = new WalletMock(); - const state: SnapState = { - accContracts: [], - erc20Tokens: [], - networks: [STARKNET_MAINNET_NETWORK, STARKNET_SEPOLIA_TESTNET_NETWORK], - transactions: [], - }; - let apiParams: ApiParamsWithKeyDeriver; - - const declarePayload = { - classHash: - '0x025ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918', - senderAddress: account1.address, - chainId: constants.StarknetChainId.SN_MAIN, - nonce: '0x1', - version: '0x0', - maxFee: 100, - } as unknown as DeclareSignerDetails; - - const requestObject: SignDeclareTransactionRequestParams = { - chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, - signerAddress: account1.address, - transaction: declarePayload, - enableAuthorize: true, - }; - - 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(); - apiParams.requestParams = requestObject; - }); - - it('should sign a transaction from an user account correctly', async function () { - sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); - sandbox.stub(utils, 'signDeclareTransaction').resolves(signature3); - const result = await signDeclareTransaction(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(result).to.be.eql(signature3); - }); - - it('should throw error if signDeclareTransaction fail', async function () { - sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); - sandbox.stub(utils, 'signDeclareTransaction').throws(new Error()); - let result; - try { - await signDeclareTransaction(apiParams); - } catch (err) { - result = err; - } finally { - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(result).to.be.an('Error'); - } - }); - - it('should return false if user deny to sign the transaction', async function () { - sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); - const stub = sandbox.stub(utils, 'signDeclareTransaction'); - walletStub.rpcStubs.snap_dialog.resolves(false); - - const result = await signDeclareTransaction(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(stub).to.have.been.callCount(0); - expect(result).to.be.eql(false); - }); - - it('should skip dialog if enableAuthorize is false', async function () { - sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); - sandbox.stub(utils, 'signDeclareTransaction').resolves(signature3); - const paramsObject = - apiParams.requestParams as SignDeclareTransactionRequestParams; - paramsObject.enableAuthorize = false; - const result = await signDeclareTransaction(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.callCount(0); - expect(result).to.be.eql(signature3); - paramsObject.enableAuthorize = true; - }); - - it('should skip dialog if enableAuthorize is omit', async function () { - sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); - sandbox.stub(utils, 'signDeclareTransaction').resolves(signature3); - const paramsObject = - apiParams.requestParams as SignDeclareTransactionRequestParams; - paramsObject.enableAuthorize = undefined; - const result = await signDeclareTransaction(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.callCount(0); - expect(result).to.be.eql(signature3); - paramsObject.enableAuthorize = true; - }); -});