Skip to content

Commit

Permalink
port production changes for op chains
Browse files Browse the repository at this point in the history
  • Loading branch information
mholtzman committed Mar 12, 2024
1 parent 697af3e commit b93b8e8
Show file tree
Hide file tree
Showing 29 changed files with 6,576 additions and 4,640 deletions.
31 changes: 11 additions & 20 deletions app/tray/Account/Requests/TransactionRequest/TxFee/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import React from 'react'
import Restore from 'react-restore'
import BigNumber from 'bignumber.js'
import { utils } from 'ethers'

import link from '../../../../../../resources/link'
import { DisplayCoinBalance, DisplayValue } from '../../../../../../resources/Components/DisplayValue'
import { GasFeesSource, usesBaseFee } from '../../../../../../resources/domain/transaction'
import { displayValueData } from '../../../../../../resources/utils/displayValue'
import { chainUsesOptimismFees, calculateOptimismL1DataFee } from '../../../../../../resources/utils/chains'
import link from '../../../../../../resources/link'
import { chainUsesOptimismFees } from '../../../../../../resources/utils/chains'
import { ClusterBox, Cluster, ClusterRow, ClusterValue } from '../../../../../../resources/Components/Cluster'

const FEE_WARNING_THRESHOLD_USD = 50
Expand Down Expand Up @@ -71,20 +70,8 @@ class TxFee extends React.Component {
super(props, context)
}

getOptimismFee = (l2Price, l2Limit, rawTx) => {
const { maxFeePerGas, maxPriorityFeePerGas, gasPrice, data, gasLimit, nonce, to, value } = rawTx
const chainId = parseInt(rawTx.chainId, 16)
const txData = { chainId, data, gasLimit, nonce, to, value }

const tx = !!maxFeePerGas
? { ...txData, maxFeePerGas, maxPriorityFeePerGas, type: 2 }
: { ...txData, gasPrice, type: 0 }

const serializedTransaction = utils.serializeTransaction(tx)

// Get current Ethereum gas price
const ethBaseFee = this.store('main.networksMeta.ethereum', 1, 'gas.price.fees.nextBaseFee')
const l1DataFee = calculateOptimismL1DataFee(serializedTransaction, ethBaseFee)
getOptimismFee = (l2Price, l2Limit, chainData) => {
const l1DataFee = BigNumber(chainData?.l1Fees).toNumber()

// Compute the L2 execution fee
const l2ExecutionFee = l2Price * l2Limit
Expand All @@ -105,7 +92,7 @@ class TxFee extends React.Component {
const maxGas = BigNumber(req.data.gasLimit, 16)
const maxFeePerGas = BigNumber(req.data[usesBaseFee(req.data) ? 'maxFeePerGas' : 'gasPrice'])
const maxFeeSourceValue = chainUsesOptimismFees(chain.id)
? this.getOptimismFee(maxFeePerGas, maxGas, req.data)
? this.getOptimismFee(maxFeePerGas, maxGas, req.chainData?.optimism)
: maxFeePerGas.multipliedBy(maxGas)
const maxFee = displayValueData(maxFeeSourceValue, {
currencyRate: nativeCurrency.usd,
Expand All @@ -119,7 +106,7 @@ class TxFee extends React.Component {
// accounts for the 50% padding in the gas estimate in the provider
const minGas = maxGas.dividedBy(BigNumber(1.5))
const minFeeSourceValue = chainUsesOptimismFees(chain.id)
? this.getOptimismFee(minFeePerGas, minGas, req.data)
? this.getOptimismFee(minFeePerGas, minGas, req.chainData?.optimism)
: minFeePerGas.multipliedBy(minGas)
const minFee = displayValueData(minFeeSourceValue, {
currencyRate: nativeCurrency.usd,
Expand All @@ -142,7 +129,11 @@ class TxFee extends React.Component {
</ClusterValue>
<ClusterValue>
<div className='txSendingValue'>
<DisplayCoinBalance amount={maxFee} symbol={nativeCurrency.symbol} />
{!maxFee.bn || maxFee.bn.isNaN() ? (
`? ${nativeCurrency.symbol}`
) : (
<DisplayCoinBalance amount={maxFee} symbol={nativeCurrency.symbol} />
)}
</div>
</ClusterValue>
</ClusterRow>
Expand Down
63 changes: 51 additions & 12 deletions main/accounts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,50 @@ import {
PermitSignatureRequest
} from './types'

import { ActionType } from '../transaction/actions'
import { openBlockExplorer } from '../windows/window'
import { ApprovalType } from '../../resources/constants'
import { accountNS } from '../../resources/domain/account'
import { getMaxTotalFee } from '../../resources/gas'
import { chainUsesOptimismFees } from '../../resources/utils/chains'

import type { Chain } from '../chains'
import type { ActionType } from '../transaction/actions'
import type { Account, AccountMetadata, Gas } from '../store/state'

type RequestWithId = [string, TransactionRequest]

function notify(title: string, body: string, action: (event: Electron.Event) => void) {
const notification = new Notification({ title, body })
notification.on('click', action)

setTimeout(() => notification.show(), 1000)
}

function toTransactionsByLayer(requests: Record<string, AccountRequest>, chainId?: number) {
return Object.entries(requests)
.filter(([_, req]) => req.type === 'transaction')
.reduce(
({ l1Transactions, l2Transactions }, [id, req]) => {
const txRequest = req as TransactionRequest
if (
!txRequest.locked &&
!txRequest.feesUpdatedByUser &&
txRequest.data.gasFeesSource === GasFeesSource.Frame &&
(!chainId || parseInt(txRequest.data.chainId, 16) === chainId)
) {
l1Transactions.push([id, txRequest])
}

if (chainUsesOptimismFees(parseInt(txRequest.data.chainId, 16))) {
l2Transactions.push([id, txRequest])
}

return { l1Transactions, l2Transactions }
},
{ l1Transactions: [] as RequestWithId[], l2Transactions: [] as RequestWithId[] }
)
}

const frameOriginId = uuidv5('frame-internal', uuidv5.DNS)

const storeApi = {
Expand Down Expand Up @@ -535,18 +563,9 @@ export class Accounts extends EventEmitter {

if (currentAccount) {
// If chainId, update pending tx requests from that chain, otherwise update all pending tx requests
const transactions = Object.entries(currentAccount.requests)
.filter(([_, req]) => req.type === 'transaction')
.map(([_, req]) => [_, req] as [string, TransactionRequest])
.filter(
([_, req]) =>
!req.locked &&
!req.feesUpdatedByUser &&
req.data.gasFeesSource === GasFeesSource.Frame &&
(!chainId || parseInt(req.data.chainId, 16) === chainId)
)
const { l1Transactions, l2Transactions } = toTransactionsByLayer(currentAccount.requests, chainId)

transactions.forEach(([id, req]) => {
l1Transactions.forEach(([id, req]) => {
try {
const tx = req.data
const chain = { type: 'ethereum', id: parseInt(tx.chainId, 16) }
Expand All @@ -570,6 +589,26 @@ export class Accounts extends EventEmitter {
log.error('Could not update gas fees for transaction', e)
}
})

if (chainId === 1) {
l2Transactions.forEach(async ([_id, req]) => {
let estimate = ''
try {
estimate = (await provider.getL1GasCost(req.data)).toHexString()
} catch (e) {
log.error('Error estimating L1 gas cost', e)
}

req.chainData = {
...req.chainData,
optimism: {
l1Fees: estimate
}
}

currentAccount.update()
})
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions main/accounts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ export interface TransactionRequest extends AccountRequest<'transaction'> {
payload: RPC.SendTransaction.Request
data: TransactionData
decodedData?: DecodedCallData
chainData?: {
optimism?: {
l1Fees: string
}
}
tx?: {
receipt?: TransactionReceipt
hash?: string
Expand Down
28 changes: 16 additions & 12 deletions main/chains/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class BlockMonitor extends EventEmitter {
}

start() {
log.verbose(`%cStarting block updates for chain ${parseInt(this.connection.chainId)}`, 'color: green')
log.verbose(`%cStarting block updates for chain ${this.chainId}`, 'color: green')

this.connection.on('message', this.handleMessage)

Expand All @@ -81,7 +81,7 @@ class BlockMonitor extends EventEmitter {
}

stop() {
log.verbose(`%cStopping block updates for chain ${parseInt(this.connection.chainId)}`, 'color: red')
log.verbose(`%cStopping block updates for chain ${this.chainId}`, 'color: red')

if (this.subscriptionId) {
this.clearSubscription()
Expand All @@ -92,6 +92,10 @@ class BlockMonitor extends EventEmitter {
}
}

get chainId() {
return parseInt(this.connection.chainId, 16)
}

private clearSubscription() {
this.connection.off('message', this.handleMessage)
this.subscriptionId = ''
Expand All @@ -106,7 +110,7 @@ class BlockMonitor extends EventEmitter {
this.connection
.send({ id: 1, jsonrpc: '2.0', method: 'eth_getBlockByNumber', params: ['latest', false] })
.then((block) => this.handleBlock(block))
.catch((err) => this.handleError(`Could not load block for chain ${this.connection.chainId}`, err))
.catch((err) => this.handleError(`Could not load block for chain ${this.chainId}`, err))
}

private handleMessage(message: SubscriptionMessage) {
Expand All @@ -115,16 +119,16 @@ class BlockMonitor extends EventEmitter {
}
}

private handleBlock(block: Block) {
log.debug(
`%cReceived block ${parseInt(block.number)} for chain ${parseInt(this.connection.chainId)}`,
'color: yellow',
{
latestBlock: parseInt(this.latestBlock)
}
)
private handleBlock(blockUpdate: unknown) {
if (!blockUpdate || typeof blockUpdate !== 'object') {
return this.handleError(`Received invalid block on chain ${this.chainId}`)
}

const block = blockUpdate as Block

if (!block) return this.handleError('handleBlock received undefined block')
log.debug(`%cReceived block ${parseInt(block.number)} for chain ${this.chainId}`, 'color: yellow', {
latestBlock: parseInt(this.latestBlock)
})

if (block.number !== this.latestBlock) {
this.latestBlock = block.number
Expand Down
113 changes: 113 additions & 0 deletions main/chains/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ const { powerMonitor } = require('electron')
const EventEmitter = require('events')
const { addHexPrefix } = require('@ethereumjs/util')
const { Hardfork } = require('@ethereumjs/common')
const { estimateL1GasCost } = require('@eth-optimism/sdk')
const { Web3Provider } = require('@ethersproject/providers')
const BigNumber = require('bignumber.js')
const provider = require('eth-provider')
const log = require('electron-log')

Expand All @@ -12,6 +15,7 @@ const { default: BlockMonitor } = require('./blocks')
const { default: chainConfig } = require('./config')
const { default: GasMonitor } = require('../transaction/gasMonitor')
const { createGasCalculator } = require('./gas')
const { chainUsesOptimismFees } = require('../../resources/utils/chains')
const { NETWORK_PRESETS } = require('../../resources/constants')

// These chain IDs are known to not support EIP-1559 and will be forced
Expand All @@ -29,6 +33,17 @@ const resError = (error, payload, res) =>
error: typeof error === 'string' ? { message: error, code: -1 } : error
})

function txEstimate(gasCost, nativeUSD) {
const usd = gasCost.shiftedBy(-18).multipliedBy(nativeUSD).toNumber()

return {
gasEstimate: addHexPrefix(gasCost.toString(16)),
cost: {
usd
}
}
}

class ChainConnection extends EventEmitter {
constructor(type, chainId) {
super()
Expand Down Expand Up @@ -82,6 +97,91 @@ class ChainConnection extends EventEmitter {
this.emit('connect')
}

async txEstimates(type, id, gasPrice, currentSymbol, provider) {
const sampleEstimates = [
{
label: `Send ${currentSymbol}`,
txExample: {
value: '0x8e1bc9bf04000',
data: '0x00',
gasLimit: addHexPrefix((21000).toString(16))
}
},
{
label: 'Send Tokens',
txExample: {
value: '0x00',
data: '0xa9059cbb000000000000000000000000c1af8ca40dfe1cb43b9c7a8c93df762c2d6ecfd90000000000000000000000000000000000000000000000008ac7230489e80000',
gasLimit: addHexPrefix((65000).toString(16))
}
},
{
label: 'Dex Swap',
txExample: {
value: '0x38d7ea4c68000',
data: '0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000065e7831900000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000038d7ea4c680000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000038d7ea4c680000000000000000000000000000000000000000000000000000b683f16dd057b6400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002b42000000000000000000000000000000000000060001f44200000000000000000000000000000000000042000000000000000000000000000000000000000000',
gasLimit: addHexPrefix((200000).toString(16))
}
}
]

const isTestnet = store('main.networks', type, id, 'isTestnet')
const nativeCurrency = store('main.networksMeta', type, id, 'nativeCurrency')
const nativeUSD = BigNumber(
nativeCurrency && nativeCurrency.usd && !isTestnet ? nativeCurrency.usd.price : 0
)

let estimates

if (chainUsesOptimismFees(id) && !isTestnet) {
estimates = await Promise.all(
sampleEstimates.map(async ({ label, txExample }) => {
const tx = {
...txExample,
type: 2,
chainId: id
}

try {
const l1GasCost = BigNumber((await estimateL1GasCost(provider, tx)).toHexString())
const l2GasCost = BigNumber(tx.gasLimit).multipliedBy(gasPrice)
const estimatedGas = l1GasCost.plus(l2GasCost)

return {
label,
gasCost: estimatedGas
}
} catch (e) {
return {
label,
gasCost: BigNumber('')
}
}
})
)
} else {
estimates = sampleEstimates.map(({ label, txExample }) => ({
label,
gasCost: BigNumber(txExample.gasLimit).multipliedBy(gasPrice)
}))
}

return estimates.map(({ label, gasCost }) => ({
estimates: {
low: txEstimate(gasCost, nativeUSD),
high: txEstimate(gasCost, nativeUSD)
},
label
}))
}

async feeEstimatesUSD(chainId, gasPrice, provider) {
const type = 'ethereum'
const currentSymbol = store('main.networksMeta', type, chainId, 'nativeCurrency', 'symbol') || 'ETH'

return this.txEstimates(type, chainId, gasPrice, currentSymbol, provider)
}

_createBlockMonitor(provider) {
const monitor = new BlockMonitor(provider)
const allowEip1559 = !legacyChains.includes(parseInt(this.chainId))
Expand Down Expand Up @@ -128,6 +228,19 @@ class ChainConnection extends EventEmitter {
})
}

if (provider.connected) {
const gasPrice = store('main.networksMeta', this.type, this.chainId, 'gas.price.levels.slow')
const estimatedGasPrice = feeMarket
? BigNumber(feeMarket.nextBaseFee).plus(BigNumber(feeMarket.maxPriorityFeePerGas))
: BigNumber(gasPrice)

this.feeEstimatesUSD(parseInt(this.chainId), estimatedGasPrice, new Web3Provider(provider)).then(
(samples) => {
store.addSampleGasCosts(this.type, this.chainId, samples)
}
)
}

store.setGasFees(this.type, this.chainId, feeMarket)
store.setBlockHeight(this.chainId, parseInt(block.number, 16))

Expand Down
Loading

0 comments on commit b93b8e8

Please sign in to comment.