coinbase-sdk 0.0.4 → 0.0.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4413913708fadc44dcbb49a00a72fe55382187c8084531b9cec43894e0ee9a0
4
- data.tar.gz: e47f6295ce5f8bc1d8888b49ba71cdbc1558d606e708e66452f0ae6a76127249
3
+ metadata.gz: 98cd32134ee8acb14c9dc018f39e7af188646b546998c4182cd50d7ae75f2816
4
+ data.tar.gz: df6ff19c63798ec10f054972aa75edef79fe65ae39c93cbe0941c77db251e5d6
5
5
  SHA512:
6
- metadata.gz: acb6233add651f82063b7f2a73da8b7a8112687603d61fb77f3160713d813ea07447e25b5b69e83dd81c1852478c1b9999c1b207c9137ae08868db495721181b
7
- data.tar.gz: 3b72b12e92386e7114e0146f257032b904a4f26a2a97369bff19c1bc722b195372c830a2bb405e1a2f2655c81608beeee8d601deab293f87c1adf5fb18ca64e9
6
+ metadata.gz: a209d25aa72fc185fb667a5ef7b8de617341dba449b0a02b15ad95bc0c434140932353e12258f4665b35ef60e37b5346efca76db811cc5a308cfc69b907f37b8
7
+ data.tar.gz: 8856f048ea3ecb664559665e2422d60b6c12d607526aea4bea75f8e8273cf0584b2206e14dcb4e99bdca3436114b03961558be36029b40614d018028f9d41183
@@ -15,7 +15,7 @@ module Coinbase
15
15
  # Returns a new Address object. Do not use this method directly. Instead, use Wallet#create_address, or use
16
16
  # the Wallet's default_address.
17
17
  # @param model [Coinbase::Client::Address] The underlying Address object
18
- # @param key [Eth::Key] The key backing the Address
18
+ # @param key [Eth::Key] The key backing the Address. Can be nil.
19
19
  def initialize(model, key)
20
20
  @model = model
21
21
  @key = key
@@ -39,6 +39,14 @@ module Coinbase
39
39
  @model.address_id
40
40
  end
41
41
 
42
+ # Sets the private key backing the Address. This key is used to sign transactions.
43
+ # @param key [Eth::Key] The key backing the Address
44
+ def key=(key)
45
+ raise 'Private key is already set' unless @key.nil?
46
+
47
+ @key = key
48
+ end
49
+
42
50
  # Returns the balances of the Address.
43
51
  # @return [BalanceMap] The balances of the Address, keyed by asset ID. Ether balances are denominated
44
52
  # in ETH.
@@ -70,6 +78,8 @@ module Coinbase
70
78
  # default address. If a String, interprets it as the address ID.
71
79
  # @return [String] The hash of the Transfer transaction.
72
80
  def transfer(amount, asset_id, destination)
81
+ raise 'Cannot transfer from address without private key loaded' if @key.nil?
82
+
73
83
  raise ArgumentError, "Unsupported asset: #{asset_id}" unless Coinbase::Asset.supported?(asset_id)
74
84
 
75
85
  if destination.is_a?(Wallet)
@@ -116,6 +126,12 @@ module Coinbase
116
126
  Coinbase::Transfer.new(transfer_model)
117
127
  end
118
128
 
129
+ # Returns whether the Address has a private key backing it to sign transactions.
130
+ # @return [Boolean] Whether the Address has a private key backing it to sign transactions.
131
+ def can_sign?
132
+ !@key.nil?
133
+ end
134
+
119
135
  # Returns a String representation of the Address.
120
136
  # @return [String] a String representation of the Address
121
137
  def to_s
@@ -142,6 +158,8 @@ module Coinbase
142
158
  # Exports the Address's private key to a hex string.
143
159
  # @return [String] The Address's private key as a hex String
144
160
  def export
161
+ raise 'Cannot export key without private key loaded' if @key.nil?
162
+
145
163
  @key.private_hex
146
164
  end
147
165
 
@@ -25,12 +25,10 @@ module Coinbase
25
25
  )
26
26
  end
27
27
 
28
- # Returns a new Asset object. Do not use this method. Instead, use the Asset constants defined in
29
- # the Coinbase module.
30
- # @param network_id [Symbol] The ID of the Network to which the Asset belongs
28
+ # Returns a new Balance object. Do not use this method. Instead, use Balance.from_model or
29
+ # Balance.from_model_and_asset_id.
30
+ # @param amount [BigDecimal] The amount of the Asset
31
31
  # @param asset_id [Symbol] The Asset ID
32
- # @param display_name [String] The Asset's display name
33
- # @param address_id [String] (Optional) The Asset's address ID, if one exists
34
32
  def initialize(amount:, asset_id:)
35
33
  @amount = amount
36
34
  @asset_id = asset_id
@@ -27,21 +27,21 @@ module Coinbase
27
27
  end
28
28
 
29
29
  # Returns a string representation of the balance map.
30
- # @return [String] The string representation of the balance
30
+ # @return [String] The string representation of the balance map
31
31
  def to_s
32
32
  to_string
33
33
  end
34
34
 
35
35
  # Returns a string representation of the balance map.
36
- # @return [String] The string representation of the balance
36
+ # @return [String] The string representation of the balance map
37
37
  def inspect
38
38
  to_string
39
39
  end
40
40
 
41
41
  private
42
42
 
43
- # Returns a string representation of the balance.
44
- # @return [String] The string representation of the balance
43
+ # Returns a string representation of the balance map.
44
+ # @return [String] The string representation of the balance map
45
45
  def to_string
46
46
  result = {}
47
47
 
@@ -12,8 +12,11 @@ module Coinbase
12
12
  # Returns the default middleware configuration for the Coinbase SDK.
13
13
  def self.config
14
14
  Coinbase::Client::Configuration.default.tap do |config|
15
+ uri = URI(Coinbase.configuration.api_url)
16
+
15
17
  config.debugging = Coinbase.configuration.debug_api
16
- config.host = Coinbase.configuration.api_url
18
+ config.host = uri.host + (uri.port ? ":#{uri.port}" : '')
19
+ config.scheme = uri.scheme if uri.scheme
17
20
  config.request(:authenticator)
18
21
  end
19
22
  end
data/lib/coinbase/user.rb CHANGED
@@ -20,21 +20,14 @@ module Coinbase
20
20
  end
21
21
 
22
22
  # Creates a new Wallet belonging to the User.
23
+ # @param network_id [String] (Optional) the ID of the blockchain network. Defaults to 'base-sepolia'.
23
24
  # @return [Coinbase::Wallet] the new Wallet
24
- def create_wallet
25
- create_wallet_request = {
26
- wallet: {
27
- # TODO: Don't hardcode this.
28
- network_id: 'base-sepolia'
29
- }
30
- }
31
- opts = { create_wallet_request: create_wallet_request }
32
-
33
- model = Coinbase.call_api do
34
- wallets_api.create_wallet(opts)
35
- end
25
+ def create_wallet(create_wallet_options = {})
26
+ # For ruby 2.7 compatibility we cannot pass in keyword args when the create wallet
27
+ # options is empty
28
+ return Wallet.create if create_wallet_options.empty?
36
29
 
37
- Wallet.new(model)
30
+ Wallet.create(**create_wallet_options)
38
31
  end
39
32
 
40
33
  # Imports a Wallet belonging to the User.
@@ -44,14 +37,35 @@ module Coinbase
44
37
  Wallet.import(data)
45
38
  end
46
39
 
47
- # Lists the IDs of the Wallets belonging to the User.
48
- # @return [Array<String>] the IDs of the Wallets belonging to the User
49
- def wallet_ids
50
- wallets = Coinbase.call_api do
51
- wallets_api.list_wallets
40
+ # Lists the Wallets belonging to the User.
41
+ # @param page_size [Integer] (Optional) the number of Wallets to return per page. Defaults to 10
42
+ # @param next_page_token [String] (Optional) the token for the next page of Wallets
43
+ # @return [Coinbase::Wallet] the Wallets belonging to the User
44
+ def wallets(page_size: 10, next_page_token: nil)
45
+ opts = {
46
+ limit: page_size
47
+ }
48
+
49
+ opts[:page] = next_page_token unless next_page_token.nil?
50
+
51
+ wallet_list = Coinbase.call_api do
52
+ wallets_api.list_wallets(opts)
52
53
  end
53
54
 
54
- wallets.data.map(&:id)
55
+ # A map from wallet_id to address models.
56
+ address_model_map = {}
57
+
58
+ wallet_list.data.each do |wallet_model|
59
+ addresses_list = Coinbase.call_api do
60
+ addresses_api.list_addresses(wallet_model.id, { limit: Coinbase::Wallet::MAX_ADDRESSES })
61
+ end
62
+
63
+ address_model_map[wallet_model.id] = addresses_list.data
64
+ end
65
+
66
+ wallet_list.data.map do |wallet_model|
67
+ Wallet.new(wallet_model, seed: '', address_models: address_model_map[wallet_model.id])
68
+ end
55
69
  end
56
70
 
57
71
  # Saves a wallet to local file system. Wallet saved this way can be re-instantiated with load_wallets_from_local
@@ -12,7 +12,10 @@ module Coinbase
12
12
  # list their balances, and transfer Assets to other Addresses. Wallets should be created through User#create_wallet or
13
13
  # User#import_wallet.
14
14
  class Wallet
15
- attr_reader :addresses
15
+ attr_reader :addresses, :model
16
+
17
+ # The maximum number of addresses in a Wallet.
18
+ MAX_ADDRESSES = 20
16
19
 
17
20
  class << self
18
21
  # Imports a Wallet from previously exported wallet data.
@@ -25,12 +28,32 @@ module Coinbase
25
28
  wallets_api.get_wallet(data.wallet_id)
26
29
  end
27
30
 
28
- # TODO: Pass these addresses in directly
29
- address_count = Coinbase.call_api do
30
- addresses_api.list_addresses(model.id).total_count
31
+ address_list = Coinbase.call_api do
32
+ addresses_api.list_addresses(model.id, { limit: MAX_ADDRESSES })
31
33
  end
32
34
 
33
- new(model, seed: data.seed, address_count: address_count)
35
+ new(model, seed: data.seed, address_models: address_list.data)
36
+ end
37
+
38
+ # Creates a new Wallet on the specified Network and generate a default address for it.
39
+ # @param network_id [String] (Optional) the ID of the blockchain network. Defaults to 'base-sepolia'.
40
+ # @return [Coinbase::Wallet] the new Wallet
41
+ def create(network_id: 'base-sepolia')
42
+ model = Coinbase.call_api do
43
+ wallets_api.create_wallet(
44
+ create_wallet_request: {
45
+ wallet: {
46
+ network_id: network_id
47
+ }
48
+ }
49
+ )
50
+ end
51
+
52
+ wallet = new(model)
53
+
54
+ wallet.create_address
55
+
56
+ wallet
34
57
  end
35
58
 
36
59
  private
@@ -49,31 +72,20 @@ module Coinbase
49
72
  # User#import_wallet.
50
73
  # @param model [Coinbase::Client::Wallet] The underlying Wallet object
51
74
  # @param seed [String] (Optional) The seed to use for the Wallet. Expects a 32-byte hexadecimal with no 0x prefix.
52
- # If not provided, a new seed will be generated.
53
- # @param address_count [Integer] (Optional) The number of addresses already registered for the Wallet.
75
+ # If nil, a new seed will be generated. If the empty string, no seed is generated, and the Wallet will be
76
+ # instantiated without a seed and its corresponding private keys.
77
+ # @param address_models [Array<Coinbase::Client::Address>] (Optional) The models of the addresses already registered
78
+ # with the Wallet. If not provided, the Wallet will derive the first default address.
54
79
  # @param client [Jimson::Client] (Optional) The JSON RPC client to use for interacting with the Network
55
- def initialize(model, seed: nil, address_count: 0)
56
- raise ArgumentError, 'Seed must be 32 bytes' if !seed.nil? && seed.length != 64
80
+ def initialize(model, seed: nil, address_models: [])
81
+ validate_seed_and_address_models(seed, address_models)
57
82
 
58
83
  @model = model
59
-
60
- @master = seed.nil? ? MoneyTree::Master.new : MoneyTree::Master.new(seed_hex: seed)
61
-
62
- # TODO: Make Network an argument to the constructor.
63
- @network_id = :base_sepolia
84
+ @master = master_node(seed)
64
85
  @addresses = []
86
+ @private_key_index = 0
65
87
 
66
- # TODO: Adjust derivation path prefix based on network protocol.
67
- @address_path_prefix = "m/44'/60'/0'/0"
68
- @address_index = 0
69
-
70
- if address_count.positive?
71
- address_count.times { derive_address }
72
- else
73
- create_address
74
- # Update the model to reflect the new default address.
75
- update_model
76
- end
88
+ derive_addresses(address_models)
77
89
  end
78
90
 
79
91
  # Returns the Wallet ID.
@@ -88,6 +100,24 @@ module Coinbase
88
100
  Coinbase.to_sym(@model.network_id)
89
101
  end
90
102
 
103
+ # Sets the seed of the Wallet. This seed is used to derive keys and sign transactions.
104
+ # @param seed [String] The seed to set. Expects a 32-byte hexadecimal with no 0x prefix.
105
+ def seed=(seed)
106
+ raise ArgumentError, 'Seed must be 32 bytes' if seed.length != 64
107
+ raise 'Seed is already set' unless @master.nil?
108
+ raise 'Cannot set seed for Wallet with non-zero private key index' if @private_key_index.positive?
109
+
110
+ @master = MoneyTree::Master.new(seed_hex: seed)
111
+
112
+ @addresses.each do
113
+ key = derive_key
114
+ a = address(key.address.to_s)
115
+ raise "Seed does not match wallet; cannot find address #{key.address}" if a.nil?
116
+
117
+ a.key = key
118
+ end
119
+ end
120
+
91
121
  # Creates a new Address in the Wallet.
92
122
  # @return [Address] The new Address
93
123
  def create_address
@@ -105,13 +135,16 @@ module Coinbase
105
135
  addresses_api.create_address(id, opts)
106
136
  end
107
137
 
138
+ # Auto-reload wallet to set default address on first address creation.
139
+ reload if addresses.empty?
140
+
108
141
  cache_address(address_model, key)
109
142
  end
110
143
 
111
144
  # Returns the default address of the Wallet.
112
145
  # @return [Address] The default address
113
146
  def default_address
114
- address(@model.default_address.address_id)
147
+ address(@model.default_address&.address_id)
115
148
  end
116
149
 
117
150
  # Returns the Address with the given ID.
@@ -153,11 +186,11 @@ module Coinbase
153
186
  # @return [Transfer] The hash of the Transfer transaction.
154
187
  def transfer(amount, asset_id, destination)
155
188
  if destination.is_a?(Wallet)
156
- raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != @network_id
189
+ raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != network_id
157
190
 
158
191
  destination = destination.default_address.id
159
192
  elsif destination.is_a?(Address)
160
- raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != @network_id
193
+ raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != network_id
161
194
 
162
195
  destination = destination.id
163
196
  end
@@ -168,6 +201,8 @@ module Coinbase
168
201
  # Exports the Wallet's data to a Data object.
169
202
  # @return [Data] The Wallet data
170
203
  def export
204
+ raise 'Cannot export Wallet without loaded seed' if @master.nil?
205
+
171
206
  Data.new(wallet_id: id, seed: @master.seed_hex)
172
207
  end
173
208
 
@@ -182,11 +217,17 @@ module Coinbase
182
217
  end
183
218
  end
184
219
 
220
+ # Returns whether the Wallet has a seed with which to derive keys and sign transactions.
221
+ # @return [Boolean] Whether the Wallet has a seed with which to derive keys and sign transactions.
222
+ def can_sign?
223
+ !@master.nil?
224
+ end
225
+
185
226
  # Returns a String representation of the Wallet.
186
227
  # @return [String] a String representation of the Wallet
187
228
  def to_s
188
229
  "Coinbase::Wallet{wallet_id: '#{id}', network_id: '#{network_id}', " \
189
- "default_address: '#{default_address.id}'}"
230
+ "default_address: '#{@model.default_address&.address_id}'}"
190
231
  end
191
232
 
192
233
  # Same as to_s.
@@ -223,14 +264,55 @@ module Coinbase
223
264
 
224
265
  private
225
266
 
267
+ # Reloads the Wallet with the latest data.
268
+ def reload
269
+ @model = Coinbase.call_api do
270
+ wallets_api.get_wallet(id)
271
+ end
272
+ end
273
+
274
+ def master_node(seed)
275
+ return MoneyTree::Master.new if seed.nil?
276
+ return nil if seed.empty?
277
+
278
+ MoneyTree::Master.new(seed_hex: seed)
279
+ end
280
+
281
+ def address_path_prefix
282
+ # TODO: Add support for other networks.
283
+ @address_path_prefix ||= case network_id.to_s.split('_').first
284
+ when 'base'
285
+ "m/44'/60'/0'/0"
286
+ else
287
+ raise ArgumentError, "Unsupported network ID: #{network_id}"
288
+ end
289
+ end
290
+
291
+ # Derives the registered Addresses in the Wallet.
292
+ # @param address_models [Array<Coinbase::Client::Address>] The models of the addresses already registered with the
293
+ # Wallet
294
+ def derive_addresses(address_models)
295
+ return unless address_models.any?
296
+
297
+ # Create a map tracking which addresses are already registered with the Wallet.
298
+ address_map = build_address_map(address_models)
299
+
300
+ address_models.each do |address_model|
301
+ # Derive the addresses using the provided models.
302
+ derive_address(address_map, address_model)
303
+ end
304
+ end
305
+
226
306
  # Derives an already registered Address in the Wallet.
307
+ # @param address_map [Hash<String, Boolean>] The map of registered Address IDs
308
+ # @param address_model [Coinbase::Client::Address] The Address model
227
309
  # @return [Address] The new Address
228
- def derive_address
229
- key = derive_key
310
+ def derive_address(address_map, address_model)
311
+ key = @master.nil? ? nil : derive_key
230
312
 
231
- address_id = key.address.to_s
232
- address_model = Coinbase.call_api do
233
- addresses_api.get_address(id, address_id)
313
+ unless key.nil?
314
+ address_from_key = key.address.to_s
315
+ raise 'Invalid address' if address_map[address_from_key].nil?
234
316
  end
235
317
 
236
318
  cache_address(address_model, key)
@@ -239,8 +321,11 @@ module Coinbase
239
321
  # Derives a key for an already registered Address in the Wallet.
240
322
  # @return [Eth::Key] The new key
241
323
  def derive_key
242
- path = "#{@address_path_prefix}/#{@address_index}"
324
+ raise 'Cannot derive key for Wallet without seed loaded' if @master.nil?
325
+
326
+ path = "#{address_path_prefix}/#{@private_key_index}"
243
327
  private_key = @master.node_for_path(path).private_key.to_hex
328
+ @private_key_index += 1
244
329
  Eth::Key.new(priv: private_key)
245
330
  end
246
331
 
@@ -251,10 +336,22 @@ module Coinbase
251
336
  def cache_address(address_model, key)
252
337
  address = Address.new(address_model, key)
253
338
  @addresses << address
254
- @address_index += 1
255
339
  address
256
340
  end
257
341
 
342
+ # Builds a Hash of the registered Addresses.
343
+ # @param address_models [Array<Coinbase::Client::Address>] The models of the addresses already registered with the
344
+ # Wallet
345
+ # @return [Hash<String, Boolean>] The Hash of registered Addresses
346
+ def build_address_map(address_models)
347
+ address_map = {}
348
+ address_models.each do |address_model|
349
+ address_map[address_model.address_id] = true
350
+ end
351
+
352
+ address_map
353
+ end
354
+
258
355
  # Creates an attestation for the Address currently being created.
259
356
  # @param key [Eth::Key] The private key of the Address
260
357
  # @return [String] The attestation
@@ -279,11 +376,18 @@ module Coinbase
279
376
  new_signature_bytes.pack('C*').unpack1('H*')
280
377
  end
281
378
 
282
- # Updates the Wallet model with the latest data.
283
- def update_model
284
- @model = Coinbase.call_api do
285
- wallets_api.get_wallet(id)
286
- end
379
+ # Validates the seed and address models passed to the constructor.
380
+ # @param seed [String] The seed to use for the Wallet
381
+ # @param address_models [Array<Coinbase::Client::Address>] The models of the addresses already registered with the
382
+ # Wallet
383
+ def validate_seed_and_address_models(seed, address_models)
384
+ raise ArgumentError, 'Seed must be 32 bytes' if !seed.nil? && !seed.empty? && seed.length != 64
385
+
386
+ raise ArgumentError, 'Seed must be present if address_models are provided' if seed.nil? && address_models.any?
387
+
388
+ return unless !seed.nil? && seed.empty? && address_models.empty?
389
+
390
+ raise ArgumentError, 'Seed must be empty if address_models are not provided'
287
391
  end
288
392
 
289
393
  def addresses_api
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coinbase-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
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-13 00:00:00.000000000 Z
11
+ date: 2024-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bigdecimal
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: eth
15
29
  requirement: !ruby/object:Gem::Requirement