Commit 78bc0e1c authored by Kelvin Fichter's avatar Kelvin Fichter

feat: add SDK contract utils and corresponding tests

parent 0d5c7693
...@@ -33,11 +33,6 @@ export interface ICrossChainProvider { ...@@ -33,11 +33,6 @@ export interface ICrossChainProvider {
*/ */
l1ChainId: number l1ChainId: number
/**
* Chain ID for the L2 network.
*/
l2ChainId: number
/** /**
* Contract objects attached to their respective providers and addresses. * Contract objects attached to their respective providers and addresses.
*/ */
...@@ -129,9 +124,9 @@ export interface ICrossChainProvider { ...@@ -129,9 +124,9 @@ export interface ICrossChainProvider {
* *
* @param message Message to wait for. * @param message Message to wait for.
* @param opts Options to pass to the waiting function. * @param opts Options to pass to the waiting function.
* - `confirmations` (number): Number of transaction confirmations to wait for before returning. * @param opts.confirmations Number of transaction confirmations to wait for before returning.
* - `pollIntervalMs` (number): Number of milliseconds to wait between polling for the receipt. * @param opts.pollIntervalMs Number of milliseconds to wait between polling for the receipt.
* - `loopsBeforeTimeout` (number): Number of times to poll before timing out. * @param opts.timeoutMs Milliseconds to wait before timing out.
* @returns CrossChainMessage receipt including receipt of the transaction that relayed the * @returns CrossChainMessage receipt including receipt of the transaction that relayed the
* given message. * given message.
*/ */
...@@ -140,7 +135,7 @@ export interface ICrossChainProvider { ...@@ -140,7 +135,7 @@ export interface ICrossChainProvider {
opts?: { opts?: {
confirmations?: number confirmations?: number
pollIntervalMs?: number pollIntervalMs?: number
loopsBeforeTimeout?: number timeoutMs?: number
} }
): Promise<MessageReceipt> ): Promise<MessageReceipt>
......
...@@ -6,37 +6,65 @@ import { ...@@ -6,37 +6,65 @@ import {
import { Signer } from '@ethersproject/abstract-signer' import { Signer } from '@ethersproject/abstract-signer'
import { Contract, BigNumber, Overrides } from 'ethers' import { Contract, BigNumber, Overrides } from 'ethers'
/**
* L1 contract references.
*/
export interface OEL1Contracts {
AddressManager: Contract
L1CrossDomainMessenger: Contract
L1StandardBridge: Contract
StateCommitmentChain: Contract
CanonicalTransactionChain: Contract
BondManager: Contract
}
/**
* L2 contract references.
*/
export interface OEL2Contracts {
L2CrossDomainMessenger: Contract
L2StandardBridge: Contract
OVM_L1BlockNumber: Contract
OVM_L2ToL1MessagePasser: Contract
OVM_DeployerWhitelist: Contract
OVM_ETH: Contract
OVM_GasPriceOracle: Contract
OVM_SequencerFeeVault: Contract
WETH: Contract
}
/** /**
* Represents Optimistic Ethereum contracts, assumed to be connected to their appropriate * Represents Optimistic Ethereum contracts, assumed to be connected to their appropriate
* providers and addresses. * providers and addresses.
*/ */
export interface OEContracts { export interface OEContracts {
/** l1: OEL1Contracts
* L1 contract references. l2: OEL2Contracts
*/ }
l1: {
AddressManager: Contract
L1CrossDomainMessenger: Contract
L1StandardBridge: Contract
StateCommitmentChain: Contract
CanonicalTransactionChain: Contract
BondManager: Contract
}
/** /**
* L2 contract references. * Convenience type for something that looks like the L1 OE contract interface but could be
*/ * addresses instead of actual contract objects.
l2: { */
L2CrossDomainMessenger: Contract export type OEL1ContractsLike = {
L2StandardBridge: Contract [K in keyof OEL1Contracts]: AddressLike
OVM_L1BlockNumber: Contract }
OVM_L2ToL1MessagePasser: Contract
OVM_DeployerWhitelist: Contract /**
OVM_ETH: Contract * Convenience type for something that looks like the L2 OE contract interface but could be
OVM_GasPriceOracle: Contract * addresses instead of actual contract objects.
OVM_SequencerFeeVault: Contract */
WETH: Contract export type OEL2ContractsLike = {
} [K in keyof OEL2Contracts]: AddressLike
}
/**
* Convenience type for something that looks like the OE contract interface but could be
* addresses instead of actual contract objects.
*/
export interface OEContractsLike {
l1: OEL1ContractsLike
l2: OEL2ContractsLike
} }
/** /**
...@@ -164,13 +192,6 @@ export interface StateRootBatch { ...@@ -164,13 +192,6 @@ export interface StateRootBatch {
stateRoots: string[] stateRoots: string[]
} }
/**
* Utility type for deep partials.
*/
export type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>
}
/** /**
* Extended Ethers overrides object with an l2GasLimit field. * Extended Ethers overrides object with an l2GasLimit field.
* Only meant to be used for L1 to L2 messages, since L2 to L1 messages don't have a specified gas * Only meant to be used for L1 to L2 messages, since L2 to L1 messages don't have a specified gas
......
...@@ -4,46 +4,13 @@ import { ...@@ -4,46 +4,13 @@ import {
TransactionReceipt, TransactionReceipt,
TransactionResponse, TransactionResponse,
} from '@ethersproject/abstract-provider' } from '@ethersproject/abstract-provider'
import { getContractInterface } from '@eth-optimism/contracts' import { ethers, BigNumber } from 'ethers'
import { ethers } from 'ethers'
import { import {
ProviderLike, ProviderLike,
TransactionLike, TransactionLike,
DirectionlessCrossChainMessage, NumberLike,
} from './interfaces' AddressLike,
} from '../interfaces'
/**
* Returns the canonical encoding of a cross chain message. This encoding is used in various
* locations within the Optimistic Ethereum smart contracts.
*
* @param message Cross chain message to encode.
* @returns Canonical encoding of the message.
*/
export const encodeCrossChainMessage = (
message: DirectionlessCrossChainMessage
): string => {
return getContractInterface('L2CrossDomainMessenger').encodeFunctionData(
'relayMessage',
[message.target, message.sender, message.message, message.messageNonce]
)
}
/**
* Returns the canonical hash of a cross chain message. This hash is used in various locations
* within the Optimistic Ethereum smart contracts and is the keccak256 hash of the result of
* encodeCrossChainMessage.
*
* @param message Cross chain message to hash.
* @returns Canonical hash of the message.
*/
export const hashCrossChainMessage = (
message: DirectionlessCrossChainMessage
): string => {
return ethers.utils.solidityKeccak256(
['bytes'],
[encodeCrossChainMessage(message)]
)
}
/** /**
* Converts a ProviderLike into a provider. Assumes that if the ProviderLike is a string then * Converts a ProviderLike into a provider. Assumes that if the ProviderLike is a string then
...@@ -84,3 +51,29 @@ export const toTransactionHash = (transaction: TransactionLike): string => { ...@@ -84,3 +51,29 @@ export const toTransactionHash = (transaction: TransactionLike): string => {
throw new Error('Invalid transaction') throw new Error('Invalid transaction')
} }
} }
/**
* Converts a number-like into an ethers BigNumber.
*
* @param num Number-like to convert into a BigNumber.
* @returns Number-like as a BigNumber.
*/
export const toBigNumber = (num: NumberLike): BigNumber => {
return ethers.BigNumber.from(num)
}
/**
* Converts an address-like into a 0x-prefixed address string.
*
* @param addr Address-like to convert into an address.
* @returns Address-like as an address.
*/
export const toAddress = (addr: AddressLike): string => {
if (typeof addr === 'string') {
assert(ethers.utils.isAddress(addr), 'Invalid address')
return ethers.utils.getAddress(addr)
} else {
assert(ethers.utils.isAddress(addr.address), 'Invalid address')
return ethers.utils.getAddress(addr.address)
}
}
import { getContractInterface, predeploys } from '@eth-optimism/contracts'
import { ethers, Contract } from 'ethers'
import {
OEContracts,
OEL1Contracts,
OEL2Contracts,
OEContractsLike,
OEL2ContractsLike,
AddressLike,
} from '../interfaces'
import { toAddress } from './coercion'
import { DeepPartial } from './type-utils'
/**
* Full list of default L2 contract addresses.
*/
export const DEFAULT_L2_CONTRACT_ADDRESSES: OEL2ContractsLike = {
L2CrossDomainMessenger: predeploys.L2CrossDomainMessenger,
L2StandardBridge: predeploys.L2StandardBridge,
OVM_L1BlockNumber: predeploys.OVM_L1BlockNumber,
OVM_L2ToL1MessagePasser: predeploys.OVM_L2ToL1MessagePasser,
OVM_DeployerWhitelist: predeploys.OVM_DeployerWhitelist,
OVM_ETH: predeploys.OVM_ETH,
OVM_GasPriceOracle: predeploys.OVM_GasPriceOracle,
OVM_SequencerFeeVault: predeploys.OVM_SequencerFeeVault,
WETH: predeploys.WETH9,
}
/**
* We've changed some contract names in this SDK to be a bit nicer. Here we remap these nicer names
* back to the original contract names so we can look them up.
*/
const NAME_REMAPPING = {
AddressManager: 'Lib_AddressManager',
OVM_L1BlockNumber: 'iOVM_L1BlockNumber',
WETH: 'WETH9',
}
/**
* Mapping of L1 chain IDs to the appropriate contract addresses for the OE deployments to the
* given network. Simplifies the process of getting the correct contract addresses for a given
* contract name.
*/
export const CONTRACT_ADDRESSES: {
[l1ChainId: number]: OEContractsLike
} = {
// Mainnet
1: {
l1: {
AddressManager: '0xdE1FCfB0851916CA5101820A69b13a4E276bd81F',
L1CrossDomainMessenger: '0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1',
L1StandardBridge: '0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1',
StateCommitmentChain: '0xBe5dAb4A2e9cd0F27300dB4aB94BeE3A233AEB19',
CanonicalTransactionChain: '0x5E4e65926BA27467555EB562121fac00D24E9dD2',
BondManager: '0xcd626E1328b41fCF24737F137BcD4CE0c32bc8d1',
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
},
// Kovan
42: {
l1: {
AddressManager: '0x100Dd3b414Df5BbA2B542864fF94aF8024aFdf3a',
L1CrossDomainMessenger: '0x4361d0F75A0186C05f971c566dC6bEa5957483fD',
L1StandardBridge: '0x22F24361D548e5FaAfb36d1437839f080363982B',
StateCommitmentChain: '0xD7754711773489F31A0602635f3F167826ce53C5',
CanonicalTransactionChain: '0xf7B88A133202d41Fe5E2Ab22e6309a1A4D50AF74',
BondManager: '0xc5a603d273E28185c18Ba4d26A0024B2d2F42740',
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
},
// Goerli
5: {
l1: {
AddressManager: '0x2F7E3cAC91b5148d336BbffB224B4dC79F09f01D',
L1CrossDomainMessenger: '0xEcC89b9EDD804850C4F343A278Be902be11AaF42',
L1StandardBridge: '0x73298186A143a54c20ae98EEE5a025bD5979De02',
StateCommitmentChain: '0x1afcA918eff169eE20fF8AB6Be75f3E872eE1C1A',
CanonicalTransactionChain: '0x2ebA8c4EfDB39A8Cd8f9eD65c50ec079f7CEBD81',
BondManager: '0xE5AE60bD6F8DEe4D0c2BC9268e23B92F1cacC58F',
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
},
}
/**
* Returns an ethers.Contract object for the given name, connected to the appropriate address for
* the given L1 chain ID. Users can also provide a custom address to connect the contract to
* instead. If the chain ID is not known then the user MUST provide a custom address or this
* function will throw an error.
*
* @param contractName Name of the contract to connect to.
* @param l1ChainId Chain ID for the L1 network where the OE contracts are deployed.
* @param opts Additional options for connecting to the contract.
* @param opts.address Custom address to connect to the contract.
* @param opts.signerOrProvider Signer or provider to connect to the contract.
* @returns An ethers.Contract object connected to the appropriate address and interface.
*/
export const getOEContract = (
contractName: keyof OEL1Contracts | keyof OEL2Contracts,
l1ChainId: number,
opts: {
address?: AddressLike
signerOrProvider?: ethers.Signer | ethers.providers.Provider
} = {}
): Contract => {
const addresses = CONTRACT_ADDRESSES[l1ChainId]
if (addresses === undefined && opts.address === undefined) {
throw new Error(
`cannot get contract ${contractName} for unknown L1 chain ID ${l1ChainId}, you must provide an address`
)
}
return new Contract(
toAddress(
opts.address || addresses.l1[contractName] || addresses.l2[contractName]
),
getContractInterface(NAME_REMAPPING[contractName] || contractName),
opts.signerOrProvider
)
}
/**
* Automatically connects to all contract addresses, both L1 and L2, for the given L1 chain ID. The
* user can provide custom contract address overrides for L1 or L2 contracts. If the given chain ID
* is not known then the user MUST provide custom contract addresses for ALL L1 contracts or this
* function will throw an error.
*
* @param l1ChainId Chain ID for the L1 network where the OE contracts are deployed.
* @param opts Additional options for connecting to the contracts.
* @param opts.l1SignerOrProvider: Signer or provider to connect to the L1 contracts.
* @param opts.l2SignerOrProvider: Signer or provider to connect to the L2 contracts.
* @param opts.overrides Custom contract address overrides for L1 or L2 contracts.
* @returns An object containing ethers.Contract objects connected to the appropriate addresses on
* both L1 and L2.
*/
export const getAllOEContracts = (
l1ChainId: number,
opts: {
l1SignerOrProvider?: ethers.Signer | ethers.providers.Provider
l2SignerOrProvider?: ethers.Signer | ethers.providers.Provider
overrides?: DeepPartial<OEContractsLike>
} = {}
): OEContracts => {
const addresses = CONTRACT_ADDRESSES[l1ChainId] || {
l1: {
AddressManager: undefined,
L1CrossDomainMessenger: undefined,
L1StandardBridge: undefined,
StateCommitmentChain: undefined,
CanonicalTransactionChain: undefined,
BondManager: undefined,
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
}
// Attach all L1 contracts.
const l1Contracts: OEL1Contracts = {} as any
for (const [contractName, contractAddress] of Object.entries(addresses.l1)) {
l1Contracts[contractName] = getOEContract(contractName as any, l1ChainId, {
address: opts.overrides?.l1?.[contractName] || contractAddress,
signerOrProvider: opts.l1SignerOrProvider,
})
}
// Attach all L2 contracts.
const l2Contracts: OEL2Contracts = {} as any
for (const [contractName, contractAddress] of Object.entries(addresses.l2)) {
l2Contracts[contractName] = getOEContract(contractName as any, l1ChainId, {
address: opts.overrides?.l2?.[contractName] || contractAddress,
signerOrProvider: opts.l2SignerOrProvider,
})
}
return {
l1: l1Contracts,
l2: l2Contracts,
}
}
export * from './coercion'
export * from './contracts'
export * from './message-encoding'
export * from './type-utils'
import { getContractInterface } from '@eth-optimism/contracts'
import { ethers } from 'ethers'
import { DirectionlessCrossChainMessage } from '../interfaces'
/**
* Returns the canonical encoding of a cross chain message. This encoding is used in various
* locations within the Optimistic Ethereum smart contracts.
*
* @param message Cross chain message to encode.
* @returns Canonical encoding of the message.
*/
export const encodeCrossChainMessage = (
message: DirectionlessCrossChainMessage
): string => {
return getContractInterface('L2CrossDomainMessenger').encodeFunctionData(
'relayMessage',
[message.target, message.sender, message.message, message.messageNonce]
)
}
/**
* Returns the canonical hash of a cross chain message. This hash is used in various locations
* within the Optimistic Ethereum smart contracts and is the keccak256 hash of the result of
* encodeCrossChainMessage.
*
* @param message Cross chain message to hash.
* @returns Canonical hash of the message.
*/
export const hashCrossChainMessage = (
message: DirectionlessCrossChainMessage
): string => {
return ethers.utils.solidityKeccak256(
['bytes'],
[encodeCrossChainMessage(message)]
)
}
/**
* Utility type for deep partials.
*/
export type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>
}
...@@ -3,16 +3,52 @@ import './setup' ...@@ -3,16 +3,52 @@ import './setup'
describe('CrossChainProvider', () => { describe('CrossChainProvider', () => {
describe('construction', () => { describe('construction', () => {
describe('basic construction (given L1 and L2 providers)', () => { describe('when given an ethers provider for the L1 provider', () => {
it('should have an l1Provider', () => {}) it('should use the provider as the L1 provider', () => {})
})
describe('when given an ethers provider for the L2 provider', () => {
it('should use the provider as the L2 provider', () => {})
})
describe('when given a string as the L1 provider', () => {
it('should create a JSON-RPC provider for the L1 provider', () => {})
})
describe('when given a string as the L2 provider', () => {
it('should create a JSON-RPC provider for the L2 provider', () => {})
})
describe('when no custom contract addresses are provided', () => {
describe('when given a known chain ID', () => {
it('should use the contract addresses for the known chain ID', () => {})
})
it('should have an l2Provider', () => {}) describe('when given an unknown chain ID', () => {
it('should throw an error', () => {})
})
})
it('should have an l1ChainId', () => {}) describe('when custom contract addresses are provided', () => {
describe('when given a known chain ID', () => {
it('should use known addresses except where custom addresses are given', () => {})
})
it('should have an l2ChainId', () => {}) describe('when given an unknown chain ID', () => {
describe('when all L1 addresses are provided', () => {
describe('when not all L2 addresses are provided', () => {
it('should use the custom L1 addresses and the default L2 addresses', () => {})
})
it('should have all contract connections', () => {}) describe('when all L2 addresses are provided', () => {
it('should use the custom addresses everywhere', () => {})
})
})
describe('when not all L1 addresses are provided', () => {
it('should throw an error', () => {})
})
})
}) })
}) })
...@@ -152,7 +188,7 @@ describe('CrossChainProvider', () => { ...@@ -152,7 +188,7 @@ describe('CrossChainProvider', () => {
}) })
describe('when the message has not been relayed', () => { describe('when the message has not been relayed', () => {
it('should return a status of READY_FOR_RELAY') it('should return a status of READY_FOR_RELAY', () => {})
}) })
}) })
}) })
......
import { expect } from './setup' import { expect } from '../setup'
import { Provider } from '@ethersproject/abstract-provider' import { Provider } from '@ethersproject/abstract-provider'
import { Contract, Signer } from 'ethers' import { Contract } from 'ethers'
import { ethers } from 'hardhat' import { ethers } from 'hardhat'
import { getContractFactory } from '@eth-optimism/contracts' import { toProvider, toTransactionHash } from '../../src'
import {
toProvider,
toTransactionHash,
CrossChainMessage,
MessageDirection,
encodeCrossChainMessage,
hashCrossChainMessage,
} from '../src'
describe('utils', () => {
let signers: Signer[]
before(async () => {
signers = (await ethers.getSigners()) as any
})
describe('encodeCrossChainMessage', () => {
let Lib_CrossDomainUtils: Contract
before(async () => {
Lib_CrossDomainUtils = (await getContractFactory(
'TestLib_CrossDomainUtils',
signers[0]
).deploy()) as any
})
it('should properly encode a message', async () => {
const message: CrossChainMessage = {
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '1234'.repeat(32),
messageNonce: 1234,
}
const actual = encodeCrossChainMessage(message)
const expected = await Lib_CrossDomainUtils.encodeXDomainCalldata(
message.target,
message.sender,
message.message,
message.messageNonce
)
expect(actual).to.equal(expected)
})
})
describe('hashCrossChainMessage', () => {
let MessageEncodingHelper: Contract
before(async () => {
MessageEncodingHelper = (await (
await ethers.getContractFactory('MessageEncodingHelper')
).deploy()) as any
})
it('should properly hash a message', async () => {
const message: CrossChainMessage = {
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '1234'.repeat(32),
messageNonce: 1234,
}
const actual = hashCrossChainMessage(message)
const expected = await MessageEncodingHelper.hashXDomainCalldata(
message.target,
message.sender,
message.message,
message.messageNonce
)
expect(actual).to.equal(expected)
})
})
describe('type coercion utils', () => {
describe('toProvider', () => { describe('toProvider', () => {
it('should convert a string to a JsonRpcProvider', () => { it('should convert a string to a JsonRpcProvider', () => {
const provider = toProvider('http://localhost:8545') const provider = toProvider('http://localhost:8545')
......
This diff is collapsed.
import { expect } from '../setup'
import { Contract, Signer } from 'ethers'
import { ethers } from 'hardhat'
import { getContractFactory } from '@eth-optimism/contracts'
import {
CrossChainMessage,
MessageDirection,
encodeCrossChainMessage,
hashCrossChainMessage,
} from '../../src'
describe('message encoding utils', () => {
let signers: Signer[]
before(async () => {
signers = (await ethers.getSigners()) as any
})
describe('encodeCrossChainMessage', () => {
let Lib_CrossDomainUtils: Contract
before(async () => {
Lib_CrossDomainUtils = (await getContractFactory(
'TestLib_CrossDomainUtils',
signers[0]
).deploy()) as any
})
it('should properly encode a message', async () => {
const message: CrossChainMessage = {
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '1234'.repeat(32),
messageNonce: 1234,
}
const actual = encodeCrossChainMessage(message)
const expected = await Lib_CrossDomainUtils.encodeXDomainCalldata(
message.target,
message.sender,
message.message,
message.messageNonce
)
expect(actual).to.equal(expected)
})
})
describe('hashCrossChainMessage', () => {
let MessageEncodingHelper: Contract
before(async () => {
MessageEncodingHelper = (await (
await ethers.getContractFactory('MessageEncodingHelper')
).deploy()) as any
})
it('should properly hash a message', async () => {
const message: CrossChainMessage = {
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '1234'.repeat(32),
messageNonce: 1234,
}
const actual = hashCrossChainMessage(message)
const expected = await MessageEncodingHelper.hashXDomainCalldata(
message.target,
message.sender,
message.message,
message.messageNonce
)
expect(actual).to.equal(expected)
})
})
})
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