coinbase-sdk 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/lib/coinbase/address.rb +63 -44
  3. data/lib/coinbase/authenticator.rb +1 -1
  4. data/lib/coinbase/client/api/server_signers_api.rb +419 -0
  5. data/lib/coinbase/client/api/trades_api.rb +342 -0
  6. data/lib/coinbase/client/models/broadcast_trade_request.rb +222 -0
  7. data/lib/coinbase/client/models/create_address_request.rb +0 -14
  8. data/lib/coinbase/client/models/create_server_signer_request.rb +239 -0
  9. data/lib/coinbase/client/models/create_trade_request.rb +256 -0
  10. data/lib/coinbase/client/models/create_wallet_request.rb +1 -1
  11. data/lib/coinbase/client/models/create_wallet_request_wallet.rb +233 -0
  12. data/lib/coinbase/client/models/seed_creation_event.rb +240 -0
  13. data/lib/coinbase/client/models/seed_creation_event_result.rb +274 -0
  14. data/lib/coinbase/client/models/server_signer.rb +235 -0
  15. data/lib/coinbase/client/models/server_signer_event.rb +239 -0
  16. data/lib/coinbase/client/models/server_signer_event_event.rb +105 -0
  17. data/lib/coinbase/client/models/server_signer_event_list.rb +275 -0
  18. data/lib/coinbase/client/models/signature_creation_event.rb +363 -0
  19. data/lib/coinbase/client/models/signature_creation_event_result.rb +329 -0
  20. data/lib/coinbase/client/models/trade.rb +356 -0
  21. data/lib/coinbase/client/models/trade_list.rb +275 -0
  22. data/lib/coinbase/client/models/transaction.rb +294 -0
  23. data/lib/coinbase/client/models/transaction_type.rb +39 -0
  24. data/lib/coinbase/client/models/wallet.rb +55 -4
  25. data/lib/coinbase/client.rb +18 -0
  26. data/lib/coinbase/transfer.rb +21 -21
  27. data/lib/coinbase/user.rb +14 -89
  28. data/lib/coinbase/wallet.rb +176 -26
  29. data/lib/coinbase.rb +16 -4
  30. metadata +34 -2
@@ -17,6 +17,17 @@ module Coinbase
17
17
  # The maximum number of addresses in a Wallet.
18
18
  MAX_ADDRESSES = 20
19
19
 
20
+ # A representation of ServerSigner status in a Wallet.
21
+ module ServerSignerStatus
22
+ # The Wallet is awaiting seed creation by the ServerSigner. At this point,
23
+ # the Wallet cannot create addresses or sign transactions.
24
+ PENDING = 'pending_seed_creation'
25
+
26
+ # The Wallet has an associated seed created by the ServerSigner. It is ready
27
+ # to create addresses and sign transactions.
28
+ ACTIVE = 'active_seed'
29
+ end
30
+
20
31
  class << self
21
32
  # Imports a Wallet from previously exported wallet data.
22
33
  # @param data [Coinbase::Wallet::Data] the Wallet data to import
@@ -37,13 +48,18 @@ module Coinbase
37
48
 
38
49
  # Creates a new Wallet on the specified Network and generate a default address for it.
39
50
  # @param network_id [String] (Optional) the ID of the blockchain network. Defaults to 'base-sepolia'.
51
+ # @param interval_seconds [Integer] The interval at which to poll the CDPService for the Wallet to
52
+ # have an active seed, if using a ServerSigner, in seconds
53
+ # @param timeout_seconds [Integer] The maximum amount of time to wait for the ServerSigner to
54
+ # create a seed for the Wallet, in seconds
40
55
  # @return [Coinbase::Wallet] the new Wallet
41
- def create(network_id: 'base-sepolia')
56
+ def create(network_id: 'base-sepolia', interval_seconds: 0.2, timeout_seconds: 20)
42
57
  model = Coinbase.call_api do
43
58
  wallets_api.create_wallet(
44
59
  create_wallet_request: {
45
60
  wallet: {
46
- network_id: network_id
61
+ network_id: network_id,
62
+ use_server_signer: Coinbase.use_server_signer?
47
63
  }
48
64
  }
49
65
  )
@@ -51,13 +67,42 @@ module Coinbase
51
67
 
52
68
  wallet = new(model)
53
69
 
54
- wallet.create_address
70
+ # When used with a ServerSigner, the Signer must first register
71
+ # with the Wallet before addresses can be created.
72
+ wait_for_signer(wallet.id, interval_seconds, timeout_seconds) if Coinbase.use_server_signer?
55
73
 
74
+ wallet.create_address
56
75
  wallet
57
76
  end
58
77
 
59
78
  private
60
79
 
80
+ # Wait_for_signer waits until the ServerSigner has created a seed for the Wallet.
81
+ # Timeout::Error if the ServerSigner takes longer than the given timeout to create the seed.
82
+ # @param wallet_id [string] The ID of the Wallet that is awaiting seed creation.
83
+ # @param interval_seconds [Integer] The interval at which to poll the CDPService, in seconds
84
+ # @param timeout_seconds [Integer] The maximum amount of time to wait for the Signer to create a seed, in seconds
85
+ # @return [Wallet] The completed Wallet object that is ready to create addresses.
86
+ def wait_for_signer(wallet_id, interval_seconds, timeout_seconds)
87
+ start_time = Time.now
88
+
89
+ loop do
90
+ model = Coinbase.call_api do
91
+ wallets_api.get_wallet(wallet_id)
92
+ end
93
+
94
+ return self if model.server_signer_status == ServerSignerStatus::ACTIVE
95
+
96
+ if Time.now - start_time > timeout_seconds
97
+ raise Timeout::Error, 'Wallet creation timed out. Check status of your Server-Signer'
98
+ end
99
+
100
+ self.sleep interval_seconds
101
+ end
102
+
103
+ self
104
+ end
105
+
61
106
  # TODO: Memoize these objects in a thread-safe way at the top-level.
62
107
  def addresses_api
63
108
  Coinbase::Client::AddressesApi.new(Coinbase.configuration.api_client)
@@ -78,12 +123,15 @@ module Coinbase
78
123
  # with the Wallet. If not provided, the Wallet will derive the first default address.
79
124
  # @param client [Jimson::Client] (Optional) The JSON RPC client to use for interacting with the Network
80
125
  def initialize(model, seed: nil, address_models: [])
81
- validate_seed_and_address_models(seed, address_models)
126
+ validate_seed_and_address_models(seed, address_models) unless Coinbase.use_server_signer?
82
127
 
83
128
  @model = model
84
- @master = master_node(seed)
85
129
  @addresses = []
86
- @private_key_index = 0
130
+
131
+ unless Coinbase.use_server_signer?
132
+ @master = master_node(seed)
133
+ @private_key_index = 0
134
+ end
87
135
 
88
136
  derive_addresses(address_models)
89
137
  end
@@ -100,6 +148,12 @@ module Coinbase
100
148
  Coinbase.to_sym(@model.network_id)
101
149
  end
102
150
 
151
+ # Returns the ServerSigner Status of the Wallet.
152
+ # @return [Symbol] The ServerSigner Status
153
+ def server_signer_status
154
+ Coinbase.to_sym(@model.server_signer_status)
155
+ end
156
+
103
157
  # Sets the seed of the Wallet. This seed is used to derive keys and sign transactions.
104
158
  # @param seed [String] The seed to set. Expects a 32-byte hexadecimal with no 0x prefix.
105
159
  def seed=(seed)
@@ -121,16 +175,19 @@ module Coinbase
121
175
  # Creates a new Address in the Wallet.
122
176
  # @return [Address] The new Address
123
177
  def create_address
124
- key = derive_key
125
- attestation = create_attestation(key)
126
- public_key = key.public_key.compressed.unpack1('H*')
178
+ opts = { create_address_request: {} }
127
179
 
128
- opts = {
129
- create_address_request: {
130
- public_key: public_key,
131
- attestation: attestation
180
+ unless Coinbase.use_server_signer?
181
+ key = derive_key
182
+
183
+ opts = {
184
+ create_address_request: {
185
+ public_key: key.public_key.compressed.unpack1('H*'),
186
+ attestation: create_attestation(key)
187
+ }
132
188
  }
133
- }
189
+ end
190
+
134
191
  address_model = Coinbase.call_api do
135
192
  addresses_api.create_address(id, opts)
136
193
  end
@@ -177,30 +234,23 @@ module Coinbase
177
234
  Coinbase::Balance.from_model_and_asset_id(response, asset_id).amount
178
235
  end
179
236
 
180
- # Transfers the given amount of the given Asset to the given address. Only same-Network Transfers are supported.
181
- # Currently only the default_address is used to source the Transfer.
237
+ # Transfers the given amount of the given Asset to the specified address or wallet.
238
+ # Only same-network Transfers are supported. Currently only the default_address is used to source the Transfer.
182
239
  # @param amount [Integer, Float, BigDecimal] The amount of the Asset to send
183
240
  # @param asset_id [Symbol] The ID of the Asset to send
184
241
  # @param destination [Wallet | Address | String] The destination of the transfer. If a Wallet, sends to the Wallet's
185
242
  # default address. If a String, interprets it as the address ID.
186
243
  # @return [Transfer] The hash of the Transfer transaction.
187
244
  def transfer(amount, asset_id, destination)
188
- if destination.is_a?(Wallet)
189
- raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != network_id
190
-
191
- destination = destination.default_address.id
192
- elsif destination.is_a?(Address)
193
- raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != network_id
194
-
195
- destination = destination.id
196
- end
197
-
198
245
  default_address.transfer(amount, asset_id, destination)
199
246
  end
200
247
 
201
248
  # Exports the Wallet's data to a Data object.
202
249
  # @return [Data] The Wallet data
203
250
  def export
251
+ # TODO: Improve this check by relying on the backend data to decide whether a wallet is server-signer backed.
252
+ raise 'Cannot export data for Server-Signer backed Wallet' if Coinbase.use_server_signer?
253
+
204
254
  raise 'Cannot export Wallet without loaded seed' if @master.nil?
205
255
 
206
256
  Data.new(wallet_id: id, seed: @master.seed_hex)
@@ -223,6 +273,87 @@ module Coinbase
223
273
  !@master.nil?
224
274
  end
225
275
 
276
+ # Saves the seed of the Wallet to the given file. Wallets whose seeds are saved this way can be
277
+ # rehydrated using load_seed. A single file can be used for multiple Wallet seeds.
278
+ # This is an insecure method of storing Wallet seeds and should only be used for development purposes.
279
+ #
280
+ # @param file_path [String] The path of the file to save the seed to
281
+ # @param encrypt [bool] (Optional) Whether the seed information persisted to the local file system should be
282
+ # encrypted or not. Data is unencrypted by default.
283
+ # @return [String] A string indicating the success of the operation
284
+ def save_seed!(file_path, encrypt: false)
285
+ raise 'Wallet does not have seed loaded' if @master.nil?
286
+
287
+ existing_seeds_in_store = existing_seeds(file_path)
288
+
289
+ seed_to_store = @master.seed_hex
290
+ auth_tag = ''
291
+ iv = ''
292
+ if encrypt
293
+ cipher = OpenSSL::Cipher.new('aes-256-gcm').encrypt
294
+ cipher.key = OpenSSL::Digest.digest('SHA256', encryption_key)
295
+ iv = cipher.random_iv
296
+ cipher.iv = iv
297
+ cipher.auth_data = ''
298
+ encrypted_data = cipher.update(@master.seed_hex) + cipher.final
299
+ auth_tag = cipher.auth_tag.unpack1('H*')
300
+ iv = iv.unpack1('H*')
301
+ seed_to_store = encrypted_data.unpack1('H*')
302
+ end
303
+
304
+ existing_seeds_in_store[id] = {
305
+ seed: seed_to_store,
306
+ encrypted: encrypt,
307
+ auth_tag: auth_tag,
308
+ iv: iv
309
+ }
310
+
311
+ File.open(file_path, 'w') do |file|
312
+ file.write(JSON.pretty_generate(existing_seeds_in_store))
313
+ end
314
+
315
+ "Successfully saved seed for wallet #{id} to #{file_path}."
316
+ end
317
+
318
+ # Loads the seed of the Wallet from the given file.
319
+ # @param file_path [String] The path of the file to load the seed from
320
+ # @return [String] A string indicating the success of the operation
321
+ def load_seed(file_path)
322
+ raise 'Wallet already has seed loaded' unless @master.nil?
323
+
324
+ existing_seeds_in_store = existing_seeds(file_path)
325
+
326
+ raise ArgumentError, "File #{file_path} does not contain seed data" if existing_seeds_in_store == {}
327
+
328
+ if existing_seeds_in_store[id].nil?
329
+ raise ArgumentError, "File #{file_path} does not contain seed data for wallet #{id}"
330
+ end
331
+
332
+ seed_data = existing_seeds_in_store[id]
333
+ local_seed = seed_data['seed']
334
+
335
+ raise ArgumentError, 'Seed data is malformed' if local_seed.nil? || local_seed == ''
336
+
337
+ if seed_data['encrypted']
338
+ raise ArgumentError, 'Encrypted seed data is malformed' if seed_data['iv'] == '' ||
339
+ seed_data['auth_tag'] == ''
340
+
341
+ cipher = OpenSSL::Cipher.new('aes-256-gcm').decrypt
342
+ cipher.key = OpenSSL::Digest.digest('SHA256', encryption_key)
343
+ iv = [seed_data['iv']].pack('H*')
344
+ cipher.iv = iv
345
+ auth_tag = [seed_data['auth_tag']].pack('H*')
346
+ cipher.auth_tag = auth_tag
347
+ cipher.auth_data = ''
348
+ hex_decoded_data = [seed_data['seed']].pack('H*')
349
+ local_seed = cipher.update(hex_decoded_data) + cipher.final
350
+ end
351
+
352
+ self.seed = local_seed
353
+
354
+ "Successfully loaded seed for wallet #{id} from #{file_path}."
355
+ end
356
+
226
357
  # Returns a String representation of the Wallet.
227
358
  # @return [String] a String representation of the Wallet
228
359
  def to_s
@@ -390,6 +521,25 @@ module Coinbase
390
521
  raise ArgumentError, 'Seed must be empty if address_models are not provided'
391
522
  end
392
523
 
524
+ # Loads the Hash of Wallet seeds from the given file.
525
+ # @param file_path [String] The path of the file to load the seed from
526
+ # @return [Hash<String, Hash>] The Hash of from Wallet IDs to seed data
527
+ def existing_seeds(file_path)
528
+ existing_seed_data = '{}'
529
+ existing_seed_data = File.read(file_path) if File.exist?(file_path)
530
+ existing_seeds = JSON.parse(existing_seed_data)
531
+ raise ArgumentError, "#{file_path} is malformed, must be a valid JSON object" unless existing_seeds.is_a?(Hash)
532
+
533
+ existing_seeds
534
+ end
535
+
536
+ # Returns the shared secret to use for encrypting the seed.
537
+ def encryption_key
538
+ pk = OpenSSL::PKey.read(Coinbase.configuration.api_key_private_key)
539
+ public_key = pk.public_key # use own public key to generate the shared secret.
540
+ pk.dh_compute_key(public_key)
541
+ end
542
+
393
543
  def addresses_api
394
544
  @addresses_api ||= Coinbase::Client::AddressesApi.new(Coinbase.configuration.api_client)
395
545
  end
data/lib/coinbase.rb CHANGED
@@ -27,27 +27,33 @@ module Coinbase
27
27
  end
28
28
 
29
29
  # Configures the Coinbase SDK.
30
+ # @return [String] A string indicating successful configuration
30
31
  def self.configure
31
32
  yield(configuration)
32
33
 
33
34
  raise InvalidConfiguration, 'API key private key is not set' unless configuration.api_key_private_key
34
35
  raise InvalidConfiguration, 'API key name is not set' unless configuration.api_key_name
36
+
37
+ 'Successfully configured Coinbase SDK'
35
38
  end
36
39
 
37
40
  # Configures the Coinbase SDK from the given CDP API Key JSON file.
38
41
  # @param file_path [String] (Optional) the path to the CDP API Key JSON file
39
42
  # file in the root directory by default.
40
- def self.configure_from_json(file_path = 'coinbase_cloud_api_key.json')
43
+ # @return [String] A string indicating successful configuration
44
+ def self.configure_from_json(file_path = 'cdp_api_key.json')
41
45
  configuration.from_json(file_path)
42
46
 
43
47
  raise InvalidConfiguration, 'API key private key is not set' unless configuration.api_key_private_key
44
48
  raise InvalidConfiguration, 'API key name is not set' unless configuration.api_key_name
49
+
50
+ 'Successfully configured Coinbase SDK'
45
51
  end
46
52
 
47
53
  # Configuration object for the Coinbase SDK.
48
54
  class Configuration
49
55
  attr_reader :base_sepolia_rpc_url, :base_sepolia_client
50
- attr_accessor :api_url, :api_key_name, :api_key_private_key, :debug_api, :backup_file_path
56
+ attr_accessor :api_url, :api_key_name, :api_key_private_key, :debug_api, :use_server_signer
51
57
 
52
58
  # Initializes the configuration object.
53
59
  def initialize
@@ -55,13 +61,13 @@ module Coinbase
55
61
  @base_sepolia_client = Jimson::Client.new(@base_sepolia_rpc_url)
56
62
  @api_url = 'https://api.cdp.coinbase.com'
57
63
  @debug_api = false
58
- @backup_file_path = 'seeds.json'
64
+ @use_server_signer = false
59
65
  end
60
66
 
61
67
  # Sets configuration values based on the provided CDP API Key JSON file.
62
68
  # @param file_path [String] (Optional) the path to the CDP API Key JSON file
63
69
  # file in the root directory by default.
64
- def from_json(file_path = 'coinbase_cloud_api_key.json')
70
+ def from_json(file_path = 'cdp_api_key.json')
65
71
  # Expand paths to respect shortcuts like ~.
66
72
  file_path = File.expand_path(file_path)
67
73
 
@@ -117,4 +123,10 @@ module Coinbase
117
123
  rescue StandardError => e
118
124
  raise e
119
125
  end
126
+
127
+ # Returns whether to use a server signer to manage private keys.
128
+ # @return [bool] whether to use a server signer to manage private keys.
129
+ def self.use_server_signer?
130
+ Coinbase.configuration.use_server_signer
131
+ end
120
132
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coinbase-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuga Cohler
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-20 00:00:00.000000000 Z
11
+ date: 2024-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal
@@ -192,6 +192,20 @@ dependencies:
192
192
  - - '='
193
193
  - !ruby/object:Gem::Version
194
194
  version: 1.63.1
195
+ - !ruby/object:Gem::Dependency
196
+ name: simplecov
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
195
209
  - !ruby/object:Gem::Dependency
196
210
  name: yard
197
211
  requirement: !ruby/object:Gem::Requirement
@@ -234,6 +248,8 @@ files:
234
248
  - lib/coinbase/balance_map.rb
235
249
  - lib/coinbase/client.rb
236
250
  - lib/coinbase/client/api/addresses_api.rb
251
+ - lib/coinbase/client/api/server_signers_api.rb
252
+ - lib/coinbase/client/api/trades_api.rb
237
253
  - lib/coinbase/client/api/transfers_api.rb
238
254
  - lib/coinbase/client/api/users_api.rb
239
255
  - lib/coinbase/client/api/wallets_api.rb
@@ -245,12 +261,28 @@ files:
245
261
  - lib/coinbase/client/models/address_list.rb
246
262
  - lib/coinbase/client/models/asset.rb
247
263
  - lib/coinbase/client/models/balance.rb
264
+ - lib/coinbase/client/models/broadcast_trade_request.rb
248
265
  - lib/coinbase/client/models/broadcast_transfer_request.rb
249
266
  - lib/coinbase/client/models/create_address_request.rb
267
+ - lib/coinbase/client/models/create_server_signer_request.rb
268
+ - lib/coinbase/client/models/create_trade_request.rb
250
269
  - lib/coinbase/client/models/create_transfer_request.rb
251
270
  - lib/coinbase/client/models/create_wallet_request.rb
271
+ - lib/coinbase/client/models/create_wallet_request_wallet.rb
252
272
  - lib/coinbase/client/models/error.rb
253
273
  - lib/coinbase/client/models/faucet_transaction.rb
274
+ - lib/coinbase/client/models/seed_creation_event.rb
275
+ - lib/coinbase/client/models/seed_creation_event_result.rb
276
+ - lib/coinbase/client/models/server_signer.rb
277
+ - lib/coinbase/client/models/server_signer_event.rb
278
+ - lib/coinbase/client/models/server_signer_event_event.rb
279
+ - lib/coinbase/client/models/server_signer_event_list.rb
280
+ - lib/coinbase/client/models/signature_creation_event.rb
281
+ - lib/coinbase/client/models/signature_creation_event_result.rb
282
+ - lib/coinbase/client/models/trade.rb
283
+ - lib/coinbase/client/models/trade_list.rb
284
+ - lib/coinbase/client/models/transaction.rb
285
+ - lib/coinbase/client/models/transaction_type.rb
254
286
  - lib/coinbase/client/models/transfer.rb
255
287
  - lib/coinbase/client/models/transfer_list.rb
256
288
  - lib/coinbase/client/models/user.rb