Skip to content

Commit 6668336

Browse files
authored
chore: merge gas price and predicate estimation requests (#3676)
1 parent ff97a6e commit 6668336

File tree

7 files changed

+219
-30
lines changed

7 files changed

+219
-30
lines changed

.changeset/curly-brooms-flash.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@fuel-ts/account": patch
3+
---
4+
5+
chore: merge gas price and predicate estimation requests

packages/account/src/providers/operations.graphql

+12
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,18 @@ query estimatePredicates($encodedTransaction: HexString!) {
557557
}
558558
}
559559

560+
query estimatePredicatesAndGasPrice(
561+
$encodedTransaction: HexString!
562+
$blockHorizon: U32!
563+
) {
564+
estimatePredicates(tx: $encodedTransaction) {
565+
...transactionEstimatePredicatesFragment
566+
}
567+
estimateGasPrice(blockHorizon: $blockHorizon) {
568+
gasPrice
569+
}
570+
}
571+
560572
query getLatestBlock {
561573
chain {
562574
latestBlock {

packages/account/src/providers/provider.ts

+86-28
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type { AddressInput } from '@fuel-ts/address';
22
import { Address } from '@fuel-ts/address';
33
import { ErrorCode, FuelError } from '@fuel-ts/errors';
4-
import { BN, bn } from '@fuel-ts/math';
4+
import type { BN } from '@fuel-ts/math';
5+
import { bn } from '@fuel-ts/math';
56
import type { Transaction } from '@fuel-ts/transactions';
67
import { InputType, InputMessageCoder, TransactionCoder } from '@fuel-ts/transactions';
78
import type { BytesLike } from '@fuel-ts/utils';
89
import { arrayify, hexlify, DateTime, isDefined } from '@fuel-ts/utils';
910
import { checkFuelCoreVersionCompatibility, versions } from '@fuel-ts/versions';
10-
import { equalBytes } from '@noble/curves/abstract/utils';
1111
import type { DocumentNode } from 'graphql';
1212
import { GraphQLClient } from 'graphql-request';
1313
import type { GraphQLClientResponse, GraphQLResponse } from 'graphql-request/src/types';
@@ -30,6 +30,7 @@ import type {
3030
GqlRelayedTransactionFailed,
3131
Requester,
3232
GqlBlockFragment,
33+
GqlEstimatePredicatesQuery,
3334
} from './__generated__/operations';
3435
import type { Coin } from './coin';
3536
import type { CoinQuantity, CoinQuantityLike } from './coin-quantity';
@@ -47,6 +48,7 @@ import type {
4748
ScriptTransactionRequest,
4849
} from './transaction-request';
4950
import {
51+
isPredicate,
5052
isTransactionTypeCreate,
5153
isTransactionTypeScript,
5254
transactionRequestify,
@@ -947,48 +949,78 @@ export default class Provider {
947949
}
948950

949951
/**
950-
* Verifies whether enough gas is available to complete transaction.
952+
* Estimates the gas usage for predicates in a transaction request.
951953
*
952954
* @template T - The type of the transaction request object.
953955
*
954-
* @param transactionRequest - The transaction request object.
955-
* @returns A promise that resolves to the estimated transaction request object.
956+
* @param transactionRequest - The transaction request to estimate predicates for.
957+
* @returns A promise that resolves to the updated transaction request with estimated gas usage for predicates.
956958
*/
957959
async estimatePredicates<T extends TransactionRequest>(transactionRequest: T): Promise<T> {
958-
const shouldEstimatePredicates = Boolean(
959-
transactionRequest.inputs.find(
960-
(input) =>
961-
'predicate' in input &&
962-
input.predicate &&
963-
!equalBytes(arrayify(input.predicate), arrayify('0x')) &&
964-
new BN(input.predicateGasUsed).isZero()
965-
)
960+
const shouldEstimatePredicates = transactionRequest.inputs.some(
961+
(input) => isPredicate(input) && bn(input.predicateGasUsed).isZero()
966962
);
963+
967964
if (!shouldEstimatePredicates) {
968965
return transactionRequest;
969966
}
967+
970968
const encodedTransaction = hexlify(transactionRequest.toTransactionBytes());
969+
971970
const response = await this.operations.estimatePredicates({
972971
encodedTransaction,
973972
});
974973

975-
const {
976-
estimatePredicates: { inputs },
977-
} = response;
974+
const { estimatePredicates } = response;
978975

979-
if (inputs) {
980-
inputs.forEach((input, index) => {
981-
if ('predicateGasUsed' in input && bn(input.predicateGasUsed).gt(0)) {
982-
// eslint-disable-next-line no-param-reassign
983-
(<CoinTransactionRequestInput>transactionRequest.inputs[index]).predicateGasUsed =
984-
input.predicateGasUsed;
985-
}
986-
});
987-
}
976+
// eslint-disable-next-line no-param-reassign
977+
transactionRequest = this.parseEstimatePredicatesResponse(
978+
transactionRequest,
979+
estimatePredicates
980+
);
988981

989982
return transactionRequest;
990983
}
991984

985+
/**
986+
* Estimates the gas price and predicates for a given transaction request and block horizon.
987+
*
988+
* @param transactionRequest - The transaction request to estimate predicates and gas price for.
989+
* @param blockHorizon - The block horizon to use for gas price estimation.
990+
* @returns A promise that resolves to an object containing the updated transaction
991+
* request and the estimated gas price.
992+
*/
993+
async estimatePredicatesAndGasPrice<T extends TransactionRequest>(
994+
transactionRequest: T,
995+
blockHorizon: number
996+
) {
997+
const shouldEstimatePredicates = transactionRequest.inputs.some(
998+
(input) => isPredicate(input) && bn(input.predicateGasUsed).isZero()
999+
);
1000+
1001+
if (!shouldEstimatePredicates) {
1002+
const gasPrice = await this.estimateGasPrice(blockHorizon);
1003+
1004+
return { transactionRequest, gasPrice };
1005+
}
1006+
1007+
const {
1008+
estimateGasPrice: { gasPrice },
1009+
estimatePredicates,
1010+
} = await this.operations.estimatePredicatesAndGasPrice({
1011+
blockHorizon: String(blockHorizon),
1012+
encodedTransaction: hexlify(transactionRequest.toTransactionBytes()),
1013+
});
1014+
1015+
// eslint-disable-next-line no-param-reassign
1016+
transactionRequest = this.parseEstimatePredicatesResponse(
1017+
transactionRequest,
1018+
estimatePredicates
1019+
);
1020+
1021+
return { transactionRequest, gasPrice: bn(gasPrice) };
1022+
}
1023+
9921024
/**
9931025
* Will dryRun a transaction and check for missing dependencies.
9941026
*
@@ -1355,10 +1387,16 @@ export default class Provider {
13551387
addedSignatures = signedRequest.witnesses.length - lengthBefore;
13561388
}
13571389

1358-
await this.estimatePredicates(signedRequest);
1359-
txRequestClone.updatePredicateGasUsed(signedRequest.inputs);
1390+
let gasPrice: BN;
13601391

1361-
const gasPrice = gasPriceParam ?? (await this.estimateGasPrice(10));
1392+
if (gasPriceParam) {
1393+
gasPrice = gasPriceParam;
1394+
await this.estimatePredicates(signedRequest);
1395+
} else {
1396+
({ gasPrice } = await this.estimatePredicatesAndGasPrice(signedRequest, 10));
1397+
}
1398+
1399+
txRequestClone.updatePredicateGasUsed(signedRequest.inputs);
13621400

13631401
/**
13641402
* Calculate minGas and maxGas based on the real transaction
@@ -2176,4 +2214,24 @@ export default class Provider {
21762214
statusReason: status.reason,
21772215
});
21782216
}
2217+
2218+
/**
2219+
* @hidden
2220+
*/
2221+
private parseEstimatePredicatesResponse<T extends TransactionRequest>(
2222+
transactionRequest: T,
2223+
{ inputs }: GqlEstimatePredicatesQuery['estimatePredicates']
2224+
): T {
2225+
if (inputs) {
2226+
inputs.forEach((input, i) => {
2227+
if (input && 'predicateGasUsed' in input && bn(input.predicateGasUsed).gt(0)) {
2228+
// eslint-disable-next-line no-param-reassign
2229+
(<CoinTransactionRequestInput>transactionRequest.inputs[i]).predicateGasUsed =
2230+
input.predicateGasUsed;
2231+
}
2232+
});
2233+
}
2234+
2235+
return transactionRequest;
2236+
}
21792237
}

packages/account/src/providers/transaction-request/helpers.test.ts

+32
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { getRandomB256, Address } from '@fuel-ts/address';
22
import { ZeroBytes32 } from '@fuel-ts/address/configs';
3+
import { randomBytes } from '@fuel-ts/crypto';
34
import { ErrorCode, FuelError } from '@fuel-ts/errors';
45
import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
56
import { bn } from '@fuel-ts/math';
67
import { InputType, OutputType } from '@fuel-ts/transactions';
8+
import { arrayify, hexlify } from '@fuel-ts/utils';
79

810
import { generateFakeCoin, generateFakeMessageCoin } from '../../test-utils/resources';
911
import {
@@ -24,6 +26,7 @@ import {
2426
cacheRequestInputsResourcesFromOwner,
2527
getBurnableAssetCount,
2628
validateTransactionForAssetBurn,
29+
isPredicate,
2730
} from './helpers';
2831
import { ScriptTransactionRequest } from './script-transaction-request';
2932

@@ -138,6 +141,35 @@ describe('helpers', () => {
138141
expect(result.messages).not.toContain(messageInput2.nonce);
139142
});
140143

144+
describe('isPredicate', () => {
145+
it('should properly identify if request input is a predicate', () => {
146+
const generateFakeResources = [
147+
generateFakeRequestInputCoin,
148+
generateFakeRequestInputMessage,
149+
];
150+
151+
generateFakeResources.forEach((generate) => {
152+
let nonPredicate = generate();
153+
expect(nonPredicate.predicate).toBeUndefined();
154+
expect(isPredicate(nonPredicate)).toBeFalsy();
155+
156+
nonPredicate = generate({ predicate: '0x' });
157+
expect(nonPredicate.predicate).toBeDefined();
158+
expect(isPredicate(nonPredicate)).toBeFalsy();
159+
160+
nonPredicate = generate({ predicate: arrayify('0x') });
161+
expect(nonPredicate.predicate).toBeDefined();
162+
expect(isPredicate(nonPredicate)).toBeFalsy();
163+
164+
let predicate = generate({ predicate: randomBytes(20) });
165+
expect(isPredicate(predicate)).toBeTruthy();
166+
167+
predicate = generate({ predicate: hexlify(randomBytes(30)) });
168+
expect(isPredicate(predicate)).toBeTruthy();
169+
});
170+
});
171+
});
172+
141173
describe('getAssetAmountInRequestInputs', () => {
142174
it('should handle empty inputs array', () => {
143175
const tx = new ScriptTransactionRequest();

packages/account/src/providers/transaction-request/helpers.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import type { Address } from '@fuel-ts/address';
22
import { ErrorCode, FuelError } from '@fuel-ts/errors';
33
import { bn } from '@fuel-ts/math';
44
import { InputType, OutputType } from '@fuel-ts/transactions';
5+
import { hexlify } from '@fuel-ts/utils';
56

6-
import type { ExcludeResourcesOption } from '../resource';
7+
import { type ExcludeResourcesOption } from '../resource';
78

89
import type {
910
TransactionRequestInput,
@@ -44,6 +45,19 @@ export const isRequestInputResourceFromOwner = (
4445
owner: Address
4546
) => getRequestInputResourceOwner(input) === owner.toB256();
4647

48+
/**
49+
* @hidden
50+
*
51+
* Checks if the given `TransactionRequestInput` is a predicate.
52+
*
53+
* @param input - The `TransactionRequestInput` to check.
54+
* @returns `true` if the input is a predicate, otherwise `false`.
55+
*/
56+
export const isPredicate = (
57+
input: TransactionRequestInput
58+
): input is Required<CoinTransactionRequestInput | MessageTransactionRequestInput> =>
59+
isRequestInputCoinOrMessage(input) && !!input.predicate && hexlify(input.predicate) !== '0x';
60+
4761
export const getAssetAmountInRequestInputs = (
4862
inputs: TransactionRequestInput[],
4963
assetId: string,

packages/fuel-gauge/src/fee.test.ts

+68
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
InputMessageCoder,
44
ScriptTransactionRequest,
55
Wallet,
6+
bn,
67
getMintedAssetId,
78
getRandomB256,
89
hexlify,
@@ -474,6 +475,73 @@ describe('Fee', () => {
474475
expect(gasUsed.toNumber()).toBeGreaterThan(0);
475476
});
476477

478+
it('ensures gas price and predicates are estimated on the same request', async () => {
479+
using launched = await launchTestNode();
480+
481+
const { provider } = launched;
482+
483+
const predicate = new PredicateU32({ provider, data: [1078] });
484+
485+
const estimateGasPrice = vi.spyOn(provider.operations, 'estimateGasPrice');
486+
const estimatePredicates = vi.spyOn(provider.operations, 'estimatePredicates');
487+
const estimatePredicatesAndGasPrice = vi.spyOn(
488+
provider.operations,
489+
'estimatePredicatesAndGasPrice'
490+
);
491+
492+
await predicate.getTransactionCost(new ScriptTransactionRequest());
493+
494+
expect(estimateGasPrice).not.toHaveBeenCalledOnce();
495+
expect(estimatePredicates).not.toHaveBeenCalledOnce();
496+
497+
expect(estimatePredicatesAndGasPrice).toHaveBeenCalledOnce();
498+
});
499+
500+
it('ensures gas price is estimated alone when no predicates are present', async () => {
501+
using launched = await launchTestNode();
502+
503+
const {
504+
provider,
505+
wallets: [wallet],
506+
} = launched;
507+
508+
const estimateGasPrice = vi.spyOn(provider.operations, 'estimateGasPrice');
509+
const estimatePredicates = vi.spyOn(provider.operations, 'estimatePredicates');
510+
const estimatePredicatesAndGasPrice = vi.spyOn(
511+
provider.operations,
512+
'estimatePredicatesAndGasPrice'
513+
);
514+
515+
await wallet.getTransactionCost(new ScriptTransactionRequest());
516+
517+
expect(estimatePredicates).not.toHaveBeenCalledOnce();
518+
expect(estimatePredicatesAndGasPrice).not.toHaveBeenCalledOnce();
519+
520+
expect(estimateGasPrice).toHaveBeenCalledOnce();
521+
});
522+
523+
it('ensures predicates are estimated alone when gas price is present', async () => {
524+
using launched = await launchTestNode();
525+
526+
const { provider } = launched;
527+
528+
const predicate = new PredicateU32({ provider, data: [1078] });
529+
530+
const estimateGasPrice = vi.spyOn(provider.operations, 'estimateGasPrice');
531+
const estimatePredicates = vi.spyOn(provider.operations, 'estimatePredicates');
532+
const estimatePredicatesAndGasPrice = vi.spyOn(
533+
provider.operations,
534+
'estimatePredicatesAndGasPrice'
535+
);
536+
537+
await predicate.getTransactionCost(new ScriptTransactionRequest(), { gasPrice: bn(1) });
538+
539+
expect(estimatePredicatesAndGasPrice).not.toHaveBeenCalledOnce();
540+
expect(estimateGasPrice).not.toHaveBeenCalledOnce();
541+
542+
expect(estimatePredicates).toHaveBeenCalledOnce();
543+
});
544+
477545
it('ensures estimateGasPrice runs only once when getting transaction cost with estimate gas and fee', async () => {
478546
using launched = await launchTestNode({
479547
contractsConfigs: [

packages/fuel-gauge/src/predicate/predicate-estimations.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ describe('Predicate', () => {
221221
const initialReceiverBalance = await receiverWallet.getBalance();
222222

223223
const dryRunSpy = vi.spyOn(provider.operations, 'dryRun');
224-
const estimatePredicatesSpy = vi.spyOn(provider.operations, 'estimatePredicates');
224+
const estimatePredicatesSpy = vi.spyOn(provider.operations, 'estimatePredicatesAndGasPrice');
225225

226226
const response = await predicateValidateTransfer.transfer(
227227
receiverWallet.address.toB256(),

0 commit comments

Comments
 (0)