diff --git a/lib/dashboard/components/contracts-section.css b/lib/dashboard/components/contracts-section.css
new file mode 100644
index 00000000..8c46c586
--- /dev/null
+++ b/lib/dashboard/components/contracts-section.css
@@ -0,0 +1,102 @@
+h2 {
+ font-size: 16px;
+ font-weight: bold;
+}
+
+.scenario p {
+ font-size: 12px;
+}
+
+.scenario p.note {
+ font-family: monospace;
+}
+
+.scenario p.error {
+ color: #ff0000;
+ font-size: 12px;
+}
+
+.code {
+ background: #dedeff;
+ padding: 0px 15px;
+ border: 1px dashed #a0a0ff;
+ font-family: monospace;
+ font-size: 12px;
+}
+
+.code button {
+ background: none;
+ border: none;
+ font-weight: bold;
+ padding: 0 10px 3px 10px;
+ font-size: 20px;
+ position: relative;
+}
+
+.code button:hover,
+.code select:hover,
+.code input:hover {
+ background: #a0a0ff;
+}
+
+.code select {
+ background: none;
+ border: 1px dashed #a0a0ff;
+ padding: 0 10px;
+}
+
+.code input {
+ background: none;
+ border: 1px dashed #a0a0ff;
+ padding: 0 10px;
+}
+
+.scenario {
+ margin-bottom: 30px;
+}
+
+
+
+
+
+
+
+code .Highlight-boolean {
+ color: #0086b3;
+}
+
+code .Highlight-class {
+ color: #0086b3;
+}
+
+code .Highlight-comment {
+ color: #969896;
+}
+
+code .Highlight-constant {
+ color: #a71d5d;
+}
+
+code .Highlight-function {
+ color: #795da3;
+}
+
+code .Highlight-keyword {
+ color: #a71d5d;
+}
+
+code .Highlight-number {
+ color: #0086b3;
+}
+
+code .Highlight-operator {
+ color: #a71d5d;
+}
+
+code .Highlight-punctuation {
+ color: #333;
+}
+
+code .Highlight-string {
+ color: #df5000;
+}
\ No newline at end of file
diff --git a/lib/dashboard/components/contracts-section.html b/lib/dashboard/components/contracts-section.html
new file mode 100644
index 00000000..3068fa1d
--- /dev/null
+++ b/lib/dashboard/components/contracts-section.html
@@ -0,0 +1,18 @@
+
+
+
+
+ Contract UI
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lib/dashboard/components/contracts-section.js b/lib/dashboard/components/contracts-section.js
new file mode 100644
index 00000000..a939f82c
--- /dev/null
+++ b/lib/dashboard/components/contracts-section.js
@@ -0,0 +1,22 @@
+import 'bootstrap';
+import 'bootstrap/dist/css/bootstrap.min.css';
+import './contracts-section.css';
+
+import EmbarkJS from 'Embark/EmbarkJS';
+import IdentityFactory from 'Embark/contracts/IdentityFactory'; // Import all contracts
+
+import ContractUI from './contracts/contract-ui';
+
+
+__embarkContext.execWhenReady(function(){
+
+ // Each contract should be available on window
+ window["IdentityFactory"] = IdentityFactory;
+
+ ReactDOM.render(
+ ,
+ document.getElementById('root')
+ );
+
+
+});
diff --git a/lib/dashboard/components/contracts/account-list.js b/lib/dashboard/components/contracts/account-list.js
new file mode 100644
index 00000000..1fabd4be
--- /dev/null
+++ b/lib/dashboard/components/contracts/account-list.js
@@ -0,0 +1,46 @@
+import ContractContext from './contract-context';
+
+class AccountList extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ error: false,
+ errorMessage: "",
+ accounts: []
+ };
+ }
+
+
+ async handleClick(e, updateAccountsCallback){
+ e.preventDefault();
+
+ try {
+ updateAccountsCallback();
+ } catch(err) {
+ this.setState({
+ error: true,
+ errorMessage: e.name + ': ' + e.message
+ })
+ }
+
+ }
+
+ render(){
+ return
+ {(context) => (
+
+
Get Accounts
+
+
+ await web3.eth.getAccounts();
+
+
accounts variable is available in the console
+ {this.state.error ? '
' + this.state.errorMessage + '
' : ''}
+
+
+ )}
+ ;
+ }
+}
+
+export default AccountList;
diff --git a/lib/dashboard/components/contracts/contract-context.js b/lib/dashboard/components/contracts/contract-context.js
new file mode 100644
index 00000000..87b2990f
--- /dev/null
+++ b/lib/dashboard/components/contracts/contract-context.js
@@ -0,0 +1,9 @@
+const ContractContext = React.createContext({
+ accounts: [],
+ instances: [],
+ updateAccounts: () => {},
+ updateInstances: (_instance) => {}
+
+});
+
+export default ContractContext;
\ No newline at end of file
diff --git a/lib/dashboard/components/contracts/contract-ui.js b/lib/dashboard/components/contracts/contract-ui.js
new file mode 100644
index 00000000..8a3c49c5
--- /dev/null
+++ b/lib/dashboard/components/contracts/contract-ui.js
@@ -0,0 +1,92 @@
+import Tab from './tab';
+import AccountList from './account-list';
+import SourceArea from './source-area';
+import InstanceSelector from './instance-selector';
+import FunctionArea from './function-area';
+import ContractContext from './contract-context';
+
+
+class ContractUI extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.updateInstances = this.updateInstances.bind(this);
+ this.updateAccounts = this.updateAccounts.bind(this);
+ this.handleInstanceSelection = this.handleInstanceSelection.bind(this);
+
+ this.state = {
+ accounts: [],
+ instances: [],
+ selectedInstance: null,
+ updateAccounts: this.updateAccounts,
+ updateInstances: this.updateInstances
+ };
+
+ if(props.contract.options.address != null){
+ this.state.instances = [props.contract.options.address];
+ this.state.selectedInstance = props.contract.options.address;
+ }
+ }
+
+ componentDidMount(){
+ this.updateAccounts();
+ }
+
+ async updateAccounts(){
+ let accounts = await web3.eth.getAccounts();
+ window.accounts = accounts;
+
+ console.log("%cawait web3.eth.getAccounts()", 'font-weight: bold');
+ console.log(accounts);
+
+ this.setState({accounts: accounts});
+ }
+
+ updateInstances(_instance){
+ this.state.instances.push(_instance);
+ this.setState({
+ instances: this.state.instances
+ });
+ }
+
+ handleInstanceSelection(_instance){
+ this.props.contract.options.address = _instance;
+ this.setState({
+ selectedInstance: _instance
+ })
+ }
+
+ render() {
+ return (
+
+
+
{this.props.name} contract
+
Open your browser's console: Tools > Developer Tools
+
Remix: http://remix.ethereum.org
+
+
+
+
+ Deploy
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default ContractUI;
\ No newline at end of file
diff --git a/lib/dashboard/components/contracts/function-area.js b/lib/dashboard/components/contracts/function-area.js
new file mode 100644
index 00000000..8123382a
--- /dev/null
+++ b/lib/dashboard/components/contracts/function-area.js
@@ -0,0 +1,24 @@
+import FunctionForm from './function-form';
+
+class FunctionArea extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { };
+ }
+
+ render(){
+ const type = this.props.type;
+ const contract = this.props.contract;
+ const contractName = this.props.contractName;
+
+ return
+ {
+ this.props.contract.options.jsonInterface
+ .filter(item => item.type == type)
+ .map((item, i) => )
+ }
+ ;
+ }
+}
+
+export default FunctionArea;
\ No newline at end of file
diff --git a/lib/dashboard/components/contracts/function-form.js b/lib/dashboard/components/contracts/function-form.js
new file mode 100644
index 00000000..1a2b549b
--- /dev/null
+++ b/lib/dashboard/components/contracts/function-form.js
@@ -0,0 +1,88 @@
+import Function from './function';
+
+class FunctionForm extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ fields: {},
+ error: false,
+ message: null,
+ receipt: null
+ };
+
+ this.showResults = this.showResults.bind(this);
+ }
+
+ _getFunctionParamFields(elem){
+ if(this.props.abi.type == 'fallback') return '';
+
+ return '(' + this.props.abi.inputs
+ .map((input, i) => )
+ .join(', ') + ')';
+ }
+
+ _getMethodType(elem){
+ return (this.props.abi.constant == true || this.props.abi.stateMutability == 'view' || this.props.abi.stateMutability == 'pure') ? 'call' : 'send';
+ }
+
+ render(){
+ const functionName = this.props.abi.name;
+ const isDuplicated = this.props.contract.options.jsonInterface.filter(x => x.name == functionName).length > 1;
+ const contract = this.props.contract;
+ const receipt = this.state.receipt;
+
+ return
+
{this.props.abi.type == 'function' ? this.props.abi.name : (this.props.abi.type == 'fallback' ? '(fallback)' : this.props.abi.name)}
+
+
+
+
+ { receipt != null ?
+
+ - Status: {receipt.status}
+ - Transaction Hash: {receipt.transactionHash}
+ {
+ receipt.events != null ?
+ - Events:
+
+ {
+ Object.keys(receipt.events).map(function(ev, index) {
+ if(!isNaN(ev)) return null;
+ const eventAbi = contract.options.jsonInterface.filter(x => x.name == ev)[0];
+ let props = [];
+ for(let prop in receipt.events[ev].returnValues){
+ if(isNaN(prop)){
+ let input = eventAbi.inputs.filter(x => x.name == prop)[0];
+ props.push(prop + ': '
+ + (input.type.indexOf('int') == -1 ? '"' : '')
+ + receipt.events[ev].returnValues[prop]
+ + (input.type.indexOf('int') == -1 ? '"' : ''));
+ }
+ }
+ return - {ev}({props.join(', ')})
;
+ })
+ }
+
+
+ : ''
+ }
+
+ : ""
+ }
+ {this.state.error ?
{this.state.message}
: '' }
+ {!this.state.error && this.state.message != null ?
{this.state.message}
: '' }
+
+
+
;
+ }
+
+ showResults(_error, _message, _receipt){
+ this.setState({
+ error: _error,
+ message: _message,
+ receipt: _receipt
+ })
+ }
+}
+
+export default FunctionForm;
\ No newline at end of file
diff --git a/lib/dashboard/components/contracts/function.js b/lib/dashboard/components/contracts/function.js
new file mode 100644
index 00000000..96b5522d
--- /dev/null
+++ b/lib/dashboard/components/contracts/function.js
@@ -0,0 +1,219 @@
+import ContractContext from './contract-context';
+
+class Function extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ onRequest: false,
+ fields: {},
+ methodFields: {
+ from: '',
+ to: '',
+ value: 0,
+ data: '',
+ gasLimit: '7000000'
+ },
+ receipt: null
+ };
+
+ this.handleParameterChange = this.handleParameterChange.bind(this);
+ this.handleMethodFieldChange = this.handleMethodFieldChange.bind(this);
+ this.handleClick = this.handleClick.bind(this);
+ }
+
+ async handleClick(e, instanceUpdateCallback){
+ e.preventDefault();
+
+ this.setState({onRequest: true, receipt: null});
+
+ this.props.resultHandler(false, null, null);
+
+ let executionParams = {
+ from: this.state.methodFields.from,
+ gasLimit: this.state.methodFields.gasLimit
+ }
+
+ if(this.props.abi.payable)
+ executionParams.value = this.state.methodFields.value;
+
+ if(this.props.abi.type == 'fallback'){
+ executionParams.data = this.state.methodFields.data;
+ executionParams.to = this.props.contract.options.address;
+ }
+
+ let fields = this.state.fields;
+
+ let functionLabel = this._getFunctionLabel();
+ let functionParams = this._getFunctionParamString();
+ let methodParams = this._getMethodString();
+ if(this.props.abi.type == "constructor")
+ functionParams = `{arguments: [${functionParams}]}`;
+
+ console.log(`%cawait ${functionLabel}(${functionParams})${this.props.abi.type != 'fallback' ? '.' + this._getMethodType() : ''}${methodParams}`, 'font-weight: bold');
+
+ let _receipt;
+
+ try {
+ if(this.props.abi.type == 'constructor'){
+ let contractInstance = await this.props.contract.deploy({arguments: Object.keys(fields).map(val => fields[val])}).send(executionParams);
+ instanceUpdateCallback(contractInstance.options.address);
+ this.setState({onRequest: false});
+ console.log(contractInstance.options.address);
+ this.props.resultHandler(false, 'New instance: ' + contractInstance.options.address);
+ } else {
+
+ if(this.props.abi.type == 'fallback')
+ _receipt = await this.web3.eth.sendTransaction(executionParams);
+ else
+ _receipt = await this.props.contract
+ .methods[this.props.abi.name + '(' + this.props.abi.inputs.map(input => input.type).join(',') + ')']
+ .apply(null, Object.keys(fields).map(val => fields[val]))
+ [this._getMethodType()](executionParams)
+
+ if(this._getMethodType() == 'call'){
+ this.props.resultHandler(false, _receipt, null);
+ } else {
+ this.props.resultHandler(false, null, _receipt);
+ }
+
+ this.setState({onRequest: false, receipt: _receipt});
+ console.log(_receipt);
+ }
+
+
+ } catch (e) {
+ console.error('%s: %s', e.name, e.message);
+ this.setState({onRequest: false});
+ this.props.resultHandler(true, e.name + ": " + e.message, _receipt);
+ }
+ }
+
+ handleParameterChange(e){
+ let newState = this.state;
+ newState.fields[e.target.getAttribute('data-name')] = e.target.value;
+ this.setState(newState);
+ }
+
+ handleMethodFieldChange(e){
+ let newState = this.state;
+ newState.methodFields[e.target.getAttribute('data-param')] = e.target.value;
+
+ if(e.target.getAttribute('data-param') == 'from'){
+ newState.selectedAccount = e.target.options[e.target.selectedIndex].text;
+ }
+ this.setState(newState);
+ }
+
+ _getFunctionLabel(){
+ if(this.props.abi.type == 'function')
+ if(!this.props.duplicated)
+ return `${this.props.contractName}.methods.${this.props.abi.name}`;
+ else {
+ return `${this.props.contractName}.methods['${this.props.abi.name + '(' + (this.props.abi.inputs != null ? this.props.abi.inputs.map(input => input.type).join(',') : '') + ')'}']`;
+ }
+ else if(this.props.abi.type == 'fallback'){
+ return `web3.eth.sendTransaction`;
+ }
+ else
+ return `${this.props.contractName}.deploy`;
+ }
+
+ _getMethodType(){
+ return (this.props.abi.constant == true || this.props.abi.stateMutability == 'view' || this.props.abi.stateMutability == 'pure') ? 'call' : 'send';
+ }
+
+ _getMethodFields(accounts){
+ let methodParams;
+ return
+ from:
+ {
+ this.props.abi.payable ?
+ , value:
+
+
+ : ''
+ }
+ {
+ this._getMethodType() == 'send' ?
+ , gasLimit:
+
+
+ : ''
+ }
+ {
+ this._getMethodType() == 'send' && this.props.abi.type == 'fallback' ?
+ , data:
+
+
+ : ''
+ }
+ ;
+ }
+
+
+ _getFunctionParamFields(){
+ return
+ {
+ this.props.abi.inputs
+ .map((input, i) => )
+ .reduce((accu, elem) => {
+ return accu === null ? [elem] : [...accu, ', ', elem]
+ }, null)
+ }
+ ;
+ }
+
+ _getFunctionParamString(){
+ if(this.props.abi.type == 'fallback') return '';
+ return this.props.abi.inputs
+ .map((input, i) => (input.type.indexOf('int') == -1 ? '"' : '') + this.state.fields[input.name] + (input.type.indexOf('int') == -1 ? '"' : ''))
+ .join(', ');
+ }
+
+ _getMethodString(elem){
+ let methodParams = "({";
+
+ methodParams += `from: ` + this.state.selectedAccount;
+ if(this._getMethodType() == 'send'){
+ methodParams += ', gasLimit: ' + this.state.methodFields.gasLimit
+ if(this.props.abi.payable){
+ methodParams += ', value: ' + this.state.methodFields.value
+ }
+ if(this.props.abi.type == 'fallback'){
+ methodParams += ', data: "' + this.state.methodFields.data + '", to: "' + this.state.methodFields.to + '"'
+ }
+ }
+ return methodParams + "})";
+ }
+
+ render(){
+ return
+ { (context) => (
+
+ await {this._getFunctionLabel()}
+ { this.props.abi.type != 'fallback' ? '(' : '' }
+ { this.props.abi.type != 'fallback' ? this._getFunctionParamFields() : '' }
+ { this.props.abi.type != 'fallback' ? ')' : '' }
+ { this.props.abi.type != 'fallback' ? '.' + this._getMethodType() : '' }
+ ({ this._getMethodFields(context.accounts) })
+
+ { this.state.onRequest ?
+
+ : ''
+ }
+
+ )}
+ ;
+ }
+
+}
+
+export default Function;
\ No newline at end of file
diff --git a/lib/dashboard/components/contracts/instance-selector.js b/lib/dashboard/components/contracts/instance-selector.js
new file mode 100644
index 00000000..1624b8f8
--- /dev/null
+++ b/lib/dashboard/components/contracts/instance-selector.js
@@ -0,0 +1,105 @@
+import ContractContext from './contract-context';
+
+class InstanceSelector extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ showInstances: false,
+ showCustomAddressField: false,
+ selectedInstance: props.selectedInstance,
+ customInstance: "",
+ error: false,
+ errorMessage: ""
+ };
+
+ this.handleShowInstances = this.handleShowInstances.bind(this);
+ this.handleChange = this.handleChange.bind(this);
+ this.handleClick = this.handleClick.bind(this);
+ this.handleTextChange = this.handleTextChange.bind(this);
+ }
+
+ handleTextChange(e){
+ this.setState({customInstance: e.target.value});
+ }
+
+ handleShowInstances(e){
+ e.preventDefault();
+ this.setState({
+ showInstances: !this.state.showInstances
+ });
+ }
+
+ handleClick(e){
+ e.preventDefault();
+
+ let instance;
+ if(this.state.selectedInstance == "custom"){
+ instance = this.state.customInstance;
+ } else {
+ instance = this.state.selectedInstance;
+ }
+
+ if(!/^0x[0-9a-f]{40}$/i.test(instance)){
+ this.setState({error: true, errorMessage: 'Not a valid Ethereum address.'});
+ console.log(this.state.errorMessage);
+ return;
+ } else {
+ this.setState({error: false});
+ }
+
+ this.props.instanceUpdate(instance);
+
+ this.setState({
+ showInstances: false,
+ showCustomAddressField: false,
+ selectedInstance: instance,
+ customInstance: this.state.selectedInstance == "custom" ? this.state.customInstance : ""
+ })
+ }
+
+ handleChange(e){
+ this.setState({
+ showCustomAddressField: e.target.value == "custom",
+ selectedInstance: e.target.value
+ });
+ }
+
+ render(){
+
+ return
+ { (context) => (
+
+ Instance Selected: {this.props.selectedInstance != null ? this.props.selectedInstance : 'none'}
+ {!this.state.showInstances ? Change : Cancel }
+
+ {this.state.showInstances ?
+
+
+ {
+ this.state.showCustomAddressField ?
+
+ : ''
+ }
+
+ {
+ this.state.error ?
+
{this.state.errorMessage}
+ : ""
+ }
+
: "" }
+
+ )}
+ ;
+ }
+
+}
+
+export default InstanceSelector;
\ No newline at end of file
diff --git a/lib/dashboard/components/contracts/source-area.js b/lib/dashboard/components/contracts/source-area.js
new file mode 100644
index 00000000..b3ab9a6c
--- /dev/null
+++ b/lib/dashboard/components/contracts/source-area.js
@@ -0,0 +1,27 @@
+class SourceArea extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ sourceCode: ""
+ };
+ }
+
+ componentDidMount(){
+ fetch(this.props.sourceURL)
+ .then(response => response.text())
+ .then(text => {
+ let colorCodedText = hljs.highlight('javascript', text, true).value;
+ this.setState({sourceCode: colorCodedText});
+ });
+ }
+
+ render(){
+ return
+ {this.props.sourceURL.split('\\').pop().split('/').pop()}
+ {this.props.sourceURL}
+
+ ;
+ }
+}
+
+export default SourceArea;
\ No newline at end of file
diff --git a/lib/dashboard/components/contracts/tab.js b/lib/dashboard/components/contracts/tab.js
new file mode 100644
index 00000000..80006f02
--- /dev/null
+++ b/lib/dashboard/components/contracts/tab.js
@@ -0,0 +1,10 @@
+class Tab extends React.Component {
+ render(){
+ return
+
{this.props.name}
+ { this.props.children }
+ ;
+ }
+}
+
+export default Tab;
\ No newline at end of file