Skip to content

Commit

Permalink
Impl bespoke significant figures for formatTokens (#5040)
Browse files Browse the repository at this point in the history
  • Loading branch information
megrogan authored Dec 19, 2023
1 parent 0680520 commit 8306314
Show file tree
Hide file tree
Showing 19 changed files with 87 additions and 67 deletions.
2 changes: 1 addition & 1 deletion frontend/app/src/components/home/AccessGateControl.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
3 changes: 1 addition & 2 deletions frontend/app/src/components/home/AccessGateIcon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
values: {
n: client.formatTokens(
BigInt(gate.minStakeE8s),
0,
tokenDetails?.decimals ?? 8,
),
},
Expand All @@ -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) },
})}`,
);
}
Expand Down
5 changes: 2 additions & 3 deletions frontend/app/src/components/home/AccessGateParameters.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
<div class="params">
<div>
{`${$_("access.amountN", {
values: { n: client.formatTokens(gate.amount, 0, tokenDetails.decimals) },
values: { n: client.formatTokens(gate.amount, tokenDetails.decimals) },
})}`}
</div>
</div>
Expand All @@ -64,8 +64,7 @@
values: {
n: client.formatTokens(
BigInt(gate.minStakeE8s),
0,
tokenDetails?.decimals ?? 8
tokenDetails?.decimals ?? 8,
),
},
})}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 1 addition & 2 deletions frontend/app/src/components/home/BalanceWithRefresh.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,7 +56,7 @@
<div class="label">{label}</div>
{/if}
<div class="amount" class:bold>
{client.formatTokens(value, minDecimals, tokenDetails.decimals)}
{client.formatTokens(value, tokenDetails.decimals)}
</div>
{#if showRefresh}
<div class="refresh" class:refreshing on:click={refresh}>
Expand Down
6 changes: 3 additions & 3 deletions frontend/app/src/components/home/MakeProposalModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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 } });
}
Expand Down Expand Up @@ -186,7 +186,7 @@
addTokenLedgerCanisterId,
addTokenInfoUrl,
addTokenHowToBuyUrl,
addTokenTransactionUrlFormat
addTokenTransactionUrlFormat,
),
};
}
Expand Down Expand Up @@ -221,7 +221,7 @@
function wrappedSummary(summary: string) {
const groupPath = routeForChatIdentifier(
selectedMultiUserChat.kind === "group_chat" ? "group_chat" : "community",
selectedMultiUserChat.id
selectedMultiUserChat.id,
);
return `${summary}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/src/components/home/PrizeWinnerContent.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/src/components/home/TipBuilder.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/src/components/home/TipThumbnail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@
@{$userStore[userId]?.username}
</div>
<div class="amount">
{client.formatTokens(amount, 0, tokenDetails.decimals)}
{client.formatTokens(amount, tokenDetails.decimals)}
</div>
{/each}
{#if userTipsList.length > 1}
<div class="total">
{client.formatTokens(totalAmount, 0, tokenDetails.decimals)}
{client.formatTokens(totalAmount, tokenDetails.decimals)}
</div>
{/if}
</div>
Expand Down
6 changes: 3 additions & 3 deletions frontend/app/src/components/home/TokenInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@
onMount(() => {
if (amount > BigInt(0)) {
inputElement.value = client.formatTokens(amount, 0, tokenDecimals, ".");
inputElement.value = client.formatTokens(amount, tokenDecimals, ".", true);
}
});
$: {
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();
}
Expand Down Expand Up @@ -85,7 +85,7 @@
<span>
{$_("tokenTransfer.fee", {
values: {
fee: client.formatTokens(transferFees, 0, tokenDecimals),
fee: client.formatTokens(transferFees, tokenDecimals),
token: symbol,
},
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,6 @@
<td
>{client.formatTokens(
transaction.amount,
0,
tokenDetails.decimals,
)}</td>
<td class="truncate">{translateMemo(transaction)}</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
{ledger}
value={$cryptoBalance[ledger]}
label={$_("cryptoAccount.shortBalanceLabel")}
minDecimals={2}
bold
on:refreshed={onBalanceRefreshed}
on:error={onBalanceRefreshError} />
Expand Down
1 change: 0 additions & 1 deletion frontend/app/src/components/home/profile/SendCrypto.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@
{ledger}
value={remainingBalance}
label={$_("cryptoAccount.shortBalanceLabel")}
minDecimals={2}
bold
on:refreshed={onBalanceRefreshed}
on:error={onBalanceRefreshError} />
Expand Down
6 changes: 2 additions & 4 deletions frontend/app/src/components/home/profile/SwapCrypto.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
);
Expand Down Expand Up @@ -203,7 +202,6 @@
ledger={ledgerIn}
value={remainingBalance}
label={$_("cryptoAccount.shortBalanceLabel")}
minDecimals={2}
bold
on:refreshed={onBalanceRefreshed}
on:error={onBalanceRefreshError} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@
if (response.amountSwapped.value.kind === "ok") {
amountOut = client.formatTokens(
response.amountSwapped.value.value,
0,
decimalsOut,
);
updateProgress(4, true);
Expand Down
2 changes: 1 addition & 1 deletion frontend/openchat-client/src/utils/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
51 changes: 28 additions & 23 deletions frontend/openchat-client/src/utils/cryptoFormatter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
});
52 changes: 38 additions & 14 deletions frontend/openchat-client/src/utils/cryptoFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 8306314

Please sign in to comment.