Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: display transfer gas cost estimation #174

Merged
merged 8 commits into from
Apr 22, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -171,6 +172,7 @@ export class FungibleTokenTransfer extends BaseComponent {
this.renderAmountOnDestination()
)}
<sygma-fungible-transfer-detail
.estimatedGasFee=${this.transferController.estimatedGas}
.selectedResource=${this.transferController.selectedResource}
.fee=${this.transferController.fee}
.sourceDomainConfig=${this.transferController.sourceDomainConfig}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { css } from 'lit';

export const styles = css`
.transferDetail {
gap: 0.5rem;
display: flex;
flex-direction: column;
}

.transferDetailContainer {
display: flex;
font-size: 0.75rem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { HTMLTemplateResult } from 'lit';
import { html } from 'lit';
import { when } from 'lit/directives/when.js';
import { customElement, property } from 'lit/decorators.js';
import { BigNumber } from 'ethers';
import { tokenBalanceToNumber } from '../../../../utils/token';
import { BaseComponent } from '../../../common/base-component';
import { styles } from './styles';
Expand All @@ -27,7 +28,13 @@ export class FungibleTransferDetail extends BaseComponent {
@property({ type: Object })
sourceDomainConfig?: BaseConfig<Network>;

getFeeParams(type: FeeHandlerType): { decimals?: number; symbol: string } {
@property({ type: Object })
estimatedGasFee?: BigNumber;

getSygmaFeeParams(type: FeeHandlerType): {
decimals?: number;
symbol: string;
} {
let decimals = undefined;
let symbol = '';

Expand All @@ -49,19 +56,47 @@ export class FungibleTransferDetail extends BaseComponent {
}
}

getFee(): string {
getGasFeeParams(): {
decimals?: number;
symbol: string;
} {
let decimals = undefined;
wainola marked this conversation as resolved.
Show resolved Hide resolved
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`
<section class="transferDetail">
Expand All @@ -70,7 +105,21 @@ export class FungibleTransferDetail extends BaseComponent {
() =>
html`<div class="transferDetailContainer">
<div class="transferDetailContainerLabel">Bridge Fee</div>
<div class="transferDetailContainerValue">${this.getFee()}</div>
<div class="transferDetailContainerValue">
${this.getSygmaFee()}
</div>
</div>`
)}
${when(
this.estimatedGasFee !== undefined,
() =>
html`<div class="transferDetailContainer">
<div class="transferDetailContainerLabel" id="gasFeeLabel">
Gas Fee
</div>
<div class="transferDetailContainerValue" id="gasFeeValue">
${this.getEstimatedGasFee()}
</div>
</div>`
)}
</section>
Expand Down
2 changes: 2 additions & 0 deletions packages/widget/src/controllers/transfers/evm/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export async function buildEvmFungibleTransactions(
!address ||
providerChaiId !== this.sourceNetwork.chainId
) {
this.estimatedGas = undefined;
this.resetFee();
return;
}
Expand Down Expand Up @@ -83,5 +84,6 @@ export async function buildEvmFungibleTransactions(
transfer,
this.fee
);
await this.estimateGas();
this.host.requestUpdate();
}
1 change: 1 addition & 0 deletions packages/widget/src/controllers/transfers/evm/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
22 changes: 22 additions & 0 deletions packages/widget/src/controllers/transfers/evm/gas-estimate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Web3Provider } from '@ethersproject/providers';
import type { EIP1193Provider } from '@web3-onboard/core';
import { ethers, type BigNumber, type PopulatedTransaction } from 'ethers';

export async function estimateEvmTransactionsGasCost(
chainId: number,
eip1193Provider: EIP1193Provider,
sender: string,
transactions: PopulatedTransaction[]
): Promise<BigNumber> {
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);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of logic in this file without tests :S

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be resolved in separe PR. Related issue #177

Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -33,6 +33,7 @@ import {
buildSubstrateFungibleTransactions,
executeNextSubstrateTransaction
} from './substrate';
import { estimateEvmTransactionsGasCost } from './evm/gas-estimate';

export type SubstrateTransaction = SubmittableExtrinsic<
'promise',
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -440,4 +443,58 @@ export class FungibleTokenTransferController implements ReactiveController {
throw new Error('Unsupported network type');
}
}

public async estimateGas(): Promise<void> {
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<void> {
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<void> {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export async function buildSubstrateFungibleTransactions(
!substrateProvider ||
!address
) {
this.estimatedGas = undefined;
this.resetFee();
return;
}

Expand All @@ -37,5 +39,6 @@ export async function buildSubstrateFungibleTransactions(
transfer,
this.fee
);
await this.estimateGas();
this.host.requestUpdate();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -42,6 +43,8 @@ describe('sygma-fungible-transfer-detail', function () {
resources: []
};

const mockedEstimatedGas: BigNumber = parseEther('0.0004');

afterEach(() => {
fixtureCleanup();
});
Expand All @@ -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);

Expand All @@ -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`
<sygma-fungible-transfer-detail
.fee=${mockedFee}
.selectedResource=${mockedResource}
.sourceDomainConfig=${mockedSourceDomainConfig}
.estimatedGasFee=${mockedEstimatedGas}
></sygma-fungible-transfer-detail>
`);

const valueElements = el.shadowRoot!.querySelector(
'#gasFeeValue'
) as HTMLElement;

assert.equal(valueElements.textContent?.trim(), MOCKED_GAS);
});
});
Loading