From 002fe1a18d89d4e6b210ea1f3ab0c0a5a52c4e32 Mon Sep 17 00:00:00 2001 From: Maxwell Lasky Date: Fri, 23 Jun 2023 09:41:52 -0600 Subject: [PATCH] feature: contacts refactor/rewrite (#2502) * contacts rewrite * contacts progress * Adds logic and deps for NNS * Send panel, contact selection UX implementation * Update history flow and code * lint and clean up * Delete contact actions foo * flow fix * flow fix * flow fix * lint * Revert change and delete AddContact dir * Remove cruft * lint and style enhancements * Clean up commented out styles * clean up comments * mock bs-neo3 * Refactor ContactFormRefactor.jsx * feature: remove infinite scroll on activity screen (#2503) * replace infinite scroll with pagination * Fix tests remove logging * Remove commented out style * remove cruft --- .flowconfig | 1 - __mocks__/@cityofzion/bs-neo3.js | 1 + .../TransactionHistoryPanel.test.js | 7 +- .../TransactionHistoryPanel.test.js.snap | 2 +- app/actions/appActions.js | 2 - app/actions/contactsActions.js | 117 ----- app/actions/transactionHistoryActions.js | 7 +- .../Transaction/N3ClaimAbstract.jsx | 5 +- .../Transaction/N3NEP11ReceiveAbstract.jsx | 43 +- .../Transaction/N3NEP11SendAbstract.jsx | 5 +- .../Blockchain/Transaction/Transaction.jsx | 139 +++--- .../Blockchain/Transaction/index.js | 11 - .../AddContactPanel/AddContactPanel.jsx | 120 ++++-- .../AddContactPanel/AddContactPanel.scss | 12 + .../Contacts/AddContactPanel/index.js | 38 +- .../Contacts/ContactForm/ContactForm.jsx | 302 ------------- .../ContactFormRefactor.jsx | 316 ++++++++++++++ .../ContactFormRefactor.scss} | 54 ++- .../index.js | 25 +- .../Contacts/ContactsPanel/ContactsPanel.jsx | 398 +++++++++++------- .../Contacts/ContactsPanel/ContactsPanel.scss | 43 +- .../Contacts/ContactsPanel/index.js | 9 - .../EditContactPanel/EditContactPanel.jsx | 104 ----- .../EditContactPanel/EditContactPanel.scss | 51 --- .../Contacts/EditContactPanel/index.js | 64 --- app/components/Inputs/AddressInput/index.js | 6 +- .../StyledReactSelect/StyledReactSelect.jsx | 4 +- .../AddContactModal/AddContactModal.jsx | 52 ++- .../Modals/AddContactModal/index.js | 7 - .../ChooseAddressFromContactModal.jsx | 148 +++++++ .../ChooseAddressFromContactModal/index.js | 1 + .../TransferNftModal/TransferNftModal.jsx | 69 ++- .../Modals/TransferNftModal/index.js | 6 - app/components/Root/Root.jsx | 13 +- app/components/Root/Routes.jsx | 3 +- .../SendRecipientListItem/index.jsx | 266 +++++++----- .../SendPanel/SendRecipientList/index.jsx | 4 +- app/components/Send/SendPanel/index.jsx | 2 + .../TransactionHistoryPanel.jsx | 80 +++- .../TransactionHistoryPanel.scss | 19 + .../TransactionHistoryPanel/index.js | 8 +- app/containers/AddContact/AddContact.jsx | 31 +- app/containers/Contacts/index.js | 11 +- app/containers/EditContact/EditContact.jsx | 34 -- app/containers/EditContact/EditContact.scss | 4 - app/containers/EditContact/index.js | 21 - .../ModalRenderer/ModalRenderer.jsx | 3 + app/containers/Receive/index.js | 4 - app/containers/Send/index.js | 6 - .../TransactionHistory.scss | 3 +- app/context/contacts/ContactsContext.js | 130 ++++++ app/core/constants.js | 1 + config/webpack.config.dev.js | 21 + config/webpack.config.prod.js | 21 + package.json | 3 + yarn.lock | 265 +++++++++++- 56 files changed, 1836 insertions(+), 1286 deletions(-) create mode 100644 __mocks__/@cityofzion/bs-neo3.js delete mode 100644 app/actions/contactsActions.js delete mode 100644 app/components/Contacts/ContactForm/ContactForm.jsx create mode 100644 app/components/Contacts/ContactFormRefactor/ContactFormRefactor.jsx rename app/components/Contacts/{ContactForm/ContactForm.scss => ContactFormRefactor/ContactFormRefactor.scss} (67%) rename app/components/Contacts/{ContactForm => ContactFormRefactor}/index.js (54%) delete mode 100644 app/components/Contacts/EditContactPanel/EditContactPanel.jsx delete mode 100644 app/components/Contacts/EditContactPanel/EditContactPanel.scss delete mode 100644 app/components/Contacts/EditContactPanel/index.js create mode 100644 app/components/Modals/ChooseAddressFromContactModal/ChooseAddressFromContactModal.jsx create mode 100644 app/components/Modals/ChooseAddressFromContactModal/index.js delete mode 100644 app/containers/EditContact/EditContact.jsx delete mode 100644 app/containers/EditContact/EditContact.scss delete mode 100644 app/containers/EditContact/index.js create mode 100644 app/context/contacts/ContactsContext.js diff --git a/.flowconfig b/.flowconfig index 8544c29aa..7efb9ad0f 100644 --- a/.flowconfig +++ b/.flowconfig @@ -23,5 +23,4 @@ module.file_ext=.json module.name_mapper='^assets' ->'/app/assets' sharedmemory.heap_size=3221225472 server.max_workers=1 - esproposal.optional_chaining=enable diff --git a/__mocks__/@cityofzion/bs-neo3.js b/__mocks__/@cityofzion/bs-neo3.js new file mode 100644 index 000000000..4ba52ba2c --- /dev/null +++ b/__mocks__/@cityofzion/bs-neo3.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/__tests__/components/TransactionHistoryPanel.test.js b/__tests__/components/TransactionHistoryPanel.test.js index b1cecd0c7..a3f36a4c7 100644 --- a/__tests__/components/TransactionHistoryPanel.test.js +++ b/__tests__/components/TransactionHistoryPanel.test.js @@ -45,7 +45,7 @@ const initialState = { batch: false, progress: LOADED, loadedCount: 1, - data: [], + data: { transactions: [] }, }, }, } @@ -114,10 +114,11 @@ describe('TransactionHistoryPanel', () => { test('correctly renders with NEO and GAS transaction history', () => { const transactionState = merge({}, initialState, { - spunky: { transactionHistory: { data: transactions } }, + spunky: { + transactionHistory: { data: { entries: transactions, count: 2 } }, + }, }) const { wrapper } = setup(transactionState, false) - const transactionList = wrapper.find('#transactionList') expect(transactionList.children().length).toEqual(2) diff --git a/__tests__/components/__snapshots__/TransactionHistoryPanel.test.js.snap b/__tests__/components/__snapshots__/TransactionHistoryPanel.test.js.snap index ed4cc3936..4de52765b 100644 --- a/__tests__/components/__snapshots__/TransactionHistoryPanel.test.js.snap +++ b/__tests__/components/__snapshots__/TransactionHistoryPanel.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TransactionHistoryPanel renders without crashing 1`] = ` - => - chain === 'neo3' ? getStorage(N3_STORAGE_KEY) : getStorage(STORAGE_KEY) - -const setContacts = async (contacts: Contacts, chain: string): Promise => - setStorage(chain === 'neo3' ? N3_STORAGE_KEY : STORAGE_KEY, contacts) - -const validateContact = (name: string, address: string, chain: string) => { - if (isEmpty(name)) { - throw new Error('Name cannot be empty.') - } - if ( - chain === 'neo3' ? !n3Wallet.isAddress(address) : !wallet.isAddress(address) - ) { - throw new Error(`Invalid address ${address}.`) - } -} - -export const ID = 'contacts' -export const addContactActions = createActions( - ID, - ({ - name, - address, - chain, - }: { - name: string, - address: string, - chain: string, - }) => async (): Promise => { - validateContact(name, address, chain) - - const contacts = await getContacts(chain) - - if (has(contacts, name)) { - throw new Error(`Contact "${name}" already exists.`) - } - - const newContacts = { ...contacts, [name]: address } - await setContacts(newContacts, chain) - - return newContacts - }, -) - -export const updateContactActions = createActions( - ID, - ({ - oldName, - newName, - newAddress, - chain, - }: { - oldName: string, - newName: string, - newAddress: string, - chain: string, - }) => async (): Promise => { - validateContact(newName, newAddress, chain) - - const contacts = await getContacts(chain) - const names = keys(contacts) - const addresses = values(contacts) - const index = indexOf(names, oldName) - - if (index === -1) { - throw new Error(`Contact "${oldName}" does not exist.`) - } - - const newContacts = zipObject( - [...names.slice(0, index), newName, ...names.slice(index + 1)], - [...addresses.slice(0, index), newAddress, ...addresses.slice(index + 1)], - ) - await setContacts(newContacts, chain) - - return newContacts - }, -) - -export const deleteContactActions = createActions( - ID, - ({ name, chain }: { name: string, chain: string }) => async (): Promise< - Contacts, - > => { - const contacts = await getContacts(chain) - - if (!has(contacts, name)) { - throw new Error(`Contact "${name}" does not exist.`) - } - - const newContacts = omit(contacts, name) - await setContacts(newContacts, chain) - - return newContacts - }, -) - -export default createActions(ID, () => async (): Promise => { - const settings = await getSettings() - const { chain } = settings - return getContacts(chain) -}) diff --git a/app/actions/transactionHistoryActions.js b/app/actions/transactionHistoryActions.js index aa9921d19..255a71d74 100644 --- a/app/actions/transactionHistoryActions.js +++ b/app/actions/transactionHistoryActions.js @@ -213,10 +213,12 @@ export default createActions( } let parsedEntries = [] + let count = 0 if (chain === 'neo3') { const network = net === 'MainNet' ? 'mainnet' : 'testnet' const data = await NeoRest.addressTXFull(address, page, network) + count = data.totalCount parsedEntries = await computeN3Activity(data, address, net) } else { const network = net === 'MainNet' ? 'mainnet' : 'testnet' @@ -225,15 +227,16 @@ export default createActions( page, network, ) + count = data.total_entries parsedEntries = await parseAbstractData(data.entries, address, net) } page += 1 if (shouldIncrementPagination) { if (page === 1) entries = [] entries.push(...parsedEntries) - return entries + return { entries, count } } entries = [...parsedEntries] - return entries + return { entries, count } }, ) diff --git a/app/components/Blockchain/Transaction/N3ClaimAbstract.jsx b/app/components/Blockchain/Transaction/N3ClaimAbstract.jsx index 05bbc04bc..09040fa39 100644 --- a/app/components/Blockchain/Transaction/N3ClaimAbstract.jsx +++ b/app/components/Blockchain/Transaction/N3ClaimAbstract.jsx @@ -34,7 +34,10 @@ class N3ClaimAbstract extends React.Component { {symbol} -
{amount}
+
+ {/* eslint-disable-next-line no-restricted-globals */} + {!isNaN(amount) && amount} +
diff --git a/app/components/Blockchain/Transaction/N3NEP11ReceiveAbstract.jsx b/app/components/Blockchain/Transaction/N3NEP11ReceiveAbstract.jsx index 38f69da54..a09d1199a 100644 --- a/app/components/Blockchain/Transaction/N3NEP11ReceiveAbstract.jsx +++ b/app/components/Blockchain/Transaction/N3NEP11ReceiveAbstract.jsx @@ -1,22 +1,38 @@ // @flow import React from 'react' import classNames from 'classnames' -import { injectIntl } from 'react-intl' +import { FormattedMessage, IntlShape, injectIntl } from 'react-intl' +import Button from '../../Button' import styles from './Transaction.scss' import ReceiveIcon from '../../../assets/icons/receive-tx.svg' +import ContactsAdd from '../../../assets/icons/contacts-add.svg' +import CopyToClipboard from '../../CopyToClipboard' type Props = { image: string, isPending: boolean, - + findContact: (address: string) => React$Node | null, + from: string, + intl: IntlShape, + showAddContactModal: (to: string) => void, symbol: string, - tokenName: string, + to: string, txDate: React$Node, } class N3NEP11ReceiveAbstract extends React.Component { render = () => { - const { image, isPending, tokenName, symbol, txDate } = this.props + const { + image, + isPending, + findContact, + from, + intl, + to, + showAddContactModal, + symbol, + txDate, + } = this.props const logo = image && ( { alt={`${symbol}`} /> ) - + const contactTo = to && findContact(to) + const contactToExists = contactTo !== to return (
@@ -47,6 +64,22 @@ class N3NEP11ReceiveAbstract extends React.Component {
+
+

{contactTo}

+ +
+
) diff --git a/app/components/Blockchain/Transaction/N3NEP11SendAbstract.jsx b/app/components/Blockchain/Transaction/N3NEP11SendAbstract.jsx index 699db3a7f..4d363a06d 100644 --- a/app/components/Blockchain/Transaction/N3NEP11SendAbstract.jsx +++ b/app/components/Blockchain/Transaction/N3NEP11SendAbstract.jsx @@ -7,6 +7,7 @@ import styles from './Transaction.scss' import ReceiveIcon from '../../../assets/icons/receive-tx.svg' import ContactsAdd from '../../../assets/icons/contacts-add.svg' import CopyToClipboard from '../../CopyToClipboard' +import SendIcon from '../../../assets/icons/send-tx.svg' type Props = { image: string, @@ -49,7 +50,7 @@ class N3NEP11SendAbstract extends React.Component {
- +
{isPending ? 'Pending' : txDate} @@ -77,7 +78,7 @@ class N3NEP11SendAbstract extends React.Component { - )} -
- ) - } + const { contacts } = useContactsContext() - findContact = (address: string): Node | string => { - const { contacts } = this.props + function findContact(address: string): string | React$Node { if (contacts && !isEmpty(contacts)) { - const label = contacts[address] - return label ? ( - - {label} - - ) : ( - address - ) + // find the contact with the matching address based on the types above and + // return the keyname for that contact + let contactName = '' + Object.keys(contacts).forEach(key => { + const contact = contacts[key] + if (contact.some(c => c.address === address)) { + contactName = key + } + }) + if (contactName) { + return ( + + {contactName} + + ) + } } + return address } - displayModal = (address: string) => { - this.props.showAddContactModal({ address }) + function displayModal(address: string) { + props.showAddContactModal({ address }) } - handleViewTransaction = () => { - const { networkId, blockExplorer, tx, chain } = this.props - let txid - + function handleViewTransaction() { + const { networkId, blockExplorer, tx, chain } = props + let { txid } = tx if (chain === 'neo3') { txid = tx.hash.substring(2) - } else { - ;({ txid } = tx) } openExplorerTx(networkId || '1', blockExplorer, txid, chain) } - renderTxDate = (time: ?number) => { + function renderTxDate(time: ?number) { if (!time) { return null } @@ -122,15 +108,15 @@ export default class Transaction extends React.Component { ) } - renderAbstract = (type: string, isN3?: boolean) => { - const { isPending, address } = this.props - const { time, label, amount, isNetworkFee, to, from, image } = this.props.tx - const contactTo = this.findContact(to) - const contactFrom = from && this.findContact(from) + function renderAbstract(type: string, isN3?: boolean) { + const { isPending, address } = props + const { time, label, amount, isNetworkFee, to, from, image } = props.tx + const contactTo = findContact(to) + const contactFrom = from && findContact(from) const contactToExists = contactTo !== to const contactFromExists = contactFrom !== from const logo = image && {`${label}`} - const txDate = this.renderTxDate(time) + const txDate = renderTxDate(time) const abstractProps = { txDate, @@ -139,27 +125,27 @@ export default class Transaction extends React.Component { amount, contactFrom, contactToExists, - findContact: this.findContact, - showAddContactModal: this.displayModal, + findContact, + showAddContactModal: displayModal, isNetworkFee, contactFromExists, from, address, - ...this.props.tx, + ...props.tx, } if (isPending) { return isN3 ? ( ) : ( ) } @@ -184,22 +170,22 @@ export default class Transaction extends React.Component { * Builds a contract invocation object. * @returns {null|*} */ - renderAbstractN3 = () => { - const { isPending, tx, address } = this.props + function renderAbstractN3() { + const { isPending, tx } = props const { time, type, sender } = tx - const txDate = this.renderTxDate(time || (tx.metadata && tx.metadata.time)) + const txDate = renderTxDate(time || (tx.metadata && tx.metadata.time)) const metadata = { txDate, isPending, sender, - findContact: this.findContact, - showAddContactModal: this.displayModal, + findContact, + showAddContactModal: displayModal, ...tx.metadata, } if (isPending) { - return this.renderAbstract(type, true) + return renderAbstract(type, true) } switch (type) { @@ -226,4 +212,21 @@ export default class Transaction extends React.Component { return null } } + + return ( +
+ {chain === 'neo3' && !renderN2Tx + ? renderAbstractN3() + : renderAbstract(type)} + {!isPending && ( + + )} +
+ ) } diff --git a/app/components/Blockchain/Transaction/index.js b/app/components/Blockchain/Transaction/index.js index de38948bb..c32004de2 100644 --- a/app/components/Blockchain/Transaction/index.js +++ b/app/components/Blockchain/Transaction/index.js @@ -1,18 +1,13 @@ // @flow import { compose } from 'recompose' import { connect } from 'react-redux' -import { withData } from 'spunky' -import { invert } from 'lodash-es' import Transaction from './Transaction' import withNetworkData from '../../../hocs/withNetworkData' -// import withExplorerData from '../../../hocs/withExplorerData' import withAuthData from '../../../hocs/withAuthData' import { showModal } from '../../../modules/modal' import { MODAL_TYPES } from '../../../core/constants' -import contactsActions from '../../../actions/contactsActions' - import withSettingsContext from '../../../hocs/withSettingsContext' const mapDispatchToProps = dispatch => ({ @@ -20,17 +15,11 @@ const mapDispatchToProps = dispatch => ({ dispatch(showModal(MODAL_TYPES.ADD_CONTACT, props)), }) -const mapContactsDataToProps = (contacts: Object) => ({ - contacts: invert(contacts), -}) - export default compose( connect( null, mapDispatchToProps, ), withAuthData(), - withData(contactsActions, mapContactsDataToProps), withNetworkData(), - // withExplorerData(), )(withSettingsContext(Transaction)) diff --git a/app/components/Contacts/AddContactPanel/AddContactPanel.jsx b/app/components/Contacts/AddContactPanel/AddContactPanel.jsx index 11354f0f4..55b2f10f1 100644 --- a/app/components/Contacts/AddContactPanel/AddContactPanel.jsx +++ b/app/components/Contacts/AddContactPanel/AddContactPanel.jsx @@ -2,64 +2,98 @@ import React from 'react' import { noop } from 'lodash-es' import { FormattedMessage } from 'react-intl' +import { Box, Center } from '@chakra-ui/react' import FullHeightPanel from '../../Panel/FullHeightPanel' -import ContactForm from '../ContactForm' -import { ROUTES } from '../../../core/constants' +import ContactForm from '../ContactFormRefactor' +import { MODAL_TYPES, ROUTES } from '../../../core/constants' import AddIcon from '../../../assets/icons/add.svg' import BackButton from '../../BackButton' +import DeleteIcon from '../../../assets/icons/delete.svg' import styles from './AddContactPanel.scss' +import { useContactsContext } from '../../../context/contacts/ContactsContext' type Props = { className: ?string, name: string, - address: string, onSave: Function, - chain: string, + showModal: (modalType: string, modalProps: Object) => any, + showSuccessNotification: ({ message: string }) => any, + history: { + push: Function, + }, } -export default class AddContactPanel extends React.Component { - static defaultProps = { - name: '', - address: '', - setName: noop, - setAddress: noop, - onSave: noop, - } - - render() { - const { className, name, address } = this.props +function AddContactPanel(props: Props) { + const { className, name, onSave, showSuccessNotification, history } = props + const { deleteContact } = useContactsContext() - return ( - } - renderBackButton={() => } - headerText={} - renderInstructions={() => ( -
- {' '} - -
- )} - > -
- } - onSubmit={this.handleSubmit} - /> -
-
- ) + function handleSubmit(name: string, address: string) { + onSave(name, address) } - handleSubmit = (name: string, address: string) => { - const { chain } = this.props - this.props.onSave(name, address, chain) + function showConfirmDeleteModal() { + props.showModal(MODAL_TYPES.CONFIRM, { + title: 'Confirm Delete', + height: '200px', + renderBody: () => ( +
+ Are you sure you want to delete this contact? +
+ ), + onClick: async () => { + await deleteContact(name) + showSuccessNotification({ + message: `Contact ${name} has been deleted.`, + }) + history.push(ROUTES.CONTACTS) + }, + }) } + + return ( + } + renderBackButton={() => } + headerText={} + renderInstructions={() => ( + + {' '} + + { + showConfirmDeleteModal() + }} + > + Remove contact + + + )} + > +
+ } + onSubmit={handleSubmit} + /> +
+
+ ) } + +AddContactPanel.defaultProps = { + name: '', + address: '', + setName: noop, + setAddress: noop, + onSave: noop, +} + +export default AddContactPanel diff --git a/app/components/Contacts/AddContactPanel/AddContactPanel.scss b/app/components/Contacts/AddContactPanel/AddContactPanel.scss index 7253c5c1d..31073624c 100644 --- a/app/components/Contacts/AddContactPanel/AddContactPanel.scss +++ b/app/components/Contacts/AddContactPanel/AddContactPanel.scss @@ -3,3 +3,15 @@ flex-direction: column; height: 100%; } + +.removeNetwork { + svg { + margin-right: 10px; + min-height: 24px; + min-width: 24px; + + path { + fill: #d355e7 !important; + } + } +} diff --git a/app/components/Contacts/AddContactPanel/index.js b/app/components/Contacts/AddContactPanel/index.js index af4657b5d..cb2b9b1c2 100644 --- a/app/components/Contacts/AddContactPanel/index.js +++ b/app/components/Contacts/AddContactPanel/index.js @@ -1,29 +1,31 @@ // @flow import { compose, withProps } from 'recompose' -import { withActions, progressValues } from 'spunky' -import { trim } from 'lodash-es' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { withRouter } from 'react-router-dom' import AddContactPanel from './AddContactPanel' -import { addContactActions } from '../../../actions/contactsActions' -import withProgressChange from '../../../hocs/withProgressChange' -import withFailureNotification from '../../../hocs/withFailureNotification' import withSettingsContext from '../../../hocs/withSettingsContext' +import { showModal } from '../../../modules/modal' +import { + showErrorNotification, + showSuccessNotification, +} from '../../../modules/notifications' -const { LOADED } = progressValues +const actionCreators = { + showModal, + showErrorNotification, + showSuccessNotification, +} -const mapContactActionsToProps = (actions: Object) => ({ - onSave: (name, address, chain) => - actions.call({ name: trim(name), address: trim(address), chain }), -}) +const mapDispatchToProps = dispatch => + bindActionCreators(actionCreators, dispatch) export default compose( - withProps(({ name }) => ({ oldName: name })), - - withProgressChange( - addContactActions, - LOADED, - (state, props) => props.onSave && props.onSave(), + connect( + null, + mapDispatchToProps, ), - withActions(addContactActions, mapContactActionsToProps), - withFailureNotification(addContactActions), + withProps(({ name }) => ({ oldName: name })), + withRouter, )(withSettingsContext(AddContactPanel)) diff --git a/app/components/Contacts/ContactForm/ContactForm.jsx b/app/components/Contacts/ContactForm/ContactForm.jsx deleted file mode 100644 index 606051c09..000000000 --- a/app/components/Contacts/ContactForm/ContactForm.jsx +++ /dev/null @@ -1,302 +0,0 @@ -// @flow -import React from 'react' -import { noop } from 'lodash-es' -import { FormattedMessage, intlShape } from 'react-intl' -import { wallet } from '@cityofzion/neon-js' -import { wallet as n3Wallet } from '@cityofzion/neon-js-next' -import { type ProgressState } from 'spunky' - -import Button from '../../Button' -import TextInput from '../../Inputs/TextInput' -import DialogueBox from '../../DialogueBox' -import AddContactIcon from '../../../assets/icons/contacts-add.svg' -import WarningIcon from '../../../assets/icons/warning.svg' -import GridIcon from '../../../assets/icons/grid.svg' -import styles from './ContactForm.scss' -import QrCodeScanner from '../../QrCodeScanner' -import Close from '../../../assets/icons/close.svg' - -type Props = { - submitLabel: string, - formName: string, - formAddress: string, - mode?: string, - contacts: Object, - setName: Function, - newAddress?: boolean, - setAddress: Function, - onSubmit: Function, - intl: intlShape, - chain: string, - cameraAvailable: boolean, - progress: ProgressState, - showScanner: boolean, -} - -type State = { - nameError: string, - addressError: string, - scannerActive: boolean, -} - -export default class ContactForm extends React.Component { - constructor(props: Props) { - super(props) - - this.state = { - nameError: '', - addressError: '', - scannerActive: false, - } - } - - toggleScanner = () => { - this.setState(prevState => ({ scannerActive: !prevState.scannerActive })) - } - - static defaultProps = { - /* $FlowFixMe */ - submitLabel: , - name: '', - address: '', - setName: noop, - setAddress: noop, - onSubmit: noop, - } - - render() { - const { - submitLabel, - formName, - formAddress, - intl, - cameraAvailable, - progress, - showScanner, - } = this.props - const { nameError, addressError, scannerActive } = this.state - - return ( -
-
- {scannerActive ? ( - -
- { - this.props.setAddress(a) - this.validateAddress(a) - this.toggleScanner() - }} - callbackProgress={progress} - width="316" - height="178" - /> -
-
- -
-
- ) : ( - - - -
- } - text={intl.formatMessage({ - id: 'editContactDisclaimer', - })} - className={styles.conactFormDialogue} - /> -
-
- {showScanner && ( - - )} - -
-
- )} -
-
- ) - } - - componentWillMount() { - const { newAddress, setAddress } = this.props - - if (newAddress) { - setAddress('') - } - } - - disableButton = (name: string, address: string) => { - const { chain } = this.props - if (name.length === 0) { - return true - } - - if (name.length > 100) { - return true - } - - if ( - chain === 'neo3' - ? !n3Wallet.isAddress(address) - : !wallet.isAddress(address) - ) { - return true - } - - return false - } - - validate = (name: string, address: string) => { - const validName = this.validateName(name) - const validAddress = this.validateAddress(address) - - return validName && validAddress - } - - validateName = (name: string) => { - const { contacts, mode, intl } = this.props - let error - - if (name.length === 0) { - error = intl.formatMessage({ id: 'errors.contact.nameNull' }) // eslint-disable-line - } - - if (name.length > 100) { - error = intl.formatMessage({ id: 'errors.contact.nameLength' }) - } - - if (mode !== 'edit') { - const nameExists = Object.keys(contacts).filter( - (contactName: string) => contactName === name, - ) - - if (nameExists.length > 0) { - error = intl.formatMessage({ id: 'errors.contact.nameDupe' }) - } - } - - if (error) { - this.setState({ nameError: error }) - return false - } - return true - } - - validateAddress = (address: string) => { - const { mode, contacts, formAddress, intl, chain } = this.props - let error - - if ( - chain === 'neo3' - ? !n3Wallet.isAddress(address) - : !wallet.isAddress(address) - ) { - error = intl.formatMessage({ id: 'errors.contact.invalidAddress' }) - } - - if (mode !== 'edit') { - const addressExists = Object.keys(contacts) - .map(acc => contacts[acc]) - .filter(adr => adr === formAddress) - - if (addressExists.length > 0) { - error = intl.formatMessage({ id: 'errors.contact.contactExists' }) - } - } - - if (error) { - this.setState({ addressError: error }) - return false - } - return true - } - - clearErrors = (name: string) => { - if (name === 'name') { - this.setState({ nameError: '' }) - } - - if (name === 'address') { - this.setState({ addressError: '' }) - } - } - - handleChangeName = (event: Object) => { - this.clearErrors(event.target.name) - this.props.setName(event.target.value) - this.validate(event.target.value, this.props.formAddress) - } - - handleChangeAddress = (event: Object) => { - this.clearErrors(event.target.name) - this.props.setAddress(event.target.value) - this.validate(this.props.formName, event.target.value) - } - - handleSubmit = (event: Object) => { - event.preventDefault() - const { onSubmit, formName, formAddress } = this.props - - const validInput = this.validate(formName, formAddress) - - if (validInput) { - onSubmit(formName, formAddress) - } - } -} diff --git a/app/components/Contacts/ContactFormRefactor/ContactFormRefactor.jsx b/app/components/Contacts/ContactFormRefactor/ContactFormRefactor.jsx new file mode 100644 index 000000000..367bf2426 --- /dev/null +++ b/app/components/Contacts/ContactFormRefactor/ContactFormRefactor.jsx @@ -0,0 +1,316 @@ +// @flow +import React from 'react' +import { intlShape } from 'react-intl' +import { wallet } from '@cityofzion/neon-js' +import { wallet as n3Wallet } from '@cityofzion/neon-js-next' +import { Box } from '@chakra-ui/react' +import { BSNeo3 } from '@cityofzion/bs-neo3' + +import Button from '../../Button' +import TextInput from '../../Inputs/TextInput' +import AddContactIcon from '../../../assets/icons/contacts-add.svg' +import styles from './ContactFormRefactor.scss' +import AddIcon from '../../../assets/icons/add.svg' +import TrashCanIcon from '../../../assets/icons/delete.svg' +import { useContactsContext } from '../../../context/contacts/ContactsContext' +import { ROUTES } from '../../../core/constants' + +type Props = { + name: string, + submitLabel: string, + intl: intlShape, + showSuccessNotification: ({ message: string }) => any, + formAddress?: string, + history: { + push: Function, + }, + handleSubmit?: () => void, +} + +export default function ContactForm(props: Props) { + const { + intl, + submitLabel, + showSuccessNotification, + history, + formAddress, + } = props + const { contacts, updateContacts, deleteContact } = useContactsContext() + const [addressCount, setAddressCount] = React.useState(1) + const [errorMapping, setErrorMapping] = React.useState({ + addresses: [], + name: '', + }) + const [name, setName] = React.useState('') + const [addresses, setAddresses] = React.useState([formAddress || '']) + const [loading, setLoading] = React.useState(false) + + // this indicates we are in edit mode + React.useEffect( + () => { + if (props.name && contacts[props.name]) { + setName(props.name) + setAddresses( + contacts[props.name] && + contacts[props.name].map(contact => contact.address), + ) + setAddressCount(contacts[props.name]?.length) + } + }, + [props.name], + ) + + function handleChangeName(event) { + const existingNames = Object.keys(contacts) + + if (existingNames.includes(event.target.value)) { + setErrorMapping({ + ...errorMapping, + name: intl.formatMessage({ id: 'errors.contact.nameDupe' }), + }) + } + setName(event.target.value) + } + + function clearErrorForGivenIndex(index) { + const nextErrorMappingForAddresses = [...errorMapping.addresses] + nextErrorMappingForAddresses[index] = '' + setErrorMapping({ + ...errorMapping, + addresses: nextErrorMappingForAddresses, + }) + } + + async function handleNNSDomain(address, index) { + setLoading(true) + const NeoBlockChainService = new BSNeo3() + const results = await NeoBlockChainService.getOwnerOfNNS(address) + if (!n3Wallet.isAddress(results)) { + // update the error mapping that the address is invalid + const nextErrorMappingForAddresses = [...errorMapping.addresses] + nextErrorMappingForAddresses[index] = intl.formatMessage({ + id: 'errors.contact.invalidAddress', + }) + return setErrorMapping({ + ...errorMapping, + addresses: nextErrorMappingForAddresses, + }) + } + clearErrorForGivenIndex(index) + setLoading(false) + return results + } + + // validates whether or not the address is a valid legacy or N3 address + // and whether or not the address already exists in the contacts list + function isValidAddress( + address: string, + index: number, + NNSDomain: string, + ): boolean { + const validAddress = + wallet.isAddress(address) || n3Wallet.isAddress(address) + + const existingAddresses = Object.values(contacts).reduce( + (acc, contact) => [ + ...acc, + // $FlowFixMe + ...contact.map(address => address.parsedAddress || address.address), + ], + [], + ) + + if (existingAddresses.includes(NNSDomain || address)) { + const nextErrorMappingForAddresses = [...errorMapping.addresses] + nextErrorMappingForAddresses[index] = intl.formatMessage({ + id: 'errors.contact.contactExists', + }) + setErrorMapping({ + ...errorMapping, + addresses: nextErrorMappingForAddresses, + }) + return false + } + if (!validAddress) { + const nextErrorMappingForAddresses = [...errorMapping.addresses] + nextErrorMappingForAddresses[index] = intl.formatMessage({ + id: 'errors.contact.invalidAddress', + }) + setErrorMapping({ + ...errorMapping, + addresses: nextErrorMappingForAddresses, + }) + return false + } + return true + } + + async function handleChangeAddress(event, index): Promise { + const nextAddresses = [...addresses] + nextAddresses[index] = event.target.value + setAddresses(nextAddresses) + let addressValue = event.target.value + if (event.target.value.includes('.neo')) { + const results = await handleNNSDomain(event.target.value, index) + if (results) { + addressValue = results + } else { + return undefined + } + } + const valid = isValidAddress( + addressValue, + index, + event.target.value.includes('.neo') ? event.target.value : '', + ) + if (valid) clearErrorForGivenIndex(index) + } + + function handleDeleteAddress(index) { + const nextAddresses = [...addresses] + nextAddresses.splice(index, 1) + setAddresses(nextAddresses) + setAddressCount(addressCount - 1) + clearErrorForGivenIndex(index) + } + + function shouldDisableSubmitButton() { + const hasValidNameAndAtLeastOneAddress = + name.length > 0 && addresses[0] !== '' + + const hasAnEmptyAddress = addresses.some(address => address === '') + + return ( + loading || + errorMapping.name || + hasAnEmptyAddress || + (errorMapping.addresses.some(address => address?.length > 0) || + !hasValidNameAndAtLeastOneAddress) + ) + } + + function parseChainFromAddress(address): string { + const chains = { + NEO_LEGACY: 'neo2', + NEO3: 'neo3', + } + if (address.includes('.neo')) { + return chains.NEO3 + } + return wallet.isAddress(address) ? chains.NEO_LEGACY : chains.NEO3 + } + + async function handleSaveAddress() { + setLoading(true) + const data = addresses.map(address => ({ + address, + chain: parseChainFromAddress(address), + })) + await updateContacts(name, data) + // this indicates an update where they changed the + // contents of the name field for an existing contact + if (props.name && props.name !== name) { + await deleteContact(props.name) + } + showSuccessNotification({ + message: props.name + ? `Successfully updated contact ${name}` + : `Successfully added contact ${name}`, + }) + setLoading(false) + + if (props.handleSubmit) { + props.handleSubmit() + } else { + history.push(ROUTES.CONTACTS) + } + } + + return ( +
+
null}> + + + {new Array(addressCount).fill(0).map((_, i) => ( + + + handleChangeAddress(e, i)} + error={errorMapping.addresses[i]} + /> + + {addresses.length > 1 && ( + handleDeleteAddress(i)} + width={60} + > + + + )} + + ))} + + + + + +
+ +
+
+
+
+ ) +} diff --git a/app/components/Contacts/ContactForm/ContactForm.scss b/app/components/Contacts/ContactFormRefactor/ContactFormRefactor.scss similarity index 67% rename from app/components/Contacts/ContactForm/ContactForm.scss rename to app/components/Contacts/ContactFormRefactor/ContactFormRefactor.scss index 9cab973ff..f566032f2 100644 --- a/app/components/Contacts/ContactForm/ContactForm.scss +++ b/app/components/Contacts/ContactFormRefactor/ContactFormRefactor.scss @@ -1,11 +1,15 @@ @import '../../../styles/variables.scss'; .contactFormContainer { - height: 100%; + width: 100%; display: flex; flex-direction: column; flex: 1; + form { + width: 100%; + } + .contactFormHeader { color: var(--panel-header-text); font-weight: 400; @@ -21,13 +25,15 @@ } .contactForm { - max-width: 500px; + min-width: 350px; + width: 100%; padding: 24px; margin: 0 auto; height: 100%; display: flex; flex-direction: column; flex: 1; + padding-bottom: 0px; .input { margin-bottom: 10px; @@ -76,8 +82,10 @@ } .submitButtonRow { + margin-top: 50px; + margin-bottom: 150px; display: flex; - margin-top: auto; + button:first-child { margin-right: 12px; } @@ -87,13 +95,51 @@ } } +.addButton { + color: var(--contacts-delete-contact-name) !important; + + svg { + margin-right: 15px; + height: 24px; + width: 24px; + + path { + fill: var(--contacts-delete-contact-name) !important; + } + } + + .addIcon { + margin-right: 5px; + height: 24px; + width: 24px; + } +} .scannerContainer { display: flex; justify-content: center; - // margin-bottom: 12px; margin: 24px 0; height: 250px; position: relative; +} +.deleteButton { + width: 100%; + border: none; + background: transparent; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + margin-bottom: 12px; + + svg { + height: 24px; + width: 24px; + min-width: 24px; + + path { + fill: #ee6d66; + } + } } diff --git a/app/components/Contacts/ContactForm/index.js b/app/components/Contacts/ContactFormRefactor/index.js similarity index 54% rename from app/components/Contacts/ContactForm/index.js rename to app/components/Contacts/ContactFormRefactor/index.js index 754b67c33..0c8a1e88f 100644 --- a/app/components/Contacts/ContactForm/index.js +++ b/app/components/Contacts/ContactFormRefactor/index.js @@ -1,20 +1,34 @@ // @flow +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' import { compose, withState } from 'recompose' import { withData } from 'spunky' import { injectIntl } from 'react-intl' +import { withRouter } from 'react-router-dom' import withAuthData from '../../../hocs/withAuthData' - -import ContactForm from './ContactForm' -import contactsActions from '../../../actions/contactsActions' +import ContactForm from './ContactFormRefactor' import withCameraAvailability from '../../../hocs/withCameraAvailability' import withSettingsContext from '../../../hocs/withSettingsContext' +import { + showErrorNotification, + showSuccessNotification, +} from '../../../modules/notifications' + +const actionCreators = { + showErrorNotification, + showSuccessNotification, +} -const mapContactsDataToProps = (contacts: Object) => ({ contacts }) +const mapDispatchToProps = dispatch => + bindActionCreators(actionCreators, dispatch) export default compose( + connect( + null, + mapDispatchToProps, + ), withAuthData(), - withData(contactsActions, mapContactsDataToProps), withState('formName', 'setName', ({ formName }) => formName || ''), withState( 'formAddress', @@ -23,4 +37,5 @@ export default compose( ), injectIntl, withCameraAvailability, + withRouter, )(withSettingsContext(ContactForm)) diff --git a/app/components/Contacts/ContactsPanel/ContactsPanel.jsx b/app/components/Contacts/ContactsPanel/ContactsPanel.jsx index a9434264d..f8d1c793b 100644 --- a/app/components/Contacts/ContactsPanel/ContactsPanel.jsx +++ b/app/components/Contacts/ContactsPanel/ContactsPanel.jsx @@ -1,9 +1,10 @@ // @flow -import React from 'react' +import React, { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { orderBy, groupBy, isEmpty } from 'lodash-es' import classNames from 'classnames' import { FormattedMessage, IntlShape } from 'react-intl' +import { Box, Divider, Text, Image } from '@chakra-ui/react' import StyledReactSelect from '../../Inputs/StyledReactSelect/StyledReactSelect' import HeaderBar from '../../HeaderBar' @@ -13,39 +14,27 @@ import Button from '../../Button' import AddIcon from '../../../assets/icons/add.svg' import InfoIcon from '../../../assets/icons/info.svg' import EditIcon from '../../../assets/icons/edit.svg' -import DeleteIcon from '../../../assets/icons/delete.svg' import SendIcon from '../../../assets/icons/send.svg' -import { ROUTES, MODAL_TYPES } from '../../../core/constants' +import { ROUTES } from '../../../core/constants' import CopyToClipboard from '../../CopyToClipboard' import LogoWithStrikethrough from '../../LogoWithStrikethrough' - +import { + useContactsContext, + type Contacts, + type ContactInfo, +} from '../../../context/contacts/ContactsContext' +import { imageMap } from '../../../assets/nep5/svg' +import OldNeoLogo from '../../../assets/images/neo-logo.png' import styles from './ContactsPanel.scss' -type Contact = { - address: string, - name: string, -} +const NEO_IMAGE = imageMap.NEO type OrderDirection = 'desc' | 'asc' -type Contacts = { - [key: string]: Contact, -} - type Props = { - history: Object, - contacts: Contacts, - deleteContact: (string, string) => void, - showSuccessNotification: ({ message: string }) => void, - showModal: (modalType: string, modalProps: Object) => any, intl: IntlShape, - chain: string, -} - -type State = { - sorting: { - label: string, - value: OrderDirection, + history: { + push: Function, }, } @@ -54,105 +43,140 @@ type SelectOption = { value: OrderDirection, } +type ParsedContact = { + // eslint-disable-next-line react/no-unused-prop-types + addresses: ContactInfo[], + // eslint-disable-next-line react/no-unused-prop-types + name: string, +} + const SORTING_OPTIONS = [ { - label: 'Sorting A-Z', + label: 'A-Z', value: 'asc', }, { - label: 'Sorting Z-A', + label: 'Z-A', value: 'desc', }, ] -const getContactsInGroups = ( - contacts: Contacts, - orderDirection: OrderDirection, -) => { - /* $FlowFixMe */ - const contactsArray: Array = Object.entries(contacts).map( - ([name, address]) => ({ - name, - address, - }), - ) +function ContactsPanel(props: Props) { + const [sorting, setSorting] = useState(SORTING_OPTIONS[0]) + const { contacts } = useContactsContext() + const [selectedContact, setSelectedContact] = useState(null) + const { intl } = props - const groupContactsByFirstLetter = groupBy( - contactsArray, - ({ name }: Contact) => { - const firstLetter = name.substr(0, 1).toUpperCase() - return /[a-zA-Z]/.test(firstLetter) ? firstLetter : '#' + function handleSort(option: SelectOption) { + setSorting(option) + } + + useEffect( + () => { + if (Object.keys(contacts).length > 0) { + setSelectedContact(Object.keys(contacts)[0]) + } }, + [contacts], ) - const groupedContacts = Object.entries(groupContactsByFirstLetter).map( - ([groupName, groupContacts]) => ({ - groupName, - /* $FlowFixMe */ - groupContacts: orderBy(groupContacts, 'name', orderDirection), - }), - ) + function handleEdit(name: string) { + props.history.push(`/contacts/edit/${encodeURIComponent(name)}`) + } - return orderBy(groupedContacts, 'groupName', orderDirection) -} + function findContactAndReturnParsedContact( + name: string, + ): ParsedContact | void { + const contactsArray: ParsedContact[] = Object.entries(contacts).map( + ([name, address]) => ({ + name, + /* $FlowFixMe */ + addresses: address, + }), + ) + return contactsArray.find(contact => contact.name === name) + } -export default class ContactsPanel extends React.Component { - state = { - sorting: SORTING_OPTIONS[0], + function getContactsInGroups( + contacts: Contacts, + orderDirection: OrderDirection, + ) { + /* $FlowFixMe */ + const contactsArray: ParsedContact[] = Object.entries(contacts).map( + ([name, address]) => ({ + name, + addresses: address, + }), + ) + const groupContactsByFirstLetter = groupBy( + contactsArray, + ({ name }: ParsedContact) => { + const firstLetter = name.substr(0, 1).toUpperCase() + return /[a-zA-Z]/.test(firstLetter) ? firstLetter : '#' + }, + ) + const groupedContacts = Object.entries(groupContactsByFirstLetter).map( + ([groupName, groupContacts]) => ({ + groupName, + groupContacts: orderBy(groupContacts, 'name', orderDirection), + }), + ) + return orderBy(groupedContacts, 'groupName', orderDirection) } - renderHeader = () => { - const { sorting } = this.state - const { contacts } = this.props + function ContactAvatar({ name }: { name: string }) { return ( -
- -
+ + {name + .split(' ') + .map(word => word[0]) + .join('')} + ) } - renderContact = (address: string, name: string, i: number) => { - const { intl } = this.props + function renderContact( + address: string, + chain: string, + parsedAddress?: string, + ) { return ( -
-
{name}
+ + {' '}
- {address} + + + + {address} + {parsedAddress && ( + + {parsedAddress} + + )} +
-
- - -
+ +
@@ -160,7 +184,7 @@ export default class ContactsPanel extends React.Component { @@ -168,84 +192,142 @@ export default class ContactsPanel extends React.Component { -
-
+ + ) } - handleSort = (option: SelectOption) => { - this.setState({ sorting: option }) - } - - handleEdit = (name: string) => { - this.props.history.push(`/contacts/edit/${encodeURIComponent(name)}`) - } + return ( + + - render() { - const { contacts } = this.props - const { sorting } = this.state + ( + + - return ( - - } - shouldRenderRefresh={false} - renderRightContent={() => ( - )} - /> - + + )} + contentClassName={styles.contactPanelContent} + > + {isEmpty(contacts) ? (
) : ( -
- {getContactsInGroups(contacts, sorting.value).map( - ({ groupName, groupContacts }) => ( -
-
{groupName}
- {groupContacts.map(({ address, name }, i) => - this.renderContact(address, name, i), - )} -
- ), + + + + + + + {getContactsInGroups(contacts, sorting.value).map( + ({ groupName, groupContacts }, i) => ( + +
{groupName}
+ {groupContacts.map(({ name }, i) => ( + setSelectedContact(name)} + className={classNames({ + [styles.contactRow]: true, + [styles.active]: name === selectedContact, + })} + > + + {' '} + + {name} + + + ))} +
+ ), + )} +
+ +
+ {selectedContact && ( + + + + + {selectedContact} + + + + + + + {/* $FlowFixMe */} + {findContactAndReturnParsedContact( + selectedContact, + ).addresses.map(({ address, chain, parsedAddress }) => + renderContact(address, chain, parsedAddress), + )} + )} -
+
)} -
-
- ) - } - - handleDelete = (name: string) => { - const { showModal, showSuccessNotification, intl, chain } = this.props - - showModal(MODAL_TYPES.CONFIRM, { - title: 'Confirm Delete', - renderBody: () => ( -
- {`${intl.formatMessage({ - id: 'confirmRemoveContact', - })}`} -

{name}

-
- ), - onClick: () => { - this.props.deleteContact(name, chain) - showSuccessNotification({ - message: 'Contact removal was successful.', - }) - }, - }) - } + + + + ) } + +export default ContactsPanel diff --git a/app/components/Contacts/ContactsPanel/ContactsPanel.scss b/app/components/Contacts/ContactsPanel/ContactsPanel.scss index b7e4af600..708b1f7e7 100644 --- a/app/components/Contacts/ContactsPanel/ContactsPanel.scss +++ b/app/components/Contacts/ContactsPanel/ContactsPanel.scss @@ -2,6 +2,7 @@ .contactsPanel { overflow-y: auto; + height: 100%; .name { width: 180px; @@ -23,9 +24,8 @@ .contact { display: flex; align-items: center; - justify-content: space-between; + padding: 12px; flex-wrap: nowrap; - padding: 12px 12px 12px 24px; button { background-color: transparent; @@ -38,12 +38,14 @@ .address { display: flex; align-items: center; - width: 375px; font-size: 14px; - justify-content: space-between; margin-bottom: -5px; span { + word-break: break-word; + padding-left: 4px; + margin-bottom: 4px; text-overflow: ellipsis; + margin-right: auto; overflow: hidden; } } @@ -73,7 +75,7 @@ display: flex; align-items: center; padding: 5px; - color: var(--panel-header-text); + color: var(--contacts-delete-contact-name); text-decoration: none; cursor: pointer; @@ -81,6 +83,10 @@ margin-right: 15px; height: 24px; width: 24px; + + path { + fill: var(--contacts-delete-contact-name); + } } .addIcon { @@ -90,8 +96,8 @@ } } -.oddNumberedRow { - background: var(--contacts-odd-numbered-row); +.editButton { + width: 100px; } .emptyContactsContainer { @@ -103,7 +109,11 @@ } .groupHeader { - padding: 10px 24px; + display: flex; + align-items: center; + padding: 0 24px; + height: 24px; + font-size: 12px; background-color: var(--contacts-group-header-background); color: var(--contacts-group-header-text); font-weight: bold; @@ -125,3 +135,20 @@ text-align: center; } } + +.contactRow { + border-left: 4px solid transparent; + + &, + & svg, + & svg path { + color: var(--sidebar-icon); + fill: currentColor; + } + + &.active, + &:hover { + background: var(--sidebar-active-background); + border-color: var(--sidebar-active-border); + } +} diff --git a/app/components/Contacts/ContactsPanel/index.js b/app/components/Contacts/ContactsPanel/index.js index 3ff532190..ae2296fce 100644 --- a/app/components/Contacts/ContactsPanel/index.js +++ b/app/components/Contacts/ContactsPanel/index.js @@ -3,12 +3,9 @@ import { connect } from 'react-redux' import { bindActionCreators } from 'redux' import { injectIntl } from 'react-intl' import { compose } from 'recompose' -import { withActions } from 'spunky' import { withRouter } from 'react-router-dom' import ContactsPanel from './ContactsPanel' -import { deleteContactActions } from '../../../actions/contactsActions' -import withFailureNotification from '../../../hocs/withFailureNotification' import { showErrorNotification, showSuccessNotification, @@ -25,17 +22,11 @@ const actionCreators = { const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch) -const mapContactActionsToProps = actions => ({ - deleteContact: (name, chain) => actions.call({ name, chain }), -}) - export default compose( connect( null, mapDispatchToProps, ), withRouter, - withActions(deleteContactActions, mapContactActionsToProps), - withFailureNotification(deleteContactActions), injectIntl, )(withSettingsContext(ContactsPanel)) diff --git a/app/components/Contacts/EditContactPanel/EditContactPanel.jsx b/app/components/Contacts/EditContactPanel/EditContactPanel.jsx deleted file mode 100644 index 488135919..000000000 --- a/app/components/Contacts/EditContactPanel/EditContactPanel.jsx +++ /dev/null @@ -1,104 +0,0 @@ -// @flow -import React from 'react' -import { Link } from 'react-router-dom' -import { noop } from 'lodash-es' -import { FormattedMessage, IntlShape } from 'react-intl' - -import FullHeightPanel from '../../Panel/FullHeightPanel' -import ContactForm from '../ContactForm' -import ArrowIcon from '../../../assets/icons/arrow.svg' -import Close from '../../../assets/icons/close.svg' -import AddIcon from '../../../assets/icons/add.svg' -import BackButton from '../../BackButton' -import { ROUTES, MODAL_TYPES } from '../../../core/constants' -import styles from './EditContactPanel.scss' - -type Props = { - className: ?string, - name: string, - address: string, - onSave: Function, - deleteContact: (string, string) => void, - showSuccessNotification: ({ message: string }) => void, - showModal: (modalType: string, modalProps: Object) => any, - intl: IntlShape, - chain: string, -} - -export default class EditContactPanel extends React.Component { - static defaultProps = { - name: '', - address: '', - setName: noop, - setAddress: noop, - onSave: noop, - } - - render() { - const { className, name, address } = this.props - - return ( - } - renderBackButton={() => } - headerText={} - renderInstructions={() => ( -
-
- -
- - - -
- )} - > -
- -
-
- ) - } - - renderHeader = () => ( - - - - - Edit Contact - - ) - - handleSubmit = (name: string, address: string) => { - const { chain } = this.props - this.props.onSave(name, address, chain) - } - - handleDelete = () => { - const { name, showModal, showSuccessNotification, intl, chain } = this.props - - showModal(MODAL_TYPES.CONFIRM, { - title: 'Confirm Delete', - renderBody: () => ( -
- {`${intl.formatMessage({ - id: 'confirmRemoveContact', - })}`} -

{name}

-
- ), - onClick: () => { - this.props.deleteContact(name, chain) - showSuccessNotification({ - message: 'Contact removal was successful.', - }) - }, - }) - } -} diff --git a/app/components/Contacts/EditContactPanel/EditContactPanel.scss b/app/components/Contacts/EditContactPanel/EditContactPanel.scss deleted file mode 100644 index e67b983e8..000000000 --- a/app/components/Contacts/EditContactPanel/EditContactPanel.scss +++ /dev/null @@ -1,51 +0,0 @@ -.editContactPanel { - max-width: 600px; - - .header { - display: flex; - align-items: center; - - .back { - margin-right: 10px; - } - } -} - -.formContainer { - display: flex; - flex-direction: column; - height: 100%; -} - -.editContactInstructions { - display: flex; - align-items: center; - justify-content: space-between; - - span { - font-family: var(--font-gotham-medium); - font-size: 14px; - color: #d355e7; - display: flex; - align-items: center; - cursor: pointer; - - svg { - path { - fill: #d355e7; - } - } - } -} -.confirmDeleteModalPrompt { - text-align: center; - width: 100%; - - h2 { - margin-top: 10px; - - font-size: 18px; - color: var(--contacts-delete-contact-name); - text-align: center; - } -} diff --git a/app/components/Contacts/EditContactPanel/index.js b/app/components/Contacts/EditContactPanel/index.js deleted file mode 100644 index 6dd1871be..000000000 --- a/app/components/Contacts/EditContactPanel/index.js +++ /dev/null @@ -1,64 +0,0 @@ -// @flow -import { connect } from 'react-redux' -import { bindActionCreators } from 'redux' -import { compose, withProps } from 'recompose' -import { withActions, progressValues } from 'spunky' -import { trim } from 'lodash-es' -import { injectIntl } from 'react-intl' - -import EditContactPanel from './EditContactPanel' -import { - updateContactActions, - deleteContactActions, -} from '../../../actions/contactsActions' -import { - showErrorNotification, - showSuccessNotification, -} from '../../../modules/notifications' -import withProgressChange from '../../../hocs/withProgressChange' -import withFailureNotification from '../../../hocs/withFailureNotification' -import { showModal } from '../../../modules/modal' -import withSettingsContext from '../../../hocs/withSettingsContext' - -const { LOADED } = progressValues - -const actionCreators = { - showModal, - showErrorNotification, - showSuccessNotification, -} - -const mapContactActionsToProps = (actions, props) => ({ - onSave: (name, address, chain) => - actions.call({ - oldName: props.oldName, - newName: trim(name), - newAddress: trim(address), - chain, - }), -}) - -const mapDispatchToProps = dispatch => - bindActionCreators(actionCreators, dispatch) - -const mapDeleteContactActionsToProps = actions => ({ - deleteContact: (name, chain) => actions.call({ name, chain }), -}) - -export default compose( - connect( - null, - mapDispatchToProps, - ), - withProps(({ name }) => ({ oldName: name })), - withProgressChange( - updateContactActions, - LOADED, - (state, props) => props.onSave && props.onSave(), - ), - withActions(updateContactActions, mapContactActionsToProps), - withActions(deleteContactActions, mapDeleteContactActionsToProps), - withFailureNotification(deleteContactActions), - withFailureNotification(updateContactActions), - injectIntl, -)(withSettingsContext(EditContactPanel)) diff --git a/app/components/Inputs/AddressInput/index.js b/app/components/Inputs/AddressInput/index.js index e54a084c0..cb0edf9c7 100644 --- a/app/components/Inputs/AddressInput/index.js +++ b/app/components/Inputs/AddressInput/index.js @@ -1,9 +1,5 @@ // @flow -import { withData } from 'spunky' import AddressInput from './AddressInput' -import contactsActions from '../../../actions/contactsActions' -const mapContactsDataToProps = (contacts: Object) => ({ contacts }) - -export default withData(contactsActions, mapContactsDataToProps)(AddressInput) +export default AddressInput diff --git a/app/components/Inputs/StyledReactSelect/StyledReactSelect.jsx b/app/components/Inputs/StyledReactSelect/StyledReactSelect.jsx index 0d5e7c813..a293ee045 100644 --- a/app/components/Inputs/StyledReactSelect/StyledReactSelect.jsx +++ b/app/components/Inputs/StyledReactSelect/StyledReactSelect.jsx @@ -56,7 +56,9 @@ const customStyles = { fontSize: props.selectProps.fontSize, fontWeight: props.selectProps.fontWeight, padding: !props.hideHighlight && '7px 15px !important', - justifyContent: props.selectProps.settingsSelect && 'flex-end', + justifyContent: + props.selectProps.alignValueContainer || + (props.selectProps.settingsSelect && 'flex-end'), ...conditionalStyles, } }, diff --git a/app/components/Modals/AddContactModal/AddContactModal.jsx b/app/components/Modals/AddContactModal/AddContactModal.jsx index c48c1415a..e68e7ee8f 100644 --- a/app/components/Modals/AddContactModal/AddContactModal.jsx +++ b/app/components/Modals/AddContactModal/AddContactModal.jsx @@ -1,40 +1,38 @@ // @flow -import React, { Component } from 'react' +import React from 'react' import { FormattedMessage } from 'react-intl' +import { Box } from '@chakra-ui/react' import BaseModal from '../BaseModal' -import ContactForm from '../../Contacts/ContactForm' +import ContactForm from '../../Contacts/ContactFormRefactor' type Props = { address: string, hideModal: () => null, - triggerSuccessNotification: (text: string) => void, - onSave: (name: string, address: string, chain: string) => any, - chain: string, } -class AddContactModal extends Component { - handleSubmit = (name: string, address: string) => { - const { onSave, hideModal, triggerSuccessNotification, chain } = this.props +function AddContactModal(props: Props) { + const { address, hideModal } = props - onSave(name, address, chain) - triggerSuccessNotification('Contact added.') - hideModal() - } - - render() { - const { address, hideModal } = this.props - - return ( - - } - onSubmit={this.handleSubmit} - /> - - ) - } + return ( + + + + } + handleSubmit={() => props.hideModal()} + /> + + + + ) } + export default AddContactModal diff --git a/app/components/Modals/AddContactModal/index.js b/app/components/Modals/AddContactModal/index.js index 3d966b80c..d17ca53e1 100644 --- a/app/components/Modals/AddContactModal/index.js +++ b/app/components/Modals/AddContactModal/index.js @@ -7,14 +7,8 @@ import { connect } from 'react-redux' import { showSuccessNotification } from '../../../modules/notifications' import AddContactModal from './AddContactModal' -import { addContactActions } from '../../../actions/contactsActions' import withSettingsContext from '../../../hocs/withSettingsContext' -const mapContactActionsToProps = (actions: Object) => ({ - onSave: (name: string, address: string, chain: string) => - actions.call({ name: trim(name), address: trim(address), chain }), -}) - const mapDispatchToProps = (dispatch: Function) => ({ triggerSuccessNotification(text: string) { dispatch(showSuccessNotification({ message: text })) @@ -26,5 +20,4 @@ export default compose( null, mapDispatchToProps, ), - withActions(addContactActions, mapContactActionsToProps), )(withSettingsContext(AddContactModal)) diff --git a/app/components/Modals/ChooseAddressFromContactModal/ChooseAddressFromContactModal.jsx b/app/components/Modals/ChooseAddressFromContactModal/ChooseAddressFromContactModal.jsx new file mode 100644 index 000000000..fc3da1f9d --- /dev/null +++ b/app/components/Modals/ChooseAddressFromContactModal/ChooseAddressFromContactModal.jsx @@ -0,0 +1,148 @@ +// @flow +import React from 'react' +import { noop } from 'lodash-es' +import { FormattedMessage } from 'react-intl' +import { Box, Divider, Text, Image } from '@chakra-ui/react' + +import BaseModal from '../BaseModal' +import Button from '../../Button' +import { useContactsContext } from '../../../context/contacts/ContactsContext' +import CheckMarkIcon from '../../../assets/icons/alternate-check.svg' +import { imageMap } from '../../../assets/nep5/svg' +import OldNeoLogo from '../../../assets/images/neo-logo.png' + +const NEO_IMAGE = imageMap.NEO + +type Props = { + onClick: Function, + onCancel: Function, + hideModal: Function, + + contactName: string, + chain: string, +} + +const ChooseAddressFromContactModal = ({ + hideModal, + onClick, + onCancel, + contactName, + chain, +}: Props) => { + const { contacts } = useContactsContext() + const contact = contacts[contactName] + const [selectedAddress, setSelectedAddress] = React.useState('') + + const filteredContactByChain = React.useMemo( + () => { + if (!contact) return [] + return contact.filter(c => c.chain === chain) + }, + [contact, chain], + ) + + return ( + hideModal() && onCancel && onCancel()} + shouldRenderHeader={false} + style={{ + content: { + height: '400px', + }, + }} + > + + + {' '} + Choose an address for {contactName}: + + + + {filteredContactByChain && + filteredContactByChain.map( + ({ address, parsedAddress, chain }, i) => ( + setSelectedAddress(parsedAddress || address)} + background={ + (selectedAddress === parsedAddress || + selectedAddress === address) && + 'var(--sidebar-active-background)' + } + > + + + + + {address} + {parsedAddress && ( + + {parsedAddress} + + )} + + + {selectedAddress === (parsedAddress || address) && ( + + )} + + + + + + ), + )} + + + + + + + + + + + ) +} + +ChooseAddressFromContactModal.defaultProps = { + width: '500px', + height: '250px', + onCancel: noop, +} + +export default ChooseAddressFromContactModal diff --git a/app/components/Modals/ChooseAddressFromContactModal/index.js b/app/components/Modals/ChooseAddressFromContactModal/index.js new file mode 100644 index 000000000..aa488e6b4 --- /dev/null +++ b/app/components/Modals/ChooseAddressFromContactModal/index.js @@ -0,0 +1 @@ +export { default } from './ChooseAddressFromContactModal' diff --git a/app/components/Modals/TransferNftModal/TransferNftModal.jsx b/app/components/Modals/TransferNftModal/TransferNftModal.jsx index 5412471b7..bbc8116b3 100644 --- a/app/components/Modals/TransferNftModal/TransferNftModal.jsx +++ b/app/components/Modals/TransferNftModal/TransferNftModal.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react' import { FormattedMessage } from 'react-intl' import { wallet as n3Wallet } from '@cityofzion/neon-js-next' +import { BSNeo3 } from '@cityofzion/bs-neo3' import { NFT } from '../../../containers/NftGallery/NftGallery' import Button from '../../Button' @@ -14,6 +15,8 @@ import { getNode, getRPCEndpoint } from '../../../actions/nodeStorageActions' import N3Helper from '../../../context/WalletConnect/helpers' import { convertToArbitraryDecimals } from '../../../core/formatters' import { addPendingTransaction } from '../../../actions/pendingTransactionActions' +import { useContactsContext } from '../../../context/contacts/ContactsContext' +import { MODAL_TYPES } from '../../../core/constants' type Props = { hideModal: () => any, @@ -23,7 +26,6 @@ type Props = { isWatchOnly: boolean, showModal: (type: string, props: any) => any, contract: string, - contacts: Object, intl: Object, net: string, address: string, @@ -36,6 +38,7 @@ type Props = { dispatch: any => any, isHardwareLogin: boolean, signingFunction: () => void, + recipientAddressProp: string, publicKey: string, } @@ -43,7 +46,6 @@ export default function TransferNftModal(props: Props) { const { hideModal, contract, - contacts, intl, net, tokenId, @@ -56,6 +58,7 @@ export default function TransferNftModal(props: Props) { showErrorNotification, showInfoNotification, hideNotification, + recipientAddressProp, publicKey, } = props function handleSubmit() {} @@ -65,12 +68,15 @@ export default function TransferNftModal(props: Props) { networkFee: 0, } - const [recipientAddress, setRecipientAddress] = useState('') + const [recipientAddress, setRecipientAddress] = useState( + recipientAddressProp ?? '', + ) const [recipientAddressError, setRecipientAddressError] = useState('') const [gasFee, setGasFee] = useState(DEFAULT_FEES) const [feesInitialized, setFeesInitialized] = useState(false) const [sendButtonDisabled, setSendButtonDisabled] = useState(false) const [loading, setLoading] = useState(true) + const { contacts } = useContactsContext() function toggleHasEnoughGas(hasGas) { setSendButtonDisabled(!hasGas) @@ -102,12 +108,50 @@ export default function TransferNftModal(props: Props) { function updateRecipient(value) { let normalizedValue = value - const isContactString = Object.keys(contacts).find( - contact => contact === value, - ) - if (isContactString) { - normalizedValue = contacts[value] + const contact = contacts[value] + // if the value is an address and contains .neo it indicates that it is potentially + // an NNS domain so we verify and manipulate the value here + if (value.includes('.neo') && !contact) { + const NeoBlockChainService = new BSNeo3() + return NeoBlockChainService.getOwnerOfNNS(value).then(results => { + // clearErrors(index, type) + // updateRowField(index, type, results) + setRecipientAddressError('') + setRecipientAddress(results) + isValidAddress(results) + }) } + + if (contact) { + const filteredByChain = contact.filter(c => c.chain === 'neo3') + + // if the contact has multiple addresses for the chain we need to render a modal + // which allows them to select the address they want to send to + if (filteredByChain.length > 1) { + return props.showModal(MODAL_TYPES.CHOOSE_ADDRESS_FROM_CONTACT, { + contactName: value, + chain: 'neo3', + onClick: address => { + setTimeout(() => { + props.showModal(MODAL_TYPES.TRANSFER_NFT, { + ...props, + recipientAddressProp: address, + }) + }, 0) + }, + onCancel: () => { + setTimeout(() => { + props.showModal(MODAL_TYPES.TRANSFER_NFT, { + ...props, + recipientAddressProp: '', + }) + }, 0) + }, + }) + } + normalizedValue = filteredByChain[0].address + } + setRecipientAddressError('') setRecipientAddress(normalizedValue) isValidAddress(normalizedValue) @@ -235,6 +279,13 @@ export default function TransferNftModal(props: Props) { testInvoke() }, []) + function createContactList(): Array { + const filteredContacts = Object.keys(contacts).filter(contact => + contacts[contact].some(address => address.chain === 'neo3'), + ) + return filteredContacts + } + return ( setRecipientAddressError('')} error={recipientAddressError} /> diff --git a/app/components/Modals/TransferNftModal/index.js b/app/components/Modals/TransferNftModal/index.js index eefff2147..db85508a2 100644 --- a/app/components/Modals/TransferNftModal/index.js +++ b/app/components/Modals/TransferNftModal/index.js @@ -1,11 +1,9 @@ // @flow import { compose } from 'recompose' -import { withData, withCall } from 'spunky' import { injectIntl } from 'react-intl' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' -import contactsActions from '../../../actions/contactsActions' import TransferNftModal from './TransferNftModal' import withNetworkData from '../../../hocs/withNetworkData' import withAuthData from '../../../hocs/withAuthData' @@ -26,15 +24,11 @@ const actionCreators = { const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch) -const mapContactsDataToProps = (contacts: Object) => ({ contacts }) - export default compose( connect( null, mapDispatchToProps, ), - withCall(contactsActions), - withData(contactsActions, mapContactsDataToProps), injectIntl, withNetworkData(), withAuthData(), diff --git a/app/components/Root/Root.jsx b/app/components/Root/Root.jsx index dddf74377..499e5f14f 100644 --- a/app/components/Root/Root.jsx +++ b/app/components/Root/Root.jsx @@ -14,6 +14,7 @@ import { } from '../../core/constants' import IntlWrapper from './IntlWrapper' import Routes from './Routes' +import { ContactsContextProvider } from '../../context/contacts/ContactsContext' type Props = { store: Object, @@ -31,11 +32,13 @@ const Root = ({ store }: Props) => ( - - - - - + + + + + + + diff --git a/app/components/Root/Routes.jsx b/app/components/Root/Routes.jsx index db3b58c5b..62f105dea 100644 --- a/app/components/Root/Routes.jsx +++ b/app/components/Root/Routes.jsx @@ -11,7 +11,6 @@ import Dashboard from '../../containers/Dashboard' import Receive from '../../containers/Receive' import Contacts from '../../containers/Contacts' import AddContact from '../../containers/AddContact' -import EditContact from '../../containers/EditContact' import Settings from '../../containers/Settings' import TransactionHistory from '../../containers/TransactionHistory' import WalletManager from '../../containers/WalletManager' @@ -94,7 +93,7 @@ export default ({ store }: { store: any }) => ( - + any, removeRow: (index: number) => any, @@ -28,28 +33,56 @@ type Props = { calculateMaxValue: (asset: string, index: number) => string, intl: IntlShape, isMigration: boolean, + showModal: (modalType: string, modalProps: Object) => any, + chain: string, } -class SendRecipientListItem extends Component { - handleFieldChange = (value: string, type: 'asset' | 'amount' | 'address') => { +function SendRecipientListItem(props: Props) { + const { contacts } = useContactsContext() + + function handleFieldChange( + value: string, + type: 'asset' | 'amount' | 'address', + ) { const { index, updateRowField, - contacts, clearErrors, calculateMaxValue, asset, isMigration, - } = this.props - + showModal, + } = props let normalizedValue = value - if (type === 'address') { - const isContactString = Object.keys(contacts).find( - contact => contact === value, - ) - if (isContactString) { - normalizedValue = contacts[value] + const contact = contacts[value] + + // if the value is an address and contains .neo it indicates that it is potentially + // an NNS domain so we verify and manipulate the value here + if (value.includes('.neo') && !contact) { + const NeoBlockChainService = new BSNeo3() + NeoBlockChainService.getOwnerOfNNS(value).then(results => { + clearErrors(index, type) + updateRowField(index, type, results) + }) + } + + if (contact) { + const filteredByChain = contact.filter(c => c.chain === props.chain) + + // if the contact has multiple addresses for the chain we need to render a modal + // which allows them to select the address they want to send to + if (filteredByChain.length > 1) { + return showModal(MODAL_TYPES.CHOOSE_ADDRESS_FROM_CONTACT, { + contactName: value, + chain: props.chain, + onClick: address => { + clearErrors(index, type) + updateRowField(index, type, address) + }, + }) + } + normalizedValue = filteredByChain[0].address } } else if (type === 'amount' && value && isNumber(value)) { const dynamicMax = calculateMaxValue(asset, index) @@ -57,118 +90,135 @@ class SendRecipientListItem extends Component { ? dynamicMax : value } - clearErrors(index, type) updateRowField(index, type, normalizedValue) } - handleMaxClick = () => { - const { index, updateRowField, calculateMaxValue, asset } = this.props + function handleMaxClick() { + const { index, updateRowField, calculateMaxValue, asset } = props const max = calculateMaxValue(asset, index) updateRowField(index, 'amount', max.toString()) } - handleDeleteRow = () => { - const { index, removeRow } = this.props + function handleDeleteRow() { + const { index, removeRow } = props removeRow(index) } - clearErrorsOnFocus = (e: Object) => { + function clearErrorsOnFocus(e: Object) { const { name } = e.target - const { clearErrors, index } = this.props + const { clearErrors, index } = props clearErrors(index, name) } - createAssetList = (): Array => Object.keys(this.props.sendableAssets) - - createContactList = (): Array => Object.keys(this.props.contacts) - - render() { - const { - index, - address, - amount, - asset, - errors, - max, - showConfirmSend, - numberOfRecipients, - intl, - isMigration, - } = this.props - - const selectInput = showConfirmSend ? ( - - ) : ( - this.handleFieldChange(value, 'asset')} - items={this.createAssetList()} - onFocus={this.clearErrorsOnFocus} - /> - ) + function createAssetList(): Array { + return Object.keys(props.sendableAssets) + } - const numberInput = showConfirmSend ? ( - - ) : ( - this.handleFieldChange(value, 'amount')} - handleMaxClick={this.handleMaxClick} - onFocus={this.clearErrorsOnFocus} - error={errors && errors.amount} - options={{ numeralDecimalScale: 8 }} - /> - ) - // TODO: this should be converted to use the StyledReactSelect component - // currently the UI does not indicate if there are no contacts - const addressInput = showConfirmSend ? ( - - ) : ( - this.handleFieldChange(value, 'address')} - items={isMigration ? [] : this.createContactList()} - onFocus={this.clearErrorsOnFocus} - error={errors && errors.address} - disabled={isMigration} - /> + function createContactList(): Array { + // filter out contacts if they do not contain + // addresses for the current chain + const { chain } = props + const filteredContacts = Object.keys(contacts).filter(contact => + contacts[contact].some(address => address.chain === chain), ) + return filteredContacts + } - const trashCanButton = showConfirmSend ? null : ( - - ) + const { + index, + address, + amount, + asset, + errors, + max, + showConfirmSend, + numberOfRecipients, + intl, + isMigration, + } = props + + const selectInput = showConfirmSend ? ( + + ) : ( + handleFieldChange(value, 'asset')} + items={createAssetList()} + onFocus={clearErrorsOnFocus} + /> + ) + + const numberInput = showConfirmSend ? ( + + ) : ( + handleFieldChange(value, 'amount')} + handleMaxClick={handleMaxClick} + onFocus={clearErrorsOnFocus} + error={errors && errors.amount} + options={{ numeralDecimalScale: 8 }} + /> + ) + // TODO: this should be converted to use the StyledReactSelect component + // currently the UI does not indicate if there are no contacts + const addressInput = showConfirmSend ? ( + + ) : ( + handleFieldChange(value, 'address')} + items={isMigration ? [] : createContactList()} + onFocus={clearErrorsOnFocus} + error={errors && errors.address} + disabled={isMigration} + /> + ) + + const trashCanButton = showConfirmSend ? null : ( + + ) + + return ( +
  • + {!isMigration && ( +
    {`${`0${index + 1}`.slice(-2)}`}
    + )} +
    {selectInput}
    +
    {numberInput}
    +
    {addressInput}
    + {!isMigration && ( +
    + {numberOfRecipients > 1 && trashCanButton} +
    + )} +
  • + ) +} - return ( -
  • - {!isMigration && ( -
    {`${`0${index + 1}`.slice( - -2, - )}`}
    - )} -
    {selectInput}
    -
    {numberInput}
    -
    {addressInput}
    - {!isMigration && ( -
    - {numberOfRecipients > 1 && trashCanButton} -
    - )} -
  • - ) - } +const actionCreators = { + showModal, } -export default injectIntl(SendRecipientListItem) +const mapDispatchToProps = dispatch => + bindActionCreators(actionCreators, dispatch) + +export default compose( + connect( + null, + mapDispatchToProps, + ), +)(injectIntl(SendRecipientListItem)) diff --git a/app/components/Send/SendPanel/SendRecipientList/index.jsx b/app/components/Send/SendPanel/SendRecipientList/index.jsx index 8f6f5bdbb..f98d39c6d 100644 --- a/app/components/Send/SendPanel/SendRecipientList/index.jsx +++ b/app/components/Send/SendPanel/SendRecipientList/index.jsx @@ -19,6 +19,7 @@ type Props = { updateRowField: (index: number, field: string, value: any) => any, calculateMaxValue: (asset: string, index: number) => string, isMigration?: boolean, + chain: string, } const SendRecipientList = ({ @@ -31,6 +32,7 @@ const SendRecipientList = ({ showConfirmSend, calculateMaxValue, isMigration, + chain, }: Props) => { const renderRows = () => sendRowDetails.map((row, index) => ( @@ -43,10 +45,10 @@ const SendRecipientList = ({ removeRow={removeRow} updateRowField={updateRowField} sendableAssets={sendableAssets} - contacts={contacts} clearErrors={clearErrors} calculateMaxValue={calculateMaxValue} isMigration={isMigration} + chain={chain} /> )) diff --git a/app/components/Send/SendPanel/index.jsx b/app/components/Send/SendPanel/index.jsx index 4c1206b4d..10cad6c6f 100644 --- a/app/components/Send/SendPanel/index.jsx +++ b/app/components/Send/SendPanel/index.jsx @@ -126,6 +126,7 @@ const SendPanel = ({ calculateMaxValue={calculateMaxValue} isWatchOnly={isWatchOnly} isMigration={isMigration} + chain={chain} /> {chain === 'neo2' && !isMigration && ( @@ -223,6 +224,7 @@ const SendPanel = ({ clearErrors={clearErrors} showConfirmSend={showConfirmSend} calculateMaxValue={calculateMaxValue} + chain={chain} /> , + count: number, handleFetchAdditionalTxData: () => void, handleGetPendingTransactionInfo: () => void, handleRefreshTxData: () => void, pendingTransactions: Array, address: string, showSuccessNotification: ({ message: string }) => void, + loading: boolean, } const REFRESH_INTERVAL_MS = 30000 @@ -38,20 +43,61 @@ export default class TransactionHistory extends React.Component { } render() { - const { className, transactions } = this.props + const { + className, + transactions, + handleFetchAdditionalTxData, + loading, + count, + } = this.props const filteredPendingTransactions = this.pruneConfirmedTransactionsFromPending() this.pruneReturnedTransactionsFromStorage() return ( - - - + <> + + {loading && !transactions.length ? ( +
    + +
    + ) : ( + + )} +
    + + {!!transactions.length && ( +
    + + Displaying {transactions.length} of {count} transactions + + + +
    + )} + ) } @@ -97,13 +143,9 @@ export default class TransactionHistory extends React.Component { handleGetPendingTransactionInfo() } } +} - handleScroll = (e: SyntheticInputEvent) => { - const { handleFetchAdditionalTxData } = this.props - const bottom = - e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight - if (bottom) { - handleFetchAdditionalTxData() - } - } +TransactionHistory.defaultProps = { + transactions: [], + pendingTransactions: [], } diff --git a/app/components/TransactionHistory/TransactionHistoryPanel/TransactionHistoryPanel.scss b/app/components/TransactionHistory/TransactionHistoryPanel/TransactionHistoryPanel.scss index 2b288b929..19f1bcc1a 100644 --- a/app/components/TransactionHistory/TransactionHistoryPanel/TransactionHistoryPanel.scss +++ b/app/components/TransactionHistory/TransactionHistoryPanel/TransactionHistoryPanel.scss @@ -26,3 +26,22 @@ .transactions { margin: -24px; } + +.loadMoreButton { + height: 40px; +} + +.transactionHistoryPanel { + margin-top: -12px; +} + +.buttonLoadingIndicator { + position: relative; + width: 15px; + height: 15px; + opacity: 0.4; + margin: auto; + left: -10px; + top: auto; + transform: none; +} diff --git a/app/components/TransactionHistory/TransactionHistoryPanel/index.js b/app/components/TransactionHistory/TransactionHistoryPanel/index.js index a3418e75e..d022aec3a 100644 --- a/app/components/TransactionHistory/TransactionHistoryPanel/index.js +++ b/app/components/TransactionHistory/TransactionHistoryPanel/index.js @@ -17,8 +17,9 @@ import { showInfoNotification, } from '../../../modules/notifications' -const mapTransactionsDataToProps = transactions => ({ - transactions, +const mapTransactionsDataToProps = data => ({ + transactions: data?.entries ?? [], + count: data?.count ?? 0, }) const mapAccountActionsToProps = (actions, { net, address }) => ({ @@ -64,9 +65,6 @@ export default compose( ), withAuthData(), withNetworkData(), - withProgressPanel(transactionHistoryActions, { - title: '', - }), withActions(transactionHistoryActions, mapAccountActionsToProps), withActions(getPendingTransactionInfo, mapPendingTransactionActionsToProps), withCall(getPendingTransactionInfo), diff --git a/app/containers/AddContact/AddContact.jsx b/app/containers/AddContact/AddContact.jsx index bf61c4cf4..62df2f9ac 100644 --- a/app/containers/AddContact/AddContact.jsx +++ b/app/containers/AddContact/AddContact.jsx @@ -2,27 +2,26 @@ import React from 'react' import AddContactPanel from '../../components/Contacts/AddContactPanel' -import { ROUTES } from '../../core/constants' import styles from './AddContact.scss' type Props = { - history: Object, name: string, - address: string, + match: { + params: { + name: string, + }, + }, } -export default class AddContact extends React.Component { - render() { - return ( -
    - -
    - ) - } +function AddContact(props: Props) { + const { match } = props + const { name } = match.params - handleSave = () => this.props.history.push(ROUTES.CONTACTS) + return ( +
    + +
    + ) } + +export default AddContact diff --git a/app/containers/Contacts/index.js b/app/containers/Contacts/index.js index 914458ba1..87e683ac8 100644 --- a/app/containers/Contacts/index.js +++ b/app/containers/Contacts/index.js @@ -1,13 +1,4 @@ // @flow -import { withCall, withData } from 'spunky' -import { compose } from 'redux' - import Contacts from './Contacts' -import contactsActions from '../../actions/contactsActions' - -const mapContactsDataToProps = (contacts: Object) => ({ contacts }) -export default compose( - withCall(contactsActions), - withData(contactsActions, mapContactsDataToProps), -)(Contacts) +export default Contacts diff --git a/app/containers/EditContact/EditContact.jsx b/app/containers/EditContact/EditContact.jsx deleted file mode 100644 index 08f505050..000000000 --- a/app/containers/EditContact/EditContact.jsx +++ /dev/null @@ -1,34 +0,0 @@ -// @flow -import React from 'react' - -import EditContactPanel from '../../components/Contacts/EditContactPanel' -import { ROUTES } from '../../core/constants' -import styles from './EditContact.scss' - -type Props = { - history: Object, - name: string, - address: string, -} - -export default class EditContact extends React.Component { - componentWillMount = () => { - if (!this.props.address) { - this.props.history.push(ROUTES.CONTACTS) - } - } - - render() { - return ( -
    - -
    - ) - } - - handleSave = () => this.props.history.push(ROUTES.CONTACTS) -} diff --git a/app/containers/EditContact/EditContact.scss b/app/containers/EditContact/EditContact.scss deleted file mode 100644 index 22a4522db..000000000 --- a/app/containers/EditContact/EditContact.scss +++ /dev/null @@ -1,4 +0,0 @@ -.editContact { - display: flex; - justify-content: center; -} diff --git a/app/containers/EditContact/index.js b/app/containers/EditContact/index.js deleted file mode 100644 index cdbde55fb..000000000 --- a/app/containers/EditContact/index.js +++ /dev/null @@ -1,21 +0,0 @@ -// @flow -import { compose, withProps } from 'recompose' -import { withData } from 'spunky' -import { withRouter } from 'react-router-dom' - -import EditContact from './EditContact' -import contactsActions from '../../actions/contactsActions' - -const mapNameToProps = props => ({ - name: decodeURIComponent(props.match.params.name), -}) - -const mapContactsDataToProps = (contacts: Object, ownProps: Object) => ({ - address: contacts[ownProps.name], -}) - -export default compose( - withRouter, - withProps(mapNameToProps), - withData(contactsActions, mapContactsDataToProps), -)(EditContact) diff --git a/app/containers/ModalRenderer/ModalRenderer.jsx b/app/containers/ModalRenderer/ModalRenderer.jsx index a69bc4dda..09bf49a98 100644 --- a/app/containers/ModalRenderer/ModalRenderer.jsx +++ b/app/containers/ModalRenderer/ModalRenderer.jsx @@ -18,6 +18,7 @@ import LedgerMigrationConfirm from '../../components/Modals/LedgerMigrationConfi import TransferNftModal from '../../components/Modals/TransferNftModal' import NetworkSwitchModal from '../../components/Modals/NetworkSwitchModal' import CustomNetworkModal from '../../components/Modals/CustomNetworkModal' +import ChooseAddressFromContactModal from '../../components/Modals/ChooseAddressFromContactModal' const { CONFIRM, @@ -35,6 +36,7 @@ const { TRANSFER_NFT, NETWORK_SWITCH, CUSTOM_NETWORK, + CHOOSE_ADDRESS_FROM_CONTACT, } = MODAL_TYPES const MODAL_COMPONENTS = { @@ -53,6 +55,7 @@ const MODAL_COMPONENTS = { [TRANSFER_NFT]: TransferNftModal, [NETWORK_SWITCH]: NetworkSwitchModal, [CUSTOM_NETWORK]: CustomNetworkModal, + [CHOOSE_ADDRESS_FROM_CONTACT]: ChooseAddressFromContactModal, } type Props = { diff --git a/app/containers/Receive/index.js b/app/containers/Receive/index.js index 30b481e17..52c2161dd 100644 --- a/app/containers/Receive/index.js +++ b/app/containers/Receive/index.js @@ -16,7 +16,6 @@ import withAuthData from '../../hocs/withAuthData' import withBalancesData from '../../hocs/withBalancesData' import withCurrencyData from '../../hocs/withCurrencyData' import withFilteredTokensData from '../../hocs/withFilteredTokensData' -import contactsActions from '../../actions/contactsActions' import accountActions from '../../actions/accountActions' import accountsActions from '../../actions/accountsActions' import withLoadingProp from '../../hocs/withLoadingProp' @@ -68,8 +67,6 @@ const mapPricesDataToProps = (prices: Object) => ({ prices, }) -const mapContactsDataToProps = (contacts: Object) => ({ contacts }) - const mapBalanceDataToProps = (balances: Object) => ({ NEO: balances ? balances.NEO : 0, GAS: balances ? balances.GAS : 0, @@ -97,7 +94,6 @@ export default compose( ), withBalancesData(mapBalanceDataToProps), withCurrencyData('currencyCode'), - withData(contactsActions, mapContactsDataToProps), withPricesData(mapPricesDataToProps), withNetworkData(), withAuthData(), diff --git a/app/containers/Send/index.js b/app/containers/Send/index.js index 7d5e6132b..c2dd08afb 100644 --- a/app/containers/Send/index.js +++ b/app/containers/Send/index.js @@ -1,7 +1,6 @@ // @flow import { compose } from 'recompose' import { values, omit } from 'lodash-es' -import { withData, withCall } from 'spunky' import { connect, type MapStateToProps } from 'react-redux' import { bindActionCreators } from 'redux' import { injectIntl } from 'react-intl' @@ -17,7 +16,6 @@ import withAuthData from '../../hocs/withAuthData' import withBalancesData from '../../hocs/withBalancesData' import withCurrencyData from '../../hocs/withCurrencyData' import withFilteredTokensData from '../../hocs/withFilteredTokensData' -import contactsActions from '../../actions/contactsActions' import balancesActions from '../../actions/balancesActions' import withSuccessNotification from '../../hocs/withSuccessNotification' import withFailureNotification from '../../hocs/withFailureNotification' @@ -73,8 +71,6 @@ const mapPricesDataToProps = (prices: Object) => ({ prices, }) -const mapContactsDataToProps = (contacts: Object) => ({ contacts }) - const mapBalanceDataToProps = (balances: Object) => ({ NEO: balances ? balances.NEO : 0, GAS: balances ? balances.GAS : 0, @@ -90,8 +86,6 @@ export default compose( withTokensData(), withBalancesData(mapBalanceDataToProps), withCurrencyData('currencyCode'), - withCall(contactsActions), - withData(contactsActions, mapContactsDataToProps), withPricesData(mapPricesDataToProps), withNetworkData(), withAuthData(), diff --git a/app/containers/TransactionHistory/TransactionHistory.scss b/app/containers/TransactionHistory/TransactionHistory.scss index 92cd51e5c..deec5c2fb 100644 --- a/app/containers/TransactionHistory/TransactionHistory.scss +++ b/app/containers/TransactionHistory/TransactionHistory.scss @@ -1,8 +1,7 @@ .transactionHistory { display: flex; flex-direction: column; - height: 100%; - max-height: calc(100vh - 120px) !important; + height: calc(100vh - 195px) !important; .transactionHistoryPanel { flex: 1 1 auto; diff --git a/app/context/contacts/ContactsContext.js b/app/context/contacts/ContactsContext.js new file mode 100644 index 000000000..c7303aabb --- /dev/null +++ b/app/context/contacts/ContactsContext.js @@ -0,0 +1,130 @@ +// @flow +import React, { useContext, useEffect } from 'react' +import { BSNeo3 } from '@cityofzion/bs-neo3' + +import { getStorage, setStorage } from '../../core/storage' + +export type ContactInfo = { + address: string, + chain: string, + // this key is dynamic and based on the current + // response of the NNS contract at runtime + parsedAddress?: string, +} + +export type Contacts = { + [name: string]: ContactInfo[], +} + +export type DeprecatedContact = { + [name: string]: string, +} + +type ContactsContextType = { + contacts: Contacts, + updateContacts: (contactName: string, data: ContactInfo[]) => Promise, + deleteContact: (contactName: string) => Promise, +} + +const STORAGE_KEY = 'multi-chain-address-book' + +const DEPRECATED_STORAGE_KEY = 'addressBook' +const DEPRECATED_N3_STORAGE_KEY = 'n3AddressBook' + +export const ContactsContext = React.createContext({}) +export const useContactsContext = () => useContext(ContactsContext) + +export const ContactsContextProvider = ({ + children, +}: { + children: React$Node, +}) => { + const [contacts, setContacts] = React.useState({}) + + const getContacts = async (): Promise => { + const contacts = await getStorage(STORAGE_KEY) + const deprecatedLegacyContacts = await getStorage(DEPRECATED_STORAGE_KEY) + const deprecatedN3Contacts = await getStorage(DEPRECATED_N3_STORAGE_KEY) + // transform the deprecated contacts into the new format + const newContacts = {} + // eslint-disable-next-line guard-for-in + for (const contactName in deprecatedLegacyContacts) { + const contactAddress = deprecatedLegacyContacts[contactName] + if (typeof contactAddress === 'string') { + newContacts[contactName] = [{ address: contactAddress, chain: 'neo2' }] + } + } + // eslint-disable-next-line guard-for-in + for (const contactName in deprecatedN3Contacts) { + const contactAddress = deprecatedN3Contacts[contactName] + if (typeof contactAddress === 'string') { + newContacts[contactName] = [{ address: contactAddress, chain: 'neo3' }] + } + } + return { ...contacts, ...newContacts } + } + + const saveContacts = async (contacts: Contacts): Promise => + setStorage(STORAGE_KEY, contacts) + + async function fetchPotentialNameServiceAddresses(contacts: Contacts) { + const NeoBlockChainService = new BSNeo3() + const newContacts = {} + // eslint-disable-next-line guard-for-in + for (const contactName in contacts) { + const contactInfo = contacts[contactName] + const newContactInfo = [] + for (const contact of contactInfo) { + const { address, chain } = contact + let parsedAddress + if (address.includes('.neo')) { + const results = await NeoBlockChainService.getOwnerOfNNS(address) + parsedAddress = results + } + newContactInfo.push({ address, chain, parsedAddress }) + } + newContacts[contactName] = newContactInfo + } + setContacts(newContacts) + } + + const updateContacts = async (contactName: string, data: ContactInfo[]) => { + // scrub the data of all parsedAddress fields + const scrubbedData = data.map(contact => { + const { parsedAddress, ...rest } = contact + return rest + }) + const contacts = await getContacts() + const newContacts = { ...contacts, [contactName]: scrubbedData } + await saveContacts(newContacts) + await fetchPotentialNameServiceAddresses(newContacts) + } + + const deleteContact = async (contactName: string) => { + const contacts = await getContacts() + const newContacts = { ...contacts } + delete newContacts[contactName] + await saveContacts(newContacts) + fetchPotentialNameServiceAddresses(newContacts) + } + + useEffect(() => { + const init = async () => { + const contacts = await getContacts() + await fetchPotentialNameServiceAddresses(contacts) + } + init() + }, []) + + const contextValue = { + updateContacts, + deleteContact, + contacts, + } + + return ( + + {children} + + ) +} diff --git a/app/core/constants.js b/app/core/constants.js index 0acdb988d..e9c32531e 100644 --- a/app/core/constants.js +++ b/app/core/constants.js @@ -195,6 +195,7 @@ export const MODAL_TYPES = { TRANSFER_NFT: 'TRANSFER_NFT', NETWORK_SWITCH: 'NETWORK_SWITCH', CUSTOM_NETWORK: 'CUSTOM_NETWORK', + CHOOSE_ADDRESS_FROM_CONTACT: 'CHOOSE_ADDRESS_FROM_CONTACT', } export const TX_TYPES = { diff --git a/config/webpack.config.dev.js b/config/webpack.config.dev.js index f46c3ec67..9bcd552a3 100644 --- a/config/webpack.config.dev.js +++ b/config/webpack.config.dev.js @@ -76,6 +76,27 @@ module.exports = { presets: [['@babel/preset-env', { targets: 'defaults' }]], }, }, + { + test: /(@cityofzion\/blockchain-service|@cityofzion\/bs-neo3\/node_modules).*\.(ts|js)x?$/, + loader: 'babel-loader', + options: { + presets: [['@babel/preset-env', { targets: 'defaults' }]], + }, + }, + { + test: /(@cityofzion\/neon-invoker).*\.(ts|js)x?$/, + loader: 'babel-loader', + options: { + presets: [['@babel/preset-env', { targets: 'defaults' }]], + }, + }, + { + test: /(@cityofzion\/neon-parser).*\.(ts|js)x?$/, + loader: 'babel-loader', + options: { + presets: [['@babel/preset-env', { targets: 'defaults' }]], + }, + }, { test: /\.jsx?$/, use: { diff --git a/config/webpack.config.prod.js b/config/webpack.config.prod.js index 003fa1c2c..6637808d4 100644 --- a/config/webpack.config.prod.js +++ b/config/webpack.config.prod.js @@ -77,6 +77,27 @@ module.exports = { presets: [['@babel/preset-env', { targets: 'defaults' }]], }, }, + { + test: /(@cityofzion\/blockchain-service|@cityofzion\/bs-neo3\/node_modules).*\.(ts|js)x?$/, + loader: 'babel-loader', + options: { + presets: [['@babel/preset-env', { targets: 'defaults' }]], + }, + }, + { + test: /(@cityofzion\/neon-invoker).*\.(ts|js)x?$/, + loader: 'babel-loader', + options: { + presets: [['@babel/preset-env', { targets: 'defaults' }]], + }, + }, + { + test: /(@cityofzion\/neon-parser).*\.(ts|js)x?$/, + loader: 'babel-loader', + options: { + presets: [['@babel/preset-env', { targets: 'defaults' }]], + }, + }, { test: /\.jsx?$/, use: { diff --git a/package.json b/package.json index d795774d6..8f4496ac4 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,9 @@ "@babel/preset-react": "^7.18.6", "@cfaester/enzyme-adapter-react-18": "^0.6.0", "@chakra-ui/react": "^2.5.1", + "@cityofzion/blockchain-service": "0.3.0", + "@cityofzion/bs-neo-legacy": "0.3.0", + "@cityofzion/bs-neo3": "0.3.0", "@cityofzion/dora-ts": "^0.0.9", "@cityofzion/neon-core": "^5.0.0-next.15", "@cityofzion/neon-js": "3.11.9", diff --git a/yarn.lock b/yarn.lock index db5389585..fb541b804 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2046,6 +2046,45 @@ resolved "https://registry.yarnpkg.com/@chakra-ui/visually-hidden/-/visually-hidden-2.0.15.tgz#60df64e0ab97d95fee4e6c61ccabd15fd5ace398" integrity sha512-WWULIiucYRBIewHKFA7BssQ2ABLHLVd9lrUo3N3SZgR0u4ZRDDVEUNOy+r+9ruDze8+36dGbN9wsN1IdELtdOw== +"@cityofzion/blockchain-service@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@cityofzion/blockchain-service/-/blockchain-service-0.3.0.tgz#350eb34ad21fef1d3d0a76621091b45a767baef2" + integrity sha512-Qe/lVI8tCAAVZZNeBglZEFrqS9khjihc5jkiiEobi6CY4l1z6uFj/xSGQ7Nnz2QlTNQ8DjUqN05iDjKuy+4LLA== + dependencies: + axios "1.3.2" + +"@cityofzion/bs-neo-legacy@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@cityofzion/bs-neo-legacy/-/bs-neo-legacy-0.3.0.tgz#589282944f8799a8027af8ed5f5baecdb83315fa" + integrity sha512-sK3K4SRGgZJ7MlSM+5gwZsgwOCKF9wxUHa2nAqXN5qve31QJotg9cwf2Ze0foEiqjAMtAqSB61+GD/xxPh0upQ== + dependencies: + "@cityofzion/blockchain-service" "0.3.0" + "@cityofzion/dora-ts" "0.0.11" + "@cityofzion/neon-js" "^4.8.3" + "@moonlight-io/asteroid-sdk-js" "git+https://github.com/Moonlight-io/asteroid-sdk-js" + axios "1.3.2" + +"@cityofzion/bs-neo3@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@cityofzion/bs-neo3/-/bs-neo3-0.3.0.tgz#79329dc26f166a3a8db49a1650119a57b7998e51" + integrity sha512-atm6AtK8TbQ/YeZWCyUsrZ/3n8X2XY+egUQy83Rx7VUktz+EU+2znTPLyYLLLSLVmkoii3+5xZ3o/IzWlYldyw== + dependencies: + "@cityofzion/blockchain-service" "0.3.0" + "@cityofzion/dora-ts" "0.0.11" + "@cityofzion/neo3-parser" "1.6.0" + "@cityofzion/neon-invoker" "1.4.0" + "@cityofzion/neon-js" "^5.4.0" + "@cityofzion/neon-parser" "1.6.2" + "@moonlight-io/asteroid-sdk-js" "git+https://github.com/Moonlight-io/asteroid-sdk-js" + axios "1.3.2" + +"@cityofzion/dora-ts@0.0.11": + version "0.0.11" + resolved "https://registry.yarnpkg.com/@cityofzion/dora-ts/-/dora-ts-0.0.11.tgz#7656bfdeea185fc9ad90113407ab1bd26350240c" + integrity sha512-LJWGU29BHPTHkt0i/OwhNdMfUGM/rgAa4rqlDwBy95spxSoUHGSoAtOD5k4aSVKmLFwq77ghNna0Lpc7lEuHBA== + dependencies: + axios "^0.21.1" + "@cityofzion/dora-ts@^0.0.9": version "0.0.9" resolved "https://registry.yarnpkg.com/@cityofzion/dora-ts/-/dora-ts-0.0.9.tgz#4147ced0af40f3fee66971efacc681860092ebc1" @@ -2053,6 +2092,16 @@ dependencies: axios "^0.21.1" +"@cityofzion/neo3-invoker@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@cityofzion/neo3-invoker/-/neo3-invoker-1.4.0.tgz#00fff375a47d73d406e9d5ba47868bbd4cbe894f" + integrity sha512-BOqJA2e6Kq3QPL/kaMYEjpPCeh9fdKTzRjEi9oIxi0MY5B7nj4rhAVl4l2A06jGNboi+VhToo/gJ/ZVzW/GaeQ== + +"@cityofzion/neo3-parser@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@cityofzion/neo3-parser/-/neo3-parser-1.6.0.tgz#154d60baa6e6c541321f0d1fa796b38f6bed6aa3" + integrity sha512-K+j3qaZEuq1cxit+fFziwAAc0hMQTdACbtllU1HUKJZ89dw7z/FY3Bmg12cyZ8tXuiktCloXvUT0Mg/IzoYqdg== + "@cityofzion/neon-api@^4.9.0": version "4.9.0" resolved "https://registry.yarnpkg.com/@cityofzion/neon-api/-/neon-api-4.9.0.tgz#ab11aef2c132baced5a764ac42573577938eaf9c" @@ -2068,6 +2117,11 @@ resolved "https://registry.yarnpkg.com/@cityofzion/neon-api/-/neon-api-5.2.2.tgz#21554ddb7ece6f0a73df08ad0aaf59bae8668d95" integrity sha512-zNaGI+0C3r/w0olV6cpbdl1OarvX/Qen8bnEO3Xlqybc8uswu0AoL/mhfjXPFd75UkWmQTC7JCziKQULTfLblg== +"@cityofzion/neon-api@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@cityofzion/neon-api/-/neon-api-5.4.0.tgz#58870b5e76a8c2cab75cac3546aa157820fdc58d" + integrity sha512-FHjdq2RnZLK37Hpd71yhVMkmbhl/iyPrmmAiEKb4mf5/rLtbNaKEEEJsxRkSNasio9+14cd7+BecIZ/35L1Sbg== + "@cityofzion/neon-core@^4.9.0": version "4.9.0" resolved "https://registry.yarnpkg.com/@cityofzion/neon-core/-/neon-core-4.9.0.tgz#aed0c67997534a7ca1a4c4fbef43858551d0cbbb" @@ -2124,7 +2178,32 @@ loglevel-plugin-prefix "0.8.4" scrypt-js "3.0.1" -"@cityofzion/neon-js-legacy-latest@npm:@cityofzion/neon-js@4.9.0": +"@cityofzion/neon-core@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@cityofzion/neon-core/-/neon-core-5.4.0.tgz#2a659594219a192f59027619130e875c5aaa0556" + integrity sha512-m5B0X71KXDY1WJPMu9oM7lyKp0b5qkLaLyYC+68LgV7/f0wlnA3BQ/40rYutb2b5p+NKlEElm3IRPbdA3INcrA== + dependencies: + bn.js "5.2.1" + bs58 "5.0.0" + buffer "6.0.3" + cross-fetch "^3.1.5" + crypto-js "4.1.1" + elliptic "6.5.4" + ethereum-cryptography "2.0.0" + lodash "4.17.21" + loglevel "1.8.1" + loglevel-plugin-prefix "0.8.4" + +"@cityofzion/neon-invoker@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@cityofzion/neon-invoker/-/neon-invoker-1.4.0.tgz#a3177ad66c4b84bb57228418f5c451bc901c1549" + integrity sha512-lopkzz3/lCFylzK3n5RoUR5H2CbIXLQK+GnRvzrCFk/fLF24uGVB2GrKwdnVJgvNXRUXjziRABtB5WC6iLgmlA== + dependencies: + "@cityofzion/neo3-invoker" "1.4.0" + "@cityofzion/neon-core" "^5.3.0" + "@cityofzion/neon-js" "^5.3.0" + +"@cityofzion/neon-js-legacy-latest@npm:@cityofzion/neon-js@4.9.0", "@cityofzion/neon-js@^4.7.1", "@cityofzion/neon-js@^4.8.3": version "4.9.0" resolved "https://registry.yarnpkg.com/@cityofzion/neon-js/-/neon-js-4.9.0.tgz#39f46720cff76f807128d5882832a3d722d9fd25" integrity sha512-YYeMbQGZJkC8Wq2UQt98OUys8f8tPCaXMplbV8GwiJEdG+PJJOFGg3NkvrDGUfcuasff0dcn8LWjXTPKPp+Gyw== @@ -2161,6 +2240,14 @@ semver "5.6.0" wif "2.0.6" +"@cityofzion/neon-js@^5.3.0", "@cityofzion/neon-js@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@cityofzion/neon-js/-/neon-js-5.4.0.tgz#44d8cade7628dfa6c6a49241eef84016f07369a8" + integrity sha512-PeK5F4xSQyiqkrq/0TvBUl08wdim4ZPVD+XrUUNCIDuyJGeFkaleKlVsSqR5ZY2kofLNpdHsgXXMjiu/HrEdzQ== + dependencies: + "@cityofzion/neon-api" "^5.4.0" + "@cityofzion/neon-core" "^5.4.0" + "@cityofzion/neon-ledger-next@npm:@cityofzion/neon-ledger@5.0.0-next.14": version "5.0.0-next.14" resolved "https://registry.yarnpkg.com/@cityofzion/neon-ledger/-/neon-ledger-5.0.0-next.14.tgz#5bff70dd0a3f12f544fd70b390daca9ec0cab127" @@ -2176,6 +2263,14 @@ dependencies: "@cityofzion/neon-core" "^4.9.0" +"@cityofzion/neon-parser@1.6.2": + version "1.6.2" + resolved "https://registry.yarnpkg.com/@cityofzion/neon-parser/-/neon-parser-1.6.2.tgz#c121a41524217de8c38ddc0372e78dc985d0e16f" + integrity sha512-1wG6qfRR3PNYm3eFqf0/sRSjLBJLVYDnXFnaWvgc31PzTrcCifUUR9ZJr2YWINmV9qnGuGCKlL2Cz+vkFfOA1g== + dependencies: + "@cityofzion/neo3-parser" "1.6.0" + "@cityofzion/neon-js" "^5.3.0" + "@concordance/react@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@concordance/react/-/react-1.0.0.tgz#fcf3cad020e5121bfd1c61d05bc3516aac25f734" @@ -2610,6 +2705,22 @@ lodash "^4.17.15" tmp-promise "^3.0.2" +"@moonlight-io/asteroid-sdk-js@git+https://github.com/Moonlight-io/asteroid-sdk-js": + version "0.25.0" + resolved "git+https://github.com/Moonlight-io/asteroid-sdk-js#7f067dc2f6be0bf77cabc10771c204035770be52" + dependencies: + "@cityofzion/neon-js" "^4.7.1" + "@types/lodash" "^4.14.157" + axios "^0.19.2" + bip39 "^3.0.2" + bs58 "^4.0.1" + build-url "^2.0.0" + crypto "^1.0.1" + elliptic "^6.5.3" + json-rpc-error "^2.0.0" + lodash "^4.17.19" + node-log-it "^2.0.0" + "@motionone/animation@^10.15.1": version "10.15.1" resolved "https://registry.yarnpkg.com/@motionone/animation/-/animation-10.15.1.tgz#4a85596c31cbc5100ae8eb8b34c459fb0ccf6807" @@ -2668,6 +2779,18 @@ resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b" integrity sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ== +"@noble/curves@1.0.0", "@noble/curves@~1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.0.0.tgz#e40be8c7daf088aaf291887cbc73f43464a92932" + integrity sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw== + dependencies: + "@noble/hashes" "1.3.0" + +"@noble/hashes@1.3.0", "@noble/hashes@^1.2.0", "@noble/hashes@~1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" + integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg== + "@npmcli/fs@^2.1.0": version "2.1.1" resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-2.1.1.tgz#c0c480b03450d8b9fc086816a50cb682668a48bf" @@ -2714,6 +2837,28 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== +"@scure/base@~1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" + integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== + +"@scure/bip32@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.0.tgz#6c8d980ef3f290987736acd0ee2e0f0d50068d87" + integrity sha512-bcKpo1oj54hGholplGLpqPHRbIsnbixFtc06nwuNM5/dwSXOq/AAYoIBRsBmnZJSdfeNW5rnff7NTAz3ZCqR9Q== + dependencies: + "@noble/curves" "~1.0.0" + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + +"@scure/bip39@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.0.tgz#a207e2ef96de354de7d0002292ba1503538fc77b" + integrity sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg== + dependencies: + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -3009,6 +3154,18 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== +"@types/lodash@^4.14.116", "@types/lodash@^4.14.157": + version "4.14.194" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76" + integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g== + +"@types/loglevel@^1.5.3": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@types/loglevel/-/loglevel-1.6.3.tgz#b9852b8fdfd773e18728caf3299e3aba6e5bcae6" + integrity sha512-v2YWQQgqtNXAzybOT9qV3CIJqSeoaMUwmBfIMTQdvhsWUybYic/zNGccKH494naWKJ7zUm+VTgFepJfTrbCCJQ== + dependencies: + loglevel "*" + "@types/minimatch@*": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" @@ -4210,6 +4367,22 @@ axios@0.21.1: dependencies: follow-redirects "^1.10.0" +axios@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.2.tgz#7ac517f0fa3ec46e0e636223fd973713a09c72b3" + integrity sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +axios@^0.19.2: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + axios@^0.21.1: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" @@ -4846,6 +5019,13 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +bip39@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.1.0.tgz#c55a418deaf48826a6ceb34ac55b3ee1577e18a3" + integrity sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A== + dependencies: + "@noble/hashes" "^1.2.0" + bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -4884,16 +5064,16 @@ bn.js@5.2.0: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== +bn.js@5.2.1, bn.js@^5.0.0, bn.js@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.8, bn.js@^4.11.9, bn.js@^4.4.0: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== -bn.js@^5.0.0, bn.js@^5.1.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" - integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== - body-parser@1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" @@ -5093,7 +5273,7 @@ browserslist@^4.20.2, browserslist@^4.21.3: node-releases "^2.0.6" update-browserslist-db "^1.0.5" -bs58@4.0.1, bs58@^4.0.0: +bs58@4.0.1, bs58@^4.0.0, bs58@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== @@ -5224,6 +5404,11 @@ buffers@~0.1.1: resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ== +build-url@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/build-url/-/build-url-2.0.0.tgz#7bdd4045e51caa96c1586990e4ca514937598fc2" + integrity sha512-LYvvOlDc9jT07wFXTQTKoQLYaXIJriVl/DgatTsSzY963+ip1O7M6G/jWBrlKKJ1L7HGD3oK+WykmOvbcSYXlQ== + builder-util-runtime@9.0.3: version "9.0.3" resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.0.3.tgz#6c62c493ba2b73c2af92432db4013b5a327f02b2" @@ -6438,6 +6623,11 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +crypto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" + integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== + css-box-model@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" @@ -6680,7 +6870,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: dependencies: ms "2.0.0" -debug@3.1.0: +debug@3.1.0, debug@=3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== @@ -8064,6 +8254,16 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +ethereum-cryptography@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.0.0.tgz#e052b49fa81affae29402e977b8d3a31f88612b6" + integrity sha512-g25m4EtfQGjstWgVE1aIz7XYYjf3kH5kG17ULWVB5dH6uLahsoltOhACzSxyDV+fhn4gbR4xRrOXGe6r2uh4Bg== + dependencies: + "@noble/curves" "1.0.0" + "@noble/hashes" "1.3.0" + "@scure/bip32" "1.3.0" + "@scure/bip39" "1.2.0" + event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" @@ -8667,11 +8867,23 @@ focus-lock@^0.11.6: dependencies: tslib "^2.0.3" +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + follow-redirects@^1.0.0, follow-redirects@^1.10.0, follow-redirects@^1.14.0, follow-redirects@^1.3.0: version "1.15.1" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -11104,6 +11316,13 @@ json-parse-even-better-errors@^2.3.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-rpc-error@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/json-rpc-error/-/json-rpc-error-2.0.0.tgz#a7af9c202838b5e905c7250e547f1aff77258a02" + integrity sha512-EwUeWP+KgAZ/xqFpaP6YDAXMtCJi+o/QQpCQFIYyxr01AdADi2y413eM8hSqJcoQym9WMePAJWoaODEJufC4Ug== + dependencies: + inherits "^2.0.1" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -11631,6 +11850,11 @@ loglevel-plugin-prefix@0.8.4: resolved "https://registry.yarnpkg.com/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz#2fe0e05f1a820317d98d8c123e634c1bd84ff644" integrity sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g== +loglevel@*, loglevel@1.8.1, loglevel@^1.6.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4" + integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg== + loglevel@1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" @@ -12183,7 +12407,7 @@ modify-filename@^1.1.0: resolved "https://registry.yarnpkg.com/modify-filename/-/modify-filename-1.1.0.tgz#9a2dec83806fbb2d975f22beec859ca26b393aa1" integrity sha512-EickqnKq3kVVaZisYuCxhtKbZjInCuwgwZWyAmRIp1NTMhri7r3380/uqwrUHfaDiPzLVTuoNy4whX66bxPVog== -moment@2.29.4: +moment@2.29.4, moment@^2.22.2: version "2.29.4" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== @@ -12458,6 +12682,17 @@ node-int64@^0.4.0: util "^0.11.0" vm-browserify "^1.0.1" +node-log-it@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/node-log-it/-/node-log-it-2.0.0.tgz#a2ca54c483a70e976b478a712f02ffc4d898fa87" + integrity sha512-+hWjAW6ZW+P70cSmq7lwka884pQvitN+nKqgEwNPyZgqNybr563Z/yM/6KXONlkGZ+Pjwin0v8/FBe12Rbthfg== + dependencies: + "@types/lodash" "^4.14.116" + "@types/loglevel" "^1.5.3" + lodash "^4.17.10" + loglevel "^1.6.1" + moment "^2.22.2" + node-notifier@^5.2.1: version "5.4.5" resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.5.tgz#0cbc1a2b0f658493b4025775a13ad938e96091ef" @@ -13674,6 +13909,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -16255,12 +16495,7 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" -tiny-invariant@^1.0.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" - integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg== - -tiny-invariant@^1.0.6: +tiny-invariant@^1.0.2, tiny-invariant@^1.0.6: version "1.3.1" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==