From 83063141ec7ad6a0910899d667bb1d926e2ea915 Mon Sep 17 00:00:00 2001 From: Matt Grogan Date: Tue, 19 Dec 2023 11:49:49 +0000 Subject: [PATCH] Impl bespoke significant figures for formatTokens (#5040) --- .../components/home/AccessGateControl.svelte | 2 +- .../src/components/home/AccessGateIcon.svelte | 3 +- .../home/AccessGateParameters.svelte | 5 +- .../home/ApproveJoiningPaymentModal.svelte | 2 +- .../components/home/BalanceWithRefresh.svelte | 3 +- .../components/home/MakeProposalModal.svelte | 6 +-- .../home/PrizeContentBuilder.svelte | 2 +- .../components/home/PrizeWinnerContent.svelte | 2 +- .../app/src/components/home/TipBuilder.svelte | 4 +- .../src/components/home/TipThumbnail.svelte | 4 +- .../app/src/components/home/TokenInput.svelte | 6 +-- .../home/profile/AccountTransactions.svelte | 1 - .../home/profile/ReceiveCrypto.svelte | 1 - .../components/home/profile/SendCrypto.svelte | 1 - .../components/home/profile/SwapCrypto.svelte | 6 +-- .../home/profile/SwapProgress.svelte | 1 - frontend/openchat-client/src/utils/chat.ts | 2 +- .../src/utils/cryptoFormatter.spec.ts | 51 ++++++++++-------- .../src/utils/cryptoFormatter.ts | 52 ++++++++++++++----- 19 files changed, 87 insertions(+), 67 deletions(-) diff --git a/frontend/app/src/components/home/AccessGateControl.svelte b/frontend/app/src/components/home/AccessGateControl.svelte index c09d06f3c4..e973793b55 100644 --- a/frontend/app/src/components/home/AccessGateControl.svelte +++ b/frontend/app/src/components/home/AccessGateControl.svelte @@ -120,7 +120,7 @@ if (isPaymentGate(gate)) { const token = client.tryGetCryptocurrency(gate.ledgerCanister); if (token !== undefined) { - return client.formatTokens(gate.amount, 0, token.decimals); + return client.formatTokens(gate.amount, token.decimals); } } diff --git a/frontend/app/src/components/home/AccessGateIcon.svelte b/frontend/app/src/components/home/AccessGateIcon.svelte index 6bf643272d..10e95d94f2 100644 --- a/frontend/app/src/components/home/AccessGateIcon.svelte +++ b/frontend/app/src/components/home/AccessGateIcon.svelte @@ -43,7 +43,6 @@ values: { n: client.formatTokens( BigInt(gate.minStakeE8s), - 0, tokenDetails?.decimals ?? 8, ), }, @@ -53,7 +52,7 @@ } else if (isPaymentGate(gate)) { parts.push( `${$_("access.amountN", { - values: { n: client.formatTokens(gate.amount, 0, tokenDetails?.decimals ?? 8) }, + values: { n: client.formatTokens(gate.amount, tokenDetails?.decimals ?? 8) }, })}`, ); } diff --git a/frontend/app/src/components/home/AccessGateParameters.svelte b/frontend/app/src/components/home/AccessGateParameters.svelte index 54bd51f3e3..9f57f81a98 100644 --- a/frontend/app/src/components/home/AccessGateParameters.svelte +++ b/frontend/app/src/components/home/AccessGateParameters.svelte @@ -40,7 +40,7 @@
{`${$_("access.amountN", { - values: { n: client.formatTokens(gate.amount, 0, tokenDetails.decimals) }, + values: { n: client.formatTokens(gate.amount, tokenDetails.decimals) }, })}`}
@@ -64,8 +64,7 @@ values: { n: client.formatTokens( BigInt(gate.minStakeE8s), - 0, - tokenDetails?.decimals ?? 8 + tokenDetails?.decimals ?? 8, ), }, })}`} diff --git a/frontend/app/src/components/home/ApproveJoiningPaymentModal.svelte b/frontend/app/src/components/home/ApproveJoiningPaymentModal.svelte index 8e2305a8dd..5f44271179 100644 --- a/frontend/app/src/components/home/ApproveJoiningPaymentModal.svelte +++ b/frontend/app/src/components/home/ApproveJoiningPaymentModal.svelte @@ -28,7 +28,7 @@ $: cryptoBalance = $cryptoBalanceStore[token.ledger] ?? BigInt(0); $: insufficientFunds = cryptoBalance < gate.amount; $: approvalMessage = interpolateLevel("access.paymentApprovalMessage", group.level, true, { - amount: client.formatTokens(gate.amount, 0, token.decimals), + amount: client.formatTokens(gate.amount, token.decimals), token: token.symbol, }); $: distributionMessage = interpolateLevel( diff --git a/frontend/app/src/components/home/BalanceWithRefresh.svelte b/frontend/app/src/components/home/BalanceWithRefresh.svelte index 30a88c90b4..a519186daf 100644 --- a/frontend/app/src/components/home/BalanceWithRefresh.svelte +++ b/frontend/app/src/components/home/BalanceWithRefresh.svelte @@ -11,7 +11,6 @@ export let ledger: string; export let value: bigint; export let label: string | undefined = undefined; - export let minDecimals = 4; export let bold = false; export let toppingUp = false; export let showTopUp = false; @@ -57,7 +56,7 @@
{label}
{/if}
- {client.formatTokens(value, minDecimals, tokenDetails.decimals)} + {client.formatTokens(value, tokenDetails.decimals)}
{#if showRefresh}
diff --git a/frontend/app/src/components/home/MakeProposalModal.svelte b/frontend/app/src/components/home/MakeProposalModal.svelte index e7f0ba292f..e966db958c 100644 --- a/frontend/app/src/components/home/MakeProposalModal.svelte +++ b/frontend/app/src/components/home/MakeProposalModal.svelte @@ -116,7 +116,7 @@ } function defaultMessage(): string { - const cost = client.formatTokens(requiredFunds, 0, tokenDetails.decimals); + const cost = client.formatTokens(requiredFunds, tokenDetails.decimals); return $_("proposal.maker.message", { values: { cost, token: symbol } }); } @@ -186,7 +186,7 @@ addTokenLedgerCanisterId, addTokenInfoUrl, addTokenHowToBuyUrl, - addTokenTransactionUrlFormat + addTokenTransactionUrlFormat, ), }; } @@ -221,7 +221,7 @@ function wrappedSummary(summary: string) { const groupPath = routeForChatIdentifier( selectedMultiUserChat.kind === "group_chat" ? "group_chat" : "community", - selectedMultiUserChat.id + selectedMultiUserChat.id, ); return `${summary} diff --git a/frontend/app/src/components/home/PrizeContentBuilder.svelte b/frontend/app/src/components/home/PrizeContentBuilder.svelte index 34b5675779..c26908e9f9 100644 --- a/frontend/app/src/components/home/PrizeContentBuilder.svelte +++ b/frontend/app/src/components/home/PrizeContentBuilder.svelte @@ -69,7 +69,7 @@ if (tokenInputState === "too_low") { error = $_("minimumAmount", { values: { - amount: client.formatTokens(minAmount, 0, tokenDetails.decimals), + amount: client.formatTokens(minAmount, tokenDetails.decimals), symbol, }, }); diff --git a/frontend/app/src/components/home/PrizeWinnerContent.svelte b/frontend/app/src/components/home/PrizeWinnerContent.svelte index e9848efead..effd0bf695 100644 --- a/frontend/app/src/components/home/PrizeWinnerContent.svelte +++ b/frontend/app/src/components/home/PrizeWinnerContent.svelte @@ -16,7 +16,7 @@ $: logo = $cryptoLookup[content.transaction.ledger]?.logo ?? ""; $: tokenDetails = $cryptoLookup[content.transaction.ledger]; $: symbol = tokenDetails.symbol; - $: amount = client.formatTokens(content.transaction.amountE8s, 0, tokenDetails.decimals); + $: amount = client.formatTokens(content.transaction.amountE8s, tokenDetails.decimals); $: winner = `${username(content.transaction.recipient)}`; $: me = $user.userId === content.transaction.recipient; $: transactionLinkText = client.buildTransactionLink($_, content.transaction); diff --git a/frontend/app/src/components/home/TipBuilder.svelte b/frontend/app/src/components/home/TipBuilder.svelte index 27032d9ea4..d874413404 100644 --- a/frontend/app/src/components/home/TipBuilder.svelte +++ b/frontend/app/src/components/home/TipBuilder.svelte @@ -51,11 +51,11 @@ $: exchangeRatesLookup = client.exchangeRatesLookupStore; $: tokenDetails = $cryptoLookup[ledger]; $: cryptoBalance = $cryptoBalanceStore[ledger] ?? 0n; - $: displayDraftAmount = client.formatTokens(draftAmount, 0, tokenDetails.decimals); - $: displayFee = client.formatTokens(tokenDetails.transferFee, 0, tokenDetails.decimals); $: exchangeRate = to2SigFigs( $exchangeRatesLookup[tokenDetails.symbol.toLowerCase()]?.toUSD ?? 0, ); + $: displayDraftAmount = client.formatTokens(draftAmount, tokenDetails.decimals); + $: displayFee = client.formatTokens(tokenDetails.transferFee, tokenDetails.decimals); $: remainingBalance = draftAmount > 0n ? cryptoBalance - draftAmount - tokenDetails.transferFee : cryptoBalance; $: valid = diff --git a/frontend/app/src/components/home/TipThumbnail.svelte b/frontend/app/src/components/home/TipThumbnail.svelte index 1825123254..370662d37b 100644 --- a/frontend/app/src/components/home/TipThumbnail.svelte +++ b/frontend/app/src/components/home/TipThumbnail.svelte @@ -54,12 +54,12 @@ @{$userStore[userId]?.username}
- {client.formatTokens(amount, 0, tokenDetails.decimals)} + {client.formatTokens(amount, tokenDetails.decimals)}
{/each} {#if userTipsList.length > 1}
- {client.formatTokens(totalAmount, 0, tokenDetails.decimals)} + {client.formatTokens(totalAmount, tokenDetails.decimals)}
{/if} diff --git a/frontend/app/src/components/home/TokenInput.svelte b/frontend/app/src/components/home/TokenInput.svelte index 07daaa56d6..3de717110b 100644 --- a/frontend/app/src/components/home/TokenInput.svelte +++ b/frontend/app/src/components/home/TokenInput.svelte @@ -27,7 +27,7 @@ onMount(() => { if (amount > BigInt(0)) { - inputElement.value = client.formatTokens(amount, 0, tokenDecimals, "."); + inputElement.value = client.formatTokens(amount, tokenDecimals, ".", true); } }); @@ -35,7 +35,7 @@ if (inputElement !== undefined) { const validateResult = client.validateTokenInput(inputElement.value, tokenDecimals); if (validateResult.amount !== amount) { - inputElement.value = client.formatTokens(amount, 0, tokenDecimals, "."); + inputElement.value = client.formatTokens(amount, tokenDecimals, ".", true); } validate(); } @@ -85,7 +85,7 @@ {$_("tokenTransfer.fee", { values: { - fee: client.formatTokens(transferFees, 0, tokenDecimals), + fee: client.formatTokens(transferFees, tokenDecimals), token: symbol, }, })} diff --git a/frontend/app/src/components/home/profile/AccountTransactions.svelte b/frontend/app/src/components/home/profile/AccountTransactions.svelte index 74b26baf99..3c92ba145f 100644 --- a/frontend/app/src/components/home/profile/AccountTransactions.svelte +++ b/frontend/app/src/components/home/profile/AccountTransactions.svelte @@ -176,7 +176,6 @@ {client.formatTokens( transaction.amount, - 0, tokenDetails.decimals, )} {translateMemo(transaction)} diff --git a/frontend/app/src/components/home/profile/ReceiveCrypto.svelte b/frontend/app/src/components/home/profile/ReceiveCrypto.svelte index bc6fbf44bf..4e590053b0 100644 --- a/frontend/app/src/components/home/profile/ReceiveCrypto.svelte +++ b/frontend/app/src/components/home/profile/ReceiveCrypto.svelte @@ -41,7 +41,6 @@ {ledger} value={$cryptoBalance[ledger]} label={$_("cryptoAccount.shortBalanceLabel")} - minDecimals={2} bold on:refreshed={onBalanceRefreshed} on:error={onBalanceRefreshError} /> diff --git a/frontend/app/src/components/home/profile/SendCrypto.svelte b/frontend/app/src/components/home/profile/SendCrypto.svelte index f1a9f1daa9..a0c72e6110 100644 --- a/frontend/app/src/components/home/profile/SendCrypto.svelte +++ b/frontend/app/src/components/home/profile/SendCrypto.svelte @@ -155,7 +155,6 @@ {ledger} value={remainingBalance} label={$_("cryptoAccount.shortBalanceLabel")} - minDecimals={2} bold on:refreshed={onBalanceRefreshed} on:error={onBalanceRefreshError} /> diff --git a/frontend/app/src/components/home/profile/SwapCrypto.svelte b/frontend/app/src/components/home/profile/SwapCrypto.svelte index 663618a27a..75048ab59e 100644 --- a/frontend/app/src/components/home/profile/SwapCrypto.svelte +++ b/frontend/app/src/components/home/profile/SwapCrypto.svelte @@ -42,7 +42,7 @@ $: detailsOut = ledgerOut !== undefined ? $cryptoLookup[ledgerOut] : undefined; $: anySwapsAvailable = Object.keys(swaps).length > 0 && detailsOut !== undefined; $: swapping = state === "swap" && busy; - $: amountInText = client.formatTokens(amountIn, 0, detailsIn.decimals); + $: amountInText = client.formatTokens(amountIn, detailsIn.decimals); $: minAmountOut = bestQuote !== undefined ? (bestQuote[1] * BigInt(98)) / BigInt(100) : BigInt(0); @@ -96,14 +96,13 @@ bestQuote = response[0]; const [dexId, quote] = bestQuote!; - const amountOutText = client.formatTokens(quote, 0, detailsOut!.decimals); + const amountOutText = client.formatTokens(quote, detailsOut!.decimals); const rate = (Number(amountOutText) / Number(amountInText)).toPrecision(3); const dex = dexName(dexId); const swapText = $_("tokenSwap.swap"); const minAmountOut = BigInt(10) * detailsOut!.transferFee; const minAmountOutText = client.formatTokens( minAmountOut, - 0, detailsOut!.decimals, ); @@ -203,7 +202,6 @@ ledger={ledgerIn} value={remainingBalance} label={$_("cryptoAccount.shortBalanceLabel")} - minDecimals={2} bold on:refreshed={onBalanceRefreshed} on:error={onBalanceRefreshError} /> diff --git a/frontend/app/src/components/home/profile/SwapProgress.svelte b/frontend/app/src/components/home/profile/SwapProgress.svelte index 0766ccf9e6..fd745a02f4 100644 --- a/frontend/app/src/components/home/profile/SwapProgress.svelte +++ b/frontend/app/src/components/home/profile/SwapProgress.svelte @@ -59,7 +59,6 @@ if (response.amountSwapped.value.kind === "ok") { amountOut = client.formatTokens( response.amountSwapped.value.value, - 0, decimalsOut, ); updateProgress(4, true); diff --git a/frontend/openchat-client/src/utils/chat.ts b/frontend/openchat-client/src/utils/chat.ts index 9b85d86feb..974fd83f04 100644 --- a/frontend/openchat-client/src/utils/chat.ts +++ b/frontend/openchat-client/src/utils/chat.ts @@ -1689,7 +1689,7 @@ export function buildCryptoTransferText( const tokenDetails = cryptoLookup[content.transfer.ledger]; const values = { - amount: formatTokens(content.transfer.amountE8s, 0, tokenDetails.decimals), + amount: formatTokens(content.transfer.amountE8s, tokenDetails.decimals), receiver: username(content.transfer.recipient), sender: username(senderId), token: tokenDetails.symbol, diff --git a/frontend/openchat-client/src/utils/cryptoFormatter.spec.ts b/frontend/openchat-client/src/utils/cryptoFormatter.spec.ts index a15b01ec2a..3047e24a2e 100644 --- a/frontend/openchat-client/src/utils/cryptoFormatter.spec.ts +++ b/frontend/openchat-client/src/utils/cryptoFormatter.spec.ts @@ -74,29 +74,34 @@ describe("crypto formatter", () => { }); describe("format", () => { - test("1 ICP with min decimals = 4", () => { - const formatted = formatTokens(BigInt(100_000_000), 4, 8); - expect(formatted).toEqual("1.0000"); - }); - - test("1 ICP with min decimals = 0", () => { - const formatted = formatTokens(BigInt(100_000_000), 0, 8); - expect(formatted).toEqual("1"); - }); - - test("1.23456789 ICP with min decimals = 0", () => { - const formatted = formatTokens(BigInt(123_456_789), 0, 8); - expect(formatted).toEqual("1.23456789"); - }); - - test("123456789.12345678 ICP with min decimals = 2", () => { - const formatted = formatTokens(BigInt(12_345_678_912_345_678), 0, 8); - expect(formatted).toEqual("123456789.12345678"); - }); - - test("123456789.12345678 ICP with comma separator", () => { - const formatted = formatTokens(BigInt(12_345_678_912_345_678), 0, 8, ","); - expect(formatted).toEqual("123456789,12345678"); + test("123.456 ICP with comma separator", () => { + const formatted = formatTokens(BigInt(12_345_600_000), 8, ","); + expect(formatted).toEqual("123,456"); + }); + + test("123456789.12345678 ICP formatted as expected", () => { + const formatted = formatTokens(BigInt(12_345_678_912_345_678), 8, "."); + expect(formatted).toEqual("123456789"); + }); + + test("123.456789 ICP formatted as expected", () => { + const formatted = formatTokens(BigInt(12_345_678_900), 8, "."); + expect(formatted).toEqual("123.456"); + }); + + test("0.000123456789000000 ckETH formatted as expected", () => { + const formatted = formatTokens(BigInt(123_456_789_000_000), 18, "."); + expect(formatted).toEqual("0.000123456"); + }); + + test("12345 ICP formatted as expected", () => { + const formatted = formatTokens(BigInt(1_234_500_000_000), 8, "."); + expect(formatted).toEqual("12345.0"); + }); + + test("1234 ICP formatted as expected", () => { + const formatted = formatTokens(BigInt(123_400_000_000), 8, "."); + expect(formatted).toEqual("1234.00"); }); }); }); diff --git a/frontend/openchat-client/src/utils/cryptoFormatter.ts b/frontend/openchat-client/src/utils/cryptoFormatter.ts index 85aa71ab96..900770b6c2 100644 --- a/frontend/openchat-client/src/utils/cryptoFormatter.ts +++ b/frontend/openchat-client/src/utils/cryptoFormatter.ts @@ -58,40 +58,64 @@ export function parseBigInt(value: string): bigint | undefined { export function formatTokens( amount: bigint, - minDecimals: number, powTenPerWhole: number, - decimalSeparatorOverride?: string + decimalSeparatorOverride?: string, + fullPrecision = false, ): string { if (amount < 0) { amount = BigInt(0); } - return format(amount, minDecimals, powTenPerWhole, decimalSeparatorOverride); + return format(amount, powTenPerWhole, decimalSeparatorOverride, fullPrecision); } function format( units: bigint, - minDecimals: number, powTenPerWhole: number, - decimalSeparatorOverride?: string + decimalSeparatorOverride: string | undefined, + fullPrecision: boolean ): string { + // This is a bespoke notion of significant figures! + const maxSignificantFigures = 6; + + // 1. Always show the full integral part of the number + // 2. If the integral part >= 6 digits then remove the fractional part + // 3. Otherwise if there is an integral part the max total number of digits (integral + fractional) = 6 + // 4. If there is no integral part then the max number of significant figures = 6 + // 5. Pad the fractional part with up to 2 '0's trying to keep the total number of digits <= 6 + const unitsPerWhole = BigInt(Math.pow(10, powTenPerWhole)); const decimalSeparator = decimalSeparatorOverride ?? getDecimalSeparator(get(locale)); const integral = units / unitsPerWhole; const integralString = integral.toString(); - const fractional = units % unitsPerWhole; + let fractionalString = fractional.toString().padStart(powTenPerWhole, "0"); - let countToTrim = 0; - while ( - fractionalString.length - countToTrim > minDecimals && - fractionalString[fractionalString.length - 1 - countToTrim] === "0" - ) { - countToTrim++; + if (!fullPrecision) { + if (integral > 0) { + const maxFractionalDecimalPlaces = Math.max(maxSignificantFigures - integralString.length, 0); + if (fractionalString.length > maxFractionalDecimalPlaces) { + fractionalString = fractionalString.substring(0, maxFractionalDecimalPlaces); + } + + } else { + const significantFigures = fractionalString.replace(/^0+/, '').length; + if (significantFigures > maxSignificantFigures) { + const indexToRemove = maxSignificantFigures + fractionalString.length - significantFigures + fractionalString = fractionalString.substring(0, indexToRemove); + } + } } - if (countToTrim > 0) { - fractionalString = fractionalString.substr(0, fractionalString.length - countToTrim); + // Remove trailing zeros leaving 0, 1, or 2 depending on how many integral digits we have already + const minDecimalPlaces = Math.max(0, Math.min(2, maxSignificantFigures - integralString.length)); + + for (let i = fractionalString.length - 1; i >= minDecimalPlaces; i--) { + if (fractionalString[i] === '0') { + fractionalString = fractionalString.slice(0, -1); + } else { + break; + } } return fractionalString.length > 0