From 427fb13353b3789acf927894884096516c2533b2 Mon Sep 17 00:00:00 2001 From: stanleyyuen <102275989+stanleyyconsensys@users.noreply.github.com> Date: Wed, 20 Nov 2024 21:30:53 +0800 Subject: [PATCH] chore: update --- packages/starknet-snap/src/index.tsx | 97 +------- .../starknet-snap/src/rpcs/execute-txn.ts | 214 ++++++++++-------- .../src/state/request-state-manager.test.ts | 173 ++++++++++++++ .../src/state/request-state-manager.ts | 124 ++++++++++ .../user-input-event-controller.ts | 173 ++++++++++++++ .../src/ui/fragments/LoadingUI.tsx | 20 ++ packages/starknet-snap/src/ui/utils.tsx | 9 + 7 files changed, 629 insertions(+), 181 deletions(-) create mode 100644 packages/starknet-snap/src/state/request-state-manager.test.ts create mode 100644 packages/starknet-snap/src/state/request-state-manager.ts create mode 100644 packages/starknet-snap/src/ui/controllers/user-input-event-controller.ts create mode 100644 packages/starknet-snap/src/ui/fragments/LoadingUI.tsx diff --git a/packages/starknet-snap/src/index.tsx b/packages/starknet-snap/src/index.tsx index e9ba8c37..19eefe07 100644 --- a/packages/starknet-snap/src/index.tsx +++ b/packages/starknet-snap/src/index.tsx @@ -6,11 +6,9 @@ import type { OnUserInputHandler, UserInputEvent, InterfaceContext, - InputChangeEvent, } from '@metamask/snaps-sdk'; -import { MethodNotFoundError, UserInputEventType } from '@metamask/snaps-sdk'; -import { Box, Heading, Link, Spinner, Text } from '@metamask/snaps-sdk/jsx'; -import { constants, ec, num as numUtils, TransactionType } from 'starknet'; +import { MethodNotFoundError } from '@metamask/snaps-sdk'; +import { Box, Link, Text } from '@metamask/snaps-sdk/jsx'; import { addNetwork } from './addNetwork'; import { Config } from './config'; @@ -97,6 +95,7 @@ import { removeNetwork, } from './utils/snapUtils'; import { getEstimatedFees } from './utils/starknetUtils'; +import { UserInputEventController } from './ui/controllers/user-input-event-controller'; declare const snap; logger.logLevel = parseInt(Config.logLevel, 10); @@ -375,92 +374,6 @@ export const onUserInput: OnUserInputHandler = async ({ event: UserInputEvent; context: InterfaceContext | null; }): Promise => { - const generateEventKey = (type: UserInputEventType, name: string) => - `${type}_${name}`; - const eventKey = generateEventKey(event.type, event.name ?? ''); - - await updateInterface( - id, - - please wait... - - , - ); - - switch (eventKey) { - case generateEventKey( - UserInputEventType.InputChangeEvent, - 'feeTokenSelector', - ): { - const networkStateMgr = new NetworkStateManager(); - const network = await networkStateMgr.getCurrentNetwork(); - const feeToken = (event as InputChangeEvent).value as FeeToken; - const request = context?.request as TransactionRequest; - const deriver = await getBip44Deriver(); - const { addressKey } = await getAddressKey(deriver, request.addressIndex); - const publicKey = ec.starkCurve.getStarkKey(addressKey); - const privateKey = numUtils.toHex(addressKey); - try { - if (request?.calls) { - const { includeDeploy, suggestedMaxFee, estimateResults } = - await getEstimatedFees( - network, - request.signer, - privateKey, - publicKey, - [ - { - type: TransactionType.INVOKE, - payload: request.calls.map((call) => ({ - calldata: call.calldata, - contractAddress: call.contractAddress, - entrypoint: call.entrypoint, - })), - }, - ], - { - version: - feeToken === FeeToken.STRK - ? constants.TRANSACTION_VERSION.V3 - : undefined, - }, - ); - const sufficientFunds = await hasSufficientFunds( - request.signer, - network, - request.calls, - feeToken, - suggestedMaxFee, - ); - if (!sufficientFunds) { - throw new Error('Not enough funds to pay for fee'); - } - request.maxFee = suggestedMaxFee; - request.selectedFeeToken = feeToken; - request.includeDeploy = includeDeploy; - request.resourceBounds = estimateResults.map( - (result) => result.resourceBounds, - ); - - await updateExecuteTxnFlow(id, request); - } - } catch (error) { - const errorMessage = - error.message === 'Not enough funds to pay for fee' - ? 'Not enough funds to pay for fee' - : 'Error calculating fees'; - // On failure, display ExecuteTxnUI with an error message - if (request) { - await updateExecuteTxnFlow(id, request, { - errors: { fees: errorMessage }, - }); - } else { - throw error; - } - } - break; - } - default: - throw new MethodNotFoundError() as unknown as Error; - } + const controller = new UserInputEventController(id, event, context); + await controller.handleEvent(); }; diff --git a/packages/starknet-snap/src/rpcs/execute-txn.ts b/packages/starknet-snap/src/rpcs/execute-txn.ts index 4ae7e532..1076cacc 100644 --- a/packages/starknet-snap/src/rpcs/execute-txn.ts +++ b/packages/starknet-snap/src/rpcs/execute-txn.ts @@ -1,11 +1,17 @@ import { type Json } from '@metamask/snaps-sdk'; import type { Call, Calldata } from 'starknet'; -import { constants, TransactionStatus, TransactionType } from 'starknet'; +import { + constants, + transaction, + TransactionStatus, + TransactionType, +} from 'starknet'; import type { Infer } from 'superstruct'; import { object, string, assign, optional, any } from 'superstruct'; import { v4 as uuidv4 } from 'uuid'; import { AccountStateManager } from '../state/account-state-manager'; +import { TransactionRequestStateManager } from '../state/request-state-manager'; import { TokenStateManager } from '../state/token-state-manager'; import { TransactionStateManager } from '../state/transaction-state-manager'; import { FeeToken } from '../types/snapApi'; @@ -58,6 +64,8 @@ export class ExecuteTxnRpc extends AccountRpcController< > { protected txnStateManager: TransactionStateManager; + protected reqStateManager: TransactionRequestStateManager; + protected accStateManager: AccountStateManager; protected tokenStateManager: TokenStateManager; @@ -69,6 +77,7 @@ export class ExecuteTxnRpc extends AccountRpcController< constructor(options?: AccountRpcControllerOptions) { super(options); this.txnStateManager = new TransactionStateManager(); + this.reqStateManager = new TransactionRequestStateManager(); this.accStateManager = new AccountStateManager(); this.tokenStateManager = new TokenStateManager(); } @@ -105,114 +114,141 @@ export class ExecuteTxnRpc extends AccountRpcController< protected async handleRequest( params: ExecuteTxnParams, ): Promise { - const { address, calls, abis, details } = params; - const { privateKey, publicKey } = this.account; - const callsArray = Array.isArray(calls) ? calls : [calls]; + const requestId = uuidv4(); - const { includeDeploy, suggestedMaxFee, estimateResults } = - await getEstimatedFees( - this.network, - address, - privateKey, - publicKey, - [ - { - type: TransactionType.INVOKE, - payload: calls, - }, - ], - details, - ); + try { + const { address, calls, abis, details } = params; + const { privateKey, publicKey } = this.account; + const callsArray = Array.isArray(calls) ? calls : [calls]; - const accountDeployed = !includeDeploy; - const version = - details?.version as unknown as constants.TRANSACTION_VERSION; - - const formattedCalls = await Promise.all( - callsArray.map(async (call) => - callToTransactionReqCall( - call, - this.network.chainId, + const { includeDeploy, suggestedMaxFee, estimateResults } = + await getEstimatedFees( + this.network, address, - this.tokenStateManager, + privateKey, + publicKey, + [ + { + type: TransactionType.INVOKE, + payload: calls, + }, + ], + details, + ); + + const accountDeployed = !includeDeploy; + const version = + details?.version as unknown as constants.TRANSACTION_VERSION; + + const formattedCalls = await Promise.all( + callsArray.map(async (call) => + callToTransactionReqCall( + call, + this.network.chainId, + address, + this.tokenStateManager, + ), ), - ), - ); + ); - const request: TransactionRequest = { - chainId: this.network.chainId, - networkName: this.network.name, - id: uuidv4(), - interfaceId: '', - type: TransactionType.INVOKE, - signer: address, - addressIndex: this.account.addressIndex, - maxFee: suggestedMaxFee, - calls: formattedCalls, - resourceBounds: estimateResults.map((result) => result.resourceBounds), - selectedFeeToken: - version === constants.TRANSACTION_VERSION.V3 - ? FeeToken.STRK - : FeeToken.ETH, - includeDeploy, - }; + const request: TransactionRequest = { + chainId: this.network.chainId, + networkName: this.network.name, + id: requestId, + interfaceId: '', + type: TransactionType.INVOKE, + signer: address, + addressIndex: this.account.addressIndex, + maxFee: suggestedMaxFee, + calls: formattedCalls, + resourceBounds: estimateResults.map((result) => result.resourceBounds), + selectedFeeToken: + version === constants.TRANSACTION_VERSION.V3 + ? FeeToken.STRK + : FeeToken.ETH, + includeDeploy, + }; - const interfaceId = await generateExecuteTxnFlow(request); + const interfaceId = await generateExecuteTxnFlow(request); - request.interfaceId = interfaceId; + request.interfaceId = interfaceId; - if (!(await createInteractiveConfirmDialog(interfaceId))) { - throw new UserRejectedOpError() as unknown as Error; - } + await this.reqStateManager.upsertTransactionRequest(request); - if (!accountDeployed) { - await createAccount({ - network: this.network, - address, - publicKey, - privateKey, - waitMode: false, - callback: async (contractAddress: string, transactionHash: string) => { - await this.updateAccountAsDeploy(contractAddress, transactionHash); - }, - version, + if (!(await createInteractiveConfirmDialog(interfaceId))) { + throw new UserRejectedOpError() as unknown as Error; + } + + const updatedRequest = await this.reqStateManager.getTransactionRequest({ + requestId, }); - } - const resourceBounds = estimateResults.map( - (result) => result.resourceBounds, - ); + if (!updatedRequest) { + throw new Error('Failed to retrieve the updated transaction request'); + } - const executeTxnResp = await executeTxnUtil( - this.network, - address, - privateKey, - calls, - abis, - { + if (!accountDeployed) { + await createAccount({ + network: this.network, + address, + publicKey, + privateKey, + waitMode: false, + callback: async ( + contractAddress: string, + transactionHash: string, + ) => { + await this.updateAccountAsDeploy(contractAddress, transactionHash); + }, + version: + updatedRequest.transactionVersion as unknown as constants.TRANSACTION_VERSION, + }); + } + + const invocationDetails = { ...details, // Aways repect the input, unless the account is not deployed // 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], - }, - ); + maxFee: updatedRequest.maxFee, + resourceBounds: + updatedRequest.resourceBounds[ + updatedRequest.resourceBounds.length - 1 + ], + version: + updatedRequest.selectedFeeToken === FeeToken.STRK + ? constants.TRANSACTION_VERSION.V3 + : undefined, + }; + + const executeTxnResp = await executeTxnUtil( + this.network, + address, + privateKey, + calls, + abis, + invocationDetails, + ); - if (!executeTxnResp?.transaction_hash) { - throw new Error('Failed to execute transaction'); - } + if (!executeTxnResp?.transaction_hash) { + throw new Error('Failed to execute transaction'); + } - // Since the RPC supports the `calls` parameter either as a single `call` object or an array of `call` objects, - // and the current state data structure does not yet support multiple `call` objects in a single transaction, - // we need to convert `calls` into a single `call` object as a temporary fix. - const call = Array.isArray(calls) ? calls[0] : calls; + // Since the RPC supports the `calls` parameter either as a single `call` object or an array of `call` objects, + // and the current state data structure does not yet support multiple `call` objects in a single transaction, + // we need to convert `calls` into a single `call` object as a temporary fix. + const call = Array.isArray(calls) ? calls[0] : calls; - await this.txnStateManager.addTransaction( - this.createInvokeTxn(address, executeTxnResp.transaction_hash, call), - ); + await this.txnStateManager.addTransaction( + this.createInvokeTxn(address, executeTxnResp.transaction_hash, call), + ); - return executeTxnResp; + return executeTxnResp; + } catch (error) { + throw error; + } finally { + await this.reqStateManager.removeTransactionRequest(requestId); + } } protected async updateAccountAsDeploy( diff --git a/packages/starknet-snap/src/state/request-state-manager.test.ts b/packages/starknet-snap/src/state/request-state-manager.test.ts new file mode 100644 index 00000000..04531eb8 --- /dev/null +++ b/packages/starknet-snap/src/state/request-state-manager.test.ts @@ -0,0 +1,173 @@ +import { constants } from 'starknet'; + +import { generateTransactionRequests } from '../__tests__/helper'; +import type { TransactionRequest } from '../types/snapState'; +import { mockAcccounts, mockState } from './__tests__/helper'; +import { TransactionRequestStateManager } from './request-state-manager'; +import { StateManagerError } from './state-manager'; + +describe('TransactionRequestStateManager', () => { + const getChainId = () => constants.StarknetChainId.SN_SEPOLIA; + + const prepareMockData = async () => { + const chainId = getChainId(); + const accounts = await mockAcccounts(chainId, 1); + const transactionRequests = generateTransactionRequests({ + chainId, + address: accounts[0].address, + cnt: 10, + }); + + const { state, setDataSpy, getDataSpy } = await mockState({ + transactionRequests, + }); + + return { + state, + setDataSpy, + getDataSpy, + account: accounts[0], + transactionRequests, + }; + }; + + const getNewEntity = (address) => { + const chainId = getChainId(); + const transactionRequests = generateTransactionRequests({ + chainId, + address, + cnt: 1, + }); + + return transactionRequests[0]; + }; + + const getUpdateEntity = (request: TransactionRequest) => { + return { + ...request, + maxFee: '999999', + }; + }; + + describe('getTransactionRequest', () => { + it('returns the transaction request', async () => { + const { + transactionRequests: [transactionRequest], + } = await prepareMockData(); + + const stateManager = new TransactionRequestStateManager(); + const result = await stateManager.getTransactionRequest({ + requestId: transactionRequest.id, + }); + + expect(result).toStrictEqual(transactionRequest); + }); + + it('finds the request by interfaceId', async () => { + const { + transactionRequests: [transactionRequest], + } = await prepareMockData(); + + const stateManager = new TransactionRequestStateManager(); + const result = await stateManager.getTransactionRequest({ + interfaceId: transactionRequest.interfaceId, + }); + + expect(result).toStrictEqual(transactionRequest); + }); + + it('returns null if the transaction request can not be found', async () => { + await prepareMockData(); + + const stateManager = new TransactionRequestStateManager(); + + const result = await stateManager.getTransactionRequest({ + requestId: 'something', + }); + expect(result).toBeNull(); + }); + + it('throws a `At least one search condition must be provided` error if no search criteria given', async () => { + const stateManager = new TransactionRequestStateManager(); + + await expect(stateManager.getTransactionRequest({})).rejects.toThrow( + 'At least one search condition must be provided', + ); + }); + }); + + describe('upsertTransactionRequest', () => { + it('updates the transaction request if the transaction request found', async () => { + const { + state, + transactionRequests: [transactionRequest], + } = await prepareMockData(); + const entity = getUpdateEntity(transactionRequest); + + const stateManager = new TransactionRequestStateManager(); + await stateManager.upsertTransactionRequest(entity); + + expect( + state.transactionRequests.find( + (req) => req.id === transactionRequest.id, + ), + ).toStrictEqual(entity); + }); + + it('add a new transaction request if the transaction request does not found', async () => { + const { state, account } = await prepareMockData(); + const entity = getNewEntity(account.address); + const orgLength = state.transactionRequests.length; + + const stateManager = new TransactionRequestStateManager(); + await stateManager.upsertTransactionRequest(entity); + + expect(state.transactionRequests).toHaveLength(orgLength + 1); + expect( + state.transactionRequests.find((req) => req.id === entity.id), + ).toStrictEqual(entity); + }); + + it('throws a `StateManagerError` error if an error was thrown', async () => { + const { account, setDataSpy } = await prepareMockData(); + const entity = getNewEntity(account.address); + setDataSpy.mockRejectedValue(new Error('Error')); + + const stateManager = new TransactionRequestStateManager(); + + await expect( + stateManager.upsertTransactionRequest(entity), + ).rejects.toThrow(StateManagerError); + }); + }); + + describe('removeTransactionRequests', () => { + it('removes the request', async () => { + const { + transactionRequests: [{ id }], + state, + } = await prepareMockData(); + const stateManager = new TransactionRequestStateManager(); + + await stateManager.removeTransactionRequest(id); + + expect( + state.transactionRequests.filter((req) => req.id === id), + ).toStrictEqual([]); + }); + + it('throws a `StateManagerError` error if an error was thrown', async () => { + const { + transactionRequests: [{ id }], + setDataSpy, + } = await prepareMockData(); + setDataSpy.mockRejectedValue(new Error('Error')); + + const stateManager = new TransactionRequestStateManager(); + + await expect(stateManager.removeTransactionRequest(id)).rejects.toThrow( + StateManagerError, + ); + }); + }); +}); diff --git a/packages/starknet-snap/src/state/request-state-manager.ts b/packages/starknet-snap/src/state/request-state-manager.ts new file mode 100644 index 00000000..e5c851cc --- /dev/null +++ b/packages/starknet-snap/src/state/request-state-manager.ts @@ -0,0 +1,124 @@ +import type { TransactionRequest, SnapState } from '../types/snapState'; +import { logger } from '../utils'; +import type { IFilter } from './filter'; +import { StringFllter } from './filter'; +import { StateManager, StateManagerError } from './state-manager'; + +export type ITransactionRequestFilter = IFilter; + +export class IdFilter + extends StringFllter + implements ITransactionRequestFilter +{ + dataKey = 'id'; +} + +export class InterfaceIdFilter + extends StringFllter + implements ITransactionRequestFilter +{ + dataKey = 'interfaceId'; +} + +export class TransactionRequestStateManager extends StateManager { + protected getCollection(state: SnapState): TransactionRequest[] { + return state.transactionRequests ?? []; + } + + protected updateEntity( + dataInState: TransactionRequest, + data: TransactionRequest, + ): void { + // This is the only field that can be updated + dataInState.maxFee = data.maxFee; + dataInState.selectedFeeToken = data.selectedFeeToken; + dataInState.resourceBounds = [...data.resourceBounds]; + } + + /** + * Finds a `TransactionRequest` object based on the given requestId or interfaceId. + * + * @param param - The param object. + * @param param.requestId - The requestId to search for. + * @param param.interfaceId - The interfaceId to search for. + * @param [state] - The optional SnapState object. + * @returns A Promise that resolves with the `TransactionRequest` object if found, or null if not found. + */ + async getTransactionRequest( + { + requestId, + interfaceId, + }: { + requestId?: string; + interfaceId?: string; + }, + state?: SnapState, + ): Promise { + const filters: ITransactionRequestFilter[] = []; + if (requestId) { + filters.push(new IdFilter([requestId])); + } + if (interfaceId) { + filters.push(new InterfaceIdFilter([interfaceId])); + } + if (filters.length === 0) { + throw new StateManagerError( + 'At least one search condition must be provided', + ); + } + return await this.find(filters, state); + } + + /** + * Upsert a `TransactionRequest` in the state with the given data. + * + * @param data - The `TransactionRequest` object. + * @returns A Promise that resolves when the upsert is complete. + */ + async upsertTransactionRequest(data: TransactionRequest): Promise { + try { + await this.update(async (state: SnapState) => { + const dataInState = await this.getTransactionRequest( + { + requestId: data.id, + }, + state, + ); + + if (dataInState === null) { + this.getCollection(state)?.push(data); + } else { + this.updateEntity(dataInState, data); + } + }); + } catch (error) { + throw new StateManagerError(error.message); + } + } + + /** + * Removes the `TransactionRequest` objects in the state with the given requestId. + * + * @param requestId - The requestId to search for. + * @returns A Promise that resolves when the remove is complete. + */ + async removeTransactionRequest(requestId: string): Promise { + try { + await this.update(async (state: SnapState) => { + const sizeOfTransactionRequests = this.getCollection(state).length; + + state.transactionRequests = this.getCollection(state).filter((req) => { + return req.id !== requestId; + }); + + // Check if the TransactionRequest was removed + if (sizeOfTransactionRequests === this.getCollection(state).length) { + // If the TransactionRequest does not exist, log a warning instead of throwing an error + logger.warn(`TransactionRequest with id ${requestId} does not exist`); + } + }); + } catch (error) { + throw new StateManagerError(error.message); + } + } +} diff --git a/packages/starknet-snap/src/ui/controllers/user-input-event-controller.ts b/packages/starknet-snap/src/ui/controllers/user-input-event-controller.ts new file mode 100644 index 00000000..e8314fed --- /dev/null +++ b/packages/starknet-snap/src/ui/controllers/user-input-event-controller.ts @@ -0,0 +1,173 @@ +import type { + InputChangeEvent, + InterfaceContext, + UserInputEvent, +} from '@metamask/snaps-sdk'; +import { UserInputEventType } from '@metamask/snaps-sdk'; +import { constants, ec, num as numUtils, TransactionType } from 'starknet'; + +import { NetworkStateManager } from '../../state/network-state-manager'; +import { TransactionRequestStateManager } from '../../state/request-state-manager'; +import { FeeToken } from '../../types/snapApi'; +import type { Network, TransactionRequest } from '../../types/snapState'; +import { getBip44Deriver, logger } from '../../utils'; +import { getAddressKey } from '../../utils/keyPair'; +import { getEstimatedFees } from '../../utils/starknetUtils'; +import { + hasSufficientFunds, + renderLoading, + updateExecuteTxnFlow, + updateInterface, +} from '../utils'; + +export enum FeeTokenSelectorEventKey { + FeeTokenChange = `feeTokenSelector_${UserInputEventType.InputChangeEvent}`, +} + +export class UserInputEventController { + context: InterfaceContext | null; + + event: UserInputEvent; + + eventId: string; + + reqStateMgr: TransactionRequestStateManager; + + networkStateMgr: NetworkStateManager; + + constructor( + eventId: string, + event: UserInputEvent, + context: InterfaceContext | null, + ) { + this.event = event; + this.context = context; + this.eventId = eventId; + this.reqStateMgr = new TransactionRequestStateManager(); + this.networkStateMgr = new NetworkStateManager(); + } + + async handleEvent() { + try { + const request = this.context?.request as TransactionRequest; + + if ( + !(await this.reqStateMgr.getTransactionRequest({ + requestId: request.id, + })) + ) { + throw new Error('Transaction request not found'); + } + + await renderLoading(this.eventId); + + const eventKey = `${this.event.name}_${this.event.type}`; + + switch (eventKey) { + case FeeTokenSelectorEventKey.FeeTokenChange: + await this.handleFeeTokenChange(); + break; + default: + break; + } + } catch (error) { + logger.error('onUserInput error:', error); + throw error; + } + } + + protected async deriveAccount(index: number) { + const deriver = await getBip44Deriver(); + const { addressKey } = await getAddressKey(deriver, index); + const publicKey = ec.starkCurve.getStarkKey(addressKey); + const privateKey = numUtils.toHex(addressKey); + return { + publicKey, + privateKey, + }; + } + + protected feeTokenToTransactionVersion(feeToken: FeeToken) { + return feeToken === FeeToken.STRK + ? constants.TRANSACTION_VERSION.V3 + : undefined; + } + + protected async getNetwork(chainId: string): Promise { + const network = await this.networkStateMgr.getNetwork({ chainId }); + + if (!network) { + throw new Error('Network not found'); + } + + return network; + } + + protected async handleFeeTokenChange() { + const request = this.context?.request as TransactionRequest; + const { addressIndex, calls, signer, chainId } = request; + + try { + const network = await this.getNetwork(chainId); + + const feeToken = (this.event as InputChangeEvent) + .value as unknown as FeeToken; + + const { publicKey, privateKey } = await this.deriveAccount(addressIndex); + + const requestTxnVersion = this.feeTokenToTransactionVersion(feeToken); + + const { includeDeploy, suggestedMaxFee, estimateResults } = + await getEstimatedFees( + network, + signer, + privateKey, + publicKey, + [ + { + type: TransactionType.INVOKE, + payload: calls.map((call) => ({ + calldata: call.calldata, + contractAddress: call.contractAddress, + entrypoint: call.entrypoint, + })), + }, + ], + { + version: requestTxnVersion, + }, + ); + + const sufficientFunds = await hasSufficientFunds( + signer, + network, + calls, + feeToken, + suggestedMaxFee, + ); + if (!sufficientFunds) { + throw new Error('Not enough funds to pay for fee'); + } + + request.maxFee = suggestedMaxFee; + request.selectedFeeToken = feeToken; + request.includeDeploy = includeDeploy; + request.resourceBounds = estimateResults.map( + (result) => result.resourceBounds, + ); + + await updateExecuteTxnFlow(this.eventId, request); + await this.reqStateMgr.upsertTransactionRequest(request); + } catch (error) { + const errorMessage = + error.message === 'Not enough funds to pay for fee' + ? 'Not enough funds to pay for fee' + : 'Error calculating fees'; + // On failure, display ExecuteTxnUI with an error message + + await updateExecuteTxnFlow(this.eventId, request, { + errors: { fees: errorMessage }, + }); + } + } +} diff --git a/packages/starknet-snap/src/ui/fragments/LoadingUI.tsx b/packages/starknet-snap/src/ui/fragments/LoadingUI.tsx new file mode 100644 index 00000000..7fd1fdd2 --- /dev/null +++ b/packages/starknet-snap/src/ui/fragments/LoadingUI.tsx @@ -0,0 +1,20 @@ +import { + Heading, + Box, + Spinner, + type SnapComponent, +} from '@metamask/snaps-sdk/jsx'; + +/** + * Builds a loading UI component. + * + * @returns A loading component. + */ +export const LoadingUI: SnapComponent = () => { + return ( + + please wait... + + + ); +}; diff --git a/packages/starknet-snap/src/ui/utils.tsx b/packages/starknet-snap/src/ui/utils.tsx index 17123358..9ab5ad54 100644 --- a/packages/starknet-snap/src/ui/utils.tsx +++ b/packages/starknet-snap/src/ui/utils.tsx @@ -12,6 +12,7 @@ import { import { getBalance } from '../utils/starknetUtils'; import type { ExecuteTxnUIErrors } from './components'; import { ExecuteTxnUI } from './components'; +import { LoadingUI } from './fragments/LoadingUI'; import type { TokenTotals } from './types'; /** @@ -159,6 +160,14 @@ export async function updateInterface( }); } +/** + * + * @param id + */ +export async function renderLoading(id: string): Promise { + await updateInterface(id, ); +} + /** * Checks if the provided address has sufficient funds in either STRK or ETH to cover * the total amount required for a set of contract calls and a suggested transaction fee.