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

Limit Orders #181

Merged
merged 45 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
de0be08
Add limit order form
JasonMHasperhoven Dec 3, 2024
a5e2b31
Construct limit order position
JasonMHasperhoven Dec 3, 2024
a9117e7
Refactor order form stores & introduce pnum util
JasonMHasperhoven Dec 4, 2024
74782f0
Update pnum description
JasonMHasperhoven Dec 4, 2024
2df6924
Use remote pnum
JasonMHasperhoven Dec 5, 2024
aae366e
Fix lint issues
JasonMHasperhoven Dec 5, 2024
8d54da7
Fix handling of input fields numbers
JasonMHasperhoven Dec 6, 2024
178013f
tmp commit
JasonMHasperhoven Dec 9, 2024
ffcdd28
Convert to base units
JasonMHasperhoven Dec 9, 2024
925091f
Start fleshing out shared position logic
cronokirby Dec 10, 2024
6cec7ed
Add tests for position math
cronokirby Dec 10, 2024
0da213e
Integrate position logic into range liquidity form
cronokirby Dec 10, 2024
c2f5f8d
Define limit order position function
cronokirby Dec 10, 2024
77d2c33
Add some documentation to position math
cronokirby Dec 10, 2024
a85105d
Wire up limit position form
cronokirby Dec 10, 2024
707034a
Remove slider from limit order form
JasonMHasperhoven Dec 10, 2024
f1f15e3
Remove key error
JasonMHasperhoven Dec 10, 2024
c943e9c
Apply same p,q,r1,r2 values as rust code
JasonMHasperhoven Dec 10, 2024
4538e10
Construct positions taking into account asset order
cronokirby Dec 10, 2024
f812cdf
Have position plans take in display units for reserves
cronokirby Dec 10, 2024
2187663
Fix limit order amount handling + gas fee calc
JasonMHasperhoven Dec 10, 2024
cc83543
Fix display amounts for range liquidity form
JasonMHasperhoven Dec 10, 2024
05b8d16
Bug: don't display weird text in order input
cronokirby Dec 11, 2024
62868fb
Define PriceLinkedInputs store
cronokirby Dec 11, 2024
ea5dcf5
Define new store for MarketOrderForm
cronokirby Dec 11, 2024
15e2e33
Move AssetInfo to separate file
cronokirby Dec 12, 2024
9e6e637
Add store for limit order position
cronokirby Dec 12, 2024
a55dcd8
Add RangeOrderFormStore
cronokirby Dec 12, 2024
ca51fa1
Basic wiring of form refactor
cronokirby Dec 12, 2024
58c0096
Implement order submission
cronokirby Dec 12, 2024
df756fc
Fix range liquidity fee tier selection
cronokirby Dec 12, 2024
4a9c90f
Correct range liquidity
cronokirby Dec 12, 2024
b3dc667
Gas fee calculation, remove unused stuff
cronokirby Dec 12, 2024
228196c
Fix remaining lints
cronokirby Dec 12, 2024
238d24a
Correct order of p and q in position construction
cronokirby Dec 12, 2024
b29b48e
Use a more robust method of getting p and q in range
cronokirby Dec 12, 2024
86d6027
Remove wheel input changes
JasonMHasperhoven Dec 13, 2024
cfc3256
Fix market slider input change
JasonMHasperhoven Dec 13, 2024
e82eb3a
Refactor setter methods for mobx strict mode, fix rerenders
JasonMHasperhoven Dec 13, 2024
c19a322
Fix hydration issue caused by connectionStore.connected
JasonMHasperhoven Dec 13, 2024
be9c496
Fix inputs disappearing
JasonMHasperhoven Dec 16, 2024
a95d19c
Add active states to select groups
JasonMHasperhoven Dec 16, 2024
790e21a
Merge branch 'main' into limitorders
JasonMHasperhoven Dec 16, 2024
8d38dcc
Remove setup connection hoc
JasonMHasperhoven Dec 16, 2024
72ca46c
Wrapp app with observer
JasonMHasperhoven Dec 16, 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
13 changes: 9 additions & 4 deletions app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
'use client';

import { ReactNode } from 'react';
import { enableStaticRendering } from 'mobx-react-lite';
import { ReactNode, useEffect } from 'react';
import { enableStaticRendering, observer } from 'mobx-react-lite';
import { QueryClientProvider } from '@tanstack/react-query';
import { ToastProvider } from '@penumbra-zone/ui/Toast';
import { TooltipProvider } from '@penumbra-zone/ui/Tooltip';
import { Header, SyncBar } from '@/widgets/header';
import { queryClient } from '@/shared/const/queryClient';
import { connectionStore } from '@/shared/model/connection';

// Used so that observer() won't subscribe to any observables used in an SSR environment
// and no garbage collection problems are introduced.
enableStaticRendering(typeof window === 'undefined');

export const App = ({ children }: { children: ReactNode }) => {
export const App = observer(({ children }: { children: ReactNode }) => {
useEffect(() => {
connectionStore.setup();
}, []);

return (
<QueryClientProvider client={queryClient}>
<TooltipProvider>
Expand All @@ -25,4 +30,4 @@ export const App = ({ children }: { children: ReactNode }) => {
</TooltipProvider>
</QueryClientProvider>
);
};
});
57 changes: 57 additions & 0 deletions src/pages/trade/model/AssetInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { AssetId, Metadata, Value } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { Amount } from '@penumbra-zone/protobuf/penumbra/core/num/v1/num_pb';
import { pnum } from '@penumbra-zone/types/pnum';

/** A basic utility class containing information we need about an asset.
*
* This extracts out the useful components we might need for the current
* asset.
*/
export class AssetInfo {
/**
* @param balance the balance, in display units.
* @param the exponent to convert from base units to display units.
*/
constructor(
public id: AssetId,
public exponent: number,
public symbol: string,
public balance?: number,
) {}

static fromMetadata(metadata: Metadata, balance?: Amount): undefined | AssetInfo {
const displayDenom = metadata.denomUnits.find(x => x.denom === metadata.display);
if (!displayDenom || !metadata.penumbraAssetId) {
return undefined;
}
return new AssetInfo(
metadata.penumbraAssetId,
displayDenom.exponent,
metadata.symbol,
balance && pnum(balance, displayDenom.exponent).toNumber(),
);
}

/** Convert an amount, in display units, into a Value (of this asset). */
value(display: number): Value {
return new Value({
amount: pnum(display, this.exponent).toAmount(),
assetId: this.id,
});
}

/** Format an amount (in display units) as a simple string. */
formatDisplayAmount(amount: number): string {
const amountString = pnum(amount, this.exponent).toFormattedString({
commas: true,
decimals: 4,
trailingZeros: false,
});
return `${amountString} ${this.symbol}`;
}

/** Format the balance of this asset as a simple string. */
formatBalance(): undefined | string {
return this.balance !== undefined ? this.formatDisplayAmount(this.balance) : undefined;
}
}
9 changes: 9 additions & 0 deletions src/pages/trade/model/useMarketPrice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useSummary } from './useSummary';

export const useMarketPrice = () => {
const { data: summary } = useSummary('1d');
if (!summary || 'noData' in summary) {
return undefined;
}
return summary.price;
};
39 changes: 18 additions & 21 deletions src/pages/trade/ui/form-tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,41 @@
import { useState } from 'react';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import { Tabs } from '@penumbra-zone/ui/Tabs';
import { Density } from '@penumbra-zone/ui/Density';
import { MarketOrderForm } from './order-form/order-form-market';
import { LimitOrderForm } from './order-form/order-form-limit';
import { RangeLiquidityOrderForm } from './order-form/order-form-range-liquidity';
import { isWhichForm, useOrderFormStore } from './order-form/store/OrderFormStore';
import { observer } from 'mobx-react-lite';

enum FormTabsType {
Market = 'market',
Limit = 'limit',
Range = 'range',
}

export const FormTabs = () => {
export const FormTabs = observer(() => {
const [parent] = useAutoAnimate();
const [tab, setTab] = useState<FormTabsType>(FormTabsType.Market);
const store = useOrderFormStore();

return (
<div ref={parent} className='flex flex-col'>
<div className='px-4 lg:pt-2 border-b border-b-other-solidStroke'>
<Density compact>
<Tabs
value={tab}
value={store.whichForm}
actionType='accent'
onChange={value => setTab(value as FormTabsType)}
onChange={value => {
if (isWhichForm(value)) {
store.setWhichForm(value);
}
}}
options={[
{ value: FormTabsType.Market, label: 'Market' },
{ value: FormTabsType.Limit, label: 'Limit' },
{ value: FormTabsType.Range, label: 'Range Liquidity' },
{ value: 'Market', label: 'Market' },
{ value: 'Limit', label: 'Limit' },
{ value: 'Range', label: 'Range Liquidity' },
]}
/>
</Density>
</div>

<div className='overflow-y-auto'>
{tab === FormTabsType.Market && <MarketOrderForm />}
{tab === FormTabsType.Limit && (
<div className='h-[380px] p-4 text-text-secondary'>Limit order form</div>
)}
{tab === FormTabsType.Range && <RangeLiquidityOrderForm />}
{store.whichForm === 'Market' && <MarketOrderForm parentStore={store} />}
{store.whichForm === 'Limit' && <LimitOrderForm parentStore={store} />}
{store.whichForm === 'Range' && <RangeLiquidityOrderForm parentStore={store} />}
</div>
</div>
);
};
});
20 changes: 20 additions & 0 deletions src/pages/trade/ui/order-form/info-row-gas-fee.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { InfoRow } from './info-row';

export const InfoRowGasFee = ({
gasFee,
symbol,
isLoading,
}: {
gasFee: string;
symbol: string;
isLoading: boolean;
}) => {
return (
<InfoRow
label='Gas Fee'
isLoading={isLoading}
value={`${gasFee} ${symbol}`}
toolTip='The gas cost of the transaction. Gas fees are burned as part of transaction processing.'
/>
);
};
12 changes: 12 additions & 0 deletions src/pages/trade/ui/order-form/info-row-trading-fee.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { InfoRow } from './info-row';

export const InfoRowTradingFee = () => {
return (
<InfoRow
label='Trading Fee'
value='Free'
valueColor='success'
toolTip='Penumbra has no platform trading fee. LPs set their own fees, which are included in the quoted price.'
/>
);
};
2 changes: 1 addition & 1 deletion src/pages/trade/ui/order-form/info-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const getValueColor = (valueColor: InfoRowProps['valueColor']) => {
if (valueColor === 'error') {
return 'destructive.main';
}
return 'text.secondary';
return 'text.primary';
};

export const InfoRow = observer(
Expand Down
89 changes: 89 additions & 0 deletions src/pages/trade/ui/order-form/order-form-limit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { observer } from 'mobx-react-lite';
import { Button } from '@penumbra-zone/ui/Button';
import { Text } from '@penumbra-zone/ui/Text';
import { connectionStore } from '@/shared/model/connection';
import { OrderInput } from './order-input';
import { SegmentedControl } from './segmented-control';
import { ConnectButton } from '@/features/connect/connect-button';
import { InfoRowTradingFee } from './info-row-trading-fee';
import { InfoRowGasFee } from './info-row-gas-fee';
import { SelectGroup } from './select-group';
import { OrderFormStore } from './store/OrderFormStore';
import { BuyLimitOrderOptions, SellLimitOrderOptions } from './store/LimitOrderFormStore';

export const LimitOrderForm = observer(({ parentStore }: { parentStore: OrderFormStore }) => {
const { connected } = connectionStore;
const store = parentStore.limitForm;

const isBuy = store.direction === 'buy';

return (
<div className='p-4'>
<SegmentedControl direction={store.direction} setDirection={store.setDirection} />
<div className='mb-4'>
<div className='mb-2'>
<OrderInput
label={`When ${store.baseAsset?.symbol} is`}
value={store.priceInput}
onChange={price => store.setPriceInput(price)}
denominator={store.quoteAsset?.symbol}
/>
</div>
<SelectGroup
options={Object.values(isBuy ? BuyLimitOrderOptions : SellLimitOrderOptions)}
value={store.priceInputOption}
onChange={option =>
store.setPriceInputOption(option as BuyLimitOrderOptions | SellLimitOrderOptions)
}
/>
</div>
<div className='mb-4'>
<OrderInput
label={isBuy ? 'Buy' : 'Sell'}
value={store.baseInput}
onChange={store.setBaseInput}
denominator={store.baseAsset?.symbol}
/>
</div>
<div className='mb-4'>
<OrderInput
label={isBuy ? 'Pay with' : 'Receive'}
value={store.quoteInput}
onChange={store.setQuoteInput}
denominator={store.quoteAsset?.symbol}
/>
</div>
<div className='mb-4'>
<InfoRowTradingFee />
<InfoRowGasFee
gasFee={parentStore.gasFee.display}
symbol={parentStore.gasFee.symbol}
isLoading={parentStore.gasFeeLoading}
/>
</div>
<div className='mb-4'>
{connected ? (
<Button
actionType='accent'
disabled={!parentStore.canSubmit}
onClick={() => void parentStore.submit()}
>
{isBuy ? 'Buy' : 'Sell'} {store.baseAsset?.symbol}
</Button>
) : (
<ConnectButton actionType='default' />
)}
</div>
{parentStore.marketPrice && (
<div className='flex justify-center p-1'>
<Text small color='text.secondary'>
1 {store.baseAsset?.symbol} ={' '}
<Text small color='text.primary'>
{store.quoteAsset?.formatDisplayAmount(parentStore.marketPrice)}
</Text>
</Text>
</div>
)}
</div>
);
});
Loading