Skip to content

Commit

Permalink
chore: change wallet rpc and error handle
Browse files Browse the repository at this point in the history
  • Loading branch information
stanleyyconsensys committed Oct 11, 2024
1 parent dde2613 commit 301585c
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 86 deletions.
70 changes: 60 additions & 10 deletions packages/get-starknet/src/__tests__/helper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { constants } from 'starknet';

import type { Network } from '../type';
import { MetaMaskSnap } from '../snap';
import type { MetaMaskProvider, Network } from '../type';
import { MetaMaskSnapWallet } from '../wallet';

export const SepoliaNetwork: Network = {
name: 'Sepolia Testnet',
Expand All @@ -11,7 +13,7 @@ export const SepoliaNetwork: Network = {
accountClassHash: '', // from argent-x repo
};

export const MainnetaNetwork: Network = {
export const MainnetNetwork: Network = {
name: 'Mainnet',
baseUrl: 'https://mainnet.starknet.io',
chainId: constants.StarknetChainId.SN_MAIN,
Expand All @@ -21,15 +23,17 @@ export const MainnetaNetwork: Network = {
};

/**
* Generate an account object.
*
* @param options0
* @param options0.addressSalt
* @param options0.publicKey
* @param options0.address
* @param options0.addressIndex
* @param options0.derivationPath
* @param options0.deployTxnHash
* @param options0.chainId
* @param params
* @param params.addressSalt - The salt of the address.
* @param params.publicKey - The public key of the account.
* @param params.address - The address of the account.
* @param params.addressIndex - The index of the address.
* @param params.derivationPath - The derivation path of the address.
* @param params.deployTxnHash - The transaction hash of the deploy transaction.
* @param params.chainId - The chain id of the account.
* @returns The account object.
*/
export function generateAccount({
addressSalt = 'addressSalt',
Expand Down Expand Up @@ -58,3 +62,49 @@ export function generateAccount({
chainId,
};
}

export class MockProvider implements MetaMaskProvider {
request = jest.fn();
}

/**
* Create a wallet instance.
*/
export function createWallet() {
return new MetaMaskSnapWallet(new MockProvider());
}

/**
* Mock the wallet init method.
*
* @param params
* @param params.install - The return value of the installIfNot method.
* @param params.currentNetwork - The return value of the getCurrentNetwork method.
* @param params.address - The address of the account.
* @returns The spy objects.
*/
export function mockWalletInit({
install = true,
currentNetwork = SepoliaNetwork,
address = '0x04882a372da3dfe1c53170ad75893832469bf87b62b13e84662565c4a88f25cd',
}: {
install?: boolean;
currentNetwork?: Network;
address?: string;
}) {
const installSpy = jest.spyOn(MetaMaskSnap.prototype, 'installIfNot');
const getCurrentNetworkSpy = jest.spyOn(MetaMaskSnap.prototype, 'getCurrentNetwork');
const recoverDefaultAccountSpy = jest.spyOn(MetaMaskSnap.prototype, 'recoverDefaultAccount');
const initSpy = jest.spyOn(MetaMaskSnapWallet.prototype, 'init');

installSpy.mockResolvedValue(install);
getCurrentNetworkSpy.mockResolvedValue(currentNetwork);
recoverDefaultAccountSpy.mockResolvedValue(generateAccount({ address }));

return {
initSpy,
installSpy,
getCurrentNetworkSpy,
recoverDefaultAccountSpy,
};
}
10 changes: 7 additions & 3 deletions packages/get-starknet/src/utils/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { RpcMessage, RpcTypeToMessageMap } from 'get-starknet-core';

import type { MetaMaskSnap } from '../snap';
import type { MetaMaskSnapWallet } from '../wallet';
import { createStarkError } from './error';

export type IStarknetWalletRpc = {
execute<Rpc extends RpcMessage['type']>(
Expand All @@ -22,9 +23,12 @@ export abstract class StarknetWalletRpc implements IStarknetWalletRpc {
async execute<Rpc extends RpcMessage['type']>(
params?: RpcTypeToMessageMap[Rpc]['params'],
): Promise<RpcTypeToMessageMap[Rpc]['result']> {
await this.wallet.init();

return this.handleRequest(params);
try {
await this.wallet.init(false);
return await this.handleRequest(params);
} catch (error) {
throw createStarkError(error?.data?.walletRpcError?.code);
}
}

abstract handleRequest<Rpc extends RpcMessage['type']>(
Expand Down
126 changes: 73 additions & 53 deletions packages/get-starknet/src/wallet.test.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,42 @@
import { Mutex } from 'async-mutex';
import { Provider } from 'starknet';

import { generateAccount, SepoliaNetwork } from './__tests__/helper';
import { SepoliaNetwork, mockWalletInit, createWallet } from './__tests__/helper';
import { MetaMaskAccount } from './accounts';
import { MetaMaskSnap } from './snap';
import type { MetaMaskProvider, Network } from './type';
import { MetaMaskSnapWallet } from './wallet';
import { WalletSupportedSpecs } from './rpcs';
import type { AccContract, Network } from './type';

describe('MetaMaskSnapWallet', () => {
class MockProvider implements MetaMaskProvider {
request = jest.fn();
}

const createWallet = () => {
return new MetaMaskSnapWallet(new MockProvider(), '*');
};

describe('enable', () => {
it('returns an account address', async () => {
const expectedAccountAddress = '0x04882a372da3dfe1c53170ad75893832469bf87b62b13e84662565c4a88f25cd'; // in hex
jest.spyOn(MetaMaskSnap.prototype, 'installIfNot').mockResolvedValue(true);
jest.spyOn(MetaMaskSnap.prototype, 'getCurrentNetwork').mockResolvedValue(SepoliaNetwork);
jest.spyOn(MetaMaskSnap.prototype, 'recoverDefaultAccount').mockResolvedValue(
generateAccount({
address: expectedAccountAddress,
}),
);
mockWalletInit({
address: expectedAccountAddress,
});

const wallet = createWallet();
const [address] = await wallet.enable();

expect(address).toStrictEqual(expectedAccountAddress);
});

it('throws `Unable to recover accounts` error if the account address not return from the Snap', async () => {
const { recoverDefaultAccountSpy } = mockWalletInit({});
recoverDefaultAccountSpy.mockResolvedValue({} as unknown as AccContract);

const wallet = createWallet();

await expect(wallet.enable()).rejects.toThrow('Unable to recover accounts');
});
});

describe('init', () => {
it('installs the snap and set the properties', async () => {
const expectedAccountAddress = '0x04882a372da3dfe1c53170ad75893832469bf87b62b13e84662565c4a88f25cd'; // in hex
const installSpy = jest.spyOn(MetaMaskSnap.prototype, 'installIfNot');
jest.spyOn(MetaMaskSnap.prototype, 'getCurrentNetwork').mockResolvedValue(SepoliaNetwork);
jest.spyOn(MetaMaskSnap.prototype, 'recoverDefaultAccount').mockResolvedValue(
generateAccount({
address: expectedAccountAddress,
}),
);
installSpy.mockResolvedValue(true);

const { installSpy } = mockWalletInit({
address: expectedAccountAddress,
});

const wallet = createWallet();
await wallet.init();
Expand All @@ -56,38 +49,26 @@ describe('MetaMaskSnapWallet', () => {
expect(wallet.account).toBeDefined();
});

it('set the properties base on the given network', async () => {
const expectedAccountAddress = '0x04882a372da3dfe1c53170ad75893832469bf87b62b13e84662565c4a88f25cd'; // in hex
const installSpy = jest.spyOn(MetaMaskSnap.prototype, 'installIfNot');
jest.spyOn(MetaMaskSnap.prototype, 'getCurrentNetwork');
jest.spyOn(MetaMaskSnap.prototype, 'recoverDefaultAccount').mockResolvedValue(
generateAccount({
address: expectedAccountAddress,
}),
);
installSpy.mockResolvedValue(true);
it('does not create the lock if the `createLock` param is false', async () => {
const runExclusiveSpy = jest.spyOn(Mutex.prototype, 'runExclusive');
runExclusiveSpy.mockReturnThis();
mockWalletInit({});

const wallet = createWallet();
await wallet.init(SepoliaNetwork);
await wallet.init(false);

expect(installSpy).toHaveBeenCalled();
expect(wallet.isConnected).toBe(true);
expect(wallet.selectedAddress).toStrictEqual(expectedAccountAddress);
expect(wallet.chainId).toStrictEqual(SepoliaNetwork.chainId);
expect(wallet.provider).toBeDefined();
expect(wallet.account).toBeDefined();
expect(runExclusiveSpy).not.toHaveBeenCalled();
});

it('throw `Snap is not installed` error if the snap is not able to install', async () => {
jest.spyOn(MetaMaskSnap.prototype, 'installIfNot').mockResolvedValue(false);
mockWalletInit({ install: false });

const wallet = createWallet();
await expect(wallet.init()).rejects.toThrow('Snap is not installed');
});

it('throw `Unable to find the selected network` error if the network is not return from snap', async () => {
jest.spyOn(MetaMaskSnap.prototype, 'installIfNot').mockResolvedValue(true);
jest.spyOn(MetaMaskSnap.prototype, 'getCurrentNetwork').mockResolvedValue(null as unknown as Network);
mockWalletInit({ currentNetwork: null as unknown as Network });

const wallet = createWallet();
await expect(wallet.init()).rejects.toThrow('Unable to find the selected network');
Expand All @@ -96,9 +77,7 @@ describe('MetaMaskSnapWallet', () => {

describe('account', () => {
it('returns an account object', async () => {
jest.spyOn(MetaMaskSnap.prototype, 'installIfNot').mockResolvedValue(true);
jest.spyOn(MetaMaskSnap.prototype, 'getCurrentNetwork').mockResolvedValue(SepoliaNetwork);
jest.spyOn(MetaMaskSnap.prototype, 'recoverDefaultAccount').mockResolvedValue(generateAccount({}));
mockWalletInit({});

const wallet = createWallet();
await wallet.enable();
Expand All @@ -115,9 +94,7 @@ describe('MetaMaskSnapWallet', () => {

describe('provider', () => {
it('returns an provider object', async () => {
jest.spyOn(MetaMaskSnap.prototype, 'installIfNot').mockResolvedValue(true);
jest.spyOn(MetaMaskSnap.prototype, 'getCurrentNetwork').mockResolvedValue(SepoliaNetwork);
jest.spyOn(MetaMaskSnap.prototype, 'recoverDefaultAccount').mockResolvedValue(generateAccount({}));
mockWalletInit({});

const wallet = createWallet();
await wallet.enable();
Expand All @@ -131,4 +108,47 @@ describe('MetaMaskSnapWallet', () => {
expect(() => wallet.provider).toThrow('Network is not set');
});
});

describe('request', () => {
it('executes a request', async () => {
const spy = jest.spyOn(WalletSupportedSpecs.prototype, 'execute');
spy.mockReturnThis();

const wallet = createWallet();
await wallet.request({ type: 'wallet_supportedSpecs' });

expect(spy).toHaveBeenCalled();
});

it('throws `WalletRpcError` if the request method does not exist', async () => {
const wallet = createWallet();
// force the 'invalid_method' as a correct type of the request to test the error
await expect(wallet.request({ type: 'invalid_method' as unknown as 'wallet_supportedSpecs' })).rejects.toThrow(
'Method not supported',
);
});
});

describe('isPreauthorized', () => {
it('returns true', async () => {
const wallet = createWallet();
expect(await wallet.isPreauthorized()).toBe(true);
});
});

describe('on', () => {
it('throws `Method not supported` error', async () => {
const wallet = createWallet();

expect(() => wallet.on()).toThrow('Method not supported');
});
});

describe('off', () => {
it('throws `Method not supported` error', async () => {
const wallet = createWallet();

expect(() => wallet.off()).toThrow('Method not supported');
});
});
});
Loading

0 comments on commit 301585c

Please sign in to comment.