From ac41722532d37b10626a8f453eafa5f84c26a9c7 Mon Sep 17 00:00:00 2001 From: khanti42 Date: Fri, 6 Sep 2024 14:26:58 +0200 Subject: [PATCH] chore: update get-starknet to match new rpcs endpoints (#349) * chore: rollback keep legacy send txn * fix: get-starknet to match new rpcs parameters * docs: mark abis as deprecated in get-starknet signTransaction * fix: address comments * chore: lint + prettier * fix: return resource bound in estimateFee and use it in executeTxn * fix: return resource bound in estimateFee and use it in executeTxn * fix: return resource bound in estimateFee and use it in executeTxn * fix: apply suggestions from code review Co-authored-by: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com> * fix: address comment --------- Co-authored-by: stanleyyuen <102275989+stanleyyconsensys@users.noreply.github.com> --- packages/get-starknet/src/signer.ts | 11 +- packages/get-starknet/src/snap.ts | 22 +- .../starknet-snap/src/__tests__/helper.ts | 37 +- packages/starknet-snap/src/executeTxn.ts | 156 ------ packages/starknet-snap/src/index.ts | 11 +- .../src/rpcs/estimateFee.test.ts | 11 +- .../starknet-snap/src/rpcs/estimateFee.ts | 7 +- .../starknet-snap/src/rpcs/executeTxn.test.ts | 10 +- packages/starknet-snap/src/rpcs/executeTxn.ts | 36 +- packages/starknet-snap/src/sendTransaction.ts | 191 ++++++++ .../src/utils/starknetUtils.test.ts | 18 +- .../starknet-snap/src/utils/starknetUtils.ts | 10 +- .../starknet-snap/test/src/executeTxn.test.ts | 340 ------------- .../test/src/sendTransaction.test.ts | 452 ++++++++++++++++++ .../wallet-ui/src/services/useStarkNetSnap.ts | 2 +- 15 files changed, 770 insertions(+), 544 deletions(-) delete mode 100644 packages/starknet-snap/src/executeTxn.ts create mode 100644 packages/starknet-snap/src/sendTransaction.ts delete mode 100644 packages/starknet-snap/test/src/executeTxn.test.ts create mode 100644 packages/starknet-snap/test/src/sendTransaction.test.ts diff --git a/packages/get-starknet/src/signer.ts b/packages/get-starknet/src/signer.ts index c2acca41..252c05fc 100644 --- a/packages/get-starknet/src/signer.ts +++ b/packages/get-starknet/src/signer.ts @@ -32,16 +32,23 @@ export class MetaMaskSigner implements SignerInterface { return new ec.starkCurve.Signature(numUtils.toBigInt(result[0]), numUtils.toBigInt(result[1])); } + /** + * Signs a transaction calling the Snap. + * + * @param transactions - The array of transactions to be signed. + * @param transactionsDetail - The details required for signing the transactions. + * @param _abis - [Deprecated] The ABI definitions for the contracts involved in the transactions. This parameter is optional and may be undefined. + * @returns A promise that resolves to the transaction signature. + */ async signTransaction( transactions: Call[], transactionsDetail: InvocationsSignerDetails, - abis?: Abi[] | undefined, + _abis?: Abi[] | undefined, ): Promise { const result = (await this.#snap.signTransaction( this.#address, transactions, transactionsDetail, - abis, )) as ArraySignatureType; return new ec.starkCurve.Signature(numUtils.toBigInt(result[0]), numUtils.toBigInt(result[1])); } diff --git a/packages/get-starknet/src/snap.ts b/packages/get-starknet/src/snap.ts index 722fc17c..bac8e669 100644 --- a/packages/get-starknet/src/snap.ts +++ b/packages/get-starknet/src/snap.ts @@ -45,10 +45,9 @@ export class MetaMaskSnap { } async signTransaction( - signerAddress: string, + address: string, transactions: Call[], transactionsDetail: InvocationsSignerDetails, - abis?: Abi[], ): Promise { return (await this.#provider.request({ method: 'wallet_invokeSnap', @@ -57,10 +56,9 @@ export class MetaMaskSnap { request: { method: 'starkNet_signTransaction', params: this.removeUndefined({ - signerAddress, + address, transactions, transactionsDetail, - abis, ...(await this.#getSnapParams()), }), }, @@ -106,10 +104,10 @@ export class MetaMaskSnap { } async execute( - senderAddress: string, - txnInvocation: AllowArray, + address: string, + calls: AllowArray, abis?: Abi[], - invocationsDetails?: InvocationsDetails, + details?: InvocationsDetails, ): Promise { return (await this.#provider.request({ method: 'wallet_invokeSnap', @@ -118,9 +116,9 @@ export class MetaMaskSnap { request: { method: 'starkNet_executeTxn', params: this.removeUndefined({ - senderAddress, - txnInvocation, - invocationsDetails, + address, + calls, + details, abis, ...(await this.#getSnapParams()), }), @@ -129,7 +127,7 @@ export class MetaMaskSnap { })) as InvokeFunctionResponse; } - async signMessage(typedDataMessage: TypedData, enableAuthorize: boolean, signerAddress: string): Promise { + async signMessage(typedDataMessage: TypedData, enableAuthorize: boolean, address: string): Promise { return (await this.#provider.request({ method: 'wallet_invokeSnap', params: { @@ -137,7 +135,7 @@ export class MetaMaskSnap { request: { method: 'starkNet_signMessage', params: this.removeUndefined({ - signerAddress, + address, typedDataMessage, enableAuthorize, ...(await this.#getSnapParams()), diff --git a/packages/starknet-snap/src/__tests__/helper.ts b/packages/starknet-snap/src/__tests__/helper.ts index 6f5c1e55..895c913f 100644 --- a/packages/starknet-snap/src/__tests__/helper.ts +++ b/packages/starknet-snap/src/__tests__/helper.ts @@ -4,7 +4,7 @@ import { } from '@metamask/key-tree'; import { generateMnemonic } from 'bip39'; import { getRandomValues } from 'crypto'; -import type { constants } from 'starknet'; +import type { constants, EstimateFee } from 'starknet'; import { ec, CallData, @@ -284,3 +284,38 @@ export function generateTransactions({ return transactions.sort((a, b) => b.timestamp - a.timestamp); } + +/** + * Method to generate a mock estimate fee response. + * + * @returns An array containing a mock EstimateFee object. + */ +export function getEstimateFees() { + return [ + { + // eslint-disable-next-line @typescript-eslint/naming-convention + overall_fee: BigInt(1500000000000000).toString(10), + // eslint-disable-next-line @typescript-eslint/naming-convention + gas_consumed: BigInt('0x0'), + suggestedMaxFee: BigInt(1500000000000000).toString(10), + // eslint-disable-next-line @typescript-eslint/naming-convention + gas_price: BigInt('0x0'), + resourceBounds: { + // eslint-disable-next-line @typescript-eslint/naming-convention + l1_gas: { + // eslint-disable-next-line @typescript-eslint/naming-convention + max_amount: '0', + // eslint-disable-next-line @typescript-eslint/naming-convention + max_price_per_unit: '0', + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + l2_gas: { + // eslint-disable-next-line @typescript-eslint/naming-convention + max_amount: '0', + // eslint-disable-next-line @typescript-eslint/naming-convention + max_price_per_unit: '0', + }, + }, + } as unknown as EstimateFee, + ]; +} diff --git a/packages/starknet-snap/src/executeTxn.ts b/packages/starknet-snap/src/executeTxn.ts deleted file mode 100644 index 075406e1..00000000 --- a/packages/starknet-snap/src/executeTxn.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { Component } from '@metamask/snaps-sdk'; -import { heading, panel, divider, DialogType } from '@metamask/snaps-sdk'; -import type { constants, Invocations } from 'starknet'; -import { TransactionType } from 'starknet'; - -import type { - ApiParamsWithKeyDeriver, - ExecuteTxnRequestParams, -} from './types/snapApi'; -import { ACCOUNT_CLASS_HASH, CAIRO_VERSION } from './utils/constants'; -import { logger } from './utils/logger'; -import { - getNetworkFromChainId, - getTxnSnapTxt, - addDialogTxt, - verifyIfAccountNeedUpgradeOrDeploy, -} from './utils/snapUtils'; -import { - getKeysFromAddress, - executeTxn as executeTxnUtil, - isAccountDeployed, - estimateFeeBulk, - getAccContractAddressAndCallData, - addFeesFromAllTransactions, - createAccount, -} from './utils/starknetUtils'; - -/** - * - * @param params - */ -export async function executeTxn(params: ApiParamsWithKeyDeriver) { - try { - const { state, keyDeriver, requestParams, wallet } = params; - const requestParamsObj = requestParams as ExecuteTxnRequestParams; - const { senderAddress: address, invocationsDetails } = requestParamsObj; - const network = getNetworkFromChainId(state, requestParamsObj.chainId); - const { privateKey, publicKey, addressIndex } = await getKeysFromAddress( - keyDeriver, - network, - state, - address, - ); - - await verifyIfAccountNeedUpgradeOrDeploy(network, address, publicKey); - - const txnInvocationArray = Array.isArray(requestParamsObj.txnInvocation) - ? requestParamsObj.txnInvocation - : [requestParamsObj.txnInvocation]; - const bulkTransactions: Invocations = txnInvocationArray.map((ele) => ({ - type: TransactionType.INVOKE, - payload: ele, - })); - - const accountDeployed = await isAccountDeployed(network, address); - const version = - invocationsDetails?.version as unknown as constants.TRANSACTION_VERSION; - - if (!accountDeployed) { - const { callData } = getAccContractAddressAndCallData(publicKey); - const deployAccountpayload = { - classHash: ACCOUNT_CLASS_HASH, - contractAddress: address, - constructorCalldata: callData, - addressSalt: publicKey, - }; - - bulkTransactions.unshift({ - type: TransactionType.DEPLOY_ACCOUNT, - payload: deployAccountpayload, - }); - } - - const fees = await estimateFeeBulk( - network, - address, - privateKey, - bulkTransactions, - invocationsDetails ?? undefined, - ); - const estimateFeeResp = addFeesFromAllTransactions(fees); - - if ( - estimateFeeResp === undefined || - estimateFeeResp.suggestedMaxFee === undefined - ) { - throw new Error('Unable to estimate fees'); - } - - const maxFee = estimateFeeResp.suggestedMaxFee.toString(10); - logger.log(`MaxFee: ${maxFee}`); - - let snapComponents: Component[] = []; - if (!accountDeployed) { - snapComponents.push(heading(`The account will be deployed`)); - addDialogTxt(snapComponents, 'Address', address); - addDialogTxt(snapComponents, 'Public Key', publicKey); - addDialogTxt(snapComponents, 'Address Index', addressIndex.toString()); - snapComponents.push(divider()); - } - - snapComponents = snapComponents.concat( - getTxnSnapTxt( - address, - network, - requestParamsObj.txnInvocation, - requestParamsObj.abis, - invocationsDetails, - ), - ); - - const response = await wallet.request({ - method: 'snap_dialog', - params: { - type: DialogType.Confirmation, - content: panel([ - heading('Do you want to sign this transaction(s)?'), - ...snapComponents, - ]), - }, - }); - if (!response) { - return false; - } - - if (!accountDeployed) { - await createAccount({ - network, - address, - publicKey, - privateKey, - waitMode: true, - callback: undefined, - version, - }); - } - const nonceSendTransaction = accountDeployed ? undefined : 1; - - return await executeTxnUtil( - network, - address, - privateKey, - requestParamsObj.txnInvocation, - requestParamsObj.abis, - { - ...invocationsDetails, - maxFee, - nonce: nonceSendTransaction, - }, - CAIRO_VERSION, - ); - } catch (error) { - logger.error(`Problem found:`, error); - throw error; - } -} diff --git a/packages/starknet-snap/src/index.ts b/packages/starknet-snap/src/index.ts index dd1ef92b..c17db8ee 100644 --- a/packages/starknet-snap/src/index.ts +++ b/packages/starknet-snap/src/index.ts @@ -23,7 +23,6 @@ import { createAccount } from './createAccount'; import { declareContract } from './declareContract'; import { estimateAccDeployFee } from './estimateAccountDeployFee'; import { estimateFees } from './estimateFees'; -import { executeTxn as executeTxnLegacy } from './executeTxn'; import { extractPublicKey } from './extractPublicKey'; import { getCurrentNetwork } from './getCurrentNetwork'; import { getErc20TokenBalance } from './getErc20TokenBalance'; @@ -54,6 +53,7 @@ import { signDeclareTransaction, verifySignature, } from './rpcs'; +import { sendTransaction } from './sendTransaction'; import { signDeployAccountTransaction } from './signDeployAccountTransaction'; import { switchNetwork } from './switchNetwork'; import type { @@ -218,8 +218,8 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { case 'starkNet_sendTransaction': apiParams.keyDeriver = await getAddressKeyDeriver(snap); - return await executeTxn.execute( - apiParams.requestParams as unknown as ExecuteTxnParams, + return await sendTransaction( + apiParams as unknown as ApiParamsWithKeyDeriver, ); case 'starkNet_getValue': @@ -267,9 +267,8 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { ); case 'starkNet_executeTxn': - apiParams.keyDeriver = await getAddressKeyDeriver(snap); - return await executeTxnLegacy( - apiParams as unknown as ApiParamsWithKeyDeriver, + return await executeTxn.execute( + apiParams.requestParams as unknown as ExecuteTxnParams, ); case 'starkNet_estimateFees': diff --git a/packages/starknet-snap/src/rpcs/estimateFee.test.ts b/packages/starknet-snap/src/rpcs/estimateFee.test.ts index c29b2560..bf2cdcf6 100644 --- a/packages/starknet-snap/src/rpcs/estimateFee.test.ts +++ b/packages/starknet-snap/src/rpcs/estimateFee.test.ts @@ -3,6 +3,7 @@ import type { Invocations } from 'starknet'; import { constants, TransactionType } from 'starknet'; import type { Infer } from 'superstruct'; +import { getEstimateFees } from '../__tests__/helper'; import { FeeTokenUnit } from '../types/snapApi'; import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../utils/constants'; import * as starknetUtils from '../utils/starknetUtils'; @@ -44,11 +45,14 @@ const prepareMockEstimateFee = ({ details: { version }, } as unknown as EstimateFeeParams; + const estimateResults = getEstimateFees(); + const estimateBulkFeeRespMock = { suggestedMaxFee: BigInt(1000000000000000).toString(10), overallFee: BigInt(1500000000000000).toString(10), unit: FeeTokenUnit.ETH, includeDeploy, + estimateResults, }; const getEstimatedFeesSpy = jest.spyOn(starknetUtils, 'getEstimatedFees'); @@ -89,7 +93,12 @@ describe('estimateFee', () => { version: constants.TRANSACTION_VERSION.V1, }, ); - expect(result).toStrictEqual(estimateBulkFeeRespMock); + expect(result).toStrictEqual({ + includeDeploy: estimateBulkFeeRespMock.includeDeploy, + overallFee: estimateBulkFeeRespMock.overallFee, + suggestedMaxFee: estimateBulkFeeRespMock.suggestedMaxFee, + unit: estimateBulkFeeRespMock.unit, + }); }); it('throws `InvalidParamsError` when request parameter is not correct', async () => { diff --git a/packages/starknet-snap/src/rpcs/estimateFee.ts b/packages/starknet-snap/src/rpcs/estimateFee.ts index dfccd922..e36df47c 100644 --- a/packages/starknet-snap/src/rpcs/estimateFee.ts +++ b/packages/starknet-snap/src/rpcs/estimateFee.ts @@ -71,7 +71,12 @@ export class EstimateFeeRpc extends AccountRpcController< details, ); - return estimateFeeResp; + return { + suggestedMaxFee: estimateFeeResp.suggestedMaxFee, + overallFee: estimateFeeResp.overallFee, + unit: estimateFeeResp.unit, + includeDeploy: estimateFeeResp.includeDeploy, + }; } } diff --git a/packages/starknet-snap/src/rpcs/executeTxn.test.ts b/packages/starknet-snap/src/rpcs/executeTxn.test.ts index 23e35384..7dfdfe11 100644 --- a/packages/starknet-snap/src/rpcs/executeTxn.test.ts +++ b/packages/starknet-snap/src/rpcs/executeTxn.test.ts @@ -6,6 +6,7 @@ import type { UniversalDetails, Call, InvokeFunctionResponse } from 'starknet'; import { constants } from 'starknet'; import callsExamples from '../__tests__/fixture/callsExamples.json'; // Assuming you have a similar fixture +import { getEstimateFees } from '../__tests__/helper'; import type { FeeTokenUnit } from '../types/snapApi'; import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../utils/constants'; import * as starknetUtils from '../utils/starknetUtils'; @@ -50,11 +51,14 @@ const prepareMockExecuteTxn = async ( transaction_hash: transactionHash, }; + const estimateResults = getEstimateFees(); + const getEstimatedFeesRepsMock = { suggestedMaxFee: BigInt(1000000000000000).toString(10), overallFee: BigInt(1000000000000000).toString(10), includeDeploy: !accountDeployed, unit: 'wei' as FeeTokenUnit, + estimateResults, }; const getEstimatedFeesSpy = jest.spyOn(starknetUtils, 'getEstimatedFees'); @@ -114,6 +118,8 @@ describe('ExecuteTxn', () => { { ...callsExample.details, maxFee: getEstimatedFeesRepsMock.suggestedMaxFee, + resourceBounds: + getEstimatedFeesRepsMock.estimateResults[0].resourceBounds, }, ); expect(getEstimatedFeesSpy).toHaveBeenCalled(); @@ -153,7 +159,7 @@ describe('ExecuteTxn', () => { privateKey: account.privateKey, publicKey: account.publicKey, version: transactionVersion, - waitMode: true, + waitMode: false, }); expect(executeTxnUtilSpy).toHaveBeenCalledWith( network, @@ -166,6 +172,8 @@ describe('ExecuteTxn', () => { version: transactionVersion, maxFee: getEstimatedFeesRepsMock.suggestedMaxFee, nonce: 1, + resourceBounds: + getEstimatedFeesRepsMock.estimateResults[0].resourceBounds, }, ); }, diff --git a/packages/starknet-snap/src/rpcs/executeTxn.ts b/packages/starknet-snap/src/rpcs/executeTxn.ts index 28434984..dcb55a00 100644 --- a/packages/starknet-snap/src/rpcs/executeTxn.ts +++ b/packages/starknet-snap/src/rpcs/executeTxn.ts @@ -7,7 +7,6 @@ import { divider, } from '@metamask/snaps-sdk'; import convert from 'ethereum-unit-converter'; -import { logger } from 'ethers'; import type { Call, Calldata } from 'starknet'; import { constants, TransactionStatus, TransactionType } from 'starknet'; import type { Infer } from 'superstruct'; @@ -27,6 +26,7 @@ import { UniversalDetailsStruct, CallsStruct, } from '../utils'; +import { logger } from '../utils/logger'; import { createAccount, executeTxn as executeTxnUtil, @@ -96,19 +96,20 @@ export class ExecuteTxnRpc extends AccountRpcController< const { address, calls, abis, details } = params; const { privateKey, publicKey } = this.account; - const { includeDeploy, suggestedMaxFee } = await getEstimatedFees( - this.network, - address, - privateKey, - publicKey, - [ - { - type: TransactionType.INVOKE, - payload: calls, - }, - ], - details, - ); + const { includeDeploy, suggestedMaxFee, estimateResults } = + await getEstimatedFees( + this.network, + address, + privateKey, + publicKey, + [ + { + type: TransactionType.INVOKE, + payload: calls, + }, + ], + details, + ); const accountDeployed = !includeDeploy; const version = @@ -132,7 +133,7 @@ export class ExecuteTxnRpc extends AccountRpcController< address, publicKey, privateKey, - waitMode: true, + waitMode: false, callback: async (contractAddress: string, transactionHash: string) => { await this.updateAccountAsDeploy(contractAddress, transactionHash); }, @@ -140,6 +141,10 @@ export class ExecuteTxnRpc extends AccountRpcController< }); } + const resourceBounds = estimateResults.map( + (result) => result.resourceBounds, + ); + const executeTxnResp = await executeTxnUtil( this.network, address, @@ -152,6 +157,7 @@ export class ExecuteTxnRpc extends AccountRpcController< // TODO: we may also need to increment the nonce base on the input, if the account is not deployed nonce: accountDeployed ? details?.nonce : 1, maxFee: suggestedMaxFee, + resourceBounds: resourceBounds[resourceBounds.length - 1], }, ); diff --git a/packages/starknet-snap/src/sendTransaction.ts b/packages/starknet-snap/src/sendTransaction.ts new file mode 100644 index 00000000..b06eb25c --- /dev/null +++ b/packages/starknet-snap/src/sendTransaction.ts @@ -0,0 +1,191 @@ +import { heading, panel, DialogType } from '@metamask/snaps-sdk'; +import { num as numUtils, constants } from 'starknet'; + +import { createAccount } from './createAccount'; +import { estimateFee } from './estimateFee'; +import type { + ApiParamsWithKeyDeriver, + SendTransactionRequestParams, +} from './types/snapApi'; +import type { Transaction } from './types/snapState'; +import { TransactionStatus, VoyagerTransactionType } from './types/snapState'; +import { logger } from './utils/logger'; +import { toJson } from './utils/serializer'; +import { + getNetworkFromChainId, + getSendTxnText, + upsertTransaction, +} from './utils/snapUtils'; +import { + validateAndParseAddress, + getKeysFromAddress, + getCallDataArray, + executeTxn, + isAccountDeployed, + isUpgradeRequired, +} from './utils/starknetUtils'; + +/** + * + * @param params + */ +export async function sendTransaction(params: ApiParamsWithKeyDeriver) { + try { + const { state, wallet, saveMutex, keyDeriver, requestParams } = params; + const requestParamsObj = requestParams as SendTransactionRequestParams; + + if ( + !requestParamsObj.contractAddress || + !requestParamsObj.senderAddress || + !requestParamsObj.contractFuncName + ) { + throw new Error( + `The given contract address, sender address, and function name need to be non-empty string, got: ${toJson( + requestParamsObj, + )}`, + ); + } + + try { + validateAndParseAddress(requestParamsObj.contractAddress); + } catch (error) { + throw new Error( + `The given contract address is invalid: ${requestParamsObj.contractAddress}`, + ); + } + try { + validateAndParseAddress(requestParamsObj.senderAddress); + } catch (error) { + throw new Error( + `The given sender address is invalid: ${requestParamsObj.senderAddress}`, + ); + } + + const { contractAddress } = requestParamsObj; + const { contractFuncName } = requestParamsObj; + const contractCallData = getCallDataArray( + requestParamsObj.contractCallData as unknown as string, + ); + const { senderAddress } = requestParamsObj; + const network = getNetworkFromChainId(state, requestParamsObj.chainId); + + if (await isUpgradeRequired(network, senderAddress)) { + throw new Error('Upgrade required'); + } + + const { privateKey: senderPrivateKey, addressIndex } = + await getKeysFromAddress(keyDeriver, network, state, senderAddress); + let maxFee = requestParamsObj.maxFee + ? numUtils.toBigInt(requestParamsObj.maxFee) + : constants.ZERO; + if (maxFee === constants.ZERO) { + const { suggestedMaxFee } = await estimateFee(params); + maxFee = numUtils.toBigInt(suggestedMaxFee); + } + + const signingTxnComponents = getSendTxnText( + state, + contractAddress, + contractFuncName, + contractCallData, + senderAddress, + maxFee, + network, + ); + const response = await wallet.request({ + method: 'snap_dialog', + params: { + type: DialogType.Confirmation, + content: panel([ + heading('Do you want to sign this transaction ?'), + ...signingTxnComponents, + ]), + }, + }); + if (!response) { + return false; + } + + const txnInvocation = { + contractAddress, + entrypoint: contractFuncName, + calldata: contractCallData, + }; + + logger.log( + `sendTransaction:\ntxnInvocation: ${toJson( + txnInvocation, + )}\nmaxFee: ${maxFee.toString()}}`, + ); + + const accountDeployed = await isAccountDeployed(network, senderAddress); + if (!accountDeployed) { + // Deploy account before sending the transaction + logger.log( + 'sendTransaction:\nFirst transaction : send deploy transaction', + ); + const createAccountApiParams = { + state, + wallet: params.wallet, + saveMutex: params.saveMutex, + keyDeriver, + requestParams: { + addressIndex, + deploy: true, + chainId: requestParamsObj.chainId, + }, + }; + await createAccount(createAccountApiParams, true, true); + } + + // In case this is the first transaction we assign a nonce of 1 to make sure it does after the deploy transaction + const nonceSendTransaction = accountDeployed ? undefined : 1; + const txnResp = await executeTxn( + network, + senderAddress, + senderPrivateKey, + txnInvocation, + undefined, + { + maxFee, + nonce: nonceSendTransaction, + }, + ); + + logger.log(`sendTransaction:\ntxnResp: ${toJson(txnResp)}`); + + if (txnResp.transaction_hash) { + const txn: Transaction = { + txnHash: txnResp.transaction_hash, + txnType: VoyagerTransactionType.INVOKE, + chainId: network.chainId, + senderAddress, + contractAddress, + contractFuncName, + contractCallData: contractCallData.map( + (data: numUtils.BigNumberish) => { + try { + return numUtils.toHex(numUtils.toBigInt(data)); + } catch (error) { + // data is already send to chain, hence we should not throw error + return '0x0'; + } + }, + ), + finalityStatus: TransactionStatus.RECEIVED, + executionStatus: TransactionStatus.RECEIVED, + status: '', // DEPRECATED LATER + failureReason: '', + eventIds: [], + timestamp: Math.floor(Date.now() / 1000), + }; + + await upsertTransaction(txn, wallet, saveMutex); + } + + return txnResp; + } catch (error) { + logger.error(`Problem found:`, error); + throw error; + } +} diff --git a/packages/starknet-snap/src/utils/starknetUtils.test.ts b/packages/starknet-snap/src/utils/starknetUtils.test.ts index e0764e19..45d882fd 100644 --- a/packages/starknet-snap/src/utils/starknetUtils.test.ts +++ b/packages/starknet-snap/src/utils/starknetUtils.test.ts @@ -1,6 +1,7 @@ import type { EstimateFee, Invocations } from 'starknet'; import { constants, TransactionType } from 'starknet'; +import { getEstimateFees } from '../__tests__/helper'; import { mockAccount, prepareMockAccount } from '../rpcs/__tests__/helper'; import { FeeTokenUnit } from '../types/snapApi'; import type { SnapState } from '../types/snapState'; @@ -39,6 +40,9 @@ describe('getEstimatedFees', () => { const accountDeployedSpy = jest.spyOn(starknetUtils, 'isAccountDeployed'); accountDeployedSpy.mockResolvedValue(deployed); + const estimateResults = getEstimateFees(); + const { resourceBounds } = estimateResults[0]; + const estimateFeeResp = { // eslint-disable-next-line @typescript-eslint/naming-convention overall_fee: overallFee, @@ -47,6 +51,7 @@ describe('getEstimatedFees', () => { suggestedMaxFee, // eslint-disable-next-line @typescript-eslint/naming-convention gas_price: BigInt('0x0'), + resourceBounds, } as unknown as EstimateFee; const estimateBulkFeeSpy = jest.spyOn(starknetUtils, 'estimateFeeBulk'); estimateBulkFeeSpy.mockResolvedValue([estimateFeeResp]); @@ -56,6 +61,7 @@ describe('getEstimatedFees', () => { invocations, accountDeployedSpy, estimateBulkFeeSpy, + estimateFeeResp, }; }; @@ -82,9 +88,8 @@ describe('getEstimatedFees', () => { expectedUnit: FeeTokenUnit; }) => { const network = STARKNET_SEPOLIA_TESTNET_NETWORK; - const { account, invocations, estimateBulkFeeSpy } = await prepareSpy( - true, - ); + const { account, invocations, estimateBulkFeeSpy, estimateFeeResp } = + await prepareSpy(true); const call = invocations[0]; const resp = await starknetUtils.getEstimatedFees( @@ -117,15 +122,15 @@ describe('getEstimatedFees', () => { overallFee: overallFee.toString(10), unit: expectedUnit, // to verify if the unit is return correctly includeDeploy: false, + estimateResults: [estimateFeeResp], }); }, ); it('estimates fees with account deploy payload if the account is not deployed', async () => { const network = STARKNET_SEPOLIA_TESTNET_NETWORK; - const { account, estimateBulkFeeSpy, invocations } = await prepareSpy( - false, - ); + const { account, estimateBulkFeeSpy, estimateFeeResp, invocations } = + await prepareSpy(false); const deployAccountpayload = starknetUtils.createAccountDeployPayload( account.address, account.publicKey, @@ -161,6 +166,7 @@ describe('getEstimatedFees', () => { overallFee: overallFee.toString(10), unit: FeeTokenUnit.ETH, includeDeploy: true, + estimateResults: [estimateFeeResp], }); }); }); diff --git a/packages/starknet-snap/src/utils/starknetUtils.ts b/packages/starknet-snap/src/utils/starknetUtils.ts index 0cafe7c2..3c3fe673 100644 --- a/packages/starknet-snap/src/utils/starknetUtils.ts +++ b/packages/starknet-snap/src/utils/starknetUtils.ts @@ -41,7 +41,6 @@ import { TransactionType as StarknetTransactionType, } from 'starknet'; -import type { EstimateFeeResponse } from '../rpcs/estimateFee'; import { FeeTokenUnit, type RpcV4GetTransactionReceiptResponse, @@ -1073,7 +1072,13 @@ export async function getEstimatedFees( publicKey: string, transactionInvocations: Invocations, invocationsDetails?: UniversalDetails, -): Promise { +): Promise<{ + suggestedMaxFee: string; + overallFee: string; + unit: FeeTokenUnit; + includeDeploy: boolean; + estimateResults: EstimateFee[]; +}> { const accountDeployed = await isAccountDeployed(network, address); if (!accountDeployed) { const deployAccountpayload = createAccountDeployPayload(address, publicKey); @@ -1102,6 +1107,7 @@ export async function getEstimatedFees( ? FeeTokenUnit.STRK : FeeTokenUnit.ETH, includeDeploy: !accountDeployed, + estimateResults: estimateBulkFeeResp, }; } diff --git a/packages/starknet-snap/test/src/executeTxn.test.ts b/packages/starknet-snap/test/src/executeTxn.test.ts deleted file mode 100644 index 36addc5b..00000000 --- a/packages/starknet-snap/test/src/executeTxn.test.ts +++ /dev/null @@ -1,340 +0,0 @@ -import chai, { expect } from 'chai'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import { WalletMock } from '../wallet.mock.test'; -import * as utils from '../../src/utils/starknetUtils'; -import { executeTxn } from '../../src/executeTxn'; -import { SnapState } from '../../src/types/snapState'; -import { - STARKNET_MAINNET_NETWORK, - STARKNET_SEPOLIA_TESTNET_NETWORK, -} from '../../src/utils/constants'; -import { - createAccountProxyTxn, - estimateDeployFeeResp, - getBip44EntropyStub, - account1, - estimateFeeResp, -} from '../constants.test'; -import { getAddressKeyDeriver } from '../../src/utils/keyPair'; -import { Mutex } from 'async-mutex'; -import { - ApiParamsWithKeyDeriver, - ExecuteTxnRequestParams, -} from '../../src/types/snapApi'; -import { GetTransactionReceiptResponse } from 'starknet'; - -chai.use(sinonChai); -const sandbox = sinon.createSandbox(); - -describe('Test function: executeTxn', function () { - this.timeout(10000); - const walletStub = new WalletMock(); - const state: SnapState = { - accContracts: [account1], - erc20Tokens: [], - networks: [STARKNET_MAINNET_NETWORK, STARKNET_SEPOLIA_TESTNET_NETWORK], - transactions: [], - }; - let apiParams: ApiParamsWithKeyDeriver; - - const requestObject: ExecuteTxnRequestParams = { - chainId: STARKNET_MAINNET_NETWORK.chainId, - senderAddress: account1.address, - txnInvocation: { - entrypoint: 'transfer', - calldata: ['0', '0', '0'], - contractAddress: createAccountProxyTxn.contractAddress, - }, - invocationsDetails: { - maxFee: 100, - }, - }; - - beforeEach(async function () { - walletStub.rpcStubs.snap_getBip44Entropy.callsFake(getBip44EntropyStub); - apiParams = { - state, - requestParams: requestObject, - wallet: walletStub, - saveMutex: new Mutex(), - keyDeriver: await getAddressKeyDeriver(walletStub), - }; - sandbox.stub(utils, 'estimateFeeBulk').callsFake(async () => { - return [estimateFeeResp]; - }); - sandbox.stub(utils, 'estimateFee').callsFake(async () => { - return estimateFeeResp; - }); - sandbox.stub(utils, 'estimateAccountDeployFee').callsFake(async () => { - return estimateDeployFeeResp; - }); - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; - }); - sandbox.useFakeTimers(createAccountProxyTxn.timestamp); - walletStub.rpcStubs.snap_dialog.resolves(true); - walletStub.rpcStubs.snap_manageState.resolves(state); - sandbox - .stub(utils, 'waitForTransaction') - .resolves({} as unknown as GetTransactionReceiptResponse); - }); - - afterEach(function () { - walletStub.reset(); - sandbox.restore(); - apiParams.requestParams = requestObject; - }); - - it('should executeTxn correctly and deploy an account', async function () { - sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); - sandbox.stub(utils, 'isAccountDeployed').resolves(false); - const createAccountStub = sandbox - .stub(utils, 'createAccount') - .resolvesThis(); - const stub = sandbox.stub(utils, 'executeTxn').resolves({ - transaction_hash: 'transaction_hash', - }); - const result = await executeTxn(apiParams); - const { privateKey } = await utils.getKeysFromAddress( - apiParams.keyDeriver, - STARKNET_MAINNET_NETWORK, - state, - account1.address, - ); - expect(result).to.eql({ - transaction_hash: 'transaction_hash', - }); - expect(stub).to.have.been.calledOnceWith( - STARKNET_MAINNET_NETWORK, - account1.address, - privateKey, - { - entrypoint: 'transfer', - calldata: ['0', '0', '0'], - contractAddress: createAccountProxyTxn.contractAddress, - }, - undefined, - { maxFee: '22702500105945', nonce: 1 }, - ); - expect(createAccountStub).to.have.been.calledOnceWith({ - network: STARKNET_MAINNET_NETWORK, - address: account1.address, - publicKey: account1.publicKey, - privateKey: privateKey, - waitMode: true, - callback: undefined, - version: undefined, - }); - }); - - it('should executeTxn multiple and deploy an account', async function () { - sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); - sandbox.stub(utils, 'isAccountDeployed').resolves(false); - const createAccountStub = sandbox - .stub(utils, 'createAccount') - .resolvesThis(); - const stub = sandbox.stub(utils, 'executeTxn').resolves({ - transaction_hash: 'transaction_hash', - }); - apiParams.requestParams = { - chainId: STARKNET_MAINNET_NETWORK.chainId, - senderAddress: account1.address, - txnInvocation: [ - { - entrypoint: 'transfer', - calldata: ['0', '0', '0'], - contractAddress: createAccountProxyTxn.contractAddress, - }, - { - entrypoint: 'transfer2', - calldata: ['0', '0', '0'], - contractAddress: createAccountProxyTxn.contractAddress, - }, - ], - invocationsDetails: { - maxFee: 100, - }, - }; - const result = await executeTxn(apiParams); - const { privateKey } = await utils.getKeysFromAddress( - apiParams.keyDeriver, - STARKNET_MAINNET_NETWORK, - state, - account1.address, - ); - - expect(result).to.eql({ - transaction_hash: 'transaction_hash', - }); - expect(stub).to.have.been.calledOnce; - expect(stub).to.have.been.calledWith( - STARKNET_MAINNET_NETWORK, - account1.address, - privateKey, - [ - { - entrypoint: 'transfer', - calldata: ['0', '0', '0'], - contractAddress: createAccountProxyTxn.contractAddress, - }, - { - entrypoint: 'transfer2', - calldata: ['0', '0', '0'], - contractAddress: createAccountProxyTxn.contractAddress, - }, - ], - undefined, - { maxFee: '22702500105945', nonce: 1 }, - ); - expect(createAccountStub).to.have.been.calledOnceWith({ - network: STARKNET_MAINNET_NETWORK, - address: account1.address, - publicKey: account1.publicKey, - privateKey: privateKey, - waitMode: true, - callback: undefined, - version: undefined, - }); - }); - - it('should executeTxn and not deploy an account', async function () { - const createAccountStub = sandbox.stub(utils, 'createAccount'); - sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); - sandbox.stub(utils, 'isAccountDeployed').resolves(true); - const stub = sandbox.stub(utils, 'executeTxn').resolves({ - transaction_hash: 'transaction_hash', - }); - const result = await executeTxn(apiParams); - const { privateKey } = await utils.getKeysFromAddress( - apiParams.keyDeriver, - STARKNET_MAINNET_NETWORK, - state, - account1.address, - ); - - expect(result).to.eql({ - transaction_hash: 'transaction_hash', - }); - expect(stub).to.have.been.calledOnce; - expect(stub).to.have.been.calledWith( - STARKNET_MAINNET_NETWORK, - account1.address, - privateKey, - { - entrypoint: 'transfer', - calldata: ['0', '0', '0'], - contractAddress: createAccountProxyTxn.contractAddress, - }, - undefined, - { maxFee: '22702500105945', nonce: undefined }, - ); - expect(createAccountStub).to.not.have.been.called; - }); - - it('should executeTxn multiple and not deploy an account', async function () { - const createAccountStub = sandbox.stub(utils, 'createAccount'); - sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); - sandbox.stub(utils, 'isAccountDeployed').resolves(true); - const stub = sandbox.stub(utils, 'executeTxn').resolves({ - transaction_hash: 'transaction_hash', - }); - apiParams.requestParams = { - chainId: STARKNET_MAINNET_NETWORK.chainId, - senderAddress: account1.address, - txnInvocation: [ - { - entrypoint: 'transfer', - calldata: ['0', '0', '0'], - contractAddress: createAccountProxyTxn.contractAddress, - }, - { - entrypoint: 'transfer2', - calldata: ['0', '0', '0'], - contractAddress: createAccountProxyTxn.contractAddress, - }, - ], - invocationsDetails: { - maxFee: 100, - }, - }; - const result = await executeTxn(apiParams); - const { privateKey } = await utils.getKeysFromAddress( - apiParams.keyDeriver, - STARKNET_MAINNET_NETWORK, - state, - account1.address, - ); - - expect(result).to.eql({ - transaction_hash: 'transaction_hash', - }); - expect(stub).to.have.been.calledOnce; - expect(stub).to.have.been.calledWith( - STARKNET_MAINNET_NETWORK, - account1.address, - privateKey, - [ - { - entrypoint: 'transfer', - calldata: ['0', '0', '0'], - contractAddress: createAccountProxyTxn.contractAddress, - }, - { - entrypoint: 'transfer2', - calldata: ['0', '0', '0'], - contractAddress: createAccountProxyTxn.contractAddress, - }, - ], - undefined, - { maxFee: '22702500105945', nonce: undefined }, - ); - expect(createAccountStub).to.not.have.been.called; - }); - - it('should throw error if executeTxn fail', async function () { - sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); - sandbox.stub(utils, 'isAccountDeployed').resolves(true); - const stub = sandbox.stub(utils, 'executeTxn').rejects('error'); - const { privateKey } = await utils.getKeysFromAddress( - apiParams.keyDeriver, - STARKNET_MAINNET_NETWORK, - state, - account1.address, - ); - let result; - try { - await executeTxn(apiParams); - } catch (e) { - result = e; - } finally { - expect(result).to.be.an('Error'); - expect(stub).to.have.been.calledOnce; - expect(stub).to.have.been.calledWith( - STARKNET_MAINNET_NETWORK, - account1.address, - privateKey, - { - entrypoint: 'transfer', - calldata: ['0', '0', '0'], - contractAddress: createAccountProxyTxn.contractAddress, - }, - undefined, - { maxFee: '22702500105945', nonce: undefined }, - ); - } - }); - - it('should return false if user rejected to sign the transaction', async function () { - sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); - sandbox.stub(utils, 'isAccountDeployed').resolves(true); - walletStub.rpcStubs.snap_dialog.resolves(false); - const stub = sandbox.stub(utils, 'executeTxn').resolves({ - transaction_hash: 'transaction_hash', - }); - - const result = await executeTxn(apiParams); - expect(result).to.equal(false); - expect(stub).to.have.been.not.called; - }); -}); diff --git a/packages/starknet-snap/test/src/sendTransaction.test.ts b/packages/starknet-snap/test/src/sendTransaction.test.ts new file mode 100644 index 00000000..5cb42d48 --- /dev/null +++ b/packages/starknet-snap/test/src/sendTransaction.test.ts @@ -0,0 +1,452 @@ +import chai, { expect } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { WalletMock } from '../wallet.mock.test'; +import * as utils from '../../src/utils/starknetUtils'; +import * as snapUtils from '../../src/utils/snapUtils'; +import { SnapState } from '../../src/types/snapState'; +import { sendTransaction } from '../../src/sendTransaction'; +import * as estimateFeeSnap from '../../src/estimateFee'; +import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../../src/utils/constants'; +import { + account1, + createAccountProxyResp, + estimateDeployFeeResp, + estimateFeeResp, + getBalanceResp, + getBip44EntropyStub, + sendTransactionFailedResp, + sendTransactionResp, + token2, + token3, + unfoundUserAddress, + Cairo1Account1, +} from '../constants.test'; +import { getAddressKeyDeriver } from '../../src/utils/keyPair'; +import { Mutex } from 'async-mutex'; +import { + ApiParamsWithKeyDeriver, + FeeTokenUnit, + SendTransactionRequestParams, +} from '../../src/types/snapApi'; +import { GetTransactionReceiptResponse } from 'starknet'; + +chai.use(sinonChai); +chai.use(chaiAsPromised); +const sandbox = sinon.createSandbox(); + +describe('Test function: sendTransaction', function () { + this.timeout(5000); + const walletStub = new WalletMock(); + const state: SnapState = { + accContracts: [], + erc20Tokens: [token2, token3], + networks: [STARKNET_SEPOLIA_TESTNET_NETWORK], + transactions: [], + }; + let apiParams: ApiParamsWithKeyDeriver; + + const requestObject: SendTransactionRequestParams = { + contractAddress: + '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', + contractFuncName: 'transfer', + contractCallData: + '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', + senderAddress: account1.address, + chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, + }; + + beforeEach(async function () { + walletStub.rpcStubs.snap_getBip44Entropy.callsFake(getBip44EntropyStub); + apiParams = { + state, + requestParams: {}, + wallet: walletStub, + saveMutex: new Mutex(), + keyDeriver: await getAddressKeyDeriver(walletStub), + }; + }); + + afterEach(function () { + walletStub.reset(); + sandbox.restore(); + }); + + describe('when request param validation fail', function () { + let invalidRequest: SendTransactionRequestParams = Object.assign( + {}, + requestObject, + ); + + afterEach(function () { + invalidRequest = Object.assign({}, requestObject); + apiParams.requestParams = requestObject; + }); + + it('should show error when request contractAddress is not given', async function () { + invalidRequest.contractAddress = undefined as unknown as string; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result.message).to.be.include( + 'The given contract address, sender address, and function name need to be non-empty string', + ); + } + }); + + it('should show error when request contractAddress is invalid', async function () { + invalidRequest.contractAddress = '0x0'; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result.message).to.be.include( + 'The given contract address is invalid', + ); + } + }); + + it('should show error when request senderAddress is not given', async function () { + invalidRequest.senderAddress = undefined as unknown as string; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result.message).to.be.include( + 'The given contract address, sender address, and function name need to be non-empty string', + ); + } + }); + + it('should show error when request contractAddress is invalid', async function () { + invalidRequest.senderAddress = '0x0'; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result.message).to.be.include( + 'The given sender address is invalid', + ); + } + }); + + it('should show error when request contractFuncName is not given', async function () { + invalidRequest.contractFuncName = undefined as unknown as string; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result.message).to.be.include( + 'The given contract address, sender address, and function name need to be non-empty string', + ); + } + }); + + it('should show error when request network not found', async function () { + invalidRequest.chainId = '0x0'; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + }); + }); + + describe('when request param validation pass', function () { + beforeEach(async function () { + apiParams.requestParams = Object.assign({}, requestObject); + }); + + afterEach(async function () { + apiParams.requestParams = Object.assign({}, requestObject); + }); + + describe('when require upgrade checking fail', function () { + it('should throw error', async function () { + const isUpgradeRequiredStub = sandbox + .stub(utils, 'isUpgradeRequired') + .throws('network error'); + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(isUpgradeRequiredStub).to.have.been.calledOnceWith( + STARKNET_SEPOLIA_TESTNET_NETWORK, + account1.address, + ); + expect(result).to.be.an('Error'); + } + }); + }); + + describe('when account require upgrade', function () { + let isUpgradeRequiredStub: sinon.SinonStub; + beforeEach(async function () { + isUpgradeRequiredStub = sandbox + .stub(utils, 'isUpgradeRequired') + .resolves(true); + }); + + it('should throw error if upgrade required', async function () { + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(isUpgradeRequiredStub).to.have.been.calledOnceWith( + STARKNET_SEPOLIA_TESTNET_NETWORK, + account1.address, + ); + expect(result).to.be.an('Error'); + } + }); + }); + + describe('when account do not require upgrade', function () { + let executeTxnResp; + let executeTxnStub: sinon.SinonStub; + beforeEach(async function () { + apiParams.requestParams = { + ...apiParams.requestParams, + senderAddress: Cairo1Account1.address, + }; + sandbox.stub(utils, 'isUpgradeRequired').resolves(false); + sandbox.stub(estimateFeeSnap, 'estimateFee').resolves({ + suggestedMaxFee: estimateFeeResp.suggestedMaxFee.toString(10), + overallFee: estimateFeeResp.overall_fee.toString(10), + unit: FeeTokenUnit.ETH, + includeDeploy: true, + }); + executeTxnResp = sendTransactionResp; + executeTxnStub = sandbox + .stub(utils, 'executeTxn') + .resolves(executeTxnResp); + walletStub.rpcStubs.snap_manageState.resolves(state); + walletStub.rpcStubs.snap_dialog.resolves(true); + sandbox + .stub(utils, 'waitForTransaction') + .resolves({} as unknown as GetTransactionReceiptResponse); + }); + + describe('when account is deployed', function () { + beforeEach(async function () { + sandbox.stub(utils, 'isAccountDeployed').resolves(true); + }); + + it('should send a transaction for transferring 10 tokens correctly', async function () { + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; + expect(result).to.be.eql(sendTransactionResp); + }); + + it('should send a transaction for transferring 10 tokens but not update snap state if transaction_hash is missing from response', async function () { + executeTxnStub.restore(); + executeTxnStub = sandbox + .stub(utils, 'executeTxn') + .resolves(sendTransactionFailedResp); + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; + expect(result).to.be.eql(sendTransactionFailedResp); + }); + + it('should send a transaction with given max fee for transferring 10 tokens correctly', async function () { + const apiRequest = + apiParams.requestParams as SendTransactionRequestParams; + apiRequest.maxFee = '15135825227039'; + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; + expect(result).to.be.eql(sendTransactionResp); + }); + + it('should send a transfer transaction for empty call data', async function () { + const apiRequest = + apiParams.requestParams as SendTransactionRequestParams; + apiRequest.contractCallData = undefined; + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; + expect(result).to.be.eql(sendTransactionResp); + }); + + it('should send a transaction for empty call data', async function () { + const apiRequest = + apiParams.requestParams as SendTransactionRequestParams; + apiRequest.contractCallData = undefined; + apiRequest.contractFuncName = 'get_signer'; + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; + expect(result).to.be.eql(sendTransactionResp); + }); + + it('should send a transaction for transferring 10 tokens from an unfound user correctly', async function () { + const apiRequest = + apiParams.requestParams as SendTransactionRequestParams; + apiRequest.senderAddress = unfoundUserAddress; + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; + expect(result).to.be.eql(sendTransactionResp); + }); + + it('should throw error if upsertTransaction failed', async function () { + sandbox.stub(snapUtils, 'upsertTransaction').throws(new Error()); + let result; + try { + await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + }); + + it('should return false if user rejected to sign the transaction', async function () { + walletStub.rpcStubs.snap_dialog.resolves(false); + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; + expect(result).to.be.eql(false); + }); + + it('should use heading, text and copyable component', async function () { + executeTxnResp = sendTransactionFailedResp; + sandbox.stub(utils, 'getSigner').callsFake(async () => { + return account1.publicKey; + }); + const requestObject: SendTransactionRequestParams = { + contractAddress: account1.address, + contractFuncName: 'get_signer', + contractCallData: '**foo**', + senderAddress: account1.address, + }; + apiParams.requestParams = requestObject; + await sendTransaction(apiParams); + const expectedDialogParams = { + type: 'confirmation', + content: { + type: 'panel', + children: [ + { + type: 'heading', + value: 'Do you want to sign this transaction ?', + }, + { + type: 'text', + value: `**Signer Address:**`, + }, + { + type: 'copyable', + value: + '0x04882a372da3dfe1c53170ad75893832469bf87b62b13e84662565c4a88f25cd', + }, + { + type: 'text', + value: `**Contract:**`, + }, + { + type: 'copyable', + value: + '0x04882a372da3dfe1c53170ad75893832469bf87b62b13e84662565c4a88f25cd', + }, + { + type: 'text', + value: `**Call Data:**`, + }, + { + type: 'copyable', + value: '[**foo**]', + }, + { + type: 'text', + value: `**Estimated Gas Fee(ETH):**`, + }, + { + type: 'copyable', + value: '0.000022702500105945', + }, + { + type: 'text', + value: `**Network:**`, + }, + { + type: 'copyable', + value: 'Sepolia Testnet', + }, + ], + }, + }; + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledWith( + expectedDialogParams, + ); + }); + }); + + describe('when account is not deployed', function () { + beforeEach(async function () { + sandbox.stub(utils, 'isAccountDeployed').resolves(false); + }); + + it('send a transaction for transferring 10 tokens and a transaction for deploy correctly', async function () { + sandbox.stub(utils, 'deployAccount').callsFake(async () => { + return createAccountProxyResp; + }); + sandbox.stub(utils, 'getBalance').callsFake(async () => { + return getBalanceResp[0]; + }); + sandbox + .stub(utils, 'estimateAccountDeployFee') + .callsFake(async () => { + return estimateDeployFeeResp; + }); + const requestObject: SendTransactionRequestParams = { + contractAddress: + '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', + contractFuncName: 'transfer', + contractCallData: + '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', + senderAddress: account1.address, + }; + apiParams.requestParams = requestObject; + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; + expect(result).to.be.eql(sendTransactionResp); + }); + }); + }); + }); +}); diff --git a/packages/wallet-ui/src/services/useStarkNetSnap.ts b/packages/wallet-ui/src/services/useStarkNetSnap.ts index f44e1262..72c9f664 100644 --- a/packages/wallet-ui/src/services/useStarkNetSnap.ts +++ b/packages/wallet-ui/src/services/useStarkNetSnap.ts @@ -392,7 +392,7 @@ export const useStarkNetSnap = () => { params: { snapId, request: { - method: 'starkNet_sendTransaction', + method: 'starkNet_executeTxn', params: { ...defaultParam, address,