-
-
Notifications
You must be signed in to change notification settings - Fork 206
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
Restore previous behavior of toChecksumHexAddress #4046
Conversation
Prior to 40acc6c, `toChecksumHexAddress` in `@metamask/controller-utils` would not throw when given `undefined` or `null`. Now it does, because `addHexPrefix` from `ethereumjs-util` has been replaced with `add0x` from `@metamask/utils`, and the latter is more strict when it comes to input. This change is causing some tests on the extension side to fail. Granted, these tests are likely creating an incomplete state object, so they ought to be fixed so that they pass a string to `toChecksumHexAddress`. However, these test failures serve as a reminder that there may be other parts of the extension codebase not covered by tests which are using `toChecksumHexAddress` incorrectly. If the extension were using TypeScript throughout, finding these problem areas would be trivial, because we would have seen type errors already. But because the extension codebase is still primarily written in JavaScript, we cannot guarantee that `toChecksumHexAddress` won't throw for some particular use case even after we fix the obvious usages. Therefore, to prevent unexpected runtime errors, this commit restores the existing behavior of `toChecksumHexAddress`.
* @returns A 0x-prefixed hexidecimal checksummed address, if address is valid. Otherwise original input 0x-prefixe, if address is valid. Otherwise original input 0x-prefixed. | ||
*/ | ||
export function toChecksumHexAddress(address: 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.
I do think that if undefined input is to be considered as part of implementation, it should be reflected in types as well? At the same time I see using them to steer towards intended usage (perhaps at some point in the future, it's a usage that can be phased out?)
So what do you think about adding undefined (should that be made wider? any
, even?) as an overload but marking it as deprecated?
* @returns A 0x-prefixed hexidecimal checksummed address, if address is valid. Otherwise original input 0x-prefixe, if address is valid. Otherwise original input 0x-prefixed. | |
*/ | |
export function toChecksumHexAddress(address: string) { | |
* @returns A 0x-prefixed hexadecimal checksummed address, if address is valid. Otherwise returns input 0x-prefixed. | |
*/ | |
export function toChecksumHexAddress(address: string): Hex; | |
/** | |
* Returns input unmodified. | |
* @deprecated This function should only be called with string input. | |
* @param address - The address to convert. | |
* @returns Input unmodified | |
*/ | |
export function toChecksumHexAddress<T extends null | undefined>(address: T): T; | |
/** | |
* Convert an address to a checksummed hexadecimal address. | |
* | |
* @param address - The address to convert. | |
* @returns A 0x-prefixed hexadecimal checksummed address, if address is valid. Otherwise if input is string, returns it 0x-prefixed. If input is not string, returns it unmodified. | |
*/ | |
export function toChecksumHexAddress<T extends null | string | undefined>( | |
address: T, | |
) { |
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 agree with @legobeat. | null | undefined
should be part of the address
param's type signature.
Especially with exactOptionalPropertyTypes
enabled, the only clear use case for | undefined
over ?:
is when undefined
is explicitly being assigned or passed in, not just an uninstantiated or omitted value. And that's exactly the case here.
I don't think overloading is necessary, especially since type signatures for overloaded functions are not inferred properly by TypeScript anyway.
- See Inconsistent type inference on overloaded function types microsoft/TypeScript#57351
- We see this with the
getNetworkClientById
method inNetworkController
. Its return type is always inferred asAutoManagedNetworkClient<CustomNetworkClientConfiguration>
.
We also don't need to narrow address
beyond string
, so there might not be a strong need to make this generic.
I think we could do this the simple way:
* @returns A 0x-prefixed hexidecimal checksummed address, if address is valid. Otherwise original input 0x-prefixe, if address is valid. Otherwise original input 0x-prefixed. | |
*/ | |
export function toChecksumHexAddress(address: string) { | |
* @returns A 0x-prefixed hexidecimal checksummed address, if address is valid. Otherwise original input 0x-prefixe, if address is valid. Otherwise original input 0x-prefixed. | |
*/ | |
export function toChecksumHexAddress(address: string | null | undefined) { |
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.
While I agree with @legobeat and @MajorLift, the logic added by @mcmire is easing the function for all types of input other than string
, so we are "supporting" many more types than just null
or undefined
- with this in mind, perhaps a string | unknown
type would make more sense?
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.
Hmm. I see the point being made here, but the problem with changing the argument type is that this also changes the return type. So I think we would need overloads in order to communicate that:
- When
address
is not a string, the return type is the same - When
address
is a string, the return type is a string (and really, it's aHex
)
Otherwise, if the argument type is unknown
, then the return type is inferred to be unknown
. This causes problems with how it's used in e.g. AddressBookController (and any other place), and the resulting address would need to be checked for whether it is really a hex string. It seems like we should avoid this.
Thoughts on the best approach?
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.
@mikesposito That's a great point! @mcmire In that case, using overloads and generics the way @legobeat originally suggested seems like the best way (with nullable types not handled explicitly).
Here's the overloaded function signature:
/** Overload signatures */
export function toChecksumHexAddress(address: string): `0x${string}`;
export function toChecksumHexAddress<T>(address: T): T;
/** Implementation signature */
export function toChecksumHexAddress(address: unknown): unknown {
...
}
And here's a Playground link showing that the correct overload signature (and return type) is inferred for string vs. non-string input.
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.
unknown
sounds reasonable! Generics do seem necessary to get the expected return type, though: https://github.com/MetaMask/core/actions/runs/8268140049/job/22620308101?pr=4046
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.
My overloaded version (#4046 (comment)) runs into this error:
packages/preferences-controller/src/PreferencesController.ts:261:24 - error TS2538: Type 'unknown' cannot be used as an index type.
261 if (identities[address]) {
~~~~~~~
packages/preferences-controller/src/PreferencesController.ts:266:20 - error TS2538: Type 'unknown' cannot be used as an index type.
266 identities[address] = {
~~~~~~~
This is fixed with the following:
diff --git a/packages/preferences-controller/src/PreferencesController.ts b/packages/preferences-controller/src/PreferencesController.ts
index 2bf761950..b7451b2c2 100644
--- a/packages/preferences-controller/src/PreferencesController.ts
+++ b/packages/preferences-controller/src/PreferencesController.ts
@@ -254,7 +254,9 @@ export class PreferencesController extends BaseController<
* @param addresses - List of addresses to use to generate new identities.
*/
addIdentities(addresses: string[]) {
- const checksummedAddresses = addresses.map(toChecksumHexAddress);
+ const checksummedAddresses = addresses.map((address) =>
+ toChecksumHexAddress(address),
+ );
this.update((state) => {
const { identities } = state;
for (const address of checksummedAddresses) {
This is interesting. I didn't know passing a callback into map
can interfere with type inference like this.
*/ | ||
export function toChecksumHexAddress(address: string) { | ||
export function toChecksumHexAddress<T>(address: T) { |
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 relies on the return type being string | T
. Setting it to T
causes the following error:
Type 'string' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'string'.ts(2322)
IMO string | T
defeats the purpose of using the generic param T
.
How do you feel about adapting the overload you suggested earlier (#4046 (comment)), so that it distinctly handles string vs. non-string input, inferring the return types for each, and narrows the non-generic return type to Hex
instead of string
(playground link)?
Here's the diff for this change, including an error fix (see #4046 (comment)). This passes yarn build
, yarn test
.
diff --git a/packages/controller-utils/src/util.ts b/packages/controller-utils/src/util.ts
index f06cf6391..e5da29a81 100644
--- a/packages/controller-utils/src/util.ts
+++ b/packages/controller-utils/src/util.ts
@@ -250,13 +250,27 @@ export async function safelyExecuteWithTimeout<Result>(
}
}
+/**
+ * Convert an address to a checksummed hexadecimal address.
+ *
+ * @param address - The address to convert.
+ * @returns A 0x-prefixed hexadecimal checksummed address, if address is valid. Otherwise returns it 0x-prefixed.
+ */
+export function toChecksumHexAddress(address: string): Hex;
+/**
+ * Returns input unmodified.
+ * @deprecated This function should only be called with string input.
+ * @param address - The address to convert.
+ * @returns Unmodified input
+ */
+export function toChecksumHexAddress<T>(address: T): T;
/**
* Convert an address to a checksummed hexadecimal address.
*
* @param address - The address to convert.
* @returns The address in 0x-prefixed hexadecimal checksummed form if it is valid, or untouched otherwise.
*/
-export function toChecksumHexAddress<T>(address: T) {
+export function toChecksumHexAddress(address: unknown): unknown {
if (typeof address !== 'string') {
// Mimic behavior of `addHexPrefix` from `ethereumjs-util` (which this
// function was previously using) for backward compatibility.
diff --git a/packages/preferences-controller/src/PreferencesController.ts b/packages/preferences-controller/src/PreferencesController.ts
index 2bf761950..b7451b2c2 100644
--- a/packages/preferences-controller/src/PreferencesController.ts
+++ b/packages/preferences-controller/src/PreferencesController.ts
@@ -254,7 +254,9 @@ export class PreferencesController extends BaseController<
* @param addresses - List of addresses to use to generate new identities.
*/
addIdentities(addresses: string[]) {
- const checksummedAddresses = addresses.map(toChecksumHexAddress);
+ const checksummedAddresses = addresses.map((address) =>
+ toChecksumHexAddress(address),
+ );
this.update((state) => {
const { identities } = state;
for (const address of checksummedAddresses) {
@MajorLift @legobeat I've incorporated the feedback above. Let me know what you think! |
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.
LGTM!
*/ | ||
export function toChecksumHexAddress(address: string) { | ||
export function toChecksumHexAddress(address: string): 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.
Good catch!
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.
Why not return Hex
? 🤔 This does not have to overlap with the generic one just below, if that's the idea?
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.
After I made that suggestion I looked back and realized that it didn't return Hex
before, it always returned string
before, regardless of the input. Hex
does make sense, but if we changed that, it would be breaking — it would not be a regression fix. So I wanted to avoid that for now. Does that make sense / sound right?
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 is narrowing the return type breaking?
As opposed to:
- Widening return type would/could be breaking
- Narrowing input parameter type is generally breaking
We've temporarily gotten around this problem in the extension with MetaMask/metamask-extension#22928, so, while this is still important to land, we have time to make sure that @legobeat is on board. |
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.
Seems more semantic to constrain the return type to Hex
for string input but otherwise looks good to go.
*/ | ||
export function toChecksumHexAddress(address: string) { | ||
export function toChecksumHexAddress(address: string): 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.
export function toChecksumHexAddress(address: string): string; | |
export function toChecksumHexAddress(address: string): Hex; |
I am going to merge this out of an abundance of caution, but the question about whether narrowing the return type causes a breaking change is a good one. I will continue the conversation above. |
## Explanation ## References ## Changelog ## [13.0.0] Accounts Controller ### Changed - Fix update setSelectedAccount to throw if the id is not found ([#4167](#4167)) - Fix normal account indexing naming with index gap ([#4089](#4089)) - **BREAKING** Bump peer dependency `@metamask/snaps-controllers` to `^6.0.3` and dependencies `@metamask/snaps-sdk` to `^3.1.1`, `@metamask/eth-snap-keyring` to `^3.0.0`([#4090](#4090)) ## [28.0.0] Assets Controller ### Added - Add reservoir migration ([#4030](#4030)) ### Changed - Fix getting nft tokenURI ([#4136](#4136)) - **BREAKING** Bump peer dependency on `@metamask/keyring-controller` ([#4090](#4090)) - Fix token detection during account change ([#4133](#4133)) - Fix update nft metadata when toggles off ([#4096](#4096)) - Adds `tokenMethodIncreaseAllowance` ([#4069](#4069)) - Fix mantle token mispriced ([#4045](#4045)) ## [15.0.0] Keyring Controller ### Changed - **BREAKING** use getAccounts on HD Keyring when calling addNewAccount ([#4158](#4158)) - Pass CAIP-2 scope to execution context ([#4090](#4090)) - Allow gas limits to be changed during #addPaymasterData ([#3942](#3942)) ## [10.0.0] Preferences Controller ### Changed - **BREAKING** Bump peer dependency on `@metamask/keyring-controller` to `^15.0.0` ([#4090](#4090)) - Restore previous behavior of toChecksumHexAddress ([#4046](#4046)) ## [15.0.0] Signature Controller ### Changed - **BREAKING** Bump peer dependency on `@metamask/keyring-controller` to `^15.0.0` ([#4090](#4090)) ## [8.0.0] User Operation Controller ### Changed - **BREAKING** Bump peer dependency on `@metamask/keyring-controller` to `^15.0.0` and Pass CAIP-2 scope to execution context ([#4090](#4090)) - Allow gas limits to be changed during #addPaymasterData ([#3942](#3942)) ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate
Explanation
Prior to 40acc6c,
toChecksumHexAddress
in@metamask/controller-utils
would not throw when givenundefined
ornull
. Now it does, becauseaddHexPrefix
fromethereumjs-util
has been replaced withadd0x
from@metamask/utils
, and the latter is more strict when it comes to input.This change is causing some tests on the extension side to fail. Granted, these tests are likely creating an incomplete state object, so they ought to be fixed so that they pass a string to
toChecksumHexAddress
. However, these test failures serve as a reminder that there may be other parts of the extension codebase not covered by tests which are usingtoChecksumHexAddress
incorrectly. If the extension were using TypeScript throughout, finding these problem areas would be trivial, because we would have seen type errors already. But because the extension codebase is still primarily written in JavaScript, we cannot guarantee thattoChecksumHexAddress
won't throw for some particular use case even after we fix the obvious usages.Therefore, to prevent unexpected runtime errors, this commit restores the existing behavior of
toChecksumHexAddress
.References
We've been talking about this in our company chat but there are also a couple of PRs open on the extension side which get around the issue described above. This is a more direct fix.
Changelog
@metamask/controller-utils
toChecksumHexAddress
to take and handle non-string inputs again, which was removed in 8.0.4.Checklist