hyperledger-fabric-sdk 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/lib/crypto_suite/ecdsa_aes.rb +86 -0
- data/lib/fabric/channel.rb +54 -0
- data/lib/fabric/client.rb +45 -0
- data/lib/fabric/configuration.rb +25 -0
- data/lib/fabric/constants.rb +18 -0
- data/lib/fabric/identity.rb +36 -0
- data/lib/fabric/orderer.rb +19 -0
- data/lib/fabric/peer.rb +58 -0
- data/lib/fabric/peer_endorser.rb +85 -0
- data/lib/fabric/transaction_id.rb +11 -0
- data/lib/fabric/user.rb +24 -0
- data/lib/fabric/version.rb +3 -0
- data/lib/fabric_ca/client.rb +51 -0
- data/lib/fabric_ca/configuration.rb +25 -0
- data/lib/fabric_ca/connection.rb +32 -0
- data/lib/fabric_ca/error.rb +11 -0
- data/lib/fabric_ca/faraday_middleware/basic_auth.rb +33 -0
- data/lib/fabric_ca/faraday_middleware/raise_http_exception.rb +47 -0
- data/lib/fabric_ca/faraday_middleware/token_auth.rb +38 -0
- data/lib/fabric_ca/request.rb +50 -0
- data/lib/fabric_ca/response.rb +10 -0
- data/lib/fabric_ca/tools.rb +35 -0
- data/lib/fabric_ca/version.rb +3 -0
- data/lib/hyperledger-fabric-sdk.rb +47 -0
- data/lib/protos/common/collection_pb.rb +42 -0
- data/lib/protos/common/common_pb.rb +106 -0
- data/lib/protos/common/configtx_pb.rb +71 -0
- data/lib/protos/common/configuration_pb.rb +33 -0
- data/lib/protos/common/ledger_pb.rb +16 -0
- data/lib/protos/common/policies_pb.rb +52 -0
- data/lib/protos/discovery/protocol_pb.rb +119 -0
- data/lib/protos/discovery/protocol_services_pb.rb +30 -0
- data/lib/protos/gossip/message_pb.rb +233 -0
- data/lib/protos/gossip/message_services_pb.rb +31 -0
- data/lib/protos/idemix/idemix_pb.rb +93 -0
- data/lib/protos/ledger/queryresult/kv_query_result_pb.rb +24 -0
- data/lib/protos/ledger/rwset/kvrwset/kv_rwset_pb.rb +85 -0
- data/lib/protos/ledger/rwset/rwset_pb.rb +46 -0
- data/lib/protos/msp/identities_pb.rb +23 -0
- data/lib/protos/msp/msp_config_pb.rb +72 -0
- data/lib/protos/msp/msp_principal_pb.rb +54 -0
- data/lib/protos/orderer/ab_pb.rb +52 -0
- data/lib/protos/orderer/ab_services_pb.rb +41 -0
- data/lib/protos/orderer/configuration_pb.rb +32 -0
- data/lib/protos/orderer/kafka_pb.rb +45 -0
- data/lib/protos/peer/admin_pb.rb +41 -0
- data/lib/protos/peer/admin_services_pb.rb +33 -0
- data/lib/protos/peer/chaincode_event_pb.rb +17 -0
- data/lib/protos/peer/chaincode_pb.rb +60 -0
- data/lib/protos/peer/chaincode_shim_pb.rb +94 -0
- data/lib/protos/peer/chaincode_shim_services_pb.rb +30 -0
- data/lib/protos/peer/configuration_pb.rb +27 -0
- data/lib/protos/peer/events_pb.rb +98 -0
- data/lib/protos/peer/events_services_pb.rb +59 -0
- data/lib/protos/peer/peer_pb.rb +21 -0
- data/lib/protos/peer/peer_services_pb.rb +37 -0
- data/lib/protos/peer/proposal_pb.rb +40 -0
- data/lib/protos/peer/proposal_response_pb.rb +35 -0
- data/lib/protos/peer/query_pb.rb +32 -0
- data/lib/protos/peer/resources_pb.rb +34 -0
- data/lib/protos/peer/signed_cc_dep_spec_pb.rb +17 -0
- data/lib/protos/peer/transaction_pb.rb +72 -0
- data/lib/protos/transientstore/transientstore_pb.rb +18 -0
- metadata +123 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8868d76ed70adc4cedf983ad30718b1964bdd4a4a06291bc336d7381bc152a74
|
4
|
+
data.tar.gz: 97809db15dee6a3f8ac534a63eb3933895e8f0fa0ebd255b10569124e5ff06c4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e4c1c49bdc093de69d552136954eacf30f0e92128222f5e878d760e71dd94ac08720ad832e8172014fb2363ce507963fe1099f1c7589878875c412704063de8f
|
7
|
+
data.tar.gz: e91abaeeee3053694b18362ab2fff01132267782ae6fdc0e01ed8bcc04540ff565fa70ae6bd5e4931068664f8ae51230008792e3a9cf1592c23ba5284bead9e0
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module CryptoSuite
|
4
|
+
class ECDSA_AES
|
5
|
+
DEFAULT_KEY_SIZE = 256.freeze
|
6
|
+
DEFAULT_NONCE_LENGTH = 24.freeze
|
7
|
+
DEFAULT_DIGEST_ALGORITHM = 'SHA256'.freeze
|
8
|
+
DEFAULT_AES_KEY_SIZE = 128.freeze
|
9
|
+
|
10
|
+
EC_CURVES = { 256 => 'prime256v1', 384 => 'secp384r1' }.freeze
|
11
|
+
|
12
|
+
attr_reader :key_size, :nonce_length, :digest_algorithm, :digest, :cipher
|
13
|
+
|
14
|
+
def initialize(opts = {})
|
15
|
+
@key_size = opts[:key_size] || DEFAULT_KEY_SIZE
|
16
|
+
@nonce_length = opts[:nonce_length] || DEFAULT_NONCE_LENGTH
|
17
|
+
@digest_algorithm = opts[:digest_algorithm] || DEFAULT_DIGEST_ALGORITHM
|
18
|
+
@digest = OpenSSL::Digest.new(digest_algorithm)
|
19
|
+
@cipher = OpenSSL::Cipher::AES.new(opts[:aes_key_size] || DEFAULT_AES_KEY_SIZE, :CBC)
|
20
|
+
end
|
21
|
+
|
22
|
+
def generate_nonce
|
23
|
+
OpenSSL::Random.random_bytes nonce_length
|
24
|
+
end
|
25
|
+
|
26
|
+
def hexdigest(message)
|
27
|
+
@digest.hexdigest message
|
28
|
+
end
|
29
|
+
|
30
|
+
def digest(message)
|
31
|
+
@digest.digest message
|
32
|
+
end
|
33
|
+
|
34
|
+
def sign(private_key, msg)
|
35
|
+
key = OpenSSL::PKey::EC.new private_key
|
36
|
+
signature = key.dsa_sign_asn1 msg
|
37
|
+
sequence = OpenSSL::ASN1.decode signature
|
38
|
+
sequence = prevent_malleability sequence, key.group.order
|
39
|
+
|
40
|
+
sequence.to_der
|
41
|
+
end
|
42
|
+
|
43
|
+
def generate_private_key
|
44
|
+
key = OpenSSL::PKey::EC.new EC_CURVES[key_size]
|
45
|
+
key.generate_key!
|
46
|
+
|
47
|
+
key.to_pem
|
48
|
+
end
|
49
|
+
|
50
|
+
def generate_csr(private_key, options = [])
|
51
|
+
key = OpenSSL::PKey::EC.new private_key
|
52
|
+
req = OpenSSL::X509::Request.new
|
53
|
+
req.public_key = key
|
54
|
+
req.subject = OpenSSL::X509::Name.new(options)
|
55
|
+
req.sign key, @digest
|
56
|
+
|
57
|
+
req
|
58
|
+
end
|
59
|
+
|
60
|
+
def encrypt(key, iv, message)
|
61
|
+
cipher.encrypt key, iv
|
62
|
+
|
63
|
+
Base64.strict_encode64(cipher.update(message) + cipher.final)
|
64
|
+
end
|
65
|
+
|
66
|
+
def decrypt(key, iv, message)
|
67
|
+
cipher.decrypt key, iv
|
68
|
+
|
69
|
+
cipher.update(Base64.strict_decode64(message)) + cipher.final
|
70
|
+
rescue OpenSSL::Cipher::CipherError
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def prevent_malleability(sequence, order)
|
77
|
+
half_order = order >> 1
|
78
|
+
|
79
|
+
if (half_key = sequence.value[1].value) > half_order
|
80
|
+
sequence.value[1].value = order - half_key
|
81
|
+
end
|
82
|
+
|
83
|
+
sequence
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'peer/transaction_pb'
|
2
|
+
|
3
|
+
module Fabric
|
4
|
+
class Channel
|
5
|
+
attr_reader :identity_context, :name, :peers, :orderers
|
6
|
+
|
7
|
+
def initialize(args)
|
8
|
+
@identity_context = args[:identity_context]
|
9
|
+
@name = args[:name]
|
10
|
+
@peers = args[:peers] || []
|
11
|
+
@orderers = args[:orderers] || []
|
12
|
+
end
|
13
|
+
|
14
|
+
def query_by_chaincode(request)
|
15
|
+
request[:targets] ||= peers
|
16
|
+
request[:transaction] = TransactionID.new identity_context
|
17
|
+
|
18
|
+
PeerEndorser.send_transaction_proposal request, name, identity_context
|
19
|
+
end
|
20
|
+
|
21
|
+
def send_transaction(request, &block)
|
22
|
+
header = Common::Header.decode request[:proposal].header
|
23
|
+
chaincode_action_payload = build_chaincode_action request
|
24
|
+
|
25
|
+
transaction_action = Protos::TransactionAction.new header: header.signature_header,
|
26
|
+
payload: chaincode_action_payload.to_proto
|
27
|
+
transaction = Protos::Transaction.new actions: [transaction_action]
|
28
|
+
|
29
|
+
payload = Common::Payload.new header: header,
|
30
|
+
data: transaction.to_proto
|
31
|
+
|
32
|
+
envelope = Common::Envelope.new signature: identity_context.identity.sign(payload.to_proto),
|
33
|
+
payload: payload.to_proto
|
34
|
+
|
35
|
+
orderers.first.send_broadcast envelope, &block
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def build_chaincode_action(request)
|
41
|
+
responses = request[:responses].select { |response| response.response.status == 200 }
|
42
|
+
endorsements = responses.map { |response| response.endorsement }
|
43
|
+
chaincode_endorser_action =
|
44
|
+
Protos::ChaincodeEndorsedAction.new proposal_response_payload: responses.first.payload,
|
45
|
+
endorsements: endorsements
|
46
|
+
|
47
|
+
payload = Protos::ChaincodeProposalPayload.decode request[:proposal].payload
|
48
|
+
payload_no_trans = Protos::ChaincodeProposalPayload.new input: payload.input
|
49
|
+
|
50
|
+
Protos::ChaincodeActionPayload.new action: chaincode_endorser_action,
|
51
|
+
chaincode_proposal_payload: payload_no_trans.to_proto
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Fabric
|
2
|
+
class Client
|
3
|
+
attr_reader :identity_context, :orderers, :peers, :channels
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
options = Fabric.options.merge(options)
|
7
|
+
|
8
|
+
@identity_context = options[:identity_context]
|
9
|
+
@orderers = {}
|
10
|
+
@channels = {}
|
11
|
+
@peers = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def config
|
15
|
+
conf = {}
|
16
|
+
Configuration::VALID_OPTIONS_KEYS.each do |key|
|
17
|
+
conf[key] = public_send key
|
18
|
+
end
|
19
|
+
|
20
|
+
conf
|
21
|
+
end
|
22
|
+
|
23
|
+
def new_channel(name)
|
24
|
+
channels[name] ||= Channel.new name: name,
|
25
|
+
orderers: orderers.values , peers: peers.values,
|
26
|
+
identity_context: identity_context
|
27
|
+
|
28
|
+
channels[name]
|
29
|
+
end
|
30
|
+
|
31
|
+
def new_peer(url, opts = {})
|
32
|
+
peers[url] ||= Peer.new url: url, opts: opts,
|
33
|
+
identity_context: identity_context
|
34
|
+
|
35
|
+
peers[url]
|
36
|
+
end
|
37
|
+
|
38
|
+
def new_orderer(url, opts = {})
|
39
|
+
orderers[url] ||= Orderer.new url: url, opts: opts,
|
40
|
+
identity_context: identity_context
|
41
|
+
|
42
|
+
orderers[url]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Fabric
|
2
|
+
module Configuration
|
3
|
+
VALID_OPTIONS_KEYS = [:identity_context, :orderers, :peers]
|
4
|
+
|
5
|
+
attr_accessor *VALID_OPTIONS_KEYS
|
6
|
+
|
7
|
+
def self.extended(base)
|
8
|
+
base.reset
|
9
|
+
end
|
10
|
+
|
11
|
+
def configure
|
12
|
+
yield self
|
13
|
+
end
|
14
|
+
|
15
|
+
def options
|
16
|
+
VALID_OPTIONS_KEYS.inject({}) do |option, key|
|
17
|
+
option.merge!(key => send(key))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def reset
|
22
|
+
VALID_OPTIONS_KEYS.each {|key| send("#{key}=", nil)}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Fabric
|
2
|
+
module Constants
|
3
|
+
## System Chaincodes
|
4
|
+
LSCC = 'lscc'.freeze
|
5
|
+
QSCC = 'qscc'.freeze
|
6
|
+
CSCC = 'cscc'.freeze
|
7
|
+
|
8
|
+
## System Channels
|
9
|
+
SYSTEM_CHANNEL_NAME = 'testchainid'.freeze
|
10
|
+
|
11
|
+
## System Functions
|
12
|
+
FUNC_GET_CHANNELS = 'GetChannels'.freeze
|
13
|
+
FUNC_GET_CONFIG_BLOCK = 'GetConfigBlock'.freeze
|
14
|
+
|
15
|
+
## Variables
|
16
|
+
CHANNEL_HEADER_VERSION = 1.freeze
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'msp/identities_pb'
|
2
|
+
|
3
|
+
module Fabric
|
4
|
+
class Identity
|
5
|
+
attr_reader :certificate, :public_key, :private_key, :msp_id, :crypto_suite,
|
6
|
+
:key, :iv
|
7
|
+
|
8
|
+
def initialize(args)
|
9
|
+
@certificate = args[:certificate]
|
10
|
+
@public_key = args[:public_key]
|
11
|
+
@private_key = args[:private_key]
|
12
|
+
@msp_id = args[:msp_id]
|
13
|
+
@crypto_suite = args[:crypto_suite]
|
14
|
+
@key = args[:key].to_s
|
15
|
+
@iv = args[:iv].to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def serialize
|
19
|
+
Msp::SerializedIdentity.new(mspid: msp_id, id_bytes: certificate).to_proto
|
20
|
+
end
|
21
|
+
|
22
|
+
def sign(message)
|
23
|
+
digest = crypto_suite.digest message
|
24
|
+
|
25
|
+
crypto_suite.sign private_key, digest
|
26
|
+
end
|
27
|
+
|
28
|
+
def encrypt(message)
|
29
|
+
crypto_suite.encrypt key, iv, message
|
30
|
+
end
|
31
|
+
|
32
|
+
def decrypt(message)
|
33
|
+
crypto_suite.decrypt key, iv, message
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'orderer/ab_services_pb.rb'
|
2
|
+
|
3
|
+
module Fabric
|
4
|
+
class Orderer
|
5
|
+
attr_reader :url, :opts, :client
|
6
|
+
|
7
|
+
def initialize(args)
|
8
|
+
@url = args[:url]
|
9
|
+
@opts = args[:opts]
|
10
|
+
@client = ::Orderer::AtomicBroadcast::Stub.new url, :this_channel_is_insecure
|
11
|
+
end
|
12
|
+
|
13
|
+
def send_broadcast(envelope)
|
14
|
+
client.broadcast([envelope]) do |response|
|
15
|
+
yield response if block_given?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/fabric/peer.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'peer/query_pb'
|
2
|
+
|
3
|
+
module Fabric
|
4
|
+
class Peer
|
5
|
+
attr_reader :url, :opts, :identity_context, :client
|
6
|
+
|
7
|
+
def initialize(args)
|
8
|
+
@url = args[:url]
|
9
|
+
@opts = args[:opts]
|
10
|
+
@identity_context = args[:identity_context]
|
11
|
+
@client = Protos::Endorser::Stub.new url, :this_channel_is_insecure
|
12
|
+
end
|
13
|
+
|
14
|
+
def send_process_proposal(proposal)
|
15
|
+
client.process_proposal proposal
|
16
|
+
end
|
17
|
+
|
18
|
+
def query_channels
|
19
|
+
request = {
|
20
|
+
targets: [self],
|
21
|
+
chaincode_id: Constants::CSCC,
|
22
|
+
transaction: TransactionID.new(identity_context),
|
23
|
+
function: Constants::FUNC_GET_CHANNELS,
|
24
|
+
args: []
|
25
|
+
}
|
26
|
+
|
27
|
+
responses = PeerEndorser.send_transaction_proposal request, '', identity_context
|
28
|
+
response = responses.first
|
29
|
+
|
30
|
+
Protos::ChannelQueryResponse.decode response.response.payload
|
31
|
+
end
|
32
|
+
|
33
|
+
def query_config_block(channel_id)
|
34
|
+
request = {
|
35
|
+
targets: [self],
|
36
|
+
chaincode_id: Constants::CSCC,
|
37
|
+
transaction: TransactionID.new(identity_context),
|
38
|
+
function: Constants::FUNC_GET_CONFIG_BLOCK,
|
39
|
+
args: [channel_id]
|
40
|
+
}
|
41
|
+
|
42
|
+
responses = PeerEndorser.send_transaction_proposal request, '', identity_context
|
43
|
+
proposal_response = responses.first
|
44
|
+
|
45
|
+
extract_config_enveloper proposal_response
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def extract_config_enveloper(proposal_response)
|
51
|
+
block = Common::Block.decode proposal_response.response.payload
|
52
|
+
envelope = Common::Envelope.decode block.data.data.first
|
53
|
+
payload = Common::Payload.decode envelope.payload
|
54
|
+
|
55
|
+
Common::ConfigEnvelope.decode payload.data
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'peer/peer_services_pb'
|
2
|
+
|
3
|
+
module Fabric
|
4
|
+
class PeerEndorser
|
5
|
+
def self.send_transaction_proposal(request, channel_id, identity_context)
|
6
|
+
args = request[:args].unshift request[:function]
|
7
|
+
|
8
|
+
transaction = request[:transaction]
|
9
|
+
|
10
|
+
channel_header = build_channel_header Common::HeaderType::ENDORSER_TRANSACTION,
|
11
|
+
channel_id, transaction.id, request[:chaincode_id]
|
12
|
+
header = build_header channel_header, identity_context.identity, transaction.nonce
|
13
|
+
proposal = build_proposal header, request[:chaincode_id], args
|
14
|
+
signed_proposal = sign_proposal identity_context.identity, proposal
|
15
|
+
|
16
|
+
responses = send_peers_process_proposal request[:targets], signed_proposal
|
17
|
+
|
18
|
+
{ responses: responses, proposal: proposal, header: header }
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def self.build_header(channel_header, identity, nonce)
|
24
|
+
signature_header = Common::SignatureHeader.new creator: identity.serialize,
|
25
|
+
nonce: nonce
|
26
|
+
|
27
|
+
Common::Header.new channel_header: channel_header.to_proto,
|
28
|
+
signature_header: signature_header.to_proto
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.build_channel_header(type, channel_id, tx_id, chaincode_id)
|
32
|
+
attrs = { type: type, channel_id: channel_id, tx_id: tx_id }
|
33
|
+
attrs[:extension] = build_channel_header_extension(chaincode_id).to_proto if chaincode_id
|
34
|
+
attrs[:timestamp] = build_current_timestamp
|
35
|
+
attrs[:version] = Constants::CHANNEL_HEADER_VERSION
|
36
|
+
|
37
|
+
Common::ChannelHeader.new attrs
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.build_channel_header_extension(chaincode_id)
|
41
|
+
id = Protos::ChaincodeID.new name: chaincode_id
|
42
|
+
|
43
|
+
Protos::ChaincodeHeaderExtension.new chaincode_id: id
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.build_current_timestamp
|
47
|
+
now = Time.current
|
48
|
+
|
49
|
+
Google::Protobuf::Timestamp.new seconds: now.to_i, nanos: now.sec
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.build_proposal(header, chaincode_id, args)
|
53
|
+
chaincode_proposal = build_chaincode_proposal chaincode_id, args
|
54
|
+
|
55
|
+
Protos::Proposal.new header: header.to_proto,
|
56
|
+
payload: chaincode_proposal.to_proto
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.build_chaincode_proposal(chaincode_id, args)
|
60
|
+
id = Protos::ChaincodeID.new name: chaincode_id
|
61
|
+
chaincode_input = Protos::ChaincodeInput.new args: args
|
62
|
+
chaincode_spec = Protos::ChaincodeSpec.new type: Protos::ChaincodeSpec::Type::GOLANG,
|
63
|
+
chaincode_id: id,
|
64
|
+
input: chaincode_input
|
65
|
+
chaincode_invocation_spec =
|
66
|
+
Protos::ChaincodeInvocationSpec.new chaincode_spec: chaincode_spec
|
67
|
+
|
68
|
+
Protos::ChaincodeProposalPayload.new input: chaincode_invocation_spec.to_proto
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.sign_proposal(identity, proposal)
|
72
|
+
proposal_bytes = proposal.to_proto
|
73
|
+
signature = identity.sign(proposal_bytes)
|
74
|
+
|
75
|
+
Protos::SignedProposal.new proposal_bytes: proposal_bytes,
|
76
|
+
signature: signature
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.send_peers_process_proposal(peers, proposal)
|
80
|
+
peers.map do |peer|
|
81
|
+
peer.send_process_proposal(proposal)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|