diff --git a/packages/boba/gateway/src/actions/devToolsAction.js b/packages/boba/gateway/src/actions/devToolsAction.js new file mode 100644 index 0000000000..ab0b064fd4 --- /dev/null +++ b/packages/boba/gateway/src/actions/devToolsAction.js @@ -0,0 +1,31 @@ +/* + Varna - A Privacy-Preserving Marketplace + Varna uses Fully Homomorphic Encryption to make markets fair. + Copyright (C) 2021 Enya Inc. Palo Alto, CA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import networkService from 'services/networkService'; +import { createAction } from './createAction' + +export function submitTxBuilder(contract, methodIndex, methodName, inputs) { + return createAction('TX_BUILDER', () => networkService.submitTxBuilder(contract, methodIndex, methodName, inputs)) +} + +export function resetTxBuilder() { + return function (dispatch) { + return dispatch({ type: 'TX_BUILDER/REST'}) + } +} diff --git a/packages/boba/gateway/src/components/input/Input.js b/packages/boba/gateway/src/components/input/Input.js index fab9113cfe..9631492983 100644 --- a/packages/boba/gateway/src/components/input/Input.js +++ b/packages/boba/gateway/src/components/input/Input.js @@ -22,7 +22,7 @@ import { selectCustomStyles } from './Select.styles' import Button from 'components/button/Button' -import { Box, Typography } from '@mui/material' +import { Box, Typography, TextareaAutosize } from '@mui/material' import { useTheme } from '@emotion/react' import { getCoinImage } from 'util/coinImage' @@ -53,7 +53,9 @@ function Input({ selectValue, style, isBridge, - openTokenPicker + openTokenPicker, + textarea = false, + maxRows = 10, }) { async function handlePaste() { @@ -89,6 +91,38 @@ function Input({ return acc }, []): null + if (textarea) { + return ( +
+ + + + {paste && ( + + PASTE + + )} + +
+ ) + } + return (
diff --git a/packages/boba/gateway/src/components/input/Input.styles.js b/packages/boba/gateway/src/components/input/Input.styles.js index da181c1c47..c306e91872 100644 --- a/packages/boba/gateway/src/components/input/Input.styles.js +++ b/packages/boba/gateway/src/components/input/Input.styles.js @@ -1,5 +1,5 @@ import { styled } from '@mui/material/styles' -import { Box, TextField } from '@mui/material' +import { Box, TextField, TextareaAutosize } from '@mui/material' export const Wrapper = styled(Box)` display: flex; @@ -63,3 +63,28 @@ export const ActionsWrapper = styled(Box)` flex: 3; margin-left: 10px; `; + +export const TextareaAutosizeWrapper = styled(TextareaAutosize)(({ theme }) => ({ + width: '100%', + backgroundColor: 'transparent', + font: 'inherit !important', + fontSize: '0.9em !important', + padding: '17.5px 15px', + borderRadius: '4px', + borderColor: theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255,255,255,0.23)', + color: theme.palette.mode === 'light' ? 'black' : 'white', + '&::placeholder': { + color: theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.35)' : 'rgba(255,255,255,0.45)', + }, + '&:hover': { + backgroundColor: theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.05)' : 'rgba(255,255,255,0.05)', + borderColor: theme.palette.mode === 'light' ? 'black' : 'white', + }, + '&:focus': { + padding: '16.5px 14px', + borderColor: '#478ddf', + borderWidth: '2px', + outline: '0px !important', + outlineOffset: '0px !important', + }, +})); diff --git a/packages/boba/gateway/src/components/pageFooter/PageFooter.js b/packages/boba/gateway/src/components/pageFooter/PageFooter.js index 06389c137d..be30fe76fc 100644 --- a/packages/boba/gateway/src/components/pageFooter/PageFooter.js +++ b/packages/boba/gateway/src/components/pageFooter/PageFooter.js @@ -66,6 +66,10 @@ const PageFooter = ({maintenance}) => { FAQs + Dev Tools BobaScope diff --git a/packages/boba/gateway/src/containers/devtools/DevTools.js b/packages/boba/gateway/src/containers/devtools/DevTools.js new file mode 100644 index 0000000000..6e34b5a56d --- /dev/null +++ b/packages/boba/gateway/src/containers/devtools/DevTools.js @@ -0,0 +1,32 @@ +import React from 'react' +import { useSelector } from 'react-redux' + +import PageTitle from 'components/pageTitle/PageTitle' +import Connect from 'containers/connect/Connect' + +import { selectLayer, selectAccountEnabled } from 'selectors/setupSelector' + +import TxBuilder from './TxBuilder' + +import * as S from './DevTools.styles' + +const DevTools = ({projectType}) => { + + const networkLayer = useSelector(selectLayer()) + const accountEnabled = useSelector(selectAccountEnabled()) + + return ( + + + + + + ) +} + +export default DevTools; diff --git a/packages/boba/gateway/src/containers/devtools/DevTools.styles.js b/packages/boba/gateway/src/containers/devtools/DevTools.styles.js new file mode 100644 index 0000000000..8fb3cdc0df --- /dev/null +++ b/packages/boba/gateway/src/containers/devtools/DevTools.styles.js @@ -0,0 +1,24 @@ +import { Box, Divider, Grid, IconButton, Typography } from '@mui/material'; +import { styled } from '@mui/material/styles'; + +export const PageContainer = styled(Box)(({ theme }) => ({ + margin: '0px auto', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-around', + padding: '10px', + paddingTop: '0px', + width: '70%', + [ theme.breakpoints.between('md', 'lg') ]: { + width: '90%', + padding: '0px', + }, + [ theme.breakpoints.between('sm', 'md') ]: { + width: '90%', + padding: '0px', + }, + [ theme.breakpoints.down('sm') ]: { + width: '100%', + padding: '0px', + }, +})); diff --git a/packages/boba/gateway/src/containers/devtools/TxBuilder.js b/packages/boba/gateway/src/containers/devtools/TxBuilder.js new file mode 100644 index 0000000000..af48ff2663 --- /dev/null +++ b/packages/boba/gateway/src/containers/devtools/TxBuilder.js @@ -0,0 +1,252 @@ +import React, { useEffect, useState } from 'react' +import { Box, Typography } from '@mui/material' +import { Contract, utils } from 'ethers' +import { useDispatch, useSelector, shallowEqual } from 'react-redux' + +import Input from 'components/input/Input' +import Button from 'components/button/Button' + +import { openError } from 'actions/uiAction' + +import { selectTxBuilder } from 'selectors/devToolsSelector' +import { selectNetwork, selectLayer } from 'selectors/setupSelector' + +import { submitTxBuilder, resetTxBuilder } from 'actions/devToolsAction' + +import { getNetwork } from 'util/masterConfig' + +import networkService from 'services/networkService' + +import * as S from './TxBuilder.styles' + +const TxBuilder = () => { + + const dispatch = useDispatch() + const TxBuilderResult = useSelector(selectTxBuilder, shallowEqual) + const networkLayer = useSelector(selectLayer()) + + const nw = getNetwork() + const masterConfig = useSelector(selectNetwork()) + const blockexploerUrl = nw[masterConfig].L2.blockExplorer + + const [ contractAddress, setContractAddress ] = useState('') + const [ contractABI, setContractABI ] = useState('') + const [ contractMethos, setContractMethods ] = useState([]) + const [ parseButtonDisabled, setParseButtonDisabled ] = useState(true) + const [ contractInputs, setContractInputs ] = useState({}) + const [ submitButtonDisabled, setSubmitButtonDisabled ] = useState(true) + + useEffect(() => { + if (contractAddress && contractABI) { + setParseButtonDisabled(false) + } else { + setParseButtonDisabled(true) + } + }, [contractAddress, contractABI]) + + useEffect(() => { + if (networkLayer === 'L2') { + setSubmitButtonDisabled(false) + } else { + setSubmitButtonDisabled(true) + } + }, [networkLayer]) + + const updateContractInput = (methodKey, inputKey, value) => { + const methods = contractInputs[methodKey] || {} + methods[inputKey] = value + setContractInputs(prevState => ({...prevState, [methodKey]: methods})) + } + + const getContractInput = (methodKey, inputKey) => { + const methods = contractInputs[methodKey] || {} + return methods[inputKey] || '' + } + + const parseContract = () => { + let contract + if (!utils.isAddress(contractAddress)) { + dispatch(openError('Invalid contract address')) + setContractAddress('') + return + } + try { + JSON.parse(contractABI) + contract = new Contract( + contractAddress, + contractABI, + networkService.L2Provider + ) + } catch { + dispatch(openError('Invalid contract ABI')) + setContractABI('') + return + } + setContractMethods([]) + for (const [key, value] of Object.entries(contract.interface.functions)) { + if (value.type === 'function') { + setContractMethods(prevState => [...prevState, {key, value}]) + } + } + dispatch(resetTxBuilder()) + } + + const submitTx = async (methodIndex) => { + const method = contractMethos[methodIndex] + const methodName = method.key + const methodInputs = method.value.inputs + const stateMutability = method.value.stateMutability + const inputs = contractInputs[methodIndex] || [] + + for (let i = 0; i < methodInputs.length; i++) { + if (typeof inputs[i] === undefined) { + dispatch(openError('Please fill all inputs')) + return + } + } + if (stateMutability === 'payable') { + if (typeof inputs[methodInputs.length] === undefined) { + dispatch(openError('Please fill all inputs')) + return + } + } + + // submit tx + let provider = networkService.L2Provider + if (networkLayer === 'L2') { + provider = networkService.provider.getSigner() + } + const contract = new Contract( + contractAddress, + contractABI, + provider + ) + dispatch(submitTxBuilder(contract, methodIndex, methodName, inputs)) + } + + const openInNewTab = url => { + window.open(url, '_blank', 'noopener,noreferrer'); + }; + + return ( + + + Tx Builder + This is a interface for a contract on L2. Use at your own risk! + + setContractAddress(i.target.value)} + fullWidth + paste + sx={{fontSize: '50px'}} + newStyle + /> +
+ setContractABI(i.target.value)} + fullWidth + paste + sx={{fontSize: '50px'}} + newStyle + textarea={true} + /> +
+ + + + {contractMethos.length > 0 && ( + + Methods + {contractMethos.map((method, methodIndex) => { + const functionName = method.key + const stateMutability = method.value.stateMutability + const inputs = method.value.inputs + const inputStyle = {borderWidth: 0, borderRadius: 0, padding: '5px 0px', backgroundColor: 'transparent'} + const TxResult = TxBuilderResult[methodIndex] || {} + return ( + + + {`${functionName} ${stateMutability}`} + {inputs.length > 0 && inputs.map((input, inputIndex) => { + return ( + updateContractInput(methodIndex, inputIndex, i.target.value)} + fullWidth + sx={{fontSize: '50px'}} + newStyle + key={inputIndex} + style={inputStyle} + /> + ) + })} + {stateMutability === 'payable' && ( + updateContractInput(methodIndex, inputs.length, i.target.value)} + fullWidth + sx={{fontSize: '50px'}} + newStyle + style={inputStyle} + /> + )} + {(typeof TxResult.err !== 'undefined' || typeof TxResult.result !== 'undefined' || typeof TxResult.result !== 'undefined') && ( + + {TxResult.err && {TxResult.err}} + {TxResult.result && {TxResult.result}} + {TxResult.transactionHash && + + Succeeded! + + + } + + )} + + + + + + ) + })} + + )} +
+
+ ) +} + +export default TxBuilder; diff --git a/packages/boba/gateway/src/containers/devtools/TxBuilder.styles.js b/packages/boba/gateway/src/containers/devtools/TxBuilder.styles.js new file mode 100644 index 0000000000..090e003bdd --- /dev/null +++ b/packages/boba/gateway/src/containers/devtools/TxBuilder.styles.js @@ -0,0 +1,52 @@ +import { Box } from '@mui/material'; +import { styled } from '@mui/material/styles'; + +export const TxBuilderWrapper = styled(Box)(({ theme }) => ({ + background: theme.palette.background.secondary, + backdropFilter: 'blur(100px)', + borderRadius: theme.palette.primary.borderRadius, + border: theme.palette.primary.border, + flex: 1, + minHeight: 'fit-content', + padding: '20px', + width: '100%', +})) + +export const Wrapper = styled(Box)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + marginTop: 20, + marginBottom: 20, + width: '100%', +})) + +export const ButtonWrapper = styled(Box)(({ theme }) => ({ + display: 'flex', + justifyContent: 'flex-end', +})) + +export const MethodsWrapper = styled(Box)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + marginBottom: 20, + width: '100%', +})) + +export const InputWrapper = styled(Box)(({ theme }) => ({ + marginTop: 20, + marginBottom: 20, + padding: 20, + borderRadius: theme.palette.primary.borderRadius, + border: theme.palette.primary.border, + backgroundColor: theme.palette.background.input, +})) + +export const TxResultWrapper = styled(Box)(({ theme }) => ({ + marginTop: 20, + marginBottom: 10, +})) + +export const TxSuccessWrapper = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', +})) diff --git a/packages/boba/gateway/src/layout/index.js b/packages/boba/gateway/src/layout/index.js index 28084db986..1741ceab67 100644 --- a/packages/boba/gateway/src/layout/index.js +++ b/packages/boba/gateway/src/layout/index.js @@ -42,6 +42,7 @@ import Projects from 'containers/ecosystem/Projects' import { DISABLE_VE_DAO, ROUTES_PATH } from 'util/constant' import VoteAndDao from 'containers/VoteAndDao' import OldDao from 'containers/dao/OldDao' +import DevTools from 'containers/devtools/DevTools' function App() { @@ -306,6 +307,7 @@ function App() { } > } /> + } /> {/* FIXME: On setting flag below to 1 below routes will not be available to user. */} {!Number(DISABLE_VE_DAO) && } />} {!Number(DISABLE_VE_DAO) && } />} diff --git a/packages/boba/gateway/src/reducers/devToolsReducer.js b/packages/boba/gateway/src/reducers/devToolsReducer.js new file mode 100644 index 0000000000..746e812fd0 --- /dev/null +++ b/packages/boba/gateway/src/reducers/devToolsReducer.js @@ -0,0 +1,48 @@ +/* +Copyright 2021-present Boba Network. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +const initialState = { + TxBuilder: {} +} + +function devToolsReducer (state = initialState, action) { + switch (action.type) { + case 'TX_BUILDER/SUCCESS': + return { + ...state, + TxBuilder: { + ...state.TxBuilder, + [action.payload.methodIndex]: action.payload.result + } + } + case 'TX_BUILDER/ERROR': + return { + ...state, + TxBuilder: { + ...state.TxBuilder, + [action.payload.methodIndex]: action.payload.result + } + } + case 'TX_BUILDER/REST': + return { + ...state, + TxBuilder: {} + } + default: + return state; + } +} + +export default devToolsReducer diff --git a/packages/boba/gateway/src/reducers/index.js b/packages/boba/gateway/src/reducers/index.js index cf329a71fb..31ec2fed46 100644 --- a/packages/boba/gateway/src/reducers/index.js +++ b/packages/boba/gateway/src/reducers/index.js @@ -37,6 +37,7 @@ import fixedReducer from './fixedReducer' import verifierReducer from './verifierReducer'; import bridgeReducer from './bridgeReducer'; import veBobaReducer from './veBobaReducer'; +import devToolsReducer from './devToolsReducer'; const rootReducer = combineReducers({ loading: loadingReducer, @@ -61,6 +62,7 @@ const rootReducer = combineReducers({ verifier: verifierReducer, bridge: bridgeReducer, veboba: veBobaReducer, + devTools: devToolsReducer, }) export default rootReducer diff --git a/packages/boba/gateway/src/selectors/devToolsSelector.js b/packages/boba/gateway/src/selectors/devToolsSelector.js new file mode 100644 index 0000000000..6a9cb99b8f --- /dev/null +++ b/packages/boba/gateway/src/selectors/devToolsSelector.js @@ -0,0 +1,19 @@ +/* +Copyright 2021-present Boba Network. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +export function selectTxBuilder (state) { + return state.devTools.TxBuilder +} + diff --git a/packages/boba/gateway/src/services/networkService.js b/packages/boba/gateway/src/services/networkService.js index 6f8630d986..5ed347797e 100644 --- a/packages/boba/gateway/src/services/networkService.js +++ b/packages/boba/gateway/src/services/networkService.js @@ -5191,8 +5191,55 @@ class NetworkService { ***********OLD DAO REMOVE ME TILL HERE * *****************************************/ + async submitTxBuilder(contract, methodIndex, methodName, inputs) { + const parseResult = (result, outputs) => { + let parseResult = [] + if (outputs.length === 1) { + return result.toString() + } + for (let i = 0; i < outputs.length; i++) { + try { + const output = outputs[i] + const key = output.name ? output.name : output.type + if (output.type.includes('uint')) { + parseResult.push({[key]: result[i].toString()}) + } else { + parseResult.push({[key]:result[i]}) + } + } catch (err) { + return 'Error: Failed to parse result' + } + } + return JSON.stringify(parseResult) + } + let parseInput = Object.values(inputs) + let value = 0 + const stateMutability = contract.interface.functions[methodName].stateMutability + const outputs = contract.interface.functions[methodName].outputs + if (stateMutability === 'payable') { + value = parseInput[parseInput.length - 1] + parseInput = parseInput.slice(0, parseInput.length - 1) + } + + let result + try { + if (stateMutability === 'view' || stateMutability === 'pure') { + result = await contract[methodName](...parseInput) + return { methodIndex, result: { result: parseResult(result, outputs), err: null }} + } else if (stateMutability === 'payable') { + console.log({ value }, ...parseInput) + const tx = await contract[methodName](...parseInput, { value }) + return { methodIndex, result: { transactionHash: tx.hash, err: null }} + } else { + const tx = await contract[methodName](...parseInput) + return { methodIndex, result: { transactionHash: tx.hash, err: null }} + } + } catch (err) { + return { methodIndex, result: { err: JSON.stringify(err) }} + } + } } const networkService = new NetworkService() diff --git a/packages/boba/gateway/src/util/constant.js b/packages/boba/gateway/src/util/constant.js index c2e04835c3..e4c2e81850 100644 --- a/packages/boba/gateway/src/util/constant.js +++ b/packages/boba/gateway/src/util/constant.js @@ -71,5 +71,6 @@ export const ROUTES_PATH = { MONSTER: '/monster', VOTE_DAO: '/votedao', DAO: '/DAO', + DEV_TOOLS: '/devtools', } export const PER_PAGE = 8