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

Show fee & Intrawallet transactions #241

Merged
merged 5 commits into from
Jan 18, 2019
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
110 changes: 93 additions & 17 deletions app/api/ada/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import Wallet from '../../domain/Wallet';
import WalletTransaction, {
transactionTypes
} from '../../domain/WalletTransaction';
import type {
TransactionType
} from '../../domain/WalletTransaction';
import WalletAddress from '../../domain/WalletAddress';
import { LOVELACES_PER_ADA } from '../../config/numbersConfig';
import {
Expand All @@ -24,6 +27,7 @@ import {
isValidAdaAddress,
newExternalAdaAddress,
getAdaAddressesByType,
getAdaAddressesList,
saveAdaAddress
} from './adaAddress';
import {
Expand Down Expand Up @@ -59,6 +63,7 @@ import type {
AdaTransaction,
AdaTransactionCondition,
AdaTransactionFee,
AdaTransactionInputOutput,
AdaTransactions,
AdaWallet,
AdaWallets,
Expand Down Expand Up @@ -219,13 +224,15 @@ export default class AdaApi {
const transactions = limit
? history[0].slice(skip, skip + limit)
: history[0];
const mappedTransactions = transactions.map(data => (
_createTransactionFromServerData(data)
));
return Promise.resolve({
transactions: mappedTransactions,
total: history[1]

const mappedTransactions = transactions.map(async data => {
const { type, amount, fee } = await _getTxFinancialInfo(data);
return _createTransactionFromServerData(data, type, amount, fee);
});
return Promise.all(mappedTransactions).then(mappedTxs => Promise.resolve({
transactions: mappedTxs,
total: history[1]
}));
} catch (error) {
Logger.error('AdaApi::refreshTransactions error: ' + stringifyError(error));
throw new GenericApiError();
Expand All @@ -237,9 +244,10 @@ export default class AdaApi {
try {
const pendingTxs = await getPendingAdaTxs();
Logger.debug('AdaApi::refreshPendingTransactions success: ' + stringifyData(pendingTxs));
return pendingTxs.map(data => (
_createTransactionFromServerData(data)
));
return Promise.all(pendingTxs.map(async data => {
const { type, amount, fee } = await _getTxFinancialInfo(data);
return _createTransactionFromServerData(data, type, amount, fee);
}));
} catch (error) {
Logger.error('AdaApi::refreshPendingTransactions error: ' + stringifyError(error));
throw new GenericApiError();
Expand Down Expand Up @@ -563,6 +571,78 @@ export default class AdaApi {

// ========== TRANSFORM SERVER DATA INTO FRONTEND MODELS =========

async function _getTxFinancialInfo(
data: AdaTransaction
): Promise<{
type: TransactionType,
amount: BigNumber,
fee: BigNumber
}> {
// Note: logic taken from the mobile version of Yoroi
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe it could be helpful to have more info from which file

// https://github.com/Emurgo/yoroi-mobile/blob/a3d72218b1e63f6362152aae2f03c8763c168795/src/crypto/transactionUtils.js#L73-L103

const adaAddresses = await getAdaAddressesList();
const addresses: Array<string> = adaAddresses.map(addr => addr.cadId);

const ownInputs = data.ctInputs.filter(input => (
addresses.includes(input[0])
));

const ownOutputs = data.ctOutputs.filter(output => (
addresses.includes(output[0])
));

const _sum = (IOs: Array<AdaTransactionInputOutput>): BigNumber => (
IOs.reduce(
(accum: BigNumber, io) => accum.plus(new BigNumber(io[1].getCCoin, 10)),
new BigNumber(0),
)
);

const totalIn = _sum(data.ctInputs);
const totalOut = _sum(data.ctOutputs);
const ownIn = _sum(ownInputs);
const ownOut = _sum(ownOutputs);

const hasOnlyOwnInputs = ownInputs.length === data.ctInputs.length;
const hasOnlyOwnOutputs = ownOutputs.length === data.ctOutputs.length;
const isIntraWallet = hasOnlyOwnInputs && hasOnlyOwnOutputs;
const isMultiParty =
ownInputs.length > 0 && ownInputs.length !== data.ctInputs.length;

const brutto = ownOut.minus(ownIn);
const totalFee = totalOut.minus(totalIn); // should be negative

if (isIntraWallet) {
return {
type: transactionTypes.SELF,
amount: new BigNumber(0),
fee: totalFee
};
}
if (isMultiParty) {
return {
type: transactionTypes.MULTI,
amount: brutto,
// note: fees not accurate but no good way of finding which UTXO paid the fees in Yoroi
fee: new BigNumber(0)
};
}
if (hasOnlyOwnInputs) {
return {
type: transactionTypes.EXPEND,
amount: brutto.minus(totalFee),
fee: totalFee
};
}

return {
type: transactionTypes.INCOME,
amount: brutto,
fee: new BigNumber(0)
};
}

const _createWalletFromServerData = action(
'AdaApi::_createWalletFromServerData',
(adaWallet: AdaWallet) => {
Expand Down Expand Up @@ -608,18 +688,14 @@ const _conditionToTxState = (condition: AdaTransactionCondition) => {

const _createTransactionFromServerData = action(
'AdaApi::_createTransactionFromServerData',
(data: AdaTransaction) => {
const coins = new BigNumber(data.ctAmount.getCCoin);
(data: AdaTransaction, type: TransactionType, amount: BigNumber, fee: BigNumber) => {
const { ctmTitle, ctmDescription, ctmDate } = data.ctMeta;
return new WalletTransaction({
id: data.ctId,
title: ctmTitle || data.ctIsOutgoing ? 'Ada sent' : 'Ada received',
type: data.ctIsOutgoing
? transactionTypes.EXPEND
: transactionTypes.INCOME,
amount: (data.ctIsOutgoing ? coins.negated() : coins).dividedBy(
LOVELACES_PER_ADA
),
type,
amount: amount.dividedBy(LOVELACES_PER_ADA).plus(fee.dividedBy(LOVELACES_PER_ADA)),
fee: fee.dividedBy(LOVELACES_PER_ADA),
date: new Date(ctmDate),
description: ctmDescription || '',
numberOfConfirmations: getLastBlockNumber() - data.ctBlockNumber,
Expand Down
67 changes: 52 additions & 15 deletions app/components/wallet/transactions/Transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@ import { assuranceLevels } from '../../../config/transactionAssuranceConfig';
import { environmentSpecificMessages } from '../../../i18n/global-messages';
import type { TransactionState } from '../../../domain/WalletTransaction';
import environment from '../../../environment';
import { Logger } from '../../../utils/logging';

const messages = defineMessages({
card: {
id: 'wallet.transaction.type.card',
defaultMessage: '!!!Card payment',
description: 'Transaction type shown for credit card payments.',
},
type: {
id: 'wallet.transaction.type',
defaultMessage: '!!!{currency} transaction',
Expand Down Expand Up @@ -58,11 +54,26 @@ const messages = defineMessages({
defaultMessage: '!!!{currency} received',
description: 'Label "{currency} received" for the transaction.',
},
intrawallet: {
id: 'wallet.transaction.type.intrawallet',
defaultMessage: '!!!{currency} intrawallet transaction',
description: 'both sender & receiver are yourself',
},
multiparty: {
id: 'wallet.transaction.type.multiparty',
defaultMessage: '!!!{currency} multiparty transaction',
description: 'only some inputs of tx belong to you',
},
fromAddress: {
id: 'wallet.transaction.address.from',
defaultMessage: '!!!From address',
description: 'From address',
},
fee: {
id: 'wallet.transaction.fee',
defaultMessage: '!!!Fee',
description: 'label for fee for tx',
},
fromAddresses: {
id: 'wallet.transaction.addresses.from',
defaultMessage: '!!!From addresses',
Expand Down Expand Up @@ -142,6 +153,36 @@ export default class Transaction extends Component<Props, State> {
this.setState(prevState => ({ isExpanded: !prevState.isExpanded }));
}

getTransactionHeaderMsg(intl, currency: string, type: TransactionType): string {
if (type === transactionTypes.EXPEND) {
return intl.formatMessage(messages.sent, { currency });
}
if (type === transactionTypes.INCOME) {
return intl.formatMessage(messages.received, { currency });
}
if (type === transactionTypes.SELF) {
return intl.formatMessage(messages.intrawallet, { currency });
}
if (type === transactionTypes.MULTI) {
Logger.error('MULTI type transaction detected.');
return intl.formatMessage(messages.multiparty, { currency });
}
// unused
if (type === transactionTypes.EXCHANGE) {
Logger.error('EXCHANGE type transactions not supported');
return '???';
}
}

getAmountStyle(amt: BigNumber) {
return classNames([
styles.amount,
amt.lt(0)
? styles.amountSent
: styles.amountReceived
]);
}

render() {
const data = this.props.data;
const { isLastInList, state, assuranceLevel, formattedWalletAmount } = this.props;
Expand All @@ -164,11 +205,6 @@ export default class Transaction extends Component<Props, State> {
isExpanded ? styles.expanded : styles.closed
]);

const amountStyles = classNames([
styles.amount,
data.type === transactionTypes.EXPEND ? styles.amountSent : styles.amountReceived
]);

const status = intl.formatMessage(assuranceLevelTranslations[assuranceLevel]);
const currency = intl.formatMessage(environmentSpecificMessages[environment.API].currency);
const symbol = adaSymbol;
Expand All @@ -181,10 +217,7 @@ export default class Transaction extends Component<Props, State> {
<div className={styles.togglerContent}>
<div className={styles.header}>
<div className={styles.title}>
{data.type === transactionTypes.EXPEND ?
intl.formatMessage(messages.sent, { currency }) :
intl.formatMessage(messages.received, { currency })
}
{ this.getTransactionHeaderMsg(intl, currency, data.type) }
</div>
<div className={styles.type}>
{moment(data.date).format('hh:mm:ss A')}
Expand All @@ -196,7 +229,7 @@ export default class Transaction extends Component<Props, State> {
{intl.formatMessage(stateTranslations[state])}
</div>
)}
<div className={amountStyles}>
<div className={this.getAmountStyle(data.amount)}>
{
// hide currency (we are showing symbol instead)
formattedWalletAmount(data.amount, false)
Expand Down Expand Up @@ -224,6 +257,10 @@ export default class Transaction extends Component<Props, State> {
</div>
)}
<div>
<h2>
{intl.formatMessage(messages.fee)}
</h2>
<span>{formattedWalletAmount(data.fee.abs(), false)}</span>
<h2>
{intl.formatMessage(messages.fromAddresses)}
</h2>
Expand Down
13 changes: 8 additions & 5 deletions app/domain/WalletTransaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { assuranceLevels } from '../config/transactionAssuranceConfig';

export type TrasactionAddresses = { from: Array<string>, to: Array<string> };
export type TransactionState = 'pending' | 'failed' | 'ok';
export type TransactionType = 'card' | 'expend' | 'income' | 'exchange';
export type TransactionType = 'card' | 'expend' | 'income' | 'exchange' | 'self' | 'multi';

export const transactionStates: {
PENDING: TransactionState,
Expand All @@ -19,23 +19,26 @@ export const transactionStates: {
};

export const transactionTypes: {
CARD: TransactionType,
EXPEND: TransactionType,
INCOME: TransactionType,
EXCHANGE: TransactionType,
SELF: TransactionType,
MULTI: TransactionType
} = {
CARD: 'card',
EXPEND: 'expend',
INCOME: 'income',
EXCHANGE: 'exchange',
SELF: 'self',
MULTI: 'multi'
};

export default class WalletTransaction {

@observable id: string = '';
@observable type: TransactionType;
@observable title: string = '';
@observable amount: BigNumber;
@observable amount: BigNumber; // fee included
@observable fee: BigNumber;
@observable date: Date;
@observable description: string = '';
@observable numberOfConfirmations: number = 0;
Expand All @@ -47,6 +50,7 @@ export default class WalletTransaction {
type: TransactionType,
title: string,
amount: BigNumber,
fee: BigNumber,
date: Date,
description: string,
numberOfConfirmations: number,
Expand All @@ -65,5 +69,4 @@ export default class WalletTransaction {
}
return assuranceLevels.HIGH;
}

}
4 changes: 3 additions & 1 deletion app/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,15 +228,17 @@
"wallet.transaction.assuranceLevel.medium": "medium",
"wallet.transaction.confirmations": "confirmations",
"wallet.transaction.conversion.rate": "Conversion rate",
"wallet.transaction.fee": "Fee",
"wallet.transaction.received": "{currency} received",
"wallet.transaction.sent": "{currency} sent",
"wallet.transaction.state.failed": "Transaction failed",
"wallet.transaction.state.pending": "Transaction pending",
"wallet.transaction.transactionAmount": "Transaction amount",
"wallet.transaction.transactionId": "Transaction ID",
"wallet.transaction.type": "{currency} transaction",
"wallet.transaction.type.card": "Card payment",
"wallet.transaction.type.exchange": "Exchange",
"wallet.transaction.type.intrawallet": "{currency} intrawallet transaction",
"wallet.transaction.type.multiparty": "{currency} multiparty transaction",
"wallet.trezor.dialog.common.step.link.helpYoroiWithTrezor": "https://youtu.be/Dp0wXwtToX0",
"wallet.trezor.dialog.common.step.link.helpYoroiWithTrezor.text": "Click here to learn more about using Yoroi with Trezor",
"wallet.trezor.dialog.connect.button.label": "Connect",
Expand Down
4 changes: 3 additions & 1 deletion app/i18n/locales/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,15 +228,17 @@
"wallet.transaction.assuranceLevel.medium": "中",
"wallet.transaction.confirmations": "確認",
"wallet.transaction.conversion.rate": "コンバージョン率",
"wallet.transaction.fee": "!!!Fee",
"wallet.transaction.received": "{currency} 受信済",
"wallet.transaction.sent": "{currency} 送信済",
"wallet.transaction.state.failed": "処理に失敗しました",
"wallet.transaction.state.pending": "処理を保留中です",
"wallet.transaction.transactionAmount": "取引額",
"wallet.transaction.transactionId": "取引ID",
"wallet.transaction.type": "{currency} 取引",
"wallet.transaction.type.card": "カード支払い",
"wallet.transaction.type.exchange": "交換",
"wallet.transaction.type.intrawallet": "!!!{currency} intrawallet transaction",
"wallet.transaction.type.multiparty": "!!!{currency} multiparty transaction",
"wallet.trezor.dialog.common.step.link.helpYoroiWithTrezor": "https://youtu.be/Dp0wXwtToX0",
"wallet.trezor.dialog.common.step.link.helpYoroiWithTrezor.text": "Trezorを使ったヨロイの使用方法はこちら",
"wallet.trezor.dialog.connect.button.label": "接続",
Expand Down
4 changes: 3 additions & 1 deletion app/i18n/locales/ko-KR.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,15 +228,17 @@
"wallet.transaction.assuranceLevel.medium": "중간",
"wallet.transaction.confirmations": "확인",
"wallet.transaction.conversion.rate": "환율",
"wallet.transaction.fee": "!!!Fee",
"wallet.transaction.received": "{currency} 받음",
"wallet.transaction.sent": "{currency} 보냄",
"wallet.transaction.state.failed": "거래 실패",
"wallet.transaction.state.pending": "거래 보류",
"wallet.transaction.transactionAmount": "거래 금액",
"wallet.transaction.transactionId": "거래 ID",
"wallet.transaction.type": "{currency} 거래",
"wallet.transaction.type.card": "카드 결제",
"wallet.transaction.type.exchange": "환율",
"wallet.transaction.type.intrawallet": "!!!{currency} intrawallet transaction",
"wallet.transaction.type.multiparty": "!!!{currency} multiparty transaction",
"wallet.trezor.dialog.common.step.link.helpYoroiWithTrezor": "https://youtu.be/Dp0wXwtToX0",
"wallet.trezor.dialog.common.step.link.helpYoroiWithTrezor.text": "TREZOR를 이용한 요로이 사용법은 여기를 클릭하십시오.",
"wallet.trezor.dialog.connect.button.label": "연결",
Expand Down
Loading