├── .gitmodules ├── .gitignore ├── .travis.yml ├── lib ├── util.js ├── hosts │ ├── ipfshostwithlocalreader.js │ └── ipfshost.js ├── manifests │ └── v1.js ├── lockfile.js ├── lockfiles │ └── v1.js ├── manifest.js ├── registries │ └── memoryregistry.js ├── config.js ├── sources.js ├── publisher.js ├── installer.js ├── preflight.js └── indexes │ └── github-examples.js ├── templates └── epm.json ├── custom-use-cases ├── owned-1.0.0 │ ├── contracts │ │ ├── mortal.sol │ │ ├── owned.sol │ │ └── transferable.sol │ └── ethpm.json ├── owned-2.0.0 │ ├── contracts │ │ ├── mortal.sol │ │ ├── owned.sol │ │ └── transferable.sol │ └── ethpm.json ├── dependency-conflict-2.0.0 │ ├── contracts │ │ ├── mortal.sol │ │ ├── owned.sol │ │ └── transferable.sol │ └── ethpm.json ├── eth-usd-oracle-1.0.0 │ ├── contracts │ │ └── PriceOracle.sol │ └── ethpm.json └── README.md ├── package.json ├── README.md ├── test ├── test_dependency_conflict.js ├── test_install.js ├── test_publish.js └── lib │ └── testhelper.js └── index.js /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: node_js 4 | node_js: 5 | - "8" 6 | 7 | install: 8 | - npm install 9 | 10 | script: 11 | - npm test 12 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var URL = require("url"); 2 | 3 | var Util = { 4 | isURI: function(url) { 5 | return URL.parse(url).protocol != null; 6 | } 7 | }; 8 | 9 | module.exports = Util; 10 | -------------------------------------------------------------------------------- /templates/epm.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 1, 3 | "package_name": "", 4 | "version": "0.0.1", 5 | "authors": [], 6 | "description": "", 7 | "sources": [], 8 | "contracts": [], 9 | "dependencies": {} 10 | } 11 | -------------------------------------------------------------------------------- /custom-use-cases/owned-1.0.0/contracts/mortal.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | import {owned} from "./owned.sol"; 4 | 5 | 6 | contract mortal is owned { 7 | function kill() public onlyowner { 8 | selfdestruct(msg.sender); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /custom-use-cases/owned-2.0.0/contracts/mortal.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | import {owned} from "./owned.sol"; 4 | 5 | 6 | contract mortal is owned { 7 | function kill() public onlyowner { 8 | selfdestruct(msg.sender); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /custom-use-cases/dependency-conflict-2.0.0/contracts/mortal.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | import {owned} from "./owned.sol"; 4 | 5 | 6 | contract mortal is owned { 7 | function kill() public onlyowner { 8 | selfdestruct(msg.sender); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /custom-use-cases/owned-1.0.0/ethpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | "Piper Merriam " 4 | ], 5 | "description": "Base contracts for things that have an owner", 6 | "keywords": [ 7 | "owned" 8 | ], 9 | "license": "MIT", 10 | "manifest_version": 1, 11 | "package_name": "owned", 12 | "version": "1.0.0" 13 | } 14 | -------------------------------------------------------------------------------- /custom-use-cases/owned-1.0.0/contracts/owned.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | 4 | contract owned { 5 | address owner; 6 | 7 | function owned() { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier onlyowner { 12 | if (msg.sender != owner) { 13 | throw; 14 | } else { 15 | _; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /custom-use-cases/owned-2.0.0/contracts/owned.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | 4 | contract owned { 5 | address owner; 6 | 7 | function owned() { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier onlyowner { 12 | if (msg.sender != owner) { 13 | throw; 14 | } else { 15 | _; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /custom-use-cases/dependency-conflict-2.0.0/contracts/owned.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | 4 | contract owned { 5 | address owner; 6 | 7 | function owned() { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier onlyowner { 12 | if (msg.sender != owner) { 13 | throw; 14 | } else { 15 | _; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /custom-use-cases/eth-usd-oracle-1.0.0/contracts/PriceOracle.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | import "owned/owned"; 4 | 5 | contract PriceOracle is owned { 6 | uint priceInCents; 7 | 8 | function getPrice() constant returns (uint) { 9 | return priceInCents; 10 | } 11 | 12 | function setPrice(uint _priceInCents) onlyowner public { 13 | priceInCents = _priceInCents; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /custom-use-cases/owned-1.0.0/contracts/transferable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | import {owned} from "./owned.sol"; 4 | 5 | 6 | contract transferable is owned { 7 | event OwnerChanged(address indexed previousOwner, address indexed newOwner); 8 | 9 | function transferOwnership(address newOwner) public onlyowner { 10 | OwnerChanged(owner, newOwner); 11 | owner = newOwner; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /custom-use-cases/owned-2.0.0/contracts/transferable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | import {owned} from "./owned.sol"; 4 | 5 | 6 | contract transferable is owned { 7 | event OwnerChanged(address indexed previousOwner, address indexed newOwner); 8 | 9 | function transferOwnership(address newOwner) public onlyowner { 10 | OwnerChanged(owner, newOwner); 11 | owner = newOwner; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /custom-use-cases/dependency-conflict-2.0.0/contracts/transferable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | import {owned} from "./owned.sol"; 4 | 5 | 6 | contract transferable is owned { 7 | event OwnerChanged(address indexed previousOwner, address indexed newOwner); 8 | 9 | function transferOwnership(address newOwner) public onlyowner { 10 | OwnerChanged(owner, newOwner); 11 | owner = newOwner; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /custom-use-cases/owned-2.0.0/ethpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | "Piper Merriam " 4 | ], 5 | "description": "Base contracts for things that have an owner", 6 | "keywords": [ 7 | "owned" 8 | ], 9 | "license": "MIT", 10 | "manifest_version": 1, 11 | "package_name": "owned", 12 | "sources": [ 13 | "./contracts/*.sol", 14 | "./contracts/**/*.sol" 15 | ], 16 | "version": "2.0.0", 17 | "x-via": "test-packager" 18 | } 19 | -------------------------------------------------------------------------------- /custom-use-cases/eth-usd-oracle-1.0.0/ethpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 1, 3 | "package_name": "eth-usd-oracle", 4 | "version": "1.0.0", 5 | "authors": [ 6 | "Piper Merriam " 8 | ], 9 | "description": "Oracle of current ETH prices in USD.", 10 | "sources": [ 11 | "./contracts/*.sol", 12 | "./contracts/**/*.sol" 13 | ], 14 | "dependencies": { 15 | "owned": "1.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /custom-use-cases/dependency-conflict-2.0.0/ethpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | "Tim Coulter " 4 | ], 5 | "description": "Package whose dependencies have dependencies that conflict.", 6 | "keywords": [ 7 | "dependency", 8 | "conflict" 9 | ], 10 | "license": "MIT", 11 | "manifest_version": 1, 12 | "package_name": "dependency-conflict", 13 | "sources": [ 14 | "./contracts/*.sol", 15 | "./contracts/**/*.sol" 16 | ], 17 | "dependencies": { 18 | "owned": "2.0.0", 19 | "eth-usd-oracle": "1.0.0" 20 | }, 21 | "version": "2.0.0" 22 | } 23 | -------------------------------------------------------------------------------- /custom-use-cases/README.md: -------------------------------------------------------------------------------- 1 | # Use Case Examples 2 | 3 | The following example packages can be used as test vectors for the supported 4 | use cases. 5 | 6 | 7 | ## Base Contract 8 | 9 | These packages do not provide any deployed instances of their contracts as they are 10 | intended to be used as base contracts or re-usable generic contracts for common 11 | use cases. 12 | 13 | * [owned](./owned-example/) 14 | * [Standard ERC20 Token](./erc20-example/) 15 | 16 | ## Library 17 | 18 | * [Safe Math Library](./safe-math-example/) 19 | 20 | 21 | ## Contract 22 | 23 | * [ETH/USD Oracle](./eth-usd-oracle-example/) 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethpm", 3 | "version": "0.0.19", 4 | "description": "Ethereum Package Installer and Publishing Library", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "git submodule update --init --recursive && mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ethpm/ethpm-js.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/ethpm/ethpm-js/issues" 17 | }, 18 | "homepage": "https://github.com/ethpm/ethpm-js#readme", 19 | "devDependencies": { 20 | "finalhandler": "^0.5.1", 21 | "ipfsd-ctl": "^0.21.0", 22 | "mocha": "^3.1.2", 23 | "solc": "^0.4.4", 24 | "temp": "^0.8.3" 25 | }, 26 | "dependencies": { 27 | "async": "^2.1.2", 28 | "ethpm-spec": "^1.0.1", 29 | "fs-extra": "^6.0.1", 30 | "glob": "^7.1.1", 31 | "ipfs-mini": "^1.1.2", 32 | "jsonschema": "^1.1.1", 33 | "lodash": "^4.17.20", 34 | "node-dir": "^0.1.16", 35 | "request": "^2.88.2", 36 | "semver": "^5.3.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/hosts/ipfshostwithlocalreader.js: -------------------------------------------------------------------------------- 1 | var request = require("request"); 2 | var inherits = require("util").inherits; 3 | var IPFSHost = require("./ipfshost"); 4 | 5 | inherits(IPFSHostWithLocalReader, IPFSHost); 6 | 7 | function IPFSHostWithLocalReader(host, port, protocol) { 8 | IPFSHost.call(this, host, port, protocol); 9 | } 10 | 11 | IPFSHostWithLocalReader.prototype.get = function(uri) { 12 | var self = this; 13 | return new Promise(function(accept, reject) { 14 | if (uri.indexOf("ipfs://") != 0) { 15 | return reject(new Error("Don't know how to resolve URI " + uri)); 16 | } 17 | 18 | var hash = uri.replace("ipfs://", ""); 19 | 20 | var path = 'api/v0/cat/' + hash; 21 | var processedUrl = `${self.protocol}://${self.host}:${self.port}/${path}`; 22 | 23 | request(processedUrl, function(error, response, body) { 24 | if(error) { 25 | reject(error); 26 | } else if (response.statusCode !== 200) { 27 | reject(new Error(`Unknown server response ${response.statusCode} when downloading hash ${hash}`)); 28 | } else { 29 | accept(body); 30 | }; 31 | }); 32 | }); 33 | }; 34 | 35 | module.exports = IPFSHostWithLocalReader; 36 | -------------------------------------------------------------------------------- /lib/manifests/v1.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | 3 | var V1 = { 4 | version: 1, 5 | validate: function(json) { 6 | // TODO: throw errors for data that *must* be there, and validate correct 7 | // format of data that is there. Perhaps use json spec. 8 | }, 9 | normalize: function(json) { 10 | // Add keys with default values if non-existent 11 | // (don't use this function for validation) 12 | var defaults = { 13 | authors: {}, 14 | license: "", 15 | description: "", 16 | keywords: [], 17 | links: {}, 18 | sources: null, 19 | dependencies: {} 20 | }; 21 | 22 | json = _.merge(defaults, json); 23 | 24 | return json; 25 | }, 26 | fromLockfile: function(lockfile) { 27 | var meta = lockfile.meta || {}; 28 | var sources = lockfile.sources || {}; 29 | 30 | var manifest = { 31 | manifest_version: this.version, 32 | package_name: lockfile.package_name, 33 | authors: meta.authors, 34 | version: lockfile.version, 35 | license: meta.license, 36 | description: meta.description, 37 | keywords: meta.keywords, 38 | links: meta.links, 39 | sources: Object.keys(sources), 40 | dependencies: lockfile.build_dependencies 41 | }; 42 | 43 | return manifest; 44 | } 45 | }; 46 | 47 | module.exports = V1; 48 | -------------------------------------------------------------------------------- /lib/lockfile.js: -------------------------------------------------------------------------------- 1 | var V1 = require("./lockfiles/v1"); 2 | var fs = require("fs"); 3 | 4 | var Lockfile = { 5 | getInterpreter: function(lockfile_version) { 6 | var interpreter = V1; 7 | 8 | if (lockfile_version == null) return interpreter; 9 | 10 | // This could be slightly more clever by tacking on a "V" onto the version and 11 | // requiring that file (and catching the error if the require fails). Will do that 12 | // if this gets too unruly. 13 | switch (parseInt(lockfile_version)) { 14 | case 1: 15 | interpreter = V1; 16 | break; 17 | default: 18 | if (lockfile_version == null) { 19 | // do nothing; use default 20 | } else { 21 | throw new Error("Unknown lockfile version " + lockfile_version); 22 | } 23 | break; 24 | } 25 | 26 | return interpreter; 27 | }, 28 | read: function(file) { 29 | var json = fs.readFileSync(file); 30 | json = JSON.parse(json); 31 | 32 | var interpreter = this.getInterpreter(json.lockfile_version); 33 | 34 | interpreter.validate(json); 35 | 36 | return interpreter.normalize(json); 37 | }, 38 | validate: function(json) { 39 | var interpreter = this.getInterpreter(json.lockfile_version); 40 | return interpreter.validate(json); 41 | } 42 | }; 43 | 44 | module.exports = Lockfile; 45 | -------------------------------------------------------------------------------- /lib/lockfiles/v1.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var validate = require('jsonschema').validate; 3 | var lockfile_spec = require("ethpm-spec/spec/release-lockfile.spec.json"); 4 | 5 | var V1 = { 6 | version: 1, 7 | validate: function(json) { 8 | return validate(json, lockfile_spec); 9 | }, 10 | normalize: function(json) { 11 | // Add keys with default values if non-existent 12 | // (don't use this function for validation) 13 | var defaults = { 14 | authors: {}, 15 | license: "", 16 | description: "", 17 | keywords: [], 18 | links: {}, 19 | sources: null, 20 | dependencies: {} 21 | }; 22 | 23 | json = _.merge(defaults, json); 24 | 25 | return json; 26 | }, 27 | // fromLockfile: function(lockfile) { 28 | // var meta = lockfile.meta || {}; 29 | // var sources = lockfile.sources || {}; 30 | // 31 | // var manifest = { 32 | // manifest_version: this.version, 33 | // package_name: lockfile.package_name, 34 | // authors: meta.authors, 35 | // version: lockfile.version, 36 | // license: meta.license, 37 | // description: meta.description, 38 | // keywords: meta.keywords, 39 | // links: meta.links, 40 | // sources: Object.keys(sources), 41 | // dependencies: lockfile.build_dependencies 42 | // }; 43 | // 44 | // return manifest; 45 | // } 46 | }; 47 | 48 | module.exports = V1; 49 | -------------------------------------------------------------------------------- /lib/manifest.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var V1 = require("./manifests/v1"); 3 | 4 | var Manifest = { 5 | getInterpreter: function(manifest_version) { 6 | var interpreter = V1; 7 | 8 | if (manifest_version == null) return interpreter; 9 | 10 | // This could be slightly more clever by tacking on a "V" onto the version and 11 | // requiring that file (and catching the error if the require fails). Will do that 12 | // if this gets too unruly. 13 | switch (manifest_version) { 14 | case 1: 15 | interpreter = V1; 16 | break; 17 | default: 18 | if (manifest_version == null) { 19 | // do nothing; use default 20 | } else { 21 | throw new Error("Unknown manifest version " + manifest_version); 22 | } 23 | break; 24 | } 25 | 26 | return interpreter; 27 | }, 28 | read: function(file) { 29 | var json = fs.readFileSync(file); 30 | json = JSON.parse(json); 31 | 32 | var interpreter = this.getInterpreter(json.manifest_version); 33 | 34 | interpreter.validate(json); 35 | 36 | return interpreter.normalize(json); 37 | }, 38 | fromLockfile: function(lockfile) { 39 | // TODO: Match up lockfile versions with manifest versions somehow. 40 | var interpreter = this.getInterpreter(); 41 | var manifest = interpreter.fromLockfile(lockfile); 42 | 43 | interpreter.validate(manifest); 44 | 45 | manifest = interpreter.normalize(manifest); 46 | 47 | return manifest; 48 | } 49 | }; 50 | 51 | module.exports = Manifest; 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ethereum Package Manager / Javascript 2 | 3 | [![Join the chat at https://gitter.im/ethpm/Lobby](https://badges.gitter.im/ethpm/Lobby.svg)](https://gitter.im/ethpm/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | ### Overview 6 | 7 | This package provides utilities for publishing and consuming Ethereum packages based on the [Ethereum Package Manager specification](https://github.com/ethpm/epm-spec). It is meant to be integrated directly into development tools to support their use of the Ethereum Package Management ecosystem. 8 | 9 | ### Usage 10 | 11 | ```javascript 12 | // Require and configure EthPM relative to a package location on disk. 13 | // `host` and `registry` must conform to Javascript Host and Registry interface. 14 | // A "host" is a service that holds the files, like IPFS. A "registry" is a 15 | // service that records package versions that have been published and their 16 | // associated lockfile on the host. 17 | var EthPM = require("ethpm"); 18 | var config = EthPM.configure(package_directory, host, registry); 19 | 20 | // Install a single package into the current package, denoted by name and version. 21 | // Returns a promise. 22 | EthPM.installDependency(config, package_name, version_range); 23 | 24 | // Install all dependencies of the current package. 25 | // Returns a promise. 26 | EthPM.installPackage(config); 27 | 28 | // Publish the current package. 29 | // Returns a promise. 30 | // `contract_metadata` is information about published contracts you'd like include 31 | // in this package. See lockfile spec for more information. 32 | EthPM.publishPackage(config, contract_metadata); 33 | ``` 34 | 35 | ### Running Tests 36 | 37 | ``` 38 | $ npm test 39 | ``` 40 | 41 | ### Contributors 42 | 43 | Initial author: Tim Coulter ([@tcoulter](https://github.com/tcoulter)) 44 | 45 | This is a joint effort by Truffle, Populus, Dapple and Eris. 46 | -------------------------------------------------------------------------------- /test/test_dependency_conflict.js: -------------------------------------------------------------------------------- 1 | var TestHelper = require("./lib/testhelper"); 2 | var path = require("path"); 3 | var EPM = require('../index.js'); 4 | var dir = require("node-dir"); 5 | var assert = require("assert"); 6 | var fs = require("fs-extra"); 7 | 8 | describe("Dependency Conflict", function() { 9 | var helper = TestHelper.setup({ 10 | packages: [ 11 | "custom-use-cases/dependency-conflict-2.0.0", 12 | "custom-use-cases/owned-1.0.0", 13 | "custom-use-cases/owned-2.0.0", 14 | "custom-use-cases/eth-usd-oracle-1.0.0" 15 | ] 16 | }); 17 | 18 | var conflict; 19 | var owned1; 20 | var owned2; 21 | var ethUSD; 22 | 23 | before("setup variables", function() { 24 | conflict = helper.packages["dependency-conflict-2.0.0"]; 25 | owned1 = helper.packages["owned-1.0.0"]; 26 | owned2 = helper.packages["owned-2.0.0"]; 27 | ethUSD = helper.packages["eth-usd-oracle-1.0.0"] 28 | }); 29 | 30 | before("published conflicting dependencies", function() { 31 | this.timeout(25000); 32 | 33 | return owned1.package.publish().then(function() { 34 | return owned2.package.publish(); 35 | }).then(function() { 36 | // eth-usd-oracle has dependencies to install 37 | return ethUSD.package.install(); 38 | }).then(function() { 39 | return ethUSD.package.publish(); 40 | }); 41 | }); 42 | 43 | it("installs should fail during installation due to conflicting dependencies", function(done) { 44 | this.timeout(25000); 45 | 46 | conflict.package.install().then(function() { 47 | return done(new Error("This error shouldn't be evaluated because another error should have come before it.")); 48 | }).catch(function(e) { 49 | assert(e.message.indexOf("Your package and its dependencies require conflicting versions of") >= 0, "Got an unexpected error: " + e.message); 50 | done(); 51 | }).catch(done); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /lib/hosts/ipfshost.js: -------------------------------------------------------------------------------- 1 | var IPFS = require("ipfs-mini"); 2 | var request = require("request"); 3 | var Readable = require('stream').Readable; 4 | var URL = require("url"); 5 | var fs = require("fs"); 6 | 7 | function IPFSHost(host, port, protocol) { 8 | this.host = host || "localhost"; 9 | this.port = port || 5001; 10 | this.protocol = protocol || "http"; 11 | 12 | this.ipfs = new IPFS({ 13 | host: this.host, 14 | port: this.port, 15 | protocol: this.protocol 16 | }); 17 | } 18 | 19 | IPFSHost.prototype.putContents = function(contents) { 20 | var self = this; 21 | 22 | return new Promise(function(accept, reject) { 23 | self.ipfs.add(contents, (err, result) => { 24 | if (err) return reject(err); 25 | accept("ipfs://" + result); 26 | }); 27 | }); 28 | } 29 | 30 | IPFSHost.prototype.putFile = function(file) { 31 | var self = this; 32 | 33 | return new Promise(function(accept, reject) { 34 | fs.readFile(file, "utf8", function(err, data) { 35 | if (err) return reject(err); 36 | 37 | self.putContents(data).then(accept).catch(reject); 38 | }); 39 | }); 40 | }; 41 | 42 | IPFSHost.prototype.get = function(uri) { 43 | var self = this; 44 | return new Promise(function(accept, reject) { 45 | if (uri.indexOf("ipfs://") != 0) { 46 | return reject(new Error("Don't know how to resolve URI " + uri)); 47 | } 48 | 49 | var hash = uri.replace("ipfs://", ""); 50 | 51 | var path = 'api/v0/cat/' + hash; 52 | var processedUrl = `${self.protocol}://${self.host}:${self.port}/${path}`; 53 | 54 | request(processedUrl, function(error, response, body) { 55 | if(error) { 56 | reject(error); 57 | } else if (response.statusCode !== 200) { 58 | reject(new Error(`Unknown server response ${response.statusCode} when downloading hash ${hash}`)); 59 | } else { 60 | accept(body); 61 | }; 62 | }); 63 | }); 64 | }; 65 | 66 | module.exports = IPFSHost; 67 | -------------------------------------------------------------------------------- /lib/registries/memoryregistry.js: -------------------------------------------------------------------------------- 1 | var semver = require("semver"); 2 | 3 | function MemoryRegistry() { 4 | this.packages = {}; 5 | }; 6 | 7 | MemoryRegistry.prototype.getAllVersions = function(package_name, callback) { 8 | var self = this; 9 | return new Promise(function(accept, reject) { 10 | if (!self.packages[package_name]) { 11 | return accept([]); 12 | } 13 | accept(Object.keys(self.packages[package_name]).sort()); 14 | }); 15 | } 16 | 17 | MemoryRegistry.prototype.resolveVersion = function(package_name, version_range) { 18 | return this.getAllVersions(package_name).then(function(versions) { 19 | // This can be optimized. 20 | var max = null; 21 | 22 | versions.forEach(function(version) { 23 | if (semver.satisfies(version, version_range)) { 24 | if (max == null || semver.gte(version, max)) { 25 | max = version; 26 | } 27 | } 28 | }); 29 | 30 | return max; 31 | }); 32 | }; 33 | 34 | MemoryRegistry.prototype.getLockfileURI = function(package_name, version_range) { 35 | var self = this; 36 | return this.resolveVersion(package_name, version_range).then(function(version) { 37 | if (version == null) { 38 | throw new Error("Cannot find package '" + package_name + "' that satisfies the version range: " + version_range); 39 | } 40 | 41 | return self.packages[package_name][version]; 42 | }); 43 | }; 44 | 45 | MemoryRegistry.prototype.register = function(package_name, version, lockfileURI) { 46 | var self = this; 47 | return new Promise(function(accept, reject) { 48 | if (self.packages[package_name] && self.packages[package_name][version]) { 49 | return reject(new Error("Version " + version + " already exists for package " + package_name)); 50 | } 51 | 52 | if (!self.packages[package_name]) { 53 | self.packages[package_name] = []; 54 | } 55 | 56 | self.packages[package_name][version] = lockfileURI; 57 | accept(); 58 | }); 59 | }; 60 | 61 | module.exports = MemoryRegistry; 62 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var _ = require("lodash"); 3 | var path = require("path"); 4 | 5 | function Config(working_directory, manifest_file) { 6 | var self = this; 7 | 8 | this._values = {}; 9 | 10 | var props = { 11 | working_directory: function() { 12 | return working_directory || process.cwd(); 13 | }, 14 | 15 | source_directory: function() { 16 | return path.join(self.working_directory, "contracts"); 17 | }, 18 | 19 | contracts_directory: function() { 20 | return path.resolve(path.join(self.working_directory, "contracts")); 21 | }, 22 | 23 | manifest_file: function() { 24 | return manifest_file || path.resolve(path.join(self.working_directory, self.default_manifest_filename)) 25 | }, 26 | 27 | base_path: function() { 28 | return path.dirname(self.manifest_file); 29 | }, 30 | 31 | // Configuration options that rely on other options. 32 | installed_packages_directory: function() { 33 | return path.join(self.working_directory, "installed_contracts"); 34 | }, 35 | 36 | default_manifest_filename: function() { 37 | return "ethpm.json"; 38 | }, 39 | 40 | default_lockfile_filename: function() { 41 | return "lock.json"; 42 | }, 43 | 44 | default_lockfile_uri_filename: function() { 45 | return "lock.uri"; 46 | } 47 | }; 48 | 49 | Object.keys(props).forEach(function(prop) { 50 | self.addProp(prop, props[prop]); 51 | }); 52 | }; 53 | 54 | Config.prototype.addProp = function(key, obj) { 55 | Object.defineProperty(this, key, { 56 | get: obj.get || function() { 57 | return this._values[key] || obj(); 58 | }, 59 | set: obj.set || function(val) { 60 | this._values[key] = val; 61 | }, 62 | configurable: true, 63 | enumerable: true 64 | }); 65 | }; 66 | 67 | Config.prototype.with = function(obj) { 68 | return _.extend(Config.default(), this, obj); 69 | }; 70 | 71 | Config.prototype.merge = function(obj) { 72 | return _.extend(this, obj); 73 | }; 74 | 75 | Config.default = function() { 76 | return new Config(); 77 | }; 78 | 79 | module.exports = Config; 80 | -------------------------------------------------------------------------------- /test/test_install.js: -------------------------------------------------------------------------------- 1 | var TestHelper = require("./lib/testhelper"); 2 | var path = require("path"); 3 | var EPM = require('../index.js'); 4 | var dir = require("node-dir"); 5 | var assert = require("assert"); 6 | var fs = require("fs-extra"); 7 | 8 | describe("Install", function() { 9 | var helper = TestHelper.setup({ 10 | packages: [ 11 | "custom-use-cases/owned-1.0.0", 12 | "custom-use-cases/eth-usd-oracle-1.0.0" 13 | ], 14 | compile: [ 15 | "owned-1.0.0" 16 | ] 17 | }); 18 | 19 | var owned; 20 | var eth_usd; 21 | 22 | before("setup variables once previous steps are finished", function() { 23 | owned = helper.packages["owned-1.0.0"]; 24 | eth_usd = helper.packages["eth-usd-oracle-1.0.0"]; 25 | }); 26 | 27 | before("published owned for use as a dependency", function() { 28 | this.timeout(25000); 29 | 30 | return owned.package.publish(owned.contract_metadata); 31 | }); 32 | 33 | it("installs eth-usd correctly with owned as a dependency", function() { 34 | this.timeout(25000); 35 | 36 | var dependency_path = path.resolve(path.join(eth_usd.package.config.installed_packages_directory, "owned")); 37 | 38 | return eth_usd.package.install().then(function() { 39 | return new Promise(function(accept, reject) { 40 | dir.files(dependency_path, function(err, files) { 41 | if (err) return reject(err); 42 | accept(files); 43 | }); 44 | }); 45 | }).then(function(files) { 46 | var assertions = []; 47 | 48 | assert.equal(files.length, 6); // three contracts, their epm.json, their lock.json and lock.uri 49 | 50 | var lockfile = fs.readFileSync(path.join(dependency_path, "lock.json"), "utf8"); 51 | lockfile = JSON.parse(lockfile); 52 | 53 | Object.keys(lockfile.sources).forEach(function(relative_file_path) { 54 | var expected_example_path = path.join(owned.package.config.working_directory, relative_file_path); 55 | var actual_path = path.join(dependency_path, relative_file_path); 56 | 57 | assertions.push(helper.assertFilesMatch(expected_example_path, actual_path)); 58 | }); 59 | 60 | // TODO: assert contents of lockfile. 61 | 62 | return Promise.all(assertions); 63 | }); 64 | }); 65 | 66 | // TODO: Test to ensure installation errors when installing a package that has a bad lockfile. 67 | }); 68 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Preflight = require("./lib/preflight"); 2 | var Installer = require("./lib/installer"); 3 | var Publisher = require("./lib/publisher"); 4 | var IPFSHost = require("./lib/hosts/ipfshost"); 5 | var IPFSHostWithLocalReader = require("./lib/hosts/ipfshostwithlocalreader"); 6 | var MemoryRegistry = require("./lib/registries/memoryregistry"); 7 | var Config = require("./lib/config"); 8 | 9 | var fs = require('fs-extra'); 10 | var path = require("path"); 11 | var _ = require("lodash"); 12 | 13 | function EthPM(directory, host, registry) { 14 | this.host = host || new IPFSHost(); 15 | this.registry = registry || new MemoryRegistry(); 16 | 17 | this.config = Config.default().with({ 18 | working_directory: directory, 19 | base_dir: directory, 20 | host: host, 21 | registry: registry 22 | }); 23 | }; 24 | 25 | _.extend(EthPM.prototype, { 26 | install: function(manifest) { 27 | var installer = new Installer(this.config, this.config.installed_packages_directory); 28 | return installer.installDependencies(manifest); 29 | }, 30 | 31 | installDependency: function(package_name, version_range) { 32 | var installer = new Installer(this.config, this.config.installed_packages_directory); 33 | return installer.installPackage(package_name, version_range); 34 | }, 35 | 36 | // Publish the current package to the host and registry. 37 | // Contract metadata is also required for all contracts listed in the `contracts` portion of the manifest. 38 | // Returns a Promise. 39 | publish: function(contract_types, deployments, manifest) { 40 | var publisher = new Publisher(this.config.registry, this.config.host); 41 | return publisher.publish(this.config, contract_types, deployments, manifest); 42 | }, 43 | 44 | installed_artifacts: function() { 45 | var manifest = {}; 46 | 47 | try { 48 | manifest = Manifest.read(this.config.manifest_file); 49 | } catch (e) { 50 | // Do nothing with the error. 51 | } 52 | 53 | return Preflight.find_artifacts(this.config.installed_packages_directory, manifest.dependencies); 54 | } 55 | }); 56 | 57 | _.extend(EthPM, { 58 | 59 | // deprecated 60 | configure: function(directory, host, registry) { 61 | return new EthPM(directory, host, registry); 62 | }, 63 | 64 | init: function(directory, host, registry, options) { 65 | options = options || {}; 66 | 67 | var json = require(path.resolve(path.join(__dirname, "templates", "epm.json"))); 68 | 69 | json = _.cloneDeep(json); 70 | json = _.merge(json, options); 71 | 72 | return fs.writeFile(path.join(directory, "epm.json"), "utf8").then(function() { 73 | return new EthPM(directory, host, registry); 74 | }); 75 | }, 76 | 77 | hosts: { 78 | IPFS: IPFSHost, 79 | IPFSWithLocalReader: IPFSHostWithLocalReader 80 | } 81 | }); 82 | 83 | module.exports = EthPM; 84 | -------------------------------------------------------------------------------- /lib/sources.js: -------------------------------------------------------------------------------- 1 | var dir = require("node-dir"); 2 | var glob = require("glob"); 3 | var fs = require("fs"); 4 | var each = require("async/each"); 5 | var path = require("path"); 6 | 7 | var Sources = { 8 | 9 | expand: function(list, basePath) { 10 | var self = this; 11 | var paths = []; 12 | 13 | return new Promise(function(accept, reject) { 14 | each(list, function(source_path, finished) { 15 | var matches = []; 16 | 17 | // If we have a glob... 18 | if (glob.hasMagic(source_path)) { 19 | self.expandGlob(source_path, basePath).then(function(result) { 20 | paths = paths.concat(result); 21 | }).then(finished).catch(finished); 22 | return; 23 | } 24 | 25 | fs.stat(source_path, function(err, stats) { 26 | if (err) return finished(err); 27 | 28 | // If it's a directory, recursively get all children and grandchildren 29 | // and add them to the list. 30 | if (stats.isDirectory()) { 31 | self.expandDirectory(source_path, basePath).then(function(result) { 32 | paths = paths.concat(result); 33 | }).then(finished).catch(finished); 34 | return; 35 | } 36 | 37 | if (stats.isFile()) { 38 | // If it's a file, just add it to the list. 39 | paths.push(source_path); 40 | return finished(); 41 | } 42 | 43 | // In the rare case it's neither a file nor directory, error. 44 | return finished(new Error("Unknown file type at path " + source_path)); 45 | }); 46 | }, function(err) { 47 | if (err) return reject(err); 48 | accept(paths); 49 | }); 50 | }); 51 | }, 52 | 53 | expandGlob: function(source_path, basePath) { 54 | var self = this; 55 | return new Promise(function(accept, reject) { 56 | glob(source_path, { 57 | cwd: basePath 58 | }, function(err, matches) { 59 | if (err) return reject(err); 60 | accept(matches); 61 | }); 62 | }).then(function(matches) { 63 | return matches.map(function(match) { 64 | return path.resolve(path.join(basePath, match)); 65 | }); 66 | }).then(function(matches) { 67 | return self.expand(matches, basePath); 68 | }); 69 | }, 70 | 71 | expandDirectory: function(source_path, basePath) { 72 | source_path = path.resolve(path.join(basePath, source_path)); 73 | return new Promise(function(accept, reject) { 74 | dir.files(source_path, function(err, files) { 75 | if (err) return reject(err); 76 | accept(files); 77 | }); 78 | }); 79 | }, 80 | 81 | findDirectories: function(basePath) { 82 | return new Promise(function(accept, reject) { 83 | glob("./*", { 84 | cwd: basePath 85 | }, function(err, matches) { 86 | if (err) return reject(err); 87 | matches = matches.map(function(match) { 88 | return path.join(basePath, match); 89 | }); 90 | accept(matches); 91 | }); 92 | }); 93 | } 94 | 95 | }; 96 | 97 | module.exports = Sources; 98 | -------------------------------------------------------------------------------- /test/test_publish.js: -------------------------------------------------------------------------------- 1 | var TestHelper = require('./lib/testhelper'); 2 | var EPM = require('../index.js'); 3 | var path = require("path"); 4 | var assert = require("assert"); 5 | var Sources = require("../lib/sources"); 6 | 7 | describe("Publishing", function() { 8 | var helper = TestHelper.setup({ 9 | packages: [ 10 | "custom-use-cases/owned-1.0.0", 11 | "custom-use-cases/owned-2.0.0", 12 | "custom-use-cases/eth-usd-oracle-1.0.0" 13 | ], 14 | compile: [ 15 | "owned-1.0.0", 16 | "owned-2.0.0" 17 | ] 18 | }); 19 | 20 | var owned; 21 | var eth_usd; 22 | 23 | before("setup variables once previous steps are finished", function() { 24 | owned = helper.packages["owned-1.0.0"]; 25 | owned2 = helper.packages["owned-2.0.0"]; 26 | eth_usd = helper.packages["eth-usd-oracle-1.0.0"]; 27 | }); 28 | 29 | it("published the correct lockfile, manifest file and source files", function() { 30 | this.timeout(25000); 31 | 32 | var lockfile; 33 | 34 | // Publish the package. 35 | return owned.package.publish(owned.contract_metadata).then(function() { 36 | // Now check the registry 37 | return helper.registry.getLockfileURI("owned", "1.0.0"); 38 | }).then(function(lockfileURI) { 39 | return helper.host.get(lockfileURI); 40 | }).then(function(data) { 41 | lockfile = JSON.parse(data); 42 | 43 | assert.equal(lockfile.version, "1.0.0"); 44 | //assert.deepEqual(lockfile.contracts, owned.contract_metadata); 45 | 46 | var promises = []; 47 | 48 | assert.equal(Object.keys(lockfile.sources).length, 3); 49 | 50 | Object.keys(lockfile.sources).forEach(function(relative_path) { 51 | var full_path = path.resolve(path.join(owned.package.config.base_path, relative_path)); 52 | var sourceURI = lockfile.sources[relative_path]; 53 | 54 | promises.push(helper.assertHostMatchesFilesystem(sourceURI, full_path)); 55 | }); 56 | 57 | return Promise.all(promises); 58 | }); 59 | }); 60 | 61 | it("recognizes x-* options in the manifest and includes them in lockfile", function() { 62 | this.timeout(25000); 63 | 64 | var lockfile; 65 | 66 | // Publish the package. 67 | return owned2.package.publish(owned2.contract_metadata).then(function() { 68 | // Now check the registry 69 | return helper.registry.getLockfileURI("owned", "2.0.0"); 70 | }).then(function(lockfileURI) { 71 | return helper.host.get(lockfileURI); 72 | }).then(function(data) { 73 | lockfile = JSON.parse(data); 74 | 75 | assert.equal(lockfile["x-via"], "test-packager"); 76 | }); 77 | }); 78 | 79 | it("correctly publishes packages with dependencies", function(){ 80 | this.timeout(25000); 81 | 82 | // First install any dependencies. 83 | return eth_usd.package.install().then(function() { 84 | return Sources.findDirectories(eth_usd.package.config.installed_packages_directory); 85 | }).then(function(installed_packages) { 86 | installed_packages = installed_packages.map(function(dir) { 87 | return dir.replace(eth_usd.package.config.installed_packages_directory + path.sep, ""); 88 | }); 89 | 90 | assert.deepEqual(installed_packages, ["owned"], "Expected the `owned` dependency to be installed."); 91 | 92 | // Now publish it. 93 | return eth_usd.package.publish(); 94 | }).then(function() { 95 | return helper.registry.getLockfileURI("eth-usd-oracle", "1.0.0"); 96 | }).then(function(lockfileURI) { 97 | return helper.host.get(lockfileURI); 98 | }).then(function(data) { 99 | lockfile = JSON.parse(data); 100 | 101 | // TODO: Verify build dependencies are correct. 102 | }); 103 | }); 104 | 105 | // TODO: Test to ensure publishing errors when the package has a bad lockfile. 106 | }); 107 | -------------------------------------------------------------------------------- /lib/publisher.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | var Manifest = require("./manifest"); 3 | var Lockfile = require("./lockfile"); 4 | var Sources = require("./sources"); 5 | var path = require("path"); 6 | 7 | function Publisher(registry, host, contract_types, deployments) { 8 | this.registry = registry; 9 | this.host = host; 10 | }; 11 | 12 | Publisher.prototype.publish = function(config, contract_types, deployments, manifest) { 13 | var self = this; 14 | 15 | var lockfile = {}; 16 | var lockfileURI; 17 | 18 | return new Promise(function(accept, reject) { 19 | // Place this in a promise so errors will be sent down the promise chain. 20 | if (!manifest) { 21 | manifest = Manifest.read(config.manifest_file); 22 | } 23 | 24 | lockfile.lockfile_version = Lockfile.getInterpreter().version + ""; // ensure string 25 | lockfile.package_name = manifest.package_name; 26 | lockfile.meta = { 27 | authors: manifest.authors, 28 | license: manifest.license, 29 | description: manifest.description, 30 | keywords: manifest.keywords, 31 | links: manifest.links 32 | }; 33 | lockfile.version = manifest.version; 34 | lockfile.contract_types = contract_types || {}; 35 | lockfile.deployments = deployments || {}; 36 | 37 | Object.keys(manifest).filter(function (option) { 38 | return option.indexOf('x-') == 0; 39 | }).forEach(function (option) { 40 | lockfile[option] = manifest[option]; 41 | }); 42 | 43 | accept(); 44 | }).then(function() { 45 | // If no sources and no empty array, assume all solidity files in the source directory (configurable). 46 | if (!manifest.sources) { 47 | manifest.sources = [ 48 | path.relative(config.working_directory, path.join(config.source_directory, "*.sol")), 49 | path.relative(config.working_directory, path.join(config.source_directory, "**", "*.sol")) 50 | ]; 51 | } 52 | 53 | return Sources.expand(manifest.sources, config.base_path); 54 | }).then(function(source_paths) { 55 | var promises = []; 56 | 57 | source_paths.forEach(function(source_path) { 58 | var promise = new Promise(function(accept, reject) { 59 | self.host.putFile(source_path).then(function(sourceURI) { 60 | var relative = "." + path.sep + path.relative(config.base_path, source_path); 61 | var obj = {}; 62 | obj[relative] = sourceURI; 63 | accept(obj); 64 | }).catch(reject); 65 | }); 66 | 67 | promises.push(promise); 68 | }); 69 | 70 | return Promise.all(promises); 71 | }).then(function(source_objects) { 72 | 73 | lockfile.sources = source_objects.reduce(function(merged_obj, source_obj) { 74 | Object.keys(source_obj).forEach(function(key) { 75 | merged_obj[key] = source_obj[key]; 76 | }); 77 | return merged_obj; 78 | }, {}); 79 | 80 | var promises = []; 81 | 82 | Object.keys(manifest.dependencies || {}).forEach(function(dependency_name) { 83 | var lockfile_uri_location = path.join(config.installed_packages_directory, dependency_name, config.default_lockfile_uri_filename); 84 | promises.push(fs.readFile(lockfile_uri_location, "utf8").then(function(uri) { 85 | return [dependency_name, uri]; 86 | })); 87 | }); 88 | 89 | return Promise.all(promises); 90 | }).then(function(lockfile_uris) { 91 | 92 | lockfile.build_dependencies = {}; 93 | 94 | lockfile_uris.forEach(function(tuple) { 95 | var dependency_name = tuple[0]; 96 | var lockfile_uri = tuple[1]; 97 | 98 | lockfile.build_dependencies[dependency_name] = lockfile_uri; 99 | }); 100 | 101 | // TODO: Validating here kinda sucks because we've already pushed a bunch of stuff 102 | // to IPFS. 103 | var results = Lockfile.validate(lockfile); 104 | 105 | if (results.errors.length > 0) { 106 | var message = results.errors[0].stack; 107 | 108 | // Remove prefixes so error context is within the manifest 109 | message = message.replace("instance.", ""); 110 | message = message.replace("meta.", ""); 111 | 112 | throw new Error("Invalid package configuration: " + message); 113 | } 114 | 115 | return self.host.putContents(JSON.stringify(lockfile)); 116 | }).then(function(lockfileURI) { 117 | return self.registry.register(manifest.package_name, manifest.version, lockfileURI); 118 | }).then(function() { 119 | return lockfile; 120 | }); 121 | } 122 | 123 | module.exports = Publisher; 124 | -------------------------------------------------------------------------------- /lib/installer.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | var path = require("path"); 3 | var Manifest = require("./manifest"); 4 | var Lockfile = require("./lockfile"); 5 | var Preflight = require("./preflight"); 6 | var Sources = require("./sources"); 7 | var Config = require("./config.js"); 8 | var Util = require("./util"); 9 | 10 | function Installer(config, destination) { 11 | this.config = config; 12 | this.registry = config.registry; 13 | this.host = config.host; 14 | this.destination = destination; 15 | }; 16 | 17 | Installer.prototype.installDependencies = function(manifest) { 18 | var self = this; 19 | 20 | // Start a promise chain to ensure errors are caught. 21 | return new Promise(function(accept, reject) { 22 | accept(); 23 | }).then(function() { 24 | 25 | if (manifest == null) { 26 | manifest = Manifest.read(self.config.manifest_file); 27 | } 28 | 29 | return Preflight.resolve_dependencies(manifest.dependencies, self.registry, self.host); 30 | }).then(function(preflight) { 31 | var promises = []; 32 | 33 | Object.keys(preflight).forEach(function(package_name) { 34 | var versions = Object.keys(preflight[package_name]); 35 | 36 | if (versions.length > 1) { 37 | throw new Error("Your package and its dependencies require conflicting versions of the '" + package_name + "' package: " + versions.join(", ") + ". This dependency structure is currently unallowed at this time but will be allowed upon updates to the Solidity compiler. Please remove conflicting dependencies in the meantime."); 38 | } 39 | }); 40 | 41 | Object.keys(manifest.dependencies).forEach(function(package_name) { 42 | var version_range = manifest.dependencies[package_name]; 43 | promises.push(self.installPackage(package_name, version_range)); 44 | }); 45 | 46 | return Promise.all(promises); 47 | }); 48 | }; 49 | 50 | Installer.prototype.installPackage = function(package_name, version_range) { 51 | var self = this; 52 | var lockfileURI; 53 | var lockfile; 54 | var version; 55 | var manifest; 56 | var package_location; 57 | 58 | return Promise.resolve().then(function() { 59 | // If we have a URI, don't go to the registry. 60 | if (Util.isURI(version_range)) { 61 | return version_range; 62 | } 63 | 64 | return self.registry.getLockfileURI(package_name, version_range); 65 | }).then(function(uri) { 66 | lockfileURI = uri; 67 | return self.host.get(lockfileURI); 68 | }).then(function(data) { 69 | lockfile = JSON.parse(data); 70 | 71 | // Validate the lockfile before moving further. 72 | var results = Lockfile.validate(lockfile); 73 | 74 | if (results.errors.length > 0) { 75 | throw new Error("Could not install package `" + package_name + "`: Invalid package specification (" + results.errors[0].stack.replace("instance.", "") + ")"); 76 | } 77 | 78 | // Create a manifest from the lockfile 79 | manifest = Manifest.fromLockfile(lockfile); 80 | 81 | package_location = path.resolve(path.join(self.destination, package_name)); 82 | var manifest_location = path.resolve(path.join(package_location, self.config.default_manifest_filename)); 83 | var lockfile_location = path.resolve(path.join(package_location, self.config.default_lockfile_filename)); 84 | var lockfile_uri_location = path.resolve(path.join(package_location, self.config.default_lockfile_uri_filename)); 85 | 86 | var file_promises = []; 87 | 88 | // Add the sources 89 | Object.keys(lockfile.sources).forEach(function(relative_source_path) { 90 | var source_path = path.resolve(path.join(package_location, relative_source_path)); 91 | var uri = lockfile.sources[relative_source_path]; 92 | 93 | file_promises.push(self.saveURI(uri, source_path)); 94 | }); 95 | 96 | // Add the lockfile itself 97 | // This requests the lockfile again, but it's easy. 98 | file_promises.push(self.saveURI(lockfileURI, lockfile_location)); 99 | 100 | // Save the URI of the lockfile for this version 101 | file_promises.push(fs.outputFile(lockfile_uri_location, lockfileURI)); 102 | 103 | // Create and save the manifest 104 | file_promises.push(fs.outputFile(manifest_location, JSON.stringify(manifest, null, 2))); 105 | 106 | return Promise.all(file_promises); 107 | }).then(function() { 108 | // After this package is installed, use the manfest to install any dependencies. 109 | var dependency_config = Config.default().with({ 110 | working_directory: package_location, 111 | base_dir: package_location, 112 | host: self.host, 113 | registry: self.registry 114 | }); 115 | var dependency_installer = new Installer(dependency_config, self.destination); 116 | 117 | return dependency_installer.installDependencies(); 118 | }); 119 | }; 120 | 121 | Installer.prototype.saveURI = function(uri, destination_path) { 122 | return this.host.get(uri).then(function(contents) { 123 | return fs.outputFile(destination_path, contents); 124 | }); 125 | }; 126 | 127 | module.exports = Installer; 128 | -------------------------------------------------------------------------------- /lib/preflight.js: -------------------------------------------------------------------------------- 1 | var Util = require('./util'); 2 | var whilst = require("async/whilst"); 3 | var fs = require("fs-extra"); 4 | var path = require("path"); 5 | 6 | var Preflight = { 7 | // Given a list of manifest dependencies, find all dependencies (even 8 | // dependencies of dependencies) and list their lockfiles here. 9 | resolve_dependencies: function(dependencies, registry, host) { 10 | // Turn object into a stack 11 | var stack = []; 12 | var promises = []; 13 | 14 | var promises = Object.keys(dependencies).map(function(package_name) { 15 | var version_range = dependencies[package_name]; 16 | 17 | // Support dependencies that are URIs. 18 | if (Util.isURI(version_range)) { 19 | // Make a promise out of the URI. 20 | return Promise.resolve(version_range); 21 | } 22 | 23 | return registry.getLockfileURI(package_name, version_range); 24 | }); 25 | 26 | return Promise.all(promises).then(function(uris) { 27 | 28 | stack = uris.map(function(uri) { 29 | return [uri, [""]]; 30 | }); 31 | 32 | return new Promise(function(accept, reject) { 33 | var resolved = {}; 34 | 35 | whilst(function() { 36 | return stack.length > 0; 37 | }, function(finished) { 38 | var current = stack.shift(); 39 | var lockfileURI = current[0]; 40 | var dependency_chain = current[1]; 41 | var version; 42 | 43 | return host.get(lockfileURI).then(function(data) { 44 | var lockfile = JSON.parse(data); 45 | 46 | var package_name = lockfile.package_name; 47 | var version = lockfile.version; 48 | 49 | if (resolved[package_name] == null) { 50 | resolved[package_name] = {}; 51 | } 52 | 53 | if (resolved[package_name][version] == null) { 54 | resolved[package_name][version] = { 55 | lockfile: lockfile, 56 | used_by: [] 57 | }; 58 | } 59 | 60 | resolved[package_name][version].used_by.push(dependency_chain); 61 | 62 | Object.keys(lockfile.build_dependencies || {}).forEach(function(key) { 63 | var uri = lockfile.build_dependencies[key]; 64 | stack.push([uri, dependency_chain.concat([package_name])]); 65 | }); 66 | 67 | finished(); 68 | 69 | }).catch(finished); 70 | 71 | }, function(err) { 72 | if (err) return reject(err); 73 | accept(resolved); 74 | }); 75 | }); 76 | }); 77 | }, 78 | 79 | // Go through installed packages looking for all artifacts (contract_types and deployments). 80 | // Note that at this point, we don't allow multiple versions to be installed, which means that 81 | // we don't need to filter the results based on the semver version ranges within the containing 82 | // package's ethpm.json. We will when we do allower multiple versions later on. 83 | find_artifacts: function(installed_packages_directory, filter) { 84 | return new Promise(function(accept, reject) { 85 | fs.readdir(installed_packages_directory, function(err, directories) { 86 | if (err) { 87 | return accept([]); 88 | } 89 | 90 | accept(directories); 91 | }); 92 | }).then(function(directories) { 93 | var lockfile_paths = directories.map(function(directory) { 94 | var expected_lockfile_path = path.resolve(path.join(installed_packages_directory, directory, "lock.json")); // Use constant somewhere (config?) 95 | 96 | return new Promise(function(accept, reject) { 97 | fs.readFile(expected_lockfile_path, "utf8", function(err, body) { 98 | if (err) return reject(err); 99 | accept(body); 100 | }); 101 | }); 102 | }); 103 | 104 | return Promise.all(lockfile_paths); 105 | }).then(function(raw_lockfiles) { 106 | return raw_lockfiles.map(function(data) { 107 | return JSON.parse(data); 108 | }); 109 | }).then(function(lockfiles) { 110 | var results = {}; 111 | 112 | // Filter out lockfiles that aren't direct dependencies of this package. 113 | if (filter != null) { 114 | lockfiles = lockfiles.filter(function(lockfile) { 115 | return filter[lockfile.package_name] != null; 116 | }); 117 | } 118 | 119 | // Return artifacts for direct dependencies. 120 | lockfiles.forEach(function(lockfile) { 121 | var package_name = lockfile.package_name; 122 | var version = lockfile.version; 123 | 124 | var deployments = lockfile.deployments || {}; 125 | var contract_types = lockfile.deployments || {}; 126 | 127 | var has_deployments = Object.keys(deployments).length > 0; 128 | var has_contract_types = Object.keys(contract_types).length > 0; 129 | 130 | if (!has_deployments && !has_contract_types) { 131 | return; 132 | } 133 | 134 | if (results[package_name] == null) { 135 | results[package_name] = { 136 | version: version 137 | }; 138 | } 139 | 140 | if (has_deployments) { 141 | results[package_name].deployments = lockfile.deployments; 142 | } 143 | 144 | if (has_contract_types) { 145 | results[package_name].contract_types = lockfile.contract_types; 146 | } 147 | }); 148 | 149 | return results; 150 | }); 151 | } 152 | }; 153 | 154 | module.exports = Preflight; 155 | -------------------------------------------------------------------------------- /test/lib/testhelper.js: -------------------------------------------------------------------------------- 1 | var ipfsd = require('ipfsd-ctl') 2 | var EPM = require("../../index.js"); 3 | var Config = require("../../lib/config"); 4 | var IPFSHost = require("../../lib/hosts/ipfshost"); 5 | var MemoryRegistry = require("../../lib/registries/memoryregistry"); 6 | var Manifest = require("../../lib/manifest.js"); 7 | 8 | var fs = require('fs-extra'); 9 | var path = require("path"); 10 | var solc = require("solc"); 11 | var dir = require("node-dir"); 12 | var each = require("async/each"); 13 | var temp = require("temp").track(); 14 | var assert = require("assert"); 15 | 16 | function Helper() { 17 | this.ipfs_server = null; 18 | this.host = null; 19 | this.registry = null; 20 | this.packages = {}; 21 | }; 22 | 23 | Helper.prototype.assertHostMatchesFilesystem = function(sourceURI, source_path) { 24 | var self = this; 25 | var source_file_contents; 26 | 27 | return fs.readFile(source_path, "utf8").then(function(contents) { 28 | source_file_contents = contents; 29 | }).then(function() { 30 | return self.host.get(sourceURI) 31 | }).then(function(sourceURI_contents) { 32 | assert.equal(sourceURI_contents, source_file_contents); 33 | }); 34 | }; 35 | 36 | Helper.prototype.assertFilesMatch = function(expected, actual) { 37 | return Promise.all([ 38 | fs.readFile(expected, "utf8"), 39 | fs.readFile(actual, "utf8") 40 | ]).then(function(results) { 41 | assert.equal(results[0], results[1]); 42 | }); 43 | }; 44 | 45 | var TestHelper = { 46 | setup: function(packages_to_setup) { 47 | var package_paths = packages_to_setup.packages; 48 | var compile = packages_to_setup.compile || []; 49 | 50 | var helper = new Helper(); 51 | 52 | before("set up ipfs server and registry", function(done) { 53 | // This code that sets up the IPFS server has widely varying runtimes... 54 | this.timeout(20000); 55 | 56 | ipfsd.disposableApi(function (err, ipfs) { 57 | if (err) return done(err); 58 | 59 | helper.ipfs_server = ipfs; 60 | 61 | helper.host = new IPFSHost(helper.ipfs_server.apiHost, helper.ipfs_server.apiPort); 62 | helper.registry = new MemoryRegistry(); 63 | 64 | done(err); 65 | }); 66 | }); 67 | 68 | package_paths.forEach(function(package_path) { 69 | package_path = package_path.split("/"); 70 | package_path = [__dirname, "../", "../"].concat(package_path); 71 | var package_name = package_path[package_path.length - 1]; 72 | 73 | var original_package_path = path.resolve(path.join.apply(path, package_path)); 74 | 75 | // This is only used for epm-spec examples (as of the writing of this comment) 76 | var lockfile_path = path.resolve(path.join(original_package_path, "1.0.0.json")); 77 | 78 | var package_data = { 79 | package: null, 80 | contract_metadata: {}, 81 | package_path: "" 82 | }; 83 | helper.packages[package_name] = package_data; 84 | 85 | before("create temporary directory", function(done) { 86 | var temp_path = temp.mkdirSync("epm-test-"); 87 | fs.copy(original_package_path, temp_path, {}).then(function() { 88 | package_data.package_path = temp_path; 89 | done(); 90 | }).catch(done); 91 | }); 92 | 93 | before("create ethpm.json file from lockfile if needed", function(done) { 94 | var epmjson_path = path.resolve(path.join(package_data.package_path, "ethpm.json")); 95 | 96 | // See if there's an ethpm.json there already. 97 | fs.stat(epmjson_path, function(err, stat) { 98 | if (err) return done(err); 99 | if (stat.isFile()) return done(); 100 | 101 | // Doesn't exist, so let's create it from the lockfile. 102 | fs.readFile(lockfile_path, "utf8", function(err, body) { 103 | if (err) return done(err); 104 | var lockfile = JSON.parse(body); 105 | var manifest = Manifest.fromLockfile(lockfile); 106 | 107 | fs.writeFile(epmjson_path, JSON.stringify(manifest), "utf8", done); 108 | }); 109 | }); 110 | }); 111 | 112 | before("set up config", function() { 113 | package_data.package = new EPM(path.resolve(package_data.package_path), helper.host, helper.registry); 114 | }); 115 | 116 | before("generate contract metadata", function(done) { 117 | this.timeout(5000); 118 | 119 | if (compile.indexOf(package_name) < 0) return done(); 120 | 121 | var sources = {}; 122 | 123 | dir.files(package_data.package.config.contracts_directory, function(err, files) { 124 | if (err) return done(err); 125 | 126 | each(files, function(file, finished) { 127 | fs.readFile(file, "utf8").then(function(contents) { 128 | sources[file] = contents; 129 | finished(); 130 | }).catch(finished) 131 | }, function(err) { 132 | if (err) return done(err); 133 | 134 | var output = solc.compile({sources: sources}, 1); 135 | 136 | Object.keys(output.contracts).forEach(function(contract_name) { 137 | var contract = output.contracts[contract_name]; 138 | package_data.contract_metadata[contract_name] = { 139 | abi: JSON.parse(contract.interface), 140 | bytecode: contract.bytecode, 141 | runtime_bytecode: contract.runtimeBytecode, 142 | compiler: { 143 | version: solc.version(), 144 | } 145 | }; 146 | }); 147 | 148 | done(); 149 | }); 150 | }); 151 | }); 152 | }); 153 | 154 | return helper; 155 | } 156 | }; 157 | 158 | module.exports = TestHelper; 159 | -------------------------------------------------------------------------------- /lib/indexes/github-examples.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var ipfsd = require("ipfsd-ctl"); 3 | var IPFSHost = require("../hosts/ipfshost"); 4 | var MemoryRegistry = require("../registries/memoryregistry"); 5 | var fs = require("fs-extra"); 6 | var request = require("request"); 7 | var parallel = require("async/parallel"); 8 | var series = require("async/series"); 9 | var whilst = require("async/whilst"); 10 | var eachSeries = require("async/eachSeries"); 11 | 12 | // These examples are ordered such that any package that depends on another in this list 13 | // will come after the one it depends on (i.e., transferable depends on owned). We do this 14 | // because we edit the lockfiles that contain deployed packages, which means we have to 15 | // update dependency's build_dependencies to reference the new lockfiles. 16 | var examples = [ 17 | "owned", 18 | "transferable", 19 | "standard-token", 20 | "safe-math-lib", 21 | "piper-coin", 22 | "escrow", 23 | "wallet" 24 | ]; 25 | 26 | module.exports = { 27 | initialize: function(options, callback) { 28 | var self = this; 29 | 30 | if (typeof options == "function") { 31 | callback = options; 32 | options = {}; 33 | } 34 | 35 | var lockfiles = {}; 36 | var sourcefiles = {}; 37 | var ipfs_daemon; 38 | var ipfs_api; 39 | var host; 40 | var registry; 41 | var lockfile_uris = {}; 42 | 43 | series([ 44 | // Download all lockfiles 45 | function(c) { 46 | var lockfiles_requests = {}; 47 | 48 | examples.forEach(function(name) { 49 | lockfiles_requests[name] = self.get.bind(self, "/" + name + "/1.0.0.json"); 50 | }); 51 | 52 | parallel(lockfiles_requests, function(err, results) { 53 | if (err) return c(err); 54 | 55 | Object.keys(results).forEach(function(package_name) { 56 | lockfiles[package_name] = JSON.parse(results[package_name]); 57 | }); 58 | 59 | c(); 60 | }); 61 | }, 62 | // Download all source files 63 | function(c) { 64 | var sourcefile_requests = {}; 65 | 66 | Object.keys(lockfiles).forEach(function(package_name) { 67 | var lockfile = lockfiles[package_name]; 68 | 69 | Object.keys(lockfile.sources || {}).forEach(function(file_path) { 70 | file_path = "/" + package_name + "/" + file_path; 71 | sourcefile_requests[file_path] = self.get.bind(self, file_path); 72 | }); 73 | }); 74 | 75 | parallel(sourcefile_requests, function(err, results) { 76 | if (err) return c(err); 77 | 78 | sourcefiles = results; 79 | c(); 80 | }); 81 | }, 82 | // Create host and registry 83 | function(c) { 84 | ipfsd.disposable(function (err, node) { 85 | if (err) return c(err); 86 | 87 | ipfs_daemon = node; 88 | 89 | node.startDaemon(function(err, ipfs) { 90 | ipfs_api = ipfs; 91 | 92 | host = new IPFSHost(ipfs_api.apiHost, ipfs_api.apiPort); 93 | 94 | registry = new MemoryRegistry(); 95 | 96 | c(); 97 | }); 98 | }); 99 | }, 100 | // Put all source files on host 101 | function(c) { 102 | var puts = []; 103 | 104 | Object.keys(sourcefiles).forEach(function(sourcefile_path) { 105 | puts.push(host.putContents(sourcefiles[sourcefile_path])); 106 | }); 107 | 108 | Promise.all(puts).then(function(results) { 109 | c(); 110 | }).catch(c); 111 | }, 112 | // Put all lockfiles on host and register versions 113 | // Do this serially so we can override build dependencies of lockfiles 114 | // that are edited. 115 | function(c) { 116 | var pushed_lockfiles = {}; 117 | var package_names = Object.keys(lockfiles); 118 | 119 | eachSeries(package_names, function(package_name, finished) { 120 | var lockfile = lockfiles[package_name]; 121 | 122 | // If we've specified a specific blockchain, override deployments to point to that blockchain. 123 | if (options.blockchain != null && lockfile.deployments != null && Object.keys(lockfile.deployments).length > 0) { 124 | var deployments = {}; 125 | 126 | // This is naive, but will coerce down to a single blockchain, which is good enough for now. 127 | Object.keys(lockfile.deployments || {}).forEach(function(blockchain) { 128 | deployments[options.blockchain] = lockfile.deployments[blockchain]; 129 | }); 130 | 131 | lockfile.deployments = deployments; 132 | } 133 | 134 | // Override any build dependencies of previously uploaded lockfiles. 135 | Object.keys(lockfile.build_dependencies || {}).forEach(function(dependency_name) { 136 | if (pushed_lockfiles[dependency_name] != null) { 137 | lockfile.build_dependencies[dependency_name] = pushed_lockfiles[dependency_name]; 138 | } 139 | }); 140 | 141 | var raw_lockfile = JSON.stringify(lockfile, null, 2); 142 | 143 | host.putContents(raw_lockfile).then(function(uri) { 144 | pushed_lockfiles[package_name] = uri; 145 | finished(); 146 | }).catch(finished); 147 | }, function(err) { 148 | if (err) return c(err); 149 | 150 | var registrations = []; 151 | 152 | package_names.forEach(function(package_name) { 153 | var uri = pushed_lockfiles[package_name]; 154 | registrations.push(registry.register(package_name, "1.0.0", uri)); 155 | }); 156 | 157 | Promise.all(registrations).then(function(results) { 158 | c(); 159 | }).catch(c); 160 | }); 161 | } 162 | ], function(err) { 163 | if (err) return callback(err); 164 | 165 | callback(null, { 166 | host: host, 167 | registry: registry, 168 | examples: examples, 169 | ipfs_daemon: ipfs_daemon, 170 | ipfs_api: ipfs_api 171 | }); 172 | }); 173 | }, 174 | 175 | get: function(examples_path, callback) { 176 | 177 | var fullPath = path.join("/ethpm/epm-spec/master/examples", examples_path); 178 | var url = "https://raw.githubusercontent.com" + fullPath; 179 | 180 | request(url, function(error, response, body) { 181 | if(error) { 182 | callback(error); 183 | } else if (response.statusCode !== 200) { 184 | callback(new Error(`Unexpected server response ${response.statusCode}`)); 185 | } else { 186 | callback(null, body); 187 | }; 188 | }); 189 | } 190 | }; 191 | --------------------------------------------------------------------------------