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

Create UI to schedule Dutch auctions #967

Merged
14 changes: 14 additions & 0 deletions apps/minifront/public/auction-gradient.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 5 additions & 4 deletions apps/minifront/src/components/dashboard/constants.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { DashboardTabMap } from './types';
import { PagePath } from '../metadata/paths';
import { EduPanel } from '../shared/edu-panels/content';
import { Tab } from '../shared/tabs';

export const dashboardTabs = [
{ title: 'Assets', href: PagePath.DASHBOARD, active: true },
{ title: 'Transactions', href: PagePath.TRANSACTIONS, active: true },
{ title: 'NFTs', href: PagePath.NFTS, active: false },
export const dashboardTabs: Tab[] = [
{ title: 'Assets', href: PagePath.DASHBOARD, enabled: true },
{ title: 'Transactions', href: PagePath.TRANSACTIONS, enabled: true },
{ title: 'NFTs', href: PagePath.NFTS, enabled: false },
];

export const dashboardTabsHelper: DashboardTabMap = {
Expand Down
8 changes: 6 additions & 2 deletions apps/minifront/src/components/metadata/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ export const metadata: Record<PagePath, PageMetadata> = {
},
[PagePath.SWAP]: {
title: 'Penumbra | Swap',
description: eduPanelContent[EduPanel.TEMP_FILLER],
description: eduPanelContent[EduPanel.SWAP],
},
[PagePath.SWAP_AUCTION]: {
title: 'Penumbra | Auction',
description: eduPanelContent[EduPanel.SWAP_AUCTION],
},
[PagePath.STAKING]: {
title: 'Penumbra | Staking',
description: eduPanelContent[EduPanel.TEMP_FILLER],
description: eduPanelContent[EduPanel.STAKING],
},
[PagePath.TRANSACTION_DETAILS]: {
title: 'Penumbra | Transaction',
Expand Down
1 change: 1 addition & 0 deletions apps/minifront/src/components/metadata/paths.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export enum PagePath {
INDEX = '/',
SWAP = '/swap',
SWAP_AUCTION = '/swap/auction',
SEND = '/send',
STAKING = '/staking',
RECEIVE = '/send/receive',
Expand Down
18 changes: 16 additions & 2 deletions apps/minifront/src/components/root-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import { SendAssetBalanceLoader, 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 { SwapLoader } from './swap/swap/swap-loader';
import { StakingLayout, StakingLoader } from './staking/layout';
import { IbcLoader } from './ibc/ibc-loader';
import { IbcLayout } from './ibc/layout';
import { Swap } from './swap/swap';
import { DutchAuction } from './swap/dutch-auction';
import { DutchAuctionLoader } from './swap/dutch-auction/dutch-auction-loader';

export const rootRouter = createHashRouter([
{
Expand Down Expand Up @@ -57,8 +60,19 @@ export const rootRouter = createHashRouter([
},
{
path: PagePath.SWAP,
loader: SwapLoader,
element: <SwapLayout />,
children: [
{
index: true,
loader: SwapLoader,
element: <Swap />,
},
{
path: PagePath.SWAP_AUCTION,
loader: DutchAuctionLoader,
element: <DutchAuction />,
},
],
},
{
path: PagePath.TRANSACTION_DETAILS,
Expand Down
4 changes: 2 additions & 2 deletions apps/minifront/src/components/send/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ export const sendTabsHelper: SendTabMap = {
};

export const sendTabs = [
{ title: 'Send', href: PagePath.SEND, active: true },
{ title: 'Receive', href: PagePath.RECEIVE, active: true },
{ title: 'Send', href: PagePath.SEND, enabled: true },
{ title: 'Receive', href: PagePath.RECEIVE, enabled: true },
];
19 changes: 11 additions & 8 deletions apps/minifront/src/components/shared/asset-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/valu
import { useEffect, useMemo, useState } from 'react';
import { IconInput } from '@penumbra-zone/ui/components/ui/icon-input';
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
import { SwapLoaderResponse } from '../swap/swap-loader';
import { useLoaderData } from 'react-router-dom';

interface AssetSelectorProps {
assets: Metadata[];
value?: Metadata;
onChange: (metadata: Metadata) => void;
/**
Expand Down Expand Up @@ -46,8 +45,7 @@ const switchAssetIfNecessary = ({
}
};

const useFilteredAssets = ({ value, onChange, filter }: AssetSelectorProps) => {
const { assets } = useLoaderData() as SwapLoaderResponse;
jessepinho marked this conversation as resolved.
Show resolved Hide resolved
const useFilteredAssets = ({ assets, value, onChange, filter }: AssetSelectorProps) => {
const sortedAssets = useMemo(
() =>
[...assets].sort((a, b) =>
Expand All @@ -66,7 +64,7 @@ const useFilteredAssets = ({ value, onChange, filter }: AssetSelectorProps) => {
[filter, value, filteredAssets, onChange],
);

return { assets: filteredAssets, search, setSearch };
return { filteredAssets, search, setSearch };
};

const bySearch = (search: string) => (asset: Metadata) =>
Expand All @@ -80,8 +78,13 @@ const bySearch = (search: string) => (asset: Metadata) =>
* For an asset selector that picks from the user's balances, use
* `<BalanceSelector />`.
*/
export const AssetSelector = ({ onChange, value, filter }: AssetSelectorProps) => {
const { assets, search, setSearch } = useFilteredAssets({ value, onChange, filter });
export const AssetSelector = ({ assets, onChange, value, filter }: AssetSelectorProps) => {
const { filteredAssets, search, setSearch } = useFilteredAssets({
assets,
value,
onChange,
filter,
});

/**
* @todo: Refactor to not use `ValueViewComponent`, since it's not intended to
Expand Down Expand Up @@ -109,7 +112,7 @@ export const AssetSelector = ({ onChange, value, filter }: AssetSelectorProps) =
onChange={setSearch}
placeholder='Search assets...'
/>
{assets.map(metadata => (
{filteredAssets.map(metadata => (
<div key={metadata.display} className='flex flex-col'>
<DialogClose>
<div
Expand Down
3 changes: 3 additions & 0 deletions apps/minifront/src/components/shared/edu-panels/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum EduPanel {
RECEIVING_FUNDS,
IBC_WITHDRAW,
SWAP,
SWAP_AUCTION,
STAKING,
TEMP_FILLER,
}
Expand All @@ -25,6 +26,8 @@ export const eduPanelContent: Record<EduPanel, string> = {
'IBC to a connected chain. Note that if the chain is a transparent chain, the transaction will be visible to others.',
[EduPanel.SWAP]:
'Shielded swaps between any kind of cryptoasset, with sealed-bid, batch pricing and no frontrunning. Only the batch totals are revealed, providing long-term privacy. Penumbra has no MEV, because transactions do not leak data about user activity.',
[EduPanel.SWAP_AUCTION]:
"Offer a specific quantity of cryptocurrency at decreasing prices until all the tokens are sold. Buyers can place bids at the price they're willing to pay, with the auction concluding when all tokens are sold or when the auction time expires. This mechanism allows for price discovery based on market demand, with participants potentially acquiring tokens at prices lower than initially offered.",
jessepinho marked this conversation as resolved.
Show resolved Hide resolved
[EduPanel.STAKING]:
'Explore the available validator nodes and their associated rewards, performance metrics, and staking requirements. Select the validator you wish to delegate your tokens to, based on factors like uptime, reputation, and expected returns. Stay informed about validator performance updates, rewards distribution, and any network upgrades to ensure a seamless staking experience.',
[EduPanel.TEMP_FILLER]:
Expand Down
10 changes: 8 additions & 2 deletions apps/minifront/src/components/shared/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import { cn } from '@penumbra-zone/ui/lib/utils';
import { PagePath } from '../metadata/paths';
import { useNavigate } from 'react-router-dom';

export interface Tab {
title: string;
enabled: boolean;
href: PagePath;
}

interface TabsProps {
tabs: { title: string; active: boolean; href: PagePath }[];
tabs: Tab[];
activeTab: PagePath;
className?: string;
}
Expand All @@ -21,7 +27,7 @@ export const Tabs = ({ tabs, activeTab, className }: TabsProps) => {
>
{tabs.map(
tab =>
tab.active && (
tab.enabled && (
<Button
className={cn(
'w-full transition-all',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Slider } from '@penumbra-zone/ui/components/ui/slider';
import { DURATION_OPTIONS } from '../../../state/dutch-auction/constants';
import { useStoreShallow } from '../../../utils/use-store-shallow';
import { AllSlices } from '../../../state';

const durationSliderSelector = (state: AllSlices) => ({
duration: state.dutchAuction.duration,
setDuration: state.dutchAuction.setDuration,
});

export const DurationSlider = () => {
jessepinho marked this conversation as resolved.
Show resolved Hide resolved
const { duration, setDuration } = useStoreShallow(durationSliderSelector);

const handleChange = (newValue: number[]) => {
const value = newValue[0]!; // We don't use multiple values in the slider
const option = DURATION_OPTIONS[value]!;

setDuration(option);
};

return (
<div className='flex flex-col items-center gap-4'>
<Slider
min={0}
max={DURATION_OPTIONS.length - 1}
value={[DURATION_OPTIONS.indexOf(duration)]}
onValueChange={handleChange}
/>
{duration}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Button } from '@penumbra-zone/ui/components/ui/button';
import { AllSlices } from '../../../state';
import { useStoreShallow } from '../../../utils/use-store-shallow';
import { InputBlock } from '../../shared/input-block';
import InputToken from '../../shared/input-token';
import { DurationSlider } from './duration-slider';
import { Price } from './price';

const dutchAuctionFormSelector = (state: AllSlices) => ({
balances: state.dutchAuction.balancesResponses,
assetIn: state.dutchAuction.assetIn,
setAssetIn: state.dutchAuction.setAssetIn,
amount: state.dutchAuction.amount,
setAmount: state.dutchAuction.setAmount,
onSubmit: state.dutchAuction.onSubmit,
submitButtonDisabled: state.dutchAuction.txInProgress || !state.dutchAuction.amount,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should block if min/max isn't chosen too right?

});

export const DutchAuctionForm = () => {
jessepinho marked this conversation as resolved.
Show resolved Hide resolved
const { amount, setAmount, assetIn, setAssetIn, balances, onSubmit, submitButtonDisabled } =
useStoreShallow(dutchAuctionFormSelector);

return (
<form
className='flex flex-col gap-4 xl:gap-3'
onSubmit={e => {
e.preventDefault();
void onSubmit();
}}
>
<InputToken
label='Amount to sell'
balances={balances}
selection={assetIn}
setSelection={setAssetIn}
value={amount}
onChange={e => {
if (Number(e.target.value) < 0) return;
setAmount(e.target.value);
}}
placeholder='Enter an amount'
/>

<InputBlock label='Duration'>
<div className='pt-2'>
<DurationSlider />
</div>
</InputBlock>

<InputBlock label='Price'>
<div className='pt-2'>
<Price />
</div>
</InputBlock>

<Button variant='gradient' type='submit' disabled={submitButtonDisabled}>
Start auctions
</Button>
</form>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { throwIfPraxNotConnectedTimeout } from '@penumbra-zone/client';
import { getSwappableBalancesResponses } from '../helpers';
import { useStore } from '../../../state';
import { getAllAssets } from '../../../fetchers/assets';

export const DutchAuctionLoader = async () => {
await throwIfPraxNotConnectedTimeout();

const [assets, balancesResponses] = await Promise.all([
getAllAssets(),
getSwappableBalancesResponses(),
]);
useStore.getState().dutchAuction.setBalancesResponses(balancesResponses);

if (balancesResponses[0]) {
useStore.getState().dutchAuction.setAssetIn(balancesResponses[0]);
useStore.getState().dutchAuction.setAssetOut(assets[0]!);
}

return assets;
};
23 changes: 23 additions & 0 deletions apps/minifront/src/components/swap/dutch-auction/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Card } from '@penumbra-zone/ui/components/ui/card';
import { EduPanel } from '../../shared/edu-panels/content';
import { EduInfoCard } from '../../shared/edu-panels/edu-info-card';
import { DutchAuctionForm } from './dutch-auction-form';

export const DutchAuction = () => {
jessepinho marked this conversation as resolved.
Show resolved Hide resolved
return (
<div className='grid gap-6 md:grid-cols-2 md:gap-4 xl:grid-cols-3 xl:gap-5'>
<div className='hidden xl:block'></div>

<Card gradient className='order-3 row-span-2 flex-1 p-5 md:order-1 md:p-4 xl:p-5'>
<DutchAuctionForm />
</Card>

<EduInfoCard
className='row-span-1 md:order-2'
src='./auction-gradient.svg'
label='Dutch Auction'
content={EduPanel.SWAP_AUCTION}
/>
</div>
);
};
Loading
Loading