Skip to content

Commit

Permalink
refactor(DeclareContract): revamp RPC starkNet_declareContract (#398)
Browse files Browse the repository at this point in the history
* feat: refactor RPC starkNet_declareContract to have superstruct validation

* Update packages/starknet-snap/src/rpcs/declare-contract.ts

Co-authored-by: Stanley Yuen <[email protected]>

* Update packages/starknet-snap/src/rpcs/declare-contract.ts

Co-authored-by: Stanley Yuen <[email protected]>

* Update packages/starknet-snap/src/rpcs/declare-contract.ts

Co-authored-by: Stanley Yuen <[email protected]>

* Update packages/starknet-snap/src/rpcs/declare-contract.ts

Co-authored-by: Stanley Yuen <[email protected]>

* Update packages/starknet-snap/src/rpcs/declare-contract.ts

Co-authored-by: Stanley Yuen <[email protected]>

* Update packages/starknet-snap/src/rpcs/declare-contract.ts

Co-authored-by: Stanley Yuen <[email protected]>

* chore: fix comments

* Update packages/starknet-snap/src/rpcs/declare-contract.test.ts

Co-authored-by: Stanley Yuen <[email protected]>

* Update packages/starknet-snap/src/rpcs/declare-contract.test.ts

Co-authored-by: Stanley Yuen <[email protected]>

* Update packages/starknet-snap/src/rpcs/declare-contract.test.ts

Co-authored-by: Stanley Yuen <[email protected]>

* Update packages/starknet-snap/src/rpcs/declare-contract.test.ts

Co-authored-by: Stanley Yuen <[email protected]>

* 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 <[email protected]>
  • Loading branch information
khanti42 and stanleyyconsensys authored Oct 22, 2024
1 parent c9c1aaf commit 5617ccf
Show file tree
Hide file tree
Showing 9 changed files with 471 additions and 229 deletions.
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
26 changes: 25 additions & 1 deletion packages/starknet-snap/src/rpcs/__tests__/helper.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
}
229 changes: 229 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,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<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: 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),
]);
});
});
Loading

0 comments on commit 5617ccf

Please sign in to comment.