Skip to content

Commit

Permalink
feat: estimate fees (#162)
Browse files Browse the repository at this point in the history
* feat: add estimate fees
  • Loading branch information
stanleyyconsensys authored Nov 10, 2023
1 parent 538704e commit 0aee946
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 2 deletions.
30 changes: 30 additions & 0 deletions packages/starknet-snap/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,18 @@ <h1>Hello, Snaps!</h1>
<input type="submit" id="declareContract" value="Declare Contract" />
</fieldset>
</form>
<form id="estimateFees">
<fieldset>
<legend>Estimate Fees</legend>
<label for="estimateFees_senderAddress">Sender Address</label>
<input type="text" id="estimateFees_senderAddress" name="estimateFees_senderAddress" /><br />
<label for="estimateFees_invocations">Invocations</label>
<input type="text" id="estimateFees_invocations" name="estimateFees_invocations" /><br />
<label for="estimateFees_invocationDetails">Invocation Details</label>
<input type="text" id="estimateFees_invocationDetails" name="estimateFees_invocationDetails" /><br />
<input type="submit" id="estimateFees" value="Estimate Fees" />
</fieldset>
</form>
</body>

<script type="module">
Expand Down Expand Up @@ -387,6 +399,10 @@ <h1>Hello, Snaps!</h1>
id: 'recoverUserAccounts',
method: recoverUserAccounts,
},
{
id: 'estimateFees',
method: estimateFees,
}
];

for (const form of formWithSubmit) {
Expand Down Expand Up @@ -707,6 +723,20 @@ <h1>Hello, Snaps!</h1>
});
}

async function estimateFees(e) {
e.preventDefault(); // to prevent default form behavior

const senderAddress = document.getElementById('estimateFees_senderAddress').value;
const invocationDetails = document.getElementById('estimateFees_invocationDetails').value;
const invocations = document.getElementById('estimateFees_invocations').value;

await callSnap('starkNet_estimateFees', {
senderAddress,
invocations: invocations ? JSON.parse(invocations): undefined,
invocationDetails: invocationDetails ? JSON.parse(invocationDetails) : undefined
});
}

async function callSnap(method, params) {
try {
const chainId = document.getElementById('targetChainId').value;
Expand Down
36 changes: 36 additions & 0 deletions packages/starknet-snap/src/estimateFees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { toJson } from './utils/serializer';
import { getNetworkFromChainId } from './utils/snapUtils';
import { getKeysFromAddress, estimateFeeBulk } from './utils/starknetUtils';
import { ApiParams, EstimateFeesRequestParams } from './types/snapApi';
import { logger } from './utils/logger';

export async function estimateFees(params: ApiParams) {
try {
const { state, keyDeriver, requestParams } = params;
const requestParamsObj = requestParams as EstimateFeesRequestParams;

logger.log(`estimateFees params: ${toJson(requestParamsObj, 2)}`);

const senderAddress = requestParamsObj.senderAddress;
const network = getNetworkFromChainId(state, requestParamsObj.chainId);
const { privateKey: senderPrivateKey } = await getKeysFromAddress(keyDeriver, network, state, senderAddress);

const fees = await estimateFeeBulk(
network,
senderAddress,
senderPrivateKey,
requestParamsObj.invocations,
requestParamsObj.invocationDetails ? requestParamsObj.invocationDetails : undefined,
);

return fees.map((fee) => ({
overall_fee: fee.overall_fee.toString(10) || '0',
gas_consumed: fee.gas_consumed.toString(10) || '0',
gas_price: fee.gas_price.toString(10) || '0',
suggestedMaxFee: fee.suggestedMaxFee.toString(10) || '0',
}));
} catch (err) {
logger.error(`Problem found: ${err}`);
throw err;
}
}
5 changes: 5 additions & 0 deletions packages/starknet-snap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { Mutex } from 'async-mutex';
import { OnRpcRequestHandler } from '@metamask/snaps-types';
import { ApiParams, ApiRequestParams } from './types/snapApi';
import { estimateAccDeployFee } from './estimateAccountDeployFee';
import { estimateFees } from './estimateFees';
import { declareContract } from './declareContract';
import { logger } from './utils/logger';

Expand Down Expand Up @@ -166,6 +167,10 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ origin, request }) =>
apiParams.keyDeriver = await getAddressKeyDeriver(snap);
return recoverAccounts(apiParams);

case 'starkNet_estimateFees':
apiParams.keyDeriver = await getAddressKeyDeriver(snap);
return estimateFees(apiParams);

case 'starkNet_declareContract':
apiParams.keyDeriver = await getAddressKeyDeriver(snap);
return declareContract(apiParams);
Expand Down
17 changes: 16 additions & 1 deletion packages/starknet-snap/src/types/snapApi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { BIP44AddressKeyDeriver } from '@metamask/key-tree';
import Mutex from 'async-mutex/lib/Mutex';
import { SnapState, VoyagerTransactionType } from './snapState';
import { Abi, Call, InvocationsSignerDetails, DeclareContractPayload, InvocationsDetails } from 'starknet';
import {
Abi,
Call,
InvocationsSignerDetails,
DeclareContractPayload,
InvocationsDetails,
Invocations,
EstimateFeeDetails,
} from 'starknet';

export interface ApiParams {
state: SnapState;
Expand Down Expand Up @@ -31,6 +39,7 @@ export type ApiRequestParams =
| GetStoredTransactionsRequestParams
| GetTransactionsRequestParams
| RecoverAccountsRequestParams
| EstimateFeesRequestParams
| DeclareContractRequestParams
| SignTransactionParams;

Expand Down Expand Up @@ -145,6 +154,12 @@ export interface RecoverAccountsRequestParams extends BaseRequestParams {
maxMissed?: string | number;
}

export interface EstimateFeesRequestParams extends BaseRequestParams {
senderAddress: string;
invocations: Invocations;
invocationDetails?: EstimateFeeDetails;
}

export interface DeclareContractRequestParams extends BaseRequestParams {
senderAddress: string;
contractPayload: DeclareContractPayload;
Expand Down
4 changes: 3 additions & 1 deletion packages/starknet-snap/src/utils/starknetUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
GetTransactionResponse,
Invocations,
validateAndParseAddress as _validateAndParseAddress,
EstimateFeeDetails,
DeclareContractPayload,
DeclareContractResponse,
InvocationsDetails,
Expand Down Expand Up @@ -109,12 +110,13 @@ export const estimateFeeBulk = async (
senderAddress: string,
privateKey: string | Uint8Array,
txnInvocation: Invocations,
invocationsDetails: EstimateFeeDetails = { blockIdentifier: 'latest' },
): Promise<EstimateFee[]> => {
// ensure always calling the sequencer endpoint since the rpc endpoint and
// starknet.js are not supported yet.
const provider = getProvider(network);
const account = new Account(provider, senderAddress, privateKey);
return account.estimateFeeBulk(txnInvocation, { blockIdentifier: 'latest' });
return account.estimateFeeBulk(txnInvocation, invocationsDetails);
};

export const executeTxn = async (
Expand Down
101 changes: 101 additions & 0 deletions packages/starknet-snap/test/src/estimateFees.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import chai, { expect } from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { WalletMock } from '../wallet.mock.test';
import { SnapState } from '../../src/types/snapState';
import { estimateFees } from '../../src/estimateFees';
import { STARKNET_TESTNET_NETWORK } from '../../src/utils/constants';
import { account2, estimateDeployFeeResp2, estimateDeployFeeResp3, getBip44EntropyStub } from '../constants.test';
import { getAddressKeyDeriver } from '../../src/utils/keyPair';
import * as utils from '../../src/utils/starknetUtils';
import { Mutex } from 'async-mutex';
import { ApiParams } from '../../src/types/snapApi';
import { TransactionType } from 'starknet';
chai.use(sinonChai);
const sandbox = sinon.createSandbox();

describe('Test function: estimateFees', function () {
this.timeout(5000);
const walletStub = new WalletMock();
const state: SnapState = {
accContracts: [account2],
erc20Tokens: [],
networks: [STARKNET_TESTNET_NETWORK],
transactions: [],
};
const apiParams: ApiParams = {
state,
requestParams: {},
wallet: walletStub,
saveMutex: new Mutex(),
};

beforeEach(async function () {
walletStub.rpcStubs.snap_getBip44Entropy.callsFake(getBip44EntropyStub);
apiParams.keyDeriver = await getAddressKeyDeriver(walletStub);
});

afterEach(function () {
walletStub.reset();
sandbox.restore();
});

it('should estimate the fee including deploy txn correctly', async function () {
const feeResult = [estimateDeployFeeResp2, estimateDeployFeeResp3];
sandbox.stub(utils, 'estimateFeeBulk').callsFake(async () => {
return feeResult;
});
apiParams.requestParams = {
senderAddress: account2.address,
chainId: STARKNET_TESTNET_NETWORK.chainId,
invocations: [
{
type: TransactionType.INVOKE,
payload: {
entrypoint: 'transfer',
contractAddress: '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7',
calldata: ['1697416752243704114657612983658108968471303240361660550219082009242042413588', '1', '0'],
},
},
],
};
const expectedResult = feeResult.map((fee) => ({
overall_fee: fee.overall_fee.toString(10) || '0',
gas_consumed: fee.gas_consumed.toString(10) || '0',
gas_price: fee.gas_price.toString(10) || '0',
suggestedMaxFee: fee.suggestedMaxFee.toString(10) || '0',
}));
const result = await estimateFees(apiParams);
expect(result).to.eql(expectedResult);
});

it('should throw error if estimateFee failed', async function () {
sandbox.stub(utils, 'getSigner').callsFake(async () => {
return account2.publicKey;
});
sandbox.stub(utils, 'estimateFeeBulk').throws(new Error());
apiParams.requestParams = {
senderAddress: account2.address,
chainId: STARKNET_TESTNET_NETWORK.chainId,
invocations: [
{
type: TransactionType.INVOKE,
payload: {
entrypoint: 'transfer',
contractAddress: '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7',
calldata: ['1697416752243704114657612983658108968471303240361660550219082009242042413588', '1', '0'],
},
},
],
};

let result;
try {
await estimateFees(apiParams);
} catch (err) {
result = err;
} finally {
expect(result).to.be.an('Error');
}
});
});

0 comments on commit 0aee946

Please sign in to comment.