Skip to content

Commit

Permalink
feat: deposit tx js helpers (ethereum-optimism#352)
Browse files Browse the repository at this point in the history
* feat: js deposit tx

Hardhat task now lists the tx hash

* rollup: new deposit hashing

* nice api!

* fix: dockerfile
  • Loading branch information
tynes authored Apr 8, 2022
1 parent 55a3868 commit ce05904
Show file tree
Hide file tree
Showing 6 changed files with 368 additions and 9 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"lint:toc": "doctoc --title=\"**Table of Contents**\" ./specs ./meta"
},
"dependencies": {
"@eth-optimism/core-utils": "^0.8.1"
"@eth-optimism/core-utils": "^0.8.1",
"@ethersproject/constants": "^5.6.0"
}
}
216 changes: 216 additions & 0 deletions packages/contracts/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import * as RLP from '@ethersproject/rlp'
import { BigNumber, BigNumberish } from '@ethersproject/bignumber'
import { getAddress } from '@ethersproject/address'
import {
hexConcat,
stripZeros,
zeroPad,
arrayify,
BytesLike,
} from '@ethersproject/bytes'
import { keccak256 } from '@ethersproject/keccak256'
import { Zero } from '@ethersproject/constants'
import { ContractReceipt, Event } from '@ethersproject/contracts'

function formatNumber(value: BigNumberish, name: string): Uint8Array {
const result = stripZeros(BigNumber.from(value).toHexString())
if (result.length > 32) {
throw new Error(`invalid length for ${name}`)
}
return result
}

function handleNumber(value: string): BigNumber {
if (value === '0x') {
return Zero
}
return BigNumber.from(value)
}

function handleAddress(value: string): string {
if (value === '0x') {
// @ts-ignore
return null
}
return getAddress(value)
}

export enum SourceHashDomain {
UserDeposit = 0,
L1InfoDeposit = 1,
}

interface DepositTxOpts {
sourceHash?: string
from: string
to: string | null
mint: BigNumberish
value: BigNumberish
gas: BigNumberish
data: string
domain?: SourceHashDomain
l1BlockHash?: string
logIndex?: BigNumberish
sequenceNumber?: BigNumberish
}

interface DepostTxExtraOpts {
domain?: SourceHashDomain
l1BlockHash?: string
logIndex?: BigNumberish
sequenceNumber?: BigNumberish
}

export class DepositTx {
public type = '0x7E'
private _sourceHash?: string
public from: string
public to: string | null
public mint: BigNumberish
public value: BigNumberish
public gas: BigNumberish
public data: BigNumberish

public domain?: SourceHashDomain
public l1BlockHash?: string
public logIndex?: BigNumberish
public sequenceNumber?: BigNumberish

constructor(opts: Partial<DepositTxOpts> = {}) {
this._sourceHash = opts.sourceHash
this.from = opts.from!
this.to = opts.to!
this.mint = opts.mint!
this.value = opts.value!
this.gas = opts.gas!
this.data = opts.data!
this.domain = opts.domain
this.l1BlockHash = opts.l1BlockHash
this.logIndex = opts.logIndex
this.sequenceNumber = opts.sequenceNumber
}

hash() {
const encoded = this.encode()
return keccak256(encoded)
}

sourceHash() {
if (!this._sourceHash) {
let marker: string
switch (this.domain) {
case SourceHashDomain.UserDeposit:
marker = BigNumber.from(this.logIndex).toHexString()
break
case SourceHashDomain.L1InfoDeposit:
marker = BigNumber.from(this.sequenceNumber).toHexString()
break
default:
throw new Error(`Unknown domain: ${this.domain}`)
}

if (!this.l1BlockHash) {
throw new Error('Need l1BlockHash to compute sourceHash')
}

const l1BlockHash = this.l1BlockHash
const input = hexConcat([l1BlockHash, zeroPad(marker, 32)])
const depositIDHash = keccak256(input)
const domain = BigNumber.from(this.domain).toHexString()
const domainInput = hexConcat([zeroPad(domain, 32), depositIDHash])
this._sourceHash = keccak256(domainInput)
}
return this._sourceHash
}

encode() {
const fields: any = [
this.sourceHash() || '0x',
getAddress(this.from) || '0x',
this.to != null ? getAddress(this.to) : '0x',
formatNumber(this.mint || 0, 'mint'),
formatNumber(this.value || 0, 'value'),
formatNumber(this.gas || 0, 'gas'),
this.data || '0x',
]

return hexConcat([this.type, RLP.encode(fields)])
}

decode(raw: BytesLike, extra: DepostTxExtraOpts = {}) {
const payload = arrayify(raw)
const transaction = RLP.decode(payload.slice(1))

this._sourceHash = transaction[0]
this.from = handleAddress(transaction[1])
this.to = handleAddress(transaction[2])
this.mint = handleNumber(transaction[3])
this.value = handleNumber(transaction[4])
this.gas = handleNumber(transaction[5])
this.data = transaction[6]

if ('l1BlockHash' in extra) {
this.l1BlockHash = extra.l1BlockHash
}
if ('domain' in extra) {
this.domain = extra.domain
}
if ('logIndex' in extra) {
this.logIndex = extra.logIndex
}
if ('sequenceNumber' in extra) {
this.sequenceNumber = extra.sequenceNumber
}
return this
}

static decode(raw: BytesLike, extra?: DepostTxExtraOpts): DepositTx {
return new this().decode(raw, extra)
}

fromL1Receipt(receipt: ContractReceipt, index: number): DepositTx {
if (!receipt.events) throw new Error('cannot parse receipt')
const event = receipt.events[index]
if (!event) {
throw new Error(`event index ${index} does not exist`)
}
return this.fromL1Event(event)
}

static fromL1Receipt(receipt: ContractReceipt, index: number): DepositTx {
return new this({}).fromL1Receipt(receipt, index)
}

fromL1Event(event: Event): DepositTx {
if (event.event !== 'TransactionDeposited')
throw new Error(`incorrect event type: ${event.event}`)
if (typeof event.args === 'undefined') throw new Error('no event args')
if (typeof event.args.from === 'undefined')
throw new Error('"from" undefined')
this.from = event.args.from
if (typeof event.args.isCreation === 'undefined')
throw new Error('"isCreation" undefined')
if (typeof event.args.to === 'undefined') throw new Error('"to" undefined')
this.to = event.args.isCreation ? null : event.args.to
if (typeof event.args.mint === 'undefined')
throw new Error('"mint" undefined')
this.mint = event.args.mint
if (typeof event.args.value === 'undefined')
throw new Error('"value" undefined')
this.value = event.args.value
if (typeof event.args.gasLimit === 'undefined')
throw new Error('"gasLimit" undefined')
this.gas = event.args.gasLimit
if (typeof event.args.data === 'undefined')
throw new Error('"data" undefined')
this.data = event.args.data
this.domain = SourceHashDomain.UserDeposit
this.l1BlockHash = event.blockHash
this.logIndex = event.logIndex
return this
}

static fromL1Event(event: Event): DepositTx {
return new this({}).fromL1Event(event)
}
}
5 changes: 5 additions & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
"main": "index.js",
"license": "MIT",
"dependencies": {
"@ethersproject/address": "^5.6.0",
"@ethersproject/bignumber": "^5.6.0",
"@ethersproject/bytes": "^5.6.1",
"@ethersproject/keccak256": "^5.6.0",
"@ethersproject/rlp": "^5.6.0",
"hardhat": "^2.7.1",
"rlp": "^2.2.7"
},
Expand Down
51 changes: 43 additions & 8 deletions packages/contracts/tasks/deposits.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import { task, types } from 'hardhat/config'
import { Contract, providers, utils, Wallet } from 'ethers'
import { Event } from '@ethersproject/contracts'
import dotenv from 'dotenv'
import { DepositTx, SourceHashDomain } from '../helpers/index'

dotenv.config()

async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}

task('deposit', 'Deposits funds onto L2.')
.addParam(
'l1ProviderUrl',
'L1 provider URL.',
'http://localhost:8545',
types.string
)
.addParam(
'l2ProviderUrl',
'L2 provider URL.',
'http://localhost:9545',
types.string
)
.addParam('to', 'Recipient address.', null, types.string)
.addParam('amountEth', 'Amount in ETH to send.', null, types.string)
.addOptionalParam(
Expand All @@ -26,11 +38,18 @@ task('deposit', 'Deposits funds onto L2.')
types.string
)
.setAction(async (args) => {
const { l1ProviderUrl, to, amountEth, depositContractAddr, privateKey } =
args
const {
l1ProviderUrl,
l2ProviderUrl,
to,
amountEth,
depositContractAddr,
privateKey,
} = args
const depositFeedArtifact = require('../artifacts/contracts/L1/DepositFeed.sol/DepositFeed.json')

const l1Provider = new providers.JsonRpcProvider(l1ProviderUrl)
const l2Provider = new providers.JsonRpcProvider(l2ProviderUrl)

let l1Wallet: Wallet | providers.JsonRpcSigner
if (privateKey) {
Expand All @@ -52,19 +71,35 @@ task('deposit', 'Deposits funds onto L2.')
).connect(l1Wallet)

const amountWei = utils.parseEther(amountEth)
console.log(`Depositing ${amountEth} ETH to ${to}...`)
const value = amountWei.add(utils.parseEther('0.01'))
console.log(`Depositing ${amountEth} ETH to ${to}`)
// Below adds 0.01 ETH to account for gas.
const tx = await depositFeed.depositTransaction(
to,
amountWei,
'3000000',
false,
[],
{
value: amountWei.add(utils.parseEther('0.01')),
}
{ value }
)
console.log(`Got TX hash ${tx.hash}. Waiting...`)
await tx.wait()
console.log('Done.')
const receipt = await tx.wait()

// find the transaction deposited event and derive
// the deposit transaction from it
const event = receipt.events.find(
(e: Event) => e.event === 'TransactionDeposited'
)
const l2tx = DepositTx.fromL1Event(event)
const hash = l2tx.hash()
console.log(`Waiting for L2 TX hash ${hash}`)

while (true) {
const tx = await l2Provider.send('eth_getTransactionByHash', [hash])
if (tx) {
console.log('Deposit success')
break
}
await sleep(500)
}
})
54 changes: 54 additions & 0 deletions packages/contracts/test/helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { expect } from 'chai'
import { DepositTx, SourceHashDomain } from '../helpers'
import { BigNumber } from '@ethersproject/bignumber'

describe('Helpers', () => {
describe('DepositTx', () => {
it('should serialize/deserialize and hash', () => {
// constants serialized using optimistic-geth
// TODO(tynes): more tests
const hash =
'0xf5f97d03e8be48a4b20ed70c9d8b11f1c851bf949bf602b7580985705bb09077'
const raw =
'0x7ef862a077fc5994647d128a4d131d273a5e89e0306aac472494068a4f1fceab83dd073594de3829a23df1479438622a08a116e8eb3f620bb594b7e390864a90b7b923c9f9310c6f98aafe43f707880e043da617250000880de0b6b3a7640000832dc6c080'

const tx = new DepositTx({
from: '0xDe3829A23DF1479438622a08a116E8Eb3f620BB5',
gas: '0x2dc6c0',
data: '0x',
to: '0xB7e390864a90b7b923C9f9310C6F98aafE43F707',
value: '0xde0b6b3a7640000',
domain: SourceHashDomain.UserDeposit,
l1BlockHash:
'0xd1a498e053451fc90bd8a597051a1039010c8e55e2659b940d3070b326e4f4c5',
logIndex: 0,
mint: '0xe043da617250000',
})

const sourceHash = tx.sourceHash()
expect(sourceHash).to.deep.eq(
'0x77fc5994647d128a4d131d273a5e89e0306aac472494068a4f1fceab83dd0735'
)

const encoded = tx.encode()
expect(encoded).to.deep.eq(raw)
const hashed = tx.hash()
expect(hashed).to.deep.eq(hash)

const decoded = DepositTx.decode(raw, {
domain: SourceHashDomain.UserDeposit,
l1BlockHash: tx.l1BlockHash,
logIndex: tx.logIndex,
})
expect(decoded.from).to.deep.eq(tx.from)
expect(decoded.gas).to.deep.eq(BigNumber.from(tx.gas))
expect(decoded.data).to.deep.eq(tx.data)
expect(decoded.to).to.deep.eq(tx.to)
expect(decoded.value).to.deep.eq(BigNumber.from(tx.value))
expect(decoded.domain).to.deep.eq(SourceHashDomain.UserDeposit)
expect(decoded.l1BlockHash).to.deep.eq(tx.l1BlockHash)
expect(decoded.logIndex).to.deep.eq(tx.logIndex)
expect(decoded.mint).to.deep.eq(BigNumber.from(tx.mint))
})
})
})
Loading

0 comments on commit ce05904

Please sign in to comment.