From 17cf04d4c87dee77ae69d81edc4919dc297bfc3e Mon Sep 17 00:00:00 2001 From: fredlacs <32464905+fredlacs@users.noreply.github.com> Date: Mon, 27 Jun 2022 15:10:05 +0100 Subject: [PATCH 01/19] docs: add expectation of address aliasing on retryable function parameters --- src/lib/message/L1ToL2Message.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index cf641d71ef..1f2ff06897 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -93,15 +93,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 address that called the L1 inbox. This is expected to be the non-aliased address, since this function will apply the alias. * @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 @@ -112,7 +112,6 @@ export abstract class L1ToL2Message { fromAddress: string, messageNumber: BigNumber, l1BaseFee: BigNumber, - destAddress: string, l2CallValue: BigNumber, l1Value: BigNumber, From 69a7a4a48a9ebcd1e435b6489be0de7312df085d Mon Sep 17 00:00:00 2001 From: gzeon <95478735+gzeoneth@users.noreply.github.com> Date: Fri, 24 Jun 2022 22:15:27 +0800 Subject: [PATCH 02/19] fix: race condition causing isExpired to throw --- src/lib/message/L1ToL2Message.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index 1f2ff06897..ed700def68 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -432,7 +432,17 @@ export class L1ToL2MessageReader extends L1ToL2Message { } // not redeemed, has it now expired - if (await this.isExpired()) { + try { + if (await this.isExpired()) { + return L1ToL2MessageStatus.EXPIRED + } + } catch (error) { + // this can happen due to a race condition that + // the retryable is redeemed/expired in between the calls + const successfulRedeemReceiptRetry = await this.getSuccessfulRedeem() + if (successfulRedeemReceiptRetry && successfulRedeemReceiptRetry.status === 1) { + return L1ToL2MessageStatus.REDEEMED + } return L1ToL2MessageStatus.EXPIRED } From 21196b809b16a4ab26e2d1936a23618e222d270c Mon Sep 17 00:00:00 2001 From: gzeon <95478735+gzeoneth@users.noreply.github.com> Date: Fri, 24 Jun 2022 22:52:26 +0800 Subject: [PATCH 03/19] fix: delayedInboxAccs --- src/lib/inbox/inbox.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/inbox/inbox.ts b/src/lib/inbox/inbox.ts index eb2c36b411..8fba328c83 100644 --- a/src/lib/inbox/inbox.ts +++ b/src/lib/inbox/inbox.ts @@ -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 } } From d6f548db7de45732b47f6128167b76d318127355 Mon Sep 17 00:00:00 2001 From: gzeon <95478735+gzeoneth@users.noreply.github.com> Date: Sat, 25 Jun 2022 00:26:23 +0800 Subject: [PATCH 04/19] fix: use Outbox.isSpent --- src/lib/message/L2ToL1Message.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/message/L2ToL1Message.ts b/src/lib/message/L2ToL1Message.ts index a6a80b9b9e..51b72ab768 100644 --- a/src/lib/message/L2ToL1Message.ts +++ b/src/lib/message/L2ToL1Message.ts @@ -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) } /** From 849df0c9339075c2afe56fa959e69547e0c5e438 Mon Sep 17 00:00:00 2001 From: fredlacs <32464905+fredlacs@users.noreply.github.com> Date: Mon, 27 Jun 2022 15:20:15 +0100 Subject: [PATCH 05/19] chore: version bump nitro package --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index aeb340ab8c..b4b7ed4d20 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 193a633a58..108ad9865c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,10 +10,10 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" -"@arbitrum/nitro-contracts@1.0.0-beta.5": - version "1.0.0-beta.5" - resolved "https://registry.yarnpkg.com/@arbitrum/nitro-contracts/-/nitro-contracts-1.0.0-beta.5.tgz#b4ca2a8d99b8a108e7dc412f6c2cb74818d5099a" - integrity sha512-hEpRXIL8S6AfTt6dqJsKCa84JufhuXuRgm2BnximdRrhJ8ukqR6Pt/mndYMa4nWRRTBMYkIxy6EA5iYxDwxWAQ== +"@arbitrum/nitro-contracts@1.0.0-beta.6": + version "1.0.0-beta.6" + resolved "https://registry.yarnpkg.com/@arbitrum/nitro-contracts/-/nitro-contracts-1.0.0-beta.6.tgz#86b4997ea373a5410fbc42c6a9097f0ab07dfa34" + integrity sha512-UyCqUsd5ew0DUZ+MxgfP7fdyGBDoOujeXbjhdg4agJW1ld3lg+E7dgKfN6S0j2v2l/+Yd63HqEp24O5bgSzxTg== dependencies: "@openzeppelin/contracts" "4.5.0" "@openzeppelin/contracts-upgradeable" "4.5.2" @@ -1288,7 +1288,7 @@ arb-bridge-peripherals@1.0.10: optionalDependencies: "@openzeppelin/upgrades-core" "^1.7.6" -arbos-precompiles@1.0.2, arbos-precompiles@^1.0.2: +arbos-precompiles@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/arbos-precompiles/-/arbos-precompiles-1.0.2.tgz#7bebd5963aef972cd259eb41f3116ea065013ea6" integrity sha512-1dOFYFJUN0kKoofh6buZJ8qCqTs+oLGSsGzHI0trA/Pka/TCERflCRsNVxez2lihOvK7MT/a2RA8AepKtBXdPQ== From 48a2f5c5e831fb276dd978831a14830e7c317a71 Mon Sep 17 00:00:00 2001 From: fredlacs <32464905+fredlacs@users.noreply.github.com> Date: Mon, 27 Jun 2022 15:31:42 +0100 Subject: [PATCH 06/19] chore: lint --- src/lib/message/L1ToL2Message.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index ed700def68..39a33b8467 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -440,7 +440,10 @@ export class L1ToL2MessageReader extends L1ToL2Message { // this can happen due to a race condition that // the retryable is redeemed/expired in between the calls const successfulRedeemReceiptRetry = await this.getSuccessfulRedeem() - if (successfulRedeemReceiptRetry && successfulRedeemReceiptRetry.status === 1) { + if ( + successfulRedeemReceiptRetry && + successfulRedeemReceiptRetry.status === 1 + ) { return L1ToL2MessageStatus.REDEEMED } return L1ToL2MessageStatus.EXPIRED From 5c98eb69a20b594cace8560804215ff67a531715 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 27 Jun 2022 17:03:10 +0200 Subject: [PATCH 07/19] Added retryable exists check --- src/lib/message/L1ToL2Message.ts | 77 +++++++++++++++++++------------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index 39a33b8467..b03c29eca7 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -293,16 +293,34 @@ 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 null. + * Returns expired true if retryable expired without being redeemed, otherwise false. */ - public async getSuccessfulRedeem(): Promise { + public async getSuccessfulRedeem(): Promise< + | { redeemReceipt: TransactionReceipt | null; expired: false } + | { redeemReceipt: null; expired: true } + > { const l2Network = await getL2Network(this.l2Provider) const eventFetcher = new EventFetcher(this.l2Provider) const creationReceipt = await this.getRetryableCreationReceipt() + if (creationReceipt) { + if (!(await this.retryableExists())) { + // the retryable was created, but no longer exists + // so it must have expired + return { + redeemReceipt: null, + expired: true, + } + } + } // check the auto redeem, if that worked we dont need to do costly log queries const autoRedeem = await this.getAutoRedeemAttempt() - if (autoRedeem && autoRedeem.status === 1) return autoRedeem + if (autoRedeem && autoRedeem.status === 1) + return { + redeemReceipt: autoRedeem, + expired: false, + } // 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 @@ -343,7 +361,8 @@ export class L1ToL2MessageReader extends L1ToL2Message { 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 successfulRedeem[0] + if (successfulRedeem.length == 1) + return { redeemReceipt: successfulRedeem[0], expired: false } const toBlock = await this.l2Provider.getBlock(toBlockNumber) if (toBlock.timestamp > timeout) { @@ -381,27 +400,38 @@ export class L1ToL2MessageReader extends L1ToL2Message { fromBlock = toBlock } } - return null + return { redeemReceipt: null, expired: false } } /** * 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 { - const currentTimestamp = BigNumber.from( - (await this.l2Provider.getBlock('latest')).timestamp - ) - const timeoutTimestamp = await this.getTimeout() + return await this.retryableExists() + } - // 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) + private async retryableExists(): Promise { + try { + 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) + } catch (err) { + return false + } } protected async receiptsToStatus( retryableCreationReceipt: TransactionReceipt | null, - successfulRedeemReceipt: TransactionReceipt | null + redeemInfo: + | { redeemReceipt: TransactionReceipt | null; expired: false } + | { redeemReceipt: null; expired: true } ): Promise { // happy path for non auto redeemable messages // NOT_YET_CREATED -> FUNDS_DEPOSITED @@ -427,27 +457,12 @@ export class L1ToL2MessageReader extends L1ToL2Message { } // ticket created, has it been auto redeemed? - if (successfulRedeemReceipt && successfulRedeemReceipt.status === 1) { + if (redeemInfo.redeemReceipt && redeemInfo.redeemReceipt.status === 1) { return L1ToL2MessageStatus.REDEEMED } // not redeemed, has it now expired - try { - if (await this.isExpired()) { - return L1ToL2MessageStatus.EXPIRED - } - } catch (error) { - // this can happen due to a race condition that - // the retryable is redeemed/expired in between the calls - const successfulRedeemReceiptRetry = await this.getSuccessfulRedeem() - if ( - successfulRedeemReceiptRetry && - successfulRedeemReceiptRetry.status === 1 - ) { - return L1ToL2MessageStatus.REDEEMED - } - return L1ToL2MessageStatus.EXPIRED - } + if (redeemInfo.expired) return L1ToL2MessageStatus.EXPIRED // ticket was created but not redeemed // this could be because @@ -503,7 +518,7 @@ export class L1ToL2MessageReader extends L1ToL2Message { if (status === L1ToL2MessageStatus.REDEEMED) { return { // if the status is redeemed we know the l2TxReceipt must exist - l2TxReceipt: l2TxReceipt!, + l2TxReceipt: l2TxReceipt.redeemReceipt!, status, } } else { From 8a7c8bf791bfc07bda10ab658ab4bf5af289113c Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 27 Jun 2022 18:20:37 +0200 Subject: [PATCH 08/19] Updated redeem or expire --- src/lib/message/L1ToL2Message.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index b03c29eca7..38d7b212bf 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -303,16 +303,9 @@ export class L1ToL2MessageReader extends L1ToL2Message { const l2Network = await getL2Network(this.l2Provider) const eventFetcher = new EventFetcher(this.l2Provider) const creationReceipt = await this.getRetryableCreationReceipt() - if (creationReceipt) { - if (!(await this.retryableExists())) { - // the retryable was created, but no longer exists - // so it must have expired - return { - redeemReceipt: null, - expired: true, - } - } - } + // if retryable was created but no longer exists then we know that it was either + // redeemed or expired + const redeemWasSuccessfulOrExpired = creationReceipt && !await this.retryableExists() // check the auto redeem, if that worked we dont need to do costly log queries const autoRedeem = await this.getAutoRedeemAttempt() @@ -399,6 +392,14 @@ export class L1ToL2MessageReader extends L1ToL2Message { fromBlock = toBlock } + + // we didnt find a redeem transaction + if(redeemWasSuccessfulOrExpired) { + return { + expired: true, + redeemReceipt: null + } + } } return { redeemReceipt: null, expired: false } } From 6412f11594003b83ec11d391d93a9a4d1b53ca1c Mon Sep 17 00:00:00 2001 From: gzeon <95478735+gzeoneth@users.noreply.github.com> Date: Fri, 24 Jun 2022 22:14:57 +0800 Subject: [PATCH 09/19] fix: remove retryable from addr aliasing --- src/lib/message/L1ToL2Message.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index 39a33b8467..f55bdaf265 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -126,16 +126,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), From 111792be17288cc9d34a1b5e27ee20d10587f4cf Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 28 Jun 2022 10:28:13 +0200 Subject: [PATCH 10/19] Update src/lib/message/L1ToL2Message.ts Co-authored-by: gzeon <95478735+gzeoneth@users.noreply.github.com> --- src/lib/message/L1ToL2Message.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index 38d7b212bf..266f0c43d1 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -415,10 +415,10 @@ export class L1ToL2MessageReader extends L1ToL2Message { private async retryableExists(): Promise { try { + const timeoutTimestamp = await this.getTimeout() 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 From 97c5d105811d1094f1e666632d976ddb12f21081 Mon Sep 17 00:00:00 2001 From: fredlacs <32464905+fredlacs@users.noreply.github.com> Date: Tue, 28 Jun 2022 09:29:44 +0100 Subject: [PATCH 11/19] docs: fix parameter expectation --- src/lib/message/L1ToL2Message.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index f55bdaf265..9a9eb16579 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -93,7 +93,7 @@ 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 the address that called the L1 inbox. This is expected to be the non-aliased address, since this function will apply the alias. + * @param fromAddress the aliased address that called the L1 inbox as emitted in the bridge event. * @param messageNumber * @param l1BaseFee * @param destAddress From 55d895464d76ede160a36a5eb1bf3d44567dd2e0 Mon Sep 17 00:00:00 2001 From: fredlacs <32464905+fredlacs@users.noreply.github.com> Date: Tue, 28 Jun 2022 09:30:56 +0100 Subject: [PATCH 12/19] refator: remove unused import --- src/lib/message/L1ToL2Message.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index 9a9eb16579..b15078ba79 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -32,7 +32,6 @@ 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' From 76cbc217d23cb3058883f0fb071222d66683f713 Mon Sep 17 00:00:00 2001 From: fredlacs <32464905+fredlacs@users.noreply.github.com> Date: Tue, 28 Jun 2022 09:37:00 +0100 Subject: [PATCH 13/19] chore: lint --- src/lib/message/L1ToL2Message.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index 0203999d65..ba02bcceea 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -301,7 +301,8 @@ export class L1ToL2MessageReader extends L1ToL2Message { const creationReceipt = await this.getRetryableCreationReceipt() // if retryable was created but no longer exists then we know that it was either // redeemed or expired - const redeemWasSuccessfulOrExpired = creationReceipt && !await this.retryableExists() + const redeemWasSuccessfulOrExpired = + creationReceipt && !(await this.retryableExists()) // check the auto redeem, if that worked we dont need to do costly log queries const autoRedeem = await this.getAutoRedeemAttempt() @@ -390,10 +391,10 @@ export class L1ToL2MessageReader extends L1ToL2Message { } // we didnt find a redeem transaction - if(redeemWasSuccessfulOrExpired) { + if (redeemWasSuccessfulOrExpired) { return { expired: true, - redeemReceipt: null + redeemReceipt: null, } } } From 97656268b704330d447865b22ce4eefd4a7a236d Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 28 Jun 2022 11:44:28 +0200 Subject: [PATCH 14/19] Updated successful redeem logic --- src/lib/message/L1ToL2Message.ts | 167 ++++++++++++++++--------------- src/lib/utils/lib.ts | 2 +- 2 files changed, 88 insertions(+), 81 deletions(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index ba02bcceea..aac20491d8 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -35,7 +35,7 @@ import { ethers, Overrides } from 'ethers' 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 { @@ -299,10 +299,18 @@ export class L1ToL2MessageReader extends L1ToL2Message { const l2Network = await getL2Network(this.l2Provider) const eventFetcher = new EventFetcher(this.l2Provider) const creationReceipt = await this.getRetryableCreationReceipt() - // if retryable was created but no longer exists then we know that it was either - // redeemed or expired - const redeemWasSuccessfulOrExpired = - creationReceipt && !(await this.retryableExists()) + if (!isDefined(creationReceipt)) { + // retryable was never created + return { redeemReceipt: null, expired: false } + } + + if (await this.retryableExists()) { + // if the retryable still exists then then it cant have been redeemed or be expired + return { redeemReceipt: null, expired: false } + } + + // 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 // check the auto redeem, if that worked we dont need to do costly log queries const autoRedeem = await this.getAutoRedeemAttempt() @@ -315,90 +323,89 @@ export class L1ToL2MessageReader extends L1ToL2Message { // 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 { redeemReceipt: successfulRedeem[0], expired: false } + + 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 { redeemReceipt: successfulRedeem[0], expired: false } - - 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) + // we will have updated the timestamp by this point to the current timeout + // if we've already surpassed that then we've checked the full range and not found + // a redeem, so we must have expired + if (toBlock.timestamp > timeout) { + return { + expired: true, + redeemReceipt: null, + } } - - fromBlock = toBlock + // 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() } - - // we didnt find a redeem transaction - if (redeemWasSuccessfulOrExpired) { - return { - expired: true, - redeemReceipt: null, - } + const processedSeconds = toBlock.timestamp - fromBlock.timestamp + if (processedSeconds != 0) { + // find the increment that cover ~ 1 day + increment = Math.ceil((increment * 86400) / processedSeconds) } + + fromBlock = toBlock + } + + // if we're here it means that we were trying to search beyond the current block number + // that means that means the timeout window hasnt closed yet. The transaction has neither + // been redeemed nor expired, but could do either in the future + return { + expired: false, + redeemReceipt: null, } - return { redeemReceipt: null, expired: false } } /** diff --git a/src/lib/utils/lib.ts b/src/lib/utils/lib.ts index 29014f5778..d0736e3daa 100644 --- a/src/lib/utils/lib.ts +++ b/src/lib/utils/lib.ts @@ -50,5 +50,5 @@ export const getTransactionReceipt = async ( } } -export const isDefined = (val: unknown): boolean => +export const isDefined = (val: T | null | undefined): val is T => typeof val !== 'undefined' && val !== null From bb5e83cb72b82e3370cb8d90dc2ccdbbb17f8593 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 28 Jun 2022 11:48:46 +0200 Subject: [PATCH 15/19] Updated comments --- src/lib/message/L1ToL2Message.ts | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index aac20491d8..9decacb8f2 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -300,12 +300,14 @@ export class L1ToL2MessageReader extends L1ToL2Message { const eventFetcher = new EventFetcher(this.l2Provider) const creationReceipt = await this.getRetryableCreationReceipt() if (!isDefined(creationReceipt)) { - // retryable was never created + // retryable was never created, or not created yet + // therefore it cant have been redeemed or be expired return { redeemReceipt: null, expired: false } } if (await this.retryableExists()) { - // if the retryable still exists then then it cant have been redeemed or be expired + // the retryable was created and still exists + // therefore it cant have been redeemed or be expired return { redeemReceipt: null, expired: false } } @@ -378,15 +380,9 @@ export class L1ToL2MessageReader extends L1ToL2Message { break } } - // we will have updated the timestamp by this point to the current timeout - // if we've already surpassed that then we've checked the full range and not found - // a redeem, so we must have expired - if (toBlock.timestamp > timeout) { - return { - expired: true, - redeemReceipt: null, - } - } + // 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() } @@ -399,11 +395,10 @@ export class L1ToL2MessageReader extends L1ToL2Message { fromBlock = toBlock } - // if we're here it means that we were trying to search beyond the current block number - // that means that means the timeout window hasnt closed yet. The transaction has neither - // been redeemed nor expired, but could do either in the future + // 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 { - expired: false, + expired: true, redeemReceipt: null, } } From 408d966e1197d26755efd992bfbf4778b8de14e6 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 28 Jun 2022 11:50:23 +0200 Subject: [PATCH 16/19] Formatting --- src/lib/message/L1ToL2Message.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index 9decacb8f2..9534b7d377 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -316,11 +316,9 @@ export class L1ToL2MessageReader extends L1ToL2Message { // check the auto redeem, if that worked we dont need to do costly log queries const autoRedeem = await this.getAutoRedeemAttempt() - if (autoRedeem && autoRedeem.status === 1) - return { - redeemReceipt: autoRedeem, - expired: false, - } + if (autoRedeem && autoRedeem.status === 1) { + return { redeemReceipt: autoRedeem, expired: false } + } // 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 @@ -397,10 +395,7 @@ export class L1ToL2MessageReader extends L1ToL2Message { // 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 { - expired: true, - redeemReceipt: null, - } + return { expired: true, redeemReceipt: null } } /** From 55ade551cab9243229d55ea0c336c30be8831faa Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 28 Jun 2022 12:00:28 +0200 Subject: [PATCH 17/19] Re-ordered to improve happy case --- src/lib/message/L1ToL2Message.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index 9534b7d377..a58dcecc45 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -305,6 +305,12 @@ export class L1ToL2MessageReader extends L1ToL2Message { return { redeemReceipt: null, expired: false } } + // 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 { redeemReceipt: autoRedeem, expired: false } + } + if (await this.retryableExists()) { // the retryable was created and still exists // therefore it cant have been redeemed or be expired @@ -314,12 +320,6 @@ export class L1ToL2MessageReader extends L1ToL2Message { // 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 - // check the auto redeem, if that worked we dont need to do costly log queries - const autoRedeem = await this.getAutoRedeemAttempt() - if (autoRedeem && autoRedeem.status === 1) { - return { redeemReceipt: autoRedeem, expired: false } - } - // 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 From 32bab70915020ec8e9f1599931b59910aa21ed5f Mon Sep 17 00:00:00 2001 From: fredlacs <32464905+fredlacs@users.noreply.github.com> Date: Tue, 28 Jun 2022 11:28:58 +0100 Subject: [PATCH 18/19] refactor: consolidate return types --- src/lib/message/L1ToL2Message.ts | 102 ++++++------------------------- 1 file changed, 17 insertions(+), 85 deletions(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index a58dcecc45..42411384c6 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -292,29 +292,31 @@ export class L1ToL2MessageReader extends L1ToL2Message { * @returns TransactionReceipt of the first successful redeem if exists, otherwise null. * Returns expired true if retryable expired without being redeemed, otherwise false. */ - public async getSuccessfulRedeem(): Promise< - | { redeemReceipt: TransactionReceipt | null; expired: false } - | { redeemReceipt: null; expired: true } - > { + public async getSuccessfulRedeem(): Promise { const l2Network = await getL2Network(this.l2Provider) const eventFetcher = new EventFetcher(this.l2Provider) const creationReceipt = await this.getRetryableCreationReceipt() + if (!isDefined(creationReceipt)) { // retryable was never created, or not created yet // therefore it cant have been redeemed or be expired - return { redeemReceipt: null, expired: false } + 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 { redeemReceipt: autoRedeem, expired: false } + 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 { redeemReceipt: null, expired: false } + return { status: L1ToL2MessageStatus.FUNDS_DEPOSITED_ON_L2 } } // from this point on we know that the retryable was created but does not exist, @@ -356,7 +358,10 @@ export class L1ToL2MessageReader extends L1ToL2Message { `Unexpected number of successful redeems. Expected only one redeem for ticket ${this.retryableCreationId}, but found ${successfulRedeem.length}.` ) if (successfulRedeem.length == 1) - return { redeemReceipt: successfulRedeem[0], expired: false } + return { + l2TxReceipt: successfulRedeem[0], + status: L1ToL2MessageStatus.REDEEMED, + } const toBlock = await this.l2Provider.getBlock(toBlockNumber) if (toBlock.timestamp > timeout) { @@ -395,7 +400,7 @@ export class L1ToL2MessageReader extends L1ToL2Message { // 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 { expired: true, redeemReceipt: null } + return { status: L1ToL2MessageStatus.EXPIRED } } /** @@ -422,63 +427,8 @@ export class L1ToL2MessageReader extends L1ToL2Message { } } - protected async receiptsToStatus( - retryableCreationReceipt: TransactionReceipt | null, - redeemInfo: - | { redeemReceipt: TransactionReceipt | null; expired: false } - | { redeemReceipt: null; expired: true } - ): Promise { - // 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 (redeemInfo.redeemReceipt && redeemInfo.redeemReceipt.status === 1) { - return L1ToL2MessageStatus.REDEEMED - } - - // not redeemed, has it now expired - if (redeemInfo.expired) return L1ToL2MessageStatus.EXPIRED - - // 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 { - return this.receiptsToStatus( - await this.getRetryableCreationReceipt(), - await this.getSuccessfulRedeem() - ) + return (await this.getSuccessfulRedeem()).status } /** @@ -498,29 +448,11 @@ export class L1ToL2MessageReader extends L1ToL2Message { timeout = 900000 ): Promise { // 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.redeemReceipt!, - status, - } - } else { - return { - status, - } - } + return await this.getSuccessfulRedeem() } /** From a9b92c126bd4ca9514ef28eb84ee3585374fbb03 Mon Sep 17 00:00:00 2001 From: fredlacs <32464905+fredlacs@users.noreply.github.com> Date: Tue, 28 Jun 2022 11:32:28 +0100 Subject: [PATCH 19/19] docs: fix return value documentation --- src/lib/message/L1ToL2Message.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/message/L1ToL2Message.ts b/src/lib/message/L1ToL2Message.ts index 42411384c6..fb49c2e981 100644 --- a/src/lib/message/L1ToL2Message.ts +++ b/src/lib/message/L1ToL2Message.ts @@ -289,8 +289,7 @@ 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 expired true if retryable expired without being redeemed, otherwise false. + * @returns TransactionReceipt of the first successful redeem if exists, otherwise the current status of the message. */ public async getSuccessfulRedeem(): Promise { const l2Network = await getL2Network(this.l2Provider)