-
-
Notifications
You must be signed in to change notification settings - Fork 183
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
Tokens controller approve reject refactor full #1268
Tokens controller approve reject refactor full #1268
Conversation
8662958
to
b9ba7a7
Compare
f808086
to
5b6ba7a
Compare
@@ -667,144 +614,48 @@ export class TokensController extends BaseController< | |||
asset: Token, | |||
type: string, | |||
interactingAddress?: string, | |||
): Promise<AssetSuggestionResult> { | |||
): Promise<void> { |
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.
If this wasn't already a breaking change I'd have avoided doing this, but the changes are already breaking.
When this wasn't awaiting for the request to be approved or declined it was returning both the pending asset and the promise to await, but now that watchAsset
handles the whole process there's no need to return the pending asset (which was not being used anyway) nor the address of the token (which was not being used either).
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.
This makes more sense when looking at the before/after of how it's handled in the clients
- PR for extension https://github.com/MetaMask/metamask-extension/pull/18829/files#diff-f3e25ebd138f61f51874e39dcbfcc24b8aff46248bf5e7deebfc9561a88d3824R39
- Mobile is similar, no result is expected
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.
For reference, the counter argument is that it could be good practice to provide useful return data in all our API methods for the sake of future-proofing. Though given the level of complexity we've reached, I'd agree to simplify where we can as we can add this back when needed.
Naturally this wouldn't be possible if the return value ever reached the dApp, but currently the only result from the middleware is a boolean.
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.
The thing is that I'm not entirely sure what would be useful to return in this case. The suggestedAssetMeta
or the Token
from addToken
.
If we do the second, and then we end up adding more types, it's going to turn into a bigger breaking change.
I think leaving it to return void is safest, as we probably won't need to update anything in every client if we eventually return something (even though it still counts as breaking change).
const { selectedAddress } = this.config; | ||
|
||
const suggestedAssetMeta: SuggestedAssetMeta & { | ||
interactingAddress: string; |
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 don't need to specify that interactingAddress
is not optional, because selectedAddress
isn't.
…controller-approve-reject-refactor-full
@@ -120,7 +85,6 @@ export interface TokensState extends BaseState { | |||
allTokens: { [key: string]: { [key: string]: Token[] } }; | |||
allIgnoredTokens: { [key: string]: { [key: string]: string[] } }; | |||
allDetectedTokens: { [key: string]: { [key: string]: Token[] } }; | |||
suggestedAssets: SuggestedAssetMeta[]; |
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.
No longer need for status if we don't want to persist them after restarts.
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.
As this was previously persisted, does this warrant a migration to delete this data on upgrade?
Not sure what the best practice is and if we prioritise fewer migrations over dormant data in the user state.
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.
How was this decision come to? Not saying I disagree with it. I'm just curious what the thinking was 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.
The ability to persist pending wallet_watchAsset
after a restart had no tests to cover that case and it was inadvertently broken in a previous PR.
When we found out about the regression, we reached out, asked if that feature was intentional, accidental or important, so that we could decide between fixing it first. The decision was to not restore that feature and have it behave the same way as other confirmations such as the ones that ask specific permissions (which are also not persisted).
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.
@adonesky1 I have discussed this with @danjm and @bschorchit and we didn't thought of any relevant use case. As so we have decided to standardise the pending wallet_watchAsset
confirmation behaviour (i.e no persisting confirmations after a restart... just like permissions for example. The only exception are transactions). But is there a use case that you can think for persisting pending approvals to add suggested assets?
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 guess the one reason I can think of to keep suggestedAsset state (though we can probably accomplish this by looking at the approval controller state) is that to avoid duplicate suggestions. This is obviously not about persisting state across reloads. We do not currently prevent duplicate suggestions:
But I was thinking maybe we ought to?
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.
It'd be easier to rate limit approvals from wallet_watchAsset
type from ApprovalController
.
3b37ef1
to
38e2ebf
Compare
} | ||
} catch (error) { | ||
this.failSuggestedAsset(suggestedAssetMeta, error); | ||
return Promise.reject(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.
This error return is maintained by:
- Throwing with what is now the
default
clause if the type is not ERC20 (this should probably be changed to throw usingethErrors
frometh-rpc-errors
). - Letting
validateTokenToWatch
throw freely.
return reject(new Error(meta.error.message)); | ||
/* istanbul ignore next */ | ||
default: | ||
return reject(new Error(`Unknown status: ${meta.status}`)); |
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.
All the cases in this switch are maintained (except the return value when it resolves):
- case 1: When it resolves, the return value is now void for the reasons explained in the
watchAsset
signature. - case 2: When it's rejected, instead of throwing an error with a string we will define a
userRejectedRequest
in the same way it is done for permissions from the FE (https://github.com/MetaMask/metamask-extension/blob/develop/ui/pages/permissions-connect/permissions-connect.component.js#L383) - case 3: If it fails for any other reason, we are no longer catching the error so it will throw freely.
- default: This cannot happen.
): Promise<void> { | ||
if (type !== 'ERC20') { | ||
throw new Error(`Asset of type ${type} not supported`); | ||
} |
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 have added this validation because I don't think there's any point on keeping the switch given that this is only for ERC20. By keeping the switch we would have to either:
-
Add a switch before validation and another one after awaiting for request approval (like it currently does in the
acceptWatchAsset
), which in turn generates a default case that is unreachable. -
Another option would be to define a variable that holds a callback and is populated in the first switch, something like:
let addToken: () => void;
switch (type) {
case 'ERC20':
validateTokenToWatch(asset);
addToken = () => { this.addToken(/* stuff */) };
break;
default:
throw new Error(`Asset of type ${type} not supported`);
}
That way, if there were multiple cases, we could avoid needing a second switch after waiting for approval. This would be my choice if there was more than one type or plans to add another type here, but that's not the case and doing it will be more difficult to read for no reason (NFTs are being handled elsewhere).
Suggestions welcome though.
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.
This sounds good to me, an if
is cleaner if we don't expect to ever have any more cases.
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.
This file has undergone chunky refactors. I suggest to review it by looking both at the diff and the final product.
* | ||
* @param suggestedAssetID - The ID of the suggestedAsset to accept. | ||
*/ | ||
rejectWatchAsset(suggestedAssetID: string) { |
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.
Nothing needs to happen now when rejecting since there's no longer status for suggestedAssets
, so this is dead code.
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.
The reason so many tests have been removed, is because some of them were testing the watchAsset
part and others were testing approval/rejection.
That all happens within watchAsset
now, so they can be simplified maintaining coverage.
.stub(tokensController, '_generateRandomId') | ||
.callsFake(() => requestId); | ||
type = 'ERC20'; | ||
it('stores token correctly under interacting address if user confirms', async function () { |
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 appreciate it's a negative test as it's verifying we don't have any error handling logic, but should we have a test to verify that the method throws if the promise from the messenger call rejects?
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 can't see what value that'll add as part of a unit test. It's a promise that is being awaited and that is not wrapped in a try/catch, so yes, it will throw.
In an e2e test we can check that, when rejected, it indeed throws a userRejectedRequest
RPC 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.
The value would be it would assert our intention that it's not catching the approval errors.
What you've said concerning the code is correct but the unit test would programmatically enforce that requirement going forward, and ensure it's not captured and handled in future without us being aware and explicitly changing our expectations.
|
||
this._requestApproval(suggestedAssetMeta); | ||
await this._requestApproval(suggestedAssetMeta); |
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.
Should we wrap this in a try/catch and handle a failure state gracefully since the catch within the method is removed?
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 don't think we should after these changes.
If, for example, the user rejects to add the token from the front end, we now rely on them sending the userRejectedRequest
error, which we want to throw as it is.
Just want to highlight this PR I've opened with NFT side changes that mirror this refactor: #1173 |
* refactor to wait for pending approval promise to resolve/reject
* refactor to wait for pending approval promise to resolve/reject
* refactor to wait for pending approval promise to resolve/reject
Description
Stops triggering the acceptance or rejection of the approval from within TokensController. That'll be the responsibility of the caller.
This is part of a series of tickets that aim to refactor and simplify how we await for the confirmation promise.
Changes
TokensController
instead of relaying onacceptWatchAsset
andrejectWatchAsset
, which are being removed.watchAsset
.References
Checklist