flow_client 0.1.0 → 0.2.1

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