coinbase-sdk 0.0.5 → 0.0.6

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.
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