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

[WIP] Migrate to TypeScript, add tests #6

Closed
wants to merge 10 commits into from

Conversation

kyranjamie
Copy link
Contributor

@kyranjamie kyranjamie commented Dec 6, 2019

Closes #5.

May as well wait on Matt's PR in stacks-network/c32check#1 to utilise its typings.

Haven't checked building/packaging this yet. Unfamiliar with microbundle , looks like it works with TypeScript though.

Thoughts/Questions

  • Shall we kill the JSDoc comments now we have type info?
  • Would vote to simplify the API of units.ts files to only accept number
    • Benefit of doing this is we don't have to handle error cases when user passes invalid string
  • Add support for BigInt?
  • What should be the correct result for stacksToMicro(Number.MAX_SAFE_INTEGER)? Currently 9.007199254740991e+21
  • Best behaviour for stacksToMicro(Infinity | NaN)
    • I'd suggest throwing error, leaving it to the callee to ensure valid units are passed

  • Type tx interface
  • Add tests

@zone117x
Copy link
Member

zone117x commented Dec 7, 2019

stacks-network/c32check#1 has been merged and deployed to npm. Just pushed a changed to this PR with the version bump -- all tests still passing

@zone117x
Copy link
Member

zone117x commented Dec 7, 2019

@kyranjamie

  • Would vote to simplify the API of units.ts files to only accept number
  • Add support for BigInt?
  • What should be the correct result for stacksToMicro(Number.MAX_SAFE_INTEGER)? Currently 9.007199254740991e+21
  • Best behaviour for stacksToMicro(Infinity | NaN)

All of these are indicative of the underlying problem with using floating point numbers for currency.

This is bad [1], [2], [3], [4], [5].

Compounding on the problem is that these topics are referring to fiat currency with two decimals places (usually representable with 64 bits). Floating points with cryptocurrency is especially bad since we are dealing with 256 bit numbers.

TLDR:
We should definitely not use js number types, and we should definitely use bigint or fixed-point numbers.

For JSON representation and lib interop, typically string representation is used. Either a string representing the full integer (in hex or base10) or the fixed decimal representation (6 decimal points for stx).

For numerical operations in the lib, I find the bignumber.js lib to be useful. It can be configured to represent a fixed decimal point, and formatted as such. The native ES2020 BigInt has a limited API and runtime support. For example, represent microstacks as stacks, using only native ES2020 BigInt, the function looks something like:

const microstacks = 18470754444446n;
const stacks = `${microstacks / 1000000n}.${microstacks % 1000000n}`;
console.log(stacks);
// > "18470754.444446"

@kyranjamie
Copy link
Contributor Author

kyranjamie commented Dec 8, 2019

Thanks for the comments Matt. Understand the floating point imprecision issues.

For a utility library like this, I'm wondering what the most helpful API to expose is. Are you saying these helper functions shouldn't accept number at all? Provided all mathematical operations performed internally use BigNumber, or equivalent, simply passing a number is safe*. Equally, I don't think this library should dictate you pass it a BigNumber type, just in case you another safe math lib.

Tbh I'm not a big fan of number strings, unless they're really needed, as the lib then needs to handle parsing errors/edge cases. Invalid strings, locale-specific formatting with commas vs full stops to separate 1000 units etc.

Given the use case, we should be okay without:

// Number.MAX_SAFE_INTEGER > total supply of stacks in micro stacks
9007199254740991 > 2048913388 * 1000000 // true

Most the downfalls of using number, as highlighted in the BigNumber docs, shouldn't present a problem for consumers of this lib.

Using number and BigInt keeps to the language spec and doesn't involve any third party lib buy-in. TypeScript added BigInt support in 3.2.. There's a BigInt babel plugin, too. I haven't used it but hopefully that'd handle the browser environments that don't support it.

If you are using BigNumber:

const sum = new BigNumber(someNumber).toNumber();
const result = new BigNumber(microToStacks(sum));

Are there any other reasons to use strings, or ways in which imprecision could be introduced? I worry that this choice would result in superfluous number.toString() calls for edge cases that won't realistically present a problem. Interested if there are any other libraries that follow a string only pattern.

* With some edge cases. Numbers outside the bounds of Number.MAX_SAFE_INTEGER or numbers greater than 15decimals.

@kyranjamie kyranjamie force-pushed the feature/migrate-typescript branch 2 times, most recently from b0e2f4d to ce41202 Compare January 3, 2020 11:09
@kyranjamie kyranjamie changed the title [WIP] Migrate to TypeScript [WIP] Migrate to TypeScript, add tests Jan 3, 2020
@kyranjamie kyranjamie force-pushed the feature/migrate-typescript branch from fdb4c64 to ca4151b Compare January 3, 2020 11:29
@jcnelson
Copy link
Member

jcnelson commented Jan 3, 2020

I want to second @zone117x 's opinion. The use of number, or any imprecise floating-point representation for that matter, should be anathema in libraries that deal with currencies. Moreover, every cryptocurrency (including ours) uses an integer representation to do its accounting -- in our case, we use microSTX (1 STX = 100,000 microSTX). So, there really is no reason to use a floating-point representation anywhere.

Provided all mathematical operations performed internally use BigNumber, or equivalent, simply passing a number is safe*.

No, it isn't. Even using number in the interface is just pushing the loss of precision it entails into the application, which is just as problematic.

With some edge cases. Numbers outside the bounds of Number.MAX_SAFE_INTEGER or numbers greater than 15decimals.

There are infinitely many Stacks -- inflation never stops, but maxes out at 300 STX/block after 15 years. The Rust implementation uses an unsigned 128-bit integer to represent microSTX, which means it can handle up to 3402823669209384634633746074317682 STX (340282366920938463463374607431768211455 microSTX) before problems arise.

@zone117x
Copy link
Member

zone117x commented Jan 3, 2020

Supporting the native BigInt as a type for microSTX in the public API makes sense.

For return values representing STX, I think a decimal string is common practice and reasonable. Here's an example in paypal's API: https://developer.paypal.com/docs/api/orders/v2/

Code dealing with localization should generally treat . as the culture-invariant format for decimal separators, and parse/format using the appropriate locale-aware code from the client.

IMO the API should look something like:

// Culture-invariant microSTX to STX operation.
// Input should contain no thousands or decimal separators.
// Returns a string with `.` decimal separator, padded to stx 6 decimals.
// ex: '1' to '0.000001', or '1000000' to '1.000000'.
function microToStacks(microStx: BigInt | string): string;

// Culture-invariant STX to microSTX operation.
// Input should contain '.' decimal separator, and not contain thousand separators.
// Returns an integer string with no decimal or thousands separators.
function stacksToMicro(stx: string, outputEncoding = 'bigint'): BigInt;
function stacksToMicro(stx: string): string;

// Locale-aware formatting for display of stacks data.
// ex: '1000.3' input to '1.000,300000' output.
function formatStacks(stx: string): string;

// Locale-aware parsing from user entry.
// ex: '1.000,3' input to '1000.300000' output.
function parseStacks(stx: string): string;

Locale-aware formatting and parsing functions/options could use Intl.NumberFormat when running on client, or req.headers['accept-language'] when running on server with next.js or w/e.

I think BigNumber.js would also be a reasonable replacement for string, as it supports fixed point decimals, and formatting with locale options.

@kyranjamie
Copy link
Contributor Author

Alright, let's not use number.

I was looking at this very much from a browser-environment perspective, where number is unavoidable. But we can leave parsing to consumers of the library.

@kyranjamie kyranjamie force-pushed the feature/migrate-typescript branch from 2e43fc9 to 489fe8d Compare January 6, 2020 13:23
@kyranjamie kyranjamie force-pushed the feature/migrate-typescript branch from 489fe8d to e5777ff Compare January 7, 2020 10:57
@kyranjamie kyranjamie force-pushed the feature/migrate-typescript branch from e5777ff to 53f1545 Compare January 7, 2020 12:32
@CLAassistant
Copy link

CLAassistant commented Nov 16, 2023

CLA assistant check
All committers have signed the CLA.

@kyranjamie kyranjamie closed this Nov 20, 2023
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.

Use TypeScript
4 participants