017 udpates
This commit is contained in:
Родитель
095220c8db
Коммит
8459dc8f3e
|
@ -8,7 +8,7 @@ coverage/
|
|||
### Node template
|
||||
package-lock.json
|
||||
|
||||
# Logs
|
||||
### Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
@ -31,3 +31,6 @@ jspm_packages/
|
|||
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
|
||||
### User template
|
||||
resources/drizzle/
|
||||
|
|
12
.npmignore
12
.npmignore
|
@ -1,12 +0,0 @@
|
|||
.vscode/
|
||||
node_modules/
|
||||
src/
|
||||
test/
|
||||
out/test/
|
||||
**/*.js.map
|
||||
.editorconfig
|
||||
.gitignore
|
||||
.vscodeignore
|
||||
package-lock.json
|
||||
tsconfig.json
|
||||
tslint.json
|
|
@ -8,6 +8,8 @@ out/**
|
|||
!out/src/mscorlib.js
|
||||
!out/src/Nethereum.Generators.DuoCode.js
|
||||
!out/src/extension.js
|
||||
!out/src/debugger.js
|
||||
!out/src/web3ProviderResolver.js
|
||||
src/**
|
||||
test/**
|
||||
.editorconfig
|
||||
|
|
|
@ -4,6 +4,11 @@ All notable changes to the "azureblockchain" extension will be documented in thi
|
|||
|
||||
|
||||
|
||||
## 0.1.7
|
||||
|
||||
- Added contract UI/interaction feature
|
||||
- Added Solidity debugging feature
|
||||
|
||||
## 0.1.6
|
||||
|
||||
- Added telemetry reporting capabilities
|
||||
|
|
39840
NOTICE.txt
39840
NOTICE.txt
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -100,4 +100,4 @@ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additio
|
|||
|
||||
## Telemetry
|
||||
|
||||
VS Code collects usage data and sends it to Microsoft to help improve our products and services. Read our [privacy statement](https://go.microsoft.com/fwlink/?LinkID=528096&clcid=0x409) to learn more. If you don’t wish to send usage data to Microsoft, you can set the `telemetry.enableTelemetry` setting to `false`. Learn more in our [FAQ](https://code.visualstudio.com/docs/supporting/faq#_how-to-disable-telemetry-reporting).
|
||||
VS Code collects usage data and sends it to Microsoft to help improve our products and services. Read our [privacy statement](https://go.microsoft.com/fwlink/?LinkID=528096&clcid=0x409) to learn more. If you don’t wish to send usage data to Microsoft, you can set the `telemetry.enableTelemetry` setting to `false`. Learn more in our [FAQ](https://code.visualstudio.com/docs/supporting/faq#_how-to-disable-telemetry-reporting).
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
[
|
||||
"@babel/plugin-transform-runtime",
|
||||
{
|
||||
"regenerator": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[**.*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,87 @@
|
|||
{
|
||||
"parser": "babel-eslint",
|
||||
"rules": {
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
"no-param-reassign": "error",
|
||||
"semi": 2,
|
||||
"sort-imports": [
|
||||
2,
|
||||
{
|
||||
"ignoreCase": true,
|
||||
"memberSyntaxSortOrder": ["single", "multiple", "all", "none"]
|
||||
}
|
||||
],
|
||||
"func-names": "off",
|
||||
"max-len": 0,
|
||||
"prefer-destructuring": 0,
|
||||
"arrow-parens": 0,
|
||||
"import/newline-after-import": 0,
|
||||
"no-prototype-builtins": 0,
|
||||
"lines-around-directive": 0,
|
||||
"no-unsafe-negation": 0,
|
||||
"import/first": 0,
|
||||
"import/order": 0,
|
||||
"import/no-unresolved": 0,
|
||||
"import/no-webpack-loader-syntax": 0,
|
||||
"prefer-spread": 0,
|
||||
"no-underscore-dangle": 0,
|
||||
"no-template-curly-in-string": 0,
|
||||
"class-methods-use-this": 0,
|
||||
"no-confusing-arrow": 0,
|
||||
"no-mixed-operators": 0,
|
||||
"no-undef-init": 0,
|
||||
"no-plusplus": 0,
|
||||
"linebreak-style": 0,
|
||||
"import/prefer-default-export": 0,
|
||||
"no-useless-escape": 0,
|
||||
"no-loop-func": 0,
|
||||
"import/no-extraneous-dependencies": 0,
|
||||
"import/extensions": 0,
|
||||
"no-lonely-if": 0,
|
||||
"comma-dangle": 0,
|
||||
"no-extra-boolean-cast": 0,
|
||||
"no-multi-assign": 0,
|
||||
"newline-per-chained-call": 0,
|
||||
"block-spacing": 0,
|
||||
"object-property-newline": 0,
|
||||
"no-useless-rename": 0,
|
||||
"no-restricted-syntax": 0,
|
||||
"no-useless-return": 0,
|
||||
"no-continue": 0,
|
||||
"no-console": 0,
|
||||
"function-paren-newline": 0,
|
||||
"object-curly-newline": 0,
|
||||
"arrow-body-style": 0,
|
||||
"react/react-in-jsx-scope": 0,
|
||||
"react/prop-types": 1,
|
||||
"react/sort-comp": 2,
|
||||
"no-await-in-loop": "off",
|
||||
"react/jsx-no-bind": [
|
||||
2,
|
||||
{
|
||||
"allowArrowFunctions": true,
|
||||
"allowBind": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"jest/globals": true
|
||||
},
|
||||
"globals": {
|
||||
"shallow": true,
|
||||
"mount": true
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"extends": [
|
||||
"airbnb/base",
|
||||
"plugin:react/recommended",
|
||||
"plugin:jest/recommended"
|
||||
],
|
||||
"plugins": ["react", "import", "jest"]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/node_modules/
|
||||
/.idea/
|
||||
/build/
|
||||
/dist/
|
||||
|
||||
package-lock.json
|
||||
.DS_Store
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"name": "client",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production webpack --mode production",
|
||||
"build:dev": "cross-env NODE_ENV=development webpack-dev-server --open",
|
||||
"build:ext": "cross-env NODE_ENV=production EXT_ENV=true webpack --mode production",
|
||||
"build:ext:dev": "cross-env NODE_ENV=development EXT_ENV=true webpack --mode development",
|
||||
"lint": "eslint ./src --ext .js",
|
||||
"lint:fix": "npm run lint -- --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
|
||||
"@babel/plugin-transform-runtime": "^7.5.5",
|
||||
"@babel/preset-env": "^7.5.5",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"babel-eslint": "^10.0.2",
|
||||
"babel-loader": "^8.0.6",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^3.1.0",
|
||||
"eslint": "^6.1.0",
|
||||
"eslint-config-airbnb": "^17.1.1",
|
||||
"eslint-loader": "^2.2.1",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-jest": "^22.14.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-react": "^7.14.3",
|
||||
"file-loader": "^4.1.0",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"less": "^3.9.0",
|
||||
"less-loader": "^5.0.0",
|
||||
"mini-css-extract-plugin": "^0.8.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"style-loader": "^0.23.1",
|
||||
"url-loader": "^2.1.0",
|
||||
"webpack": "^4.38.0",
|
||||
"webpack-cli": "^3.3.6",
|
||||
"webpack-plugin-replace": "^1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.2.1",
|
||||
"@material-ui/icons": "^4.2.1",
|
||||
"drizzle": "^1.4.0",
|
||||
"drizzle-react": "^1.3.0",
|
||||
"drizzle-react-components": "^1.4.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { Container } from '@material-ui/core';
|
||||
import { DrizzleContext } from 'drizzle-react';
|
||||
import { IPCService } from './ipcService';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { SingleContractView } from 'views';
|
||||
import {
|
||||
ContractSelector,
|
||||
Message
|
||||
} from 'components';
|
||||
import {
|
||||
createMuiTheme,
|
||||
MuiThemeProvider
|
||||
} from '@material-ui/core/styles';
|
||||
import './app.less';
|
||||
|
||||
class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
updating: false,
|
||||
contractInstances: [],
|
||||
selectedContractId: undefined,
|
||||
theme: createMuiTheme({
|
||||
palette: {
|
||||
type: 'light'
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
this.target = document.querySelector('body');
|
||||
this.observer = new MutationObserver(this.updateTheme);
|
||||
|
||||
this.observer.observe(this.target, { attributes: true, childList: false });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
IPCService.on('contracts', this.updateContracts);
|
||||
IPCService.postMessage('documentReady', true);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const {
|
||||
updating,
|
||||
contractInstances,
|
||||
selectedContractId
|
||||
} = this.state;
|
||||
|
||||
if (
|
||||
!updating
|
||||
&& !selectedContractId
|
||||
&& contractInstances.length !== 0
|
||||
) {
|
||||
const contractInstance = contractInstances[0];
|
||||
this.setContract(contractInstance);
|
||||
}
|
||||
}
|
||||
|
||||
updateContracts = contractInstances => this.setState({
|
||||
contractInstances: contractInstances.reverse()
|
||||
});
|
||||
|
||||
onContractChange = (e) => {
|
||||
const { value } = e.target;
|
||||
const { contractInstances } = this.state;
|
||||
|
||||
this.setContract(contractInstances.find(instance => instance.id === value));
|
||||
}
|
||||
|
||||
setContract = async contractInstance => {
|
||||
this.setState({ updating: true });
|
||||
|
||||
const { drizzle } = this.props;
|
||||
const { contract, provider } = contractInstance;
|
||||
|
||||
drizzle.deleteAllContracts();
|
||||
|
||||
if (provider) {
|
||||
// TODO cannot connect to http (error with subscription)
|
||||
if (!provider.host.toLowerCase().startsWith('ws://')) {
|
||||
provider.host = `ws://${provider.host}`;
|
||||
}
|
||||
|
||||
drizzle.web3.setProvider(provider.host);
|
||||
}
|
||||
|
||||
const address = contractInstance.address;
|
||||
const accounts = await drizzle.web3.eth.getAccounts();
|
||||
|
||||
const contractConfig = {
|
||||
contractName: contract.contractName,
|
||||
web3Contract: new drizzle.web3.eth.Contract(
|
||||
contract.abi,
|
||||
address,
|
||||
{
|
||||
from: accounts[0],
|
||||
data: contract.bytecode,
|
||||
networks: contract.networks,
|
||||
isOnline: !!provider
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
drizzle.addContract(contractConfig);
|
||||
this.setState({
|
||||
selectedContractId: contractInstance.id,
|
||||
updating: false
|
||||
});
|
||||
}
|
||||
|
||||
updateTheme = () => {
|
||||
const type = Array
|
||||
.from(this.target.classList)
|
||||
.includes('vscode-dark') ? 'dark' : 'light';
|
||||
|
||||
if (this.state.theme.palette.type !== type) {
|
||||
this.setState({
|
||||
theme: createMuiTheme({ palette: { type } })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { contractInstances, selectedContractId, theme } = this.state;
|
||||
const { drizzle: { contractList }, initialized } = this.props;
|
||||
|
||||
if (!initialized) {
|
||||
return <Container className='message container'>
|
||||
<Message message='⚙️ Loading dapp...' />
|
||||
</Container>;
|
||||
}
|
||||
|
||||
if (!contractList || contractList.length === 0) {
|
||||
return <Container className='message container'>
|
||||
<Message message='⚠️ No contracts available' />
|
||||
</Container>;
|
||||
}
|
||||
|
||||
return <MuiThemeProvider theme={theme}>
|
||||
<Container>
|
||||
<ContractSelector
|
||||
selectedContractId={selectedContractId}
|
||||
contractInstances={contractInstances}
|
||||
onChange={this.onContractChange}
|
||||
/>
|
||||
<SingleContractView />
|
||||
</Container>
|
||||
</MuiThemeProvider>;
|
||||
}
|
||||
}
|
||||
|
||||
const HOC = () => <DrizzleContext.Consumer>
|
||||
{props => <App {...props} />}
|
||||
</DrizzleContext.Consumer>;
|
||||
|
||||
export default HOC;
|
||||
|
||||
App.propTypes = {
|
||||
drizzle: PropTypes.any,
|
||||
drizzleState: PropTypes.any,
|
||||
initialized: PropTypes.bool.isRequired,
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
margin: 0;
|
||||
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vscode-dark {
|
||||
background-color: #292929;
|
||||
|
||||
.MuiChip-outlinedSecondary {
|
||||
color: #fff;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.vscode-light {
|
||||
background-color: #fdfdfd;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
color: #d4d4d4;
|
||||
|
||||
.paper {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.message.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Typography
|
||||
} from '@material-ui/core';
|
||||
|
||||
class BaseViewComponent extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
header,
|
||||
children,
|
||||
className
|
||||
} = this.props;
|
||||
|
||||
return <Paper className={`paper ${className || ''}`}>
|
||||
<Typography
|
||||
variant='h5'
|
||||
component='h3'
|
||||
align='left'
|
||||
>
|
||||
{header}
|
||||
</Typography>
|
||||
{children}
|
||||
</Paper>;
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseViewComponent;
|
||||
|
||||
BaseViewComponent.propTypes = {
|
||||
header: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.any
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { BaseViewComponent } from 'components';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
Container,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select
|
||||
} from '@material-ui/core';
|
||||
import './contractSelector.less';
|
||||
|
||||
class ContractSelector extends React.Component {
|
||||
renderOption = (contractInstance, index) => {
|
||||
const {
|
||||
id,
|
||||
updateDate
|
||||
} = contractInstance;
|
||||
|
||||
const itemName = (new Date(updateDate)).toGMTString();
|
||||
|
||||
return <MenuItem
|
||||
key={index}
|
||||
value={id}
|
||||
>
|
||||
{itemName}
|
||||
</MenuItem>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
selectedContractId = '',
|
||||
contractInstances = [],
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
return <BaseViewComponent
|
||||
className='selector component'
|
||||
header='Select a contract version'
|
||||
>
|
||||
<Container className='container'>
|
||||
<InputLabel htmlFor='contract-selector'>
|
||||
Contract deployment date
|
||||
</InputLabel>
|
||||
<Select
|
||||
id='contract-selector'
|
||||
value={selectedContractId}
|
||||
onChange={onChange}
|
||||
>
|
||||
{contractInstances.map(this.renderOption)}
|
||||
</Select>
|
||||
</Container>
|
||||
</BaseViewComponent>;
|
||||
}
|
||||
}
|
||||
|
||||
export default ContractSelector;
|
||||
|
||||
ContractSelector.propTypes = {
|
||||
selectedContractId: PropTypes.string,
|
||||
contractInstances: PropTypes.array,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
.selector.component {
|
||||
@media (min-width: 600px) {
|
||||
margin: 15px 24px 0 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
margin: 15px 32px 0 32px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
color: white;
|
||||
|
||||
.MuiFormLabel-root {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.MuiSelect-select {
|
||||
min-width: 180px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { TextField } from '@material-ui/core';
|
||||
|
||||
class EventSection extends React.Component {
|
||||
render() {
|
||||
const { events } = this.props;
|
||||
|
||||
const eventsNotifications = events
|
||||
.reverse()
|
||||
.map(({ event }) => `${event} was called`)
|
||||
.join('\n');
|
||||
|
||||
return <TextField
|
||||
className='metadata text field'
|
||||
disabled
|
||||
multiline
|
||||
fullWidth
|
||||
rows={8}
|
||||
id='contract-address'
|
||||
label='event:'
|
||||
value={eventsNotifications}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
export default EventSection;
|
||||
|
||||
EventSection.propTypes = {
|
||||
events: PropTypes.array.isRequired
|
||||
};
|
|
@ -0,0 +1,96 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { newContextComponents } from 'drizzle-react-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select
|
||||
} from '@material-ui/core';
|
||||
import { InputComponentMapping, UnsupportedInput } from './inputControlsCollection';
|
||||
import './executionSection.less';
|
||||
|
||||
const { ContractForm } = newContextComponents;
|
||||
|
||||
class ExecutionSection extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
executedMethod: undefined
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({ executedMethod: this.props.actions[0] });
|
||||
}
|
||||
|
||||
onChange = (e) => this.setState({
|
||||
executedMethod: e.target.value
|
||||
});
|
||||
|
||||
renderExecutionSection = ({ inputs, handleInputChange, handleSubmit }) => {
|
||||
const containsUnsupportedInput = inputs.some(input => !InputComponentMapping[input.type]);
|
||||
|
||||
return <Container className='execution-section'>
|
||||
<Container className='action'>
|
||||
<InputLabel className='input-label'>Contract Action</InputLabel>
|
||||
<Select
|
||||
className='execution-method'
|
||||
value={this.state.executedMethod}
|
||||
onChange={this.onChange}
|
||||
inputProps={{
|
||||
id: 'action-method',
|
||||
name: 'action'
|
||||
}}
|
||||
>
|
||||
{this.props.actions.map((action, index) => {
|
||||
return (
|
||||
<MenuItem key={index} value={action}>{action}</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Container>
|
||||
<Container className='input-fields'>
|
||||
{inputs.map((item, index) => {
|
||||
const InputControl = InputComponentMapping[item.type] || UnsupportedInput;
|
||||
return <InputControl
|
||||
key={index}
|
||||
item={item}
|
||||
handleInputChange={handleInputChange}
|
||||
/>;
|
||||
})}
|
||||
</Container>
|
||||
<Button
|
||||
className='execute-button'
|
||||
variant='contained'
|
||||
onClick={handleSubmit}
|
||||
disabled={containsUnsupportedInput}
|
||||
>
|
||||
Execute
|
||||
</Button>
|
||||
</Container>;
|
||||
}
|
||||
|
||||
render() {
|
||||
return <ContractForm
|
||||
key={this.state.executedMethod}
|
||||
contract={this.props.contractName}
|
||||
method={this.state.executedMethod}
|
||||
drizzle={this.props.drizzle}
|
||||
render={this.renderExecutionSection}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
export default ExecutionSection;
|
||||
|
||||
ExecutionSection.propTypes = {
|
||||
actions: PropTypes.any.isRequired,
|
||||
contractName: PropTypes.string.isRequired,
|
||||
drizzle: PropTypes.any.isRequired,
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
.execution-section {
|
||||
&.MuiContainer-root {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
padding: 0;
|
||||
|
||||
.input-label {
|
||||
padding-left: 0;
|
||||
flex: initial;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.execution-method {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.input-fields {
|
||||
padding: 0;
|
||||
|
||||
.input-field {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.MuiTextField-root {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.boolean-input {
|
||||
margin-top: 16px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.unsupported-input {
|
||||
margin-top: 16px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.execute-button {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
Container,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select
|
||||
} from '@material-ui/core';
|
||||
|
||||
class BooleanInput extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { value: false };
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
onChange(event) {
|
||||
this.setState({ value: event.target.value });
|
||||
this.props.handleInputChange(event);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Container className='boolean-input'>
|
||||
<InputLabel shrink>
|
||||
{this.props.item.name}
|
||||
</InputLabel>
|
||||
<Select
|
||||
id={this.props.item.name}
|
||||
name={this.props.item.name}
|
||||
onChange={this.onChange}
|
||||
value={this.state.value}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem key='false' value={false}>False</MenuItem>
|
||||
<MenuItem key='true' value={true}>True</MenuItem>
|
||||
</Select>
|
||||
</Container>;
|
||||
}
|
||||
}
|
||||
|
||||
export default BooleanInput;
|
||||
|
||||
BooleanInput.propTypes = {
|
||||
item: PropTypes.any.isRequired,
|
||||
handleInputChange: PropTypes.any.isRequired,
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import BooleanInput from './BooleanInput';
|
||||
import TextInput from './TextInput';
|
||||
|
||||
const InputComponentMapping = {
|
||||
address: TextInput,
|
||||
string: TextInput,
|
||||
uint: TextInput,
|
||||
bool: BooleanInput,
|
||||
};
|
||||
|
||||
export default InputComponentMapping;
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { TextField } from '@material-ui/core';
|
||||
|
||||
class TextInput extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { value: '' };
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
onChange(event) {
|
||||
this.setState({ value: event.target.value });
|
||||
this.props.handleInputChange(event);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <TextField
|
||||
className='input-field'
|
||||
id={this.props.item.name}
|
||||
label={`${this.props.item.name} :`}
|
||||
name={this.props.item.name}
|
||||
onChange={this.onChange}
|
||||
value={this.state.value}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
export default TextInput;
|
||||
|
||||
TextInput.propTypes = {
|
||||
item: PropTypes.any.isRequired,
|
||||
handleInputChange: PropTypes.any.isRequired,
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Container, FormHelperText, FormLabel } from '@material-ui/core';
|
||||
|
||||
class UnsupportedInput extends React.Component {
|
||||
render() {
|
||||
return <Container className='unsupported-input'>
|
||||
<FormHelperText>{this.props.item.name}</FormHelperText>
|
||||
<FormLabel >Unsupported input type</FormLabel>
|
||||
</Container>;
|
||||
}
|
||||
}
|
||||
|
||||
export default UnsupportedInput;
|
||||
|
||||
UnsupportedInput.propTypes = {
|
||||
item: PropTypes.any.isRequired,
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import BooleanInput from './BooleanInput';
|
||||
import InputComponentMapping from './InputComponentMapping';
|
||||
import TextInput from './TextInput';
|
||||
import UnsupportedInput from './UnsupportedInput';
|
||||
|
||||
export {
|
||||
BooleanInput,
|
||||
InputComponentMapping,
|
||||
TextInput,
|
||||
UnsupportedInput,
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import BaseViewComponent from './baseViewComponent/BaseViewComponent';
|
||||
import ContractSelector from './contractSelector/ContractSelector';
|
||||
import EventSection from './eventsSection/EventSection';
|
||||
import ExecutionSection from './executionSection/ExecutionSection';
|
||||
import Interaction from './interaction/Interaction';
|
||||
import LabelWithIcon from './labelWithIcon/LabelWithIcon';
|
||||
import Message from './message/Message';
|
||||
import Metadata from './metadata/Metadata';
|
||||
import StateSection from './stateSection/StateSection';
|
||||
import TextFieldWithTooltip from './textFieldWithTooltip/TextFieldWithTooltip';
|
||||
|
||||
export {
|
||||
BaseViewComponent,
|
||||
ContractSelector,
|
||||
EventSection,
|
||||
ExecutionSection,
|
||||
Interaction,
|
||||
LabelWithIcon,
|
||||
Message,
|
||||
Metadata,
|
||||
StateSection,
|
||||
TextFieldWithTooltip
|
||||
};
|
|
@ -0,0 +1,117 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { Container } from '@material-ui/core';
|
||||
import { deepEqual } from 'helpers';
|
||||
import { DrizzleContext } from 'drizzle-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
BaseViewComponent,
|
||||
EventSection,
|
||||
ExecutionSection,
|
||||
StateSection
|
||||
} from 'components';
|
||||
import './interaction.less';
|
||||
|
||||
const getExecutableMethods = (contract) => {
|
||||
const isExecutableAbiMethod = (abiEl) => (abiEl.type === 'function' && abiEl.constant === false);
|
||||
const executableAbiMethods = contract.abi.filter(isExecutableAbiMethod).map(el => el.name);
|
||||
|
||||
return Object.keys(contract.methods).filter(m => executableAbiMethods.indexOf(m) !== -1);
|
||||
};
|
||||
|
||||
class Interaction extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
methods: undefined,
|
||||
contractName: undefined,
|
||||
subscription: undefined,
|
||||
events: []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.componentDidUpdate();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const contract = this.props.drizzle.contractList[0];
|
||||
|
||||
if (!contract) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { contractName } = contract;
|
||||
const methods = Object.keys(contract);
|
||||
|
||||
const state = {
|
||||
...this.state,
|
||||
methods,
|
||||
contractName
|
||||
};
|
||||
|
||||
if (!deepEqual(this.state, state)) {
|
||||
if (!!this.state.subscription) {
|
||||
this.state.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
const subscription = contract.events.allEvents()
|
||||
.on('data', (event) => this.setState({ events: [...this.state.events, event] }));
|
||||
|
||||
this.setState({ ...state, subscription });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { events } = this.state;
|
||||
|
||||
return <BaseViewComponent
|
||||
header='Interaction'
|
||||
className='interaction component'
|
||||
>
|
||||
<Container className='container'>
|
||||
<Container className='input'>
|
||||
<ExecutionSection
|
||||
actions={getExecutableMethods(this.props.drizzle.contractList[0])}
|
||||
contractName={this.props.drizzle.contractList[0].contractName}
|
||||
drizzle={this.props.drizzle}
|
||||
render={this.renderExecutionSection}
|
||||
/>
|
||||
</Container>
|
||||
<Container className='output'>
|
||||
<StateSection
|
||||
contract={this.props.drizzle.contractList[0]}
|
||||
/>
|
||||
<EventSection
|
||||
events={events}
|
||||
/>
|
||||
</Container>
|
||||
</Container>
|
||||
</BaseViewComponent>;
|
||||
}
|
||||
}
|
||||
|
||||
const HOC = () => <DrizzleContext.Consumer>
|
||||
{props => <Interaction {...props} />}
|
||||
</DrizzleContext.Consumer>;
|
||||
|
||||
export default HOC;
|
||||
|
||||
Interaction.propTypes = {
|
||||
drizzle: PropTypes.shape({
|
||||
contractList: PropTypes.arrayOf(PropTypes.shape({
|
||||
contractName: PropTypes.string.isRequired,
|
||||
methods: PropTypes.object.isRequired,
|
||||
events: PropTypes.shape({
|
||||
allEvents: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
options: PropTypes.shape({
|
||||
isOnline: PropTypes.bool.isRequired
|
||||
}).isRequired
|
||||
})).isRequired
|
||||
}).isRequired,
|
||||
drizzleState: PropTypes.any
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
.interaction.component {
|
||||
.container {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
margin-left: 12px;
|
||||
margin-right: 6px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.output {
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
margin-left: 6px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Tooltip } from '@material-ui/core';
|
||||
import './labelWithIcon.less';
|
||||
|
||||
class LabelWithIcon extends React.Component {
|
||||
render() {
|
||||
const { text, icon } = this.props;
|
||||
|
||||
return <Tooltip title='Copy' placement='top-end'>
|
||||
<div className='label-with-icon'>
|
||||
<span className='text'>{text}</span>
|
||||
{icon}
|
||||
</div>
|
||||
</Tooltip>;
|
||||
}
|
||||
}
|
||||
|
||||
export default LabelWithIcon;
|
||||
|
||||
LabelWithIcon.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
icon: PropTypes.node.isRequired
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
.label-with-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.MuiSvgIcon-root {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { Chip } from '@material-ui/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
class Message extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
className = '',
|
||||
message
|
||||
} = this.props;
|
||||
|
||||
return <Chip
|
||||
className={className}
|
||||
color='secondary'
|
||||
variant='outlined'
|
||||
label={message}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
export default Message;
|
||||
|
||||
Message.propTypes = {
|
||||
className: PropTypes.string,
|
||||
message: PropTypes.string.isRequired
|
||||
};
|
|
@ -0,0 +1,236 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { DrizzleContext } from 'drizzle-react';
|
||||
import { FileCopy } from '@material-ui/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
BaseViewComponent,
|
||||
LabelWithIcon,
|
||||
Message,
|
||||
TextFieldWithTooltip
|
||||
} from 'components';
|
||||
import {
|
||||
Container,
|
||||
TextField
|
||||
} from '@material-ui/core';
|
||||
import { copy, deepEqual } from 'helpers';
|
||||
import './metadata.less';
|
||||
|
||||
class Metadata extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
abi: undefined,
|
||||
address: undefined,
|
||||
bytecode: undefined,
|
||||
events: undefined,
|
||||
network: undefined,
|
||||
showBytecodeTooltip: false,
|
||||
showABITooltip: false,
|
||||
transaction: {
|
||||
from: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.componentDidUpdate();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const contract = this.props.drizzle.contractList[0];
|
||||
|
||||
if (!contract) {
|
||||
return;
|
||||
}
|
||||
|
||||
const abi = JSON.stringify(contract.abi);
|
||||
const address = contract.address;
|
||||
const bytecode = contract.options.data;
|
||||
const events = this.getEvents();
|
||||
const network = this.getNetwork();
|
||||
|
||||
const state = {
|
||||
...this.state,
|
||||
abi,
|
||||
address,
|
||||
bytecode,
|
||||
events,
|
||||
network
|
||||
};
|
||||
|
||||
if (!deepEqual(this.state, state)) {
|
||||
this.getTransactionInfo(state.network.transactionHash)
|
||||
.then((transaction) => this.setState({ ...state, transaction }))
|
||||
.catch(() => this.setState(state));
|
||||
}
|
||||
}
|
||||
|
||||
copyBytecodeToClipboard = () => {
|
||||
copy(this.state.bytecode);
|
||||
this.showBytecodeTooltip();
|
||||
};
|
||||
|
||||
copyABIToClipboard = () => {
|
||||
copy(this.state.abi);
|
||||
this.showABITooltip();
|
||||
}
|
||||
|
||||
getEvents = () => {
|
||||
const { drizzle } = this.props;
|
||||
const { events } = drizzle.contractList[0];
|
||||
|
||||
return Object.keys(events)
|
||||
.find(key => key.match(/0x(\d|\S)+/g)) || '';
|
||||
}
|
||||
|
||||
getNetwork = () => {
|
||||
const { drizzle } = this.props;
|
||||
const contract = drizzle.contractList[0];
|
||||
const { networks } = contract.options;
|
||||
|
||||
const networkKey = Object.keys(networks)
|
||||
.find((key) => networks[key].address === contract.address);
|
||||
|
||||
return Object.assign(networks[networkKey], { id: networkKey });
|
||||
}
|
||||
|
||||
getTransactionInfo = (transactionHash) => {
|
||||
const { drizzle } = this.props;
|
||||
const { getTransaction } = drizzle.contractList[0].web3.eth;
|
||||
|
||||
return getTransaction(transactionHash);
|
||||
}
|
||||
|
||||
showBytecodeTooltip = () => this.setState({ showBytecodeTooltip: true });
|
||||
|
||||
showABITooltip = () => this.setState({ showABITooltip: true });
|
||||
|
||||
hideBytecodeTooltip = () => this.setState({ showBytecodeTooltip: false });
|
||||
|
||||
hideABITooltip = () => this.setState({ showABITooltip: false });
|
||||
|
||||
render() {
|
||||
const {
|
||||
abi,
|
||||
address,
|
||||
events,
|
||||
network,
|
||||
bytecode,
|
||||
transaction,
|
||||
showABITooltip,
|
||||
showBytecodeTooltip
|
||||
} = this.state;
|
||||
|
||||
if (!abi || !bytecode) {
|
||||
return <BaseViewComponent header='Metadata' className='metadata component'>
|
||||
<Message message='Contract not loaded (ABI or bytecode not defined)' />
|
||||
</BaseViewComponent>;
|
||||
}
|
||||
|
||||
const byteCodeLabel = <LabelWithIcon text='Bytecode:' icon={<FileCopy />} />;
|
||||
const ABILabel = <LabelWithIcon text='ABI:' icon={<FileCopy />} />;
|
||||
|
||||
return <BaseViewComponent
|
||||
header='Metadata'
|
||||
className='metadata component'
|
||||
>
|
||||
<Container className='list'>
|
||||
<TextField
|
||||
disabled
|
||||
fullWidth
|
||||
id='contract-address'
|
||||
label='Contract Address:'
|
||||
value={address}
|
||||
className='text field'
|
||||
/>
|
||||
<TextField
|
||||
disabled
|
||||
fullWidth
|
||||
id='creator-account'
|
||||
label='Creator Account:'
|
||||
value={transaction && transaction.from || ''}
|
||||
className='text field'
|
||||
/>
|
||||
<TextField
|
||||
disabled
|
||||
fullWidth
|
||||
id='deployed-location'
|
||||
label='Deployed Location:'
|
||||
value={network.id} // TODO: get network name by network id
|
||||
className='text field'
|
||||
/>
|
||||
<TextFieldWithTooltip
|
||||
open={showBytecodeTooltip}
|
||||
title='Bytecode copied to clipboard!'
|
||||
leaveDelay={900}
|
||||
onClose={this.hideBytecodeTooltip}
|
||||
disabled
|
||||
fullWidth
|
||||
id='bytecode'
|
||||
label={byteCodeLabel}
|
||||
onClick={this.copyBytecodeToClipboard}
|
||||
value={bytecode}
|
||||
className='text field'
|
||||
/>
|
||||
<TextFieldWithTooltip
|
||||
open={showABITooltip}
|
||||
title='ABI copied to clipboard!'
|
||||
leaveDelay={900}
|
||||
onClose={this.hideABITooltip}
|
||||
disabled
|
||||
fullWidth
|
||||
id='abi'
|
||||
label={ABILabel}
|
||||
onClick={this.copyABIToClipboard}
|
||||
value={abi}
|
||||
className='text field'
|
||||
/>
|
||||
<TextField
|
||||
disabled
|
||||
fullWidth
|
||||
id='tx-history'
|
||||
label='TX History:'
|
||||
value={network.transactionHash}
|
||||
className='text field'
|
||||
/>
|
||||
<TextField
|
||||
disabled
|
||||
fullWidth
|
||||
id='events'
|
||||
label='Events:'
|
||||
value={events}
|
||||
className='text field'
|
||||
/>
|
||||
</Container>
|
||||
</BaseViewComponent>;
|
||||
}
|
||||
}
|
||||
|
||||
const HOC = () => <DrizzleContext.Consumer>
|
||||
{props => <Metadata {...props} />}
|
||||
</DrizzleContext.Consumer>;
|
||||
|
||||
export default HOC;
|
||||
|
||||
Metadata.propTypes = {
|
||||
drizzle: PropTypes.shape({
|
||||
contractList: PropTypes.arrayOf(PropTypes.shape({
|
||||
abi: PropTypes.array.isRequired,
|
||||
address: PropTypes.string.isRequired,
|
||||
events: PropTypes.object.isRequired,
|
||||
options: PropTypes.shape({
|
||||
data: PropTypes.string.isRequired,
|
||||
networks: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
web3: PropTypes.shape({
|
||||
eth: PropTypes.shape({
|
||||
getTransaction: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
})).isRequired
|
||||
}).isRequired
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
.metadata.component {
|
||||
.list {
|
||||
.text.field {
|
||||
margin-bottom: 12px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.MuiContainer-root {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import ContractProperty from './contractProperty/ContractProperty';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
class StateSection extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
stateObjects: this.getStateObjects(),
|
||||
};
|
||||
}
|
||||
|
||||
getStateObjects = () => {
|
||||
const abi = this.props.contract.abi;
|
||||
const methods = this.props.contract.methods;
|
||||
|
||||
const constants = abi.filter(element => element.constant);
|
||||
|
||||
const stateObjects = [];
|
||||
for (let i = 0; i < constants.length; i++) {
|
||||
const stateMethod = methods[constants[i].name];
|
||||
const stateObject = Object.assign(
|
||||
constants[i],
|
||||
{ method: stateMethod },
|
||||
{ value: '' });
|
||||
stateObjects.push(stateObject);
|
||||
}
|
||||
|
||||
return stateObjects;
|
||||
}
|
||||
|
||||
renderStateObjects() {
|
||||
return this.state.stateObjects.map((stateObject, index) => {
|
||||
return (
|
||||
<ContractProperty
|
||||
key={index}
|
||||
property={stateObject}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <>
|
||||
{this.renderStateObjects()}
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
export default StateSection;
|
||||
|
||||
StateSection.propTypes = {
|
||||
contract: PropTypes.shape({
|
||||
abi: PropTypes.array.isRequired,
|
||||
methods: PropTypes.object.isRequired,
|
||||
}).isRequired
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { TextField } from '@material-ui/core';
|
||||
|
||||
class ContractProperty extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
property: {
|
||||
name: '',
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getPropertyInfo();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.getPropertyInfo();
|
||||
}
|
||||
|
||||
getPropertyInfo = () => {
|
||||
this.props.property.method().call().then(result => {
|
||||
const property = Object.assign({ name: this.props.property.name, value: result });
|
||||
if (property.name !== this.state.property.name
|
||||
|| property.value !== this.state.property.value) {
|
||||
this.setState({ property });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { property } = this.state;
|
||||
|
||||
return <TextField
|
||||
className='metadata text field'
|
||||
disabled
|
||||
fullWidth
|
||||
id='contract-address'
|
||||
label={property.name}
|
||||
value={property.value}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
export default ContractProperty;
|
||||
|
||||
ContractProperty.propTypes = {
|
||||
property: PropTypes.object.isRequired,
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
TextField,
|
||||
Tooltip
|
||||
} from '@material-ui/core';
|
||||
|
||||
class TextFieldWithTooltip extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
open,
|
||||
title,
|
||||
leaveDelay,
|
||||
onClose,
|
||||
onClick,
|
||||
disabled = false,
|
||||
className = '',
|
||||
fullWidth = false,
|
||||
label,
|
||||
value,
|
||||
placement = 'top'
|
||||
} = this.props;
|
||||
|
||||
return <Tooltip
|
||||
open={open}
|
||||
title={title}
|
||||
leaveDelay={leaveDelay}
|
||||
placement={placement}
|
||||
onClose={onClose}
|
||||
>
|
||||
<TextField
|
||||
id={id}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
fullWidth={fullWidth}
|
||||
onClick={onClick}
|
||||
label={label}
|
||||
value={value}
|
||||
/>
|
||||
</Tooltip>;
|
||||
}
|
||||
}
|
||||
|
||||
export default TextFieldWithTooltip;
|
||||
|
||||
TextFieldWithTooltip.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
leaveDelay: PropTypes.number.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
fullWidth: PropTypes.bool,
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
placement: PropTypes.string
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
export const copy = (text) => {
|
||||
const span = document.createElement('span');
|
||||
span.textContent = text;
|
||||
span.style.whiteSpace = 'pre';
|
||||
|
||||
document.body.appendChild(span);
|
||||
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
|
||||
selection.removeAllRanges();
|
||||
range.selectNode(span);
|
||||
selection.addRange(range);
|
||||
|
||||
document.execCommand('copy');
|
||||
|
||||
selection.removeAllRanges();
|
||||
|
||||
document.body.removeChild(span);
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
export const deepEqual = (first, second) => {
|
||||
if (first === second) return true;
|
||||
|
||||
if (
|
||||
first && second
|
||||
&& typeof first === 'object' && typeof second === 'object'
|
||||
) {
|
||||
if (first.constructor !== second.constructor) return false;
|
||||
|
||||
if (Array.isArray(first) && Array.isArray(second)) {
|
||||
if (first.length !== second.length) return false;
|
||||
|
||||
for (let i = 0; i < first.length; i++) {
|
||||
if (!deepEqual(first[i], second[i])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const firstKeys = Object.keys(first);
|
||||
const secondKeys = Object.keys(second);
|
||||
|
||||
if (firstKeys.length !== secondKeys.length) return false;
|
||||
|
||||
for (const key of firstKeys) {
|
||||
if (!second.hasOwnProperty(key)) return false;
|
||||
}
|
||||
|
||||
for (const key of firstKeys) {
|
||||
if (!deepEqual(first[key], second[key])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { copy } from './copy';
|
||||
import { deepEqual } from './deepEqual';
|
||||
|
||||
export {
|
||||
copy,
|
||||
deepEqual
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Client</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import App from './app';
|
||||
import { Drizzle } from 'drizzle';
|
||||
import { DrizzleContext } from 'drizzle-react';
|
||||
import { LocalStorage } from 'polyfills/localStorage';
|
||||
import { newContextComponents } from 'drizzle-react-components';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
|
||||
const storage = new LocalStorage();
|
||||
|
||||
window.ls = new Proxy(storage, {
|
||||
set: (_, prop, value) => {
|
||||
if (LocalStorage.prototype.hasOwnProperty(prop)) {
|
||||
storage[prop] = value;
|
||||
} else {
|
||||
storage.setItem(prop, value);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
get: (_, name) => {
|
||||
if (LocalStorage.prototype.hasOwnProperty(name)) {
|
||||
return storage[name];
|
||||
}
|
||||
if (storage.values.has(name)) {
|
||||
return storage.getItem(name);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
Drizzle.prototype.deleteAllContracts = function () {
|
||||
Object.keys(this.contracts)
|
||||
.forEach(contractName => this.deleteContract(contractName));
|
||||
};
|
||||
|
||||
newContextComponents.ContractForm.prototype.handleInputChange = function (event) {
|
||||
const value = event.target.type === 'checkbox'
|
||||
? event.target.checked
|
||||
: event.target.value;
|
||||
this.setState({ [event.target.name]: value });
|
||||
};
|
||||
|
||||
const options = { contracts: [] };
|
||||
const drizzle = new Drizzle(options);
|
||||
|
||||
render(
|
||||
<DrizzleContext.Provider drizzle={drizzle}>
|
||||
<App />
|
||||
</DrizzleContext.Provider>,
|
||||
document.getElementById('root')
|
||||
);
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
const getCommandList = commands => commands
|
||||
.trim()
|
||||
.split(' ')
|
||||
.map(command => command.trim());
|
||||
|
||||
const execute = (listeners, command, data) => {
|
||||
if (listeners.has(command)) {
|
||||
const handlers = listeners.get(command);
|
||||
|
||||
handlers.forEach(handler => handler(data));
|
||||
}
|
||||
};
|
||||
|
||||
const addToListeners = (listeners, commands, handler) => {
|
||||
const commandsList = getCommandList(commands);
|
||||
|
||||
commandsList.forEach(command => {
|
||||
if (!listeners.has(command)) {
|
||||
listeners.set(command, []);
|
||||
}
|
||||
|
||||
const handlers = listeners.get(command);
|
||||
|
||||
handlers.push(handler);
|
||||
});
|
||||
};
|
||||
|
||||
const removeFromListeners = (listeners, commands, handler) => {
|
||||
const commandsList = getCommandList(commands);
|
||||
|
||||
commandsList.forEach(command => {
|
||||
if (listeners.has(command)) {
|
||||
if (!handler) {
|
||||
listeners.delete(command);
|
||||
return;
|
||||
}
|
||||
|
||||
let handlers = listeners.get(command);
|
||||
|
||||
handlers = handlers.filter(element => element !== handler);
|
||||
|
||||
listeners.set(command, handlers);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const removeAllListers = (listeners, command) => listeners.delete(command);
|
||||
|
||||
export const Helpers = {
|
||||
execute,
|
||||
addToListeners,
|
||||
removeFromListeners,
|
||||
removeAllListers
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { Helpers } from './helpers';
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
const listeners = new Map();
|
||||
const onceListeners = new Map();
|
||||
|
||||
const on = (commands, handler) => Helpers.addToListeners(listeners, commands, handler);
|
||||
|
||||
const once = (commands, handler) => Helpers.addToListeners(onceListeners, commands, handler);
|
||||
|
||||
const off = (commands, handler) => {
|
||||
Helpers.removeFromListeners(listeners, commands, handler);
|
||||
Helpers.removeFromListeners(onceListeners, commands, handler);
|
||||
};
|
||||
|
||||
const postMessage = (command, value) => vscode.postMessage({ command, value });
|
||||
|
||||
const messageReceived = (event) => {
|
||||
const { command, value: data } = event.data;
|
||||
|
||||
Helpers.execute(listeners, command, data);
|
||||
Helpers.execute(onceListeners, command, data);
|
||||
|
||||
Helpers.removeAllListers(onceListeners, command);
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageReceived, false);
|
||||
|
||||
export const IPCService = {
|
||||
on,
|
||||
once,
|
||||
off,
|
||||
postMessage
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
export class LocalStorage {
|
||||
constructor() {
|
||||
this.values = new Map();
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
const stringKey = String(key);
|
||||
if (this.values.has(stringKey)) {
|
||||
return String(this.values.get(stringKey));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
this.values.set(String(key), String(value));
|
||||
}
|
||||
|
||||
removeItem(key) {
|
||||
this.values.delete(String(key));
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.values.clear();
|
||||
}
|
||||
|
||||
key(i) {
|
||||
return Array.from(this.values.keys())[i];
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.values.size;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import SingleContractView from './singleContractView/SingleContractView';
|
||||
|
||||
export {
|
||||
SingleContractView
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { Container } from '@material-ui/core';
|
||||
import { DrizzleContext } from 'drizzle-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
Interaction,
|
||||
Message,
|
||||
Metadata,
|
||||
} from 'components';
|
||||
import './singleContractView.less';
|
||||
|
||||
class SingleContractView extends React.PureComponent {
|
||||
render() {
|
||||
const { drizzle } = this.props;
|
||||
const contract = drizzle.contractList[0];
|
||||
const { isOnline } = contract.options;
|
||||
|
||||
if (!contract) {
|
||||
return <Message message='No contract'/>;
|
||||
}
|
||||
|
||||
if (!isOnline) {
|
||||
return <Message className='red-error' message='Connection error' />;
|
||||
}
|
||||
|
||||
return <Container className='single contract view'>
|
||||
<Interaction/>
|
||||
<Metadata />
|
||||
</Container>;
|
||||
}
|
||||
}
|
||||
|
||||
const HOC = () => <DrizzleContext.Consumer>
|
||||
{ props => <SingleContractView {...props} /> }
|
||||
</DrizzleContext.Consumer>;
|
||||
|
||||
export default HOC;
|
||||
|
||||
SingleContractView.propTypes = {
|
||||
drizzle: PropTypes.any,
|
||||
drizzleState: PropTypes.any
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
.single.contract.view {
|
||||
& > * {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.MuiChip-outlinedSecondary.red-error {
|
||||
background: #f44336;
|
||||
width: 94.7%;
|
||||
color: #fff !important;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #f44336 !important;
|
||||
margin-top: 15px;
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
const path = require('path');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const ReplacePlugin = require('webpack-plugin-replace');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = () => {
|
||||
const ENV = process.env.NODE_ENV || 'development';
|
||||
const EXT = !!process.env.EXT_ENV || false;
|
||||
const PATH = EXT ? path.join(__dirname, '../resources/drizzle') : path.resolve(__dirname, 'dist');
|
||||
const PUBLIC_PATH = EXT ? '{{root}}/resources/drizzle' : '/';
|
||||
|
||||
const config = {
|
||||
target: 'web',
|
||||
entry: {
|
||||
app: './src/index.js'
|
||||
},
|
||||
output: {
|
||||
path: PATH,
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[name].chunk.js',
|
||||
publicPath: PUBLIC_PATH
|
||||
},
|
||||
devServer: {
|
||||
contentBase: PATH,
|
||||
historyApiFallback: true
|
||||
},
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
stats: {
|
||||
colors: true,
|
||||
chunks: false,
|
||||
modules: false,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
components: path.join(__dirname, './src/components'),
|
||||
helpers: path.join(__dirname, './src/helpers'),
|
||||
polyfills: path.join(__dirname, './src/polyfills'),
|
||||
views: path.join(__dirname, './src/views')
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: [/\.js$/, /\.jsx$/],
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
use: ['style-loader', 'css-loader', 'less-loader']
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: ['style-loader', 'css-loader']
|
||||
},
|
||||
{
|
||||
test: /\.jpe?g$|\.gif$|\.png$|\.ttf$|\.eot$|\.svg$/,
|
||||
use: 'file-loader?name=[name].[ext]?[hash]'
|
||||
},
|
||||
{
|
||||
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
loader: 'url-loader?limit=10000&mimetype=application/fontwoff'
|
||||
},
|
||||
{
|
||||
test: [/\.js$/, /\.jsx$/],
|
||||
exclude: /node_modules/,
|
||||
loader: 'eslint-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
alwaysWriteToDisk: true,
|
||||
template: './src/index.html',
|
||||
inject: 'body',
|
||||
hash: ENV !== 'development',
|
||||
chunks: ['app'],
|
||||
filename: 'index.html'
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].css',
|
||||
allChunks: true,
|
||||
}),
|
||||
new ReplacePlugin({
|
||||
exclude: path.join(__dirname, 'src', 'index.js'),
|
||||
include: true,
|
||||
patterns: [{
|
||||
regex: /localStorage/g,
|
||||
value: 'ls'
|
||||
}],
|
||||
values: {
|
||||
ls: 'ls'
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.AggressiveMergingPlugin(),
|
||||
]
|
||||
};
|
||||
|
||||
switch (ENV) {
|
||||
case 'production':
|
||||
config.devtool = false;
|
||||
config.optimization = { minimize: true };
|
||||
break;
|
||||
case 'development':
|
||||
config.watch = !EXT;
|
||||
config.devtool = 'source-map';
|
||||
break;
|
||||
case 'test':
|
||||
config.devtool = 'source-map';
|
||||
delete config.entry;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
152
package.json
152
package.json
|
@ -5,7 +5,7 @@
|
|||
"publisher": "AzBlockchain",
|
||||
"preview": true,
|
||||
"icon": "images/blockchain-service-logo.png",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.7",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/vscode-azure-blockchain-ethereum"
|
||||
|
@ -46,10 +46,12 @@
|
|||
"onCommand:azureBlockchainService.generateEventPublishingWorkflows",
|
||||
"onCommand:azureBlockchainService.generateDataPublishingWorkflows",
|
||||
"onCommand:azureBlockchainService.connectConsortium",
|
||||
"onCommand:drizzle.generateSmartContractUI",
|
||||
"onCommand:azureBlockchainService.showSmartContractPage",
|
||||
"onCommand:azureBlockchainService.copyRPCEndpointAddress",
|
||||
"onCommand:azureBlockchainService.createConsortium",
|
||||
"onCommand:azureBlockchainService.disconnectConsortium"
|
||||
"onCommand:azureBlockchainService.disconnectConsortium",
|
||||
"onCommand:extension.truffle.debugTransaction",
|
||||
"onDebug"
|
||||
],
|
||||
"contributes": {
|
||||
"commands": [
|
||||
|
@ -118,8 +120,8 @@
|
|||
"category": "Azure Blockchain"
|
||||
},
|
||||
{
|
||||
"command": "drizzle.generateSmartContractUI",
|
||||
"title": "Generate Smart Contract UI",
|
||||
"command": "azureBlockchainService.showSmartContractPage",
|
||||
"title": "Show Smart Contract Interaction Page",
|
||||
"category": "Azure Blockchain"
|
||||
},
|
||||
{
|
||||
|
@ -151,13 +153,26 @@
|
|||
"command": "azureBlockchainService.getPrivateKey",
|
||||
"title": "Retrieve private key",
|
||||
"category": "Azure Blockchain"
|
||||
},
|
||||
{
|
||||
"command": "extension.truffle.debugTransaction",
|
||||
"title": "Debug Transaction",
|
||||
"category": "Azure Blockchain"
|
||||
}
|
||||
],
|
||||
"breakpoints": [
|
||||
{
|
||||
"language": "solidity"
|
||||
},
|
||||
{
|
||||
"language": "sol"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
"when": "false",
|
||||
"command": "drizzle.generateSmartContractUI"
|
||||
"command": "azureBlockchainService.showSmartContractPage"
|
||||
},
|
||||
{
|
||||
"when": "false",
|
||||
|
@ -202,10 +217,6 @@
|
|||
{
|
||||
"when": "azureBlockchainService:isWorkspaceOpen",
|
||||
"command": "truffle.deployContracts"
|
||||
},
|
||||
{
|
||||
"when": "azureBlockchainService:isWorkspaceOpen && false",
|
||||
"command": "drizzle.generateSmartContractUI"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
|
@ -266,8 +277,8 @@
|
|||
"group": "8_buildContractGroup"
|
||||
},
|
||||
{
|
||||
"when": "resourceLangId == json && false",
|
||||
"command": "drizzle.generateSmartContractUI",
|
||||
"when": "resourceLangId == solidity",
|
||||
"command": "azureBlockchainService.showSmartContractPage",
|
||||
"group": "8_buildContractGroup"
|
||||
},
|
||||
{
|
||||
|
@ -318,13 +329,93 @@
|
|||
"id": "AzureBlockchain",
|
||||
"name": "Azure Blockchain"
|
||||
}
|
||||
],
|
||||
"debug": [
|
||||
{
|
||||
"id": "InstructionView",
|
||||
"name": "Instructions"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"debuggers": [
|
||||
{
|
||||
"type": "truffle",
|
||||
"label": "Truffle Debugger",
|
||||
"program": "./out/src/debugger.js",
|
||||
"runtime": "node",
|
||||
"configurationAttributes": {
|
||||
"launch": {
|
||||
"required": [],
|
||||
"properties": {
|
||||
"stopOnEntry": {
|
||||
"type": "boolean",
|
||||
"description": "Automatically stop after launch.",
|
||||
"default": true
|
||||
},
|
||||
"trace": {
|
||||
"type": "boolean",
|
||||
"description": "Enable logging of the Debug Adapter Protocol.",
|
||||
"default": true
|
||||
},
|
||||
"txHash": {
|
||||
"type": "string",
|
||||
"description": "Transaction hash to debug",
|
||||
"default": "0x"
|
||||
},
|
||||
"files": {
|
||||
"type": "string[]",
|
||||
"description": "Array of file paths of solidity files to debug",
|
||||
"default": []
|
||||
},
|
||||
"workingDirectory": {
|
||||
"type": "string",
|
||||
"description": "Directory of truffle box",
|
||||
"default": "${workspaceFolder}"
|
||||
},
|
||||
"providerUrl": {
|
||||
"type": "string",
|
||||
"description": "URL of provider",
|
||||
"default": "http://127.0.0.1:8545"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"initialConfigurations": [
|
||||
{
|
||||
"type": "truffle",
|
||||
"request": "launch",
|
||||
"name": "Debug Transaction with Truffle",
|
||||
"stopOnEntry": false,
|
||||
"txHash": "0x",
|
||||
"files": [],
|
||||
"workingDirectory": "${workspaceFolder}",
|
||||
"providerUrl": "http://127.0.0.1:8545"
|
||||
}
|
||||
],
|
||||
"configurationSnippets": [
|
||||
{
|
||||
"label": "Truffle Debugger: Launch",
|
||||
"description": "Runs the Truffle debugger (truffle) and attaches to a TestRPC instance",
|
||||
"body": {
|
||||
"type": "truffle",
|
||||
"request": "launch",
|
||||
"name": "Debug Transaction with Truffle",
|
||||
"stopOnEntry": false,
|
||||
"txHash": "0x",
|
||||
"files": [],
|
||||
"workingDirectory": "^\"\\${workspaceFolder}/",
|
||||
"providerUrl": "http://127.0.0.1:8545"
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"package": "npx vsce package",
|
||||
"publish": "npx vsce publish",
|
||||
"vscode:prepublish": "npm run webpack:prod",
|
||||
"vscode:prepublish": "npm run preinstall && npm i && npm run webpack:prod && cd ./drizzleUI && npm i && npm run build:ext",
|
||||
"compile": "tsc -p ./",
|
||||
"webpack:prod": "webpack --mode production",
|
||||
"watch": "tsc -watch -p ./",
|
||||
|
@ -332,42 +423,48 @@
|
|||
"tslint": "tslint -t verbose src/**/*.ts test/**/*.ts",
|
||||
"tslint:fix": "npm run tslint -- --fix",
|
||||
"version": "tsc -v",
|
||||
"test": "npm run compile && node ./node_modules/vscode/bin/test"
|
||||
"test": "npm run compile && node ./node_modules/vscode/bin/test",
|
||||
"preinstall": "npm run clean:node_modules",
|
||||
"clean:node_modules": "npx rimraf -- node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@machinomy/types-ethereumjs-wallet": "0.0.12",
|
||||
"@machinomy/types-ethereumjs-wallet": "^0.0.12",
|
||||
"@types/astring": "^1.3.0",
|
||||
"@types/estree": "^0.0.39",
|
||||
"@types/fs-extra": "^7.0.0",
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/hdkey": "^0.7.0",
|
||||
"@types/istanbul": "^0.4.30",
|
||||
"@types/mocha": "^5.2.7",
|
||||
"@types/node": "^10.12.21",
|
||||
"@types/request": "^2.48.1",
|
||||
"@types/request-promise": "^4.1.44",
|
||||
"@types/semver": "^6.0.0",
|
||||
"@types/uuid": "^3.4.4",
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/rewire": "^2.5.28",
|
||||
"@types/semver": "^6.0.0",
|
||||
"@types/sinon": "^7.0.11",
|
||||
"@types/uuid": "^3.4.4",
|
||||
"copy-webpack-plugin": "^5.0.3",
|
||||
"decache": "^4.5.1",
|
||||
"sinon": "^7.3.2",
|
||||
"husky": "^2.4.0",
|
||||
"glob": "^7.1.4",
|
||||
"husky": "^2.4.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"lint-staged": "^8.2.0",
|
||||
"remap-istanbul": "^0.13.0",
|
||||
"rewire": "^4.0.1",
|
||||
"sinon": "^7.3.2",
|
||||
"truffle": "^5.0.14",
|
||||
"ts-loader": "^6.0.2",
|
||||
"tslint": "^5.18.0",
|
||||
"tslint-microsoft-contrib": "^6.2.0",
|
||||
"typescript": "^3.3.1",
|
||||
"vscode": "^1.1.33",
|
||||
"webpack": "^4.30.0",
|
||||
"webpack-cli": "^3.3.1"
|
||||
"vscode-debugadapter": "^1.33.0",
|
||||
"vscode-debugprotocol": "^1.33.0",
|
||||
"webpack": "^4.35.3",
|
||||
"webpack-cli": "^3.3.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"abi-decoder": "^2.0.1",
|
||||
"acorn": "^6.1.1",
|
||||
"acorn-walk": "^6.1.1",
|
||||
"astring": "^1.4.0",
|
||||
|
@ -383,7 +480,14 @@
|
|||
"semver": "^6.0.0",
|
||||
"uuid": "^3.3.2",
|
||||
"vscode-azureextensionui": "^0.25.1",
|
||||
"vscode-extension-telemetry": "^0.1.2"
|
||||
"vscode-extension-telemetry": "^0.1.1",
|
||||
"truffle-debug-utils": "1.0.18",
|
||||
"truffle-debugger": "5.0.19",
|
||||
"truffle-decode-utils": "1.0.14",
|
||||
"truffle-decoder": "3.0.6",
|
||||
"truffle-contract": "4.0.24",
|
||||
"truffle-interface-adapter": "0.2.0",
|
||||
"truffle-provider": "0.1.11"
|
||||
},
|
||||
"extensionDependencies": [
|
||||
"JuanBlanco.solidity",
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { window } from 'vscode';
|
||||
import { Constants } from './Constants';
|
||||
import { vscodeEnvironment } from './helpers';
|
||||
import { Telemetry } from './TelemetryClient';
|
||||
import { ConsortiumView } from './ViewItems';
|
||||
|
||||
export namespace AzureBlockchain {
|
||||
export async function copyRPCEndpointAddress(consortiumNode: ConsortiumView): Promise<void> {
|
||||
Telemetry.sendEvent('AzureBlockchain.copyRPCEndpointAddress.commandStarted');
|
||||
const rpcEndpointAddress = await consortiumNode.getRPCAddress();
|
||||
Telemetry.sendEvent(
|
||||
'AzureBlockchain.copyRPCEndpointAddress.getRPCAddress',
|
||||
{ data: Telemetry.obfuscate(rpcEndpointAddress) },
|
||||
);
|
||||
await AzureBlockchain.addDataInClipboard(Constants.rpcEndpointAddress, rpcEndpointAddress);
|
||||
}
|
||||
|
||||
export async function addDataInClipboard(typeOfData: string, data?: string | null) {
|
||||
if (data) {
|
||||
await vscodeEnvironment.writeToClipboard(data);
|
||||
window.showInformationMessage(typeOfData + Constants.dataCopied);
|
||||
}
|
||||
}
|
||||
|
||||
export async function viewGasEstimates(): Promise<void> {
|
||||
window.showInformationMessage('"View Gas Estimates with Remix" command does not implemented yet.');
|
||||
}
|
||||
}
|
|
@ -88,6 +88,12 @@ export class Constants {
|
|||
};
|
||||
|
||||
public static webViewPages = {
|
||||
contractUI: {
|
||||
path: '',
|
||||
showOnStartup: 'showOnStartupContractUI',
|
||||
title: 'Smart Contract UI',
|
||||
viewType: 'contractUIPage',
|
||||
},
|
||||
requirements: {
|
||||
path: '',
|
||||
showOnStartup: 'showOnStartupRequirementsPage',
|
||||
|
@ -299,12 +305,10 @@ export class Constants {
|
|||
},
|
||||
};
|
||||
|
||||
public static nodeModulesPath = '';
|
||||
|
||||
public static dataCopied = ' copied to clipboard';
|
||||
public static rpcEndpointAddress = 'RPCEndpointAddress';
|
||||
|
||||
public static rpcGanacheMethod = 'net_listening';
|
||||
public static rpcMethods = {
|
||||
netListening: 'net_listening',
|
||||
netVersion: 'net_version',
|
||||
};
|
||||
|
||||
public static ganacheCommandStrings = {
|
||||
cannotStartServer: 'Cannot start ganache server',
|
||||
|
@ -335,6 +339,7 @@ export class Constants {
|
|||
BuildContractsBeforeGenerating: 'Please build contracts before generating',
|
||||
BuildContractsDirIsEmpty: Constants.getMessageContractsBuildDirectoryIsEmpty,
|
||||
BuildContractsDirIsNotExist: Constants.getMessageContractsBuildDirectoryIsNotExist,
|
||||
CompiledContractIsMissing: 'Compiled contract is missing for solidity file.',
|
||||
DirectoryIsNotEmpty: 'Directory is not empty. Open another one?',
|
||||
GetMessageChildAlreadyConnected: Constants.getMessageChildAlreadyConnected,
|
||||
GitIsNotInstalled: 'Git is not installed',
|
||||
|
@ -344,6 +349,7 @@ export class Constants {
|
|||
LoadConsortiumTreeFailed: 'Load consortium tree has failed.',
|
||||
MnemonicFileHaveNoText: 'Mnemonic file have no text',
|
||||
NetworkAlreadyExist: Constants.getMessageNetworkAlreadyExist,
|
||||
NetworkNotFound: Constants.getMessageNetworkNotFound,
|
||||
NewProjectCreationFailed: 'Command createProject has failed.',
|
||||
NoSubscriptionFound: 'No subscription found.',
|
||||
NoSubscriptionFoundClick: 'No subscription found, click an Azure account ' +
|
||||
|
@ -362,6 +368,8 @@ export class Constants {
|
|||
cancelButton: 'Cancel',
|
||||
consortiumDoesNotHaveMemberWithUrl: 'Consortium does not have member with url',
|
||||
consortiumNameValidating: 'Consortium name validating...',
|
||||
contractNotDeployed: 'Contract not deployed yet.',
|
||||
deployButton: 'Deploy',
|
||||
detailsButton: 'Details',
|
||||
generatedLogicApp: 'Generated the logic app!',
|
||||
invalidRequiredVersion: 'Required app is not installed or has an old version.',
|
||||
|
@ -370,6 +378,7 @@ export class Constants {
|
|||
newProjectCreationStarted: 'New project creation is started',
|
||||
openButton: 'Open',
|
||||
privateKeyWasCopiedToClipboard: 'Private key was copied to clipboard',
|
||||
rpcEndpointCopiedToClipboard: 'RPCEndpointAddress copied to clipboard',
|
||||
seeDetailsRequirementsPage: 'Please see details on the Requirements Page',
|
||||
};
|
||||
|
||||
|
@ -404,9 +413,9 @@ export class Constants {
|
|||
public static initialize(context: ExtensionContext) {
|
||||
this.extensionContext = context;
|
||||
this.temporaryDirectory = context.storagePath ? context.storagePath : os.tmpdir();
|
||||
this.webViewPages.contractUI.path = context.asAbsolutePath(path.join('resources', 'drizzle', 'index.html'));
|
||||
this.webViewPages.welcome.path = context.asAbsolutePath(path.join('resources', 'welcome', 'index.html'));
|
||||
this.webViewPages.requirements.path = context.asAbsolutePath(path.join('resources', 'welcome', 'prereqs.html'));
|
||||
this.nodeModulesPath = context.asAbsolutePath('node_modules');
|
||||
|
||||
this.icons.blockchainService = {
|
||||
dark: context.asAbsolutePath(path.join('resources/dark', 'blockchainService.svg')),
|
||||
|
@ -452,6 +461,10 @@ export class Constants {
|
|||
return `Network with name "${networkName}" already existed in truffle-config.js`;
|
||||
}
|
||||
|
||||
private static getMessageNetworkNotFound(networkName: string): string {
|
||||
return `Network with name "${networkName}" not found in truffle-config.js`;
|
||||
}
|
||||
|
||||
private static getMessageVariableShouldBeDefined(variable: string): string {
|
||||
return `${variable} should be defined`;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { ChildProcess, spawn } from 'child_process';
|
||||
import { OutputChannel, window } from 'vscode';
|
||||
import { Constants, RequiredApps } from '../Constants';
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import * as rp from 'request-promise';
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { Constants } from '../Constants';
|
||||
import { HttpService } from '../services';
|
||||
import { Telemetry } from '../TelemetryClient';
|
||||
|
||||
export async function isGanacheServer(port: number | string): Promise<boolean> {
|
||||
try {
|
||||
const response = await sendRPCRequest(port, Constants.rpcGanacheMethod);
|
||||
const response = await HttpService.sendRPCRequest(
|
||||
`http://${Constants.localhost}:${port}`,
|
||||
Constants.rpcMethods.netListening);
|
||||
return response && !!response.result || false;
|
||||
} catch (error) {
|
||||
Telemetry.sendException(error);
|
||||
|
@ -28,21 +33,3 @@ export async function waitGanacheStarted(port: number | string, maxRetries: numb
|
|||
};
|
||||
await retry(0);
|
||||
}
|
||||
|
||||
export async function sendRPCRequest(
|
||||
port: number | string,
|
||||
methodName: string,
|
||||
): Promise<{ result?: any } | undefined> {
|
||||
return rp.post(
|
||||
`http://${Constants.localhost}:${port}`,
|
||||
{
|
||||
body: {
|
||||
jsonrpc: '2.0',
|
||||
method: methodName,
|
||||
params: [],
|
||||
},
|
||||
json: true,
|
||||
})
|
||||
.then((result) => result)
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { vscodeEnvironment } from './helpers';
|
||||
|
||||
export class UiServer {
|
||||
|
||||
public static launchWebview() {
|
||||
const workspacePath = vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : '';
|
||||
const htmlPath = path.join(workspacePath, 'drizzle', 'index.html');
|
||||
|
||||
vscodeEnvironment.openExternal(vscode.Uri.parse(`file://${htmlPath}`));
|
||||
}
|
||||
|
||||
public static async startServer(): Promise<void> {
|
||||
this.launchWebview();
|
||||
}
|
||||
}
|
|
@ -1,10 +1,50 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { UiServer } from '../UiServer';
|
||||
import { commands, ExtensionContext, Uri, window } from 'vscode';
|
||||
import { Constants } from '../Constants';
|
||||
import { CancellationEvent } from '../Models';
|
||||
import { ContractUI } from '../pages/ContractUI';
|
||||
import { ContractDB, ContractService } from '../services';
|
||||
import { Telemetry } from '../TelemetryClient';
|
||||
|
||||
export namespace ContractCommands {
|
||||
export async function generateSmartContractUI(): Promise<void> {
|
||||
return UiServer.startServer();
|
||||
export async function showSmartContractPage(context: ExtensionContext, uri: Uri): Promise<void> {
|
||||
Telemetry.sendEvent('ContractCommands.showSmartContract.commandStarted');
|
||||
const contractName = ContractService.getContractNameBySolidityFile(uri.fsPath);
|
||||
|
||||
let contractHistory = await ContractDB.getContractInstances(contractName);
|
||||
|
||||
// First attempt
|
||||
if (!contractHistory.length) {
|
||||
Telemetry.sendEvent('ContractCommands.showSmartContract.needUserAction');
|
||||
const result = await window.showWarningMessage(
|
||||
Constants.informationMessage.contractNotDeployed,
|
||||
Constants.informationMessage.deployButton,
|
||||
Constants.informationMessage.cancelButton,
|
||||
);
|
||||
|
||||
if (result === Constants.informationMessage.deployButton) {
|
||||
Telemetry.sendEvent('ContractCommands.showSmartContract.deployContracts');
|
||||
await commands.executeCommand('truffle.deployContracts');
|
||||
} else {
|
||||
Telemetry.sendEvent('ContractCommands.showSmartContract.userCancellation');
|
||||
throw new CancellationEvent();
|
||||
}
|
||||
|
||||
contractHistory = await ContractDB.getContractInstances(contractName);
|
||||
}
|
||||
|
||||
// Second attempt after deploy
|
||||
if (!contractHistory.length) {
|
||||
const error = new Error(Constants.errorMessageStrings.CompiledContractIsMissing);
|
||||
Telemetry.sendException(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const contractPage = new ContractUI(context, contractName);
|
||||
|
||||
await contractPage.show();
|
||||
Telemetry.sendEvent('ContractCommands.showSmartContract.commandFinished');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import {
|
||||
debug,
|
||||
DebugConfiguration,
|
||||
QuickPickItem,
|
||||
window,
|
||||
workspace,
|
||||
} from 'vscode';
|
||||
|
||||
import * as path from 'path';
|
||||
import { DEBUG_TYPE } from '../debugAdapter/constants/debugAdapter';
|
||||
import { DebugNetwork } from '../debugAdapter/debugNetwork';
|
||||
import { TransactionProvider } from '../debugAdapter/transaction/transactionProvider';
|
||||
import { Web3Wrapper } from '../debugAdapter/web3Wrapper';
|
||||
|
||||
export async function startSolidityDebugger() {
|
||||
const workingDirectory = getWorkingDirectory();
|
||||
if (!workingDirectory) {
|
||||
return;
|
||||
}
|
||||
const debugNetwork = new DebugNetwork(workingDirectory);
|
||||
await debugNetwork.load();
|
||||
const contractBuildDir = debugNetwork.getTruffleConfiguration()!.contracts_build_directory;
|
||||
const debugNetworkOptions = debugNetwork.getNetwork()!.options;
|
||||
const web3 = new Web3Wrapper(debugNetworkOptions);
|
||||
const providerUrl = web3.getProviderUrl();
|
||||
|
||||
if (debugNetwork.isLocalNetwork()) {
|
||||
// if local network then provide last transactions to choose
|
||||
const transactionProvider = new TransactionProvider(web3, contractBuildDir);
|
||||
const quickPickItems = await getQuickPickItems(transactionProvider);
|
||||
|
||||
const quickPick = window.createQuickPick();
|
||||
quickPick.placeholder = 'Enter the transaction hash to debug';
|
||||
quickPick.ignoreFocusOut = true;
|
||||
const onTransactionSelected = (selection: QuickPickItem[]) => {
|
||||
if (selection[0]) {
|
||||
const txHash = selection[0].label;
|
||||
const config = generateDebugAdapterConfig(txHash, workingDirectory, providerUrl);
|
||||
debug.startDebugging(undefined, config);
|
||||
}
|
||||
quickPick.hide();
|
||||
};
|
||||
|
||||
quickPick.items = quickPickItems;
|
||||
quickPick.onDidChangeSelection(onTransactionSelected);
|
||||
quickPick.onDidHide(() => quickPick.dispose());
|
||||
quickPick.show();
|
||||
} else {
|
||||
// if remote network then require txHash
|
||||
const placeHolder = 'Type the transaction hash you want to debug (0x...)';
|
||||
const txHash = await window.showInputBox({ placeHolder });
|
||||
if (txHash) {
|
||||
const config = generateDebugAdapterConfig(txHash, workingDirectory, providerUrl);
|
||||
debug.startDebugging(undefined, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getQuickPickItems(txProvider: TransactionProvider) {
|
||||
const txHashes = await txProvider.getLastTransactionHashes();
|
||||
const txInfos = await txProvider.getTransactionsInfo(txHashes);
|
||||
|
||||
return txInfos.map((txInfo) => {
|
||||
const description = generateDescription(txInfo.contractName, txInfo.methodName);
|
||||
return { alwaysShow: true, label: txInfo.hash, description } as QuickPickItem;
|
||||
});
|
||||
}
|
||||
|
||||
function getWorkingDirectory() {
|
||||
if (typeof workspace.workspaceFolders === 'undefined' || workspace.workspaceFolders.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return workspace.workspaceFolders[0].uri.fsPath;
|
||||
}
|
||||
|
||||
function generateDebugAdapterConfig(txHash: string, workingDirectory: string, providerUrl: string)
|
||||
: DebugConfiguration {
|
||||
return {
|
||||
files: [],
|
||||
name: 'Debug Transactions',
|
||||
providerUrl,
|
||||
request: 'launch',
|
||||
txHash,
|
||||
type: DEBUG_TYPE,
|
||||
workingDirectory,
|
||||
} as DebugConfiguration;
|
||||
}
|
||||
|
||||
// Migration.json, setComplete => Migration.setComplete()
|
||||
function generateDescription(contractName?: string, methodName?: string) {
|
||||
const contractNameWithoutExt = path.basename(contractName || '', '.json');
|
||||
return `${contractNameWithoutExt}.${methodName}()`;
|
||||
}
|
|
@ -27,8 +27,10 @@ import {
|
|||
MainNetworkConsortium,
|
||||
} from '../Models';
|
||||
import { Output } from '../Output';
|
||||
import { ContractDB } from '../services';
|
||||
import { Telemetry } from '../TelemetryClient';
|
||||
import { ConsortiumTreeManager } from '../treeService/ConsortiumTreeManager';
|
||||
import { ConsortiumView } from '../ViewItems';
|
||||
import { ConsortiumCommands } from './ConsortiumCommands';
|
||||
|
||||
interface IDeployDestination {
|
||||
|
@ -93,22 +95,27 @@ export namespace TruffleCommands {
|
|||
placeHolder: Constants.placeholders.selectDeployDestination,
|
||||
},
|
||||
);
|
||||
|
||||
Telemetry.sendEvent('TruffleCommands.deployContracts.selectedDestination',
|
||||
{
|
||||
cid: Telemetry.obfuscate((command.consortiumId || '').toString()),
|
||||
url: Telemetry.obfuscate((command.description || '').toString()),
|
||||
});
|
||||
|
||||
// this code should be below showQuickPick because it takes time and it affects on responsiveness
|
||||
if (!await required.checkAppsSilent(RequiredApps.truffle)) {
|
||||
Telemetry.sendEvent('TruffleCommands.deployContracts.installTruffle');
|
||||
await required.installTruffle(required.Scope.locally);
|
||||
}
|
||||
|
||||
if (await required.isHdWalletProviderRequired()
|
||||
&& !(await required.checkHdWalletProviderVersion())) {
|
||||
Telemetry.sendEvent('TruffleCommands.deployContracts.installTruffleHdWalletProvider');
|
||||
await required.installTruffleHdWalletProvider();
|
||||
}
|
||||
|
||||
await command.cmd();
|
||||
|
||||
Telemetry.sendEvent('TruffleCommands.deployContracts.commandFinished');
|
||||
}
|
||||
|
||||
|
@ -128,15 +135,15 @@ export namespace TruffleCommands {
|
|||
Telemetry.sendEvent('TruffleCommands.writeBytecodeToBuffer.commandFinished');
|
||||
}
|
||||
|
||||
export async function acquireCompiledContractUri(uri: Uri): Promise<Uri> {
|
||||
if (path.extname(uri.fsPath) === Constants.contractExtension.json) {
|
||||
Telemetry.sendEvent('TruffleCommands.acquireCompiledContractUri.jsonExtension');
|
||||
return uri;
|
||||
} else {
|
||||
const error = new Error(Constants.errorMessageStrings.InvalidContract);
|
||||
Telemetry.sendException(error);
|
||||
throw error;
|
||||
}
|
||||
export async function writeRPCEndpointAddressToBuffer(consortiumNode: ConsortiumView): Promise<void> {
|
||||
Telemetry.sendEvent('TruffleCommands.writeRPCEndpointAddressToBuffer.commandStarted');
|
||||
const rpcEndpointAddress = await consortiumNode.getRPCAddress();
|
||||
Telemetry.sendEvent('TruffleCommands.writeRPCEndpointAddressToBuffer.getRPCAddress',
|
||||
{ data: Telemetry.obfuscate(rpcEndpointAddress) },
|
||||
);
|
||||
|
||||
await vscodeEnvironment.writeToClipboard(rpcEndpointAddress);
|
||||
window.showInformationMessage(Constants.informationMessage.rpcEndpointCopiedToClipboard);
|
||||
}
|
||||
|
||||
export async function getPrivateKeyFromMnemonic(): Promise<void> {
|
||||
|
@ -312,6 +319,8 @@ async function deployToNetwork(networkName: string, truffleConfigPath: string):
|
|||
'npx',
|
||||
RequiredApps.truffle, 'migrate', '--reset', '--network', networkName,
|
||||
);
|
||||
|
||||
await ContractDB.updateContracts();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -336,8 +345,19 @@ async function deployToMainNetwork(networkName: string, truffleConfigPath: strin
|
|||
await deployToNetwork(networkName, truffleConfigPath);
|
||||
}
|
||||
|
||||
async function acquireCompiledContractUri(uri: Uri): Promise<Uri> {
|
||||
if (path.extname(uri.fsPath) !== Constants.contractExtension.json) {
|
||||
const error = new Error(Constants.errorMessageStrings.InvalidContract);
|
||||
Telemetry.sendException(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
Telemetry.sendEvent('TruffleCommands.acquireCompiledContractUri.jsonExtension');
|
||||
return uri;
|
||||
}
|
||||
|
||||
async function readCompiledContract(uri: Uri): Promise<any> {
|
||||
const contractUri = await TruffleCommands.acquireCompiledContractUri(uri);
|
||||
const contractUri = await acquireCompiledContractUri(uri);
|
||||
const data = fs.readFileSync(contractUri.fsPath, null);
|
||||
|
||||
return JSON.parse(data.toString());
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import * as cp from 'child_process';
|
||||
import * as os from 'os';
|
||||
|
||||
// The same implementation as in helpers/truffleConfig.ts
|
||||
// The difference is that all code which uses 'vscode' module is removed.
|
||||
// TODO: think how to reuse code
|
||||
export interface ICommandResult {
|
||||
code: number;
|
||||
cmdOutput: string;
|
||||
cmdOutputIncludingStderr: string;
|
||||
}
|
||||
|
||||
export async function executeCommand(
|
||||
workingDirectory: string | undefined,
|
||||
commands: string,
|
||||
...args: string[]
|
||||
): Promise<string> {
|
||||
const result: ICommandResult = await tryExecuteCommand(workingDirectory, commands, ...args);
|
||||
|
||||
if (result.code !== 0) {
|
||||
throw new Error('Error while executin command: ' + commands.concat(' ', ...args.join(' ')));
|
||||
}
|
||||
|
||||
return result.cmdOutput;
|
||||
}
|
||||
|
||||
async function tryExecuteCommand(workingDirectory: string | undefined, commands: string, ...args: string[])
|
||||
: Promise<ICommandResult> {
|
||||
return new Promise((resolve: (res: any) => void, reject: (error: Error) => void): void => {
|
||||
let cmdOutput: string = '';
|
||||
let cmdOutputIncludingStderr: string = '';
|
||||
|
||||
const options: cp.SpawnOptions = { cwd: workingDirectory || os.tmpdir(), shell: true };
|
||||
const childProcess: cp.ChildProcess = cp.spawn(commands, args, options);
|
||||
|
||||
childProcess.stdout.on('data', (data: string | Buffer) => {
|
||||
data = data.toString();
|
||||
cmdOutput = cmdOutput.concat(data);
|
||||
cmdOutputIncludingStderr = cmdOutputIncludingStderr.concat(data);
|
||||
});
|
||||
|
||||
childProcess.stderr.on('data', (data: string | Buffer) => {
|
||||
data = data.toString();
|
||||
cmdOutputIncludingStderr = cmdOutputIncludingStderr.concat(data);
|
||||
});
|
||||
|
||||
childProcess.on('error', reject);
|
||||
childProcess.on('close', (code: number) => {
|
||||
resolve({
|
||||
cmdOutput,
|
||||
cmdOutputIncludingStderr,
|
||||
code,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import {
|
||||
DebugAdapterDescriptor,
|
||||
DebugAdapterDescriptorFactory,
|
||||
DebugAdapterExecutable,
|
||||
DebugAdapterServer,
|
||||
DebugSession,
|
||||
ProviderResult,
|
||||
} from 'vscode';
|
||||
|
||||
import * as Net from 'net';
|
||||
import { SolidityDebugSession } from '../debugSession';
|
||||
|
||||
export default class TruffleDebugAdapterDescriptorFactory implements DebugAdapterDescriptorFactory {
|
||||
private _server?: Net.Server;
|
||||
|
||||
public createDebugAdapterDescriptor(
|
||||
_session: DebugSession,
|
||||
_executable: DebugAdapterExecutable | undefined): ProviderResult<DebugAdapterDescriptor> {
|
||||
|
||||
if (!this._server) {
|
||||
// start listening on a random port
|
||||
this._server = Net.createServer((socket) => {
|
||||
const debugSession = new SolidityDebugSession();
|
||||
debugSession.setRunAsServer(true);
|
||||
debugSession.start(socket as NodeJS.ReadableStream, socket);
|
||||
}).listen(0);
|
||||
}
|
||||
|
||||
// make VS Code connect to debug server
|
||||
const address: any = this._server.address();
|
||||
return new DebugAdapterServer(address.port);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
if (this._server) {
|
||||
this._server.close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import {
|
||||
CancellationToken,
|
||||
DebugConfiguration,
|
||||
DebugConfigurationProvider,
|
||||
ProviderResult,
|
||||
WorkspaceFolder,
|
||||
} from 'vscode';
|
||||
|
||||
export default class TruffleDebuggerConfigurationProvider implements DebugConfigurationProvider {
|
||||
/**
|
||||
* Massage a debug configuration just before a debug session is being launched,
|
||||
* e.g. add all missing attributes to the debug configuration.
|
||||
*/
|
||||
public resolveDebugConfiguration(
|
||||
_folder: WorkspaceFolder | undefined,
|
||||
config: DebugConfiguration,
|
||||
_token?: CancellationToken): ProviderResult<DebugConfiguration> {
|
||||
return config;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { debug, ExtensionContext } from 'vscode';
|
||||
import { DEBUG_TYPE, EMBED_DEBUG_ADAPTER } from '../constants/debugAdapter';
|
||||
import DebugAdapterTrackerFactory from '../debugAdapterTracker/debugAdapterTrackerFactory';
|
||||
import DebugAdapterDescriptorFactory from './debugAdapterDescriptorFactory';
|
||||
import DebuggerConfigurationProvider from './debugConfigurationProvider';
|
||||
|
||||
export class DebuggerConfiguration {
|
||||
public static initialize(context: ExtensionContext) {
|
||||
const debugConfiProvider = new DebuggerConfigurationProvider();
|
||||
context.subscriptions.push(debug.registerDebugConfigurationProvider(DEBUG_TYPE, debugConfiProvider));
|
||||
|
||||
if (EMBED_DEBUG_ADAPTER) {
|
||||
const factory = new DebugAdapterDescriptorFactory();
|
||||
context.subscriptions.push(debug.registerDebugAdapterDescriptorFactory(DEBUG_TYPE, factory));
|
||||
context.subscriptions.push(factory);
|
||||
}
|
||||
|
||||
const debugAdapterTrackerFactory = new DebugAdapterTrackerFactory();
|
||||
const trackerFactory = debug.registerDebugAdapterTrackerFactory(DEBUG_TYPE, debugAdapterTrackerFactory);
|
||||
context.subscriptions.push(trackerFactory);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,622 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
// The same implementation as in helpers/truffleConfig.ts
|
||||
// The difference is that all code which uses 'vscode' module is removed.
|
||||
// TODO: think how to reuse code
|
||||
import * as acorn from 'acorn';
|
||||
// @ts-ignore
|
||||
import * as walk from 'acorn-walk';
|
||||
import { generate } from 'astring';
|
||||
import * as bip39 from 'bip39';
|
||||
import * as crypto from 'crypto';
|
||||
import * as ESTree from 'estree';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
export namespace ConfigurationReader {
|
||||
const notAllowedSymbols = new RegExp(
|
||||
/`|~|!|@|#|\$|%|\^|&|\*|\(|\)|\+|-|=|\[|{|]|}|\||\\|'|<|,|>|\?|\/|""|;|:|"|№|\s/g,
|
||||
);
|
||||
|
||||
interface IFound {
|
||||
node: ESTree.Node;
|
||||
state: string;
|
||||
}
|
||||
|
||||
export interface IProvider {
|
||||
mnemonic?: string;
|
||||
raw?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface INetworkOption {
|
||||
/**
|
||||
* Ethereum public network
|
||||
* if "*" - match any network
|
||||
*/
|
||||
network_id: string | number;
|
||||
port?: number;
|
||||
host?: string;
|
||||
/**
|
||||
* You will need this enabled to use the confirmations listener
|
||||
* or to hear Events using .on or .once. Default is false.
|
||||
*/
|
||||
websockets?: boolean;
|
||||
/**
|
||||
* Gas limit used for deploys. Default is 4712388.
|
||||
*/
|
||||
gas?: number;
|
||||
/**
|
||||
* Gas price used for deploys. Default is 100000000000 (100 Shannon).
|
||||
*/
|
||||
gasPrice?: number;
|
||||
/**
|
||||
* From address used during migrations. Defaults to the first available account provided by your Ethereum client.
|
||||
*/
|
||||
from?: string;
|
||||
/**
|
||||
* Function that returns a web3 provider instance.
|
||||
* web3 provider instance Truffle should use to talk to the Ethereum network.
|
||||
* if specified, host and port are ignored.
|
||||
* Default web3 provider using host and port options: new Web3.providers.HttpProvider("http://<host>:<port>")
|
||||
*/
|
||||
provider?: IProvider;
|
||||
/**
|
||||
* true if you don't want to test run the migration locally before the actual migration (default is false)
|
||||
*/
|
||||
skipDryRun?: boolean;
|
||||
/**
|
||||
* if a transaction is not mined, keep waiting for this number of blocks (default is 50)
|
||||
*/
|
||||
timeoutBlocks?: number;
|
||||
/**
|
||||
* This identifier needs just for out extension.
|
||||
*/
|
||||
consortium_id?: number;
|
||||
}
|
||||
|
||||
export interface INetwork {
|
||||
name: string;
|
||||
options: INetworkOption;
|
||||
}
|
||||
|
||||
export interface ISolCompiler {
|
||||
/**
|
||||
* A version or constraint - Ex. "^0.5.0" . Can also be set to "native" to use a native solc
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Use a version obtained through docker
|
||||
*/
|
||||
docker?: boolean;
|
||||
settings?: {
|
||||
optimizer?: {
|
||||
enabled: boolean,
|
||||
/**
|
||||
* Optimize for how many times you intend to run the code
|
||||
*/
|
||||
runs: number,
|
||||
},
|
||||
/**
|
||||
* Default: "byzantium"
|
||||
*/
|
||||
evmVersion?: string,
|
||||
};
|
||||
}
|
||||
|
||||
export interface IExternalCompiler {
|
||||
command: string;
|
||||
workingDirectory: string;
|
||||
targets: object[];
|
||||
}
|
||||
|
||||
export interface IConfiguration {
|
||||
contracts_directory: string;
|
||||
contracts_build_directory: string;
|
||||
migrations_directory: string;
|
||||
networks?: INetwork[];
|
||||
compilers?: {
|
||||
solc?: ISolCompiler;
|
||||
external?: IExternalCompiler;
|
||||
};
|
||||
}
|
||||
|
||||
export function generateMnemonic(): string {
|
||||
return bip39.entropyToMnemonic(crypto.randomBytes(16).toString('hex'));
|
||||
}
|
||||
|
||||
/**
|
||||
* looking for truffle config named in old style
|
||||
* and rename it to truffle-config.js
|
||||
*/
|
||||
export function checkTruffleConfigNaming(workspaceRoot: string): void {
|
||||
// old-style of truffle config naming
|
||||
if (fs.pathExistsSync(path.join(workspaceRoot, 'truffle.js'))) {
|
||||
fs.renameSync(
|
||||
path.join(workspaceRoot, 'truffle.js'),
|
||||
path.join(workspaceRoot, 'truffle-config.js'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class TruffleConfig {
|
||||
private readonly ast: ESTree.Node;
|
||||
|
||||
constructor(private readonly filePath: string) {
|
||||
const file = fs.readFileSync(this.filePath, 'utf8');
|
||||
this.ast = acorn.parse(file, {
|
||||
allowHashBang: true,
|
||||
allowReserved: true,
|
||||
sourceType: 'module',
|
||||
}) as ESTree.Node;
|
||||
}
|
||||
|
||||
public getAST(): ESTree.Node {
|
||||
return this.ast;
|
||||
}
|
||||
|
||||
public writeAST(): void {
|
||||
return fs.writeFileSync(this.filePath, generate(this.ast, { comments: true }));
|
||||
}
|
||||
|
||||
public getNetworks(): INetwork[] {
|
||||
const moduleExports = getModuleExportsObjectExpression(this.ast);
|
||||
|
||||
if (moduleExports) {
|
||||
const networksNode = findProperty(moduleExports, 'networks');
|
||||
if (networksNode && networksNode.value.type === 'ObjectExpression') {
|
||||
return astToNetworks(networksNode.value);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public setNetworks(network: INetwork): void {
|
||||
const moduleExports = getModuleExportsObjectExpression(this.ast);
|
||||
|
||||
if (moduleExports) {
|
||||
let networksNode = findProperty(moduleExports, 'networks');
|
||||
if (!networksNode) {
|
||||
networksNode = generateProperty('networks', generateObjectExpression());
|
||||
moduleExports.properties.push(networksNode);
|
||||
}
|
||||
|
||||
if (networksNode.value.type === 'ObjectExpression') {
|
||||
const isExist = findProperty(networksNode.value, network.name);
|
||||
if (isExist) {
|
||||
throw new Error('Network already exists');
|
||||
} else {
|
||||
const networkNode = generateProperty(network.name, generateObjectExpression());
|
||||
networkNode.value = networkOptionsToAst(network);
|
||||
networksNode.value.properties.push(networkNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.writeAST();
|
||||
}
|
||||
|
||||
public importPackage(variableName: string, packageName: string): void {
|
||||
const packageRequired: IFound = walk.findNodeAt(this.ast, null, null, isVarDeclaration(variableName));
|
||||
if (!packageRequired) {
|
||||
const declaration = generateVariableDeclaration(variableName, 'require', packageName);
|
||||
(this.ast as ESTree.Program).body.unshift(declaration);
|
||||
this.writeAST();
|
||||
}
|
||||
}
|
||||
|
||||
public getConfiguration(): IConfiguration {
|
||||
const moduleExports = getModuleExportsObjectExpression(this.ast);
|
||||
|
||||
if (moduleExports) {
|
||||
return astToConfiguration(moduleExports);
|
||||
}
|
||||
|
||||
return getDefaultConfiguration();
|
||||
}
|
||||
|
||||
public isHdWalletProviderDeclared(): boolean {
|
||||
try {
|
||||
const moduleExports = walk.findNodeAt(this.ast, null, null, isHdWalletProviderDeclaration);
|
||||
return !!moduleExports;
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isHdWalletProviderDeclaration(nodeType: string, node: ESTree.Node): boolean {
|
||||
if (nodeType !== 'NewExpression') {
|
||||
return false;
|
||||
}
|
||||
node = node as ESTree.NewExpression;
|
||||
node = node.callee as ESTree.Identifier;
|
||||
return node.name === 'HDWalletProvider';
|
||||
}
|
||||
|
||||
function getModuleExportsObjectExpression(ast: ESTree.Node): ESTree.ObjectExpression | void {
|
||||
const moduleExports: IFound = walk.findNodeAt(ast, null, null, isModuleExportsExpression);
|
||||
|
||||
if (moduleExports && moduleExports.node.type === 'ExpressionStatement') {
|
||||
const rightExpression = (moduleExports.node.expression as ESTree.AssignmentExpression).right;
|
||||
|
||||
if (rightExpression.type === 'ObjectExpression') {
|
||||
return rightExpression;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultConfiguration(): IConfiguration {
|
||||
return {
|
||||
contracts_build_directory: path.join('./', 'build', 'contracts'),
|
||||
contracts_directory: path.join('./', 'contracts'),
|
||||
migrations_directory: path.join('./', 'migrations'),
|
||||
};
|
||||
}
|
||||
|
||||
function isModuleExportsExpression(nodeType: string, node: ESTree.Node): boolean {
|
||||
if (nodeType !== 'ExpressionStatement') {
|
||||
return false;
|
||||
}
|
||||
|
||||
node = node as ESTree.ExpressionStatement;
|
||||
|
||||
if (
|
||||
node.expression.type === 'AssignmentExpression' &&
|
||||
node.expression.left.type === 'MemberExpression' &&
|
||||
node.expression.left.object.type === 'Identifier' &&
|
||||
node.expression.left.object.name === 'module'
|
||||
) {
|
||||
if (
|
||||
(node.expression.left.property.type === 'Identifier' && node.expression.left.property.name === 'exports') ||
|
||||
(node.expression.left.property.type === 'Literal' && node.expression.left.property.value === 'module')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isHDWalletProvider(nodeType: string, node: ESTree.Node): boolean {
|
||||
if (nodeType === 'NewExpression') {
|
||||
node = node as ESTree.NewExpression;
|
||||
if (node.callee.type === 'Identifier' && node.callee.name === 'HDWalletProvider') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isVarDeclaration(varName: string): (nodeType: string, node: ESTree.Node) => boolean {
|
||||
return (nodeType: string, node: ESTree.Node) => {
|
||||
if (nodeType === 'VariableDeclaration') {
|
||||
node = node as ESTree.VariableDeclaration;
|
||||
if (node.declarations[0].type === 'VariableDeclarator'
|
||||
&& (node.declarations[0].id as ESTree.Identifier).name === varName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
function findProperty(node: ESTree.Node, propertyName: string): ESTree.Property | void {
|
||||
if (node.type === 'ObjectExpression') {
|
||||
node = node as ESTree.ObjectExpression;
|
||||
|
||||
return node.properties.find((property: ESTree.Property) => {
|
||||
return (property.key.type === 'Identifier' && property.key.name === propertyName) ||
|
||||
(property.key.type === 'Literal' && `${property.key.value}` === propertyName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function astToNetworkOptions(node: ESTree.ObjectExpression): INetworkOption {
|
||||
const options: INetworkOption = {
|
||||
network_id: '*',
|
||||
};
|
||||
|
||||
const id = findProperty(node, 'network_id');
|
||||
if (id && id.value.type === 'Literal' &&
|
||||
(typeof id.value.value === 'string' || typeof id.value.value === 'number')) {
|
||||
options.network_id = id.value.value;
|
||||
}
|
||||
|
||||
const port = findProperty(node, 'port');
|
||||
if (port && port.value.type === 'Literal' && typeof port.value.value === 'number') {
|
||||
options.port = port.value.value;
|
||||
}
|
||||
|
||||
const host = findProperty(node, 'host');
|
||||
if (host && host.value.type === 'Literal' && typeof host.value.value === 'string') {
|
||||
options.host = host.value.value;
|
||||
}
|
||||
|
||||
const websockets = findProperty(node, 'websockets');
|
||||
if (websockets && websockets.value.type === 'Literal' && typeof websockets.value.value === 'boolean') {
|
||||
options.websockets = websockets.value.value;
|
||||
}
|
||||
|
||||
const gas = findProperty(node, 'gas');
|
||||
if (gas && gas.value.type === 'Literal' && typeof gas.value.value === 'number') {
|
||||
options.gas = gas.value.value;
|
||||
}
|
||||
|
||||
const gasPrice = findProperty(node, 'gasPrice');
|
||||
if (gasPrice && gasPrice.value.type === 'Literal' && typeof gasPrice.value.value === 'number') {
|
||||
options.gasPrice = gasPrice.value.value;
|
||||
}
|
||||
|
||||
const from = findProperty(node, 'from');
|
||||
if (from && from.value.type === 'Literal' && typeof from.value.value === 'string') {
|
||||
options.from = from.value.value;
|
||||
}
|
||||
|
||||
const skipDryRun = findProperty(node, 'skipDryRun');
|
||||
if (skipDryRun && skipDryRun.value.type === 'Literal' && typeof skipDryRun.value.value === 'boolean') {
|
||||
options.skipDryRun = skipDryRun.value.value;
|
||||
}
|
||||
|
||||
const timeoutBlocks = findProperty(node, 'timeoutBlocks');
|
||||
if (timeoutBlocks && timeoutBlocks.value.type === 'Literal' && typeof timeoutBlocks.value.value === 'number') {
|
||||
options.timeoutBlocks = timeoutBlocks.value.value;
|
||||
}
|
||||
|
||||
const provider = findProperty(node, 'provider');
|
||||
if (provider && provider.value.type === 'FunctionExpression') {
|
||||
const hdWalletProvider: IFound = walk.findNodeAt(provider, null, null, isHDWalletProvider);
|
||||
if (hdWalletProvider && hdWalletProvider.node.type === 'NewExpression') {
|
||||
options.provider = astToHDWalletProvider(hdWalletProvider.node);
|
||||
}
|
||||
}
|
||||
|
||||
if (provider && provider.value.type === 'NewExpression') {
|
||||
options.provider = astToHDWalletProvider(provider.value);
|
||||
}
|
||||
|
||||
const consortiumId = findProperty(node, 'consortium_id');
|
||||
if (consortiumId && consortiumId.value.type === 'Literal' && typeof consortiumId.value.value === 'number') {
|
||||
options.consortium_id = consortiumId.value.value;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function networkOptionsToAst(network: INetwork): ESTree.ObjectExpression {
|
||||
const obj: ESTree.ObjectExpression = {
|
||||
properties: [],
|
||||
type: 'ObjectExpression',
|
||||
};
|
||||
const options = network.options;
|
||||
|
||||
if (options.network_id !== undefined) {
|
||||
obj.properties.push(generateProperty('network_id', generateLiteral(options.network_id)));
|
||||
}
|
||||
|
||||
if (options.port !== undefined) {
|
||||
obj.properties.push(generateProperty('port', generateLiteral(options.port)));
|
||||
}
|
||||
|
||||
if (options.host !== undefined) {
|
||||
obj.properties.push(generateProperty('host', generateLiteral(options.host)));
|
||||
}
|
||||
|
||||
if (options.websockets !== undefined) {
|
||||
obj.properties.push(generateProperty('websockets', generateLiteral(options.websockets)));
|
||||
}
|
||||
|
||||
if (options.gas !== undefined) {
|
||||
obj.properties.push(generateProperty('gas', generateLiteral(options.gas)));
|
||||
}
|
||||
|
||||
if (options.gasPrice !== undefined) {
|
||||
obj.properties.push(generateProperty('gasPrice', generateLiteral(options.gasPrice)));
|
||||
}
|
||||
|
||||
if (options.from !== undefined) {
|
||||
obj.properties.push(generateProperty('from', generateLiteral(options.from)));
|
||||
}
|
||||
|
||||
if (options.skipDryRun !== undefined) {
|
||||
obj.properties.push(generateProperty('skipDryRun', generateLiteral(options.skipDryRun)));
|
||||
}
|
||||
|
||||
if (options.timeoutBlocks !== undefined) {
|
||||
obj.properties.push(generateProperty('timeoutBlocks', generateLiteral(options.timeoutBlocks)));
|
||||
}
|
||||
|
||||
if (options.provider !== undefined) {
|
||||
obj.properties.push(generateProperty('provider', hdWalletProviderToAst(options.provider)));
|
||||
}
|
||||
|
||||
if (options.consortium_id !== undefined) {
|
||||
obj.properties.push(generateProperty('consortium_id', generateLiteral(options.consortium_id)));
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function astToHDWalletProvider(node: ESTree.NewExpression): IProvider {
|
||||
const provider: IProvider = {
|
||||
raw: generate(node),
|
||||
};
|
||||
|
||||
const mnemonicNode = node.arguments[0];
|
||||
if (mnemonicNode && mnemonicNode.type === 'Literal') {
|
||||
provider.mnemonic = '' + mnemonicNode.value;
|
||||
}
|
||||
|
||||
const urlNode = node.arguments[1];
|
||||
if (urlNode && urlNode.type === 'Literal') {
|
||||
provider.url = '' + urlNode.value;
|
||||
}
|
||||
|
||||
if (urlNode && urlNode.type !== 'Literal') {
|
||||
provider.url = generate(urlNode);
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
function hdWalletProviderToAst(provider: IProvider): ESTree.NewExpression {
|
||||
return {
|
||||
arguments: [
|
||||
generateFsReadExpression('fs.readFileSync', (provider.mnemonic || '').replace(/\\/g, '\\\\')),
|
||||
generateLiteral(provider.url || ''),
|
||||
],
|
||||
callee: {
|
||||
name: 'HDWalletProvider',
|
||||
type: 'Identifier',
|
||||
},
|
||||
type: 'NewExpression',
|
||||
};
|
||||
}
|
||||
|
||||
function astToConfiguration(node: ESTree.ObjectExpression): IConfiguration {
|
||||
const configuration = getDefaultConfiguration();
|
||||
|
||||
const contractsDir = findProperty(node, 'contracts_directory');
|
||||
if (contractsDir && contractsDir.value.type === 'Literal' &&
|
||||
typeof contractsDir.value.value === 'string' && contractsDir.value.value) {
|
||||
configuration.contracts_directory = contractsDir.value.value;
|
||||
}
|
||||
|
||||
const contractsBuildDir = findProperty(node, 'contracts_build_directory');
|
||||
if (contractsBuildDir && contractsBuildDir.value.type === 'Literal' &&
|
||||
typeof contractsBuildDir.value.value === 'string' && contractsBuildDir.value.value) {
|
||||
configuration.contracts_build_directory = contractsBuildDir.value.value;
|
||||
}
|
||||
|
||||
const migrationsDir = findProperty(node, 'migrations_directory');
|
||||
if (migrationsDir && migrationsDir.value.type === 'Literal' &&
|
||||
typeof migrationsDir.value.value === 'string' && migrationsDir.value.value) {
|
||||
configuration.migrations_directory = migrationsDir.value.value;
|
||||
}
|
||||
|
||||
const networks = findProperty(node, 'networks');
|
||||
if (networks && networks.value.type === 'ObjectExpression') {
|
||||
configuration.networks = astToNetworks(networks.value);
|
||||
}
|
||||
|
||||
// TODO: compilers
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
function astToNetworks(node: ESTree.ObjectExpression): INetwork[] {
|
||||
const networks: INetwork[] = [];
|
||||
|
||||
node.properties.forEach((property: ESTree.Property) => {
|
||||
if (property.key.type === 'Identifier') {
|
||||
networks.push({
|
||||
name: property.key.name,
|
||||
options: astToNetworkOptions(property.value as ESTree.ObjectExpression),
|
||||
});
|
||||
}
|
||||
|
||||
if (property.key.type === 'Literal') {
|
||||
networks.push({
|
||||
name: '' + property.key.value,
|
||||
options: astToNetworkOptions(property.value as ESTree.ObjectExpression),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return networks;
|
||||
}
|
||||
|
||||
function generateProperty(name: string, value: ESTree.Expression): ESTree.Property {
|
||||
notAllowedSymbols.lastIndex = 0;
|
||||
const isLiteral = notAllowedSymbols.test(name);
|
||||
|
||||
return {
|
||||
computed: false,
|
||||
key: isLiteral ? generateLiteral(name) : generateIdentifier(name),
|
||||
kind: 'init',
|
||||
method: false,
|
||||
shorthand: false,
|
||||
type: 'Property',
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
function generateObjectExpression(): ESTree.ObjectExpression {
|
||||
return {
|
||||
properties: [],
|
||||
type: 'ObjectExpression',
|
||||
};
|
||||
}
|
||||
|
||||
function generateIdentifier(name: string): ESTree.Identifier {
|
||||
return {
|
||||
name,
|
||||
type: 'Identifier',
|
||||
};
|
||||
}
|
||||
|
||||
function generateLiteral(value: string | number | boolean | null): ESTree.Literal {
|
||||
return {
|
||||
raw: JSON.stringify(value),
|
||||
type: 'Literal',
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
function generateFsReadExpression(operator: string, args: string): ESTree.CallExpression {
|
||||
const call = {
|
||||
arguments: [
|
||||
{
|
||||
raw: `\'${args}\'`,
|
||||
type: 'Literal',
|
||||
value: `${args}`,
|
||||
},
|
||||
{
|
||||
raw: "\'utf-8\'",
|
||||
type: 'Literal',
|
||||
value: 'utf-8',
|
||||
},
|
||||
],
|
||||
callee: {
|
||||
name: operator,
|
||||
type: 'Identifier',
|
||||
},
|
||||
type: 'CallExpression',
|
||||
};
|
||||
return call as ESTree.CallExpression;
|
||||
}
|
||||
|
||||
function generateVariableDeclaration(varName: string, loader: string, loaderArg: string): ESTree.VariableDeclaration {
|
||||
const declaration = {
|
||||
declarations: [
|
||||
{
|
||||
id: {
|
||||
name: varName,
|
||||
type: 'Identifier',
|
||||
},
|
||||
init: {
|
||||
arguments: [
|
||||
{
|
||||
raw: `\'${loaderArg}\'`,
|
||||
type: 'Literal',
|
||||
value: `${loaderArg}`,
|
||||
},
|
||||
],
|
||||
callee: {
|
||||
name: loader,
|
||||
type: 'Identifier',
|
||||
},
|
||||
type: 'CallExpression',
|
||||
},
|
||||
type: 'VariableDeclarator',
|
||||
},
|
||||
],
|
||||
kind: 'const',
|
||||
type: 'VariableDeclaration',
|
||||
};
|
||||
return declaration as ESTree.VariableDeclaration;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export const CONTRACT_JSON_EXTENSION = '.json';
|
||||
export const CONTRACT_JSON_ENCODING = 'utf-8';
|
|
@ -0,0 +1,39 @@
|
|||
export const EVENT_TYPES = {
|
||||
breakpointValidated: 'breakpointValidated',
|
||||
end: 'end',
|
||||
launched: 'launched',
|
||||
stopOnBreakpoint: 'stopOnBreakpoint',
|
||||
stopOnEntry: 'stopOnEntry',
|
||||
stopOnException: 'stopOnException',
|
||||
stopOnStepIn: 'stopOnStepIn',
|
||||
stopOnStepOut: 'stopOnStepOut',
|
||||
stopOnStepOver: 'stopOnStepOver',
|
||||
stopped: 'stopped',
|
||||
};
|
||||
|
||||
export const EVENT_REASONS = {
|
||||
breakpoint: 'breakpoint',
|
||||
changed: 'changed',
|
||||
entry: 'entry',
|
||||
exception: 'exception',
|
||||
stepIn: 'stepin',
|
||||
stepOut: 'stepout',
|
||||
stepOver: 'step',
|
||||
};
|
||||
|
||||
// we don't support multiple threads, so we can use a hardcoded ID for the default thread
|
||||
export const MAIN_THREAD = {
|
||||
id: 1,
|
||||
name: 'thread 1',
|
||||
};
|
||||
|
||||
export const EVALUATE_REQUEST_TYPES = {
|
||||
hover: 'hover',
|
||||
watch: 'watch',
|
||||
};
|
||||
|
||||
export const DEBUG_TYPE = 'truffle';
|
||||
|
||||
export const EMBED_DEBUG_ADAPTER = !!(typeof(IS_BUNDLE_TIME) === 'undefined');
|
||||
|
||||
export const ERROR_MESSAGE_ID = 1;
|
|
@ -0,0 +1,2 @@
|
|||
export const GET_INSTRUCTIONS = 'requestInstructions';
|
||||
export const GET_CURRENT_INSTRUCTION = 'requestCurrentInstruction';
|
|
@ -0,0 +1,3 @@
|
|||
export const TRANSACTION_NUMBER_TO_SHOW = 5;
|
||||
export const TRANSACTION_DEFAULT_METHOD_NAME = 'constructor';
|
||||
export const LAST_BLOCK_QUERY = 'latest';
|
|
@ -0,0 +1,2 @@
|
|||
export const TRUFFLE_CONFIG_NAME = 'truffle-config.js';
|
||||
export const TRUFFLE_CONFIG_DEBUG_NETWORK_TYPE = 'development';
|
|
@ -0,0 +1,14 @@
|
|||
const VARIABLE_REFERANCE = {
|
||||
all: 1,
|
||||
dynamicVariables: 2,
|
||||
};
|
||||
|
||||
export const SCOPES = {
|
||||
all: {
|
||||
name: 'All',
|
||||
ref: VARIABLE_REFERANCE.all,
|
||||
},
|
||||
dynamicVariables: {
|
||||
ref: VARIABLE_REFERANCE.dynamicVariables,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,70 @@
|
|||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { CONTRACT_JSON_ENCODING } from '../constants/contractJson';
|
||||
import { IContractJsonModel } from '../models/IContractJsonModel';
|
||||
|
||||
export class ContractJsonsProvider {
|
||||
private contractBuildDirectory: string;
|
||||
private contractJsonEncoding: string;
|
||||
private _cachedContractJsons: { [fileName: string]: IContractJsonModel } | undefined;
|
||||
|
||||
constructor(contractBuildDirectory: string, contractJsonEncoding = CONTRACT_JSON_ENCODING) {
|
||||
this.contractBuildDirectory = contractBuildDirectory;
|
||||
this.contractJsonEncoding = contractJsonEncoding;
|
||||
}
|
||||
|
||||
public isDirectoryExist(): Promise<boolean> {
|
||||
return new Promise(((accept: any, reject: any) => {
|
||||
fs.pathExists(this.contractBuildDirectory, (err: any, isExist) => {
|
||||
if (!err) {
|
||||
accept(isExist);
|
||||
} else {
|
||||
reject(`Error while reading ${this.contractBuildDirectory}`);
|
||||
}
|
||||
});
|
||||
}).bind(this));
|
||||
}
|
||||
|
||||
public async getJsonsContents(): Promise<{ [fileName: string]: IContractJsonModel }> {
|
||||
if (!this._cachedContractJsons) {
|
||||
const isDirectoryExist = await this.isDirectoryExist();
|
||||
if (!isDirectoryExist) {
|
||||
this._cachedContractJsons = {};
|
||||
} else {
|
||||
const dir = this.contractBuildDirectory;
|
||||
const response = new Promise<{ [fileName: string]: IContractJsonModel }>((accept) => {
|
||||
fs.readdir(dir, async (error: any, files: any[]) => {
|
||||
if (error) {
|
||||
throw new Error(`Error occured while reading directory ${dir}`);
|
||||
}
|
||||
const result: { [fileName: string]: IContractJsonModel } = {};
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(dir, file);
|
||||
const content = await this.readFile(fullPath, this.contractJsonEncoding);
|
||||
try {
|
||||
result[file] = JSON.parse(content) as IContractJsonModel;
|
||||
} catch (e) {
|
||||
throw new Error(`Error while parsing ${fullPath}. Invalid json file.`);
|
||||
}
|
||||
}
|
||||
accept(result);
|
||||
});
|
||||
});
|
||||
this._cachedContractJsons = await response;
|
||||
}
|
||||
}
|
||||
|
||||
return this._cachedContractJsons;
|
||||
}
|
||||
|
||||
private readFile(filePath: string, encoding: string): Promise<string> {
|
||||
return new Promise((accept) => {
|
||||
fs.readFile(filePath, encoding, (error: any, content: string) => {
|
||||
if (error) {
|
||||
throw new Error(`Error occured while reading file ${filePath}. ${error}`);
|
||||
}
|
||||
accept(content);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { DebugNetwork } from '../debugNetwork';
|
||||
import { IContractModel } from '../models/IContractModel';
|
||||
import { Web3Wrapper } from '../web3Wrapper';
|
||||
import { ContractJsonsProvider } from './contractJsonsProvider';
|
||||
|
||||
export async function prepareContracts(workingDirectory: any) {
|
||||
// TODO, the same code in the debuggerCommands.ts, do refactoring
|
||||
const debugNetwork = new DebugNetwork(workingDirectory);
|
||||
await debugNetwork.load();
|
||||
const contractBuildDir = debugNetwork.getTruffleConfiguration()!.contracts_build_directory;
|
||||
const debugNetworkOptions = debugNetwork.getNetwork()!.options;
|
||||
const web3 = new Web3Wrapper(debugNetworkOptions);
|
||||
const provider = await web3.getProvider();
|
||||
const debugNetworkId = await web3.getNetworkId();
|
||||
|
||||
const contracts: IContractModel[] = [];
|
||||
const contractBuildsProvider = new ContractJsonsProvider(contractBuildDir);
|
||||
const contractJsons = await contractBuildsProvider.getJsonsContents();
|
||||
Object.keys(contractJsons).forEach((fileName) => {
|
||||
const contractJson = contractJsons[fileName];
|
||||
const contractModel = {
|
||||
address: contractJson.networks
|
||||
&& contractJson.networks[debugNetworkId]
|
||||
&& contractJson.networks[debugNetworkId].address,
|
||||
binary: contractJson.bytecode,
|
||||
deployedBinary: contractJson.deployedBytecode,
|
||||
...contractJson,
|
||||
} as IContractModel;
|
||||
contracts.push(contractModel);
|
||||
});
|
||||
|
||||
return {
|
||||
contracts,
|
||||
provider,
|
||||
};
|
||||
}
|
||||
|
||||
export function filterContractsWithAddress(contracts: IContractModel[]): IContractModel[] {
|
||||
return contracts.filter((c) => c.address);
|
||||
}
|
||||
|
||||
// Base contracts are not deployed as particular contract that's why they don't have address
|
||||
export function filterBaseContracts(contracts: IContractModel[]): IContractModel[] {
|
||||
return contracts.filter((c) => !c.address);
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { DebugAdapterTracker, DebugSession, window } from 'vscode';
|
||||
import InstructionView from '../instructionsView/instructionView';
|
||||
|
||||
import { EVENT_TYPES } from '../constants/debugAdapter';
|
||||
import { GET_CURRENT_INSTRUCTION, GET_INSTRUCTIONS } from '../constants/debugSessionCommands';
|
||||
import { IInstruction } from '../models/IInstruction';
|
||||
|
||||
export default class SolidityDebugAdapterTracker implements DebugAdapterTracker {
|
||||
private session: DebugSession;
|
||||
private instructionView: InstructionView;
|
||||
|
||||
constructor(session: DebugSession) {
|
||||
this.session = session;
|
||||
this.instructionView = new InstructionView();
|
||||
}
|
||||
|
||||
public onDidSendMessage(message: any): void {
|
||||
if (message.success === false) {
|
||||
window.showErrorMessage('Error occured in debug mode: ' + message.body.error.format);
|
||||
return;
|
||||
}
|
||||
switch (message.event) {
|
||||
case EVENT_TYPES.launched: // init instructions after launch
|
||||
this.requestForInstructions();
|
||||
return;
|
||||
case EVENT_TYPES.stopped: // get current instruction on every stop event
|
||||
this.requestForCurrentInstruction();
|
||||
return;
|
||||
}
|
||||
switch (message.command) {
|
||||
case GET_INSTRUCTIONS:
|
||||
this.updateInstructionView(message.body.instructions);
|
||||
return;
|
||||
case GET_CURRENT_INSTRUCTION:
|
||||
this.revealInstruction(message.body.currentInstruction);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private requestForInstructions() {
|
||||
this.session.customRequest(GET_INSTRUCTIONS);
|
||||
}
|
||||
|
||||
private requestForCurrentInstruction() {
|
||||
this.session.customRequest(GET_CURRENT_INSTRUCTION);
|
||||
}
|
||||
|
||||
private updateInstructionView(instructions: IInstruction[]) {
|
||||
this.instructionView.update(instructions);
|
||||
}
|
||||
|
||||
private revealInstruction(instruction: IInstruction) {
|
||||
this.instructionView.revealInstruction(instruction);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import {
|
||||
DebugAdapterTracker,
|
||||
DebugAdapterTrackerFactory,
|
||||
DebugSession,
|
||||
ProviderResult,
|
||||
} from 'vscode';
|
||||
import SolidityDebugAdapterTracker from './debugAdapterTracker';
|
||||
|
||||
export default class SolidityDebugAdapterTrackerFactory implements DebugAdapterTrackerFactory {
|
||||
public createDebugAdapterTracker(session: DebugSession): ProviderResult<DebugAdapterTracker> {
|
||||
return new SolidityDebugAdapterTracker(session);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
import * as path from 'path';
|
||||
import { executeCommand } from './cmdCommandExecutor';
|
||||
import { ConfigurationReader } from './configurationReader';
|
||||
import { TRUFFLE_CONFIG_DEBUG_NETWORK_TYPE, TRUFFLE_CONFIG_NAME } from './constants/truffleConfig';
|
||||
|
||||
export class DebugNetwork {
|
||||
public workingDirectory: string;
|
||||
private _basedConfig: ConfigurationReader.TruffleConfig;
|
||||
private _truffleConfiguration: ConfigurationReader.IConfiguration | undefined;
|
||||
private _networkForDebug: ConfigurationReader.INetwork | undefined;
|
||||
constructor(truffleConfigDirectory: string) {
|
||||
this._basedConfig =
|
||||
new ConfigurationReader.TruffleConfig(path.join(truffleConfigDirectory, TRUFFLE_CONFIG_NAME));
|
||||
this.workingDirectory = truffleConfigDirectory;
|
||||
}
|
||||
|
||||
public async load(): Promise<void> {
|
||||
this._truffleConfiguration = this.loadConfiguration();
|
||||
this._networkForDebug = await this.loadNetworkForDebug();
|
||||
}
|
||||
|
||||
public getTruffleConfiguration() {
|
||||
return this._truffleConfiguration;
|
||||
}
|
||||
|
||||
public getNetwork() {
|
||||
return this._networkForDebug;
|
||||
}
|
||||
|
||||
// Port and host are defined
|
||||
public isLocalNetwork() {
|
||||
if (!this._networkForDebug || !this._networkForDebug.options) {
|
||||
throw new Error('Network is not defined. Try to call this.load()');
|
||||
}
|
||||
const options = this._networkForDebug.options;
|
||||
return !!(options.host && options.port);
|
||||
}
|
||||
|
||||
private loadConfiguration(): ConfigurationReader.IConfiguration {
|
||||
const configuration = this._basedConfig.getConfiguration();
|
||||
configuration.contracts_build_directory =
|
||||
path.join(this.workingDirectory, configuration.contracts_build_directory);
|
||||
configuration.contracts_directory =
|
||||
path.join(this.workingDirectory, configuration.contracts_directory);
|
||||
configuration.migrations_directory =
|
||||
path.join(this.workingDirectory, configuration.migrations_directory);
|
||||
return configuration;
|
||||
}
|
||||
|
||||
private async loadNetworkForDebug(): Promise<ConfigurationReader.INetwork> {
|
||||
const networks = this._basedConfig.getNetworks();
|
||||
const networkForDebug = networks
|
||||
.find((n) => n.name === TRUFFLE_CONFIG_DEBUG_NETWORK_TYPE);
|
||||
if (!this.isNetworkForDebugValid(networkForDebug)) {
|
||||
const provider = await this.getProviderByResolvingConfig(TRUFFLE_CONFIG_DEBUG_NETWORK_TYPE);
|
||||
if (provider.url) {
|
||||
networkForDebug!.options.provider = {
|
||||
url: provider.url,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Truffle config is not properly defined.
|
||||
Please create ${TRUFFLE_CONFIG_DEBUG_NETWORK_TYPE} network,
|
||||
set host+port, or hdwallet/http/websocket provider.`);
|
||||
}
|
||||
}
|
||||
|
||||
return networkForDebug!;
|
||||
}
|
||||
|
||||
private isNetworkForDebugValid(networkForDebug: ConfigurationReader.INetwork | undefined): boolean {
|
||||
if (!networkForDebug || !networkForDebug.options) {
|
||||
throw new Error(`No ${TRUFFLE_CONFIG_DEBUG_NETWORK_TYPE} network in the truffle config`);
|
||||
}
|
||||
|
||||
if (networkForDebug.options.host && networkForDebug.options.port) {
|
||||
return true;
|
||||
}
|
||||
// truffle-config helper can read only hdwallet provider
|
||||
const isHdWalletProvider = !!networkForDebug.options.provider;
|
||||
if (isHdWalletProvider) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async getProviderByResolvingConfig(network: string) {
|
||||
// use truffle exec web3ProviderResolver.js to solve http- or websocket- web3 provider
|
||||
const truffleConfigReaderPath = path.join(__dirname, 'web3ProviderResolver.js');
|
||||
const args = [ 'truffle', 'exec', truffleConfigReaderPath, '--network', network ];
|
||||
const result = await executeCommand(this.workingDirectory, 'npx', ...args);
|
||||
const providerJson = result.split('provider%=')[1];
|
||||
return JSON.parse(providerJson);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,287 @@
|
|||
import { basename } from 'path';
|
||||
import {
|
||||
Breakpoint, BreakpointEvent, InitializedEvent, logger, Logger, LoggingDebugSession,
|
||||
Source, StackFrame, StoppedEvent, TerminatedEvent, Thread,
|
||||
} from 'vscode-debugadapter';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import {
|
||||
ERROR_MESSAGE_ID,
|
||||
EVALUATE_REQUEST_TYPES,
|
||||
EVENT_REASONS,
|
||||
EVENT_TYPES,
|
||||
MAIN_THREAD,
|
||||
} from './constants/debugAdapter';
|
||||
import {
|
||||
GET_CURRENT_INSTRUCTION,
|
||||
GET_INSTRUCTIONS,
|
||||
} from './constants/debugSessionCommands';
|
||||
import { DebuggerTypes } from './models/debuggerTypes';
|
||||
import RuntimeInterface from './runtimeInterface';
|
||||
import VariablesHandler from './variablesHandler';
|
||||
|
||||
export class SolidityDebugSession extends LoggingDebugSession {
|
||||
private _runtime: RuntimeInterface;
|
||||
private _variablesHandler: VariablesHandler;
|
||||
|
||||
public constructor() {
|
||||
super('debugAdapter.txt');
|
||||
// this debugger uses zero-based lines and columns
|
||||
this.setDebuggerLinesStartAt1(false);
|
||||
this.setDebuggerColumnsStartAt1(false);
|
||||
|
||||
this._runtime = new RuntimeInterface();
|
||||
this._variablesHandler = new VariablesHandler(this._runtime);
|
||||
|
||||
this._runtime.on(EVENT_TYPES.stopOnEntry, () => {
|
||||
this.sendEvent(new StoppedEvent(EVENT_REASONS.entry, MAIN_THREAD.id));
|
||||
});
|
||||
this._runtime.on(EVENT_TYPES.stopOnStepOver, () => {
|
||||
this.sendEvent(new StoppedEvent(EVENT_REASONS.stepOver, MAIN_THREAD.id));
|
||||
});
|
||||
this._runtime.on(EVENT_TYPES.stopOnStepIn, () => {
|
||||
this.sendEvent(new StoppedEvent(EVENT_REASONS.stepIn, MAIN_THREAD.id));
|
||||
});
|
||||
this._runtime.on(EVENT_TYPES.stopOnStepOut, () => {
|
||||
this.sendEvent(new StoppedEvent(EVENT_REASONS.stepOut, MAIN_THREAD.id));
|
||||
});
|
||||
this._runtime.on(EVENT_TYPES.stopOnBreakpoint, () => {
|
||||
this.sendEvent(new StoppedEvent(EVENT_REASONS.breakpoint, MAIN_THREAD.id));
|
||||
});
|
||||
this._runtime.on(EVENT_TYPES.stopOnException, () => {
|
||||
this.sendEvent(new StoppedEvent(EVENT_REASONS.exception, MAIN_THREAD.id));
|
||||
});
|
||||
this._runtime.on(EVENT_TYPES.breakpointValidated, (bp: DebuggerTypes.IBreakpoint) => {
|
||||
this.sendEvent(
|
||||
new BreakpointEvent(EVENT_REASONS.changed, { verified: true, id: bp.id } as DebugProtocol.Breakpoint));
|
||||
});
|
||||
this._runtime.on(EVENT_TYPES.end, () => {
|
||||
this.sendEvent(new TerminatedEvent());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The 'initialize' request is the first request called by the frontend
|
||||
* to interrogate the features the debug adapter provides.
|
||||
*/
|
||||
protected initializeRequest(response: DebugProtocol.InitializeResponse,
|
||||
/* args: DebugProtocol.InitializeRequestArguments */): void {
|
||||
// build and return the capabilities of this debug adapter:
|
||||
response.body = response.body || {};
|
||||
|
||||
// TODO: enable it if needed. (if the adapter implements the configurationDoneRequest.)
|
||||
response.body.supportsConfigurationDoneRequest = false;
|
||||
response.body.supportsEvaluateForHovers = true;
|
||||
|
||||
// TODO: think about implementation of stepback
|
||||
// protected stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments)
|
||||
response.body.supportsStepBack = false;
|
||||
|
||||
response.body.supportsSetVariable = false;
|
||||
response.body.supportsSetExpression = false;
|
||||
|
||||
this.sendResponse(response);
|
||||
|
||||
// since this debug adapter can accept configuration requests like 'setBreakpoint' at any time,
|
||||
// we request them early by sending an 'initializeRequest' to the frontend.
|
||||
// The frontend will end the configuration sequence by calling 'configurationDone' request.
|
||||
this.sendEvent(new InitializedEvent());
|
||||
}
|
||||
|
||||
protected async launchRequest(response: DebugProtocol.LaunchResponse,
|
||||
args: DebuggerTypes.ILaunchRequestArguments): Promise<void> {
|
||||
await this.sendErrorIfFailed(response, async () => {
|
||||
// make sure to 'Stop' the buffered logging if 'trace' is not set
|
||||
// logger.setup enable logs in client
|
||||
logger.setup(args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Stop, false);
|
||||
|
||||
this.sendResponse(response);
|
||||
// start the program in the runtime
|
||||
await this._runtime.attach(args.txHash, args.workingDirectory);
|
||||
await this._runtime.processInitialBreakPoints();
|
||||
|
||||
// Events order is important
|
||||
this.sendEvent(new DebuggerTypes.LaunchedEvent());
|
||||
this.sendEvent(new StoppedEvent(EVENT_REASONS.breakpoint, MAIN_THREAD.id));
|
||||
});
|
||||
}
|
||||
|
||||
protected async disconnectRequest(response: DebugProtocol.DisconnectResponse,
|
||||
/* args: DebugProtocol.DisconnectArguments */) {
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected async setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse,
|
||||
args: DebugProtocol.SetBreakpointsArguments): Promise<void> {
|
||||
await this.sendErrorIfFailed(response, async () => {
|
||||
const path = args.source.path as string;
|
||||
const clientLines = args.lines || [];
|
||||
// set and verify breakpoint locations
|
||||
const actualBreakpoints: DebugProtocol.Breakpoint[] = [];
|
||||
await this._runtime.clearBreakpoints();
|
||||
|
||||
for (const clientLine of clientLines) {
|
||||
const debuggerBreakpoint =
|
||||
await this._runtime.setBreakpoint(path, this.convertClientLineToDebugger(clientLine));
|
||||
if (debuggerBreakpoint) {
|
||||
const bp = new Breakpoint(true,
|
||||
this.convertDebuggerLineToClient(debuggerBreakpoint.line)) as DebugProtocol.Breakpoint;
|
||||
bp.id = debuggerBreakpoint.id;
|
||||
actualBreakpoints.push(bp);
|
||||
}
|
||||
}
|
||||
|
||||
// setBreakPointsRequest is hit before runtime debugger is attached
|
||||
if (!this._runtime.isDebuggerAttached) {
|
||||
const debuggerLines = clientLines.map((l: number) => this.convertClientLineToDebugger(l));
|
||||
this._runtime.storeInitialBreakPoints(path, debuggerLines);
|
||||
}
|
||||
|
||||
// send back the actual breakpoint positions
|
||||
response.body = {
|
||||
breakpoints: actualBreakpoints,
|
||||
};
|
||||
this.sendResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
protected threadsRequest(response: DebugProtocol.ThreadsResponse): void {
|
||||
// return a default thread
|
||||
response.body = {
|
||||
threads: [ new Thread(MAIN_THREAD.id, MAIN_THREAD.name) ],
|
||||
};
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected async stackTraceRequest(response: DebugProtocol.StackTraceResponse,
|
||||
/* args: DebugProtocol.StackTraceArguments */): Promise<void> {
|
||||
await this.sendErrorIfFailed(response, async () => {
|
||||
let callStack: any[] = [];
|
||||
callStack = this._runtime.callStack();
|
||||
if (callStack !== null) {
|
||||
const stackFrames = callStack.reverse().map((c: any) => {
|
||||
return new StackFrame(0, c.isCurrent ? 'Current Call' : 'Previous Call', this.createSource(c.file),
|
||||
this.convertDebuggerLineToClient(c.line), c.column);
|
||||
});
|
||||
response.body = {
|
||||
stackFrames,
|
||||
totalFrames: 1,
|
||||
};
|
||||
} else {
|
||||
response.body = {
|
||||
stackFrames: [],
|
||||
totalFrames: 0,
|
||||
};
|
||||
}
|
||||
|
||||
this.sendResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
protected scopesRequest(response: DebugProtocol.ScopesResponse,
|
||||
/* args: DebugProtocol.ScopesArguments */): void {
|
||||
response.body = {
|
||||
scopes: this._variablesHandler.getScopes(),
|
||||
};
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected async variablesRequest(response: DebugProtocol.VariablesResponse,
|
||||
args: DebugProtocol.VariablesArguments): Promise<void> {
|
||||
const variables =
|
||||
await this._variablesHandler.getVariableAttributesByVariableRef(args.variablesReference);
|
||||
|
||||
response.body = {
|
||||
variables,
|
||||
};
|
||||
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected async continueRequest(response: DebugProtocol.ContinueResponse,
|
||||
/* args: DebugProtocol.ContinueArguments */): Promise<void> {
|
||||
await this.sendErrorIfFailed(response, async () => {
|
||||
await this._runtime.continue();
|
||||
this.sendResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
protected reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse,
|
||||
/* args: DebugProtocol.ReverseContinueArguments */): void {
|
||||
this._runtime.continueReverse();
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected async nextRequest(response: DebugProtocol.NextResponse,
|
||||
/* args: DebugProtocol.NextArguments */): Promise<void> {
|
||||
await this.sendErrorIfFailed(response, async () => {
|
||||
await this._runtime.stepNext();
|
||||
this.sendResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
protected async stepInRequest(response: DebugProtocol.StepInResponse,
|
||||
/* args: DebugProtocol.StepInArguments */): Promise<void> {
|
||||
await this.sendErrorIfFailed(response, async () => {
|
||||
await this._runtime.stepIn();
|
||||
this.sendResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
protected async stepOutRequest(response: DebugProtocol.StepOutResponse,
|
||||
/* args: DebugProtocol.StepOutArguments */): Promise<void> {
|
||||
await this.sendErrorIfFailed(response, async () => {
|
||||
await this._runtime.stepOut();
|
||||
this.sendResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
protected async evaluateRequest(response: DebugProtocol.EvaluateResponse,
|
||||
args: DebugProtocol.EvaluateArguments): Promise<void> {
|
||||
await this.sendErrorIfFailed(response, async () => {
|
||||
if (args.context === EVALUATE_REQUEST_TYPES.watch
|
||||
|| args.context === EVALUATE_REQUEST_TYPES.hover
|
||||
|| args.context === undefined) {
|
||||
const { result, variablesReference } =
|
||||
await this._variablesHandler.evaluateExpression(args.expression);
|
||||
response.body = { result, variablesReference };
|
||||
this.sendResponse(response);
|
||||
} else {
|
||||
response.body = { result: '', variablesReference: -1 };
|
||||
this.sendResponse(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// is invoked via debugAdaterTrackerFactory
|
||||
protected async customRequest(command: string, response: DebugProtocol.Response/*, args: any*/): Promise<void> {
|
||||
await this.sendErrorIfFailed(response, async () => {
|
||||
switch (command) {
|
||||
case GET_INSTRUCTIONS:
|
||||
response.body = {
|
||||
instructions: this._runtime.getInstructionSteps(),
|
||||
};
|
||||
break;
|
||||
case GET_CURRENT_INSTRUCTION:
|
||||
response.body = {
|
||||
currentInstruction: this._runtime.getCurrentInstructionStep(),
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
this.sendResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
private createSource(filePath: string): Source {
|
||||
return new Source(basename(filePath),
|
||||
this.convertDebuggerPathToClient(filePath), undefined, undefined, null);
|
||||
}
|
||||
|
||||
private async sendErrorIfFailed(response: DebugProtocol.Response, fn: () => {}) {
|
||||
try {
|
||||
await fn();
|
||||
} catch (e) {
|
||||
this.sendErrorResponse(response, { id: ERROR_MESSAGE_ID, format: e.message }, '', null, undefined);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// from "D:/test/test1.sol" -> "d:\test\test1.sol"
|
||||
export function toWindowsPath(filePath: string): string {
|
||||
const res = filePath.replace(/\//g, '\\');
|
||||
return res[0].toLowerCase() + res.substr(1);
|
||||
}
|
||||
|
||||
export function groupBy(array: any[], key: any) {
|
||||
const a = array.reduce((rv, x) => {
|
||||
(rv[x[key]] = rv[x[key]] || []).push(x);
|
||||
return rv;
|
||||
}, {});
|
||||
return a;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import { IInstruction } from '../models/IInstruction';
|
||||
import InstructionTreeNode from './instructionTreeNode';
|
||||
|
||||
export default class InstructionDataManager {
|
||||
private instructionObject: any = {};
|
||||
|
||||
public load(instructions: IInstruction[]) {
|
||||
this.instructionObject = this.mapInstructionsArrayToObject(instructions);
|
||||
}
|
||||
|
||||
public getChidren(element?: InstructionTreeNode): string[] {
|
||||
if (!element) {
|
||||
return Object.keys(this.instructionObject).map((k) => k);
|
||||
}
|
||||
|
||||
const item = this.getItemByPath(element.getPaths());
|
||||
if (item === undefined) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(item);
|
||||
}
|
||||
|
||||
public getItem(element: InstructionTreeNode): any {
|
||||
return this.getItemByPath(element.getPaths());
|
||||
}
|
||||
|
||||
public getItemParent(element: InstructionTreeNode): any {
|
||||
return this.getItemByPath(element.getParentPaths());
|
||||
}
|
||||
|
||||
// Map from array to object in order to used in view
|
||||
// [{pc:1,op: ''},{pc:2,op:''}] ==> { 1:{pc:1,op:''}, 2:{pc:2,op:''}}
|
||||
private mapInstructionsArrayToObject(steps: IInstruction[]): any {
|
||||
const res: any = {};
|
||||
steps.forEach((s) => {
|
||||
res[s.pc] = s;
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private getItemByPath(paths: string[]): any {
|
||||
if (paths.length === 0) {
|
||||
return void 0;
|
||||
}
|
||||
let item = this.instructionObject;
|
||||
paths.forEach((key) => {
|
||||
item = item[key];
|
||||
});
|
||||
return item;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { Event, EventEmitter, ProviderResult, TreeDataProvider, TreeItem, TreeItemCollapsibleState } from 'vscode';
|
||||
import { IInstruction } from '../models/IInstruction';
|
||||
import InstructionDataManager from './instructionDataManager';
|
||||
import InstructionTreeNode from './instructionTreeNode';
|
||||
|
||||
export default class InstructionDataProvider implements TreeDataProvider<InstructionTreeNode> {
|
||||
public _onDidChangeTreeData: EventEmitter<InstructionTreeNode> = new EventEmitter<InstructionTreeNode>();
|
||||
public readonly onDidChangeTreeData: Event<InstructionTreeNode> = this._onDidChangeTreeData.event;
|
||||
|
||||
private instructionDataManager: InstructionDataManager;
|
||||
|
||||
constructor(instructionDataManager: InstructionDataManager) {
|
||||
this.instructionDataManager = instructionDataManager;
|
||||
}
|
||||
|
||||
public refresh(newInstructions: IInstruction[]) {
|
||||
this.instructionDataManager.load(newInstructions);
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
public getChildren(element?: InstructionTreeNode): ProviderResult<InstructionTreeNode[]> {
|
||||
const items = this.instructionDataManager.getChidren(element);
|
||||
return items.map((e) => new InstructionTreeNode(e, element));
|
||||
}
|
||||
|
||||
public getTreeItem(element: InstructionTreeNode): TreeItem {
|
||||
const item = this.instructionDataManager.getItem(element);
|
||||
const isSpecificObjectValueType = this.isSpecificObjectValueType(item);
|
||||
const collapsibleState = isSpecificObjectValueType
|
||||
? TreeItemCollapsibleState.Collapsed
|
||||
: TreeItemCollapsibleState.None;
|
||||
|
||||
return {
|
||||
collapsibleState,
|
||||
id: element.getId(),
|
||||
label: this.generateTreeItemLabel(element.getKey(), item, isSpecificObjectValueType),
|
||||
};
|
||||
}
|
||||
|
||||
public getParent(element: InstructionTreeNode): ProviderResult<InstructionTreeNode> {
|
||||
return element.getParent();
|
||||
}
|
||||
|
||||
private generateTreeItemLabel(treeItemKey: string, treeItemValue: any, isSpecificObjectValueType: boolean) {
|
||||
return isSpecificObjectValueType
|
||||
? treeItemKey
|
||||
: `${treeItemKey}: ${JSON.stringify(treeItemValue)}`;
|
||||
}
|
||||
|
||||
// TODO: refactroign - same method in variablesHandler
|
||||
private isSpecificObjectValueType(item: any) {
|
||||
return !Array.isArray(item)
|
||||
&& item !== null
|
||||
&& item !== undefined
|
||||
&& typeof(item) === 'object';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { IInstruction } from '../models/IInstruction';
|
||||
|
||||
export default class InstructionTreeNode {
|
||||
private key: string;
|
||||
private path: string;
|
||||
private parent?: InstructionTreeNode;
|
||||
|
||||
constructor(key?: string, parent?: InstructionTreeNode, instructionData?: IInstruction) {
|
||||
if (!key && (!instructionData || (!instructionData.pc && instructionData.pc !== 0))) {
|
||||
throw new Error('Incorrect input params');
|
||||
}
|
||||
|
||||
const nodeKey = instructionData
|
||||
? instructionData.pc.toString()
|
||||
: (key || '');
|
||||
this.key = nodeKey;
|
||||
this.parent = parent;
|
||||
this.path = this.generatePath(nodeKey, parent);
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
public getKey(): string {
|
||||
return this.key;
|
||||
}
|
||||
|
||||
public getParent(): InstructionTreeNode | undefined {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
public getParentPaths(): string[] {
|
||||
return this.path.split(/\//).slice(1, this.path.length - 1); // not take first empty element and last
|
||||
}
|
||||
|
||||
public getPaths(): string[] {
|
||||
return this.path.split(/\//).slice(1); // not take first empty element
|
||||
}
|
||||
|
||||
private generatePath(key: string, parent?: InstructionTreeNode) {
|
||||
const parentPath = parent ? parent.path : '';
|
||||
return `${parentPath}/${key}`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { TreeView, window } from 'vscode';
|
||||
import { IInstruction } from '../models/IInstruction';
|
||||
import InstructionDataManager from './instructionDataManager';
|
||||
import InstructionDataProvider from './instructionDataProvider';
|
||||
import InstructionTreeNode from './instructionTreeNode';
|
||||
|
||||
const INSTRUCTION_VIEW_ID = 'InstructionView';
|
||||
|
||||
export default class InstructionView {
|
||||
private view: TreeView<InstructionTreeNode>;
|
||||
private dataProvider: InstructionDataProvider;
|
||||
constructor() {
|
||||
const dataManager = new InstructionDataManager();
|
||||
this.dataProvider = new InstructionDataProvider(dataManager);
|
||||
this.view = window.createTreeView(INSTRUCTION_VIEW_ID,
|
||||
{ treeDataProvider: this.dataProvider, showCollapseAll: true });
|
||||
}
|
||||
|
||||
public update(newInstructions: IInstruction[]) {
|
||||
this.dataProvider.refresh(newInstructions);
|
||||
}
|
||||
|
||||
public revealInstruction(instruction: IInstruction) {
|
||||
if (this.view.visible) {
|
||||
const treeNode = new InstructionTreeNode(undefined, undefined, instruction);
|
||||
this.view.reveal(treeNode, { focus: true, select: true, expand: true });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
export interface IContractJsonModel {
|
||||
abi: [];
|
||||
ast: any;
|
||||
bytecode: string;
|
||||
compiler: any;
|
||||
contractName: string;
|
||||
deployedBytecode: string;
|
||||
deployedSourceMap: string;
|
||||
source: string;
|
||||
sourceMap: string;
|
||||
sourcePath: string;
|
||||
networks: {
|
||||
[networkId: string]: {
|
||||
address: string;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
export interface IContractModel {
|
||||
address?: string;
|
||||
abi: [];
|
||||
ast: any;
|
||||
binary: string;
|
||||
compiler: any;
|
||||
contractName: string;
|
||||
deployedBinary: string;
|
||||
deployedSourceMap: string;
|
||||
source: string;
|
||||
sourceMap: string;
|
||||
sourcePath: string;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface IExpressionEval {
|
||||
result: string;
|
||||
variablesReference: number;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface IInstruction {
|
||||
pc: number;
|
||||
op: string;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface ITransactionInputData {
|
||||
methodName: string;
|
||||
params: any[];
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export interface ITransactionResponse {
|
||||
hash: string;
|
||||
methodName: string;
|
||||
contractName: string;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { EVENT_TYPES } from '../constants/debugAdapter';
|
||||
|
||||
export namespace DebuggerTypes {
|
||||
export interface IBreakpoint {
|
||||
id: number;
|
||||
sourceId: number;
|
||||
line: number;
|
||||
}
|
||||
|
||||
export interface IFrame {
|
||||
file: string;
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
// The interface should always match schema in the package.json.
|
||||
export interface ILaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
|
||||
// Automatically stop target after launch. If not specified, target does not stop.
|
||||
stopOnEntry?: boolean;
|
||||
// enable logging the Debug Adapter Protocol
|
||||
trace?: boolean;
|
||||
host?: string;
|
||||
txHash: string;
|
||||
files: string[];
|
||||
workingDirectory: string;
|
||||
providerUrl: string;
|
||||
}
|
||||
|
||||
export class LaunchedEvent implements DebugProtocol.Event {
|
||||
public event: string = EVENT_TYPES.launched;
|
||||
public seq: number = 1000;
|
||||
public type: string = 'event';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import * as path from 'path';
|
||||
|
||||
const nativeModules = [
|
||||
{
|
||||
jsPath: '/scrypt.js/js.js',
|
||||
nodePath: '/scrypt.js/node.js',
|
||||
},
|
||||
{
|
||||
jsPath: '/web3-eth-accounts/node_modules/scrypt.js/js.js',
|
||||
nodePath: '/web3-eth-accounts/node_modules/scrypt.js/node.js',
|
||||
},
|
||||
];
|
||||
|
||||
// Override .node implementation by pure js implementation
|
||||
// IMPORTANT:
|
||||
// it doesn't work in 100% of cases and may stop working in future versions of node (this is not public api)
|
||||
function overrideNativeModule(_module: { jsPath: string, nodePath: string }) {
|
||||
try {
|
||||
const absolutePathToNodeModule = path.join(__dirname, '../../../../node_modules' + _module.nodePath);
|
||||
const absolutePathToJsModule = path.join(__dirname, '../../../../node_modules' + _module.jsPath);
|
||||
|
||||
require('module')._cache[absolutePathToNodeModule] = require(absolutePathToJsModule);
|
||||
} catch (e) {
|
||||
// ignore since there may be no files in node_modules
|
||||
}
|
||||
}
|
||||
|
||||
export default function overrideNativeModules() {
|
||||
// IS_BUNDLE_TIME is initialized in webpack.config
|
||||
// skip this process when bundle time goes since webpack already overrides by using resolve.alias
|
||||
if (typeof(IS_BUNDLE_TIME) === 'undefined') {
|
||||
nativeModules.forEach((m) => {
|
||||
overrideNativeModule(m);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import * as os from 'os';
|
||||
import * as truffleDebugUtils from 'truffle-debug-utils';
|
||||
import * as truffleDebugger from 'truffle-debugger';
|
||||
import {
|
||||
filterBaseContracts,
|
||||
filterContractsWithAddress,
|
||||
prepareContracts,
|
||||
} from './contracts/contractsPrepareHelpers';
|
||||
import { toWindowsPath } from './helpers';
|
||||
import { DebuggerTypes } from './models/debuggerTypes';
|
||||
import { IContractModel } from './models/IContractModel';
|
||||
import { IInstruction } from './models/IInstruction';
|
||||
|
||||
const WIN32 = 'win32';
|
||||
// this is a flag for the case when truffle-debugger lib is updated with custom stacktrace
|
||||
const CUSTOM_STACK_TRACE_IS_INJECTED = false;
|
||||
|
||||
export default class RuntimeInterface extends EventEmitter {
|
||||
public isDebuggerAttached: boolean;
|
||||
private _session: truffleDebugger.Session | undefined;
|
||||
private _selectors: truffleDebugger.Selectors;
|
||||
private _numBreakpoints: number;
|
||||
private _osPlatform: string;
|
||||
private _contractsWithAddress: IContractModel[];
|
||||
private _baseContracts: IContractModel[];
|
||||
private _initialBreakPoints: Array<{ path: string, lines: number[] }>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._selectors = truffleDebugger.selectors;
|
||||
this._numBreakpoints = 0;
|
||||
this._osPlatform = os.platform();
|
||||
this.isDebuggerAttached = false;
|
||||
this._initialBreakPoints = [];
|
||||
this._contractsWithAddress = [];
|
||||
this._baseContracts = [];
|
||||
}
|
||||
|
||||
public clearBreakpoints(): Promise<void> {
|
||||
return this._session
|
||||
? this._session.removeAllBreakpoints()
|
||||
: Promise.resolve();
|
||||
}
|
||||
|
||||
public storeInitialBreakPoints(path: string, lines: number[]) {
|
||||
this._initialBreakPoints.push({ path, lines });
|
||||
}
|
||||
|
||||
public async processInitialBreakPoints() {
|
||||
if (this._initialBreakPoints.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const initialBreakPoint of this._initialBreakPoints) {
|
||||
const { path, lines } = initialBreakPoint;
|
||||
for (const line of lines) {
|
||||
await this.setBreakpoint(path, line);
|
||||
}
|
||||
}
|
||||
this._initialBreakPoints.length = 0; // remove them since they are no longer needed
|
||||
}
|
||||
|
||||
public async setBreakpoint(path: string, line: number): Promise<DebuggerTypes.IBreakpoint | null> {
|
||||
if (!this._session) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
let breakpoint: DebuggerTypes.IBreakpoint;
|
||||
|
||||
// we'll need the debugger-internal ID of this source
|
||||
const debuggerSources: any = this._session.view(this._selectors.solidity.info.sources);
|
||||
const matchingSources: any = Object.values(debuggerSources).filter((sourceObject: any) => {
|
||||
const formattedSourPath = this._osPlatform === WIN32
|
||||
? toWindowsPath(sourceObject.sourcePath)
|
||||
: sourceObject.sourcePath;
|
||||
return formattedSourPath.includes(path);
|
||||
});
|
||||
const sourceId = matchingSources[0].id;
|
||||
|
||||
breakpoint = {
|
||||
id: this._numBreakpoints,
|
||||
line,
|
||||
sourceId,
|
||||
};
|
||||
|
||||
this._numBreakpoints++;
|
||||
await this._session.addBreakpoint(breakpoint);
|
||||
return breakpoint;
|
||||
}
|
||||
|
||||
// Get stack trace (without method name)
|
||||
public callStack(): any[] {
|
||||
this.validateSession();
|
||||
const result: any[] = [];
|
||||
const callStack = this._session!.view(this._selectors.evm.current.callstack);
|
||||
const currentLine = this.currentLine();
|
||||
const defaultLine = { line: 0, column: 0 };
|
||||
for (const fn of callStack) {
|
||||
// source is stored for previous call only
|
||||
if (CUSTOM_STACK_TRACE_IS_INJECTED && fn.source) {
|
||||
const { file, line, column } = fn.source;
|
||||
result.push({ file, line, column, isCurrent: false });
|
||||
} else {
|
||||
let isCurrent: boolean;
|
||||
let linesInfo: any;
|
||||
// if currentLine from baseContract then consider this is current call
|
||||
const isBaseContract = this._baseContracts.some((c) => c.sourcePath === currentLine.file);
|
||||
if (isBaseContract) {
|
||||
isCurrent = true;
|
||||
linesInfo = currentLine;
|
||||
} else {
|
||||
const contract = this._contractsWithAddress
|
||||
.find((c: any) => c.address === (fn.address || fn.storageAddress));
|
||||
if (contract === undefined) {
|
||||
throw new Error(`Coundn\'t find contract by address ${fn.address || fn.storageAddress}`);
|
||||
}
|
||||
isCurrent = contract.sourcePath === currentLine.file;
|
||||
linesInfo = isCurrent ? currentLine : { file: contract.sourcePath, ...defaultLine };
|
||||
}
|
||||
|
||||
result.push({ ...linesInfo, address: fn.address, isCurrent });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async variables(/* args?: DebugProtocol.VariablesArguments */): Promise<any> {
|
||||
if (this._session) {
|
||||
const variables = await this._session.variables();
|
||||
return truffleDebugUtils.nativize(variables);
|
||||
} else {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
}
|
||||
|
||||
public async continue(): Promise<void> {
|
||||
this.validateSession();
|
||||
await this._session!.continueUntilBreakpoint();
|
||||
this.processSteping('stopOnBreakpoint');
|
||||
}
|
||||
|
||||
public continueReverse(): void {
|
||||
this.validateSession();
|
||||
this.sendEvent('stopOnBreakpoint');
|
||||
}
|
||||
|
||||
public async stepNext(): Promise<void> {
|
||||
this.validateSession();
|
||||
await this._session!.stepNext();
|
||||
this.processSteping('stopOnStepOver');
|
||||
}
|
||||
|
||||
public async stepIn(): Promise<void> {
|
||||
this.validateSession();
|
||||
await this._session!.stepInto();
|
||||
this.processSteping('stopOnStepIn');
|
||||
}
|
||||
|
||||
public async stepOut(): Promise<void> {
|
||||
this.validateSession();
|
||||
await this._session!.stepOut();
|
||||
this.processSteping('stopOnStepOut');
|
||||
}
|
||||
|
||||
public async attach(txHash: string, workingDirectory: string): Promise<void> {
|
||||
const result: any = await prepareContracts(workingDirectory);
|
||||
|
||||
this._contractsWithAddress = filterContractsWithAddress(result.contracts);
|
||||
this._baseContracts = filterBaseContracts(result.contracts);
|
||||
|
||||
const debuggerOptions = {
|
||||
contracts: result.contracts,
|
||||
provider: result.provider,
|
||||
};
|
||||
const bugger = await truffleDebugger.forTx(txHash, debuggerOptions);
|
||||
this._session = bugger.connect();
|
||||
this.isDebuggerAttached = true;
|
||||
}
|
||||
|
||||
public currentLine(): DebuggerTypes.IFrame {
|
||||
this.validateSession();
|
||||
const currentLocation = this._session!.view(this._selectors.controller.current.location);
|
||||
const sourcePath = this._session!.view(this._selectors.solidity.current.source).sourcePath;
|
||||
if (!sourcePath) {
|
||||
throw new Error('No source file');
|
||||
}
|
||||
|
||||
return {
|
||||
column: currentLocation.sourceRange ? currentLocation.sourceRange.lines.start.column : 0,
|
||||
file: sourcePath,
|
||||
line: currentLocation.sourceRange ? currentLocation.sourceRange.lines.start.line : 0,
|
||||
};
|
||||
}
|
||||
|
||||
public getInstructionSteps(): IInstruction[] {
|
||||
this.validateSession();
|
||||
const steps: IInstruction[] = this._session!.view(this._selectors.trace.steps);
|
||||
return steps;
|
||||
}
|
||||
|
||||
public getCurrentInstructionStep(): IInstruction {
|
||||
this.validateSession();
|
||||
return this._session!.view(this._selectors.solidity.current.instruction) as IInstruction;
|
||||
}
|
||||
|
||||
public sendEvent(event: string, ...args: any[]) {
|
||||
setImmediate(() => {
|
||||
this.emit(event, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
private processSteping(event: any) {
|
||||
const isEndOfTransactionTrace = this._session!.view(this._selectors.trace.finished);
|
||||
if (isEndOfTransactionTrace) {
|
||||
this.sendEvent('end');
|
||||
} else {
|
||||
this.sendEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
private validateSession() {
|
||||
if (!this._session) {
|
||||
throw new Error('Debug session is undefined');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import * as abiDecoder from 'abi-decoder';
|
||||
import { TRANSACTION_DEFAULT_METHOD_NAME } from '../constants/transaction';
|
||||
import { ITransactionInputData } from '../models/ITransactionInputData';
|
||||
|
||||
export class TransactionInputDataDecoder {
|
||||
public addContractAbi(abi: []) {
|
||||
abiDecoder.addABI(abi);
|
||||
}
|
||||
|
||||
// Use addContractAbi before using decode
|
||||
public async decode(txInput: string): Promise<ITransactionInputData> {
|
||||
const decodedInput = abiDecoder.decodeMethod(txInput);
|
||||
return decodedInput
|
||||
? { methodName: decodedInput.name, params: decodedInput.params }
|
||||
: { methodName: TRANSACTION_DEFAULT_METHOD_NAME, params: [] };
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import { TRANSACTION_NUMBER_TO_SHOW } from '../constants/transaction';
|
||||
import { ContractJsonsProvider } from '../contracts/contractJsonsProvider';
|
||||
import { groupBy } from '../helpers';
|
||||
import { IContractJsonModel } from '../models/IContractJsonModel';
|
||||
import { ITransactionInputData } from '../models/ITransactionInputData';
|
||||
import { ITransactionResponse } from '../models/ITransactionResponse';
|
||||
import { Web3Wrapper } from '../web3Wrapper';
|
||||
import { TransactionInputDataDecoder } from './transactionInputDataDecoder';
|
||||
|
||||
export class TransactionProvider {
|
||||
private _web3: Web3Wrapper;
|
||||
private _contractBuildsDirectory: string;
|
||||
private _contractJsonsProvider: ContractJsonsProvider;
|
||||
private _transactionInputDecoder: TransactionInputDataDecoder;
|
||||
private _isTransactionInputDecoderReady: boolean;
|
||||
constructor(web3: Web3Wrapper, contractBuildsDirectory: string) {
|
||||
this._web3 = web3;
|
||||
this._contractBuildsDirectory = contractBuildsDirectory;
|
||||
this._contractJsonsProvider = new ContractJsonsProvider(contractBuildsDirectory);
|
||||
this._transactionInputDecoder = new TransactionInputDataDecoder();
|
||||
this._isTransactionInputDecoderReady = false;
|
||||
}
|
||||
|
||||
public async getLastTransactionHashes(take: number = TRANSACTION_NUMBER_TO_SHOW): Promise<string[]> {
|
||||
const latestBlockNumber = await this._web3.eth.getBlockNumber();
|
||||
const latestBlock = await this._web3.eth.getBlock(latestBlockNumber);
|
||||
const txHashes: string[] = [];
|
||||
let block = latestBlock;
|
||||
while (txHashes.length <= take && block.number > 0) {
|
||||
for (let i = 0; i < block.transactions.length && txHashes.length < TRANSACTION_NUMBER_TO_SHOW; i++) {
|
||||
txHashes.push(block.transactions[i]);
|
||||
}
|
||||
block = await this._web3.eth.getBlock(block.number - 1);
|
||||
}
|
||||
|
||||
return txHashes;
|
||||
}
|
||||
|
||||
public async getTransactionsInfo(txHashes: string[]): Promise<ITransactionResponse[]> {
|
||||
const batchRequest = this._web3.createBatchRequest();
|
||||
txHashes.forEach((txHash) => {
|
||||
batchRequest.add(this._web3.eth.getTransaction, txHash);
|
||||
batchRequest.add(this._web3.eth.getTransactionReceipt, txHash);
|
||||
});
|
||||
const result: any[] = await batchRequest.execute();
|
||||
const hashKey = 'hash';
|
||||
result.forEach((txI) => (txI[hashKey] = txI[hashKey] || txI.transactionHash)); // fill hash property
|
||||
const groupsByHash = groupBy(result, hashKey);
|
||||
const promises = Object.keys(groupsByHash).map((hash) => {
|
||||
return this.buildTransactionResponse(hash, groupsByHash[hash]);
|
||||
}, this);
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
private async buildTransactionResponse(hash: string, infos: any[]): Promise<ITransactionResponse> {
|
||||
const infoWithInput = infos.find((txInfo) => (txInfo.input)) || {};
|
||||
const infoWithAddress = infos.find((txInfo) => (txInfo.to || txInfo.contractAddress)) || {};
|
||||
const { methodName } = await this.getDecodedTransactionInput(infoWithInput.input);
|
||||
const contractName = await this.getContractNameByAddress(infoWithAddress.to || infoWithAddress.contractAddress);
|
||||
return {
|
||||
contractName,
|
||||
hash,
|
||||
methodName,
|
||||
} as ITransactionResponse;
|
||||
}
|
||||
|
||||
private async getDecodedTransactionInput(input: string): Promise<ITransactionInputData> {
|
||||
await this.prepareTransactionInputDecoder();
|
||||
return this._transactionInputDecoder.decode(input);
|
||||
}
|
||||
|
||||
private async getContractNameByAddress(address?: string): Promise<string> {
|
||||
const contractJsons = await this.getContractJsons();
|
||||
if (!address) { return ''; }
|
||||
const currentNetworkId = await this._web3.getNetworkId();
|
||||
const contractNames = Object.keys(contractJsons);
|
||||
for (const contractName of contractNames) {
|
||||
const contractJson = contractJsons[contractName];
|
||||
const networks = contractJson.networks;
|
||||
if (networks) {
|
||||
const network = networks[currentNetworkId];
|
||||
if (network && network.address
|
||||
&& network.address.toLowerCase() === address.toLowerCase()) {
|
||||
return contractName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private async prepareTransactionInputDecoder(): Promise<void> {
|
||||
if (this._isTransactionInputDecoderReady) {
|
||||
return;
|
||||
}
|
||||
const contractJsons = await this.getContractJsons();
|
||||
Object.keys(contractJsons).forEach((file) =>
|
||||
(this._transactionInputDecoder.addContractAbi(contractJsons[file].abi)));
|
||||
this._isTransactionInputDecoderReady = true;
|
||||
}
|
||||
|
||||
private async getContractJsons(): Promise<{ [fileName: string]: IContractJsonModel }> {
|
||||
const filesContents = await this._contractJsonsProvider.getJsonsContents();
|
||||
if (Object.keys(filesContents).length === 0) {
|
||||
throw new Error(`No compiled contracts found in ${this._contractBuildsDirectory}`);
|
||||
}
|
||||
return filesContents;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
declare module 'abi-decoder' {
|
||||
|
||||
interface Decoded {
|
||||
name: string,
|
||||
params: any[];
|
||||
}
|
||||
|
||||
function addABI(abi: []): void;
|
||||
function decodeMethod(input: string): Decoded;
|
||||
|
||||
export { addABI, decodeMethod };
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
declare module 'truffle-debug-utils' {
|
||||
function nativize(variables: any): any;
|
||||
|
||||
export { nativize };
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
declare module 'truffle-debugger' {
|
||||
interface Selectors {
|
||||
solidity: any,
|
||||
evm: any,
|
||||
controller: any,
|
||||
trace: any,
|
||||
}
|
||||
const selectors: Selectors;
|
||||
|
||||
interface Session {
|
||||
removeAllBreakpoints: () => Promise<void>,
|
||||
view: (selectors: any) => any,
|
||||
addBreakpoint: (breakPoint: any) => {},
|
||||
variables: () => Promise<any>,
|
||||
continueUntilBreakpoint: () => Promise<void>,
|
||||
stepNext: () => Promise<void>,
|
||||
stepInto: () => Promise<void>,
|
||||
stepOut: () => Promise<void>,
|
||||
}
|
||||
|
||||
interface Debugger {
|
||||
connect: () => Session,
|
||||
}
|
||||
|
||||
function forTx(txHash: string, debugOptions: any): Promise<Debugger>;
|
||||
|
||||
export { selectors, Selectors, forTx, Session };
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
declare module 'truffle-provider' {
|
||||
interface IProviderOptions {
|
||||
provider?: any,
|
||||
host?: string,
|
||||
port?: number,
|
||||
websockets?: boolean,
|
||||
}
|
||||
function create(networkOptions: IProviderOptions): any;
|
||||
export { create };
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
declare module Web3 {
|
||||
interface IProvider {
|
||||
currentProvider: any,
|
||||
}
|
||||
interface IBlockResponse {
|
||||
number: number;
|
||||
transactions: string[];
|
||||
}
|
||||
interface ITransactionResponse {
|
||||
input: string // encoded methodName and params
|
||||
}
|
||||
interface ITransactionReceiptResponse {
|
||||
from: string;
|
||||
to?: string;
|
||||
contractAddress?: string;
|
||||
}
|
||||
interface IBatchRequest {
|
||||
add: (request: any) => void,
|
||||
execute: () => Promise<any>,
|
||||
}
|
||||
interface IRequest {
|
||||
request: (...args: any[]) => any;
|
||||
}
|
||||
|
||||
interface IWeb3Eth extends IProvider {
|
||||
net: {
|
||||
getId: () => Promise<number>,
|
||||
},
|
||||
getBlock: any;
|
||||
getTransaction: any;
|
||||
getTransactionReceipt: any;
|
||||
getBlockNumber: any;
|
||||
currentProvider: any;
|
||||
}
|
||||
const providers: {
|
||||
HttpProvider: new(providerUrl: string) => IProvider,
|
||||
WebsocketProvider: new(providerUrl: string) => IProvider,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
declare class Web3 {
|
||||
constructor (provider: Web3.IProvider);
|
||||
eth: Web3.IWeb3Eth;
|
||||
BatchRequest: new() => Web3.IBatchRequest;
|
||||
}
|
||||
|
||||
declare module 'web3' {
|
||||
export = Web3;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
declare const IS_BUNDLE_TIME: boolean | undefined;
|
|
@ -0,0 +1,116 @@
|
|||
import { Handles, Scope } from 'vscode-debugadapter';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { SCOPES } from './constants/variablesView';
|
||||
import { IExpressionEval } from './models/IExpressionEval';
|
||||
import RuntimeInterface from './runtimeInterface';
|
||||
|
||||
export default class VariablesHandler {
|
||||
private _runtime: RuntimeInterface;
|
||||
private _scopes: Scope[];
|
||||
private _handlers: Handles<string>;
|
||||
|
||||
constructor(runtime: RuntimeInterface) {
|
||||
this._runtime = runtime;
|
||||
this._scopes = new Array<Scope>();
|
||||
this._scopes.push(new Scope(SCOPES.all.name, SCOPES.all.ref, false));
|
||||
this._handlers = new Handles<string>(SCOPES.dynamicVariables.ref);
|
||||
}
|
||||
|
||||
public getScopes(): Scope[] {
|
||||
return this._scopes;
|
||||
}
|
||||
|
||||
public getHandlers(): Handles<string> {
|
||||
return this._handlers;
|
||||
}
|
||||
|
||||
public async getVariableAttributesByVariableRef(variablesReference: number)
|
||||
: Promise<DebugProtocol.Variable[]> {
|
||||
let result: DebugProtocol.Variable[] = [];
|
||||
let variable: any;
|
||||
let variablePath: string = '';
|
||||
switch (variablesReference) {
|
||||
case SCOPES.all.ref:
|
||||
variable = await this._runtime.variables();
|
||||
result = this.mapToDebuggableVariables(variablePath, variable);
|
||||
break;
|
||||
default: // requesting object variable
|
||||
variablePath = this._handlers.get(variablesReference);
|
||||
variable = await this._runtime.variables();
|
||||
variable = this.getVariableAttriburesByKeyPath(variablePath, variable);
|
||||
|
||||
result = this.mapToDebuggableVariables(variablePath, variable);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// expression = "attribute"
|
||||
// expression = "parent.childA.child1"
|
||||
public async evaluateExpression(expression: string): Promise<IExpressionEval> {
|
||||
const variablesObj = await this._runtime.variables();
|
||||
const variable = this.getVariableAttriburesByKeyPath(expression, variablesObj);
|
||||
const isObjType = this.isSpecificObjectTypeValue(variable, typeof(variable));
|
||||
const isUndefinedOrNull = (v: any) => (v === undefined || v === null);
|
||||
return {
|
||||
result: isObjType
|
||||
? 'Object'
|
||||
: isUndefinedOrNull(variable)
|
||||
? typeof(variable)
|
||||
: variable.toString(),
|
||||
variablesReference: isObjType
|
||||
? this._handlers.create(this.generateVariablesAttrKey('', expression))
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
private isSpecificObjectTypeValue(value: any, valueType: string) {
|
||||
return !Array.isArray(value)
|
||||
&& value !== null
|
||||
&& value !== undefined
|
||||
&& valueType === 'object';
|
||||
}
|
||||
|
||||
// replace "." by "/" and generate "path/to/variable"
|
||||
private generateVariablesAttrKey(variablePath: string, attribute: string): string {
|
||||
return `${variablePath.replace(/\./g, '/')}/${attribute.replace(/\./g, '/')}`;
|
||||
}
|
||||
|
||||
private getVariableAttriburesByKeyPath(keyPath: string, variable: any): any {
|
||||
// keyPath = "/parent/childA/child1"
|
||||
// or keyPath = "parent.childA.child1"
|
||||
let keys = keyPath.split(/\/|\./);
|
||||
if (keys[0] === '') {
|
||||
keys = keys.slice(1);
|
||||
}
|
||||
try {
|
||||
keys.forEach((key) => {
|
||||
variable = variable[key];
|
||||
});
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
return variable;
|
||||
}
|
||||
|
||||
private mapToDebuggableVariables(variablePath: string, variable: any): DebugProtocol.Variable[] {
|
||||
const result: DebugProtocol.Variable[] = [];
|
||||
for (const attr in variable) {
|
||||
if (variable.hasOwnProperty(attr)) {
|
||||
const value = variable[attr];
|
||||
const type = typeof (value);
|
||||
const isRef = this.isSpecificObjectTypeValue(value, type);
|
||||
result.push({
|
||||
name: attr,
|
||||
type,
|
||||
value: isRef ? 'Object' : JSON.stringify(value),
|
||||
variablesReference: isRef
|
||||
? this._handlers.create(this.generateVariablesAttrKey(variablePath, attr))
|
||||
: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// This is a script for truffle exec command. The purpose is to resolve the provider
|
||||
// We cann't use truffle-config since it requires specific "npm install" command in user project
|
||||
// (npm install against the concrete version of electron the VS Code is built on)
|
||||
module.exports = function(callback) {
|
||||
var currentProvider = web3.eth.currentProvider;
|
||||
var providerUrl = '';
|
||||
var protocol = '';
|
||||
var constructor = currentProvider.constructor + '';
|
||||
if (constructor.indexOf('HttpProvider') !== -1) {
|
||||
protocol = 'http';
|
||||
providerUrl = currentProvider.host;
|
||||
}
|
||||
if (constructor.indexOf('WebsocketProvider') !== -1) {
|
||||
protocol = 'ws';
|
||||
providerUrl = currentProvider.connection.url;
|
||||
}
|
||||
var result = 'provider%=' + JSON.stringify({ url: providerUrl, protocol: protocol });
|
||||
console.log(result);
|
||||
callback();
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
import * as truffleProvider from 'truffle-provider';
|
||||
import * as web3 from 'web3';
|
||||
import { TruffleConfiguration } from '../helpers/truffleConfig';
|
||||
|
||||
export class Web3Wrapper extends web3 {
|
||||
private _networkId: number | undefined;
|
||||
private _options: TruffleConfiguration.INetworkOption;
|
||||
private _cachedProvider: any;
|
||||
|
||||
public constructor(options: TruffleConfiguration.INetworkOption) {
|
||||
const innerProvider = getWeb3InnerProvider(options);
|
||||
super(innerProvider);
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
// Important! It's a truffle provider instance (from truffle-provider library)
|
||||
public async getProvider() {
|
||||
if (!this._cachedProvider) {
|
||||
const web3Provider = this.eth.currentProvider;
|
||||
const truffleProviderOptions = {
|
||||
provider: () => {
|
||||
return web3Provider;
|
||||
},
|
||||
};
|
||||
this._cachedProvider = truffleProvider.create(truffleProviderOptions);
|
||||
}
|
||||
|
||||
return this._cachedProvider;
|
||||
}
|
||||
|
||||
public getProviderUrl() {
|
||||
return getProviderUrl(this._options);
|
||||
}
|
||||
|
||||
public async getNetworkId(): Promise<number> {
|
||||
if (!this._networkId) {
|
||||
this._networkId = await this.eth.net.getId();
|
||||
this.validateNetworkId(this._networkId);
|
||||
}
|
||||
|
||||
return this._networkId;
|
||||
}
|
||||
|
||||
public createBatchRequest() {
|
||||
return PromiseBatch(new this.BatchRequest());
|
||||
}
|
||||
|
||||
private async validateNetworkId(networkId: number) {
|
||||
const declaredNetworkId = this._options.network_id;
|
||||
if (declaredNetworkId !== '*') {
|
||||
if (networkId.toString() !== declaredNetworkId.toString()) {
|
||||
const error =
|
||||
'The network id specified in the truffle config ' +
|
||||
`(${networkId}) does not match the one returned by the network ` +
|
||||
`(${declaredNetworkId}). Ensure that both the network and the ` +
|
||||
'provider are properly configured.';
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function PromiseBatch(batch: web3.IBatchRequest) {
|
||||
const _batch = batch;
|
||||
const _requests: Array<Promise<any>> = [];
|
||||
return {
|
||||
add(method: any, hash: string) {
|
||||
const promise = new Promise<any>((accept, reject) => {
|
||||
_batch.add(method.request(hash, (error: any, result: any) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
accept(result);
|
||||
}));
|
||||
});
|
||||
_requests.push(promise);
|
||||
},
|
||||
async execute(): Promise<any> {
|
||||
_batch.execute();
|
||||
return Promise.all(_requests);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getProviderUrl(options: TruffleConfiguration.INetworkOption): string {
|
||||
if (options.host && options.port) {
|
||||
const protocol = options.websockets ? 'ws' : 'http';
|
||||
return `${protocol}://${options.host}:${options.port}`;
|
||||
}
|
||||
|
||||
if (options.provider && options.provider.url) {
|
||||
return options.provider.url;
|
||||
}
|
||||
|
||||
throw new Error('Undefined network options.');
|
||||
}
|
||||
|
||||
function getWeb3InnerProvider(options: TruffleConfiguration.INetworkOption): web3.IProvider {
|
||||
const providerUrl = getProviderUrl(options);
|
||||
let provider: web3.IProvider;
|
||||
if (providerUrl.startsWith('ws')) {
|
||||
provider = new web3.providers.WebsocketProvider(providerUrl);
|
||||
} else {
|
||||
provider = new web3.providers.HttpProvider(providerUrl);
|
||||
}
|
||||
return provider;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// workaround to load native modules
|
||||
// (since https://github.com/Microsoft/vscode/issues/658 doesn't work on win10)
|
||||
import nativeModulesLoader from './debugAdapter/nativeModules/loader';
|
||||
nativeModulesLoader();
|
||||
|
||||
import { SolidityDebugSession } from './debugAdapter/debugSession';
|
||||
|
||||
SolidityDebugSession.run(SolidityDebugSession);
|
|
@ -1,29 +1,36 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
// workaround to load native modules
|
||||
// (since https://github.com/Microsoft/vscode/issues/658 doesn't work on win10)
|
||||
import nativeModulesLoader from './debugAdapter/nativeModules/loader';
|
||||
nativeModulesLoader();
|
||||
|
||||
import { commands, ExtensionContext, Uri, window } from 'vscode';
|
||||
import { AzureBlockchain } from './AzureBlockchain';
|
||||
import { ConsortiumCommands } from './commands/ConsortiumCommands';
|
||||
import { ContractCommands } from './commands/ContractCommands';
|
||||
import { startSolidityDebugger } from './commands/DebuggerCommands';
|
||||
import { GanacheCommands } from './commands/GanacheCommands';
|
||||
import { LogicAppCommands } from './commands/LogicAppCommands';
|
||||
import { ProjectCommands } from './commands/ProjectCommands';
|
||||
import { TruffleCommands } from './commands/TruffleCommands';
|
||||
import { Constants } from './Constants';
|
||||
import { GanacheService } from './GanacheService/GanacheService';
|
||||
import { CommandContext, isWorkspaceOpen, required, setCommandContext } from './helpers';
|
||||
import { MnemonicRepository } from './MnemonicService/MnemonicRepository';
|
||||
import { CancellationEvent } from './Models';
|
||||
import { Output } from './Output';
|
||||
import { RequirementsPage, WelcomePage } from './pages';
|
||||
import { AdapterType, ContractDB, GanacheService, MnemonicRepository } from './services';
|
||||
import { Telemetry } from './TelemetryClient';
|
||||
import { ConsortiumTree } from './treeService/ConsortiumTree';
|
||||
import { ConsortiumTreeManager } from './treeService/ConsortiumTreeManager';
|
||||
import { ConsortiumView } from './ViewItems';
|
||||
|
||||
import { DebuggerConfiguration } from './debugAdapter/configuration/debuggerConfiguration';
|
||||
|
||||
export async function activate(context: ExtensionContext) {
|
||||
Constants.initialize(context);
|
||||
MnemonicRepository.initialize(context.globalState);
|
||||
DebuggerConfiguration.initialize(context);
|
||||
|
||||
setCommandContext(CommandContext.Enabled, true);
|
||||
setCommandContext(CommandContext.IsWorkspaceOpen, isWorkspaceOpen());
|
||||
|
@ -34,6 +41,7 @@ export async function activate(context: ExtensionContext) {
|
|||
const consortiumTree = new ConsortiumTree(consortiumTreeManager);
|
||||
|
||||
await welcomePage.checkAndShow();
|
||||
await ContractDB.initialize(AdapterType.IN_MEMORY);
|
||||
window.registerTreeDataProvider('AzureBlockchain', consortiumTree);
|
||||
|
||||
//#region azureBlockchain extension commands
|
||||
|
@ -47,10 +55,6 @@ export async function activate(context: ExtensionContext) {
|
|||
async (checkShowOnStartup: boolean) => {
|
||||
return checkShowOnStartup ? requirementsPage.checkAndShow() : requirementsPage.show();
|
||||
});
|
||||
const copyRPCEndpointAddress = commands.registerCommand('azureBlockchainService.copyRPCEndpointAddress',
|
||||
async (viewItem: ConsortiumView) => {
|
||||
await tryExecute(() => AzureBlockchain.copyRPCEndpointAddress(viewItem));
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region Ganache extension commands
|
||||
|
@ -81,6 +85,10 @@ export async function activate(context: ExtensionContext) {
|
|||
const copyABI = commands.registerCommand('contract.copyABI', async (uri: Uri) => {
|
||||
await tryExecute(() => TruffleCommands.writeAbiToBuffer(uri));
|
||||
});
|
||||
const copyRPCEndpointAddress = commands.registerCommand('azureBlockchainService.copyRPCEndpointAddress',
|
||||
async (viewItem: ConsortiumView) => {
|
||||
await tryExecute(() => TruffleCommands.writeRPCEndpointAddressToBuffer(viewItem));
|
||||
});
|
||||
const getPrivateKeyFromMnemonic = commands.registerCommand('azureBlockchainService.getPrivateKey', async () => {
|
||||
await tryExecute(() => TruffleCommands.getPrivateKeyFromMnemonic());
|
||||
});
|
||||
|
@ -99,10 +107,12 @@ export async function activate(context: ExtensionContext) {
|
|||
});
|
||||
//#endregion
|
||||
|
||||
//#region remix commands
|
||||
const generateSmartContractUI = commands.registerCommand('drizzle.generateSmartContractUI', async () => {
|
||||
await tryExecute(() => ContractCommands.generateSmartContractUI());
|
||||
});
|
||||
//#region contract commands
|
||||
const showSmartContractPage = commands.registerCommand(
|
||||
'azureBlockchainService.showSmartContractPage',
|
||||
async (contractPath: Uri) => {
|
||||
await tryExecute(() => ContractCommands.showSmartContractPage(context, contractPath));
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region logic app commands
|
||||
|
@ -128,9 +138,12 @@ export async function activate(context: ExtensionContext) {
|
|||
});
|
||||
//#endregion
|
||||
|
||||
const startDebugger = commands.registerCommand(
|
||||
'extension.truffle.debugTransaction', startSolidityDebugger);
|
||||
|
||||
context.subscriptions.push(showWelcomePage);
|
||||
context.subscriptions.push(showRequirementsPage);
|
||||
context.subscriptions.push(generateSmartContractUI);
|
||||
context.subscriptions.push(showSmartContractPage);
|
||||
context.subscriptions.push(refresh);
|
||||
context.subscriptions.push(newSolidityProject);
|
||||
context.subscriptions.push(buildContracts);
|
||||
|
@ -148,6 +161,7 @@ export async function activate(context: ExtensionContext) {
|
|||
context.subscriptions.push(generateEventPublishingWorkflows);
|
||||
context.subscriptions.push(generateReportPublishingWorkflows);
|
||||
context.subscriptions.push(getPrivateKeyFromMnemonic);
|
||||
context.subscriptions.push(startDebugger);
|
||||
|
||||
Telemetry.sendEvent(Constants.telemetryEvents.extensionActivated);
|
||||
return required.checkAllApps();
|
||||
|
@ -158,6 +172,7 @@ export async function deactivate(): Promise<void> {
|
|||
await Output.dispose();
|
||||
await Telemetry.dispose();
|
||||
await GanacheService.dispose();
|
||||
await ContractDB.dispose();
|
||||
}
|
||||
|
||||
async function tryExecute(func: () => Promise<any>, errorMessage: string | null = null): Promise<void> {
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче