Skip to content

Commit

Permalink
eth/solidity: add solidity compiler bindings (#66)
Browse files Browse the repository at this point in the history
* ci: install solidity compiler solc

* eth/solidity: add solidity compiler bindings

* eth/solidity: add tests and test contracts

* docs: update readme

* eth/solidity: fix error message

* eth/solidity: improve extension handling

* eth/solidity: test deploying a deposit contract

* spec: run rufo

* If read.length is greater than 8192, call recvmsg one more time to extract

Not yet known if this is a Mac OS specific problem.

* Use end_with?

* client/ipc: loop socket receive buffers until they close

Co-authored-by: q9f <q9f@users.noreply.github.com>
Co-authored-by: Yuta Kurotaki <yuta.kurotaki@gmail.com>
  • Loading branch information
3 people authored Mar 28, 2022
1 parent 0773bbf commit c1169ea
Show file tree
Hide file tree
Showing 10 changed files with 409 additions and 3 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ jobs:
- name: MacOs Dependencies
run: |
brew tap ethereum/ethereum
brew install --verbose pkg-config automake autogen ethereum
brew install --verbose pkg-config automake autogen ethereum solidity
if: startsWith(matrix.os, 'macOS')
- name: Ubuntu Dependencies
run: |
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install ethereum
sudo apt-get install ethereum solc
if: startsWith(matrix.os, 'Ubuntu')
- name: Run Geth
run: |
Expand Down
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ What you get:
- [x] ABI-Encoder and Decoder (including type parser)
- [x] RLP-Encoder and Decoder (including sedes)
- [x] RPC-Client (IPC/HTTP) for Execution-Layer APIs
- [x] Solidity bindings (compile contracts from Ruby)

Soon (TM):
- [ ] Smart Contracts and Solidity Support
- [ ] Smart Contract Support
- [ ] EIP-1271 Smart-Contract Authentification
- [ ] HD-Wallets (BIP-32) and Mnemonics (BIP-39)

Expand All @@ -44,6 +45,7 @@ Contents:
- [2.5. Ethereum ABI Encoder and Decoder](#25-ethereum-abi-encoder-and-decoder)
- [2.6. Ethereum RLP Encoder and Decoder](#26-ethereum-rlp-encoder-and-decoder)
- [2.7. Ethereum RPC-Client](#27-ethereum-rpc-client)
- [2.8 Solidity Compiler Bindings](#28-solidity-compiler-bindings)
- [3. Documentation](#3-documentation)
- [4. Testing](#4-testing)
- [5. Contributing](#5-contributing)
Expand Down Expand Up @@ -229,6 +231,28 @@ cli.get_nonce cli.eth_coinbase["result"]

Check out `Eth::Api` for a list of supported RPC-APIs or consult the [Documentation](https://q9f.github.io/eth.rb/) for more details.

### 2.8 Solidity Compiler Bindings
Link a system-level Solidity compiler (`solc`) to your Ruby library and compile contracts.

```ruby
solc = Eth::Solidity.new
# => #<Eth::Solidity:0x000055f05040c6d0 @compiler="/usr/bin/solc">
contract = solc.compile "spec/fixtures/contracts/greeter.sol"
# => {"Greeter"=>
# {"abi"=>
# [{"inputs"=>[{"internalType"=>"string", "name"=>"message", "type"=>"string"}], "stateMutability"=>"nonpayable", "type"=>"constructor"},
# {"inputs"=>[], "name"=>"greet", "outputs"=>[{"internalType"=>"string", "name"=>"", "type"=>"string"}], "stateMutability"=>"view", "type"=>"function"},
# {"inputs"=>[], "name"=>"kill", "outputs"=>[], "stateMutability"=>"nonpayable", "type"=>"function"}],
# "bin"=>
# "6080604052348015...6c634300080c0033"},
# "Mortal"=>
# {"abi"=>[{"inputs"=>[], "stateMutability"=>"nonpayable", "type"=>"constructor"}, {"inputs"=>[], "name"=>"kill", "outputs"=>[], "stateMutability"=>"nonpayable", "type"=>"function"}],
# "bin"=>
# "6080604052348015...6c634300080c0033"}}
```

The `contract["Greeter"]["bin"]` could be directly used to deploy the contract as `Eth::Tx` payload. Check out the [Documentation](https://q9f.github.io/eth.rb/) for more details.

## 3. Documentation
The documentation can be found at: https://q9f.github.io/eth.rb

Expand Down
1 change: 1 addition & 0 deletions lib/eth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module Eth
require "eth/key"
require "eth/rlp"
require "eth/signature"
require "eth/solidity"
require "eth/tx"
require "eth/unit"
require "eth/util"
Expand Down
3 changes: 3 additions & 0 deletions lib/eth/client/ipc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ def send(payload)
socket = UNIXSocket.new(@path)
socket.puts(payload)
read = socket.recvmsg(nil)[0]
until read.end_with?("\n")
read = read << socket.recvmsg(nil)[0]
end
socket.close
return read
end
Expand Down
75 changes: 75 additions & 0 deletions lib/eth/solidity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Copyright (c) 2016-2022 The Ruby-Eth Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require "open3"

# Provides the {Eth} module.
module Eth

# Class to create {Solidity} compiler bingings for Ruby.
class Solidity

# Provides a Compiler Error in case the contract does not compile.
class CompilerError < StandardError; end

# Solidity compiler binary path.
attr_reader :compiler

# Instantiates a Solidity `solc` system compiler binding that can be
# used to compile Solidity contracts.
def initialize

# Currently only supports `solc`.
solc = get_compiler_path
raise SystemCallError, "Unable to find the solc compiler path!" if solc.nil?
@compiler = solc
end

# Use the bound Solidity executable to compile the given contract.
#
# @param contract [String] path of the contract to compile.
# @return [Array] JSON containing the compiled contract and ABI for all contracts.
def compile(contract)
raise Errno::ENOENT, "Contract file not found: #{contract}" unless File.exist? contract
command = "#{@compiler} --optimize --combined-json bin,abi #{contract}"
output, error, status = Open3.capture3 command
raise SystemCallError, "Unable to run solc compiler!" if status.exitstatus === 127
raise CompilerError, error unless status.success?
json = JSON.parse output
result = {}
json["contracts"].each do |key, value|
_file, name = key.split ":"
result[name] = {}
result[name]["abi"] = value["abi"]
result[name]["bin"] = value["bin"]
end
return result
end

private

# Tries to find a system executable path for the given compiler binary name.
def get_compiler_path(name = "solc")
extensions = [""]
extensions = ENV["PATHEXT"].split(";") unless ENV["PATHEXT"].nil?
ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
extensions.each do |ext|
executable = File.join path, "#{name}#{ext}"
return executable if File.executable? executable and !File.directory? executable
end
end
return nil
end
end
end
64 changes: 64 additions & 0 deletions spec/eth/solidity_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
require "spec_helper"

describe Solidity do
it "finds a solc compiler" do

# This fails if no `solc` is in the $PATH.
expect(Solidity.new).to be
end

subject(:solc) { Solidity.new }

it "compiles the dummy contract" do
contract = "#{Dir.pwd}/spec/fixtures/contracts/dummy.sol"
result = solc.compile contract
expect(result.keys).to eq ["Dummy"]
expect(result["Dummy"]["abi"]).to eq JSON.parse '[{"inputs":[],"name":"get","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"x","type":"uint256"}],"name":"set","outputs":[],"stateMutability":"nonpayable","type":"function"}]'
expect(result["Dummy"]["bin"]).to start_with "6080604052348015600f57600080fd5b5060"
end

it "compiles the greeter contract" do
contract = "#{Dir.pwd}/spec/fixtures/contracts/greeter.sol"
result = solc.compile contract
expect(result.keys).to eq ["Greeter", "Mortal"]
expect(result["Mortal"]["abi"]).to eq JSON.parse '[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"kill","outputs":[],"stateMutability":"nonpayable","type":"function"}]'
expect(result["Greeter"]["abi"]).to eq JSON.parse '[{"inputs":[{"internalType":"string","name":"message","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"greet","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"kill","outputs":[],"stateMutability":"nonpayable","type":"function"}]'
expect(result["Mortal"]["bin"]).to start_with "6080604052348015600f57600080fd5b5060"
expect(result["Greeter"]["bin"]).to start_with "608060405234801561001057600080fd5b5060"
end

it "deploys an ethereum-consensus deposit contract" do
geth = Client.create "/tmp/geth.ipc"
contract = "#{Dir.pwd}/spec/fixtures/contracts/deposit.sol"
result = solc.compile contract
expect(result["DepositContract"]).to be
payload = result["DepositContract"]["bin"]
expect(payload).to start_with "60806040523480156200001157600080fd5b5060"
params = {
from: geth.default_account,
priority_fee: 0,
max_gas_fee: Unit::GWEI,
gas_limit: Tx.estimate_intrinsic_gas(payload),
data: payload,
}
deploy = geth.eth_send_transaction(params)
hash = deploy["result"]
expect(hash).to start_with "0x"
geth.wait_for_tx hash
receipt = geth.eth_get_transaction_receipt hash
expect(receipt["result"]).to be
address = receipt["result"]["contractAddress"]
expect(address).to be
expect { Address.new address }.not_to raise_error
end

it "handles file-system errors" do
contract = "#{Dir.pwd}/spec/fixtures/contracts/null.sol"
expect { solc.compile contract }.to raise_error Errno::ENOENT, /No such file or directory - Contract file not found:/
end

it "handles compiler errors" do
contract = "#{Dir.pwd}/spec/fixtures/contracts/error.sol"
expect { solc.compile contract }.to raise_error Solidity::CompilerError, /Error: Identifier not found or not unique./
end
end
Loading

0 comments on commit c1169ea

Please sign in to comment.