Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

Speed up safe creation #2473

Merged
merged 12 commits into from
Jul 6, 2021
88 changes: 88 additions & 0 deletions src/logic/safe/transactions/txMonitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { TransactionReceipt } from 'web3-core'

import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { sameString } from 'src/utils/strings'

type TxMonitorProps = {
sender: string
hash: string
data: string
nonce?: number
gasPrice?: string
}

type TxMonitorOptions = {
delay?: number
}

/**
* 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 {function(txReceipt: TransactionReceipt): void} cb - called with the tx receipt as argument when tx is mined
* @param {object} options
* @param {number} options.delay
*/
export const txMonitor = async (
{ sender, hash, data, nonce, gasPrice }: TxMonitorProps,
cb: (txReceipt: TransactionReceipt) => void,
options?: TxMonitorOptions,
): Promise<void> => {
setTimeout(async () => {
if (nonce === undefined || gasPrice === undefined) {
// this block is accessed only the first time, to lookup the tx nonce and gasPrice
// find the nonce for the current tx
const transaction = await web3ReadOnly.eth.getTransaction(hash)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are several places like this in txMonitor, where we don't catch a potential async exception.
I think txMonitor should return a promise instead of taking a callback. Then we can try-catch each await call and reject the returned promise in the catch.


if (transaction !== null) {
// transaction found
return txMonitor({ sender, hash, data, nonce: transaction.nonce, gasPrice: transaction.gasPrice }, cb, options)
} else {
return txMonitor({ sender, hash, data }, cb, options)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like txMonitor itself also implements a recursive timeout loop.
So we could use waitWithTimeout here as well, and use a promise instead of the callback:

export const txMonitor = (txMonitorProps) => {
  return new Promise((resolve, reject) => {
    waitWithTimeout(() => { ... }, resolve, reject)
  })
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I solved it in a simpler way, you can see the change in the last commit

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was smart! 👏

}
}

web3ReadOnly.eth.getTransactionReceipt(hash)

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)
)
})

if (replacementTransaction) {
const transactionReceipt = await web3ReadOnly.eth.getTransactionReceipt(replacementTransaction.hash)
if (transactionReceipt === null) {
// pending transaction
return txMonitor(
{
sender,
hash: replacementTransaction.hash,
data: replacementTransaction.input,
nonce,
gasPrice: replacementTransaction.gasPrice,
},
cb,
options,
)
}
cb(transactionReceipt)
return
}

return txMonitor({ sender, hash, data, nonce, gasPrice }, cb, options)
}, options?.delay ?? 500)
}
30 changes: 23 additions & 7 deletions src/routes/open/container/Open.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Loader } from '@gnosis.pm/safe-react-components'
import { backOff } from 'exponential-backoff'
import queryString from 'query-string'
import React, { useEffect, useState, ReactElement } from 'react'
import React, { ReactElement, useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useLocation } from 'react-router-dom'
import { TransactionReceipt } from 'web3-core'
Expand Down Expand Up @@ -29,6 +29,7 @@ import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe'
import { useAnalytics } from 'src/utils/googleAnalytics'
import { sleep } from 'src/utils/timer'
import { txMonitor } from 'src/logic/safe/transactions/txMonitor'

const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY'

Expand Down Expand Up @@ -82,17 +83,32 @@ export const createSafe = async (values: CreateSafeValues, userAccount: string):
const ownerAddresses = getAccountsFrom(values)
const safeCreationSalt = getSafeCreationSaltFrom(values)
const deploymentTx = getSafeDeploymentTransaction(ownerAddresses, confirmations, safeCreationSalt)

const receipt = await deploymentTx
let receiptResult
deploymentTx
.send({
from: userAccount,
gas: values?.gasLimit,
})
.once('transactionHash', (txHash) => {
saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, { txHash, ...values })
txMonitor({ sender: userAccount, hash: txHash, data: deploymentTx.encodeABI() }, (txReceipt) => {
receiptResult = txReceipt
})
})
.then((txReceipt) => {
receiptResult = txReceipt
})
juampibermani marked this conversation as resolved.
Show resolved Hide resolved

return receipt
const waitForTxReceipt = (resolve) => {
setTimeout(() => {
if (receiptResult) {
resolve(receiptResult)
} else {
waitForTxReceipt(resolve)
}
}, 500)
juampibermani marked this conversation as resolved.
Show resolved Hide resolved
}
return new Promise((resolve) => waitForTxReceipt(resolve))
}

const Open = (): ReactElement => {
Expand Down Expand Up @@ -170,9 +186,6 @@ const Open = (): ReactElement => {
const safe = makeAddressBookEntry({ address: safeAddress, name })
await dispatch(addressBookSafeLoad([...owners, safe]))

const safeProps = await buildSafe(safeAddress)
await dispatch(addOrUpdateSafe(safeProps))

trackEvent({
category: 'User',
action: 'Created a safe',
Expand All @@ -189,6 +202,9 @@ const Open = (): ReactElement => {
},
})

const safeProps = await buildSafe(safeAddress)
await dispatch(addOrUpdateSafe(safeProps))

await removeFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
const url = {
pathname: `${SAFELIST_ADDRESS}/${safeProps.address}/balances`,
Expand Down