Skip to content

Commit

Permalink
Transaction expired status (#2272)
Browse files Browse the repository at this point in the history
* Introduce Transaction EXPIRED status in transactions list

* Delete tx improvement while tx in failed state

* Testing command for creating expired transaction added

* Adds CHANGELOG entry

* Update cardano-wallet version tag

* Run translation manager

Co-authored-by: Nikola Glumac <[email protected]>
  • Loading branch information
Tomislav Horaček and nikolaglumac authored Dec 10, 2020
1 parent acf79fc commit 383fcfb
Show file tree
Hide file tree
Showing 24 changed files with 287 additions and 46 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Changelog

### Fixes

- Fixed handling of expired transactions ([PR 2272](https://github.com/input-output-hk/daedalus/pull/2272))
- Fixed visual glitch on the transaction list switching between filters ([PR 2261](https://github.com/input-output-hk/daedalus/pull/2261))

### Chores
Expand Down
2 changes: 1 addition & 1 deletion nix/sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"type": "tarball",
"url": "https://github.com/input-output-hk/cardano-wallet/archive/bcbed39dbe61b978453d6dee94140504bd0f6f9b.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz",
"version": "v2020-11-26"
"version": "v2020-12-08"
},
"gitignore": {
"branch": "master",
Expand Down
80 changes: 78 additions & 2 deletions source/renderer/app/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,74 @@ export default class AdaApi {
}
};

// For testing purpose ONLY
createExpiredTransaction = async (request: any): Promise<*> => {
if (global.environment.isDev) {
logger.debug('AdaApi::createTransaction called', {
parameters: filterLogData(request),
});
const {
walletId,
address,
amount,
passphrase,
isLegacy,
withdrawal = TransactionWithdrawal,
ttl,
} = request;
try {
const data = {
payments: [
{
address,
amount: {
quantity: amount,
unit: WalletUnits.LOVELACE,
},
},
],
passphrase,
time_to_live: {
quantity: ttl,
unit: 'second',
},
};

let response: Transaction;
if (isLegacy) {
response = await createByronWalletTransaction(this.config, {
walletId,
data,
});
} else {
response = await createTransaction(this.config, {
walletId,
data: { ...data, withdrawal },
});
}

logger.debug('AdaApi::createTransaction success', {
transaction: response,
});

return _createTransactionFromServerData(response);
} catch (error) {
logger.error('AdaApi::createTransaction error', { error });
throw new ApiError(error)
.set('wrongEncryptionPassphrase')
.where('code', 'bad_request')
.inc('message', 'passphrase is too short')
.set('transactionIsTooBig', true, {
linkLabel: 'tooBigTransactionErrorLinkLabel',
linkURL: 'tooBigTransactionErrorLinkURL',
})
.where('code', 'transaction_is_too_big')
.result();
}
}
return null;
};

calculateTransactionFee = async (
request: GetTransactionFeeRequest
): Promise<BigNumber> => {
Expand Down Expand Up @@ -2113,8 +2181,16 @@ const _createAddressFromServerData = action(
}
);

const _conditionToTxState = (condition: string) =>
TransactionStates[condition === 'pending' ? 'PENDING' : 'OK'];
const _conditionToTxState = (condition: string) => {
switch (condition) {
case 'pending':
return TransactionStates.PENDING;
case 'expired':
return TransactionStates.FAILED;
default:
return TransactionStates.OK;
}
};

const _createTransactionFromServerData = action(
'AdaApi::_createTransactionFromServerData',
Expand Down
2 changes: 1 addition & 1 deletion source/renderer/app/api/transactions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export type TransactionWithdrawals = {
};
export type TransactionWithdrawalType = 'self' | Array<string>;

export type TransactionState = 'pending' | 'in_ledger';
export type TransactionState = 'pending' | 'in_ledger' | 'expired';

export type TrasactionAddresses = {
from: Array<?string>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@ import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin';
import styles from './CancelTransactionButton.scss';

const messages = defineMessages({
label: {
cancelLabel: {
id: 'wallet.transaction.pending.cancelTransactionButton',
defaultMessage: '!!!Cancel pending transaction',
description: 'Label for the cancel pending transaction button',
},
removeLabel: {
id: 'wallet.transaction.failed.removeTransactionButton',
defaultMessage: '!!!Remove failed transaction',
description: 'Label for the remove failed transaction button',
},
});

type Props = {
onClick: Function,
state: 'cancel' | 'remove',
};

export default class CancelTransactionButton extends Component<Props> {
Expand All @@ -23,8 +29,9 @@ export default class CancelTransactionButton extends Component<Props> {
};

render() {
const { onClick } = this.props;
const label = this.context.intl.formatMessage(messages.label);
const { onClick, state } = this.props;
const label = this.context.intl.formatMessage(messages[`${state}Label`]);

return (
<Button
className="attention"
Expand Down
97 changes: 69 additions & 28 deletions source/renderer/app/components/wallet/transactions/Transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ const messages = defineMessages({
'!!!This transaction has been pending for a long time. To release the funds used by this transaction, you can try canceling it.',
description: 'Note to cancel a transaction that has been pending too long',
},
supportArticleLink: {
id: 'wallet.transaction.pending.supportArticleLink',
cancelPendingTxnSupportArticle: {
id: 'wallet.transaction.pending.cancelPendingTxnSupportArticle',
defaultMessage: '!!!Why should I cancel this transaction?',
description: 'Link to support article for canceling a pending transaction',
},
Expand All @@ -123,6 +123,17 @@ const messages = defineMessages({
defaultMessage: '!!!to see these addresses.',
description: 'Unresolved Input Addresses additional label.',
},
cancelFailedTxnNote: {
id: 'wallet.transaction.failed.cancelFailedTxnNote',
defaultMessage:
'!!!This transaction was submitted to the Cardano network, but it expired, so it failed. Transactions on the Cardano network have a ‘time to live’ attribute, which passed before the network processed the transaction. Please, remove it to release the funds (UTXOs) used by this transaction to use those funds in another transaction.',
description: 'Note to cancel a transaction that has been failed',
},
cancelFailedTxnSupportArticle: {
id: 'wallet.transaction.failed.cancelFailedTxnSupportArticle',
defaultMessage: '!!!Why should I cancel failed transactions?',
description: 'Link to support article for removing a failed transaction',
},
});

const stateTranslations = defineMessages({
Expand All @@ -136,6 +147,11 @@ const stateTranslations = defineMessages({
defaultMessage: '!!!Transaction pending',
description: 'Transaction state "pending"',
},
[TransactionStates.FAILED]: {
id: 'wallet.transaction.state.failed',
defaultMessage: '!!!Transaction failed',
description: 'Transaction state "failed"',
},
});

type Props = {
Expand Down Expand Up @@ -182,7 +198,10 @@ export default class Transaction extends Component<Props, State> {
deletePendingTransaction = async () => {
const { data, walletId } = this.props;
const { id: transactionId, state } = data;
if (state !== TransactionStates.PENDING) {
if (
state !== TransactionStates.PENDING &&
state !== TransactionStates.FAILED
) {
return this.hideConfirmationDialog();
}
await this.props.deletePendingTransaction({
Expand Down Expand Up @@ -222,33 +241,50 @@ export default class Transaction extends Component<Props, State> {
};

renderCancelPendingTxnContent = () => {
const { data } = this.props;
const { state } = data;
const { intl } = this.context;
const overPendingTimeLimit = this.hasExceededPendingTimeLimit();

if (!overPendingTimeLimit) return null;

return (
<Fragment>
<div className={styles.pendingTxnNote}>
{intl.formatMessage(messages.cancelPendingTxnNote)}
<Link
className={styles.articleLink}
onClick={this.handleOpenSupportArticle}
label={intl.formatMessage(messages.supportArticleLink)}
underlineOnHover
skin={LinkSkin}
/>
</div>
<div>
<CancelTransactionButton onClick={this.showConfirmationDialog} />
</div>
</Fragment>
);
if (overPendingTimeLimit || state === TransactionStates.FAILED) {
return (
<Fragment>
<div className={styles.pendingTxnNote}>
{state === TransactionStates.PENDING
? intl.formatMessage(messages.cancelPendingTxnNote)
: intl.formatMessage(messages.cancelFailedTxnNote)}
<Link
className={styles.articleLink}
onClick={this.handleOpenSupportArticle}
label={
state === TransactionStates.PENDING
? intl.formatMessage(messages.cancelPendingTxnSupportArticle)
: intl.formatMessage(messages.cancelFailedTxnSupportArticle)
}
underlineOnHover
skin={LinkSkin}
/>
</div>
<div>
<CancelTransactionButton
state={state === TransactionStates.PENDING ? 'cancel' : 'remove'}
onClick={
state === TransactionStates.PENDING
? this.showConfirmationDialog
: this.deletePendingTransaction
}
/>
</div>
</Fragment>
);
}
return null;
};

renderTxnStateTag = () => {
const { intl } = this.context;
const { state } = this.props;

const styleLabel = this.hasExceededPendingTimeLimit()
? `${state}WarningLabel`
: `${state}Label`;
Expand All @@ -275,8 +311,6 @@ export default class Transaction extends Component<Props, State> {
const { intl } = this.context;
const { showConfirmationDialog } = this.state;

const isPendingTransaction = state === TransactionStates.PENDING;

const componentStyles = classNames([
styles.component,
isExpanded ? 'Transaction_expanded' : null,
Expand All @@ -302,9 +336,16 @@ export default class Transaction extends Component<Props, State> {
const currency = intl.formatMessage(globalMessages.currency);
const symbol = adaSymbol;

const iconType = isPendingTransaction
? TransactionStates.PENDING
: data.type;
const getIconType = (txState) => {
switch (txState) {
case TransactionStates.PENDING:
return TransactionStates.PENDING;
case TransactionStates.FAILED:
return TransactionStates.FAILED;
default:
return data.type;
}
};

const exceedsPendingTimeLimit = this.hasExceededPendingTimeLimit();

Expand Down Expand Up @@ -364,7 +405,7 @@ export default class Transaction extends Component<Props, State> {
<div className={styles.toggler}>
<TransactionTypeIcon
exceedsPendingTimeLimit={exceedsPendingTimeLimit}
iconType={iconType}
iconType={getIconType(state)}
/>

<div className={styles.togglerContent}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@

.pendingLabel,
.okLabel,
.expiredLabel,
.in_ledgerLabel,
.pendingWarningLabel {
background-color: var(--theme-transactions-state-pending-background-color);
Expand All @@ -106,6 +107,12 @@
);
}

.expiredLabel {
background-color: var(
--theme-transactions-state-pending-warning-background-color
);
}

.type {
font-family: var(--font-light);
font-size: 14px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,24 @@ export default class TransactionTypeIcon extends Component<Props> {
</div>
);

renderFailedIcon = () => {
return (
<SVGInline svg={pendingIcon} className={styles.transactionTypeIcon} />
);
};

renderIcon = (icon: string) => {
let iconType;
if (this.props.iconType === TransactionStates.PENDING) {
return this.renderPendingIcon();
iconType = this.renderPendingIcon();
} else if (this.props.iconType === TransactionStates.FAILED) {
iconType = this.renderFailedIcon();
} else {
iconType = (
<SVGInline svg={icon} className={styles.transactionTypeIcon} />
);
}
return <SVGInline svg={icon} className={styles.transactionTypeIcon} />;
return iconType;
};

render() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
);
}
}

&.expired {
background: var(--theme-transactions-icon-type-failed-background-color);
}
}

.pendingTxnIconWrapper {
Expand Down
1 change: 1 addition & 0 deletions source/renderer/app/domains/WalletTransaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const TransactionStates: EnumMap<string, TransactionState> = {
PENDING: 'pending',
OK: 'in_ledger',
IN_LEDGER: 'in_ledger',
FAILED: 'expired',
};

export const TransactionTypes: EnumMap<string, TransactionType> = {
Expand Down
Loading

0 comments on commit 383fcfb

Please sign in to comment.