diff --git a/packages/starknet-snap/src/index.test.ts b/packages/starknet-snap/src/index.test.ts index 8ae24b5c..d0bbd14d 100644 --- a/packages/starknet-snap/src/index.test.ts +++ b/packages/starknet-snap/src/index.test.ts @@ -1,3 +1,4 @@ +import { MethodNotFoundError, SnapError } from '@metamask/snaps-sdk'; import { constants } from 'starknet'; import { onRpcRequest, onHomePage } from '.'; @@ -50,15 +51,30 @@ describe('onRpcRequest', () => { expect(createAccountSpy).toHaveBeenCalledTimes(1); }); - it('throws `Unable to execute the rpc request` error if an error has thrown', async () => { - const { createAccountSpy, keyPairSpy } = createMockSpy(); + it('throws `MethodNotFoundError` if the request method not found', async () => { + await expect( + onRpcRequest({ + ...createMockRequest(), + request: { + ...createMockRequest().request, + method: 'method_not_found', + }, + }), + ).rejects.toThrow(MethodNotFoundError); + }); - createAccountSpy.mockRejectedValue(new Error('Custom Error')); - keyPairSpy.mockReturnThis(); + it('throws `SnapError` if the error is an instance of SnapError', async () => { + const { createAccountSpy } = createMockSpy(); + createAccountSpy.mockRejectedValue(new SnapError('error')); - await expect(onRpcRequest(createMockRequest())).rejects.toThrow( - 'Unable to execute the rpc request', - ); + await expect(onRpcRequest(createMockRequest())).rejects.toThrow(SnapError); + }); + + it('throws `SnapError` if the error is not an instance of SnapError', async () => { + const { createAccountSpy } = createMockSpy(); + createAccountSpy.mockRejectedValue(new Error('error')); + + await expect(onRpcRequest(createMockRequest())).rejects.toThrow(SnapError); }); }); diff --git a/packages/starknet-snap/src/index.ts b/packages/starknet-snap/src/index.ts index 1941bb49..3f436d6d 100644 --- a/packages/starknet-snap/src/index.ts +++ b/packages/starknet-snap/src/index.ts @@ -12,6 +12,7 @@ import { text, copyable, SnapError, + MethodNotFoundError, } from '@metamask/snaps-sdk'; import { ethers } from 'ethers'; @@ -62,6 +63,7 @@ import type { } from './types/snapApi'; import type { SnapState } from './types/snapState'; import { upgradeAccContract } from './upgradeAccContract'; +import { isSnapRpcError } from './utils'; import { CAIRO_VERSION_LEGACY, ETHER_MAINNET, @@ -73,7 +75,7 @@ import { } from './utils/constants'; import { getAddressKeyDeriver } from './utils/keyPair'; import { acquireLock } from './utils/lock'; -import { logger, LogLevel } from './utils/logger'; +import { logger } from './utils/logger'; import { toJson } from './utils/serializer'; import { dappUrl, @@ -293,19 +295,18 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { return await getStarkName(apiParams); default: - throw new Error('Method not found.'); + throw new MethodNotFoundError() as unknown as Error; } } catch (error) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - logger.error(`Error: ${error}`); - // We don't want to expose the error message to the user when it is a production build - if (logger.logLevel === LogLevel.OFF) { - throw new SnapError( - 'Unable to execute the rpc request', - ) as unknown as Error; - } else { - throw new SnapError(error.message) as unknown as Error; + let snapError = error; + + if (!isSnapRpcError(error)) { + snapError = new SnapError('Unable to execute the rpc request'); } + logger.error( + `onRpcRequest error: ${JSON.stringify(snapError.toJSON(), null, 2)}`, + ); + throw snapError; } }; diff --git a/packages/starknet-snap/src/utils/rpc.test.ts b/packages/starknet-snap/src/utils/rpc.test.ts index 4594e296..38268d9e 100644 --- a/packages/starknet-snap/src/utils/rpc.test.ts +++ b/packages/starknet-snap/src/utils/rpc.test.ts @@ -85,26 +85,6 @@ describe('RpcController', () => { expect(result).toBe('done test'); }); - - it('throws `Failed to execute the rpc method` if an error was thrown', async () => { - const rpc = new MockRpc(); - - jest - .spyOn(MockRpc.prototype, 'handleRequest') - .mockRejectedValue(new Error('error')); - - await expect(rpc.execute('test')).rejects.toThrow( - 'Failed to execute the rpc method', - ); - }); - - it('throws the actual error if an snap error was thrown', async () => { - const rpc = new MockRpc(); - - await expect(rpc.execute(1 as unknown as string)).rejects.toThrow( - 'Expected a string, but received: 1', - ); - }); }); describe('AccountRpcController', () => { diff --git a/packages/starknet-snap/src/utils/rpc.ts b/packages/starknet-snap/src/utils/rpc.ts index 3b3cc925..b4a8809e 100644 --- a/packages/starknet-snap/src/utils/rpc.ts +++ b/packages/starknet-snap/src/utils/rpc.ts @@ -78,20 +78,10 @@ export abstract class RpcController< * @returns A promise that resolves to an json. */ async execute(params: Request): Promise { - try { - await this.preExecute(params); - const resp = await this.handleRequest(params); - await this.postExecute(resp); - return resp; - } catch (error) { - logger.error('Failed to execute the rpc method', error); - - if (error instanceof SnapError) { - throw error as unknown as Error; - } - - throw new Error('Failed to execute the rpc method'); - } + await this.preExecute(params); + const resp = await this.handleRequest(params); + await this.postExecute(resp); + return resp; } } diff --git a/packages/starknet-snap/test/src/index.test.ts b/packages/starknet-snap/test/src/index.test.ts deleted file mode 100644 index fd1fc0b3..00000000 --- a/packages/starknet-snap/test/src/index.test.ts +++ /dev/null @@ -1,236 +0,0 @@ -import chai, { expect } from 'chai'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import { SnapError } from '@metamask/snaps-sdk'; - -import { WalletMock } from '../wallet.mock.test'; -import { getBip44EntropyStub, account1 } from '../constants.test'; -import { SnapState } from '../../src/types/snapState'; -import { - ETHER_MAINNET, - ETHER_SEPOLIA_TESTNET, - STARKNET_MAINNET_NETWORK, - STARKNET_SEPOLIA_TESTNET_NETWORK, -} from '../../src/utils/constants'; -import * as starknetUtils from '../../src/utils/starknetUtils'; -import * as createAccountApi from '../../src/createAccount'; -import * as keyPairUtils from '../../src/utils/keyPair'; -import { onHomePage, onRpcRequest } from '../../src'; - -chai.use(sinonChai); -const sandbox = sinon.createSandbox(); - -describe('onRpcRequest', function () { - const walletStub = new WalletMock(); - - const mockSnap = () => { - const globalAny: any = global; - globalAny.snap = walletStub; - }; - - afterEach(function () { - walletStub.reset(); - sandbox.restore(); - }); - - it('processes request successfully', async function () { - mockSnap(); - const createAccountSpy = sandbox.stub(createAccountApi, 'createAccount'); - const keyPairSpy = sandbox.stub(keyPairUtils, 'getAddressKeyDeriver'); - - createAccountSpy.resolvesThis(); - keyPairSpy.resolvesThis(); - - await onRpcRequest({ - origin: 'http://localhost:3000', - request: { - method: 'starkNet_createAccount', - params: [], - jsonrpc: '2.0', - id: 1, - }, - }); - - expect(keyPairSpy).to.have.been.calledOnce; - expect(createAccountSpy).to.have.been.calledOnce; - }); - - it('throws `Unable to execute the rpc request` error if an error has thrown and `LogLevel` is `OFF`', async function () { - mockSnap(); - const createAccountSpy = sandbox.stub(createAccountApi, 'createAccount'); - const keyPairSpy = sandbox.stub(keyPairUtils, 'getAddressKeyDeriver'); - - createAccountSpy.rejects(new Error('Custom Error')); - keyPairSpy.resolvesThis(); - - let expectedError; - - try { - await onRpcRequest({ - origin: 'http://localhost:3000', - request: { - method: 'starkNet_createAccount', - params: [], - jsonrpc: '2.0', - id: 1, - }, - }); - } catch (error) { - expectedError = error; - } finally { - expect(expectedError).to.be.instanceOf(SnapError); - expect(expectedError.message).to.equal( - 'Unable to execute the rpc request', - ); - } - }); -}); - -describe('onHomePage', function () { - const walletStub = new WalletMock(); - // eslint-disable-next-line no-restricted-globals, @typescript-eslint/no-explicit-any - const globalAny: any = global; - const state: SnapState = { - accContracts: [], - erc20Tokens: [ETHER_MAINNET, ETHER_SEPOLIA_TESTNET], - networks: [STARKNET_SEPOLIA_TESTNET_NETWORK, STARKNET_MAINNET_NETWORK], - transactions: [], - currentNetwork: undefined, - }; - - beforeEach(function () { - globalAny.snap = walletStub; - walletStub.rpcStubs.snap_getBip44Entropy.callsFake(getBip44EntropyStub); - }); - - afterEach(function () { - walletStub.reset(); - sandbox.restore(); - globalAny.snap = undefined; - }); - - const prepareAccountDiscovery = () => { - const getKeysFromAddressIndexSpy = sandbox.stub( - starknetUtils, - 'getKeysFromAddressIndex', - ); - const getCorrectContractAddressSpy = sandbox.stub( - starknetUtils, - 'getCorrectContractAddress', - ); - const getBalanceSpy = sandbox.stub(starknetUtils, 'getBalance'); - - getKeysFromAddressIndexSpy.resolves({ - privateKey: 'pk', - publicKey: 'pubkey', - addressIndex: 1, - derivationPath: `m / bip32:1' / bip32:1' / bip32:1' / bip32:1'`, - }); - - getCorrectContractAddressSpy.resolves({ - address: account1.address, - signerPubKey: account1.publicKey, - upgradeRequired: false, - deployRequired: false, - }); - - getBalanceSpy.resolves('1000'); - }; - - it('renders user address, user balance and network', async function () { - walletStub.rpcStubs.snap_manageState.resolves(state); - prepareAccountDiscovery(); - - const result = await onHomePage(); - expect(result).to.eql({ - content: { - type: 'panel', - children: [ - { type: 'text', value: 'Address' }, - { - type: 'copyable', - value: account1.address, - }, - { - type: 'row', - label: 'Network', - value: { - type: 'text', - value: STARKNET_SEPOLIA_TESTNET_NETWORK.name, - }, - }, - { - type: 'row', - label: 'Balance', - value: { - type: 'text', - value: '0.000000000000001 ETH', - }, - }, - { type: 'divider' }, - { - type: 'text', - value: - 'Visit the [companion dapp for Starknet](https://snaps.consensys.io/starknet) to manage your account.', - }, - ], - }, - }); - }); - - it('renders selected network from state if `currentNetwork` is not undefined', async function () { - walletStub.rpcStubs.snap_manageState.resolves({ - ...state, - currentNetwork: ETHER_MAINNET, - }); - prepareAccountDiscovery(); - - const result = await onHomePage(); - expect(result).to.eql({ - content: { - type: 'panel', - children: [ - { type: 'text', value: 'Address' }, - { - type: 'copyable', - value: account1.address, - }, - { - type: 'row', - label: 'Network', - value: { - type: 'text', - value: ETHER_MAINNET.name, - }, - }, - { - type: 'row', - label: 'Balance', - value: { - type: 'text', - value: '0.000000000000001 ETH', - }, - }, - { type: 'divider' }, - { - type: 'text', - value: - 'Visit the [companion dapp for Starknet](https://snaps.consensys.io/starknet) to manage your account.', - }, - ], - }, - }); - }); - - it('throws `Unable to initialize Snap HomePage` error when state not found', async function () { - let error; - try { - await onHomePage(); - } catch (err) { - error = err; - } finally { - expect(error).to.be.instanceOf(SnapError); - expect(error.message).to.equal('Unable to initialize Snap HomePage'); - } - }); -});