Skip to content

Commit

Permalink
feat(ui): #2053: implement SwapAction view
Browse files Browse the repository at this point in the history
  • Loading branch information
VanishMax committed Feb 14, 2025
1 parent 77b5e5c commit 32ce990
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 18 deletions.
7 changes: 4 additions & 3 deletions packages/ui/src/ActionView/action-view.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { FC } from 'react';
import { ActionView as ActionViewMessage } from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb';
import { ActionViewType, ActionViewValueType } from './types';
import { UnknownAction } from './unknown';

import { SpendAction } from './actions/spend';
import { OutputAction } from './actions/output';
import { FC } from 'react';
import { SwapAction } from './actions/swap';

export interface ActionViewProps {
action: ActionViewMessage;
Expand All @@ -13,6 +14,8 @@ export interface ActionViewProps {
const componentMap = {
spend: SpendAction,
output: OutputAction,
swap: SwapAction,
swapClaim: UnknownAction,
delegate: UnknownAction,
delegatorVote: UnknownAction,
ibcRelayAction: UnknownAction,
Expand All @@ -24,8 +27,6 @@ const componentMap = {
proposalDepositClaim: UnknownAction,
proposalSubmit: UnknownAction,
proposalWithdraw: UnknownAction,
swap: UnknownAction,
swapClaim: UnknownAction,
undelegate: UnknownAction,
undelegateClaim: UnknownAction,
validatorDefinition: UnknownAction,
Expand Down
29 changes: 29 additions & 0 deletions packages/ui/src/ActionView/actions/action-row.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ReactNode } from 'react';
import cn from 'clsx';
import { detailTechnical } from '../../utils/typography';
import { CopyToClipboardButton } from '../../CopyToClipboardButton';
import { Density } from '../../Density';

export interface ActionRowProps {
label: ReactNode;
info: ReactNode;
copyText?: string;
}

export const ActionRow = ({ label, copyText, info }: ActionRowProps) => {
return (
<div className={cn('flex items-center gap-2 text-text-secondary', detailTechnical)}>
{label}
<div className='h-px grow border-t border-dashed border-other-tonalStroke stroke-1' />
{info}

{copyText && (
<Density key='swap-claim' slim>
<div className='size-4 [&>button]:text-neutral-light'>
<CopyToClipboardButton text={copyText} />
</div>
</Density>
)}
</div>
);
};
4 changes: 2 additions & 2 deletions packages/ui/src/ActionView/actions/output.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { ActionWrapper } from './wrapper';
import { ValueViewComponent } from '../../ValueView';
import { AddressViewComponent } from '../../AddressView';

export interface SpendActionProps {
export interface OutputActionProps {
value: OutputView;
}

export const OutputAction = ({ value }: SpendActionProps) => {
export const OutputAction = ({ value }: OutputActionProps) => {
const density = useDensity();

return (
Expand Down
135 changes: 135 additions & 0 deletions packages/ui/src/ActionView/actions/swap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { useMemo } from 'react';
import { ArrowRight } from 'lucide-react';
import { SwapView } from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb';
import { Metadata, ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import {
getAsset1Metadata,
getAsset2Metadata,
getClaimFeeFromSwapView,
getClaimTx,
} from '@penumbra-zone/getters/swap-view';
import { getOneWaySwapValues, isOneWaySwap } from '@penumbra-zone/types/swap';
import { getAmount, getMetadata } from '@penumbra-zone/getters/value-view';
import { isZero } from '@penumbra-zone/types/amount';
import { getFormattedAmtFromValueView } from '@penumbra-zone/types/value-view';
import { uint8ArrayToHex } from '@penumbra-zone/types/hex';
import { shorten } from '@penumbra-zone/types/string';
import { ValueViewComponent } from '../../ValueView';
import { useDensity } from '../../utils/density';
import { Density } from '../../Density';
import { ActionWrapper } from './wrapper';
import { ActionRow } from './action-row';

export interface SwapActionProps {
value: SwapView;
}

const renderAmount = (value?: ValueView) => {
if (!value) {
return undefined;
}
const symbol = getMetadata.optional(value)?.symbol;
return symbol ? `${getFormattedAmtFromValueView(value)} ${symbol}` : undefined;
};

export const SwapAction = ({ value }: SwapActionProps) => {
const density = useDensity();

const isOneWay = isOneWaySwap(value);
const swap = isOneWay ? getOneWaySwapValues(value) : undefined;
const showOutput = !!getAmount.optional(swap?.output) && !isZero(getAmount(swap?.output));

const unfilled = useMemo(() => {
return renderAmount(swap?.unfilled);
}, [swap]);

const txId = useMemo(() => {
const claim = getClaimTx.optional(value);
if (!claim) {
return undefined;
}
return uint8ArrayToHex(claim.inner);
}, [value]);

// This function calculates metadata based on fee's AssetId from input or output metadata.
// TODO: implement fees paid from non-input/output assets (e.g. connect with registry)
const fee = useMemo(() => {
const claimFee = getClaimFeeFromSwapView.optional(value);
if (!claimFee) {
return undefined;
}

let metadata: Metadata | undefined = undefined;
const asset1 = getAsset1Metadata.optional(value);
const asset2 = getAsset2Metadata.optional(value);
if (claimFee.assetId?.equals(asset1?.penumbraAssetId)) {
metadata = asset1;
}
if (claimFee.assetId?.equals(asset2?.penumbraAssetId)) {
metadata = asset1;
}

if (metadata) {
return renderAmount(
new ValueView({
valueView: {
case: 'knownAssetId',
value: {
metadata,
amount: claimFee.amount,
},
},
}),
);
}

return renderAmount(
new ValueView({
valueView: {
case: 'unknownAssetId',
value: {
assetId: claimFee.assetId,
amount: claimFee.amount,
},
},
}),
);
}, [value]);

if (!isOneWay) {
return (
<ActionWrapper title='Two-way swap: unsupported' opaque={value.swapView.case === 'opaque'} />
);
}

return (
<ActionWrapper
title='Swap'
opaque={value.swapView.case === 'opaque'}
infoRows={
<>
{!!fee && <ActionRow label='Swap Claim fee' info={fee} />}
{!!txId && (
<ActionRow label='Swap Claim Transaction' info={shorten(txId, 8)} copyText={txId} />
)}
{unfilled && <ActionRow label='Unfilled Amount' info={unfilled} />}
</>
}
>
{value.swapView.case === 'visible' && swap && (
<Density slim>
<ValueViewComponent
valueView={swap.input}
priority={density === 'sparse' ? 'primary' : 'tertiary'}
/>
<ArrowRight className='size-3 text-neutral-contrast' />
<ValueViewComponent
valueView={swap.output}
showValue={showOutput}
priority={density === 'sparse' ? 'primary' : 'tertiary'}
/>
</Density>
)}
</ActionWrapper>
);
};
40 changes: 35 additions & 5 deletions packages/ui/src/ActionView/actions/wrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import cn from 'clsx';
import { ReactNode } from 'react';
import { IncognitoIcon } from './incognito-icon';
import { Text } from '../../Text';
import { IncognitoIcon } from './incognito-icon';
import { useDensity } from '../../utils/density';

export interface ActionWrapperProps {
title: string;
opaque?: boolean;
children?: ReactNode;
/** Use `ActionRow` component to render an array of additional info rows */
infoRows?: ReactNode;
}

export const ActionWrapper = ({ opaque, children, title }: ActionWrapperProps) => {
// const density = useDensity();

const ActionWrapperHeader = ({ opaque, children, title }: ActionWrapperProps) => {
return (
<div className='flex h-10 w-full items-center justify-between gap-1 rounded-sm bg-other-tonalFill5 px-3 py-2'>
<>
{opaque && (
<i className='block text-neutral-light'>
<IncognitoIcon />
Expand All @@ -26,6 +28,34 @@ export const ActionWrapper = ({ opaque, children, title }: ActionWrapperProps) =
</div>

{!opaque && <div className='flex items-center gap-1'>{children}</div>}
</>
);
};

export const ActionWrapper = (props: ActionWrapperProps) => {
const density = useDensity();
const { infoRows } = props;

if (!infoRows) {
return (
<div className='flex h-10 w-full items-center justify-between gap-1 rounded-sm bg-other-tonalFill5 px-3 py-2'>
<ActionWrapperHeader {...props} />
</div>
);
}

return (
<div
className={cn(
'flex flex-col rounded-sm bg-other-tonalFill5 px-3 py-2',
density === 'sparse' ? 'gap-2' : 'gap-1',
)}
>
<div className='flex w-full items-center justify-between gap-1'>
<ActionWrapperHeader {...props} />
</div>

{infoRows}
</div>
);
};
3 changes: 2 additions & 1 deletion packages/ui/src/ActionView/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import type { Meta, StoryObj } from '@storybook/react';
import { ActionView as ActionViewMessage } from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb';

import { ActionView } from '.';
import { SpendAction, OutputAction } from '../utils/bufs';
import { SpendAction, OutputAction, SwapAction } from '../utils/bufs';

const OPTIONS: Record<string, ActionViewMessage> = {
Spend: SpendAction,
Output: OutputAction,
Swap: SwapAction,
};

const meta: Meta<typeof ActionView> = {
Expand Down
42 changes: 41 additions & 1 deletion packages/ui/src/utils/bufs/action-view.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { ActionView } from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb';
import {
NoteView,
OutputView,
SpendView,
} from '@penumbra-zone/protobuf/penumbra/core/component/shielded_pool/v1/shielded_pool_pb';
import { SwapView } from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb';
import { ADDRESS_VIEW_DECODED } from './address-view';
import { PENUMBRA_VALUE_VIEW } from './value-view';
import { PENUMBRA_VALUE_VIEW, PENUMBRA_VALUE_VIEW_ZERO, USDC_VALUE_VIEW } from './value-view';
import { PENUMBRA_METADATA, USDC_METADATA } from './metadata';
import { AMOUNT_123_456_789, AMOUNT_999, AMOUNT_ZERO } from './amount';
import { TransactionId } from '@penumbra-zone/protobuf/penumbra/core/txhash/v1/txhash_pb';
import { Fee } from '@penumbra-zone/protobuf/penumbra/core/component/fee/v1/fee_pb';

export const SpendAction = new ActionView({
actionView: {
Expand Down Expand Up @@ -39,3 +45,37 @@ export const OutputAction = new ActionView({
}),
},
});

export const SwapAction = new ActionView({
actionView: {
case: 'swap',
value: new SwapView({
swapView: {
case: 'visible',
value: {
claimTx: new TransactionId({
inner: new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]),
}),
asset1Metadata: USDC_METADATA,
asset2Metadata: PENUMBRA_METADATA,
output1: new NoteView({
address: ADDRESS_VIEW_DECODED,
value: USDC_VALUE_VIEW,
}),
output2: new NoteView({
address: ADDRESS_VIEW_DECODED,
value: PENUMBRA_VALUE_VIEW_ZERO,
}),
swapPlaintext: {
claimFee: new Fee({
amount: AMOUNT_999,
assetId: PENUMBRA_METADATA.penumbraAssetId,
}),
delta1I: AMOUNT_123_456_789,
delta2I: AMOUNT_ZERO,
},
},
},
}),
},
});
16 changes: 16 additions & 0 deletions packages/ui/src/utils/bufs/amount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Amount } from '@penumbra-zone/protobuf/penumbra/core/num/v1/num_pb';

export const AMOUNT_ZERO = new Amount({
hi: 0n,
lo: 0n,
});

export const AMOUNT_999 = new Amount({
hi: 0n,
lo: 999n,
});

export const AMOUNT_123_456_789 = new Amount({
hi: 0n,
lo: 123_456_789n,
});
Loading

0 comments on commit 32ce990

Please sign in to comment.