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
Speed up safe creation #2473
Merged
Merged
Speed up safe creation #2473
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
5416595
wip: txMonitor function
fernandomg d6e4560
Merge remote-tracking branch 'origin/dev' into feature/2362-speedup-s…
juampibermani d5bda12
Wait for speed up tx confirmation
juampibermani f281f1e
Error and indefinitely pending tx handling
juampibermani d07f61f
Delete unnecessary polling
juampibermani 99fac3c
Console logs for debugging
juampibermani 808a19a
End txMonitor if first tx is mined
juampibermani 391d2f9
Added an await
juampibermani 0b824f0
Merge branch 'dev' into feature/2362-speedup-safe-creation
11e5689
Add more delay to txMonitor to check each 5 seconds
b194f53
Timeout after 1 hour of pending txs
juampibermani 665dafe
Refactor tx monitor (#2511)
katspaugh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it wasn't being done before because the interface itself shows an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't see the notification
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was no notification, I meant that the page itself shows an error graphic and suggests to retry.
Notification doesn't hurt though.