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