Skip to content

Commit

Permalink
chore: Retry failed transactions in the wire task
Browse files Browse the repository at this point in the history
  • Loading branch information
janjakubnanista committed Jan 9, 2024
1 parent 126ed3b commit 13965b7
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 36 deletions.
61 changes: 44 additions & 17 deletions packages/ua-devtools-evm-hardhat/src/tasks/oapp/wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,19 @@ import {
import { createSignAndSend, OmniTransaction } from '@layerzerolabs/devtools'
import { createProgressBar, printLogo, printRecords, render } from '@layerzerolabs/io-devtools/swag'
import { validateAndTransformOappConfig } from '@/utils/taskHelpers'
import { SignAndSendResult } from '@layerzerolabs/devtools'

interface TaskArgs {
oappConfig: string
logLevel?: string
ci?: boolean
}

const action: ActionType<TaskArgs> = async ({ oappConfig: oappConfigPath, logLevel = 'info', ci = false }) => {
const action: ActionType<TaskArgs> = async ({
oappConfig: oappConfigPath,
logLevel = 'info',
ci = false,
}): Promise<SignAndSendResult> => {
printLogo()

// We only want to be asking users for input if we are not in interactive mode
Expand Down Expand Up @@ -59,7 +64,7 @@ const action: ActionType<TaskArgs> = async ({ oappConfig: oappConfigPath, logLev
if (transactions.length === 0) {
logger.info(`The OApp is wired, no action is necessary`)

return []
return [[], [], []]
}

// Tell the user about the transactions
Expand All @@ -78,29 +83,47 @@ const action: ActionType<TaskArgs> = async ({ oappConfig: oappConfigPath, logLev
if (previewTransactions) printRecords(transactions.map(formatOmniTransaction))

// Now ask the user whether they want to go ahead with signing them
//
// If they don't, we'll just return the list of pending transactions
const shouldSubmit = isInteractive
? await promptToContinue(`Would you like to submit the required transactions?`)
: true
if (!shouldSubmit) return logger.verbose(`User cancelled the operation, exiting`), undefined
if (!shouldSubmit) return logger.verbose(`User cancelled the operation, exiting`), [[], [], transactions]

// The last step is to execute those transactions
//
// For now we are only allowing sign & send using the accounts confgiured in hardhat config
const signAndSend = createSignAndSend(createSignerFactory())

// We'll use this variable to store the transactions to be signed
//
// In case of an error, when a user decides to retry, we'll update this array
// with the transactions yet to be signed
let transactionsToSign = transactions

// We will run an infinite retry loop when signing the transactions
//
// This loop will be broken in these scenarios:
// - if all the transactions succeed
// - if some of the transactions fail
// - in the interactive mode, if the user decides not to retry the failed transactions
// - in the non-interactive mode
//
// eslint-disable-next-line no-constant-condition
while (true) {
// Now we render a progressbar to monitor the task progress
const progressBar = render(createProgressBar({ before: 'Signing... ', after: ` 0/${transactions.length}` }))
const progressBar = render(
createProgressBar({ before: 'Signing... ', after: ` 0/${transactionsToSign.length}` })
)

logger.verbose(`Sending the transactions`)
const [successful, errors] = await signAndSend(transactions, (result, results) => {
const [successful, errors, pendingTransactions] = await signAndSend(transactionsToSign, (result, results) => {
// We'll keep updating the progressbar as we sign the transactions
progressBar.rerender(
createProgressBar({
progress: results.length / transactions.length,
progress: results.length / transactionsToSign.length,
before: 'Signing... ',
after: ` ${results.length}/${transactions.length}`,
after: ` ${results.length}/${transactionsToSign.length}`,
})
)
})
Expand All @@ -114,7 +137,7 @@ const action: ActionType<TaskArgs> = async ({ oappConfig: oappConfigPath, logLev

logger.info(
pluralizeNoun(
transactions.length,
successful.length,
`Successfully sent 1 transaction`,
`Successfully sent ${successful.length} transactions`
)
Expand All @@ -124,23 +147,24 @@ const action: ActionType<TaskArgs> = async ({ oappConfig: oappConfigPath, logLev
if (errors.length === 0) {
logger.info(`${printBoolean(true)} Your OApp is now configured`)

return [successful, errors]
return [successful, errors, pendingTransactions]
}

// Now we bring the bad news to the user
logger.error(
pluralizeNoun(
transactions.length,
`Failed to send 1 transaction`,
`Failed to send ${errors.length} transactions`
)
pluralizeNoun(errors.length, `Failed to send 1 transaction`, `Failed to send ${errors.length} transactions`)
)

// FIXME Show errors along with the transactions
const previewErrors = isInteractive
? await promptToContinue(`Would you like to preview the failed transactions?`)
: true
if (previewErrors) printRecords(transactions.map(formatOmniTransaction))
if (previewErrors)
printRecords(
errors.map(({ error, transaction }) => ({
error: String(error),
...formatOmniTransaction(transaction),
}))
)

// We'll ask the user if they want to retry if we're in interactive mode
//
Expand All @@ -149,8 +173,11 @@ const action: ActionType<TaskArgs> = async ({ oappConfig: oappConfigPath, logLev
if (!retry) {
logger.error(`${printBoolean(false)} Failed to configure the OApp`)

return [successful, errors]
return [successful, errors, pendingTransactions]
}

// If we are retrying, we'll update the array of pendingTransactions with the failed transactions plus the pending transactions
transactionsToSign = pendingTransactions
}
}
task(TASK_LZ_WIRE_OAPP, 'Wire LayerZero OApp')
Expand Down
92 changes: 73 additions & 19 deletions tests/ua-devtools-evm-hardhat-test/test/task/oapp/wire.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,16 @@ describe('task/oapp/wire', () => {
expect(promptToContinueMock).toHaveBeenCalledTimes(2)
})

it('should return undefined if the user decides not to continue', async () => {
it('should return a list of pending transactions if the user decides not to continue', async () => {
const oappConfig = configPathFixture('valid.config.connected.js')

promptToContinueMock.mockResolvedValue(false)

const result = await hre.run(TASK_LZ_WIRE_OAPP, { oappConfig })
const [successful, errors, pending] = await hre.run(TASK_LZ_WIRE_OAPP, { oappConfig })

expect(result).toBeUndefined()
expect(successful).toEqual([])
expect(errors).toEqual([])
expect(pending).toHaveLength(2)
expect(promptToContinueMock).toHaveBeenCalledTimes(2)
})

Expand All @@ -164,6 +166,12 @@ describe('task/oapp/wire', () => {
})

describe('if a transaction fails', () => {
// Helper matcher object that checks for OmniPoint objects
const expectOmniPoint = { address: expect.any(String), eid: expect.any(Number) }
// Helper matcher object that checks for OmniTransaction objects
const expectTransaction = { data: expect.any(String), point: expectOmniPoint }
const expectTransactionWithReceipt = { receipt: expect.any(Object), transaction: expectTransaction }

let sendTransactionMock: jest.SpyInstance

beforeEach(() => {
Expand All @@ -174,25 +182,26 @@ describe('task/oapp/wire', () => {
sendTransactionMock.mockRestore()
})

it('should return a list of failed transactions in the CI mode', async () => {
it.only('should return a list of failed transactions in the CI mode', async () => {
const error = new Error('Oh god dammit')

// We want to make the fail
sendTransactionMock.mockRejectedValue(error)

const oappConfig = configPathFixture('valid.config.connected.js')
const [successful, errors] = await hre.run(TASK_LZ_WIRE_OAPP, { oappConfig, ci: true })
const [successful, errors, pending] = await hre.run(TASK_LZ_WIRE_OAPP, { oappConfig, ci: true })

expect(successful).toEqual([])
expect(errors).toEqual([
{
error,
point: {
address: expect.any(String),
eid: expect.any(Number),
},
transaction: expectTransaction,
},
])

// Since we failed on the first transaction, we expect
// all the transaction to still be pending and none of them to be successful
expect(successful).toEqual([])
expect(pending).toEqual([expectTransaction, expectTransaction])
})

it('should ask the user to retry if not in the CI mode', async () => {
Expand All @@ -207,20 +216,20 @@ describe('task/oapp/wire', () => {
promptToContinueMock
.mockResolvedValueOnce(false) // We don't want to see the list of transactions
.mockResolvedValueOnce(true) // We want to continue
.mockResolvedValueOnce(false) // We don't want to see the list of failed transactions
.mockResolvedValueOnce(true) // We want to see the list of failed transactions
.mockResolvedValueOnce(true) // We want to retry

const oappConfig = configPathFixture('valid.config.connected.js')
const [successful, errors] = await hre.run(TASK_LZ_WIRE_OAPP, { oappConfig })
const [successful, errors, pending] = await hre.run(TASK_LZ_WIRE_OAPP, { oappConfig })

// Check that the user has been asked to retry
expect(promptToContinueMock).toHaveBeenCalledWith(`Would you like to preview the failed transactions?`)
expect(promptToContinueMock).toHaveBeenCalledWith(`Would you like to retry?`, true)

// After retrying, the signer should not fail anymore
const expectTransactionWithReceipt = { receipt: expect.any(Object), transaction: expect.any(Object) }
expect(successful).toEqual([expectTransactionWithReceipt, expectTransactionWithReceipt])
expect(errors).toEqual([])
expect(pending).toEqual([])
})

it('should not retry if the user decides not to if not in the CI mode', async () => {
Expand All @@ -239,23 +248,68 @@ describe('task/oapp/wire', () => {
.mockResolvedValueOnce(false) // We don't want to retry

const oappConfig = configPathFixture('valid.config.connected.js')
const [successful, errors] = await hre.run(TASK_LZ_WIRE_OAPP, { oappConfig })
const [successful, errors, pending] = await hre.run(TASK_LZ_WIRE_OAPP, { oappConfig })

// Check that the user has been asked to retry
expect(promptToContinueMock).toHaveBeenCalledWith(`Would you like to preview the failed transactions?`)
expect(promptToContinueMock).toHaveBeenCalledWith(`Would you like to retry?`, true)

// Check that we got the failures back
expect(successful).toEqual([])
expect(errors).toEqual([
{
error,
point: {
address: expect.any(String),
eid: expect.any(Number),
},
transaction: expectTransaction,
},
])

// Since we failed on the first transaction, we expect
// all the transaction to still be pending and none of them to be successful
expect(successful).toEqual([])
expect(pending).toEqual([expectTransaction, expectTransaction])
})

it('should not retry successful transactions', async () => {
const error = new Error('Oh god dammit')

// Mock the second sendTransaction call to reject
//
// This way we simulate a situation in which the first call goes through, then the second call rejects
sendTransactionMock
.mockImplementationOnce(sendTransactionMock.getMockImplementation()!)
.mockRejectedValueOnce(error)
.mockRejectedValueOnce(error)

// In the non-CI mode we need to answer the prompts
promptToContinueMock
.mockResolvedValueOnce(false) // We don't want to see the list of transactions
.mockResolvedValueOnce(true) // We want to continue
.mockResolvedValueOnce(true) // We want to see the list of failed transactions
.mockResolvedValueOnce(true) // We want to retry
.mockResolvedValueOnce(true) // We want to see the list of failed transactions
.mockResolvedValueOnce(true) // We want to retry

const oappConfig = configPathFixture('valid.config.connected.js')
const [successful, errors, pending] = await hre.run(TASK_LZ_WIRE_OAPP, { oappConfig })

// Check that the user has been asked to retry
expect(promptToContinueMock).toHaveBeenCalledWith(`Would you like to preview the failed transactions?`)
expect(promptToContinueMock).toHaveBeenCalledWith(`Would you like to retry?`, true)

// After retrying, the signer should not fail anymore
expect(successful).toEqual([expectTransactionWithReceipt, expectTransactionWithReceipt])
expect(errors).toEqual([])
expect(pending).toEqual([])

expect(sendTransactionMock).toHaveBeenCalledTimes(
// The first successful call
1 +
// The first failed call
1 +
// The retry of the failed call
1 +
// The retry of the failed call
1
)
})
})
})
Expand Down

0 comments on commit 13965b7

Please sign in to comment.