Skip to content

Commit

Permalink
fix!: transaction funding (#1372)
Browse files Browse the repository at this point in the history
  • Loading branch information
Torres-ssf authored Nov 8, 2023
1 parent 54e613c commit 0ee6377
Show file tree
Hide file tree
Showing 23 changed files with 585 additions and 187 deletions.
11 changes: 11 additions & 0 deletions .changeset/thirty-monkeys-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@fuel-ts/docs-snippets": minor
"@fuel-ts/contract": minor
"@fuel-ts/interfaces": minor
"@fuel-ts/program": minor
"@fuel-ts/providers": minor
"@fuel-ts/script": minor
"@fuel-ts/wallet": minor
---

Fixing transaction funding
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ describe(__filename, () => {
})
.getTransactionCost();

expect(cost.fee).toBeDefined();
expect(cost.minFee).toBeDefined();
expect(cost.maxFee).toBeDefined();
expect(cost.gasPrice).toBeDefined();
expect(cost.gasUsed).toBeDefined();
expect(cost.minGasPrice).toBeDefined();
Expand All @@ -55,7 +56,8 @@ describe(__filename, () => {

const cost = await scope.getTransactionCost();

expect(cost.fee).toBeDefined();
expect(cost.minFee).toBeDefined();
expect(cost.maxFee).toBeDefined();
expect(cost.gasPrice).toBeDefined();
expect(cost.gasUsed).toBeDefined();
expect(cost.minGasPrice).toBeDefined();
Expand Down
26 changes: 11 additions & 15 deletions apps/docs-snippets/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,6 @@ export const getTestWallet = async (seedQuantities?: CoinQuantityLike[]) => {
// instantiate the genesis wallet with its secret key
const genesisWallet = new WalletUnlocked(process.env.GENESIS_SECRET || '0x01', provider);

// define the quantity of assets to transfer to the test wallet
const quantities: CoinQuantityLike[] = seedQuantities || [
{
amount: 1_000_000,
assetId: BaseAssetId,
},
];

// retrieve resources needed to spend the specified quantities
const resources = await genesisWallet.getResourcesToSpend(quantities);

// create a new test wallet
const testWallet = Wallet.generate({ provider });

Expand All @@ -42,14 +31,20 @@ export const getTestWallet = async (seedQuantities?: CoinQuantityLike[]) => {
gasPrice: minGasPrice,
});

// add the UTXO inputs to the transaction request
request.addResources(resources);

// add the transaction outputs (coins to be sent to the test wallet)
quantities
(seedQuantities || [[1_000_000, BaseAssetId]])
.map(coinQuantityfy)
.forEach(({ amount, assetId }) => request.addCoinOutput(testWallet.address, amount, assetId));

// get the cost of the transaction
const { minFee, requiredQuantities, gasUsed } =
await genesisWallet.provider.getTransactionCost(request);

request.gasLimit = gasUsed;

// funding the transaction with the required quantities
await genesisWallet.fund(request, requiredQuantities, minFee);

// execute the transaction, transferring resources to the test wallet
const response = await genesisWallet.sendTransaction(request);

Expand All @@ -73,6 +68,7 @@ export const createAndDeployContractFromProject = async (
return contractFactory.deployContract({
storageSlots,
gasPrice: minGasPrice,
gasLimit: 0,
});
};

Expand Down
1 change: 1 addition & 0 deletions packages/contract/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"license": "Apache-2.0",
"dependencies": {
"@fuel-ts/abi-coder": "workspace:*",
"@fuel-ts/address": "workspace:*",
"@fuel-ts/crypto": "workspace:*",
"@fuel-ts/merkle": "workspace:*",
"@fuel-ts/program": "workspace:*",
Expand Down
8 changes: 7 additions & 1 deletion packages/contract/src/contract-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,13 @@ export default class ContractFactory {
}

const { contractId, transactionRequest } = this.createTransactionRequest(deployContractOptions);
await this.account.fund(transactionRequest);

const { requiredQuantities, maxFee, gasUsed } =
await this.account.provider.getTransactionCost(transactionRequest);

transactionRequest.gasLimit = gasUsed;

await this.account.fund(transactionRequest, requiredQuantities, maxFee);
const response = await this.account.sendTransaction(transactionRequest);
await response.wait();

Expand Down
19 changes: 16 additions & 3 deletions packages/fuel-gauge/src/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ describe('Contract', () => {
const transactionCost = await invocationScope.getTransactionCost();

expect(toNumber(transactionCost.gasPrice)).toBe(gasPrice.toNumber());
expect(toNumber(transactionCost.fee)).toBeGreaterThanOrEqual(0);
expect(toNumber(transactionCost.minFee)).toBeGreaterThanOrEqual(0);
expect(toNumber(transactionCost.gasUsed)).toBeGreaterThan(300);

const { value } = await invocationScope
Expand Down Expand Up @@ -549,7 +549,7 @@ describe('Contract', () => {
const transactionCost = await invocationScope.getTransactionCost();

expect(toNumber(transactionCost.gasPrice)).toBe(minGasPrice.toNumber());
expect(toNumber(transactionCost.fee)).toBeGreaterThanOrEqual(1);
expect(toNumber(transactionCost.minFee)).toBeGreaterThanOrEqual(1);
expect(toNumber(transactionCost.gasUsed)).toBeGreaterThan(300);

// Test that gasUsed is correctly calculated
Expand Down Expand Up @@ -706,6 +706,8 @@ describe('Contract', () => {
const struct = { a: true, b: 1337 };
const invocationScopes = [contract.functions.foo(num), contract.functions.boo(struct)];
const multiCallScope = contract.multiCall(invocationScopes).txParams({ gasPrice });
const { maxFee } = await multiCallScope.getTransactionCost();
await multiCallScope.fundWithRequiredCoins(maxFee);

const transactionRequest = await multiCallScope.getTransactionRequest();

Expand Down Expand Up @@ -745,8 +747,12 @@ describe('Contract', () => {

const transactionRequestParsed = transactionRequestify(txRequestParsed);

const { requiredQuantities, maxFee } =
await provider.getTransactionCost(transactionRequestParsed);

// Fund tx
await wallet.fund(transactionRequestParsed);
await wallet.fund(transactionRequestParsed, requiredQuantities, maxFee);

// Send tx
const response = await wallet.sendTransaction(transactionRequestParsed);
const result = await response.waitForResult();
Expand Down Expand Up @@ -804,6 +810,13 @@ describe('Contract', () => {

const transactionRequestParsed = transactionRequestify(txRequestParsed);

const { gasUsed, minFee, requiredQuantities } =
await contract.provider.getTransactionCost(transactionRequestParsed);

transactionRequestParsed.gasLimit = gasUsed;

await contract.account.fund(transactionRequestParsed, requiredQuantities, minFee);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const response = await contract.account!.sendTransaction(transactionRequestParsed);
const {
Expand Down
4 changes: 3 additions & 1 deletion packages/fuel-gauge/src/fee.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ describe('Fee', () => {
const gasPrice = randomGasPrice(minGasPrice, 15);
const factory = new ContractFactory(binHexlified, abiContents, wallet);
const { transactionRequest } = factory.createTransactionRequest({ gasPrice });
await wallet.fund(transactionRequest);
const { maxFee, requiredQuantities } = await provider.getTransactionCost(transactionRequest);

await wallet.fund(transactionRequest, requiredQuantities, maxFee);

const tx = await wallet.sendTransaction(transactionRequest);
const { fee } = await tx.wait();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('Predicate', () => {
});

it('calls a no argument predicate and returns true', async () => {
const amountToPredicate = 100_000;
const amountToPredicate = 200_000;
const amountToReceiver = 50;
const initialReceiverBalance = await receiver.getBalance();

Expand All @@ -45,7 +45,7 @@ describe('Predicate', () => {
});

it('calls a no argument predicate and returns false', async () => {
const amountToPredicate = 100;
const amountToPredicate = 200_000;
const amountToReceiver = 50;

predicate = new Predicate(predicateBytesFalse, provider);
Expand Down
12 changes: 10 additions & 2 deletions packages/fuel-gauge/src/predicate/utils/predicate/fundPredicate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BaseAssetId } from 'fuels';
import { BaseAssetId, ScriptTransactionRequest } from 'fuels';
import type { InputValue, BN, BigNumberish, WalletUnlocked, Predicate } from 'fuels';

export const fundPredicate = async <T extends InputValue[]>(
Expand All @@ -7,9 +7,17 @@ export const fundPredicate = async <T extends InputValue[]>(
amountToPredicate: BigNumberish
): Promise<BN> => {
const { minGasPrice } = wallet.provider.getGasConfig();
const tx = await wallet.transfer(predicate.address, amountToPredicate, BaseAssetId, {

const request = new ScriptTransactionRequest({
gasPrice: minGasPrice,
});

request.addCoinOutput(predicate.address, amountToPredicate, BaseAssetId);
const { minFee, requiredQuantities, gasUsed } = await wallet.provider.getTransactionCost(request);
request.gasLimit = gasUsed;
await wallet.fund(request, requiredQuantities, minFee);

const tx = await wallet.sendTransaction(request);
await tx.waitForResult();

return predicate.getBalance();
Expand Down
2 changes: 1 addition & 1 deletion packages/interfaces/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ export abstract class AbstractAccount {
abstract getResourcesToSpend(quantities: any[], options?: any): any;
abstract sendTransaction(transactionRequest: any): any;
abstract simulateTransaction(transactionRequest: any): any;
abstract fund(transactionRequest: any, quantities: any, fee: any): Promise<void>;
}

/**
* @hidden
*/
Expand Down
51 changes: 33 additions & 18 deletions packages/program/src/functions/base-invocation-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import type { InputValue } from '@fuel-ts/abi-coder';
import { ErrorCode, FuelError } from '@fuel-ts/errors';
import type { AbstractContract, AbstractProgram } from '@fuel-ts/interfaces';
import type { BN } from '@fuel-ts/math';
import { bn, toNumber } from '@fuel-ts/math';
import type { Provider, CoinQuantity } from '@fuel-ts/providers';
import { transactionRequestify, ScriptTransactionRequest } from '@fuel-ts/providers';
import { ScriptTransactionRequest } from '@fuel-ts/providers';
import { InputType } from '@fuel-ts/transactions';
import type { BaseWalletUnlocked } from '@fuel-ts/wallet';
import * as asm from '@fuels/vm-asm';
Expand Down Expand Up @@ -118,16 +119,13 @@ export class BaseInvocationScope<TReturn = any> {
* @returns An array of required coin quantities.
*/
protected getRequiredCoins(): Array<CoinQuantity> {
const { gasPriceFactor } = this.getProvider().getGasConfig();

const assets = this.calls
const forwardingAssets = this.calls
.map((call) => ({
assetId: String(call.assetId),
amount: bn(call.amount || 0),
}))
.concat(this.transactionRequest.calculateFee(gasPriceFactor))
.filter(({ assetId, amount }) => assetId && !bn(amount).isZero());
return assets;
return forwardingAssets;
}

/**
Expand Down Expand Up @@ -191,10 +189,6 @@ export class BaseInvocationScope<TReturn = any> {
// Check if gasLimit is less than the
// sum of all call gasLimits
this.checkGasLimitTotal();

if (this.program.account) {
await this.fundWithRequiredCoins();
}
}

/**
Expand All @@ -219,10 +213,9 @@ export class BaseInvocationScope<TReturn = any> {
async getTransactionCost(options?: TransactionCostOptions) {
const provider = this.getProvider();

await this.prepareTransaction();
const request = transactionRequestify(this.transactionRequest);
const request = await this.getTransactionRequest();
request.gasPrice = bn(toNumber(request.gasPrice) || toNumber(options?.gasPrice || 0));
const txCost = await provider.getTransactionCost(request);
const txCost = await provider.getTransactionCost(request, this.getRequiredCoins());

return txCost;
}
Expand All @@ -232,13 +225,14 @@ export class BaseInvocationScope<TReturn = any> {
*
* @returns The current instance of the class.
*/
async fundWithRequiredCoins() {
async fundWithRequiredCoins(fee: BN) {
// Clean coin inputs before add new coins to the request
this.transactionRequest.inputs = this.transactionRequest.inputs.filter(
(i) => i.type !== InputType.Coin
);
const resources = await this.program.account?.getResourcesToSpend(this.requiredCoins);
this.transactionRequest.addResources(resources || []);

await this.program.account?.fund(this.transactionRequest, this.requiredCoins, fee);

return this;
}

Expand Down Expand Up @@ -292,6 +286,18 @@ export class BaseInvocationScope<TReturn = any> {
assert(this.program.account, 'Wallet is required!');

const transactionRequest = await this.getTransactionRequest();

const { maxFee, gasUsed } = await this.getTransactionCost();

if (gasUsed.gt(bn(transactionRequest.gasLimit))) {
throw new FuelError(
ErrorCode.GAS_LIMIT_TOO_LOW,
`Gas limit '${transactionRequest.gasLimit}' is lower than the required: '${gasUsed}'.`
);
}

await this.fundWithRequiredCoins(maxFee);

const response = await this.program.account.sendTransaction(transactionRequest);

return FunctionInvocationResult.build<T>(
Expand Down Expand Up @@ -324,6 +330,11 @@ export class BaseInvocationScope<TReturn = any> {
}

const transactionRequest = await this.getTransactionRequest();

const { maxFee } = await this.getTransactionCost();

await this.fundWithRequiredCoins(maxFee);

const result = await this.program.account.simulateTransaction(transactionRequest);

return InvocationCallResult.build<T>(this.functionInvocationScopes, result, this.isMultiCall);
Expand All @@ -335,11 +346,15 @@ export class BaseInvocationScope<TReturn = any> {
* @returns The result of the invocation call.
*/
async dryRun<T = TReturn>(): Promise<InvocationCallResult<T>> {
assert(this.program.account, 'Wallet is required!');

const provider = this.getProvider();

const { maxFee } = await this.getTransactionCost();

await this.fundWithRequiredCoins(maxFee);
const transactionRequest = await this.getTransactionRequest();
const request = transactionRequestify(transactionRequest);
const response = await provider.call(request, {
const response = await provider.call(transactionRequest, {
utxoValidation: false,
});

Expand Down
20 changes: 20 additions & 0 deletions packages/providers/src/coin-quantity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,23 @@ export const coinQuantityfy = (coinQuantityLike: CoinQuantityLike): CoinQuantity
max: max ? bn(max) : undefined,
};
};

export interface IAddAmountToAssetParams {
assetId: string;
amount: BN;
coinQuantities: CoinQuantity[];
}

export const addAmountToAsset = (params: IAddAmountToAssetParams): CoinQuantity[] => {
const { amount, assetId, coinQuantities } = params;

const assetIdx = coinQuantities.findIndex((coinQuantity) => coinQuantity.assetId === assetId);

if (assetIdx !== -1) {
coinQuantities[assetIdx].amount = coinQuantities[assetIdx].amount.add(amount);
} else {
coinQuantities.push({ assetId, amount });
}

return coinQuantities;
};
Loading

0 comments on commit 0ee6377

Please sign in to comment.