fabric-gateway 0.0.1 → 0.3.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +3 -0
  3. data/.github/workflows/rspec.yml +37 -0
  4. data/.github/workflows/rubocop.yml +28 -0
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +23 -0
  7. data/.ruby-version +1 -0
  8. data/.vscode/settings.json +7 -0
  9. data/.yardopts +2 -0
  10. data/CODE_OF_CONDUCT.md +105 -46
  11. data/Gemfile +5 -3
  12. data/LICENSE.txt +1 -1
  13. data/README.md +105 -7
  14. data/Rakefile +7 -3
  15. data/bin/console +4 -3
  16. data/bin/regenerate +19 -0
  17. data/bin/release +5 -0
  18. data/fabric-gateway.gemspec +32 -17
  19. data/lib/common/common_pb.rb +113 -0
  20. data/lib/common/policies_pb.rb +61 -0
  21. data/lib/fabric/accessors/contract.rb +51 -0
  22. data/lib/fabric/accessors/gateway.rb +33 -0
  23. data/lib/fabric/accessors/network.rb +40 -0
  24. data/lib/fabric/client.rb +146 -0
  25. data/lib/fabric/constants.rb +8 -0
  26. data/lib/fabric/contract.rb +154 -0
  27. data/lib/fabric/ec_crypto_suite.rb +199 -0
  28. data/lib/fabric/entities/envelope.rb +153 -0
  29. data/lib/fabric/entities/identity.rb +87 -0
  30. data/lib/fabric/entities/proposal.rb +189 -0
  31. data/lib/fabric/entities/proposed_transaction.rb +163 -0
  32. data/lib/fabric/entities/status.rb +32 -0
  33. data/lib/fabric/entities/transaction.rb +247 -0
  34. data/lib/fabric/gateway.rb +31 -6
  35. data/lib/fabric/network.rb +56 -0
  36. data/lib/fabric/version.rb +5 -0
  37. data/lib/fabric.rb +57 -0
  38. data/lib/gossip/message_pb.rb +236 -0
  39. data/lib/gossip/message_services_pb.rb +31 -0
  40. data/lib/msp/identities_pb.rb +25 -0
  41. data/lib/msp/msp_principal_pb.rb +57 -0
  42. data/lib/orderer/ab_pb.rb +64 -0
  43. data/lib/orderer/ab_services_pb.rb +30 -0
  44. data/lib/peer/chaincode_event_pb.rb +19 -0
  45. data/lib/peer/chaincode_pb.rb +69 -0
  46. data/lib/peer/proposal_pb.rb +41 -0
  47. data/lib/peer/proposal_response_pb.rb +52 -0
  48. data/lib/peer/transaction_pb.rb +74 -0
  49. metadata +184 -10
  50. data/lib/fabric/gateway/version.rb +0 -5
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ # Adapted from:
6
+ # https://github.com/kirshin/hyperledger-fabric-sdk/blob/95a5a1a37001852312df25946e960a9ff149207e/lib/fabric/crypto_suite.rb
7
+ module Fabric
8
+ #
9
+ # Elliptic-curve Crypto Suite using OpenSSL
10
+ #
11
+ # @todo missing tests
12
+ class ECCryptoSuite # rubocop:disable Metrics/ClassLength
13
+ DEFAULT_KEY_SIZE = 256
14
+ DEFAULT_DIGEST_ALGORITHM = 'SHA256'
15
+ DEFAULT_AES_KEY_SIZE = 128
16
+
17
+ EC_CURVES = { 256 => 'prime256v1', 384 => 'secp384r1' }.freeze
18
+
19
+ CIPHER = 'aes-256-cbc'
20
+
21
+ attr_reader :key_size, :digest_algorithm, :digest_instance, :curve, :cipher
22
+
23
+ def initialize(opts = {})
24
+ @key_size = opts[:key_size] || DEFAULT_KEY_SIZE
25
+ @digest_algorithm = opts[:digest_algorithm] || DEFAULT_DIGEST_ALGORITHM
26
+ @digest_instance = OpenSSL::Digest.new digest_algorithm
27
+ @curve = EC_CURVES[key_size]
28
+ @cipher = opts[:cipher] || CIPHER
29
+ end
30
+
31
+ def sign(private_key, message)
32
+ digest = digest message
33
+ key = pkey_from_private_key private_key
34
+ signature = key.dsa_sign_asn1 digest
35
+ sequence = OpenSSL::ASN1.decode signature
36
+ sequence = prevent_malleability sequence, key.group.order
37
+
38
+ sequence.to_der
39
+ end
40
+
41
+ def verify(public_key, message, signature)
42
+ digest = digest message
43
+ openssl_pkey = openssl_pkey_from_public_key public_key
44
+ sequence = OpenSSL::ASN1.decode signature
45
+ return false unless check_malleability sequence, openssl_pkey.group.order
46
+
47
+ openssl_pkey.dsa_verify_asn1(digest, signature)
48
+ end
49
+
50
+ def generate_private_key
51
+ key = OpenSSL::PKey::EC.new curve
52
+ key.generate_key!
53
+
54
+ key.private_key.to_s(16).downcase
55
+ end
56
+
57
+ def generate_csr(private_key, attrs = [])
58
+ key = pkey_from_private_key private_key
59
+
60
+ req = OpenSSL::X509::Request.new
61
+ req.public_key = key
62
+ req.subject = OpenSSL::X509::Name.new attrs
63
+ req.sign key, @digest_instance
64
+
65
+ req
66
+ end
67
+
68
+ def generate_nonce(length = 24)
69
+ OpenSSL::Random.random_bytes length
70
+ end
71
+
72
+ def hexdigest(message)
73
+ @digest_instance.hexdigest message
74
+ end
75
+
76
+ def digest(message)
77
+ @digest_instance.digest message
78
+ end
79
+
80
+ def encode_hex(bytes)
81
+ bytes.unpack1('H*')
82
+ end
83
+
84
+ def decode_hex(string)
85
+ [string].pack('H*')
86
+ end
87
+
88
+ def restore_public_key(private_key)
89
+ private_bn = OpenSSL::BN.new private_key, 16
90
+ group = OpenSSL::PKey::EC::Group.new curve
91
+ public_bn = group.generator.mul(private_bn).to_bn
92
+ public_bn = OpenSSL::PKey::EC::Point.new(group, public_bn).to_bn
93
+
94
+ public_bn.to_s(16).downcase
95
+ end
96
+
97
+ def address_from_public_key(public_key)
98
+ bytes = decode_hex public_key
99
+ address_bytes = digest(bytes[1..])[-20..]
100
+
101
+ encode_hex address_bytes
102
+ end
103
+
104
+ def build_shared_key(private_key, public_key)
105
+ pkey = pkey_from_private_key private_key
106
+ public_bn = OpenSSL::BN.new public_key, 16
107
+ group = OpenSSL::PKey::EC::Group.new curve
108
+ public_point = OpenSSL::PKey::EC::Point.new group, public_bn
109
+
110
+ encode_hex pkey.dh_compute_key(public_point)
111
+ end
112
+
113
+ def encrypt(secret, data)
114
+ aes = OpenSSL::Cipher.new cipher
115
+ aes.encrypt
116
+ aes.key = decode_hex(secret)
117
+ iv = aes.random_iv
118
+ aes.iv = iv
119
+
120
+ Base64.strict_encode64(iv + aes.update(data) + aes.final)
121
+ end
122
+
123
+ def decrypt(secret, data)
124
+ return unless data
125
+
126
+ encrypted_data = Base64.strict_decode64 data
127
+ aes = OpenSSL::Cipher.new cipher
128
+ aes.decrypt
129
+ aes.key = decode_hex(secret)
130
+ aes.iv = encrypted_data[0..15]
131
+ encrypted_data = encrypted_data[16..]
132
+
133
+ aes.update(encrypted_data) + aes.final
134
+ end
135
+
136
+ def pkey_pem_from_private_key(private_key)
137
+ public_key = restore_public_key private_key
138
+ key = OpenSSL::PKey::EC.new curve
139
+ key.private_key = OpenSSL::BN.new private_key, 16
140
+ key.public_key = OpenSSL::PKey::EC::Point.new key.group,
141
+ OpenSSL::BN.new(public_key, 16)
142
+
143
+ pkey = OpenSSL::PKey::EC.new(key.public_key.group)
144
+ pkey.public_key = key.public_key
145
+
146
+ pkey.to_pem
147
+ end
148
+
149
+ def key_from_pem(pem)
150
+ key = OpenSSL::PKey::EC.new(pem)
151
+ key.private_key.to_s(16).downcase
152
+ end
153
+
154
+ def pkey_from_x509_certificate(certificate)
155
+ cert = OpenSSL::X509::Certificate.new(certificate)
156
+ cert.public_key.public_key.to_bn.to_s(16).downcase
157
+ end
158
+
159
+ def openssl_pkey_from_public_key(public_key)
160
+ pkey = OpenSSL::PKey::EC.new curve
161
+ pkey.public_key = OpenSSL::PKey::EC::Point.new(pkey.group, OpenSSL::BN.new(public_key, 16))
162
+
163
+ pkey
164
+ end
165
+
166
+ private
167
+
168
+ def pkey_from_private_key(private_key)
169
+ public_key = restore_public_key private_key
170
+ key = OpenSSL::PKey::EC.new curve
171
+ key.private_key = OpenSSL::BN.new private_key, 16
172
+ key.public_key = OpenSSL::PKey::EC::Point.new key.group,
173
+ OpenSSL::BN.new(public_key, 16)
174
+
175
+ key
176
+ end
177
+
178
+ # barely understand this code - this link provides a good explanation:
179
+ # http://coders-errand.com/malleability-ecdsa-signatures/
180
+ def prevent_malleability(sequence, order)
181
+ half_order = order >> 1
182
+
183
+ if (half_key = sequence.value[1].value) > half_order
184
+ sequence.value[1].value = order - half_key
185
+ end
186
+
187
+ sequence
188
+ end
189
+
190
+ # ported from python code, understanding extremely limited.
191
+ # from what I gather, sequence.value[0] and sequence.value[1]
192
+ # are the r and s values from the python implementation
193
+ # https://github.com/hyperledger/fabric-sdk-py/blob/25209f61518873da68d28313582607c29b5bae7d/hfc/util/crypto/crypto.py#L259
194
+ def check_malleability(sequence, order)
195
+ half_order = order >> 1
196
+ sequence.value[1].value <= half_order
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fabric
4
+ #
5
+ # Encapsulates an Envelop protobuf message
6
+ #
7
+ class Envelope
8
+ # @return [Common::Envelope] transaction envelope
9
+ attr_reader :envelope
10
+
11
+ #
12
+ # Creates a new Envelope instance.
13
+ #
14
+ # @param [Common::Envelope] envelope
15
+ #
16
+ def initialize(envelope)
17
+ @envelope = envelope
18
+ end
19
+
20
+ #
21
+ # Checks if the envelope has been signed.
22
+ #
23
+ # @return [Boolean] true if the envelope has been signed; otherwise false.
24
+ #
25
+ def signed?
26
+ !envelope.signature.empty?
27
+ end
28
+
29
+ #
30
+ # The protobuffer serialized form of the envelope payload.
31
+ #
32
+ # @return [String] serialized payload
33
+ #
34
+ def payload_bytes
35
+ envelope.payload
36
+ end
37
+
38
+ #
39
+ # The digest of the payload.
40
+ #
41
+ # @return [String] payload digest
42
+ #
43
+ def payload_digest
44
+ Fabric.crypto_suite.digest(envelope.payload)
45
+ end
46
+
47
+ #
48
+ # Sets the envelope signature.
49
+ #
50
+ # @param [String] signature
51
+ #
52
+ # @return [Void]
53
+ #
54
+ def signature=(signature)
55
+ envelope.signature = signature
56
+ end
57
+
58
+ def result
59
+ @result ||= parse_result_from_payload
60
+ end
61
+
62
+ #
63
+ # Returns the deserialized payload.
64
+ #
65
+ # @return [Common::Payload] Envelope payload
66
+ #
67
+ def payload
68
+ @payload ||= Common::Payload.decode(envelope.payload)
69
+ end
70
+
71
+ #
72
+ # Returns the envelope payload header.
73
+ #
74
+ # Envelope => Payload => Header
75
+ #
76
+ # @return [Common::Header] Envelope Payload Header
77
+ #
78
+ def header
79
+ raise Fabric::Error, 'Missing header' if payload.header.nil?
80
+
81
+ @header ||= payload.header
82
+ end
83
+
84
+ #
85
+ # Returns the deserialized transaction channel header
86
+ #
87
+ # Envelope => Payload => Header => ChannelHeader
88
+ #
89
+ # @return [Common::ChannelHeader] envelop payload header channel header
90
+ #
91
+ def channel_header
92
+ @channel_header ||= Common::ChannelHeader.decode(header.channel_header)
93
+ end
94
+
95
+ #
96
+ # Grabs the channel_name frmo the depths of the envelope.
97
+ #
98
+ # @return [String] channel name
99
+ #
100
+ def channel_name
101
+ channel_header.channel_id
102
+ end
103
+
104
+ #
105
+ # Returns the deserialized transaction
106
+ #
107
+ # @return [Protos::Transaction] transaction
108
+ #
109
+ def transaction
110
+ @transaction ||= Protos::Transaction.decode(payload.data)
111
+ end
112
+
113
+ private
114
+
115
+ #
116
+ # Parse the transaction actinos from the payload looking for the transaction result payload.
117
+ #
118
+ # @return [String] result payload
119
+ # @raise [Fabric::Error] if the transaction result payload is not found
120
+ #
121
+ def parse_result_from_payload
122
+ errors = []
123
+ transaction.actions.each do |action|
124
+ return parse_result_from_transaction_action(action)
125
+ rescue Fabric::Error => e
126
+ errors << e
127
+ end
128
+
129
+ raise Fabric::Error, "No proposal response found: #{errors.inspect}"
130
+ end
131
+
132
+ #
133
+ # Parse a single transaction action looking for the transaction result payload.
134
+ #
135
+ # @param [Protos::TransactionAction] transaction_action
136
+ #
137
+ # @return [Payload] transaction result payload
138
+ # @raise [Fabric::Error] if the endorsed_action is missing or the chaincode response is missing
139
+ #
140
+ def parse_result_from_transaction_action(transaction_action)
141
+ action_payload = Protos::ChaincodeActionPayload.decode(transaction_action.payload)
142
+ endorsed_action = action_payload.action
143
+ raise Fabric::Error, 'Missing endorsed action' if endorsed_action.nil?
144
+
145
+ response_payload = Protos::ProposalResponsePayload.decode(endorsed_action.proposal_response_payload)
146
+ chaincode_action = Protos::ChaincodeAction.decode(response_payload.extension)
147
+ chaincode_response = chaincode_action.response
148
+ raise Fabric::Error, 'Missing chaincode response' if chaincode_response.nil?
149
+
150
+ chaincode_response.payload
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'msp/identities_pb'
4
+ require 'base64'
5
+
6
+ # Adapted from:
7
+ # https://github.com/kirshin/hyperledger-fabric-sdk/blob/95a5a1a37001852312df25946e960a9ff149207e/lib/fabric/identity.rb
8
+
9
+ module Fabric
10
+ #
11
+ # @attr_reader [String] private_key raw private key in hex format
12
+ # @attr_reader [String] public_key raw public key in hex format
13
+ # @attr_reader [String] certificate raw certificate in pem format
14
+ # @attr_reader [String] msp_id MSP (Membership Service Provider) Identifier
15
+ #
16
+ class Identity
17
+ attr_reader :private_key,
18
+ :public_key,
19
+ :address, # TODO: possibly unnecessary
20
+ :crypto_suite
21
+
22
+ attr_accessor :certificate, :msp_id
23
+
24
+ def initialize(private_key: nil, public_key: nil, certificate: nil, msp_id: nil, crypto_suite: nil)
25
+ @crypto_suite = crypto_suite || Fabric.crypto_suite
26
+
27
+ @private_key = private_key || @crypto_suite.generate_private_key
28
+ @public_key = public_key || @crypto_suite.restore_public_key(@private_key)
29
+ @certificate = certificate
30
+ @msp_id = msp_id
31
+
32
+ @address = @crypto_suite.address_from_public_key @public_key
33
+
34
+ return unless @certificate
35
+
36
+ raise Fabric::Error, 'Key mismatch (public_key or certificate) for identity' unless validate_key_integrity
37
+ end
38
+
39
+ #
40
+ # Validates that the private_key, public_key, and certificate are valid and match
41
+ #
42
+ # @return [boolean] true if valid, false otherwise
43
+ #
44
+ def validate_key_integrity
45
+ cert_pubkey = @crypto_suite.pkey_from_x509_certificate(certificate)
46
+ priv_pubkey = @crypto_suite.restore_public_key(@private_key)
47
+
48
+ @public_key == cert_pubkey && @public_key == priv_pubkey
49
+ end
50
+
51
+ def generate_csr(attrs = [])
52
+ @crypto_suite.generate_csr private_key, attrs
53
+ end
54
+
55
+ def sign(message)
56
+ @crypto_suite.sign(private_key, message)
57
+ end
58
+
59
+ def digest(message)
60
+ @crypto_suite.digest message
61
+ end
62
+
63
+ # TODO: Do we need this?
64
+ def shared_secret_by(public_key)
65
+ @crypto_suite.build_shared_key private_key, public_key
66
+ end
67
+
68
+ def as_proto
69
+ @as_proto ||= Msp::SerializedIdentity.new(mspid: msp_id, id_bytes: certificate)
70
+ end
71
+
72
+ def to_proto
73
+ @to_proto ||= Msp::SerializedIdentity.new(mspid: msp_id, id_bytes: certificate).to_proto
74
+ end
75
+
76
+ #
77
+ # Creates a new gateway passing in the current identity
78
+ #
79
+ # @param [Fabric::Client] client
80
+ #
81
+ # @return [Fabric::Gateway] gateway
82
+ #
83
+ def new_gateway(client)
84
+ Fabric::Gateway.new(self, client)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,189 @@
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
+ include Fabric::Accessors::Contract
26
+
27
+ def transaction_id
28
+ proposed_transaction.transaction_id
29
+ end
30
+
31
+ #
32
+ # Returns the proposal message as a protobuf Message object.
33
+ #
34
+ # @return [Protos::Proposal|nil] Proposal message
35
+ #
36
+ def proposal
37
+ proposed_transaction.proposal
38
+ end
39
+
40
+ #
41
+ # Returns the signed proposal
42
+ #
43
+ # <rant>
44
+ # Fabric message naming scheme is a mess:
45
+ # ProposedTransaction has a Proposal which is a SignedProposal
46
+ # which has a Proposal which is a Proposal
47
+ # so.... which proposal do you want to access? Adding this function for clarity
48
+ # </rant>
49
+ #
50
+ # @return [Protos::SignedProposal|nil] SignedProposal message
51
+ #
52
+ def signed_proposal
53
+ proposed_transaction.proposed_transaction.proposal
54
+ end
55
+
56
+ #
57
+ # Serialized bytes of the proposal message in proto3 format.
58
+ #
59
+ # @return [String] Binary representation of the proposal message.
60
+ #
61
+ def to_proto
62
+ proposed_transaction.to_proto
63
+ end
64
+
65
+ #
66
+ # Proposal digest which can be utilized for offline signing.
67
+ # If signing offline, call signature= to set signature once
68
+ # computed.
69
+ #
70
+ # @return [String] raw binary digest of the proposal message.
71
+ #
72
+ def digest
73
+ Fabric.crypto_suite.digest(proposal.to_proto)
74
+ end
75
+
76
+ #
77
+ # Sets the signature of the signed proposal in the proposed transaction
78
+ #
79
+ # @param [String] signature raw byte string signature of the proposal message
80
+ # (should be the signature of the proposed message digest)
81
+ #
82
+ def signature=(signature)
83
+ proposed_transaction.signed_proposal.signature = signature
84
+ end
85
+
86
+ #
87
+ # Returns the signed proposal signature
88
+ #
89
+ # @return [String] Raw byte string signature
90
+ #
91
+ def signature
92
+ proposed_transaction.signed_proposal.signature
93
+ end
94
+
95
+ #
96
+ # Returns true if the signed proposal has a signature
97
+ #
98
+ # @return [Boolean] true|false
99
+ #
100
+ def signed?
101
+ # signature cannot be nil because google protobuf won't let it
102
+ !proposed_transaction.signed_proposal.signature.empty?
103
+ end
104
+
105
+ #
106
+ # Utilizes the signer to sign the proposal message if it has not been signed yet.
107
+ #
108
+ def sign
109
+ return if signed?
110
+
111
+ self.signature = signer.sign proposal.to_proto
112
+ end
113
+
114
+ #
115
+ # Evaluate the transaction proposal and obtain its result, without updating the ledger. This runs the transaction
116
+ # on a peer to obtain a transaction result, but does not submit the endorsed transaction to the orderer to be
117
+ # committed to the ledger.
118
+ #
119
+ # @param [Hash] options gRPC call options @see https://www.rubydoc.info/gems/grpc/GRPC%2FClientStub:request_response
120
+ #
121
+ # @return [String] The result returned by the transaction function
122
+ #
123
+ def evaluate(options = {})
124
+ sign
125
+
126
+ evaluate_response = client.evaluate(new_evaluate_request, options)
127
+ evaluate_response.result.payload
128
+ end
129
+
130
+ #
131
+ # Obtain endorsement for the transaction proposal from sufficient peers to allow it to be committed to the ledger.
132
+ #
133
+ # @param [Hash] options gRPC call options @see https://www.rubydoc.info/gems/grpc/GRPC%2FClientStub:request_response
134
+ #
135
+ # @return [Fabric::Transaction] An endorsed transaction that can be submitted to the ledger.
136
+ #
137
+ def endorse(options = {})
138
+ sign
139
+ endorse_response = client.endorse(new_endorse_request, options)
140
+
141
+ raise Fabric::Error, 'Missing transaction envelope' if endorse_response.prepared_transaction.nil?
142
+
143
+ prepared_transaction = new_prepared_transaction(endorse_response.prepared_transaction)
144
+
145
+ Fabric::Transaction.new(network, prepared_transaction)
146
+ end
147
+
148
+ #
149
+ # Generates an evaluate request from this proposal.
150
+ #
151
+ # @return [Gateway::EvaluateRequest] evaluation request with the current proposal
152
+ #
153
+ def new_evaluate_request
154
+ ::Gateway::EvaluateRequest.new(
155
+ channel_id: network_name,
156
+ proposed_transaction: signed_proposal,
157
+ target_organizations: proposed_transaction.endorsing_organizations
158
+ )
159
+ end
160
+
161
+ #
162
+ # Creates a new endorse request from this proposal.
163
+ #
164
+ # @return [Gateway::EndorseRequest] EndorseRequest protobuf message
165
+ #
166
+ def new_endorse_request
167
+ ::Gateway::EndorseRequest.new(
168
+ transaction_id: transaction_id,
169
+ channel_id: network_name,
170
+ proposed_transaction: signed_proposal,
171
+ endorsing_organizations: proposed_transaction.endorsing_organizations
172
+ )
173
+ end
174
+
175
+ #
176
+ # Creates a new prepared transaction from a transaction envelope.
177
+ #
178
+ # @param [Common::Envelope] envelope transaction envelope
179
+ #
180
+ # @return [Gateway::PreparedTransaction] prepared transaction protobuf message
181
+ #
182
+ def new_prepared_transaction(envelope)
183
+ ::Gateway::PreparedTransaction.new(
184
+ transaction_id: transaction_id,
185
+ envelope: envelope
186
+ )
187
+ end
188
+ end
189
+ end