forked from ethereum-optimism/optimism
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: deposit tx js helpers (ethereum-optimism#352)
* feat: js deposit tx Hardhat task now lists the tx hash * rollup: new deposit hashing * nice api! * fix: dockerfile
- Loading branch information
Showing
6 changed files
with
368 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.