-
Notifications
You must be signed in to change notification settings - Fork 393
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Transaction replacement - Allow speeding up transactions #3456
Conversation
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 haven't tested it yet but the first few comments at first glance.
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.
const isEIP1559Tx = isEIP1559TransactionRequest(tx) | ||
|
||
if (!tx.gasPrice || (isEIP1559Tx && !tx.maxFeePerGas)) { | ||
throw new Error("Cannot speed up transaction without a valid gas price") |
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.
Do we not want to display a message in this case? The user does not really know what happens when something goes wrong as in this case.
Screen.Recording.2023-06-08.at.15.30.26.mov
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.
Agree, another concern I have here is related to block times and chains without mempool, for example it doesn't make sense to show the option to speed up transactions on Optimism, and, on Polygon we get fast enough block times that most attempts to speed up result in the replacement transaction being dropped. Maybe we could use block times or check if we have lower than average gas fees to improve UX a bit here 🤔 .
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 we should make an allowlist for the speedup tx button 🤔
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.
Couple of comments, haven't tested yet
@@ -64,24 +78,32 @@ function getAmount(tx: EnrichedEVMTransaction): string { | |||
return `${convertToEth(value) || "0"} ${symbol}` | |||
} | |||
|
|||
function getBlockHeight(tx: EnrichedEVMTransaction): string { | |||
type TransactionStatus = "pending" | "dropped" | "failed" | "completed" |
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.
Would it make sense to make it an enum? We are using these strings quite a lot
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.
We've generally avoided using enums in the past as they don't bring much to the table compared to simple type unions like this one (other than extra verbosity 😅). Is there something in particular we want to get out of using them?
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.
Ah I just like their syntax and the fact that I know just looking at it that it is some defined structure, not just a string, I don't have to wonder if this should be for example translated somewhere or not. Not a super strong preference anyway 😉
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.
Ah yeah, fair, this is def a situation where I lean on the editor/language server a lot. I hear your point though, esp the ability to easily identify that something should be translated. Will ponder that a little bit.
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.
Yeah I think these should go into an enum, I ended up reverting this after having conflicting thoughts over similar values for activities. I agree that enums are verbose although one redeeming quality I think would be that with an union we'd have to do an intersection to get one of the members as a fixed type without losing the reference to the group, like the following:
type A = "a" | "b"
type B = {
// something is "a", if we remove "a" from the type above this becomes never
something: A & "a"
}
Whereas with enums we can do:
enum A {
a,
b,
}
type B = {
// If we remove "a" from the enum above we get an error here instead of `never`
// as the type for something
something: A.a
}
One "downside" with these is that if we use string values for this enum then we have to reference the enum members directly, strings/constants do not work.
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.
something is "a", if we remove "a" from the type above this becomes never
This is only an issue if something
is never actually interacted with though, right? If things actually interact with it, we should get hit with errors at those points—and if we don't have such uses, then it's probably not a correctness issue… Open to more thinking here, but it feels like an artifact rather than a deep problem.
So I've thought about this a little bit. enums, their mapping of variable names to strings to numbers, the fact that they're a language-level feature (which TypeScript generally doesn't do), and the fact that they have a long list of things to keep in mind in terms of how they work at runtime and compile time, etc, just feel like they're not worth the trouble.
For translations specifically, I think the type union ultimately helps us, because we can use it directly to reference i18n keys and we will know if we've hit all the i18n keys. We can get something similar with enum types if we set them all as explicit strings, but to do that we have to repeat every value (the enum name and then the string value), in addition to referring to the underlying type union in a relatively arcane way.
Overall, enums represent extra layers of indirection between us and the raw values that we're passing around. They give us some small advantages, but I don't think they're worth the tradeoff.
With that said, I'm going to escalate here 😁 and suggest we add a linter rule to prohibit enums altogether! <_<
|
||
const replacement = replacementTransactions.find( | ||
(request) => | ||
request.hash === transaction.hash && |
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.
in the other place we are checking tx's author and nonce - so what should we check to know if it is a replacement tx? 🤔
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.
A tx is a replacement if it's a matching nonce, fwiw—but sometimes the previous version of the nonce will get included in the block if it was put into a block by a miner before the replacement made it.
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 this is bad naming on my part, this logic will run a few times, specifically for: pending transactions, completed transactions and once again for these two if they've been enriched. Here we're checking if we're already tracking it to get info about the transaction it's replacing so that we can remove it from the list if the replacement has been mined. I'll add this as a comment.
@hyphenized can you put some more high-level details on approach & changes in the PR description when you get a chance? |
Screen.Recording.2023-06-12.at.13.46.31.mov
Screen.Recording.2023-06-12.at.13.51.45.mov |
When opening and closing two activities in sequence, the first activity details would display momentarily because react tries to reuse the already rendered activity details component
Talked about this with @hyphenized, about gas settings behaviour. |
Handles a special case where the replaced transaction is re-added again after being dropped
A bigger issue here is the disconnect between what we show on estimate network and what we show in gas settings. |
Got it. Sounds like we need to put this fellow into stasis. Because this is a bigger piece of work, I'd like to suggest landing the effort so far hidden behind its feature flag. Wdyt @hyphenized ? |
Easiest might be to block gas settings customization and ship this or, merge this behind the feature flag and break down the problems left into issues. Gas settings set on the replacement transaction are being ignored now and that's causing an immediate drop so will have to take a look into what's happening 😕 |
I'd like to merge so we can stick to shifting focus this week, but agree on capturing issues. Let's maybe ask QA to look at a build of this PR with feature flag disabled and confirm transaction flow looks good, spin up issues, and merge it in. |
I'm happy to do some regression on this with the feature flag disabled. Do we think we'd get it into the coming release cut with the flag disabled? If so, I was just thinking that the regression for that effort would cover it. But if it's going to be separate that fine too, I can test with the disabled flag whenever, if needed. |
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.
selectTrackedReplacementTransactions | ||
) | ||
|
||
const hasReplacementTx = replacementTransactions.some( |
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.
const hasReplacementTx = replacementTransactions.some( | |
const hasReplacementTx = replacementTransactions?.some( |
tx.chainID === currentAccount.network.chainID | ||
) | ||
|
||
const isReplacementTx = replacementTransactions.some( |
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.
const isReplacementTx = replacementTransactions.some( | |
const isReplacementTx = replacementTransactions?.some( |
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.
Looking at the scope of changes and how much of the most fragile parts of the codebase were changed and refactored I would say we should do full regression and exploratory tests here, on the branch, before merging it even in this state.
I'd say if this has even the slightest chance of messing up with gas prices which can cost our users real money being burned for gas then we shouldn't rush to merge it.
<div className="icon_transfer" /> | ||
<DestinationCard | ||
label="To" | ||
address={activityItem.recipient.address || "(Contract creation)"} |
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.
"(Contract creation)"
should be added to translations
@@ -134,24 +186,58 @@ export default function WalletActivityDetails( | |||
?.focus() | |||
}, [activityItem?.hash, blockExplorerUrl]) | |||
|
|||
useEffect(() => { | |||
useInterval(() => { |
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.
shouldn't we clean this interval for activities that are already fully fetched and successful?
}), | ||
) => { | ||
immerState.activities = initializeActivitiesFromTransactions(payload) | ||
// Clear these at extension restart |
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'm not 100% sure about this, what if we had some transactions and then chrome decides to update the extension? Unlikely but not impossible 🤔
I found one more issue, we aren't able to send transaction for custom networks. Screen.Recording.2023-07-28.at.12.55.36.mov |
Ok, rereading the description and skimming the contents again, it looks like maybe we went too far down a rabbit hole in one PR in that case. I think we should break out the work that got done (refactors, etc) into separate issues referencing this PR, close it, and then we can pick the package up when cycles become available and run the underlying changes through in separate PRs with some git elbow grease. |
Allows sending a replacement transaction from within an account activity details; A replacement transaction consists of a transaction with the same nonce as another sent before, i.e., the original transaction, usually as an attempt to speed up or cancel it's processing altogether.
Since within activity details we have access to most of the data we need to create this new transaction, we can send this back to the background script through an async thunk, and handle signing and the actual sending from there. Replacement and original transactions are tracked on Redux, a migration was not necessary due to the fact that activities are initialized at extension startup. Furthermore, this data persists until the extension restarts.
This PR also refactors code from activities to accommodate improved typing and internationalization, it also fixes a bug associated to the polling of block prices for custom networks.
Testing Env
To Test
Latest build: extension-builds-3456 (as of Fri, 28 Jul 2023 10:37:20 GMT).