Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: autoCost for transaction estimation and funding #3539

Merged
merged 3 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/shaggy-zebras-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/account": minor
"@fuel-ts/program": minor
---

feat!: `autoCost` for transaction estimation and funding
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,8 @@ transactionRequest.addCoinOutput(
provider.getBaseAssetId()
);

const txCost = await wallet.getTransactionCost(transactionRequest);

transactionRequest.gasLimit = txCost.gasUsed;
transactionRequest.maxFee = txCost.maxFee;

await wallet.fund(transactionRequest, txCost);
// Estimate and fund the transaction
await transactionRequest.autoCost(wallet);

// Submit the transaction
const response = await wallet.sendTransaction(transactionRequest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,15 @@ const resources = await predicate.getResourcesToSpend([
amount: amountToReceiver,
},
]);

request.addResources(resources);
request.addWitness('0x');

// Add witnesses including the signer
// Estimate the predicate inputs
const txCost = await predicate.getTransactionCost(request, {
// Estimate and fund the request
request.addWitness('0x');
await request.autoCost(predicate, {
signatureCallback: (txRequest) => txRequest.addAccountWitnesses(signer),
});

request.updatePredicateGasUsed(txCost.estimatedPredicates);

request.gasLimit = txCost.gasUsed;
request.maxFee = txCost.maxFee;

await predicate.fund(request, txCost);

// Add the signer as a witness
await request.addAccountWitnesses(signer);

// Send the transaction
Expand Down
7 changes: 2 additions & 5 deletions apps/docs/src/guide/encoding/snippets/encode-and-decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,8 @@ const encodedArguments = abiInterface.encodeType(argument, [argumentToAdd]);
// The encoded value can now be set on the transaction via the script data property
request.scriptData = encodedArguments;

// Now we can build out the rest of the transaction and then fund it
const txCost = await wallet.getTransactionCost(request);
request.maxFee = txCost.maxFee;
request.gasLimit = txCost.gasUsed;
await wallet.fund(request, txCost);
// Now we can estimate and fund the transaction
await request.autoCost(wallet);

// Finally, submit the built transaction
const response = await wallet.sendTransaction(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@ customRequest.addResources(predicateResources);
customRequest.addCoinOutput(receiver.address, amountToReceiver, assetId);

// Estimate the transaction cost and fund accordingly
const txCost = await predicate.getTransactionCost(customRequest);
customRequest.gasLimit = txCost.gasUsed;
customRequest.maxFee = txCost.maxFee;
await predicate.fund(customRequest, txCost);
await customRequest.autoCost(predicate);

// Submit the transaction and await it's result
const predicateTx = await predicate.sendTransaction(customRequest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,8 @@ const predicateCoins = await predicate.getResourcesToSpend([
// Add the predicate input and resources.
transactionRequest.addResources(predicateCoins);

const txCost = await predicate.getTransactionCost(transactionRequest);

transactionRequest.gasLimit = txCost.gasUsed;
transactionRequest.maxFee = txCost.maxFee;

await predicate.fund(transactionRequest, txCost);
// Estimate and fund the transaction
await transactionRequest.autoCost(predicate);

// Send the transaction using the predicate
const result = await predicate.sendTransaction(transactionRequest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,8 @@ transactionRequest.addCoinOutput(
provider.getBaseAssetId()
);

const txCost = await predicate.getTransactionCost(transactionRequest);

transactionRequest.gasLimit = txCost.gasUsed;
transactionRequest.maxFee = txCost.maxFee;

await predicate.fund(transactionRequest, txCost);
// Estimate and fund the transaction
await transactionRequest.autoCost(predicate);

const result = await predicate.simulateTransaction(transactionRequest);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,8 @@ const quantities = [
coinQuantityfy([500, ASSET_B]),
];

// 5. Calculate the transaction fee
const txCost = await wallet.getTransactionCost(request, { quantities });

request.gasLimit = txCost.gasUsed;
request.maxFee = txCost.maxFee;

await wallet.fund(request, txCost);
// 5. Estimate and fund the transaction
await request.autoCost(wallet, { quantities });

// 6. Send the transaction
const tx = await wallet.sendTransaction(request);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Provider, ScriptTransactionRequest, Wallet } from 'fuels';

import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env';
import { ScriptSum } from '../../../../typegend';

const provider = await Provider.create(LOCAL_NETWORK_URL);
const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);

// #region auto-cost
const transactionRequest = new ScriptTransactionRequest({
script: ScriptSum.bytecode,
});

await transactionRequest.autoCost(wallet);

await wallet.sendTransaction(transactionRequest);
// #endregion auto-cost
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,7 @@ const scriptMainFunctionArguments = [1];
transactionRequest.setData(ScriptSum.abi, scriptMainFunctionArguments);

// Fund the transaction
const txCost = await wallet.getTransactionCost(transactionRequest);

transactionRequest.maxFee = txCost.maxFee;
transactionRequest.gasLimit = txCost.gasUsed;

await wallet.fund(transactionRequest, txCost);
await transactionRequest.autoCost(wallet);

// Submit the transaction
const response = await wallet.sendTransaction(transactionRequest);
Expand Down
10 changes: 8 additions & 2 deletions apps/docs/src/guide/transactions/transaction-request.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,17 @@ Including `OutputCoin`s in the transaction request specifies the UTXOs that will

### Estimating and Funding the Transaction Request

Before submitting a transaction, it is essential to ensure it is properly funded to meet its requirements and cover the associated fee:
Before submitting a transaction, it is essential to ensure it is properly funded to meet its requirements and cover the associated fee. The SDK offers two approaches for this, one is to use the `autoCost` helper:

<<< @./snippets/transaction-request/auto-cost.ts#auto-cost{ts:line-numbers}

This approach provides a simple one-liner for estimating and funding the transaction request. Ensuring that the `gasLimit` and `maxFee` are accurately calculated and that the required amounts for `OutputCoin`s are fulfilled, as well as fetching and adding any missing resources from the calling account.

The other more manual approach is as so:

<<< @./snippets/transaction-request/estimate-and-fund.ts#transaction-request-4{ts:line-numbers}

This is the recommended approach for manually estimating and funding a transaction before submission. It ensures that the `gasLimit` and `maxFee` are accurately calculated and that the required amounts for `OutputCoin`s are fulfilled. The `fund` method automatically fetches any missing resource amounts from the calling account and adds them to the transaction request.
This approach provides the same behaviour as the `autoCost` helper, but gives more granular control over the transaction request. The `getTransactionCost` method also returns various information about the simulated request that you may want to use to further modify the transaction request, more on that can be found in the [API reference](https://fuels-ts-docs-api.vercel.app/types/_fuel_ts_account.TransactionCost.html).

### Manually Fetching Resources

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@ const request = new ScriptTransactionRequest({

request.addCoinOutput(receiverAddress, 1000, provider.getBaseAssetId());

const txCost = await sender.getTransactionCost(request);

request.gasLimit = txCost.gasUsed;
request.maxFee = txCost.maxFee;

await sender.fund(request, txCost);
await request.autoCost(sender);

const tx = await sender.sendTransaction(request);
await tx.waitForResult();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,7 @@ const request = new ScriptTransactionRequest({

request.addCoinOutput(receiverAddress, 1000, provider.getBaseAssetId());

const txCost = await sender.getTransactionCost(request);

request.gasLimit = txCost.gasUsed;
request.maxFee = txCost.maxFee;

await sender.fund(request, txCost);
await request.autoCost(sender);

const signedTransaction = await sender.signTransaction(request);
const transactionId = request.getTransactionId(provider.getChainId());
Expand Down
28 changes: 4 additions & 24 deletions packages/account/src/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,12 +610,7 @@ describe('Account', () => {
[amount, assetIdB],
]);

const txCost = await sender.getTransactionCost(request);

request.gasLimit = txCost.gasUsed;
request.maxFee = txCost.maxFee;

await sender.fund(request, txCost);
await request.autoCost(sender);

const response = await sender.sendTransaction(request);

Expand Down Expand Up @@ -722,12 +717,7 @@ describe('Account', () => {
request.addCoinOutput(sender.address, amount.div(3), provider.getBaseAssetId());
}

const txCost = await fundingWallet.getTransactionCost(request);

request.gasLimit = txCost.gasUsed;
request.maxFee = txCost.maxFee;

await fundingWallet.fund(request, txCost);
await request.autoCost(fundingWallet);

const tx1 = await fundingWallet.sendTransaction(request);
await tx1.waitForResult();
Expand Down Expand Up @@ -799,12 +789,7 @@ describe('Account', () => {
request.addCoinOutput(sender.address, fundingAmount.div(3), provider.getBaseAssetId());
}

const txCost = await fundingWallet.getTransactionCost(request);

request.gasLimit = txCost.gasUsed;
request.maxFee = txCost.maxFee;

await fundingWallet.fund(request, txCost);
await request.autoCost(fundingWallet);

const tx1 = await fundingWallet.sendTransaction(request);
await tx1.waitForResult();
Expand Down Expand Up @@ -967,12 +952,7 @@ describe('Account', () => {
const request = new ScriptTransactionRequest();
request.addCoinOutput(wallet.address, 30_000, provider.getBaseAssetId());

const txCost = await wallet.getTransactionCost(request);

request.gasLimit = txCost.gasUsed;
request.maxFee = txCost.maxFee;

await expectToThrowFuelError(() => wallet.fund(request, txCost), {
await expectToThrowFuelError(() => request.autoCost(wallet), {
code: ErrorCode.MAX_COINS_REACHED,
message:
'The account retrieving coins has exceeded the maximum number of coins per asset. Please consider combining your coins into a single UTXO.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { InputType, OutputType, TransactionType } from '@fuel-ts/transactions';
import { arrayify, hexlify } from '@fuel-ts/utils';
import { clone } from 'ramda';

import type { ChainInfo, GasCosts } from '../provider';
import type { Account } from '../../account';
import type { ChainInfo, GasCosts, TransactionCostParams } from '../provider';
import { calculateMetadataGasForTxScript, getMaxGas } from '../utils/gas';

import { hashTransaction } from './hash-transaction';
Expand Down Expand Up @@ -67,6 +68,27 @@ export class ScriptTransactionRequest extends BaseTransactionRequest {
this.abis = rest.abis;
}

/**
* Helper function to fund the transaction request with a specified account.
*
* @param account - The account to fund the transaction.
* @param params - The parameters for the transaction cost.
* @returns The current instance of the `ScriptTransactionRequest` funded.
*/
async autoCost(
account: Account,
{ signatureCallback, quantities = [] }: TransactionCostParams = {}
): Promise<ScriptTransactionRequest> {
const txCost = await account.getTransactionCost(this, { signatureCallback, quantities });

this.maxFee = txCost.maxFee;
this.gasLimit = txCost.gasUsed;

await account.fund(this, txCost);

return this;
}

/**
* Converts the transaction request to a `TransactionScript`.
*
Expand Down
14 changes: 2 additions & 12 deletions packages/fuel-gauge/src/advanced-logging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,7 @@ describe('Advanced Logging', () => {
])
.getTransactionRequest();

const txCost = await wallet.getTransactionCost(request);

request.gasLimit = txCost.gasUsed;
request.maxFee = txCost.maxFee;

await wallet.fund(request, txCost);
await request.autoCost(wallet);

const tx = await wallet.sendTransaction(request, { estimateTxDependencies: false });

Expand Down Expand Up @@ -312,12 +307,7 @@ describe('Advanced Logging', () => {
.addContracts([advancedLogContract, otherAdvancedLogContract])
.getTransactionRequest();

const txCost = await wallet.getTransactionCost(request);

request.gasLimit = txCost.gasUsed;
request.maxFee = txCost.maxFee;

await wallet.fund(request, txCost);
await request.autoCost(wallet);

const tx = await wallet.sendTransaction(request);

Expand Down
2 changes: 1 addition & 1 deletion packages/fuel-gauge/src/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ describe('Contract', () => {
const struct = { a: true, b: 1337 };
const invocationScopes = [contract.functions.foo(num), contract.functions.boo(struct)];
const multiCallScope = contract.multiCall(invocationScopes);
const transactionRequest = await multiCallScope.fundWithRequiredCoins();
const transactionRequest = await multiCallScope.autoCost();

const txRequest = JSON.stringify(transactionRequest);
const txRequestParsed = JSON.parse(txRequest);
Expand Down
7 changes: 1 addition & 6 deletions packages/fuel-gauge/src/coverage-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,12 +665,7 @@ describe('Coverage Contract', { timeout: 15_000 }, () => {

request.addCoinOutput(recipient.address, 10, provider.getBaseAssetId());

const txCost = await sender.getTransactionCost(request);

request.gasLimit = txCost.gasUsed;
request.maxFee = txCost.maxFee;

await sender.fund(request, txCost);
await request.autoCost(sender);

const response = await sender.sendTransaction(request);
const result = await response.waitForResult();
Expand Down
7 changes: 1 addition & 6 deletions packages/fuel-gauge/src/fee.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,7 @@ describe('Fee', () => {
request.addCoinOutput(destination2.address, amountToTransfer, ASSET_A);
request.addCoinOutput(destination3.address, amountToTransfer, ASSET_B);

const txCost = await wallet.getTransactionCost(request);

request.gasLimit = txCost.gasUsed;
request.maxFee = txCost.maxFee;

await wallet.fund(request, txCost);
await request.autoCost(wallet);

const tx = await wallet.sendTransaction(request);
const { fee } = await tx.wait();
Expand Down
Loading
Loading