Skip to content

Commit

Permalink
feat: reduce number of dry run calls (FuelLabs#1767)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedsalk authored Feb 20, 2024
1 parent 116d642 commit 880f91a
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 136 deletions.
11 changes: 11 additions & 0 deletions .changeset/shy-jars-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@fuel-ts/account": minor
"@fuel-ts/program": minor
---

- For a contract call, reduced the number of dry run calls before the call from 4 to 1
- For a contract simulation, reduced the number of dry run calls before the simulation from 3 to 1
- For a transfer from an account, reduced the number of dry run calls from 2 to 1
- Optimized predicate estimation so that there are no calls to the node if all predicates in a transaction have been estimated
- `Predicate.estimateTxDependencies` now returns receipts which are used for the purposes of the optimizations mentioned above
- `BaseInvocationScope.fundWithRequiredCoins` now calculates the `fee` parameter internally so it was removed from the function signature
4 changes: 2 additions & 2 deletions packages/account/src/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ describe('Account', () => {

const estimateTxDependencies = vi
.spyOn(providersMod.Provider.prototype, 'estimateTxDependencies')
.mockImplementation(() => Promise.resolve());
.mockImplementation(() => Promise.resolve({ receipts: [] }));

const sendTransaction = vi
.spyOn(providersMod.Provider.prototype, 'sendTransaction')
Expand Down Expand Up @@ -341,7 +341,7 @@ describe('Account', () => {

const estimateTxDependencies = vi
.spyOn(providersMod.Provider.prototype, 'estimateTxDependencies')
.mockImplementation(() => Promise.resolve());
.mockImplementation(() => Promise.resolve({ receipts: [] }));

const simulate = vi
.spyOn(providersMod.Provider.prototype, 'simulate')
Expand Down
28 changes: 21 additions & 7 deletions packages/account/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
ScriptTransactionRequestLike,
ProviderSendTxParams,
TransactionResponse,
EstimateTransactionParams,
} from './providers';
import {
withdrawScript,
Expand Down Expand Up @@ -330,7 +331,11 @@ export class Account extends AbstractAccount {
const params = { gasPrice: minGasPrice, ...txParams };
const request = new ScriptTransactionRequest(params);
request.addCoinOutput(Address.fromAddressOrString(destination), amount, assetId);
const { maxFee, requiredQuantities, gasUsed } = await this.provider.getTransactionCost(request);
const { maxFee, requiredQuantities, gasUsed, estimatedInputs } =
await this.provider.getTransactionCost(request, [], {
estimateTxDependencies: true,
resourcesOwner: this,
});

request.gasPrice = bn(txParams.gasPrice ?? minGasPrice);
request.gasLimit = bn(txParams.gasLimit ?? gasUsed);
Expand All @@ -344,6 +349,8 @@ export class Account extends AbstractAccount {

await this.fund(request, requiredQuantities, maxFee);

request.updatePredicateInputs(estimatedInputs);

return request;
}

Expand All @@ -367,7 +374,7 @@ export class Account extends AbstractAccount {
txParams: TxParamsType = {}
): Promise<TransactionResponse> {
const request = await this.createTransfer(destination, amount, assetId, txParams);
return this.sendTransaction(request);
return this.sendTransaction(request, { estimateTxDependencies: false });
}

/**
Expand Down Expand Up @@ -497,17 +504,19 @@ export class Account extends AbstractAccount {
*/
async sendTransaction(
transactionRequestLike: TransactionRequestLike,
options?: Pick<ProviderSendTxParams, 'awaitExecution'>
{ estimateTxDependencies = true, awaitExecution }: ProviderSendTxParams = {}
): Promise<TransactionResponse> {
if (this._connector) {
return this.provider.getTransactionResponse(
await this._connector.sendTransaction(this.address.toString(), transactionRequestLike)
);
}
const transactionRequest = transactionRequestify(transactionRequestLike);
await this.provider.estimateTxDependencies(transactionRequest);
if (estimateTxDependencies) {
await this.provider.estimateTxDependencies(transactionRequest);
}
return this.provider.sendTransaction(transactionRequest, {
...options,
awaitExecution,
estimateTxDependencies: false,
});
}
Expand All @@ -518,9 +527,14 @@ export class Account extends AbstractAccount {
* @param transactionRequestLike - The transaction request to be simulated.
* @returns A promise that resolves to the call result.
*/
async simulateTransaction(transactionRequestLike: TransactionRequestLike): Promise<CallResult> {
async simulateTransaction(
transactionRequestLike: TransactionRequestLike,
{ estimateTxDependencies = true }: EstimateTransactionParams = {}
): Promise<CallResult> {
const transactionRequest = transactionRequestify(transactionRequestLike);
await this.provider.estimateTxDependencies(transactionRequest);
if (estimateTxDependencies) {
await this.provider.estimateTxDependencies(transactionRequest);
}
return this.provider.simulate(transactionRequest, { estimateTxDependencies: false });
}

Expand Down
148 changes: 86 additions & 62 deletions packages/account/src/providers/provider.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { Address } from '@fuel-ts/address';
import { ErrorCode, FuelError } from '@fuel-ts/errors';
import type { AbstractAddress } from '@fuel-ts/interfaces';
import type { BN } from '@fuel-ts/math';
import { bn, max } from '@fuel-ts/math';
import type { AbstractAccount, AbstractAddress } from '@fuel-ts/interfaces';
import { BN, bn, max } from '@fuel-ts/math';
import type { Transaction } from '@fuel-ts/transactions';
import {
InputType,
TransactionType,
InputMessageCoder,
TransactionCoder,
} from '@fuel-ts/transactions';
import { arrayify } from '@fuel-ts/utils';
import { checkFuelCoreVersionCompatibility } from '@fuel-ts/versions';
import { equalBytes } from '@noble/curves/abstract/utils';
import type { BytesLike } from 'ethers';
import { getBytesCopy, hexlify, Network } from 'ethers';
import type { DocumentNode } from 'graphql';
import { GraphQLClient } from 'graphql-request';
import { clone } from 'ramda';

import type { Predicate } from '../predicate';

import { getSdk as getOperationsSdk } from './__generated__/operations';
import type {
GqlChainInfoFragmentFragment,
Expand All @@ -36,7 +39,6 @@ import type {
TransactionRequest,
TransactionRequestInput,
CoinTransactionRequestInput,
ScriptTransactionRequest,
} from './transaction-request';
import { transactionRequestify } from './transaction-request';
import type { TransactionResultReceipt } from './transaction-response';
Expand Down Expand Up @@ -244,7 +246,7 @@ export type EstimatePredicateParams = {

export type TransactionCostParams = EstimateTransactionParams &
EstimatePredicateParams & {
resourcesOwner?: AbstractAddress;
resourcesOwner?: AbstractAccount;
};

/**
Expand Down Expand Up @@ -625,7 +627,7 @@ export default class Provider {
): Promise<CallResult> {
const transactionRequest = transactionRequestify(transactionRequestLike);
if (estimateTxDependencies) {
await this.estimateTxDependencies(transactionRequest);
return this.estimateTxDependencies(transactionRequest);
}
const encodedTransaction = hexlify(transactionRequest.toTransactionBytes());
const { dryRun: gqlReceipts } = await this.operations.dryRun({
Expand All @@ -645,6 +647,18 @@ export default class Provider {
* @returns A promise that resolves to the estimated transaction request object.
*/
async estimatePredicates(transactionRequest: TransactionRequest): Promise<TransactionRequest> {
const shouldEstimatePredicates = Boolean(
transactionRequest.inputs.find(
(input) =>
'predicate' in input &&
input.predicate &&
!equalBytes(arrayify(input.predicate), arrayify('0x')) &&
new BN(input.predicateGasUsed).isZero()
)
);
if (!shouldEstimatePredicates) {
return transactionRequest;
}
const encodedTransaction = hexlify(transactionRequest.toTransactionBytes());
const response = await this.operations.estimatePredicates({
encodedTransaction,
Expand Down Expand Up @@ -680,45 +694,42 @@ export default class Provider {
* @param transactionRequest - The transaction request object.
* @returns A promise.
*/
async estimateTxDependencies(transactionRequest: TransactionRequest): Promise<void> {
let missingOutputVariableCount = 0;
let missingOutputContractIdsCount = 0;
let tries = 0;

async estimateTxDependencies(transactionRequest: TransactionRequest): Promise<CallResult> {
if (transactionRequest.type === TransactionType.Create) {
return;
return {
receipts: [],
};
}

let txRequest = transactionRequest;
await this.estimatePredicates(transactionRequest);

if (txRequest.hasPredicateInput()) {
txRequest = (await this.estimatePredicates(txRequest)) as ScriptTransactionRequest;
}
let receipts: TransactionResultReceipt[] = [];

while (tries < MAX_RETRIES) {
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
const { dryRun: gqlReceipts } = await this.operations.dryRun({
encodedTransaction: hexlify(txRequest.toTransactionBytes()),
encodedTransaction: hexlify(transactionRequest.toTransactionBytes()),
utxoValidation: false,
});
const receipts = gqlReceipts.map(processGqlReceipt);
receipts = gqlReceipts.map(processGqlReceipt);
const { missingOutputVariables, missingOutputContractIds } =
getReceiptsWithMissingData(receipts);

missingOutputVariableCount = missingOutputVariables.length;
missingOutputContractIdsCount = missingOutputContractIds.length;
const hasMissingOutputs =
missingOutputVariables.length !== 0 || missingOutputContractIds.length !== 0;

if (missingOutputVariableCount === 0 && missingOutputContractIdsCount === 0) {
return;
if (hasMissingOutputs) {
transactionRequest.addVariableOutputs(missingOutputVariables.length);
missingOutputContractIds.forEach(({ contractId }) => {
transactionRequest.addContractInputAndOutput(Address.fromString(contractId));
});
} else {
break;
}

txRequest.addVariableOutputs(missingOutputVariableCount);

missingOutputContractIds.forEach(({ contractId }) =>
txRequest.addContractInputAndOutput(Address.fromString(contractId))
);

tries += 1;
}

return {
receipts,
};
}

/**
Expand All @@ -737,7 +748,7 @@ export default class Provider {
): Promise<CallResult> {
const transactionRequest = transactionRequestify(transactionRequestLike);
if (estimateTxDependencies) {
await this.estimateTxDependencies(transactionRequest);
return this.estimateTxDependencies(transactionRequest);
}
const encodedTransaction = hexlify(transactionRequest.toTransactionBytes());
const { dryRun: gqlReceipts } = await this.operations.dryRun({
Expand Down Expand Up @@ -773,48 +784,60 @@ export default class Provider {
estimatePredicates = true,
resourcesOwner,
}: TransactionCostParams = {}
): Promise<TransactionCost> {
const transactionRequest = transactionRequestify(clone(transactionRequestLike));
): Promise<
TransactionCost & {
estimatedInputs: TransactionRequest['inputs'];
estimatedOutputs: TransactionRequest['outputs'];
}
> {
const txRequestClone = clone(transactionRequestify(transactionRequestLike));
const chainInfo = this.getChain();
const { gasPriceFactor, minGasPrice, maxGasPerTx } = this.getGasConfig();
const gasPrice = max(transactionRequest.gasPrice, minGasPrice);
const isScriptTransaction = transactionRequest.type === TransactionType.Script;
const gasPrice = max(txRequestClone.gasPrice, minGasPrice);
const isScriptTransaction = txRequestClone.type === TransactionType.Script;

// Fund with fake UTXOs to avoid not enough funds error
// Getting coin quantities from amounts being transferred
const coinOutputsQuantities = txRequestClone.getCoinOutputsQuantities();
// Combining coin quantities from amounts being transferred and forwarding to contracts
const allQuantities = mergeQuantities(coinOutputsQuantities, forwardingQuantities);
// Funding transaction with fake utxos
txRequestClone.fundWithFakeUtxos(allQuantities, resourcesOwner?.address);

/**
* Estimate predicates gasUsed
*/
if (transactionRequest.hasPredicateInput() && estimatePredicates) {
if (estimatePredicates) {
// Remove gasLimit to avoid gasLimit when estimating predicates
if (isScriptTransaction) {
transactionRequest.gasLimit = bn(0);
txRequestClone.gasLimit = bn(0);
}
await this.estimatePredicates(transactionRequest);

/**
* The fake utxos added above can be from a predicate
* If the resources owner is a predicate,
* we need to populate the resources with the predicate's data
* so that predicate estimation can happen.
*/
if (resourcesOwner && 'populateTransactionPredicateData' in resourcesOwner) {
(resourcesOwner as Predicate<[]>).populateTransactionPredicateData(txRequestClone);
}
await this.estimatePredicates(txRequestClone);
}

/**
* Calculate minGas and maxGas based on the real transaction
*/
const minGas = transactionRequest.calculateMinGas(chainInfo);
const maxGas = transactionRequest.calculateMaxGas(chainInfo, minGas);

/**
* Fund with fake UTXOs to avoid not enough funds error
*/
// Getting coin quantities from amounts being transferred
const coinOutputsQuantities = transactionRequest.getCoinOutputsQuantities();
// Combining coin quantities from amounts being transferred and forwarding to contracts
const allQuantities = mergeQuantities(coinOutputsQuantities, forwardingQuantities);
// Funding transaction with fake utxos
transactionRequest.fundWithFakeUtxos(allQuantities, resourcesOwner);
const minGas = txRequestClone.calculateMinGas(chainInfo);
const maxGas = txRequestClone.calculateMaxGas(chainInfo, minGas);

/**
* Estimate gasUsed for script transactions
*/

let gasUsed = minGas;
let receipts: TransactionResultReceipt[] = [];
// Transactions of type Create does not consume any gas so we can the dryRun
if (isScriptTransaction) {
if (isScriptTransaction && estimateTxDependencies) {
/**
* Setting the gasPrice to 0 on a dryRun will result in no fees being charged.
* This simplifies the funding with fake utxos, since the coin quantities required
Expand All @@ -823,19 +846,18 @@ export default class Provider {
*/
// Calculate the gasLimit again as we insert a fake UTXO and signer

transactionRequest.gasPrice = bn(0);
transactionRequest.gasLimit = bn(maxGasPerTx.sub(maxGas).toNumber() * 0.9);
txRequestClone.gasPrice = bn(0);
txRequestClone.gasLimit = bn(maxGasPerTx.sub(maxGas).toNumber() * 0.9);

// Executing dryRun with fake utxos to get gasUsed
const result = await this.call(transactionRequest, {
estimateTxDependencies,
});
const result = await this.estimateTxDependencies(txRequestClone);

receipts = result.receipts;
gasUsed = getGasUsedFromReceipts(receipts);
} else {
// For CreateTransaction the gasUsed is going to be the minGas
gasUsed = minGas;
}

// For CreateTransaction the gasUsed is going to be the minGas
const gasUsed = isScriptTransaction ? getGasUsedFromReceipts(receipts) : minGas;

const usedFee = calculatePriceWithFactor(
gasUsed,
gasPrice,
Expand All @@ -855,6 +877,8 @@ export default class Provider {
usedFee,
minFee,
maxFee,
estimatedInputs: txRequestClone.inputs,
estimatedOutputs: txRequestClone.outputs,
};
}

Expand Down
Loading

0 comments on commit 880f91a

Please sign in to comment.