flow_client 0.1.1 → 0.2.2

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.
@@ -6,6 +6,12 @@ require "json"
6
6
 
7
7
  # Collection of classes to interact with the Flow blockchain
8
8
  module FlowClient
9
+ class CadenceRuntimeError < StandardError
10
+ end
11
+
12
+ class ClientError < StandardError
13
+ end
14
+
9
15
  # Flow client
10
16
  class Client
11
17
  attr_accessor :address_aliases
@@ -20,65 +26,348 @@ module FlowClient
20
26
  @stub.ping(req)
21
27
  end
22
28
 
23
- # Accounts
29
+ # :section: Accounts
30
+
31
+ # Returns an account for the address specified at the latest
32
+ # block.
33
+ #
34
+ # @param [String] the address string value
35
+ #
36
+ # @return [FlowClient::Account] the account
24
37
  def get_account(address)
25
38
  req = Access::GetAccountAtLatestBlockRequest.new(address: to_bytes(address))
26
- res = @stub.get_account_at_latest_block(req)
27
- res.account
39
+
40
+ begin
41
+ res = @stub.get_account_at_latest_block(req)
42
+ rescue GRPC::BadStatus => e
43
+ raise ClientError, e.details
44
+ else
45
+ account = FlowClient::Account.new(
46
+ address: res.account.address.unpack1("H*"),
47
+ balance: res.account.balance/100000000.0
48
+ )
49
+
50
+ res.account.keys.each do |key|
51
+ account.keys << FlowClient::AccountKey.new(
52
+ public_key: key.public_key.unpack1("H*"),
53
+ index: key.index,
54
+ sequence_number: key.sequence_number,
55
+ revoked: key.revoked,
56
+ weight: key.weight
57
+ )
58
+ end
59
+
60
+ account.contracts = res.account.contracts
61
+ account
62
+ end
63
+ end
64
+
65
+ # Creates a new account
66
+ #
67
+ # @return [FlowClient::Account] the newly created account
68
+ def create_account(new_account_public_keys, contracts, payer_account, signer)
69
+ script = File.read(File.join("lib", "cadence", "templates", "create-account.cdc"))
70
+
71
+ arguments = [
72
+ CadenceType.Array(
73
+ new_account_public_keys.to_a.map { |key| CadenceType.String(key) }
74
+ ),
75
+ CadenceType.Dictionary(
76
+ contracts.to_a.map { |name, code| CadenceType.DictionaryValue(
77
+ CadenceType.String(name), CadenceType.String(code.unpack1("H*"))
78
+ ) }
79
+ ),
80
+ ]
81
+
82
+ transaction = FlowClient::Transaction.new
83
+ transaction.script = script
84
+ transaction.reference_block_id = get_latest_block.id
85
+ transaction.proposer_address = payer_account.address
86
+ transaction.proposer_key_index = 0
87
+ transaction.arguments = arguments
88
+ transaction.proposer_key_sequence_number = get_account(payer_account.address).keys.first.sequence_number
89
+ transaction.payer_address = payer_account.address
90
+ transaction.authorizer_addresses = [payer_account.address]
91
+ transaction.add_envelope_signature(payer_account.address, 0, signer)
92
+ res = send_transaction(transaction)
93
+
94
+ new_account = nil
95
+ wait_for_transaction(res.id) do |response|
96
+ raise CadenceRuntimeError, response.error_message if response.status_code != 0
97
+
98
+ event_payload = response.events.select { |e| e.type == "flow.AccountCreated" }.first.payload
99
+ payload_json = JSON.parse(event_payload)
100
+ new_account_address = payload_json["value"]["fields"][0]["value"]["value"]
101
+ new_account = get_account(new_account_address)
102
+ end
103
+
104
+ new_account
105
+ end
106
+
107
+ # Adds a public key to an account
108
+ def add_account_key(public_key_hex, payer_account, signer, weight)
109
+ script = File.read(File.join("lib", "cadence", "templates", "add-account-key.cdc"))
110
+
111
+ arguments = [
112
+ CadenceType.String(public_key_hex),
113
+ CadenceType.UFix64(weight)
114
+ ]
115
+
116
+ transaction = FlowClient::Transaction.new
117
+ transaction.script = script
118
+ transaction.reference_block_id = get_latest_block.id
119
+ transaction.proposer_address = payer_account.address
120
+ transaction.proposer_key_index = 0
121
+ transaction.arguments = arguments
122
+ transaction.proposer_key_sequence_number = get_account(payer_account.address).keys.first.sequence_number
123
+ transaction.payer_address = payer_account.address
124
+ transaction.authorizer_addresses = [payer_account.address]
125
+ transaction.add_envelope_signature(payer_account.address, 0, signer)
126
+ res = send_transaction(transaction)
127
+
128
+ wait_for_transaction(res.id) do |response|
129
+ raise CadenceRuntimeError, response.error_message if response.status_code != 0
130
+ end
131
+ end
132
+
133
+ # Adds a contract to an account
134
+ def add_contract(name, code, payer_account, signer)
135
+ script = File.read(File.join("lib", "cadence", "templates", "add-contract.cdc"))
136
+ code_hex = code.unpack1("H*")
137
+
138
+ arguments = [
139
+ CadenceType.String(name),
140
+ CadenceType.String(code_hex)
141
+ ]
142
+
143
+ transaction = FlowClient::Transaction.new
144
+ transaction.script = script
145
+ transaction.reference_block_id = get_latest_block.id
146
+ transaction.proposer_address = payer_account.address
147
+ transaction.proposer_key_index = 0
148
+ transaction.arguments = arguments
149
+ transaction.proposer_key_sequence_number = get_account(payer_account.address).keys.first.sequence_number
150
+ transaction.payer_address = payer_account.address
151
+ transaction.authorizer_addresses = [payer_account.address]
152
+ transaction.add_envelope_signature(payer_account.address, 0, signer)
153
+ res = send_transaction(transaction)
154
+
155
+ wait_for_transaction(res.id) do |response|
156
+ raise CadenceRuntimeError, response.error_message if response.status_code != 0
157
+ end
28
158
  end
29
159
 
30
- # Scripts
160
+ # Removes a contract from an account
161
+ def remove_contract(name, payer_account, signer)
162
+ script = File.read(File.join("lib", "cadence", "templates", "remove-contract.cdc"))
163
+
164
+ arguments = [
165
+ CadenceType.String(name),
166
+ ]
167
+
168
+ transaction = FlowClient::Transaction.new
169
+ transaction.script = script
170
+ transaction.reference_block_id = get_latest_block.id
171
+ transaction.proposer_address = payer_account.address
172
+ transaction.proposer_key_index = 0
173
+ transaction.arguments = arguments
174
+ transaction.proposer_key_sequence_number = get_account(payer_account.address).keys.first.sequence_number
175
+ transaction.payer_address = payer_account.address
176
+ transaction.authorizer_addresses = [payer_account.address]
177
+ transaction.add_envelope_signature(payer_account.address, 0, signer)
178
+ res = send_transaction(transaction)
179
+
180
+ wait_for_transaction(res.id) do |response|
181
+ raise CadenceRuntimeError, response.error_message if response.status_code != 0
182
+ end
183
+ end
184
+
185
+ # Updates a contract on an account
186
+ def update_contract(name, code, payer_account, signer)
187
+ script = File.read(File.join("lib", "cadence", "templates", "update-contract.cdc"))
188
+ code_hex = code.unpack1("H*")
189
+
190
+ arguments = [
191
+ CadenceType.String(name),
192
+ CadenceType.String(code_hex)
193
+ ]
194
+
195
+ transaction = FlowClient::Transaction.new
196
+ transaction.script = script
197
+ transaction.reference_block_id = get_latest_block.id
198
+ transaction.proposer_address = payer_account.address
199
+ transaction.proposer_key_index = 0
200
+ transaction.arguments = arguments
201
+ transaction.proposer_key_sequence_number = get_account(payer_account.address).keys.first.sequence_number
202
+ transaction.payer_address = payer_account.address
203
+ transaction.authorizer_addresses = [payer_account.address]
204
+ transaction.add_envelope_signature(payer_account.address, 0, signer)
205
+ res = send_transaction(transaction)
206
+
207
+ wait_for_transaction(res.id) do |response|
208
+ raise CadenceRuntimeError, response.error_message if response.status_code != 0
209
+ end
210
+ end
211
+
212
+ # :section: Scripts
213
+
214
+ # Executes a script on the blockchain
31
215
  def execute_script(script, args = [])
216
+ processed_args = []
217
+ args.to_a.each do |arg|
218
+ processed_arg = arg.class == OpenStruct ? Utils.openstruct_to_json(arg) : arg
219
+ processed_args << processed_arg
220
+ end
221
+
32
222
  req = Access::ExecuteScriptAtLatestBlockRequest.new(
33
223
  script: FlowClient::Utils.substitute_address_aliases(script, @address_aliases),
34
- arguments: args
224
+ arguments: processed_args
35
225
  )
226
+
36
227
  res = @stub.execute_script_at_latest_block(req)
37
228
  parse_json(res.value)
38
229
  end
39
230
 
40
- # Blocks
231
+ # :section: Blocks
232
+
233
+ # Returns the latest block
234
+ #
235
+ # @return [FlowClient::Block] the block
41
236
  def get_latest_block(is_sealed: true)
42
237
  req = Access::GetLatestBlockRequest.new(
43
238
  is_sealed: is_sealed
44
239
  )
240
+ res = @stub.get_latest_block(req)
241
+ Block.parse_grpc_block_response(res)
242
+ end
45
243
 
46
- @stub.get_latest_block(req)
244
+ # Returns the block with id
245
+ #
246
+ # @return [FlowClient::Block] the block
247
+ def get_block_by_id(id)
248
+ req = Access::GetBlockByIDRequest.new(
249
+ id: to_bytes(id)
250
+ )
251
+ res = @stub.get_block_by_id(req)
252
+ Block.parse_grpc_block_response(res)
47
253
  end
48
254
 
49
- # Events
255
+ # Returns the latest with height
256
+ #
257
+ # @param [Integer] block height
258
+ #
259
+ # @return [FlowClient::Block] the block
260
+ def get_block_by_height(height)
261
+ req = Access::GetBlockByHeightRequest.new(
262
+ height: height
263
+ )
264
+ res = @stub.get_block_by_height(req)
265
+ Block.parse_grpc_block_response(res)
266
+ end
267
+
268
+ # :section: Collections
269
+
270
+ # Returns the collection with id
271
+ #
272
+ # @param [String] collection id
273
+ #
274
+ # @return [FlowClient::Collection] the collection
275
+ def get_collection_by_id(id)
276
+ req = Access::GetCollectionByIDRequest.new(
277
+ id: to_bytes(id)
278
+ )
279
+ res = @stub.get_collection_by_id(req)
280
+ Collection.parse_grpc_type(res)
281
+ end
282
+
283
+ # :section: Events
284
+
285
+ # Returns events of the given type between the start and end block heights
286
+ #
287
+ # @param [String] event name
288
+ # @param [Integer] start block height
289
+ # @param [Integer] end block height
290
+ #
291
+ # @return [FlowClient::EventsResult] the events response
50
292
  def get_events(type, start_height, end_height)
51
293
  req = Access::GetEventsForHeightRangeRequest.new(
52
294
  type: type,
53
295
  start_height: start_height,
54
296
  end_height: end_height
55
297
  )
56
- @stub.get_events_for_height_range(req)
298
+ begin
299
+ res = @stub.get_events_for_height_range(req)
300
+ rescue GRPC::BadStatus => e
301
+ raise ClientError, e.details
302
+ else
303
+ res.results.map { |event| EventsResult.parse_grpc_type(event) }
304
+ end
57
305
  end
58
306
 
59
- # Transactions
307
+ # :section: Transactions
60
308
 
61
- # Send a FlowClient::Transaction transaction to the blockchain
309
+ # Sends a transaction to the blockchain
310
+ #
311
+ # @return [FlowClient::TransactionResponse] the transaction response
62
312
  def send_transaction(transaction)
63
313
  transaction.address_aliases = @address_aliases
64
314
  req = Access::SendTransactionRequest.new(
65
315
  transaction: transaction.to_protobuf_message
66
316
  )
67
- @stub.send_transaction(req)
317
+
318
+ begin
319
+ res = @stub.send_transaction(req)
320
+ rescue GRPC::BadStatus => e
321
+ raise ClientError, e.details
322
+ else
323
+ TransactionResponse.parse_grpc_type(res)
324
+ end
68
325
  end
69
326
 
327
+ # Returns the transaction with transaction_id
328
+ #
329
+ # @return [FlowClient::Transaction] the transaction
70
330
  def get_transaction(transaction_id)
71
331
  req = Access::GetTransactionRequest.new(
72
332
  id: to_bytes(transaction_id)
73
333
  )
74
- @stub.get_transaction(req)
334
+
335
+ begin
336
+ res = @stub.get_transaction(req)
337
+ rescue GRPC::BadStatus => e
338
+ raise ClientError, e.details
339
+ else
340
+ Transaction.parse_grpc_type(res.transaction)
341
+ end
75
342
  end
76
343
 
344
+ # Returns a transaction result
345
+ #
346
+ # @return [FlowClient::TransactionResult] the transaction result
77
347
  def get_transaction_result(transaction_id)
78
348
  req = Access::GetTransactionRequest.new(
79
349
  id: to_bytes(transaction_id)
80
350
  )
81
- @stub.get_transaction_result(req)
351
+
352
+ begin
353
+ res = @stub.get_transaction_result(req)
354
+ rescue GRPC::BadStatus => e
355
+ raise ClientError, e.details
356
+ else
357
+ TransactionResult.parse_grpc_type(res)
358
+ end
359
+ end
360
+
361
+ # Polls the blockchain for the transaction result until it is sealed
362
+ # or expired
363
+ def wait_for_transaction(transaction_id)
364
+ response = get_transaction_result(transaction_id)
365
+ while ![:SEALED, :EXPIRED].include? response.status
366
+ sleep(0.5)
367
+ response = get_transaction_result(transaction_id)
368
+ end
369
+
370
+ yield(response)
82
371
  end
83
372
 
84
373
  private
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowClient
4
+ class Collection
5
+ attr_accessor :id, :transaction_ids
6
+
7
+ def initialize
8
+ @id = nil
9
+ @transaction_ids = []
10
+ end
11
+
12
+ def self.parse_grpc_type(grpc_type)
13
+ collection = Collection.new
14
+ collection.id = grpc_type.collection.id.unpack1("H*")
15
+ collection.transaction_ids = grpc_type.collection.transaction_ids.to_a.map { |tid| tid.unpack1("H*") }
16
+ collection
17
+ end
18
+ end
19
+
20
+ class CollectionGuarantee
21
+ attr_accessor :collection_id, :signatures
22
+
23
+ def initialize
24
+ @collection_id = nil
25
+ @signatures = []
26
+ end
27
+
28
+ def self.parse_grpc_type(grpc_type)
29
+ collection_guarantee = CollectionGuarantee.new
30
+ collection_guarantee.collection_id = grpc_type.collection_id.unpack1("H*")
31
+ collection_guarantee.signatures = grpc_type.signatures.to_a.map { |s| s.unpack1("H*") }
32
+ collection_guarantee
33
+ end
34
+ end
35
+ end
@@ -3,31 +3,60 @@
3
3
  require "openssl"
4
4
 
5
5
  module FlowClient
6
+ # Crypto helpers
6
7
  class Crypto
7
- def self.sign(data, key)
8
- digest = OpenSSL::Digest.digest("SHA3-256", data)
9
- asn = key.dsa_sign_asn1(digest)
10
- asn1 = OpenSSL::ASN1.decode(asn)
11
- r, s = asn1.value
8
+ module Curves
9
+ P256 = "prime256v1"
10
+ SECP256K1 = "secp256k1"
11
+ end
12
+
13
+ module HashAlgos
14
+ SHA2_256 = "SHA2-256"
15
+ SHA3_256 = "SHA3-256"
16
+ end
17
+
18
+ # Sign data using the provided key
19
+ def self.sign(data, private_key_hex, hash_algo = HashAlgos::SHA3_256)
20
+ ssl_key = FlowClient::Crypto.key_from_hex_keys(private_key_hex)
21
+ # TODO: Fix this so that both hashing algos will work
22
+ asn = ssl_key.dsa_sign_asn1(OpenSSL::Digest.digest(hash_algo, data))
23
+ r, s = OpenSSL::ASN1.decode(asn).value
12
24
  combined_bytes = Utils.left_pad_bytes([r.value.to_s(16)].pack("H*").unpack("C*"), 32) +
13
25
  Utils.left_pad_bytes([s.value.to_s(16)].pack("H*").unpack("C*"), 32)
14
26
  combined_bytes.pack("C*")
15
27
  end
16
28
 
17
- # TODO: Handle both sig algos here
29
+ # Constructs an OpenSSL::PKey::EC key from an octet string
30
+ # keypair.
31
+ #
18
32
  # secp256k1
19
33
  # prime256v1
20
- def self.key_from_hex_keys(private_hex, public_hex)
21
- asn1 = OpenSSL::ASN1::Sequence(
22
- [
23
- OpenSSL::ASN1::Integer(1),
24
- OpenSSL::ASN1::OctetString([private_hex].pack("H*")),
25
- OpenSSL::ASN1::ObjectId("prime256v1", 0, :EXPLICIT),
26
- OpenSSL::ASN1::BitString([public_hex].pack("H*"), 1, :EXPLICIT)
27
- ]
28
- )
34
+ def self.key_from_hex_keys(private_hex, curve = Curves::P256)
35
+ group = OpenSSL::PKey::EC::Group.new(curve)
36
+ new_key = OpenSSL::PKey::EC.new(group)
37
+ new_key.private_key = OpenSSL::BN.new(private_hex, 16)
38
+ new_key.public_key = group.generator.mul(new_key.private_key)
39
+ new_key
40
+ end
29
41
 
30
- OpenSSL::PKey::EC.new(asn1.to_der)
42
+ # Returns an octet string keypair.
43
+ #
44
+ # Supported ECC curves are:
45
+ # Crypto::Curves::P256
46
+ # Crypto::Curves::SECP256K1
47
+ #
48
+ # The 04 prefix indicating that the public key is uncompressed is stripped.
49
+ # @see https://datatracker.ietf.org/doc/html/rfc5480
50
+ #
51
+ # Usage example:
52
+ # private_key, public_key = FlowClient::Crypto.generate_key_pair(FlowClient::Crypto::Curves::P256)
53
+ def self.generate_key_pair(curve = Curves::P256)
54
+ key = OpenSSL::PKey::EC.new(curve).generate_key
55
+ public_key = key.public_key.to_bn.to_s(16).downcase
56
+ [
57
+ key.private_key.to_s(16).downcase,
58
+ public_key[2..public_key.length]
59
+ ]
31
60
  end
32
61
  end
33
62
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowClient
4
+ class EventsResult
5
+ attr_accessor :block_id,
6
+ :block_height,
7
+ :events,
8
+ :block_timestamp
9
+
10
+ def initialize
11
+ @block_id = nil
12
+ @block_height = nil
13
+ @events = nil
14
+ @block_timestamp = nil
15
+ end
16
+
17
+ def self.parse_grpc_type(type)
18
+ event = EventsResult.new
19
+ event.block_id = type.block_id.unpack1("H*")
20
+ event.block_height = type.block_height
21
+ event.block_timestamp = FlowClient::Utils.parse_protobuf_timestamp(type.block_timestamp)
22
+ event.events = type.events.map { |event| FlowClient::Event.parse_grpc_type(event) }
23
+ event
24
+ end
25
+ end
26
+
27
+ class Event
28
+ attr_accessor :type,
29
+ :transaction_id,
30
+ :transaction_index,
31
+ :event_index,
32
+ :payload
33
+
34
+ def initialize
35
+ @type = nil
36
+ @transaction_id = nil
37
+ @transaction_index = nil
38
+ @event_index = nil
39
+ @payload = nil
40
+ end
41
+
42
+ def self.parse_grpc_type(type)
43
+ event = Event.new
44
+ event.type = type.type
45
+ event.transaction_id = type.transaction_id.unpack1("H*")
46
+ event.transaction_index = type.transaction_index
47
+ event.event_index = type.event_index
48
+ event.payload = type.payload
49
+ event
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowClient
4
+ class ProposalKey
5
+ attr_accessor :address,
6
+ :sequence_number,
7
+ :key_id
8
+
9
+ def initialize(address: nil, key_id: nil, sequence_number: nil)
10
+ @address = address
11
+ @sequence_number = sequence_number
12
+ @key_id = key_id
13
+ end
14
+
15
+ def self.parse_grpc_type(type)
16
+ signature = ProposalKey.new
17
+ signature.address = type.address.unpack1("H*")
18
+ signature.sequence_number = type.sequence_number
19
+ signature.key_id = type.key_id
20
+ signature
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowClient
4
+ class Signature
5
+ attr_accessor :address,
6
+ :signature,
7
+ :key_id
8
+
9
+ def initialize
10
+ @address = nil
11
+ @signature = nil
12
+ @key_id = nil
13
+ end
14
+
15
+ def self.parse_grpc_type(pb_signature)
16
+ signature = Signature.new
17
+ signature.address = pb_signature.address.unpack1("H*")
18
+ signature.signature = pb_signature.signature.unpack1("H*")
19
+ signature.key_id = pb_signature.key_id
20
+ signature
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowClient
4
+ # An abstract super class for the transaction signers. Subclasses must
5
+ # implement the sign method to sign transactions.
6
+ class Signer
7
+ def sign(data); end
8
+ end
9
+
10
+ # Implements a local singer using an in-memory key.
11
+ class LocalSigner < Signer
12
+ def initialize(private_key)
13
+ super()
14
+ @private_key = private_key
15
+ end
16
+
17
+ def sign(data)
18
+ super(data)
19
+ FlowClient::Crypto.sign(data, @private_key)
20
+ end
21
+ end
22
+ end