From 7114f62d1aa525a664096d198bc3c9aaf50edac6 Mon Sep 17 00:00:00 2001 From: Saad Ahmed <48211799+saadjhk@users.noreply.github.com> Date: Mon, 22 Apr 2024 13:54:07 +0500 Subject: [PATCH] feat: display transfer gas cost estimation (#174) ## Description Display gas estimation of transactions that will be executed ## Related Issue Or Context Closes: #157 ## How Has This Been Tested? Testing details. - [x] Mocked gas fee test ## Types of changes - [x] Transactions gas fee estimations for evm and substrate networks ## Checklist: - [x] Add tests. --- .../fungible/fungible-token-transfer.ts | 2 + .../fungible/transfer-detail/styles.ts | 6 ++ .../transfer-detail/transfer-detail.ts | 64 +++++++++++++++++-- .../src/controllers/transfers/evm/build.ts | 2 + .../src/controllers/transfers/evm/execute.ts | 1 + .../controllers/transfers/evm/gas-estimate.ts | 31 +++++++++ .../transfers/fungible-token-transfer.ts | 61 +++++++++++++++++- .../controllers/transfers/substrate/build.ts | 3 + .../transfer-detail/transfer-detail.test.ts | 26 +++++++- 9 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 packages/widget/src/controllers/transfers/evm/gas-estimate.ts diff --git a/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts b/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts index 9f1c097e..cd2f8982 100644 --- a/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts +++ b/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts @@ -15,6 +15,7 @@ import '../../common/buttons/button'; import '../../address-input'; import '../../resource-amount-selector'; import './transfer-button'; +import './transfer-detail'; import './transfer-status'; import '../../network-selector'; import { BaseComponent } from '../../common'; @@ -171,6 +172,7 @@ export class FungibleTokenTransfer extends BaseComponent { this.renderAmountOnDestination() )} ; - getFeeParams(type: FeeHandlerType): { decimals?: number; symbol: string } { + @property({ type: Object }) + estimatedGasFee?: BigNumber; + + getSygmaFeeParams(type: FeeHandlerType): { + decimals?: number; + symbol: string; + } { let decimals = undefined; let symbol = ''; @@ -49,28 +57,70 @@ export class FungibleTransferDetail extends BaseComponent { } } - getFee(): string { + getGasFeeParams(): { + decimals?: number; + symbol: string; + } { + let decimals = undefined; + let symbol = ''; + if (this.sourceDomainConfig) { + decimals = Number(this.sourceDomainConfig.nativeTokenDecimals); + symbol = this.sourceDomainConfig.nativeTokenSymbol.toUpperCase(); + } + return { decimals, symbol }; + } + + getSygmaFee(): string { if (!this.fee) return ''; - const { symbol, decimals } = this.getFeeParams(this.fee.type); + const { symbol, decimals } = this.getSygmaFeeParams(this.fee.type); const { fee } = this.fee; let _fee = ''; if (decimals) { - _fee = tokenBalanceToNumber(fee, decimals, 4); + // * BigNumber.from(fee.toString()) from + // * substrate gas + // * hex doesn't start with 0x :shrug: + _fee = tokenBalanceToNumber(BigNumber.from(fee.toString()), decimals, 4); } return `${_fee} ${symbol}`; } + getEstimatedGasFee(): string { + if (!this.estimatedGasFee) return ''; + const { symbol, decimals } = this.getGasFeeParams(); + + if (decimals && this.estimatedGasFee) { + const gasFee = tokenBalanceToNumber(this.estimatedGasFee, decimals, 4); + return `${gasFee} ${symbol}`; + } + + return 'calculating...'; + } + render(): HTMLTemplateResult { return html`
${when( - this.fee !== undefined, + this.fee !== null, () => html`
Bridge Fee
-
${this.getFee()}
+
+ ${this.getSygmaFee()} +
+
` + )} + ${when( + this.estimatedGasFee !== undefined, + () => + html`
+
+ Gas Fee +
+
+ ${this.getEstimatedGasFee()} +
` )}
diff --git a/packages/widget/src/controllers/transfers/evm/build.ts b/packages/widget/src/controllers/transfers/evm/build.ts index a85c375d..f4150ce0 100644 --- a/packages/widget/src/controllers/transfers/evm/build.ts +++ b/packages/widget/src/controllers/transfers/evm/build.ts @@ -28,6 +28,7 @@ export async function buildEvmFungibleTransactions( !address || providerChaiId !== this.sourceNetwork.chainId ) { + this.estimatedGas = undefined; this.resetFee(); return; } @@ -83,5 +84,6 @@ export async function buildEvmFungibleTransactions( transfer, this.fee ); + await this.estimateGas(); this.host.requestUpdate(); } diff --git a/packages/widget/src/controllers/transfers/evm/execute.ts b/packages/widget/src/controllers/transfers/evm/execute.ts index a759f206..64131ec5 100644 --- a/packages/widget/src/controllers/transfers/evm/execute.ts +++ b/packages/widget/src/controllers/transfers/evm/execute.ts @@ -28,6 +28,7 @@ export async function executeNextEvmTransaction( this.host.requestUpdate(); await tx.wait(); this.pendingEvmApprovalTransactions.shift(); + await this.estimateGas(); } catch (e) { console.log(e); this.errorMessage = 'Approval transaction reverted or rejected'; diff --git a/packages/widget/src/controllers/transfers/evm/gas-estimate.ts b/packages/widget/src/controllers/transfers/evm/gas-estimate.ts new file mode 100644 index 00000000..53625deb --- /dev/null +++ b/packages/widget/src/controllers/transfers/evm/gas-estimate.ts @@ -0,0 +1,31 @@ +import { Web3Provider } from '@ethersproject/providers'; +import type { EIP1193Provider } from '@web3-onboard/core'; +import { ethers, type BigNumber, type PopulatedTransaction } from 'ethers'; + +/** + * This method calculate the amount of gas + * list of transactions will cost + * @param {number} chainId blockchain ID + * @param {Eip1193Provider} eip1193Provider EIP compatible provider + * @param {string} sender address of signer connected with provider + * @param {PopulatedTransaction[]} transactions list of EVM transactions + * @returns {Promise} gas cost in 18 decimals // or chain native decimals + */ +export async function estimateEvmTransactionsGasCost( + chainId: number, + eip1193Provider: EIP1193Provider, + sender: string, + transactions: PopulatedTransaction[] +): Promise { + const provider = new Web3Provider(eip1193Provider, chainId); + const signer = provider.getSigner(sender); + + let cost = ethers.constants.Zero; + for (const transaction of transactions) { + const _cost = await signer.estimateGas(transaction); + cost = cost.add(_cost); + } + + const gasPrice = await provider.getGasPrice(); + return gasPrice.mul(cost); +} diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index d9ac38c2..0b30c19c 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -13,8 +13,8 @@ import { getRoutes } from '@buildwithsygma/sygma-sdk-core'; import { ContextConsumer } from '@lit/context'; -import type { UnsignedTransaction, BigNumber } from 'ethers'; -import { ethers } from 'ethers'; +import { BigNumber, ethers } from 'ethers'; +import type { UnsignedTransaction, PopulatedTransaction } from 'ethers'; import type { ReactiveController, ReactiveElement } from 'lit'; import type { SubmittableExtrinsic } from '@polkadot/api/types'; import type { ApiPromise, SubmittableResult } from '@polkadot/api'; @@ -33,6 +33,7 @@ import { buildSubstrateFungibleTransactions, executeNextSubstrateTransaction } from './substrate'; +import { estimateEvmTransactionsGasCost } from './evm/gas-estimate'; export type SubstrateTransaction = SubmittableExtrinsic< 'promise', @@ -71,6 +72,7 @@ export class FungibleTokenTransferController implements ReactiveController { public supportedDestinationNetworks: Domain[] = []; public supportedResources: Resource[] = []; public fee: EvmFee | SubstrateFee | null = null; + public estimatedGas: BigNumber | undefined; //Evm transfer protected buildEvmTransactions = buildEvmFungibleTransactions; @@ -208,6 +210,7 @@ export class FungibleTokenTransferController implements ReactiveController { this.waitingTxExecution = false; this.waitingUserConfirmation = false; this.transferTransactionId = undefined; + this.estimatedGas = undefined; this.resetFee(); void this.init(this.env); } @@ -440,4 +443,58 @@ export class FungibleTokenTransferController implements ReactiveController { throw new Error('Unsupported network type'); } } + + public async estimateGas(): Promise { + if (!this.sourceNetwork) return; + switch (this.sourceNetwork.type) { + case Network.EVM: + await this.estimateEvmGas(); + break; + case Network.SUBSTRATE: + await this.estimateSubstrateGas(); + break; + } + } + + private async estimateSubstrateGas(): Promise { + if (!this.walletContext.value?.substrateWallet?.signerAddress) return; + const sender = this.walletContext.value?.substrateWallet?.signerAddress; + + const paymentInfo = await ( + this.pendingTransferTransaction as SubstrateTransaction + ).paymentInfo(sender); + + const { partialFee } = paymentInfo; + this.estimatedGas = BigNumber.from(partialFee.toString()); + } + + private async estimateEvmGas(): Promise { + if ( + !this.sourceNetwork?.chainId || + !this.walletContext.value?.evmWallet?.provider || + !this.walletContext.value.evmWallet.address + ) + return; + + const state = this.getTransferState(); + const transactions = []; + + switch (state) { + case FungibleTransferState.PENDING_APPROVALS: + transactions.push(...this.pendingEvmApprovalTransactions); + break; + case FungibleTransferState.PENDING_TRANSFER: + transactions.push(this.pendingTransferTransaction); + break; + } + + const estimatedGas = await estimateEvmTransactionsGasCost( + this.sourceNetwork?.chainId, + this.walletContext.value?.evmWallet?.provider, + this.walletContext.value.evmWallet.address, + transactions as PopulatedTransaction[] + ); + + this.estimatedGas = estimatedGas; + } } diff --git a/packages/widget/src/controllers/transfers/substrate/build.ts b/packages/widget/src/controllers/transfers/substrate/build.ts index ab9427f1..38a36031 100644 --- a/packages/widget/src/controllers/transfers/substrate/build.ts +++ b/packages/widget/src/controllers/transfers/substrate/build.ts @@ -16,6 +16,8 @@ export async function buildSubstrateFungibleTransactions( !substrateProvider || !address ) { + this.estimatedGas = undefined; + this.resetFee(); return; } @@ -37,5 +39,6 @@ export async function buildSubstrateFungibleTransactions( transfer, this.fee ); + await this.estimateGas(); this.host.requestUpdate(); } diff --git a/packages/widget/tests/unit/components/transfer-detail/transfer-detail.test.ts b/packages/widget/tests/unit/components/transfer-detail/transfer-detail.test.ts index be01d1f7..08fce4d2 100644 --- a/packages/widget/tests/unit/components/transfer-detail/transfer-detail.test.ts +++ b/packages/widget/tests/unit/components/transfer-detail/transfer-detail.test.ts @@ -11,8 +11,9 @@ import { Network, ResourceType } from '@buildwithsygma/sygma-sdk-core'; +import type { BigNumber } from 'ethers'; import { constants } from 'ethers'; -import { parseUnits } from 'ethers/lib/utils'; +import { parseEther, parseUnits } from 'ethers/lib/utils'; import { FungibleTransferDetail } from '../../../../src/components/transfer/fungible/transfer-detail'; describe('sygma-fungible-transfer-detail', function () { @@ -42,6 +43,8 @@ describe('sygma-fungible-transfer-detail', function () { resources: [] }; + const mockedEstimatedGas: BigNumber = parseEther('0.0004'); + afterEach(() => { fixtureCleanup(); }); @@ -67,7 +70,7 @@ describe('sygma-fungible-transfer-detail', function () { assert.isNotNull(transferDetail); }); - it('shows fee correctly', async () => { + it('shows sygma fee correctly', async () => { const value = '1.02 ETH'; mockedFee.fee = parseUnits('1.02', 18); @@ -85,4 +88,23 @@ describe('sygma-fungible-transfer-detail', function () { assert.include(transferDetail.innerHTML, value); }); + + it('shows gas fee correctly', async () => { + const MOCKED_GAS = '0.0004 ETH'; + + const el = await fixture(html` + + `); + + const valueElements = el.shadowRoot!.querySelector( + '#gasFeeValue' + ) as HTMLElement; + + assert.equal(valueElements.textContent?.trim(), MOCKED_GAS); + }); });