This repository has been archived by the owner on Nov 10, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 363
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* wip: txMonitor function * Wait for speed up tx confirmation * Error and indefinitely pending tx handling * Delete unnecessary polling * Console logs for debugging * End txMonitor if first tx is mined * Added an await * Add more delay to txMonitor to check each 5 seconds * Timeout after 1 hour of pending txs * Refactor tx monitor (#2511) * Refactor * Add unit tests Co-authored-by: fernandomg <fernando.greco@gmail.com> Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm> Co-authored-by: katspaugh <katspaugh@users.noreply.github.com>
- Loading branch information
1 parent
e006cdd
commit 68f4363
Showing
8 changed files
with
304 additions
and
21 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
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
122 changes: 122 additions & 0 deletions
122
src/logic/safe/transactions/__tests__/txMonitor.test.ts
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,122 @@ | ||
import { txMonitor } from 'src/logic/safe/transactions/txMonitor' | ||
import { web3ReadOnly } from 'src/logic/wallets/getWeb3' | ||
|
||
jest.mock('src/logic/wallets/getWeb3', () => ({ | ||
web3ReadOnly: { | ||
eth: {}, | ||
}, | ||
})) | ||
|
||
const params = { | ||
sender: '0x474e5Ded6b5D078163BFB8F6dBa355C3aA5478C8', | ||
hash: '0x510bec3129a8dcc57075b67de6292ada338fa05518d18ec81b2cda3cea593a64', | ||
nonce: 1, | ||
data: '0', | ||
gasPrice: '1', | ||
} | ||
|
||
const options = { | ||
delay: 0, | ||
maxRetries: 10, | ||
} | ||
|
||
describe('txMonitor', () => { | ||
beforeEach(() => { | ||
web3ReadOnly.eth.getTransaction = jest.fn(() => Promise.reject('getTransaction')) as any | ||
web3ReadOnly.eth.getTransactionReceipt = jest.fn(() => Promise.reject('getTransactionReceipt')) as any | ||
web3ReadOnly.eth.getBlock = jest.fn(() => Promise.reject('getBlock')) as any | ||
}) | ||
|
||
it('should reject when max retries are reached', async () => { | ||
try { | ||
await txMonitor(params, options) | ||
expect(false).toBe('Should not go here') | ||
} catch (e) { | ||
expect(e.message).toBe('Code 805: TX monitor error (max retries reached)') | ||
} | ||
expect(web3ReadOnly.eth.getTransaction).toHaveBeenCalledTimes(0) | ||
expect(web3ReadOnly.eth.getTransactionReceipt).toHaveBeenCalledTimes(11) | ||
}) | ||
|
||
it('should load original tx if nonce is undefined', async () => { | ||
web3ReadOnly.eth.getTransaction = jest.fn(() => Promise.resolve({ nonce: 1, gasPrice: 1 })) as any | ||
web3ReadOnly.eth.getTransactionReceipt = jest.fn((hash) => Promise.resolve({ hash, status: 'success' })) as any | ||
|
||
await expect(txMonitor({ ...params, nonce: undefined }, options)).resolves.toEqual({ | ||
status: 'success', | ||
hash: '0x510bec3129a8dcc57075b67de6292ada338fa05518d18ec81b2cda3cea593a64', | ||
}) | ||
expect(web3ReadOnly.eth.getTransaction).toHaveBeenCalledTimes(1) | ||
expect(web3ReadOnly.eth.getTransactionReceipt).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it('should fail if it cannot load the original tx receipt', async () => { | ||
web3ReadOnly.eth.getTransaction = jest.fn(() => Promise.resolve({ nonce: 1, gasPrice: 1 })) as any | ||
web3ReadOnly.eth.getTransactionReceipt = jest.fn(() => Promise.reject('No receipt')) | ||
|
||
try { | ||
await txMonitor(params, options) | ||
expect(false).toBe('Should not go here') | ||
} catch (e) { | ||
expect(e.message).toBe('Code 805: TX monitor error (max retries reached)') | ||
} | ||
|
||
expect(web3ReadOnly.eth.getTransaction).toHaveBeenCalledTimes(0) | ||
expect(web3ReadOnly.eth.getTransactionReceipt).toHaveBeenCalledTimes(11) | ||
}) | ||
|
||
it('should return speed-up tx receipt', async () => { | ||
web3ReadOnly.eth.getBlock = jest.fn(() => | ||
Promise.resolve({ | ||
transactions: [ | ||
{ | ||
hash: '0xSPEEDY', | ||
from: params.sender, | ||
nonce: params.nonce, | ||
input: params.data, | ||
}, | ||
], | ||
}), | ||
) as any | ||
|
||
web3ReadOnly.eth.getTransactionReceipt = jest.fn((hash) => { | ||
return hash === '0xSPEEDY' | ||
? Promise.resolve({ hash, status: 'success' } as any) | ||
: Promise.reject('No original receipt') | ||
}) | ||
|
||
await expect(txMonitor(params, options)).resolves.toEqual({ | ||
status: 'success', | ||
hash: '0xSPEEDY', | ||
}) | ||
expect(web3ReadOnly.eth.getBlock).toHaveBeenCalledTimes(1) | ||
expect(web3ReadOnly.eth.getTransactionReceipt).toHaveBeenCalledTimes(2) | ||
}) | ||
|
||
it('should fail if it cannot find a speed-up tx', async () => { | ||
web3ReadOnly.eth.getBlock = jest.fn(() => | ||
Promise.resolve({ | ||
transactions: [ | ||
{ | ||
hash: '0x123', | ||
from: 'Someone', | ||
nonce: 12, | ||
input: '123', | ||
}, | ||
], | ||
}), | ||
) as any | ||
|
||
web3ReadOnly.eth.getTransactionReceipt = jest.fn(() => Promise.reject('No original receipt')) | ||
|
||
try { | ||
await txMonitor(params, options) | ||
expect(false).toBe('Should not go here') | ||
} catch (e) { | ||
expect(e.message).toBe('Code 805: TX monitor error (max retries reached)') | ||
} | ||
|
||
expect(web3ReadOnly.eth.getBlock).toHaveBeenCalledTimes(11) | ||
expect(web3ReadOnly.eth.getTransactionReceipt).toHaveBeenCalledTimes(11) | ||
}) | ||
}) |
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,134 @@ | ||
import { Transaction, TransactionReceipt } from 'web3-core' | ||
|
||
import { web3ReadOnly } from 'src/logic/wallets/getWeb3' | ||
import { sameAddress } from 'src/logic/wallets/ethAddresses' | ||
import { sameString } from 'src/utils/strings' | ||
import { CodedException, Errors } from 'src/logic/exceptions/CodedException' | ||
|
||
type TxMonitorProps = { | ||
sender: string | ||
hash: string | ||
data: string | ||
nonce?: number | ||
gasPrice?: string | ||
} | ||
|
||
type TxMonitorOptions = { | ||
delay?: number | ||
maxRetries?: number | ||
} | ||
|
||
const MAX_RETRIES = 720 | ||
const DEFAULT_DELAY = 5000 | ||
|
||
async function findSpeedupTx({ sender, hash, nonce, data }: TxMonitorProps): Promise<Transaction | undefined> { | ||
const latestBlock = await web3ReadOnly.eth.getBlock('latest', true) | ||
|
||
const replacementTransaction = latestBlock.transactions.find((transaction) => { | ||
// TODO: use gasPrice, timestamp or another better way to differentiate | ||
return ( | ||
sameAddress(transaction.from, sender) && | ||
transaction.nonce === nonce && | ||
!sameString(transaction.hash, hash) && | ||
// if `data` differs, then it's a replacement tx, not a speedup | ||
sameString(transaction.input, data) | ||
) | ||
}) | ||
|
||
return replacementTransaction | ||
} | ||
|
||
/** | ||
* Recursively inspects a pending tx. Until it's found, and returns the mined tx receipt | ||
* | ||
* @param {object} txParams | ||
* @param {string} txParams.sender | ||
* @param {string} txParams.hash | ||
* @param {string} txParams.data | ||
* @param {number | undefined} txParams.nonce | ||
* @param {string | undefined} txParams.gasPrice | ||
* @param {object} options | ||
* @param {number} options.delay | ||
* @returns {Promise<TransactionReceipt>} | ||
*/ | ||
export const txMonitor = ( | ||
{ sender, hash, data, nonce, gasPrice }: TxMonitorProps, | ||
options?: TxMonitorOptions, | ||
tries = 0, | ||
): Promise<TransactionReceipt> => { | ||
return new Promise<TransactionReceipt>((resolve, reject) => { | ||
const { maxRetries = MAX_RETRIES } = options || {} | ||
if (tries > maxRetries) { | ||
reject(new CodedException(Errors._805, 'max retries reached')) | ||
return | ||
} | ||
|
||
const monitorFn = async (): Promise<unknown> => { | ||
// Case 1: this block is accessed for the first time, no nonce | ||
if (nonce == null || gasPrice == null) { | ||
let params: TxMonitorProps = { sender, hash, data } | ||
try { | ||
// Find the nonce for the current tx | ||
const transaction = await web3ReadOnly.eth.getTransaction(hash) | ||
if (transaction) { | ||
params = { ...params, nonce: transaction.nonce, gasPrice: transaction.gasPrice } | ||
} | ||
} catch (e) { | ||
// ignore error | ||
} | ||
|
||
return txMonitor(params, options, tries + 1) | ||
.then(resolve) | ||
.catch(reject) | ||
} | ||
|
||
// Case 2: the nonce exists, try to get the receipt for the original tx | ||
try { | ||
const firstTxReceipt = await web3ReadOnly.eth.getTransactionReceipt(hash) | ||
if (firstTxReceipt) { | ||
return resolve(firstTxReceipt) | ||
} | ||
} catch (e) { | ||
// proceed to case 3 | ||
} | ||
|
||
// Case 3: original tx not found, try to find a sped-up tx | ||
try { | ||
const replacementTx = await findSpeedupTx({ sender, hash, nonce, data }) | ||
|
||
if (replacementTx) { | ||
const replacementReceipt = await web3ReadOnly.eth.getTransactionReceipt(replacementTx.hash) | ||
|
||
// goal achieved | ||
if (replacementReceipt) { | ||
return resolve(replacementReceipt) | ||
} | ||
|
||
// tx exists but no receipt yet, it's pending | ||
return txMonitor( | ||
{ | ||
sender, | ||
nonce, | ||
hash: replacementTx.hash, | ||
data: replacementTx.input, | ||
gasPrice: replacementTx.gasPrice, | ||
}, | ||
options, | ||
tries + 1, | ||
) | ||
.then(resolve) | ||
.catch(reject) | ||
} | ||
} catch (e) { | ||
// ignore error | ||
} | ||
|
||
// Neither the original nor a replacement transactions were found, try again | ||
txMonitor({ sender, hash, data, nonce, gasPrice }, options, tries + 1) | ||
.then(resolve) | ||
.catch(reject) | ||
} | ||
|
||
setTimeout(monitorFn, options?.delay ?? DEFAULT_DELAY) | ||
}) | ||
} |
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
Oops, something went wrong.