diff --git a/app/components/multisigwallet.js b/app/components/multisigwallet.js new file mode 100644 index 0000000..3e5d294 --- /dev/null +++ b/app/components/multisigwallet.js @@ -0,0 +1,46 @@ +import EmbarkJS from 'Embark/EmbarkJS'; +import MultiSigWallet from 'Embark/contracts/MultiSigWallet'; +import React from 'react'; +import {Tabs, Tab} from 'react-bootstrap'; +import MSWTransactionTable from './multisigwallet/transaction-table'; +import MSWOwnerTable from './multisigwallet/owner-table'; + +class MultiSigWalletUI extends React.Component { + + constructor(props) { + super(props); + this.state = { + account: props.account, + mswInstance: props.instance, + isOwner: props.isOwner, + activeKey: 1 + } + + } + + _addToLog(txt){ + console.log(txt); + } + + handleSelect(key) { + this.setState({ activeKey: key }); + } + render() { + return ( + +

{this.state.mswInstance._address}

+ + + + + + + + +
+ ); + } +} + + +export default MultiSigWalletUI; diff --git a/app/components/multisigwallet/confirmation.js b/app/components/multisigwallet/confirmation.js new file mode 100644 index 0000000..d6b71c2 --- /dev/null +++ b/app/components/multisigwallet/confirmation.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { Button } from 'react-bootstrap'; + +class MSWConfirmation extends React.Component { + constructor(props){ + super(props); + this.state = { + control: props.control, + mswInstance: props.instance, + transactionId: props.id, + account: props.account, + accountConfirmed: false, + confirmations: 0, + required: 0, + error: null, + mined: null + }; + this.load() + } + + load() { + this.state.mswInstance.methods.required().call().then((required) => { + this.setState({required: required}); + }) + this.state.mswInstance.methods.getConfirmationCount(this.state.transactionId).call().then((confirmations) => { + this.setState({confirmations: confirmations}); + }) + if(this.state.control){ + this.state.mswInstance.methods.confirmations(this.state.transactionId, this.state.account).call().then((accountConfirmed) => { + this.setState({accountConfirmed: accountConfirmed}); + }) + } + + } + + async handleClick(e){ + e.preventDefault(); + + const {transactionId, value} = this.state; + + this.setState({output: null, error: null, receipt: null}); + + try { + + const toSend = accountConfirmed ? mswInstance.methods.revokeConfirmation(transactionId) : mswInstance.methods.confirmTransaction(transactionId); + const estimatedGas = await toSend.estimateGas({from: web3.eth.defaultAccount}); + const receipt = await toSend.send({ + from: web3.eth.defaultAccount, + gasLimit: estimatedGas + }); + + console.log(receipt); + + this.setState({receipt}); + + + } catch(err) { + console.error(err); + this.setState({error: err.message}); + } + } + + render(){ + const {accountConfirmed, confirmations, required} = this.state; + const txt = " ["+confirmations+"/"+required+"]"; + return( +
+ +
) + } +} + +export default MSWConfirmation; \ No newline at end of file diff --git a/app/components/multisigwallet/owner-add.js b/app/components/multisigwallet/owner-add.js new file mode 100644 index 0000000..c06617a --- /dev/null +++ b/app/components/multisigwallet/owner-add.js @@ -0,0 +1,100 @@ +import React, { Fragment } from 'react'; +import { FormGroup, ControlLabel, FormControl, Button, Alert, } from 'react-bootstrap'; + + +class MSWAddOwner extends React.Component { + constructor(props){ + super(props); + this.state = { + account: props.account, + mswInstance: props.instance, + input: { + owner: '' + }, + error: null, + mined: null + }; + } + + handleChangeFile(e) { + const {input} = this.state; + input.file = [e.target]; + this.setState({input}); + } + + handleChange(e, name) { + const {input} = this.state; + input[name] = e.target.value; + this.setState({input}); + } + + handleCheckbox(e, name) { + const {input} = this.state; + input[name] = e.target.checked; + this.setState({input}); + } + + async handleClick(e){ + e.preventDefault(); + + const {input, value} = this.state; + + this.setState({output: null, error: null, receipt: null}); + + try { + + const toSend = this.state.mswInstance.methods.addOwner(input.owner); + + const MsSend = this.state.mswInstance.submitTransaction( + this.state.mswInstance.address, 0, toSend.encodeABI + ) + const estimatedGas = await MsSend.estimateGas({from: this.state.account}); + + const receipt = await MsSend.send({ + from: this.state.account, + gasLimit: estimatedGas + }); + + console.log(receipt); + + this.setState({receipt}); + + + } catch(err) { + console.error(err); + this.setState({error: err.message}); + } + } + + render(){ + const {input, value, error, output, receipt} = this.state; + + return
+

addOwner

+
+ + owner + this.handleChange(e, 'owner')} + /> + + + { error != null && {error} } + + + { + receipt && + + {isSuccess(receipt.status) ? 'Success' : 'Failure / Revert'} - Transaction Hash: {receipt.transactionHash} + + + } +
+
; + } +} + +export default MSWAddOwner; \ No newline at end of file diff --git a/app/components/multisigwallet/owner-table.js b/app/components/multisigwallet/owner-table.js new file mode 100644 index 0000000..1d6cca9 --- /dev/null +++ b/app/components/multisigwallet/owner-table.js @@ -0,0 +1,86 @@ +import React from 'react'; +import { Table, Button } from 'react-bootstrap'; +import MSWAddOwner from './owner-add'; + +class MSWOwnerTable extends React.Component { + + constructor(props) { + super(props); + this.state = { + control: props.control, + account: props.account, + mswInstance: props.instance, + owners: [] + } + this.load() + } + + load() { + this.state.mswInstance.methods.getOwners().call().then((owners) => { + this.setState({owners:owners}); + }) + } + + async removeOwner(e, account){ + e.preventDefault(); + + + this.setState({output: null, error: null, receipt: null}); + + try { + + const toSend = this.state.mswInstance.methods.removeOwner(account); + const MsSend = this.state.mswInstance.submitTransaction( + this.state.mswInstance.address, 0, toSend.encodeABI + ) + const estimatedGas = await MsSend.estimateGas({from: this.state.account}); + + const receipt = await MsSend.send({ + from: this.state.account, + gasLimit: estimatedGas + }); + + console.log(receipt); + + this.setState({receipt}); + + + } catch(err) { + console.error(err); + this.setState({error: err.message}); + } + } + + render() { + + const owners = this.state.owners.map((address, index) => ( + + {address} + + ) + ) + + return ( + + {this.state.control && } +
+ + + + + + + + + {owners} + +
Owner
+ +
+
+ ) + + } +} + +export default MSWOwnerTable; \ No newline at end of file diff --git a/app/components/multisigwallet/transaction-submit.js b/app/components/multisigwallet/transaction-submit.js new file mode 100644 index 0000000..0165895 --- /dev/null +++ b/app/components/multisigwallet/transaction-submit.js @@ -0,0 +1,115 @@ +import React from 'react'; +import { FormGroup, ControlLabel, FormControl, Button, Alert } from 'react-bootstrap'; +class MSWSubmitTransaction extends React.Component { + constructor(props){ + super(props); + this.state = { + account: props.account, + mswInstance: props.instance, + input: { + destination: '', + value: '', + data: '' + }, + error: null, + mined: null + }; + } + + handleChangeFile(e) { + const {input} = this.state; + input.file = [e.target]; + this.setState({input}); + } + + handleChange(e, name) { + const {input} = this.state; + input[name] = e.target.value; + this.setState({input}); + } + + handleCheckbox(e, name) { + const {input} = this.state; + input[name] = e.target.checked; + this.setState({input}); + } + + async handleClick(e){ + e.preventDefault(); + + const {input, value} = this.state; + + this.setState({output: null, error: null, receipt: null}); + + try { + + const toSend = this.state.mswInstance.methods.submitTransaction(input.destination, input.value, input.data); + + const estimatedGas = await toSend.estimateGas({from: this.state.account}); + + const receipt = await toSend.send({ + from: this.state.account, + gasLimit: estimatedGas + }); + + console.log(receipt); + + this.setState({receipt}); + + + } catch(err) { + console.error(err); + this.setState({error: err.message}); + } + } + + render(){ + const {input, value, error, output, receipt} = this.state; + + return
+

submitTransaction

+
+ + destination + this.handleChange(e, 'destination')} + /> + + + value + this.handleChange(e, 'value')} + /> + + + data + this.handleChange(e, 'data')} + /> + + + { error != null && {error} } + + + { + receipt && + + {isSuccess(receipt.status) ? 'Success' : 'Failure / Revert'} - Transaction Hash: {receipt.transactionHash} + + + } +
+
; + } +} + +export default MSWSubmitTransaction; \ No newline at end of file diff --git a/app/components/multisigwallet/transaction-table.js b/app/components/multisigwallet/transaction-table.js new file mode 100644 index 0000000..fca1802 --- /dev/null +++ b/app/components/multisigwallet/transaction-table.js @@ -0,0 +1,101 @@ +import React from 'react'; +import { Table } from 'react-bootstrap'; +import MSWConfirmation from './confirmation'; +import MSWSubmitTransaction from './transaction-submit'; + + +class MSWTransactionTable extends React.Component { + + constructor(props) { + super(props); + this.state = { + control: props.control, + account: props.account, + mswInstance: props.instance, + transactionCount: 0, + executedTxs: [], + pendingTxs: [] + } + this.loadAll() + } + + loadAll() { + this.state.mswInstance.methods.transactionCount().call().then((count) => { + this.transactionCount = count; + if(count > 0){ + for(var i=0; i < count; i++){ + this.loadTx(i); + } + } + + }) + + } + + loadTx(txId) { + this.state.mswInstance.methods.transactions(txId).call().then((val) => { + val.id = txId; + if(val.executed){ + this.state.executedTxs.push(val); + } else { + this.state.pendingTxs.push(val); + } + this.forceUpdate(); + }) + } + + render() { + const pendingTxs = this.state.pendingTxs.map((tx, index) => ( + + {tx.id} + + To: {tx.destination}
+ Value: {tx.value}
+ Data: {tx.data} + ) + ) + const executedTxs = this.state.executedTxs.map((tx, index) => ( + + {tx.id} + To: {tx.destination}
+ Value: {tx.value}
+ Data: {tx.data} + ) + ) + return ( + + {this.state.control &&

Warning: You are legally responsable by what you approve.

} + {this.state.control &&

Only approve when you are sure the execution is desired.

} + {this.state.control && } +
+ + + + + + + + + + {pendingTxs} + +
TxIdConfirmationsTx Info
+ + + + + + + + + {executedTxs} + +
TxIdTx Info
+
+
+ ) + + } +} + +export default MSWTransactionTable; \ No newline at end of file diff --git a/app/dapp.js b/app/dapp.js new file mode 100644 index 0000000..ce8b7bd --- /dev/null +++ b/app/dapp.js @@ -0,0 +1,98 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import EmbarkJS from 'Embark/EmbarkJS'; +import MultiSigWallet from 'Embark/contracts/MultiSigWallet'; +import MultiSigWalletUI from './components/multisigwallet'; +import { Alert, FormGroup, ControlLabel, FormControl } from 'react-bootstrap'; +import './dapp.css'; + +class App extends React.Component { + + constructor(props) { + super(props); + this.state = { + error: null, + activeKey: 1, + blockchainEnabled: false , + mswInstance: null, + account: null, + isOwner: false, + contractSetError: null, + contractAddress: null + }; + } + + componentDidMount() { + EmbarkJS.onReady((err) => { + this.setState({blockchainEnabled: true}); + if (err) { + // If err is not null then it means something went wrong connecting to ethereum + // you can use this to ask the user to enable metamask for e.g + return this.setState({error: err.message || err}); + } + }); + } + + + setContractAddress(e) { + this.setState({contractSetError: null}) + try{ + const contractAddress = web3.utils.toChecksumAddress(e.target.value); + web3.eth.getCode(contractAddress).then((code) => { + if(code.length > 2){ + let mswInstance = new web3.eth.Contract(MultiSigWallet._jsonInterface, contractAddress) + mswInstance.methods.required().call().then((req) => { + if(req > 0){ + EmbarkJS.enableEthereum().then((s)=>{ + if(s){ + let defaultAccount = web3.utils.toChecksumAddress(s[0]); + mswInstance.methods.isOwner(defaultAccount).call().then((isOwner) => { + this.setState({isOwner:isOwner, account: defaultAccount, contractAddress: contractAddress, mswInstance: mswInstance}); + }) + } + }) + + } else { + this.setState({contractSetError: "Invalid MultiSigWallet"}) + } + }).catch((e) => { + this.setState({contractSetError: "Not a MultiSigWallet"}) + }) + } else { + this.setState({contractSetError: "Not a smart contract"}) + } + }) + }catch(e){ + this.setState({contractSetError: e.toString()}) + } + + } + render() { + if (this.state.error) { + return (
+
Something went wrong connecting to ethereum. Please make sure you have a node running or are using metamask to connect to the ethereum network:
+
{this.state.error}
+
); + } + return ( +
+

MultiSig Wallet

+ + {!this.state.mswInstance &&
+ + Contract Address: + this.setContractAddress(e)} + /> + + { this.state.contractSetError != null && {this.state.contractSetError} } +
} + { this.state.mswInstance && } +
); + } +} + +ReactDOM.render(, document.getElementById('app')); diff --git a/app/index.html b/app/index.html index 594df14..b2625c5 100644 --- a/app/index.html +++ b/app/index.html @@ -1,7 +1,7 @@ - MultiSigWalletWithDailyLimit + MultiSigWallet