From d23cffdcafe04986df3f46bad41b5f7887f4810c Mon Sep 17 00:00:00 2001 From: Lucas Araujo Date: Mon, 1 Apr 2024 13:49:09 -0300 Subject: [PATCH] feat: add transaction expiry date --- .../DappTransactionContainer.tsx | 3 + .../src/hooks/useInitializeTx.ts | 3 +- .../src/lib/translations/en.json | 6 +- .../utils/__tests__/era-slot-datetime.test.ts | 33 +++++++++++ .../src/utils/era-slot-datetime.ts | 19 +++++++ .../components/SendTransactionSummary.tsx | 11 +++- .../features/send-transaction/types.ts | 1 + .../DappTransaction/DappTransaction.tsx | 25 ++++++++- .../OutputSummaryList.module.scss | 9 +++ .../OutputSummaryList/OutputSummaryList.tsx | 40 +++++++++++++- .../dapp-transaction-summary.stories.tsx | 11 ++++ .../dapp-transaction-text-field.component.tsx | 55 +++++++++++++++++++ .../dapp-transaction-text-field.css.ts | 31 +++++++++++ .../dapp-transaction-summary/index.ts | 1 + packages/ui/src/design-system/index.ts | 5 +- 15 files changed, 244 insertions(+), 9 deletions(-) create mode 100644 apps/browser-extension-wallet/src/utils/__tests__/era-slot-datetime.test.ts create mode 100644 apps/browser-extension-wallet/src/utils/era-slot-datetime.ts create mode 100644 packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-text-field.component.tsx create mode 100644 packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-text-field.css.ts diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx index 73a523c67..52458f66c 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx @@ -28,6 +28,7 @@ import { useCurrencyStore, useAppSettingsContext } from '@providers'; import { logger, signingCoordinator } from '@lib/wallet-api-ui'; import { useComputeTxCollateral } from '@hooks/useComputeTxCollateral'; import { utxoAndBackendChainHistoryResolver } from '@src/utils/utxo-chain-history-resolver'; +import { eraSlotDateTime } from '@src/utils/era-slot-datetime'; interface DappTransactionContainerProps { errorMessage?: string; @@ -106,6 +107,7 @@ export const DappTransactionContainer = withAddressBookContext( const userRewardAccounts = useObservable(inMemoryWallet.delegation.rewardAccounts$); const rewardAccountsAddresses = useMemo(() => userRewardAccounts?.map((key) => key.address), [userRewardAccounts]); const protocolParameters = useObservable(inMemoryWallet?.protocolParameters$); + const eraSummaries = useObservable(inMemoryWallet?.eraSummaries$); useEffect(() => { if (!req || !protocolParameters) { @@ -175,6 +177,7 @@ export const DappTransactionContainer = withAddressBookContext( errorMessage={errorMessage} toAddress={toAddressTokens} collateral={txCollateral} + expiresBy={eraSlotDateTime(eraSummaries, tx.body.validityInterval?.invalidHereafter)} /> ) : ( diff --git a/apps/browser-extension-wallet/src/hooks/useInitializeTx.ts b/apps/browser-extension-wallet/src/hooks/useInitializeTx.ts index da290dd8e..c60f76532 100644 --- a/apps/browser-extension-wallet/src/hooks/useInitializeTx.ts +++ b/apps/browser-extension-wallet/src/hooks/useInitializeTx.ts @@ -85,7 +85,8 @@ export const useInitializeTx = ( fee: inspection.inputSelection.fee, hash: inspection.hash, outputs: inspection.inputSelection.outputs, - handleResolutions: inspection.handleResolutions + handleResolutions: inspection.handleResolutions, + validityInterval: inspection.body.validityInterval }, tx, totalMinimumCoins, diff --git a/apps/browser-extension-wallet/src/lib/translations/en.json b/apps/browser-extension-wallet/src/lib/translations/en.json index 80d602092..08bcacaa9 100644 --- a/apps/browser-extension-wallet/src/lib/translations/en.json +++ b/apps/browser-extension-wallet/src/lib/translations/en.json @@ -1568,7 +1568,11 @@ "txFee": "Transaction fee", "metaData": "Metadata", "deposit": "Deposit", - "output": "Bundle" + "output": "Bundle", + "expiresBy": "Expires by", + "expiresByTooltip": "This transaction can be added to the blockchain until the specified time. After this, it will automatically expire and no longer be valid.", + "noLimit": "No limit", + "utc": "UTC" }, "sendReceive": { "send": "Send", diff --git a/apps/browser-extension-wallet/src/utils/__tests__/era-slot-datetime.test.ts b/apps/browser-extension-wallet/src/utils/__tests__/era-slot-datetime.test.ts new file mode 100644 index 000000000..2d799cf7a --- /dev/null +++ b/apps/browser-extension-wallet/src/utils/__tests__/era-slot-datetime.test.ts @@ -0,0 +1,33 @@ +/* eslint-disable unicorn/no-useless-undefined */ +/* eslint-disable no-magic-numbers */ +import { Cardano, EraSummary } from '@cardano-sdk/core'; +import { eraSlotDateTime } from '../era-slot-datetime'; +import { fromSerializableObject } from '@cardano-sdk/util'; + +export const eraSummaries = fromSerializableObject([ + { + parameters: { epochLength: 4320, slotLength: 20_000 }, + start: { slot: 0, time: { __type: 'Date', value: 1_666_656_000_000 } } + }, + { + parameters: { epochLength: 86_400, slotLength: 1000 }, + start: { slot: 0, time: { __type: 'Date', value: 1_666_656_000_000 } } + } +]); + +const testSlot = Cardano.Slot(36_201_583); + +describe('Testing eraSlotDateTime', () => { + test('should return undefined', async () => { + expect(eraSlotDateTime(undefined, testSlot)).toEqual(undefined); + }); + test('should return undefined', async () => { + expect(eraSlotDateTime(eraSummaries, undefined)).toEqual(undefined); + }); + test('should return formatted time', async () => { + expect(eraSlotDateTime(eraSummaries, testSlot)).toEqual({ + utcDate: '12/17/2023', + utcTime: '23:59:43' + }); + }); +}); diff --git a/apps/browser-extension-wallet/src/utils/era-slot-datetime.ts b/apps/browser-extension-wallet/src/utils/era-slot-datetime.ts new file mode 100644 index 000000000..3258dc43c --- /dev/null +++ b/apps/browser-extension-wallet/src/utils/era-slot-datetime.ts @@ -0,0 +1,19 @@ +/* eslint-disable consistent-return */ +import { EraSummary } from '@cardano-sdk/core'; +import { Wallet } from '@lace/cardano'; +import { formatDate, formatTime } from './format-date'; + +export const eraSlotDateTime = ( + eraSummaries: EraSummary[] | undefined, + slot: Wallet.Cardano.Slot | undefined +): { utcDate: string; utcTime: string } | undefined => { + if (!eraSummaries || !slot) { + return undefined; + } + const slotTimeCalc = Wallet.createSlotTimeCalc(eraSummaries); + const date = slotTimeCalc(slot); + return { + utcDate: formatDate({ date, format: 'MM/DD/YYYY', type: 'utc' }), + utcTime: formatTime({ date, type: 'utc' }) + }; +}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/SendTransactionSummary.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/SendTransactionSummary.tsx index 12479e269..d87f75cb0 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/SendTransactionSummary.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/SendTransactionSummary.tsx @@ -17,6 +17,7 @@ import { getTokenAmountInFiat, parseFiat } from '@src/utils/assets-transformers' import { useObservable, Banner } from '@lace/common'; import ExclamationIcon from '../../../../../assets/icons/exclamation-triangle-red.component.svg'; import { WalletType } from '@cardano-sdk/web-extension'; +import { eraSlotDateTime } from '@src/utils/era-slot-datetime'; const { Text } = Typography; @@ -98,7 +99,7 @@ interface SendTransactionSummaryProps { export const SendTransactionSummary = withAddressBookContext( ({ isPopupView = false }: SendTransactionSummaryProps): React.ReactElement => { const { t } = useTranslation(); - const { builtTxData: { uiTx: { fee, outputs } = {} } = {} } = useBuiltTxState(); + const { builtTxData: { uiTx: { fee, outputs, validityInterval } = {} } = {} } = useBuiltTxState(); const [metadata] = useMetadata(); const { inMemoryWallet, @@ -113,6 +114,7 @@ export const SendTransactionSummary = withAddressBookContext( const { fiatCurrency } = useCurrencyStore(); const assetsInfo = useObservable(inMemoryWallet.assetInfo$); + const eraSummaries = useObservable(inMemoryWallet.eraSummaries$); const outputSummaryListTranslation = { recipientAddress: t('core.outputSummaryList.recipientAddress'), @@ -120,7 +122,11 @@ export const SendTransactionSummary = withAddressBookContext( output: t('core.outputSummaryList.output'), metadata: t('core.outputSummaryList.metaData'), deposit: t('core.outputSummaryList.deposit'), - txFee: t('core.outputSummaryList.txFee') + txFee: t('core.outputSummaryList.txFee'), + expiresBy: t('core.outputSummaryList.expiresBy'), + expiresByTooltip: t('core.outputSummaryList.expiresByTooltip'), + noLimit: t('core.outputSummaryList.noLimit'), + utc: t('core.outputSummaryList.utc') }; const addressToNameMap = useMemo( @@ -140,6 +146,7 @@ export const SendTransactionSummary = withAddressBookContext( <> ; fee: Wallet.Cardano.Lovelace; handleResolutions?: HandleResolution[]; + validityInterval?: Wallet.Cardano.ValidityInterval; }; error?: string; reachedMaxAmountList?: (string | Wallet.Cardano.AssetId)[]; diff --git a/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx b/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx index 86e5673d8..bc9bb890d 100644 --- a/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx +++ b/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx @@ -11,7 +11,7 @@ import styles from './DappTransaction.module.scss'; import { useTranslate } from '@src/ui/hooks'; import { TransactionFee, Collateral } from '@ui/components/ActivityDetail'; -import { TransactionType, DappTransactionSummary, TransactionAssets } from '@lace/ui'; +import { TransactionType, DappTransactionSummary, TransactionAssets, DappTransactionTextField, Flex } from '@lace/ui'; import { DappAddressSections } from '../DappAddressSections/DappAddressSections'; const amountTransformer = (fiat: { price: number; code: string }) => (ada: string) => @@ -27,6 +27,7 @@ export interface DappTransactionProps { fiatCurrencyCode?: string; fiatCurrencyPrice?: number; coinSymbol?: string; + expiresBy?: { utcDate: string; utcTime: string }; /** tokens send to being sent to or from the user */ fromAddress: Map; toAddress: Map; @@ -110,7 +111,8 @@ export const DappTransaction = ({ fiatCurrencyCode, fiatCurrencyPrice, coinSymbol, - dappInfo + dappInfo, + expiresBy }: DappTransactionProps): React.ReactElement => { const { t } = useTranslate(); @@ -120,6 +122,17 @@ export const DappTransaction = ({ const isFromAddressesEnabled = groupedFromAddresses.size > 0; const isToAddressesEnabled = groupedToAddresses.size > 0; + const expireByText = expiresBy ? ( + + {expiresBy.utcDate} + + {expiresBy.utcTime} {t('core.outputSummaryList.utc')} + + + ) : ( + t('core.outputSummaryList.noLimit') + ); + return (
{errorMessage && } @@ -162,6 +175,14 @@ export const DappTransaction = ({ /> )} +
+ +
+ {returnedDeposit !== BigInt(0) && ( ; + translations: TranslationsFor< + | 'recipientAddress' + | 'sending' + | 'txFee' + | 'deposit' + | 'metadata' + | 'output' + | 'expiresBy' + | 'expiresByTooltip' + | 'noLimit' + | 'utc' + >; onFeeTooltipHover?: () => unknown; onDepositTooltipHover?: () => unknown; } @@ -32,6 +45,7 @@ export const OutputSummaryList = ({ metadata, deposit, translations, + expiresBy, onFeeTooltipHover, onDepositTooltipHover }: OutputSummaryListProps): React.ReactElement => { @@ -40,6 +54,17 @@ export const OutputSummaryList = ({ sending: translations.sending }; + const expireByText = expiresBy ? ( + + {expiresBy.utcDate} + + {expiresBy.utcTime} {translations.utc} + + + ) : ( + translations.noLimit + ); + return (
{rows.map((row, idx) => ( @@ -52,7 +77,6 @@ export const OutputSummaryList = ({
))} - {metadata && (
@@ -65,6 +89,18 @@ export const OutputSummaryList = ({ )}
+ + {renderLabel({ + label: translations.expiresBy, + tooltipContent: translations.expiresByTooltip, + dataTestId: 'validity-interval-expires-by-label' + })} + + + {expireByText} + + + {deposit && ( {renderLabel({ diff --git a/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-summary.stories.tsx b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-summary.stories.tsx index d5a01c37a..bdb82430b 100644 --- a/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-summary.stories.tsx +++ b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-summary.stories.tsx @@ -13,6 +13,7 @@ import { Grid, Cell } from '../grid'; import { TransactionAssets } from './dapp-transaction-assets.component'; import { TransactionSummary } from './dapp-transaction-summary.component'; +import { TransactionTextField } from './dapp-transaction-text-field.component'; import { TransactionType } from './dapp-transaction-type.component'; const subtitle = `Control that displays data items in rows.`; @@ -82,6 +83,13 @@ const Example = (): JSX.Element => ( tokenName={value.tokenName} /> ))} + + + ); @@ -104,6 +112,9 @@ const MainComponents = (): JSX.Element => ( /> ))} + + + diff --git a/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-text-field.component.tsx b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-text-field.component.tsx new file mode 100644 index 000000000..a8b4209c2 --- /dev/null +++ b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-text-field.component.tsx @@ -0,0 +1,55 @@ +import type { ReactNode } from 'react'; +import React from 'react'; + +import { ReactComponent as InfoIcon } from '@lace/icons/dist/InfoComponent'; + +import { Box } from '../box'; +import { Flex } from '../flex'; +import { Grid, Cell } from '../grid'; +import { Tooltip } from '../tooltip'; +import * as Typography from '../typography'; + +import * as cx from './dapp-transaction-text-field.css'; + +import type { OmitClassName } from '../../types'; + +type Props = OmitClassName<'div'> & { + label: string; + text: ReactNode | string; + tooltip?: string; +}; + +export const TransactionTextField = ({ + label, + text, + tooltip, + ...props +}: Readonly): JSX.Element => { + return ( + + + + + {label} + + {tooltip !== undefined && ( + + +
+ +
+
+
+ )} +
+
+ + + + {text} + + + +
+ ); +}; diff --git a/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-text-field.css.ts b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-text-field.css.ts new file mode 100644 index 000000000..6e534d9fc --- /dev/null +++ b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-text-field.css.ts @@ -0,0 +1,31 @@ +import { sx, style } from '../../design-tokens'; + +export const label = sx({ + color: '$transaction_summary_label_color', + fontWeight: '$bold', +}); + +export const text = style([ + sx({ + color: '$transaction_summary_label_color', + fontWeight: '$semibold', + }), + { + wordBreak: 'break-all', + }, +]); + +export const tooltip = style([ + sx({ + color: '$transaction_summary_secondary_label_color', + width: '$24', + height: '$24', + fontSize: '$25', + }), +]); + +export const tooltipText = style([ + sx({ + display: 'flex', + }), +]); diff --git a/packages/ui/src/design-system/dapp-transaction-summary/index.ts b/packages/ui/src/design-system/dapp-transaction-summary/index.ts index 24037ad5d..fb69cd8b3 100644 --- a/packages/ui/src/design-system/dapp-transaction-summary/index.ts +++ b/packages/ui/src/design-system/dapp-transaction-summary/index.ts @@ -1,3 +1,4 @@ export { TransactionType } from './dapp-transaction-type.component'; export { TransactionSummary } from './dapp-transaction-summary.component'; export { TransactionAssets } from './dapp-transaction-assets.component'; +export { TransactionTextField } from './dapp-transaction-text-field.component'; diff --git a/packages/ui/src/design-system/index.ts b/packages/ui/src/design-system/index.ts index dcc11a288..8d7c226be 100644 --- a/packages/ui/src/design-system/index.ts +++ b/packages/ui/src/design-system/index.ts @@ -47,7 +47,10 @@ export { SelectGroup } from './select'; export { ActionCard } from './action-card'; export { Loader } from './loader'; export { TransactionType } from './dapp-transaction-summary'; -export { TransactionSummary as DappTransactionSummary } from './dapp-transaction-summary'; +export { + TransactionSummary as DappTransactionSummary, + TransactionTextField as DappTransactionTextField, +} from './dapp-transaction-summary'; export { TransactionAssets } from './dapp-transaction-summary'; export { SummaryExpander } from './summary-expander'; export * from './auto-suggest-box';