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.
- 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
|