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

Add TokenAmount #757

Closed
wants to merge 8 commits into from
Closed

Add TokenAmount #757

wants to merge 8 commits into from

Conversation

bpierre
Copy link
Contributor

@bpierre bpierre commented Apr 12, 2020

This class will help to consolidate a token amount into its own type. For now it is mostly about using its format() method, but we can imagine adding other things in the future.

We might also want to make some aragonUI components aware of it, e.g. a <TransferAmount /> component could be used to represent the transferred amounts in the Finance app, and accept TokenAmount instances.

About this, any thoughts on exporting a class to use with new TokenAmount() vs. a tokenAmount() function that would do the same thing? Since this is primarily a components library, I was thinking that we might want to avoid exporting names that might conflict with component names in the future.

@sohkai
Copy link
Contributor

sohkai commented Apr 12, 2020

About this, any thoughts on exporting a class to use with new TokenAmount() vs. a tokenAmount() function that would do the same thing? Since this is primarily a components library, I was thinking that we might want to avoid exporting names that might conflict with component names in the future.

I like leaving it as a class—that was it is and introducing a factory just to hide the export is contrived. I wonder if we could hide it under a namespace somehow, e.g. import { TokenAmount } from '@aragon/ui/utils'? Or maybe under a formatters namespace or export?

Copy link
Contributor

@sohkai sohkai left a comment

Choose a reason for hiding this comment

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

❤️ 😍 Can't wait to use this!

src/utils/TokenAmount.js Outdated Show resolved Hide resolved
* @returns {BigInt}
*/
amount() {
return BigInt(this.#amount.toString())
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we return a copy of the JSBI object instead?

Or maybe do a quick availability check for BigInt existing natively?

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 would avoid returning a different type implicitly: JSBI and BigInt have different APIs and we should be able to rely on one of them from the other side.

What about adding a .amountJsbi() method? I’d like to keep .amount() for BigInt only, as it is the only one we should use eventually.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I had assumed JSBI and BigInt were as close as possible API-wise.

What do you think about just returning a string here instead as the default API? I think we'll still be using strings to represent BNs for a while until we've all moved to BigInt... this will make it less confusing / failure-prone if we wanted to transfer a TokenAmount into one of the other BN libraries (especially web3 / ethers—they always accept strings, but the BN types are a mishmash).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could, but the downside is that we will have to break compatibility at some point if we want to keep using amount() to return a BigInt. Something I like about amountString() is that it is just verbose enough to push users towards using amount() as soon as they can 😄

What do you think about using a asString boolean parameter? amount() would return a BigInt while amount(true) would return a String.

About BigInt more generally, I think the most important thing would be to get support in Safari, because we can’t use BigInt at all without it, even as an intermediary step. I am not as worried for libraries support, because users can convert the type themselves before feeding it to their library. I’m also not too worried about libraries adding support for it: they all support string integers already, so supporting BigInt only requires a String(value) on the parameter.

For Safari, it seems that support for BigInt is coming in the next version 🎉🎉🎉 https://developer.apple.com/safari/technology-preview/release-notes/

Ethers.js v5 will have full support for BigInt: ethers-io/ethers.js#594 (comment)

Web3.js seems to have plans to support BigInt, but it doesn’t seem to be done yet.

Copy link
Contributor

@sohkai sohkai Apr 14, 2020

Choose a reason for hiding this comment

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

I'd honestly default to returning strings as the "base denominator" BN type until we see massive user adoption of BigInts, just so people can fall into the "pit of success" today.

I greatly prefer this over forcing users to learn how to use a new type / look up why their toolchain doesn't support BigInts natively and have to do a string cast. This should be easy.

I really don't think the string serialization into a BigInt will cost that much; if someone is serializing that many strings, they probably have much bigger performance problems than the small waste of us doing a roundtrip back/forth BigInts/strings.


if we want to have amount() return a BigInt

As you can tell, I'm not convinced we will want to do this in the next two years. Is there a reason that makes you think exposing the BigInt type is going to be preferred in that time frame?

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'd honestly default to returning strings as the "base denominator" BN type until we see massive user adoption of BigInts, just so people can fall into the "pit of success" today.

I think it’s also a valid choice, let’s do this for now then. We have a bunch of other things returning strings rather than BigInt (e.g. useWallet()), so we could wait and move them all at the same time whenever it makes sense.

As you can tell, I'm not convinced we will want to do this in the next two years. Is there a reason that makes you think exposing the BigInt type is going to be preferred in that time frame?

Two years seems really far to me if Safari support is right around the corner, but time will tell! I anticipate a quick adoption of BigInt in the web3 ecosystem once Safari supports it, at least as input values. In I might be wrong, but I think it provides clear benefits:

  • No need to import and ship one or multiple libraries to deal with big integers (on both sides, libraries and apps).
  • Being able to use operators directly e.g. amount = 38n * 10n ** 18n vs. amount = (new BN('38')).mul((new BN('10')).pow(new BN('18')).
  • More than operators, being able to use the rest of the language with it, e.g. Intl.NumberFormat.
  • For libraries, no need to export non-standard types, provide the libraries to work with them, or have to deal with version incompatibilities (e.g. Ethers’s embedded bignumber utility).
  • Stop doing conversions everywhere between the different types (not so much for the performances cost than for the additional complexity).
  • On input, supporting BigInt for libraries is trivial and can be done today, even without Safari support.
  • On output (assuming support in Safari is there), it would break compatibility for existing libraries. But for authors already using BigInt, first class support for it might become an important factor when choosing a library.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

src/utils/TokenAmount.js Show resolved Hide resolved
src/utils/TokenAmount.js Outdated Show resolved Hide resolved
@bpierre
Copy link
Contributor Author

bpierre commented Apr 14, 2020

@sohkai I forgot to answer this comment:

I like leaving it as a class—that was it is and introducing a factory just to hide the export is contrived.

I agree about the nice semantics of a constructor, my main concern is really about conflicting with a potential <TokenAmount /> component in the future. But I also see other reasons why we would want to use a factory:

  • To be consistent with color() (same reason, <Color /> might exist one day), and more generally, trying to reserve PascalCase for React components.
  • new can look a bit awkward when a temporary object is needed:
// factory
<Tag
  background={color('#FFF').alpha(0.5)}
  label={tokenAmount(amount, decimals).format()}
/>

// constructor
<Tag
  background={(new Color('#FFF')).alpha(0.5)}
  label={(new TokenAmount(amount, decimals)).format()}
/>

Also factories are pretty common in JS, even within the language recently (Symbol(), BigInt()), so I don’t think it would be confusing for users.

I wonder if we could hide it under a namespace somehow, e.g. import { TokenAmount } from '@aragon/ui/utils'?

This doesn’t work, because the main & module fields of package.json only apply to the root… or we would need a @aragon/ui/cjs/utils and a @aragon/ui/utils ☹️

Also we would users would still have to do things like this if TokenAmount becomes a component one day:

import { TokenAmount } from '@aragon/ui'
import { TokenAmount as TokenAmountSomething } from '@aragon/ui/utils'

function App() {
  const amount = new TokenAmountSomething()
  return <TokenAmount amount={amount} />
}

Or maybe under a formatters namespace or export?

That could be an idea yes! I guess we would also need to introduce modules like math, date etc. in that case. The only issue is that it will make it impossible to tree shake inside these modules, but I guess we could split them if it becomes needed.

Now in case of conflict, users would still deal with similar names, but at least they wouldn’t have to rename their imports:

// using utils for now because I’m not sure what could be a category for the TokenAmount class
import { utils, TokenAmount } from '@aragon/ui'

function App() {
  const amount = new utils.TokenAmount()
  return <TokenAmount amount={amount} />
}

@Evalir
Copy link
Contributor

Evalir commented May 4, 2020

Or maybe under a formatters namespace or export?

I believe this is the way to go, for a few reasons:

  • As @bpierre said, lets us introduce new modules for date, math utils and etc; these might be useful to expose as standalone packages.
  • This also lets us do something which I think is quite nice: being able to release separate packages from aragonUI that contain these utils. Material UI does this, and I believe it gives a good separation of concerns over what is where, because right now there's a lot to think about when you import components, utilities and hooks from aragonUI. See this pattern (how we do it, alphabetized) vs. this pattern, which separates components, hooks and utilities (Even though I would've personally put all components together, but, I still think it's a nice approach and you get the idea 😄).

my main concern is really about conflicting with a potential <TokenAmount /> component in the future.

I feel such a component would be unnatural (at least for me) — I expect a component to be purely visual from an "end-user" standpoint, and <TokenAmount /> for me feels like trying to accomplish the same thing as having a formatting function, bundled with some HTML for display, which would end up quite coupled to the design system.

For the reason mentioned above, I prefer the class approach; it's clear to me, and also, thinking about how I would use it, I expect to pass this newly created TokenAmount instance to various places; I feel more comfortable having a class/object that I feel is "malleable" rather than a factory, which I feel it forces me to re-declare this variable to reuse it (doing something like tokenAmount().format(). doesn't feel too nice for me).

@stale stale bot added the abandoned label Jun 3, 2020
@aragon aragon deleted a comment from stale bot Jun 3, 2020
@stale stale bot removed the abandoned label Jun 3, 2020
@bpierre
Copy link
Contributor Author

bpierre commented Jun 4, 2020

Closing for now, TokenAmount will move in its own micro library https://github.com/aragon/aragon-ui/issues/772

@bpierre bpierre closed this Jun 4, 2020
@sohkai sohkai deleted the token-amount branch June 7, 2020 21:05
@sohkai
Copy link
Contributor

sohkai commented Jun 7, 2020

To come back to this point @Evalir:

This also lets us do something which I think is quite nice: being able to release separate packages from aragonUI that contain these utils. Material UI does this, and I believe it gives a good separation of concerns

Agreed, and I think what we're seeing internally for frontend libraries (leading to #772) is:

  • aragonUI is our collection of easily reusable React components
  • A number of supporting packages (use-x, token-amount, etc.) that are single purpose and may be useful even without aragonUI
    • Over time, some utilities of aragonUI may be moved out into supporting packages, rather than publishing them as aragonUI modules
    • It is not clear how we could best organize these yet, but single repos for each is never wrong.

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.

3 participants