Skip to content

Commit

Permalink
Create ZQuery package for managing data in Zustand (#1199)
Browse files Browse the repository at this point in the history
* Create ZQuery lib

* Remove unused code

* Tweak syntax and fix performance

* Refactor a little

* Change slice name

* Fix API to no longer use useStore

* Simplify a bit

* Make it possible to pass args to fetch()

* Remove fetch args; go back to exposing hooks

* Fix hooks

* Use ZQuery for fetching unclaimed swaps

* Coerce types for now

* Rework a bit to simplify how the store is used

* Remove unnecessary passing around of revalidate() function

* Document the parameters

* Add some spacing

* Reorder destructuring

* Add a ton more docs

* Clarify docs a bit

* Rename generic variable

* Add more docs and accommodate more store typings

* Take advantage of typings fixes

* Move ZQuery to a package

* fix type of use hook

* Sort unclaimed swaps in fetcher; simplify component

* Export the type

* Install vitest

* Write first tests

* Significantly refactor ZQuery to handle streaming responses

* Fix typing issues

* Fix tests

* Fix remaining type issues

* Use updated ZQuery APi in UnclaimedSwaps and DutchAuction slices

* Remove dupe code

* Fix type

* Clean up a bit; add explanatory comment

* Remove duplicate type

* Fix import

* Support fetch args

* Fix complaint about non-extensible data object

* Use useAuctionInfos

* Fix types again

* Fix auction metadata

* Fix QueryLatestStateButton

* Remove unused DataTypeInState type

* Require stream to be a function that returns the desired type

* Fix type

* Fix type

* Handle errors in the stream

* Fix circular reference issue

* Convert status slice to use ZQuery

* Simplify type

* Fix import

* Fix more type issues; support referencing counting

* Rename var

* Stop streaming results if aborted

* Update docs

* Add comment

* Use correct source for latestKnownBlockHeight in TokenSwapInput

* Remove unused abortController

* Rename fetch types

* Remove unneeded named types

* Remove fetch options for now

* Remove unnecessary optional checks

* Update doc

* Make exponents optional

* Remove outdated comment

* Add changeset
  • Loading branch information
jessepinho authored Jun 12, 2024
1 parent adf3a28 commit 6b06e04
Show file tree
Hide file tree
Showing 29 changed files with 893 additions and 326 deletions.
8 changes: 8 additions & 0 deletions .changeset/ninety-pandas-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@penumbra-zone/zquery': major
'@penumbra-zone/getters': minor
'minifront': patch
'@penumbra-zone/ui': patch
---

Introduce ZQuery package and use throughout minifront
1 change: 1 addition & 0 deletions apps/minifront/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@penumbra-zone/transport-dom": "workspace:*",
"@penumbra-zone/types": "workspace:*",
"@penumbra-zone/ui": "workspace:*",
"@penumbra-zone/zquery": "workspace:*",
"@radix-ui/react-icons": "^1.3.0",
"@tanstack/react-query": "4.36.1",
"bech32": "^2.0.0",
Expand Down
14 changes: 0 additions & 14 deletions apps/minifront/src/components/Status.tsx

This file was deleted.

18 changes: 6 additions & 12 deletions apps/minifront/src/components/header/sync-status-section.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
import { CondensedBlockSyncStatus } from '@penumbra-zone/ui/components/ui/block-sync-status/condensed';
import { AllSlices } from '../../state';
import { useStoreShallow } from '../../utils/use-store-shallow';

const syncStatusSectionSelector = (state: AllSlices) => ({
fullSyncHeight: state.status.fullSyncHeight,
latestKnownBlockHeight: state.status.latestKnownBlockHeight,
error: state.status.error,
});
import { useStatus } from '../../state/status';

export const SyncStatusSection = () => {
const { fullSyncHeight, latestKnownBlockHeight, error } =
useStoreShallow(syncStatusSectionSelector);
const { data, error } = useStatus();

return (
<div className='relative z-30 flex w-full flex-col'>
<CondensedBlockSyncStatus
fullSyncHeight={fullSyncHeight ? Number(fullSyncHeight) : undefined}
latestKnownBlockHeight={latestKnownBlockHeight ? Number(latestKnownBlockHeight) : undefined}
fullSyncHeight={data?.fullSyncHeight ? Number(data.fullSyncHeight) : undefined}
latestKnownBlockHeight={
data?.latestKnownBlockHeight ? Number(data.latestKnownBlockHeight) : undefined
}
error={error}
/>
</div>
Expand Down
2 changes: 0 additions & 2 deletions apps/minifront/src/components/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import '@penumbra-zone/ui/styles/globals.css';
import { getChainId } from '../fetchers/chain-id';
import { useEffect, useState } from 'react';
import { TestnetBanner } from '@penumbra-zone/ui/components/ui/testnet-banner';
import { Status } from './Status';
import { MotionConfig } from 'framer-motion';

export const Layout = () => {
Expand All @@ -19,7 +18,6 @@ export const Layout = () => {

return (
<MotionConfig transition={{ duration: 0.1 }}>
<Status />
<TestnetBanner chainId={chainId} />
<HeadTag />
<div className='flex min-h-screen w-full flex-col'>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { describe, expect, it } from 'vitest';
import { getFilteredAuctionInfos } from './get-filtered-auction-infos';
import { AuctionInfo } from '../../../state/swap/dutch-auction';
import {
AuctionId,
DutchAuction,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/auction/v1/auction_pb';
import { AuctionInfo } from '../../../fetchers/auction-infos';

const MOCK_AUCTION_1 = new DutchAuction({
description: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AuctionInfo, Filter } from '../../../state/swap/dutch-auction';
import { AuctionInfo } from '../../../fetchers/auction-infos';
import { Filter } from '../../../state/swap/dutch-auction';

type FilterMatchableAuctionInfo = AuctionInfo & {
auction: {
Expand Down
17 changes: 2 additions & 15 deletions apps/minifront/src/components/swap/auction-list/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import {
AssetId,
Metadata,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { AuctionInfo, Filter } from '../../../state/swap/dutch-auction';
import { bech32mAssetId } from '@penumbra-zone/bech32m/passet';
import { AuctionInfo } from '../../../fetchers/auction-infos';
import { Filter } from '../../../state/swap/dutch-auction';

const byStartHeight =
(direction: 'ascending' | 'descending') => (a: AuctionInfo, b: AuctionInfo) => {
Expand All @@ -19,12 +15,3 @@ export const SORT_FUNCTIONS: Record<Filter, (a: AuctionInfo, b: AuctionInfo) =>
active: byStartHeight('descending'),
upcoming: byStartHeight('ascending'),
};

export const getMetadata = (metadataByAssetId: Record<string, Metadata>, assetId?: AssetId) => {
let metadata: Metadata | undefined;
if (assetId && (metadata = metadataByAssetId[bech32mAssetId(assetId)])) {
return metadata;
}

return new Metadata({ penumbraAssetId: assetId });
};
37 changes: 12 additions & 25 deletions apps/minifront/src/components/swap/auction-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ import { SegmentedPicker } from '@penumbra-zone/ui/components/ui/segmented-picke
import { useMemo } from 'react';
import { getFilteredAuctionInfos } from './get-filtered-auction-infos';
import { LayoutGroup, motion } from 'framer-motion';
import { SORT_FUNCTIONS, getMetadata } from './helpers';
import { SORT_FUNCTIONS } from './helpers';
import { useAuctionInfos } from '../../../state/swap/dutch-auction';
import { useStatus } from '../../../state/status';

const auctionListSelector = (state: AllSlices) => ({
auctionInfos: state.swap.dutchAuction.auctionInfos,
metadataByAssetId: state.swap.dutchAuction.metadataByAssetId,
fullSyncHeight: state.status.fullSyncHeight,
endAuction: state.swap.dutchAuction.endAuction,
withdraw: state.swap.dutchAuction.withdraw,
filter: state.swap.dutchAuction.filter,
Expand All @@ -39,22 +38,16 @@ const getButtonProps = (
};

export const AuctionList = () => {
const {
auctionInfos,
metadataByAssetId,
fullSyncHeight,
endAuction,
withdraw,
filter,
setFilter,
} = useStoreShallow(auctionListSelector);
const auctionInfos = useAuctionInfos();
const { endAuction, withdraw, filter, setFilter } = useStoreShallow(auctionListSelector);
const { data: status } = useStatus();

const filteredAuctionInfos = useMemo(
() =>
[...getFilteredAuctionInfos(auctionInfos, filter, fullSyncHeight)].sort(
[...getFilteredAuctionInfos(auctionInfos.data ?? [], filter, status?.fullSyncHeight)].sort(
SORT_FUNCTIONS[filter],
),
[auctionInfos, filter, fullSyncHeight],
[auctionInfos, filter, status?.fullSyncHeight],
);

return (
Expand All @@ -63,7 +56,7 @@ export const AuctionList = () => {
<GradientHeader layout>My Auctions</GradientHeader>

<motion.div layout className='flex items-center gap-2'>
{!!auctionInfos.length && <QueryLatestStateButton />}
{!!auctionInfos.data?.length && <QueryLatestStateButton />}

<SegmentedPicker
value={filter}
Expand Down Expand Up @@ -97,15 +90,9 @@ export const AuctionList = () => {
<DutchAuctionComponent
auctionId={auctionInfo.id}
dutchAuction={auctionInfo.auction}
inputMetadata={getMetadata(
metadataByAssetId,
auctionInfo.auction.description?.input?.assetId,
)}
outputMetadata={getMetadata(
metadataByAssetId,
auctionInfo.auction.description?.outputId,
)}
fullSyncHeight={fullSyncHeight}
inputMetadata={auctionInfo.inputMetadata}
outputMetadata={auctionInfo.outputMetadata}
fullSyncHeight={status?.fullSyncHeight}
{...getButtonProps(
auctionInfo.id,
endAuction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@ import {
TooltipTrigger,
} from '@penumbra-zone/ui/components/ui/tooltip';
import { ReloadIcon } from '@radix-ui/react-icons';
import { useStore } from '../../../state';
import { useRevalidateAuctionInfos } from '../../../state/swap/dutch-auction';

export const QueryLatestStateButton = () => {
const loadAuctionInfos = useStore(state => state.swap.dutchAuction.loadAuctionInfos);
const handleClick = () => void loadAuctionInfos(true);
const revalidate = useRevalidateAuctionInfos();

return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger
onClick={handleClick}
onClick={() => revalidate({ queryLatestState: true })}
aria-label='Get the current auction reserves (makes a request to a fullnode)'
>
<div className='p-2'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { AssetSelector } from '../../shared/asset-selector';
import BalanceSelector from '../../shared/balance-selector';
import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1/num_pb';
import { useStatus } from '../../../state/status';

const isValidAmount = (amount: string, assetIn?: BalancesResponse) =>
Number(amount) >= 0 && (!assetIn || !amountMoreThanBalance(assetIn, amount));
Expand Down Expand Up @@ -61,7 +62,6 @@ const tokenSwapInputSelector = (state: AllSlices) => ({
setAmount: state.swap.setAmount,
balancesResponses: state.swap.balancesResponses,
priceHistory: state.swap.priceHistory,
latestKnownBlockHeight: state.status.latestKnownBlockHeight,
assetOutBalance: assetOutBalanceSelector(state),
});

Expand All @@ -72,6 +72,8 @@ const tokenSwapInputSelector = (state: AllSlices) => ({
* amount.
*/
export const TokenSwapInput = () => {
const status = useStatus();
const latestKnownBlockHeight = status.data?.latestKnownBlockHeight ?? 0n;
const {
swappableAssets,
amount,
Expand All @@ -82,7 +84,6 @@ export const TokenSwapInput = () => {
setAssetOut,
balancesResponses,
priceHistory,
latestKnownBlockHeight = 0n,
assetOutBalance,
} = useStoreShallow(tokenSwapInputSelector);

Expand Down
40 changes: 3 additions & 37 deletions apps/minifront/src/components/swap/swap-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ import { useStore } from '../../state';
import { abortLoader } from '../../abort-loader';
import { SwapRecord } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';
import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { fetchUnclaimedSwaps } from '../../fetchers/unclaimed-swaps';
import { viewClient } from '../../clients';
import { getSwapAsset1, getSwapAsset2 } from '@penumbra-zone/getters/swap-record';
import { uint8ArrayToBase64 } from '@penumbra-zone/types/base64';
import { getSwappableBalancesResponses, isSwappable } from './helpers';
import { getAllAssets } from '../../fetchers/assets';

Expand All @@ -30,44 +26,14 @@ const getAndSetDefaultAssetBalances = async (swappableAssets: Metadata[]) => {
return balancesResponses;
};

const fetchMetadataForSwap = async (swap: SwapRecord): Promise<UnclaimedSwapsWithMetadata> => {
const assetId1 = getSwapAsset1(swap);
const assetId2 = getSwapAsset2(swap);

const [{ denomMetadata: asset1Metadata }, { denomMetadata: asset2Metadata }] = await Promise.all([
viewClient.assetMetadataById({ assetId: assetId1 }),
viewClient.assetMetadataById({ assetId: assetId2 }),
]);

return {
swap,
// If no metadata, uses assetId for asset icon display
asset1: asset1Metadata
? asset1Metadata
: new Metadata({ display: uint8ArrayToBase64(assetId1.inner) }),
asset2: asset2Metadata
? asset2Metadata
: new Metadata({ display: uint8ArrayToBase64(assetId2.inner) }),
};
};

export const unclaimedSwapsWithMetadata = async (): Promise<UnclaimedSwapsWithMetadata[]> => {
const unclaimedSwaps = await fetchUnclaimedSwaps();
return Promise.all(unclaimedSwaps.map(fetchMetadataForSwap));
};

export const SwapLoader: LoaderFunction = async (): Promise<SwapLoaderResponse> => {
export const SwapLoader: LoaderFunction = async (): Promise<null> => {
await abortLoader();
const assets = await getAllAssets();
const swappableAssets = assets.filter(isSwappable);

const [balancesResponses, unclaimedSwaps] = await Promise.all([
getAndSetDefaultAssetBalances(swappableAssets),
unclaimedSwapsWithMetadata(),
]);
const balancesResponses = await getAndSetDefaultAssetBalances(swappableAssets);
useStore.getState().swap.setBalancesResponses(balancesResponses);
useStore.getState().swap.setSwappableAssets(swappableAssets);
void useStore.getState().swap.dutchAuction.loadAuctionInfos();

return unclaimedSwaps;
return null;
};
33 changes: 13 additions & 20 deletions apps/minifront/src/components/swap/unclaimed-swaps.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,28 @@
import { Button } from '@penumbra-zone/ui/components/ui/button';
import { Card } from '@penumbra-zone/ui/components/ui/card';
import { useLoaderData, useRevalidator } from 'react-router-dom';
import { SwapLoaderResponse, UnclaimedSwapsWithMetadata } from './swap-loader';
import { AssetIcon } from '@penumbra-zone/ui/components/ui/tx/view/asset-icon';
import { useStore } from '../../state';
import { unclaimedSwapsSelector } from '../../state/unclaimed-swaps';
import { AllSlices } from '../../state';
import { useUnclaimedSwaps } from '../../state/unclaimed-swaps';
import { getSwapRecordCommitment } from '@penumbra-zone/getters/swap-record';
import { uint8ArrayToBase64 } from '@penumbra-zone/types/base64';
import { GradientHeader } from '@penumbra-zone/ui/components/ui/gradient-header';
import { useStoreShallow } from '../../utils/use-store-shallow';

const unclaimedSwapsSelector = (state: AllSlices) => ({
claimSwap: state.unclaimedSwaps.claimSwap,
isInProgress: state.unclaimedSwaps.isInProgress,
});

export const UnclaimedSwaps = () => {
const unclaimedSwaps = useLoaderData() as SwapLoaderResponse;
const unclaimedSwaps = useUnclaimedSwaps();
const { claimSwap, isInProgress } = useStoreShallow(unclaimedSwapsSelector);

const sortedUnclaimedSwaps = unclaimedSwaps.sort(
(a, b) => Number(b.swap.outputData?.height) - Number(a.swap.outputData?.height),
);
return !unclaimedSwaps.length ? (
return !unclaimedSwaps.data?.length ? (
<div className='hidden xl:block'></div>
) : (
<_UnclaimedSwaps unclaimedSwaps={sortedUnclaimedSwaps}></_UnclaimedSwaps>
);
};

const _UnclaimedSwaps = ({ unclaimedSwaps }: { unclaimedSwaps: UnclaimedSwapsWithMetadata[] }) => {
const { revalidate } = useRevalidator();
const { claimSwap, isInProgress } = useStore(unclaimedSwapsSelector);

return (
<Card layout>
<GradientHeader layout>Unclaimed Swaps</GradientHeader>
{unclaimedSwaps.map(({ swap, asset1, asset2 }) => {
{unclaimedSwaps.data.map(({ swap, asset1, asset2 }) => {
const id = uint8ArrayToBase64(getSwapRecordCommitment(swap).inner);

return (
Expand All @@ -46,7 +39,7 @@ const _UnclaimedSwaps = ({ unclaimedSwaps }: { unclaimedSwaps: UnclaimedSwapsWit

<Button
className='ml-auto w-20'
onClick={() => void claimSwap(id, swap, revalidate)}
onClick={() => void claimSwap(id, swap)}
disabled={isInProgress(id)}
>
{isInProgress(id) ? 'Claiming' : 'Claim'}
Expand Down
Loading

0 comments on commit 6b06e04

Please sign in to comment.