Skip to content

Commit

Permalink
[apps][part 1] add common utilities to @mysten/core (MystenLabs#8623)
Browse files Browse the repository at this point in the history
## Description 

This PR consolidates some duplicated frontend utilities like
`SentryRpcClient` and `useFormatCoin` by moving them into
`@mysten/core`. I also added all the typical stuff like linting,
formatting, testing, etc. to the library which is by no means perfect
but a good starting point for reducing duplication across our
applications. Specifically, I think we could look into sharing basic
ESLint, Prettier, and TypeScript settings to provide more consistency on
the developer experience front but that's for another day 😛

This is just part 1 where we're adding the utilities to `@mysten/core`;
we'll actually use the utilities in
MystenLabs#8632 since there were a lot of
files to change.

## Test Plan 
- Unit tests for `useFormatCoin` in the wallet and explorer were
identical albeit not fully comprehensive
- Did some spot-checking across the wallet and explorer to make price
formatting works as intended
- Tested all the npm scripts work for linting and testing
  • Loading branch information
williamrobertson13 authored Feb 28, 2023
1 parent 4593333 commit 46f1455
Show file tree
Hide file tree
Showing 16 changed files with 530 additions and 235 deletions.
12 changes: 12 additions & 0 deletions apps/core/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

module.exports = {
root: true,
extends: ['react-app', 'prettier', 'plugin:prettier/recommended'],
settings: {
react: {
version: '18',
},
},
};
4 changes: 4 additions & 0 deletions apps/core/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"tabWidth": 4
}
3 changes: 3 additions & 0 deletions apps/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `@mysten/core`

This JavaScript library contains helper utilities meant to be used across Mysten Lab's frontend applications.
50 changes: 42 additions & 8 deletions apps/core/package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,44 @@
{
"name": "@mysten/core",
"private": true,
"devDependencies": {
"@headlessui/tailwindcss": "^0.1.2",
"@tailwindcss/aspect-ratio": "^0.4.2",
"tailwindcss": "^3.2.4",
"typescript": "^4.9.4"
}
"name": "@mysten/core",
"main": "src/index.ts",
"private": true,
"sideEffects": false,
"author": "Mysten Labs <[email protected]>",
"repository": {
"type": "git",
"url": "github.com:MystenLabs/sui.git"
},
"license": "Apache-2.0",
"scripts": {
"prettier:check": "prettier -c --ignore-unknown .",
"prettier:fix": "prettier -w --ignore-unknown .",
"prettier:fix:watch": "onchange '**' -i -f add -f change -j 5 -- prettier -w --ignore-unknown {{file}}",
"eslint:check": "eslint --max-warnings=0 .eslintrc.js .",
"eslint:fix": "pnpm run eslint:check --fix",
"lint": "pnpm run eslint:check && pnpm run prettier:check",
"lint:fix": "pnpm run eslint:fix && pnpm run prettier:fix",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@mysten/sui.js": "workspace:*",
"@sentry/react": "^7.38.0",
"@sentry/tracing": "^7.38.0",
"@tanstack/react-query": "^4.20.9",
"bignumber.js": "^9.1.1",
"react": "^18.2.0"
},
"devDependencies": {
"@headlessui/tailwindcss": "^0.1.2",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@types/react": "^18.0.26",
"eslint": "^8.31.0",
"eslint-config-prettier": "^8.6.0",
"postcss": "^8.4.19",
"prettier": "^2.8.1",
"tailwindcss": "^3.2.4",
"typescript": "^4.9.4",
"vite": "^4.0.4",
"vitest": "^0.26.3"
}
}
17 changes: 17 additions & 0 deletions apps/core/src/api/RpcClientContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { type JsonRpcProvider } from '@mysten/sui.js';
import { createContext, useContext } from 'react';

export const RpcClientContext = createContext<JsonRpcProvider | undefined>(
undefined
);

export function useRpcClient() {
const rpcClient = useContext(RpcClientContext);
if (!rpcClient) {
throw new Error('useRpcClient must be within RpcClientContext');
}
return rpcClient;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { JsonRpcClient, type RpcParams } from '@mysten/sui.js';
import * as Sentry from '@sentry/react';
import { type SpanStatusType } from '@sentry/tracing';

export class SentryRPCClient extends JsonRpcClient {
export class SentryRpcClient extends JsonRpcClient {
#url: string;
constructor(url: string) {
super(url);
Expand Down
74 changes: 74 additions & 0 deletions apps/core/src/hooks/__tests__/useFormatCoin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import BigNumber from 'bignumber.js';
import { describe, it, expect } from 'vitest';

import { formatBalance, CoinFormat } from '../useFormatCoin';

const SUI_DECIMALS = 9;

function toMist(sui: string) {
return new BigNumber(sui).shiftedBy(SUI_DECIMALS).toString();
}

describe('formatBalance', () => {
it('formats zero amounts correctly', () => {
expect(formatBalance('0', 0)).toEqual('0');
expect(formatBalance('0', SUI_DECIMALS)).toEqual('0');
});

it('formats decimal amounts correctly', () => {
expect(formatBalance('0', SUI_DECIMALS)).toEqual('0');
expect(formatBalance('0.000', SUI_DECIMALS)).toEqual('0');
});

it('formats integer amounts correctly', () => {
expect(formatBalance(toMist('1'), SUI_DECIMALS)).toEqual('1');
expect(formatBalance(toMist('1.0001'), SUI_DECIMALS)).toEqual('1');
expect(formatBalance(toMist('1.1201'), SUI_DECIMALS)).toEqual('1.12');
expect(formatBalance(toMist('1.1234'), SUI_DECIMALS)).toEqual('1.123');
expect(formatBalance(toMist('1.1239'), SUI_DECIMALS)).toEqual('1.123');

expect(formatBalance(toMist('9999.9999'), SUI_DECIMALS)).toEqual(
'9,999.999'
);
// 10k + handling:
expect(formatBalance(toMist('10000'), SUI_DECIMALS)).toEqual('10 K');
expect(formatBalance(toMist('12345'), SUI_DECIMALS)).toEqual(
'12.345 K'
);
// Millions:
expect(formatBalance(toMist('1234000'), SUI_DECIMALS)).toEqual(
'1.234 M'
);
// Billions:
expect(formatBalance(toMist('1234000000'), SUI_DECIMALS)).toEqual(
'1.234 B'
);
});

it('formats integer amounts with full CoinFormat', () => {
expect(
formatBalance(toMist('1'), SUI_DECIMALS, CoinFormat.FULL)
).toEqual('1');
expect(
formatBalance(toMist('1.123456789'), SUI_DECIMALS, CoinFormat.FULL)
).toEqual('1.123456789');
expect(
formatBalance(toMist('9999.9999'), SUI_DECIMALS, CoinFormat.FULL)
).toEqual('9,999.9999');
expect(
formatBalance(toMist('10000'), SUI_DECIMALS, CoinFormat.FULL)
).toEqual('10,000');
expect(
formatBalance(toMist('12345'), SUI_DECIMALS, CoinFormat.FULL)
).toEqual('12,345');
expect(
formatBalance(toMist('1234000'), SUI_DECIMALS, CoinFormat.FULL)
).toEqual('1,234,000');
expect(
formatBalance(toMist('1234000000'), SUI_DECIMALS, CoinFormat.FULL)
).toEqual('1,234,000,000');
});
});
93 changes: 93 additions & 0 deletions apps/core/src/hooks/useFormatCoin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { Coin } from '@mysten/sui.js';
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useMemo } from 'react';
import { useRpcClient } from '../api/RpcClientContext';
import { formatAmount } from '../utils/formatAmount';

type FormattedCoin = [
formattedBalance: string,
coinSymbol: string,
queryResult: UseQueryResult
];

export enum CoinFormat {
ROUNDED = 'ROUNDED',
FULL = 'FULL',
}

/**
* Formats a coin balance based on our standard coin display logic.
* If the balance is less than 1, it will be displayed in its full decimal form.
* For values greater than 1, it will be truncated to 3 decimal places.
*/
export function formatBalance(
balance: bigint | number | string,
decimals: number,
format: CoinFormat = CoinFormat.ROUNDED
) {
const bn = new BigNumber(balance.toString()).shiftedBy(-1 * decimals);

if (format === CoinFormat.FULL) {
return bn.toFormat();
}

return formatAmount(bn);
}

export function useCoinDecimals(coinType?: string | null) {
const rpc = useRpcClient();
const queryResult = useQuery(
['denomination', coinType],
async () => {
if (!coinType) {
throw new Error(
'Fetching coin denomination should be disabled when coin type is disabled.'
);
}

return rpc.getCoinMetadata(coinType);
},
{
// This is currently expected to fail for non-SUI tokens, so disable retries:
retry: false,
enabled: !!coinType,
// Never consider this data to be stale:
staleTime: Infinity,
// Keep this data in the cache for 24 hours.
// We allow this to be GC'd after a very long time to avoid unbounded cache growth.
cacheTime: 24 * 60 * 60 * 1000,
}
);

return [queryResult.data?.decimals || 0, queryResult] as const;
}

// TODO #1: This handles undefined values to make it easier to integrate with
// the reset of the app as it is today, but it really shouldn't in a perfect world.
export function useFormatCoin(
balance?: bigint | number | string | null,
coinType?: string | null,
format: CoinFormat = CoinFormat.ROUNDED
): FormattedCoin {
const symbol = useMemo(
() => (coinType ? Coin.getCoinSymbol(coinType) : ''),
[coinType]
);

const [decimals, queryResult] = useCoinDecimals(coinType);
const { isFetched } = queryResult;

const formatted = useMemo(() => {
if (typeof balance === 'undefined' || balance === null) return '';

if (!isFetched) return '...';

return formatBalance(balance, decimals, format);
}, [decimals, isFetched, balance, format]);

return [formatted, symbol, queryResult];
}
7 changes: 7 additions & 0 deletions apps/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

export * from './api/SentryRpcClient';
export * from './hooks/useFormatCoin';
export * from './utils/formatAmount';
export * from './api/RpcClientContext';
32 changes: 32 additions & 0 deletions apps/core/src/utils/formatAmount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import BigNumber from 'bignumber.js';

export function formatAmount(
amount?: BigNumber | bigint | number | string | null
) {
if (typeof amount === 'undefined' || amount === null) {
return '--';
}

let postfix = '';
let bn = new BigNumber(amount.toString());

if (bn.gte(1_000_000_000)) {
bn = bn.shiftedBy(-9);
postfix = ' B';
} else if (bn.gte(1_000_000)) {
bn = bn.shiftedBy(-6);
postfix = ' M';
} else if (bn.gte(10_000)) {
bn = bn.shiftedBy(-3);
postfix = ' K';
}

if (bn.gte(1)) {
bn = bn.decimalPlaces(3, BigNumber.ROUND_DOWN);
}

return bn.toFormat() + postfix;
}
Loading

0 comments on commit 46f1455

Please sign in to comment.