diff --git a/.changeset/curly-squids-learn.md b/.changeset/curly-squids-learn.md new file mode 100644 index 0000000000..7624ee2e07 --- /dev/null +++ b/.changeset/curly-squids-learn.md @@ -0,0 +1,5 @@ +--- +'@penumbra-zone/types': minor +--- + +Adding shortify + round utilities diff --git a/.changeset/loud-beers-yawn.md b/.changeset/loud-beers-yawn.md new file mode 100644 index 0000000000..1ef8c23008 --- /dev/null +++ b/.changeset/loud-beers-yawn.md @@ -0,0 +1,5 @@ +--- +'@penumbra-zone/ui': minor +--- + +Updating ValueViewComponent to support shortening symbols diff --git a/apps/minifront/package.json b/apps/minifront/package.json index 67d60ceb18..94ae56fb4e 100644 --- a/apps/minifront/package.json +++ b/apps/minifront/package.json @@ -62,7 +62,7 @@ "devDependencies": { "@chain-registry/types": "^0.45.38", "@eslint/compat": "^1.1.0", - "@types/lodash": "^4.17.4", + "@types/lodash": "^4.17.7", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", "@types/react-helmet": "^6.1.11", diff --git a/apps/minifront/src/components/swap/swap-form/simulate-swap-result/traces/trace/price.tsx b/apps/minifront/src/components/swap/swap-form/simulate-swap-result/traces/trace/price.tsx index 74dd6bc4ed..ad7c5face5 100644 --- a/apps/minifront/src/components/swap/swap-form/simulate-swap-result/traces/trace/price.tsx +++ b/apps/minifront/src/components/swap/swap-form/simulate-swap-result/traces/trace/price.tsx @@ -2,8 +2,9 @@ import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_p import { SwapExecution_Trace } from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; import { bech32mAssetId } from '@penumbra-zone/bech32m/passet'; import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata'; -import { formatAmount, removeTrailingZeros } from '@penumbra-zone/types/amount'; +import { formatAmount } from '@penumbra-zone/types/amount'; import { BigNumber } from 'bignumber.js'; +import { removeTrailingZeros } from '@penumbra-zone/types/shortify'; export const Price = ({ trace, diff --git a/packages/types/package.json b/packages/types/package.json index 311c1fb987..25ede5534f 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -37,13 +37,15 @@ "dependencies": { "bignumber.js": "^9.1.2", "idb": "^8.0.0", + "lodash": "^4.17.21", "zod": "^3.23.8" }, "devDependencies": { "@bufbuild/protobuf": "^1.10.0", "@penumbra-zone/bech32m": "workspace:*", "@penumbra-zone/getters": "workspace:*", - "@penumbra-zone/protobuf": "workspace:*" + "@penumbra-zone/protobuf": "workspace:*", + "@types/lodash": "^4.17.7" }, "peerDependencies": { "@bufbuild/protobuf": "^1.10.0", diff --git a/packages/types/src/amount.ts b/packages/types/src/amount.ts index a1ce76c955..b07087e4fa 100644 --- a/packages/types/src/amount.ts +++ b/packages/types/src/amount.ts @@ -3,6 +3,7 @@ import { fromBaseUnit, joinLoHi, splitLoHi, toBaseUnit } from './lo-hi.js'; import { BigNumber } from 'bignumber.js'; import { ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; import { getAmount, getDisplayDenomExponentFromValueView } from '@penumbra-zone/getters/value-view'; +import { removeTrailingZeros } from './shortify.js'; export const joinLoHiAmount = (amount: Amount): bigint => { return joinLoHi(amount.lo, amount.hi); @@ -81,10 +82,6 @@ export const formatNumber = (number: number, options: FormatOptions): string => : parseFloat(number.toFixed(precision)).toString(); }; -export const removeTrailingZeros = (strNum: string): string => { - return strNum.replace(/(\.\d*?[1-9])0+$|\.0*$/, '$1'); -}; - export const formatAmount = ({ amount, exponent = 0, diff --git a/packages/types/src/round.test.ts b/packages/types/src/round.test.ts new file mode 100644 index 0000000000..60b7f71ddc --- /dev/null +++ b/packages/types/src/round.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, it } from 'vitest'; +import { round, RoundOptions } from './round.js'; + +describe('round function', () => { + const testCases: { + description: string; + options: RoundOptions; + expected: string; + }[] = [ + // Default rounding mode ('round') + { + description: 'should round up using default rounding (round)', + options: { value: 1.2345, decimals: 3 }, + expected: '1.235', + }, + { + description: 'should round down using default rounding (round)', + options: { value: 1.2341, decimals: 3 }, + expected: '1.234', + }, + { + description: 'should round a negative number using default rounding (round)', + options: { value: -1.2345, decimals: 2 }, + expected: '-1.23', + }, + { + description: 'should round zero', + options: { value: 0, decimals: 2 }, + expected: '0.00', + }, + { + description: 'should round an integer with decimals', + options: { value: 5, decimals: 2 }, + expected: '5.00', + }, + // Rounding mode: 'ceil' + { + description: 'should ceil to 2 decimals', + options: { value: 1.2345, decimals: 2, roundingMode: 'ceil' }, + expected: '1.24', + }, + { + description: 'should ceil a negative number', + options: { value: -1.2345, decimals: 2, roundingMode: 'ceil' }, + expected: '-1.23', + }, + { + description: 'should ceil with zero decimals', + options: { value: 1.5, decimals: 0, roundingMode: 'ceil' }, + expected: '2', + }, + // Rounding mode: 'floor' + { + description: 'should floor to 2 decimals', + options: { value: 1.2399, decimals: 2, roundingMode: 'floor' }, + expected: '1.23', + }, + { + description: 'should floor a negative number', + options: { value: -1.2345, decimals: 2, roundingMode: 'floor' }, + expected: '-1.24', + }, + { + description: 'should floor with zero decimals', + options: { value: 1.9, decimals: 0, roundingMode: 'floor' }, + expected: '1', + }, + // Edge Cases + { + description: 'should handle very large numbers', + options: { value: 1.23456789e10, decimals: 2, roundingMode: 'round' }, + expected: '12345678900.00', + }, + { + description: 'should handle very small numbers', + options: { value: 0.000123456, decimals: 8, roundingMode: 'round' }, + expected: '0.00012346', + }, + { + description: 'should handle Infinity', + options: { value: Infinity, decimals: 2, roundingMode: 'round' }, + expected: 'Infinity', + }, + { + description: 'should handle -Infinity', + options: { value: -Infinity, decimals: 2, roundingMode: 'floor' }, + expected: '-Infinity', + }, + { + description: 'should handle NaN', + options: { value: NaN, decimals: 2, roundingMode: 'ceil' }, + expected: 'NaN', + }, + { + description: 'should handle decimals greater than available decimal places', + options: { value: 1.2, decimals: 5, roundingMode: 'floor' }, + expected: '1.20000', + }, + // Rounding to integer + { + description: 'should round to integer using round mode', + options: { value: 2.5, decimals: 0, roundingMode: 'round' }, + expected: '3', + }, + { + description: 'should ceil to integer', + options: { value: 2.1, decimals: 0, roundingMode: 'ceil' }, + expected: '3', + }, + { + description: 'should floor to integer', + options: { value: 2.9, decimals: 0, roundingMode: 'floor' }, + expected: '2', + }, + ]; + + testCases.forEach(({ description, options, expected }) => { + // eslint-disable-next-line vitest/valid-title + it(description, () => { + const result = round(options); + expect(result).toBe(expected); + }); + }); +}); diff --git a/packages/types/src/round.ts b/packages/types/src/round.ts new file mode 100644 index 0000000000..c4f9e0eeee --- /dev/null +++ b/packages/types/src/round.ts @@ -0,0 +1,39 @@ +import { ceil as lodashCeil, floor as lodashFloor, round as lodashRound } from 'lodash'; + +export type RoundingMode = 'round' | 'ceil' | 'floor'; + +export interface RoundOptions { + value: number; + decimals: number; + roundingMode?: RoundingMode; +} + +const roundingStrategies = { + ceil: lodashCeil, + floor: lodashFloor, + round: lodashRound, +} as const; + +/** + * Rounds a number based on the specified options. + * + * @param options - An object containing the properties: + * - value: The number to round. + * - decimals: The number of decimal places to round to. + * - roundingMode: The mode of rounding ('round', 'ceil', 'floor'). Defaults to 'round'. + * + * @returns A string representation of the rounded number. + * + * @example + * + * ```typescript + * round({ value: 1.2345, decimals: 2, roundingMode: 'ceil' }); // "1.24" + * round({ value: 1.2345, decimals: 2, roundingMode: 'floor' }); // "1.23" + * round({ value: 1.2345, decimals: 2 }); // "1.23" (default rounding) + * ``` + */ +export function round({ value, decimals, roundingMode = 'round' }: RoundOptions): string { + const roundingFn = roundingStrategies[roundingMode]; + const roundedNumber = roundingFn(value, decimals); + return roundedNumber.toFixed(decimals); +} diff --git a/packages/types/src/shortify.test.ts b/packages/types/src/shortify.test.ts new file mode 100644 index 0000000000..84269afffd --- /dev/null +++ b/packages/types/src/shortify.test.ts @@ -0,0 +1,211 @@ +import { describe, expect, it } from 'vitest'; +import { removeTrailingZeros, shortify } from './shortify.js'; + +describe('shortify Function', () => { + describe('No suffix needed (|value| < 1,000)', () => { + it('should return the integer part for positive numbers', () => { + expect(shortify(0)).toBe('0'); + expect(shortify(999)).toBe('999'); + expect(shortify(999.9999)).toBe('999'); + expect(shortify(99.9999)).toBe('99.9'); + expect(shortify(9.9999)).toBe('9.99'); + expect(shortify(123.456)).toBe('123'); // floor rounding + }); + + it('should return the integer part for negative numbers', () => { + expect(shortify(-999)).toBe('-999'); + expect(shortify(-123.456)).toBe('-123'); // ceil rounding + }); + }); + + describe('Thousands suffix (K)', () => { + it('should format positive numbers correctly', () => { + expect(shortify(1000)).toBe('1K'); + expect(shortify(1500)).toBe('1.5K'); + expect(shortify(999999)).toBe('999K'); + expect(shortify(1234)).toBe('1.23K'); + expect(shortify(12345)).toBe('12.3K'); + expect(shortify(123456)).toBe('123K'); + }); + + it('should format negative numbers correctly', () => { + expect(shortify(-1000)).toBe('-1K'); + expect(shortify(-1500)).toBe('-1.5K'); + expect(shortify(-999999)).toBe('-999K'); + expect(shortify(-1234)).toBe('-1.23K'); + expect(shortify(-12345)).toBe('-12.3K'); + expect(shortify(-123456)).toBe('-123K'); + }); + }); + + describe('Millions suffix (M)', () => { + it('should format positive numbers correctly', () => { + expect(shortify(1_000_000)).toBe('1M'); + expect(shortify(1_500_000)).toBe('1.5M'); + expect(shortify(999_999_999)).toBe('999M'); + expect(shortify(1234567)).toBe('1.23M'); + expect(shortify(12345678)).toBe('12.3M'); + expect(shortify(123456789)).toBe('123M'); + }); + + it('should format negative numbers correctly', () => { + expect(shortify(-1_000_000)).toBe('-1M'); + expect(shortify(-1_500_000)).toBe('-1.5M'); + expect(shortify(-999_999_999)).toBe('-999M'); + expect(shortify(-1234567)).toBe('-1.23M'); + expect(shortify(-12345678)).toBe('-12.3M'); + expect(shortify(-123456789)).toBe('-123M'); + }); + }); + + describe('Billions suffix (B)', () => { + it('should format positive numbers correctly', () => { + expect(shortify(1_000_000_000)).toBe('1B'); + expect(shortify(1_500_000_000)).toBe('1.5B'); + expect(shortify(999_999_999_999)).toBe('999B'); + expect(shortify(1234567890)).toBe('1.23B'); + expect(shortify(12345678901)).toBe('12.3B'); + expect(shortify(123456789012)).toBe('123B'); + }); + + it('should format negative numbers correctly', () => { + expect(shortify(-1_000_000_000)).toBe('-1B'); + expect(shortify(-1_500_000_000)).toBe('-1.5B'); + expect(shortify(-999_999_999_999)).toBe('-999B'); + expect(shortify(-1234567890)).toBe('-1.23B'); + expect(shortify(-12345678901)).toBe('-12.3B'); + expect(shortify(-123456789012)).toBe('-123B'); + }); + }); + + describe('Trillions suffix (T)', () => { + it('should format positive numbers correctly', () => { + expect(shortify(1_000_000_000_000)).toBe('1T'); + expect(shortify(1_500_000_000_000)).toBe('1.5T'); + expect(shortify(1234567890123)).toBe('1.23T'); + expect(shortify(12345678901234)).toBe('12.3T'); + expect(shortify(123456789012345)).toBe('123T'); + expect(shortify(1e15)).toBe('1000T'); // Edge case: beyond trillion + }); + + it('should format negative numbers correctly', () => { + expect(shortify(-1_000_000_000_000)).toBe('-1T'); + expect(shortify(-1_500_000_000_000)).toBe('-1.5T'); + expect(shortify(-1234567890123)).toBe('-1.23T'); + expect(shortify(-12345678901234)).toBe('-12.3T'); + expect(shortify(-123456789012345)).toBe('-123T'); + expect(shortify(-1e15)).toBe('-1000T'); // Edge case: beyond trillion + }); + }); + + describe('Trailing zeros removal', () => { + it('should remove trailing zeros after decimal', () => { + expect(shortify(1000)).toBe('1K'); + expect(shortify(1200)).toBe('1.2K'); + expect(shortify(1230)).toBe('1.23K'); + expect(shortify(1000000)).toBe('1M'); + expect(shortify(1200000)).toBe('1.2M'); + expect(shortify(1230000)).toBe('1.23M'); + expect(shortify(1000000000)).toBe('1B'); + expect(shortify(1200000000)).toBe('1.2B'); + expect(shortify(1230000000)).toBe('1.23B'); + expect(shortify(1000000000000)).toBe('1T'); + expect(shortify(1200000000000)).toBe('1.2T'); + expect(shortify(1230000000000)).toBe('1.23T'); + + expect(shortify(-1000)).toBe('-1K'); + expect(shortify(-1200)).toBe('-1.2K'); + expect(shortify(-1230)).toBe('-1.23K'); + expect(shortify(-1000000)).toBe('-1M'); + expect(shortify(-1200000)).toBe('-1.2M'); + expect(shortify(-1230000)).toBe('-1.23M'); + expect(shortify(-1000000000)).toBe('-1B'); + expect(shortify(-1200000000)).toBe('-1.2B'); + expect(shortify(-1230000000)).toBe('-1.23B'); + expect(shortify(-1000000000000)).toBe('-1T'); + expect(shortify(-1200000000000)).toBe('-1.2T'); + expect(shortify(-1230000000000)).toBe('-1.23T'); + }); + }); + + describe('Very large numbers beyond trillion', () => { + it('should handle numbers larger than 1 trillion', () => { + expect(shortify(1_500_000_000_000)).toBe('1.5T'); + expect(shortify(12_345_678_901_234)).toBe('12.3T'); + expect(shortify(123_456_789_012_345)).toBe('123T'); + expect(shortify(999_999_999_999_999)).toBe('999T'); + expect(shortify(1e15)).toBe('1000T'); + expect(shortify(-1_500_000_000_000)).toBe('-1.5T'); + expect(shortify(-12_345_678_901_234)).toBe('-12.3T'); + expect(shortify(-123_456_789_012_345)).toBe('-123T'); + expect(shortify(-999_999_999_999_999)).toBe('-999T'); + expect(shortify(-1e15)).toBe('-1000T'); + }); + }); + + describe('Rounding behavior', () => { + it('should floor positive numbers and ceil negative numbers', () => { + // Positive numbers + expect(shortify(1234)).toBe('1.23K'); // floor to 2 decimals + expect(shortify(12345)).toBe('12.3K'); // floor to 1 decimal + expect(shortify(123456)).toBe('123K'); // floor to 0 decimals + + // Negative numbers + expect(shortify(-1234)).toBe('-1.23K'); // ceil to 2 decimals + expect(shortify(-12345)).toBe('-12.3K'); // ceil to 1 decimal + expect(shortify(-123456)).toBe('-123K'); // ceil to 0 decimals + }); + }); +}); + +describe('removeTrailingZeros', () => { + it('should remove trailing zeros after decimal point', () => { + expect(removeTrailingZeros('123.45000')).toBe('123.45'); + expect(removeTrailingZeros('0.5000')).toBe('0.5'); + expect(removeTrailingZeros('3.14159000')).toBe('3.14159'); + }); + + it('should remove decimal point if all decimal digits are zeros', () => { + expect(removeTrailingZeros('123.000')).toBe('123'); + expect(removeTrailingZeros('0.0000')).toBe('0'); + expect(removeTrailingZeros('456.0')).toBe('456'); + }); + + it('should handle numbers without decimal points', () => { + expect(removeTrailingZeros('123')).toBe('123'); + expect(removeTrailingZeros('0')).toBe('0'); + expect(removeTrailingZeros('456789')).toBe('456789'); + }); + + it('should handle numbers with no trailing zeros', () => { + expect(removeTrailingZeros('123.45')).toBe('123.45'); + expect(removeTrailingZeros('0.5')).toBe('0.5'); + expect(removeTrailingZeros('789.123456')).toBe('789.123456'); + }); + + it('should handle negative numbers', () => { + expect(removeTrailingZeros('-123.45000')).toBe('-123.45'); + expect(removeTrailingZeros('-0.000')).toBe('-0'); + expect(removeTrailingZeros('-456')).toBe('-456'); + }); + + it('should handle empty string gracefully', () => { + expect(removeTrailingZeros('')).toBe(''); + }); + + it('should handle very large numbers', () => { + expect(removeTrailingZeros('12345678901234567890.000000')).toBe('12345678901234567890'); + expect(removeTrailingZeros('9876543210.09876543210000')).toBe('9876543210.0987654321'); + }); + + it('should handle numbers with leading zeros', () => { + expect(removeTrailingZeros('000123.45000')).toBe('000123.45'); + expect(removeTrailingZeros('000.000')).toBe('000'); + expect(removeTrailingZeros('000456')).toBe('000456'); + }); + + it('should handle numbers with no digits after decimal point', () => { + expect(removeTrailingZeros('123.')).toBe('123.'); + expect(removeTrailingZeros('-456.')).toBe('-456.'); + }); +}); diff --git a/packages/types/src/shortify.ts b/packages/types/src/shortify.ts new file mode 100644 index 0000000000..6b1aef092b --- /dev/null +++ b/packages/types/src/shortify.ts @@ -0,0 +1,71 @@ +import { round } from './round.js'; + +/** + * Removes all trailing zeros after the decimal point. + * If the string ends with ".0", it removes the decimal point as well. + * + * Examples: + * - "1.0000" becomes "1" + * - "1.2000" becomes "1.2" + * - "1.2300" becomes "1.23" + * - "1.2340" becomes "1.234" + * - "1.2345" remains "1.2345" + */ +export const removeTrailingZeros = (str: string): string => { + return str.replace(/(\.\d*?[1-9])0+$/, '$1').replace(/\.0+$/, ''); +}; + +/** + * Makes large numbers shorter. Examples: + * - 999 -> 999 + * - 1_000 -> 1K + * - 1_000_000 -> 1M + * - 1_000_000_000 -> 1B + * - 1_000_000_000_000 -> 1T + */ +export const shortify = (value: number): string => { + // Rounding toward zero + const roundingMode = value >= 0 ? 'floor' : 'ceil'; + + let shortValue: number; + let suffix = ''; + let decimals = 0; + + if (value < 1_000 && value > -1_000) { + shortValue = value; + suffix = ''; + } else if (value < 1_000_000 && value > -1_000_000) { + shortValue = value / 1_000; + suffix = 'K'; + } else if (value < 1_000_000_000 && value > -1_000_000_000) { + shortValue = value / 1_000_000; + suffix = 'M'; + } else if (value < 1_000_000_000_000 && value > -1_000_000_000_000) { + shortValue = value / 1_000_000_000; + suffix = 'B'; + } else { + shortValue = value / 1_000_000_000_000; + suffix = 'T'; + } + + // Determine the number of integer digits in the shortValue + const absShortValue = Math.abs(shortValue); + const integerDigits = Math.floor(absShortValue).toString().length; + + // Set decimals to set significant digits + if (integerDigits >= 3) { + decimals = 0; + } else { + decimals = 3 - integerDigits; + } + + // Round the shortValue based on the decimals + const roundedShortValueStr = round({ + value: shortValue, + decimals: decimals, + roundingMode, + }); + + // Remove trailing zeros and append the suffix + return `${removeTrailingZeros(roundedShortValueStr)}${suffix}`; +}; diff --git a/packages/ui/src/ValueView/index.tsx b/packages/ui/src/ValueView/index.tsx index d2fe2e9ab3..d4a4f5eba8 100644 --- a/packages/ui/src/ValueView/index.tsx +++ b/packages/ui/src/ValueView/index.tsx @@ -8,6 +8,8 @@ import { Text } from '../Text'; import { AssetIcon } from '../AssetIcon'; import { Density, useDensity } from '../utils/density'; import cn from 'clsx'; +import { shortify } from '@penumbra-zone/types/shortify'; +import { detailTechnical, technical } from '../utils/typography.ts'; type Context = 'default' | 'table'; @@ -33,6 +35,14 @@ export interface ValueViewComponentProps { * numeraire. */ priority?: PillProps['priority']; + /** + * If true, the asset symbol will be hidden. + */ + hideSymbol?: boolean; + /** + * If true, the displayed amount will be shortened. + */ + abbreviate?: boolean; } /** @@ -46,6 +56,8 @@ export const ValueViewComponent = ( valueView, context, priority = 'primary', + hideSymbol = false, + abbreviate = false, }: ValueViewComponentProps) => { const density = useDensity(); @@ -53,7 +65,14 @@ export const ValueViewComponent = ( return null; } - const formattedAmount = getFormattedAmtFromValueView(valueView, true); + let formattedAmount: string; + if (abbreviate) { + const amount = getFormattedAmtFromValueView(valueView, false); + formattedAmount = shortify(Number(amount)); + } else { + formattedAmount = getFormattedAmtFromValueView(valueView, true); + } + const metadata = getMetadata.optional(valueView); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- possibly empty string const symbol = metadata?.symbol || 'Unknown'; @@ -68,28 +87,58 @@ export const ValueViewComponent = ( )} + else={children => ( + + {children} + + )} > - +
- +
-
- {formattedAmount} -
-
- {symbol} +
+ {formattedAmount}
+ {!hideSymbol && ( +
+ {symbol} +
+ )}
); }; + +const getGap = (density: Density) => { + if (density === 'sparse') { + return 'gap-2'; + } + if (density === 'medium') { + return 'gap-1.5'; + } + return 'gap-1'; +}; + +const getIconSize = (density: Density) => { + if (density === 'sparse') { + return 'lg'; + } + if (density === 'medium') { + return 'md'; + } + return 'sm'; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99fb159076..775e93d3fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,7 +40,7 @@ importers: version: 8.4.2(@storybook/test@8.4.2(storybook@8.4.2(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@5.0.10)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.18.1)(storybook@8.4.2(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@5.0.10))(typescript@5.5.3)(vite@5.3.3(@types/node@22.8.6)(terser@5.36.0)) '@testing-library/jest-dom': specifier: ^6.4.5 - version: 6.4.6(vitest@1.6.0(@types/node@22.8.6)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.36.0)) + version: 6.4.6(vitest@1.6.0) '@testing-library/react': specifier: ^15.0.7 version: 15.0.7(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -103,7 +103,7 @@ importers: version: 2.0.12(eslint@9.6.0) eslint-plugin-vitest: specifier: ^0.5.4 - version: 0.5.4(eslint@9.6.0)(typescript@5.5.3)(vitest@1.6.0(@types/node@22.8.6)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.36.0)) + version: 0.5.4(eslint@9.6.0)(typescript@5.5.3)(vitest@1.6.0) jsdom: specifier: ^24.0.0 version: 24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -292,8 +292,8 @@ importers: specifier: ^1.1.0 version: 1.1.0 '@types/lodash': - specifier: ^4.17.4 - version: 4.17.6 + specifier: ^4.17.7 + version: 4.17.13 '@types/react': specifier: ^18.3.2 version: 18.3.3 @@ -598,6 +598,9 @@ importers: idb: specifier: ^8.0.0 version: 8.0.0 + lodash: + specifier: ^4.17.21 + version: 4.17.21 zod: specifier: ^3.23.8 version: 3.23.8 @@ -614,6 +617,9 @@ importers: '@penumbra-zone/protobuf': specifier: workspace:* version: link:../protobuf + '@types/lodash': + specifier: ^4.17.7 + version: 4.17.13 packages/ui: dependencies: @@ -4423,8 +4429,8 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/lodash@4.17.6': - resolution: {integrity: sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==} + '@types/lodash@4.17.13': + resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} '@types/long@4.0.2': resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} @@ -15148,7 +15154,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.6(vitest@1.6.0(@types/node@22.8.6)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.36.0))': + '@testing-library/jest-dom@6.4.6(vitest@1.6.0)': dependencies: '@adobe/css-tools': 4.4.0 '@babel/runtime': 7.24.7 @@ -15366,7 +15372,7 @@ snapshots: '@types/json5@0.0.29': {} - '@types/lodash@4.17.6': {} + '@types/lodash@4.17.13': {} '@types/long@4.0.2': {} @@ -15610,7 +15616,7 @@ snapshots: '@visx/responsive@3.10.2(react@18.3.1)': dependencies: - '@types/lodash': 4.17.6 + '@types/lodash': 4.17.13 '@types/react': 18.3.12 lodash: 4.17.21 prop-types: 15.8.1 @@ -15624,7 +15630,7 @@ snapshots: dependencies: '@types/d3-path': 1.0.11 '@types/d3-shape': 1.3.12 - '@types/lodash': 4.17.6 + '@types/lodash': 4.17.13 '@types/react': 18.3.12 '@visx/curve': 3.3.0 '@visx/group': 3.3.0(react@18.3.1) @@ -15649,7 +15655,7 @@ snapshots: '@visx/text@3.3.0(react@18.3.1)': dependencies: - '@types/lodash': 4.17.6 + '@types/lodash': 4.17.13 '@types/react': 18.3.12 classnames: 2.5.1 lodash: 4.17.21 @@ -17492,7 +17498,7 @@ snapshots: debug: 4.3.5 enhanced-resolve: 5.17.0 eslint: 9.6.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.16.0(eslint@9.6.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.16.0(eslint@9.6.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@9.6.0))(eslint@9.6.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.16.0(eslint@9.6.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@9.6.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.16.0(eslint@9.6.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@9.6.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 @@ -17504,7 +17510,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.16.0(eslint@9.6.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.16.0(eslint@9.6.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@9.6.0))(eslint@9.6.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.16.0(eslint@9.6.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@9.6.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -17542,7 +17548,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.6.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.16.0(eslint@9.6.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.16.0(eslint@9.6.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@9.6.0))(eslint@9.6.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.16.0(eslint@9.6.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@9.6.0) hasown: 2.0.2 is-core-module: 2.14.0 is-glob: 4.0.3 @@ -17616,7 +17622,7 @@ snapshots: dotenv: 16.0.3 eslint: 9.6.0 - eslint-plugin-vitest@0.5.4(eslint@9.6.0)(typescript@5.5.3)(vitest@1.6.0(@types/node@22.8.6)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.36.0)): + eslint-plugin-vitest@0.5.4(eslint@9.6.0)(typescript@5.5.3)(vitest@1.6.0): dependencies: '@typescript-eslint/utils': 7.16.0(eslint@9.6.0)(typescript@5.5.3) eslint: 9.6.0