Skip to content

Commit

Permalink
Refactoring spend max (#1760)
Browse files Browse the repository at this point in the history
* refactor to use zustand and remove extraneous state

* more refactor and linting
  • Loading branch information
TalDerei authored Sep 3, 2024
1 parent 8e3e822 commit af47b9d
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 163 deletions.
45 changes: 36 additions & 9 deletions apps/minifront/src/components/send/send-form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@ import { Input } from '@penumbra-zone/ui/components/ui/input';
import { useStore } from '../../../state';
import { sendSelector, sendValidationErrors } from '../../../state/send';
import { InputBlock } from '../../shared/input-block';
import { useMemo } from 'react';
import { useEffect, useMemo } from 'react';
import { penumbraAddrValidation } from '../helpers';
import InputToken from '../../shared/input-token';
import { GasFee } from '../../shared/gas-fee';
import { useBalancesResponses, useStakingTokenMetadata } from '../../../state/shared';
import { useBalancesResponses, useGasPrices, useStakingTokenMetadata } from '../../../state/shared';
import { NonNativeFeeWarning } from '../../shared/non-native-fee-warning';
import { transferableBalancesResponsesSelector } from '../../../state/send/helpers';
import { useRefreshFee } from '../../v2/transfer-layout/send-page/use-refresh-fee';
import { hasStakingToken } from '../../../fetchers/gas-prices';

export const SendForm = () => {
// Retrieve the staking token metadata and gas prices from the zustand
const stakingTokenMetadata = useStakingTokenMetadata();
const gasPrices = useGasPrices();

const transferableBalancesResponses = useBalancesResponses({
select: transferableBalancesResponsesSelector,
});
Expand All @@ -38,6 +42,35 @@ export const SendForm = () => {

useRefreshFee();

// Determine if the selected token is the staking token based on the current balances and metadata
const isStakingToken = hasStakingToken(
transferableBalancesResponses?.data,
stakingTokenMetadata.data,
selection,
);

// useEffect here defers the state updates until after the rendering phase is complete,
// preventing direct state modifications during rendering.
useEffect(() => {
const updateStakingTokenAndGasPrices = () => {
// Update the zustand store and local state
setStakingToken(isStakingToken);
if (gasPrices.data) {
setGasPrices(gasPrices.data);
}
};

updateStakingTokenAndGasPrices();
}, [
transferableBalancesResponses,
stakingTokenMetadata,
selection,
gasPrices,
isStakingToken,
setGasPrices,
setStakingToken,
]);

const validationErrors = useMemo(() => {
return sendValidationErrors(selection, amount, recipient);
}, [selection, amount, recipient]);
Expand Down Expand Up @@ -93,13 +126,7 @@ export const SendForm = () => {
loading={transferableBalancesResponses?.loading}
/>

<NonNativeFeeWarning
balancesResponses={transferableBalancesResponses?.data}
amount={Number(amount)}
source={selection}
setGasPrices={setGasPrices}
setStakingToken={setStakingToken}
/>
<NonNativeFeeWarning amount={Number(amount)} hasStakingToken={isStakingToken} />

<GasFee
fee={fee}
Expand Down
124 changes: 4 additions & 120 deletions apps/minifront/src/components/shared/non-native-fee-warning.tsx
Original file line number Diff line number Diff line change
@@ -1,114 +1,20 @@
import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';
import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { getAssetIdFromValueView } from '@penumbra-zone/getters/value-view';
import { useStakingTokenMetadata } from '../../state/shared';
import { ReactNode, useCallback, useEffect, useState } from 'react';
import {
getAddressIndex,
getAmount,
getAssetIdFromBalancesResponse,
} from '@penumbra-zone/getters/balances-response';
import { ViewService } from '@penumbra-zone/protobuf';
import { GasPrices } from '@penumbra-zone/protobuf/penumbra/core/component/fee/v1/fee_pb';
import { getAssetId } from '@penumbra-zone/getters/metadata';
import { penumbra } from '../../prax';

const hasTokenBalance = ({
source,
balancesResponses = [],
gasPrices,
stakingAssetMetadata,
setStakingToken,
}: {
source?: BalancesResponse;
balancesResponses: BalancesResponse[];
gasPrices: GasPrices[];
stakingAssetMetadata?: Metadata;
setStakingToken: (stakingToken: boolean) => void;
}): boolean => {
const account = getAddressIndex.optional()(source)?.account;
if (typeof account === 'undefined') {
return false;
}

// Finds the UM token in the user's account balances
const hasStakingToken = balancesResponses.some(
asset =>
getAssetIdFromValueView
.optional()(asset.balanceView)
?.equals(getAssetId.optional()(stakingAssetMetadata)) &&
getAddressIndex.optional()(asset)?.account === account,
);

// Set the staking token status in the state
setStakingToken(hasStakingToken);

if (hasStakingToken) {
return false;
}

const accountAssets = balancesResponses.filter(
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 => {
const amount = getAmount(balance);
const hasBalance = amount.lo !== 0n || amount.hi !== 0n;
if (!hasBalance) {
return false;
}

return gasPrices.some(price =>
price.assetId?.equals(getAssetIdFromBalancesResponse.optional()(balance)),
);
});

return hasAltTokens;
};

const useGasPrices = () => {
const [prices, setPrices] = useState<GasPrices[]>([]);

const fetchGasPrices = useCallback(async () => {
const res = await penumbra.service(ViewService).gasPrices({});
setPrices(res.altGasPrices);
}, []);

useEffect(() => {
void fetchGasPrices();
}, [fetchGasPrices]);

return prices;
};
import { ReactNode } from 'react';

/**
* Renders a non-native fee warning if
* 1. the user does not have any balance (in the selected account) of the staking token to use for fees
* 2. the user does not have sufficient balances in alternative tokens to cover the fees
*/
export const NonNativeFeeWarning = ({
balancesResponses = [],
amount,
source,
hasStakingToken,
wrap = children => children,
setGasPrices,
setStakingToken,
}: {
/**
* The user's balances that are relevant to this transaction, from which
* `<NonNativeFeeWarning />` will determine whether to render.
*/
balancesResponses?: BalancesResponse[];
/**
* The amount that the user is putting into this transaction, which will help
* determine whether the warning should render.
*/
amount: number;
/**
* A source token – helps determine whether the user has UM token
* in the same account as `source` to use for fees.
*/
source?: BalancesResponse;
/*
* Since this component determines for itself whether to render, a parent
* component can't optionally render wrapper markup depending on whether this
Expand All @@ -126,32 +32,10 @@ export const NonNativeFeeWarning = ({
* />
* ```
*/
hasStakingToken: boolean;
wrap?: (children: ReactNode) => ReactNode;
setGasPrices: (prices: GasPrices[]) => void;
setStakingToken: (stakingToken: boolean) => void;
}) => {
const gasPrices = useGasPrices();
const stakingTokenMetadata = useStakingTokenMetadata();
const [hasStakingToken, setHasStakingToken] = useState(false);

// useEffect here defers the state updates until after the rendering phase is complete,
// preventing direct state modifications during rendering.
useEffect(() => {
setGasPrices(gasPrices);
}, [gasPrices, setGasPrices]);

useEffect(() => {
const stakingToken = hasTokenBalance({
source,
balancesResponses,
gasPrices,
stakingAssetMetadata: stakingTokenMetadata.data,
setStakingToken,
});
setHasStakingToken(stakingToken);
}, [source, balancesResponses, gasPrices, stakingTokenMetadata, setStakingToken]);

const shouldRender = !!amount && hasStakingToken;
const shouldRender = !!amount && !hasStakingToken;
if (!shouldRender) {
return null;
}
Expand Down
32 changes: 14 additions & 18 deletions apps/minifront/src/components/swap/swap-form/token-swap-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ import { zeroValueView } from '../../../utils/zero-value-view';
import { isValidAmount } from '../../../state/helpers';
import { NonNativeFeeWarning } from '../../shared/non-native-fee-warning';
import { NumberInput } from '../../shared/number-input';
import { useBalancesResponses, useAssets } from '../../../state/shared';
import { useBalancesResponses, useAssets, useStakingTokenMetadata } from '../../../state/shared';
import { FadeIn } from '@penumbra-zone/ui/components/ui/fade-in';
import { getBalanceByMatchingMetadataAndAddressIndex } from '../../../state/swap/getters';
import {
swappableAssetsSelector,
swappableBalancesResponsesSelector,
} from '../../../state/swap/helpers';
import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata';
import { hasStakingToken } from '../../../fetchers/gas-prices';

const getAssetOutBalance = (
balancesResponses: BalancesResponse[] = [],
Expand Down Expand Up @@ -55,8 +56,6 @@ const tokenSwapInputSelector = (state: AllSlices) => ({
amount: state.swap.amount,
setAmount: state.swap.setAmount,
reverse: state.swap.reverse,
setGasPrices: state.swap.setGasPrices,
setStakingToken: state.swap.setStakingToken,
});

/**
Expand All @@ -68,17 +67,8 @@ const tokenSwapInputSelector = (state: AllSlices) => ({
export const TokenSwapInput = () => {
const balancesResponses = useBalancesResponses({ select: swappableBalancesResponsesSelector });
const swappableAssets = useAssets({ select: swappableAssetsSelector });
const {
amount,
setAmount,
assetIn,
setAssetIn,
assetOut,
setAssetOut,
reverse,
setGasPrices,
setStakingToken,
} = useStoreShallow(tokenSwapInputSelector);
const { amount, setAmount, assetIn, setAssetIn, assetOut, setAssetOut, reverse } =
useStoreShallow(tokenSwapInputSelector);
const assetOutBalance = getAssetOutBalance(balancesResponses?.data, assetIn, assetOut);
const assetInExponent = useMemo(() => {
return getDisplayDenomExponent.optional()(getMetadataFromBalancesResponseOptional(assetIn));
Expand All @@ -94,6 +84,15 @@ export const TokenSwapInput = () => {
}
};

const stakingTokenMetadata = useStakingTokenMetadata();

// Determine if the selected token is the staking token based on the current balances and metadata
const isStakingToken = hasStakingToken(
balancesResponses?.data,
stakingTokenMetadata.data,
assetIn,
);

return (
<Box label='Trade' layout>
<div className='flex flex-col items-stretch gap-4 sm:flex-row'>
Expand Down Expand Up @@ -169,11 +168,8 @@ export const TokenSwapInput = () => {
</div>

<NonNativeFeeWarning
balancesResponses={balancesResponses?.data}
amount={Number(amount)}
source={assetIn}
setGasPrices={setGasPrices}
setStakingToken={setStakingToken}
hasStakingToken={isStakingToken}
wrap={children => (
<>
{/* This div adds an empty line */} <div className='h-4' />
Expand Down
38 changes: 38 additions & 0 deletions apps/minifront/src/fetchers/gas-prices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ViewService } from '@penumbra-zone/protobuf';
import { penumbra } from '../prax';
import { GasPrices } from '@penumbra-zone/protobuf/penumbra/core/component/fee/v1/fee_pb';
import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';
import { getAssetIdFromValueView } from '@penumbra-zone/getters/value-view';
import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { getAddressIndex } from '@penumbra-zone/getters/balances-response';
import { getAssetId } from '@penumbra-zone/getters/metadata';

// Fetches gas prices
export const getGasPrices = async (): Promise<GasPrices[]> => {
const res = await penumbra.service(ViewService).gasPrices({});
return res.altGasPrices;
};

// Determines if the user has UM token in their account balances
export const hasStakingToken = (
balancesResponses?: BalancesResponse[],
stakingAssetMetadata?: Metadata,
source?: BalancesResponse,
): boolean => {
if (!balancesResponses || !stakingAssetMetadata || !source) {
return false;
}

const account = getAddressIndex.optional()(source)?.account;
if (typeof account === 'undefined') {
return false;
}

return balancesResponses.some(
asset =>
getAssetIdFromValueView
.optional()(asset.balanceView)
?.equals(getAssetId.optional()(stakingAssetMetadata)) &&
getAddressIndex.optional()(asset)?.account === account,
);
};
17 changes: 17 additions & 0 deletions apps/minifront/src/state/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { getStakingTokenMetadata } from '../fetchers/registry';
import { getBalancesStream } from '../fetchers/balances';
import { getAllAssets } from '../fetchers/assets';
import { uint8ArrayToHex } from '@penumbra-zone/types/hex';
import { GasPrices } from '@penumbra-zone/protobuf/penumbra/core/component/fee/v1/fee_pb';
import { getGasPrices } from '../fetchers/gas-prices';

/**
* For Noble specifically we need to use a Bech32 encoding rather than Bech32m,
Expand Down Expand Up @@ -88,16 +90,31 @@ export const { assets, useAssets } = createZQuery({
},
});

export const { gasPrices, useGasPrices } = createZQuery({
name: 'gasPrices',
fetch: getGasPrices,
getUseStore: () => useStore,
get: state => state.shared.gasPrices,
set: setter => {
const newState = setter(useStore.getState().shared.gasPrices);
useStore.setState(state => {
state.shared.gasPrices = newState;
});
},
});

export interface SharedSlice {
assets: ZQueryState<Metadata[]>;
balancesResponses: ZQueryState<BalancesResponse[], Parameters<typeof getBalancesStream>>;
stakingTokenMetadata: ZQueryState<Metadata>;
gasPrices: ZQueryState<GasPrices[]>;
}

export const createSharedSlice = (): SliceCreator<SharedSlice> => () => ({
assets,
balancesResponses,
stakingTokenMetadata,
gasPrices,
});

export interface BalancesByAccount {
Expand Down
Loading

0 comments on commit af47b9d

Please sign in to comment.