evm_client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +26 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.travis.yml +32 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/LICENSE.txt +21 -0
- data/PREREQUISITES.md +75 -0
- data/README.md +665 -0
- data/Rakefile +11 -0
- data/bin/console +14 -0
- data/bin/install_parity +22 -0
- data/bin/setup +7 -0
- data/contracts/AccountingLib.sol +112 -0
- data/contracts/AuditorInterface.sol +4 -0
- data/contracts/AuditorRegistry.sol +14 -0
- data/contracts/CustodianInterface.sol +27 -0
- data/contracts/CustodianRegistry.sol +40 -0
- data/contracts/DigixConfiguration.sol +68 -0
- data/contracts/Directory.sol +67 -0
- data/contracts/DoublyLinked.sol +54 -0
- data/contracts/GenericInterface.sol +56 -0
- data/contracts/GenericRegistry.sol +15 -0
- data/contracts/Gold.sol +105 -0
- data/contracts/GoldRegistry.sol +82 -0
- data/contracts/GoldTokenLedger.sol +3 -0
- data/contracts/Interface.sol +27 -0
- data/contracts/Minter.sol +3 -0
- data/contracts/Recaster.sol +3 -0
- data/contracts/Testing.sol +59 -0
- data/contracts/VendorInterface.sol +82 -0
- data/contracts/VendorRegistry.sol +39 -0
- data/contracts/classic/Digixbot.sol +106 -0
- data/contracts/classic/DigixbotConfiguration.sol +62 -0
- data/contracts/classic/DigixbotEthereum.sol +86 -0
- data/contracts/classic/DigixbotUsers.sol +103 -0
- data/contracts/classic/Gold.sol +497 -0
- data/contracts/classic/GoldRegistry.sol +503 -0
- data/contracts/classic/GoldTokenLedger.sol +560 -0
- data/contracts/classic/GoldTokenMinter.sol +607 -0
- data/contracts/classic/ParticipantRegistry.sol +94 -0
- data/contracts/classic/QueueSample.sol +54 -0
- data/evm_client.gemspec +36 -0
- data/lib/evm_client.rb +15 -0
- data/lib/evm_client/abi.rb +32 -0
- data/lib/evm_client/client.rb +146 -0
- data/lib/evm_client/contract.rb +341 -0
- data/lib/evm_client/contract_event.rb +32 -0
- data/lib/evm_client/contract_initializer.rb +54 -0
- data/lib/evm_client/decoder.rb +99 -0
- data/lib/evm_client/deployment.rb +49 -0
- data/lib/evm_client/encoder.rb +118 -0
- data/lib/evm_client/event_log.rb +88 -0
- data/lib/evm_client/explorer_url_helper.rb +25 -0
- data/lib/evm_client/formatter.rb +146 -0
- data/lib/evm_client/function.rb +40 -0
- data/lib/evm_client/function_input.rb +14 -0
- data/lib/evm_client/function_output.rb +14 -0
- data/lib/evm_client/http_client.rb +44 -0
- data/lib/evm_client/initializer.rb +27 -0
- data/lib/evm_client/ipc_client.rb +57 -0
- data/lib/evm_client/project_initializer.rb +28 -0
- data/lib/evm_client/railtie.rb +12 -0
- data/lib/evm_client/singleton.rb +39 -0
- data/lib/evm_client/solidity.rb +40 -0
- data/lib/evm_client/transaction.rb +41 -0
- data/lib/evm_client/version.rb +3 -0
- data/lib/tasks/ethereum_contract.rake +27 -0
- data/lib/tasks/ethereum_node.rake +52 -0
- data/lib/tasks/ethereum_test.rake +32 -0
- data/lib/tasks/ethereum_transaction.rake +24 -0
- metadata +219 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
contract ParticipantRegistry {
|
2
|
+
|
3
|
+
enum ParticipantTypes { Admin, Vendor, Custodian, Auditor }
|
4
|
+
struct ParticipantDatabase {
|
5
|
+
mapping (address => bool) auditors;
|
6
|
+
mapping (address => bool) vendors;
|
7
|
+
mapping (address => bool) custodians;
|
8
|
+
mapping (address => bool) registrars;
|
9
|
+
}
|
10
|
+
|
11
|
+
address owner;
|
12
|
+
ParticipantDatabase participantdb;
|
13
|
+
|
14
|
+
event AddParticipant(address indexed participant, uint indexed participanttype);
|
15
|
+
event RemoveParticipant(address indexed participant, uint indexed participanttype);
|
16
|
+
|
17
|
+
modifier ifowner { if (msg.sender == owner) _ }
|
18
|
+
modifier ifregistrar { if ((participantdb.registrars[tx.origin]) || (tx.origin == owner)) _ }
|
19
|
+
modifier onlyvendor { if (isVendor(tx.origin) == true) _ }
|
20
|
+
modifier onlycustodian { if (isCustodian(tx.origin) == true) _ }
|
21
|
+
modifier onlyauditor { if (isAuditor(tx.origin) == true) _ }
|
22
|
+
|
23
|
+
function ParticipantRegistry() {
|
24
|
+
owner = msg.sender;
|
25
|
+
}
|
26
|
+
|
27
|
+
function getOwner() returns (address oa) {
|
28
|
+
oa = owner;
|
29
|
+
}
|
30
|
+
|
31
|
+
function setOwner(address nown) ifowner {
|
32
|
+
owner = nown;
|
33
|
+
}
|
34
|
+
|
35
|
+
function registerAdmin(address regraddr) ifowner {
|
36
|
+
participantdb.registrars[regraddr] = true;
|
37
|
+
AddParticipant(regraddr, uint(ParticipantTypes.Admin));
|
38
|
+
}
|
39
|
+
|
40
|
+
function unregisterAdmin(address regraddr) ifowner {
|
41
|
+
participantdb.registrars[regraddr] = false;
|
42
|
+
RemoveParticipant(regraddr, uint(ParticipantTypes.Admin));
|
43
|
+
}
|
44
|
+
|
45
|
+
function registerVendor(address vendoraddress) ifregistrar {
|
46
|
+
participantdb.vendors[vendoraddress] = true;
|
47
|
+
AddParticipant(vendoraddress, uint(ParticipantTypes.Vendor));
|
48
|
+
}
|
49
|
+
|
50
|
+
function unregisterVendor(address vendoraddress) ifregistrar {
|
51
|
+
participantdb.vendors[vendoraddress] = false;
|
52
|
+
RemoveParticipant(vendoraddress, uint(ParticipantTypes.Vendor));
|
53
|
+
}
|
54
|
+
|
55
|
+
function registerCustodian(address custodianaddress) ifregistrar {
|
56
|
+
participantdb.custodians[custodianaddress] = true;
|
57
|
+
AddParticipant(custodianaddress, uint(ParticipantTypes.Custodian));
|
58
|
+
}
|
59
|
+
|
60
|
+
function unregisterCustodian(address custodianaddress) ifregistrar {
|
61
|
+
participantdb.custodians[custodianaddress] = false;
|
62
|
+
RemoveParticipant(custodianaddress, uint(ParticipantTypes.Custodian));
|
63
|
+
}
|
64
|
+
|
65
|
+
function registerAuditor(address auditoraddress) ifregistrar {
|
66
|
+
participantdb.auditors[auditoraddress] = true;
|
67
|
+
AddParticipant(auditoraddress, uint(ParticipantTypes.Auditor));
|
68
|
+
}
|
69
|
+
|
70
|
+
function unregisterAuditor(address auditoraddress) ifregistrar {
|
71
|
+
participantdb.auditors[auditoraddress] = false;
|
72
|
+
RemoveParticipant(auditoraddress, uint(ParticipantTypes.Auditor));
|
73
|
+
}
|
74
|
+
|
75
|
+
function isVendor(address vendoraddress) returns (bool) {
|
76
|
+
return participantdb.vendors[vendoraddress];
|
77
|
+
}
|
78
|
+
|
79
|
+
function isCustodian(address custodianaddress) returns (bool) {
|
80
|
+
return participantdb.custodians[custodianaddress];
|
81
|
+
}
|
82
|
+
|
83
|
+
function isAuditor(address auditoraddress) returns (bool) {
|
84
|
+
return participantdb.auditors[auditoraddress];
|
85
|
+
}
|
86
|
+
|
87
|
+
function isRegistrar(address regraddr) returns (bool) {
|
88
|
+
return participantdb.registrars[regraddr];
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
|
93
|
+
|
94
|
+
//#include [ParticipantRegistry]
|
@@ -0,0 +1,54 @@
|
|
1
|
+
////////////////////////////////////////////////////////////
|
2
|
+
// This is an example contract hacked together at a meetup.
|
3
|
+
// It is by far not complete and only used to show some
|
4
|
+
// features of Solidity.
|
5
|
+
////////////////////////////////////////////////////////////
|
6
|
+
contract QueueContract
|
7
|
+
{
|
8
|
+
struct Queue {
|
9
|
+
uint[] data;
|
10
|
+
uint front;
|
11
|
+
uint back;
|
12
|
+
}
|
13
|
+
/// @dev the number of elements stored in the queue.
|
14
|
+
function length(Queue storage q) constant internal returns (uint) {
|
15
|
+
return q.back - q.front;
|
16
|
+
}
|
17
|
+
/// @dev the number of elements this queue can hold
|
18
|
+
function capacity(Queue storage q) constant internal returns (uint) {
|
19
|
+
return q.data.length - 1;
|
20
|
+
}
|
21
|
+
/// @dev push a new element to the back of the queue
|
22
|
+
function push(Queue storage q, uint data) internal
|
23
|
+
{
|
24
|
+
if ((q.back + 1) % q.data.length == q.front)
|
25
|
+
return; // throw;
|
26
|
+
q.data[q.back] = data;
|
27
|
+
q.back = (q.back + 1) % q.data.length;
|
28
|
+
}
|
29
|
+
/// @dev remove and return the element at the front of the queue
|
30
|
+
function pop(Queue storage q) internal returns (uint r)
|
31
|
+
{
|
32
|
+
if (q.back == q.front)
|
33
|
+
return; // throw;
|
34
|
+
r = q.data[q.front];
|
35
|
+
delete q.data[q.front];
|
36
|
+
q.front = (q.front + 1) % q.data.length;
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
contract QueueUserMayBeDeliveryDroneCotnrol is QueueContract {
|
41
|
+
Queue requests;
|
42
|
+
function QueueUserMayBeDeliveryDroneCotnrol() {
|
43
|
+
requests.data.length = 200;
|
44
|
+
}
|
45
|
+
function addRequest(uint d) {
|
46
|
+
push(requests, d);
|
47
|
+
}
|
48
|
+
function popRequest() returns (uint) {
|
49
|
+
return pop(requests);
|
50
|
+
}
|
51
|
+
function queueLength() returns (uint) {
|
52
|
+
return length(requests);
|
53
|
+
}
|
54
|
+
}
|
data/evm_client.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'evm_client/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "evm_client"
|
8
|
+
spec.version = EvmClient::VERSION
|
9
|
+
spec.authors = ["shideneyu"]
|
10
|
+
spec.email = ["shideneyu@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Ruby Ethereum client using the JSON-RPC interface}
|
13
|
+
spec.description = %q{EvmClient is Ruby Ethereum client using the JSON-RPC interface. Provides interface for sending transactions, creating and interacting with contracts as well as usefull toolkit to work with Ethereum node.}
|
14
|
+
spec.homepage = "https://github.com/shideneyu/evm_client"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
if spec.respond_to?(:metadata)
|
18
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
19
|
+
else
|
20
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
21
|
+
end
|
22
|
+
|
23
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
+
spec.bindir = "exe"
|
25
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ["lib", "bin"]
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
29
|
+
spec.add_development_dependency "rake", "~> 12.0"
|
30
|
+
spec.add_development_dependency "rspec", "~> 3.5"
|
31
|
+
spec.add_development_dependency "pry", "~> 0.10"
|
32
|
+
spec.add_development_dependency "eth", "~> 0.4"
|
33
|
+
|
34
|
+
spec.add_dependency "activesupport", ">= 4.0"
|
35
|
+
spec.add_dependency "digest-sha3", "~> 1.1"
|
36
|
+
end
|
data/lib/evm_client.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative 'evm_client/version'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_support/core_ext'
|
4
|
+
require 'digest/sha3'
|
5
|
+
require 'pry'
|
6
|
+
|
7
|
+
module EvmClient
|
8
|
+
ruby_project_files = Dir[File.join(File.dirname(__FILE__), '**', '*.rb')]
|
9
|
+
|
10
|
+
ruby_project_files.sort_by!{ |file_name| file_name.downcase }.each do |path|
|
11
|
+
require_relative path unless path.match?(/rail/)
|
12
|
+
end
|
13
|
+
|
14
|
+
require_relative 'ethereum/railtie' if defined?(Rails)
|
15
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module EvmClient
|
2
|
+
class Abi
|
3
|
+
|
4
|
+
def self.parse_abi(abi)
|
5
|
+
constructor = abi.detect {|x| x["type"] == "constructor"}
|
6
|
+
if constructor.present?
|
7
|
+
constructor_inputs = constructor["inputs"].map { |input| EvmClient::FunctionInput.new(input) }
|
8
|
+
else
|
9
|
+
constructor_inputs = []
|
10
|
+
end
|
11
|
+
functions = abi.select {|x| x["type"] == "function" }.map { |fun| EvmClient::Function.new(fun) }
|
12
|
+
events = abi.select {|x| x["type"] == "event" }.map { |evt| EvmClient::ContractEvent.new(evt) }
|
13
|
+
[constructor_inputs, functions, events]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.parse_type(type)
|
17
|
+
raise NotImplementedError if type.ends_with?("]")
|
18
|
+
match = /(\D+)(\d.*)?/.match(type)
|
19
|
+
[match[1], match[2]]
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.parse_array_type(type)
|
23
|
+
match = /(.+)\[(\d*)\]\z/.match(type)
|
24
|
+
if match
|
25
|
+
[true, match[2].present? ? match[2].to_i : nil, match[1]]
|
26
|
+
else
|
27
|
+
[false, nil, nil]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module EvmClient
|
2
|
+
class Client
|
3
|
+
|
4
|
+
DEFAULT_GAS_LIMIT = 4_000_000.freeze
|
5
|
+
DEFAULT_GAS_PRICE = 22_000_000_000.freeze
|
6
|
+
DEFAULT_BLOCK_NUMBER = 'latest'.freeze
|
7
|
+
|
8
|
+
# https://github.com/ethereum/wiki/wiki/JSON-RPC
|
9
|
+
RPC_COMMANDS = %w(web3_clientVersion web3_sha3 net_version net_peerCount net_listening eth_protocolVersion eth_syncing eth_coinbase eth_mining eth_hashrate eth_gasPrice eth_accounts eth_blockNumber eth_getBalance eth_getStorageAt eth_getTransactionCount eth_getBlockTransactionCountByHash eth_getBlockTransactionCountByNumber eth_getUncleCountByBlockHash eth_getUncleCountByBlockNumber eth_getCode eth_sign eth_sendTransaction eth_sendRawTransaction eth_call eth_estimateGas eth_getBlockByHash eth_getBlockByNumber eth_getTransactionByHash eth_getTransactionByBlockHashAndIndex eth_getTransactionByBlockNumberAndIndex eth_getTransactionReceipt eth_getUncleByBlockHashAndIndex eth_getUncleByBlockNumberAndIndex eth_getCompilers eth_compileLLL eth_compileSolidity eth_compileSerpent eth_newFilter eth_newBlockFilter eth_newPendingTransactionFilter eth_uninstallFilter eth_getFilterChanges eth_getFilterLogs eth_getLogs eth_getWork eth_submitWork eth_submitHashrate db_putString db_getString db_putHex db_getHex shh_post shh_version shh_newIdentity shh_hasIdentity shh_newGroup shh_addToGroup shh_newFilter shh_uninstallFilter shh_getFilterChanges shh_getMessages)
|
10
|
+
|
11
|
+
# https://github.com/ethereum/go-ethereum/wiki/Management-APIs
|
12
|
+
RPC_MANAGEMENT_COMMANDS = %w(admin_addPeer admin_datadir admin_nodeInfo admin_peers admin_setSolc admin_startRPC admin_startWS admin_stopRPC admin_stopWS debug_backtraceAt debug_blockProfile debug_cpuProfile debug_dumpBlock debug_gcStats debug_getBlockRlp debug_goTrace debug_memStats debug_seedHash debug_setHead debug_setBlockProfileRate debug_stacks debug_startCPUProfile debug_startGoTrace debug_stopCPUProfile debug_stopGoTrace debug_traceBlock debug_traceBlockByNumber debug_traceBlockByHash debug_traceBlockFromFile debug_traceTransaction debug_verbosity debug_vmodule debug_writeBlockProfile debug_writeMemProfile miner_hashrate miner_makeDAG miner_setExtra miner_setGasPrice miner_start miner_startAutoDAG miner_stop miner_stopAutoDAG personal_importRawKey personal_listAccounts personal_lockAccount personal_newAccount personal_unlockAccount personal_sendTransaction txpool_content txpool_inspect txpool_status)
|
13
|
+
|
14
|
+
attr_accessor :command, :id, :log, :logger, :default_account, :gas_price, :gas_limit, :block_number
|
15
|
+
|
16
|
+
def initialize(log = false)
|
17
|
+
@id = 0
|
18
|
+
@log = log
|
19
|
+
@batch = nil
|
20
|
+
@formatter = ::EvmClient::Formatter.new
|
21
|
+
@gas_price = DEFAULT_GAS_PRICE
|
22
|
+
@gas_limit = DEFAULT_GAS_LIMIT
|
23
|
+
@block_number = DEFAULT_BLOCK_NUMBER
|
24
|
+
|
25
|
+
if @log == true
|
26
|
+
@logger = Logger.new("/tmp/ethereum_ruby_http.log")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.create(host_or_ipcpath, log = false)
|
31
|
+
return IpcClient.new(host_or_ipcpath, log) if host_or_ipcpath.end_with? '.ipc'
|
32
|
+
return HttpClient.new(host_or_ipcpath, log) if host_or_ipcpath.start_with? 'http'
|
33
|
+
raise ArgumentError.new('Unable to detect client type')
|
34
|
+
end
|
35
|
+
|
36
|
+
def batch
|
37
|
+
@batch = []
|
38
|
+
|
39
|
+
yield
|
40
|
+
result = send_batch(@batch)
|
41
|
+
|
42
|
+
@batch = nil
|
43
|
+
reset_id
|
44
|
+
|
45
|
+
return result
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_id
|
49
|
+
@id += 1
|
50
|
+
return @id
|
51
|
+
end
|
52
|
+
|
53
|
+
def reset_id
|
54
|
+
@id = 0
|
55
|
+
end
|
56
|
+
|
57
|
+
def default_account
|
58
|
+
@default_account ||= eth_accounts["result"][0]
|
59
|
+
end
|
60
|
+
|
61
|
+
def int_to_hex(p)
|
62
|
+
p.is_a?(Integer) ? "0x#{p.to_s(16)}" : p
|
63
|
+
end
|
64
|
+
|
65
|
+
def encode_params(params)
|
66
|
+
params.map(&method(:int_to_hex))
|
67
|
+
end
|
68
|
+
|
69
|
+
def get_balance(address)
|
70
|
+
eth_get_balance(address)["result"].to_i(16)
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_chain
|
74
|
+
@net_version ||= net_version["result"].to_i
|
75
|
+
end
|
76
|
+
|
77
|
+
def get_nonce(address)
|
78
|
+
eth_get_transaction_count(address, "pending")["result"].to_i(16)
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def transfer_to(address, amount)
|
83
|
+
eth_send_transaction({to: address, value: int_to_hex(amount)})
|
84
|
+
end
|
85
|
+
|
86
|
+
def transfer_to_and_wait(address, amount)
|
87
|
+
wait_for(transfer_to(address, amount)["result"])
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def transfer(key, address, amount)
|
92
|
+
Eth.configure { |c| c.chain_id = net_version["result"].to_i }
|
93
|
+
args = {
|
94
|
+
from: key.address,
|
95
|
+
to: address,
|
96
|
+
value: amount,
|
97
|
+
data: "",
|
98
|
+
nonce: get_nonce(key.address),
|
99
|
+
gas_limit: gas_limit,
|
100
|
+
gas_price: gas_price
|
101
|
+
}
|
102
|
+
tx = Eth::Tx.new(args)
|
103
|
+
tx.sign key
|
104
|
+
eth_send_raw_transaction(tx.hex)["result"]
|
105
|
+
end
|
106
|
+
|
107
|
+
def transfer_and_wait(key, address, amount)
|
108
|
+
return wait_for(transfer(key, address, amount))
|
109
|
+
end
|
110
|
+
|
111
|
+
def wait_for(tx)
|
112
|
+
transaction = EvmClient::Transaction.new(tx, self, "", [])
|
113
|
+
transaction.wait_for_miner
|
114
|
+
return transaction
|
115
|
+
end
|
116
|
+
|
117
|
+
def send_command(command,args)
|
118
|
+
if ["eth_getBalance", "eth_call"].include?(command)
|
119
|
+
args << block_number
|
120
|
+
end
|
121
|
+
|
122
|
+
payload = {jsonrpc: "2.0", method: command, params: encode_params(args), id: get_id}
|
123
|
+
@logger.info("Sending #{payload.to_json}") if @log
|
124
|
+
if @batch
|
125
|
+
@batch << payload
|
126
|
+
return true
|
127
|
+
else
|
128
|
+
output = JSON.parse(send_single(payload.to_json))
|
129
|
+
@logger.info("Received #{output.to_json}") if @log
|
130
|
+
reset_id
|
131
|
+
raise IOError, output["error"]["message"] if output["error"]
|
132
|
+
return output
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
(RPC_COMMANDS + RPC_MANAGEMENT_COMMANDS).each do |rpc_command|
|
137
|
+
method_name = rpc_command.underscore
|
138
|
+
define_method method_name do |*args|
|
139
|
+
send_command(rpc_command, args)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
@@ -0,0 +1,341 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
module EvmClient
|
3
|
+
class Contract
|
4
|
+
|
5
|
+
attr_reader :address
|
6
|
+
attr_accessor :key
|
7
|
+
attr_accessor :gas_limit, :gas_price, :nonce
|
8
|
+
attr_accessor :code, :name, :abi, :class_object, :sender, :deployment, :client
|
9
|
+
attr_accessor :events, :functions, :constructor_inputs
|
10
|
+
attr_accessor :call_raw_proxy, :call_proxy, :transact_proxy, :transact_and_wait_proxy
|
11
|
+
attr_accessor :new_filter_proxy, :get_filter_logs_proxy, :get_filter_change_proxy
|
12
|
+
|
13
|
+
def initialize(name, code, abi, client = EvmClient::Singleton.instance)
|
14
|
+
@name = name
|
15
|
+
@code = code
|
16
|
+
@abi = abi
|
17
|
+
@constructor_inputs, @functions, @events = EvmClient::Abi.parse_abi(abi)
|
18
|
+
@formatter = EvmClient::Formatter.new
|
19
|
+
@client = client
|
20
|
+
@sender = client.default_account
|
21
|
+
@encoder = Encoder.new
|
22
|
+
@decoder = Decoder.new
|
23
|
+
@gas_limit = @client.gas_limit
|
24
|
+
@gas_price = @client.gas_price
|
25
|
+
end
|
26
|
+
|
27
|
+
# Creates a contract wrapper.
|
28
|
+
# This method attempts to instantiate a contract object from a Solidity source file, from a Truffle
|
29
|
+
# artifacts file, or from explicit API and bytecode.
|
30
|
+
# - If *:file* is present, the method compiles it and looks up the contract factory at *:contract_index*
|
31
|
+
# (0 if not provided). *:abi* and *:code* are ignored.
|
32
|
+
# - If *:truffle* is present, the method looks up the Truffle artifacts data for *:name* and uses
|
33
|
+
# those data to build a contract instance.
|
34
|
+
# - Otherwise, the method uses *:name*, *:code*, and *:abi* to build the contract instance.
|
35
|
+
#
|
36
|
+
# @param opts [Hash] Options to the method.
|
37
|
+
# @option opts [String] :file Path to the Solidity source that contains the contract code.
|
38
|
+
# @option opts [EvmClient::Singleton] :client The client to use.
|
39
|
+
# @option opts [String] :code The hex representation of the contract's bytecode.
|
40
|
+
# @option opts [Array,String] :abi The contract's ABI; a string is assumed to contain a JSON representation
|
41
|
+
# of the ABI.
|
42
|
+
# @option opts [String] :address The contract's address; if not present and +:truffle+ is present,
|
43
|
+
# the method attempts to determine the address from the artifacts' +networks+ key and the client's
|
44
|
+
# network id.
|
45
|
+
# @option opts [String] :name The contract name.
|
46
|
+
# @option opts [Integer] :contract_index The index of the contract data in the compiled file.
|
47
|
+
# @option opts [Hash] :truffle If this parameter is present, the method uses Truffle information to
|
48
|
+
# generate the contract wrapper.
|
49
|
+
# - *:paths* An array of strings containing the list of paths where to look up Truffle artifacts files.
|
50
|
+
# See also {#find_truffle_artifacts}.
|
51
|
+
#
|
52
|
+
# @return [EvmClient::Contract] Returns a contract wrapper.
|
53
|
+
|
54
|
+
def self.create(file: nil, client: EvmClient::Singleton.instance, code: nil, abi: nil, address: nil, name: nil, contract_index: nil, truffle: nil)
|
55
|
+
contract = nil
|
56
|
+
if file.present?
|
57
|
+
contracts = EvmClient::Initializer.new(file, client).build_all
|
58
|
+
raise "No contracts compiled" if contracts.empty?
|
59
|
+
if contract_index
|
60
|
+
contract = contracts[contract_index].class_object.new
|
61
|
+
else
|
62
|
+
contract = contracts.first.class_object.new
|
63
|
+
end
|
64
|
+
else
|
65
|
+
if truffle.present? && truffle.is_a?(Hash)
|
66
|
+
artifacts = find_truffle_artifacts(name, (truffle[:paths].is_a?(Array)) ? truffle[:paths] : [])
|
67
|
+
if artifacts
|
68
|
+
abi = artifacts['abi']
|
69
|
+
# The truffle artifacts store bytecodes with a 0x tag, which we need to remove
|
70
|
+
# this may need to be 'deployedBytecode'
|
71
|
+
code_key = artifacts['bytecode'].present? ? 'bytecode' : 'unlinked_binary'
|
72
|
+
code = (artifacts[code_key].start_with?('0x')) ? artifacts[code_key][2, artifacts[code_key].length] : artifacts[code_key]
|
73
|
+
unless address
|
74
|
+
address = if client
|
75
|
+
network_id = client.net_version['result']
|
76
|
+
(artifacts['networks'][network_id]) ? artifacts['networks'][network_id]['address'] : nil
|
77
|
+
else
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
else
|
82
|
+
abi = nil
|
83
|
+
code = nil
|
84
|
+
end
|
85
|
+
else
|
86
|
+
abi = abi.is_a?(String) ? JSON.parse(abi) : abi.map(&:deep_stringify_keys)
|
87
|
+
end
|
88
|
+
contract = EvmClient::Contract.new(name, code, abi, client)
|
89
|
+
contract.build
|
90
|
+
contract = contract.class_object.new
|
91
|
+
end
|
92
|
+
contract.address = address
|
93
|
+
contract
|
94
|
+
end
|
95
|
+
|
96
|
+
def address=(addr)
|
97
|
+
@address = addr
|
98
|
+
@events.each do |event|
|
99
|
+
event.set_address(addr)
|
100
|
+
event.set_client(@client)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def deploy_payload(params)
|
105
|
+
if @constructor_inputs.present?
|
106
|
+
raise ArgumentError, "Wrong number of arguments in a constructor" and return if params.length != @constructor_inputs.length
|
107
|
+
end
|
108
|
+
deploy_arguments = @encoder.encode_arguments(@constructor_inputs, params)
|
109
|
+
"0x" + @code + deploy_arguments
|
110
|
+
end
|
111
|
+
|
112
|
+
def deploy_args(params)
|
113
|
+
add_gas_options_args({from: sender, data: deploy_payload(params)})
|
114
|
+
end
|
115
|
+
|
116
|
+
def send_transaction(tx_args)
|
117
|
+
@client.eth_send_transaction(tx_args)["result"]
|
118
|
+
end
|
119
|
+
|
120
|
+
def send_raw_transaction(payload, to = nil)
|
121
|
+
Eth.configure { |c| c.chain_id = @client.net_version["result"].to_i }
|
122
|
+
@nonce ||= @client.get_nonce(key.address)
|
123
|
+
args = {
|
124
|
+
from: key.address,
|
125
|
+
value: 0,
|
126
|
+
data: payload,
|
127
|
+
nonce: @nonce,
|
128
|
+
gas_limit: gas_limit,
|
129
|
+
gas_price: gas_price
|
130
|
+
}
|
131
|
+
args[:to] = to if to
|
132
|
+
tx = Eth::Tx.new(args)
|
133
|
+
tx.sign key
|
134
|
+
@client.eth_send_raw_transaction(tx.hex)["result"]
|
135
|
+
end
|
136
|
+
|
137
|
+
def deploy(*params)
|
138
|
+
if key
|
139
|
+
tx = send_raw_transaction(deploy_payload(params))
|
140
|
+
else
|
141
|
+
tx = send_transaction(deploy_args(params))
|
142
|
+
end
|
143
|
+
tx_failed = tx.nil? || tx == "0x0000000000000000000000000000000000000000000000000000000000000000"
|
144
|
+
raise IOError, "Failed to deploy, did you unlock #{sender} account? Transaction hash: #{tx}" if tx_failed
|
145
|
+
@deployment = EvmClient::Deployment.new(tx, @client)
|
146
|
+
end
|
147
|
+
|
148
|
+
def deploy_and_wait(*params, **args, &block)
|
149
|
+
deploy(*params)
|
150
|
+
@deployment.wait_for_deployment(**args, &block)
|
151
|
+
self.events.each do |event|
|
152
|
+
event.set_address(@address)
|
153
|
+
event.set_client(@client)
|
154
|
+
end
|
155
|
+
@address = @deployment.contract_address
|
156
|
+
end
|
157
|
+
|
158
|
+
def estimate(*params)
|
159
|
+
result = @client.eth_estimate_gas(deploy_args(params))
|
160
|
+
@decoder.decode_int(result["result"])
|
161
|
+
end
|
162
|
+
|
163
|
+
def call_payload(fun, args)
|
164
|
+
"0x" + fun.minified_signature + (@encoder.encode_arguments(fun.inputs, args))
|
165
|
+
end
|
166
|
+
|
167
|
+
def call_args(fun, args)
|
168
|
+
options = {to: @address, from: @sender, data: call_payload(fun, args)}
|
169
|
+
|
170
|
+
fun.constant ? options : add_gas_options_args(options)
|
171
|
+
end
|
172
|
+
|
173
|
+
def call_raw(fun, *args)
|
174
|
+
raw_result = @client.eth_call(call_args(fun, args))["result"]
|
175
|
+
output = @decoder.decode_arguments(fun.outputs, raw_result)
|
176
|
+
return {data: call_payload(fun, args), raw: raw_result, formatted: output}
|
177
|
+
end
|
178
|
+
|
179
|
+
def call(fun, *args)
|
180
|
+
output = call_raw(fun, *args)[:formatted]
|
181
|
+
if output.length == 1
|
182
|
+
return output[0]
|
183
|
+
else
|
184
|
+
return output
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def transact(fun, *args)
|
189
|
+
if key
|
190
|
+
tx = send_raw_transaction(call_payload(fun, args), address)
|
191
|
+
else
|
192
|
+
tx = send_transaction(call_args(fun, args))
|
193
|
+
end
|
194
|
+
return EvmClient::Transaction.new(tx, @client, call_payload(fun, args), args)
|
195
|
+
end
|
196
|
+
|
197
|
+
def transact_and_wait(fun, *args)
|
198
|
+
tx = transact(fun, *args)
|
199
|
+
tx.wait_for_miner
|
200
|
+
return tx
|
201
|
+
end
|
202
|
+
|
203
|
+
def create_filter(evt, **params)
|
204
|
+
params[:to_block] ||= "latest"
|
205
|
+
params[:from_block] ||= "0x0"
|
206
|
+
params[:address] ||= @address
|
207
|
+
params[:topics] = @encoder.ensure_prefix(evt.signature)
|
208
|
+
payload = {topics: [params[:topics]], fromBlock: params[:from_block], toBlock: params[:to_block], address: @encoder.ensure_prefix(params[:address])}
|
209
|
+
filter_id = @client.eth_new_filter(payload)
|
210
|
+
return @decoder.decode_int(filter_id["result"])
|
211
|
+
end
|
212
|
+
|
213
|
+
def parse_filter_data(evt, logs)
|
214
|
+
formatter = EvmClient::Formatter.new
|
215
|
+
collection = []
|
216
|
+
logs["result"].each do |result|
|
217
|
+
inputs = evt.input_types
|
218
|
+
outputs = inputs.zip(result["topics"][1..-1])
|
219
|
+
data = {blockNumber: result["blockNumber"].hex, transactionHash: result["transactionHash"], blockHash: result["blockHash"], transactionIndex: result["transactionIndex"].hex, topics: []}
|
220
|
+
outputs.each do |output|
|
221
|
+
data[:topics] << formatter.from_payload(output)
|
222
|
+
end
|
223
|
+
collection << data
|
224
|
+
end
|
225
|
+
return collection
|
226
|
+
end
|
227
|
+
|
228
|
+
def get_filter_logs(evt, filter_id)
|
229
|
+
parse_filter_data evt, @client.eth_get_filter_logs(filter_id)
|
230
|
+
end
|
231
|
+
|
232
|
+
def get_filter_changes(evt, filter_id)
|
233
|
+
parse_filter_data evt, @client.eth_get_filter_changes(filter_id)
|
234
|
+
end
|
235
|
+
|
236
|
+
def function_name(fun)
|
237
|
+
count = functions.select {|x| x.name == fun.name }.count
|
238
|
+
name = (count == 1) ? "#{fun.name.underscore}" : "#{fun.name.underscore}__#{fun.inputs.collect {|x| x.type}.join("__")}"
|
239
|
+
name.to_sym
|
240
|
+
end
|
241
|
+
|
242
|
+
def build
|
243
|
+
class_name = @name.camelize
|
244
|
+
parent = self
|
245
|
+
create_function_proxies
|
246
|
+
create_event_proxies
|
247
|
+
class_methods = Class.new do
|
248
|
+
extend Forwardable
|
249
|
+
def_delegators :parent, :deploy_payload, :deploy_args, :call_payload, :call_args, :functions
|
250
|
+
def_delegators :parent, :signed_deploy, :key, :key=
|
251
|
+
def_delegators :parent, :gas_limit, :gas_price, :gas_limit=, :gas_price=, :nonce, :nonce=
|
252
|
+
def_delegators :parent, :abi, :deployment, :events, :name
|
253
|
+
def_delegators :parent, :estimate, :deploy, :deploy_and_wait
|
254
|
+
def_delegators :parent, :address, :address=, :sender, :sender=
|
255
|
+
def_delegator :parent, :call_raw_proxy, :call_raw
|
256
|
+
def_delegator :parent, :call_proxy, :call
|
257
|
+
def_delegator :parent, :transact_proxy, :transact
|
258
|
+
def_delegator :parent, :transact_and_wait_proxy, :transact_and_wait
|
259
|
+
def_delegator :parent, :new_filter_proxy, :new_filter
|
260
|
+
def_delegator :parent, :get_filter_logs_proxy, :get_filter_logs
|
261
|
+
def_delegator :parent, :get_filter_change_proxy, :get_filter_changes
|
262
|
+
define_method :parent do
|
263
|
+
parent
|
264
|
+
end
|
265
|
+
end
|
266
|
+
EvmClient::Contract.send(:remove_const, class_name) if EvmClient::Contract.const_defined?(class_name, false)
|
267
|
+
EvmClient::Contract.const_set(class_name, class_methods)
|
268
|
+
@class_object = class_methods
|
269
|
+
end
|
270
|
+
|
271
|
+
# Get the list of paths where to look up Truffle artifacts files.
|
272
|
+
#
|
273
|
+
# @return [Array<String>] Returns the array containing the list of lookup paths.
|
274
|
+
|
275
|
+
def self.truffle_paths()
|
276
|
+
@truffle_paths = [] unless @truffle_paths
|
277
|
+
@truffle_paths
|
278
|
+
end
|
279
|
+
|
280
|
+
# Set the list of paths where to look up Truffle artifacts files.
|
281
|
+
#
|
282
|
+
# @param paths [Array<String>, nil] The array containing the list of lookup paths; a `nil` value is
|
283
|
+
# converted to the empty array.
|
284
|
+
|
285
|
+
def self.truffle_paths=(paths)
|
286
|
+
@truffle_paths = (paths.is_a?(Array)) ? paths : []
|
287
|
+
end
|
288
|
+
|
289
|
+
# Looks up and loads a Truffle artifacts file.
|
290
|
+
# This method iterates over the `truffle_path` elements, looking for an artifact file in the
|
291
|
+
# `build/contracts` subdirectory.
|
292
|
+
#
|
293
|
+
# @param name [String] The name of the contract whose artifacts to look up.
|
294
|
+
# @param paths [Array<String>] An additional list of paths to look up; this list, if present, is
|
295
|
+
# prepended to the `truffle_path`.
|
296
|
+
#
|
297
|
+
# @return [Hash,nil] Returns a hash containing the parsed JSON from the artifacts file; if no file
|
298
|
+
# was found, returns `nil`.
|
299
|
+
|
300
|
+
def self.find_truffle_artifacts(name, paths = [])
|
301
|
+
subpath = File.join('build', 'contracts', "#{name}.json")
|
302
|
+
|
303
|
+
found = paths.concat(truffle_paths).find { |p| File.file?(File.join(p, subpath)) }
|
304
|
+
if (found)
|
305
|
+
JSON.parse(IO.read(File.join(found, subpath)))
|
306
|
+
else
|
307
|
+
nil
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
private
|
312
|
+
def add_gas_options_args(args)
|
313
|
+
args[:gas] = @client.int_to_hex(gas_limit) if gas_limit.present?
|
314
|
+
args[:gasPrice] = @client.int_to_hex(gas_price) if gas_price.present?
|
315
|
+
args
|
316
|
+
end
|
317
|
+
|
318
|
+
def create_function_proxies
|
319
|
+
parent = self
|
320
|
+
call_raw_proxy, call_proxy, transact_proxy, transact_and_wait_proxy = Class.new, Class.new, Class.new, Class.new
|
321
|
+
@functions.each do |fun|
|
322
|
+
call_raw_proxy.send(:define_method, parent.function_name(fun)) { |*args| parent.call_raw(fun, *args) }
|
323
|
+
call_proxy.send(:define_method, parent.function_name(fun)) { |*args| parent.call(fun, *args) }
|
324
|
+
transact_proxy.send(:define_method, parent.function_name(fun)) { |*args| parent.transact(fun, *args) }
|
325
|
+
transact_and_wait_proxy.send(:define_method, parent.function_name(fun)) { |*args| parent.transact_and_wait(fun, *args) }
|
326
|
+
end
|
327
|
+
@call_raw_proxy, @call_proxy, @transact_proxy, @transact_and_wait_proxy = call_raw_proxy.allocate, call_proxy.allocate, transact_proxy.allocate, transact_and_wait_proxy.allocate
|
328
|
+
end
|
329
|
+
|
330
|
+
def create_event_proxies
|
331
|
+
parent = self
|
332
|
+
new_filter_proxy, get_filter_logs_proxy, get_filter_change_proxy = Class.new, Class.new, Class.new
|
333
|
+
events.each do |evt|
|
334
|
+
new_filter_proxy.send(:define_method, evt.name.underscore) { |*args| parent.create_filter(evt, *args) }
|
335
|
+
get_filter_logs_proxy.send(:define_method, evt.name.underscore) { |*args| parent.get_filter_logs(evt, *args) }
|
336
|
+
get_filter_change_proxy.send(:define_method, evt.name.underscore) { |*args| parent.get_filter_changes(evt, *args) }
|
337
|
+
end
|
338
|
+
@new_filter_proxy, @get_filter_logs_proxy, @get_filter_change_proxy = new_filter_proxy.allocate, get_filter_logs_proxy.allocate, get_filter_change_proxy.allocate
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|