Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(extension): LW-9178 collateral output in tx activity details #844

Merged
merged 13 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
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
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');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be good to have a dedicated tooltip's text for review and error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a status bar below the collateral when phase 2 fails.

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