forked from MystenLabs/sui
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[apps][part 1] add common utilities to @mysten/core (MystenLabs#8623)
## 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
1 parent
4593333
commit 46f1455
Showing
16 changed files
with
530 additions
and
235 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"singleQuote": true, | ||
"tabWidth": 4 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.