Skip to content
This repository was archived by the owner on May 23, 2023. It is now read-only.

Commit e4994a7

Browse files
committed
solidity wrapper rewrite
- exposed plain functions - using --libraries from the solc compiler instead of string substituion
1 parent 655a0b5 commit e4994a7

File tree

2 files changed

+235
-122
lines changed

2 files changed

+235
-122
lines changed

ethereum/_solidity.py

Lines changed: 202 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,242 @@
1+
# -*- coding: utf8 -*-
12
import re
23
import subprocess
34
import os
4-
import yaml # use yaml instead of json to get non unicode
5+
6+
import yaml
7+
8+
BINARY = 'solc'
59

610

711
class CompileError(Exception):
812
pass
913

1014

11-
class solc_wrapper(object):
15+
def get_compiler_path():
16+
""" Return the path to the solc compiler.
1217
13-
"wraps solc binary"
18+
This funtion will search for the solc binary in the $PATH and return the
19+
path of the first executable occurence.
20+
"""
21+
for path in os.getenv('PATH', '').split(os.pathsep):
22+
path = path.strip('"')
23+
executable_path = os.path.join(path, BINARY)
1424

15-
@classmethod
16-
def compiler_available(cls):
17-
program = 'solc'
25+
if os.path.isfile(executable_path) and os.access(executable_path, os.X_OK):
26+
return executable_path
1827

19-
def is_exe(fpath):
20-
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
28+
return None
2129

22-
fpath, fname = os.path.split(program)
23-
if fpath:
24-
if is_exe(program):
25-
return program
26-
else:
27-
for path in os.environ["PATH"].split(os.pathsep):
28-
path = path.strip('"')
29-
exe_file = os.path.join(path, program)
30-
if is_exe(exe_file):
31-
return exe_file
3230

33-
return None
31+
def get_solidity():
32+
""" Return the singleton used to interact with the solc compiler. """
33+
if get_compiler_path() is None:
34+
return None # the compiler wasn't found in $PATH
3435

35-
@classmethod
36-
def contract_names(cls, code):
37-
return re.findall(r'^\s*(contract|library) (\S*) ', code, re.MULTILINE)
36+
return solc_wrapper
37+
38+
39+
def solc_arguments(libraries=None, combined='bin,abi', optimize=True):
40+
""" Build the arguments to call the solc binary. """
41+
args = [
42+
'--combined-json', combined,
43+
'--add-std',
44+
]
45+
46+
if optimize:
47+
args.append('--optmize')
48+
49+
if libraries is not None:
50+
addresses = [
51+
'{name}:{address}'.format(name=name, address=address)
52+
for name, address in libraries.items()
53+
]
54+
args.extend([
55+
'--libraries',
56+
','.join(addresses),
57+
])
58+
59+
return args
60+
61+
62+
def solc_parse_output(compiler_output):
63+
""" Parses the compiler output. """
64+
result = yaml.safe_load(compiler_output)['contracts']
65+
66+
if 'bin' in result.values()[0]:
67+
for value in result.values():
68+
value['bin_hex'] = value['bin']
69+
value['bin'] = value['bin_hex'].decode('hex')
70+
71+
for json_data in ('abi', 'devdoc', 'userdoc'):
72+
# the values in the output can be configured through the
73+
# --combined-json flag, check that it's present in the first value and
74+
# assume all values are consistent
75+
if json_data not in result.values()[0]:
76+
continue
77+
78+
for value in result.values():
79+
value[json_data] = yaml.safe_load(value[json_data])
80+
81+
return result
82+
83+
84+
def compiler_version():
85+
""" Return the version of the installed solc. """
86+
version_info = subprocess.check_output(['solc', '--version'])
87+
match = re.search('^Version: ([0-9a-z.-]+)/', version_info, re.MULTILINE)
88+
89+
if match:
90+
return match.group(1)
91+
92+
93+
def solidity_names(code):
94+
""" Return the library and contract names in order of appearence. """
95+
# the special sequence \s is equivalent to the set [ \t\n\r\f\v]
96+
return re.findall(r'(contract|library)\s+([a-zA-Z][a-zA-Z0-9]*)', code, re.MULTILINE)
97+
98+
99+
def compile_file(filepath, libraries=None, combined='bin,abi', optimize=True):
100+
""" Return the compile contract code.
101+
102+
Args:
103+
filepath (str): The path to the contract source code.
104+
libraries (dict): A dictionary mapping library name to address.
105+
combined (str: The flags passed to the solidity compiler to defined
106+
what output should be used.
107+
optimize (bool): Flag to set up compiler optimization.
108+
109+
Returns:
110+
dict: A mapping from the contract name to it's binary.
111+
"""
112+
113+
workdir, filename = os.path.split(filepath)
114+
115+
args = solc_arguments(libraries=libraries, combined=combined, optimize=optimize)
116+
args.insert(0, get_compiler_path())
117+
args.append(filename)
118+
119+
output = subprocess.check_output(args, cwd=workdir)
120+
121+
return solc_parse_output(output)
122+
123+
124+
def compile_contract(filepath, contract_name, libraries=None, combined='bin,abi', optimize=True):
125+
all_contracts = compile_file(
126+
filepath,
127+
libraries=libraries,
128+
combined=combined,
129+
optimize=optimize,
130+
)
131+
132+
return all_contracts[contract_name]
133+
134+
135+
def compile_last_contract(filepath, libraries=None, combined='bin,abi', optimize=True):
136+
with open(filepath) as handler:
137+
all_names = solidity_names(handler.read())
138+
139+
all_contract_names = [
140+
name
141+
for kind, name in all_names
142+
# if kind == 'contract'
143+
]
144+
145+
last_contract = all_contract_names[-1]
146+
147+
return compile_contract(
148+
filepath,
149+
last_contract,
150+
libraries=libraries,
151+
combined=combined,
152+
optimize=optimize,
153+
)
154+
155+
156+
def compile_code(sourcecode, libraries=None, combined='bin,abi', optimize=True):
157+
args = solc_arguments(libraries=libraries, combined=combined, optimize=optimize)
158+
args.insert(0, get_compiler_path())
159+
160+
process = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
161+
stdoutdata, _ = process.communicate(input=sourcecode)
162+
163+
return solc_parse_output(stdoutdata)
164+
165+
166+
class Solc(object):
167+
""" Wraps the solc binary. """
168+
169+
compiler_available = staticmethod(get_compiler_path)
170+
contract_names = staticmethod(solidity_names)
171+
compiler_version = staticmethod(compiler_version)
172+
173+
@staticmethod
174+
def _code_or_path(sourcecode, path, contract_name, libraries, combined):
175+
if sourcecode and path:
176+
raise ValueError('sourcecode and path are mutually exclusive.')
177+
178+
if path and contract_name:
179+
return compile_contract(path, contract_name, libraries=libraries, combined=combined)
180+
181+
if path:
182+
return compile_last_contract(path, libraries=libraries, combined=combined)
183+
184+
all_names = solidity_names(sourcecode)
185+
all_contract_names = [
186+
name
187+
for kind, name in all_names
188+
if kind == 'contract'
189+
]
190+
last_contract = all_contract_names[-1]
191+
192+
result = compile_code(sourcecode, libraries=libraries, combined=combined)
193+
return result[last_contract]
38194

39195
@classmethod
40196
def compile(cls, code, path=None, libraries=None, contract_name=''):
41-
"returns binary of last contract in code"
42-
sorted_contracts = cls.combined(code, path=path)
43-
if contract_name:
44-
idx = [x[0] for x in sorted_contracts].index(contract_name)
45-
else:
46-
idx = -1
47-
if libraries:
48-
if cls.compiler_version() < "0.1.2":
49-
raise CompileError('Compiler does not support libraries. Please update compiler.')
50-
for lib_name, lib_address in libraries.iteritems():
51-
sorted_contracts[idx][1]['bin'] = sorted_contracts[idx][1]['bin'].replace(
52-
"__{}{}".format(lib_name, "_" * (38 - len(lib_name))), lib_address)
53-
return sorted_contracts[idx][1]['bin'].decode('hex')
197+
""" Return the binary of last contract in code. """
198+
result = cls._code_or_path(code, path, contract_name, libraries, 'bin')
199+
return result['bin']
54200

55201
@classmethod
56202
def mk_full_signature(cls, code, path=None, libraries=None, contract_name=''):
57203
"returns signature of last contract in code"
58-
sorted_contracts = cls.combined(code, path=path)
59-
if contract_name:
60-
idx = [x[0] for x in sorted_contracts].index(contract_name)
61-
else:
62-
idx = -1
63-
return sorted_contracts[idx][1]['abi']
204+
205+
result = cls._code_or_path(code, path, contract_name, libraries, 'abi')
206+
return result['abi']
64207

65208
@classmethod
66209
def combined(cls, code, path=None):
67-
"""compile combined-json with abi,bin,devdoc,userdoc
210+
""" Compile combined-json with abi,bin,devdoc,userdoc.
211+
68212
@param code: literal solidity code as a string.
69213
@param path: absolute path to solidity-file. Note: code & path are exclusive!
70214
"""
71-
p = None
72-
if path is None:
73-
p = subprocess.Popen(['solc', '--add-std', '--optimize', '--combined-json', 'abi,bin,devdoc,userdoc'],
74-
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
75-
stdoutdata, stderrdata = p.communicate(input=code)
76-
else:
77-
assert code is None or len(code) == 0, "`code` and `path` are exclusive!"
78-
workdir, fn = os.path.split(path)
79-
p = subprocess.Popen(['solc', '--add-std', '--optimize', '--combined-json', 'abi,bin,devdoc,userdoc', fn],
80-
stdout=subprocess.PIPE, cwd=workdir)
81-
stdoutdata = p.stdout.read().strip()
82-
p.terminate()
83-
if p.returncode:
84-
raise CompileError('compilation failed')
85-
# contracts = json.loads(stdoutdata)['contracts']
86-
contracts = yaml.safe_load(stdoutdata)['contracts']
87-
for contract_name, data in contracts.items():
88-
data['abi'] = yaml.safe_load(data['abi'])
89-
data['devdoc'] = yaml.safe_load(data['devdoc'])
90-
data['userdoc'] = yaml.safe_load(data['userdoc'])
91-
92-
names = cls.contract_names(code or open(path).read())
93-
assert len(names) <= len(contracts) # imported contracts are not returned
215+
216+
contracts = cls._code_or_path(
217+
sourcecode=code,
218+
path=path,
219+
contract_name=None,
220+
libraries=None,
221+
combined='abi,bin,devdoc,userdoc',
222+
)
223+
224+
if path:
225+
with open(path) as handler:
226+
code = handler.read()
227+
94228
sorted_contracts = []
95-
for name in names:
229+
for name in solidity_names(code):
96230
sorted_contracts.append((name[1], contracts[name[1]]))
97231
return sorted_contracts
98232

99-
@classmethod
100-
def compiler_version(cls):
101-
version_info = subprocess.check_output(['solc', '--version'])
102-
match = re.search("^Version: ([0-9a-z.-]+)/", version_info, re.MULTILINE)
103-
if match:
104-
return match.group(1)
105-
106233
@classmethod
107234
def compile_rich(cls, code, path=None):
108235
"""full format as returned by jsonrpc"""
109236

110237
return {
111238
contract_name: {
112-
'code': "0x" + contract.get('bin'),
239+
'code': '0x' + contract.get('bin'),
113240
'info': {
114241
'abiDefinition': contract.get('abi'),
115242
'compilerVersion': cls.compiler_version(),
@@ -125,51 +252,4 @@ def compile_rich(cls, code, path=None):
125252
}
126253

127254

128-
def get_solidity(try_import=False):
129-
if try_import:
130-
try:
131-
import solidity, tester
132-
tester.languages['solidity'] = solidity
133-
except ImportError:
134-
pass
135-
if not solc_wrapper.compiler_available():
136-
return None
137-
return solc_wrapper
138-
139-
140-
if __name__ == '__main__':
141-
import tester
142-
assert 'solidity' in tester.languages
143-
144-
one_contract = """
145-
contract foo {
146-
function seven() returns (int256 y) {
147-
y = 7;
148-
}
149-
function mul2(int256 x) returns (int256 y) {
150-
y = x * 2;
151-
}
152-
}
153-
"""
154-
155-
two_contracts = one_contract + """
156-
contract baz {
157-
function echo(address a) returns (address b) {
158-
b = a;
159-
return b;
160-
}
161-
function eight() returns (int256 y) {
162-
y = 8;
163-
}
164-
}
165-
"""
166-
167-
# test
168-
assert 'solidity' in tester.languages
169-
170-
s = tester.state()
171-
172-
c1 = s.abi_contract(one_contract, language='solidity')
173-
assert c1.seven() == 7
174-
assert c1.mul2(2) == 4
175-
assert c1.mul2(-2) == -4
255+
solc_wrapper = Solc # pylint: disable=invalid-name

0 commit comments

Comments
 (0)