coinbase-sdk 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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