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

enhancement: add gas price polling during send flow #609

Merged
merged 9 commits into from
Sep 6, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@
import { SendFlowRoute } from './send-flow-route.enum'
import { sendFlowRoute } from './send-flow.router'
import { InputTokenAmountView, SelectRecipientView, SelectTokenView, TransactionSummaryView } from './views'
import { onDestroy } from 'svelte'
import { stopPollingEvmChainGasPrices } from '@core/layer-2/actions'

export let onTransactionSummaryMount: (..._: any[]) => Promise<void> = async () => {}

$: if (features.analytics.dashboardRoute.wallet.sendFlow.enabled && $sendFlowRoute) {
Platform.trackEvent('send-flow-route', { route: $sendFlowRoute })
}

onDestroy(() => {
stopPollingEvmChainGasPrices()
})
</script>

<send-flow-router>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
import { selectedAccountIndex } from '@core/account/stores'
import { ContactManager } from '@core/contact/classes'
import { localize } from '@core/i18n'
import { IChain, IIscpChainConfiguration, INetwork, NetworkId, getActiveNetworkId, network } from '@core/network'
import {
IChain,
IIscpChainConfiguration,
INetwork,
NetworkId,
getActiveNetworkId,
network,
isEvmChain,
} from '@core/network'
import { visibleActiveAccounts } from '@core/profile/stores'
import {
SendFlowType,
Expand All @@ -16,24 +24,40 @@
import { closePopup } from '@desktop/auxiliary/popup'
import features from '@features/features'
import { INetworkRecipientSelectorOption, NetworkRecipientSelector } from '@ui'
import { onMount } from 'svelte'
import { onDestroy, onMount } from 'svelte'
import { sendFlowRouter } from '../send-flow.router'
import SendFlowTemplate from './SendFlowTemplate.svelte'
import { getTokenStandardFromSendFlowParameters } from '@core/wallet/utils'
import { TokenStandard } from '@core/token'
import { canAccountMakeEvmTransaction } from 'shared/src/lib/core/layer-2/actions'
import { handleError } from 'shared/src/lib/core/error/handlers'
import {
canAccountMakeEvmTransaction,
pollEvmChainGasPrices,
stopPollingEvmChainGasPrices,
} from '@core/layer-2/actions'

let selector: NetworkRecipientSelector
let selectorOptions: INetworkRecipientSelectorOption[] = []
let selectedIndex = -1

let hasNetworkRecipientError: boolean = false

const assetName = getAssetName()

$: selectedRecipient = selectorOptions[selectedIndex]?.selectedRecipient
let selectedNetworkId: NetworkId
$: selectedNetworkId = selectorOptions[selectedIndex]?.networkId
$: selectedRecipient = selectorOptions[selectedIndex]?.selectedRecipient

let hasNetworkRecipientError: boolean = false
$: {
const originNetworkId = getNetworkIdFromSendFlowParameters($sendFlowParameters)
if (isEvmChain(originNetworkId)) {
hasNetworkRecipientError = !canAccountMakeEvmTransaction(
$selectedAccountIndex,
originNetworkId,
$sendFlowParameters?.type
)
} else {
hasNetworkRecipientError = false
}
}

function getAssetName(): string | undefined {
if ($sendFlowParameters?.type === SendFlowType.BaseCoinTransfer) {
Expand Down Expand Up @@ -158,7 +182,7 @@
} else if (sourceChain) {
// if we are on layer 2
networkRecipientOptions = [
...(features.wallet.assets.unwrapToken && [getLayer1RecipientOption($network)]),
...(features.wallet.assets.unwrapToken.enabled && [getLayer1RecipientOption($network)]),
getRecipientOptionFromChain(sourceChain, $selectedAccountIndex),
]
}
Expand All @@ -173,18 +197,12 @@
return networkRecipientOptions
}

async function onNetworkClick(): Promise<void> {
try {
const originNetworkId = getNetworkIdFromSendFlowParameters($sendFlowParameters)
hasNetworkRecipientError =
(await canAccountMakeEvmTransaction(
$selectedAccountIndex,
originNetworkId,
$sendFlowParameters.type
)) ?? false
} catch (err) {
handleError(err)
}
function startPollingEvmChainGasPrices(): void {
const activeNetworkId = getActiveNetworkId()
const networkIdsToPoll = selectorOptions
.filter((option) => option.networkId !== activeNetworkId)
.map((option) => option.networkId)
pollEvmChainGasPrices(networkIdsToPoll)
}

function onContinueClick(): void {
Expand Down Expand Up @@ -220,6 +238,11 @@
}
onMount(() => {
buildNetworkRecipientOptions()
startPollingEvmChainGasPrices()
})
onDestroy(() => {
const chainsToIgnore = isEvmChain(selectedNetworkId) ? [selectedNetworkId] : []
stopPollingEvmChainGasPrices(chainsToIgnore)
})
</script>

Expand All @@ -238,7 +261,6 @@
}}
>
<NetworkRecipientSelector
onNetworkSelected={onNetworkClick}
hasError={hasNetworkRecipientError}
bind:this={selector}
bind:options={selectorOptions}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@
import { selectedAccountIndex } from '@core/account/stores'
import { handleError } from '@core/error/handlers'
import { localize } from '@core/i18n'
import { canAccountMakeEvmTransaction } from '@core/layer-2/actions'
import { canAccountMakeEvmTransaction, pollEvmChainGasPrice } from '@core/layer-2/actions'
import { marketCoinPrices } from '@core/market/stores'
import { getNetwork } from '@core/network'
import { AccountTokens, BASE_TOKEN_ID, IToken, ITokenWithBalance, TokenStandard } from '@core/token'
import { NetworkId, getNetwork, isEvmChain } from '@core/network'
import {
AccountTokens,
BASE_TOKEN_ID,
IAccountTokensPerNetwork,
IToken,
ITokenWithBalance,
TokenStandard,
} from '@core/token'
import { getAccountTokensForSelectedAccount, getTokenBalance } from '@core/token/actions'
import { selectedAccountTokens } from '@core/token/stores'
import { SendFlowType, sendFlowParameters, setSendFlowParameters } from '@core/wallet'
Expand All @@ -27,12 +34,29 @@
let accountTokens: AccountTokens
$: accountTokens = getAccountTokensForSelectedAccount($marketCoinPrices)
$: accountTokens, searchValue, setFilteredTokenList()

let hasTokenError: boolean = false
$: if (isEvmChain(selectedToken?.networkId)) {
hasTokenError = !canAccountMakeEvmTransaction(
$selectedAccountIndex,
selectedToken.networkId,
$sendFlowParameters?.type
)
} else {
hasTokenError = false
}

let tokenList: ITokenWithBalance[]
function getTokenList(): ITokenWithBalance[] {
const list = []
for (const tokensPerNetwork of Object.values(accountTokens)) {
for (const [networkId, tokensPerNetwork] of Object.entries(accountTokens) as [
NetworkId,
IAccountTokensPerNetwork,
][]) {
if (isEvmChain(networkId)) {
pollEvmChainGasPrice(networkId)
}

if (tokensPerNetwork?.baseCoin) {
list.push(tokensPerNetwork.baseCoin)
}
Expand Down Expand Up @@ -62,15 +86,9 @@
)
}

async function onTokenClick(token: ITokenWithBalance): Promise<void> {
function onTokenClick(token: ITokenWithBalance): void {
try {
selectedToken = token
hasTokenError =
(await canAccountMakeEvmTransaction(
$selectedAccountIndex,
token.networkId,
SendFlowType.BaseCoinTransfer
)) ?? false
} catch (err) {
handleError(err)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
<script lang="ts">
import { NetworkRecipientItem } from '@ui'
import { INetworkRecipientSelectorOption } from '../interfaces'
import { UiEventFunction } from '../../lib/core/utils'

export let options: INetworkRecipientSelectorOption[]
export let selectedIndex = -1
export let onNetworkSelected: UiEventFunction
export let hasError: boolean = false

const reipientItems: Record<number, NetworkRecipientItem> = {}
Expand All @@ -14,9 +12,8 @@
reipientItems[selectedIndex]?.validate()
}

function onItemClick(index: number) {
function onItemClick(index: number): void {
selectedIndex = index
onNetworkSelected && onNetworkSelected()
}
</script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ import { NetworkId } from '@core/network/types'
import { isEvmChain } from '@core/network/utils'
import { SendFlowType } from '@core/wallet/stores'

import { getLayer2AccountBalanceForToken } from '../stores'
import { getGasPriceInWei } from './getGasPriceInWei'
import { getEvmChainGasPrice, getLayer2AccountBalanceForToken } from '../stores'

import { FALLBACK_ESTIMATED_GAS, GAS_LIMIT_MULTIPLIER } from '../constants'
import { calculateGasFeeInGlow } from '../helpers'

export async function canAccountMakeEvmTransaction(
export function canAccountMakeEvmTransaction(
accountIndex: number,
networkId: NetworkId,
sendFlowType: SendFlowType
): Promise<boolean | undefined> {
): boolean | undefined {
if (!isEvmChain(networkId)) {
return undefined
}
Expand All @@ -21,7 +20,10 @@ export async function canAccountMakeEvmTransaction(
const gasLimit = Math.floor(
FALLBACK_ESTIMATED_GAS[sendFlowType ?? SendFlowType.BaseCoinTransfer] * GAS_LIMIT_MULTIPLIER
)
const gasPrice = await getGasPriceInWei(networkId)
const gasPrice = getEvmChainGasPrice(networkId)
if (gasPrice === undefined) {
return undefined
}
const minimumGasFee = calculateGasFeeInGlow(gasLimit, gasPrice)
return BigInt(baseTokenAccountBalance) < BigInt(minimumGasFee.toString())
return BigInt(baseTokenAccountBalance) > BigInt(minimumGasFee.toString())
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { SendFlowParameters } from '@core/wallet'
import { BigIntLike } from '@ethereumjs/util'
import { SendFlowParameters } from '@core/wallet/types'
import { GAS_LIMIT_MULTIPLIER } from '../constants'
import { calculateGasFeeInGlow } from '../helpers'
import { getEvmChainGasPrice } from '../stores'
import { estimateGasForLayer1ToLayer2Transaction } from './estimateGasForLayer1ToLayer2Transaction'
import { getGasPriceInWei } from './getGasPriceInWei'

export async function getGasFeesForLayer1ToLayer2Transaction(
sendFlowParameters: SendFlowParameters
Expand All @@ -12,7 +12,7 @@ export async function getGasFeesForLayer1ToLayer2Transaction(
if (sendFlowParameters.destinationNetworkId) {
const estimatedGas = await estimateGasForLayer1ToLayer2Transaction(sendFlowParameters)
const gasLimit = Math.floor(estimatedGas * GAS_LIMIT_MULTIPLIER)
const gasPrice = await getGasPriceInWei(sendFlowParameters.destinationNetworkId)
const gasPrice = getEvmChainGasPrice(sendFlowParameters.destinationNetworkId)
const estimatedGasFee = calculateGasFeeInGlow(estimatedGas, gasPrice)
const maxGasFee = calculateGasFeeInGlow(gasLimit, gasPrice)
return { estimatedGasFee, maxGasFee }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NetworkId } from '@core/network/types'
import { getNetwork } from '@core/network/stores'
import { Converter } from '@core/utils'

export async function getGasPriceInWei(networkId: NetworkId): Promise<string | undefined> {
export async function getGasPriceForNetwork(networkId: NetworkId): Promise<string | undefined> {
const chain = getNetwork()?.getChain(networkId)

const provider = chain?.getProvider()
Expand Down
4 changes: 3 additions & 1 deletion packages/shared/src/lib/core/layer-2/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ export * from './canAccountMakeEvmTransaction'
export * from './estimateGasForLayer1ToLayer2Transaction'
export * from './fetchSelectedAccountLayer2Balance'
export * from './generateAndStoreEvmAddressForAccount'
export * from './getGasPriceFromProvider'
export * from './getNetworkIdFromAddress'
export * from './getGasFeesForLayer1ToLayer2Transaction'
export * from './getGasPriceInWei'
export * from './getIscpTransferSmartContractData'
export * from './getLayer2MetadataForTransfer'
export * from './getLayer2NetworkFromAddress'
export * from './pollEvmChainGasPrices'
export * from './pollLayer2Tokens'
export * from './signEvmTransactionWithStronghold'
export * from './updateEvmChainGasPrice'
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { NetworkId } from '@core/network/types'
import { MILLISECONDS_PER_SECOND } from '@core/utils'
import { updateEvmChainGasPrice } from './updateEvmChainGasPrice'

const EVM_CHAIN_GAS_PRICE_POLLING_INTERVAL: number = 60 * MILLISECONDS_PER_SECOND

const pollIntervalMap: { [id in NetworkId]?: number } = {}

export function pollEvmChainGasPrice(networkId: NetworkId): void {
if (!networkId || isPollingEvmChainGasPrice(networkId)) {
return
}
void updateEvmChainGasPrice(networkId)
pollIntervalMap[networkId] = window.setInterval(() => {
void updateEvmChainGasPrice(networkId)
}, EVM_CHAIN_GAS_PRICE_POLLING_INTERVAL)
}

export function pollEvmChainGasPrices(networkIds: NetworkId[]): void {
stopPollingEvmChainGasPrices()
for (const networkId of networkIds) {
pollEvmChainGasPrice(networkId)
}
}

export function stopPollingEvmChainGasPrices(networkIdsToIgnore?: NetworkId[]): void {
for (const networkId of Object.keys(pollIntervalMap) as NetworkId[]) {
if (!networkIdsToIgnore?.includes(networkId)) {
if (isPollingEvmChainGasPrice(networkId)) {
clearInterval(pollIntervalMap[networkId])
}
}
}
}

export function isPollingEvmChainGasPrice(networkId: NetworkId): boolean {
return typeof pollIntervalMap[networkId] === 'number'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { handleError } from '@core/error/handlers'
import { NetworkId } from '@core/network/types'
import { setEvmChainGasPrice } from '../stores'
import { getGasPriceForNetwork } from './getGasPriceFromProvider'

export async function updateEvmChainGasPrice(chainId: NetworkId): Promise<void> {
try {
const gasPrice = await getGasPriceForNetwork(chainId)
if (gasPrice) {
setEvmChainGasPrice(BigInt(gasPrice), chainId)
}
} catch (err) {
handleError(err)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { get, writable } from 'svelte/store'
import { NetworkId } from '@core/network/types'
import { isEvmChain } from '@core/network/utils'
import { EvmChainGasPrices } from '../types'

export const evmChainGasPrices = writable<EvmChainGasPrices>({})

export function getEvmChainGasPrice(networkId: NetworkId): bigint | undefined {
return get(evmChainGasPrices)[networkId]
}

export function setEvmChainGasPrice(price: bigint, networkId: NetworkId): void {
evmChainGasPrices.update((state) => {
if (typeof price === 'bigint' && isEvmChain(networkId)) {
return {
...state,
[networkId]: price,
}
} else {
return state
}
})
}
1 change: 1 addition & 0 deletions packages/shared/src/lib/core/layer-2/stores/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './evm-chain-gas-prices.store'
export * from './layer2-balances.store'
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { NetworkId } from '@core/network/types'

export type EvmChainGasPrices = {
[id in NetworkId]?: bigint
}
Loading
Loading