fix(@cockpit/deployment): Check if contracts deployed when connected to metamask

When connected to metamask, and “Injected Web3” is selected on the Deployment page of Cockpit, check to see if contracts have already been deployed or not.

For contracts that have not been deployed, or set to an address in the config, these should now all be re-deployable.

Supports libraries (that have bytecode with `0x73<address><bytecode>`).
This commit is contained in:
emizzle 2019-03-27 14:42:26 +11:00 committed by Iuri Matias
parent 4c3ec26fff
commit c23316351e
7 changed files with 110 additions and 20 deletions

View File

@ -360,6 +360,13 @@ export const web3EstimateGas = {
failure: (error, payload) => action(WEB3_ESTIMAGE_GAS[FAILURE], {web3Error: error, contract: payload.contract}) failure: (error, payload) => action(WEB3_ESTIMAGE_GAS[FAILURE], {web3Error: error, contract: payload.contract})
}; };
export const WEB3_IS_DEPLOYED = createRequestTypes('WEB3_IS_DEPLOYED');
export const web3IsDeployed = {
request: (contract, args) => action(WEB3_IS_DEPLOYED[REQUEST], {contract, args}),
success: (isDeployed, payload) => action(WEB3_IS_DEPLOYED[SUCCESS], {contract: payload.contract, isDeployed}),
failure: (error, payload) => action(WEB3_IS_DEPLOYED[FAILURE], {web3Error: error, contract: payload.contract})
};
export const START_DEBUG = createRequestTypes('START_DEBUG'); export const START_DEBUG = createRequestTypes('START_DEBUG');
export const startDebug = { export const startDebug = {
request: (txHash) => action(START_DEBUG[REQUEST], {txHash}), request: (txHash) => action(START_DEBUG[REQUEST], {txHash}),

View File

@ -18,6 +18,7 @@ import classNames from 'classnames';
import {DEPLOYMENT_PIPELINES} from '../constants'; import {DEPLOYMENT_PIPELINES} from '../constants';
import Description from './Description'; import Description from './Description';
import ContractOverviewContainer from '../containers/ContractOverviewContainer'; import ContractOverviewContainer from '../containers/ContractOverviewContainer';
import FontAwesome from 'react-fontawesome';
const orderClassName = (address) => { const orderClassName = (address) => {
return classNames('mr-4', { return classNames('mr-4', {
@ -38,12 +39,14 @@ const NoWeb3 = () => (
</Row> </Row>
); );
const LayoutContract = ({contract, children, cardTitle}) => ( const LayoutContract = ({contract, children, title = contract.className, isLoading = false, isDeployed = false, deployedTitleSuffix, notDeployedTitleSuffix}) => (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
<span className={orderClassName(contract.address)}>{contract.deployIndex + 1}</span> <span className={orderClassName(contract.address)}>{contract.deployIndex + 1}</span>
{cardTitle} {title}&nbsp;
{isLoading && <FontAwesome name="spinner" spin />}
{!isLoading && <span>{(isDeployed && deployedTitleSuffix) || notDeployedTitleSuffix}</span>}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
@ -55,7 +58,11 @@ const LayoutContract = ({contract, children, cardTitle}) => (
LayoutContract.propTypes = { LayoutContract.propTypes = {
contract: PropTypes.object, contract: PropTypes.object,
children: PropTypes.any, children: PropTypes.any,
cardTitle: PropTypes.any isLoading: PropTypes.bool,
isDeployed: PropTypes.bool,
title: PropTypes.any,
deployedTitleSuffix: PropTypes.string,
notDeployedTitleSuffix: PropTypes.string
}; };
const DeploymentResult = ({deployment}) => { const DeploymentResult = ({deployment}) => {
@ -127,13 +134,29 @@ class Web3Contract extends React.Component {
return this.inputsAsArray().length !== constructor.inputs.length; return this.inputsAsArray().length !== constructor.inputs.length;
} }
isDeployed() {
const contractInStore = this.props.web3ContractsDeployed[this.props.contract.className];
return contractInStore && contractInStore.isDeployed;
}
isCheckingIfDeployed() {
const contractInStore = this.props.web3ContractsDeployed[this.props.contract.className];
return contractInStore && contractInStore.running;
}
render() { render() {
const abiConstructor = findConstructor(this.props.contract.abiDefinition); const abiConstructor = findConstructor(this.props.contract.abiDefinition);
const argumentsRequired = abiConstructor && abiConstructor.inputs.length > 0; const argumentsRequired = abiConstructor && abiConstructor.inputs.length > 0;
const isDeployed = this.isDeployed();
const isCheckingIfDeployed = this.isCheckingIfDeployed();
return ( return (
<LayoutContract contract={this.props.contract} cardTitle={this.props.contract.className}> <LayoutContract contract={this.props.contract}
isLoading={isCheckingIfDeployed}
isDeployed={isDeployed}
deployedTitleSuffix={`deployed at ${this.props.contract.deployedAddress}`}
notDeployedTitleSuffix={`not deployed`}>
<Row> <Row>
<Col md={6}> <Col md={6}>
{isDeployed && <p><strong>Contract deployed</strong></p>}
{argumentsRequired && {argumentsRequired &&
<React.Fragment> <React.Fragment>
<h5>Arguments:</h5> <h5>Arguments:</h5>
@ -178,16 +201,16 @@ Web3Contract.propTypes = {
web3EstimateGas: PropTypes.func, web3EstimateGas: PropTypes.func,
web3Deploy: PropTypes.func, web3Deploy: PropTypes.func,
gasEstimate: PropTypes.object, gasEstimate: PropTypes.object,
deployment: PropTypes.object deployment: PropTypes.object,
web3ContractsDeployed: PropTypes.object
}; };
const EmbarkContract = ({contract, toggleContractOverview}) => ( const EmbarkContract = ({contract, toggleContractOverview}) => (
<LayoutContract contract={contract} cardTitle={ <LayoutContract contract={contract}
<React.Fragment> isDeployed={!!contract.address}
<a href='#toggleContract' onClick={() => toggleContractOverview(contract)}>{contract.className}</a>&nbsp; deployedTitleSuffix={`deployed at ${contract.address}`}
<span>{(contract.address && `deployed at ${contract.address}`) || 'not deployed'}</span> notDeployedTitleSuffix={'not deployed'}
</React.Fragment> title={<a href='#toggleContract' onClick={() => toggleContractOverview(contract)}>{contract.className}</a>}>
}>
{contract.address && <p><strong>Arguments:</strong> {JSON.stringify(contract.args)}</p>} {contract.address && <p><strong>Arguments:</strong> {JSON.stringify(contract.args)}</p>}
{contract.transactionHash && {contract.transactionHash &&
<React.Fragment> <React.Fragment>
@ -251,7 +274,7 @@ ContractsHeader.propTypes = {
updateDeploymentPipeline: PropTypes.func updateDeploymentPipeline: PropTypes.func
}; };
const Contract = ({web3, contract, deploymentPipeline, web3Deploy, web3EstimateGas, web3Deployments, web3GasEstimates, toggleContractOverview}) => { const Contract = ({web3, contract, deploymentPipeline, web3Deploy, web3EstimateGas, web3Deployments, web3GasEstimates, web3ContractsDeployed, toggleContractOverview}) => {
const deployment = web3Deployments[contract.className]; const deployment = web3Deployments[contract.className];
const gasEstimate = web3GasEstimates[contract.className]; const gasEstimate = web3GasEstimates[contract.className];
switch (deploymentPipeline) { switch (deploymentPipeline) {
@ -263,7 +286,8 @@ const Contract = ({web3, contract, deploymentPipeline, web3Deploy, web3EstimateG
gasEstimate={gasEstimate} gasEstimate={gasEstimate}
contract={contract} contract={contract}
web3Deploy={web3Deploy} web3Deploy={web3Deploy}
web3EstimateGas={web3EstimateGas}/>; web3EstimateGas={web3EstimateGas}
web3ContractsDeployed={web3ContractsDeployed}/>;
default: default:
return <React.Fragment/>; return <React.Fragment/>;
} }
@ -280,7 +304,8 @@ Contract.propTypes = {
web3Deploy: PropTypes.func, web3Deploy: PropTypes.func,
web3Deployments: PropTypes.object, web3Deployments: PropTypes.object,
web3EstimateGas: PropTypes.func, web3EstimateGas: PropTypes.func,
web3GasEstimates: PropTypes.object web3GasEstimates: PropTypes.object,
web3ContractsDeployed: PropTypes.object
}; };
class ContractsDeployment extends React.Component { class ContractsDeployment extends React.Component {
@ -343,7 +368,8 @@ ContractsDeployment.propTypes = {
web3GasEstimates: PropTypes.object, web3GasEstimates: PropTypes.object,
web3: PropTypes.object, web3: PropTypes.object,
web3Deploy: PropTypes.func, web3Deploy: PropTypes.func,
web3EstimateGas: PropTypes.func web3EstimateGas: PropTypes.func,
web3ContractsDeployed: PropTypes.object
}; };
export default ContractsDeployment; export default ContractsDeployment;

View File

@ -10,7 +10,14 @@ import {
import ContractsDeployment from '../components/ContractsDeployment'; import ContractsDeployment from '../components/ContractsDeployment';
import DataWrapper from "../components/DataWrapper"; import DataWrapper from "../components/DataWrapper";
import PageHead from '../components/PageHead'; import PageHead from '../components/PageHead';
import {getContracts, getDeploymentPipeline, getWeb3, getWeb3GasEstimates, getWeb3Deployments} from "../reducers/selectors"; import {
getContracts,
getDeploymentPipeline,
getWeb3,
getWeb3GasEstimates,
getWeb3Deployments,
getWeb3ContractsDeployed
} from "../reducers/selectors";
class DeploymentContainer extends Component { class DeploymentContainer extends Component {
componentDidMount() { componentDidMount() {
@ -29,6 +36,7 @@ class DeploymentContainer extends Component {
web3EstimateGas={this.props.web3EstimateGas} web3EstimateGas={this.props.web3EstimateGas}
web3Deployments={this.props.web3Deployments} web3Deployments={this.props.web3Deployments}
web3GasEstimates={this.props.web3GasEstimates} web3GasEstimates={this.props.web3GasEstimates}
web3ContractsDeployed={this.props.web3ContractsDeployed}
updateDeploymentPipeline={this.props.updateDeploymentPipeline} /> updateDeploymentPipeline={this.props.updateDeploymentPipeline} />
)} /> )} />
</React.Fragment> </React.Fragment>
@ -43,6 +51,7 @@ function mapStateToProps(state) {
web3: getWeb3(state), web3: getWeb3(state),
web3Deployments: getWeb3Deployments(state), web3Deployments: getWeb3Deployments(state),
web3GasEstimates: getWeb3GasEstimates(state), web3GasEstimates: getWeb3GasEstimates(state),
web3ContractsDeployed: getWeb3ContractsDeployed(state),
error: state.errorMessage, error: state.errorMessage,
loading: state.loading loading: state.loading
}; };
@ -60,7 +69,7 @@ DeploymentContainer.propTypes = {
web3Deploy: PropTypes.func, web3Deploy: PropTypes.func,
web3Deployments: PropTypes.object, web3Deployments: PropTypes.object,
web3EstimateGas: PropTypes.func, web3EstimateGas: PropTypes.func,
web3GasEstimates: PropTypes.object, web3GasEstimates: PropTypes.object
}; };
export default connect( export default connect(

View File

@ -3,7 +3,7 @@ import {REQUEST, SUCCESS, FAILURE, CONTRACT_COMPILE, FILES, LOGOUT, AUTHENTICATE
FETCH_CREDENTIALS, UPDATE_BASE_ETHER, CHANGE_THEME, FETCH_THEME, EXPLORER_SEARCH, DEBUGGER_INFO, FETCH_CREDENTIALS, UPDATE_BASE_ETHER, CHANGE_THEME, FETCH_THEME, EXPLORER_SEARCH, DEBUGGER_INFO,
SIGN_MESSAGE, VERIFY_MESSAGE, TOGGLE_BREAKPOINT, UPDATE_PREVIEW_URL, SIGN_MESSAGE, VERIFY_MESSAGE, TOGGLE_BREAKPOINT, UPDATE_PREVIEW_URL,
UPDATE_DEPLOYMENT_PIPELINE, WEB3_CONNECT, WEB3_DEPLOY, WEB3_ESTIMAGE_GAS, FETCH_EDITOR_TABS, UPDATE_DEPLOYMENT_PIPELINE, WEB3_CONNECT, WEB3_DEPLOY, WEB3_ESTIMAGE_GAS, FETCH_EDITOR_TABS,
SAVE_FILE, SAVE_FOLDER, REMOVE_FILE, DECODED_TRANSACTION} from "../actions"; SAVE_FILE, SAVE_FOLDER, REMOVE_FILE, DECODED_TRANSACTION, WEB3_IS_DEPLOYED} from "../actions";
import {EMBARK_PROCESS_NAME, DARK_THEME, DEPLOYMENT_PIPELINES, DEFAULT_HOST, ELEMENTS_LIMIT} from '../constants'; import {EMBARK_PROCESS_NAME, DARK_THEME, DEPLOYMENT_PIPELINES, DEFAULT_HOST, ELEMENTS_LIMIT} from '../constants';
const BN_FACTOR = 10000; const BN_FACTOR = 10000;
@ -353,7 +353,7 @@ function breakpoints(state = {}, action) {
return state; return state;
} }
function web3(state = {deployments: {}, gasEstimates: {}}, action) { function web3(state = {deployments: {}, gasEstimates: {}, contractsDeployed: {}}, action) {
if (action.type === WEB3_CONNECT[SUCCESS]) { if (action.type === WEB3_CONNECT[SUCCESS]) {
return {...state, instance: action.web3}; return {...state, instance: action.web3};
} else if (action.type === WEB3_DEPLOY[REQUEST]) { } else if (action.type === WEB3_DEPLOY[REQUEST]) {
@ -368,6 +368,12 @@ function web3(state = {deployments: {}, gasEstimates: {}}, action) {
return {...state, gasEstimates: {...state['gasEstimates'], [action.contract.className]: {gas: action.gas, running: false, error: null}}}; return {...state, gasEstimates: {...state['gasEstimates'], [action.contract.className]: {gas: action.gas, running: false, error: null}}};
} else if (action.type === WEB3_ESTIMAGE_GAS[FAILURE]){ } else if (action.type === WEB3_ESTIMAGE_GAS[FAILURE]){
return {...state, gasEstimates: {...state['gasEstimates'], [action.contract.className]: {error: action.web3Error, running: false}}}; return {...state, gasEstimates: {...state['gasEstimates'], [action.contract.className]: {error: action.web3Error, running: false}}};
} else if (action.type === WEB3_IS_DEPLOYED[REQUEST]) {
return {...state, contractsDeployed: {...state['contractsDeployed'], [action.contract.className]: {running: true, error: null}}};
} else if (action.type === WEB3_IS_DEPLOYED[SUCCESS]){
return {...state, contractsDeployed: {...state['contractsDeployed'], [action.contract.className]: {isDeployed: action.isDeployed, running: false, error: null}}};
} else if (action.type === WEB3_IS_DEPLOYED[FAILURE]){
return {...state, contractsDeployed: {...state['contractsDeployed'], [action.contract.className]: {error: action.web3Error, running: false}}};
} }
return state return state

View File

@ -237,6 +237,10 @@ export function getWeb3Deployments(state) {
return state.web3.deployments; return state.web3.deployments;
} }
export function getWeb3ContractsDeployed(state) {
return state.web3.contractsDeployed;
}
export function getDebuggerInfo(state) { export function getDebuggerInfo(state) {
return state.debuggerInfo; return state.debuggerInfo;
} }

View File

@ -4,7 +4,7 @@ import * as storage from '../services/storage';
import * as web3Service from '../services/web3'; import * as web3Service from '../services/web3';
import {eventChannel} from 'redux-saga'; import {eventChannel} from 'redux-saga';
import {all, call, fork, put, takeLatest, takeEvery, take, select, race} from 'redux-saga/effects'; import {all, call, fork, put, takeLatest, takeEvery, take, select, race} from 'redux-saga/effects';
import {getCredentials, getWeb3} from '../reducers/selectors'; import {getCredentials, getWeb3, getContracts} from '../reducers/selectors';
import { DEPLOYMENT_PIPELINES } from '../constants'; import { DEPLOYMENT_PIPELINES } from '../constants';
import {searchExplorer} from './searchSaga'; import {searchExplorer} from './searchSaga';
@ -20,6 +20,9 @@ function *doRequest(entity, serviceFn, payload) {
function *doWeb3Request(entity, serviceFn, payload) { function *doWeb3Request(entity, serviceFn, payload) {
payload.web3 = yield select(getWeb3); payload.web3 = yield select(getWeb3);
if(payload.type === actions.WEB3_DEPLOY[actions.SUCCESS]) {
payload.contract.deployedAddress = payload.receipt.contractAddress;
}
try { try {
const result = yield call(serviceFn, payload); const result = yield call(serviceFn, payload);
yield put(entity.success(result, payload)); yield put(entity.success(result, payload));
@ -40,6 +43,14 @@ function *web3Connect(action) {
} }
} }
function *web3ContractsDeployed(action) {
const contracts = yield select(getContracts);
let i = 0;
while(i < contracts.length) {
yield put(actions.web3IsDeployed.request(contracts[i++]));
}
}
export const fetchPlugins = doRequest.bind(null, actions.plugins, api.fetchPlugins); export const fetchPlugins = doRequest.bind(null, actions.plugins, api.fetchPlugins);
export const fetchVersions = doRequest.bind(null, actions.versions, api.fetchVersions); export const fetchVersions = doRequest.bind(null, actions.versions, api.fetchVersions);
export const fetchAccount = doRequest.bind(null, actions.account, api.fetchAccount); export const fetchAccount = doRequest.bind(null, actions.account, api.fetchAccount);
@ -99,6 +110,7 @@ export const explorerSearch = searchExplorer.bind(null, actions.explorerSearch);
export const web3Deploy = doWeb3Request.bind(null, actions.web3Deploy, web3Service.deploy); export const web3Deploy = doWeb3Request.bind(null, actions.web3Deploy, web3Service.deploy);
export const web3EstimateGas = doWeb3Request.bind(null, actions.web3EstimateGas, web3Service.estimateGas); export const web3EstimateGas = doWeb3Request.bind(null, actions.web3EstimateGas, web3Service.estimateGas);
export const web3IsDeployed = doWeb3Request.bind(null, actions.web3IsDeployed, web3Service.isDeployed);
export function *watchFetchTransaction() { export function *watchFetchTransaction() {
@ -325,16 +337,25 @@ export function *watchVerifyMessage() {
export function *watchWeb3Deploy() { export function *watchWeb3Deploy() {
yield takeEvery(actions.WEB3_DEPLOY[actions.REQUEST], web3Deploy); yield takeEvery(actions.WEB3_DEPLOY[actions.REQUEST], web3Deploy);
yield takeEvery(actions.WEB3_DEPLOY[actions.SUCCESS], web3IsDeployed);
} }
export function *watchWeb3EstimateGas() { export function *watchWeb3EstimateGas() {
yield takeEvery(actions.WEB3_ESTIMAGE_GAS[actions.REQUEST], web3EstimateGas); yield takeEvery(actions.WEB3_ESTIMAGE_GAS[actions.REQUEST], web3EstimateGas);
} }
export function *watchWeb3IsDeployed() {
yield takeEvery(actions.WEB3_IS_DEPLOYED[actions.REQUEST], web3IsDeployed);
}
export function *watchUpdateDeploymentPipeline() { export function *watchUpdateDeploymentPipeline() {
yield takeEvery(actions.UPDATE_DEPLOYMENT_PIPELINE, web3Connect); yield takeEvery(actions.UPDATE_DEPLOYMENT_PIPELINE, web3Connect);
} }
export function *watchWeb3Connect() {
yield takeEvery(actions.WEB3_CONNECT[actions.SUCCESS], web3ContractsDeployed);
}
export function *watchFetchEditorTabs() { export function *watchFetchEditorTabs() {
yield takeEvery(actions.FETCH_EDITOR_TABS[actions.REQUEST], fetchEditorTabs); yield takeEvery(actions.FETCH_EDITOR_TABS[actions.REQUEST], fetchEditorTabs);
} }
@ -615,7 +636,9 @@ export default function *root() {
fork(watchVerifyMessage), fork(watchVerifyMessage),
fork(watchWeb3EstimateGas), fork(watchWeb3EstimateGas),
fork(watchWeb3Deploy), fork(watchWeb3Deploy),
fork(watchWeb3IsDeployed),
fork(watchUpdateDeploymentPipeline), fork(watchUpdateDeploymentPipeline),
fork(watchWeb3Connect),
fork(watchListenDebugger), fork(watchListenDebugger),
fork(watchFetchEditorTabs), fork(watchFetchEditorTabs),
fork(watchAddEditorTabs), fork(watchAddEditorTabs),

View File

@ -30,3 +30,18 @@ export function deploy({web3, contract, args}) {
.then(() => {}); .then(() => {});
}); });
} }
export function isDeployed({web3, contract}) {
if(!contract.deployedAddress) return Promise.resolve(false);
return new Promise((resolve, reject) => {
web3.eth.getCode(contract.deployedAddress, (err, byteCode) => {
if(err) return reject(err);
const deployedAddress = contract.deployedAddress.replace("0x", "").toLowerCase();
resolve(
byteCode === `0x${contract.runtimeBytecode}` || // case when contract has been deployed or redeployed
byteCode === `0x73${deployedAddress}${contract.runtimeBytecode.replace("730000000000000000000000000000000000000000", "")}` || // case when library deployed already
byteCode === `0x${contract.runtimeBytecode.replace("0000000000000000000000000000000000000000", deployedAddress)}` // case when library has been redeployed
);
});
});
}