diff --git a/apps/admin_api/test/admin_api/v1/controllers/blockchain_wallet_controller_test.exs b/apps/admin_api/test/admin_api/v1/controllers/blockchain_wallet_controller_test.exs index 38956a2b9..d0e71849e 100644 --- a/apps/admin_api/test/admin_api/v1/controllers/blockchain_wallet_controller_test.exs +++ b/apps/admin_api/test/admin_api/v1/controllers/blockchain_wallet_controller_test.exs @@ -99,6 +99,41 @@ defmodule AdminAPI.V1.BlockchainWalletControllerTest do end) end + test_with_auths "accepts string `amount` attribute" do + identifier = BlockchainHelper.rootchain_identifier() + hot_wallet = BlockchainWallet.get_primary_hot_wallet(identifier) + + token = + insert(:external_blockchain_token, + blockchain_address: "0x0000000000000000000000000000000000000000" + ) + + adapter = BlockchainHelper.adapter() + {:ok, _adapter_pid} = adapter.server().start_link([]) + + attrs = %{ + token_id: token.id, + amount: Integer.to_string(100), + address: hot_wallet.address, + idempotency_token: UUID.generate() + } + + response = request("/blockchain_wallet.deposit_to_childchain", attrs) + assert response["success"] + assert response["data"]["from"]["amount"] == 100 + + transaction = Transaction.get(response["data"]["id"], preload: :blockchain_transaction) + {:ok, pid} = BlockchainTransactionTracker.lookup(transaction.blockchain_transaction_uuid) + + {:ok, %{pid: blockchain_listener_pid}} = + adapter.lookup_listener(transaction.blockchain_transaction.hash) + + on_exit(fn -> + :ok = GenServer.stop(pid) + :ok = GenServer.stop(blockchain_listener_pid) + end) + end + test_with_auths "fails to deposit with a missing address" do token = insert(:token, blockchain_address: "0x0000000000000000000000000000000000000000") diff --git a/apps/ewallet/lib/ewallet/gates/transactions/childchain_gate.ex b/apps/ewallet/lib/ewallet/gates/transactions/childchain_gate.ex index 779caa764..28ffca002 100644 --- a/apps/ewallet/lib/ewallet/gates/transactions/childchain_gate.ex +++ b/apps/ewallet/lib/ewallet/gates/transactions/childchain_gate.ex @@ -22,7 +22,8 @@ defmodule EWallet.TransactionGate.Childchain do @eth BlockchainHelper.adapter().helper().default_token().address - def deposit(actor, %{"amount" => amount} = attrs) when is_integer(amount) do + def deposit(actor, %{"amount" => amount} = attrs) + when is_integer(amount) or is_binary(amount) do case build_transaction_attrs(attrs) do {:ok, attrs} -> validation_tuple = address_validation_tuple(attrs) diff --git a/apps/ewallet/test/ewallet/gates/transactions/childchain_transaction_gate_test.exs b/apps/ewallet/test/ewallet/gates/transactions/childchain_transaction_gate_test.exs index 782d45397..a3c054a61 100644 --- a/apps/ewallet/test/ewallet/gates/transactions/childchain_transaction_gate_test.exs +++ b/apps/ewallet/test/ewallet/gates/transactions/childchain_transaction_gate_test.exs @@ -70,28 +70,47 @@ defmodule EWallet.TransactionGate.ChildchainTest do end) end - test "returns an error if the amount is not an integer" do + test "accepts a string amount", meta do admin = insert(:admin, global_role: "super_admin") primary_blockchain_token = - insert(:token, blockchain_address: "0x0000000000000000000000000000000000000000") + insert(:token, + blockchain_status: "confirmed", + blockchain_address: "0x0000000000000000000000000000000000000000" + ) rootchain_identifier = BlockchainHelper.rootchain_identifier() - hot_wallet = BlockchainWallet.get_primary_hot_wallet(rootchain_identifier) attrs = %{ "idempotency_token" => UUID.generate(), "address" => hot_wallet.address, "token_id" => primary_blockchain_token.id, - "amount" => "1", + "amount" => Integer.to_string(1), "originator" => %System{} } - {res, code, error} = TransactionGate.Childchain.deposit(admin, attrs) - assert res == :error - assert code == :invalid_parameter - assert error == "Invalid parameter provided. `amount` is required." + {:ok, transaction} = TransactionGate.Childchain.deposit(admin, attrs) + + {:ok, vault_address} = BlockchainHelper.call(:get_childchain_eth_vault_address) + + assert transaction.status == TransactionState.blockchain_submitted() + assert transaction.type == Transaction.deposit() + assert transaction.blockchain_transaction.rootchain_identifier == rootchain_identifier + assert transaction.blockchain_transaction.childchain_identifier == nil + assert transaction.from_blockchain_address == hot_wallet.address + assert transaction.to_blockchain_address == vault_address + + {:ok, pid} = BlockchainTransactionTracker.lookup(transaction.blockchain_transaction_uuid) + + {:ok, %{pid: blockchain_listener_pid}} = + meta[:adapter].lookup_listener(transaction.blockchain_transaction.hash) + + # to update the transactions after the test is done. + on_exit(fn -> + :ok = GenServer.stop(pid) + :ok = GenServer.stop(blockchain_listener_pid) + end) end end end diff --git a/apps/ewallet_config/config/config.exs b/apps/ewallet_config/config/config.exs index 0cc3d804a..d61a8abe8 100644 --- a/apps/ewallet_config/config/config.exs +++ b/apps/ewallet_config/config/config.exs @@ -209,14 +209,14 @@ config :ewallet_config, value: "http://localhost:9656", type: "string", position: 210, - description: "The url used by the eWallet to interact with the OmiseGO Network's node." + description: "The URL for the OMG Network child chain." }, "omisego_watcher_url" => %{ key: "omisego_watcher_url", value: "http://localhost:7534", type: "string", position: 211, - description: "The url used by the eWallet to interact with the OmiseGO Network's watcher." + description: "The URL for the OMG Network information service." }, "omisego_plasma_framework_address" => %{ key: "omisego_plasma_framework_address", diff --git a/apps/frontend/assets/src/omg-blockchain-wallet/selector.js b/apps/frontend/assets/src/omg-blockchain-wallet/selector.js index 57a488256..5144eba80 100644 --- a/apps/frontend/assets/src/omg-blockchain-wallet/selector.js +++ b/apps/frontend/assets/src/omg-blockchain-wallet/selector.js @@ -20,6 +20,6 @@ export const selectBlockchainWalletById = state => id => state.blockchainWallets export const selectBlockchainWalletsLoadingStatus = state => state.loadingStatus.blockchainWallets -export const selectPlasmaDepositByAddress = state => address => { +export const selectPlasmaDepositByAddress = address => state => { return state.plasmaDeposits[address] } diff --git a/apps/frontend/assets/src/omg-page-blockchain-wallet-detail/BlockchainActionSelector.tsx b/apps/frontend/assets/src/omg-page-blockchain-wallet-detail/BlockchainActionSelector.tsx new file mode 100644 index 000000000..66403e5bb --- /dev/null +++ b/apps/frontend/assets/src/omg-page-blockchain-wallet-detail/BlockchainActionSelector.tsx @@ -0,0 +1,104 @@ +import React from 'react' +import { useDispatch } from 'react-redux' +import styled from 'styled-components' + +import PopperRenderer from 'omg-popper' +import withDropdownState from 'omg-uikit/dropdown/withDropdownState' +import { openModal } from 'omg-modal/action' +import { Icon, Button } from 'omg-uikit' +import { DropdownBox } from 'omg-uikit/dropdown' + +const DropdownItem = styled.div` + padding: 7px 10px; + padding-right: 20px; + font-size: 12px; + color: ${props => props.theme.colors.B100}; + cursor: pointer; + i, + span { + vertical-align: middle; + display: inline-block; + } + :hover { + color: ${props => props.theme.colors.B400}; + } + i { + margin-right: 5px; + } +` +const ButtonStyle = styled(Button)` + margin-left: 10px; + i { + margin-left: 10px; + margin-right: 0 !important; + } +` + +interface BlockchainActionSelectorProps { + name: string + onClickButton: React.MouseEventHandler + open: boolean + actions: Array<{ + name: string + modal: { id: string; args: {} } + icon: string + }> + fromAddress: string +} + +const BlockchainActionSelector = ({ + name, + actions, + open, + onClickButton, + fromAddress +}: BlockchainActionSelectorProps) => { + const dispatch = useDispatch() + + const renderDropdown = () => { + return ( + + {actions.map((action, index) => { + const onClick = () => + dispatch( + openModal({ + id: action.modal.id, + fromAddress, + ...action.modal.args + }) + ) + return ( + + + {action.name} + + ) + })} + + ) + } + + const renderButton = () => { + return ( + + {name} + {open ? : } + + ) + } + + return ( + + ) +} + +export default withDropdownState(BlockchainActionSelector) diff --git a/apps/frontend/assets/src/omg-page-blockchain-wallet-detail/HotWalletTransferChooser.js b/apps/frontend/assets/src/omg-page-blockchain-wallet-detail/HotWalletTransferChooser.js deleted file mode 100644 index 73f16c0ba..000000000 --- a/apps/frontend/assets/src/omg-page-blockchain-wallet-detail/HotWalletTransferChooser.js +++ /dev/null @@ -1,115 +0,0 @@ -import React, { Component } from 'react' -import { connect } from 'react-redux' -import PropTypes from 'prop-types' -import styled from 'styled-components' -import { compose } from 'recompose' - -import PopperRenderer from '../omg-popper' -import { openModal } from '../omg-modal/action' -import withDropdownState from '../omg-uikit/dropdown/withDropdownState' -import { Icon, Button } from '../omg-uikit' -import { DropdownBox } from '../omg-uikit/dropdown' - -const DropdownItem = styled.div` - padding: 7px 10px; - padding-right: 20px; - font-size: 12px; - color: ${props => props.theme.colors.B100}; - cursor: pointer; - i, - span { - vertical-align: middle; - display: inline-block; - } - :hover { - color: ${props => props.theme.colors.B400}; - } - i { - margin-right: 5px; - } -` -const ButtonStyle = styled(Button)` - margin-left: 10px; - i { - margin-left: 10px; - margin-right: 0 !important; - } -` -class HotWalletTransferChooser extends Component { - static propTypes = { - open: PropTypes.bool, - onClickButton: PropTypes.func, - onDepositComplete: PropTypes.func, - openModal: PropTypes.func, - fromAddress: PropTypes.string, - isColdWallet: PropTypes.bool - } - renderDropdown = () => { - return ( - - {this.props.isColdWallet && ( - { - this.props.openModal({ - id: 'hotWalletTransferModal', - fromAddress: this.props.fromAddress - }) - }} - > - - Transfer to Cold Wallet - - )} - this.props.openModal({ - id: 'plasmaDepositModal', - fromAddress: this.props.fromAddress, - onDepositComplete: this.props.onDepositComplete - })} - > - - OMG Network Deposit - - - ) - } - renderButton = () => { - return ( - - Transfer - {this.props.open - ? - : - } - - ) - } - render () { - return ( - - ) - } -} - -const enhance = compose( - withDropdownState, - connect( - null, - { openModal } - ) -) - -export default enhance(HotWalletTransferChooser) diff --git a/apps/frontend/assets/src/omg-page-blockchain-wallet-detail/index.js b/apps/frontend/assets/src/omg-page-blockchain-wallet-detail/index.js deleted file mode 100644 index 72f82b27e..000000000 --- a/apps/frontend/assets/src/omg-page-blockchain-wallet-detail/index.js +++ /dev/null @@ -1,157 +0,0 @@ -import React, { useEffect, useState } from 'react' -import PropTypes from 'prop-types' -import { Route, Switch, Redirect } from 'react-router-dom' -import { connect, useSelector, useDispatch } from 'react-redux' - -import { Button } from '../omg-uikit' -import { enableMetamaskEthereumConnection } from '../omg-web3/action' -import { selectMetamaskUsable } from '../omg-web3/selector' -import { - selectBlockchainWallets, - selectBlockchainWalletBalance, - selectBlockchainWalletById, - selectPlasmaDepositByAddress -} from '../omg-blockchain-wallet/selector' -import { getAllBlockchainWallets, getBlockchainWalletBalance } from '../omg-blockchain-wallet/action' -import CreateBlockchainTransactionButton from '../omg-transaction/CreateBlockchainTransactionButton' -import TopNavigation from '../omg-page-layout/TopNavigation' -import { getTransactionById } from '../omg-transaction/action' - -import HotWalletTransferChooser from './HotWalletTransferChooser' -import BlockchainSettingsPage from './BlockchainSettingsPage' -import BlockchainTransactionsPage from './BlockchainTransactionsPage' -import BlockchainTokensPage from './BlockchainTokensPage' - -const BlockchainWalletDetailPage = ({ - match, - selectBlockchainWalletBalance, - selectBlockchainWalletById, - selectPlasmaDepositByAddress, - getAllBlockchainWallets, - getBlockchainWalletBalance, - getTransactionById, - selectBlockchainWallets, - ...rest -}) => { - const { address } = match.params - const dispatch = useDispatch() - const metamaskUsable = useSelector(selectMetamaskUsable) - const balance = selectBlockchainWalletBalance(address) - .reduce((acc, curr) => acc + curr.amount, 0) - const walletType = selectBlockchainWalletById(address).type - const isColdWallet = !!selectBlockchainWallets.filter(i => i.type === 'cold').length - - const [pollingState, setPollingState] = useState(false) - - useEffect(() => { - if (!walletType) { - getAllBlockchainWallets({ - page: 1, - perPage: 10 - }) - } - }, [getAllBlockchainWallets, walletType]) - - useEffect(() => { - if (pollingState) { - const pollBalance = async () => { - try { - const { id: depositTransactionId } = selectPlasmaDepositByAddress(address) - const { data: { status } } = await getTransactionById(depositTransactionId) - - if (status === 'confirmed') { - getBlockchainWalletBalance({ - address: address, - cacheKey: { address: address, entity: 'plasmadeposits' } - }) - clearInterval(balancePolling) - setPollingState(false) - } else { - // keep polling until confirmed - } - } catch (e) { - clearInterval(balancePolling) - setPollingState(false) - } - } - const balancePolling = setInterval(pollBalance, 2000) - return () => clearInterval(balancePolling) - } - }, [address, getBlockchainWalletBalance, getTransactionById, pollingState, selectPlasmaDepositByAddress]) - - const renderTopupButton = () => ( - - ) - - const renderMetamaskConnectButton = () => ( - - ) - - const renderActionButton = () => { - if (walletType === 'hot' && balance > 0) { - return ( - setPollingState(true)} - /> - ) - } - if (metamaskUsable) { - return balance ? renderTopupButton() : null - } - return renderMetamaskConnectButton() - } - - return ( - <> - - - - - - - - - ) -} - -BlockchainWalletDetailPage.propTypes = { - match: PropTypes.object, - selectBlockchainWalletBalance: PropTypes.func, - selectBlockchainWalletById: PropTypes.func, - selectBlockchainWallets: PropTypes.array, - selectPlasmaDepositByAddress: PropTypes.func, - getAllBlockchainWallets: PropTypes.func, - getBlockchainWalletBalance: PropTypes.func, - getTransactionById: PropTypes.func -} - -export default connect( - state => ({ - selectBlockchainWalletBalance: selectBlockchainWalletBalance(state), - selectBlockchainWalletById: selectBlockchainWalletById(state), - selectBlockchainWallets: selectBlockchainWallets(state), - selectPlasmaDepositByAddress: selectPlasmaDepositByAddress(state) - }), - { getAllBlockchainWallets, getBlockchainWalletBalance, getTransactionById } -)(BlockchainWalletDetailPage) diff --git a/apps/frontend/assets/src/omg-page-blockchain-wallet-detail/index.tsx b/apps/frontend/assets/src/omg-page-blockchain-wallet-detail/index.tsx new file mode 100644 index 000000000..e32cfef0c --- /dev/null +++ b/apps/frontend/assets/src/omg-page-blockchain-wallet-detail/index.tsx @@ -0,0 +1,175 @@ +import React, { useEffect, useState } from 'react' +import { Route, Switch, Redirect, RouteComponentProps } from 'react-router-dom' +import { connect, useDispatch, useSelector } from 'react-redux' +import _ from 'lodash' + +import { + selectBlockchainWalletById, + selectPlasmaDepositByAddress +} from 'omg-blockchain-wallet/selector' +import { + getAllBlockchainWallets, + getBlockchainWalletBalance +} from 'omg-blockchain-wallet/action' +import TopNavigation from 'omg-page-layout/TopNavigation' +import { getTransactionById } from 'omg-transaction/action' +import { Button } from 'omg-uikit' +import theme from 'adminPanelApp/theme' + +import BlockchainActionSelector from './BlockchainActionSelector' +import BlockchainSettingsPage from './BlockchainSettingsPage' +import BlockchainTransactionsPage from './BlockchainTransactionsPage' +import BlockchainTokensPage from './BlockchainTokensPage' + +interface BlockchainWalletDetailPageProps extends RouteComponentProps { + getAllBlockchainWallets: Function + getBlockchainWalletBalance: Function +} + +const BlockchainWalletDetailPage = ({ + match, + getAllBlockchainWallets, + getBlockchainWalletBalance +}: BlockchainWalletDetailPageProps) => { + const dispatch = useDispatch() + const address = _.get(match, ['params', 'address']) + + const walletType = useSelector(state => + selectBlockchainWalletById(state)(address) + ).type + + const [pollingState, setPollingState] = useState(false) + + const latestDeposit = useSelector(selectPlasmaDepositByAddress(address)) + + useEffect(() => { + if (!walletType) { + getAllBlockchainWallets({ + page: 1, + perPage: 10 + }) + } + }, [getAllBlockchainWallets, walletType]) + + useEffect(() => { + if (pollingState) { + const pollBalance = async () => { + try { + const { id } = latestDeposit + const { data } = await getTransactionById(id)(dispatch) + + if (data.status === 'confirmed') { + getBlockchainWalletBalance({ + address: address, + cacheKey: { address: address, entity: 'plasmadeposits' } + }) + clearInterval(balancePolling) + setPollingState(false) + } else { + _.noop() /* Keep polling until confirmed */ + } + } catch (e) { + clearInterval(balancePolling) + setPollingState(false) + } + } + const balancePolling = setInterval(pollBalance, 2000) + return () => clearInterval(balancePolling) + } + }, [ + address, + dispatch, + getBlockchainWalletBalance, + latestDeposit, + pollingState + ]) + + const renderActionButtons = () => { + const ethereumActions = [ + { + name: 'Transfer to Cold Wallet', + modal: { id: 'hotWalletTransferModal' }, + icon: 'Transaction' + } + ] + + const plasmaActions = [ + { + name: 'Deposit to the OMG Network', + modal: { + id: 'plasmaDepositModal', + args: { onDepositComplete: () => setPollingState(true) } + }, + icon: 'Download' + } + ] + + if (walletType === 'hot') { + return ( + <> + + + + + ) + } + } + + return ( + <> + + + + + + + + + ) +} + +export default connect(null, { + getAllBlockchainWallets, + getBlockchainWalletBalance +})(BlockchainWalletDetailPage) diff --git a/apps/frontend/assets/src/omg-page-configuration/BlockchainSettings.tsx b/apps/frontend/assets/src/omg-page-configuration/BlockchainSettings.tsx index 56dbefc15..c24c665ba 100644 --- a/apps/frontend/assets/src/omg-page-configuration/BlockchainSettings.tsx +++ b/apps/frontend/assets/src/omg-page-configuration/BlockchainSettings.tsx @@ -51,63 +51,74 @@ const BlockchainSettings = (props: BlockchainSettingsProps) => { displayName: string disableUpdate: boolean inputValidator?: (...args: any) => boolean - position: number } } const settings: displaySettings = { blockchain_json_rpc_url: { displayName: 'Blockchain JSON-RPC URL', - disableUpdate: true, - position: 0 + disableUpdate: true }, blockchain_chain_id: { displayName: 'Blockchain Chain ID', - disableUpdate: true, - position: 1 + disableUpdate: true }, blockchain_confirmations_threshold: { displayName: 'Blockchain Confirmations Threshold', disableUpdate: false, - inputValidator: isPositiveInteger, - position: 2 + inputValidator: isPositiveInteger }, blockchain_deposit_pooling_interval: { displayName: 'Blockchain Deposit Polling Interval', disableUpdate: false, - inputValidator: isPositiveInteger, - position: 3 + inputValidator: isPositiveInteger }, blockchain_transaction_poll_interval: { displayName: 'Blockchain Transaction Poll Interval', disableUpdate: false, - inputValidator: isPositiveInteger, - position: 4 + inputValidator: isPositiveInteger }, blockchain_state_save_interval: { displayName: 'Blockchain State Save Interval', disableUpdate: false, - inputValidator: isPositiveInteger, - position: 5 + inputValidator: isPositiveInteger }, blockchain_sync_interval: { displayName: 'Blockchain Sync Interval', disableUpdate: false, - inputValidator: isPositiveInteger, - position: 6 + inputValidator: isPositiveInteger }, blockchain_poll_interval: { displayName: 'Blockchain Poll Interval', disableUpdate: false, - inputValidator: isPositiveInteger, - position: 7 + inputValidator: isPositiveInteger + }, + omisego_childchain_url: { + disableUpdate: true, + displayName: 'OMG Network Child Chain URL' + }, + omisego_erc20_vault_address: { + disableUpdate: true, + displayName: 'OMG Network ERC20 Vault Address' + }, + omisego_eth_vault_address: { + disableUpdate: true, + displayName: 'OMG Network ETH Vault Address' + }, + omisego_plasma_framework_address: { + disableUpdate: true, + displayName: 'OMG Network Plasma Framework Address' + }, + omisego_watcher_url: { + disableUpdate: true, + displayName: 'OMG Network Information Service URL' } } const renderBlockchainSettings = () => { const sortedConfigurationList = _.sortBy( _.values(_.pick(configurations, _.keys(settings))), - config => settings[config.key].position + 'position' ) return ( diff --git a/apps/frontend/assets/src/omg-page-configuration/index.js b/apps/frontend/assets/src/omg-page-configuration/index.js index d8edb40b7..cc94ba459 100644 --- a/apps/frontend/assets/src/omg-page-configuration/index.js +++ b/apps/frontend/assets/src/omg-page-configuration/index.js @@ -135,6 +135,11 @@ class ConfigurationPage extends React.Component { blockchainStateSaveInterval: config.blockchain_state_save_interval.value, blockchainSyncInterval: config.blockchain_sync_interval.value, blockchainTransactionPollInterval: config.blockchain_transaction_poll_interval.value, + omisegoChildchainUrl: config.omisego_childchain_url.value, + omisegoErc20VaultAddress: config.omisego_erc20_vault_address.value, + omisegoEthVaultAddress: config.omisego_eth_vault_address.value, + omisegoPlasmaFrameworkAddress: config.omisego_plasma_framework_address.value, + omisegoWatcherUrl: config.omisego_watcher_url.value, } return { originalState: derivedState, @@ -396,9 +401,9 @@ class ConfigurationPage extends React.Component { ]} types={false} /> - + - {`The app is currently ${_.get(this.props.configurations, 'enable_blockchain.value', false) ? '' : ' not'} connected to Ethereum.`} + {`The app is currently ${_.get(this.props.configurations, 'blockchain_enabled.value', false) ? '' : ' not'} connected to Ethereum.`} diff --git a/apps/frontend/assets/src/omg-plasma-deposit-modal/index.js b/apps/frontend/assets/src/omg-plasma-deposit-modal/index.js index afcae23a5..e47588c7e 100644 --- a/apps/frontend/assets/src/omg-plasma-deposit-modal/index.js +++ b/apps/frontend/assets/src/omg-plasma-deposit-modal/index.js @@ -111,7 +111,7 @@ class PlasmaDeposit extends Component { ) const result = await this.props.plasmaDeposit({ address: this.state.fromAddress.trim(), - amount: parseInt(fromAmount), + amount: fromAmount, tokenId: _.get(this.state.fromTokenSelected, 'token.id') }) if (result.data) { @@ -199,7 +199,7 @@ class PlasmaDeposit extends Component {
-

OMG Network Network Deposit

+

OMG Network Deposit

{this.renderFromSection()}