Skip to content

Commit 0fd1666

Browse files
author
Lucas
authored
feat: add transaction expiry date (#1008)
1 parent 7d765d7 commit 0fd1666

File tree

16 files changed

+233
-11
lines changed

16 files changed

+233
-11
lines changed

apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { useCurrencyStore, useAppSettingsContext } from '@providers';
2323
import { logger, walletRepository } from '@lib/wallet-api-ui';
2424
import { useComputeTxCollateral } from '@hooks/useComputeTxCollateral';
2525
import { utxoAndBackendChainHistoryResolver } from '@src/utils/utxo-chain-history-resolver';
26+
import { eraSlotDateTime } from '@src/utils/era-slot-datetime';
2627
import { AddressBookSchema, useDbStateValue } from '@lib/storage';
2728
import { getAllWalletsAddresses } from '@src/utils/get-all-wallets-addresses';
2829

@@ -83,6 +84,7 @@ export const DappTransactionContainer = withAddressBookContext(
8384
const userRewardAccounts = useObservable(inMemoryWallet.delegation.rewardAccounts$);
8485
const rewardAccountsAddresses = useMemo(() => userRewardAccounts?.map((key) => key.address), [userRewardAccounts]);
8586
const protocolParameters = useObservable(inMemoryWallet?.protocolParameters$);
87+
const eraSummaries = useObservable(inMemoryWallet?.eraSummaries$);
8688
const allWalletsAddresses = getAllWalletsAddresses(useObservable(walletRepository.wallets$));
8789

8890
useEffect(() => {
@@ -153,6 +155,7 @@ export const DappTransactionContainer = withAddressBookContext(
153155
errorMessage={errorMessage}
154156
toAddress={toAddressTokens}
155157
collateral={txCollateral}
158+
expiresBy={eraSlotDateTime(eraSummaries, tx.body.validityInterval?.invalidHereafter)}
156159
ownAddresses={allWalletsAddresses.length > 0 ? allWalletsAddresses : ownAddresses}
157160
addressToNameMap={addressToNameMap}
158161
/>

apps/browser-extension-wallet/src/hooks/useInitializeTx.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ export const useInitializeTx = (
8686
fee: inspection.inputSelection.fee,
8787
hash: inspection.hash,
8888
outputs: inspection.inputSelection.outputs,
89-
handleResolutions: inspection.handleResolutions
89+
handleResolutions: inspection.handleResolutions,
90+
validityInterval: inspection.body.validityInterval
9091
},
9192
tx,
9293
totalMinimumCoins,

apps/browser-extension-wallet/src/lib/translations/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,10 @@
838838
"core.outputSummaryList.metaData": "Metadata",
839839
"core.outputSummaryList.deposit": "Deposit",
840840
"core.outputSummaryList.output": "Bundle",
841+
"core.outputSummaryList.expiresBy": "Expires by",
842+
"core.outputSummaryList.expiresByTooltip": "This transaction can be added to the blockchain until the specified time. After this, it will automatically expire and no longer be valid.",
843+
"core.outputSummaryList.noLimit": "No limit",
844+
"core.outputSummaryList.utc": "UTC",
841845
"core.sendReceive.send": "Send",
842846
"core.sendReceive.receive": "Receive",
843847
"core.coinInputSelection.assetSelection": "Select tokens or NFTs",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/* eslint-disable unicorn/no-useless-undefined */
2+
/* eslint-disable no-magic-numbers */
3+
import { Cardano, EraSummary } from '@cardano-sdk/core';
4+
import { eraSlotDateTime } from '../era-slot-datetime';
5+
import { fromSerializableObject } from '@cardano-sdk/util';
6+
7+
export const eraSummaries = fromSerializableObject<EraSummary[]>([
8+
{
9+
parameters: { epochLength: 4320, slotLength: 20_000 },
10+
start: { slot: 0, time: { __type: 'Date', value: 1_666_656_000_000 } }
11+
},
12+
{
13+
parameters: { epochLength: 86_400, slotLength: 1000 },
14+
start: { slot: 0, time: { __type: 'Date', value: 1_666_656_000_000 } }
15+
}
16+
]);
17+
18+
const testSlot = Cardano.Slot(36_201_583);
19+
20+
describe('Testing eraSlotDateTime', () => {
21+
test('should return undefined', async () => {
22+
expect(eraSlotDateTime(undefined, testSlot)).toEqual(undefined);
23+
});
24+
test('should return undefined', async () => {
25+
expect(eraSlotDateTime(eraSummaries, undefined)).toEqual(undefined);
26+
});
27+
test('should return formatted time', async () => {
28+
expect(eraSlotDateTime(eraSummaries, testSlot)).toEqual({
29+
utcDate: '12/17/2023',
30+
utcTime: '23:59:43'
31+
});
32+
});
33+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* eslint-disable consistent-return */
2+
import { EraSummary } from '@cardano-sdk/core';
3+
import { Wallet } from '@lace/cardano';
4+
import { formatDate, formatTime } from './format-date';
5+
6+
export const eraSlotDateTime = (
7+
eraSummaries: EraSummary[] | undefined,
8+
slot: Wallet.Cardano.Slot | undefined
9+
): { utcDate: string; utcTime: string } | undefined => {
10+
if (!eraSummaries || !slot) {
11+
return undefined;
12+
}
13+
const slotTimeCalc = Wallet.createSlotTimeCalc(eraSummaries);
14+
const date = slotTimeCalc(slot);
15+
return {
16+
utcDate: formatDate({ date, format: 'MM/DD/YYYY', type: 'utc' }),
17+
utcTime: formatTime({ date, type: 'utc' })
18+
};
19+
};

apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/SendTransactionSummary.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { getTokenAmountInFiat, parseFiat } from '@src/utils/assets-transformers'
1717
import { useObservable, Banner } from '@lace/common';
1818
import ExclamationIcon from '../../../../../assets/icons/exclamation-triangle-red.component.svg';
1919
import { WalletType } from '@cardano-sdk/web-extension';
20+
import { eraSlotDateTime } from '@src/utils/era-slot-datetime';
2021
import { getAllWalletsAddresses } from '@src/utils/get-all-wallets-addresses';
2122
import { walletRepository } from '@lib/wallet-api-ui';
2223

@@ -100,7 +101,8 @@ interface SendTransactionSummaryProps {
100101
export const SendTransactionSummary = withAddressBookContext(
101102
({ isPopupView = false }: SendTransactionSummaryProps): React.ReactElement => {
102103
const { t } = useTranslation();
103-
const { builtTxData: { uiTx: { fee, outputs, handleResolutions } = {} } = {} } = useBuiltTxState();
104+
const { builtTxData: { uiTx: { fee, outputs, handleResolutions, validityInterval } = {} } = {} } =
105+
useBuiltTxState();
104106
const [metadata] = useMetadata();
105107
const {
106108
inMemoryWallet,
@@ -115,14 +117,19 @@ export const SendTransactionSummary = withAddressBookContext(
115117
const { fiatCurrency } = useCurrencyStore();
116118

117119
const assetsInfo = useObservable(inMemoryWallet.assetInfo$);
120+
const eraSummaries = useObservable(inMemoryWallet.eraSummaries$);
118121

119122
const outputSummaryListTranslation = {
120123
recipientAddress: t('core.outputSummaryList.recipientAddress'),
121124
sending: t('core.outputSummaryList.sending'),
122125
output: t('core.outputSummaryList.output'),
123126
metadata: t('core.outputSummaryList.metaData'),
124127
deposit: t('core.outputSummaryList.deposit'),
125-
txFee: t('core.outputSummaryList.txFee')
128+
txFee: t('core.outputSummaryList.txFee'),
129+
expiresBy: t('core.outputSummaryList.expiresBy'),
130+
expiresByTooltip: t('core.outputSummaryList.expiresByTooltip'),
131+
noLimit: t('core.outputSummaryList.noLimit'),
132+
utc: t('core.outputSummaryList.utc')
126133
};
127134

128135
const addressToNameMap = useMemo(
@@ -149,6 +156,7 @@ export const SendTransactionSummary = withAddressBookContext(
149156
<>
150157
<OutputSummaryList
151158
rows={rows}
159+
expiresBy={eraSlotDateTime(eraSummaries, validityInterval?.invalidHereafter)}
152160
txFee={{
153161
...getFee(fee?.toString(), priceResult?.cardano?.price, cardanoCoin, fiatCurrency),
154162
tootipText: t('send.theAmountYoullBeChargedToProcessYourTransaction')

apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface BuiltTxData {
3737
outputs: Set<Wallet.Cardano.TxOut & { handleResolution?: HandleResolution }>;
3838
fee: Wallet.Cardano.Lovelace;
3939
handleResolutions?: HandleResolution[];
40+
validityInterval?: Wallet.Cardano.ValidityInterval;
4041
};
4142
error?: string;
4243
reachedMaxAmountList?: (string | Wallet.Cardano.AssetId)[];

packages/core/src/ui/components/DappTransaction/DappTransaction.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,16 @@ import styles from './DappTransaction.module.scss';
1010
import { useTranslate } from '@src/ui/hooks';
1111
import { TransactionFee, Collateral } from '@ui/components/ActivityDetail';
1212

13-
import { TransactionType, DappTransactionSummary, TransactionAssets, Text, Box, Divider } from '@lace/ui';
13+
import {
14+
TransactionType,
15+
DappTransactionSummary,
16+
TransactionAssets,
17+
DappTransactionTextField,
18+
Flex,
19+
Text,
20+
Box,
21+
Divider
22+
} from '@lace/ui';
1423
import { DappAddressSections } from '../DappAddressSections/DappAddressSections';
1524

1625
const amountTransformer = (fiat: { price: number; code: string }) => (ada: string) =>
@@ -26,6 +35,7 @@ export interface DappTransactionProps {
2635
fiatCurrencyCode?: string;
2736
fiatCurrencyPrice?: number;
2837
coinSymbol?: string;
38+
expiresBy?: { utcDate: string; utcTime: string };
2939
/** tokens send to being sent to or from the user */
3040
fromAddress: Map<Cardano.PaymentAddress, TokenTransferValue>;
3141
toAddress: Map<Cardano.PaymentAddress, TokenTransferValue>;
@@ -113,6 +123,7 @@ export const DappTransaction = ({
113123
fiatCurrencyPrice,
114124
coinSymbol,
115125
dappInfo,
126+
expiresBy,
116127
ownAddresses = [],
117128
addressToNameMap = new Map()
118129
}: DappTransactionProps): React.ReactElement => {
@@ -124,6 +135,17 @@ export const DappTransaction = ({
124135
const isFromAddressesEnabled = groupedFromAddresses.size > 0;
125136
const isToAddressesEnabled = groupedToAddresses.size > 0;
126137

138+
const expireByText = expiresBy ? (
139+
<Flex flexDirection="column" alignItems="flex-end">
140+
<span>{expiresBy.utcDate}</span>
141+
<span>
142+
{expiresBy.utcTime} {t('core.outputSummaryList.utc')}
143+
</span>
144+
</Flex>
145+
) : (
146+
t('core.outputSummaryList.noLimit')
147+
);
148+
127149
return (
128150
<div>
129151
{errorMessage && <ErrorPane error={errorMessage} className={styles.error} />}
@@ -184,6 +206,14 @@ export const DappTransaction = ({
184206
/>
185207
)}
186208

209+
<div className={styles.depositContainer}>
210+
<DappTransactionTextField
211+
text={expireByText}
212+
label={t('core.outputSummaryList.expiresBy')}
213+
tooltip={t('core.outputSummaryList.expiresByTooltip')}
214+
/>
215+
</div>
216+
187217
{returnedDeposit !== BigInt(0) && (
188218
<TransactionFee
189219
fee={Wallet.util.lovelacesToAdaString(returnedDeposit.toString())}

packages/core/src/ui/components/OutputSummaryList/OutputSummaryList.module.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@
2727
color: var(--text-color-primary);
2828
}
2929

30+
.validityIntervalExpiresBy {
31+
@include text-body-medium;
32+
display: flex;
33+
flex-direction: column;
34+
color: var(--text-color-primary);
35+
overflow-wrap: break-word;
36+
text-align: right;
37+
}
38+
3039
.metadataRow {
3140
display: flex;
3241
justify-content: space-between;

packages/core/src/ui/components/OutputSummaryList/OutputSummaryList.tsx

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { renderAmountInfo, renderLabel, RowContainer } from '../OutputSummary/Ou
44
import { Typography } from 'antd';
55
import styles from './OutputSummaryList.module.scss';
66
import { TranslationsFor } from '@ui/utils/types';
7+
import { Flex } from '@lace/ui';
78

89
const { Text } = Typography;
910

@@ -19,9 +20,21 @@ export type Costs = {
1920
export interface OutputSummaryListProps {
2021
rows: OutputSummaryProps[];
2122
txFee: Costs & TootipText;
23+
expiresBy: { utcDate: string; utcTime: string };
2224
deposit?: Costs & TootipText;
2325
metadata?: string;
24-
translations: TranslationsFor<'recipientAddress' | 'sending' | 'txFee' | 'deposit' | 'metadata' | 'output'>;
26+
translations: TranslationsFor<
27+
| 'recipientAddress'
28+
| 'sending'
29+
| 'txFee'
30+
| 'deposit'
31+
| 'metadata'
32+
| 'output'
33+
| 'expiresBy'
34+
| 'expiresByTooltip'
35+
| 'noLimit'
36+
| 'utc'
37+
>;
2538
ownAddresses?: string[];
2639
onFeeTooltipHover?: () => unknown;
2740
onDepositTooltipHover?: () => unknown;
@@ -33,6 +46,7 @@ export const OutputSummaryList = ({
3346
metadata,
3447
deposit,
3548
translations,
49+
expiresBy,
3650
ownAddresses,
3751
onFeeTooltipHover,
3852
onDepositTooltipHover
@@ -42,6 +56,17 @@ export const OutputSummaryList = ({
4256
sending: translations.sending
4357
};
4458

59+
const expireByText = expiresBy ? (
60+
<Flex flexDirection="column" alignItems="flex-end">
61+
<span>{expiresBy.utcDate}</span>
62+
<span>
63+
{expiresBy.utcTime} {translations.utc}
64+
</span>
65+
</Flex>
66+
) : (
67+
translations.noLimit
68+
);
69+
4570
return (
4671
<div className={styles.listContainer}>
4772
{rows.map((row, idx) => (
@@ -54,7 +79,6 @@ export const OutputSummaryList = ({
5479
<OutputSummary {...row} translations={outputSummaryTranslations} ownAddresses={ownAddresses} />
5580
</div>
5681
))}
57-
5882
{metadata && (
5983
<div className={styles.metadataRow} data-testid="metadata-container">
6084
<Text className={styles.metadataLabel} data-testid="metadata-label">
@@ -67,6 +91,18 @@ export const OutputSummaryList = ({
6791
)}
6892

6993
<div className={styles.feeContainer} data-testid="summary-fee-container">
94+
<RowContainer>
95+
{renderLabel({
96+
label: translations.expiresBy,
97+
tooltipContent: translations.expiresByTooltip,
98+
dataTestId: 'validity-interval-expires-by-label'
99+
})}
100+
101+
<Text className={styles.validityIntervalExpiresBy} data-testid="validity-interval-expires-by-value">
102+
{expireByText}
103+
</Text>
104+
</RowContainer>
105+
70106
{deposit && (
71107
<RowContainer>
72108
{renderLabel({

0 commit comments

Comments
 (0)