Skip to content

Commit

Permalink
LP action views (#1739)
Browse files Browse the repository at this point in the history
* Owned positions swap section

* working example

* Add metadata & action views

* Review updates

* Fix function labels
  • Loading branch information
grod220 authored Aug 29, 2024
1 parent c814666 commit e7d0767
Show file tree
Hide file tree
Showing 16 changed files with 567 additions and 28 deletions.
6 changes: 6 additions & 0 deletions .changeset/nice-mugs-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@penumbra-zone/query': minor
'@penumbra-zone/wasm': minor
---

Customize symbol for LP position NFTs
7 changes: 7 additions & 0 deletions .changeset/quick-buttons-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@penumbra-zone/perspective': minor
'minifront': minor
'@penumbra-zone/ui': minor
---

Support for displaying LP position action views
3 changes: 3 additions & 0 deletions apps/minifront/src/components/swap/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export const SwapLayout = () => {

<UnclaimedSwaps />

{/* TODO: Will enable in subsequent PR */}
{/* <LpPositions />*/}

<AuctionList />
</div>

Expand Down
27 changes: 27 additions & 0 deletions apps/minifront/src/components/swap/lp-positions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Card } from '@penumbra-zone/ui/components/ui/card';
import { GradientHeader } from '@penumbra-zone/ui/components/ui/gradient-header';
import { useOwnedPositions } from '../../state/swap/lp-positions.ts';
import { bech32mPositionId } from '@penumbra-zone/bech32m/plpid';

// TODO: Ids are not sufficient in taking action on these
// Required to move forward with this: https://github.com/penumbra-zone/penumbra/pull/4837
export const LpPositions = () => {
const { data, error } = useOwnedPositions();

return !data?.length ? (
<div className='hidden xl:block' />
) : (
<Card layout>
<GradientHeader layout>Limit orders</GradientHeader>
{!!error && <div>❌ There was an error loading your limit orders: ${String(error)}</div>}
{data.map(({ positionId }) => {
const base64Id = bech32mPositionId(positionId ?? { inner: new Uint8Array() });
return (
<div key={base64Id} className='flex items-center gap-4 p-2'>
{base64Id}
</div>
);
})}
</Card>
);
};
3 changes: 3 additions & 0 deletions apps/minifront/src/components/swap/swap-form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export const SwapForm = () => {
<DurationSlider />
</InputBlock>

{/* TODO: Enable in subsequent PR */}
{/* <LimitOrder />*/}

{duration === 'instant' ? (
<SimulateSwap layoutId={sharedLayoutId} />
) : (
Expand Down
20 changes: 20 additions & 0 deletions apps/minifront/src/components/swap/swap-form/limit-order.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { AllSlices } from '../../../state';
import { useStoreShallow } from '../../../utils/use-store-shallow.ts';

const limitOrderSelector = (state: AllSlices) => ({
assetIn: state.swap.assetIn,
assetOut: state.swap.assetOut,
amount: state.swap.amount,
onSubmit: state.swap.lpPositions.onSubmit,
});

export const LimitOrder = () => {
const { onSubmit } = useStoreShallow(limitOrderSelector);

return (
<div>
<h1>Limit order</h1>
<button onClick={() => void onSubmit()}>SEND LIMIT ORDER</button>
</div>
);
};
7 changes: 5 additions & 2 deletions apps/minifront/src/state/swap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ import { createPriceHistorySlice, PriceHistorySlice } from './price-history';
import { isValidAmount } from '../helpers';

import { setSwapQueryParams } from './query-params';
import { swappableBalancesResponsesSelector, swappableAssetsSelector } from './helpers';
import { swappableAssetsSelector, swappableBalancesResponsesSelector } from './helpers';
import { getMetadataFromBalancesResponse } from '@penumbra-zone/getters/balances-response';
import { getAddressIndex } from '@penumbra-zone/getters/address-view';
import { emptyBalanceResponse } from '../../utils/empty-balance-response';
import {
balancesResponseAndMetadataAreSameAsset,
getBalanceByMatchingMetadataAndAddressIndex,
getFirstBalancesResponseMatchingMetadata,
getFirstBalancesResponseNotMatchingMetadata,
getFirstMetadataNotMatchingBalancesResponse,
getBalanceByMatchingMetadataAndAddressIndex,
} from './getters';
import { createLpPositionsSlice, LpPositionsSlice } from './lp-positions.ts';

export interface SimulateSwapResult {
metadataByAssetId: Record<string, Metadata>;
Expand Down Expand Up @@ -58,6 +59,7 @@ interface Subslices {
dutchAuction: DutchAuctionSlice;
instantSwap: InstantSwapSlice;
priceHistory: PriceHistorySlice;
lpPositions: LpPositionsSlice;
}

const INITIAL_STATE: State = {
Expand All @@ -72,6 +74,7 @@ export const createSwapSlice = (): SliceCreator<SwapSlice> => (set, get, store)
...INITIAL_STATE,
dutchAuction: createDutchAuctionSlice()(set, get, store),
instantSwap: createInstantSwapSlice()(set, get, store),
lpPositions: createLpPositionsSlice()(set, get, store),
priceHistory: createPriceHistorySlice()(set, get, store),
setAssetIn: asset => {
get().swap.resetSubslices();
Expand Down
125 changes: 125 additions & 0 deletions apps/minifront/src/state/swap/lp-positions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { SliceCreator, useStore } from '..';
import { createZQuery, ZQueryState } from '@penumbra-zone/zquery';
import { penumbra } from '../../prax.ts';
import { ViewService } from '@penumbra-zone/protobuf/penumbra/view/v1/view_connect';
import {
OwnedPositionIdsResponse,
TransactionPlannerRequest,
} from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';
import { SwapSlice } from './index.ts';
import { isValidAmount, planBuildBroadcast } from '../helpers.ts';
import { getAddressIndex } from '@penumbra-zone/getters/address-view';
import { getAssetIdFromValueView } from '@penumbra-zone/getters/value-view';
import { PositionState_PositionStateEnum } from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb';
import { base64ToUint8Array } from '@penumbra-zone/types/base64';

export const { ownedPositions, useOwnedPositions } = createZQuery({
name: 'ownedPositions',
fetch: () => penumbra.service(ViewService).ownedPositionIds({}),
stream: () => {
return {
onValue: (
prevState: OwnedPositionIdsResponse[] | undefined,
response: OwnedPositionIdsResponse,
) => {
return [...(prevState ?? []), response];
},
};
},
getUseStore: () => useStore,
get: state => state.swap.lpPositions.ownedPositions,
set: setter => {
const newState = setter(useStore.getState().swap.lpPositions.ownedPositions);
useStore.setState(state => {
state.swap.lpPositions.ownedPositions = newState;
});
},
});

interface Actions {
onSubmit: () => Promise<void>;
}

interface State {
ownedPositions: ZQueryState<OwnedPositionIdsResponse[]>;
}

export type LpPositionsSlice = Actions & State;

const INITIAL_STATE: State = {
ownedPositions,
};

export const createLpPositionsSlice = (): SliceCreator<LpPositionsSlice> => (set, get) => {
return {
...INITIAL_STATE,
onSubmit: async () => {
try {
set(state => {
state.swap.txInProgress = true;
});

const txPlannerRequest = assembleLimitOrderReq(get().swap);
await planBuildBroadcast('positionOpen', txPlannerRequest);

set(state => {
state.swap.amount = '';
});
get().shared.balancesResponses.revalidate();
} finally {
set(state => {
state.swap.txInProgress = false;
});
}
},
};
};

// TODO: This is temporary data for testing purposes. Update with inputs when component is ready.
const assembleLimitOrderReq = ({ assetIn, amount, assetOut }: SwapSlice) => {
if (!assetIn) {
throw new Error('`assetIn` is undefined');
}
if (!assetOut) {
throw new Error('`assetOut` is undefined');
}
if (!isValidAmount(amount, assetIn)) {
throw new Error('Invalid amount');
}

return new TransactionPlannerRequest({
positionOpens: [
{
position: {
phi: {
component: { p: { lo: 1000000n }, q: { lo: 1000000n } },
pair: {
asset1: getAssetIdFromValueView(assetIn.balanceView),
asset2: assetOut.penumbraAssetId,
},
},
nonce: crypto.getRandomValues(new Uint8Array(32)),
state: { state: PositionState_PositionStateEnum.OPENED },
reserves: { r1: { lo: 1n }, r2: {} },
closeOnFill: true,
},
},
],
positionCloses: [
{
positionId: { inner: base64ToUint8Array('/C9cn0d8veH0IGt2SCghzfcCWkPWbgUDXpXOPgZyA8c=') },
},
],
positionWithdraws: [
{
positionId: { inner: base64ToUint8Array('+vbub7BbEAAKLqRorZbNZ4yixPNVFzGl1BAexym3mDc=') },
reserves: { r1: { lo: 1000000n }, r2: {} },
tradingPair: {
asset1: getAssetIdFromValueView(assetIn.balanceView),
asset2: assetOut.penumbraAssetId,
},
},
],
source: getAddressIndex(assetIn.accountAddress),
});
};
83 changes: 72 additions & 11 deletions packages/perspective/src/plan/view-action-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,21 +379,82 @@ export const viewActionPlan =
actionView: actionPlan.action,
});

case undefined:
throw new Error('No action case in action plan');
default:
/**
* `<ActionViewComponent />` only renders data about the `spend` and
* `output` cases. For all other cases, it just renders the action name.
*
* @todo As we render more data about other action types, add them as
* cases above.
*/
case 'positionOpen':
return new ActionView({
actionView: actionPlan.action,
});

case 'positionClose':
return new ActionView({
actionView: actionPlan.action,
});

case 'positionWithdraw':
return new ActionView({
actionView: actionPlan.action,
});

case 'validatorDefinition': {
return new ActionView({
actionView: actionPlan.action,
});
}

case 'ibcRelayAction': {
return new ActionView({
actionView: actionPlan.action,
});
}

case 'proposalSubmit': {
return new ActionView({
actionView: actionPlan.action,
});
}

case 'proposalWithdraw': {
return new ActionView({
actionView: actionPlan.action,
});
}

case 'proposalDepositClaim': {
return new ActionView({
actionView: actionPlan.action,
});
}

case 'communityPoolSpend': {
return new ActionView({
actionView: actionPlan.action,
});
}

case 'communityPoolOutput': {
return new ActionView({
actionView: actionPlan.action,
});
}

case 'communityPoolDeposit': {
return new ActionView({
actionView: actionPlan.action,
});
}

// Deprecated
case 'positionRewardClaim': {
return new ActionView({
actionView: {
case: actionPlan.action.case,
case: 'positionRewardClaim',
value: {},
},
});
}

case undefined:
return new ActionView({
actionView: actionPlan.action,
});
}
};
6 changes: 4 additions & 2 deletions packages/query/src/block-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,8 +594,9 @@ export class BlockProcessor implements BlockProcessorInterface {
if (action.case === 'positionOpen' && action.value.position) {
for (const state of POSITION_STATES) {
const metadata = getLpNftMetadata(computePositionId(action.value.position), state);
const customized = customizeSymbol(metadata);
await this.indexedDb.saveAssetsMetadata({
...metadata,
...customized,
penumbraAssetId: getAssetId(metadata),
});
}
Expand All @@ -619,8 +620,9 @@ export class BlockProcessor implements BlockProcessorInterface {
sequence: action.value.sequence,
});
const metadata = getLpNftMetadata(action.value.positionId, positionState);
const customized = customizeSymbol(metadata);
await this.indexedDb.saveAssetsMetadata({
...metadata,
...customized,
penumbraAssetId: getAssetId(metadata),
});

Expand Down
9 changes: 6 additions & 3 deletions packages/ui/components/ui/tx/action-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { ActionDutchAuctionWithdrawViewComponent } from './actions-views/action-
import { ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { DelegatorVoteComponent } from './actions-views/delegator-vote.tsx';
import { ValidatorVoteComponent } from './actions-views/validator-vote.tsx';
import { PositionOpenComponent } from './actions-views/position-open.tsx';
import { PositionCloseComponent } from './actions-views/position-close.tsx';
import { PositionWithdrawComponent } from './actions-views/position-withdraw.tsx';

type Case = Exclude<ActionView['actionView']['case'], undefined>;

Expand Down Expand Up @@ -121,13 +124,13 @@ export const ActionViewComponent = ({
return <UnimplementedView label='Proposal Deposit Claim' />;

case 'positionOpen':
return <UnimplementedView label='Position Open' />;
return <PositionOpenComponent value={actionView.value} />;

case 'positionClose':
return <UnimplementedView label='Position Close' />;
return <PositionCloseComponent value={actionView.value} />;

case 'positionWithdraw':
return <UnimplementedView label='Position Withdraw' />;
return <PositionWithdrawComponent value={actionView.value} />;

case 'positionRewardClaim':
return <UnimplementedView label='Position Reward Claim' />;
Expand Down
Loading

0 comments on commit e7d0767

Please sign in to comment.