appchain.rb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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