diff --git a/.changeset/khaki-tables-give.md b/.changeset/khaki-tables-give.md new file mode 100644 index 0000000000..8000eb0fe6 --- /dev/null +++ b/.changeset/khaki-tables-give.md @@ -0,0 +1,5 @@ +--- +'@penumbra-zone/getters': major +--- + +don't export optional getters diff --git a/.changeset/slow-seas-fix.md b/.changeset/slow-seas-fix.md new file mode 100644 index 0000000000..3ab6685d65 --- /dev/null +++ b/.changeset/slow-seas-fix.md @@ -0,0 +1,5 @@ +--- +'@penumbra-zone/getters': major +--- + +improve getter type diff --git a/apps/minifront/src/components/dashboard/assets-table/equivalent-values.tsx b/apps/minifront/src/components/dashboard/assets-table/equivalent-values.tsx index 0ce194a8e6..994efe2cd2 100644 --- a/apps/minifront/src/components/dashboard/assets-table/equivalent-values.tsx +++ b/apps/minifront/src/components/dashboard/assets-table/equivalent-values.tsx @@ -4,7 +4,7 @@ import { getDisplayDenomFromView, getEquivalentValues } from '@penumbra-zone/get import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/value'; export const EquivalentValues = ({ valueView }: { valueView?: ValueView }) => { - const equivalentValuesAsValueViews = (getEquivalentValues.optional()(valueView) ?? []).map( + const equivalentValuesAsValueViews = (getEquivalentValues.optional(valueView) ?? []).map( asValueView, ); diff --git a/apps/minifront/src/components/dashboard/assets-table/index.tsx b/apps/minifront/src/components/dashboard/assets-table/index.tsx index 6c7658472e..6688f2240b 100644 --- a/apps/minifront/src/components/dashboard/assets-table/index.tsx +++ b/apps/minifront/src/components/dashboard/assets-table/index.tsx @@ -14,7 +14,7 @@ import { EquivalentValues } from './equivalent-values'; import { Fragment } from 'react'; import { PagePath } from '../../metadata/paths'; import { Link } from 'react-router-dom'; -import { getMetadataFromBalancesResponseOptional } from '@penumbra-zone/getters/balances-response'; +import { getMetadataFromBalancesResponse } from '@penumbra-zone/getters/balances-response'; import { getAddressIndex } from '@penumbra-zone/getters/address-view'; import { BalancesByAccount, groupByAccount, useBalancesResponses } from '../../../state/shared'; import { AbridgedZQueryState } from '@penumbra-zone/zquery/src/types'; @@ -24,7 +24,7 @@ import { LineWave } from 'react-loader-spinner'; import { cn } from '@penumbra-zone/ui/lib/utils'; const getTradeLink = (balance: BalancesResponse): string => { - const metadata = getMetadataFromBalancesResponseOptional(balance); + const metadata = getMetadataFromBalancesResponse.optional(balance); const accountIndex = getAddressIndex(balance.accountAddress).account; const accountQuery = accountIndex ? `&account=${accountIndex}` : ''; return metadata ? `${PagePath.SWAP}?from=${metadata.symbol}${accountQuery}` : PagePath.SWAP; diff --git a/apps/minifront/src/components/shared/input-token.tsx b/apps/minifront/src/components/shared/input-token.tsx index 2ce3608bf2..9e3232d8b7 100644 --- a/apps/minifront/src/components/shared/input-token.tsx +++ b/apps/minifront/src/components/shared/input-token.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb'; import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata'; -import { getMetadataFromBalancesResponseOptional } from '@penumbra-zone/getters/balances-response'; +import { getMetadataFromBalancesResponse } from '@penumbra-zone/getters/balances-response'; import { BalanceValueView } from '@penumbra-zone/ui/components/ui/balance-value-view'; import { cn } from '@penumbra-zone/ui/lib/utils'; import BalanceSelector from './selectors/balance-selector'; @@ -38,7 +38,7 @@ export default function InputToken({ loading, }: InputTokenProps) { const tokenExponent = useMemo(() => { - return getDisplayDenomExponent.optional()(getMetadataFromBalancesResponseOptional(selection)); + return getDisplayDenomExponent.optional(getMetadataFromBalancesResponse.optional(selection)); }, [selection]); const setInputToBalanceMax = () => { diff --git a/apps/minifront/src/components/shared/non-native-fee-warning.tsx b/apps/minifront/src/components/shared/non-native-fee-warning.tsx index 4c150c749c..ffc6bde0a5 100644 --- a/apps/minifront/src/components/shared/non-native-fee-warning.tsx +++ b/apps/minifront/src/components/shared/non-native-fee-warning.tsx @@ -24,7 +24,7 @@ const hasTokenBalance = ({ gasPrices: GasPrices[]; stakingAssetMetadata?: Metadata; }): boolean => { - const account = getAddressIndex.optional()(source)?.account; + const account = getAddressIndex.optional(source)?.account; if (typeof account === 'undefined') { return false; } @@ -33,9 +33,9 @@ const hasTokenBalance = ({ const hasStakingToken = balancesResponses.some( asset => getAssetIdFromValueView - .optional()(asset.balanceView) - ?.equals(getAssetId.optional()(stakingAssetMetadata)) && - getAddressIndex.optional()(asset)?.account === account, + .optional(asset.balanceView) + ?.equals(getAssetId.optional(stakingAssetMetadata)) && + getAddressIndex.optional(asset)?.account === account, ); if (hasStakingToken) { @@ -43,7 +43,7 @@ const hasTokenBalance = ({ } const accountAssets = balancesResponses.filter( - balance => getAddressIndex.optional()(balance)?.account === account, + balance => getAddressIndex.optional(balance)?.account === account, ); // Finds the alt tokens in the user's account balances that can be used for fees const hasAltTokens = accountAssets.some(balance => { @@ -54,7 +54,7 @@ const hasTokenBalance = ({ } return gasPrices.some(price => - price.assetId?.equals(getAssetIdFromBalancesResponse.optional()(balance)), + price.assetId?.equals(getAssetIdFromBalancesResponse.optional(balance)), ); }); diff --git a/apps/minifront/src/components/shared/selectors/balance-item.tsx b/apps/minifront/src/components/shared/selectors/balance-item.tsx index fa2bd665ad..d68f3ef6b1 100644 --- a/apps/minifront/src/components/shared/selectors/balance-item.tsx +++ b/apps/minifront/src/components/shared/selectors/balance-item.tsx @@ -1,9 +1,6 @@ import { BalanceOrMetadata, isBalance, isMetadata } from './helpers'; import { getAddressIndex } from '@penumbra-zone/getters/address-view'; -import { - getMetadataFromBalancesResponse, - getMetadataFromBalancesResponseOptional, -} from '@penumbra-zone/getters/balances-response'; +import { getMetadataFromBalancesResponse } from '@penumbra-zone/getters/balances-response'; import { useMemo } from 'react'; import { DialogClose } from '@penumbra-zone/ui/components/ui/dialog'; import { cn } from '@penumbra-zone/ui/lib/utils'; @@ -22,8 +19,8 @@ export const BalanceItem = ({ asset, value, onSelect }: BalanceItemProps) => { const account = isBalance(asset) ? getAddressIndex(asset.accountAddress).account : undefined; const metadataFromAsset = isMetadata(asset) ? asset - : getMetadataFromBalancesResponseOptional(asset); - const metadataFromValue = getMetadataFromBalancesResponse.optional()(value); + : getMetadataFromBalancesResponse.optional(asset); + const metadataFromValue = getMetadataFromBalancesResponse.optional(value); const isSelected = useMemo(() => { if (!value) { diff --git a/apps/minifront/src/components/shared/selectors/helpers.ts b/apps/minifront/src/components/shared/selectors/helpers.ts index 94f7c0ae4e..639531802b 100644 --- a/apps/minifront/src/components/shared/selectors/helpers.ts +++ b/apps/minifront/src/components/shared/selectors/helpers.ts @@ -2,7 +2,7 @@ import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_ import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; import { getAddressIndex, - getMetadataFromBalancesResponseOptional, + getMetadataFromBalancesResponse, } from '@penumbra-zone/getters/balances-response'; import { useEffect } from 'react'; @@ -22,7 +22,7 @@ export const mergeBalancesAndAssets = ( ): BalanceOrMetadata[] => { const filteredAssets = assets.filter(asset => { return !balances.some(balance => { - const balanceMetadata = getMetadataFromBalancesResponseOptional(balance); + const balanceMetadata = getMetadataFromBalancesResponse.optional(balance); return balanceMetadata?.equals(asset); }); }); @@ -43,10 +43,10 @@ export const useSyncSelectedBalance = ({ if (value) { const matchedValue = balances?.find(balance => { return ( - getAddressIndex.optional()(balance)?.equals(getAddressIndex.optional()(value)) && - getMetadataFromBalancesResponseOptional(balance)?.equals( - getMetadataFromBalancesResponseOptional(value), - ) + getAddressIndex.optional(balance)?.equals(getAddressIndex.optional(value)) && + getMetadataFromBalancesResponse + .optional(balance) + ?.equals(getMetadataFromBalancesResponse.optional(value)) ); }); if (matchedValue && !matchedValue.equals(value)) { diff --git a/apps/minifront/src/components/shared/selectors/search-filters.ts b/apps/minifront/src/components/shared/selectors/search-filters.ts index cb9a879e8f..91f3f4ae9e 100644 --- a/apps/minifront/src/components/shared/selectors/search-filters.ts +++ b/apps/minifront/src/components/shared/selectors/search-filters.ts @@ -7,7 +7,7 @@ import { getValueViewCaseFromBalancesResponse } from '@penumbra-zone/getters/bal export const balanceBySearch = (search: string) => (balancesResponse: BalancesResponse): boolean => - getValueViewCaseFromBalancesResponse.optional()(balancesResponse) === 'knownAssetId' && + getValueViewCaseFromBalancesResponse.optional(balancesResponse) === 'knownAssetId' && (getDisplayDenomFromView(balancesResponse.balanceView) .toLocaleLowerCase() .includes(search.toLocaleLowerCase()) || diff --git a/apps/minifront/src/components/staking/account/delegation-value-view/validator-info-component.tsx b/apps/minifront/src/components/staking/account/delegation-value-view/validator-info-component.tsx index fa2d450bf1..c217018f4f 100644 --- a/apps/minifront/src/components/staking/account/delegation-value-view/validator-info-component.tsx +++ b/apps/minifront/src/components/staking/account/delegation-value-view/validator-info-component.tsx @@ -35,7 +35,7 @@ export const ValidatorInfoComponent = ({ const showTooltips = useStore(state => !state.staking.loading); const validator = getValidator(validatorInfo); const identityKey = getIdentityKeyFromValidatorInfo(validatorInfo); - const state = getValidatorState.optional()(validatorInfo); + const state = getValidatorState.optional(validatorInfo); return ( diff --git a/apps/minifront/src/components/swap/swap-form/output/estimated-output-explanation.tsx b/apps/minifront/src/components/swap/swap-form/output/estimated-output-explanation.tsx index 54ad934a50..cd7399e5fb 100644 --- a/apps/minifront/src/components/swap/swap-form/output/estimated-output-explanation.tsx +++ b/apps/minifront/src/components/swap/swap-form/output/estimated-output-explanation.tsx @@ -24,9 +24,9 @@ export const EstimatedOutputExplanation = () => { const formattedAmount = formatAmount({ amount: estimatedOutput, - exponent: getDisplayDenomExponent.optional()(assetOut), + exponent: getDisplayDenomExponent.optional(assetOut), }); - const assetInSymbol = getSymbolFromValueView.optional()(assetIn?.balanceView); + const assetInSymbol = getSymbolFromValueView.optional(assetIn?.balanceView); return (
diff --git a/apps/minifront/src/components/swap/swap-form/price-history.tsx b/apps/minifront/src/components/swap/swap-form/price-history.tsx index d5fab7f6be..d143245b90 100644 --- a/apps/minifront/src/components/swap/swap-form/price-history.tsx +++ b/apps/minifront/src/components/swap/swap-form/price-history.tsx @@ -1,4 +1,4 @@ -import { getMetadataFromBalancesResponseOptional } from '@penumbra-zone/getters/balances-response'; +import { getMetadataFromBalancesResponse } from '@penumbra-zone/getters/balances-response'; import { AbridgedZQueryState } from '@penumbra-zone/zquery/src/types'; import { Box } from '@penumbra-zone/ui/components/ui/box'; import { CandlestickPlot } from '@penumbra-zone/ui/components/ui/candlestick-plot'; @@ -12,7 +12,7 @@ import { useStoreShallow } from '../../../utils/use-store-shallow'; import { Button } from '@penumbra-zone/ui/components/ui/button'; const priceHistorySelector = (state: AllSlices) => ({ - startMetadata: getMetadataFromBalancesResponseOptional(state.swap.assetIn), + startMetadata: getMetadataFromBalancesResponse.optional(state.swap.assetIn), endMetadata: state.swap.assetOut, historyLimit: state.swap.priceHistory.historyLimit, historyStart: state.swap.priceHistory.historyStart, diff --git a/apps/minifront/src/components/swap/swap-form/simulate-swap-result/index.tsx b/apps/minifront/src/components/swap/swap-form/simulate-swap-result/index.tsx index dae0e997fe..b019e4636c 100644 --- a/apps/minifront/src/components/swap/swap-form/simulate-swap-result/index.tsx +++ b/apps/minifront/src/components/swap/swap-form/simulate-swap-result/index.tsx @@ -25,9 +25,9 @@ const simulateSwapResultSelector = (state: AllSlices) => ({ value: { amount: toBaseUnit( new BigNumber(state.swap.amount || 0), - getDisplayDenomExponentFromValueView.optional()(state.swap.assetIn?.balanceView), + getDisplayDenomExponentFromValueView.optional(state.swap.assetIn?.balanceView), ), - metadata: getMetadata.optional()(state.swap.assetIn?.balanceView), + metadata: getMetadata.optional(state.swap.assetIn?.balanceView), }, }, }), 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 e9c2689e74..74dd6bc4ed 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 @@ -21,8 +21,8 @@ export const Price = ({ const lastValueMetadata = metadataByAssetId[bech32mAssetId(outputValue.assetId)]; if (firstValueMetadata?.symbol && lastValueMetadata?.symbol) { - const inputDisplayDenomExponent = getDisplayDenomExponent.optional()(firstValueMetadata) ?? 0; - const outputDisplayDenomExponent = getDisplayDenomExponent.optional()(lastValueMetadata) ?? 0; + const inputDisplayDenomExponent = getDisplayDenomExponent.optional(firstValueMetadata) ?? 0; + const outputDisplayDenomExponent = getDisplayDenomExponent.optional(lastValueMetadata) ?? 0; const formattedInputAmount = formatAmount({ amount: inputValue.amount, exponent: inputDisplayDenomExponent, diff --git a/apps/minifront/src/components/swap/swap-form/token-swap-input.tsx b/apps/minifront/src/components/swap/swap-form/token-swap-input.tsx index 6fe017615c..8ad808d21e 100644 --- a/apps/minifront/src/components/swap/swap-form/token-swap-input.tsx +++ b/apps/minifront/src/components/swap/swap-form/token-swap-input.tsx @@ -6,7 +6,7 @@ import { joinLoHiAmount } from '@penumbra-zone/types/amount'; import { getAmount, getBalanceView, - getMetadataFromBalancesResponseOptional, + getMetadataFromBalancesResponse, } from '@penumbra-zone/getters/balances-response'; import { ArrowRight } from 'lucide-react'; import { useMemo } from 'react'; @@ -43,7 +43,7 @@ const getAssetOutBalance = ( getAddressIndex(assetIn.accountAddress), assetOut, ); - const matchedBalance = getBalanceView.optional()(match); + const matchedBalance = getBalanceView.optional(match); return matchedBalance ?? zeroValueView(assetOut); }; @@ -70,10 +70,10 @@ export const TokenSwapInput = () => { useStoreShallow(tokenSwapInputSelector); const assetOutBalance = getAssetOutBalance(balancesResponses?.data, assetIn, assetOut); const assetInExponent = useMemo(() => { - return getDisplayDenomExponent.optional()(getMetadataFromBalancesResponseOptional(assetIn)); + return getDisplayDenomExponent.optional(getMetadataFromBalancesResponse.optional(assetIn)); }, [assetIn]); - const maxAmount = getAmount.optional()(assetIn); + const maxAmount = getAmount.optional(assetIn); const maxAmountAsString = maxAmount ? joinLoHiAmount(maxAmount).toString() : undefined; const setInputToBalanceMax = () => { diff --git a/apps/minifront/src/components/v2/dashboard-layout/assets-page/equivalent-values.tsx b/apps/minifront/src/components/v2/dashboard-layout/assets-page/equivalent-values.tsx index 8ab394fe8e..db172933e7 100644 --- a/apps/minifront/src/components/v2/dashboard-layout/assets-page/equivalent-values.tsx +++ b/apps/minifront/src/components/v2/dashboard-layout/assets-page/equivalent-values.tsx @@ -4,7 +4,7 @@ import { getDisplayDenomFromView, getEquivalentValues } from '@penumbra-zone/get import { ValueViewComponent } from '@penumbra-zone/ui/ValueViewComponent'; export const EquivalentValues = ({ valueView }: { valueView?: ValueView }) => { - const equivalentValuesAsValueViews = (getEquivalentValues.optional()(valueView) ?? []).map( + const equivalentValuesAsValueViews = (getEquivalentValues.optional(valueView) ?? []).map( asValueView, ); diff --git a/apps/minifront/src/components/v2/dashboard-layout/assets-page/index.tsx b/apps/minifront/src/components/v2/dashboard-layout/assets-page/index.tsx index e5e2cd8490..8ad96547cb 100644 --- a/apps/minifront/src/components/v2/dashboard-layout/assets-page/index.tsx +++ b/apps/minifront/src/components/v2/dashboard-layout/assets-page/index.tsx @@ -4,7 +4,7 @@ import { BalancesByAccount, groupByAccount, useBalancesResponses } from '../../. import { shouldDisplay } from '../../../../fetchers/balances/should-display'; import { sortByPriorityScore } from '../../../../fetchers/balances/by-priority-score'; import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb'; -import { getMetadataFromBalancesResponseOptional } from '@penumbra-zone/getters/balances-response'; +import { getMetadataFromBalancesResponse } from '@penumbra-zone/getters/balances-response'; import { PagePath } from '../../../metadata/paths'; import { getAddressIndex } from '@penumbra-zone/getters/address-view'; import { AbridgedZQueryState } from '@penumbra-zone/zquery/src/types'; @@ -19,7 +19,7 @@ import { ConditionalWrap } from '@penumbra-zone/ui/ConditionalWrap'; import { LayoutGroup } from 'framer-motion'; const getTradeLink = (balance: BalancesResponse): string => { - const metadata = getMetadataFromBalancesResponseOptional(balance); + const metadata = getMetadataFromBalancesResponse.optional(balance); const accountIndex = getAddressIndex(balance.accountAddress).account; const accountQuery = accountIndex ? `&account=${accountIndex}` : ''; return metadata ? `${PagePath.SWAP}?from=${metadata.symbol}${accountQuery}` : PagePath.SWAP; diff --git a/apps/minifront/src/fetchers/auction-infos.ts b/apps/minifront/src/fetchers/auction-infos.ts index b83d32ec5d..b3f4914882 100644 --- a/apps/minifront/src/fetchers/auction-infos.ts +++ b/apps/minifront/src/fetchers/auction-infos.ts @@ -31,8 +31,8 @@ export const getAuctionInfos = async function* ({ const auction = DutchAuction.fromBinary(response.auction.value); - const inputAssetId = getInputAssetId.optional()(auction); - const outputAssetId = getOutputAssetId.optional()(auction); + const inputAssetId = getInputAssetId.optional(auction); + const outputAssetId = getOutputAssetId.optional(auction); const inputMetadataPromise = inputAssetId ? penumbra.service(ViewService).assetMetadataById({ assetId: inputAssetId }) diff --git a/apps/minifront/src/fetchers/balances/by-priority-score.ts b/apps/minifront/src/fetchers/balances/by-priority-score.ts index 9cf98616b5..664dc3cc69 100644 --- a/apps/minifront/src/fetchers/balances/by-priority-score.ts +++ b/apps/minifront/src/fetchers/balances/by-priority-score.ts @@ -1,17 +1,17 @@ import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb'; import { - getMetadataFromBalancesResponseOptional, + getMetadataFromBalancesResponse, getAmount, getAddressIndex, } from '@penumbra-zone/getters/balances-response'; import { multiplyAmountByNumber, joinLoHiAmount } from '@penumbra-zone/types/amount'; export const sortByPriorityScore = (a: BalancesResponse, b: BalancesResponse) => { - const aScore = getMetadataFromBalancesResponseOptional(a)?.priorityScore ?? 1n; - const bScore = getMetadataFromBalancesResponseOptional(b)?.priorityScore ?? 1n; + const aScore = getMetadataFromBalancesResponse.optional(a)?.priorityScore ?? 1n; + const bScore = getMetadataFromBalancesResponse.optional(b)?.priorityScore ?? 1n; - const aAmount = getAmount.optional()(a); - const bAmount = getAmount.optional()(b); + const aAmount = getAmount.optional(a); + const bAmount = getAmount.optional(b); const aPriority = aAmount ? joinLoHiAmount(multiplyAmountByNumber(aAmount, Number(aScore))) diff --git a/apps/minifront/src/state/helpers.ts b/apps/minifront/src/state/helpers.ts index a0918f5b0e..016ef4e6c9 100644 --- a/apps/minifront/src/state/helpers.ts +++ b/apps/minifront/src/state/helpers.ts @@ -22,7 +22,7 @@ import { uint8ArrayToHex } from '@penumbra-zone/types/hex'; import { fromValueView } from '@penumbra-zone/types/amount'; import { BigNumber } from 'bignumber.js'; import { - getMetadataFromBalancesResponseOptional, + getMetadataFromBalancesResponse, getValueViewCaseFromBalancesResponse, } from '@penumbra-zone/getters/balances-response'; import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata'; @@ -201,8 +201,8 @@ export const isIncorrectDecimal = ( throw new Error('Missing balanceView'); } - const exponent = getDisplayDenomExponent.optional()( - getMetadataFromBalancesResponseOptional(asset), + const exponent = getDisplayDenomExponent.optional( + getMetadataFromBalancesResponse.optional(asset), ); const fraction = amountInDisplayDenom.split('.')[1]?.length; return typeof exponent !== 'undefined' && typeof fraction !== 'undefined' && fraction > exponent; @@ -214,4 +214,4 @@ export const isValidAmount = (amount: string, assetIn?: BalancesResponse) => (!assetIn || !isIncorrectDecimal(assetIn, amount)); export const isKnown = (balancesResponse: BalancesResponse) => - getValueViewCaseFromBalancesResponse.optional()(balancesResponse) === 'knownAssetId'; + getValueViewCaseFromBalancesResponse.optional(balancesResponse) === 'knownAssetId'; diff --git a/apps/minifront/src/state/ibc-out.ts b/apps/minifront/src/state/ibc-out.ts index 6087315512..b53a3bde6f 100644 --- a/apps/minifront/src/state/ibc-out.ts +++ b/apps/minifront/src/state/ibc-out.ts @@ -282,7 +282,7 @@ export const filterBalancesPerChain = ( registryAssets: Metadata[], stakingTokenMetadata?: Metadata, ): BalancesResponse[] => { - const penumbraAssetId = getAssetId.optional()(stakingTokenMetadata); + const penumbraAssetId = getAssetId.optional(stakingTokenMetadata); const assetsWithMatchingChannel = registryAssets .filter(a => { const match = assetPatterns.ibc.capture(a.base); diff --git a/apps/minifront/src/state/send/index.ts b/apps/minifront/src/state/send/index.ts index 1f7235cb20..58782df2cd 100644 --- a/apps/minifront/src/state/send/index.ts +++ b/apps/minifront/src/state/send/index.ts @@ -133,7 +133,7 @@ const assembleRequest = ({ amount, feeTier, recipient, selection, memo }: SendSl value: { amount: toBaseUnit( BigNumber(amount), - getDisplayDenomExponentFromValueView.optional()(selection?.balanceView), + getDisplayDenomExponentFromValueView.optional(selection?.balanceView), ), assetId: getAssetIdFromValueView(selection?.balanceView), }, diff --git a/apps/minifront/src/state/swap/dutch-auction/assemble-schedule-request.ts b/apps/minifront/src/state/swap/dutch-auction/assemble-schedule-request.ts index dd8eb3b68e..8cc72d1513 100644 --- a/apps/minifront/src/state/swap/dutch-auction/assemble-schedule-request.ts +++ b/apps/minifront/src/state/swap/dutch-auction/assemble-schedule-request.ts @@ -13,7 +13,7 @@ export const assembleScheduleRequest = async ({ duration, }: Pick & Pick): Promise => { - const source = getAddressIndex.optional()(assetIn); + const source = getAddressIndex.optional(assetIn); return new TransactionPlannerRequest({ dutchAuctionScheduleActions: await getSubAuctions({ diff --git a/apps/minifront/src/state/swap/dutch-auction/index.ts b/apps/minifront/src/state/swap/dutch-auction/index.ts index b7cc2bcad4..b3bdff1e21 100644 --- a/apps/minifront/src/state/swap/dutch-auction/index.ts +++ b/apps/minifront/src/state/swap/dutch-auction/index.ts @@ -122,7 +122,7 @@ export const createDutchAuctionSlice = (): SliceCreator => (s swap.dutchAuction.minOutput = ''; } else { const minMinOutput = getSmallestPossibleAmountAboveZero(get().swap.assetOut); - const exponent = getDisplayDenomExponent.optional()(get().swap.assetOut) ?? 0; + const exponent = getDisplayDenomExponent.optional(get().swap.assetOut) ?? 0; const minOutputAsBaseUnit = Number(minOutput) * 10 ** exponent; const outputLimitAsDisplayUnit = (OUTPUT_LIMIT / 10 ** exponent).toString(); @@ -144,7 +144,7 @@ export const createDutchAuctionSlice = (): SliceCreator => (s swap.dutchAuction.maxOutput = ''; } else { const minMaxOutput = getSmallestPossibleAmountAboveZero(get().swap.assetOut); - const exponent = getDisplayDenomExponent.optional()(get().swap.assetOut) ?? 0; + const exponent = getDisplayDenomExponent.optional(get().swap.assetOut) ?? 0; const maxOutputAsBaseUnit = Number(maxOutput) * 10 ** exponent; const outputLimitAsDisplayUnit = (OUTPUT_LIMIT / 10 ** exponent).toString(); diff --git a/apps/minifront/src/state/swap/getters.ts b/apps/minifront/src/state/swap/getters.ts index 28c6cfa236..e3a33b7f19 100644 --- a/apps/minifront/src/state/swap/getters.ts +++ b/apps/minifront/src/state/swap/getters.ts @@ -1,14 +1,14 @@ import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb'; import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; import { AddressIndex } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; -import { getMetadataFromBalancesResponseOptional } from '@penumbra-zone/getters/balances-response'; +import { getMetadataFromBalancesResponse } from '@penumbra-zone/getters/balances-response'; import { getAddressIndex } from '@penumbra-zone/getters/address-view'; import { getMetadata } from '@penumbra-zone/getters/value-view'; export const balancesResponseAndMetadataAreSameAsset = ( balancesResponse?: BalancesResponse, metadata?: Metadata, -) => getMetadata.optional()(balancesResponse?.balanceView)?.equals(metadata); +) => getMetadata.optional(balancesResponse?.balanceView)?.equals(metadata); export const getFirstBalancesResponseNotMatchingMetadata = ( balancesResponses: BalancesResponse[], @@ -38,7 +38,7 @@ export const getBalanceByMatchingMetadataAndAddressIndex = ( metadata: Metadata, ) => { return balances.find(balance => { - const balanceViewMetadata = getMetadataFromBalancesResponseOptional(balance); + const balanceViewMetadata = getMetadataFromBalancesResponse.optional(balance); return ( getAddressIndex(balance.accountAddress).account === addressIndex.account && diff --git a/apps/minifront/src/state/swap/query-params.ts b/apps/minifront/src/state/swap/query-params.ts index 222666ecd1..ce6663026c 100644 --- a/apps/minifront/src/state/swap/query-params.ts +++ b/apps/minifront/src/state/swap/query-params.ts @@ -1,7 +1,7 @@ import type { AllSlices } from '..'; import { getAddressIndex, - getMetadataFromBalancesResponseOptional, + getMetadataFromBalancesResponse, } from '@penumbra-zone/getters/balances-response'; interface SwapQueryParams { @@ -31,9 +31,9 @@ export const getSwapQueryParams = (): SwapQueryParams => { * Sets the swap query parameters in the URL hash based on the store state */ export const setSwapQueryParams = (state: AllSlices): void => { - const fromSymbol = getMetadataFromBalancesResponseOptional(state.swap.assetIn)?.symbol; + const fromSymbol = getMetadataFromBalancesResponse.optional(state.swap.assetIn)?.symbol; const toSymbol = state.swap.assetOut?.symbol; - const accountIndex = getAddressIndex.optional()(state.swap.assetIn)?.account; + const accountIndex = getAddressIndex.optional(state.swap.assetIn)?.account; const searchParams = new URLSearchParams(); if (fromSymbol) { diff --git a/apps/minifront/src/state/swap/swap-balances-middleware.ts b/apps/minifront/src/state/swap/swap-balances-middleware.ts index cf8a571d60..d13e38bd58 100644 --- a/apps/minifront/src/state/swap/swap-balances-middleware.ts +++ b/apps/minifront/src/state/swap/swap-balances-middleware.ts @@ -32,7 +32,7 @@ const getAssetIn = (state: AllSlices, from?: string, account?: number) => { if (from) { const matchingBalancesResponse = filteredSwappableBalancesResponses.find( - balance => getMetadataFromBalancesResponse.optional()(balance)?.symbol === from, + balance => getMetadataFromBalancesResponse.optional(balance)?.symbol === from, ); if (matchingBalancesResponse) { return matchingBalancesResponse; @@ -73,7 +73,7 @@ const getAssetOut = (state: AllSlices, to?: string, assetIn?: BalancesResponse) return state.swap.assetOut; } - if (getMetadataFromBalancesResponse.optional()(assetIn)?.equals(swappableAssets[0])) { + if (getMetadataFromBalancesResponse.optional(assetIn)?.equals(swappableAssets[0])) { return swappableAssets[1]; } diff --git a/packages/getters/README.md b/packages/getters/README.md index 0b5c7cba14..dcaf93ba0c 100644 --- a/packages/getters/README.md +++ b/packages/getters/README.md @@ -83,7 +83,7 @@ At this point, it's worth mentioning that getters are _required by default_. If What if the value you're getting _is_ optional, though? What if you don't want your getter to throw if either the value it's passed, or the value it returns, is `undefined`? That's what the `.optional()` property on the getter is for: ```tsx -const addressView = getAddressView.optional()(memoView) +const addressView = getAddressView.optional(memoView)
{addressView && } diff --git a/packages/getters/src/balances-response.ts b/packages/getters/src/balances-response.ts index 5eca268938..bc3aeab8e6 100644 --- a/packages/getters/src/balances-response.ts +++ b/packages/getters/src/balances-response.ts @@ -11,11 +11,7 @@ export const getAssetIdFromBalancesResponse = getBalanceView.pipe(getMetadata).p export const getMetadataFromBalancesResponse = getBalanceView.pipe(getMetadata); -export const getDisplayFromBalancesResponse = getMetadataFromBalancesResponse - .optional() - .pipe(getDisplay); - -export const getMetadataFromBalancesResponseOptional = getBalanceView.optional().pipe(getMetadata); +export const getDisplayFromBalancesResponse = getMetadataFromBalancesResponse.pipe(getDisplay); export const getAddressIndex = createGetter((balancesResponse?: BalancesResponse) => balancesResponse?.accountAddress?.addressView.case === 'decoded' diff --git a/packages/getters/src/swap-claim-view.ts b/packages/getters/src/swap-claim-view.ts index 9bb0821d04..45f02ca3d0 100644 --- a/packages/getters/src/swap-claim-view.ts +++ b/packages/getters/src/swap-claim-view.ts @@ -14,8 +14,8 @@ export const getOutput2 = createGetter((swapClaimView?: SwapClaimView) => : undefined, ); -export const getOutput1ValueOptional = getOutput1.optional().pipe(getValue); -export const getOutput2ValueOptional = getOutput2.optional().pipe(getValue); +export const getOutput1Value = getOutput1.pipe(getValue); +export const getOutput2Value = getOutput2.pipe(getValue); export const getSwapClaimFee = createGetter((swapClaimView?: SwapClaimView) => swapClaimView?.swapClaimView.case === 'visible' || swapClaimView?.swapClaimView.case === 'opaque' diff --git a/packages/getters/src/utils/create-getter.test.ts b/packages/getters/src/utils/create-getter.test.ts index f60bb5c3d1..e52559b6da 100644 --- a/packages/getters/src/utils/create-getter.test.ts +++ b/packages/getters/src/utils/create-getter.test.ts @@ -1,110 +1,215 @@ -import { assertType, describe, expect, it } from 'vitest'; +/* eslint-disable no-restricted-syntax */ + +import { describe, expect, expectTypeOf, it, test } from 'vitest'; import { createGetter } from './create-getter.js'; +import { Getter } from './getter.js'; + +type City = 'Seattle' | 'San Francisco' | 'New York City'; interface Address { - city: string; - state: string; - country?: string; + city: City; + state?: string; } -interface Employee { +interface Person { firstName: string; lastName?: string; address?: Address; } -const employee: Employee = { +// complete data +const alice: Person = { firstName: 'Alice', + lastName: 'Liddell', address: { city: 'San Francisco', state: 'California', }, }; -const getFirstName = createGetter((employee?: Employee) => employee?.firstName); -const getLastName = createGetter((employee?: Employee) => employee?.lastName); -const getAddress = createGetter((employee?: Employee) => employee?.address); -const getCity = createGetter((address?: Address) => address?.city); -const getCountry = createGetter((address?: Address) => address?.country); -const getFirstLetter = createGetter((value?: string) => value?.[0]); +const bob: Person = { + firstName: 'Bob', + // missing lastName + address: { + city: 'Seattle', + // missing state + }, +}; + +const charlie: Person = { + firstName: 'Charlie', + lastName: '', // falsy lastName + // missing address +}; describe('createGetter()', () => { - describe('getter()', () => { - it('gets the value via the function passed into `createGetter()`', () => { - expect(getFirstName(employee)).toBe('Alice'); - }); + const selectFirstName = (p?: Person) => p?.firstName; + const selectStringIndexOne = (a?: string) => a?.[1]; + + it('creates a getter', () => { + let getFirstNameFromPerson; + expectTypeOf((getFirstNameFromPerson = createGetter(selectFirstName))).toEqualTypeOf< + Getter + >(); + expect(getFirstNameFromPerson).toBeInstanceOf(Function); + expect(getFirstNameFromPerson).toHaveProperty(['optional', 'pipe']); + }); + + it('creates a getter with optional', () => { + let getFirstNameFromPerson_optional; + expectTypeOf( + (getFirstNameFromPerson_optional = createGetter(selectFirstName).optional), + ).toEqualTypeOf>(); + + expect(getFirstNameFromPerson_optional).toBeInstanceOf(Function); + expect(getFirstNameFromPerson_optional).toHaveProperty('optional'); + expect(getFirstNameFromPerson_optional).toHaveProperty('pipe'); + + expect(getFirstNameFromPerson_optional.optional).toBe(getFirstNameFromPerson_optional); + }); + + it('creates a getter pipe', () => { + const getFirstName = createGetter(selectFirstName); + const getSecondLetter = createGetter(selectStringIndexOne); + + let getSecondLetterOfFirstNameFromPerson; + expectTypeOf( + (getSecondLetterOfFirstNameFromPerson = getFirstName.pipe(getSecondLetter)), + ).toEqualTypeOf>(); + + expect(getSecondLetterOfFirstNameFromPerson).toBeInstanceOf(Function); + expect(getSecondLetterOfFirstNameFromPerson).toHaveProperty('optional'); + expect(getSecondLetterOfFirstNameFromPerson).toHaveProperty('pipe'); + }); +}); - it('throws when the whole value is undefined', () => { +describe('getting values and optional', () => { + const getFirstName = createGetter((p?: Person) => p?.firstName); + const getLastName = createGetter((p?: Person) => p?.lastName); + + it('gets the expected value', () => { + expect(getFirstName(alice)).toBe('Alice'); + expect(getFirstName.optional(alice)).toBe('Alice'); + + expect(getLastName(alice)).toBe('Liddell'); + expect(getLastName.optional(alice)).toBe('Liddell'); + }); + + describe('undefined in the getter', () => { + it('handles undefined input', () => { expect(() => getFirstName(undefined)).toThrow(); + expect(getFirstName.optional(undefined)).toBeUndefined(); }); - it('throws for an undefined property', () => { - expect(() => getLastName(employee)).toThrow(); + it('handles undefined property', () => { + expect(() => getLastName(bob)).toThrow(); + expect(getLastName.optional(bob)).toBeUndefined(); }); + }); - it('does not throw if a value is falsey but not undefined', () => { - const employee: Employee = { firstName: 'Alice', lastName: '' }; - expect(() => getLastName(employee)).not.toThrow(); - }); + test('successfully returns a falsy value', () => { + expect(() => getLastName(charlie)).not.toThrow(); }); +}); - describe('getter.optional()', () => { - it('returns `undefined` when the whole value is undefined', () => { - expect(getLastName.optional()(undefined)).toBeUndefined(); - }); +describe('getter pipes', () => { + const selectAddress = (p?: Person) => p?.address; + const getAddressFromPerson = createGetter(selectAddress); + const selectCity = (a?: Address) => a?.city; + const getCityFromAddress = createGetter(selectCity); + const getStateFromAddress = createGetter((a?: Address) => a?.state); - it('returns `undefined` for an undefined property', () => { - expect(getLastName.optional()(employee)).toBeUndefined(); - }); + it('pipes the getters together and returns the final result', () => { + let getCityFromPerson; + + expectTypeOf((getCityFromPerson = getAddressFromPerson.pipe(getCityFromAddress))).toEqualTypeOf< + Getter + >(); + + expect(getCityFromPerson(alice)).toBe('San Francisco'); + expect(getCityFromPerson(bob)).toBe('Seattle'); }); - describe('getter.pipe()', () => { - it('pipes the getters together and returns the final result', () => { - expect(getAddress.pipe(getCity)(employee)).toBe('San Francisco'); - }); + describe('undefined in the pipe', () => { + let getStateFromPerson; + + expectTypeOf( + (getStateFromPerson = getAddressFromPerson.pipe(getStateFromAddress)), + ).toEqualTypeOf>(); - it('throws when any value in the property chain is undefined', () => { - expect(() => getAddress.pipe(getCity)(undefined)).toThrow(); - expect(() => getAddress.pipe(getCity)({ firstName: 'Alice' })).toThrow(); - expect(() => getAddress.pipe(getCountry)(employee)).toThrow(); + it('throws on undefined', () => { + expect(() => getStateFromPerson(undefined)).toThrow(); + expect(getStateFromPerson(alice)).toBe('California'); + expect(() => getStateFromPerson(bob)).toThrow(); }); - describe('getter.pipe() with .optional())', () => { - const employee: Employee = { - firstName: 'Alice', - address: { - city: '', // `getFirstLetter` will return undefined - state: 'California', - }, - }; - - it('does not throw when the first getter is used with `.optional()` and some value in the chain is undefined', () => { - expect(() => - getAddress.optional().pipe(getCity).pipe(getFirstLetter)(employee), - ).not.toThrow(); - }); - - it('does not throw when a later getter is used with `.optional()` and some value in the chain is undefined', () => { - const baseGetter = getAddress.pipe(getCity).pipe(getFirstLetter); - - // Before testing that it _doesn't_ throw with `.optional()`, first make - // sure that it _does_ throw without it, to ensure that this test is - // valid. - expect(() => baseGetter(employee)).toThrow(); - expect(() => baseGetter.optional()(employee)).not.toThrow(); - }); - - it('does throw when used without `.optional()` and some value in the chain is undefined', () => { - expect(() => getAddress.pipe(getCity).pipe(getFirstLetter)(employee)).toThrow(); - }); + it("doesn't throw on undefined when optional", () => { + expect(getStateFromPerson.optional(undefined)).toBeUndefined(); + expect(getStateFromPerson.optional(alice)).toBe('California'); + expect(getStateFromPerson.optional(bob)).toBeUndefined(); }); }); - // Type assertions - these will be run at build time, rather than at test - // time. - assertType(getAddress.pipe(getCity)(employee)); - // @ts-expect-error - Assert that `string` on its own is incorrect for an - // optional getter -- it should be `string | undefined`. - assertType(getAddress.pipe(getCity).optional()(employee)); - assertType(getAddress.pipe(getCity).optional()(employee)); + describe('longer chains', () => { + const getSecondLetter = createGetter((s?: string) => s?.[1]); + it('applies optional to the chain', () => { + const notOptionalStateLetter = getAddressFromPerson + .pipe(getStateFromAddress) + .pipe(getSecondLetter); + expect(notOptionalStateLetter(alice)).toBe('a'); + expect(() => notOptionalStateLetter(bob)).toThrow(); + expect(() => notOptionalStateLetter(charlie)).toThrow(); + + const notOptionalCityLetter = getAddressFromPerson + .pipe(getCityFromAddress) + .pipe(getSecondLetter); + expect(notOptionalCityLetter(alice)).toBe('a'); + expect(notOptionalCityLetter(bob)).toBe('e'); + expect(() => notOptionalCityLetter(charlie)).toThrow(); + + const midOptionalStateLetter = getAddressFromPerson // address is required + .pipe(getStateFromAddress) // state is optional, turning the whole chain optional + .optional.pipe(getSecondLetter); // letter is required + expect(midOptionalStateLetter(alice)).toBe('a'); + expect(midOptionalStateLetter(bob)).toBeUndefined(); + expect(midOptionalStateLetter(charlie)).toBeUndefined(); + + const midOptionalCityLetter = getAddressFromPerson // address is required + .pipe(getCityFromAddress) // city is optional, turning the whole chain optional + .optional.pipe(getSecondLetter); // letter is required + expect(midOptionalCityLetter(alice)).toBe('a'); + expect(midOptionalCityLetter(bob)).toBe('e'); + expect(midOptionalCityLetter(charlie)).toBeUndefined(); + + const firstOptionalStateLetter = getAddressFromPerson.optional // address is optional, turning the whole chain optional + .pipe(getStateFromAddress) // state is required + .pipe(getSecondLetter); // letter is required + expect(firstOptionalStateLetter(alice)).toBe('a'); + expect(firstOptionalStateLetter(bob)).toBeUndefined(); + expect(firstOptionalStateLetter(charlie)).toBeUndefined(); + + const firstOptionalCityLetter = getAddressFromPerson.optional // address is optional, turning the whole chain optional + .pipe(getCityFromAddress) // city is required + .pipe(getSecondLetter); // letter is required + expect(firstOptionalCityLetter(alice)).toBe('a'); + expect(firstOptionalCityLetter(bob)).toBe('e'); + expect(firstOptionalCityLetter(charlie)).toBeUndefined(); + }); + + it('applies required to the chain', () => { + const allOptionalStateLetter = getAddressFromPerson.optional + .pipe(getStateFromAddress) + .optional.pipe(getSecondLetter).optional; + expect(allOptionalStateLetter.required(alice)).toBe('a'); + expect(() => allOptionalStateLetter.required(bob)).toThrow(); + expect(() => allOptionalStateLetter.required(charlie)).toThrow(); + + const allOptionalCityLetter = getAddressFromPerson.optional + .pipe(getCityFromAddress) + .optional.pipe(getSecondLetter).optional; + expect(allOptionalCityLetter.required(alice)).toBe('a'); + expect(allOptionalCityLetter.required(bob)).toBe('e'); + expect(() => allOptionalCityLetter.required(charlie)).toThrow(); + }); + }); }); diff --git a/packages/getters/src/utils/create-getter.ts b/packages/getters/src/utils/create-getter.ts index b376262dc2..a2d8cad6fd 100644 --- a/packages/getters/src/utils/create-getter.ts +++ b/packages/getters/src/utils/create-getter.ts @@ -1,47 +1,101 @@ import { Getter } from './getter.js'; import { GetterMissingValueError } from './getter-missing-value-error.js'; -export const createGetter = ( - getterFunction: (value: SourceType | undefined) => TargetType | undefined, - optional?: Optional, -): Getter => { - const getter: Getter = value => { - const result = getterFunction(value); - if (result === undefined && !optional) { - const errorMessage = `Failed to extract from ${JSON.stringify(value)}`; - throw new GetterMissingValueError(errorMessage); - } - return result as Optional extends true ? TargetType | undefined : TargetType; +const createPiper = + (firstSelector: (s?: PipeSourceType) => IntermediateType) => + ( + secondSelector: (i?: IntermediateType) => PipeTargetType, + ): Getter => { + const pipedFn = (source?: PipeSourceType) => { + const intermediate: IntermediateType = firstSelector(source); + const target: PipeTargetType = secondSelector(intermediate); + return target; + }; + + const pipedGetter = Object.defineProperties(pipedFn, { + pipe: { + enumerable: true, + get: () => createPiper(pipedFn), + }, + optional: { + enumerable: true, + get: () => createOptional(pipedFn), + }, + required: { + enumerable: true, + get: () => createRequired(pipedFn), + }, + }) as Getter; + + return pipedGetter; }; - getter.optional = () => - createGetter(value => { - try { - return getterFunction(value); - } catch (e) { - if (e instanceof GetterMissingValueError) { - return undefined; - } else { - throw e; - } - } - }, true); - - getter.pipe = ( - next: Getter, - ) => { - return createGetter(value => { - try { - return next(getterFunction(value)); - } catch (e) { - if (!optional || !(e instanceof GetterMissingValueError)) { - throw e; - } else { - return undefined; - } +const createOptional = ( + selector: (v?: SourceType) => TargetType | undefined, +): Getter => { + const optionalFn = (source?: SourceType) => { + try { + return selector(source); + } catch (e) { + if (e instanceof GetterMissingValueError) { + return undefined; } - }, optional); + throw e; + } }; - return getter; + const optionalGetter = Object.defineProperties(optionalFn, { + pipe: { + enumerable: true, + value: (nextSelector: (i?: TargetType) => NextTargetType) => { + return createPiper(optionalFn)(nextSelector).optional; + }, + }, + required: { + enumerable: true, + get: () => createRequired(selector), + }, + }) as Getter; + + Object.defineProperty(optionalGetter, 'optional', { + enumerable: true, + get: () => optionalGetter, + }); + + return optionalGetter; }; + +const createRequired = ( + selector: (v?: SourceType) => TargetType | undefined, +): Getter> => { + const requiredFn = (source?: SourceType) => { + const required = selector(source); + if (required == null) { + throw new GetterMissingValueError( + `Failed to select value from "${String(source)}" with "${selector.name}"`, + { cause: { source, selector } } satisfies ErrorOptions, + ); + } + return required; + }; + + const requiredGetter = Object.defineProperties(requiredFn, { + pipe: { + enumerable: true, + get: () => createPiper(requiredFn), + }, + optional: { + enumerable: true, + get: () => createOptional(selector), + }, + }) as Getter>; + + Object.defineProperty(requiredGetter, 'required', { + enumerable: true, + get: () => requiredGetter, + }); + + return requiredGetter; +}; + +export { createRequired as createGetter }; diff --git a/packages/getters/src/utils/getter-missing-value-error.ts b/packages/getters/src/utils/getter-missing-value-error.ts index 755f855d4f..77a94480a3 100644 --- a/packages/getters/src/utils/getter-missing-value-error.ts +++ b/packages/getters/src/utils/getter-missing-value-error.ts @@ -1,9 +1,11 @@ /** - * This error will be thrown when a getter that hasn't been marked `.optional()` - * returns `undefined`. You can import this error class in your code to - * differentiate between this specific type of error and others. (If you want to - * catch this error just to make a getter optional, though, it's easier to just - * call `.optional()` on the getter first: - * `getAddressIndex.optional()(addressView)`.) + * This error will be thrown when a getter that isn't called `optional` returns + * `undefined`. You can import this error class in your code to differentiate + * between this specific type of error and others. + * + * If you want to catch this error just to suppress it, it's easier to just call + * the getter as `optional` instead. + * + * `getAddressIndex.optional(addressView)`.) */ export class GetterMissingValueError extends Error {} diff --git a/packages/getters/src/utils/getter.ts b/packages/getters/src/utils/getter.ts index bc49b4ad6c..d07096c9e4 100644 --- a/packages/getters/src/utils/getter.ts +++ b/packages/getters/src/utils/getter.ts @@ -1,36 +1,46 @@ -/** - * Given a value of type `SourceType`, returns a (possibly nested) property of - * that value, of type `TargetType`. If `Optional` is `true`, returns - * undefined if the property or an ancestor is undefined; if `false`, throws - * when the property or an ancestor is undefined. - */ -type GetterFunction = ( - value: SourceType | undefined, -) => Optional extends true ? TargetType | undefined : TargetType; +export interface Getter { + /** + * Given an input value of `SourceType`, asserts successful retrieval of a + * value of `TargetType`, by the naive retrieval function passed to + * `createGetter`. + * + * If undefined access occurs while retrieving `TargetType`, a + * `GetterMissingValueError` is thrown. + */ + (value?: SourceType): TargetType; -interface GetterMethods { /** - * Returns a getter that, when given a value of type `SourceType`, returns a - * (possibly nested) property of that value, of type `TargetType`. If the - * property or any of its ancestors are undefined, returned undefined. + * Every getter contains an `optional` getter, which adds `undefined` to the + * target type, and which will not throw `GetterMissingValueError`. * * @example * ```ts - * const getMetadata = createGetter(valueView => + * const getMetadataFromValueView = createGetter(valueView => * valueView?.valueView.case === 'knownAssetId' ? valueView.valueView.value.metadata : undefined, * ); * - * // Note that `valueView` has no metadata, nor even a `case`. - * const valueView = new ValueView(); + * // Note that this `emptyValueView` has no metadata, nor even a `case`. + * const emptyValueView = new ValueView(); * * // Doesn't throw, even though the metadata is missing. - * const metadata = getMetadata.optional()(valueView); + * const noMetadata: Metadata | undefined = getMetadataFromValueView.optional(emptyValueView); * ``` */ - optional: () => Getter; + readonly optional: Getter; /** - * Pipes the output of this getter to another getter or getter function. + * Every getter contains a `required` getter, which wraps `NonNullable` around + * the target type, and which will throw `GetterMissingValueError` if the + * accessed result is `undefined`. + */ + readonly required: Getter>; + + /** + * Call `pipe` to create a getter for the return type of another getter or + * selector function provided as a parameter. Your parameter function must + * accept the output of this getter as its 0th input parameter. If the return + * type of your pipe parameter includes `undefined`, the return type of the + * created getter will also include undefined (it will be an optional getter). * * @example * ```ts @@ -38,19 +48,16 @@ interface GetterMethods( - pipedGetter: Getter, - ) => Getter; + readonly pipe: ( + pipeSelector: + | Getter + | ((value?: TargetType | NonNullable | undefined) => PipeTargetType), + ) => Getter< + SourceType, + TargetType extends undefined ? PipeTargetType | undefined : PipeTargetType + >; } - -export type Getter< - SourceType = unknown, - TargetType = unknown, - Optional extends boolean = false, -> = GetterFunction & - GetterMethods; diff --git a/packages/getters/src/validator-info.ts b/packages/getters/src/validator-info.ts index 512b7b3814..2744f36e72 100644 --- a/packages/getters/src/validator-info.ts +++ b/packages/getters/src/validator-info.ts @@ -26,10 +26,7 @@ export const getBondingStateEnumFromValidatorInfo = getStatus .pipe(getBondingState) .pipe(getBondingStateEnum); -export const getValidatorRewardRateFromValidatorInfoOptional = getStatus - .optional() - .pipe(getRateData) - .pipe(getValidatorRewardRate); +export const getValidatorRewardRateFromValidatorInfo = getRateData.pipe(getValidatorRewardRate); export const getFundingStreamsFromValidatorInfo = getValidator.pipe(getFundingStreams); diff --git a/packages/getters/src/value-view.ts b/packages/getters/src/value-view.ts index 574c41d632..affd3c7644 100644 --- a/packages/getters/src/value-view.ts +++ b/packages/getters/src/value-view.ts @@ -22,15 +22,19 @@ export const getEquivalentValues = createGetter((valueView?: ValueView) => : undefined, ); -const getValidatorInfo = createGetter((any?: Any) => - any ? ValidatorInfo.fromBinary(any.value) : undefined, -); - /** * Only to be used on `ValueView`s that contain delegation tokens -- and thus, * validator infos. */ -export const getValidatorInfoFromValueView = getExtendedMetadata.pipe(getValidatorInfo); +export const getValidatorInfoFromValueView = getExtendedMetadata.pipe( + createGetter((a?: Any) => { + const validatorInfo = new ValidatorInfo(); + if (a?.unpackTo(validatorInfo)) { + return validatorInfo; + } + return undefined; + }), +); /** * Only to be used on `ValueView`s that contain delegation tokens -- and thus, @@ -42,7 +46,7 @@ export const getValidatorIdentityKeyFromValueView = getValidatorInfoFromValueVie export const getDisplayDenomExponentFromValueView = createGetter((valueView?: ValueView) => valueView?.valueView.case === 'knownAssetId' - ? getDisplayDenomExponent.optional()(valueView.valueView.value.metadata) + ? getDisplayDenomExponent(valueView.valueView.value.metadata) : undefined, ); @@ -61,10 +65,7 @@ export const getAmount = createGetter( (valueView?: ValueView) => valueView?.valueView.value?.amount, ); -export const getSymbolFromValueView = createGetter((valueView?: ValueView) => { - const metadata = getMetadata.optional()(valueView); - return getSymbol.optional()(metadata); -}); +export const getSymbolFromValueView = getMetadata.pipe(getSymbol); export const getDisplayDenomFromView = createGetter((view?: ValueView) => { if (view?.valueView.case === 'unknownAssetId') { diff --git a/packages/perspective/src/plan/view-action-plan.ts b/packages/perspective/src/plan/view-action-plan.ts index be136dad31..e0906f7d4d 100644 --- a/packages/perspective/src/plan/view-action-plan.ts +++ b/packages/perspective/src/plan/view-action-plan.ts @@ -324,8 +324,8 @@ export const viewActionPlan = }); case 'actionDutchAuctionSchedule': { - const inputAssetId = getInputAssetId.optional()(actionPlan.action.value.description); - const outputAssetId = getOutputAssetId.optional()(actionPlan.action.value.description); + const inputAssetId = getInputAssetId.optional(actionPlan.action.value.description); + const outputAssetId = getOutputAssetId.optional(actionPlan.action.value.description); const [inputMetadata, outputMetadata] = await Promise.all([ inputAssetId ? await denomMetadataByAssetId(inputAssetId) : undefined, outputAssetId ? await denomMetadataByAssetId(outputAssetId) : undefined, diff --git a/packages/services/src/view-service/balances.ts b/packages/services/src/view-service/balances.ts index 3e8942a693..0878c47527 100644 --- a/packages/services/src/view-service/balances.ts +++ b/packages/services/src/view-service/balances.ts @@ -157,7 +157,7 @@ class BalancesAggregator { * of the equivalent value and the `amount` of the `ValueView`. */ private async aggregateEquivalentValues(valueView: ValueView, toAdd: SpendableNoteRecord) { - const assetId = getAssetIdFromRecord.optional()(toAdd); + const assetId = getAssetIdFromRecord.optional(toAdd); if (!assetId?.inner) { return; } diff --git a/packages/services/src/view-service/unbonding-tokens-by-address-index/helpers.ts b/packages/services/src/view-service/unbonding-tokens-by-address-index/helpers.ts index a0f28e7b82..0e1bb2f121 100644 --- a/packages/services/src/view-service/unbonding-tokens-by-address-index/helpers.ts +++ b/packages/services/src/view-service/unbonding-tokens-by-address-index/helpers.ts @@ -11,7 +11,7 @@ import { status } from '../status.js'; import { appParameters } from '../app-parameters.js'; export const isUnbondingTokenBalance = (balancesResponse: PartialMessage) => { - const display = getDisplayFromBalancesResponse(new BalancesResponse(balancesResponse)); + const display = getDisplayFromBalancesResponse.optional(new BalancesResponse(balancesResponse)); return display ? assetPatterns.unbondingToken.matches(display) : false; }; @@ -40,7 +40,7 @@ export const getIsClaimable = async ( return false; } - const display = getDisplayFromBalancesResponse(new BalancesResponse(balancesResponse)); + const display = getDisplayFromBalancesResponse.optional(new BalancesResponse(balancesResponse)); if (!display) { return false; } diff --git a/packages/services/src/view-service/unbonding-tokens-by-address-index/index.ts b/packages/services/src/view-service/unbonding-tokens-by-address-index/index.ts index af61c9c6b2..6da3b4032c 100644 --- a/packages/services/src/view-service/unbonding-tokens-by-address-index/index.ts +++ b/packages/services/src/view-service/unbonding-tokens-by-address-index/index.ts @@ -44,7 +44,7 @@ export const unbondingTokensByAddressIndex: Impl['unbondingTokensByAddressIndex' } const regexResult = assetPatterns.unbondingToken.capture( - getDisplayFromBalancesResponse(new BalancesResponse(balancesResponse)) ?? '', + getDisplayFromBalancesResponse.optional(new BalancesResponse(balancesResponse)) ?? '', ); if (!regexResult) { throw new Error('expected delegation token identity key not present'); diff --git a/packages/types/src/swap.ts b/packages/types/src/swap.ts index 1be3bd2608..a39765e7be 100644 --- a/packages/types/src/swap.ts +++ b/packages/types/src/swap.ts @@ -35,8 +35,8 @@ const getUnfilledAmount = (swapView: SwapView): ValueView | undefined => { const delta1I = getDelta1IFromSwapView(swapView); const delta2I = getDelta2IFromSwapView(swapView); - const output1Value = getOutput1Value.optional()(swapView); - const output2Value = getOutput2Value.optional()(swapView); + const output1Value = getOutput1Value.optional(swapView); + const output2Value = getOutput2Value.optional(swapView); const is1To2Swap = isZero(delta2I); const is2To1Swap = isZero(delta1I); @@ -69,8 +69,8 @@ export const getOneWaySwapValues = ( ); } - const output1 = getOutput1Value.optional()(swapView); - const output2 = getOutput2Value.optional()(swapView); + const output1 = getOutput1Value.optional(swapView); + const output2 = getOutput2Value.optional(swapView); const delta1I = getDelta1IFromSwapView(swapView); const delta2I = getDelta2IFromSwapView(swapView); diff --git a/packages/types/src/value-view.ts b/packages/types/src/value-view.ts index 1e31625897..28e257eb7b 100644 --- a/packages/types/src/value-view.ts +++ b/packages/types/src/value-view.ts @@ -13,7 +13,7 @@ export const getFormattedAmtFromValueView = (v: ValueView, commas = false): stri if (v.valueView.case === 'knownAssetId' && v.valueView.value.metadata) { const { amount = new Amount(), metadata } = v.valueView.value; - const exponent = getDisplayDenomExponent.optional()(metadata); + const exponent = getDisplayDenomExponent.optional(metadata); return formatAmount({ amount, exponent, commas }); } else { const { amount = new Amount() } = v.valueView.value; diff --git a/packages/ui/components/ui/asset-icon/index.tsx b/packages/ui/components/ui/asset-icon/index.tsx index ad35fbb2d5..a6b10e763a 100644 --- a/packages/ui/components/ui/asset-icon/index.tsx +++ b/packages/ui/components/ui/asset-icon/index.tsx @@ -21,7 +21,7 @@ export const AssetIcon = ({ size === 'sm' && 'size-6', size === 'lg' && 'size-12', ); - const display = getDisplay.optional()(metadata); + const display = getDisplay.optional(metadata); const isDelegationToken = display ? assetPatterns.delegationToken.matches(display) : false; const isUnbondingToken = display ? assetPatterns.unbondingToken.matches(display) : false; diff --git a/packages/ui/components/ui/balance-value-view/index.tsx b/packages/ui/components/ui/balance-value-view/index.tsx index 53cb0157b2..69e6092a48 100644 --- a/packages/ui/components/ui/balance-value-view/index.tsx +++ b/packages/ui/components/ui/balance-value-view/index.tsx @@ -18,8 +18,8 @@ export const BalanceValueView = ({ error?: boolean; onClick?: (valueView: ValueView) => void; }) => { - const exponent = getDisplayDenomExponentFromValueView.optional()(valueView); - const amount = getAmount.optional()(valueView) ?? new Amount({ hi: 0n, lo: 0n }); + const exponent = getDisplayDenomExponentFromValueView.optional(valueView); + const amount = getAmount.optional(valueView) ?? new Amount({ hi: 0n, lo: 0n }); const formattedAmount = formatAmount({ amount, exponent, commas: true }); return ( diff --git a/packages/ui/components/ui/dutch-auction-component/expanded-details/get-price.ts b/packages/ui/components/ui/dutch-auction-component/expanded-details/get-price.ts index de8cbd8214..7405a591d4 100644 --- a/packages/ui/components/ui/dutch-auction-component/expanded-details/get-price.ts +++ b/packages/ui/components/ui/dutch-auction-component/expanded-details/get-price.ts @@ -46,7 +46,7 @@ export const getPrice = ( // The input, scaled up by `step_count` to match. const inputScaled = (stepCount - 1n) * input; - const inputDisplayDenomExponent = getDisplayDenomExponent.optional()(inputMetadata); + const inputDisplayDenomExponent = getDisplayDenomExponent.optional(inputMetadata); const multiplier = 10 ** (inputDisplayDenomExponent ?? 0); const price = Math.round((Number(targetOutputScaled) / Number(inputScaled)) * multiplier); diff --git a/packages/ui/components/ui/dutch-auction-component/expanded-details/index.tsx b/packages/ui/components/ui/dutch-auction-component/expanded-details/index.tsx index caac99a5b5..5921a7b53c 100644 --- a/packages/ui/components/ui/dutch-auction-component/expanded-details/index.tsx +++ b/packages/ui/components/ui/dutch-auction-component/expanded-details/index.tsx @@ -45,8 +45,8 @@ export const ExpandedDetails = ({ fullSyncHeight >= description.startHeight && fullSyncHeight <= description.endHeight; - const inputExponent = getDisplayDenomExponent.optional()(inputMetadata); - const outputExponent = getDisplayDenomExponent.optional()(outputMetadata); + const inputExponent = getDisplayDenomExponent.optional(inputMetadata); + const outputExponent = getDisplayDenomExponent.optional(outputMetadata); return (
diff --git a/packages/ui/components/ui/tx/actions-views/delegator-vote.tsx b/packages/ui/components/ui/tx/actions-views/delegator-vote.tsx index c0912ab6ca..de2b6ade38 100644 --- a/packages/ui/components/ui/tx/actions-views/delegator-vote.tsx +++ b/packages/ui/components/ui/tx/actions-views/delegator-vote.tsx @@ -54,11 +54,11 @@ const umValueView = (amount?: Amount) => { }; export const DelegatorVoteComponent = ({ value }: { value: DelegatorVoteView }) => { - const body = getDelegatorVoteBody.optional()(value); + const body = getDelegatorVoteBody.optional(value); if (value.delegatorVote.case === 'visible') { const note = value.delegatorVote.value.note; - const address = getAddress.optional()(note); + const address = getAddress.optional(note); return ( { if (value.swapView.case === 'visible') { - const claimTx = getClaimTx.optional()(value); - const addressView = getAddressView.optional()(value); + const claimTx = getClaimTx.optional(value); + const addressView = getAddressView.optional(value); const oneWaySwap = isOneWaySwap(value) ? getOneWaySwapValues(value) : undefined; // The 'Fee' protobuf definition does not include assetMetadata. diff --git a/packages/ui/components/ui/tx/actions-views/swap/one-way-swap.tsx b/packages/ui/components/ui/tx/actions-views/swap/one-way-swap.tsx index c7aaf3b1a0..6fa0c7f7d1 100644 --- a/packages/ui/components/ui/tx/actions-views/swap/one-way-swap.tsx +++ b/packages/ui/components/ui/tx/actions-views/swap/one-way-swap.tsx @@ -10,7 +10,7 @@ import { getAmount } from '@penumbra-zone/getters/value-view'; * 1.23INPUT -> 4.56OUTPUT */ export const OneWaySwap = ({ input, output }: { input: ValueView; output: ValueView }) => { - const outputAmount = getAmount.optional()(output); + const outputAmount = getAmount.optional(output); return (
diff --git a/packages/ui/components/ui/tx/actions-views/swap/swap-claim.tsx b/packages/ui/components/ui/tx/actions-views/swap/swap-claim.tsx index c36d3a58ed..749d109ca3 100644 --- a/packages/ui/components/ui/tx/actions-views/swap/swap-claim.tsx +++ b/packages/ui/components/ui/tx/actions-views/swap/swap-claim.tsx @@ -3,8 +3,8 @@ import { SwapClaimView } from '@penumbra-zone/protobuf/penumbra/core/component/d import { TransactionIdComponent } from './transaction-id'; import { ActionDetails } from '../action-details'; import { - getOutput1ValueOptional, - getOutput2ValueOptional, + getOutput1Value, + getOutput2Value, getSwapClaimFee, } from '@penumbra-zone/getters/swap-claim-view'; import { getAmount } from '@penumbra-zone/getters/value-view'; @@ -30,10 +30,10 @@ const getClaimLabel = ( export const SwapClaimViewComponent = ({ value }: { value: SwapClaimView }) => { if (value.swapClaimView.case === 'visible') { const swapTxId = value.swapClaimView.value.swapTx; - const output1Value = getOutput1ValueOptional(value); - const output2Value = getOutput2ValueOptional(value); - const output1Amount = getAmount.optional()(output1Value); - const output2Amount = getAmount.optional()(output2Value); + const output1Value = getOutput1Value.optional(value); + const output2Value = getOutput2Value.optional(value); + const output1Amount = getAmount.optional(output1Value); + const output2Amount = getAmount.optional(output2Value); const claimLabel = getClaimLabel(output1Amount, output2Amount); return ( diff --git a/packages/ui/src/AddressViewComponent/index.tsx b/packages/ui/src/AddressViewComponent/index.tsx index 967cde6b2c..bd3e8679f3 100644 --- a/packages/ui/src/AddressViewComponent/index.tsx +++ b/packages/ui/src/AddressViewComponent/index.tsx @@ -25,7 +25,7 @@ export const AddressViewComponent = ({ addressView, copyable = true }: AddressVi return null; } - const addressIndex = getAddressIndex.optional()(addressView); + const addressIndex = getAddressIndex.optional(addressView); // a randomized index has nonzero randomizer bytes const isRandomized = addressIndex?.randomizer.some(v => v); diff --git a/packages/ui/src/AssetIcon/index.tsx b/packages/ui/src/AssetIcon/index.tsx index d4f87c27e4..515593a94e 100644 --- a/packages/ui/src/AssetIcon/index.tsx +++ b/packages/ui/src/AssetIcon/index.tsx @@ -39,7 +39,7 @@ export interface AssetIconProps { export const AssetIcon = ({ metadata, size = 'md' }: AssetIconProps) => { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- possibly empty string const icon = metadata?.images[0]?.png || metadata?.images[0]?.svg; - const display = getDisplay.optional()(metadata); + const display = getDisplay.optional(metadata); const isDelegationToken = display ? assetPatterns.delegationToken.matches(display) : false; const isUnbondingToken = display ? assetPatterns.unbondingToken.matches(display) : false; diff --git a/packages/ui/src/AssetSelector/AssetSelectorDialogContent/MetadataOrBalancesResponse/Balance.tsx b/packages/ui/src/AssetSelector/AssetSelectorDialogContent/MetadataOrBalancesResponse/Balance.tsx index 1309f75436..55129b7a97 100644 --- a/packages/ui/src/AssetSelector/AssetSelectorDialogContent/MetadataOrBalancesResponse/Balance.tsx +++ b/packages/ui/src/AssetSelector/AssetSelectorDialogContent/MetadataOrBalancesResponse/Balance.tsx @@ -15,8 +15,8 @@ export interface BalanceProps { } export const Balance = ({ balancesResponse }: BalanceProps) => { - const addressIndexAccount = getAddressIndex.optional()(balancesResponse)?.account; - const valueView = getBalanceView.optional()(balancesResponse); + const addressIndexAccount = getAddressIndex.optional(balancesResponse)?.account; + const valueView = getBalanceView.optional(balancesResponse); return ( {valueView && {getFormattedAmtFromValueView(valueView, true)}} diff --git a/packages/ui/src/AssetSelector/AssetSelectorDialogContent/MetadataOrBalancesResponse/index.tsx b/packages/ui/src/AssetSelector/AssetSelectorDialogContent/MetadataOrBalancesResponse/index.tsx index 38d18a0439..007c647fc4 100644 --- a/packages/ui/src/AssetSelector/AssetSelectorDialogContent/MetadataOrBalancesResponse/index.tsx +++ b/packages/ui/src/AssetSelector/AssetSelectorDialogContent/MetadataOrBalancesResponse/index.tsx @@ -44,7 +44,7 @@ export const MetadataOrBalancesResponse = ({ isSelected, onSelect, }: MetadataOrBalancesResponseProps) => { - const metadata = isMetadata(value) ? value : getMetadataFromBalancesResponse.optional()(value); + const metadata = isMetadata(value) ? value : getMetadataFromBalancesResponse.optional(value); const isParentAnimating = useIsAnimating(); const [scope, animate] = useAnimate(); const animationControls = useRef(); diff --git a/packages/ui/src/AssetSelector/index.tsx b/packages/ui/src/AssetSelector/index.tsx index e783f76fe6..76e7efc658 100644 --- a/packages/ui/src/AssetSelector/index.tsx +++ b/packages/ui/src/AssetSelector/index.tsx @@ -60,7 +60,7 @@ export const AssetSelector = ) => { const layoutId = useId(); const density = useDensity(); - const metadata = isMetadata(value) ? value : getMetadataFromBalancesResponse.optional()(value); + const metadata = isMetadata(value) ? value : getMetadataFromBalancesResponse.optional(value); const [isOpen, setIsOpen] = useState(false); diff --git a/packages/ui/src/ValueViewComponent/index.tsx b/packages/ui/src/ValueViewComponent/index.tsx index 1292f580c0..2d1a2f5492 100644 --- a/packages/ui/src/ValueViewComponent/index.tsx +++ b/packages/ui/src/ValueViewComponent/index.tsx @@ -99,7 +99,7 @@ export const ValueViewComponent = ( } const formattedAmount = getFormattedAmtFromValueView(valueView, true); - const metadata = getMetadata.optional()(valueView); + const metadata = getMetadata.optional(valueView); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- possibly empty string const symbol = metadata?.symbol || 'Unknown'; diff --git a/packages/ui/src/WalletBalance/index.tsx b/packages/ui/src/WalletBalance/index.tsx index bb79916f53..58ea2564ed 100644 --- a/packages/ui/src/WalletBalance/index.tsx +++ b/packages/ui/src/WalletBalance/index.tsx @@ -99,9 +99,9 @@ export const WalletBalance = ({ disabled, onClick, }: WalletBalanceProps) => { - const account = getAddressIndex.optional()(balance); - const valueView = getBalanceView.optional()(balance); - const metadata = getMetadataFromBalancesResponse.optional()(balance); + const account = getAddressIndex.optional(balance); + const valueView = getBalanceView.optional(balance); + const metadata = getMetadataFromBalancesResponse.optional(balance); if (!valueView || !account || !metadata) { return null; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 44fb6188dc..1fb370fe78 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -1,19 +1,10 @@ { "compilerOptions": { "target": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "moduleResolution": "bundler", + "lib": ["ESNext", "DOM", "DOM.Iterable"], "exactOptionalPropertyTypes": false, - "allowImportingTsExtensions": true, - "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "declaration": true + "composite": true }, "extends": ["@tsconfig/strictest/tsconfig.json", "@tsconfig/vite-react/tsconfig.json"], "include": ["components", "lib", "src", "tests-setup.ts"]