diff --git a/.changeset/poor-kiwis-attend.md b/.changeset/poor-kiwis-attend.md new file mode 100644 index 00000000000..4db91615e13 --- /dev/null +++ b/.changeset/poor-kiwis-attend.md @@ -0,0 +1,31 @@ +--- +"@fuel-ts/abi-coder": minor +"@fuel-ts/address": minor +"@fuel-ts/constants": minor +"@fuel-ts/contract": minor +"@fuel-ts/example-contract": minor +"fuelchain": minor +"fuels": minor +"@fuel-ts/hasher": minor +"@fuel-ts/hdwallet": minor +"@fuel-ts/interfaces": minor +"@fuel-ts/keystore": minor +"@fuel-ts/math": minor +"@fuel-ts/merkle": minor +"@fuel-ts/merkle-shared": minor +"@fuel-ts/merklesum": minor +"@fuel-ts/mnemonic": minor +"@fuel-ts/predicate": minor +"@fuel-ts/providers": minor +"@fuel-ts/script": minor +"@fuel-ts/signer": minor +"@fuel-ts/sparsemerkle": minor +"@fuel-ts/testcases": minor +"@fuel-ts/transactions": minor +"typechain-target-fuels": minor +"@fuel-ts/wallet": minor +"@fuel-ts/wallet-manager": minor +"@fuel-ts/wordlists": minor +--- + +add output variables to transactions diff --git a/packages/fuel-gauge/src/predicate.test.ts b/packages/fuel-gauge/src/predicate.test.ts index 9fe8c62cab2..50c92422bf7 100644 --- a/packages/fuel-gauge/src/predicate.test.ts +++ b/packages/fuel-gauge/src/predicate.test.ts @@ -1,6 +1,6 @@ import { readFileSync } from 'fs'; import { Address, NativeAssetId, bn, toHex, toNumber, Provider, TestUtils, Predicate } from 'fuels'; -import type { AbstractAddress, BigNumberish, BN, Wallet, BaseWalletLocked } from 'fuels'; +import type { AbstractAddress, BigNumberish, BN, BaseWalletLocked } from 'fuels'; import { join } from 'path'; import testPredicateAddress from '../test-projects/predicate-address'; diff --git a/packages/fuel-gauge/src/token-test-contract.test.ts b/packages/fuel-gauge/src/token-test-contract.test.ts index 20b37c6fc99..13480608cc1 100644 --- a/packages/fuel-gauge/src/token-test-contract.test.ts +++ b/packages/fuel-gauge/src/token-test-contract.test.ts @@ -53,4 +53,59 @@ describe('TokenTestContract', () => { const tokenBalance = balances.find((b) => b.assetId === token.id.toB256()); expect(tokenBalance?.amount.toHex()).toEqual(toHex(50)); }); + + it('Automatically add variableOuputs', async () => { + const [wallet1, wallet2, wallet3] = Array.from({ length: 3 }, () => + Wallet.generate({ provider }) + ); + + const addresses = [wallet1, wallet2, wallet3].map((wallet) => ({ value: wallet.address })); + + const token = await setup(); + + const functionCallOne = token.functions.mint_to_addresses(10, addresses); + await functionCallOne.dryRun(); + await functionCallOne.call(); + + let balances = await wallet1.getBalances(); + let tokenBalance = balances.find((b) => b.assetId === token.id.toB256()); + expect(tokenBalance?.amount.toHex()).toEqual(toHex(10)); + + balances = await wallet2.getBalances(); + tokenBalance = balances.find((b) => b.assetId === token.id.toB256()); + expect(tokenBalance?.amount.toHex()).toEqual(toHex(10)); + + balances = await wallet3.getBalances(); + tokenBalance = balances.find((b) => b.assetId === token.id.toB256()); + expect(tokenBalance?.amount.toHex()).toEqual(toHex(10)); + + const functionCallTwo = token.functions.mint_to_addresses(10, addresses); + await functionCallTwo.simulate(); + await functionCallTwo.call(); + + balances = await wallet1.getBalances(); + tokenBalance = balances.find((b) => b.assetId === token.id.toB256()); + expect(tokenBalance?.amount.toHex()).toEqual(toHex(20)); + + balances = await wallet2.getBalances(); + tokenBalance = balances.find((b) => b.assetId === token.id.toB256()); + expect(tokenBalance?.amount.toHex()).toEqual(toHex(20)); + + balances = await wallet3.getBalances(); + tokenBalance = balances.find((b) => b.assetId === token.id.toB256()); + expect(tokenBalance?.amount.toHex()).toEqual(toHex(20)); + + await token.functions.mint_to_addresses(10, addresses).call(); + balances = await wallet1.getBalances(); + tokenBalance = balances.find((b) => b.assetId === token.id.toB256()); + expect(tokenBalance?.amount.toHex()).toEqual(toHex(30)); + + balances = await wallet2.getBalances(); + tokenBalance = balances.find((b) => b.assetId === token.id.toB256()); + expect(tokenBalance?.amount.toHex()).toEqual(toHex(30)); + + balances = await wallet3.getBalances(); + tokenBalance = balances.find((b) => b.assetId === token.id.toB256()); + expect(tokenBalance?.amount.toHex()).toEqual(toHex(30)); + }); }); diff --git a/packages/fuel-gauge/test-projects/token_abi/src/main.sw b/packages/fuel-gauge/test-projects/token_abi/src/main.sw index 370b1d1aea4..244bd82dd90 100644 --- a/packages/fuel-gauge/test-projects/token_abi/src/main.sw +++ b/packages/fuel-gauge/test-projects/token_abi/src/main.sw @@ -4,8 +4,10 @@ use std::{address::Address, contract_id::ContractId, token::*}; abi Token { fn mint_coins(mint_amount: u64, a: u32); + fn mint_to_addresses(mint_amount: u64, addresses: [Address; 3]); fn burn_coins(burn_amount: u64, a: u32); fn force_transfer_coins(coins: u64, asset_id: ContractId, target: ContractId); fn transfer_coins_to_output(coins: u64, asset_id: ContractId, recipient: Address); fn get_balance(target: ContractId, asset_id: ContractId) -> u64; + fn get_msg_amount() -> u64; } diff --git a/packages/fuel-gauge/test-projects/token_contract/src/main.sw b/packages/fuel-gauge/test-projects/token_contract/src/main.sw index 8499c4c31a8..d53bdfdbd11 100644 --- a/packages/fuel-gauge/test-projects/token_contract/src/main.sw +++ b/packages/fuel-gauge/test-projects/token_contract/src/main.sw @@ -1,6 +1,6 @@ contract; -use std::{address::Address, context::balance_of, contract_id::ContractId, token::*}; +use std::{context::balance_of, context::msg_amount, token::*}; use token_abi::Token; impl Token for Contract { @@ -8,6 +8,14 @@ impl Token for Contract { mint(mint_amount); } + fn mint_to_addresses(mint_amount: u64, addresses: [Address; 3]) { + let mut counter = 0; + while counter < 3 { + mint_to_address(mint_amount, addresses[counter]); + counter = counter + 1; + } + } + fn burn_coins(burn_amount: u64, a: u32) { burn(burn_amount); } @@ -23,4 +31,8 @@ impl Token for Contract { fn get_balance(target: ContractId, asset_id: ContractId) -> u64 { balance_of(target, asset_id) } + + fn get_msg_amount() -> u64 { + msg_amount() + } } diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index bffba807476..421e906781a 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -11,6 +11,7 @@ import type { BigNumberish, BN } from '@fuel-ts/math'; import { max, bn, multiply } from '@fuel-ts/math'; import type { Transaction } from '@fuel-ts/transactions'; import { + TransactionType, InputMessageCoder, GAS_PRICE_FACTOR, MAX_GAS_PER_TX, @@ -34,13 +35,19 @@ import type { Message } from './message'; import type { ExcludeResourcesOption, RawCoin, Resources } from './resource'; import { isCoin } from './resource'; import { ScriptTransactionRequest, transactionRequestify } from './transaction-request'; -import type { TransactionRequestLike } from './transaction-request'; +import type { TransactionRequestLike, TransactionRequest } from './transaction-request'; import type { TransactionResult, TransactionResultReceipt, } from './transaction-response/transaction-response'; import { TransactionResponse } from './transaction-response/transaction-response'; -import { calculatePriceWithFactor, getGasUsedFromReceipts } from './util'; +import { + calculatePriceWithFactor, + getGasUsedFromReceipts, + getReceiptsWithMissingOutputVariables, +} from './util'; + +const MAX_RETRIES = 10; export type CallResult = { receipts: TransactionResultReceipt[]; @@ -233,11 +240,15 @@ export default class Provider { /** * Submits a transaction to the chain to be executed + * If the transaction is missing VariableOuputs + * the transaction will be mutate and VariableOuputs will be added */ async sendTransaction( transactionRequestLike: TransactionRequestLike ): Promise { const transactionRequest = transactionRequestify(transactionRequestLike); + await this.addMissingVariableOutputs(transactionRequest); + const encodedTransaction = hexlify(transactionRequest.toTransactionBytes()); const { gasUsed, minGasPrice } = await this.getTransactionCost(transactionRequest, 0); @@ -263,12 +274,16 @@ export default class Provider { /** * Executes a transaction without actually submitting it to the chain + * If the transaction is missing VariableOuputs + * the transaction will be mutate and VariableOuputs will be added */ async call( transactionRequestLike: TransactionRequestLike, { utxoValidation }: ProviderCallParams = {} ): Promise { const transactionRequest = transactionRequestify(transactionRequestLike); + await this.addMissingVariableOutputs(transactionRequest); + const encodedTransaction = hexlify(transactionRequest.toTransactionBytes()); const { dryRun: gqlReceipts } = await this.operations.dryRun({ encodedTransaction, @@ -280,12 +295,44 @@ export default class Provider { }; } + /** + * Will dryRun a transaction and check for missing VariableOutputs + * + * If there are missing VariableOutputs + * `addVariableOutputs` is called on the transaction. + * This process is done at most 10 times + */ + addMissingVariableOutputs = async ( + transactionRequest: TransactionRequest, + tries: number = 0 + ): Promise => { + let missingOutputVariableCount = 0; + + if (transactionRequest.type === TransactionType.Create) { + return; + } + + do { + const encodedTransaction = hexlify(transactionRequest.toTransactionBytes()); + const { dryRun: gqlReceipts } = await this.operations.dryRun({ + encodedTransaction, + utxoValidation: false, + }); + const receipts = gqlReceipts.map(processGqlReceipt); + missingOutputVariableCount = getReceiptsWithMissingOutputVariables(receipts).length; + transactionRequest.addVariableOutputs(missingOutputVariableCount); + } while (tries > MAX_RETRIES || missingOutputVariableCount > 0); + }; + /** * Executes a signed transaction without applying the states changes * on the chain. + * If the transaction is missing VariableOuputs + * the transaction will be mutate and VariableOuputs will be added */ async simulate(transactionRequestLike: TransactionRequestLike): Promise { const transactionRequest = transactionRequestify(transactionRequestLike); + await this.addMissingVariableOutputs(transactionRequest); const encodedTransaction = hexlify(transactionRequest.toTransactionBytes()); const { dryRun: gqlReceipts } = await this.operations.dryRun({ encodedTransaction, diff --git a/packages/providers/src/util.ts b/packages/providers/src/util.ts index 336e08190d9..9ffa4466634 100644 --- a/packages/providers/src/util.ts +++ b/packages/providers/src/util.ts @@ -2,7 +2,7 @@ import type { BytesLike } from '@ethersproject/bytes'; import { arrayify } from '@ethersproject/bytes'; import type { BN } from '@fuel-ts/math'; import { bn } from '@fuel-ts/math'; -import { ReceiptType } from '@fuel-ts/transactions'; +import { FAILED_TRANSFER_TO_ADDRESS_SIGNAL, ReceiptType } from '@fuel-ts/transactions'; import type { TransactionResultReceipt } from './transaction-response'; @@ -32,3 +32,12 @@ export const getGasUsedFromReceipts = (receipts: Array return bn(0); }; + +export const getReceiptsWithMissingOutputVariables = ( + receipts: Array +): Array => + receipts.filter( + (receipt) => + receipt.type === ReceiptType.Revert && + receipt.val.toString('hex') === FAILED_TRANSFER_TO_ADDRESS_SIGNAL + ); diff --git a/packages/script/src/errors.ts b/packages/script/src/errors.ts index 134538e9160..766509aaeb6 100644 --- a/packages/script/src/errors.ts +++ b/packages/script/src/errors.ts @@ -23,7 +23,7 @@ export class ScriptResultDecoderError extends Error { ) as TransactionResultRevertReceipt[]; const revertsText = revertReceipts.length ? `Reverts:\n${revertReceipts - .map(({ type, id, ...r }) => + .map(({ id, ...r }) => printLineWithId(id, `${r.val} ${JSON.stringify(r, bigintReplacer)}`) ) .join('\n')}` diff --git a/packages/transactions/src/consts.ts b/packages/transactions/src/consts.ts index 09dfe23d441..319ad7611d3 100644 --- a/packages/transactions/src/consts.ts +++ b/packages/transactions/src/consts.ts @@ -37,3 +37,5 @@ export const MAX_PREDICATE_LENGTH = 1024 * 1024; // TODO: set max predicate data length value /** Maximum length of predicate data, in bytes. */ export const MAX_PREDICATE_DATA_LENGTH = 1024 * 1024; + +export const FAILED_TRANSFER_TO_ADDRESS_SIGNAL = '0xffffffffffff0001'; diff --git a/packages/wallet/src/base-locked-wallet.ts b/packages/wallet/src/base-locked-wallet.ts index 71b18eb7540..b80c4aa15fb 100644 --- a/packages/wallet/src/base-locked-wallet.ts +++ b/packages/wallet/src/base-locked-wallet.ts @@ -215,6 +215,7 @@ export class BaseWalletLocked extends AbstractWallet { transactionRequestLike: TransactionRequestLike ): Promise { const transactionRequest = transactionRequestify(transactionRequestLike); + await this.provider.addMissingVariableOutputs(transactionRequest); return this.provider.sendTransaction(transactionRequest); } @@ -226,6 +227,7 @@ export class BaseWalletLocked extends AbstractWallet { */ async simulateTransaction(transactionRequestLike: TransactionRequestLike): Promise { const transactionRequest = transactionRequestify(transactionRequestLike); + await this.provider.addMissingVariableOutputs(transactionRequest); return this.provider.simulate(transactionRequest); } diff --git a/packages/wallet/src/base-unlocked-wallet.ts b/packages/wallet/src/base-unlocked-wallet.ts index d9a9765b4ba..682d0cd328e 100644 --- a/packages/wallet/src/base-unlocked-wallet.ts +++ b/packages/wallet/src/base-unlocked-wallet.ts @@ -81,7 +81,7 @@ export class BaseWalletUnlocked extends BaseWalletLocked { transactionRequestLike: TransactionRequestLike ): Promise { const transactionRequest = transactionRequestify(transactionRequestLike); - + await this.provider.addMissingVariableOutputs(transactionRequest); return this.provider.sendTransaction( await this.populateTransactionWitnessesSignature(transactionRequest) ); @@ -95,7 +95,7 @@ export class BaseWalletUnlocked extends BaseWalletLocked { */ async simulateTransaction(transactionRequestLike: TransactionRequestLike): Promise { const transactionRequest = transactionRequestify(transactionRequestLike); - + await this.provider.addMissingVariableOutputs(transactionRequest); return this.provider.call( await this.populateTransactionWitnessesSignature(transactionRequest), {