Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refine(DeclareContract): revamp RPC starkNet_declareContract #399

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
75 changes: 0 additions & 75 deletions packages/starknet-snap/src/declareContract.ts

This file was deleted.

8 changes: 4 additions & 4 deletions packages/starknet-snap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -35,12 +34,14 @@ import type {
VerifySignatureParams,
SwitchNetworkParams,
GetDeploymentDataParams,
DeclareContractParams,
WatchAssetParams,
} from './rpcs';
import {
displayPrivateKey,
estimateFee,
executeTxn,
declareContract,
signMessage,
signTransaction,
signDeclareTransaction,
Expand Down Expand Up @@ -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':
Expand Down
272 changes: 272 additions & 0 deletions packages/starknet-snap/src/rpcs/declare-contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import { BigNumber, utils } from 'ethers';
import type { Abi } 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 {
mockAccount,
prepareConfirmDialog,
prepareMockAccount,
} from './__tests__/helper';
import { declareContract } from './declare-contract';
import type { DeclareContractResponse } from './declare-contract';

jest.mock('../utils/snap');
jest.mock('../utils/logger');

type DeclareContractPayload = Infer<typeof DeclareContractPayloadStruct>;

// 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: any,
) => {
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: BigNumber.from(1000000000000000).toString() };
const transactionHash = '0x123';

const {
account,
request,
declareContractRespMock,
confirmDialogSpy,
declareContractUtilSpy,
} = await prepareMockDeclareContract(transactionHash, payload, details);

confirmDialogSpy.mockResolvedValue(true);

const result = await declareContract.execute(request);

expect(result).toStrictEqual(declareContractRespMock);
expect(declareContractUtilSpy).toHaveBeenCalledWith(
STARKNET_SEPOLIA_TESTNET_NETWORK,
account.address,
account.privateKey,
request.payload,
request.details,
);
});

it('throws UserRejectedOpError if user cancels the dialog', async () => {
const payload = generateExpectedDeclareTransactionPayload();
const details = { maxFee: BigNumber.from(1000000000000000).toString() };
const transactionHash = '0x123';

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 any)).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: BigNumber.from(1000000000000000).toString() };
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: BigNumber.from(1000000000000000).toString() };
// Convert maxFee to ETH from Wei
const maxFeeInEth = utils.formatUnits(details.maxFee, 'ether');
const transactionHash = '0x123';

const { request, confirmDialogSpy, account } =
await prepareMockDeclareContract(transactionHash, payload, details);

await declareContract.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: 'Signer Address',
value: {
value: account.address,
markdown: false,
type: 'text',
},
},
{
type: 'divider',
},
{
type: 'row',
label: 'Network',
value: {
value: STARKNET_SEPOLIA_TESTNET_NETWORK.name,
markdown: false,
type: 'text',
},
},
{
type: 'divider',
},
{
type: 'row',
label: 'Contract',
value: {
value: toJson(payload.contract),
markdown: false,
type: 'text',
},
},
{
type: 'divider',
},
{
type: 'row',
label: 'Compiled Class Hash',
value: {
value: payload.compiledClassHash,
markdown: false,
type: 'text',
},
},
{
type: 'divider',
},
{
type: 'row',
label: 'Class Hash',
value: {
value: payload.classHash,
markdown: false,
type: 'text',
},
},
{
type: 'divider',
},
{
type: 'row',
label: 'Max Fee (ETH)',
value: {
value: maxFeeInEth,
markdown: false,
type: 'text',
},
},
]);
});
});
Loading
Loading