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

re-enable spend max feature #1698

Merged
merged 32 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4581b33
reenable spend max in frontend
TalDerei Aug 15, 2024
4d90cc2
max send invariants
TalDerei Aug 15, 2024
cd6e080
reenable spend max in planner
TalDerei Aug 15, 2024
3848db9
lintingg
TalDerei Aug 15, 2024
bc2854f
refactor assertion checks
TalDerei Aug 15, 2024
d1264db
fix swaps and state updates
TalDerei Aug 15, 2024
aec8843
modify privacy warning
TalDerei Aug 15, 2024
8e3e822
merge main
TalDerei Sep 3, 2024
af47b9d
Refactoring spend max (#1760)
TalDerei Sep 3, 2024
543f04b
Merge branch 'main' into reenable-spend-max
TalDerei Sep 14, 2024
7a2ee66
fix bug with non max amounts
TalDerei Sep 14, 2024
147f16b
forgot to add unit tests
TalDerei Sep 15, 2024
67a6cdc
error message
TalDerei Sep 15, 2024
ec412a1
linting
TalDerei Sep 15, 2024
c68a883
changeset
TalDerei Sep 15, 2024
0f78dac
bump wasm deps to v0.80.5
TalDerei Sep 19, 2024
2c5182f
extra safety checks in wasm planner
TalDerei Sep 19, 2024
02bb4dc
update lockfile
TalDerei Sep 19, 2024
337b4b9
split wasm code
TalDerei Sep 21, 2024
04be8cd
refactoring assertions
TalDerei Sep 30, 2024
063f5e1
Merge branch 'main' into reenable-spend-max
TalDerei Sep 30, 2024
755c87f
Merge branch 'main' into reenable-spend-max
TalDerei Oct 2, 2024
a8d3b20
assertion cleanup
TalDerei Oct 3, 2024
e298e36
merge main
TalDerei Oct 16, 2024
e472dc6
fix import
TalDerei Oct 16, 2024
2a2f3c4
gabe's suggestion's
TalDerei Oct 17, 2024
b3a5d8f
revert
TalDerei Oct 20, 2024
e8b9e99
linting galore
TalDerei Oct 20, 2024
73e74d9
extra extra extra validation for sanity checking
TalDerei Oct 21, 2024
ff98319
Suggestions (#1867)
grod220 Oct 22, 2024
923346c
changeset for updates packages
TalDerei Oct 30, 2024
193194d
delete outdated changeset
TalDerei Oct 30, 2024
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
7 changes: 7 additions & 0 deletions .changeset/short-walls-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@penumbra-zone/services': minor
'minifront': minor
'@penumbra-zone/wasm': minor
---

enable send max feature
45 changes: 38 additions & 7 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 @@ -31,11 +35,42 @@ export const SendForm = () => {
setFeeTier,
setMemo,
sendTx,
setGasPrices,
setStakingToken,
txInProgress,
} = useStore(sendSelector);

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,
);
TalDerei marked this conversation as resolved.
Show resolved Hide resolved

// 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,
]);
TalDerei marked this conversation as resolved.
Show resolved Hide resolved

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

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

<GasFee
fee={fee}
Expand Down
108 changes: 6 additions & 102 deletions apps/minifront/src/components/shared/non-native-fee-warning.tsx
Original file line number Diff line number Diff line change
@@ -1,107 +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,
}: {
source?: BalancesResponse;
balancesResponses: BalancesResponse[];
gasPrices: GasPrices[];
stakingAssetMetadata?: Metadata;
}): 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,
);

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,
}: {
/**
* 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 @@ -119,19 +32,10 @@ export const NonNativeFeeWarning = ({
* />
* ```
*/
hasStakingToken: boolean;
wrap?: (children: ReactNode) => ReactNode;
}) => {
const gasPrices = useGasPrices();
const stakingTokenMetadata = useStakingTokenMetadata();
const shouldRender =
!!amount &&
hasTokenBalance({
source,
balancesResponses,
gasPrices,
stakingAssetMetadata: stakingTokenMetadata.data,
});

const shouldRender = !!amount && !hasStakingToken;
if (!shouldRender) {
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
return null;
}
Expand All @@ -140,8 +44,8 @@ export const NonNativeFeeWarning = ({
<div className='rounded border border-yellow-500 p-4 text-yellow-500'>
<strong>Privacy Warning:</strong>
<span className='block'>
Using non-native tokens for transaction fees may pose a privacy risk. It is recommended to
use the native token (UM) for better privacy and security.
You are using an alternative token for transaction fees, which may pose a privacy risk. It
is recommended to use the native token (UM) for better privacy and security.
</span>
</div>,
);
Expand Down
15 changes: 12 additions & 3 deletions apps/minifront/src/components/swap/swap-form/token-swap-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ 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 { getBalanceByMatchingMetadataAndAddressIndex } from '../../../state/swap/getters';
import {
swappableAssetsSelector,
swappableBalancesResponsesSelector,
} from '../../../state/swap/helpers';
import { hasStakingToken } from '../../../fetchers/gas-prices';
import { TokenInputError } from './token-input-error.tsx';

const getAssetOutBalance = (
Expand Down Expand Up @@ -74,6 +75,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,
);
TalDerei marked this conversation as resolved.
Show resolved Hide resolved

return (
<Box label='Trade' layout headerContent={<TokenInputError />}>
<div className='flex flex-col items-stretch gap-4 sm:flex-row'>
Expand Down Expand Up @@ -141,9 +151,8 @@ export const TokenSwapInput = () => {
</div>

<NonNativeFeeWarning
balancesResponses={balancesResponses?.data}
amount={Number(amount)}
source={assetIn}
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,
);
};
Loading
Loading