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

fees: ux for multi-asset fees #1268

Merged
merged 29 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f0b146f
add index for spendable notes table in idb
TalDerei Jun 9, 2024
1f44143
scaffolding planner support for multi-asset fees
TalDerei Jun 9, 2024
c92ddfa
filter for alt fee asset id
TalDerei Jun 10, 2024
837d72b
fix fee rendering
TalDerei Jun 10, 2024
e8e0538
full multi-asset fee support ~
TalDerei Jun 10, 2024
6f0244e
attempt to pass CI
TalDerei Jun 10, 2024
45bfe51
Merge branch 'main' into ux-multi-fees
TalDerei Jun 10, 2024
c1b2aee
partially address comments
TalDerei Jun 12, 2024
23b6e19
valentine + jesse feedback
TalDerei Jun 13, 2024
fbc25cb
merge main
TalDerei Jun 13, 2024
d9b7373
fix broken lockfile, update deps, update db version
TalDerei Jun 13, 2024
f53027a
remove dangling minifront_url
TalDerei Jun 13, 2024
6532dbc
co-locate idb version with storage package
TalDerei Jun 13, 2024
f11da6a
remove idb version from extension .env
TalDerei Jun 13, 2024
89a24c1
support actions for alt fees
TalDerei Jun 14, 2024
ccfe1cf
linting and organization cleanup
TalDerei Jun 14, 2024
81e04bb
merge main post prax migration
TalDerei Jun 14, 2024
92e3dbd
update lockfile
TalDerei Jun 14, 2024
12afb80
Merge branch 'main' into ux-multi-fees
TalDerei Jun 15, 2024
1c8a6a4
remove extension dir trace
TalDerei Jun 15, 2024
2b60977
fix lockfile?
TalDerei Jun 15, 2024
b20ea66
address valentine comments
TalDerei Jun 17, 2024
0717d90
merge main
TalDerei Jun 17, 2024
04a0d65
fix test suite
TalDerei Jun 17, 2024
8721bc2
try fixing rust tests
Valentine1898 Jun 17, 2024
e570f2e
rust lint
Valentine1898 Jun 17, 2024
71ce2d3
rust lint
Valentine1898 Jun 17, 2024
3e9101f
add TODOs for #1310
Valentine1898 Jun 17, 2024
5de82b8
fix lint
Valentine1898 Jun 17, 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
29 changes: 23 additions & 6 deletions apps/minifront/src/components/send/send-form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useStore } from '../../../state';
import { sendSelector, sendValidationErrors } from '../../../state/send';
import { InputBlock } from '../../shared/input-block';
import { LoaderFunction, useLoaderData } from 'react-router-dom';
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { getTransferableBalancesResponses, penumbraAddrValidation } from '../helpers';
import { abortLoader } from '../../../abort-loader';
import InputToken from '../../shared/input-token';
Expand All @@ -13,10 +13,11 @@ import { GasFee } from '../../shared/gas-fee';
import { BalancesResponse } 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 { getStakingTokenMetadata } from '../../../fetchers/registry';
import { hasStakingToken } from '../../../fetchers/staking-token';

export interface SendLoaderResponse {
assetBalances: BalancesResponse[];
feeAssetMetadata: Metadata;
stakingAssetMetadata: Metadata;
}

export const SendAssetBalanceLoader: LoaderFunction = async (): Promise<SendLoaderResponse> => {
Expand All @@ -29,13 +30,13 @@ export const SendAssetBalanceLoader: LoaderFunction = async (): Promise<SendLoad
state.send.selection = assetBalances[0];
});
}
const feeAssetMetadata = await getStakingTokenMetadata();
const stakingAssetMetadata = await getStakingTokenMetadata();

return { assetBalances, feeAssetMetadata };
return { assetBalances, stakingAssetMetadata };
};

export const SendForm = () => {
const { assetBalances, feeAssetMetadata } = useLoaderData() as SendLoaderResponse;
const { assetBalances, stakingAssetMetadata } = useLoaderData() as SendLoaderResponse;
const {
selection,
amount,
Expand All @@ -51,6 +52,11 @@ export const SendForm = () => {
sendTx,
txInProgress,
} = useStore(sendSelector);
// State to manage privacy warning display
const [showNonNativeFeeWarning, setshowNonNativeFeeWarning] = useState(false);

// Check if the user has native staking tokens
const stakingToken = hasStakingToken(assetBalances, stakingAssetMetadata);

useRefreshFee();

Expand Down Expand Up @@ -90,6 +96,8 @@ export const SendForm = () => {
onInputChange={amount => {
if (Number(amount) < 0) return;
setAmount(amount);
// Conditionally prompt a privacy warning about non-native fee tokens
setshowNonNativeFeeWarning(Number(amount) > 0 && !stakingToken);
}}
validations={[
{
Expand All @@ -100,11 +108,20 @@ export const SendForm = () => {
]}
balances={assetBalances}
/>
{showNonNativeFeeWarning && (
<div className='rounded border border-yellow-500 bg-gray-800 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.
</span>
</div>
)}
Comment on lines +111 to +119
Copy link
Contributor

Choose a reason for hiding this comment

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

it makes sense to make this a separate component to avoid duplication

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will cleanup the remaining suggestions in separately


<GasFee
fee={fee}
feeTier={feeTier}
feeAssetMetadata={feeAssetMetadata}
stakingAssetMetadata={stakingAssetMetadata}
setFeeTier={setFeeTier}
/>

Expand Down
6 changes: 3 additions & 3 deletions apps/minifront/src/components/shared/gas-fee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,20 @@ const FEE_TIER_OPTIONS: SegmentedPickerOption<FeeTier_Tier>[] = [
export const GasFee = ({
fee,
feeTier,
feeAssetMetadata,
stakingAssetMetadata,
setFeeTier,
}: {
fee: Fee | undefined;
feeTier: FeeTier_Tier;
feeAssetMetadata: Metadata;
stakingAssetMetadata: Metadata;
setFeeTier: (feeTier: FeeTier_Tier) => void;
}) => {
let feeValueView: ValueView | undefined;
if (fee?.amount)
feeValueView = new ValueView({
valueView: {
case: 'knownAssetId',
value: { amount: fee.amount, metadata: feeAssetMetadata },
value: { amount: fee.amount, metadata: stakingAssetMetadata },
},
});

Expand Down
24 changes: 22 additions & 2 deletions apps/minifront/src/components/swap/swap-form/token-swap-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
getMetadataFromBalancesResponse,
} from '@penumbra-zone/getters/balances-response';
import { ArrowRight } from 'lucide-react';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { getBlockDate } from '../../../fetchers/block-date';
import { AllSlices } from '../../../state';
import { amountMoreThanBalance } from '../../../state/send';
Expand All @@ -25,6 +25,7 @@ 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';
import { hasStakingToken } from '../../../fetchers/staking-token';

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

/**
Expand All @@ -85,7 +87,13 @@ export const TokenSwapInput = () => {
balancesResponses,
priceHistory,
assetOutBalance,
hasStakingTokenMeta,
} = useStoreShallow(tokenSwapInputSelector);
// State to manage privacy warning display
const [showNonNativeFeeWarning, setshowNonNativeFeeWarning] = useState(false);

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

useEffect(() => {
if (!assetIn || !assetOut) return;
Expand Down Expand Up @@ -123,9 +131,9 @@ export const TokenSwapInput = () => {
onChange={e => {
if (!isValidAmount(e.target.value, assetIn)) return;
setAmount(e.target.value);
setshowNonNativeFeeWarning(Number(e.target.value) > 0 && !stakingToken);
}}
/>

<div className='flex gap-4 sm:contents'>
{assetIn && (
<div className='ml-auto hidden h-full flex-col justify-end self-end sm:flex'>
Expand Down Expand Up @@ -162,6 +170,18 @@ export const TokenSwapInput = () => {
/>
) : null}
</div>
{showNonNativeFeeWarning && (
<>
<div className='h-4'></div> {/* This div adds an empty line */}
<div className='rounded border border-yellow-500 bg-gray-800 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.
</span>
</div>
</>
)}
</Box>
);
};
3 changes: 3 additions & 0 deletions apps/minifront/src/components/swap/swap-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SwapRecord } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/vie
import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { getSwappableBalancesResponses, isSwappable } from './helpers';
import { getAllAssets } from '../../fetchers/assets';
import { getStakingTokenMetadata } from '../../fetchers/registry';

export interface UnclaimedSwapsWithMetadata {
swap: SwapRecord;
Expand All @@ -16,11 +17,13 @@ export type SwapLoaderResponse = UnclaimedSwapsWithMetadata[];

const getAndSetDefaultAssetBalances = async (swappableAssets: Metadata[]) => {
const balancesResponses = await getSwappableBalancesResponses();
const stakingTokenAssetMetadata = await getStakingTokenMetadata();

// set initial denom in if there is an available balance
if (balancesResponses[0]) {
useStore.getState().swap.setAssetIn(balancesResponses[0]);
useStore.getState().swap.setAssetOut(swappableAssets[0]!);
useStore.getState().swap.setStakingAssetMetadata(stakingTokenAssetMetadata);
}

return balancesResponses;
Expand Down
13 changes: 13 additions & 0 deletions apps/minifront/src/fetchers/staking-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { BalancesResponse } 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 { getAssetIdFromValueView } from '@penumbra-zone/getters/value-view';
import { getAssetId } from '@penumbra-zone/getters/metadata';

export const hasStakingToken = (
assetBalances: BalancesResponse[],
stakingAssetMetadata: Metadata,
): boolean => {
return assetBalances.some(asset =>
getAssetIdFromValueView(asset.balanceView).equals(getAssetId(stakingAssetMetadata)),
);
};
8 changes: 8 additions & 0 deletions apps/minifront/src/state/swap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface SimulateSwapResult {

interface Actions {
setBalancesResponses: (balancesResponses: BalancesResponse[]) => void;
setStakingAssetMetadata: (metadata: Metadata) => void;
setSwappableAssets: (assets: Metadata[]) => void;
setAssetIn: (asset: BalancesResponse) => void;
setAmount: (amount: string) => void;
Expand All @@ -39,6 +40,7 @@ interface Actions {

interface State {
balancesResponses: BalancesResponse[];
stakingAssetMetadata: Metadata;
swappableAssets: Metadata[];
assetIn?: BalancesResponse;
amount: string;
Expand All @@ -59,6 +61,7 @@ const INITIAL_STATE: State = {
balancesResponses: [],
duration: 'instant',
txInProgress: false,
stakingAssetMetadata: new Metadata(),
};

export type SwapSlice = Actions & State & Subslices;
Expand Down Expand Up @@ -92,6 +95,11 @@ export const createSwapSlice = (): SliceCreator<SwapSlice> => (set, get, store)
state.swap.balancesResponses = balancesResponses;
});
},
setStakingAssetMetadata: stakingAssetMetadata => {
set(state => {
state.swap.stakingAssetMetadata = stakingAssetMetadata;
});
},
setSwappableAssets: swappableAssets => {
set(state => {
state.swap.swappableAssets = swappableAssets;
Expand Down
4 changes: 4 additions & 0 deletions packages/query/src/block-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,12 @@ export class BlockProcessor implements BlockProcessorInterface {
await this.indexedDb.saveFmdParams(compactBlock.fmdParameters);
}
if (compactBlock.gasPrices) {
// TODO #1310 pre-populate assetId for native GasPrices using stakingTokenAssetId
await this.indexedDb.saveGasPrices(compactBlock.gasPrices);
}
// if (compactBlock.altGasPrices) {
// TODO #1310 save altGasPrices to indexed-db
// }

// wasm view server scan
// - decrypts new notes
Expand Down
2 changes: 2 additions & 0 deletions packages/services/src/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export interface IndexedDbMock {
getPricesForAsset?: Mock;
getAuction?: Mock;
getAuctionOutstandingReserves?: Mock;
hasStakingAssetBalance?: Mock;
fetchStakingTokenId?: Mock;
}

export interface AuctionMock {
Expand Down
49 changes: 49 additions & 0 deletions packages/services/src/view-service/fees.ts
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { AssetId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import {
TransactionPlannerRequest,
TransactionPlannerRequest_ActionDutchAuctionEnd,
TransactionPlannerRequest_ActionDutchAuctionSchedule,
TransactionPlannerRequest_ActionDutchAuctionWithdraw,
TransactionPlannerRequest_Output,
TransactionPlannerRequest_Swap,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';
import { Code, ConnectError } from '@connectrpc/connect';

export const extractAltFee = (request: TransactionPlannerRequest): AssetId | undefined => {
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
// Note: expand the possible types as we expand our support to more actions in the future.
const fields = [
{ name: 'outputs', value: request.outputs },
{ name: 'swaps', value: request.swaps },
{ name: 'dutchAuctionScheduleActions', value: request.dutchAuctionScheduleActions },
{ name: 'dutchAuctionEndActions', value: request.dutchAuctionEndActions },
{ name: 'dutchAuctionWithdrawActions', value: request.dutchAuctionWithdrawActions },
];

const nonEmptyField = fields.find(field => field.value.length > 0);

if (!nonEmptyField) {
throw new ConnectError('No non-empty field found in the request.', Code.InvalidArgument);
}

const action = nonEmptyField.value[0]!;

switch (nonEmptyField.name) {
case 'outputs':
return (action as TransactionPlannerRequest_Output).value?.assetId;
case 'swaps':
return (action as TransactionPlannerRequest_Swap).value?.assetId;
case 'dutchAuctionScheduleActions':
return (action as TransactionPlannerRequest_ActionDutchAuctionSchedule).description?.outputId;
case 'dutchAuctionEndActions':
return new AssetId({
inner: (action as TransactionPlannerRequest_ActionDutchAuctionEnd).auctionId?.inner,
});
case 'dutchAuctionWithdrawActions':
return new AssetId({
inner: (action as TransactionPlannerRequest_ActionDutchAuctionWithdraw).auctionId?.inner,
});
default:
console.warn('Unsupported action type.');
throw new ConnectError('Unsupported action type.', Code.InvalidArgument);
}
};
1 change: 1 addition & 0 deletions packages/services/src/view-service/gas-prices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ export const gasPrices: Impl['gasPrices'] = async (_, ctx) => {

return {
gasPrices,
// TODO #1310 add altGasPrices
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ describe('TransactionPlanner request handler', () => {
getAppParams: vi.fn(),
getGasPrices: vi.fn(),
constants: vi.fn(),
fetchStakingTokenId: vi.fn(),
hasStakingAssetBalance: vi.fn(),
};

mockServices = {
Expand Down Expand Up @@ -77,47 +79,11 @@ describe('TransactionPlanner request handler', () => {
compactBlockSpacePrice: 120n,
}),
);

mockIndexedDb.fetchStakingTokenId?.mockResolvedValueOnce(true);
mockIndexedDb.hasStakingAssetBalance?.mockResolvedValueOnce(true);
await transactionPlanner(req, mockCtx);

expect(mockPlanTransaction.mock.calls.length === 1).toBeTruthy();
});

test('should throw error if FmdParameters not available', async () => {
await expect(transactionPlanner(req, mockCtx)).rejects.toThrow('FmdParameters not available');
});

test('should throw error if SctParameters not available', async () => {
mockIndexedDb.getFmdParams?.mockResolvedValueOnce(new FmdParameters());
mockIndexedDb.getAppParams?.mockResolvedValueOnce(
new AppParameters({
chainId: 'penumbra-testnet-mock',
}),
);
await expect(transactionPlanner(req, mockCtx)).rejects.toThrow('SctParameters not available');
});

test('should throw error if ChainId not available', async () => {
mockIndexedDb.getFmdParams?.mockResolvedValueOnce(new FmdParameters());
mockIndexedDb.getAppParams?.mockResolvedValueOnce(
new AppParameters({
sctParams: new SctParameters({
epochDuration: 719n,
}),
}),
);
await expect(transactionPlanner(req, mockCtx)).rejects.toThrow('ChainId not available');
});

test('should throw error if Gas prices is not available', async () => {
mockIndexedDb.getFmdParams?.mockResolvedValueOnce(new FmdParameters());
mockIndexedDb.getAppParams?.mockResolvedValueOnce(
new AppParameters({
chainId: 'penumbra-testnet-mock',
sctParams: new SctParameters({
epochDuration: 719n,
}),
}),
);
await expect(transactionPlanner(req, mockCtx)).rejects.toThrow('Gas prices is not available');
});
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
});
Loading
Loading