appchain.rb 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/.gitignore +11 -0
- data/.gitmodules +6 -0
- data/.pryrc +4 -0
- data/.rspec +3 -0
- data/.rubocop.yml +103 -0
- data/.travis.yml +16 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +95 -0
- data/LICENSE.txt +21 -0
- data/README.md +88 -0
- data/Rakefile +6 -0
- data/appchain.gemspec +47 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/appchain.rb +19 -0
- data/lib/appchain/address.rb +26 -0
- data/lib/appchain/client.rb +17 -0
- data/lib/appchain/contract.rb +98 -0
- data/lib/appchain/http.rb +57 -0
- data/lib/appchain/protos/blockchain_pb.rb +110 -0
- data/lib/appchain/rpc.rb +78 -0
- data/lib/appchain/transaction.rb +47 -0
- data/lib/appchain/transaction_signer.rb +79 -0
- data/lib/appchain/utils.rb +75 -0
- data/lib/appchain/version.rb +5 -0
- data/lib/web3_eth/contract.rb +28 -0
- metadata +226 -0
data/Rakefile
ADDED
data/appchain.gemspec
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "appchain/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "appchain.rb"
|
8
|
+
spec.version = AppChain::VERSION
|
9
|
+
spec.authors = ["classicalliu"]
|
10
|
+
spec.email = ["classicalliu@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Ruby Nervos AppChain SDK}
|
13
|
+
spec.description = %q{Ruby Nervos AppChain SDK for signature and rpc call}
|
14
|
+
spec.homepage = "https://github.com/cryptape/appchain.rb"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
23
|
+
"public gem pushes."
|
24
|
+
end
|
25
|
+
|
26
|
+
# Specify which files should be added to the gem when it is released.
|
27
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
28
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
29
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
30
|
+
end
|
31
|
+
spec.bindir = "exe"
|
32
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
33
|
+
spec.require_paths = ["lib"]
|
34
|
+
|
35
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
36
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
37
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
38
|
+
spec.add_development_dependency "pry", "~> 0.11"
|
39
|
+
spec.add_development_dependency "awesome_print", "~> 1.8"
|
40
|
+
spec.add_development_dependency "rubocop", "~> 0.59"
|
41
|
+
|
42
|
+
spec.add_dependency "google-protobuf", "~> 3.6"
|
43
|
+
spec.add_dependency "ciri-crypto", "0.1.1"
|
44
|
+
spec.add_dependency "faraday", "~> 0.15.3"
|
45
|
+
spec.add_dependency "activesupport", "~> 5.2.1"
|
46
|
+
spec.add_dependency "web3-eth", "~> 0.2.16"
|
47
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "appchain"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
require "pry"
|
11
|
+
Pry.start
|
12
|
+
|
13
|
+
# require "irb"
|
14
|
+
# IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/appchain.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "appchain/version"
|
4
|
+
|
5
|
+
module AppChain
|
6
|
+
# Your code goes here...
|
7
|
+
require "appchain/protos/blockchain_pb"
|
8
|
+
|
9
|
+
require "web3_eth/contract"
|
10
|
+
|
11
|
+
require "appchain/address"
|
12
|
+
require "appchain/transaction"
|
13
|
+
require "appchain/transaction_signer"
|
14
|
+
require "appchain/utils"
|
15
|
+
require "appchain/http"
|
16
|
+
require "appchain/rpc"
|
17
|
+
require "appchain/client"
|
18
|
+
require "appchain/contract"
|
19
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppChain
|
4
|
+
class Address
|
5
|
+
# @param str [String]
|
6
|
+
def initialize(str)
|
7
|
+
@addr = Utils.remove_hex_prefix(str)
|
8
|
+
end
|
9
|
+
|
10
|
+
# get address with `0x` prefix
|
11
|
+
#
|
12
|
+
# @return [String] address hex string with `0x` prefix
|
13
|
+
def addr
|
14
|
+
Utils.add_hex_prefix(@addr)
|
15
|
+
end
|
16
|
+
|
17
|
+
# compare address is equal
|
18
|
+
#
|
19
|
+
# @param other [AppChain::Address]
|
20
|
+
def ==(other)
|
21
|
+
addr.casecmp(other.addr)
|
22
|
+
end
|
23
|
+
|
24
|
+
# TODO: valid? method that check address is valid?
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppChain
|
4
|
+
class Client
|
5
|
+
attr_reader :url, :rpc, :http, :contract
|
6
|
+
|
7
|
+
def initialize(url)
|
8
|
+
@url = url
|
9
|
+
@rpc = RPC.new(url)
|
10
|
+
@http = rpc.http
|
11
|
+
end
|
12
|
+
|
13
|
+
def contract_at(abi, address)
|
14
|
+
@contract = Contract.new(abi, url, address, rpc)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "web3/eth"
|
4
|
+
|
5
|
+
module AppChain
|
6
|
+
class Contract
|
7
|
+
include Web3::Eth::Abi::AbiCoder
|
8
|
+
|
9
|
+
attr_reader :url, :abi, :address, :rpc
|
10
|
+
|
11
|
+
# @param abi [String | Hash] json string or hash
|
12
|
+
# @param url [String] chain url
|
13
|
+
# @param address [String] contract address
|
14
|
+
# @param rpc [AppChain::RPC]
|
15
|
+
#
|
16
|
+
# @return [void]
|
17
|
+
def initialize(abi, url, address = nil, rpc = nil)
|
18
|
+
@url = url
|
19
|
+
@abi = abi
|
20
|
+
@address = address
|
21
|
+
@rpc = rpc
|
22
|
+
parse_url
|
23
|
+
end
|
24
|
+
|
25
|
+
# wrapper Web3::Eth abi encoder for encoded data
|
26
|
+
#
|
27
|
+
# @param method_name [Symbol | String] method name you call
|
28
|
+
# @param *params [Array] method params you call
|
29
|
+
#
|
30
|
+
# @return [String] hex data
|
31
|
+
def function_data(method_name, *params)
|
32
|
+
data, _output_types = function_data_with_ot(method_name, *params)
|
33
|
+
data
|
34
|
+
end
|
35
|
+
|
36
|
+
# call contract functions by rpc `call` method
|
37
|
+
#
|
38
|
+
# @param method [Symbol | String] the method name you call
|
39
|
+
# @param params [Array] the method params you call
|
40
|
+
# @param tx [Hash] see rpc `call` doc for more info
|
41
|
+
#
|
42
|
+
# @return [any]
|
43
|
+
def call_func(method:, params: [], tx: {})
|
44
|
+
data, output_types = function_data_with_ot(method, *params)
|
45
|
+
resp = @rpc.call_rpc(:call, params: [tx.merge(data: data, to: address), "latest"])
|
46
|
+
result = resp["result"]
|
47
|
+
|
48
|
+
data = [Utils.remove_hex_prefix(result)].pack("H*")
|
49
|
+
return if data.nil?
|
50
|
+
|
51
|
+
re = decode_abi output_types, data
|
52
|
+
re.length == 1 ? re.first : re
|
53
|
+
end
|
54
|
+
|
55
|
+
# call contract functions by sendRawTransaction
|
56
|
+
#
|
57
|
+
# @param tx [Hash | AppChain::Transaction]
|
58
|
+
# @param private_key [String] hex string
|
59
|
+
# @param method [Symbol | String] method name you call
|
60
|
+
# @param *params [Array] your params
|
61
|
+
#
|
62
|
+
# @return [nil | Hash] {hash: "", status: ""}, sendRawTransactionResult
|
63
|
+
def send_func(tx:, private_key:, method:, params: [])
|
64
|
+
data, _output_types = function_data_with_ot(method, *params)
|
65
|
+
transaction = if tx.is_a?(Hash)
|
66
|
+
Transaction.from_hash(tx)
|
67
|
+
else
|
68
|
+
tx
|
69
|
+
end
|
70
|
+
transaction.data = data
|
71
|
+
resp = @rpc.send_transaction(transaction, private_key)
|
72
|
+
|
73
|
+
resp&.dig("result")
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# parse url to host, port and scheme
|
79
|
+
def parse_url
|
80
|
+
uri = URI.parse(@url)
|
81
|
+
@host = uri.host
|
82
|
+
@port = uri.port
|
83
|
+
@scheme = uri.scheme
|
84
|
+
end
|
85
|
+
|
86
|
+
# is this url in https?
|
87
|
+
def https?
|
88
|
+
@scheme == "https"
|
89
|
+
end
|
90
|
+
|
91
|
+
# wrapper Web3::Eth abi encoder for encoded data
|
92
|
+
def function_data_with_ot(method_name, *params)
|
93
|
+
web3 = Web3::Eth::Rpc.new host: @host, port: @port, connect_options: { use_ssl: https? }
|
94
|
+
contract = web3.eth.contract(abi).at(address)
|
95
|
+
contract.function_data(method_name, *params)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "json"
|
3
|
+
require "faraday"
|
4
|
+
|
5
|
+
module AppChain
|
6
|
+
class Http
|
7
|
+
attr_accessor :url
|
8
|
+
|
9
|
+
DEFAULT_JSONRPC = "2.0"
|
10
|
+
DEFAULT_PARAMS = [].freeze
|
11
|
+
DEFAULT_ID = 83
|
12
|
+
|
13
|
+
def initialize(url)
|
14
|
+
@url = url
|
15
|
+
end
|
16
|
+
|
17
|
+
# wrapper for call rpc method
|
18
|
+
#
|
19
|
+
# @param method [String] method you want to call
|
20
|
+
# @param jsonrpc [String] jsonrpc version
|
21
|
+
# @param params [Array] rpc params
|
22
|
+
# @param id [Integer] jsonrpc id
|
23
|
+
#
|
24
|
+
# @return [Faraday::Response]
|
25
|
+
def call_rpc(method, jsonrpc: DEFAULT_JSONRPC, params: DEFAULT_PARAMS, id: DEFAULT_ID)
|
26
|
+
conn.post("/", rpc_params(method, jsonrpc: jsonrpc, params: params, id: id))
|
27
|
+
end
|
28
|
+
|
29
|
+
# wrapper for rpc params
|
30
|
+
#
|
31
|
+
# @param method [String] method you want to call
|
32
|
+
# @param jsonrpc [String] jsonrpc version
|
33
|
+
# @param params [Array] rpc params
|
34
|
+
# @param id [Integer] jsonrpc id
|
35
|
+
#
|
36
|
+
# @return [String] json string
|
37
|
+
def rpc_params(method, jsonrpc: DEFAULT_JSONRPC, params: DEFAULT_PARAMS, id: DEFAULT_ID)
|
38
|
+
{
|
39
|
+
jsonrpc: jsonrpc,
|
40
|
+
id: id,
|
41
|
+
method: method,
|
42
|
+
params: params
|
43
|
+
}.to_json
|
44
|
+
end
|
45
|
+
|
46
|
+
# wrapper faraday object with AppChain URL and Content-Type
|
47
|
+
#
|
48
|
+
# @return [Faraday]
|
49
|
+
def conn
|
50
|
+
Faraday.new(url: url) do |faraday|
|
51
|
+
faraday.headers["Content-Type"] = "application/json"
|
52
|
+
faraday.request :url_encoded # form-encode POST params
|
53
|
+
faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
2
|
+
# source: blockchain.proto
|
3
|
+
|
4
|
+
require 'google/protobuf'
|
5
|
+
|
6
|
+
Google::Protobuf::DescriptorPool.generated_pool.build do
|
7
|
+
add_message "Proof" do
|
8
|
+
optional :content, :bytes, 1
|
9
|
+
optional :type, :enum, 2, "ProofType"
|
10
|
+
end
|
11
|
+
add_message "BlockHeader" do
|
12
|
+
optional :prevhash, :bytes, 1
|
13
|
+
optional :timestamp, :uint64, 2
|
14
|
+
optional :height, :uint64, 3
|
15
|
+
optional :state_root, :bytes, 4
|
16
|
+
optional :transactions_root, :bytes, 5
|
17
|
+
optional :receipts_root, :bytes, 6
|
18
|
+
optional :gas_used, :uint64, 7
|
19
|
+
optional :gas_limit, :uint64, 8
|
20
|
+
optional :proof, :message, 9, "Proof"
|
21
|
+
optional :proposer, :bytes, 10
|
22
|
+
end
|
23
|
+
add_message "Status" do
|
24
|
+
optional :hash, :bytes, 1
|
25
|
+
optional :height, :uint64, 2
|
26
|
+
end
|
27
|
+
add_message "AccountGasLimit" do
|
28
|
+
optional :common_gas_limit, :uint64, 1
|
29
|
+
map :specific_gas_limit, :string, :uint64, 2
|
30
|
+
end
|
31
|
+
add_message "RichStatus" do
|
32
|
+
optional :hash, :bytes, 1
|
33
|
+
optional :height, :uint64, 2
|
34
|
+
repeated :nodes, :bytes, 3
|
35
|
+
optional :interval, :uint64, 4
|
36
|
+
optional :version, :uint32, 5
|
37
|
+
end
|
38
|
+
add_message "Transaction" do
|
39
|
+
optional :to, :string, 1
|
40
|
+
optional :nonce, :string, 2
|
41
|
+
optional :quota, :uint64, 3
|
42
|
+
optional :valid_until_block, :uint64, 4
|
43
|
+
optional :data, :bytes, 5
|
44
|
+
optional :value, :bytes, 6
|
45
|
+
optional :chain_id, :uint32, 7
|
46
|
+
optional :version, :uint32, 8
|
47
|
+
end
|
48
|
+
add_message "UnverifiedTransaction" do
|
49
|
+
optional :transaction, :message, 1, "Transaction"
|
50
|
+
optional :signature, :bytes, 2
|
51
|
+
optional :crypto, :enum, 3, "Crypto"
|
52
|
+
end
|
53
|
+
add_message "SignedTransaction" do
|
54
|
+
optional :transaction_with_sig, :message, 1, "UnverifiedTransaction"
|
55
|
+
optional :tx_hash, :bytes, 2
|
56
|
+
optional :signer, :bytes, 3
|
57
|
+
end
|
58
|
+
add_message "BlockBody" do
|
59
|
+
repeated :transactions, :message, 1, "SignedTransaction"
|
60
|
+
end
|
61
|
+
add_message "Block" do
|
62
|
+
optional :version, :uint32, 1
|
63
|
+
optional :header, :message, 2, "BlockHeader"
|
64
|
+
optional :body, :message, 3, "BlockBody"
|
65
|
+
end
|
66
|
+
add_message "BlockWithProof" do
|
67
|
+
optional :blk, :message, 1, "Block"
|
68
|
+
optional :proof, :message, 2, "Proof"
|
69
|
+
end
|
70
|
+
add_message "BlockTxs" do
|
71
|
+
optional :height, :uint64, 1
|
72
|
+
optional :body, :message, 3, "BlockBody"
|
73
|
+
end
|
74
|
+
add_message "BlackList" do
|
75
|
+
repeated :black_list, :bytes, 1
|
76
|
+
repeated :clear_list, :bytes, 2
|
77
|
+
end
|
78
|
+
add_message "StateSignal" do
|
79
|
+
optional :height, :uint64, 1
|
80
|
+
end
|
81
|
+
add_enum "ProofType" do
|
82
|
+
value :AuthorityRound, 0
|
83
|
+
value :Raft, 1
|
84
|
+
value :Bft, 2
|
85
|
+
end
|
86
|
+
add_enum "Crypto" do
|
87
|
+
value :SECP, 0
|
88
|
+
value :SM2, 1
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
module AppChain::Protos
|
93
|
+
Proof = Google::Protobuf::DescriptorPool.generated_pool.lookup("Proof").msgclass
|
94
|
+
BlockHeader = Google::Protobuf::DescriptorPool.generated_pool.lookup("BlockHeader").msgclass
|
95
|
+
Status = Google::Protobuf::DescriptorPool.generated_pool.lookup("Status").msgclass
|
96
|
+
AccountGasLimit = Google::Protobuf::DescriptorPool.generated_pool.lookup("AccountGasLimit").msgclass
|
97
|
+
RichStatus = Google::Protobuf::DescriptorPool.generated_pool.lookup("RichStatus").msgclass
|
98
|
+
Transaction = Google::Protobuf::DescriptorPool.generated_pool.lookup("Transaction").msgclass
|
99
|
+
UnverifiedTransaction = Google::Protobuf::DescriptorPool.generated_pool.lookup("UnverifiedTransaction").msgclass
|
100
|
+
SignedTransaction = Google::Protobuf::DescriptorPool.generated_pool.lookup("SignedTransaction").msgclass
|
101
|
+
BlockBody = Google::Protobuf::DescriptorPool.generated_pool.lookup("BlockBody").msgclass
|
102
|
+
Block = Google::Protobuf::DescriptorPool.generated_pool.lookup("Block").msgclass
|
103
|
+
BlockWithProof = Google::Protobuf::DescriptorPool.generated_pool.lookup("BlockWithProof").msgclass
|
104
|
+
BlockTxs = Google::Protobuf::DescriptorPool.generated_pool.lookup("BlockTxs").msgclass
|
105
|
+
BlackList = Google::Protobuf::DescriptorPool.generated_pool.lookup("BlackList").msgclass
|
106
|
+
StateSignal = Google::Protobuf::DescriptorPool.generated_pool.lookup("StateSignal").msgclass
|
107
|
+
ProofType = Google::Protobuf::DescriptorPool.generated_pool.lookup("ProofType").enummodule
|
108
|
+
Crypto = Google::Protobuf::DescriptorPool.generated_pool.lookup("Crypto").enummodule
|
109
|
+
end
|
110
|
+
|
data/lib/appchain/rpc.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "active_support/inflector"
|
3
|
+
|
4
|
+
module AppChain
|
5
|
+
class RPC
|
6
|
+
attr_reader :url, :http
|
7
|
+
|
8
|
+
# CITA v0.18 RPC list
|
9
|
+
METHOD_NAMES = %w(
|
10
|
+
peerCount
|
11
|
+
blockNumber
|
12
|
+
sendRawTransaction
|
13
|
+
getBlockByHash
|
14
|
+
getBlockByNumber
|
15
|
+
getTransaction
|
16
|
+
getTransactionReceipt
|
17
|
+
getLogs
|
18
|
+
call
|
19
|
+
getTransactionCount
|
20
|
+
getCode
|
21
|
+
getAbi
|
22
|
+
getBalance
|
23
|
+
newFilter
|
24
|
+
newBlockFilter
|
25
|
+
uninstallFilter
|
26
|
+
getFilterChanges
|
27
|
+
getFilterLogs
|
28
|
+
getTransactionProof
|
29
|
+
getMetaData
|
30
|
+
getBlockHeader
|
31
|
+
getStateProof
|
32
|
+
).freeze
|
33
|
+
|
34
|
+
def initialize(url)
|
35
|
+
@url = url
|
36
|
+
@http = Http.new(@url)
|
37
|
+
end
|
38
|
+
|
39
|
+
# generate rpc methods
|
40
|
+
METHOD_NAMES.each do |name|
|
41
|
+
define_method name do |*params|
|
42
|
+
call_rpc(name, params: params)
|
43
|
+
end
|
44
|
+
|
45
|
+
define_method name.underscore do |*params|
|
46
|
+
send(name, *params)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Hash] response body
|
51
|
+
def call_rpc(method, jsonrpc: Http::DEFAULT_JSONRPC, params: Http::DEFAULT_PARAMS, id: Http::DEFAULT_ID)
|
52
|
+
resp = http.call_rpc(method, params: params, jsonrpc: jsonrpc, id: id)
|
53
|
+
JSON.parse(resp.body)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param transaction [AppChain::Transaction]
|
57
|
+
# @return [Hash]
|
58
|
+
def send_transaction(transaction, private_key)
|
59
|
+
content = TransactionSigner.encode(transaction, private_key)
|
60
|
+
send_raw_transaction(content)
|
61
|
+
end
|
62
|
+
|
63
|
+
# easy to transfer tokens
|
64
|
+
#
|
65
|
+
# @param to [String] to address
|
66
|
+
# @param private_key [String]
|
67
|
+
# @param value [String | Integer] hex string or decimal integer
|
68
|
+
# @param quota [Integer] default to 30_000
|
69
|
+
#
|
70
|
+
# @return [Hash]
|
71
|
+
def transfer(to:, private_key:, value:, quota: 30_000)
|
72
|
+
valid_until_block = block_number["result"].hex + 88
|
73
|
+
chain_id = get_meta_data("latest").dig "result", "chainId"
|
74
|
+
transaction = Transaction.new(nonce: Utils.nonce, valid_until_block: valid_until_block, chain_id: chain_id, to: to, value: value, quota: quota)
|
75
|
+
send_transaction(transaction, private_key)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|