From e5406df7aa846f032faa848a963ce8d954d06410 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Mon, 2 Sep 2024 16:36:08 +0200 Subject: [PATCH 1/3] Add lp positions list --- apps/minifront/src/components/swap/layout.tsx | 4 +- .../src/components/swap/lp-positions.tsx | 64 +++++++++++-- apps/minifront/src/state/swap/lp-positions.ts | 90 ++++++++++++++++--- packages/query/src/block-processor.ts | 1 + 4 files changed, 140 insertions(+), 19 deletions(-) diff --git a/apps/minifront/src/components/swap/layout.tsx b/apps/minifront/src/components/swap/layout.tsx index 0b2bc29509..83f2aa0a0d 100644 --- a/apps/minifront/src/components/swap/layout.tsx +++ b/apps/minifront/src/components/swap/layout.tsx @@ -4,6 +4,7 @@ import { UnclaimedSwaps } from './unclaimed-swaps'; import { AuctionList } from './auction-list'; import { SwapInfoCard } from './swap-info-card'; import { LayoutGroup } from 'framer-motion'; +import { LpPositions } from './lp-positions.tsx'; export const SwapLayout = () => { return ( @@ -15,8 +16,7 @@ export const SwapLayout = () => { - {/* TODO: Will enable in subsequent PR */} - {/* */} + diff --git a/apps/minifront/src/components/swap/lp-positions.tsx b/apps/minifront/src/components/swap/lp-positions.tsx index 161e4e4219..6034ef6503 100644 --- a/apps/minifront/src/components/swap/lp-positions.tsx +++ b/apps/minifront/src/components/swap/lp-positions.tsx @@ -2,9 +2,33 @@ 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'; +import { PositionState_PositionStateEnum } from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; +import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/value'; +import { uint8ArrayToBase64 } from '@penumbra-zone/types/base64'; + +const stateToString = (state?: PositionState_PositionStateEnum): string => { + switch (state) { + case PositionState_PositionStateEnum.UNSPECIFIED: { + return 'UNSPECIFIED'; + } + case PositionState_PositionStateEnum.OPENED: { + return 'OPENED'; + } + case PositionState_PositionStateEnum.CLOSED: { + return 'CLOSED'; + } + case PositionState_PositionStateEnum.WITHDRAWN: { + return 'WITHDRAWN'; + } + case PositionState_PositionStateEnum.CLAIMED: { + return 'CLAIMED'; + } + case undefined: { + return 'UNSPECIFIED'; + } + } +}; -// 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(); @@ -14,11 +38,39 @@ export const LpPositions = () => { Limit orders {!!error &&
❌ There was an error loading your limit orders: ${String(error)}
} - {data.map(({ positionId }) => { - const base64Id = bech32mPositionId(positionId ?? { inner: new Uint8Array() }); + {data.map(({ position, id, r1ValueView, r2ValueView }) => { + const bech32Id = bech32mPositionId(id); + const base64Id = uint8ArrayToBase64(id.inner); return ( -
- {base64Id} +
+
{bech32Id}
+
{base64Id}
+
{stateToString(position.state?.state)}
+ + + + + {/*
{position.state?.sequence ? position.state.sequence.toString() : '0'}
*/} + +
+ {!!position.phi?.component?.fee && position.phi.component.fee} +
+ + {/*
{uint8ArrayToBase64(position.nonce)}
*/} + + {/*
{position.closeOnFill ? 'true' : 'false'}
*/} + + {/*
*/} + {/* {position.phi?.component?.p && joinLoHiAmount(position.phi.component.p).toString()}*/} + {/*
*/} + + {/*
*/} + {/* {position.phi?.component?.q && joinLoHiAmount(position.phi.component.q).toString()}*/} + {/*
*/} + + {/*
{position.reserves?.r1 && joinLoHiAmount(position.reserves.r1).toString()}
*/} + + {/*
{position.reserves?.r2 && joinLoHiAmount(position.reserves.r2).toString()}
*/}
); })} diff --git a/apps/minifront/src/state/swap/lp-positions.ts b/apps/minifront/src/state/swap/lp-positions.ts index 88fedc5561..7d8c98bfd6 100644 --- a/apps/minifront/src/state/swap/lp-positions.ts +++ b/apps/minifront/src/state/swap/lp-positions.ts @@ -2,26 +2,94 @@ 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 { 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 { + Position, + PositionId, + PositionState_PositionStateEnum, +} from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; import { base64ToUint8Array } from '@penumbra-zone/types/base64'; +import { DexService } from '@penumbra-zone/protobuf'; +import { AssetId, ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; +import { Amount } from '@penumbra-zone/protobuf/penumbra/core/num/v1/num_pb'; + +export interface PositionWithId { + id: PositionId; + position: Position; + r1ValueView: ValueView; + r2ValueView: ValueView; +} + +const toValueView = async (assetId?: AssetId, amount?: Amount) => { + if (!assetId) { + return new ValueView({ + valueView: { case: 'unknownAssetId', value: { amount, assetId } }, + }); + } + + const { denomMetadata } = await penumbra.service(ViewService).assetMetadataById({ assetId }); + if (denomMetadata) { + return new ValueView({ + valueView: { case: 'knownAssetId', value: { amount, metadata: denomMetadata } }, + }); + } else { + return new ValueView({ + valueView: { case: 'unknownAssetId', value: { amount, assetId } }, + }); + } +}; + +// Collects the stream of owned positions and then yields the positions results in a stream +const fetchOwnedPositions = async function* (): AsyncIterable { + // We only care about opened and closed state. If withdrawn, not helpful to display in the UI. + const openedIds = await Array.fromAsync( + penumbra + .service(ViewService) + .ownedPositionIds({ positionState: { state: PositionState_PositionStateEnum.OPENED } }), + ); + const closedIds = await Array.fromAsync( + penumbra + .service(ViewService) + .ownedPositionIds({ positionState: { state: PositionState_PositionStateEnum.CLOSED } }), + ); + + const allPositionIds = [...closedIds].map(i => i.positionId).filter(Boolean) as PositionId[]; + + const iterable = penumbra + .service(DexService) + .liquidityPositionsById({ positionId: allPositionIds }); + + let index = 0; + for await (const res of iterable) { + console.log(res); + const id = allPositionIds[index]; // responses are emitted in the order of the input ids + if (!id) { + throw new Error(`No corresponding ID in request array for position index ${index}`); + } + + if (res.data) { + yield { + id, + position: res.data, + r1ValueView: await toValueView(res.data.phi?.pair?.asset1, res.data.reserves?.r1), + r2ValueView: await toValueView(res.data.phi?.pair?.asset2, res.data.reserves?.r2), + }; + } + + index = index + 1; + } +}; export const { ownedPositions, useOwnedPositions } = createZQuery({ name: 'ownedPositions', - fetch: () => penumbra.service(ViewService).ownedPositionIds({}), + fetch: fetchOwnedPositions, stream: () => { return { - onValue: ( - prevState: OwnedPositionIdsResponse[] | undefined, - response: OwnedPositionIdsResponse, - ) => { + onValue: (prevState: PositionWithId[] | undefined, response: PositionWithId) => { return [...(prevState ?? []), response]; }, }; @@ -41,7 +109,7 @@ interface Actions { } interface State { - ownedPositions: ZQueryState; + ownedPositions: ZQueryState; } export type LpPositionsSlice = Actions & State; diff --git a/packages/query/src/block-processor.ts b/packages/query/src/block-processor.ts index 356869ed1a..b36bf5d5ff 100644 --- a/packages/query/src/block-processor.ts +++ b/packages/query/src/block-processor.ts @@ -460,6 +460,7 @@ export class BlockProcessor implements BlockProcessorInterface { for (const tx of blockTx) { let txId: TransactionId | undefined; + // Something here? Not getting closed/withdrawn actions const txCommitments = (tx.body?.actions ?? []).flatMap(({ action }) => { switch (action.case) { case 'output': From a87b5d03455fa4dc128482f25183f2f49d104255 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Fri, 13 Sep 2024 12:02:43 +0200 Subject: [PATCH 2/3] Displays positions --- .../src/components/swap/lp-positions.tsx | 81 ++++++++++--------- apps/minifront/src/state/swap/lp-positions.ts | 17 +++- 2 files changed, 59 insertions(+), 39 deletions(-) diff --git a/apps/minifront/src/components/swap/lp-positions.tsx b/apps/minifront/src/components/swap/lp-positions.tsx index 6034ef6503..301411b3f2 100644 --- a/apps/minifront/src/components/swap/lp-positions.tsx +++ b/apps/minifront/src/components/swap/lp-positions.tsx @@ -4,7 +4,8 @@ import { useOwnedPositions } from '../../state/swap/lp-positions.ts'; import { bech32mPositionId } from '@penumbra-zone/bech32m/plpid'; import { PositionState_PositionStateEnum } from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/value'; -import { uint8ArrayToBase64 } from '@penumbra-zone/types/base64'; +import { Button } from '@penumbra-zone/ui/components/ui/button'; +import { cn } from '@penumbra-zone/ui/lib/utils'; const stateToString = (state?: PositionState_PositionStateEnum): string => { switch (state) { @@ -38,42 +39,50 @@ export const LpPositions = () => { Limit orders {!!error &&
❌ There was an error loading your limit orders: ${String(error)}
} - {data.map(({ position, id, r1ValueView, r2ValueView }) => { - const bech32Id = bech32mPositionId(id); - const base64Id = uint8ArrayToBase64(id.inner); - return ( -
-
{bech32Id}
-
{base64Id}
-
{stateToString(position.state?.state)}
- - - - - {/*
{position.state?.sequence ? position.state.sequence.toString() : '0'}
*/} - -
- {!!position.phi?.component?.fee && position.phi.component.fee} +
+ {data.map(({ position, id, r1ValueView, r2ValueView }) => { + const bech32Id = bech32mPositionId(id); + return ( +
+
+
+
+
+ {stateToString(position.state?.state)} +
+
+ {bech32Id} +
+
+
+ + +
+
+
+ {position.state?.state === PositionState_PositionStateEnum.OPENED && ( + + )} + {position.state?.state === PositionState_PositionStateEnum.CLOSED && ( + + )} +
+
- - {/*
{uint8ArrayToBase64(position.nonce)}
*/} - - {/*
{position.closeOnFill ? 'true' : 'false'}
*/} - - {/*
*/} - {/* {position.phi?.component?.p && joinLoHiAmount(position.phi.component.p).toString()}*/} - {/*
*/} - - {/*
*/} - {/* {position.phi?.component?.q && joinLoHiAmount(position.phi.component.q).toString()}*/} - {/*
*/} - - {/*
{position.reserves?.r1 && joinLoHiAmount(position.reserves.r1).toString()}
*/} - - {/*
{position.reserves?.r2 && joinLoHiAmount(position.reserves.r2).toString()}
*/} -
- ); - })} + ); + })} +
); }; diff --git a/apps/minifront/src/state/swap/lp-positions.ts b/apps/minifront/src/state/swap/lp-positions.ts index 7d8c98bfd6..384a2d109c 100644 --- a/apps/minifront/src/state/swap/lp-positions.ts +++ b/apps/minifront/src/state/swap/lp-positions.ts @@ -57,21 +57,32 @@ const fetchOwnedPositions = async function* (): AsyncIterable { .ownedPositionIds({ positionState: { state: PositionState_PositionStateEnum.CLOSED } }), ); - const allPositionIds = [...closedIds].map(i => i.positionId).filter(Boolean) as PositionId[]; + const allPositionIds = [...openedIds, ...closedIds] + .map(i => i.positionId) + .filter(Boolean) as PositionId[]; + // We then need to retrieve the LP data for each of these ID's from the node const iterable = penumbra .service(DexService) .liquidityPositionsById({ positionId: allPositionIds }); let index = 0; for await (const res of iterable) { - console.log(res); const id = allPositionIds[index]; // responses are emitted in the order of the input ids if (!id) { throw new Error(`No corresponding ID in request array for position index ${index}`); } - if (res.data) { + if ( + res.data && + // TODO: Bug w/ testnet whale seedphrase on penumbra-1 LP position + // https://dex.penumbra.sevenseas.capital/lp/plpid17wqat9ppwkjk8hffpk2jz669c3u5hzm8268jjlf6j88qju7d238qak905k + // Stored as an opened position in indexeddb, but when querying `liquidityPositionsById()`, it's actually already withdrawn + // Doing a temp additional filter here so users don't see that state. + [PositionState_PositionStateEnum.OPENED, PositionState_PositionStateEnum.CLOSED].includes( + res.data.state?.state ?? PositionState_PositionStateEnum.UNSPECIFIED, + ) + ) { yield { id, position: res.data, From 5099ca27ddd8923532030add456c64de06819846 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Mon, 16 Sep 2024 19:44:10 +0200 Subject: [PATCH 3/3] Actions --- apps/minifront/src/components/swap/layout.tsx | 4 +- .../src/components/swap/lp-positions.tsx | 40 ++++++-- .../components/swap/swap-form/limit-order.tsx | 6 +- apps/minifront/src/state/swap/lp-positions.ts | 93 ++++++++++++++----- packages/query/src/block-processor.ts | 1 - 5 files changed, 105 insertions(+), 39 deletions(-) diff --git a/apps/minifront/src/components/swap/layout.tsx b/apps/minifront/src/components/swap/layout.tsx index 83f2aa0a0d..0b2bc29509 100644 --- a/apps/minifront/src/components/swap/layout.tsx +++ b/apps/minifront/src/components/swap/layout.tsx @@ -4,7 +4,6 @@ import { UnclaimedSwaps } from './unclaimed-swaps'; import { AuctionList } from './auction-list'; import { SwapInfoCard } from './swap-info-card'; import { LayoutGroup } from 'framer-motion'; -import { LpPositions } from './lp-positions.tsx'; export const SwapLayout = () => { return ( @@ -16,7 +15,8 @@ export const SwapLayout = () => { - + {/* TODO: Will enable in subsequent PR */} + {/* */}
diff --git a/apps/minifront/src/components/swap/lp-positions.tsx b/apps/minifront/src/components/swap/lp-positions.tsx index 301411b3f2..77194b8a0a 100644 --- a/apps/minifront/src/components/swap/lp-positions.tsx +++ b/apps/minifront/src/components/swap/lp-positions.tsx @@ -6,6 +6,8 @@ import { PositionState_PositionStateEnum } from '@penumbra-zone/protobuf/penumbr import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/value'; import { Button } from '@penumbra-zone/ui/components/ui/button'; import { cn } from '@penumbra-zone/ui/lib/utils'; +import { AllSlices } from '../../state'; +import { useStoreShallow } from '../../utils/use-store-shallow.ts'; const stateToString = (state?: PositionState_PositionStateEnum): string => { switch (state) { @@ -30,8 +32,14 @@ const stateToString = (state?: PositionState_PositionStateEnum): string => { } }; +const lPActionSelector = ({ swap }: AllSlices) => ({ + onAction: swap.lpPositions.onAction, + txInProgress: swap.txInProgress, +}); + export const LpPositions = () => { const { data, error } = useOwnedPositions(); + const { onAction, txInProgress } = useStoreShallow(lPActionSelector); return !data?.length ? (
@@ -40,8 +48,8 @@ export const LpPositions = () => { Limit orders {!!error &&
❌ There was an error loading your limit orders: ${String(error)}
}
- {data.map(({ position, id, r1ValueView, r2ValueView }) => { - const bech32Id = bech32mPositionId(id); + {data.map(p => { + const bech32Id = bech32mPositionId(p.id); return (
@@ -50,30 +58,42 @@ export const LpPositions = () => {
- {stateToString(position.state?.state)} + {stateToString(p.position.state?.state)}
{bech32Id}
- - + +
- {position.state?.state === PositionState_PositionStateEnum.OPENED && ( - )} - {position.state?.state === PositionState_PositionStateEnum.CLOSED && ( - )} diff --git a/apps/minifront/src/components/swap/swap-form/limit-order.tsx b/apps/minifront/src/components/swap/swap-form/limit-order.tsx index 5895ffe2dd..85c879452c 100644 --- a/apps/minifront/src/components/swap/swap-form/limit-order.tsx +++ b/apps/minifront/src/components/swap/swap-form/limit-order.tsx @@ -5,16 +5,16 @@ const limitOrderSelector = (state: AllSlices) => ({ assetIn: state.swap.assetIn, assetOut: state.swap.assetOut, amount: state.swap.amount, - onSubmit: state.swap.lpPositions.onSubmit, + open: state.swap.lpPositions.open, }); export const LimitOrder = () => { - const { onSubmit } = useStoreShallow(limitOrderSelector); + const { open } = useStoreShallow(limitOrderSelector); return (

Limit order

- +
); }; diff --git a/apps/minifront/src/state/swap/lp-positions.ts b/apps/minifront/src/state/swap/lp-positions.ts index 384a2d109c..790244b8c6 100644 --- a/apps/minifront/src/state/swap/lp-positions.ts +++ b/apps/minifront/src/state/swap/lp-positions.ts @@ -12,10 +12,13 @@ import { PositionId, PositionState_PositionStateEnum, } from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; -import { base64ToUint8Array } from '@penumbra-zone/types/base64'; import { DexService } from '@penumbra-zone/protobuf'; import { AssetId, ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; import { Amount } from '@penumbra-zone/protobuf/penumbra/core/num/v1/num_pb'; +import { AddressIndex } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; +import { getBalancesStream } from '../../fetchers/balances'; +import { bech32mPositionId } from '@penumbra-zone/bech32m/plpid'; +import { getMetadataFromBalancesResponse } from '@penumbra-zone/getters/balances-response'; export interface PositionWithId { id: PositionId; @@ -116,7 +119,8 @@ export const { ownedPositions, useOwnedPositions } = createZQuery({ }); interface Actions { - onSubmit: () => Promise; + onAction: (action: 'positionClose' | 'positionWithdraw', p: PositionWithId) => Promise; + open: () => Promise; } interface State { @@ -129,22 +133,56 @@ const INITIAL_STATE: State = { ownedPositions, }; +// TODO: This is a slow operation. We should revise LiquidityPositionsResponse to: +// message LiquidityPositionsResponse { +// Position data = 1; +// // === new === +// PositionId position_id = 2; +// SpendableNoteRecord note_record = 3; +// ValueView r1_value_view = 4; +// ValueView r2_value_view = 5; +// } +const getSourceForPosition = async (pId: PositionId): Promise => { + const bech32Id = bech32mPositionId(pId); + const responses = await Array.fromAsync(getBalancesStream()); + for (const r of responses) { + const baseDenom = getMetadataFromBalancesResponse.optional(r)?.base; + if (baseDenom?.includes(bech32Id)) { + return getAddressIndex.optional(r.accountAddress); + } + } + return undefined; +}; + export const createLpPositionsSlice = (): SliceCreator => (set, get) => { return { ...INITIAL_STATE, - onSubmit: async () => { + onAction: async (action, { id, position }) => { try { set(state => { state.swap.txInProgress = true; }); - const txPlannerRequest = assembleLimitOrderReq(get().swap); - await planBuildBroadcast('positionOpen', txPlannerRequest); + const txPlannerRequest = await assembleReq(action, id, position); + await planBuildBroadcast(action, txPlannerRequest); + get().swap.lpPositions.ownedPositions.revalidate(); + } finally { + set(state => { + state.swap.txInProgress = false; + }); + } + }, + open: async () => { + try { set(state => { - state.swap.amount = ''; + state.swap.txInProgress = true; }); - get().shared.balancesResponses.revalidate(); + + const txPlannerRequest = assembleLimitOrderReq(get().swap); + await planBuildBroadcast('positionOpen', txPlannerRequest); + + get().swap.lpPositions.ownedPositions.revalidate(); } finally { set(state => { state.swap.txInProgress = false; @@ -154,7 +192,31 @@ export const createLpPositionsSlice = (): SliceCreator => (set }; }; -// TODO: This is temporary data for testing purposes. Update with inputs when component is ready. +const assembleReq = async (action: string, positionId: PositionId, position: Position) => { + const source = await getSourceForPosition(positionId); + if (!source) { + throw new Error(`Could not find source for ${bech32mPositionId(positionId)}`); + } + + if (action === 'positionClose') { + return new TransactionPlannerRequest({ + positionCloses: [{ positionId }], + source, + }); + } + + if (action === 'positionWithdraw') { + return new TransactionPlannerRequest({ + positionWithdraws: [ + { positionId, reserves: position.reserves, tradingPair: position.phi?.pair }, + ], + source, + }); + } + + throw new Error(`Action not implemented: ${action}`); +}; + const assembleLimitOrderReq = ({ assetIn, amount, assetOut }: SwapSlice) => { if (!assetIn) { throw new Error('`assetIn` is undefined'); @@ -184,21 +246,6 @@ const assembleLimitOrderReq = ({ assetIn, amount, assetOut }: SwapSlice) => { }, }, ], - 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), }); }; diff --git a/packages/query/src/block-processor.ts b/packages/query/src/block-processor.ts index b36bf5d5ff..356869ed1a 100644 --- a/packages/query/src/block-processor.ts +++ b/packages/query/src/block-processor.ts @@ -460,7 +460,6 @@ export class BlockProcessor implements BlockProcessorInterface { for (const tx of blockTx) { let txId: TransactionId | undefined; - // Something here? Not getting closed/withdrawn actions const txCommitments = (tx.body?.actions ?? []).flatMap(({ action }) => { switch (action.case) { case 'output':