appchain.rb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
+
@@ -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