Module: Ethereum::Tester::SolidityWrapper

Defined in:
lib/ethereum/tester/solidity_wrapper.rb

Defined Under Namespace

Classes: CompileError

Class Method Summary collapse

Class Method Details

.combined(code, path: nil) ⇒ Object

Compile combined-json with abi,bin,devdoc,userdoc.

Raises:



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 44

def combined(code, path: nil)
  raise ValueError, "code and path are mutually exclusive." if code && path

  if path
    contracts = compile_file path
    code = File.read(path)
  elsif code
    contracts = compile_code code
  else
    raise ValueError, 'either code or path needs to be supplied.'
  end

  solidity_names(code).map do |(kind, name)|
    [name, contracts[name]]
  end
end

.compile(code, contract_name: '', libraries: nil, path: nil) ⇒ Object

Returns binary of last contract in code.



28
29
30
31
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 28

def compile(code, contract_name: '', libraries: nil, path: nil)
  result = compile_code_or_path code, path, contract_name, libraries, 'bin'
  result['bin']
end

.compile_code(code, libraries: nil, combined: 'bin,abi', optimize: true) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 84

def compile_code(code, libraries: nil, combined:'bin,abi', optimize: true)
  args = solc_arguments libraries: libraries, combined: combined, optimize: optimize
  args.unshift solc_path

  out = Tempfile.new 'solc_output_'
  pipe = IO.popen(args, 'w', [:out, :err] => out)
  pipe.write code
  pipe.close_write
  unless $?.success?
    out.rewind
    output = out.read
    raise CompileError, "compilation failed:\n#{output}"
  end

  out.rewind
  solc_parse_output out.read
end

.compile_code_or_path(code, path, contract_name, libraries, combined) ⇒ Object

Raises:



12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 12

def compile_code_or_path(code, path, contract_name, libraries, combined)
  raise ValueError, 'code and path are mutually exclusive' if code && path

  return compile_contract(path, contract_name, libraries: libraries, combined: combined) if path && contract_name.true?
  return compile_last_contract(path, libraries: libraries, combined: combined) if path

  all_names = solidity_names code
  all_contract_names = all_names.map(&:last)

  result = compile_code(code, libraries: libraries, combined: combined)
  result[all_contract_names.last]
end

.compile_contract(path, contract_name, libraries: nil, combined: 'bin,abi', optimize: true) ⇒ Object



108
109
110
111
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 108

def compile_contract(path, contract_name, libraries: nil, combined: 'bin,abi', optimize: true)
  all_contracts = compile_file path, libraries: libraries, combined: combined, optimize: optimize
  all_contracts[contract_name]
end

.compile_file(path, libraries: nil, combined: 'bin,abi', optimize: true) ⇒ Hash

Return the compiled contract code.

Parameters:

  • Path to the contract source code.

  • (defaults to: nil)

    A hash mapping library name to its address.

  • (defaults to: 'bin,abi')

    The argument for solc's --combined-json.

  • (defaults to: true)

    Enable/disables compiler optimization.

Returns:

  • A mapping from the contract name to it's bytecode.



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 123

def compile_file(path, libraries: nil, combined: 'bin,abi', optimize: true)
  workdir = File.dirname path
  filename = File.basename path

  args = solc_arguments libraries: libraries, combined: combined, optimize: optimize
  args.unshift solc_path
  args.push filename

  out = Tempfile.new 'solc_output_'
  Dir.chdir(workdir) do
    pipe = IO.popen(args, 'w', [:out, :err] => out)
    pipe.close_write
  end

  out.rewind
  solc_parse_output out.read
end

.compile_last_contract(path, libraries: nil, combined: 'bin,abi', optimize: true) ⇒ Object



102
103
104
105
106
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 102

def compile_last_contract(path, libraries: nil, combined: 'bin,abi', optimize: true)
  all_names = solidity_names File.read(path)
  all_contract_names = all_names.map(&:last) # don't filter libraries
  compile_contract path, all_contract_names.last, libraries: libraries, combined: combined, optimize: optimize
end

.compile_rich(code, path: nil) ⇒ Object

Full format as returned by jsonrpc.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 64

def compile_rich(code, path: nil)
  combined(code, path: path).map do |(name, contract)|
    [
      name,
      {
        'code' => "0x#{contract['bin_hex']}",
        'info' => {
          'abiDefinition' => contract['abi'],
          'compilerVersion' => compiler_version,
          'developerDoc' => contract['devdoc'],
          'language' => 'Solidity',
          'languageVersion' => '0',
          'source' => code,
          'userDoc' => contract['userdoc']
        }
      }
    ]
  end.to_h
end

.compiler_versionObject



252
253
254
255
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 252

def compiler_version
  output = `#{solc_path} --version`.strip
  output =~ /^Version: ([0-9a-zA-Z.\-+]+)/m ? $1 : nil
end

.mk_full_signature(code, contract_name: '', libraries: nil, path: nil) ⇒ Object

Returns signature of last contract in code.



36
37
38
39
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 36

def mk_full_signature(code, contract_name: '', libraries: nil, path: nil)
  result = compile_code_or_path code, path, contract_name, libraries, 'abi'
  result['abi']
end

.solc_arguments(libraries: nil, combined: 'bin,abi', optimize: true) ⇒ Object



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 287

def solc_arguments(libraries: nil, combined: 'bin,abi', optimize: true)
  args = [
    '--combined-json', combined,
    '--add-std'
  ]

  args.push '--optimize' if optimize

  if libraries && !libraries.empty?
    addresses = libraries.map {|name, addr| "#{name}:#{addr}" }
    args.push '--libraries'
    args.push addresses.join(',')
  end

  args
end

.solc_parse_output(compiler_output) ⇒ Object

Parse compiler output.



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 260

def solc_parse_output(compiler_output)
  result = JSON.parse(compiler_output)['contracts']

  if result.values.first.has_key?('bin')
    result.each_value do |v|
      v['bin_hex'] = v['bin']

      # decoding can fail if the compiled contract has unresolved symbols
      begin
        v['bin'] = Utils.decode_hex v['bin']
      rescue TypeError
        # do nothing
      end
    end
  end

  %w(abi devdoc userdoc).each do |json_data|
    next unless result.values.first.has_key?(json_data)

    result.each_value do |v|
      v[json_data] = JSON.parse v[json_data]
    end
  end

  result
end

.solc_pathObject



304
305
306
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 304

def solc_path
  @solc_path ||= which('solc')
end

.solc_path=(v) ⇒ Object



308
309
310
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 308

def solc_path=(v)
  @solc_path = v
end

.solidity_library_symbol(library_name) ⇒ Object

Return the symbol used in the bytecode to represent the ‘library_name`.

The symbol is always 40 characters in length with the minimum of two leading and trailing underscores.



197
198
199
200
201
202
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 197

def solidity_library_symbol(library_name)
  len = [library_name.size, 36].min
  lib_piece = library_name[0,len]
  hold_piece = '_' * (36 - len)
  "__#{lib_piece}#{hold_piece}__"
end

.solidity_names(code) ⇒ Object

Return the library and contract names in order of appearence.



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 144

def solidity_names(code)
  names = []
  in_string = nil
  backslash = false
  comment = nil

  # "parse" the code by hand to handle the corner cases:
  #
  # - the contract or library can be inside a comment or string
  # - multiline comments
  # - the contract and library keywords could not be at the start of the line
  code.each_char.with_index do |char, pos|
    if in_string
      if !backslash && in_string == char
        in_string = nil
        backslash = false
      end

      backslash = char == "\\"
    elsif comment == "//"
      comment = nil if ["\n", "\r"].include?(char)
    elsif comment == "/*"
      comment = nil if char == "*" && code[pos + 1] == "/"
    else
      in_string = char if %w(' ").include?(char)

      if char == "/"
        char2 = code[pos + 1]
        comment = char + char2 if %w(/ *).include?(char2)
      end

      if char == 'c' && code[pos, 8] == 'contract'
        result = code[pos..-1] =~ /^contract[^_$a-zA-Z]+([_$a-zA-Z][_$a-zA-Z0-9]*)/
        names.push ['contract', $1] if result
      end

      if char == 'l' && code[pos, 7] == 'library'
        result = code[pos..-1] =~ /^library[^_$a-zA-Z]+([_$a-zA-Z][_$a-zA-Z0-9]*)/
        names.push ['library', $1] if result
      end
    end
  end

  names
end

.solidity_resolve_address(hex_code, library_symbol, library_address) ⇒ String

Change the bytecode to use the given library address.

Parameters:

  • The bytecode encoded in hex.

  • The library that will be resolved.

  • The address of the library.

Returns:

  • The bytecode encoded in hex with the library references.

Raises:



214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 214

def solidity_resolve_address(hex_code, library_symbol, library_address)
  raise ValueError, "Address should not contain the 0x prefix" if library_address =~ /\A0x/
  raise ValueError, "Address with wrong length" if library_symbol.size != 40 || library_address.size != 40

  begin
    Utils.decode_hex library_address
  rescue TypeError
    raise ValueError, "library_address contains invalid characters, it must be hex encoded."
  end

  hex_code.gsub library_symbol, library_address
end

.solidity_resolve_symbols(hex_code, libraries) ⇒ Object



227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 227

def solidity_resolve_symbols(hex_code, libraries)
  symbol_address = libraries
    .map {|name, addr| [solidity_library_symbol(name), addr] }
    .to_h

  solidity_unresolved_symbols(hex_code).each do |unresolved|
    address = symbol_address[unresolved]
    hex_code = solidity_resolve_address(hex_code, unresolved, address)
  end

  hex_code
end

.solidity_unresolved_symbols(hex_code) ⇒ Object

Return the unresolved symbols contained in the ‘hex_code`.

Note: the binary representation should not be provided since this function relies on the fact that the ‘_’ is invalid in hex encoding.

Parameters:

  • The bytecode encoded as hex.



248
249
250
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 248

def solidity_unresolved_symbols(hex_code)
  hex_code.scan(/_.{39}/).uniq
end

.which(cmd) ⇒ Object



312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/ethereum/tester/solidity_wrapper.rb', line 312

def which(cmd)
  return ENV['SOLC_BINARY'] if ENV['SOLC_BINARY']

  exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
  ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
    exts.each { |ext|
      exe = File.join(path, "#{cmd}#{ext}")
      return exe if File.executable?(exe) && !File.directory?(exe)
    }
  end
  return nil
end