Skip to content

Commit

Permalink
fix: TX estimation when InputMessage contains data (#3078)
Browse files Browse the repository at this point in the history
  • Loading branch information
Torres-ssf authored Sep 3, 2024
1 parent b00fd02 commit 7d74c8c
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-kids-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/account": patch
---

fix: TX estimation when `InputMessage` contains data
12 changes: 8 additions & 4 deletions packages/account/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AbstractAccount } from '@fuel-ts/interfaces';
import type { AbstractAddress, BytesLike } from '@fuel-ts/interfaces';
import type { BigNumberish, BN } from '@fuel-ts/math';
import { bn } from '@fuel-ts/math';
import { InputType } from '@fuel-ts/transactions';
import { arrayify, hexlify, isDefined } from '@fuel-ts/utils';
import { clone } from 'ramda';

Expand Down Expand Up @@ -41,6 +42,7 @@ import {
cacheRequestInputsResourcesFromOwner,
getAssetAmountInRequestInputs,
isRequestInputCoin,
isRequestInputMessageWithoutData,
isRequestInputResource,
} from './providers/transaction-request/helpers';
import { mergeQuantities } from './providers/utils/merge-quantities';
Expand Down Expand Up @@ -270,7 +272,7 @@ export class Account extends AbstractAccount {
});

const totalBaseAssetOnInputs = getAssetAmountInRequestInputs(
request.inputs,
request.inputs.filter(isRequestInputResource),
baseAssetId,
baseAssetId
);
Expand Down Expand Up @@ -542,13 +544,15 @@ export class Account extends AbstractAccount {

const findAssetInput = (assetId: string) =>
txRequestClone.inputs.find((input) => {
if ('assetId' in input) {
if (input.type === InputType.Coin) {
return input.assetId === assetId;
}
if ('recipient' in input) {

// We only consider the message input if it has no data.
// Messages with `data` cannot fund the gas of a transaction.
if (isRequestInputMessageWithoutData(input)) {
return baseAssetId === assetId;
}

return false;
});

Expand Down
3 changes: 2 additions & 1 deletion packages/account/src/predicate/predicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
transactionRequestify,
isRequestInputResource,
isRequestInputResourceFromOwner,
isRequestInputCoinOrMessage,
} from '../providers';
import type {
CallResult,
Expand Down Expand Up @@ -95,7 +96,7 @@ export class Predicate<
request.removeWitness(placeholderIndex);
}

request.inputs.filter(isRequestInputResource).forEach((input) => {
request.inputs.filter(isRequestInputCoinOrMessage).forEach((input) => {
if (isRequestInputResourceFromOwner(input, this.address)) {
// eslint-disable-next-line no-param-reassign
input.predicate = hexlify(this.bytes);
Expand Down
5 changes: 5 additions & 0 deletions packages/account/src/providers/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export type Message = {
amount: BN;
data: BytesLike;
daHeight: BN;
predicate?: BytesLike;
predicateData?: BytesLike;
};
// #endregion Message-shape

Expand Down Expand Up @@ -71,3 +73,6 @@ export type MessageProof = {
export type MessageStatus = {
state: GqlMessageState;
};

export const isMessageCoin = (message: Message | MessageCoin): message is MessageCoin =>
!('data' in message);
12 changes: 11 additions & 1 deletion packages/account/src/providers/transaction-request/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,21 @@ export const isRequestInputMessage = (
input: TransactionRequestInput
): input is MessageTransactionRequestInput => input.type === InputType.Message;

export const isRequestInputResource = (
export const isRequestInputMessageWithoutData = (
input: TransactionRequestInput
): input is MessageTransactionRequestInput =>
input.type === InputType.Message && bn(input.data).isZero();

export const isRequestInputCoinOrMessage = (
input: TransactionRequestInput
): input is CoinTransactionRequestInput | MessageTransactionRequestInput =>
isRequestInputCoin(input) || isRequestInputMessage(input);

export const isRequestInputResource = (
input: TransactionRequestInput
): input is CoinTransactionRequestInput | MessageTransactionRequestInput =>
isRequestInputCoin(input) || isRequestInputMessageWithoutData(input);

export const getRequestInputResourceOwner = (
input: CoinTransactionRequestInput | MessageTransactionRequestInput
) => (isRequestInputCoin(input) ? input.owner : input.recipient);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import type { Account } from '../../account';
import type { Coin } from '../coin';
import type { CoinQuantity, CoinQuantityLike } from '../coin-quantity';
import { coinQuantityfy } from '../coin-quantity';
import type { MessageCoin } from '../message';
import { isMessageCoin, type Message, type MessageCoin } from '../message';
import type { ChainInfo, GasCosts } from '../provider';
import type { Resource } from '../resource';
import { isCoin } from '../resource';
Expand All @@ -35,6 +35,7 @@ import { getMaxGas, getMinGas } from '../utils/gas';
import { NoWitnessAtIndexError } from './errors';
import {
getRequestInputResourceOwner,
isRequestInputCoinOrMessage,
isRequestInputResource,
isRequestInputResourceFromOwner,
} from './helpers';
Expand Down Expand Up @@ -398,8 +399,8 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi
*
* @param message - Message resource.
*/
addMessageInput(message: MessageCoin) {
const { recipient, sender, amount, predicate, nonce, assetId, predicateData } = message;
addMessageInput(message: Message | MessageCoin) {
const { recipient, sender, amount, predicate, nonce, predicateData } = message;

let witnessIndex;

Expand All @@ -419,6 +420,7 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi
type: InputType.Message,
sender: sender.toB256(),
recipient: recipient.toB256(),
data: isMessageCoin(message) ? '0x' : message.data,
amount,
witnessIndex,
predicate,
Expand All @@ -429,7 +431,9 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi
this.pushInput(input);

// Insert a ChangeOutput if it does not exist
this.addChangeOutput(recipient, assetId);
if (isMessageCoin(message)) {
this.addChangeOutput(recipient, message.assetId);
}
}

/**
Expand Down Expand Up @@ -676,7 +680,7 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi
}

updatePredicateGasUsed(inputs: TransactionRequestInput[]) {
const inputsToExtractGasUsed = inputs.filter(isRequestInputResource);
const inputsToExtractGasUsed = inputs.filter(isRequestInputCoinOrMessage);

this.inputs.filter(isRequestInputResource).forEach((i) => {
const owner = getRequestInputResourceOwner(i);
Expand Down
103 changes: 101 additions & 2 deletions packages/fuel-gauge/src/fee.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { ContractFactory, ScriptTransactionRequest, Wallet, getRandomB256 } from 'fuels';
import {
ContractFactory,
InputMessageCoder,
ScriptTransactionRequest,
Wallet,
getRandomB256,
hexlify,
isCoin,
} from 'fuels';
import type { BN } from 'fuels';
import { launchTestNode, ASSET_A, ASSET_B, expectToBeInRange } from 'fuels/test-utils';
import { launchTestNode, ASSET_A, ASSET_B, expectToBeInRange, TestMessage } from 'fuels/test-utils';

import {
CallTestContractFactory,
Expand Down Expand Up @@ -333,4 +341,95 @@ describe('Fee', () => {
max: balanceDiff + 20,
});
});

describe('Estimation with Message containing data within TX request inputs', () => {
// Message with data and amount
const testMessage1 = new TestMessage({
data: hexlify(InputMessageCoder.encodeData('0x09')),
amount: 100_000_000,
});

// Message with data and without amount
const testMessage2 = new TestMessage({
data: hexlify(InputMessageCoder.encodeData('0x10')),
amount: 0,
});

it('should not fail [W/ UTXO within inputs]', async () => {
using launched = await launchTestNode({
contractsConfigs: [
{
factory: CallTestContractFactory,
},
],
walletsConfig: {
count: 2,
messages: [testMessage1, testMessage2],
},
});

const {
provider,
contracts: [contract],
wallets: [fundedWallet],
} = launched;

const baseAssetId = provider.getBaseAssetId();

const {
messages: [message1, message2],
} = await fundedWallet.getMessages();

const request = await contract.functions.foo(10).getTransactionRequest();

const resources = await fundedWallet.getResourcesToSpend([[1000, baseAssetId]]);

// Should include only UTXOs resources
expect(resources.every(isCoin)).toBeTruthy();

request.addMessageInput(message1);
request.addMessageInput(message2);
request.addResources(resources);

const cost = await fundedWallet.getTransactionCost(request);

expect(cost.dryRunStatus?.type).toBe('DryRunSuccessStatus');
});

it('should not fail [W/out UTXO within inputs]', async () => {
using launched = await launchTestNode({
contractsConfigs: [
{
factory: CallTestContractFactory,
},
],
walletsConfig: {
count: 2,
messages: [testMessage1, testMessage2],
},
});

const {
provider,
contracts: [contract],
wallets: [fundedWallet],
} = launched;

const baseAssetId = provider.getBaseAssetId();

const {
messages: [message1, message2],
} = await fundedWallet.getMessages();

const request = await contract.functions.foo(10).getTransactionRequest();

request.addCoinOutput(fundedWallet.address, 1000, baseAssetId);
request.addMessageInput(message1);
request.addMessageInput(message2);

const cost = await fundedWallet.getTransactionCost(request);

expect(cost.dryRunStatus?.type).toBe('DryRunSuccessStatus');
});
});
});
Loading

0 comments on commit 7d74c8c

Please sign in to comment.