Skip to content

Commit

Permalink
chore: update
Browse files Browse the repository at this point in the history
  • Loading branch information
stanleyyconsensys committed Nov 20, 2024
1 parent 6b4fc58 commit 427fb13
Show file tree
Hide file tree
Showing 7 changed files with 629 additions and 181 deletions.
97 changes: 5 additions & 92 deletions packages/starknet-snap/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -375,92 +374,6 @@ export const onUserInput: OnUserInputHandler = async ({
event: UserInputEvent;
context: InterfaceContext | null;
}): Promise<void> => {
const generateEventKey = (type: UserInputEventType, name: string) =>
`${type}_${name}`;
const eventKey = generateEventKey(event.type, event.name ?? '');

await updateInterface(
id,
<Box alignment="space-between" center={true}>
<Heading>please wait...</Heading>
<Spinner />
</Box>,
);

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();
};
214 changes: 125 additions & 89 deletions packages/starknet-snap/src/rpcs/execute-txn.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -58,6 +64,8 @@ export class ExecuteTxnRpc extends AccountRpcController<
> {
protected txnStateManager: TransactionStateManager;

protected reqStateManager: TransactionRequestStateManager;

protected accStateManager: AccountStateManager;

protected tokenStateManager: TokenStateManager;
Expand All @@ -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();
}
Expand Down Expand Up @@ -105,114 +114,141 @@ export class ExecuteTxnRpc extends AccountRpcController<
protected async handleRequest(
params: ExecuteTxnParams,
): Promise<ExecuteTxnResponse> {
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(
Expand Down
Loading

0 comments on commit 427fb13

Please sign in to comment.