diff --git a/.changeset/heavy-trainers-serve.md b/.changeset/heavy-trainers-serve.md new file mode 100644 index 00000000000..198391b8c79 --- /dev/null +++ b/.changeset/heavy-trainers-serve.md @@ -0,0 +1,9 @@ +--- +"@fuel-ts/account": minor +"@fuel-ts/program": minor +--- + +- Add `outputVariables` and `missingContractIds` to the return of `estimateTxDependencies` +- Removed `estimatedOutputs` from return of `getTransactionCost` +- Add `outputVariables` and `missingContractIds` to the return of `getTransactionCost` +- Avoid reassigning `inputs` and `outputs` from the estimated TX at `BaseInvocationScope` diff --git a/packages/account/src/account.test.ts b/packages/account/src/account.test.ts index f7ec0bc74c8..c54263d1e26 100644 --- a/packages/account/src/account.test.ts +++ b/packages/account/src/account.test.ts @@ -303,7 +303,9 @@ describe('Account', () => { const estimateTxDependencies = vi .spyOn(providersMod.Provider.prototype, 'estimateTxDependencies') - .mockImplementation(() => Promise.resolve({ receipts: [] })); + .mockImplementation(() => + Promise.resolve({ receipts: [], missingContractIds: [], outputVariables: 0 }) + ); const sendTransaction = vi .spyOn(providersMod.Provider.prototype, 'sendTransaction') @@ -341,7 +343,9 @@ describe('Account', () => { const estimateTxDependencies = vi .spyOn(providersMod.Provider.prototype, 'estimateTxDependencies') - .mockImplementation(() => Promise.resolve({ receipts: [] })); + .mockImplementation(() => + Promise.resolve({ receipts: [], missingContractIds: [], outputVariables: 0 }) + ); const simulate = vi .spyOn(providersMod.Provider.prototype, 'simulate') diff --git a/packages/account/src/providers/provider.ts b/packages/account/src/providers/provider.ts index ff93762b696..93dd34cc5f0 100644 --- a/packages/account/src/providers/provider.ts +++ b/packages/account/src/providers/provider.ts @@ -59,6 +59,11 @@ export type CallResult = { receipts: TransactionResultReceipt[]; }; +export type EstimateTxDependenciesReturns = CallResult & { + outputVariables: number; + missingContractIds: string[]; +}; + /** * A Fuel block */ @@ -693,16 +698,22 @@ export default class Provider { * @param transactionRequest - The transaction request object. * @returns A promise. */ - async estimateTxDependencies(transactionRequest: TransactionRequest): Promise { + async estimateTxDependencies( + transactionRequest: TransactionRequest + ): Promise { if (transactionRequest.type === TransactionType.Create) { return { receipts: [], + outputVariables: 0, + missingContractIds: [], }; } await this.estimatePredicates(transactionRequest); let receipts: TransactionResultReceipt[] = []; + const missingContractIds: string[] = []; + let outputVariables = 0; for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { const { dryRun: gqlReceipts } = await this.operations.dryRun({ @@ -717,9 +728,11 @@ export default class Provider { missingOutputVariables.length !== 0 || missingOutputContractIds.length !== 0; if (hasMissingOutputs) { + outputVariables += missingOutputVariables.length; transactionRequest.addVariableOutputs(missingOutputVariables.length); missingOutputContractIds.forEach(({ contractId }) => { transactionRequest.addContractInputAndOutput(Address.fromString(contractId)); + missingContractIds.push(contractId); }); } else { break; @@ -728,6 +741,8 @@ export default class Provider { return { receipts, + outputVariables, + missingContractIds, }; } @@ -786,7 +801,8 @@ export default class Provider { ): Promise< TransactionCost & { estimatedInputs: TransactionRequest['inputs']; - estimatedOutputs: TransactionRequest['outputs']; + outputVariables: number; + missingContractIds: string[]; } > { const txRequestClone = clone(transactionRequestify(transactionRequestLike)); @@ -835,6 +851,8 @@ export default class Provider { */ let receipts: TransactionResultReceipt[] = []; + let missingContractIds: string[] = []; + let outputVariables = 0; // Transactions of type Create does not consume any gas so we can the dryRun if (isScriptTransaction && estimateTxDependencies) { /** @@ -852,6 +870,8 @@ export default class Provider { const result = await this.estimateTxDependencies(txRequestClone); receipts = result.receipts; + outputVariables = result.outputVariables; + missingContractIds = result.missingContractIds; } // For CreateTransaction the gasUsed is going to be the minGas @@ -877,7 +897,8 @@ export default class Provider { minFee, maxFee, estimatedInputs: txRequestClone.inputs, - estimatedOutputs: txRequestClone.outputs, + outputVariables, + missingContractIds, }; } diff --git a/packages/account/src/providers/transaction-request/transaction-request.ts b/packages/account/src/providers/transaction-request/transaction-request.ts index b90f6db6278..d66336a0e48 100644 --- a/packages/account/src/providers/transaction-request/transaction-request.ts +++ b/packages/account/src/providers/transaction-request/transaction-request.ts @@ -642,8 +642,6 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi this.inputs.forEach((i) => { let correspondingInput: TransactionRequestInput | undefined; switch (i.type) { - case InputType.Contract: - return; case InputType.Coin: correspondingInput = inputs.find((x) => x.type === InputType.Coin && x.owner === i.owner); break; @@ -653,7 +651,7 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi ); break; default: - break; + return; } if ( correspondingInput && diff --git a/packages/account/src/wallet/wallet-unlocked.test.ts b/packages/account/src/wallet/wallet-unlocked.test.ts index f5aa611d832..b715574fd24 100644 --- a/packages/account/src/wallet/wallet-unlocked.test.ts +++ b/packages/account/src/wallet/wallet-unlocked.test.ts @@ -226,7 +226,9 @@ describe('WalletUnlocked', () => { const estimateTxDependencies = vi .spyOn(providersMod.Provider.prototype, 'estimateTxDependencies') - .mockImplementation(() => Promise.resolve({ receipts: [] })); + .mockImplementation(() => + Promise.resolve({ receipts: [], missingContractIds: [], outputVariables: 0 }) + ); const call = vi .spyOn(providersMod.Provider.prototype, 'call') diff --git a/packages/program/src/functions/base-invocation-scope.ts b/packages/program/src/functions/base-invocation-scope.ts index 69c891a8a5e..3e331a33e14 100644 --- a/packages/program/src/functions/base-invocation-scope.ts +++ b/packages/program/src/functions/base-invocation-scope.ts @@ -3,11 +3,12 @@ import type { InputValue } from '@fuel-ts/abi-coder'; import type { BaseWalletUnlocked, Provider, CoinQuantity } from '@fuel-ts/account'; import { ScriptTransactionRequest } from '@fuel-ts/account'; +import { Address } from '@fuel-ts/address'; import { ErrorCode, FuelError } from '@fuel-ts/errors'; import type { AbstractAccount, AbstractContract, AbstractProgram } from '@fuel-ts/interfaces'; import type { BN } from '@fuel-ts/math'; import { bn, toNumber } from '@fuel-ts/math'; -import { InputType, OutputType } from '@fuel-ts/transactions'; +import { InputType } from '@fuel-ts/transactions'; import * as asm from '@fuels/vm-asm'; import { getContractCallScript } from '../contract-call-script'; @@ -230,33 +231,34 @@ export class BaseInvocationScope { async fundWithRequiredCoins() { const transactionRequest = await this.getTransactionRequest(); - const { maxFee, gasUsed, minGasPrice, estimatedInputs, estimatedOutputs } = - await this.getTransactionCost(); - + const { + maxFee, + gasUsed, + minGasPrice, + estimatedInputs, + outputVariables, + missingContractIds, + requiredQuantities, + } = await this.getTransactionCost(); this.setDefaultTxParams(transactionRequest, minGasPrice, gasUsed); - transactionRequest.outputs = estimatedOutputs; - // Clean coin inputs before add new coins to the request - this.transactionRequest.inputs = estimatedInputs.filter((i) => i.type !== InputType.Coin); + this.transactionRequest.inputs = this.transactionRequest.inputs.filter( + (i) => i.type !== InputType.Coin + ); - await this.program.account?.fund(this.transactionRequest, this.requiredCoins, maxFee); + await this.program.account?.fund(this.transactionRequest, requiredQuantities, maxFee); - // update predicate inputs with estimated predicate-related info because the funding removes it this.transactionRequest.updatePredicateInputs(estimatedInputs); - // Update output coin indexes after funding because the funding reordered the inputs - this.transactionRequest.outputs = this.transactionRequest.outputs.filter( - (x) => x.type !== OutputType.Contract - ); - - this.transactionRequest.inputs.forEach((input, inputIndex) => { - if (input.type !== InputType.Contract) { - return; - } - this.transactionRequest.outputs.push({ type: OutputType.Contract, inputIndex }); + // Adding missing contract ids + missingContractIds.forEach((contractId) => { + this.transactionRequest.addContractInputAndOutput(Address.fromString(contractId)); }); + // Adding required number of OutputVariables + this.transactionRequest.addVariableOutputs(outputVariables); + return this; }