From 754acf519d23a82f7d4a133d4a5ffffe89e38233 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Thu, 7 Dec 2023 19:36:47 -0300 Subject: [PATCH] feat: add support for nano contract details --- src/api/nanoApi.js | 39 ++++++++++ src/components/tx/TxData.js | 91 +++++++++++++++++----- src/screens/NanoContractDetail.js | 125 ++++++++++++++++++++++++++++++ 3 files changed, 236 insertions(+), 19 deletions(-) create mode 100644 src/api/nanoApi.js create mode 100644 src/screens/NanoContractDetail.js diff --git a/src/api/nanoApi.js b/src/api/nanoApi.js new file mode 100644 index 00000000..44666761 --- /dev/null +++ b/src/api/nanoApi.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import requestExplorerServiceV1 from './axiosInstance'; + +const nanoApi = { + getState(id, fields, balances, calls) { + const data = { id, fields, balances, calls }; + return requestExplorerServiceV1.get(`node_api/nc_state`, {params: data}).then((res) => { + return res.data + }, (res) => { + throw new Error(res.data.message); + }); + }, + + getHistory(id) { + const data = { id }; + return requestExplorerServiceV1.get(`node_api/nc_history`, {params: data}).then((res) => { + return res.data + }, (res) => { + throw new Error(res.data.message); + }); + }, + + getBlueprintInformation(blueprintId) { + const data = { blueprint_id: blueprintId }; + return requestExplorerServiceV1.get(`node_api/nc_blueprint_information`, {params: data}).then((res) => { + return res.data + }, (res) => { + throw new Error(res.data.message); + }); + }, +}; + +export default nanoApi; \ No newline at end of file diff --git a/src/components/tx/TxData.js b/src/components/tx/TxData.js index 28e34781..c8f15c02 100644 --- a/src/components/tx/TxData.js +++ b/src/components/tx/TxData.js @@ -62,20 +62,38 @@ class TxData extends React.Component { label: 'Funds neighbors', ...this.baseItemGraph } - ] + ], + ncDeserializer: null, }; // Array of token uid that was already found to show the symbol tokensFound = []; - componentDidMount = () => { - this.calculateTokens(); + componentDidMount = async () => { + await this.handleDataFetch(); } - componentDidUpdate = (prevProps) => { + componentDidUpdate = async (prevProps) => { if (prevProps.transaction !== this.props.transaction) { - this.calculateTokens(); + await this.handleDataFetch(); + } + } + + handleDataFetch = async () => { + await this.handleNanoContractFetch(); + this.calculateTokens(); + } + + handleNanoContractFetch = async () => { + if (this.props.transaction.version !== hathorLib.constants.NANO_CONTRACTS_VERSION) { + return; } + + const ncData = this.props.transaction; + const deserializer = new hathorLib.NanoContractTransactionParser(ncData.nc_blueprint_id, ncData.nc_method, ncData.nc_pubkey, ncData.nc_args); + deserializer.parseAddress(); + await deserializer.parseArguments(); + this.setState({ ncDeserializer: deserializer }); } /** @@ -235,7 +253,7 @@ class TxData extends React.Component { render() { const renderBlockOrTransaction = () => { - if (hathorLib.helpers.isBlock(this.props.transaction)) { + if (hathorLib.transactionUtils.isBlock(this.props.transaction)) { return 'block'; } else { return 'transaction'; @@ -255,15 +273,15 @@ class TxData extends React.Component { const renderOutputToken = (output) => { return ( - {this.getOutputToken(hathorLib.wallet.getTokenIndex(output.token_data))} + {this.getOutputToken(hathorLib.tokensUtils.getTokenIndexFromData(output.token_data))} ); } const outputValue = (output) => { - if (hathorLib.wallet.isAuthorityOutput(output)) { - if (hathorLib.wallet.isMintOutput(output)) { + if (hathorLib.transactionUtils.isAuthorityOutput(output)) { + if (hathorLib.transactionUtils.isMint(output)) { return 'Mint authority'; - } else if (hathorLib.wallet.isMeltOutput(output)) { + } else if (hathorLib.transactionUtils.isMelt(output)) { return 'Melt authority'; } else { // Should never come here @@ -277,7 +295,7 @@ class TxData extends React.Component { } // if it's an NFT token we should show integer value - const uid = this.getUIDFromTokenData(hathorLib.wallet.getTokenIndex(output.token_data)); + const uid = this.getUIDFromTokenData(hathorLib.tokensUtils.getTokenIndexFromData(output.token_data)); const tokenData = this.state.tokens.find((token) => token.uid === uid); const isNFT = tokenData && tokenData.meta && tokenData.meta.nft; return helpers.renderValue(output.value, isNFT); @@ -463,7 +481,7 @@ class TxData extends React.Component { const renderGraph = (graphIndex) => { return ( -
+
this.toggleGraph(e, graphIndex)}>{this.state.graphs[graphIndex].showNeighbors ? 'Click to hide' : 'Click to show'} @@ -559,6 +577,38 @@ class TxData extends React.Component { ); } + const renderNCData = () => { + const deserializer = this.state.ncDeserializer; + if (!deserializer) { + // This should never happen + return null; + } + return ( +
+
{this.props.transaction.nc_id}
+
{deserializer.address.base58}
+
{this.props.transaction.nc_method}
+
{renderNCArguments(deserializer.parsedArgs)}
+
+ ); + } + + const renderNCArguments = (args) => { + return args.map((arg) => ( +
+ {renderArgValue(arg)} +
+ )); + } + + const renderArgValue = (arg) => { + if (arg.type === 'bytes') { + return arg.parsed.toString('hex'); + } + + return arg.parsed; + } + const isNFTCreation = () => { if (this.props.transaction.version !== hathorLib.constants.CREATE_TOKEN_TX_VERSION) { return false; @@ -574,22 +624,25 @@ class TxData extends React.Component {
{this.props.showConflicts ? renderConflicts() : ''} -
{this.props.transaction.hash}
+
{this.props.transaction.hash}
-
{hathorLib.helpers.getTxType(this.props.transaction)} {isNFTCreation() && '(NFT)'}
+
{hathorLib.transactionUtils.getTxType(this.props.transaction)} {isNFTCreation() && '(NFT)'}
{dateFormatter.parseTimestamp(this.props.transaction.timestamp)}
{this.props.transaction.nonce}
{helpers.roundFloat(this.props.transaction.weight)}
- {!hathorLib.helpers.isBlock(this.props.transaction) && renderFirstBlockDiv()} + {!hathorLib.transactionUtils.isBlock(this.props.transaction) && renderFirstBlockDiv()}
- {hathorLib.helpers.isBlock(this.props.transaction) && renderHeight()} - {hathorLib.helpers.isBlock(this.props.transaction) && renderScore()} - {!hathorLib.helpers.isBlock(this.props.transaction) && renderAccWeightDiv()} - {!hathorLib.helpers.isBlock(this.props.transaction) && renderConfirmationLevel()} + {hathorLib.transactionUtils.isBlock(this.props.transaction) && renderHeight()} + {hathorLib.transactionUtils.isBlock(this.props.transaction) && renderScore()} + {!hathorLib.transactionUtils.isBlock(this.props.transaction) && renderAccWeightDiv()} + {!hathorLib.transactionUtils.isBlock(this.props.transaction) && renderConfirmationLevel()}
+
+ {this.props.transaction.version === hathorLib.constants.NANO_CONTRACTS_VERSION && renderNCData()} +
diff --git a/src/screens/NanoContractDetail.js b/src/screens/NanoContractDetail.js new file mode 100644 index 00000000..c550abe7 --- /dev/null +++ b/src/screens/NanoContractDetail.js @@ -0,0 +1,125 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import Loading from '../components/Loading'; +import TxRow from '../components/tx/TxRow'; +import hathorLib from '@hathor/wallet-lib'; +import nanoApi from '../api/nanoApi'; +import txApi from '../api/txApi'; + + +/** + * Details of a Nano Contract + * + * @memberof Screens + */ +class NanoContractDetail extends React.Component { + + state = { + loadingDetail: true, + loadingHistory: true, + ncState: null, + history: null, + errorMessage: null, + } + + componentDidMount = () => { + this.loadBlueprintInformation(); + this.loadNCHistory(); + } + + loadBlueprintInformation = async () => { + this.setState({ loadingDetail: true, data: null }); + try { + const transactionData = await txApi.getTransaction(this.props.match.params.nc_id); + if (transactionData.tx.version !== hathorLib.constants.NANO_CONTRACTS_VERSION) { + this.setState({ errorMessage: 'Transaction is not a nano contract.' }); + } + + const blueprintInformation = await nanoApi.getBlueprintInformation(transactionData.tx.nc_blueprint_id); + const ncState = await nanoApi.getState(this.props.match.params.nc_id, Object.keys(blueprintInformation.attributes), [], []); + this.setState({ loadingDetail: false, ncState }); + } catch (e) { + this.setState({ loadingDetail: false, errorMessage: 'Error getting nano contract state.' }); + } + } + + loadNCHistory = () => { + this.setState({ loadingHistory: true, history: null }); + nanoApi.getHistory(this.props.match.params.nc_id).then((data) => { + this.setState({ loadingHistory: false, history: data.history }); + }, (e) => { + // Error in request + this.setState({ loadingHistory: false, errorMessage: 'Error getting nano contract history.' }); + }); + } + + render() { + if (this.state.errorMessage) { + return

{this.state.errorMessage}

; + } + + if (this.state.loadingHistory || this.state.loadingDetail) { + return ; + } + + const loadTable = () => { + return ( +
+ + + + + + + + + + {loadTableBody()} + +
HashTimestampHash
Timestamp
+
+ ); + } + + const loadTableBody = () => { + return this.state.history.map((tx, idx) => { + // For some reason this API returns tx.hash instead of tx.tx_id like the others + tx.tx_id = tx.hash; + return ( + + ); + }); + } + + const renderNCAttributes = () => { + return Object.keys(this.state.ncState.fields).map((field) => ( +

{field}: {this.state.ncState.fields[field].value}

+ )); + } + + // TODO + //const isTokenNFT = isNFT(this.state.data.nc_data.token); + return ( +
+

Nano Contract Detail

+
+

Nano Contract ID: {this.props.match.params.nc_id}

+

Blueprint: {this.state.ncState.blueprint_name}

+

Attributes

+ { renderNCAttributes() } +
+

History

+ {this.state.history && loadTable()} +
+
+ ); + } +} + +export default NanoContractDetail; \ No newline at end of file