fabric-gateway 0.0.1 → 0.3.0

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