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

Eip 4844 #1698

Merged
merged 12 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 11 additions & 19 deletions app/tray/Account/Requests/TransactionRequest/TxFee/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React from 'react'
import Restore from 'react-restore'
import BigNumber from 'bignumber.js'
import { utils } from 'ethers'

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 { chainUsesOptimismFees } from '../../../../../../resources/utils/chains'
import link from '../../../../../../resources/link'
import {
ClusterBox,
Expand Down Expand Up @@ -66,20 +65,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 @@ -100,8 +87,9 @@ 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,
isTestnet
Expand All @@ -114,7 +102,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 @@ -137,7 +125,11 @@ class TxFee extends React.Component {
<ClusterColumn grow={2}>
<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>
<ClusterValue>
Expand Down
61 changes: 50 additions & 11 deletions main/accounts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { ActionType } from '../transaction/actions'
import { openBlockExplorer } from '../windows/window'
import { ApprovalType } from '../../resources/constants'
import { accountNS } from '../../resources/domain/account'
import { chainUsesOptimismFees } from '../../resources/utils/chains'

function notify(title: string, body: string, action: (event: Electron.Event) => void) {
const notification = new Notification({ title, body })
Expand All @@ -41,6 +42,31 @@ function notify(title: string, body: string, action: (event: Electron.Event) =>
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 All @@ -65,6 +91,8 @@ export {
AddTokenRequest
} from './types'

type RequestWithId = [string, TransactionRequest]

export class Accounts extends EventEmitter {
_current: string
accounts: Record<string, FrameAccount>
Expand Down Expand Up @@ -532,18 +560,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 @@ -561,6 +580,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
113 changes: 113 additions & 0 deletions main/chains/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,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: chainConfig } = require('./config')
const { default: GasMonitor } = require('../transaction/gasMonitor')
const { createGasCalculator } = require('./gas')
const { NETWORK_PRESETS } = require('../../resources/constants')
const { chainUsesOptimismFees } = require('../../resources/utils/chains')

// These chain IDs are known to not support EIP-1559 and will be forced
// not to use that mechanism
Expand All @@ -28,6 +32,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 @@ -81,6 +96,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: '0x13e1e16b2a10c9',
data: '0x049639fb0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000042000000000000000000000000000000000000420000000000000000000000000000000000000000000000000013e1e16b2a10c900000000000000000000000000000000000000000000000045575639011cb45400000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000038812aa3caf000000000000000000000000b63aae6c353636d66df13b89ba4425cfe13d10ba00000000000000000000000042000000000000000000000000000000000000060000000000000000000000004200000000000000000000000000000000000042000000000000000000000000b63aae6c353636d66df13b89ba4425cfe13d10ba000000000000000000000000a7ca2c8673bcfa5a26d8ceec2887f2cc2b0db22a0000000000000000000000000000000000000000000000000013e1e16b2a10c900000000000000000000000000000000000000000000000045575639011cb455000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001f50000000000000000000000000000000000000000000000000000000001d700a007e5c0d20000000000000000000000000000000000000000000000000001b30001505126a132dab612db5cb9fc9ac426a0cc215a3423f9c942000000000000000000000000000000000000060004f41766d8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011edea400000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000b63aae6c353636d66df13b89ba4425cfe13d10ba0000000000000000000000000000000000000000000000000000000065e75a70000000000000000000000000000000000000000000000000000000000000000100000000000000000000000042000000000000000000000000000000000000060000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607000000000000000000000000000000000000000000000000000000000000000102a000000000000000000000000000000000000000000000000045575639011cb455ee63c1e5801d751bc1a723accf1942122ca9aa82d49d08d2ae7f5c764cbc14f9669b88837ca1490cca17c316071111111254eeb25477b68fb85ed929f73a96058200000000000000000000000bd34b36000000000000000000000000000000000000000000000000',
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 @@ -127,6 +227,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
29 changes: 27 additions & 2 deletions main/provider/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { v4 as uuid } from 'uuid'
import EventEmitter from 'events'
import crypto from 'crypto'
import log from 'electron-log'
import { v4 as uuid } from 'uuid'
import { Web3Provider } from '@ethersproject/providers'
import { BigNumber } from 'ethers'
import { estimateL1GasCost } from '@eth-optimism/sdk'
import { recoverTypedSignature, SignTypedDataVersion } from '@metamask/eth-sig-util'
import { isAddress } from '@ethersproject/address'
import crypto from 'crypto'
import { addHexPrefix, intToHex, isHexString, isHexPrefixed, fromUtf8 } from '@ethereumjs/util'

import store from '../store'
Expand Down Expand Up @@ -299,6 +302,27 @@ export class Provider extends EventEmitter {
})
}

async getL1GasCost(txData: TransactionData) {
const { chainId, type, ...tx } = txData

const txRequest = {
...tx,
type: parseInt(type, 16),
chainId: parseInt(chainId, 16)
}

const connection = this.connection.connections['ethereum'][txRequest.chainId] as any
const connectedProvider = connection?.primary?.connected
? connection.primary?.provider
: connection.secondary?.provider

if (!connectedProvider) {
return BigNumber.from(0)
}

return estimateL1GasCost(new Web3Provider(connectedProvider), txRequest)
}

signAndSend(req: TransactionRequest, cb: Callback<string>) {
const rawTx = req.data
const res = (data: any) => {
Expand Down Expand Up @@ -413,6 +437,7 @@ export class Provider extends EventEmitter {
jsonrpc: '2.0',
id: 1
}

const targetChain: Chain = {
type: 'ethereum',
id: parseInt(rawTx.chainId, 16)
Expand Down
5 changes: 5 additions & 0 deletions main/store/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,11 @@ module.exports = {
u('main.networksMeta', netType, netId, 'gas.price.lastLevel', () => level)
}
},
addSampleGasCosts: (u, netType, netId, samples) => {
u('main.networksMeta', netType, netId, 'gas.samples', () => {
return samples
})
},
setNativeCurrencyData: (u, netType, netId, currency) => {
u('main.networksMeta', netType, netId, 'nativeCurrency', (existing) => ({ ...existing, ...currency }))
},
Expand Down
Loading
Loading