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}
+ />
+
+
+ parseContract()}
+ color='primary'
+ variant="outlined"
+ disabled={parseButtonDisabled}
+ tooltip="Click the button to parse your ABI"
+ >
+ Parse
+
+
+ {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!
+ openInNewTab(`${blockexploerUrl}tx/${TxResult.transactionHash}`)}
+ color='primary'
+ variant="outlined"
+ >
+ View Transaction
+
+
+ }
+
+ )}
+
+
+ submitTx(methodIndex)}
+ color='primary'
+ variant="outlined"
+ disabled={stateMutability === 'view' ? false: submitButtonDisabled}
+ tooltip={
+ stateMutability === 'view' ? 'Click the button to view it':
+ submitButtonDisabled ? 'Please connect to Boba Network': 'Click the button to submit your transaction'
+ }
+ >
+ {stateMutability === 'view' ? 'View' : 'Write'}
+
+
+
+ )
+ })}
+
+ )}
+
+
+ )
+}
+
+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