diff --git a/.changeset/violet-grapes-matter.md b/.changeset/violet-grapes-matter.md new file mode 100644 index 00000000..592e228c --- /dev/null +++ b/.changeset/violet-grapes-matter.md @@ -0,0 +1,5 @@ +--- +"@fuel-connectors/walletconnect-connector": minor +--- + +Fix transaction fees in WalletConnectConnector. Now users don't need to force maxFee or gasLimit diff --git a/examples/react-app/src/components/counter.tsx b/examples/react-app/src/components/counter.tsx index 72890e8c..0d784179 100644 --- a/examples/react-app/src/components/counter.tsx +++ b/examples/react-app/src/components/counter.tsx @@ -65,10 +65,7 @@ export default function ContractCounter({ isSigning, setIsSigning }: Props) { setIsSigning(true); const contract = CounterAbi__factory.connect(COUNTER_CONTRACT_ID, wallet); try { - await contract.functions - .increment_counter() - .txParams({ gasLimit: bn(200_000), maxFee: bn(150_000) }) - .call(); + await contract.functions.increment_counter().call(); getCount(); diff --git a/packages/walletconnect-connector/src/WalletConnectConnector.ts b/packages/walletconnect-connector/src/WalletConnectConnector.ts index 455d54cb..5043b05d 100644 --- a/packages/walletconnect-connector/src/WalletConnectConnector.ts +++ b/packages/walletconnect-connector/src/WalletConnectConnector.ts @@ -19,7 +19,10 @@ import { type Network, type TransactionRequestLike, type Version, + ZeroBytes32, bn, + calculateGasFee, + concat, transactionRequestify, } from 'fuels'; @@ -28,6 +31,7 @@ import { ETHEREUM_ICON, TESTNET_URL } from './constants'; import type { Predicate, PredicateConfig, WalletConnectConfig } from './types'; import { PredicateAccount } from './utils/Predicate'; import { createModalConfig } from './utils/wagmiConfig'; + export class WalletConnectConnector extends FuelConnector { name = 'Ethereum Wallets'; @@ -300,28 +304,64 @@ export class WalletConnectConnector extends FuelConnector { throw Error(`No account found for ${address}`); } const transactionRequest = transactionRequestify(transaction); + const transactionFee = transactionRequest.maxFee.toNumber(); + + const predicateSignatureIndex = transactionRequest.witnesses.length - 1; // Create a predicate and set the witness index to call in predicate` const predicate = this.predicateAccount.createPredicate( evmAccount, fuelProvider, - [transactionRequest.witnesses.length - 1], + [predicateSignatureIndex], ); predicate.connect(fuelProvider); - // Attach missing inputs (including estimated predicate gas usage) / outputs to the request - await predicate.provider.estimateTxDependencies(transactionRequest); - // To each input of the request, attach the predicate and its data const requestWithPredicateAttached = predicate.populateTransactionPredicateData(transactionRequest); + const maxGasUsed = + await this.predicateAccount.getMaxPredicateGasUsed(fuelProvider); + + let predictedGasUsedPredicate = bn(0); requestWithPredicateAttached.inputs.forEach((input) => { if ('predicate' in input && input.predicate) { input.witnessIndex = 0; + predictedGasUsedPredicate = predictedGasUsedPredicate.add(maxGasUsed); } }); + // Add a placeholder for the predicate signature to count on bytes measurement from start. It will be replaced later + requestWithPredicateAttached.witnesses[predicateSignatureIndex] = concat([ + ZeroBytes32, + ZeroBytes32, + ]); + + const { gasPriceFactor } = await predicate.provider.getGasConfig(); + const { maxFee, gasPrice } = await predicate.provider.estimateTxGasAndFee({ + transactionRequest: requestWithPredicateAttached, + }); + + const predicateSuccessFeeDiff = calculateGasFee({ + gas: predictedGasUsedPredicate, + priceFactor: gasPriceFactor, + gasPrice, + }); + + const feeWithFat = maxFee.add(predicateSuccessFeeDiff); + const isNeededFatFee = feeWithFat.gt(transactionFee); + + if (isNeededFatFee) { + // add more 10 just in case sdk fee estimation is not accurate + requestWithPredicateAttached.maxFee = feeWithFat.add(10); + } + + // Attach missing inputs (including estimated predicate gas usage) / outputs to the request + await predicate.provider.estimateTxDependencies( + requestWithPredicateAttached, + ); + + // gets the transactionID in fuel and ask to sign in eth wallet const txID = requestWithPredicateAttached.getTransactionId(chainId); // biome-ignore lint/suspicious/noExplicitAny: const provider: any = await getAccount( @@ -334,7 +374,8 @@ export class WalletConnectConnector extends FuelConnector { // Transform the signature into compact form for Sway to understand const compactSignature = splitSignature(hexToBytes(signature)).compact; - transactionRequest.witnesses.push(compactSignature); + requestWithPredicateAttached.witnesses[predicateSignatureIndex] = + compactSignature; const transactionWithPredicateEstimated = await fuelProvider.estimatePredicates(requestWithPredicateAttached); diff --git a/packages/walletconnect-connector/src/utils/Predicate.ts b/packages/walletconnect-connector/src/utils/Predicate.ts index 4d6a7a92..83840273 100644 --- a/packages/walletconnect-connector/src/utils/Predicate.ts +++ b/packages/walletconnect-connector/src/utils/Predicate.ts @@ -1,13 +1,19 @@ import { arrayify } from '@ethersproject/bytes'; import { Address, + type BN, type InputValue, type JsonAbi, Predicate, type Provider, + ScriptTransactionRequest, + ZeroBytes32, + bn, getPredicateRoot, } from 'fuels'; import memoize from 'memoizee'; +import { privateKeyToAccount } from 'viem/accounts'; + import type { PredicateConfig } from '../types'; export class PredicateAccount { @@ -54,6 +60,35 @@ export class PredicateAccount { }, ); + getMaxPredicateGasUsed = memoize(async (provider: Provider): Promise => { + const account = privateKeyToAccount( + '0x0000000000000000000000000000000000000000000000000000000000000001', + ); + const chainId = provider.getChainId(); + const fakePredicate = this.createPredicate(account.address, provider, [0]); + const request = new ScriptTransactionRequest(); + request.addCoinInput({ + id: ZeroBytes32, + assetId: ZeroBytes32, + amount: bn(), + owner: fakePredicate.address, + blockCreated: bn(), + txCreatedIdx: bn(), + }); + fakePredicate.populateTransactionPredicateData(request); + const txId = request.getTransactionId(chainId); + const signature = await account.signMessage({ + message: txId, + }); + request.witnesses = [signature]; + await fakePredicate.provider.estimatePredicates(request); + const predicateInput = request.inputs[0]; + if (predicateInput && 'predicate' in predicateInput) { + return bn(predicateInput.predicateGasUsed); + } + return bn(); + }); + getEVMAddress(address: string, evmAccounts: Array = []) { return evmAccounts.find( (account) => this.getPredicateAddress(account) === address,