diff --git a/.circleci/config.yml b/.circleci/config.yml index 32605b7d2b..7f963545eb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,7 +23,7 @@ jobs: - checkout - run: npm install && npm run bootstrap - run: cd remix-debug && npm test - + remix-analyzer: docker: - image: circleci/node:10 @@ -33,7 +33,7 @@ jobs: - checkout - run: npm install && npm run bootstrap - run: cd remix-analyzer && npm test - + remix-tests: docker: - image: circleci/node:10 @@ -43,7 +43,7 @@ jobs: - checkout - run: npm install && npm run bootstrap - run: cd remix-tests && npm test - + remix-simulator: docker: - image: circleci/node:10 @@ -53,7 +53,17 @@ jobs: - checkout - run: npm install && npm run bootstrap - run: cd remix-simulator && npm test - + + remix-url-resolver: + docker: + - image: circleci/node:10 + environment: + working_directory: ~/repo + steps: + - checkout + - run: npm install && npm run bootstrap + - run: cd remix-url-resolver && npm run build && npm test + workflows: version: 2 @@ -64,3 +74,4 @@ workflows: - remix-analyzer - remix-tests - remix-simulator + - remix-url-resolver diff --git a/lerna.json b/lerna.json index 648f59d921..5f360141ef 100644 --- a/lerna.json +++ b/lerna.json @@ -6,7 +6,8 @@ "remix-solidity", "remix-analyzer", "remix-tests", - "remix-simulator" + "remix-simulator", + "remix-url-resolver" ], "command": { "init": { diff --git a/remix-url-resolver/.gitignore b/remix-url-resolver/.gitignore new file mode 100644 index 0000000000..849ddff3b7 --- /dev/null +++ b/remix-url-resolver/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/remix-url-resolver/README.md b/remix-url-resolver/README.md new file mode 100644 index 0000000000..8025920925 --- /dev/null +++ b/remix-url-resolver/README.md @@ -0,0 +1,32 @@ +## Remix URL resolver engine + +`resolve(url, urlHandler)` + +Returns `json` object with exact same path as `import` statement. + +**Output** +```json +{ + content: 'pragma solidity ^0.5.0;\nimport "./mortal.sol";\n\ncontract Greeter is Mortal {\n /* Define variable greeting of the type string */\n string greeting;\n\n /* This runs when the contract is executed */\n constructor(string memory _greeting) public {\n greeting = _greeting;\n }\n\n /* Main function */\n function greet() public view returns (string memory) {\n return greeting;\n }\n}\n', + cleanURL: '../greeter.sol', + type: 'local' +} +``` + +#### Usage + +`resolve(url, urlHandler)` function should be called from within `handleImportCb` function of `solc.compile(input, handleImportCb)`. + +```ts +import { RemixResolve } from 'remix-url-resolver' + +const remixResolve = new RemixResolve() +const fileName: string = '../greeter.sol' +remixResolve.resolve(fileName, urlHandler) + .then((sources: object) => { + console.log(sources) + }) + .catch((e: Error) => { + throw e + }) +``` diff --git a/remix-url-resolver/package.json b/remix-url-resolver/package.json new file mode 100644 index 0000000000..59abc61cab --- /dev/null +++ b/remix-url-resolver/package.json @@ -0,0 +1,49 @@ +{ + "name": "remix-url-resolver", + "version": "0.0.1", + "description": "Solidity import url resolver engine", + "main": "./src/index.js", + "bin": { + "remix-url-resolver": "./bin/remix-url-resolver" + }, + "scripts": { + "build": "tsc", + "lint": "standard", + "test": "standard && mocha --require ts-node/register tests/*.ts -t 300000" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ethereum/remix.git" + }, + "keywords": [ + "solidity", + "remix", + "resolve", + "import" + ], + "author": "Remix Team", + "license": "MIT", + "standard": { + "ignore": [ + "tests/" + ] + }, + "dependencies": { + "axios": "^0.18.0", + "solc": "^0.5.0", + "url": "^0.11.0", + "valid-url": "^1.0.9" + }, + "devDependencies": { + "@types/chai": "^4.1.7", + "@types/mocha": "^5.2.5", + "@types/node": "^10.12.18", + "chai": "^4.2.0", + "mocha": "^5.1.0", + "remix-plugin": "0.0.1-alpha.2", + "standard": "^12.0.1", + "ts-node": "^7.0.1", + "tslint": "^5.11.0", + "typescript": "^3.1.6" + } +} diff --git a/remix-url-resolver/src/index.ts b/remix-url-resolver/src/index.ts new file mode 100644 index 0000000000..d0104e83ca --- /dev/null +++ b/remix-url-resolver/src/index.ts @@ -0,0 +1 @@ +export * from './resolve' diff --git a/remix-url-resolver/src/resolve.ts b/remix-url-resolver/src/resolve.ts new file mode 100644 index 0000000000..adc3f650d1 --- /dev/null +++ b/remix-url-resolver/src/resolve.ts @@ -0,0 +1,133 @@ +import axios, { AxiosResponse } from 'axios' + +export interface Imported { + content: string; + cleanURL: string; + type: string; +} + +interface PreviouslyHandledImports { + [filePath: string]: Imported +} + +interface Handler { + type: string; + match(url: string): any; + handle(match: any): any; +} + +export class RemixURLResolver { + private previouslyHandled: PreviouslyHandledImports + constructor() { + this.previouslyHandled = {} + } + /** + * Handle an import statement based on github + * @params root The root of the github import statement + * @params filePath path of the file in github + */ + async handleGithubCall(root: string, filePath: string) { + try { + let req: string = 'https://api.github.com/repos/' + root + '/contents/' + filePath + const response: AxiosResponse = await axios.get(req) + return Buffer.from(response.data.content, 'base64').toString() + } catch(e) { + throw e + } + } + /** + * Handle an import statement based on http + * @params url The url of the import statement + * @params cleanURL + */ + async handleHttp(url: string, _: string) { + try { + const response: AxiosResponse = await axios.get(url) + return response.data + } catch(e) { + throw e + } + } + /** + * Handle an import statement based on https + * @params url The url of the import statement + * @params cleanURL + */ + async handleHttps(url: string, _: string) { + try { + const response: AxiosResponse = await axios.get(url) + return response.data + } catch(e) { + throw e + } + } + handleSwarm(url: string, cleanURL: string) { + return + } + /** + * Handle an import statement based on IPFS + * @params url The url of the IPFS import statement + */ + async handleIPFS(url: string) { + // replace ipfs:// with /ipfs/ + url = url.replace(/^ipfs:\/\/?/, 'ipfs/') + try { + const req = 'https://gateway.ipfs.io/' + url + // If you don't find greeter.sol on ipfs gateway use local + // const req = 'http://localhost:8080/' + url + const response: AxiosResponse = await axios.get(req) + return response.data + } catch (e) { + throw e + } + } + getHandlers(): Handler[] { + return [ + { + type: 'github', + match: (url) => { return /^(https?:\/\/)?(www.)?github.com\/([^/]*\/[^/]*)\/(.*)/.exec(url) }, + handle: (match) => this.handleGithubCall(match[3], match[4]) + }, + { + type: 'http', + match: (url) => { return /^(http?:\/\/?(.*))$/.exec(url) }, + handle: (match) => this.handleHttp(match[1], match[2]) + }, + { + type: 'https', + match: (url) => { return /^(https?:\/\/?(.*))$/.exec(url) }, + handle: (match) => this.handleHttps(match[1], match[2]) + }, + { + type: 'swarm', + match: (url) => { return /^(bzz-raw?:\/\/?(.*))$/.exec(url) }, + handle: (match) => this.handleSwarm(match[1], match[2]) + }, + { + type: 'ipfs', + match: (url) => { return /^(ipfs:\/\/?.+)/.exec(url) }, + handle: (match) => this.handleIPFS(match[1]) + } + ] + } + + public async resolve(filePath: string, customHandlers?: Handler[]): Promise { + var imported: Imported = this.previouslyHandled[filePath] + if(imported) { + return imported + } + const builtinHandlers: Handler[] = this.getHandlers() + const handlers: Handler[] = customHandlers ? [...builtinHandlers, ...customHandlers] : [...builtinHandlers] + const matchedHandler = handlers.filter(handler => handler.match(filePath)) + const handler: Handler = matchedHandler[0] + const match = handler.match(filePath) + const content: string = await handler.handle(match) + imported = { + content, + cleanURL: filePath, + type: handler.type + } + this.previouslyHandled[filePath] = imported + return imported + } +} diff --git a/remix-url-resolver/tests/example_1/greeter.sol b/remix-url-resolver/tests/example_1/greeter.sol new file mode 100644 index 0000000000..8425568af9 --- /dev/null +++ b/remix-url-resolver/tests/example_1/greeter.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.5.0; +import "./mortal.sol"; + +contract Greeter is Mortal { + /* Define variable greeting of the type string */ + string greeting; + + /* This runs when the contract is executed */ + constructor(string memory _greeting) public { + greeting = _greeting; + } + + /* Main function */ + function greet() public view returns (string memory) { + return greeting; + } +} diff --git a/remix-url-resolver/tests/example_1/mortal.sol b/remix-url-resolver/tests/example_1/mortal.sol new file mode 100644 index 0000000000..e1a1fa4de8 --- /dev/null +++ b/remix-url-resolver/tests/example_1/mortal.sol @@ -0,0 +1,12 @@ +pragma solidity ^0.5.0; + +contract Mortal { + /* Define variable owner of the type address */ + address payable owner; + + /* This function is executed at initialization and sets the owner of the contract */ + function mortal() public { owner = msg.sender; } + + /* Function to recover the funds on the contract */ + function kill() public { if (msg.sender == owner) selfdestruct(owner); } +} diff --git a/remix-url-resolver/tests/test.ts b/remix-url-resolver/tests/test.ts new file mode 100644 index 0000000000..2387651355 --- /dev/null +++ b/remix-url-resolver/tests/test.ts @@ -0,0 +1,146 @@ +import { RemixURLResolver } from '../src' +import * as fs from 'fs' +import * as path from 'path' +import * as assert from 'assert' + +describe('testRunner', () => { + describe('# RemixResolve.resolve()', () => { + describe('* test without AppManager', () => { + describe('test example_1 [local imports]', () => { + const remixURLResolve = new RemixURLResolver() + const fileName: string = '../remix-url-resolver/tests/example_1/greeter.sol' + let results: object = {} + + before(done => { + function handleLocal(pathString: string, filePath: string) { + // if no relative/absolute path given then search in node_modules folder + if (pathString && pathString.indexOf('.') !== 0 && pathString.indexOf('/') !== 0) { + // return handleNodeModulesImport(pathString, filePath, pathString) + return + } else { + const o = { encoding: 'UTF-8' } + const p = pathString ? path.resolve(pathString, filePath) : path.resolve(pathString, filePath) + const content = fs.readFileSync(p, o) + return content + } + } + const localFSHandler = [ + { + type: 'local', + match: (url: string) => { return /(^(?!(?:http:\/\/)|(?:https:\/\/)?(?:www.)?(?:github.com)))(^\/*[\w+-_/]*\/)*?(\w+\.sol)/g.exec(url) }, + handle: (match: Array) => { return handleLocal(match[2], match[3]) } + } + ] + remixURLResolve.resolve(fileName, localFSHandler) + .then((sources: object) => { + results = sources + done() + }) + .catch((e: Error) => { + throw e + }) + }) + + it('should have 3 items', () => { + assert.equal(Object.keys(results).length, 3) + }) + it('should return contract content of given local path', () => { + const expt = { + content: 'pragma solidity ^0.5.0;\nimport "./mortal.sol";\n\ncontract Greeter is Mortal {\n /* Define variable greeting of the type string */\n string greeting;\n\n /* This runs when the contract is executed */\n constructor(string memory _greeting) public {\n greeting = _greeting;\n }\n\n /* Main function */\n function greet() public view returns (string memory) {\n return greeting;\n }\n}\n', + cleanURL: '../remix-url-resolver/tests/example_1/greeter.sol', + type: 'local' + } + assert.deepEqual(results, expt) + }) + }) + // Test github import + describe('test getting github imports', () => { + const remixURLResolve = new RemixURLResolver() + const fileName: string = 'github.com/ethereum/populus/docs/assets/Greeter.sol' + let results: object = {} + + before(done => { + remixURLResolve.resolve(fileName) + .then((sources: object) => { + results = sources + done() + }) + .catch((e: Error) => { + throw e + }) + }) + + it('should have 3 items', () => { + assert.equal(Object.keys(results).length, 3) + }) + it('should return contract content of given github path', () => { + const expt: object = { + cleanURL: 'github.com/ethereum/populus/docs/assets/Greeter.sol', + content: 'pragma solidity ^0.4.0;\n\ncontract Greeter {\n string public greeting;\n\n // TODO: Populus seems to get no bytecode if `internal`\n function Greeter() public {\n greeting = \'Hello\';\n }\n\n function setGreeting(string _greeting) public {\n greeting = _greeting;\n }\n\n function greet() public constant returns (string) {\n return greeting;\n }\n}\n', + type: 'github' + } + assert.deepEqual(results, expt) + }) + }) + // Test https imports + describe('test getting https imports', () => { + const remixURLResolve = new RemixURLResolver() + const fileName: string = 'https://gist.githubusercontent.com/roneilr/7901633d7c2f52957d22/raw/d9b9d54760f6e4f4cfbac4b321bee6a6983a1048/greeter.sol' + let results: object = {} + + before(done => { + remixURLResolve.resolve(fileName) + .then((sources: object) => { + results = sources + done() + }) + .catch((e: Error) => { + throw e + }) + }) + + it('should have 3 items', () => { + assert.equal(Object.keys(results).length, 3) + }) + it('should return contract content from raw github url', () => { + const expt: object = { + content: 'contract mortal {\n /* Define variable owner of the type address*/\n address owner;\n\n /* this function is executed at initialization and sets the owner of the contract */\n function mortal() { owner = msg.sender; }\n\n /* Function to recover the funds on the contract */\n function kill() { if (msg.sender == owner) suicide(owner); }\n}\n\ncontract greeter is mortal {\n /* define variable greeting of the type string */\n string greeting;\n\n /* this runs when the contract is executed */\n function greeter(string _greeting) public {\n greeting = _greeting;\n }\n\n /* main function */\n function greet() constant returns (string) {\n return greeting;\n }\n}', + cleanURL: 'https://gist.githubusercontent.com/roneilr/7901633d7c2f52957d22/raw/d9b9d54760f6e4f4cfbac4b321bee6a6983a1048/greeter.sol', + type: 'https' + } + assert.deepEqual(results, expt) + }) + }) + + // Test http imports + describe('test getting http imports', () => { + const remixURLResolve = new RemixURLResolver() + const fileName: string = 'http://gist.githubusercontent.com/roneilr/7901633d7c2f52957d22/raw/d9b9d54760f6e4f4cfbac4b321bee6a6983a1048/greeter.sol' + let results: object = {} + + before(done => { + remixURLResolve.resolve(fileName) + .then((sources: object) => { + results = sources + done() + }) + .catch((e: Error) => { + throw e + }) + }) + + it('should have 3 items', () => { + assert.equal(Object.keys(results).length, 3) + }) + it('should return contract content from raw github url', () => { + const expt: object = { + content: 'contract mortal {\n /* Define variable owner of the type address*/\n address owner;\n\n /* this function is executed at initialization and sets the owner of the contract */\n function mortal() { owner = msg.sender; }\n\n /* Function to recover the funds on the contract */\n function kill() { if (msg.sender == owner) suicide(owner); }\n}\n\ncontract greeter is mortal {\n /* define variable greeting of the type string */\n string greeting;\n\n /* this runs when the contract is executed */\n function greeter(string _greeting) public {\n greeting = _greeting;\n }\n\n /* main function */\n function greet() constant returns (string) {\n return greeting;\n }\n}', + cleanURL: 'http://gist.githubusercontent.com/roneilr/7901633d7c2f52957d22/raw/d9b9d54760f6e4f4cfbac4b321bee6a6983a1048/greeter.sol', + type: 'http' + } + assert.deepEqual(results, expt) + }) + }) + }) + }) +}) diff --git a/remix-url-resolver/tsconfig.json b/remix-url-resolver/tsconfig.json new file mode 100644 index 0000000000..2e20354bf7 --- /dev/null +++ b/remix-url-resolver/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compileOnSave": false, + "include": ["./src"], + "compilerOptions": { + "baseUrl": "./src", + "outDir": "./dist", + "sourceMap": true, + "declaration": false, + "module": "commonjs", + "strict": true, + "noImplicitAny": false, + "strictPropertyInitialization": false, + "experimentalDecorators": true, + "target": "es5", + "typeRoots": ["node_modules/@types"], + "lib": ["dom", "es2018"], + "paths": { + "remix-url-resolver": ["./"] + } + } +} diff --git a/remix-url-resolver/tslint.json b/remix-url-resolver/tslint.json new file mode 100644 index 0000000000..56c85deb96 --- /dev/null +++ b/remix-url-resolver/tslint.json @@ -0,0 +1,113 @@ +{ + "rules": { + "arrow-return-shorthand": true, + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": false, + "deprecation": { + "severity": "warn" + }, + "forin": false, + "import-spacing": true, + "indent": [ + true, + "spaces" + ], + "interface-over-type-literal": true, + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-arg": true, + "no-bitwise": false, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-duplicate-super": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-misused-new": true, + "no-non-null-assertion": true, + "no-redundant-jsdoc": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unnecessary-initializer": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + false, + "single" + ], + "radix": true, + "semicolon": [ + true, + "never" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } + } +