Commit 69779a27 authored by smartcontracts's avatar smartcontracts Committed by GitHub

Merge pull request #2163 from ethereum-optimism/sc/sdk-l2-provider

feat(sdk): implement asL2Provider
parents 560036e3 152df378
---
'@eth-optimism/sdk': patch
---
This update implements the asL2Provider function
---
'@eth-optimism/integration-tests': patch
---
Use new asL2Provider function for integration tests
/* Imports: External */
import { ethers } from 'hardhat'
import { injectL2Context, expectApprox } from '@eth-optimism/core-utils'
import { expectApprox } from '@eth-optimism/core-utils'
import { predeploys } from '@eth-optimism/contracts'
import { asL2Provider } from '@eth-optimism/sdk'
import { Contract, BigNumber } from 'ethers'
/* Imports: Internal */
......@@ -21,7 +22,7 @@ import { Direction } from './shared/watcher-utils'
* must be equal to the blocknumber/timestamp of the L1 transaction.
*/
describe('OVM Context: Layer 2 EVM Context', () => {
const L2Provider = injectL2Context(l2Provider)
const L2Provider = asL2Provider(l2Provider)
let env: OptimismEnv
before(async () => {
env = await OptimismEnv.new()
......
/* Imports: Internal */
import { providers } from 'ethers'
import { injectL2Context, applyL1ToL2Alias } from '@eth-optimism/core-utils'
import { applyL1ToL2Alias } from '@eth-optimism/core-utils'
import { asL2Provider } from '@eth-optimism/sdk'
/* Imports: External */
import { expect } from './shared/setup'
......@@ -13,7 +14,7 @@ describe('Queue Ingestion', () => {
let l2Provider: providers.JsonRpcProvider
before(async () => {
env = await OptimismEnv.new()
l2Provider = injectL2Context(env.l2Wallet.provider as any)
l2Provider = asL2Provider(env.l2Wallet.provider as any)
})
// The batch submitter will notice that there are transactions
......
/* Imports: External */
import { expectApprox, injectL2Context, sleep } from '@eth-optimism/core-utils'
import { expectApprox, sleep } from '@eth-optimism/core-utils'
import { asL2Provider } from '@eth-optimism/sdk'
import { Wallet, BigNumber, Contract, ContractFactory, constants } from 'ethers'
import { serialize } from '@ethersproject/transactions'
import { ethers } from 'hardhat'
......@@ -26,7 +27,7 @@ describe('Basic RPC tests', () => {
let env: OptimismEnv
let wallet: Wallet
const provider = injectL2Context(l2Provider)
const provider = asL2Provider(l2Provider)
let Reverter: Contract
let ValueContext: Contract
......
......@@ -12,8 +12,12 @@ import {
getContractInterface,
predeploys,
} from '@eth-optimism/contracts'
import { injectL2Context, remove0x } from '@eth-optimism/core-utils'
import { CrossChainMessenger, NumberLike } from '@eth-optimism/sdk'
import { remove0x } from '@eth-optimism/core-utils'
import {
CrossChainMessenger,
NumberLike,
asL2Provider,
} from '@eth-optimism/sdk'
import { cleanEnv, str, num, bool, makeValidator } from 'envalid'
import dotenv from 'dotenv'
dotenv.config()
......@@ -112,17 +116,17 @@ export const envConfig = procEnv
export const l1Provider = new providers.JsonRpcProvider(procEnv.L1_URL)
l1Provider.pollingInterval = procEnv.L1_POLLING_INTERVAL
export const l2Provider = injectL2Context(
export const l2Provider = asL2Provider(
new providers.JsonRpcProvider(procEnv.L2_URL)
)
l2Provider.pollingInterval = procEnv.L2_POLLING_INTERVAL
export const replicaProvider = injectL2Context(
export const replicaProvider = asL2Provider(
new providers.JsonRpcProvider(procEnv.REPLICA_URL)
)
replicaProvider.pollingInterval = procEnv.REPLICA_POLLING_INTERVAL
export const verifierProvider = injectL2Context(
export const verifierProvider = asL2Provider(
new providers.JsonRpcProvider(procEnv.VERIFIER_URL)
)
verifierProvider.pollingInterval = procEnv.L2_POLLING_INTERVAL
......
......@@ -33,6 +33,7 @@
"devDependencies": {
"@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/abstract-signer": "^5.5.0",
"@ethersproject/transactions": "^5.5.0",
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@types/chai": "^4.2.18",
......@@ -64,6 +65,7 @@
"dependencies": {
"@eth-optimism/contracts": "0.5.13",
"@eth-optimism/core-utils": "0.7.6",
"lodash": "^4.17.21",
"merkletreejs": "^0.2.27",
"rlp": "^2.2.7"
},
......
......@@ -2,3 +2,4 @@ export * from './interfaces'
export * from './utils'
export * from './cross-chain-messenger'
export * from './adapters'
export * from './l2-provider'
import { Provider, TransactionRequest } from '@ethersproject/abstract-provider'
import {
Provider,
TransactionRequest,
TransactionResponse,
Block,
BlockWithTransactions,
} from '@ethersproject/abstract-provider'
import { BigNumber } from 'ethers'
/**
* JSON transaction representation when returned by L2Geth nodes. This is simply an extension to
* the standard transaction response type. You do NOT need to use this type unless you care about
* having typed access to L2-specific fields.
*/
export interface L2Transaction extends TransactionResponse {
l1BlockNumber: number
l1TxOrigin: string
queueOrigin: string
rawTransaction: string
}
/**
* JSON block representation when returned by L2Geth nodes. Just a normal block but with
* an added stateRoot field.
*/
export interface L2Block extends Block {
stateRoot: string
}
/**
* JSON block representation when returned by L2Geth nodes. Just a normal block but with
* L2Transaction objects instead of the standard transaction response object.
*/
export interface L2BlockWithTransactions extends BlockWithTransactions {
stateRoot: string
transactions: [L2Transaction]
}
/**
* Represents an extended version of an normal ethers Provider that returns additional L2 info and
* has special functions for L2-specific interactions.
*/
export interface L2Provider extends Provider {
export type L2Provider<TProvider extends Provider> = TProvider & {
/**
* Gets the current L1 (data) gas price.
*
......
import { Provider, TransactionRequest } from '@ethersproject/abstract-provider'
import { serialize } from '@ethersproject/transactions'
import { Contract, BigNumber } from 'ethers'
import { predeploys, getContractInterface } from '@eth-optimism/contracts'
import cloneDeep from 'lodash/cloneDeep'
import { L2Provider, ProviderLike, NumberLike } from './interfaces'
import { toProvider, toNumber, toBigNumber } from './utils'
/**
* Returns a Contract object for the GasPriceOracle.
*
* @param provider Provider to attach the contract to.
* @returns Contract object for the GasPriceOracle.
*/
const connectGasPriceOracle = (provider: ProviderLike): Contract => {
return new Contract(
predeploys.OVM_GasPriceOracle,
getContractInterface('OVM_GasPriceOracle'),
toProvider(provider)
)
}
/**
* Gets the current L1 gas price as seen on L2.
*
* @param l2Provider L2 provider to query the L1 gas price from.
* @returns Current L1 gas price as seen on L2.
*/
export const getL1GasPrice = async (
l2Provider: ProviderLike
): Promise<BigNumber> => {
const gpo = connectGasPriceOracle(l2Provider)
return gpo.gasPrice()
}
/**
* Estimates the amount of L1 gas required for a given L2 transaction.
*
* @param l2Provider L2 provider to query the gas usage from.
* @param tx Transaction to estimate L1 gas for.
* @returns Estimated L1 gas.
*/
export const estimateL1Gas = async (
l2Provider: ProviderLike,
tx: TransactionRequest
): Promise<BigNumber> => {
const gpo = connectGasPriceOracle(l2Provider)
return gpo.getL1GasUsed(
serialize({
...tx,
nonce: toNumber(tx.nonce as NumberLike),
})
)
}
/**
* Estimates the amount of L1 gas cost for a given L2 transaction in wei.
*
* @param l2Provider L2 provider to query the gas usage from.
* @param tx Transaction to estimate L1 gas cost for.
* @returns Estimated L1 gas cost.
*/
export const estimateL1GasCost = async (
l2Provider: ProviderLike,
tx: TransactionRequest
): Promise<BigNumber> => {
const gpo = connectGasPriceOracle(l2Provider)
return gpo.getL1Fee(
serialize({
...tx,
nonce: toNumber(tx.nonce as NumberLike),
})
)
}
/**
* Estimates the L2 gas cost for a given L2 transaction in wei.
*
* @param l2Provider L2 provider to query the gas usage from.
* @param tx Transaction to estimate L2 gas cost for.
* @returns Estimated L2 gas cost.
*/
export const estimateL2GasCost = async (
l2Provider: ProviderLike,
tx: TransactionRequest
): Promise<BigNumber> => {
const parsed = toProvider(l2Provider)
const l2GasPrice = await parsed.getGasPrice()
const l2GasCost = await parsed.estimateGas(tx)
return l2GasPrice.mul(l2GasCost)
}
/**
* Estimates the total gas cost for a given L2 transaction in wei.
*
* @param l2Provider L2 provider to query the gas usage from.
* @param tx Transaction to estimate total gas cost for.
* @returns Estimated total gas cost.
*/
export const estimateTotalGasCost = async (
l2Provider: ProviderLike,
tx: TransactionRequest
): Promise<BigNumber> => {
const l1GasCost = await estimateL1GasCost(l2Provider, tx)
const l2GasCost = await estimateL2GasCost(l2Provider, tx)
return l1GasCost.add(l2GasCost)
}
/**
* Returns an provider wrapped as an Optimism L2 provider. Adds a few extra helper functions to
* simplify the process of estimating the gas usage for a transaction on Optimism. Returns a COPY
* of the original provider.
*
* @param provider Provider to wrap into an L2 provider.
* @returns Provider wrapped as an L2 provider.
*/
export const asL2Provider = <TProvider extends Provider>(
provider: TProvider
): L2Provider<TProvider> => {
// Make a copy of the provider since we'll be modifying some internals and don't want to mess
// with the original object.
const l2Provider = cloneDeep(provider) as any
// Skip if we've already wrapped this provider.
if (l2Provider._isL2Provider) {
return l2Provider
}
// Not exactly sure when the provider wouldn't have a formatter function, but throw an error if
// it doesn't have one. The Provider type doesn't define it but every provider I've dealt with
// seems to have it.
const formatter = l2Provider.formatter
if (formatter === undefined) {
throw new Error(`provider.formatter must be defined`)
}
// Modify the block formatter to return the state root. Not strictly related to Optimism, just a
// generally useful thing that really should've been on the Ethers block object to begin with.
// TODO: Maybe we should make a PR to add this to the Ethers library?
const ogBlockFormatter = formatter.block.bind(formatter)
formatter.block = (block: any) => {
const parsed = ogBlockFormatter(block)
parsed.stateRoot = block.stateRoot
return parsed
}
// Modify the block formatter to include all the L2 fields for transactions.
const ogBlockWithTxFormatter = formatter.blockWithTransactions.bind(formatter)
formatter.blockWithTransactions = (block: any) => {
const parsed = ogBlockWithTxFormatter(block)
parsed.stateRoot = block.stateRoot
parsed.transactions = parsed.transactions.map((tx: any, idx: number) => {
const ogTx = block.transactions[idx]
tx.l1BlockNumber = ogTx.l1BlockNumber
? toNumber(ogTx.l1BlockNumber)
: ogTx.l1BlockNumber
tx.l1Timestamp = ogTx.l1Timestamp
? toNumber(ogTx.l1Timestamp)
: ogTx.l1Timestamp
tx.l1TxOrigin = ogTx.l1TxOrigin
tx.queueOrigin = ogTx.queueOrigin
tx.rawTransaction = ogTx.rawTransaction
return tx
})
return parsed
}
// Modify the transaction formatter to include all the L2 fields for transactions.
const ogTxResponseFormatter = formatter.transactionResponse.bind(formatter)
formatter.transactionResponse = (tx: any) => {
const parsed = ogTxResponseFormatter(tx)
parsed.txType = tx.txType
parsed.queueOrigin = tx.queueOrigin
parsed.rawTransaction = tx.rawTransaction
parsed.l1TxOrigin = tx.l1TxOrigin
parsed.l1BlockNumber = tx.l1BlockNumber
? parseInt(tx.l1BlockNumber, 16)
: tx.l1BlockNumbers
return parsed
}
// Modify the receipt formatter to include all the L2 fields.
const ogReceiptFormatter = formatter.receipt.bind(formatter)
formatter.receipt = (receipt: any) => {
const parsed = ogReceiptFormatter(receipt)
parsed.l1GasPrice = toBigNumber(receipt.l1GasPrice)
parsed.l1GasUsed = toBigNumber(receipt.l1GasUsed)
parsed.l1Fee = toBigNumber(receipt.l1Fee)
parsed.l1FeeScalar = parseFloat(receipt.l1FeeScalar)
return parsed
}
// Connect extra functions.
l2Provider.getL1GasPrice = async () => {
return getL1GasPrice(l2Provider)
}
l2Provider.estimateL1Gas = async (tx: TransactionRequest) => {
return estimateL1Gas(l2Provider, tx)
}
l2Provider.estimateL1GasCost = async (tx: TransactionRequest) => {
return estimateL1GasCost(l2Provider, tx)
}
l2Provider.estimateL2GasCost = async (tx: TransactionRequest) => {
return estimateL2GasCost(l2Provider, tx)
}
l2Provider.estimateTotalGasCost = async (tx: TransactionRequest) => {
return estimateTotalGasCost(l2Provider, tx)
}
l2Provider._isL2Provider = true
return l2Provider
}
......@@ -10,6 +10,7 @@ import { ethers, BigNumber } from 'ethers'
import {
SignerOrProviderLike,
ProviderLike,
TransactionLike,
NumberLike,
AddressLike,
......@@ -36,6 +37,23 @@ export const toSignerOrProvider = (
}
}
/**
* Converts a ProviderLike into a Provider. Assumes that if the input is a string then it is a
* JSON-RPC url.
*
* @param provider ProviderLike to turn into a Provider.
* @returns Input as a Provider.
*/
export const toProvider = (provider: ProviderLike): Provider => {
if (typeof provider === 'string') {
return new ethers.providers.JsonRpcProvider(provider)
} else if (Provider.isProvider(provider)) {
return provider as Provider
} else {
throw new Error('Invalid provider')
}
}
/**
* Pulls a transaction hash out of a TransactionLike object.
*
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment