Skip to content

Commit

Permalink
feat: user friendly Weight scale (#544)
Browse files Browse the repository at this point in the history
  • Loading branch information
peetzweg authored Feb 16, 2024
1 parent 7ca81fd commit 2dae16a
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 45 deletions.
1 change: 1 addition & 0 deletions src/lib/formatBalance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { TypeRegistry } from '@polkadot/types';
import { beforeAll, describe, expect, it } from 'vitest';
import { formatBalance } from './formatBalance';

describe('formatBalance', () => {
let registry: TypeRegistry;

Expand Down
32 changes: 5 additions & 27 deletions src/lib/formatBalance.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,17 @@
// Copyright 2022-2024 @paritytech/contracts-ui authors & contributors
// SPDX-License-Identifier: GPL-3.0-only

import { Balance } from 'types';

type FormattingOptions = {
decimals: number;
symbol: string | undefined;
fractionDigits: number;
};
import { Balance } from '@polkadot/types/interfaces';
import type { FormattingOptions } from './formatUInt';
import { formatUInt } from './formatUInt';

const DEFAULT_OPTIONS: FormattingOptions = {
decimals: 12,
fractionDigits: 2,
symbol: undefined,
digitGrouping: true,
};

export const formatBalance = (balance: Balance, partialOptions?: Partial<FormattingOptions>) => {
const options: FormattingOptions = { ...DEFAULT_OPTIONS, ...partialOptions };

if (options.decimals < 0) throw new Error('Decimals must be positive');
if (options.fractionDigits < 0) throw new Error('Fraction digits must be positive');
if (options.decimals < options.fractionDigits)
throw new Error('Decimals must be greater than fraction digits');

const balanceString = balance.toString();
const integerDigits = balanceString.split('');
const fractionDigits = integerDigits.splice(-options.decimals);

const fractionalPart = fractionDigits.join('').padStart(options.decimals, '0');
const integerPart = integerDigits.length ? integerDigits.join('') : '0';

return (
Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(BigInt(integerPart)) +
'.' +
fractionalPart.toString().slice(0, options.fractionDigits).padEnd(options.fractionDigits, '0') +
(options.symbol ? ` ${options.symbol}` : '')
);
return formatUInt(balance, { ...DEFAULT_OPTIONS, ...partialOptions });
};
46 changes: 46 additions & 0 deletions src/lib/formatUInt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2022-2024 @paritytech/contracts-ui authors & contributors
// SPDX-License-Identifier: GPL-3.0-only

import type { Compact, UInt } from '@polkadot/types-codec';

export type FormattingOptions = {
decimals: number;
symbol: string | undefined;
fractionDigits: number;
digitGrouping: boolean;
};

export const formatUInt = (value: UInt | Compact<UInt>, options: FormattingOptions) => {
if (options.decimals < 0) throw new Error('Decimals must be positive');
if (options.fractionDigits < 0) throw new Error('Fraction digits must be positive');
if (options.decimals < options.fractionDigits)
throw new Error('Decimals must be greater than fraction digits');

const valueString = value.toString();
const integerDigits = valueString.split('');

let fractionalPart = ''.padStart(options.decimals, '0');
if (options.decimals !== 0) {
const fractionDigits = integerDigits.splice(-options.decimals);
fractionalPart = fractionDigits.join('').padStart(options.decimals, '0');
}

let integerPart = integerDigits.length ? integerDigits.join('') : '0';

if (options.digitGrouping) {
integerPart = Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(
BigInt(integerPart),
);
}

if (options.fractionDigits === 0) {
return integerPart + (options.symbol ? ` ${options.symbol}` : '');
} else {
return (
integerPart +
'.' +
fractionalPart.slice(0, options.fractionDigits) +
(options.symbol ? ` ${options.symbol}` : '')
);
}
};
69 changes: 69 additions & 0 deletions src/lib/formatWeight.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2022-2024 @paritytech/contracts-ui authors & contributors
// SPDX-License-Identifier: GPL-3.0-only

import { TypeRegistry } from '@polkadot/types';
import { beforeAll, describe, expect, it } from 'vitest';
import { formatProofSize, formatRefTime } from './formatWeight';

describe('formatProofSize', () => {
let registry: TypeRegistry;

beforeAll(() => {
registry = new TypeRegistry();
});

it('should format edge cases correctly', () => {
[
{ value: 0n, expected: '0.00 MB' },
{ value: 1_000_000n, expected: '1.00 MB' },
// u64::MAX value
{
value: 18_446_744_073_709_551_615n,
expected: '18446744073709.55 MB',
},
].forEach(({ value, expected }) => {
const proofSize = registry.createType('Compact<u64>', value);
expect(formatProofSize(proofSize, 'MB')).toBe(expected);
});
});

it('should format edge cases correctly', () => {
[
{ value: 0n, expected: '0 bytes' },
{ value: 1_000_000n, expected: '1000000 bytes' },
// u64::MAX value
{
value: 18_446_744_073_709_551_615n,
expected: '18446744073709551615 bytes',
},
].forEach(({ value, expected }) => {
const proofSize = registry.createType('Compact<u64>', value);
expect(formatProofSize(proofSize, 'bytes')).toBe(expected);
});
});
});

describe('formatRefTime', () => {
let registry: TypeRegistry;

beforeAll(() => {
registry = new TypeRegistry();
});

it('should format edge cases correctly', () => {
[
{ value: 0n, expected: '0.00 ms' },
{ value: 123n, expected: '0.00 ms' },
{ value: 123_000_000n, expected: '0.12 ms' },
{ value: 1_000_000_000n, expected: '1.00 ms' },
// u64::MAX value
{
value: 18_446_744_073_709_551_615n,
expected: '18446744073.70 ms',
},
].forEach(({ value, expected }) => {
const refTime = registry.createType('Compact<u64>', value);
expect(formatRefTime(refTime, 'ms')).toBe(expected);
});
});
});
67 changes: 67 additions & 0 deletions src/lib/formatWeight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2022-2024 @paritytech/contracts-ui authors & contributors
// SPDX-License-Identifier: GPL-3.0-only

import { WeightV2 } from '@polkadot/types/interfaces';
import { formatUInt } from './formatUInt';

/**
* Formats given Reference Time value from expected picoseconds to milliseconds.
* @param refTime
* @returns formatted refTime in milliseconds, with 2 decimal places
*/
export function formatRefTime(refTime: WeightV2['refTime'], unit: 'ms' | 'ps' = 'ps'): string {
switch (unit) {
case 'ps':
return formatUInt(refTime, {
decimals: 0,
digitGrouping: false,
fractionDigits: 0,
symbol: unit,
});
case 'ms':
return formatUInt(refTime, {
decimals: 9,
digitGrouping: false,
fractionDigits: 2,
symbol: unit,
});
default:
throw new Error('Unsupported unit');
}
}

/**
* Formats given Proof Size value from expected bytes to megabytes.
* @param refTime
* @returns formatted refTime in megabytes, with 2 decimal places
*/
export function formatProofSize(
proofSize: WeightV2['proofSize'],
unit: 'MB' | 'bytes' | 'kb' = 'bytes',
): string {
switch (unit) {
case 'bytes':
return formatUInt(proofSize, {
decimals: 0,
digitGrouping: false,
fractionDigits: 0,
symbol: unit,
});
case 'kb':
return formatUInt(proofSize, {
decimals: 3,
digitGrouping: false,
fractionDigits: 2,
symbol: unit,
});
case 'MB':
return formatUInt(proofSize, {
decimals: 6,
digitGrouping: false,
fractionDigits: 2,
symbol: unit,
});
default:
throw new Error('Unsupported unit');
}
}
17 changes: 9 additions & 8 deletions src/ui/components/contract/DryRunResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ContractExecResult, Registry } from 'types';
import { useApi } from 'ui/contexts';
import { getDecodedOutput } from 'lib/output';
import { decodeStorageDeposit } from 'lib/callOptions';
import { formatProofSize, formatRefTime } from '../../../lib/formatWeight';

interface Props {
outcome: ContractExecResult;
Expand Down Expand Up @@ -78,17 +79,17 @@ export function DryRunResult({
<div data-cy="dry-run-estimations">
<span>GasConsumed</span>
<div className="flex flex-row gap-4">
<div className="flex-1">
<div className="flex-1" title={formatRefTime(gasConsumed.refTime)}>
<OutcomeItem
displayValue={`refTime: ${gasConsumed.refTime.toString()}`}
displayValue={`refTime: ${formatRefTime(gasConsumed.refTime, 'ms')}`}
id={`gcr-${message.method}`}
key={`gcr-${message.method}`}
title=""
/>
</div>
<div className="flex-1">
<div className="flex-1" title={formatProofSize(gasConsumed.proofSize)}>
<OutcomeItem
displayValue={`proofSize: ${gasConsumed.proofSize.toString()}`}
displayValue={`proofSize: ${formatProofSize(gasConsumed.proofSize, 'MB')}`}
id={`gcp-${message.method}`}
key={`gcp-${message.method}`}
title=""
Expand All @@ -100,17 +101,17 @@ export function DryRunResult({
<>
<span>GasRequired</span>
<div className="flex">
<div className="basis-1/2 pr-2">
<div className="basis-1/2 pr-2" title={formatRefTime(gasRequired.refTime)}>
<OutcomeItem
displayValue={`refTime: ${gasRequired.refTime.toString()}`}
displayValue={`refTime: ${formatRefTime(gasRequired.refTime, 'ms')}`}
id={`grr-${message.method}`}
key={`grr-${message.method}`}
title=""
/>
</div>
<div className="basis-1/2 pl-2">
<div className="basis-1/2 pl-2" title={formatProofSize(gasRequired.proofSize)}>
<OutcomeItem
displayValue={`proofSize: ${gasRequired.proofSize.toString()}`}
displayValue={`proofSize: ${formatProofSize(gasRequired.proofSize, 'MB')}`}
id={`grp-${message.method}`}
key={`grp-${message.method}`}
title=""
Expand Down
33 changes: 23 additions & 10 deletions src/ui/components/instantiate/DryRun.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
// SPDX-License-Identifier: GPL-3.0-only

import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/outline';
import { SidePanel } from '../common/SidePanel';
import { formatProofSize, formatRefTime } from '../../../lib/formatWeight';
import { Account } from '../account/Account';
import { SidePanel } from '../common/SidePanel';
import { OutcomeItem } from '../contract/OutcomeItem';
import { useApi, useInstantiate } from 'ui/contexts';
import { hasRevertFlag } from 'lib/hasRevertFlag';
import { useApi, useInstantiate } from 'ui/contexts';

export function DryRun() {
const {
Expand All @@ -30,16 +31,22 @@ export function DryRun() {
<>
<span>GasConsumed</span>
<div className="flex">
<div className="basis-1/2 pr-2">
<div
className="basis-1/2 pr-2"
title={formatRefTime(dryRunResult.gasConsumed.refTime)}
>
<OutcomeItem
displayValue={`refTime: ${dryRunResult.gasConsumed.refTime.toString()}`}
displayValue={`refTime: ${formatRefTime(dryRunResult.gasConsumed.refTime, 'ms')}`}
key={`gcr-${constructorIndex}`}
title=""
/>
</div>
<div className="basis-1/2 pl-2">
<div
className="basis-1/2 pl-2"
title={formatProofSize(dryRunResult.gasConsumed.proofSize)}
>
<OutcomeItem
displayValue={`proofSize: ${dryRunResult.gasConsumed.proofSize.toString()}`}
displayValue={`proofSize: ${formatProofSize(dryRunResult.gasConsumed.proofSize, 'MB')}`}
key={`gcp-${constructorIndex}`}
title=""
/>
Expand All @@ -51,16 +58,22 @@ export function DryRun() {
<>
<span>GasRequired</span>
<div className="flex">
<div className="basis-1/2 pr-2">
<div
className="basis-1/2 pr-2"
title={formatRefTime(dryRunResult.gasRequired.refTime)}
>
<OutcomeItem
displayValue={`refTime: ${dryRunResult.gasRequired.refTime.toString()}`}
displayValue={`refTime: ${formatRefTime(dryRunResult.gasRequired.refTime, 'ms')}`}
key={`grr-${constructorIndex}`}
title=""
/>
</div>
<div className="basis-1/2 pl-2">
<div
className="basis-1/2 pl-2"
title={formatProofSize(dryRunResult.gasRequired.proofSize)}
>
<OutcomeItem
displayValue={`proofSize: ${dryRunResult.gasRequired.proofSize.toString()}`}
displayValue={`proofSize: ${formatProofSize(dryRunResult.gasRequired.proofSize, 'MB')}`}
key={`grp-${constructorIndex}`}
title=""
/>
Expand Down

0 comments on commit 2dae16a

Please sign in to comment.