diff --git a/code/client/package.json b/code/client/package.json index 6e5bbdd9..cc032d4c 100644 --- a/code/client/package.json +++ b/code/client/package.json @@ -1,6 +1,6 @@ { "name": "one-wallet-web", - "version": "0.9.1", + "version": "0.9.2", "private": true, "license": "Apache-2.0", "main": "client.js", diff --git a/code/client/src/components/AddressInput.jsx b/code/client/src/components/AddressInput.jsx index c34c7440..ae1a9608 100644 --- a/code/client/src/components/AddressInput.jsx +++ b/code/client/src/components/AddressInput.jsx @@ -17,7 +17,7 @@ const delayDomainOperationMillis = 1000 * Renders address input that provides type ahead search for any known addresses. * Known addresses are addresses that have been entered by user for at least once. */ -const AddressInput = ({ setAddressCallback, currentWallet, addressValue, extraSelectOptions }) => { +const AddressInput = ({ setAddressCallback, currentWallet, addressValue, extraSelectOptions, disableManualInput }) => { const dispatch = useDispatch() const [searchingAddress, setSearchingAddress] = useState(false) @@ -176,7 +176,7 @@ const AddressInput = ({ setAddressCallback, currentWallet, addressValue, extraSe } }, [knownAddresses, setAddressCallback]) - const showSelectManualInputAddress = util.safeOneAddress(addressValue.value) && + const showSelectManualInputAddress = !disableManualInput && util.safeOneAddress(addressValue.value) && !wallets[util.safeNormalizedAddress(addressValue.value)] && !Object.keys(knownAddresses).includes(util.safeNormalizedAddress(addressValue.value)) diff --git a/code/client/src/components/Text.jsx b/code/client/src/components/Text.jsx index e3735d20..a8a54472 100644 --- a/code/client/src/components/Text.jsx +++ b/code/client/src/components/Text.jsx @@ -51,3 +51,11 @@ export const ExplorerLink = styled(Link).attrs(e => ({ ...e, style: { color: '#8 opacity: ${props => props['data-show-on-hover'] ? 1.0 : 0.8}; } ` + +export const Ul = styled.ul` + list-style: none!important; + margin-left: 0; + padding-left: 1em; + text-indent: -1em; +` +export const Li = styled.li`` diff --git a/code/client/src/constants/paths.js b/code/client/src/constants/paths.js index 42028099..4847beba 100644 --- a/code/client/src/constants/paths.js +++ b/code/client/src/constants/paths.js @@ -4,7 +4,19 @@ export default { dev: base + '/dev', create: base + '/create', wallets: base + '/wallets', - show: base + '/show/:address/:action?', restore: base + '/restore', - showAddress: (address, action) => base + `/show/${address}${action ? `/${action}` : ''}` + + show: base + '/show/:address/:action?', + showAddress: (address, action) => base + `/show/${address}${action ? `/${action}` : ''}`, + + auth: base + '/auth/:action?/:address?', + doRedirect: (action, address) => { + if (!action) { + return base + '/redirect' + } + if (!address) { + return base + `/redirect/${action}` + } + return base + `/redirect/${action}/${address}` + }, } diff --git a/code/client/src/integration/Connect.jsx b/code/client/src/integration/Connect.jsx new file mode 100644 index 00000000..406aefb0 --- /dev/null +++ b/code/client/src/integration/Connect.jsx @@ -0,0 +1,109 @@ +import { Button, message, Row, Space, Typography, Select, Col, Tooltip } from 'antd' +import AnimatedSection from '../components/AnimatedSection' +import { AverageRow } from '../components/Grid' +import { Li, Ul } from '../components/Text' +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import { SearchOutlined } from '@ant-design/icons' +import util from '../util' +const { Title, Text, Paragraph } = Typography +const ConnectWallet = ({ caller, callback }) => { + const network = useSelector(state => state.wallet.network) + const wallets = useSelector(state => state.wallet.wallets) + const walletList = Object.keys(wallets).map(e => wallets[e]).filter(e => e.network === network) + const [selectedAddress, setSelectedAddress] = useState(walletList.length === 0 + ? {} + : { value: walletList[0].address, label: `(${walletList[0].name}) ${util.ellipsisAddress(util.safeOneAddress(walletList[0].address))}` }) + const connect = () => { + if (!selectedAddress.value) { + return message.error('No address is selected') + } + if (!callback) { + message.error('The app did not specify a callback URL. Please contact the app developer.') + return + } + window.location.href = callback + `?address=${selectedAddress.value}&success=1` + } + const cancel = () => { + if (!callback) { + message.error('The app did not specify a callback URL. Please contact the app developer.') + return + } + window.location.href = callback + '?success=0' + } + return ( + + + + "{caller}" wants to connect to your 1wallet + + The app will be able to: +
    +
  • View the address of the connected wallet
  • +
+ The app cannot: +
    +
  • Do anything without your permission (e.g. transferring funds, sign transactions, ...)
  • +
+
+
+
+ + + Select a wallet you want to connect: + + + + + + + + + +
+ ) +} + +export default ConnectWallet diff --git a/code/client/src/integration/RequestPayment.jsx b/code/client/src/integration/RequestPayment.jsx new file mode 100644 index 00000000..5a270144 --- /dev/null +++ b/code/client/src/integration/RequestPayment.jsx @@ -0,0 +1,155 @@ +import { Button, message, Row, Space, Typography, Select, Col, Tooltip } from 'antd' +import BN from 'bn.js' +import AnimatedSection from '../components/AnimatedSection' +import { AverageRow } from '../components/Grid' +import { Hint } from '../components/Text' +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import { SearchOutlined } from '@ant-design/icons' +import util from '../util' +import WalletAddress from '../components/WalletAddress' +import Send from '../pages/Show/Send' +import { handleAddressError } from '../handler' +import { HarmonyONE } from '../components/TokenAssets' +const { Title, Text, Paragraph } = Typography +const RequestPaymennt = ({ caller, callback, amount, dest, from }) => { + dest = util.safeNormalizedAddress(dest) + const network = useSelector(state => state.wallet.network) + const balances = useSelector(state => state.wallet.balances) + const price = useSelector(state => state.wallet.price) + const wallets = useSelector(state => state.wallet.wallets) + const walletList = Object.keys(wallets).map(e => wallets[e]).filter(e => e.network === network) + const selectedWallet = from && wallets[from] + const defaultUserAddress = (walletList.length === 0 ? {} : { value: walletList[0].address, label: `(${walletList[0].name}) ${util.ellipsisAddress(util.safeOneAddress(walletList[0].address))}` }) + const [selectedAddress, setSelectedAddress] = useState(from ? (selectedWallet || {}) : defaultUserAddress) + const { formatted: amountFormatted, fiatFormatted: amountFiatFormatted } = util.computeBalance(amount, price) + const [showSend, setShowSend] = useState(false) + const checkCallback = () => { + if (!callback) { + message.error('The app did not specify a callback URL. Please contact the app developer.') + return false + } + return true + } + const next = () => { + if (!selectedAddress.value) { + return message.error('No address is selected') + } + const normalizedAddress = util.safeExec(util.normalizedAddress, [selectedAddress.value], handleAddressError) + if (!normalizedAddress) { + return message.error(`normalizedAddress=${normalizedAddress}`) + } + const balance = balances[selectedAddress.value] + if (!(new BN(amount).lte(new BN(balance)))) { + const { formatted: balanceFormatted } = util.computeBalance(balance) + return message.error(`Insufficient balance (${balanceFormatted} ONE) in the selected wallet`) + } + if (!checkCallback()) return + setShowSend(true) + } + const cancel = () => { + if (!checkCallback()) return + window.location.href = callback + '?success=0' + } + const onSendClose = () => { + setShowSend(false) + } + const onSuccess = (txId) => { + if (!checkCallback()) return + window.location.href = callback + `?success=1&txId=${txId}` + } + + return ( + <> + + + + "{caller}" wants you to pay + {amountFormatted} ONE <Hint>(≈ ${amountFiatFormatted} USD)</Hint> + To: + + + {from && !selectedWallet && + + + The app wants you to pay from address: + + However, you do not have that 1wallet address. Please go back to the app, and choose an 1wallet address that you own. If you do own that 1wallet address but it is not appearing in your wallets, you need restore the wallet first using "Restore" feature with your Google Authenticator. + + } + {from && selectedWallet && + + Paying from: + } + {!from && + <> + + Select a wallet you want to use: + + + + + } + {!showSend && + + + + } + + + {showSend && + } + + ) +} + +export default RequestPaymennt diff --git a/code/client/src/integration/WalletAuth.jsx b/code/client/src/integration/WalletAuth.jsx new file mode 100644 index 00000000..83e012ff --- /dev/null +++ b/code/client/src/integration/WalletAuth.jsx @@ -0,0 +1,32 @@ +import { Redirect, useLocation, useRouteMatch } from 'react-router' +import Paths from '../constants/paths' +import util from '../util' +import React from 'react' +import querystring from 'query-string' +import ConnectWallet from './Connect' +import RequestPayment from './RequestPayment' + +const WalletAuth = () => { + const location = useLocation() + const match = useRouteMatch(Paths.auth) + const { action, address: routeAddress } = match ? match.params : {} + const oneAddress = util.safeOneAddress(routeAddress) + const address = util.safeNormalizedAddress(routeAddress) + + const qs = querystring.parse(location.search) + const callback = qs.callback && Buffer.from(qs.callback, 'base64').toString() + const caller = qs.caller + const { amount, dest, from } = qs + + if (!action || !callback || !caller) { + return + } + return ( + <> + {action === 'connect' && } + {action === 'pay' && } + + ) +} + +export default WalletAuth diff --git a/code/client/src/pages/Show/Send.jsx b/code/client/src/pages/Show/Send.jsx index 25d28583..39991dca 100644 --- a/code/client/src/pages/Show/Send.jsx +++ b/code/client/src/pages/Show/Send.jsx @@ -23,7 +23,11 @@ const { Title } = Typography const Send = ({ address, show, - onClose, + onClose, // optional + onSuccess, // optional + overrideToken, // optional + prefillAmount, // string, number of tokens, in whole amount (not wei) + prefillDest, // string, hex format }) => { const dispatch = useDispatch() const wallets = useSelector(state => state.wallet.wallets) @@ -42,14 +46,14 @@ const Send = ({ const balances = useSelector(state => state.wallet.balances) const price = useSelector(state => state.wallet.price) const tokenBalances = wallet.tokenBalances || [] - const selectedToken = wallet?.selectedToken || HarmonyONE + const selectedToken = overrideToken || wallet?.selectedToken || HarmonyONE const selectedTokenBalance = selectedToken.key === 'one' ? (balances[address] || 0) : (tokenBalances[selectedToken.key] || 0) const selectedTokenDecimals = selectedToken.decimals const { formatted } = util.computeBalance(selectedTokenBalance, price, selectedTokenDecimals) - const [transferTo, setTransferTo] = useState({ value: '', label: '' }) - const [inputAmount, setInputAmount] = useState('') + const [transferTo, setTransferTo] = useState({ value: prefillDest || '', label: prefillDest ? util.oneAddress(prefillDest) : '' }) + const [inputAmount, setInputAmount] = useState(prefillAmount || '') const isNFT = util.isNFT(selectedToken) const { metadata } = selectedToken @@ -113,6 +117,7 @@ const Send = ({ onRevealAttemptFailed, onRevealSuccess: (txId) => { onRevealSuccess(txId) + onSuccess && onSuccess(txId) Chaining.refreshBalance(dispatch, intersection(Object.keys(wallets), [dest, address])) } }) @@ -136,6 +141,7 @@ const Send = ({ onRevealAttemptFailed, onRevealSuccess: (txId) => { onRevealSuccess(txId) + onSuccess && onSuccess(txId) Chaining.refreshTokenBalance({ dispatch, address, token: selectedToken }) } }) @@ -161,7 +167,7 @@ const Send = ({ - setInputAmount(value)} /> + setInputAmount(value)} disabled={!!prefillAmount} /> {!isNFT && {selectedToken.symbol}} diff --git a/code/client/src/routes.js b/code/client/src/routes.js index 2c2c1299..08a87d28 100644 --- a/code/client/src/routes.js +++ b/code/client/src/routes.js @@ -10,6 +10,7 @@ import CreatePage from './pages/Create' import ListPage from './pages/List' import RestorePage from './pages/Restore' import ShowPage from './pages/Show' +import WalletAuth from './integration/WalletAuth' import { walletActions } from './state/modules/wallet' import config from './config' import util, { useWindowDimensions } from './util' @@ -34,6 +35,7 @@ const LocalRoutes = () => { return }} /> + diff --git a/code/client/src/util.js b/code/client/src/util.js index e8a085dc..52868b45 100644 --- a/code/client/src/util.js +++ b/code/client/src/util.js @@ -7,6 +7,7 @@ import { AddressError } from './constants/errors' import config from './config' export default { + // TODO: rewrite using BN to achieve 100% precision formatNumber: (number, maxPrecision) => { maxPrecision = maxPrecision || 4 number = parseFloat(number) @@ -15,7 +16,9 @@ export default { } const order = Math.ceil(Math.log10(Math.max(number, 1))) const digits = Math.max(0, maxPrecision - order) - return number.toFixed(digits) + // https://www.jacklmoore.com/notes/rounding-in-javascript/ + const floored = Number(`${Math.floor(`${number}e+${digits}`)}e-${digits}`) + return floored.toString() }, ellipsisAddress: (address) => { diff --git a/code/lib/config/common.js b/code/lib/config/common.js index 50d14eb6..b47a7be9 100644 --- a/code/lib/config/common.js +++ b/code/lib/config/common.js @@ -2,8 +2,8 @@ const DEBUG = process.env.DEBUG module.exports = { appId: 'ONEWallet', - appName: 'ONE Wallet', - version: 'v0.9.1', + appName: '1wallet', + version: 'v0.9.2', minWalletVersion: parseInt(process.env.MIN_WALLET_VERSION || 3), minUpgradableVersion: parseInt(process.env.MIN_UPGRADABLE_WALLET_VERSION || 9), defaults: {