Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"typechain": "7.0.0"
},
"devDependencies": {
"@arbitrum/nitro-contracts": "1.0.0-beta.5",
"@arbitrum/nitro-contracts": "1.0.0-beta.6",
"@nomiclabs/hardhat-ethers": "^2.0.4",
"@types/chai": "^4.2.11",
"@types/mocha": "^9.0.0",
Expand Down
4 changes: 3 additions & 1 deletion src/lib/inbox/inbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,9 @@ export class InboxTools {
return null
}

const delayedAcc = await bridge.inboxAccs(eventInfo.event.messageIndex)
const delayedAcc = await bridge.delayedInboxAccs(
eventInfo.event.messageIndex
)

return { ...eventInfo, delayedAcc: delayedAcc }
}
Expand Down
277 changes: 115 additions & 162 deletions src/lib/message/L1ToL2Message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,10 @@ import {
} from '../dataEntities/signerOrProvider'
import { ArbSdkError } from '../dataEntities/errors'
import { ethers, Overrides } from 'ethers'
import { Address } from '../dataEntities/address'
import { L2TransactionReceipt, RedeemTransaction } from './L2Transaction'
import { getL2Network } from '../../lib/dataEntities/networks'
import { RetryableMessageParams } from '../dataEntities/message'
import { getTransactionReceipt } from '../utils/lib'
import { getTransactionReceipt, isDefined } from '../utils/lib'
import { EventFetcher } from '../utils/eventFetcher'

export enum L2TxnType {
Expand Down Expand Up @@ -93,15 +92,15 @@ export abstract class L1ToL2Message {
* The submit retryable transactions use the typed transaction envelope 2718.
* The id of these transactions is the hash of the RLP encoded transaction.
* @param l2ChainId
* @param fromAddress
* @param fromAddress the aliased address that called the L1 inbox as emitted in the bridge event.
* @param messageNumber
* @param l1BaseFee
* @param destAddress
* @param l2CallValue
* @param l1Value
* @param maxSubmissionFee
* @param excessFeeRefundAddress
* @param callValueRefundAddress
* @param excessFeeRefundAddress refund address specified in the retryable creation. Note the L1 inbox aliases this address if it is a L1 smart contract. The user is expected to provide this value already aliased when needed.
* @param callValueRefundAddress refund address specified in the retryable creation. Note the L1 inbox aliases this address if it is a L1 smart contract. The user is expected to provide this value already aliased when needed.
* @param gasLimit
* @param maxFeePerGas
* @param data
Expand All @@ -112,7 +111,6 @@ export abstract class L1ToL2Message {
fromAddress: string,
messageNumber: BigNumber,
l1BaseFee: BigNumber,

destAddress: string,
l2CallValue: BigNumber,
l1Value: BigNumber,
Expand All @@ -127,16 +125,13 @@ export abstract class L1ToL2Message {
return ethers.utils.stripZeros(value.toHexString())
}

const addressAlias = new Address(fromAddress)

const from = addressAlias.applyAlias()
const chainId = BigNumber.from(l2ChainId)
const msgNum = BigNumber.from(messageNumber)

const fields: any[] = [
formatNumber(chainId),
zeroPad(formatNumber(msgNum), 32),
from.value,
fromAddress,
formatNumber(l1BaseFee),

formatNumber(l1Value),
Expand Down Expand Up @@ -294,169 +289,145 @@ export class L1ToL2MessageReader extends L1ToL2Message {

/**
* Receipt for the successful l2 transaction created by this message.
* @returns TransactionReceipt of the first successful redeem if exists, otherwise null
* @returns TransactionReceipt of the first successful redeem if exists, otherwise the current status of the message.
*/
public async getSuccessfulRedeem(): Promise<TransactionReceipt | null> {
public async getSuccessfulRedeem(): Promise<L1ToL2MessageWaitResult> {
const l2Network = await getL2Network(this.l2Provider)
const eventFetcher = new EventFetcher(this.l2Provider)
const creationReceipt = await this.getRetryableCreationReceipt()

// check the auto redeem, if that worked we dont need to do costly log queries
if (!isDefined(creationReceipt)) {
// retryable was never created, or not created yet
// therefore it cant have been redeemed or be expired
return { status: L1ToL2MessageStatus.NOT_YET_CREATED }
}

if (creationReceipt.status === 0) {
return { status: L1ToL2MessageStatus.CREATION_FAILED }
}

// check the auto redeem first to avoid doing costly log queries in the happy case
const autoRedeem = await this.getAutoRedeemAttempt()
if (autoRedeem && autoRedeem.status === 1) return autoRedeem
if (autoRedeem && autoRedeem.status === 1) {
return { l2TxReceipt: autoRedeem, status: L1ToL2MessageStatus.REDEEMED }
}

if (await this.retryableExists()) {
// the retryable was created and still exists
// therefore it cant have been redeemed or be expired
return { status: L1ToL2MessageStatus.FUNDS_DEPOSITED_ON_L2 }
}

// from this point on we know that the retryable was created but does not exist,
// so the retryable was either successfully redeemed, or it expired

// the auto redeem didnt exist or wasnt successful, look for a later manual redeem
// to do this we need to filter through the whole lifetime of the ticket looking
// for relevant redeem scheduled events
if (creationReceipt) {
let increment = 1000
let fromBlock = await this.l2Provider.getBlock(
creationReceipt.blockNumber
let increment = 1000
let fromBlock = await this.l2Provider.getBlock(creationReceipt.blockNumber)
let timeout = fromBlock.timestamp + l2Network.retryableLifetimeSeconds
const queriedRange: { from: number; to: number }[] = []
const maxBlock = await this.l2Provider.getBlockNumber()
while (fromBlock.number < maxBlock) {
const toBlockNumber = Math.min(fromBlock.number + increment, maxBlock)

// using fromBlock.number would lead to 1 block overlap
// not fixing it here to keep the code simple
const blockRange = { from: fromBlock.number, to: toBlockNumber }
queriedRange.push(blockRange)
const redeemEvents = await eventFetcher.getEvents(
ARB_RETRYABLE_TX_ADDRESS,
ArbRetryableTx__factory,
contract => contract.filters.RedeemScheduled(this.retryableCreationId),
{
fromBlock: blockRange.from,
toBlock: blockRange.to,
}
)
let timeout = fromBlock.timestamp + l2Network.retryableLifetimeSeconds
const queriedRange: { from: number; to: number }[] = []
const maxBlock = await this.l2Provider.getBlockNumber()
while (fromBlock.number < maxBlock) {
const toBlockNumber = Math.min(fromBlock.number + increment, maxBlock)

// using fromBlock.number would lead to 1 block overlap
// not fixing it here to keep the code simple
const blockRange = { from: fromBlock.number, to: toBlockNumber }
queriedRange.push(blockRange)
const redeemEvents = await eventFetcher.getEvents(
ARB_RETRYABLE_TX_ADDRESS,
ArbRetryableTx__factory,
contract =>
contract.filters.RedeemScheduled(this.retryableCreationId),
{
fromBlock: blockRange.from,
toBlock: blockRange.to,
}
)
const successfulRedeem = (
await Promise.all(
redeemEvents.map(e =>
this.l2Provider.getTransactionReceipt(e.event.retryTxHash)
)
const successfulRedeem = (
await Promise.all(
redeemEvents.map(e =>
this.l2Provider.getTransactionReceipt(e.event.retryTxHash)
)
).filter(r => r.status === 1)
if (successfulRedeem.length > 1)
throw new ArbSdkError(
`Unexpected number of successful redeems. Expected only one redeem for ticket ${this.retryableCreationId}, but found ${successfulRedeem.length}.`
)
).filter(r => r.status === 1)
if (successfulRedeem.length > 1)
throw new ArbSdkError(
`Unexpected number of successful redeems. Expected only one redeem for ticket ${this.retryableCreationId}, but found ${successfulRedeem.length}.`
)
if (successfulRedeem.length == 1)
return {
l2TxReceipt: successfulRedeem[0],
status: L1ToL2MessageStatus.REDEEMED,
}

const toBlock = await this.l2Provider.getBlock(toBlockNumber)
if (toBlock.timestamp > timeout) {
// Check for LifetimeExtended event
while (queriedRange.length > 0) {
const blockRange = queriedRange.shift()
const keepaliveEvents = await eventFetcher.getEvents(
ARB_RETRYABLE_TX_ADDRESS,
ArbRetryableTx__factory,
contract =>
contract.filters.LifetimeExtended(this.retryableCreationId),
{ fromBlock: blockRange!.from, toBlock: blockRange!.to }
)
if (successfulRedeem.length == 1) return successfulRedeem[0]

const toBlock = await this.l2Provider.getBlock(toBlockNumber)
if (toBlock.timestamp > timeout) {
// Check for LifetimeExtended event
while (queriedRange.length > 0) {
const blockRange = queriedRange.shift()
const keepaliveEvents = await eventFetcher.getEvents(
ARB_RETRYABLE_TX_ADDRESS,
ArbRetryableTx__factory,
contract =>
contract.filters.LifetimeExtended(this.retryableCreationId),
{
fromBlock: blockRange!.from,
toBlock: blockRange!.to,
}
)
if (keepaliveEvents.length > 0) {
timeout = keepaliveEvents
.map(e => e.event.newTimeout.toNumber())
.sort()
.reverse()[0]
break
}
if (keepaliveEvents.length > 0) {
timeout = keepaliveEvents
.map(e => e.event.newTimeout.toNumber())
.sort()
.reverse()[0]
break
}
if (toBlock.timestamp > timeout) break
// It is possible to have another keepalive in the last range as it might include block after previous timeout
while (queriedRange.length > 1) queriedRange.shift()
}
const processedSeconds = toBlock.timestamp - fromBlock.timestamp
if (processedSeconds != 0) {
// find the increment that cover ~ 1 day
increment = Math.ceil((increment * 86400) / processedSeconds)
}

fromBlock = toBlock
// the retryable no longer exists, but we've searched beyond the timeout
// so it must have expired
if (toBlock.timestamp > timeout) break
// It is possible to have another keepalive in the last range as it might include block after previous timeout
while (queriedRange.length > 1) queriedRange.shift()
}
const processedSeconds = toBlock.timestamp - fromBlock.timestamp
if (processedSeconds != 0) {
// find the increment that cover ~ 1 day
increment = Math.ceil((increment * 86400) / processedSeconds)
}

fromBlock = toBlock
}
return null

// we know from earlier that the retryable no longer exists, so if we havent found the redemption
// we know that it must have expired
return { status: L1ToL2MessageStatus.EXPIRED }
}

/**
* Has this message expired. Once expired the retryable ticket can no longer be redeemed.
* @deprecated Will be removed in v3.0.0
* @returns
*/
public async isExpired(): Promise<boolean> {
const currentTimestamp = BigNumber.from(
(await this.l2Provider.getBlock('latest')).timestamp
)
const timeoutTimestamp = await this.getTimeout()

// timeoutTimestamp returns the timestamp at which the retryable ticket expires
// it can also return 0 if the ticket l2Tx does not exist
return currentTimestamp.gte(timeoutTimestamp)
return await this.retryableExists()
}

protected async receiptsToStatus(
retryableCreationReceipt: TransactionReceipt | null,
successfulRedeemReceipt: TransactionReceipt | null
): Promise<L1ToL2MessageStatus> {
// happy path for non auto redeemable messages
// NOT_YET_CREATED -> FUNDS_DEPOSITED
// these will later either transition to EXPIRED after the timeout
// (this is what happens to eth deposits since they don't need to be
// redeemed) or to REDEEMED if the retryable is manually redeemed

// happy path for auto redeemable messages
// NOT_YET_CREATED -> FUNDS_DEPOSITED -> REDEEMED
// an attempt to auto redeem executable messages is made immediately
// after the retryable is created - which if successful will transition
// the status to REDEEMED. If the auto redeem fails then the ticket
// will transition to REDEEMED if manually redeemed, or EXPIRE
// after the timeout is reached and the ticket is not redeemed

// we test the retryable receipt first as if this doesnt exist there's
// no point looking to see if expired
if (!retryableCreationReceipt) {
return L1ToL2MessageStatus.NOT_YET_CREATED
}
if (retryableCreationReceipt.status === 0) {
return L1ToL2MessageStatus.CREATION_FAILED
}

// ticket created, has it been auto redeemed?
if (successfulRedeemReceipt && successfulRedeemReceipt.status === 1) {
return L1ToL2MessageStatus.REDEEMED
}
private async retryableExists(): Promise<boolean> {
try {
const timeoutTimestamp = await this.getTimeout()
const currentTimestamp = BigNumber.from(
(await this.l2Provider.getBlock('latest')).timestamp
)

// not redeemed, has it now expired
if (await this.isExpired()) {
return L1ToL2MessageStatus.EXPIRED
// timeoutTimestamp returns the timestamp at which the retryable ticket expires
// it can also return 0 if the ticket l2Tx does not exist
return currentTimestamp.gte(timeoutTimestamp)
} catch (err) {
return false
}

// ticket was created but not redeemed
// this could be because
// a) the ticket is non auto redeemable (l2GasPrice == 0 || l2GasLimit == 0) -
// this is usually an eth deposit. But in some rare case the
// user may still want to manually redeem it
// b) the ticket is auto redeemable, but the auto redeem failed

// the fact that the auto redeem failed isn't usually useful to the user
// if they're doing an eth deposit they don't care about redemption
// and if they do want execution to occur they will know that they're
// here because the auto redeem failed. If they really want to check
// they can fetch the auto redeem receipt and check the status on it
return L1ToL2MessageStatus.FUNDS_DEPOSITED_ON_L2
}

public async status(): Promise<L1ToL2MessageStatus> {
return this.receiptsToStatus(
await this.getRetryableCreationReceipt(),
await this.getSuccessfulRedeem()
)
return (await this.getSuccessfulRedeem()).status
}

/**
Expand All @@ -476,29 +447,11 @@ export class L1ToL2MessageReader extends L1ToL2Message {
timeout = 900000
): Promise<L1ToL2MessageWaitResult> {
// try to wait for the retryable ticket to be created
const retryableCreationReceipt = await this.getRetryableCreationReceipt(
const _retryableCreationReceipt = await this.getRetryableCreationReceipt(
confirmations,
timeout
)

// get the successful redeem transaction, if one exists
const l2TxReceipt = await this.getSuccessfulRedeem()

const status = await this.receiptsToStatus(
retryableCreationReceipt,
l2TxReceipt
)
if (status === L1ToL2MessageStatus.REDEEMED) {
return {
// if the status is redeemed we know the l2TxReceipt must exist
l2TxReceipt: l2TxReceipt!,
status,
}
} else {
return {
status,
}
}
return await this.getSuccessfulRedeem()
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/lib/message/L2ToL1Message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export class L2ToL1MessageReader extends L2ToL1Message {
this.l1Provider
)

return outbox.callStatic.spent(this.event.position)
return outbox.callStatic.isSpent(this.event.position)
}

/**
Expand Down
Loading