diff --git a/src/actions/proposals.js b/src/actions/proposals.js index 04e698c..7c07bf9 100644 --- a/src/actions/proposals.js +++ b/src/actions/proposals.js @@ -200,7 +200,7 @@ export const fetchProposalTally = (id) => (dispatch) => { }, }) .then((res) => { - dispatch(fetchProposalTallySuccess(res.data && res.data.result, id)); + dispatch(fetchProposalTallySuccess(res.data && res.data.tally, id)); }) .catch((error) => { dispatch(fetchProposalTallyError( diff --git a/src/assets/MetaMask.png b/src/assets/MetaMask.png new file mode 100644 index 0000000..0848739 Binary files /dev/null and b/src/assets/MetaMask.png differ diff --git a/src/containers/Home/ClaimDialog/index.js b/src/containers/Home/ClaimDialog/index.js index 9bc4060..585c38f 100644 --- a/src/containers/Home/ClaimDialog/index.js +++ b/src/containers/Home/ClaimDialog/index.js @@ -11,7 +11,7 @@ import { import { connect } from 'react-redux'; import '../../Stake/DelegateDialog/index.css'; import ValidatorsSelectField from './ValidatorsSelectField'; -import { cosmoStationSign, signTxAndBroadcast } from '../../../helper'; +import { cosmoStationSign, metaMaskSign, signTxAndBroadcast } from '../../../helper'; import { showMessage } from '../../../actions/snackbar'; import { fetchRewards, fetchVestingBalance, getBalance } from '../../../actions/accounts'; import { config } from '../../../config'; @@ -61,6 +61,11 @@ const ClaimDialog = (props) => { return; } + if (localStorage.getItem('of_co_wallet') === 'metamask') { + metaMaskSign(updatedTx, props.address, handleFetch); + return; + } + signTxAndBroadcast(updatedTx, props.address, handleFetch); }; @@ -112,6 +117,11 @@ const ClaimDialog = (props) => { return; } + if (localStorage.getItem('of_co_wallet') === 'metamask') { + metaMaskSign(updatedTx, props.address, handleFetch); + return; + } + signTxAndBroadcast(updatedTx, props.address, handleFetch); }; diff --git a/src/containers/NavBar/ConnectDialog/MetaMaskConnectButton.js b/src/containers/NavBar/ConnectDialog/MetaMaskConnectButton.js new file mode 100644 index 0000000..09fe450 --- /dev/null +++ b/src/containers/NavBar/ConnectDialog/MetaMaskConnectButton.js @@ -0,0 +1,102 @@ +import React, { useState } from 'react'; +import { Button } from '@material-ui/core'; +import * as PropTypes from 'prop-types'; +import { initializeMetaMask } from '../../../helper'; +import { + fetchRewards, + fetchVestingBalance, + getBalance, + getDelegations, + getUnBondingDelegations, + setAccountAddress, + showSelectAccountDialog, +} from '../../../actions/accounts'; +import { connect } from 'react-redux'; +import { showMessage } from '../../../actions/snackbar'; +import { encode } from 'js-base64'; +import { getDelegatedValidatorsDetails } from '../../../actions/stake'; +import MetaMaskIcon from '../../../assets/MetaMask.png'; +import { hideConnectDialog } from '../../../actions/navBar'; +import variables from '../../../utils/variables'; + +const MetaMaskConnectButton = (props) => { + const [inProgress, setInProgress] = useState(false); + + const initKeplr = () => { + setInProgress(true); + initializeMetaMask((error, addressList) => { + setInProgress(false); + if (error) { + localStorage.removeItem('of_co_address'); + props.showMessage(error); + + return; + } + + props.setAccountAddress(addressList && addressList.address && addressList.address.address); + props.hideConnectDialog(); + if (!props.proposalTab && !props.stake) { + props.getUnBondingDelegations(addressList && addressList.address && addressList.address.address); + props.fetchRewards(addressList && addressList.address && addressList.address.address); + } + if (!props.proposalTab) { + props.getDelegations(addressList && addressList.address && addressList.address.address); + } + props.getBalance(addressList && addressList.address && addressList.address.address); + props.fetchVestingBalance(addressList && addressList.address && addressList.address.address); + if (!props.proposalTab) { + props.getDelegatedValidatorsDetails(addressList && addressList.address && addressList.address.address); + } + localStorage.setItem('of_co_address', encode(addressList && addressList.address && addressList.address.address)); + localStorage.setItem('of_co_wallet', 'metamask'); + }); + }; + + return ( + + ); +}; + +MetaMaskConnectButton.propTypes = { + fetchRewards: PropTypes.func.isRequired, + fetchVestingBalance: PropTypes.func.isRequired, + getBalance: PropTypes.func.isRequired, + getDelegatedValidatorsDetails: PropTypes.func.isRequired, + getDelegations: PropTypes.func.isRequired, + getUnBondingDelegations: PropTypes.func.isRequired, + hideConnectDialog: PropTypes.func.isRequired, + lang: PropTypes.string.isRequired, + setAccountAddress: PropTypes.func.isRequired, + showDialog: PropTypes.func.isRequired, + showMessage: PropTypes.func.isRequired, + proposalTab: PropTypes.bool, + stake: PropTypes.bool, +}; + +const stateToProps = (state) => { + return { + lang: state.language, + }; +}; + +const actionsToProps = { + showMessage, + setAccountAddress, + showDialog: showSelectAccountDialog, + getDelegations, + getDelegatedValidatorsDetails, + fetchVestingBalance, + hideConnectDialog, + getBalance, + getUnBondingDelegations, + fetchRewards, +}; + +export default connect(stateToProps, actionsToProps)(MetaMaskConnectButton); diff --git a/src/containers/NavBar/ConnectDialog/index.js b/src/containers/NavBar/ConnectDialog/index.js index 5c7f945..fbe3a77 100644 --- a/src/containers/NavBar/ConnectDialog/index.js +++ b/src/containers/NavBar/ConnectDialog/index.js @@ -12,6 +12,7 @@ import insync from '../../../assets/insync.png'; // import poweredBy from '../../../assets/powered_by.jpeg'; import './index.css'; import { config } from '../../../config'; +import MetaMaskConnectButton from './MetaMaskConnectButton'; const LightTooltip = withStyles((theme) => ({ tooltip: { @@ -36,6 +37,16 @@ const ConnectDialog = (props) => { supported wallets
+
+ + + window.open('https://chromewebstore.google.com/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn')}> + + + +
diff --git a/src/containers/NavBar/index.js b/src/containers/NavBar/index.js index e35391b..18c4d97 100644 --- a/src/containers/NavBar/index.js +++ b/src/containers/NavBar/index.js @@ -9,7 +9,7 @@ import { connect } from 'react-redux'; import ClassNames from 'classnames'; import { hideSideBar, showConnectDialog } from '../../actions/navBar'; import Icon from '../../components/Icon'; -import { initializeChain, initializeCosmoStation } from '../../helper'; +import { initializeChain, initializeCosmoStation, initializeMetaMask } from '../../helper'; import { decode, encode } from 'js-base64'; import { config } from '../../config'; import { showMessage } from '../../actions/snackbar'; @@ -49,6 +49,7 @@ class NavBar extends Component { this.getValidatorImage = this.getValidatorImage.bind(this); this.getProposalDetails = this.getProposalDetails.bind(this); this.handleCosmoStation = this.handleCosmoStation.bind(this); + this.handleMetaMask = this.handleMetaMask.bind(this); } componentDidMount () { @@ -56,6 +57,10 @@ class NavBar extends Component { setTimeout(() => { this.handleCosmoStation(true); }, 600); + } else if (localStorage.getItem('of_co_address') && (localStorage.getItem('of_co_wallet') === 'metamask')) { + setTimeout(() => { + this.handleMetaMask(true); + }, 600); } else if (localStorage.getItem('of_co_address')) { setTimeout(() => { this.initKeplr(); @@ -70,7 +75,7 @@ class NavBar extends Component { const array = []; result.map((val) => { const filter = this.props.proposalDetails && Object.keys(this.props.proposalDetails).length && - Object.keys(this.props.proposalDetails).find((key) => key === val.proposal_id); + Object.keys(this.props.proposalDetails).find((key) => key === val.id); if (!filter) { if (this.props.home && (val.status !== 'PROPOSAL_STATUS_VOTING_PERIOD')) { return null; @@ -79,7 +84,7 @@ class NavBar extends Component { array.push(val.proposal_id); } if (val.status === 2 || val.status === 'PROPOSAL_STATUS_VOTING_PERIOD') { - this.props.fetchProposalTally(val.proposal_id); + this.props.fetchProposalTally(val.id); } return null; @@ -93,7 +98,7 @@ class NavBar extends Component { const array = []; this.props.proposals.map((val) => { const filter = this.props.proposalDetails && Object.keys(this.props.proposalDetails).length && - Object.keys(this.props.proposalDetails).find((key) => key === val.proposal_id); + Object.keys(this.props.proposalDetails).find((key) => key === val.id); if (!filter) { if (this.props.home && (val.status !== 'PROPOSAL_STATUS_VOTING_PERIOD')) { return null; @@ -102,7 +107,7 @@ class NavBar extends Component { array.push(val.proposal_id); } if (val.status === 2 || val.status === 'PROPOSAL_STATUS_VOTING_PERIOD') { - this.props.fetchProposalTally(val.proposal_id); + this.props.fetchProposalTally(val.id); } return null; @@ -142,6 +147,17 @@ class NavBar extends Component { }); } }; + + if (window.ethereum) { + window.ethereum && window.ethereum.on('accountsChanged', (accounts) => { + if (accounts.length === 0) { + showMessage('Please connect to MetaMask.'); + return; + } + + this.handleMetaMask(); + }); + } } componentDidUpdate (pp, ps, ss) { @@ -149,12 +165,12 @@ class NavBar extends Component { this.props.proposals && this.props.proposals.length) || ((pp.address !== this.props.address) && (pp.address === '') && (this.props.address !== ''))) { this.props.proposals.map((val) => { - const votedOption = this.props.voteDetails && this.props.voteDetails.length && val && val.proposal_id && - this.props.voteDetails.filter((vote) => vote.proposal_id === val.proposal_id)[0]; + const votedOption = this.props.voteDetails && this.props.voteDetails.length && val && val.id && + this.props.voteDetails.filter((vote) => vote && vote.id === val.id)[0]; if ((val.status === 2 || val.status === 'PROPOSAL_STATUS_VOTING_PERIOD') && !votedOption && this.props.address) { - this.props.fetchVoteDetails(val.proposal_id, this.props.address); + this.props.fetchVoteDetails(val.id, this.props.address); } return null; @@ -168,7 +184,7 @@ class NavBar extends Component { const array = []; result.map((val) => { const filter = this.props.proposalDetails && Object.keys(this.props.proposalDetails).length && - Object.keys(this.props.proposalDetails).find((key) => key === val.proposal_id); + Object.keys(this.props.proposalDetails).find((key) => key === val.id); if (!filter) { if (this.props.home && (val.status !== 'PROPOSAL_STATUS_VOTING_PERIOD')) { return null; @@ -177,8 +193,8 @@ class NavBar extends Component { array.push(val.proposal_id); } if (val.status === 2 || val.status === 'PROPOSAL_STATUS_VOTING_PERIOD') { - this.props.fetchProposalTally(val.proposal_id); - this.props.fetchVoteDetails(val.proposal_id, this.props.address); + this.props.fetchProposalTally(val.id); + this.props.fetchVoteDetails(val.id, this.props.address); } return null; @@ -324,6 +340,24 @@ class NavBar extends Component { }); } + handleMetaMask (fetch) { + initializeMetaMask((error, account) => { + if (error) { + this.props.showMessage(error); + localStorage.removeItem('of_co_address'); + + return; + } + + this.props.setAccountAddress(account && account.address && account.address.address); + if (fetch) { + this.handleFetch(account && account.address && account.address.address); + } + localStorage.setItem('of_co_address', encode(account && account.address && account.address.address)); + localStorage.setItem('of_co_wallet', 'metamask'); + }); + } + render () { return (
{ const VoteCalculation = (proposal, val) => { if (proposal.status === 2 || proposal.status === 'PROPOSAL_STATUS_VOTING_PERIOD') { const value = props.tallyDetails && props.tallyDetails[proposal.id]; - const sum = value && value.yes_count && value.no_count && value.no_with_veto_count && value.abstain_count && - (parseInt(value.yes_count) + parseInt(value.no_count) + parseInt(value.no_with_veto_count) + parseInt(value.abstain_count)); - - return (props.tallyDetails && props.tallyDetails[proposal.id] && props.tallyDetails[proposal.id][val] - ? tally(props.tallyDetails[proposal.id][val], sum) : '0%'); + const sum = value && value.yes && value.no && value.no_with_veto && value.abstain && + (parseInt(value.yes) + parseInt(value.no) + parseInt(value.no_with_veto) + parseInt(value.abstain)); + let val1 = null; + if (val === 'yes_count') { + val1 = 'yes'; + } else if (val === 'no_count') { + val1 = 'no'; + } else if (val === 'no_with_veto_count') { + val1 = 'no_with_veto'; + } else if (val === 'abstain_count') { + val1 = 'abstain'; + } + + return (props.tallyDetails && props.tallyDetails[proposal.id] && props.tallyDetails[proposal.id][val1] + ? tally(props.tallyDetails[proposal.id][val1], sum) : '0%'); } else { const sum = proposal.final_tally_result && proposal.final_tally_result.yes_count && proposal.final_tally_result.no_count && proposal.final_tally_result.no_with_veto_count && @@ -63,7 +73,7 @@ const Cards = (props) => { if (index < (page * rowsPerPage) && index >= (page - 1) * rowsPerPage) { const votedOption = props.voteDetails && props.voteDetails.length && proposal && proposal.id && - props.voteDetails.filter((vote) => vote.id === proposal.id)[0]; + props.voteDetails.filter((vote) => vote && vote.id === proposal.id)[0]; let proposer = proposal.proposer; props.proposalDetails && Object.keys(props.proposalDetails).length && Object.keys(props.proposalDetails).filter((key) => { diff --git a/src/containers/Proposals/ProposalDialog/Voting.js b/src/containers/Proposals/ProposalDialog/Voting.js index 0b44444..46150ee 100644 --- a/src/containers/Proposals/ProposalDialog/Voting.js +++ b/src/containers/Proposals/ProposalDialog/Voting.js @@ -4,7 +4,7 @@ import { fetchProposalTally, fetchVoteDetails, hideProposalDialog } from '../../ import { connect } from 'react-redux'; import { Button, FormControlLabel, Radio, RadioGroup } from '@material-ui/core'; import CircularProgress from '../../../components/CircularProgress'; -import { cosmoStationSign, signTxAndBroadcast } from '../../../helper'; +import { cosmoStationSign, metaMaskSign, signTxAndBroadcast } from '../../../helper'; import { config } from '../../../config'; import variables from '../../../utils/variables'; import { showMessage } from '../../../actions/snackbar'; @@ -68,6 +68,11 @@ const Voting = (props) => { return; } + if (localStorage.getItem('of_co_wallet') === 'metamask') { + metaMaskSign(tx, props.address, handleFetch); + return; + } + signTxAndBroadcast(tx, props.address, handleFetch); }; diff --git a/src/containers/Proposals/ProposalDialog/index.js b/src/containers/Proposals/ProposalDialog/index.js index 095fbd4..be97fa6 100644 --- a/src/containers/Proposals/ProposalDialog/index.js +++ b/src/containers/Proposals/ProposalDialog/index.js @@ -38,7 +38,7 @@ class ProposalDialog extends Component { componentDidMount () { const votedOption = this.props.voteDetails && this.props.voteDetails.length && this.props.proposal && this.props.proposal.id && - this.props.voteDetails.filter((vote) => vote.id === this.props.proposal.id)[0]; + this.props.voteDetails.filter((vote) => vote && vote.id === this.props.proposal.id)[0]; if (!votedOption && this.props.proposal && this.props.proposal.id && this.props.address) { this.props.fetchVoteDetails(this.props.proposal.id, this.props.address); @@ -74,8 +74,8 @@ class ProposalDialog extends Component { if (proposal && (proposal.status === 2 || proposal.status === 'PROPOSAL_STATUS_VOTING_PERIOD')) { const value = this.props.tallyDetails && this.props.tallyDetails[proposal.id]; - const sum = value && value.yes_count && value.no_count && value.no_with_veto_count && value.abstain_count && - (parseInt(value.yes_count) + parseInt(value.no_count) + parseInt(value.no_with_veto_count) + parseInt(value.abstain_count)); + const sum = value && value.yes && value.no && value.no_with_veto && value.abstain && + (parseInt(value.yes) + parseInt(value.no) + parseInt(value.no_with_veto) + parseInt(value.abstain)); return (this.props.tallyDetails && this.props.tallyDetails[proposal.id] && this.props.tallyDetails[proposal.id][val] ? tally(this.props.tallyDetails[proposal.id][val], sum) : '0%'); @@ -100,7 +100,7 @@ class ProposalDialog extends Component { render () { let votedOption = this.props.voteDetails && this.props.voteDetails.length && this.props.proposal && this.props.proposal.id && - this.props.voteDetails.filter((vote) => vote.id === this.props.proposal.id)[0]; + this.props.voteDetails.filter((vote) => vote && vote.id === this.props.proposal.id)[0]; let proposer = this.props.proposal && this.props.proposal.proposer; this.props.proposalDetails && Object.keys(this.props.proposalDetails).length && diff --git a/src/containers/Stake/DelegateDialog/index.js b/src/containers/Stake/DelegateDialog/index.js index b9646ae..adb839c 100644 --- a/src/containers/Stake/DelegateDialog/index.js +++ b/src/containers/Stake/DelegateDialog/index.js @@ -13,7 +13,7 @@ import { import ValidatorSelectField from './ValidatorSelectField'; import TokensTextField from './TokensTextField'; import ToValidatorSelectField from './ToValidatorSelectField'; -import { cosmoStationSign, signTxAndBroadcast } from '../../../helper'; +import { cosmoStationSign, metaMaskSign, signTxAndBroadcast } from '../../../helper'; import { fetchRewards, fetchVestingBalance, @@ -61,6 +61,11 @@ const DelegateDialog = (props) => { return; } + if (localStorage.getItem('of_co_wallet') === 'metamask') { + metaMaskSign(updatedTx, props.address, handleFetch); + return; + } + signTxAndBroadcast(updatedTx, props.address, handleFetch); }; diff --git a/src/helper.js b/src/helper.js index edae05d..cb40095 100644 --- a/src/helper.js +++ b/src/helper.js @@ -119,6 +119,93 @@ export const initializeCosmoStation = (cb) => { })(); }; +export const initializeMetaMask = (cb) => { + (async () => { + if (!window.ethereum) { + const error = 'Download the MetaMask Extension'; + cb(error); + } + + if (window.ethereum) { + const result = await window.ethereum.request({ method: 'wallet_getSnaps' }); + const installed = Object.keys(result).includes('npm:@cosmsnap/snap'); + + // Install Snap + if (!installed) { + window.ethereum.request({ + method: 'wallet_requestSnaps', + params: { + 'npm:@cosmsnap/snap': { + version: '^0.1.0', + }, + }, + }).then((result) => { + metamaskInitialize(cb); + }).catch((error) => { + cb((error && error.message) || error); + }); + } + + metamaskInitialize(cb); + } else { + return null; + } + })(); +}; + +const metamaskInitialize = (cb) => { + (async () => { + try { + const initialized = await window.ethereum.request({ + method: 'wallet_invokeSnap', + params: { + snapId: 'npm:@cosmsnap/snap', + request: { + method: 'initialized', + }, + }, + }); + + if (!initialized) { + // Initialize the Snap with default chains + window.ethereum.request({ + method: 'wallet_invokeSnap', + params: { + snapId: 'npm:@cosmsnap/snap', + request: { + method: 'initialize', + }, + }, + }).then((result) => { + }).catch((error) => { + cb((error && error.message) || error); + }); + } + + window.ethereum.request({ + method: 'wallet_invokeSnap', + params: { + snapId: 'npm:@cosmsnap/snap', + request: { + method: 'getChainAddress', + params: { + chain_id: chainId, + }, + }, + }, + }).then((result) => { + if (result && result.data) { + cb(null, result && result.data); + } + }).catch((error) => { + cb((error && error.message) || error); + }); + } catch (error) { + cb((error && error.message) || error); + } + })(); +}; + export const signTxAndBroadcast = (tx, address, cb) => { (async () => { await window.keplr && window.keplr.enable(chainId); @@ -178,3 +265,57 @@ export const cosmoStationSign = (tx, address, cb) => { }); })(); }; + +export const metaMaskSign = (tx, address, cb) => { + (async () => { + const msgs = tx.msgs ? tx.msgs : [tx.msg]; + const fees = tx.fee; + if (fees && fees.amount && fees.amount.length) { + const array = []; + fees.amount.map((val) => { + let obj = {}; + if (val && val.amount) { + obj = { + amount: String(Number(val.amount) * 100), + denom: config.COIN_MINIMAL_DENOM, + }; + } + + array.push(obj); + }); + + fees.amount = array; + } + + window.ethereum.request({ + method: 'wallet_invokeSnap', + params: { + snapId: 'npm:@cosmsnap/snap', + request: { + method: 'transact', + params: { + chain_id: chainId, + msgs: JSON.stringify(msgs), + fees: JSON.stringify(fees), + }, + }, + }, + }).then((result) => { + if (result && result.data && result.data.code !== undefined && result.data.code !== 0) { + cb(result.data.log || result.data.rawLog); + } else if (result && !result.success) { + const message = 'unsuccess'; + cb(message); + } else { + cb(null, result && result.data); + } + }).catch((error) => { + const message = 'success'; + if (error && error.message === 'Invalid string. Length must be a multiple of 4') { + cb(null, message); + } else { + cb(error && error.message); + } + }); + })(); +}; diff --git a/src/utils/variables.js b/src/utils/variables.js index 4c0d669..7c076c7 100644 --- a/src/utils/variables.js +++ b/src/utils/variables.js @@ -63,6 +63,7 @@ const variables = { connect_account: 'Account not connected. Please connect to wallet', connecting: 'connecting', cosmostation: 'Cosmostation', + metamask: 'MetaMask', }, };