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})
};
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 startDebug = {
request: (txHash) => action(START_DEBUG[REQUEST], {txHash}),

View File

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

View File

@ -10,7 +10,14 @@ import {
import ContractsDeployment from '../components/ContractsDeployment';
import DataWrapper from "../components/DataWrapper";
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 {
componentDidMount() {
@ -29,6 +36,7 @@ class DeploymentContainer extends Component {
web3EstimateGas={this.props.web3EstimateGas}
web3Deployments={this.props.web3Deployments}
web3GasEstimates={this.props.web3GasEstimates}
web3ContractsDeployed={this.props.web3ContractsDeployed}
updateDeploymentPipeline={this.props.updateDeploymentPipeline} />
)} />
</React.Fragment>
@ -43,6 +51,7 @@ function mapStateToProps(state) {
web3: getWeb3(state),
web3Deployments: getWeb3Deployments(state),
web3GasEstimates: getWeb3GasEstimates(state),
web3ContractsDeployed: getWeb3ContractsDeployed(state),
error: state.errorMessage,
loading: state.loading
};
@ -60,7 +69,7 @@ DeploymentContainer.propTypes = {
web3Deploy: PropTypes.func,
web3Deployments: PropTypes.object,
web3EstimateGas: PropTypes.func,
web3GasEstimates: PropTypes.object,
web3GasEstimates: PropTypes.object
};
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,
SIGN_MESSAGE, VERIFY_MESSAGE, TOGGLE_BREAKPOINT, UPDATE_PREVIEW_URL,
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';
const BN_FACTOR = 10000;
@ -353,7 +353,7 @@ function breakpoints(state = {}, action) {
return state;
}
function web3(state = {deployments: {}, gasEstimates: {}}, action) {
function web3(state = {deployments: {}, gasEstimates: {}, contractsDeployed: {}}, action) {
if (action.type === WEB3_CONNECT[SUCCESS]) {
return {...state, instance: action.web3};
} 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}}};
} else if (action.type === WEB3_ESTIMAGE_GAS[FAILURE]){
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

View File

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

View File

@ -4,7 +4,7 @@ import * as storage from '../services/storage';
import * as web3Service from '../services/web3';
import {eventChannel} from 'redux-saga';
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 {searchExplorer} from './searchSaga';
@ -20,6 +20,9 @@ function *doRequest(entity, serviceFn, payload) {
function *doWeb3Request(entity, serviceFn, payload) {
payload.web3 = yield select(getWeb3);
if(payload.type === actions.WEB3_DEPLOY[actions.SUCCESS]) {
payload.contract.deployedAddress = payload.receipt.contractAddress;
}
try {
const result = yield call(serviceFn, 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 fetchVersions = doRequest.bind(null, actions.versions, api.fetchVersions);
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 web3EstimateGas = doWeb3Request.bind(null, actions.web3EstimateGas, web3Service.estimateGas);
export const web3IsDeployed = doWeb3Request.bind(null, actions.web3IsDeployed, web3Service.isDeployed);
export function *watchFetchTransaction() {
@ -325,16 +337,25 @@ export function *watchVerifyMessage() {
export function *watchWeb3Deploy() {
yield takeEvery(actions.WEB3_DEPLOY[actions.REQUEST], web3Deploy);
yield takeEvery(actions.WEB3_DEPLOY[actions.SUCCESS], web3IsDeployed);
}
export function *watchWeb3EstimateGas() {
yield takeEvery(actions.WEB3_ESTIMAGE_GAS[actions.REQUEST], web3EstimateGas);
}
export function *watchWeb3IsDeployed() {
yield takeEvery(actions.WEB3_IS_DEPLOYED[actions.REQUEST], web3IsDeployed);
}
export function *watchUpdateDeploymentPipeline() {
yield takeEvery(actions.UPDATE_DEPLOYMENT_PIPELINE, web3Connect);
}
export function *watchWeb3Connect() {
yield takeEvery(actions.WEB3_CONNECT[actions.SUCCESS], web3ContractsDeployed);
}
export function *watchFetchEditorTabs() {
yield takeEvery(actions.FETCH_EDITOR_TABS[actions.REQUEST], fetchEditorTabs);
}
@ -615,7 +636,9 @@ export default function *root() {
fork(watchVerifyMessage),
fork(watchWeb3EstimateGas),
fork(watchWeb3Deploy),
fork(watchWeb3IsDeployed),
fork(watchUpdateDeploymentPipeline),
fork(watchWeb3Connect),
fork(watchListenDebugger),
fork(watchFetchEditorTabs),
fork(watchAddEditorTabs),

View File

@ -30,3 +30,18 @@ export function deploy({web3, contract, args}) {
.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
);
});
});
}