Commit b6f89fad authored by Kelvin Fichter's avatar Kelvin Fichter

feat: add sdk utils and corresponding tests

parent dc9d738a
---
'@eth-optimism/contracts': patch
---
Adds a new TestLib_CrossDomainUtils so we can properly test cross chain encoding functions
......@@ -24,6 +24,7 @@ FROM node as builder
WORKDIR /optimism
COPY .git ./.git
COPY *.json yarn.lock ./
COPY packages/sdk/package.json ./packages/sdk/package.json
COPY packages/core-utils/package.json ./packages/core-utils/package.json
COPY packages/common-ts/package.json ./packages/common-ts/package.json
COPY packages/contracts/package.json ./packages/contracts/package.json
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
/* Library Imports */
import { Lib_CrossDomainUtils } from "../../libraries/bridge/Lib_CrossDomainUtils.sol";
/**
* @title TestLib_CrossDomainUtils
*/
library TestLib_CrossDomainUtils {
function encodeXDomainCalldata(
address _target,
address _sender,
bytes memory _message,
uint256 _messageNonce
) public pure returns (bytes memory) {
return
Lib_CrossDomainUtils.encodeXDomainCalldata(_target, _sender, _message, _messageNonce);
}
}
import { HardhatUserConfig } from 'hardhat/types'
import '@nomiclabs/hardhat-ethers'
import '@nomiclabs/hardhat-waffle'
const config: HardhatUserConfig = {
solidity: {
version: '0.8.9',
},
paths: {
sources: './test/contracts',
},
}
export default config
......@@ -14,7 +14,9 @@
"lint": "yarn lint:fix && yarn lint:check",
"lint:check": "eslint .",
"lint:fix": "yarn lint:check --fix",
"pre-commit": "lint-staged"
"pre-commit": "lint-staged",
"test": "hardhat test",
"test:coverage": "nyc hardhat test && nyc merge .nyc_output coverage.json"
},
"keywords": [
"optimism",
......
export * from './interfaces'
export * from './utils'
......@@ -106,6 +106,14 @@ export interface CrossChainMessage {
messageNonce: number
}
/**
* Convenience type for when you don't care which direction the message is going in.
*/
export type DirectionlessCrossChainMessage = Omit<
CrossChainMessage,
'direction'
>
/**
* Describes a token withdrawal or deposit, along with the underlying raw cross chain message
* behind the deposit or withdrawal.
......@@ -188,7 +196,7 @@ export type MessageLike =
/**
* Stuff that can be coerced into a provider.
*/
export type ProviderLike = string | Provider
export type ProviderLike = string | Provider | any
/**
* Stuff that can be coerced into a signer.
......
import assert from 'assert'
import {
Provider,
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import { getContractInterface } from '@eth-optimism/contracts'
import { ethers } from 'ethers'
import {
ProviderLike,
TransactionLike,
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)]
)
}
/**
* Converts a ProviderLike into a provider. Assumes that if the ProviderLike is a string then
* it is a JSON-RPC url.
*
* @param provider ProviderLike to turn into a provider.
* @returns ProviderLike 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
} else {
throw new Error('Invalid provider')
}
}
/**
* Pulls a transaction hash out of a TransactionLike object.
*
* @param transaction TransactionLike to convert into a transaction hash.
* @returns Transaction hash corresponding to the TransactionLike input.
*/
export const toTransactionHash = (transaction: TransactionLike): string => {
if (typeof transaction === 'string') {
assert(
ethers.utils.isHexString(transaction, 32),
'Invalid transaction hash'
)
return transaction
} else if ((transaction as TransactionReceipt).transactionHash) {
return (transaction as TransactionReceipt).transactionHash
} else if ((transaction as TransactionResponse).hash) {
return (transaction as TransactionResponse).hash
} else {
throw new Error('Invalid transaction')
}
}
pragma solidity ^0.8.9;
contract AbsolutelyNothing {
function doAbsolutelyNothing() public {
return;
}
}
pragma solidity ^0.8.9;
contract MessageEncodingHelper {
// This function is copy/pasted from the Lib_CrossDomainUtils library. We have to do this
// because the Lib_CrossDomainUtils library does not provide a function for hashing. Instead,
// I'm duplicating the functionality of the library here and exposing an additional method that
// does the required hashing. This is fragile and will break if we ever update the way that our
// contracts hash the encoded data, but at least it works for now.
// TODO: Next time we're planning to upgrade the contracts, make sure that the library also
// contains a function for hashing.
function encodeXDomainCalldata(
address _target,
address _sender,
bytes memory _message,
uint256 _messageNonce
) public pure returns (bytes memory) {
return
abi.encodeWithSignature(
"relayMessage(address,address,bytes,uint256)",
_target,
_sender,
_message,
_messageNonce
);
}
function hashXDomainCalldata(
address _target,
address _sender,
bytes memory _message,
uint256 _messageNonce
) public pure returns (bytes32) {
return keccak256(
encodeXDomainCalldata(
_target,
_sender,
_message,
_messageNonce
)
);
}
}
import { expect } from './setup'
import { Provider } from '@ethersproject/abstract-provider'
import { Contract, Signer } from 'ethers'
import { ethers } from 'hardhat'
import { getContractFactory } from '@eth-optimism/contracts'
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('toProvider', () => {
it('should convert a string to a JsonRpcProvider', () => {
const provider = toProvider('http://localhost:8545')
expect(Provider.isProvider(provider)).to.be.true
})
it('should not do anything with a provider', () => {
const provider = toProvider(ethers.provider)
expect(provider).to.deep.equal(ethers.provider)
})
})
describe('toTransactionHash', () => {
describe('string inputs', () => {
it('should return the input if the input is a valid transaction hash', () => {
const input = '0x' + '11'.repeat(32)
expect(toTransactionHash(input)).to.equal(input)
})
it('should throw an error if the input is a hex string but not a transaction hash', () => {
const input = '0x' + '11'.repeat(31)
expect(() => toTransactionHash(input)).to.throw(
'Invalid transaction hash'
)
})
it('should throw an error if the input is not a hex string', () => {
const input = 'hi mom look at me go'
expect(() => toTransactionHash(input)).to.throw(
'Invalid transaction hash'
)
})
})
describe('transaction inputs', () => {
let AbsolutelyNothing: Contract
before(async () => {
AbsolutelyNothing = (await (
await ethers.getContractFactory('AbsolutelyNothing')
).deploy()) as any
})
it('should return the transaction hash if the input is a transaction response', async () => {
const tx = await AbsolutelyNothing.doAbsolutelyNothing()
expect(toTransactionHash(tx)).to.equal(tx.hash)
})
it('should return the transaction hash if the input is a transaction receipt', async () => {
const tx = await AbsolutelyNothing.doAbsolutelyNothing()
const receipt = await tx.wait()
expect(toTransactionHash(receipt)).to.equal(receipt.transactionHash)
})
})
describe('other types', () => {
it('should throw if given a number as an input', () => {
expect(() => toTransactionHash(1234 as any)).to.throw(
'Invalid transaction'
)
})
it('should throw if given a function as an input', () => {
expect(() =>
toTransactionHash((() => {
return 1234
}) as any)
).to.throw('Invalid transaction')
})
})
})
})
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