Replies: 2 comments
-
Dang, now I wish I'd waited for 3 more issues to be filed before mine just to be #1000 :( |
Beta Was this translation helpful? Give feedback.
-
LOL! Better luck next time. ;) I personally use ethers a lot... Like, a lot-lot. Which is why I wrote and maintain it in the first place. Like, I can't imagine someone using it more than me. :p Which is one of the reasons I use (optionally) deferred types, which is a feature of ethers I use extensively, both as a consumer of the library and internally. It also allows the library to be "more optimal" in many cases, since otherwise I see people performing multiple Deferred types are only used when the entry point itself is a something that is asynchronous; for example, I won't argue that async things are often difficult to debug, but things are often difficult to debug (in general), so I don't think that is a good enough reason to remove something I consider so useful. You can of course pass in only resolved properties, in which case many of these (but I agree, not all) go away.
I've thought of this before as well, and am considering it for v6. The syntax overhead goes through the roof, but for people who want to use it, it might make sense. Keep in mind that Deferred types could still work fine, as a uint48 (or smaller) could accept a Which brings up a quick point, I have tried both deferred and non-deferred solutions; I often (when making the beta major changes) make changes like this, and port an application or few over to use the new method to determine how it "feels". In v6, the Anyways, I don't know that I've appeased you at all, but I am highly unlikely to removed the deferrable types; I am working on making error messaging more clear as to why/where things failed though. I'll leave this around though, in case you or anyone else wants to continue the discussion, but I'll move it to the new discussions features. :) Thanks! :) (sorry for the delay, but I knew this would be a longer-than-usualy-resposne, and I still didn't get to all the points that went through my head, but it's probably better to get something out, than leave this around seemingly abandoned. ;)) |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Hi!
Interfacing with ethereum leads to a few tough challenges. One of the biggest ones arguably being parameter conversions and formatting. Currently, ethers.js goes to great lengths to try and (asynchronously) resolve arbitrary input parameters for contract functions to their respective parameter types. I am assuming that this was done to try and improve the developer experience by being less restrictive when passing parameters to contract functions. While the intent is commendable, the concrete implementation leads to a plethora of problems that arguably negate the superficial DX improvement. Ambiguous parameter types and type conversions and passing of deferred values lead to impossible-to-debug scenarios as well as gridlocks of inter-dependent promises and undesired side-effects (e.g. ENS address resolution).
Here are just a few of the more concrete problems that arise from this:
Handling of input parameters for contract calls
Example: Deploying two contracts that allow each other to be registered through their respective constructor arguments. Since it's allowed by the API to pass each of the contract instances as arguments to each other, it's possible to get stuck in a gridlock. Now, if both of these deployments happen side-by-side in the same file it's easy to find the culprit, but in a more complex deployment script it can be quite tricky to find the reason for the script getting stuck.
Imagine you accidentally passed a wrong value (promise of an arbitrary string) to a function parameter of ParamType "address", ethers.js will now try to resolve this promise for you, realizes it's not a valid address and then go through a range of different attempts to resolve this address. Once it fails to resolve it, the error the user is presented with comes from deep down in the function call stack of ethers.js and it's impossible to find the actual culprit (the line of code where you are accidentally passing the wrong parameter). In summary: Functions should never resolve promises on behalf of the callee unless that's their only purpose (so you can await it so that the error bubbles up all the way to the callee in a predictable way).
Attempting to automatically resolve strings through ENS when passed to an address ParamType without clear intent communicated by the callee could lead to potential dire consequences. The scenario is quite unlikely but it's possible that you end up calling your function with a wrong value without even realizing what's going on. Furthermore, if the resolution of the address fails, the error is again coming form deep down in the ethers.js call stack making it extremely hard to debug. I've seen people complain about this in the buidler social media channels quite regularly over the past weeks ("Why is Buidler trying to call ENS?"). TLDR: A function should not attempt to arbitrarily upcast values through an asynchronous service that yields unpredictable results unless the callee is explicitly signalling intent to do so (by explicitly calling and awaiting a function for that)
Suggestion for enforcing strict TypeScript scalar types (including runtime validation)
TypeScript actually has quite a bit more to offer than just it's automatic type inference and static type definitions. One of the features that could prove to be quite interesting and helpful for this particular problem space (interfacing with the strict type universe of solidity) are so called "custom" or "user defined" type guards (built-in typeguards are e.g.
typeof
orinstanceof
and derived checks). Together with unique symbols, it would be possible to create a solidity type dictionary within ethers.js that could then be enforced in user space, resulting in a few great benefits (despite the arguably increased syntax verbosity).Let me illustrate that through an example using e.g. the
address
scalar:Note how this could also be used to strictly type the output of such a call. In this case, uint256 would simply be a constrained type wrapper around BigNumber.
Any incorrectly passed parameters or otherwise wrong values passed to such a function would immediately fail in user-space code and be super easy to pinpoint and debug because due to TypeScript requiring the usage of the type guard, the user would have no choice other than call
address()
on the passed argument. Hence, we'd be covering both development-time and runtime errors.Beta Was this translation helpful? Give feedback.
All reactions