Skip to content
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

Migrate from JavaScript to TypeScript #74

Merged
merged 12 commits into from Jan 22, 2020
Merged

Conversation

KiChjang
Copy link
Contributor

@KiChjang KiChjang commented Jan 8, 2020

Fixes #59. Fixes #70.

As I feared initially, there are some difficulties when trying to type some parameters and values correctly. msgParams.data in particular is quite problematic -- I've come to understand it as a union between an object and a string according to usage, but the function body does not seem to indicate that it properly handles the cases where it is a string type, and instead assumes that it's always an object.

I'm creating a draft pull request now just to have more eyes looking at what I've managed to convert. Currently it cannot be compiled by the TypeScript compiler due to the aforementioned problems I pointed out above.

@KiChjang
Copy link
Contributor Author

KiChjang commented Jan 8, 2020

I just found out from #68 that there used to be a typings declaration file for this project, but was removed in that PR. I think I'd be able to get some ideas from that file.

@KiChjang
Copy link
Contributor Author

KiChjang commented Jan 8, 2020

@danfinlay I'm pinging you now because the msgParams typing is completely out of whack. According to the function bodies of recoverTypedSignature{,Legacy,_v4}, we should expect that msgParams.data to be of the string type. However, in the test files, recoverTypedSignatureLegacy is called with a msgParams.data field with an object type.

I am not sure how to proceed from here -- arguably the test file should stringize the object type first (by way of hashing I suppose?), but I can see that it would also make sense for recoverTypedSignature* functions to hash msgParams.data should they detect that the data field is not of the string type.

Secondly, I am not sure what the purpose of the TypedDataUtils.sign function is, because it seems to me that it wants to construct a properly typed object that conforms to the message schema, and so it would make sense for the typedData parameter to be of an object type that somewhat follows the message schema, perhaps with some fields missing. However, according to the call sites for this function, it often accepts a string type for the typedData parameter. This makes me highly suspect that there has been subtle bugs on the implementation for this function, since there is a disagreement about the inferred types from usage when looking at the function body vs the inferred types at the call site.

I will push a commit on my current work on attempting to type msgParams and typedData correctly. I'm sure there are other places and functions where type disagreements happen. What do you think is the best course of action here?

@danfinlay
Copy link
Contributor

danfinlay commented Jan 8, 2020

Thanks for the quick investigation, @KiChjang.

I think it's very likely that the typefile is just bad, and we may need to disregard a lot of it.

I definitely think using an object as params is plainly superior, and we should refer to the testfile and the sig-examples repo as more realistic reference points instead when possible.

As for TypedUtils.sign, it seems clear it does no signing, it is clearly actually a hashing method. I believe it was broken out so that the hashing functionality could be tested independently.

As some bug fixes show up along the way, I am happy to add to the bounty, but it is tricky to price fixes along the way fairly, so feel free to suggest what would keep you feeling motivated, and if we agree to continue, we will pay out, and if not, we will simply pay you the bounty for identifying additional issues and complexity that need to be addressed before the conversion can be easily completed.

@KiChjang
Copy link
Contributor Author

KiChjang commented Jan 9, 2020

@danfinlay I forgot to mention -- the type mismatches that I'm seeing right now is preventing the testfile to pass. I think I will change the behavior of certain functions a slight bit (e.g. asserting that msgParams.data must be of the string type instead of simply checking for undefinedness) and push a new commit up that makes the testfile pass, then it'll be ready for review.

With this PR, I believe behaviorally, nothing will change in this library (aside from the slight modifications I mentioned earlier, which I think is inconsequential because other functions would have thrown an exception if there is a type mismatch), so we could simply merge it and file follow-up bug issues and place a bounty on them accordingly.

@KiChjang KiChjang marked this pull request as ready for review January 9, 2020 00:48
@KiChjang
Copy link
Contributor Author

KiChjang commented Jan 9, 2020

The testfile has an interesting testcase where it references a variable called alice that doesn't exist. Should I remove that testcase altogether?

@danfinlay
Copy link
Contributor

Yes, removing broken tests like that is fine, I'll open an issue for adding a correct test for that case.

@KiChjang
Copy link
Contributor Author

KiChjang commented Jan 9, 2020

@danfinlay Let me know whether the current state on this PR is good to go.

@KiChjang
Copy link
Contributor Author

Rebased to master.

Copy link
Contributor

@whymarrh whymarrh left a comment

Choose a reason for hiding this comment

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

@KiChjang this is a great start! I've left a few comments, I think we might need to refine a few of these types to avoid using never and some of the casts in the test file.

package.json Outdated
@@ -29,6 +31,8 @@
"tweetnacl-util": "^0.15.0"
},
"devDependencies": {
"tape": "^4.9.1"
"tape": "^4.9.1",
"ts-node": "^8.5.4",
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe we can avoid this dependency by compiling the test file as well and executing the generated JS file directly. (Related to my comments on the tsconfig below.)

Copy link
Contributor

Choose a reason for hiding this comment

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

We can add the test directory to .include in the tscconfig file.

tsconfig.json Outdated
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"outDir": "./build",
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's leave this property out and output the JS files alongside the TS files—that produces the exact same package structure as before, allowing us to not have to modify the publish process.

package.json Outdated
@@ -4,7 +4,8 @@
"description": "A few useful functions for signing ethereum data",
"main": "index.js",
"scripts": {
"test": "node test/index.js"
"build": "tsc -d",
"test": "npm run build && ts-node test/index.ts"
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"test": "npm run build && ts-node test/index.ts"
"test": "npm run build && node test/index.js"

package.json Outdated
"tape": "^4.9.1"
"tape": "^4.9.1",
"ts-node": "^8.5.4",
"typescript": "3.4.3"
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use the latest version of TypeScript here?

package.json Outdated
@@ -4,7 +4,8 @@
"description": "A few useful functions for signing ethereum data",
"main": "index.js",
"scripts": {
"test": "node test/index.js"
"build": "tsc -d",
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"build": "tsc -d",
"build": "tsc --project .",

Let's use the --project flag here and keep all the compiler options in the tsconfig file. (We'll need to add "declaration": true to .compilerOptions there.)

test/index.ts Outdated
@@ -41,11 +41,11 @@ test('personalSign and recover', function (t) {
const privKeyHex = '4af1bceebf7f3634ec3cff8a2c38e51178d5d4ce585c52d6043e5e2cc3418bb0'
const privKey = Buffer.from(privKeyHex, 'hex')
const message = 'Hello, world!'
const msgParams = { data: message }
const msgParams: sigUtil.MsgParams<never> = { data: message }
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we using the never type here? The never type doesn't make sense to me, semantically. We might have some of these types wrong if we need this?

Copy link
Contributor Author

@KiChjang KiChjang Jan 10, 2020

Choose a reason for hiding this comment

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

Since TypedData<T> is a union type of string | EIP712TypedData | EIP712TypedData[] | TypedMessage<T>, the never type here signifies that we know for sure that the data field will never be of the TypedMessage<T> type, so it's safe to use never.

I don't particularly like this syntax either, so I'm open to suggestions on improving this.

Copy link
Contributor

Choose a reason for hiding this comment

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

so it's safe to use never

I don't contest that it's safe to use never here, I guess I'm more wondering if we can avoid using it in favour of something more semantic? I'll spend a bit more time thinking of an alternative.

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 have an idea -- instead of trying to parameterize on the TypedData type, we can instead parameterize on the API functions themselves, i.e. make TypedData parameter-less, and then on the functions, require MsgParams<TypedData | TypedMessage<T>>.

test/index.ts Outdated
@@ -453,7 +447,7 @@ test('signedTypeData', (t) => {
{ name: 'contents', type: 'string' }
],
},
primaryType: 'Mail',
primaryType: 'Mail' as 'Mail',
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need to cast this? primaryType should be a string, no? There are a few instances of @param {string} primaryType in the JSDoc comments and TYPED_MESSAGE_SCHEMA defines it as a string?

Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly for all of the below casts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

primaryType should be a string, no?

Not exactly. This is the type definition of TypedMessage:

export interface TypedMessage<T extends MessageTypes> {
  types: T;
  primaryType: keyof T;
  // ...
}

In other words, the type of primaryType should only consist of the keys that appear in types. I could have made this string instead, but I figured out that we'd probably want stronger typing guarantees, and what's the point of having a type system if we can't leverage it? ;)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, fair, I'm definitely in favour of stronger type guarantees. That said, this cast seems unnecessary—how can we best remove the need for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As I recall, this is currently a limitation of TypeScript, which doesn't allow object fields to be narrowed down to their literal types; perhaps this is the tracking issue? microsoft/TypeScript#16896

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A marginally better way would be to use as const instead of as 'Mail' here, but it's purely cosmetic.

package.json Outdated
@@ -21,6 +22,7 @@
},
"homepage": "https://github.com/MetaMask/eth-sig-util#readme",
"dependencies": {
"@types/node": "^13.1.2",
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we downgrade this to ^10 (refs #77)

@KiChjang
Copy link
Contributor Author

KiChjang commented Jan 10, 2020

Hmm... should I .gitignore the javascript files? I don't quite want future contributors to edit those files.

EDIT: Nevermind, I think I will do just that.

@KiChjang
Copy link
Contributor Author

@whymarrh Please have a look, I believe all your review comments are adequately addressed.

@whymarrh
Copy link
Contributor

@KiChjang thanks I'll have some time later today to look at this

Copy link
Contributor

@whymarrh whymarrh left a comment

Choose a reason for hiding this comment

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

This LGTM, the types definitely make a bit more sense now

@KiChjang
Copy link
Contributor Author

@danfinlay Looks like we have an OK from a reviewer, is there anything else left in this PR that you'd like me to address, or is it good to go?

@whymarrh
Copy link
Contributor

@rekmarks want to double-check this as well

@whymarrh whymarrh mentioned this pull request Jan 18, 2020
6 tasks
@rekmarks
Copy link
Member

@whymarrh I'll review over the course of the weekend. One initial thought is that we should get some linting up in here. I think there's an internal discussion to be had to determine our TypeScript lint rules (e.g. gaba is doing stuff I don't care for), so let's continue to not worry about style in this PR?

@whymarrh
Copy link
Contributor

One initial thought is that we should get some linting up in here. I think there's an internal discussion to be had to determine our TypeScript lint rules (e.g. gaba is doing stuff I don't care for), so let's continue to not worry about style in this PR?

Yeah let's ignore styling for this PR, once this is merged we can iterate on the project (linting, docs, etc.)

whymarrh
whymarrh previously approved these changes Jan 22, 2020
Copy link
Contributor

@whymarrh whymarrh left a comment

Choose a reason for hiding this comment

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

Thanks @KiChjang!

Copy link
Contributor

@whymarrh whymarrh left a comment

Choose a reason for hiding this comment

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

Fixed the build 😅 thanks again

@whymarrh whymarrh merged commit ff95e45 into MetaMask:master Jan 22, 2020
@KiChjang KiChjang deleted the typescript branch January 29, 2020 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Convert project to TypeScript Typings don't compile
4 participants