fabric-gateway 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fabric
4
+ #
5
+ # Network represents a blockchain network, or Fabric channel. The Network can be used to access deployed smart
6
+ # contracts, and to listen for events emitted when blocks are committed to the ledger.
7
+ #
8
+ # JChan & THowe believe this should be called a Channel, however Hyperledger Fabric has decided upon the terminology
9
+ # network - https://github.com/hyperledger/fabric-gateway/issues/355#issuecomment-997888071
10
+ #
11
+ class Network
12
+ attr_reader :gateway, :name
13
+
14
+ def initialize(gateway, name)
15
+ @gateway = gateway
16
+ @name = name
17
+ end
18
+
19
+ def client
20
+ gateway.client
21
+ end
22
+
23
+ def signer
24
+ gateway.signer
25
+ end
26
+
27
+ #
28
+ # Creates a new contract instance
29
+ #
30
+ # @param [string] chaincode_name name of the chaincode
31
+ # @param [string] contract_name optional name of the contract
32
+ #
33
+ # @return [Fabric::Contract] new contract instance
34
+ #
35
+ def new_contract(chaincode_name, contract_name = '')
36
+ Contract.new(self, chaincode_name, contract_name)
37
+ end
38
+
39
+ #
40
+ # @TODO: original SDK has getChaincodeEvents and newChaincodeEventsRequest methods
41
+ # @see https://github.com/hyperledger/fabric-gateway/blob/08118cf0a792898925d0b2710b0a9e7c5ec23228/node/src/network.ts
42
+ # @see https://github.com/hyperledger/fabric-gateway/blob/main/pkg/client/network.go
43
+ #
44
+ # @return [?] ?
45
+ #
46
+ def new_chaincode_events
47
+ raise NotYetImplemented
48
+ end
49
+
50
+ #
51
+ # @TODO: original SDK has getChaincodeEvents and newChaincodeEventsRequest methods
52
+ # @see https://github.com/hyperledger/fabric-gateway/blob/08118cf0a792898925d0b2710b0a9e7c5ec23228/node/src/network.ts
53
+ # @see https://github.com/hyperledger/fabric-gateway/blob/main/pkg/client/network.go
54
+ #
55
+ # @return [?] ?
56
+ #
57
+ def new_chaincode_events_request
58
+ raise NotImplementedError
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fabric
4
+ #
5
+ # Proposal represents a transaction proposal that can be sent to peers for endorsement or evaluated as a query.
6
+ #
7
+ # Combined ProposalBuilder with Proposal. Utilizing instance variables and functions in proposal seem adaquate enough
8
+ # to fully create the proposal. ProposalBuilder did not seem like a native ruby design pattern.
9
+ class Proposal
10
+ attr_reader :proposed_transaction
11
+
12
+ #
13
+ # Instantiates a new Proposal
14
+ #
15
+ # @param [Fabric::ProposedTransaction] proposed_transaction ProposedTransaction container class
16
+ #
17
+ def initialize(proposed_transaction)
18
+ @proposed_transaction = proposed_transaction
19
+ end
20
+
21
+ def contract
22
+ @proposed_transaction.contract
23
+ end
24
+
25
+ def network
26
+ contract.network
27
+ end
28
+
29
+ def client
30
+ network.client
31
+ end
32
+
33
+ def signer
34
+ network.signer
35
+ end
36
+
37
+ def gateway
38
+ network.gateway
39
+ end
40
+
41
+ def network_name
42
+ network.name
43
+ end
44
+
45
+ def contract_name
46
+ contract.contract_name
47
+ end
48
+
49
+ def chaincode_name
50
+ contract.chaincode_name
51
+ end
52
+
53
+ def transaction_id
54
+ proposed_transaction.transaction_id
55
+ end
56
+
57
+ #
58
+ # Returns the proposal message as a protobuf Message object.
59
+ #
60
+ # @return [Protos::Proposal|nil] Proposal message
61
+ #
62
+ def proposal
63
+ proposed_transaction.proposal
64
+ end
65
+
66
+ #
67
+ # Returns the signed proposal
68
+ #
69
+ # <rant>
70
+ # Fabric message naming scheme is a mess:
71
+ # ProposedTransaction has a Proposal which is a SignedProposal
72
+ # which has a Proposal which is a Proposal
73
+ # so.... which proposal do you want to access? Adding this function for clarity
74
+ # </rant>
75
+ #
76
+ # @return [Protos::SignedProposal|nil] SignedProposal message
77
+ #
78
+ def signed_proposal
79
+ proposed_transaction.proposed_transaction.proposal
80
+ end
81
+
82
+ #
83
+ # Serialized bytes of the proposal message in proto3 format.
84
+ #
85
+ # @return [String] Binary representation of the proposal message.
86
+ #
87
+ def to_proto
88
+ proposed_transaction.to_proto
89
+ end
90
+
91
+ #
92
+ # Proposal digest which can be utilized for offline signing.
93
+ # If signing offline, call signature= to set signature once
94
+ # computed.
95
+ #
96
+ # @return [String] raw binary digest of the proposal message.
97
+ #
98
+ def digest
99
+ signer.digest(proposal.to_proto)
100
+ end
101
+
102
+ #
103
+ # Sets the signature of the signed proposal in the proposed transaction
104
+ #
105
+ # @param [String] signature raw byte string signature of the proposal message
106
+ # (should be the signature of the proposed message digest)
107
+ #
108
+ def signature=(signature)
109
+ proposed_transaction.signed_proposal.signature = signature
110
+ end
111
+
112
+ #
113
+ # Returns the signed proposal signature
114
+ #
115
+ # @return [String] Raw byte string signature
116
+ #
117
+ def signature
118
+ proposed_transaction.signed_proposal.signature
119
+ end
120
+
121
+ #
122
+ # Returns true if the signed proposal has a signature
123
+ #
124
+ # @return [Boolean] true|false
125
+ #
126
+ def signed?
127
+ # signature cannot be nil because google protobuf won't let it
128
+ !proposed_transaction.signed_proposal.signature.empty?
129
+ end
130
+
131
+ #
132
+ # Utilizes the signer to sign the proposal message if it has not been signed yet.
133
+ #
134
+ def sign
135
+ return if signed?
136
+
137
+ self.signature = signer.sign proposal.to_proto
138
+ end
139
+
140
+ #
141
+ # Evaluate the transaction proposal and obtain its result, without updating the ledger. This runs the transaction
142
+ # on a peer to obtain a transaction result, but does not submit the endorsed transaction to the orderer to be
143
+ # committed to the ledger.
144
+ #
145
+ # @param [Hash] options gRPC call options @see https://www.rubydoc.info/gems/grpc/GRPC%2FClientStub:request_response
146
+ #
147
+ # @return [String] The result returned by the transaction function
148
+ #
149
+ def evaluate(options = {})
150
+ sign
151
+
152
+ evaluate_response = client.evaluate(new_evaluate_request, options)
153
+ evaluate_response.result.payload
154
+ end
155
+
156
+ def endorse
157
+ # TODO: endorse proposal
158
+ end
159
+
160
+ #
161
+ # Generates an evaluate request from this proposal.
162
+ #
163
+ # @return [Gateway::EvaluateRequest] evaluation request with the current proposal
164
+ #
165
+ def new_evaluate_request
166
+ ::Gateway::EvaluateRequest.new(
167
+ channel_id: network_name,
168
+ proposed_transaction: signed_proposal,
169
+ target_organizations: proposed_transaction.endorsing_organizations
170
+ )
171
+ end
172
+
173
+ def new_endorse_request
174
+ # TODO
175
+ end
176
+
177
+ def new_prepared_transaction
178
+ # TODO
179
+ # used in endorse
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fabric
4
+ #
5
+ # Manages the instantiation and creation of the Gateway::ProposedTransaction Protobuf Message.
6
+ #
7
+ # Adapted from official fabric-gateway SDK ProposalBuilder and hyperledger-fabric-sdk:
8
+ # https://github.com/hyperledger/fabric-gateway/blob/1518e03ed3d6db1b6809e23e61a92744fd18e724/node/src/proposalbuilder.ts
9
+ # https://github.com/kirshin/hyperledger-fabric-sdk/blob/95a5a1a37001852312df25946e960a9ff149207e/lib/fabric/proposal.rb
10
+ class ProposedTransaction
11
+ attr_reader :contract,
12
+ :transaction_name,
13
+ :transient_data,
14
+ :arguments,
15
+ :proposed_transaction
16
+
17
+ # Specifies the set of organizations that will attempt to endorse the proposal.
18
+ # No other organizations' peers will be sent this proposal.
19
+ # This is usually used in conjunction with transientData for private data scenarios.
20
+ attr_reader :endorsing_organizations
21
+
22
+ def initialize(contract, transaction_name, arguments: [], transient_data: {}, endorsing_organizations: [])
23
+ @contract = contract
24
+ @transaction_name = transaction_name
25
+ @arguments = arguments
26
+ @transient_data = transient_data
27
+ @endorsing_organizations = endorsing_organizations
28
+
29
+ generate_proposed_transaction
30
+ end
31
+
32
+ def network
33
+ contract.network
34
+ end
35
+
36
+ def client
37
+ network.client
38
+ end
39
+
40
+ def signer
41
+ network.signer
42
+ end
43
+
44
+ def gateway
45
+ network.gateway
46
+ end
47
+
48
+ def network_name
49
+ network.name
50
+ end
51
+
52
+ def contract_name
53
+ contract.contract_name
54
+ end
55
+
56
+ def chaincode_name
57
+ contract.chaincode_name
58
+ end
59
+
60
+ #
61
+ # Builds the proposed transaction protobuf message
62
+ #
63
+ # @return [Gateway::ProposedTransaction]
64
+ #
65
+ def generate_proposed_transaction
66
+ @proposed_transaction = ::Gateway::ProposedTransaction.new(
67
+ transaction_id: transaction_id,
68
+ proposal: signed_proposal,
69
+ endorsing_organizations: endorsing_organizations
70
+ )
71
+ end
72
+
73
+ def signed_proposal
74
+ @signed_proposal ||= Protos::SignedProposal.new(
75
+ proposal_bytes: proposal.to_proto
76
+ )
77
+ end
78
+
79
+ def proposal
80
+ @proposal ||= Protos::Proposal.new header: header.to_proto,
81
+ payload: chaincode_proposal_payload.to_proto
82
+ end
83
+
84
+ def header
85
+ Common::Header.new channel_header: channel_header.to_proto,
86
+ signature_header: signature_header.to_proto
87
+ end
88
+
89
+ def channel_header
90
+ Common::ChannelHeader.new type: Common::HeaderType::ENDORSER_TRANSACTION,
91
+ channel_id: network_name, tx_id: transaction_id,
92
+ extension: channel_header_extension.to_proto,
93
+ timestamp: timestamp, epoch: 0
94
+ # version: Constants::CHANNEL_HEADER_VERSION # official SDK does not send this.
95
+ end
96
+
97
+ def channel_header_extension
98
+ Protos::ChaincodeHeaderExtension.new chaincode_id: chaincode_id
99
+ end
100
+
101
+ def chaincode_id
102
+ Protos::ChaincodeID.new name: chaincode_name
103
+ end
104
+
105
+ def chaincode_proposal_payload
106
+ chaincode_input = Protos::ChaincodeInput.new args: [transaction_name] + arguments
107
+ chaincode_spec = Protos::ChaincodeSpec.new type: Protos::ChaincodeSpec::Type::NODE,
108
+ chaincode_id: chaincode_id,
109
+ input: chaincode_input
110
+ input = Protos::ChaincodeInvocationSpec.new chaincode_spec: chaincode_spec
111
+
112
+ Protos::ChaincodeProposalPayload.new input: input.to_proto, TransientMap: transient_data
113
+ end
114
+
115
+ #
116
+ # Returns the current timestamp
117
+ #
118
+ # @return [Google::Protobuf::Timestamp] gRPC timestamp
119
+ #
120
+ def timestamp
121
+ now = Time.now
122
+
123
+ @timestamp ||= Google::Protobuf::Timestamp.new seconds: now.to_i, nanos: now.nsec
124
+ end
125
+
126
+ #
127
+ # Generates a random nonce
128
+ #
129
+ # @return [String] random nonce
130
+ #
131
+ def nonce
132
+ @nonce ||= signer.crypto_suite.generate_nonce
133
+ end
134
+
135
+ #
136
+ # Generates a unique transaction ID for the transaction based on a random number and the signer
137
+ # or returns the existing transaction ID if it has already been generated.
138
+ #
139
+ # @return [String] transaction ID
140
+ #
141
+ def transaction_id
142
+ @transaction_id ||= signer.crypto_suite.hexdigest(nonce + signer.to_proto)
143
+ end
144
+
145
+ #
146
+ # Generates a SignatureHeader protobuf message from the signer and nonce
147
+ #
148
+ # @return [Common::SignatureHeader] signature header protobuf message instance
149
+ #
150
+ def signature_header
151
+ Common::SignatureHeader.new creator: signer.to_proto, nonce: nonce
152
+ end
153
+
154
+ # Dev note: if we have more classes that encapsulate protobuffer messages, consider
155
+ # creating an EncapsulatedPBMessage to hold the message and expose the following methods
156
+ # as common interface.
157
+
158
+ #
159
+ # Returns the protobuf message instance
160
+ #
161
+ # @return [Gateway::ProposedTransaction] protobuf message instance
162
+ #
163
+ def as_proto
164
+ proposed_transaction
165
+ end
166
+
167
+ #
168
+ # Returns the serialized Protobuf binary form of the proposed transaction
169
+ #
170
+ # @return [String] serialized Protobuf binary form of the proposed transaction
171
+ #
172
+ def to_proto
173
+ proposed_transaction.to_proto
174
+ end
175
+
176
+ #
177
+ # Returns the serialized JSON form of the proposed transaction
178
+ #
179
+ # @param [Hash] options JSON serialization options @see https://ruby-doc.org/stdlib-2.6.3/libdoc/json/rdoc/JSON.html#method-i-generate
180
+ #
181
+ # @return [String] serialized JSON form of the proposed transaction
182
+ #
183
+ def to_json(options = {})
184
+ proposed_transaction.to_json(options)
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fabric
4
+ VERSION = '0.2.0'
5
+ end
data/lib/fabric.rb ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fabric/constants'
4
+ require 'fabric/contract'
5
+ require 'fabric/client'
6
+ require 'fabric/ec_crypto_suite'
7
+ require 'fabric/gateway'
8
+ require 'fabric/identity'
9
+ require 'fabric/network'
10
+ require 'fabric/proposal'
11
+ require 'fabric/proposed_transaction'
12
+ require 'fabric/version'
13
+
14
+ require 'gateway/gateway_pb'
15
+ require 'gateway/gateway_services_pb'
16
+
17
+ #
18
+ # Hyperledger Fabric Gateway SDK
19
+ #
20
+ module Fabric
21
+ class Error < StandardError; end
22
+ class InvalidArgument < Error; end
23
+ class NotYetImplemented < Error; end
24
+
25
+ #
26
+ # CommitError
27
+ #
28
+ # @TODO: TEST ME!
29
+ #
30
+ class CommitError < Error
31
+ attr_reader :code, :transaction_id
32
+
33
+ def initialize(status)
34
+ super("Transaction #{status.transaction_id} failed to commit with status code #{status.code} -" +
35
+ Protos::TxValidationCode.lookup(status.code).to_s)
36
+ @code = code
37
+ @transaction_id = status.transaction_id
38
+ end
39
+ end
40
+
41
+ def self.crypto_suite(opts = {})
42
+ @crypto_suite ||= Fabric::ECCryptoSuite.new opts
43
+ end
44
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fabric-gateway
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Chan
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-12-17 00:00:00.000000000 Z
11
+ date: 2021-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: google-protobuf
@@ -38,6 +38,34 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.42'
41
+ - !ruby/object:Gem::Dependency
42
+ name: codecov
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.6.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.6.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: factory_bot
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 6.2.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 6.2.0
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: grpc-tools
43
71
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +80,62 @@ dependencies:
52
80
  - - "~>"
53
81
  - !ruby/object:Gem::Version
54
82
  version: '1.42'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 1.23.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 1.23.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 2.6.0
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 2.6.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.21.2
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.21.2
125
+ - !ruby/object:Gem::Dependency
126
+ name: timecop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 0.9.4
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 0.9.4
55
139
  description: 'Hyperledger Fabric Gateway gRPC SDK generated directly from protos found
56
140
  at: https://github.com/hyperledger/fabric-protos.'
57
141
  email:
@@ -60,33 +144,43 @@ executables: []
60
144
  extensions: []
61
145
  extra_rdoc_files: []
62
146
  files:
147
+ - ".editorconfig"
148
+ - ".github/workflows/rspec.yml"
149
+ - ".github/workflows/rubocop.yml"
63
150
  - ".gitignore"
64
151
  - ".gitmodules"
65
152
  - ".rspec"
153
+ - ".rubocop.yml"
154
+ - ".ruby-version"
66
155
  - ".travis.yml"
156
+ - ".vscode/settings.json"
67
157
  - CHANGELOG.md
68
158
  - CODE_OF_CONDUCT.md
69
159
  - Gemfile
70
- - Gemfile.lock
71
160
  - LICENSE.txt
72
161
  - README.md
73
162
  - Rakefile
74
163
  - TAGS
75
164
  - bin/console
76
165
  - bin/regenerate
166
+ - bin/release
77
167
  - bin/setup
78
168
  - fabric-gateway.gemspec
79
169
  - lib/.DS_Store
80
170
  - lib/common/common_pb.rb
81
171
  - lib/common/policies_pb.rb
172
+ - lib/fabric.rb
82
173
  - lib/fabric/.DS_Store
174
+ - lib/fabric/client.rb
175
+ - lib/fabric/constants.rb
176
+ - lib/fabric/contract.rb
177
+ - lib/fabric/ec_crypto_suite.rb
83
178
  - lib/fabric/gateway.rb
84
- - lib/fabric/gateway/client.rb
85
- - lib/fabric/gateway/constants.rb
86
- - lib/fabric/gateway/ec_crypto_suite.rb
87
- - lib/fabric/gateway/identity.rb
88
- - lib/fabric/gateway/proposal.rb
89
- - lib/fabric/gateway/version.rb
179
+ - lib/fabric/identity.rb
180
+ - lib/fabric/network.rb
181
+ - lib/fabric/proposal.rb
182
+ - lib/fabric/proposed_transaction.rb
183
+ - lib/fabric/version.rb
90
184
  - lib/gateway/gateway_pb.rb
91
185
  - lib/gateway/gateway_services_pb.rb
92
186
  - lib/gossip/message_pb.rb
@@ -104,10 +198,7 @@ homepage: https://github.com/ethicalidentity/fabric-gateway-ruby
104
198
  licenses:
105
199
  - MIT
106
200
  metadata:
107
- allowed_push_host: https://rubygems.org
108
- homepage_uri: https://github.com/ethicalidentity/fabric-gateway-ruby
109
- source_code_uri: https://github.com/ethicalidentity/fabric-gateway-ruby
110
- changelog_uri: https://github.com/EthicalIdentity/fabric-gateway-ruby/blob/master/CHANGELOG.md
201
+ rubygems_mfa_required: 'true'
111
202
  post_install_message:
112
203
  rdoc_options: []
113
204
  require_paths:
@@ -116,7 +207,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
116
207
  requirements:
117
208
  - - ">="
118
209
  - !ruby/object:Gem::Version
119
- version: 2.3.0
210
+ version: 2.6.0
120
211
  required_rubygems_version: !ruby/object:Gem::Requirement
121
212
  requirements:
122
213
  - - ">="
data/Gemfile.lock DELETED
@@ -1,44 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- fabric-gateway (0.0.2)
5
- google-protobuf (>= 3.19.1)
6
- grpc (~> 1.42)
7
-
8
- GEM
9
- remote: https://rubygems.org/
10
- specs:
11
- diff-lcs (1.4.4)
12
- google-protobuf (3.19.1)
13
- googleapis-common-protos-types (1.3.0)
14
- google-protobuf (~> 3.14)
15
- grpc (1.42.0)
16
- google-protobuf (~> 3.18)
17
- googleapis-common-protos-types (~> 1.0)
18
- grpc-tools (1.42.0)
19
- rake (12.3.2)
20
- rspec (3.10.0)
21
- rspec-core (~> 3.10.0)
22
- rspec-expectations (~> 3.10.0)
23
- rspec-mocks (~> 3.10.0)
24
- rspec-core (3.10.1)
25
- rspec-support (~> 3.10.0)
26
- rspec-expectations (3.10.1)
27
- diff-lcs (>= 1.2.0, < 2.0)
28
- rspec-support (~> 3.10.0)
29
- rspec-mocks (3.10.2)
30
- diff-lcs (>= 1.2.0, < 2.0)
31
- rspec-support (~> 3.10.0)
32
- rspec-support (3.10.3)
33
-
34
- PLATFORMS
35
- ruby
36
-
37
- DEPENDENCIES
38
- fabric-gateway!
39
- grpc-tools (~> 1.42)
40
- rake (~> 12.0)
41
- rspec (~> 3.0)
42
-
43
- BUNDLED WITH
44
- 2.1.4