Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stop using loader functions for swap page #1316

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/minifront/src/components/root-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { SendForm } from './send/send-form';
import { Receive } from './send/receive';
import { ErrorBoundary } from './shared/error-boundary';
import { SwapLayout } from './swap/layout';
import { SwapLoader } from './swap/swap-loader';
import { StakingLayout } from './staking/layout';
import { IbcLoader } from './ibc/ibc-loader';
import { IbcLayout } from './ibc/layout';
Expand Down Expand Up @@ -56,7 +55,7 @@ export const rootRouter: Router = createHashRouter([
},
{
path: PagePath.SWAP,
loader: SwapLoader,
loader: abortLoader,
element: <SwapLayout />,
},
{
Expand Down
4 changes: 2 additions & 2 deletions apps/minifront/src/components/send/send-form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const SendForm = () => {
txInProgress,
} = useStore(sendSelector);
// State to manage privacy warning display
const [showNonNativeFeeWarning, setshowNonNativeFeeWarning] = useState(false);
const [showNonNativeFeeWarning, setShowNonNativeFeeWarning] = useState(false);

// Check if the user has native staking tokens
const stakingToken = hasStakingToken(
Expand Down Expand Up @@ -81,7 +81,7 @@ export const SendForm = () => {
if (Number(amount) < 0) return;
setAmount(amount);
// Conditionally prompt a privacy warning about non-native fee tokens
setshowNonNativeFeeWarning(Number(amount) > 0 && !stakingToken);
setShowNonNativeFeeWarning(Number(amount) > 0 && !stakingToken);
}}
validations={[
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,10 @@ const zeroBalance = new ValueView({

const validatorInfo = new ValidatorInfo({ validator: {} });

let MOCK_STAKING_TOKENS_AND_FILTER:
| {
unstakedTokensByAccount: Map<number, ValueView | undefined>;
accountSwitcherFilter: number[];
}
| undefined;
let MOCK_STAKING_TOKENS_AND_FILTER: {
unstakedTokensByAccount: Map<number, ValueView | undefined>;
accountSwitcherFilter: number[];
} | null = vi.hoisted(() => null);

vi.mock('../../../../../state/staking', async () => ({
...(await vi.importActual('../../../../../state/staking')),
Expand All @@ -53,7 +51,7 @@ vi.mock('../../../../../utils/use-store-shallow', async () => ({

describe('<StakingActions />', () => {
beforeEach(() => {
MOCK_STAKING_TOKENS_AND_FILTER = undefined;
MOCK_STAKING_TOKENS_AND_FILTER = null;
});

it('renders an enabled Delegate button there is a non-zero balance of unstaked tokens', () => {
Expand Down Expand Up @@ -91,7 +89,7 @@ describe('<StakingActions />', () => {
});

it('renders a disabled Delegate button when unstaked tokens are undefined', () => {
MOCK_STAKING_TOKENS_AND_FILTER = undefined;
MOCK_STAKING_TOKENS_AND_FILTER = null;

const { getByText } = render(
<StakingActions delegationTokens={nonZeroBalance} validatorInfo={validatorInfo} />,
Expand Down
98 changes: 60 additions & 38 deletions apps/minifront/src/components/swap/swap-form/token-swap-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ 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';
import { hasStakingToken } from '../../../fetchers/staking-token';
import { useStakingTokenMetadata } from '../../../state/shared';
import { useBalancesResponses, useSwappableAssets } from '../../../state/swap';
import { FadeIn } from '@repo/ui/components/ui/fade-in';

const isValidAmount = (amount: string, assetIn?: BalancesResponse) =>
Number(amount) >= 0 && (!assetIn || !amountMoreThanBalance(assetIn, amount));
Expand All @@ -39,7 +42,11 @@ const getKnownZeroValueView = (metadata?: Metadata) => {
});
};

const assetOutBalanceSelector = ({ swap: { balancesResponses, assetIn, assetOut } }: AllSlices) => {
const getAssetOutBalance = (
balancesResponses: BalancesResponse[] = [],
assetIn?: BalancesResponse,
assetOut?: Metadata,
) => {
if (!assetIn || !assetOut) return getKnownZeroValueView();

const match = balancesResponses.find(balance => {
Expand All @@ -54,17 +61,13 @@ const assetOutBalanceSelector = ({ swap: { balancesResponses, assetIn, assetOut
};

const tokenSwapInputSelector = (state: AllSlices) => ({
swappableAssets: state.swap.swappableAssets,
assetIn: state.swap.assetIn,
setAssetIn: state.swap.setAssetIn,
assetOut: state.swap.assetOut,
setAssetOut: state.swap.setAssetOut,
amount: state.swap.amount,
setAmount: state.swap.setAmount,
balancesResponses: state.swap.balancesResponses,
priceHistory: state.swap.priceHistory,
assetOutBalance: assetOutBalanceSelector(state),
hasStakingTokenMeta: state.swap.stakingAssetMetadata,
});

/**
Expand All @@ -76,24 +79,16 @@ const tokenSwapInputSelector = (state: AllSlices) => ({
export const TokenSwapInput = () => {
const status = useStatus();
const latestKnownBlockHeight = status.data?.latestKnownBlockHeight ?? 0n;
const {
swappableAssets,
amount,
setAmount,
assetIn,
setAssetIn,
assetOut,
setAssetOut,
balancesResponses,
priceHistory,
assetOutBalance,
hasStakingTokenMeta,
} = useStoreShallow(tokenSwapInputSelector);
const stakingTokenMetadata = useStakingTokenMetadata();
const balancesResponses = useBalancesResponses();
const swappableAssets = useSwappableAssets();
const { amount, setAmount, assetIn, setAssetIn, assetOut, setAssetOut, priceHistory } =
useStoreShallow(tokenSwapInputSelector);
// State to manage privacy warning display
const [showNonNativeFeeWarning, setshowNonNativeFeeWarning] = useState(false);
const [showNonNativeFeeWarning, setShowNonNativeFeeWarning] = useState(false);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo fix

const assetOutBalance = getAssetOutBalance(balancesResponses.data, assetIn, assetOut);

// Check if the user has native staking tokens
const stakingToken = hasStakingToken(balancesResponses, hasStakingTokenMeta);
const userHasStakingToken = hasStakingToken(balancesResponses.data, stakingTokenMetadata.data);

useEffect(() => {
if (!assetIn || !assetOut) return;
Expand Down Expand Up @@ -131,10 +126,10 @@ export const TokenSwapInput = () => {
onChange={e => {
if (!isValidAmount(e.target.value, assetIn)) return;
setAmount(e.target.value);
setshowNonNativeFeeWarning(Number(e.target.value) > 0 && !stakingToken);
setShowNonNativeFeeWarning(Number(e.target.value) > 0 && !userHasStakingToken);
}}
/>
<div className='flex gap-4 sm:contents'>
<div className='flex gap-4'>
{assetIn && (
<div className='ml-auto hidden h-full flex-col justify-end self-end sm:flex'>
<span className='mr-2 block whitespace-nowrap text-xs text-muted-foreground'>
Expand All @@ -143,21 +138,48 @@ export const TokenSwapInput = () => {
</div>
)}

<div className='flex h-full flex-col gap-2'>
<BalanceSelector value={assetIn} onChange={setAssetIn} balances={balancesResponses} />
{assetIn?.balanceView && (
<BalanceValueView valueView={assetIn.balanceView} onClick={setInputToBalanceMax} />
)}
</div>

<div className='flex flex-col gap-2 pt-2'>
<ArrowRight size={16} className='text-muted-foreground' />
</div>

<div className='flex h-full flex-col gap-2'>
<AssetSelector assets={swappableAssets} value={assetOut} onChange={setAssetOut} />
{assetOut && <BalanceValueView valueView={assetOutBalance} />}
</div>
<FadeIn condition={!!balancesResponses.error || !!swappableAssets.error}>
<div className='flex gap-4 text-red'>
{balancesResponses.error instanceof Error && balancesResponses.error.toString()}
{swappableAssets.error instanceof Error && swappableAssets.error.toString()}
</div>
</FadeIn>

<FadeIn condition={!!balancesResponses.data && !!swappableAssets.data}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fades in the input/output selectors once the necessary data has loaded:

screencap

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we handle the error case as well? Maybe some red text with a loading error message?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

<div className='flex gap-4'>
<div className='flex h-full flex-col gap-2'>
{balancesResponses.data && (
<BalanceSelector
value={assetIn}
onChange={setAssetIn}
balances={balancesResponses.data}
/>
)}
{assetIn?.balanceView && (
<BalanceValueView
valueView={assetIn.balanceView}
onClick={setInputToBalanceMax}
/>
)}
</div>

<div className='size-4 pt-2'>
<ArrowRight size={16} className='text-muted-foreground' />
</div>

<div className='flex h-full flex-col gap-2'>
{swappableAssets.data && (
<AssetSelector
assets={swappableAssets.data}
value={assetOut}
onChange={setAssetOut}
/>
)}

{assetOut && <BalanceValueView valueView={assetOutBalance} />}
</div>
</div>
</FadeIn>
</div>
{priceHistory.startMetadata && priceHistory.endMetadata && priceHistory.candles.length ? (
<CandlestickPlot
Expand Down
42 changes: 0 additions & 42 deletions apps/minifront/src/components/swap/swap-loader.ts

This file was deleted.

4 changes: 2 additions & 2 deletions apps/minifront/src/state/ibc-out.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ export const filterBalancesPerChain = (
registryAssets: Metadata[],
stakingTokenMetadata?: Metadata,
): BalancesResponse[] => {
const penumbraAssetId = getAssetId(stakingTokenMetadata);
const penumbraAssetId = getAssetId.optional()(stakingTokenMetadata);
const assetsWithMatchingChannel = registryAssets
.filter(a => {
const match = assetPatterns.ibc.capture(a.base);
Expand All @@ -244,6 +244,6 @@ export const filterBalancesPerChain = (
const assetIdsToCheck = [penumbraAssetId, ...assetsWithMatchingChannel];

return allBalances.filter(({ balanceView }) => {
return assetIdsToCheck.some(assetId => assetId.equals(getAssetIdFromValueView(balanceView)));
return assetIdsToCheck.some(assetId => assetId?.equals(getAssetIdFromValueView(balanceView)));
});
};
16 changes: 10 additions & 6 deletions apps/minifront/src/state/swap/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ describe('Swap Slice', () => {
expect(useStore.getState().swap.amount).toBe('');
expect(useStore.getState().swap.assetIn).toBeUndefined();
expect(useStore.getState().swap.assetOut).toBeUndefined();
expect(useStore.getState().swap.swappableAssets).toEqual([]);
expect(useStore.getState().swap.balancesResponses).toEqual([]);
expect(useStore.getState().swap.swappableAssets).toEqual(
expect.objectContaining({ data: undefined, error: undefined, loading: false }),
);
expect(useStore.getState().swap.balancesResponses).toEqual(
expect.objectContaining({ data: undefined, error: undefined, loading: false }),
);
expect(useStore.getState().swap.duration).toBe('instant');
expect(useStore.getState().swap.txInProgress).toBe(false);
});
Expand Down Expand Up @@ -102,8 +106,8 @@ describe('Swap Slice', () => {
});

useStore.setState(state => {
state.swap.swappableAssets = [metadata1, metadata2];
state.swap.balancesResponses = [
state.swap.swappableAssets.data = [metadata1, metadata2];
state.swap.balancesResponses.data = [
balancesResponseWithMetadata1,
balancesResponseWithMetadata2,
];
Expand Down Expand Up @@ -149,8 +153,8 @@ describe('Swap Slice', () => {
});

useStore.setState(state => {
state.swap.swappableAssets = [metadata1, metadata2];
state.swap.balancesResponses = [
state.swap.swappableAssets.data = [metadata1, metadata2];
state.swap.balancesResponses.data = [
balancesResponseWithMetadata1,
balancesResponseWithMetadata2,
];
Expand Down
Loading
Loading