Skip to content

Commit

Permalink
feat(extension): LW-9178 collateral output in tx activity details (#844)
Browse files Browse the repository at this point in the history
* feat(ui): create info bar component

* fix(ui): add zIndex property to tooltip

* feat(core): add collateral tooltip and info bar

* feat(extension): display collateral info on activity details

* refactor(extension): use wallet state

* refactor(extension): reduce argument scope

* refactor(extension): move ternary operation to variable declaration

* refactor(core): rename tooltip function

* refactor(ui): remove unused variable

* refactor(core): ignore spendable activity status

* feat(extension): display collateral balance

* refactor(extension): move phase 2 validation func to utils
  • Loading branch information
renanvalentin authored Mar 25, 2024
1 parent 57138c6 commit 2db23bf
Show file tree
Hide file tree
Showing 21 changed files with 407 additions and 34 deletions.
10 changes: 8 additions & 2 deletions apps/browser-extension-wallet/src/lib/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,14 @@
"multipleAddresses": "Multiple addresses",
"pools": "Pool(s)",
"epoch": "Epoch",
"collateral": "Collateral",
"collateralInfo": "Amount set as collateral to cover contract execution failure. In case of no failure collateral remains unspent."
"collateral": {
"label": "Collateral",
"tooltip": {
"info": "Amount set as collateral to cover contract execution failure. In case of no failure collateral remains unspent.",
"success": "Amount set as collateral. Since this Tx was successful collateral was not used"
},
"error": "Since the transaction was unsuccessful, collateral has been used to cover the costs incurred from the contract execution failure."
}
},
"walletNameAndPasswordSetupStep": {
"title": "Let's set up your new wallet",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import {
governanceProposalsTransformer,
votingProceduresTransformer
} from '@src/views/browser-view/features/activity/helpers/common-tx-transformer';
import { createHistoricalOwnInputResolver, HistoricalOwnInputResolverArgs } from '@src/utils/own-input-resolver';
import { getCollateral } from '@cardano-sdk/core';
import { hasPhase2ValidationFailed } from '@src/utils/phase2-validation';

/**
* validates if the transaction is confirmed
Expand Down Expand Up @@ -60,16 +63,22 @@ const transactionMetadataTransformer = (
[...metadata.entries()].map(([key, value]) => ({ key: key.toString(), value: Wallet.cardanoMetadatumToObj(value) }));

const shouldIncludeFee = (
tx: Wallet.Cardano.HydratedTx | Wallet.Cardano.Tx,
type: ActivityType,
delegationInfo: Wallet.Cardano.StakeDelegationCertificate[] | undefined
) =>
!(
) => {
if (hasPhase2ValidationFailed(tx)) {
return false;
}

return !(
type === DelegationActivityType.delegationRegistration ||
// Existence of any (new) delegationInfo means that this "de-registration"
// activity is accompanied by a "delegation" activity, which carries the fees.
// However, fees should be shown if de-registration activity is standalone.
(type === DelegationActivityType.delegationDeregistration && !!delegationInfo?.length)
);
};

const getPoolInfos = async (poolIds: Wallet.Cardano.PoolId[], stakePoolProvider: Wallet.StakePoolProvider) => {
const filters: Wallet.QueryStakePoolsArgs = {
Expand All @@ -89,6 +98,22 @@ const getPoolInfos = async (poolIds: Wallet.Cardano.PoolId[], stakePoolProvider:
return pools;
};

const computeCollateral = async (
{ addresses, transactions }: HistoricalOwnInputResolverArgs,
tx?: Wallet.Cardano.Tx
): Promise<bigint> => {
const inputResolver = createHistoricalOwnInputResolver({
addresses,
transactions
});

return await getCollateral(
tx,
inputResolver,
addresses.map((addr) => addr.address)
);
};

/**
* fetches asset information
*/
Expand All @@ -106,7 +131,8 @@ const buildGetActivityDetail =
inMemoryWallet: wallet,
walletUI: { cardanoCoin },
activityDetail,
walletInfo
walletInfo,
walletState
} = get();

set({ fetchingActivityInfo: true });
Expand Down Expand Up @@ -204,6 +230,8 @@ const buildGetActivityDetail =
? Wallet.util.lovelacesToAdaString(depositReclaimValue.toString())
: undefined;
const feeInAda = Wallet.util.lovelacesToAdaString(tx.body.fee.toString());
const collateral = await computeCollateral(walletState, tx);
const collateralInAda = collateral > 0 ? Wallet.util.lovelacesToAdaString(collateral.toString()) : undefined;

// Delegation tx additional data (LW-3324)

Expand All @@ -214,7 +242,7 @@ const buildGetActivityDetail =
let transaction: ActivityDetail['activity'] = {
hash: tx.id.toString(),
totalOutput: totalOutputInAda,
fee: shouldIncludeFee(type, delegationInfo) ? feeInAda : undefined,
fee: shouldIncludeFee(tx, type, delegationInfo) ? feeInAda : undefined,
deposit,
depositReclaim,
addrInputs: inputs,
Expand All @@ -230,7 +258,8 @@ const buildGetActivityDetail =
fiatCurrency,
proposalProcedures: tx.body.proposalProcedures
}),
certificates: certificateTransformer(cardanoCoin, coinPrices, fiatCurrency, tx.body.certificates)
certificates: certificateTransformer(cardanoCoin, coinPrices, fiatCurrency, tx.body.certificates),
collateral: collateralInAda
};

if (type === DelegationActivityType.delegation && delegationInfo) {
Expand Down
1 change: 1 addition & 0 deletions apps/browser-extension-wallet/src/types/activity-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type TransactionActivity = {
includedUtcTime?: string;
totalOutput?: string;
fee?: string;
collateral?: string;
depositReclaim?: string;
deposit?: string;
addrInputs?: TxOutputInput[];
Expand Down
4 changes: 2 additions & 2 deletions apps/browser-extension-wallet/src/utils/own-input-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
import { ObservableWalletState } from '@hooks/useWalletState';
import { Wallet } from '@lace/cardano';

type Args = Pick<ObservableWalletState, 'addresses'> & {
export type HistoricalOwnInputResolverArgs = Pick<ObservableWalletState, 'addresses'> & {
transactions: Pick<ObservableWalletState['transactions'], 'history'>;
};

export const createHistoricalOwnInputResolver = ({
transactions: { history: txs },
addresses
}: Args): Wallet.Cardano.InputResolver => ({
}: HistoricalOwnInputResolverArgs): Wallet.Cardano.InputResolver => ({
async resolveInput({ txId, index }: Wallet.Cardano.TxIn) {
for (const tx of txs) {
if (txId !== tx.id) {
Expand Down
5 changes: 5 additions & 0 deletions apps/browser-extension-wallet/src/utils/phase2-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Wallet } from '@lace/cardano';

export const hasPhase2ValidationFailed = (
tx: Wallet.TxInFlight | Wallet.Cardano.HydratedTx | Wallet.Cardano.Tx
): boolean => 'inputSource' in tx && tx.inputSource === Wallet.Cardano.InputSource.collaterals;
6 changes: 5 additions & 1 deletion apps/browser-extension-wallet/src/utils/tx-inspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '@lace/core';
import { TxDirection, TxDirections } from '@src/types';

const { CertificateType, GovernanceActionType, Vote, VoterType } = Wallet.Cardano;
const { CertificateType, GovernanceActionType, Vote, VoterType, InputSource } = Wallet.Cardano;

const hasWalletStakeAddress = (
withdrawals: Wallet.Cardano.HydratedTx['body']['withdrawals'],
Expand Down Expand Up @@ -142,6 +142,10 @@ export const inspectTxType = async ({
tx: Wallet.Cardano.HydratedTx;
inputResolver: Wallet.Cardano.InputResolver;
}): Promise<Exclude<ActivityType, TransactionActivityType.rewards>> => {
if (tx.inputSource === InputSource.collaterals) {
return TransactionActivityType.outgoing;
}

const { paymentAddresses, rewardAccounts } = getWalletAccounts(walletAddresses);

const inspectionProperties = await createTxInspector({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export const TransactionDetailsProxy = withAddressBookContext(
metadata,
proposalProcedures,
votingProcedures,
certificates
certificates,
collateral
} = activityInfo.activity;
const txSummary = useMemo(
() =>
Expand Down Expand Up @@ -104,6 +105,7 @@ export const TransactionDetailsProxy = withAddressBookContext(
certificates={certificates}
handleOpenExternalHashLink={handleOpenExternalHashLink}
openExternalLink={openExternalLink}
collateral={collateral}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import isEmpty from 'lodash/isEmpty';
import { PriceResult } from '@hooks';
import { formatPercentages } from '@lace/common';
import { depositPaidWithSymbol } from '@src/features/dapp/components/confirm-transaction/utils';
import { hasPhase2ValidationFailed } from '@src/utils/phase2-validation';

const { util, GovernanceActionType, PlutusLanguageVersion, CertificateType } = Wallet.Cardano;

Expand Down Expand Up @@ -98,7 +99,14 @@ const splitDelegationTx = (tx: TransformedActivity): TransformedTransactionActiv
];
};

const transformTransactionStatus = (status: Wallet.TransactionStatus): ActivityStatus => {
const transformTransactionStatus = (
tx: Wallet.TxInFlight | Wallet.Cardano.HydratedTx,
status: Wallet.TransactionStatus
): ActivityStatus => {
if (hasPhase2ValidationFailed(tx)) {
return ActivityStatus.ERROR;
}

const statuses = {
[Wallet.TransactionStatus.PENDING]: ActivityStatus.PENDING,
[Wallet.TransactionStatus.ERROR]: ActivityStatus.ERROR,
Expand All @@ -107,6 +115,53 @@ const transformTransactionStatus = (status: Wallet.TransactionStatus): ActivityS
};
return statuses[status];
};

type GetTxFormattedAmount = (
args: Pick<
TxTransformerInput,
'walletAddresses' | 'tx' | 'direction' | 'resolveInput' | 'cardanoCoin' | 'fiatCurrency' | 'fiatPrice'
>
) => Promise<{
amount: string;
fiatAmount: string;
}>;

const getTxFormattedAmount: GetTxFormattedAmount = async ({
resolveInput,
tx,
walletAddresses,
direction,
cardanoCoin,
fiatCurrency,
fiatPrice
}) => {
if (hasPhase2ValidationFailed(tx)) {
return {
amount: Wallet.util.getFormattedAmount({ amount: tx.body.totalCollateral.toString(), cardanoCoin }),
fiatAmount: getFormattedFiatAmount({
amount: new BigNumber(tx.body.totalCollateral?.toString() ?? '0'),
fiatCurrency,
fiatPrice
})
};
}

const outputAmount = await getTransactionTotalAmount({
addresses: walletAddresses,
inputs: tx.body.inputs,
outputs: tx.body.outputs,
fee: tx.body.fee,
direction,
withdrawals: tx.body.withdrawals,
resolveInput
});

return {
amount: Wallet.util.getFormattedAmount({ amount: outputAmount.toString(), cardanoCoin }),
fiatAmount: getFormattedFiatAmount({ amount: outputAmount, fiatCurrency, fiatPrice })
};
};

/**
Simplifies the transaction object to be used in the activity list
Expand Down Expand Up @@ -145,15 +200,7 @@ export const txTransformer = async ({
tx: tx as unknown as Wallet.Cardano.HydratedTx,
direction
});
const outputAmount = await getTransactionTotalAmount({
addresses: walletAddresses,
inputs: tx.body.inputs,
outputs: tx.body.outputs,
fee: tx.body.fee,
direction,
withdrawals: tx.body.withdrawals,
resolveInput
});

const formattedDate = dayjs().isSame(date, 'day')
? 'Today'
: formatDate({ date, format: 'DD MMMM YYYY', type: 'local' });
Expand All @@ -168,14 +215,24 @@ export const txTransformer = async ({
.sort((a, b) => Number(b.val) - Number(a.val))
: [];

const formattedAmount = await getTxFormattedAmount({
cardanoCoin,
fiatCurrency,
resolveInput,
tx,
walletAddresses,
direction,
fiatPrice
});

const baseTransformedActivity = {
id: tx.id.toString(),
deposit,
depositReclaim,
fee: Wallet.util.lovelacesToAdaString(tx.body.fee.toString()),
status: transformTransactionStatus(status),
amount: Wallet.util.getFormattedAmount({ amount: outputAmount.toString(), cardanoCoin }),
fiatAmount: getFormattedFiatAmount({ amount: outputAmount, fiatCurrency, fiatPrice }),
status: transformTransactionStatus(tx, status),
amount: formattedAmount.amount,
fiatAmount: formattedAmount.fiatAmount,
assets: assetsEntries,
assetsNumber: (assets?.size ?? 0) + 1,
date,
Expand Down
50 changes: 42 additions & 8 deletions packages/core/src/ui/components/ActivityDetail/Collateral.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,55 @@
import React from 'react';
import { useTranslate } from '@src/ui/hooks';
import { TransactionSummary } from '@lace/ui';
import { Box, TransactionSummary, InfoBar } from '@lace/ui';
import { ReactComponent as InfoIcon } from '@lace/icons/dist/InfoComponent';

export enum CollateralStatus {
REVIEW = 'review',
SUCCESS = 'success',
ERROR = 'error',
NONE = 'none'
}

export interface Props {
collateral: string;
amountTransformer: (amount: string) => string;
coinSymbol: string;
status?: CollateralStatus;
}
export const Collateral = ({ collateral, amountTransformer, coinSymbol }: Props): React.ReactElement => {

export const Collateral = ({
collateral,
amountTransformer,
coinSymbol,
status = CollateralStatus.REVIEW
}: Props): React.ReactElement => {
const { t } = useTranslate();

const getTooltipText = (): string => {
switch (status) {
case 'review':
case 'error':
return t('package.core.activityDetails.collateral.tooltip.info');
case 'success':
return t('package.core.activityDetails.collateral.tooltip.success');
}

return '';
};

return (
<TransactionSummary.Amount
amount={`${collateral} ${coinSymbol}`}
fiatPrice={amountTransformer(collateral)}
label={t('package.core.activityDetails.collateral')}
tooltip={t('package.core.activityDetails.collateralInfo')}
/>
<>
<TransactionSummary.Amount
amount={`${collateral} ${coinSymbol}`}
fiatPrice={amountTransformer(collateral)}
label={t('package.core.activityDetails.collateral.label')}
tooltip={getTooltipText()}
/>
{status === CollateralStatus.ERROR && (
<Box mt="$32">
<InfoBar icon={<InfoIcon />} message={t('package.core.activityDetails.collateral.error')} />
</Box>
)}
</>
);
};
Loading

0 comments on commit 2db23bf

Please sign in to comment.