Skip to content

Commit 60719f4

Browse files
authored
Eip 4844 (#1698)
* implement eip-4844 for op chains * use optimism calculator to calculate L1 fees * remove test * fix tx estimates * cleanup tx estimates * more cleanup * remove underscores * move formatting to UI * remove from addresses * remove all to addresses * handle errors * remove console logs, pin optimism dep
1 parent 00d0eed commit 60719f4

File tree

13 files changed

+935
-216
lines changed

13 files changed

+935
-216
lines changed

app/tray/Account/Requests/TransactionRequest/TxFee/index.js

+11-19
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import React from 'react'
22
import Restore from 'react-restore'
33
import BigNumber from 'bignumber.js'
4-
import { utils } from 'ethers'
54

65
import { DisplayCoinBalance, DisplayValue } from '../../../../../../resources/Components/DisplayValue'
76
import { GasFeesSource, usesBaseFee } from '../../../../../../resources/domain/transaction'
87
import { displayValueData } from '../../../../../../resources/utils/displayValue'
9-
import { chainUsesOptimismFees, calculateOptimismL1DataFee } from '../../../../../../resources/utils/chains'
8+
import { chainUsesOptimismFees } from '../../../../../../resources/utils/chains'
109
import link from '../../../../../../resources/link'
1110
import {
1211
ClusterBox,
@@ -66,20 +65,8 @@ class TxFee extends React.Component {
6665
super(props, context)
6766
}
6867

69-
getOptimismFee = (l2Price, l2Limit, rawTx) => {
70-
const { maxFeePerGas, maxPriorityFeePerGas, gasPrice, data, gasLimit, nonce, to, value } = rawTx
71-
const chainId = parseInt(rawTx.chainId, 16)
72-
const txData = { chainId, data, gasLimit, nonce, to, value }
73-
74-
const tx = !!maxFeePerGas
75-
? { ...txData, maxFeePerGas, maxPriorityFeePerGas, type: 2 }
76-
: { ...txData, gasPrice, type: 0 }
77-
78-
const serializedTransaction = utils.serializeTransaction(tx)
79-
80-
// Get current Ethereum gas price
81-
const ethBaseFee = this.store('main.networksMeta.ethereum', 1, 'gas.price.fees.nextBaseFee')
82-
const l1DataFee = calculateOptimismL1DataFee(serializedTransaction, ethBaseFee)
68+
getOptimismFee = (l2Price, l2Limit, chainData) => {
69+
const l1DataFee = BigNumber(chainData?.l1Fees).toNumber()
8370

8471
// Compute the L2 execution fee
8572
const l2ExecutionFee = l2Price * l2Limit
@@ -100,8 +87,9 @@ class TxFee extends React.Component {
10087
const maxGas = BigNumber(req.data.gasLimit, 16)
10188
const maxFeePerGas = BigNumber(req.data[usesBaseFee(req.data) ? 'maxFeePerGas' : 'gasPrice'])
10289
const maxFeeSourceValue = chainUsesOptimismFees(chain.id)
103-
? this.getOptimismFee(maxFeePerGas, maxGas, req.data)
90+
? this.getOptimismFee(maxFeePerGas, maxGas, req.chainData?.optimism)
10491
: maxFeePerGas.multipliedBy(maxGas)
92+
10593
const maxFee = displayValueData(maxFeeSourceValue, {
10694
currencyRate: nativeCurrency.usd,
10795
isTestnet
@@ -114,7 +102,7 @@ class TxFee extends React.Component {
114102
// accounts for the 50% padding in the gas estimate in the provider
115103
const minGas = maxGas.dividedBy(BigNumber(1.5))
116104
const minFeeSourceValue = chainUsesOptimismFees(chain.id)
117-
? this.getOptimismFee(minFeePerGas, minGas, req.data)
105+
? this.getOptimismFee(minFeePerGas, minGas, req.chainData?.optimism)
118106
: minFeePerGas.multipliedBy(minGas)
119107
const minFee = displayValueData(minFeeSourceValue, {
120108
currencyRate: nativeCurrency.usd,
@@ -137,7 +125,11 @@ class TxFee extends React.Component {
137125
<ClusterColumn grow={2}>
138126
<ClusterValue>
139127
<div className='txSendingValue'>
140-
<DisplayCoinBalance amount={maxFee} symbol={nativeCurrency.symbol} />
128+
{!maxFee.bn || maxFee.bn.isNaN() ? (
129+
`? ${nativeCurrency.symbol}`
130+
) : (
131+
<DisplayCoinBalance amount={maxFee} symbol={nativeCurrency.symbol} />
132+
)}
141133
</div>
142134
</ClusterValue>
143135
<ClusterValue>

main/accounts/index.ts

+50-11
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { ActionType } from '../transaction/actions'
3333
import { openBlockExplorer } from '../windows/window'
3434
import { ApprovalType } from '../../resources/constants'
3535
import { accountNS } from '../../resources/domain/account'
36+
import { chainUsesOptimismFees } from '../../resources/utils/chains'
3637

3738
function notify(title: string, body: string, action: (event: Electron.Event) => void) {
3839
const notification = new Notification({ title, body })
@@ -41,6 +42,31 @@ function notify(title: string, body: string, action: (event: Electron.Event) =>
4142
setTimeout(() => notification.show(), 1000)
4243
}
4344

45+
function toTransactionsByLayer(requests: Record<string, AccountRequest>, chainId?: number) {
46+
return Object.entries(requests)
47+
.filter(([_, req]) => req.type === 'transaction')
48+
.reduce(
49+
({ l1Transactions, l2Transactions }, [id, req]) => {
50+
const txRequest = req as TransactionRequest
51+
if (
52+
!txRequest.locked &&
53+
!txRequest.feesUpdatedByUser &&
54+
txRequest.data.gasFeesSource === GasFeesSource.Frame &&
55+
(!chainId || parseInt(txRequest.data.chainId, 16) === chainId)
56+
) {
57+
l1Transactions.push([id, txRequest])
58+
}
59+
60+
if (chainUsesOptimismFees(parseInt(txRequest.data.chainId, 16))) {
61+
l2Transactions.push([id, txRequest])
62+
}
63+
64+
return { l1Transactions, l2Transactions }
65+
},
66+
{ l1Transactions: [] as RequestWithId[], l2Transactions: [] as RequestWithId[] }
67+
)
68+
}
69+
4470
const frameOriginId = uuidv5('frame-internal', uuidv5.DNS)
4571

4672
const storeApi = {
@@ -65,6 +91,8 @@ export {
6591
AddTokenRequest
6692
} from './types'
6793

94+
type RequestWithId = [string, TransactionRequest]
95+
6896
export class Accounts extends EventEmitter {
6997
_current: string
7098
accounts: Record<string, FrameAccount>
@@ -532,18 +560,9 @@ export class Accounts extends EventEmitter {
532560

533561
if (currentAccount) {
534562
// If chainId, update pending tx requests from that chain, otherwise update all pending tx requests
535-
const transactions = Object.entries(currentAccount.requests)
536-
.filter(([_, req]) => req.type === 'transaction')
537-
.map(([_, req]) => [_, req] as [string, TransactionRequest])
538-
.filter(
539-
([_, req]) =>
540-
!req.locked &&
541-
!req.feesUpdatedByUser &&
542-
req.data.gasFeesSource === GasFeesSource.Frame &&
543-
(!chainId || parseInt(req.data.chainId, 16) === chainId)
544-
)
563+
const { l1Transactions, l2Transactions } = toTransactionsByLayer(currentAccount.requests, chainId)
545564

546-
transactions.forEach(([id, req]) => {
565+
l1Transactions.forEach(([id, req]) => {
547566
try {
548567
const tx = req.data
549568
const chain = { type: 'ethereum', id: parseInt(tx.chainId, 16) }
@@ -561,6 +580,26 @@ export class Accounts extends EventEmitter {
561580
log.error('Could not update gas fees for transaction', e)
562581
}
563582
})
583+
584+
if (chainId === 1) {
585+
l2Transactions.forEach(async ([_id, req]) => {
586+
let estimate = ''
587+
try {
588+
estimate = (await provider.getL1GasCost(req.data)).toHexString()
589+
} catch (e) {
590+
log.error('Error estimating L1 gas cost', e)
591+
}
592+
593+
req.chainData = {
594+
...req.chainData,
595+
optimism: {
596+
l1Fees: estimate
597+
}
598+
}
599+
600+
currentAccount.update()
601+
})
602+
}
564603
}
565604
}
566605

main/accounts/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ export interface TransactionRequest extends AccountRequest<'transaction'> {
101101
payload: RPC.SendTransaction.Request
102102
data: TransactionData
103103
decodedData?: DecodedCallData
104+
chainData?: {
105+
optimism?: {
106+
l1Fees: string
107+
}
108+
}
104109
tx?: {
105110
receipt?: TransactionReceipt
106111
hash?: string

main/chains/index.js

+113
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ const { powerMonitor } = require('electron')
33
const EventEmitter = require('events')
44
const { addHexPrefix } = require('@ethereumjs/util')
55
const { Hardfork } = require('@ethereumjs/common')
6+
const { estimateL1GasCost } = require('@eth-optimism/sdk')
7+
const { Web3Provider } = require('@ethersproject/providers')
8+
const BigNumber = require('bignumber.js')
69
const provider = require('eth-provider')
710
const log = require('electron-log')
811

@@ -12,6 +15,7 @@ const { default: chainConfig } = require('./config')
1215
const { default: GasMonitor } = require('../transaction/gasMonitor')
1316
const { createGasCalculator } = require('./gas')
1417
const { NETWORK_PRESETS } = require('../../resources/constants')
18+
const { chainUsesOptimismFees } = require('../../resources/utils/chains')
1519

1620
// These chain IDs are known to not support EIP-1559 and will be forced
1721
// not to use that mechanism
@@ -28,6 +32,17 @@ const resError = (error, payload, res) =>
2832
error: typeof error === 'string' ? { message: error, code: -1 } : error
2933
})
3034

35+
function txEstimate(gasCost, nativeUSD) {
36+
const usd = gasCost.shiftedBy(-18).multipliedBy(nativeUSD).toNumber()
37+
38+
return {
39+
gasEstimate: addHexPrefix(gasCost.toString(16)),
40+
cost: {
41+
usd
42+
}
43+
}
44+
}
45+
3146
class ChainConnection extends EventEmitter {
3247
constructor(type, chainId) {
3348
super()
@@ -81,6 +96,91 @@ class ChainConnection extends EventEmitter {
8196
this.emit('connect')
8297
}
8398

99+
async txEstimates(type, id, gasPrice, currentSymbol, provider) {
100+
const sampleEstimates = [
101+
{
102+
label: `Send ${currentSymbol}`,
103+
txExample: {
104+
value: '0x8e1bc9bf04000',
105+
data: '0x00',
106+
gasLimit: addHexPrefix((21000).toString(16))
107+
}
108+
},
109+
{
110+
label: 'Send Tokens',
111+
txExample: {
112+
value: '0x00',
113+
data: '0xa9059cbb000000000000000000000000c1af8ca40dfe1cb43b9c7a8c93df762c2d6ecfd90000000000000000000000000000000000000000000000008ac7230489e80000',
114+
gasLimit: addHexPrefix((65000).toString(16))
115+
}
116+
},
117+
{
118+
label: 'Dex Swap',
119+
txExample: {
120+
value: '0x13e1e16b2a10c9',
121+
data: '0x049639fb0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000042000000000000000000000000000000000000420000000000000000000000000000000000000000000000000013e1e16b2a10c900000000000000000000000000000000000000000000000045575639011cb45400000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000038812aa3caf000000000000000000000000b63aae6c353636d66df13b89ba4425cfe13d10ba00000000000000000000000042000000000000000000000000000000000000060000000000000000000000004200000000000000000000000000000000000042000000000000000000000000b63aae6c353636d66df13b89ba4425cfe13d10ba000000000000000000000000a7ca2c8673bcfa5a26d8ceec2887f2cc2b0db22a0000000000000000000000000000000000000000000000000013e1e16b2a10c900000000000000000000000000000000000000000000000045575639011cb455000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001f50000000000000000000000000000000000000000000000000000000001d700a007e5c0d20000000000000000000000000000000000000000000000000001b30001505126a132dab612db5cb9fc9ac426a0cc215a3423f9c942000000000000000000000000000000000000060004f41766d8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011edea400000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000b63aae6c353636d66df13b89ba4425cfe13d10ba0000000000000000000000000000000000000000000000000000000065e75a70000000000000000000000000000000000000000000000000000000000000000100000000000000000000000042000000000000000000000000000000000000060000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607000000000000000000000000000000000000000000000000000000000000000102a000000000000000000000000000000000000000000000000045575639011cb455ee63c1e5801d751bc1a723accf1942122ca9aa82d49d08d2ae7f5c764cbc14f9669b88837ca1490cca17c316071111111254eeb25477b68fb85ed929f73a96058200000000000000000000000bd34b36000000000000000000000000000000000000000000000000',
122+
gasLimit: addHexPrefix((200000).toString(16))
123+
}
124+
}
125+
]
126+
127+
const isTestnet = store('main.networks', type, id, 'isTestnet')
128+
const nativeCurrency = store('main.networksMeta', type, id, 'nativeCurrency')
129+
const nativeUSD = BigNumber(
130+
nativeCurrency && nativeCurrency.usd && !isTestnet ? nativeCurrency.usd.price : 0
131+
)
132+
133+
let estimates
134+
135+
if (chainUsesOptimismFees(id) && !isTestnet) {
136+
estimates = await Promise.all(
137+
sampleEstimates.map(async ({ label, txExample }) => {
138+
const tx = {
139+
...txExample,
140+
type: 2,
141+
chainId: id
142+
}
143+
144+
try {
145+
const l1GasCost = BigNumber((await estimateL1GasCost(provider, tx)).toHexString())
146+
const l2GasCost = BigNumber(tx.gasLimit).multipliedBy(gasPrice)
147+
const estimatedGas = l1GasCost.plus(l2GasCost)
148+
149+
return {
150+
label,
151+
gasCost: estimatedGas
152+
}
153+
} catch (e) {
154+
return {
155+
label,
156+
gasCost: BigNumber('')
157+
}
158+
}
159+
})
160+
)
161+
} else {
162+
estimates = sampleEstimates.map(({ label, txExample }) => ({
163+
label,
164+
gasCost: BigNumber(txExample.gasLimit).multipliedBy(gasPrice)
165+
}))
166+
}
167+
168+
return estimates.map(({ label, gasCost }) => ({
169+
estimates: {
170+
low: txEstimate(gasCost, nativeUSD),
171+
high: txEstimate(gasCost, nativeUSD)
172+
},
173+
label
174+
}))
175+
}
176+
177+
async feeEstimatesUSD(chainId, gasPrice, provider) {
178+
const type = 'ethereum'
179+
const currentSymbol = store('main.networksMeta', type, chainId, 'nativeCurrency', 'symbol') || 'ETH'
180+
181+
return this.txEstimates(type, chainId, gasPrice, currentSymbol, provider)
182+
}
183+
84184
_createBlockMonitor(provider) {
85185
const monitor = new BlockMonitor(provider)
86186
const allowEip1559 = !legacyChains.includes(parseInt(this.chainId))
@@ -127,6 +227,19 @@ class ChainConnection extends EventEmitter {
127227
})
128228
}
129229

230+
if (provider.connected) {
231+
const gasPrice = store('main.networksMeta', this.type, this.chainId, 'gas.price.levels.slow')
232+
const estimatedGasPrice = feeMarket
233+
? BigNumber(feeMarket.nextBaseFee).plus(BigNumber(feeMarket.maxPriorityFeePerGas))
234+
: BigNumber(gasPrice)
235+
236+
this.feeEstimatesUSD(parseInt(this.chainId), estimatedGasPrice, new Web3Provider(provider)).then(
237+
(samples) => {
238+
store.addSampleGasCosts(this.type, this.chainId, samples)
239+
}
240+
)
241+
}
242+
130243
store.setGasFees(this.type, this.chainId, feeMarket)
131244
store.setBlockHeight(this.chainId, parseInt(block.number, 16))
132245

main/provider/index.ts

+27-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import { v4 as uuid } from 'uuid'
21
import EventEmitter from 'events'
2+
import crypto from 'crypto'
33
import log from 'electron-log'
4+
import { v4 as uuid } from 'uuid'
5+
import { Web3Provider } from '@ethersproject/providers'
6+
import { BigNumber } from 'ethers'
7+
import { estimateL1GasCost } from '@eth-optimism/sdk'
48
import { recoverTypedSignature, SignTypedDataVersion } from '@metamask/eth-sig-util'
59
import { isAddress } from '@ethersproject/address'
6-
import crypto from 'crypto'
710
import { addHexPrefix, intToHex, isHexString, isHexPrefixed, fromUtf8 } from '@ethereumjs/util'
811

912
import store from '../store'
@@ -299,6 +302,27 @@ export class Provider extends EventEmitter {
299302
})
300303
}
301304

305+
async getL1GasCost(txData: TransactionData) {
306+
const { chainId, type, ...tx } = txData
307+
308+
const txRequest = {
309+
...tx,
310+
type: parseInt(type, 16),
311+
chainId: parseInt(chainId, 16)
312+
}
313+
314+
const connection = this.connection.connections['ethereum'][txRequest.chainId] as any
315+
const connectedProvider = connection?.primary?.connected
316+
? connection.primary?.provider
317+
: connection.secondary?.provider
318+
319+
if (!connectedProvider) {
320+
return BigNumber.from(0)
321+
}
322+
323+
return estimateL1GasCost(new Web3Provider(connectedProvider), txRequest)
324+
}
325+
302326
signAndSend(req: TransactionRequest, cb: Callback<string>) {
303327
const rawTx = req.data
304328
const res = (data: any) => {
@@ -413,6 +437,7 @@ export class Provider extends EventEmitter {
413437
jsonrpc: '2.0',
414438
id: 1
415439
}
440+
416441
const targetChain: Chain = {
417442
type: 'ethereum',
418443
id: parseInt(rawTx.chainId, 16)

main/store/actions/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,11 @@ module.exports = {
254254
u('main.networksMeta', netType, netId, 'gas.price.lastLevel', () => level)
255255
}
256256
},
257+
addSampleGasCosts: (u, netType, netId, samples) => {
258+
u('main.networksMeta', netType, netId, 'gas.samples', () => {
259+
return samples
260+
})
261+
},
257262
setNativeCurrencyData: (u, netType, netId, currency) => {
258263
u('main.networksMeta', netType, netId, 'nativeCurrency', (existing) => ({ ...existing, ...currency }))
259264
},

0 commit comments

Comments
 (0)