Skip to content

Commit 2c437df

Browse files
[DDW-521] Display Transaction Metadata (#2338)
* [DDW-521] Prepare json-bigint integration * [DDW-521] Fix json-bigint dependency and lockfile * [DDW-521] Render transaction metadata * [DDW-521] Fix flow issues * [DDW-521] Add changelog entry * [DDW-521] Finish api integration with json stringify rendering * [DDW-521] improve handling of metadata maps * [DDW-521] add more metadata map examples * [DDW-521] Fix small linting issue * [DDW-521] Add 0x prefix for metadata byte strings * [DDW-521] Fix tx metadata toggling recalculation * [DDW-521] Fix distance of tx metadata section * [DDW-521] Translations and improvements Co-authored-by: Nikola Glumac <niglumac@gmail.com>
1 parent b527992 commit 2c437df

File tree

23 files changed

+495
-70
lines changed

23 files changed

+495
-70
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Changelog
55

66
### Features
77

8+
- Implemented transaction metadata display ([PR 2338](https://github.com/input-output-hk/daedalus/pull/2338))
89
- Displayed fee and deposit info in transaction details and in the delegation wizard ([PR 2339](https://github.com/input-output-hk/daedalus/pull/2339))
910
- Added SMASH server configuration options ([PR 2259](https://github.com/input-output-hk/daedalus/pull/2259))
1011

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@
201201
"history": "4.10.1",
202202
"humanize-duration": "3.23.1",
203203
"inquirer": "7.3.3",
204+
"json-bigint": "1.0.0",
204205
"lodash": "4.17.20",
205206
"lodash-es": "4.17.15",
206207
"mime-types": "2.1.27",

source/renderer/app/api/api.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2313,6 +2313,7 @@ const _createTransactionFromServerData = action(
23132313
outputs,
23142314
withdrawals,
23152315
status,
2316+
metadata,
23162317
} = data;
23172318
const state = _conditionToTxState(status);
23182319
const stateInfo =
@@ -2343,6 +2344,7 @@ const _createTransactionFromServerData = action(
23432344
withdrawals: withdrawals.map(({ stake_address: address }) => address),
23442345
},
23452346
state,
2347+
metadata,
23462348
});
23472349
}
23482350
);

source/renderer/app/api/transactions/types.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import BigNumber from 'bignumber.js';
33
import { WalletTransaction } from '../../domains/WalletTransaction';
44
import { WalletUnits } from '../../domains/Wallet';
55
import type { DelegationAction } from '../../types/stakingTypes';
6+
import type { TransactionMetadata } from '../../types/TransactionMetadata';
67

78
export type TransactionAmount = {
89
quantity: number,
@@ -50,6 +51,7 @@ export type Transaction = {
5051
outputs: Array<TransactionOutputs>,
5152
withdrawals: Array<TransactionWithdrawals>,
5253
status: TransactionState,
54+
metadata?: TransactionMetadata,
5355
};
5456

5557
export type Transactions = Array<Transaction>;

source/renderer/app/api/utils/request.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// @flow
22
import { includes, omit, size } from 'lodash';
3+
import JSONBigInt from 'json-bigint';
34
import querystring from 'querystring';
45
import { getContentLength } from '.';
56

@@ -106,10 +107,10 @@ function typedRequest<Response>(
106107
"data": ${data}
107108
}`;
108109
}
109-
resolve(JSON.parse(body));
110+
resolve(JSONBigInt.parse(body));
110111
} else if (body) {
111112
// Error response with a body
112-
const parsedBody = JSON.parse(body);
113+
const parsedBody = JSONBigInt.parse(body);
113114
if (parsedBody.code && parsedBody.message) {
114115
reject(parsedBody);
115116
} else {

source/renderer/app/components/wallet/transactions/Transaction.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import classNames from 'classnames';
88
import { Link } from 'react-polymorph/lib/components/Link';
99
import { LinkSkin } from 'react-polymorph/lib/skins/simple/LinkSkin';
1010
import CancelTransactionButton from './CancelTransactionButton';
11+
import { TransactionMetadataView } from './metadata/TransactionMetadataView';
1112
import styles from './Transaction.scss';
1213
import TransactionTypeIcon from './TransactionTypeIcon';
1314
import adaSymbol from '../../../assets/images/ada-symbol.inline.svg';
@@ -46,6 +47,22 @@ const messages = defineMessages({
4647
defaultMessage: '!!!Transaction ID',
4748
description: 'Transaction ID.',
4849
},
50+
metadataLabel: {
51+
id: 'wallet.transaction.metadataLabel',
52+
defaultMessage: '!!!Transaction metadata',
53+
description: 'Transaction metadata label',
54+
},
55+
metadataDisclaimer: {
56+
id: 'wallet.transaction.metadataDisclaimer',
57+
defaultMessage:
58+
'!!!Transaction metadata is not moderated and may contain inappropriate content.',
59+
description: 'Transaction metadata disclaimer',
60+
},
61+
metadataConfirmationLabel: {
62+
id: 'wallet.transaction.metadataConfirmationLabel',
63+
defaultMessage: '!!!Show unmoderated content',
64+
description: 'Transaction metadata confirmation toggle',
65+
},
4966
conversionRate: {
5067
id: 'wallet.transaction.conversion.rate',
5168
defaultMessage: '!!!Conversion rate',
@@ -171,9 +188,11 @@ type Props = {
171188
isExpanded: boolean,
172189
isRestoreActive: boolean,
173190
isLastInList: boolean,
191+
isShowingMetadata: boolean,
174192
formattedWalletAmount: Function,
175193
onDetailsToggled: ?Function,
176194
onOpenExternalLink: Function,
195+
onShowMetadata: () => void,
177196
getUrlByType: Function,
178197
currentTimeFormat: string,
179198
walletId: string,
@@ -182,6 +201,7 @@ type Props = {
182201

183202
type State = {
184203
showConfirmationDialog: boolean,
204+
showUnmoderatedMetadata: boolean,
185205
};
186206

187207
export default class Transaction extends Component<Props, State> {
@@ -191,8 +211,20 @@ export default class Transaction extends Component<Props, State> {
191211

192212
state = {
193213
showConfirmationDialog: false,
214+
showUnmoderatedMetadata: false,
194215
};
195216

217+
componentDidUpdate(prevProps: Props, prevState: State) {
218+
// Tell parent components that meta data was toggled
219+
if (
220+
!prevState.showUnmoderatedMetadata &&
221+
this.state.showUnmoderatedMetadata &&
222+
this.props.onShowMetadata
223+
) {
224+
this.props.onShowMetadata();
225+
}
226+
}
227+
196228
toggleDetails() {
197229
const { onDetailsToggled } = this.props;
198230
if (onDetailsToggled) onDetailsToggled();
@@ -310,6 +342,7 @@ export default class Transaction extends Component<Props, State> {
310342
const {
311343
data,
312344
isLastInList,
345+
isShowingMetadata,
313346
state,
314347
formattedWalletAmount,
315348
onOpenExternalLink,
@@ -532,6 +565,35 @@ export default class Transaction extends Component<Props, State> {
532565
/>
533566
</div>
534567
{this.renderCancelPendingTxnContent()}
568+
569+
{data.metadata != null && (
570+
<div className={styles.metadata}>
571+
<h2>{intl.formatMessage(messages.metadataLabel)}</h2>
572+
{data.metadata &&
573+
(this.state.showUnmoderatedMetadata ||
574+
isShowingMetadata) ? (
575+
<TransactionMetadataView data={data.metadata} />
576+
) : (
577+
<>
578+
<p className={styles.metadataDisclaimer}>
579+
{intl.formatMessage(messages.metadataDisclaimer)}
580+
</p>
581+
<Link
582+
isUnderlined={false}
583+
hasIconAfter={false}
584+
underlineOnHover
585+
label={intl.formatMessage(
586+
messages.metadataConfirmationLabel
587+
)}
588+
onClick={(e) => {
589+
e.preventDefault();
590+
this.setState({ showUnmoderatedMetadata: true });
591+
}}
592+
/>
593+
</>
594+
)}
595+
</div>
596+
)}
535597
</div>
536598
</div>
537599
<SVGInline svg={arrow} className={arrowStyles} />

source/renderer/app/components/wallet/transactions/Transaction.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,3 +256,18 @@
256256
}
257257
}
258258
}
259+
260+
.metadataDisclaimer {
261+
font-family: var(--font-light);
262+
font-size: 16px;
263+
line-height: 22px;
264+
}
265+
266+
.metadata {
267+
margin-top: 20px;
268+
269+
pre {
270+
white-space: pre-wrap;
271+
word-break: break-all;
272+
}
273+
}

source/renderer/app/components/wallet/transactions/WalletTransactionsList.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export default class WalletTransactionsList extends Component<Props> {
8383
};
8484

8585
expandedTransactionIds: Map<string, WalletTransaction> = new Map();
86+
transactionsShowingMetadata: Map<string, WalletTransaction> = new Map();
8687
virtualList: ?VirtualTransactionList;
8788
simpleList: ?SimpleTransactionList;
8889
loadingSpinner: ?LoadingSpinner;
@@ -136,6 +137,9 @@ export default class WalletTransactionsList extends Component<Props> {
136137
isTxExpanded = (tx: WalletTransaction) =>
137138
this.expandedTransactionIds.has(tx.id);
138139

140+
isTxShowingMetadata = (tx: WalletTransaction) =>
141+
this.transactionsShowingMetadata.has(tx.id);
142+
139143
toggleTransactionExpandedState = (tx: WalletTransaction) => {
140144
const isExpanded = this.isTxExpanded(tx);
141145
if (isExpanded) {
@@ -150,6 +154,19 @@ export default class WalletTransactionsList extends Component<Props> {
150154
}
151155
};
152156

157+
/**
158+
* Update the height of the transaction when metadata is shown
159+
* @param tx
160+
*/
161+
onShowMetadata = (tx: WalletTransaction) => {
162+
this.transactionsShowingMetadata.set(tx.id, tx);
163+
if (this.virtualList) {
164+
this.virtualList.updateTxRowHeight(tx, true, true);
165+
} else if (this.simpleList) {
166+
this.simpleList.forceUpdate();
167+
}
168+
};
169+
153170
onShowMoreTransactions = (walletId: string) => {
154171
if (this.props.onShowMoreTransactions) {
155172
this.props.onShowMoreTransactions(walletId);
@@ -186,10 +203,12 @@ export default class WalletTransactionsList extends Component<Props> {
186203
deletePendingTransaction={deletePendingTransaction}
187204
formattedWalletAmount={formattedWalletAmount}
188205
isExpanded={this.isTxExpanded(tx)}
206+
isShowingMetadata={this.isTxShowingMetadata(tx)}
189207
isLastInList={isLastInGroup}
190208
isRestoreActive={isRestoreActive}
191209
onDetailsToggled={() => this.toggleTransactionExpandedState(tx)}
192210
onOpenExternalLink={onOpenExternalLink}
211+
onShowMetadata={() => this.onShowMetadata(tx)}
193212
getUrlByType={getUrlByType}
194213
state={tx.state}
195214
walletId={walletId}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// @flow
2+
import React from 'react';
3+
import type {
4+
MetadataBytes,
5+
MetadataInteger,
6+
MetadataList,
7+
MetadataMap,
8+
MetadataString,
9+
MetadataValue,
10+
} from '../../../../types/TransactionMetadata';
11+
import styles from './TransactionMetadataView.scss';
12+
13+
/**
14+
* NOTE: These components are currently not used because we simply
15+
* JSON.stringify the metadata for transactions. This can be
16+
* used as the basis for a more sophisticated implementation
17+
* later on:
18+
*/
19+
20+
function IntegerView({ value }: { value: MetadataInteger }) {
21+
return <p>{value.int}</p>;
22+
}
23+
24+
function StringView({ value }: { value: MetadataString }) {
25+
return <p>{value.string}</p>;
26+
}
27+
28+
function BytesView({ value }: { value: MetadataBytes }) {
29+
return <p>{value.bytes}</p>;
30+
}
31+
32+
function ListView({ value }: { value: MetadataList }) {
33+
return (
34+
<ol className={styles.list}>
35+
{value.list.map((v, index) => (
36+
// eslint-disable-next-line react/no-array-index-key
37+
<li key={index}>
38+
<MetadataValueView value={v} />
39+
</li>
40+
))}
41+
</ol>
42+
);
43+
}
44+
45+
function MapView({ value }: { value: MetadataMap }) {
46+
return (
47+
<ol className={styles.map}>
48+
{value.map.map((v, index) => (
49+
// eslint-disable-next-line react/no-array-index-key
50+
<li key={index}>
51+
<MetadataValueView value={v.k} />: <MetadataValueView value={v.v} />
52+
</li>
53+
))}
54+
</ol>
55+
);
56+
}
57+
58+
function MetadataValueView(props: { value: MetadataValue }) {
59+
const { value } = props;
60+
if (value.int) {
61+
return <IntegerView value={value} />;
62+
}
63+
if (value.string) {
64+
return <StringView value={value} />;
65+
}
66+
if (value.bytes) {
67+
return <BytesView value={value} />;
68+
}
69+
if (value.list) {
70+
return <ListView value={value} />;
71+
}
72+
if (value.map) {
73+
return <MapView value={value} />;
74+
}
75+
return null;
76+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// @flow
2+
import React from 'react';
3+
import JSONBigInt from 'json-bigint';
4+
import type {
5+
MetadataMapValue,
6+
MetadataValue,
7+
TransactionMetadata,
8+
} from '../../../../types/TransactionMetadata';
9+
import styles from './TransactionMetadataView.scss';
10+
11+
function flattenMetadata(data: MetadataValue) {
12+
if (data.int) {
13+
return data.int;
14+
}
15+
if (data.string) {
16+
return data.string;
17+
}
18+
if (data.bytes) {
19+
return `0x${data.bytes}`;
20+
}
21+
if (data.list) {
22+
return data.list.map((v: MetadataValue) => flattenMetadata(v));
23+
}
24+
if (data.map) {
25+
return data.map.map((v: MetadataMapValue) => {
26+
if (v.k.list || v.k.map) {
27+
return {
28+
key: flattenMetadata(v.k),
29+
value: flattenMetadata(v.v),
30+
};
31+
}
32+
if (v.k.int) {
33+
return { [v.k.int]: flattenMetadata(v.v) };
34+
}
35+
if (v.k.string) {
36+
return { [v.k.string]: flattenMetadata(v.v) };
37+
}
38+
if (v.k.bytes) {
39+
return { [v.k.bytes]: flattenMetadata(v.v) };
40+
}
41+
return null;
42+
});
43+
}
44+
return null;
45+
}
46+
47+
export function TransactionMetadataView(props: { data: TransactionMetadata }) {
48+
return (
49+
<div className={styles.root}>
50+
{Object.keys(props.data).map((key: string) => (
51+
<pre key={key}>
52+
{JSONBigInt.stringify(flattenMetadata(props.data[key]), null, 2)}
53+
</pre>
54+
))}
55+
</div>
56+
);
57+
}

0 commit comments

Comments
 (0)