├── .babelrc ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── lib ├── bin │ └── doxity.js ├── build.js ├── compile │ ├── index.js │ ├── parse-abi.js │ ├── parse-asm.js │ └── solc.js ├── constants.js ├── develop.js ├── helpers.js ├── index.js ├── init.js └── publish.js ├── package-lock.json ├── package.json └── src ├── bin └── doxity.js ├── build.js ├── compile ├── index.js ├── parse-abi.js ├── parse-asm.js └── solc.js ├── constants.js ├── develop.js ├── helpers.js ├── index.js ├── init.js └── publish.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | extends: "eslint-config-airbnb" 2 | parser: "babel-eslint" 3 | 4 | rules: 5 | arrow-body-style: 0 6 | max-len: 0 7 | no-underscore-dangle: 0 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, DigixGlobal Pte. Ltd. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doxity 2 | 3 | 0.4.0 now works with truffle! 💻 4 | 5 | ### Documentation Generator for Solidity 6 | 7 | [Demo Site](https://hitchcott.github.io/doxity-demo/docs/MetaCoin/) 8 | 9 | Uses [gatsby](https://github.com/gatsbyjs/gatsby) to generate beautiful Solidity docs automatically via [natspec](https://github.com/ethereum/wiki/wiki/Ethereum-Natural-Specification-Format). 10 | 11 | ## Features 12 | 13 | * Automatically document contracts and methods from your code 14 | * Generate static HTML documentation websites that can be served directly from github 15 | * Fully customizable output using React 16 | * Minimalist UX from [semantic-ui](https://github.com/Semantic-Org/Semantic-UI-React) 17 | * Solidity Syntax highlighting 18 | * For each contract, options for whitelisting 19 | * Methods Documentation 20 | * ABI 21 | * Bytecode 22 | * Source Code 23 | 24 | ![Doxity Screenshot](http://i.imgur.com/4ojFGfs.png) 25 | 26 | ## Installation 27 | 28 | You can install `@digix/doxity` globally or locally in your project. 29 | 30 | You'll also need `solc 0.4.X` ([native](http://solidity.readthedocs.io/en/develop/installing-solidity.html#binary-packages) until [solc-js is supported](https://github.com/ethereum/solc-js/issues/70)) and libssl-dev installed on your machine. 31 | 32 | ```bash 33 | # globally 34 | npm install -g @digix/doxity 35 | # or within project folder 36 | npm install --save-dev @digix/doxity 37 | ``` 38 | 39 | ## Quickstart 40 | 41 | 1. Have a project that contains natspecced* `.sol` contracts in a `contracts` directory, a `package.json` and `README.md`. 42 | 1. `doxity init` will clone and set up the boilerplate gatsby project - files found in `./scripts/doxity` 43 | 1. `doxity build` will generate static HTML containing documentation to `./docs` 44 | 45 | **Customize Markup and Publish it to github** 46 | 47 | 1. `doxity develop` will start a development server for editing gatsby project 48 | 1. `doxity compile` will compile the contracts and update the contract data 49 | 1. Ensure you have set `linkPrefix` in `scripts/doxity/config.toml` to be equal to your repo's name (e.g. `/my-project`) 50 | 1. `doxity publish` will generate static HTML containing documentation to `./docs` 51 | 1. After publishing, you'll end up with a `./docs` folder in your project which can be easily deployed 52 | 1. Push it to `master` on github 53 | 1. Go to your repo options, update 'Github Pages -> Set Source' to 'master branch /docs folder' 54 | 1. Your documentation is live! Why not set up a Travis CI script to automate that whenever you commit? 55 | 56 | \* N.B. Currently Solidity doesn't support multiple `@return` values. Pass it a JSON object until it's patched. EG: 57 | 58 | ```javascript 59 | // natspec example - appears above each method 60 | /** 61 | @notice Get user's information from their EOA/Contract address 62 | @dev Some more techncial explanation here 63 | @param _account the EOA or contract address associated with the user 64 | @param _anotherParam this is just an example of passing a second param 65 | @return { 66 | "_feeaccount": "The contract address for storage fee payments", 67 | "_recastaccount": "The contract address for recasting tokens", 68 | "_assetcount": "The number of items associated with this account", 69 | "_assetstartindex": "The starting index of the user's items collection" 70 | } 71 | */ 72 | function getUser(address _account) ... 73 | ``` 74 | 75 | ## Usage 76 | 77 | ### `.doxityrc` 78 | 79 | You can configure all of doxity's options using a `.doxityrc` file at the root of your project, with the following structure: 80 | 81 | ```javascript 82 | // .doxityrc 83 | { 84 | // gatsby project source files directory 85 | "target": "scripts/doxity", 86 | // folder that contains the contracts you want to compile 87 | "src": "contracts/*", 88 | // folder in gatsby project to dump contract data 89 | "dir": "pages/docs", 90 | // folder to output the generated html (relative to project root) 91 | "out": "docs", 92 | // tarball for bootstrapping the gatsby project 93 | "source": "https://github.com/DigixGlobal/doxity-gatsby-starter-project/archive/9445d59056058159ce25d7cd1643039523718553.tar.gz", 94 | // for truffle projects, you can get deployed contract info 95 | // use https://github.com/DigixGlobal/doxity-gatsby-starter-project/archive/74df3b2b7a2484714540e4a9153a8f1d0f95a380.tar.gz for experimental interactive mode! 96 | "interaction": { 97 | "network": "2", 98 | "providerUrl": "https://morden.infura.io/sign_up_to_get_a_hash" 99 | }, 100 | // option to whitelist various data 101 | "whitelist": { 102 | // the keyname `all` will be used for whitelist defaults 103 | "all": { 104 | "abi": true, 105 | "methods": true, 106 | "bytecode": false, // bytecode is false or undefined, it won't be shown 107 | "source": false // source is false or undefined, won't be shown 108 | }, 109 | "DigixMath": { 110 | "source": true // source code uniquely shown for this contract, bytecode still hidden 111 | } 112 | } 113 | } 114 | ``` 115 | 116 | ### Command Line Interface 117 | 118 | You can also override these options by passing them to a command tool. 119 | 120 | Unless you override them, default arguments will be used: 121 | 122 | - `doxity init --target --source` (with init, you can also pass any arguments to save them to `.doxityrc`) 123 | - `doxity compile --target --src --dir` 124 | - `doxity develop --target` 125 | - `doxity publish --target --out` 126 | 127 | When passing to `src` in the CLI, wrap the filename in quotes; e.g. `--src "contracts/*"` - it is passed directly to `solc`. 128 | 129 | **Protip:** If you are installing locally, you could add the following to your `package.json`: 130 | 131 | ```javascript 132 | "scripts" : { 133 | "docs:init": "node_modules/.bin/doxity init", // add your custom arguments (see API below) 134 | "docs:compile": "node_modules/.bin/doxity compile", 135 | "docs:develop": "node_modules/.bin/doxity develop", 136 | "docs:publish": "node_modules/.bin/doxity publish", 137 | "docs:build": "node_modules/.bin/doxity build", // compile + publish 138 | ... 139 | }, 140 | ``` 141 | 142 | You can then use `npm run docs:[command]` as a proxy for `doxity [command]`. 143 | 144 | ## TODO 145 | 146 | * 1.0.0 147 | * AST parsing (pending solidity update) 148 | * pragma version 149 | * Imports 150 | * Modifiers, variables, private functions, etc. 151 | * Sourcemaps 152 | * Inline Code Snippets 153 | * Tree view 154 | * Methods filtering 155 | * Tests 156 | * 1.x 157 | * Multiple Versioning 158 | * Pudding integration? Automatically generate forms + web3 instance for testing via GUI? 159 | 160 | ## License 161 | 162 | BSD-3-Clause 2016 163 | -------------------------------------------------------------------------------- /lib/bin/doxity.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var _fs = require('fs'); 5 | 6 | var _fs2 = _interopRequireDefault(_fs); 7 | 8 | var _path = require('path'); 9 | 10 | var _path2 = _interopRequireDefault(_path); 11 | 12 | var _minimist = require('minimist'); 13 | 14 | var _minimist2 = _interopRequireDefault(_minimist); 15 | 16 | var _index = require('../index'); 17 | 18 | var Doxity = _interopRequireWildcard(_index); 19 | 20 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 21 | 22 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 23 | 24 | var args = (0, _minimist2.default)(process.argv.slice(2)); 25 | 26 | // get json version 27 | 28 | if (!args._[0]) { 29 | var _JSON$parse = JSON.parse(_fs2.default.readFileSync(_path2.default.join(__dirname, '../../package.json')).toString()), 30 | version = _JSON$parse.version; 31 | 32 | console.log('\nDoxity v' + version + '\n\nCommands:\n\ninit Initialize your project for use with doxity\ncompile Compile solidity contracts to generate docs data\ndevelop Spin up a development server for customizing output\npublish Generate static HTML documenation\nbuild compile + publish\n\nParameters:\n\n--target Gatsby project source files directory\n--src Folder that contains the contracts you want to compile\n--dir Folder in gatsby project to dump contract data\n--out Folder to output the generated html (relative to project root)\n--source Git url for bootstrapping the gatsby project\n '); 33 | process.exit(); 34 | } else { 35 | Doxity.default[args._[0]](args); 36 | } -------------------------------------------------------------------------------- /lib/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (args) { 8 | (0, _compile2.default)(args).then(function () { 9 | (0, _publish2.default)(args); 10 | }); 11 | }; 12 | 13 | var _compile = require('./compile'); 14 | 15 | var _compile2 = _interopRequireDefault(_compile); 16 | 17 | var _publish = require('./publish'); 18 | 19 | var _publish2 = _interopRequireDefault(_publish); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -------------------------------------------------------------------------------- /lib/compile/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /* eslint-disable global-require */ 8 | 9 | exports.default = function (opts) { 10 | var output = process.env.PWD + '/' + opts.target + '/' + opts.dir; 11 | if (!_fs2.default.existsSync(output)) { 12 | throw new Error('Output directory ' + output + ' not found, are you in the right directory?'); 13 | } 14 | // clear out the output folder (remove all json files) 15 | _glob2.default.sync(output + '/*.json').forEach(function (file) { 16 | return _fs2.default.unlinkSync(file); 17 | }); 18 | // get the natspec 19 | return (0, _solc2.default)(opts.src).then(function (_ref2) { 20 | var contracts = _ref2.contracts; 21 | 22 | compile(_extends({}, opts, { output: output, contracts: contracts })); 23 | }); 24 | }; 25 | 26 | var _fs = require('fs'); 27 | 28 | var _fs2 = _interopRequireDefault(_fs); 29 | 30 | var _glob = require('glob'); 31 | 32 | var _glob2 = _interopRequireDefault(_glob); 33 | 34 | var _toml = require('toml'); 35 | 36 | var _toml2 = _interopRequireDefault(_toml); 37 | 38 | var _tomlifyJ = require('tomlify-j0.4'); 39 | 40 | var _tomlifyJ2 = _interopRequireDefault(_tomlifyJ); 41 | 42 | var _constants = require('../constants'); 43 | 44 | var _solc = require('./solc'); 45 | 46 | var _solc2 = _interopRequireDefault(_solc); 47 | 48 | var _parseAbi = require('./parse-abi'); 49 | 50 | var _parseAbi2 = _interopRequireDefault(_parseAbi); 51 | 52 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 53 | 54 | function compile(_ref) { 55 | var whitelist = _ref.whitelist, 56 | contracts = _ref.contracts, 57 | output = _ref.output, 58 | target = _ref.target, 59 | version = _ref.version; 60 | 61 | // do we need to check for whitelist? 62 | var defaultWhitelist = { source: true, bytecode: true, abi: true, methods: true }; 63 | // if (whitelist && Object.keys(whitelist).length > 0) { 64 | // defaultWhitelist = whitelist.all || {}; 65 | // } 66 | process.stdout.write('Generating output for ' + Object.keys(contracts).length + ' contracts...\n'); 67 | Object.keys(contracts).forEach(function (contractName) { 68 | var contract = contracts[contractName]; 69 | // determine whether we should be skipped 70 | if (whitelist && !whitelist[contractName]) { 71 | return null; 72 | } 73 | // otherwise, pick up the defaultss 74 | var myWhitelist = _extends({}, defaultWhitelist, (whitelist || {})[contractName]); 75 | // get the source file 76 | var fileName = contract.fileName; 77 | 78 | if (!fileName) { 79 | return null; 80 | } // there was an error parsing... 81 | // TODO fix me 82 | // const interaction = {}; 83 | // get deploy info from truffle 84 | // let address; 85 | // if (interaction) { 86 | // try { 87 | // const instance = require(`${process.env.PWD}/build/contracts/${contractName}.sol.js`); 88 | // address = instance.all_networks[interaction.network].address; 89 | // } catch (e) { /* do noithing */ } 90 | // } 91 | var bin = contract.bin, 92 | opcodes = contract.opcodes, 93 | abi = contract.abi, 94 | devdoc = contract.devdoc; 95 | var author = devdoc.author, 96 | title = devdoc.title; 97 | 98 | var data = { 99 | author: author, 100 | title: title, 101 | fileName: fileName.replace(process.env.PWD, ''), 102 | // address, 103 | name: contractName, 104 | // only pass these if they are whitelisted 105 | abi: myWhitelist.abi && abi, 106 | bin: myWhitelist.bytecode && bin, 107 | opcodes: myWhitelist.bytecode && opcodes, 108 | source: myWhitelist.source && _fs2.default.readFileSync(fileName).toString(), 109 | abiDocs: myWhitelist.methods && (0, _parseAbi2.default)(contract) 110 | }; 111 | return _fs2.default.writeFileSync(output + '/' + contractName + '.json', JSON.stringify(data) + '\n'); 112 | }); 113 | 114 | // TODO find in a better way? 115 | var pkgConfig = {}; 116 | try { 117 | pkgConfig = JSON.parse(_fs2.default.readFileSync(process.env.PWD + '/package.json')); 118 | } catch (e) { 119 | // console.log('package.json not found, add one for more output'); 120 | } 121 | 122 | var config = { 123 | compiler: version, 124 | name: pkgConfig.name, 125 | license: pkgConfig.license, 126 | version: pkgConfig.version, 127 | description: pkgConfig.description, 128 | homepage: pkgConfig.homepage, 129 | interaction: {}, // TODO implement 130 | author: pkgConfig.author && pkgConfig.author.name || pkgConfig.author, 131 | buildTime: new Date() 132 | }; 133 | 134 | var configFile = process.env.PWD + '/' + target + '/' + _constants.CONFIG_FILE; 135 | 136 | try { 137 | // try marginging with old config 138 | config = _extends({}, _toml2.default.parse(_fs2.default.readFileSync(configFile).toString()), config); 139 | } catch (e) { 140 | /* do nothing */ 141 | // console.log('Error copying config'); 142 | } 143 | 144 | try { 145 | // try marginging with doxity config 146 | config = _extends({}, config, JSON.parse(_fs2.default.readFileSync(process.env.PWD + '/' + _constants.DOXITYRC_FILE).toString())); 147 | } catch (e) {} /* do nothing */ 148 | 149 | // write the config 150 | if (_fs2.default.existsSync(configFile)) { 151 | _fs2.default.unlinkSync(configFile); 152 | } 153 | _fs2.default.writeFileSync(configFile, '' + (0, _tomlifyJ2.default)(config)); 154 | 155 | // copy the readme 156 | try { 157 | var readmeFile = _glob2.default.sync(process.env.PWD + '/' + _constants.README_FILE, { nocase: true })[0]; 158 | var readmeTarget = process.env.PWD + '/' + target + '/' + _constants.README_TARGET; 159 | if (_fs2.default.existsSync(readmeTarget)) { 160 | _fs2.default.unlinkSync(readmeTarget); 161 | } 162 | _fs2.default.writeFileSync(readmeTarget, _fs2.default.readFileSync(readmeFile)); 163 | } catch (e) { 164 | /* do nothing */ 165 | // console.log('Readme file not found, ignoring...'); 166 | } 167 | process.stdout.write(' done!\n'); 168 | } -------------------------------------------------------------------------------- /lib/compile/parse-abi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | exports.default = function (contract) { 10 | return contract.abi.map(function (method) { 11 | // get find relevent docs 12 | var inputParams = method.inputs || []; 13 | var signature = method.name && method.name + '(' + inputParams.map(function (i) { 14 | return i.type; 15 | }).join(',') + ')'; 16 | var devDocs = (contract.devdoc.methods || {})[signature] || {}; 17 | var userDocs = (contract.userdoc.methods || {})[signature] || {}; 18 | // map abi inputs to devdoc inputs 19 | var params = devDocs.params || {}; 20 | var inputs = inputParams.map(function (param) { 21 | return _extends({}, param, { description: params[param.name] }); 22 | }); 23 | // don't write this 24 | delete devDocs.params; 25 | 26 | // START HACK workaround pending https://github.com/ethereum/solidity/issues/1277 27 | // TODO map outputs properly once compiler splits them out 28 | // in the meantime, use json array 29 | // parse devDocs.return as a json object 30 | var outputParams = void 0; 31 | var outputs = void 0; 32 | try { 33 | outputParams = JSON.parse(devDocs.return); 34 | } catch (e) { 35 | try { 36 | var split = devDocs.return.split(' '); 37 | var name = split.shift(); 38 | outputParams = _defineProperty({}, name, split.join(' ')); 39 | } catch (e2) {/* */} 40 | } 41 | try { 42 | outputs = method.outputs.map(function (param) { 43 | return _extends({}, param, { description: outputParams[param.name] }); 44 | }); 45 | } catch (e) {} /* */ 46 | // END HACK 47 | 48 | return _extends({}, method, devDocs, userDocs, { 49 | inputs: inputs, 50 | outputs: outputs, 51 | signature: signature, 52 | signatureHash: signature && (0, _helpers.getFunctionSignature)(signature) 53 | }); 54 | }); 55 | }; 56 | 57 | var _helpers = require('../helpers'); 58 | 59 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -------------------------------------------------------------------------------- /lib/compile/parse-asm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 8 | 9 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 10 | 11 | exports.default = function (_ref) { 12 | var contract = _ref.contract, 13 | source = _ref.source; 14 | 15 | 16 | var definitions = { 17 | PragmaDirective: function PragmaDirective(_ref2) { 18 | var attributes = _ref2.attributes; 19 | 20 | var _attributes$literals = _toArray(attributes.literals), 21 | compiler = _attributes$literals[0], 22 | version = _attributes$literals.slice(1); 23 | 24 | return { pragma: compiler + ' ' + version.join('') }; 25 | }, 26 | ContractDefinition: function ContractDefinition(_ref3) { 27 | var attributes = _ref3.attributes, 28 | children = _ref3.children; 29 | 30 | // console.log(children); 31 | return _extends({}, attributes, { 32 | definitions: children.map(parseDefinition).filter(function (c) { 33 | return c; 34 | }) 35 | }); 36 | }, 37 | _definition: function _definition(args) { 38 | // console.log('definitoin', args); 39 | if (args.attributes.name === 'find') { 40 | // console.log(JSON.stringify(args.children, null, 2)); 41 | // merge inputs and outputs 42 | var signatureParams = []; 43 | 44 | var _args$children$filter = args.children.filter(function (child) { 45 | return child.name === "ParameterList"; 46 | }).map(function (params) { 47 | return params.children.map(function (param) { 48 | var type = param.attributes.type; 49 | 50 | if (param.children[0].name === 'UserDefinedTypeName') { 51 | type = contract.name + '.' + param.attributes.type.split(' ').slice(1, 3).join(' '); 52 | } 53 | return _extends({}, param.attributes, { type: type }); 54 | }); 55 | }), 56 | _args$children$filter2 = _slicedToArray(_args$children$filter, 2), 57 | inputs = _args$children$filter2[0], 58 | outputs = _args$children$filter2[1]; 59 | 60 | console.log("params", args.attributes.name, inputs, outputs); 61 | } 62 | // add devdocs 63 | return _extends({}, args.attributes); 64 | }, 65 | VariableDeclaration: function VariableDeclaration(args) { 66 | return _extends({ definition: 'variable' }, this._definition(args)); 67 | }, 68 | ModifierDefinition: function ModifierDefinition(args) { 69 | return _extends({ definition: 'modifier' }, this._definition(args)); 70 | }, 71 | StructDefinition: function StructDefinition(args) { 72 | return _extends({ definition: 'struct' }, this._definition(args)); 73 | }, 74 | FunctionDefinition: function FunctionDefinition(args) { 75 | return _extends({ definition: 'function' }, this._definition(args)); 76 | } 77 | }; 78 | 79 | function parseDefinition(definition) { 80 | return definitions[definition.name] && definitions[definition.name](definition); 81 | } 82 | source.AST.children.map(parseDefinition); 83 | return []; 84 | }; 85 | 86 | var _helpers = require('../helpers'); 87 | 88 | function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); } 89 | 90 | /* 91 | WORK IN PROGRESS 92 | 93 | we want to end up with a structure like: 94 | 95 | { 96 | name: ContractResolver, 97 | pragma: '0.4.2', 98 | imports: [ 99 | { 100 | name: ContractResolver, 101 | path: '../aux/blah', 102 | } 103 | ], 104 | docs: [ 105 | 106 | ] 107 | : 108 | } 109 | */ -------------------------------------------------------------------------------- /lib/compile/solc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | exports.default = function (src) { 10 | // detect if we're in a truffle project 11 | return new Promise(function (resolve) { 12 | if (_fs2.default.existsSync(process.env.PWD + '/truffle.js')) { 13 | var config = _truffleConfig2.default.default(); 14 | config.resolver = new _truffleResolver2.default(config); 15 | config.rawData = true; 16 | _truffleCompile2.default.all(config, function (err, res) { 17 | if (err) { 18 | throw err; 19 | } 20 | resolve({ 21 | contracts: Object.keys(res).reduce(function (o, k) { 22 | var _res$k$rawData = res[k].rawData, 23 | metadata = _res$k$rawData.metadata, 24 | data = _objectWithoutProperties(_res$k$rawData, ['metadata']); 25 | 26 | try { 27 | var parsed = JSON.parse(metadata); 28 | var fN = Object.keys(parsed.settings.compilationTarget)[0]; 29 | data.fileName = fN.indexOf(process.env.PWD) === 0 ? fN : process.env.PWD + '/node_modules/' + fN; 30 | data.output = parsed.output; 31 | } catch (e) { 32 | console.log('\u26A0\uFE0F Error parsing Contract: ' + k); 33 | } 34 | return _extends({}, o, _defineProperty({}, k, data)); 35 | }, {}) 36 | }); 37 | }); 38 | } else { 39 | var exec = 'solc --combined-json abi,asm,ast,bin,bin-runtime,clone-bin,devdoc,interface,opcodes,srcmap,srcmap-runtime,userdoc ' + src; 40 | var res = JSON.parse(_child_process2.default.execSync(exec)); 41 | resolve({ 42 | contracts: Object.keys(res.contracts).reduce(function (o, k) { 43 | var file = k.split(':')[0]; 44 | var fileFragments = file.split('/'); 45 | var contractName = fileFragments[fileFragments.length - 1].split('.sol')[0]; 46 | var contract = res.contracts[k]; 47 | var fileName = process.env.PWD + '/' + k.split(':')[0]; 48 | return _extends({}, o, _defineProperty({}, contractName, _extends({}, contract, { 49 | fileName: fileName, 50 | abi: JSON.parse(contract.abi), 51 | devdoc: JSON.parse(contract.devdoc), 52 | userdoc: JSON.parse(contract.userdoc) 53 | }))); 54 | }, {}) 55 | }); 56 | } 57 | }); 58 | }; 59 | 60 | var _fs = require('fs'); 61 | 62 | var _fs2 = _interopRequireDefault(_fs); 63 | 64 | var _child_process = require('child_process'); 65 | 66 | var _child_process2 = _interopRequireDefault(_child_process); 67 | 68 | var _truffleConfig = require('truffle-config'); 69 | 70 | var _truffleConfig2 = _interopRequireDefault(_truffleConfig); 71 | 72 | var _truffleResolver = require('truffle-resolver'); 73 | 74 | var _truffleResolver2 = _interopRequireDefault(_truffleResolver); 75 | 76 | var _truffleCompile = require('truffle-compile'); 77 | 78 | var _truffleCompile2 = _interopRequireDefault(_truffleCompile); 79 | 80 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 81 | 82 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 83 | 84 | function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var DEFAULT_SOURCE = exports.DEFAULT_SOURCE = 'https://github.com/DigixGlobal/doxity-gatsby-starter-project/archive/a4886b7a7a04c018ac04fed3125d7d4785e74bed.tar.gz'; 7 | var DOXITYRC_FILE = exports.DOXITYRC_FILE = '.doxityrc'; 8 | var DEFAULT_TARGET = exports.DEFAULT_TARGET = 'scripts/doxity'; 9 | var DEFAULT_PAGES_DIR = exports.DEFAULT_PAGES_DIR = 'pages/docs'; 10 | var DEFAULT_SRC_DIR = exports.DEFAULT_SRC_DIR = 'contracts/*'; 11 | var DEFAULT_PUBLISH_DIR = exports.DEFAULT_PUBLISH_DIR = 'docs'; 12 | var CONFIG_FILE = exports.CONFIG_FILE = 'config.toml'; 13 | var README_FILE = exports.README_FILE = 'README.md'; 14 | var README_TARGET = exports.README_TARGET = 'pages/index.md'; -------------------------------------------------------------------------------- /lib/develop.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (_ref) { 8 | var target = _ref.target; 9 | 10 | // TODO check we're in ther right folder.. ? 11 | var runDev = _child_process2.default.spawn('npm', ['run', 'develop'], { cwd: process.env.PWD + '/' + target }); 12 | runDev.stdout.pipe(process.stdout); 13 | runDev.stderr.pipe(process.stderr); 14 | runDev.on('close', function () { 15 | return process.exit(); 16 | }); 17 | }; 18 | 19 | var _child_process = require('child_process'); 20 | 21 | var _child_process2 = _interopRequireDefault(_child_process); 22 | 23 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.clearDirectory = clearDirectory; 7 | exports.getFunctionSignature = getFunctionSignature; 8 | 9 | var _rimraf = require('rimraf'); 10 | 11 | var _rimraf2 = _interopRequireDefault(_rimraf); 12 | 13 | var _mkdirp = require('mkdirp'); 14 | 15 | var _mkdirp2 = _interopRequireDefault(_mkdirp); 16 | 17 | var _keccakjs = require('keccakjs'); 18 | 19 | var _keccakjs2 = _interopRequireDefault(_keccakjs); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | function clearDirectory(target) { 24 | return new Promise(function (resolve, reject) { 25 | (0, _mkdirp2.default)(target, function () { 26 | (0, _rimraf2.default)(target, function (err) { 27 | if (err) { 28 | return reject(err); 29 | } 30 | return resolve(); 31 | }); 32 | }); 33 | }); 34 | } 35 | 36 | function getFunctionSignature(signature) { 37 | return new _keccakjs2.default(256).update(signature).digest('hex').substr(0, 8); 38 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _fs = require('fs'); 10 | 11 | var _fs2 = _interopRequireDefault(_fs); 12 | 13 | var _constants = require('./constants'); 14 | 15 | var _init = require('./init'); 16 | 17 | var _init2 = _interopRequireDefault(_init); 18 | 19 | var _compile = require('./compile'); 20 | 21 | var _compile2 = _interopRequireDefault(_compile); 22 | 23 | var _develop = require('./develop'); 24 | 25 | var _develop2 = _interopRequireDefault(_develop); 26 | 27 | var _publish = require('./publish'); 28 | 29 | var _publish2 = _interopRequireDefault(_publish); 30 | 31 | var _build = require('./build'); 32 | 33 | var _build2 = _interopRequireDefault(_build); 34 | 35 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 36 | 37 | var methods = { 38 | compile: _compile2.default, 39 | init: _init2.default, 40 | develop: _develop2.default, 41 | publish: _publish2.default, 42 | build: _build2.default 43 | }; 44 | 45 | function populateArguments(passed) { 46 | // cruft from minimist 47 | delete passed._; 48 | // fallback to defaults 49 | var defaults = { 50 | target: _constants.DEFAULT_TARGET, 51 | src: _constants.DEFAULT_SRC_DIR, 52 | dir: _constants.DEFAULT_PAGES_DIR, 53 | source: _constants.DEFAULT_SOURCE, 54 | out: _constants.DEFAULT_PUBLISH_DIR 55 | }; 56 | // merge with .doxityrc 57 | var saved = {}; 58 | try { 59 | saved = JSON.parse(_fs2.default.readFileSync(process.env.PWD + '/' + _constants.DOXITYRC_FILE).toString()); 60 | } catch (e) { 61 | console.log('.doxityrc not found or unreadable'); 62 | } 63 | // return merge 64 | return _extends({}, defaults, saved, passed); 65 | } 66 | // wire up defaults 67 | var wrappedMethods = {}; 68 | Object.keys(methods).forEach(function (key) { 69 | wrappedMethods[key] = function (args) { 70 | var newArgs = populateArguments(args); 71 | return methods[key](newArgs); 72 | }; 73 | }); 74 | 75 | exports.default = wrappedMethods; -------------------------------------------------------------------------------- /lib/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (args) { 8 | var source = args.source, 9 | target = args.target; 10 | // TODO check folder exists... 11 | 12 | var absoluteTarget = process.env.PWD + '/' + target; 13 | var tmpTarget = _path2.default.resolve(process.env.PWD + '/' + target + '/../doxity-tmp-' + new Date()); 14 | // clear the target dir 15 | (0, _helpers.clearDirectory)(absoluteTarget).then(function () { 16 | // clone the repo 17 | process.stdout.write('Getting ' + source + '...\n'); 18 | // pipe package to thingy. 19 | return new Promise(function (resolve) { 20 | _request2.default.get(source).pipe((0, _tar2.default)().createWriteStream(tmpTarget)).on('finish', resolve); 21 | }); 22 | }) 23 | // rename the downloaded folder to doxity 24 | .then(function () { 25 | _fs2.default.renameSync(tmpTarget + '/' + _fs2.default.readdirSync(tmpTarget)[0], absoluteTarget); 26 | _fs2.default.rmdirSync(tmpTarget); 27 | }).then(function () { 28 | // fancy spinner 29 | var i = 0; 30 | var seq = '⣷⣯⣟⡿⢿⣻⣽⣾'.split(''); 31 | var message = 'Setting up doxity project with npm install. This may take a while...'; 32 | var spinner = setInterval(function () { 33 | i++; 34 | if (i >= seq.length) { 35 | i = 0; 36 | } 37 | process.stdout.write('\r' + seq[i] + ' ' + message); 38 | }, 1000 / 24); 39 | // install the deps 40 | var npmInstall = _child_process2.default.spawn('npm', ['install'], { cwd: absoluteTarget }); 41 | npmInstall.stdout.removeAllListeners('data'); 42 | npmInstall.stderr.removeAllListeners('data'); 43 | npmInstall.stdout.pipe(process.stdout); 44 | npmInstall.stderr.pipe(process.stderr); 45 | npmInstall.on('close', function () { 46 | clearInterval(spinner); 47 | var doxityrcFile = process.env.PWD + '/' + _constants.DOXITYRC_FILE; 48 | // overwrite doxityrc file 49 | if (_fs2.default.existsSync(doxityrcFile)) { 50 | _fs2.default.unlinkSync(doxityrcFile); 51 | } 52 | _fs2.default.writeFileSync(doxityrcFile, JSON.stringify(args, null, 2) + '\n'); 53 | 54 | process.stdout.write('Doxity is initialized! Now run `doxity build`\n'); 55 | process.exit(); 56 | }); 57 | }); 58 | }; 59 | 60 | var _fs = require('fs'); 61 | 62 | var _fs2 = _interopRequireDefault(_fs); 63 | 64 | var _child_process = require('child_process'); 65 | 66 | var _child_process2 = _interopRequireDefault(_child_process); 67 | 68 | var _request = require('request'); 69 | 70 | var _request2 = _interopRequireDefault(_request); 71 | 72 | var _path = require('path'); 73 | 74 | var _path2 = _interopRequireDefault(_path); 75 | 76 | var _tar = require('tar.gz'); 77 | 78 | var _tar2 = _interopRequireDefault(_tar); 79 | 80 | var _helpers = require('./helpers'); 81 | 82 | var _constants = require('./constants'); 83 | 84 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -------------------------------------------------------------------------------- /lib/publish.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (_ref) { 8 | var target = _ref.target, 9 | out = _ref.out; 10 | 11 | // TODO check folder exists... 12 | var cwd = process.env.PWD + '/' + target; 13 | var outputFolder = cwd + '/public'; 14 | var destFolder = process.env.PWD + '/' + out; 15 | (0, _helpers.clearDirectory)(destFolder).then(function () { 16 | var runDev = _child_process2.default.spawn('npm', ['run', 'build'], { cwd: cwd }); 17 | runDev.stdout.pipe(process.stdout); 18 | runDev.stderr.pipe(process.stderr); 19 | runDev.on('close', function () { 20 | _fs2.default.renameSync(outputFolder, destFolder); 21 | process.stdout.write('Published Documentation to ' + destFolder + '\n'); 22 | process.exit(); 23 | }); 24 | }); 25 | }; 26 | 27 | var _fs = require('fs'); 28 | 29 | var _fs2 = _interopRequireDefault(_fs); 30 | 31 | var _child_process = require('child_process'); 32 | 33 | var _child_process2 = _interopRequireDefault(_child_process); 34 | 35 | var _helpers = require('./helpers'); 36 | 37 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@digix/doxity", 3 | "version": "0.5.2", 4 | "description": "Documentation Generator for Solidity Contracts", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "dev": "./node_modules/.bin/mocha --watch --compilers js:babel-register", 8 | "test": "./node_modules/.bin/mocha --compilers js:babel-register", 9 | "compile": "rm -rf lib/* && ./node_modules/.bin/babel src -d lib && chmod +x ./lib/bin/doxity.js", 10 | "prepublish": "npm run compile" 11 | }, 12 | "bin": { 13 | "doxity": "./lib/bin/doxity.js" 14 | }, 15 | "license": "BSD-3-Clause", 16 | "author": "Chris Hitchcott (http://hitchcott.com)", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/DigixGlobal/doxidity.git" 20 | }, 21 | "homepage": "https://github.com/DigixGlobal/doxidity", 22 | "devDependencies": { 23 | "babel-cli": "^6.18.0", 24 | "babel-preset-es2015": "^6.16.0", 25 | "babel-preset-stage-2": "^6.17.0", 26 | "babel-eslint": "^7.2.1", 27 | "babel-register": "^6.16.3", 28 | "eslint": "^3.7.1", 29 | "eslint-config-airbnb": "^12.0.0", 30 | "eslint-plugin-import": "^1.16.0", 31 | "eslint-plugin-jsx-a11y": "^2.2.3", 32 | "eslint-plugin-react": "^6.4.1" 33 | }, 34 | "dependencies": { 35 | "glob": "^7.1.1", 36 | "keccakjs": "^0.2.1", 37 | "minimist": "^1.2.0", 38 | "mkdirp": "^0.5.1", 39 | "request": "^2.79.0", 40 | "rimraf": "^2.5.4", 41 | "tar.gz": "^1.0.5", 42 | "toml": "^2.3.0", 43 | "tomlify-j0.4": "^1.0.1", 44 | "truffle-compile": "git://github.com/digixglobal/truffle-compile.git#017e1962623f7e84bd0c52d8bea8bf278ae79f5e", 45 | "truffle-config": "^1.0.2", 46 | "truffle-resolver": "^3.0.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/bin/doxity.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import minimist from 'minimist'; 5 | import * as Doxity from '../index'; 6 | 7 | const args = minimist(process.argv.slice(2)); 8 | 9 | // get json version 10 | 11 | if (!args._[0]) { 12 | const { version } = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()); 13 | console.log(` 14 | Doxity v${version} 15 | 16 | Commands: 17 | 18 | init Initialize your project for use with doxity 19 | compile Compile solidity contracts to generate docs data 20 | develop Spin up a development server for customizing output 21 | publish Generate static HTML documenation 22 | build compile + publish 23 | 24 | Parameters: 25 | 26 | --target Gatsby project source files directory 27 | --src Folder that contains the contracts you want to compile 28 | --dir Folder in gatsby project to dump contract data 29 | --out Folder to output the generated html (relative to project root) 30 | --source Git url for bootstrapping the gatsby project 31 | `); 32 | process.exit(); 33 | } else { 34 | Doxity.default[args._[0]](args); 35 | } 36 | -------------------------------------------------------------------------------- /src/build.js: -------------------------------------------------------------------------------- 1 | import compile from './compile'; 2 | import publish from './publish'; 3 | 4 | export default function (args) { 5 | compile(args).then(() => { 6 | publish(args); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/compile/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | import fs from 'fs'; 4 | import glob from 'glob'; 5 | import toml from 'toml'; 6 | import tomlify from 'tomlify-j0.4'; 7 | 8 | import { CONFIG_FILE, README_FILE, README_TARGET, DOXITYRC_FILE } from '../constants'; 9 | 10 | import solc from './solc'; 11 | import parseAbi from './parse-abi'; 12 | 13 | function compile({ whitelist, contracts, output, target, version }) { 14 | // do we need to check for whitelist? 15 | const defaultWhitelist = { source: true, bytecode: true, abi: true, methods: true }; 16 | // if (whitelist && Object.keys(whitelist).length > 0) { 17 | // defaultWhitelist = whitelist.all || {}; 18 | // } 19 | process.stdout.write(`Generating output for ${Object.keys(contracts).length} contracts...\n`); 20 | Object.keys(contracts).forEach((contractName) => { 21 | const contract = contracts[contractName]; 22 | // determine whether we should be skipped 23 | if (whitelist && !whitelist[contractName]) { return null; } 24 | // otherwise, pick up the defaultss 25 | const myWhitelist = { ...defaultWhitelist, ...(whitelist || {})[contractName] }; 26 | // get the source file 27 | const { fileName } = contract; 28 | if (!fileName) { return null; } // there was an error parsing... 29 | // TODO fix me 30 | // const interaction = {}; 31 | // get deploy info from truffle 32 | // let address; 33 | // if (interaction) { 34 | // try { 35 | // const instance = require(`${process.env.PWD}/build/contracts/${contractName}.sol.js`); 36 | // address = instance.all_networks[interaction.network].address; 37 | // } catch (e) { /* do noithing */ } 38 | // } 39 | const { bin, opcodes, abi, devdoc } = contract; 40 | const { author, title } = devdoc; 41 | const data = { 42 | author, 43 | title, 44 | fileName: fileName.replace(process.env.PWD, ''), 45 | // address, 46 | name: contractName, 47 | // only pass these if they are whitelisted 48 | abi: myWhitelist.abi && abi, 49 | bin: myWhitelist.bytecode && bin, 50 | opcodes: myWhitelist.bytecode && opcodes, 51 | source: myWhitelist.source && fs.readFileSync(fileName).toString(), 52 | abiDocs: myWhitelist.methods && parseAbi(contract), 53 | }; 54 | return fs.writeFileSync(`${output}/${contractName}.json`, `${JSON.stringify(data)}\n`); 55 | }); 56 | 57 | // TODO find in a better way? 58 | let pkgConfig = {}; 59 | try { 60 | pkgConfig = JSON.parse(fs.readFileSync(`${process.env.PWD}/package.json`)); 61 | } catch (e) { 62 | // console.log('package.json not found, add one for more output'); 63 | } 64 | 65 | let config = { 66 | compiler: version, 67 | name: pkgConfig.name, 68 | license: pkgConfig.license, 69 | version: pkgConfig.version, 70 | description: pkgConfig.description, 71 | homepage: pkgConfig.homepage, 72 | interaction: {}, // TODO implement 73 | author: (pkgConfig.author && pkgConfig.author.name) || pkgConfig.author, 74 | buildTime: new Date(), 75 | }; 76 | 77 | const configFile = `${process.env.PWD}/${target}/${CONFIG_FILE}`; 78 | 79 | try { // try marginging with old config 80 | config = { ...toml.parse(fs.readFileSync(configFile).toString()), ...config }; 81 | } catch (e) { 82 | /* do nothing */ 83 | // console.log('Error copying config'); 84 | } 85 | 86 | try { // try marginging with doxity config 87 | config = { ...config, ...JSON.parse(fs.readFileSync(`${process.env.PWD}/${DOXITYRC_FILE}`).toString()) }; 88 | } catch (e) { /* do nothing */ } 89 | 90 | // write the config 91 | if (fs.existsSync(configFile)) { fs.unlinkSync(configFile); } 92 | fs.writeFileSync(configFile, `${tomlify(config)}`); 93 | 94 | // copy the readme 95 | try { 96 | const readmeFile = glob.sync(`${process.env.PWD}/${README_FILE}`, { nocase: true })[0]; 97 | const readmeTarget = `${process.env.PWD}/${target}/${README_TARGET}`; 98 | if (fs.existsSync(readmeTarget)) { fs.unlinkSync(readmeTarget); } 99 | fs.writeFileSync(readmeTarget, fs.readFileSync(readmeFile)); 100 | } catch (e) { 101 | /* do nothing */ 102 | // console.log('Readme file not found, ignoring...'); 103 | } 104 | process.stdout.write(' done!\n'); 105 | } 106 | 107 | export default function (opts) { 108 | const output = `${process.env.PWD}/${opts.target}/${opts.dir}`; 109 | if (!fs.existsSync(output)) { throw new Error(`Output directory ${output} not found, are you in the right directory?`); } 110 | // clear out the output folder (remove all json files) 111 | glob.sync(`${output}/*.json`).forEach(file => fs.unlinkSync(file)); 112 | // get the natspec 113 | return solc(opts.src).then(({ contracts }) => { 114 | compile({ ...opts, output, contracts }); 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /src/compile/parse-abi.js: -------------------------------------------------------------------------------- 1 | import { getFunctionSignature } from '../helpers'; 2 | 3 | export default function (contract) { 4 | return contract.abi.map((method) => { 5 | // get find relevent docs 6 | const inputParams = method.inputs || []; 7 | const signature = method.name && `${method.name}(${inputParams.map(i => i.type).join(',')})`; 8 | const devDocs = (contract.devdoc.methods || {})[signature] || {}; 9 | const userDocs = (contract.userdoc.methods || {})[signature] || {}; 10 | // map abi inputs to devdoc inputs 11 | const params = devDocs.params || {}; 12 | const inputs = inputParams.map(param => ({ ...param, description: params[param.name] })); 13 | // don't write this 14 | delete devDocs.params; 15 | 16 | // START HACK workaround pending https://github.com/ethereum/solidity/issues/1277 17 | // TODO map outputs properly once compiler splits them out 18 | // in the meantime, use json array 19 | // parse devDocs.return as a json object 20 | let outputParams; 21 | let outputs; 22 | try { 23 | outputParams = JSON.parse(devDocs.return); 24 | } catch (e) { 25 | try { 26 | const split = devDocs.return.split(' '); 27 | const name = split.shift(); 28 | outputParams = { [name]: split.join(' ') }; 29 | } catch (e2) { /* */ } 30 | } 31 | try { 32 | outputs = method.outputs.map(param => ({ ...param, description: outputParams[param.name] })); 33 | } catch (e) { /* */ } 34 | // END HACK 35 | 36 | return { 37 | ...method, 38 | ...devDocs, 39 | ...userDocs, 40 | inputs, 41 | outputs, 42 | signature, 43 | signatureHash: signature && getFunctionSignature(signature), 44 | }; 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /src/compile/parse-asm.js: -------------------------------------------------------------------------------- 1 | import { getFunctionSignature } from '../helpers'; 2 | 3 | /* 4 | WORK IN PROGRESS 5 | 6 | we want to end up with a structure like: 7 | 8 | { 9 | name: ContractResolver, 10 | pragma: '0.4.2', 11 | imports: [ 12 | { 13 | name: ContractResolver, 14 | path: '../aux/blah', 15 | } 16 | ], 17 | docs: [ 18 | 19 | ] 20 | : 21 | } 22 | */ 23 | 24 | export default function ({ contract, source }) { 25 | 26 | const definitions = { 27 | PragmaDirective({ attributes }) { 28 | const [compiler, ...version] = attributes.literals; 29 | return { pragma: `${compiler} ${version.join('')}` }; 30 | }, 31 | ContractDefinition({ attributes, children }) { 32 | // console.log(children); 33 | return { 34 | ...attributes, 35 | definitions: children.map(parseDefinition).filter(c => c), 36 | }; 37 | }, 38 | _definition(args) { 39 | // console.log('definitoin', args); 40 | if (args.attributes.name === 'find') { 41 | // console.log(JSON.stringify(args.children, null, 2)); 42 | // merge inputs and outputs 43 | let signatureParams = []; 44 | const [ inputs, outputs ] = args.children.filter(child => child.name === "ParameterList") 45 | .map(params => params.children.map(param => { 46 | let { type } = param.attributes; 47 | if (param.children[0].name === 'UserDefinedTypeName') { 48 | type = `${contract.name}.${param.attributes.type.split(' ').slice(1, 3).join(' ')}` 49 | } 50 | return { ...param.attributes, type }; 51 | })); 52 | console.log("params", args.attributes.name, inputs, outputs); 53 | } 54 | // add devdocs 55 | return { 56 | ...args.attributes, 57 | // signature: 58 | } 59 | // return name, 60 | // signature 61 | }, 62 | VariableDeclaration(args) { 63 | return { definition: 'variable', ...this._definition(args) }; 64 | }, 65 | ModifierDefinition(args) { 66 | return { definition: 'modifier', ...this._definition(args) }; 67 | }, 68 | StructDefinition(args) { 69 | return { definition: 'struct', ...this._definition(args) }; 70 | }, 71 | FunctionDefinition(args) { 72 | return { definition: 'function', ...this._definition(args) }; 73 | }, 74 | } 75 | 76 | function parseDefinition(definition) { 77 | return definitions[definition.name] && definitions[definition.name](definition); 78 | } 79 | source.AST.children.map(parseDefinition); 80 | return []; 81 | } 82 | -------------------------------------------------------------------------------- /src/compile/solc.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import childProcess from 'child_process'; 3 | import Config from 'truffle-config'; 4 | import Resolver from 'truffle-resolver'; 5 | import compile from 'truffle-compile'; 6 | 7 | export default function (src) { 8 | // detect if we're in a truffle project 9 | return new Promise((resolve) => { 10 | if (fs.existsSync(`${process.env.PWD}/truffle.js`)) { 11 | const config = Config.default(); 12 | config.resolver = new Resolver(config); 13 | config.rawData = true; 14 | compile.all(config, (err, res) => { 15 | if (err) { throw err; } 16 | resolve({ 17 | contracts: Object.keys(res).reduce((o, k) => { 18 | const { metadata, ...data } = res[k].rawData; 19 | try { 20 | const parsed = JSON.parse(metadata); 21 | const fN = Object.keys(parsed.settings.compilationTarget)[0]; 22 | data.fileName = fN.indexOf(process.env.PWD) === 0 ? fN : `${process.env.PWD}/node_modules/${fN}`; 23 | data.output = parsed.output; 24 | } catch (e) { 25 | console.log(`⚠️ Error parsing Contract: ${k}`); 26 | } 27 | return { 28 | ...o, 29 | [k]: data, 30 | }; 31 | }, {}), 32 | }); 33 | }); 34 | } else { 35 | const exec = `solc --combined-json abi,asm,ast,bin,bin-runtime,clone-bin,devdoc,interface,opcodes,srcmap,srcmap-runtime,userdoc ${src}`; 36 | const res = JSON.parse(childProcess.execSync(exec)); 37 | resolve({ 38 | contracts: Object.keys(res.contracts).reduce((o, k) => { 39 | const file = k.split(':')[0]; 40 | const fileFragments = file.split('/'); 41 | const contractName = fileFragments[fileFragments.length - 1].split('.sol')[0]; 42 | const contract = res.contracts[k]; 43 | const fileName = `${process.env.PWD}/${k.split(':')[0]}`; 44 | return { 45 | ...o, 46 | [contractName]: { 47 | ...contract, 48 | fileName, 49 | abi: JSON.parse(contract.abi), 50 | devdoc: JSON.parse(contract.devdoc), 51 | userdoc: JSON.parse(contract.userdoc), 52 | }, 53 | }; 54 | }, {}), 55 | }); 56 | } 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_SOURCE = 'https://github.com/DigixGlobal/doxity-gatsby-starter-project/archive/a4886b7a7a04c018ac04fed3125d7d4785e74bed.tar.gz'; 2 | export const DOXITYRC_FILE = '.doxityrc'; 3 | export const DEFAULT_TARGET = 'scripts/doxity'; 4 | export const DEFAULT_PAGES_DIR = 'pages/docs'; 5 | export const DEFAULT_SRC_DIR = 'contracts/*'; 6 | export const DEFAULT_PUBLISH_DIR = 'docs'; 7 | export const CONFIG_FILE = 'config.toml'; 8 | export const README_FILE = 'README.md'; 9 | export const README_TARGET = 'pages/index.md'; 10 | -------------------------------------------------------------------------------- /src/develop.js: -------------------------------------------------------------------------------- 1 | import childProcess from 'child_process'; 2 | 3 | export default function ({ target }) { 4 | // TODO check we're in ther right folder.. ? 5 | const runDev = childProcess.spawn('npm', ['run', 'develop'], { cwd: `${process.env.PWD}/${target}` }); 6 | runDev.stdout.pipe(process.stdout); 7 | runDev.stderr.pipe(process.stderr); 8 | runDev.on('close', () => process.exit()); 9 | } 10 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | import rmdir from 'rimraf'; 2 | import mkdirp from 'mkdirp'; 3 | import Keccak from 'keccakjs'; 4 | 5 | export function clearDirectory(target) { 6 | return new Promise((resolve, reject) => { 7 | mkdirp(target, () => { 8 | rmdir(target, (err) => { 9 | if (err) { return reject(err); } 10 | return resolve(); 11 | }); 12 | }); 13 | }); 14 | } 15 | 16 | export function getFunctionSignature(signature) { 17 | return new Keccak(256).update(signature).digest('hex').substr(0, 8); 18 | } 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import { 4 | DOXITYRC_FILE, 5 | DEFAULT_SOURCE, 6 | DEFAULT_TARGET, 7 | DEFAULT_PAGES_DIR, 8 | DEFAULT_SRC_DIR, 9 | DEFAULT_PUBLISH_DIR, 10 | } from './constants'; 11 | 12 | import init from './init'; 13 | import compile from './compile'; 14 | import develop from './develop'; 15 | import publish from './publish'; 16 | import build from './build'; 17 | 18 | const methods = { 19 | compile, 20 | init, 21 | develop, 22 | publish, 23 | build, 24 | }; 25 | 26 | function populateArguments(passed) { 27 | // cruft from minimist 28 | delete passed._; 29 | // fallback to defaults 30 | const defaults = { 31 | target: DEFAULT_TARGET, 32 | src: DEFAULT_SRC_DIR, 33 | dir: DEFAULT_PAGES_DIR, 34 | source: DEFAULT_SOURCE, 35 | out: DEFAULT_PUBLISH_DIR, 36 | }; 37 | // merge with .doxityrc 38 | let saved = {}; 39 | try { 40 | saved = JSON.parse(fs.readFileSync(`${process.env.PWD}/${DOXITYRC_FILE}`).toString()); 41 | } catch (e) { 42 | console.log('.doxityrc not found or unreadable'); 43 | } 44 | // return merge 45 | return { ...defaults, ...saved, ...passed }; 46 | } 47 | // wire up defaults 48 | const wrappedMethods = {}; 49 | Object.keys(methods).forEach((key) => { 50 | wrappedMethods[key] = (args) => { 51 | const newArgs = populateArguments(args); 52 | return methods[key](newArgs); 53 | }; 54 | }); 55 | 56 | export default wrappedMethods; 57 | -------------------------------------------------------------------------------- /src/init.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import childProcess from 'child_process'; 3 | import request from 'request'; 4 | import path from 'path'; 5 | import targz from 'tar.gz'; 6 | 7 | import { clearDirectory } from './helpers'; 8 | 9 | import { DOXITYRC_FILE } from './constants'; 10 | 11 | export default function (args) { 12 | const { source, target } = args; 13 | // TODO check folder exists... 14 | const absoluteTarget = `${process.env.PWD}/${target}`; 15 | const tmpTarget = path.resolve(`${process.env.PWD}/${target}/../doxity-tmp-${new Date()}`); 16 | // clear the target dir 17 | clearDirectory(absoluteTarget) 18 | .then(() => { 19 | // clone the repo 20 | process.stdout.write(`Getting ${source}...\n`); 21 | // pipe package to thingy. 22 | return new Promise((resolve) => { 23 | request.get(source) 24 | .pipe(targz().createWriteStream(tmpTarget)) 25 | .on('finish', resolve); 26 | }); 27 | }) 28 | // rename the downloaded folder to doxity 29 | .then(() => { 30 | fs.renameSync(`${tmpTarget}/${fs.readdirSync(tmpTarget)[0]}`, absoluteTarget); 31 | fs.rmdirSync(tmpTarget); 32 | }) 33 | .then(() => { 34 | // fancy spinner 35 | let i = 0; 36 | const seq = '⣷⣯⣟⡿⢿⣻⣽⣾'.split(''); 37 | const message = 'Setting up doxity project with npm install. This may take a while...'; 38 | const spinner = setInterval(() => { 39 | i++; 40 | if (i >= seq.length) { i = 0; } 41 | process.stdout.write(`\r${seq[i]} ${message}`); 42 | }, 1000 / 24); 43 | // install the deps 44 | const npmInstall = childProcess.spawn('npm', ['install'], { cwd: absoluteTarget }); 45 | npmInstall.stdout.removeAllListeners('data'); 46 | npmInstall.stderr.removeAllListeners('data'); 47 | npmInstall.stdout.pipe(process.stdout); 48 | npmInstall.stderr.pipe(process.stderr); 49 | npmInstall.on('close', () => { 50 | clearInterval(spinner); 51 | const doxityrcFile = `${process.env.PWD}/${DOXITYRC_FILE}`; 52 | // overwrite doxityrc file 53 | if (fs.existsSync(doxityrcFile)) { fs.unlinkSync(doxityrcFile); } 54 | fs.writeFileSync(doxityrcFile, `${JSON.stringify(args, null, 2)}\n`); 55 | 56 | process.stdout.write('Doxity is initialized! Now run `doxity build`\n'); 57 | process.exit(); 58 | }); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /src/publish.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import childProcess from 'child_process'; 3 | 4 | import { clearDirectory } from './helpers'; 5 | 6 | export default function ({ target, out }) { 7 | // TODO check folder exists... 8 | const cwd = `${process.env.PWD}/${target}`; 9 | const outputFolder = `${cwd}/public`; 10 | const destFolder = `${process.env.PWD}/${out}`; 11 | clearDirectory(destFolder).then(() => { 12 | const runDev = childProcess.spawn('npm', ['run', 'build'], { cwd }); 13 | runDev.stdout.pipe(process.stdout); 14 | runDev.stderr.pipe(process.stderr); 15 | runDev.on('close', () => { 16 | fs.renameSync(outputFolder, destFolder); 17 | process.stdout.write(`Published Documentation to ${destFolder}\n`); 18 | process.exit(); 19 | }); 20 | }); 21 | } 22 | --------------------------------------------------------------------------------