Skip to content

Commit 35c858e

Browse files
authored
Merge pull request #373 from embark-framework/features/import-http-from-contract
Enable importing from HTTP inside sol contracts
2 parents 9a79df6 + d48c971 commit 35c858e

File tree

12 files changed

+322
-150
lines changed

12 files changed

+322
-150
lines changed

lib/core/config.js

Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -143,49 +143,6 @@ Config.prototype.loadContractsConfigFile = function() {
143143
this.contractsConfig = this._mergeConfig(configFilePath, configObject, this.env);
144144
};
145145

146-
Config.prototype.getExternalContractUrl = function (contract) {
147-
let url;
148-
const RAW_URL = 'https://raw.githubusercontent.com/';
149-
const MALFORMED_ERROR = 'Malformed Github URL for ';
150-
if (contract.file.startsWith('https://github')) {
151-
const match = contract.file.match(/https:\/\/github\.[a-z]+\/(.*)/);
152-
if (!match) {
153-
this.logger.error(MALFORMED_ERROR + contract.file);
154-
return null;
155-
}
156-
url = `${RAW_URL}${match[1].replace('blob/', '')}`;
157-
} else if (contract.file.startsWith('git')) {
158-
// Match values
159-
// [0] entire input
160-
// [1] git://
161-
// [2] user
162-
// [3] repository
163-
// [4] path
164-
// [5] branch
165-
const match = contract.file.match(
166-
/(git:\/\/)?github\.[a-z]+\/([a-zA-Z0-9_\-.]+)\/([a-zA-Z0-9_\-]+)\/([a-zA-Z0-9_\-\/.]+)#?([a-zA-Z0-1_\-.]*)?/
167-
);
168-
if (!match) {
169-
this.logger.error(MALFORMED_ERROR + contract.file);
170-
return null;
171-
}
172-
let branch = match[5];
173-
if (!branch) {
174-
branch = 'master';
175-
}
176-
url = `${RAW_URL}${match[2]}/${match[3]}/${branch}/${match[4]}`;
177-
} else {
178-
url = contract.file;
179-
}
180-
const match = url.match(
181-
/\.[a-z]+\/([a-zA-Z0-9_\-\/.]+)/
182-
);
183-
return {
184-
url,
185-
filePath: match[1]
186-
};
187-
};
188-
189146
Config.prototype.loadExternalContractsFiles = function() {
190147
let contracts = this.contractsConfig.contracts;
191148
for (let contractName in contracts) {
@@ -194,11 +151,11 @@ Config.prototype.loadExternalContractsFiles = function() {
194151
continue;
195152
}
196153
if (contract.file.startsWith('http') || contract.file.startsWith('git')) {
197-
const fileObj = this.getExternalContractUrl(contract);
154+
const fileObj = utils.getExternalContractUrl(contract.file);
198155
if (!fileObj) {
199156
return this.logger.error("HTTP contract file not found: " + contract.file);
200157
}
201-
const localFile = constants.httpContractsDirectory + fileObj.filePath;
158+
const localFile = fileObj.filePath;
202159
this.contractsFiles.push(new File({filename: localFile, type: File.types.http, basedir: '', path: fileObj.url}));
203160
} else if (fs.existsSync(contract.file)) {
204161
this.contractsFiles.push(new File({filename: contract.file, type: File.types.dapp_file, basedir: '', path: contract.file}));

lib/core/file.js

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const async = require('async');
22
const fs = require('./fs.js');
33
const path = require('path');
44
const request = require('request');
5+
const utils = require('../utils/utils');
56

67
class File {
78

@@ -13,28 +14,51 @@ class File {
1314
this.resolver = options.resolver;
1415
}
1516

16-
parseFileForImport(content, callback) {
17+
parseFileForImport(content, isHttpContract, callback) {
1718
const self = this;
19+
if (typeof isHttpContract === 'function') {
20+
callback = isHttpContract;
21+
isHttpContract = false;
22+
}
1823
if (self.filename.indexOf('.sol') < 0) {
1924
// Only supported in Solidity
20-
return callback();
25+
return callback(null, content);
2126
}
22-
const regex = /import "([a-zA-Z0-9_\-.\\\/]+)";/g;
27+
const regex = /import ["|']([-a-zA-Z0-9@:%_+.~#?&\/=]+)["|'];/g;
2328
let matches;
2429
const filesToDownload = [];
2530
const pathWithoutFile = path.dirname(self.path);
2631
while ((matches = regex.exec(content))) {
27-
filesToDownload.push({
32+
const httpFileObj = utils.getExternalContractUrl(matches[1]);
33+
const fileObj = {
2834
fileRelativePath: path.join(path.dirname(self.filename), matches[1]),
2935
url: `${pathWithoutFile}/${matches[1]}`
30-
});
36+
};
37+
if (httpFileObj) {
38+
// Replace http import by filePath import in content
39+
content = content.replace(matches[1], httpFileObj.filePath);
40+
41+
fileObj.fileRelativePath = httpFileObj.filePath;
42+
fileObj.url = httpFileObj.url;
43+
} else if (!isHttpContract) {
44+
// Just a normal import
45+
continue;
46+
}
47+
filesToDownload.push(fileObj);
3148
}
3249

50+
if (self.downloadedImports) {
51+
// We already parsed this file
52+
return callback(null, content);
53+
}
54+
self.downloadedImports = true;
3355
async.each(filesToDownload, ((fileObj, eachCb) => {
3456
self.downloadFile(fileObj.fileRelativePath, fileObj.url, (_content) => {
3557
eachCb();
3658
});
37-
}), callback);
59+
}), (err) => {
60+
callback(err, content);
61+
});
3862
}
3963

4064
downloadFile (filename, url, callback) {
@@ -63,7 +87,7 @@ class File {
6387
fs.readFile(filename, next);
6488
},
6589
function parseForImports(content, next) {
66-
self.parseFileForImport(content, (err) => {
90+
self.parseFileForImport(content, true, (err) => {
6791
next(err, content);
6892
});
6993
}
@@ -77,14 +101,19 @@ class File {
77101
}
78102

79103
content (callback) {
104+
let content;
80105
if (this.type === File.types.embark_internal) {
81-
return callback(fs.readFileSync(fs.embarkPath(this.path)).toString());
106+
content = fs.readFileSync(fs.embarkPath(this.path)).toString();
82107
} else if (this.type === File.types.dapp_file) {
83-
return callback(fs.readFileSync(this.path).toString());
108+
content = fs.readFileSync(this.path).toString();
84109
} else if (this.type === File.types.custom) {
85-
return this.resolver(callback);
110+
return this.resolver((theContent) => {
111+
this.parseFileForImport(theContent, (err, newContent) => {
112+
callback(newContent);
113+
});
114+
});
86115
} else if (this.type === File.types.http) {
87-
this.downloadFile(this.filename, this.path, (content) => {
116+
return this.downloadFile(this.filename, this.path, (content) => {
88117
if (!content) {
89118
return callback(content);
90119
}
@@ -95,6 +124,9 @@ class File {
95124
} else {
96125
throw new Error("unknown file: " + this.filename);
97126
}
127+
return this.parseFileForImport(content, (err, newContent) => {
128+
callback(newContent);
129+
});
98130
}
99131

100132
}

lib/modules/solidity/solcP.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ let solc;
33
const fs = require('fs-extra');
44
const path = require('path');
55
const constants = require('../../constants');
6+
const Utils = require('../../utils/utils');
67

78
function findImports(filename) {
9+
if (filename.startsWith('http') || filename.startsWith('git')) {
10+
const fileObj = Utils.getExternalContractUrl(filename);
11+
filename = fileObj.filePath;
12+
}
813
if (fs.existsSync(filename)) {
914
return {contents: fs.readFileSync(filename).toString()};
1015
}

lib/utils/utils.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ let https = require('follow-redirects').https;
66
let shelljs = require('shelljs');
77
var tar = require('tar');
88
var propose = require('propose');
9+
const constants = require('../constants');
910

1011
//let fs = require('../core/fs.js');
1112
let o_fs = require('fs-extra');
@@ -127,6 +128,51 @@ function pwd() {
127128
return process.env.PWD || process.cwd();
128129
}
129130

131+
function getExternalContractUrl(file) {
132+
let url;
133+
const RAW_URL = 'https://raw.githubusercontent.com/';
134+
const MALFORMED_ERROR = 'Malformed Github URL for ';
135+
if (file.startsWith('https://github')) {
136+
const match = file.match(/https:\/\/github\.[a-z]+\/(.*)/);
137+
if (!match) {
138+
console.error(MALFORMED_ERROR + file);
139+
return null;
140+
}
141+
url = `${RAW_URL}${match[1].replace('blob/', '')}`;
142+
} else if (file.startsWith('git')) {
143+
// Match values
144+
// [0] entire input
145+
// [1] git://
146+
// [2] user
147+
// [3] repository
148+
// [4] path
149+
// [5] branch
150+
const match = file.match(
151+
/(git:\/\/)?github\.[a-z]+\/([-a-zA-Z0-9@:%_+.~#?&=]+)\/([-a-zA-Z0-9@:%_+.~#?&=]+)\/([-a-zA-Z0-9@:%_+.~?\/&=]+)#?([a-zA-Z0-1\/_.-]*)?/
152+
);
153+
if (!match) {
154+
console.error(MALFORMED_ERROR + file);
155+
return null;
156+
}
157+
let branch = match[5];
158+
if (!branch) {
159+
branch = 'master';
160+
}
161+
url = `${RAW_URL}${match[2]}/${match[3]}/${branch}/${match[4]}`;
162+
} else if (file.startsWith('http')) {
163+
url = file;
164+
} else {
165+
return null;
166+
}
167+
const match = url.match(
168+
/\.[a-z]+\/([-a-zA-Z0-9@:%_+.~#?&\/=]+)/
169+
);
170+
return {
171+
url,
172+
filePath: constants.httpContractsDirectory + match[1]
173+
};
174+
}
175+
130176
module.exports = {
131177
joinPath: joinPath,
132178
filesMatchingPattern: filesMatchingPattern,
@@ -143,5 +189,6 @@ module.exports = {
143189
downloadFile: downloadFile,
144190
extractTar: extractTar,
145191
proposeAlternative: proposeAlternative,
146-
pwd: pwd
192+
pwd: pwd,
193+
getExternalContractUrl
147194
};

test/config.js

Lines changed: 0 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -57,94 +57,6 @@ describe('embark.Config', function () {
5757
});
5858
});
5959

60-
describe('#getExternalContractUrl', function () {
61-
it('should get the right url for a https://github file', function () {
62-
const fileObj = config.getExternalContractUrl(
63-
{file: 'https://github.com/embark-framework/embark/blob/master/test_app/app/contracts/simple_storage.sol'}
64-
);
65-
assert.deepEqual(fileObj,
66-
{
67-
filePath: 'embark-framework/embark/master/test_app/app/contracts/simple_storage.sol',
68-
url: 'https://raw.githubusercontent.com/embark-framework/embark/master/test_app/app/contracts/simple_storage.sol'
69-
});
70-
});
71-
72-
it('should fail for a malformed https://github file', function () {
73-
const fileObj = config.getExternalContractUrl(
74-
{file: 'https://github/embark-framework/embark/blob/master/test_app/app/contracts/simple_storage.sol'}
75-
);
76-
assert.strictEqual(fileObj, null);
77-
});
78-
79-
it('should get the right url for a git:// file with no branch #', function () {
80-
const fileObj = config.getExternalContractUrl(
81-
{file: 'git://github.com/status-im/contracts/contracts/identity/ERC725.sol'}
82-
);
83-
assert.deepEqual(fileObj,
84-
{
85-
filePath: 'status-im/contracts/master/contracts/identity/ERC725.sol',
86-
url: 'https://raw.githubusercontent.com/status-im/contracts/master/contracts/identity/ERC725.sol'
87-
});
88-
});
89-
90-
it('should get the right url for a git:// file with a branch #', function () {
91-
const fileObj = config.getExternalContractUrl(
92-
{file: 'git://github.com/status-im/contracts/contracts/identity/ERC725.sol#myBranch'}
93-
);
94-
assert.deepEqual(fileObj,
95-
{
96-
filePath: 'status-im/contracts/myBranch/contracts/identity/ERC725.sol',
97-
url: 'https://raw.githubusercontent.com/status-im/contracts/myBranch/contracts/identity/ERC725.sol'
98-
});
99-
});
100-
101-
it('should fail when the git:// file is malformed', function () {
102-
const fileObj = config.getExternalContractUrl(
103-
{file: 'git://github.com/identity/ERC725.sol#myBranch'}
104-
);
105-
assert.strictEqual(fileObj, null);
106-
});
107-
108-
it('should get the right url with a github.com file without branch #', function () {
109-
const fileObj = config.getExternalContractUrl(
110-
{file: 'github.com/status-im/contracts/contracts/identity/ERC725.sol'}
111-
);
112-
assert.deepEqual(fileObj,
113-
{
114-
filePath: 'status-im/contracts/master/contracts/identity/ERC725.sol',
115-
url: 'https://raw.githubusercontent.com/status-im/contracts/master/contracts/identity/ERC725.sol'
116-
});
117-
});
118-
119-
it('should get the right url with a github.com file with branch #', function () {
120-
const fileObj = config.getExternalContractUrl(
121-
{file: 'github.com/status-im/contracts/contracts/identity/ERC725.sol#theBranch'}
122-
);
123-
assert.deepEqual(fileObj,
124-
{
125-
filePath: 'status-im/contracts/theBranch/contracts/identity/ERC725.sol',
126-
url: 'https://raw.githubusercontent.com/status-im/contracts/theBranch/contracts/identity/ERC725.sol'
127-
});
128-
});
129-
130-
it('should fail with a malformed github.com url', function () {
131-
const fileObj = config.getExternalContractUrl(
132-
{file: 'github/status-im/contracts/contracts/identity/ERC725.sol#theBranch'}
133-
);
134-
assert.strictEqual(fileObj, null);
135-
});
136-
137-
it('should succeed with a generic http url', function () {
138-
const fileObj = config.getExternalContractUrl(
139-
{file: 'http://myurl.com/myFile.sol'}
140-
);
141-
assert.deepEqual(fileObj, {
142-
filePath: 'myFile.sol',
143-
url: 'http://myurl.com/myFile.sol'
144-
});
145-
});
146-
});
147-
14860
describe('#loadExternalContractsFiles', function () {
14961
it('should create the right list of files and download', function () {
15062
config.contractsFiles = [];
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
pragma solidity ^0.4.7;
2+
import "https://github.com/embark-framework/embark/blob/develop/test_apps/contracts_app/contracts/contract_args.sol";
3+
contract SimpleStorage {
4+
uint public storedData;
5+
6+
function SimpleStorage(uint initialValue) {
7+
storedData = initialValue;
8+
}
9+
10+
function set(uint x) {
11+
storedData = x;
12+
}
13+
14+
function get() constant returns (uint retVal) {
15+
return storedData;
16+
}
17+
18+
}
19+

test/contracts/contract_with_import.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
pragma solidity ^0.4.7;
2+
import "./ownable.sol";
23
contract SimpleStorage {
34
uint public storedData;
4-
import "./ownable.sol";
55

66
function SimpleStorage(uint initialValue) {
77
storedData = initialValue;

0 commit comments

Comments
 (0)