coinbase-sdk 0.0.3 → 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: cac6b46a83009db9f165f0a2c4396122a2bf4bfa5bf895867b135695c346465e
4
- data.tar.gz: 0b8cfea979b389b9890a09104b8c7533e334873a789b19d7f704789cdcf6a70d
3
+ metadata.gz: 98cd32134ee8acb14c9dc018f39e7af188646b546998c4182cd50d7ae75f2816
4
+ data.tar.gz: df6ff19c63798ec10f054972aa75edef79fe65ae39c93cbe0941c77db251e5d6
5
5
  SHA512:
6
- metadata.gz: bccd3af15fec48789603988eadf3c67ad4ec67e5b3081fd2f06c7877d6ac35c6d2eda16db533ff149cf2f9f38d59ba38527284a0205529c84da6ac781dad8e3a
7
- data.tar.gz: 072d5372f38da2b9a3eb97a024f4a289ac7ef1906df8cf1da22de0b37c77e18b9a724db7771019fc9c6eb3834720f48010edb869f310dcc114429cb0e0bd1403
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
@@ -35,45 +35,40 @@ module Coinbase
35
35
 
36
36
  # Returns the Address ID.
37
37
  # @return [String] The Address ID
38
- def address_id
38
+ def id
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.
45
- def list_balances
53
+ def balances
46
54
  response = Coinbase.call_api do
47
- addresses_api.list_address_balances(wallet_id, address_id)
55
+ addresses_api.list_address_balances(wallet_id, id)
48
56
  end
49
57
 
50
- Coinbase.to_balance_map(response)
58
+ Coinbase::BalanceMap.from_balances(response.data)
51
59
  end
52
60
 
53
61
  # Returns the balance of the provided Asset.
54
62
  # @param asset_id [Symbol] The Asset to retrieve the balance for
55
63
  # @return [BigDecimal] The balance of the Asset
56
- def get_balance(asset_id)
57
- normalized_asset_id = normalize_asset_id(asset_id)
58
-
64
+ def balance(asset_id)
59
65
  response = Coinbase.call_api do
60
- addresses_api.get_address_balance(wallet_id, address_id, normalized_asset_id.to_s)
66
+ addresses_api.get_address_balance(wallet_id, id, Coinbase::Asset.primary_denomination(asset_id).to_s)
61
67
  end
62
68
 
63
69
  return BigDecimal('0') if response.nil?
64
70
 
65
- amount = BigDecimal(response.amount)
66
-
67
- case asset_id
68
- when :eth
69
- amount / BigDecimal(Coinbase::WEI_PER_ETHER.to_s)
70
- when :gwei
71
- amount / BigDecimal(Coinbase::GWEI_PER_ETHER.to_s)
72
- when :usdc
73
- amount / BigDecimal(Coinbase::ATOMIC_UNITS_PER_USDC.to_s)
74
- else
75
- amount
76
- end
71
+ Coinbase::Balance.from_model_and_asset_id(response, asset_id).amount
77
72
  end
78
73
 
79
74
  # Transfers the given amount of the given Asset to the given address. Only same-Network Transfers are supported.
@@ -83,36 +78,34 @@ module Coinbase
83
78
  # default address. If a String, interprets it as the address ID.
84
79
  # @return [String] The hash of the Transfer transaction.
85
80
  def transfer(amount, asset_id, destination)
86
- raise ArgumentError, "Unsupported asset: #{asset_id}" unless Coinbase::SUPPORTED_ASSET_IDS[asset_id]
81
+ raise 'Cannot transfer from address without private key loaded' if @key.nil?
82
+
83
+ raise ArgumentError, "Unsupported asset: #{asset_id}" unless Coinbase::Asset.supported?(asset_id)
87
84
 
88
85
  if destination.is_a?(Wallet)
89
86
  raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != network_id
90
87
 
91
- destination = destination.default_address.address_id
88
+ destination = destination.default_address.id
92
89
  elsif destination.is_a?(Address)
93
90
  raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != network_id
94
91
 
95
- destination = destination.address_id
92
+ destination = destination.id
96
93
  end
97
94
 
98
- current_balance = get_balance(asset_id)
95
+ current_balance = balance(asset_id)
99
96
  if current_balance < amount
100
97
  raise ArgumentError, "Insufficient funds: #{amount} requested, but only #{current_balance} available"
101
98
  end
102
99
 
103
- normalized_amount = normalize_asset_amount(amount, asset_id)
104
-
105
- normalized_asset_id = normalize_asset_id(asset_id)
106
-
107
100
  create_transfer_request = {
108
- amount: normalized_amount.to_i.to_s,
101
+ amount: Coinbase::Asset.to_atomic_amount(amount, asset_id).to_i.to_s,
109
102
  network_id: network_id,
110
- asset_id: normalized_asset_id.to_s,
103
+ asset_id: Coinbase::Asset.primary_denomination(asset_id).to_s,
111
104
  destination: destination
112
105
  }
113
106
 
114
107
  transfer_model = Coinbase.call_api do
115
- transfers_api.create_transfer(wallet_id, address_id, create_transfer_request)
108
+ transfers_api.create_transfer(wallet_id, id, create_transfer_request)
116
109
  end
117
110
 
118
111
  transfer = Coinbase::Transfer.new(transfer_model)
@@ -127,16 +120,22 @@ module Coinbase
127
120
  }
128
121
 
129
122
  transfer_model = Coinbase.call_api do
130
- transfers_api.broadcast_transfer(wallet_id, address_id, transfer.transfer_id, broadcast_transfer_request)
123
+ transfers_api.broadcast_transfer(wallet_id, id, transfer.id, broadcast_transfer_request)
131
124
  end
132
125
 
133
126
  Coinbase::Transfer.new(transfer_model)
134
127
  end
135
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
+
136
135
  # Returns a String representation of the Address.
137
136
  # @return [String] a String representation of the Address
138
137
  def to_s
139
- "Coinbase::Address{address_id: '#{address_id}', network_id: '#{network_id}', wallet_id: '#{wallet_id}'}"
138
+ "Coinbase::Address{id: '#{id}', network_id: '#{network_id}', wallet_id: '#{wallet_id}'}"
140
139
  end
141
140
 
142
141
  # Same as to_s.
@@ -148,75 +147,46 @@ module Coinbase
148
147
  # Requests funds for the address from the faucet and returns the faucet transaction.
149
148
  # This is only supported on testnet networks.
150
149
  # @return [Coinbase::FaucetTransaction] The successful faucet transaction
151
- # @raise [Coinbase::FaucetLimitReached] If the faucet limit has been reached for the address or user.
150
+ # @raise [Coinbase::FaucetLimitReachedError] If the faucet limit has been reached for the address or user.
152
151
  # @raise [Coinbase::Client::ApiError] If an unexpected error occurs while requesting faucet funds.
153
152
  def faucet
154
153
  Coinbase.call_api do
155
- Coinbase::FaucetTransaction.new(addresses_api.request_faucet_funds(wallet_id, address_id))
154
+ Coinbase::FaucetTransaction.new(addresses_api.request_faucet_funds(wallet_id, id))
156
155
  end
157
156
  end
158
157
 
159
158
  # Exports the Address's private key to a hex string.
160
159
  # @return [String] The Address's private key as a hex String
161
160
  def export
161
+ raise 'Cannot export key without private key loaded' if @key.nil?
162
+
162
163
  @key.private_hex
163
164
  end
164
165
 
165
- # Lists the IDs of all Transfers associated with the given Wallet and Address.
166
- # @return [Array<String>] The IDs of all Transfers belonging to the Wallet and Address
167
- def list_transfer_ids
168
- transfer_ids = []
166
+ # Returns all of the transfers associated with the address.
167
+ # @return [Array<Coinbase::Transfer>] The transfers associated with the address
168
+ def transfers
169
+ transfers = []
169
170
  page = nil
170
171
 
171
172
  loop do
173
+ puts "fetch transfers page: #{page}"
172
174
  response = Coinbase.call_api do
173
- transfers_api.list_transfers(wallet_id, address_id, { limit: 100, page: page })
175
+ transfers_api.list_transfers(wallet_id, id, { limit: 100, page: page })
174
176
  end
175
177
 
176
- transfer_ids.concat(response.data.map(&:transfer_id)) if response.data
178
+ transfers.concat(response.data.map { |transfer| Coinbase::Transfer.new(transfer) }) if response.data
177
179
 
178
180
  break unless response.has_more
179
181
 
180
182
  page = response.next_page
181
183
  end
182
184
 
183
- transfer_ids
185
+ transfers
184
186
  end
185
187
 
186
188
  private
187
189
 
188
- # Normalizes the amount of the Asset to send to the atomic unit.
189
- # @param amount [Integer, Float, BigDecimal] The amount to normalize
190
- # @param asset_id [Symbol] The ID of the Asset being transferred
191
- # @return [BigDecimal] The normalized amount in atomic units
192
- def normalize_asset_amount(amount, asset_id)
193
- big_amount = BigDecimal(amount.to_s)
194
-
195
- case asset_id
196
- when :eth
197
- big_amount * Coinbase::WEI_PER_ETHER
198
- when :gwei
199
- big_amount * Coinbase::WEI_PER_GWEI
200
- when :usdc
201
- big_amount * Coinbase::ATOMIC_UNITS_PER_USDC
202
- when :weth
203
- big_amount * Coinbase::WEI_PER_ETHER
204
- else
205
- big_amount
206
- end
207
- end
208
-
209
- # Normalizes the asset ID to use during requests.
210
- # @param asset_id [Symbol] The asset ID to normalize
211
- # @return [Symbol] The normalized asset ID
212
- def normalize_asset_id(asset_id)
213
- if %i[wei gwei].include?(asset_id)
214
- :eth
215
- else
216
- asset_id
217
- end
218
- end
219
-
220
190
  def addresses_api
221
191
  @addresses_api ||= Coinbase::Client::AddressesApi.new(Coinbase.configuration.api_client)
222
192
  end
@@ -3,7 +3,63 @@
3
3
  module Coinbase
4
4
  # A representation of an Asset.
5
5
  class Asset
6
- attr_reader :network_id, :asset_id, :display_name, :address_id
6
+ # Retuns whether the provided asset ID is supported.
7
+ # @param asset_id [Symbol] The Asset ID
8
+ # @return [Boolean] Whether the Asset ID is supported
9
+ def self.supported?(asset_id)
10
+ !!Coinbase::SUPPORTED_ASSET_IDS[asset_id]
11
+ end
12
+
13
+ # Converts the amount of the Asset to the atomic units of the primary denomination of the Asset.
14
+ # @param amount [Integer, Float, BigDecimal] The amount to normalize
15
+ # @param asset_id [Symbol] The ID of the Asset being transferred
16
+ # @return [BigDecimal] The normalized amount in atomic units
17
+ def self.to_atomic_amount(amount, asset_id)
18
+ case asset_id
19
+ when :eth
20
+ amount * BigDecimal(Coinbase::WEI_PER_ETHER.to_s)
21
+ when :gwei
22
+ amount * BigDecimal(Coinbase::WEI_PER_GWEI.to_s)
23
+ when :usdc
24
+ amount * BigDecimal(Coinbase::ATOMIC_UNITS_PER_USDC.to_s)
25
+ when :weth
26
+ amount * BigDecimal(Coinbase::WEI_PER_ETHER)
27
+ else
28
+ amount
29
+ end
30
+ end
31
+
32
+ # Converts an amount from the atomic value of the primary denomination of the provided Asset ID
33
+ # to whole units of the specified asset ID.
34
+ # @param atomic_amount [BigDecimal] The amount in atomic units
35
+ # @param asset_id [Symbol] The Asset ID
36
+ # @return [BigDecimal] The amount in whole units of the specified asset ID
37
+ def self.from_atomic_amount(atomic_amount, asset_id)
38
+ case asset_id
39
+ when :eth
40
+ atomic_amount / BigDecimal(Coinbase::WEI_PER_ETHER.to_s)
41
+ when :gwei
42
+ atomic_amount / BigDecimal(Coinbase::WEI_PER_GWEI.to_s)
43
+ when :usdc
44
+ atomic_amount / BigDecimal(Coinbase::ATOMIC_UNITS_PER_USDC.to_s)
45
+ when :weth
46
+ atomic_amount / BigDecimal(Coinbase::WEI_PER_ETHER)
47
+ else
48
+ atomic_amount
49
+ end
50
+ end
51
+
52
+ # Returns the primary denomination for the provided Asset ID.
53
+ # For assets with multiple denominations, e.g. eth can also be denominated in wei and gwei,
54
+ # this method will return the primary denomination.
55
+ # e.g. eth.
56
+ # @param asset_id [Symbol] The Asset ID
57
+ # @return [Symbol] The primary denomination for the Asset ID
58
+ def self.primary_denomination(asset_id)
59
+ return :eth if %i[wei gwei].include?(asset_id)
60
+
61
+ asset_id
62
+ end
7
63
 
8
64
  # Returns a new Asset object. Do not use this method. Instead, use the Asset constants defined in
9
65
  # the Coinbase module.
@@ -17,5 +73,20 @@ module Coinbase
17
73
  @display_name = display_name
18
74
  @address_id = address_id
19
75
  end
76
+
77
+ attr_reader :network_id, :asset_id, :display_name, :address_id
78
+
79
+ # Returns a string representation of the Asset.
80
+ # @return [String] a string representation of the Asset
81
+ def to_s
82
+ "Coinbase::Asset{network_id: '#{network_id}', asset_id: '#{asset_id}', display_name: '#{display_name}'" +
83
+ (address_id.nil? ? '}' : ", address_id: '#{address_id}'}")
84
+ end
85
+
86
+ # Same as to_s.
87
+ # @return [String] a string representation of the Balance
88
+ def inspect
89
+ to_s
90
+ end
20
91
  end
21
92
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coinbase
4
+ # A representation of an Balance.
5
+ class Balance
6
+ # Converts a Coinbase::Client::Balance model to a Coinbase::Balance
7
+ # @param balance_model [Coinbase::Client::Balance] The balance fetched from the API.
8
+ # @return [Balance] The converted Balance object.
9
+ def self.from_model(balance_model)
10
+ asset_id = Coinbase.to_sym(balance_model.asset.asset_id.downcase)
11
+
12
+ from_model_and_asset_id(balance_model, asset_id)
13
+ end
14
+
15
+ # Converts a Coinbase::Client::Balance model and asset ID to a Coinbase::Balance
16
+ # This can be used to specify a non-primary denomination that we want the balance
17
+ # to be converted to.
18
+ # @param balance_model [Coinbase::Client::Balance] The balance fetched from the API.
19
+ # @param asset_id [Symbol] The Asset ID of the denomination we want returned.
20
+ # @return [Balance] The converted Balance object.
21
+ def self.from_model_and_asset_id(balance_model, asset_id)
22
+ new(
23
+ amount: Coinbase::Asset.from_atomic_amount(BigDecimal(balance_model.amount), asset_id),
24
+ asset_id: asset_id
25
+ )
26
+ end
27
+
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
+ # @param asset_id [Symbol] The Asset ID
32
+ def initialize(amount:, asset_id:)
33
+ @amount = amount
34
+ @asset_id = asset_id
35
+ end
36
+
37
+ attr_reader :amount, :asset_id
38
+
39
+ # Returns a string representation of the Balance.
40
+ # @return [String] a string representation of the Balance
41
+ def to_s
42
+ "Coinbase::Balance{amount: '#{amount.to_i}', asset_id: '#{asset_id}'}"
43
+ end
44
+
45
+ # Same as to_s.
46
+ # @return [String] a string representation of the Balance
47
+ def inspect
48
+ to_s
49
+ end
50
+ end
51
+ end
@@ -5,31 +5,43 @@ require 'bigdecimal'
5
5
  module Coinbase
6
6
  # A convenience class for printing out Asset balances in a human-readable format.
7
7
  class BalanceMap < Hash
8
- # Returns a new BalanceMap object.
9
- # @param hash [Map<Symbol, BigDecimal>] The hash to initialize with
10
- def initialize(hash = {})
11
- super()
12
- hash.each do |key, value|
13
- self[key] = value
8
+ # Converts a list of Coinbase::Client::Balance models to a Coinbase::BalanceMap.
9
+ # @param balances [Array<Coinbase::Client::Balance>] The list of balances fetched from the API.
10
+ # @return [BalanceMap] The converted BalanceMap object.
11
+ def self.from_balances(balances)
12
+ BalanceMap.new.tap do |balance_map|
13
+ balances.each do |balance_model|
14
+ balance = Coinbase::Balance.from_model(balance_model)
15
+
16
+ balance_map.add(balance)
17
+ end
14
18
  end
15
19
  end
16
20
 
21
+ # Adds a balance to the map.
22
+ # @param balance [Coinbase::Balance] The balance to add to the map.
23
+ def add(balance)
24
+ raise ArgumentError, 'balance must be a Coinbase::Balance' unless balance.is_a?(Coinbase::Balance)
25
+
26
+ self[balance.asset_id] = balance.amount
27
+ end
28
+
17
29
  # Returns a string representation of the balance map.
18
- # @return [String] The string representation of the balance
30
+ # @return [String] The string representation of the balance map
19
31
  def to_s
20
32
  to_string
21
33
  end
22
34
 
23
35
  # Returns a string representation of the balance map.
24
- # @return [String] The string representation of the balance
36
+ # @return [String] The string representation of the balance map
25
37
  def inspect
26
38
  to_string
27
39
  end
28
40
 
29
41
  private
30
42
 
31
- # Returns a string representation of the balance.
32
- # @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
33
45
  def to_string
34
46
  result = {}
35
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
@@ -37,7 +37,7 @@ module Coinbase
37
37
 
38
38
  # Returns the Transfer ID.
39
39
  # @return [String] The Transfer ID
40
- def transfer_id
40
+ def id
41
41
  @model.transfer_id
42
42
  end
43
43
 
@@ -179,7 +179,7 @@ module Coinbase
179
179
  # Returns a String representation of the Transfer.
180
180
  # @return [String] a String representation of the Transfer
181
181
  def to_s
182
- "Coinbase::Transfer{transfer_id: '#{transfer_id}', network_id: '#{network_id}', " \
182
+ "Coinbase::Transfer{transfer_id: '#{id}', network_id: '#{network_id}', " \
183
183
  "from_address_id: '#{from_address_id}', destination_address_id: '#{destination_address_id}', " \
184
184
  "asset_id: '#{asset_id}', amount: '#{amount}', transaction_hash: '#{transaction_hash}', " \
185
185
  "transaction_link: '#{transaction_link}', status: '#{status}'}"
data/lib/coinbase/user.rb CHANGED
@@ -15,57 +15,63 @@ module Coinbase
15
15
 
16
16
  # Returns the User ID.
17
17
  # @return [String] the User ID
18
- def user_id
18
+ def id
19
19
  @model.id
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.
41
34
  # @param data [Coinbase::Wallet::Data] the Wallet data to import
42
35
  # @return [Coinbase::Wallet] the imported Wallet
43
36
  def import_wallet(data)
44
- model = Coinbase.call_api do
45
- wallets_api.get_wallet(data.wallet_id)
46
- end
37
+ Wallet.import(data)
38
+ end
39
+
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?
47
50
 
48
- address_count = Coinbase.call_api do
49
- addresses_api.list_addresses(model.id).total_count
51
+ wallet_list = Coinbase.call_api do
52
+ wallets_api.list_wallets(opts)
50
53
  end
51
54
 
52
- Wallet.new(model, seed: data.seed, address_count: address_count)
53
- end
55
+ # A map from wallet_id to address models.
56
+ address_model_map = {}
54
57
 
55
- # Lists the IDs of the Wallets belonging to the User.
56
- # @return [Array<String>] the IDs of the Wallets belonging to the User
57
- def list_wallet_ids
58
- wallets = Coinbase.call_api do
59
- wallets_api.list_wallets
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
60
64
  end
61
65
 
62
- wallets.data.map(&:id)
66
+ wallet_list.data.map do |wallet_model|
67
+ Wallet.new(wallet_model, seed: '', address_models: address_model_map[wallet_model.id])
68
+ end
63
69
  end
64
70
 
65
- # Saves a wallet to local file system. Wallet saved this way can be re-instantiated with `load_wallets` function,
66
- # provided the backup_file is available. This is an insecure method of storing wallet seeds and should only be used
67
- # for development purposes. If you call save_wallet twice with wallets containing the same wallet_id, the backup
68
- # will be overwritten during the second attempt.
71
+ # Saves a wallet to local file system. Wallet saved this way can be re-instantiated with load_wallets_from_local
72
+ # function, provided the backup_file is available. This is an insecure method of storing wallet seeds and should
73
+ # only be used for development purposes. If you call save_wallet_locally! twice with wallets containing the same
74
+ # wallet_id, the backup will be overwritten during the second attempt.
69
75
  # The default backup_file is `seeds.json` in the root folder. It can be configured by changing
70
76
  # Coinbase.configuration.backup_file_path.
71
77
  #
@@ -73,7 +79,7 @@ module Coinbase
73
79
  # @param encrypt [bool] (Optional) Boolean representing whether the backup persisted to local file system should be
74
80
  # encrypted or not. Data is unencrypted by default.
75
81
  # @return [Coinbase::Wallet] the saved wallet.
76
- def save_wallet(wallet, encrypt: false)
82
+ def save_wallet_locally!(wallet, encrypt: false)
77
83
  existing_seeds_in_store = existing_seeds
78
84
  data = wallet.export
79
85
  seed_to_store = data.seed
@@ -107,7 +113,7 @@ module Coinbase
107
113
 
108
114
  # Loads all wallets belonging to the User with backup persisted to the local file system.
109
115
  # @return [Map<String>Coinbase::Wallet] the map of wallet_ids to the wallets.
110
- def load_wallets
116
+ def load_wallets_from_local
111
117
  existing_seeds_in_store = existing_seeds
112
118
  raise ArgumentError, 'Backup file not found' if existing_seeds_in_store == {}
113
119
 
@@ -141,7 +147,7 @@ module Coinbase
141
147
  # Returns a string representation of the User.
142
148
  # @return [String] a string representation of the User
143
149
  def to_s
144
- "Coinbase::User{user_id: '#{user_id}'}"
150
+ "Coinbase::User{user_id: '#{id}'}"
145
151
  end
146
152
 
147
153
  # Same as to_s.
@@ -12,40 +12,85 @@ 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, :model
16
+
17
+ # The maximum number of addresses in a Wallet.
18
+ MAX_ADDRESSES = 20
19
+
20
+ class << self
21
+ # Imports a Wallet from previously exported wallet data.
22
+ # @param data [Coinbase::Wallet::Data] the Wallet data to import
23
+ # @return [Coinbase::Wallet] the imported Wallet
24
+ def import(data)
25
+ raise ArgumentError, 'data must be a Coinbase::Wallet::Data object' unless data.is_a?(Data)
26
+
27
+ model = Coinbase.call_api do
28
+ wallets_api.get_wallet(data.wallet_id)
29
+ end
30
+
31
+ address_list = Coinbase.call_api do
32
+ addresses_api.list_addresses(model.id, { limit: MAX_ADDRESSES })
33
+ end
34
+
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
57
+ end
58
+
59
+ private
60
+
61
+ # TODO: Memoize these objects in a thread-safe way at the top-level.
62
+ def addresses_api
63
+ Coinbase::Client::AddressesApi.new(Coinbase.configuration.api_client)
64
+ end
65
+
66
+ def wallets_api
67
+ Coinbase::Client::WalletsApi.new(Coinbase.configuration.api_client)
68
+ end
69
+ end
70
+
15
71
  # Returns a new Wallet object. Do not use this method directly. Instead, use User#create_wallet or
16
72
  # User#import_wallet.
17
73
  # @param model [Coinbase::Client::Wallet] The underlying Wallet object
18
74
  # @param seed [String] (Optional) The seed to use for the Wallet. Expects a 32-byte hexadecimal with no 0x prefix.
19
- # If not provided, a new seed will be generated.
20
- # @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.
21
79
  # @param client [Jimson::Client] (Optional) The JSON RPC client to use for interacting with the Network
22
- def initialize(model, seed: nil, address_count: 0)
23
- 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)
24
82
 
25
83
  @model = model
26
-
27
- @master = seed.nil? ? MoneyTree::Master.new : MoneyTree::Master.new(seed_hex: seed)
28
-
29
- # TODO: Make Network an argument to the constructor.
30
- @network_id = :base_sepolia
84
+ @master = master_node(seed)
31
85
  @addresses = []
86
+ @private_key_index = 0
32
87
 
33
- # TODO: Adjust derivation path prefix based on network protocol.
34
- @address_path_prefix = "m/44'/60'/0'/0"
35
- @address_index = 0
36
-
37
- if address_count.positive?
38
- address_count.times { derive_address }
39
- else
40
- create_address
41
- # Update the model to reflect the new default address.
42
- update_model
43
- end
88
+ derive_addresses(address_models)
44
89
  end
45
90
 
46
91
  # Returns the Wallet ID.
47
92
  # @return [String] The Wallet ID
48
- def wallet_id
93
+ def id
49
94
  @model.id
50
95
  end
51
96
 
@@ -55,6 +100,24 @@ module Coinbase
55
100
  Coinbase.to_sym(@model.network_id)
56
101
  end
57
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
+
58
121
  # Creates a new Address in the Wallet.
59
122
  # @return [Address] The new Address
60
123
  def create_address
@@ -69,69 +132,49 @@ module Coinbase
69
132
  }
70
133
  }
71
134
  address_model = Coinbase.call_api do
72
- addresses_api.create_address(wallet_id, opts)
135
+ addresses_api.create_address(id, opts)
73
136
  end
74
137
 
138
+ # Auto-reload wallet to set default address on first address creation.
139
+ reload if addresses.empty?
140
+
75
141
  cache_address(address_model, key)
76
142
  end
77
143
 
78
144
  # Returns the default address of the Wallet.
79
145
  # @return [Address] The default address
80
146
  def default_address
81
- @addresses.find { |address| address.address_id == @model.default_address.address_id }
147
+ address(@model.default_address&.address_id)
82
148
  end
83
149
 
84
150
  # Returns the Address with the given ID.
85
151
  # @param address_id [String] The ID of the Address to retrieve
86
152
  # @return [Address] The Address
87
- def get_address(address_id)
88
- @addresses.find { |address| address.address_id == address_id }
89
- end
90
-
91
- # Returns the list of Addresses in the Wallet.
92
- # @return [Array<Address>] The list of Addresses
93
- def list_addresses
94
- @addresses
153
+ def address(address_id)
154
+ @addresses.find { |address| address.id == address_id }
95
155
  end
96
156
 
97
157
  # Returns the list of balances of this Wallet. Balances are aggregated across all Addresses in the Wallet.
98
158
  # @return [BalanceMap] The list of balances. The key is the Asset ID, and the value is the balance.
99
- def list_balances
159
+ def balances
100
160
  response = Coinbase.call_api do
101
- wallets_api.list_wallet_balances(wallet_id)
161
+ wallets_api.list_wallet_balances(id)
102
162
  end
103
163
 
104
- Coinbase.to_balance_map(response)
164
+ Coinbase::BalanceMap.from_balances(response.data)
105
165
  end
106
166
 
107
167
  # Returns the balance of the provided Asset. Balances are aggregated across all Addresses in the Wallet.
108
168
  # @param asset_id [Symbol] The ID of the Asset to retrieve the balance for
109
169
  # @return [BigDecimal] The balance of the Asset
110
- def get_balance(asset_id)
111
- normalized_asset_id = if %i[wei gwei].include?(asset_id)
112
- :eth
113
- else
114
- asset_id
115
- end
116
-
170
+ def balance(asset_id)
117
171
  response = Coinbase.call_api do
118
- wallets_api.get_wallet_balance(wallet_id, normalized_asset_id.to_s)
172
+ wallets_api.get_wallet_balance(id, Coinbase::Asset.primary_denomination(asset_id).to_s)
119
173
  end
120
174
 
121
175
  return BigDecimal('0') if response.nil?
122
176
 
123
- amount = BigDecimal(response.amount)
124
-
125
- case asset_id
126
- when :eth
127
- amount / BigDecimal(Coinbase::WEI_PER_ETHER.to_s)
128
- when :gwei
129
- amount / BigDecimal(Coinbase::GWEI_PER_ETHER.to_s)
130
- when :usdc
131
- amount / BigDecimal(Coinbase::ATOMIC_UNITS_PER_USDC.to_s)
132
- else
133
- amount
134
- end
177
+ Coinbase::Balance.from_model_and_asset_id(response, asset_id).amount
135
178
  end
136
179
 
137
180
  # Transfers the given amount of the given Asset to the given address. Only same-Network Transfers are supported.
@@ -143,13 +186,13 @@ module Coinbase
143
186
  # @return [Transfer] The hash of the Transfer transaction.
144
187
  def transfer(amount, asset_id, destination)
145
188
  if destination.is_a?(Wallet)
146
- 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
147
190
 
148
- destination = destination.default_address.address_id
191
+ destination = destination.default_address.id
149
192
  elsif destination.is_a?(Address)
150
- 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
151
194
 
152
- destination = destination.address_id
195
+ destination = destination.id
153
196
  end
154
197
 
155
198
  default_address.transfer(amount, asset_id, destination)
@@ -158,14 +201,33 @@ module Coinbase
158
201
  # Exports the Wallet's data to a Data object.
159
202
  # @return [Data] The Wallet data
160
203
  def export
161
- Data.new(wallet_id: wallet_id, seed: @master.seed_hex)
204
+ raise 'Cannot export Wallet without loaded seed' if @master.nil?
205
+
206
+ Data.new(wallet_id: id, seed: @master.seed_hex)
207
+ end
208
+
209
+ # Requests funds from the faucet for the Wallet's default address and returns the faucet transaction.
210
+ # This is only supported on testnet networks.
211
+ # @return [Coinbase::FaucetTransaction] The successful faucet transaction
212
+ # @raise [Coinbase::FaucetLimitReachedError] If the faucet limit has been reached for the address or user.
213
+ # @raise [Coinbase::Client::ApiError] If an unexpected error occurs while requesting faucet funds.
214
+ def faucet
215
+ Coinbase.call_api do
216
+ Coinbase::FaucetTransaction.new(addresses_api.request_faucet_funds(id, default_address.id))
217
+ end
218
+ end
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?
162
224
  end
163
225
 
164
226
  # Returns a String representation of the Wallet.
165
227
  # @return [String] a String representation of the Wallet
166
228
  def to_s
167
- "Coinbase::Wallet{wallet_id: '#{wallet_id}', network_id: '#{network_id}', " \
168
- "default_address: '#{default_address.address_id}'}"
229
+ "Coinbase::Wallet{wallet_id: '#{id}', network_id: '#{network_id}', " \
230
+ "default_address: '#{@model.default_address&.address_id}'}"
169
231
  end
170
232
 
171
233
  # Same as to_s.
@@ -202,14 +264,55 @@ module Coinbase
202
264
 
203
265
  private
204
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
+
205
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
206
309
  # @return [Address] The new Address
207
- def derive_address
208
- key = derive_key
310
+ def derive_address(address_map, address_model)
311
+ key = @master.nil? ? nil : derive_key
209
312
 
210
- address_id = key.address.to_s
211
- address_model = Coinbase.call_api do
212
- addresses_api.get_address(wallet_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?
213
316
  end
214
317
 
215
318
  cache_address(address_model, key)
@@ -218,8 +321,11 @@ module Coinbase
218
321
  # Derives a key for an already registered Address in the Wallet.
219
322
  # @return [Eth::Key] The new key
220
323
  def derive_key
221
- 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}"
222
327
  private_key = @master.node_for_path(path).private_key.to_hex
328
+ @private_key_index += 1
223
329
  Eth::Key.new(priv: private_key)
224
330
  end
225
331
 
@@ -230,17 +336,29 @@ module Coinbase
230
336
  def cache_address(address_model, key)
231
337
  address = Address.new(address_model, key)
232
338
  @addresses << address
233
- @address_index += 1
234
339
  address
235
340
  end
236
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
+
237
355
  # Creates an attestation for the Address currently being created.
238
356
  # @param key [Eth::Key] The private key of the Address
239
357
  # @return [String] The attestation
240
358
  def create_attestation(key)
241
359
  public_key = key.public_key.compressed.unpack1('H*')
242
360
  payload = {
243
- wallet_id: wallet_id,
361
+ wallet_id: id,
244
362
  public_key: public_key
245
363
  }.to_json
246
364
  hashed_payload = Digest::SHA256.digest(payload)
@@ -258,11 +376,18 @@ module Coinbase
258
376
  new_signature_bytes.pack('C*').unpack1('H*')
259
377
  end
260
378
 
261
- # Updates the Wallet model with the latest data.
262
- def update_model
263
- @model = Coinbase.call_api do
264
- wallets_api.get_wallet(wallet_id)
265
- 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'
266
391
  end
267
392
 
268
393
  def addresses_api
data/lib/coinbase.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative 'coinbase/address'
4
4
  require_relative 'coinbase/asset'
5
5
  require_relative 'coinbase/authenticator'
6
+ require_relative 'coinbase/balance'
6
7
  require_relative 'coinbase/balance_map'
7
8
  require_relative 'coinbase/client'
8
9
  require_relative 'coinbase/constants'
@@ -18,7 +19,6 @@ require 'json'
18
19
  # The Coinbase SDK.
19
20
  module Coinbase
20
21
  class InvalidConfiguration < StandardError; end
21
- class FaucetLimitReached < StandardError; end
22
22
 
23
23
  # Returns the configuration object.
24
24
  # @return [Configuration] the configuration object
@@ -100,30 +100,6 @@ module Coinbase
100
100
  value.to_s.gsub('-', '_').to_sym
101
101
  end
102
102
 
103
- # Converts a Coinbase::Client::AddressBalanceList to a BalanceMap.
104
- # @param address_balance_list [Coinbase::Client::AddressBalanceList] The AddressBalanceList to convert
105
- # @return [BalanceMap] The converted BalanceMap
106
- def self.to_balance_map(address_balance_list)
107
- balances = {}
108
-
109
- address_balance_list.data.each do |balance|
110
- asset_id = Coinbase.to_sym(balance.asset.asset_id.downcase)
111
- amount = case asset_id
112
- when :eth
113
- BigDecimal(balance.amount) / BigDecimal(Coinbase::WEI_PER_ETHER)
114
- when :usdc
115
- BigDecimal(balance.amount) / BigDecimal(Coinbase::ATOMIC_UNITS_PER_USDC)
116
- when :weth
117
- BigDecimal(balance.amount) / BigDecimal(Coinbase::WEI_PER_ETHER)
118
- else
119
- BigDecimal(balance.amount)
120
- end
121
- balances[asset_id] = amount
122
- end
123
-
124
- BalanceMap.new(balances)
125
- end
126
-
127
103
  # Loads the default user.
128
104
  # @return [Coinbase::User] the default user
129
105
  def self.load_default_user
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.3
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-07 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
@@ -216,6 +230,7 @@ files:
216
230
  - lib/coinbase/address.rb
217
231
  - lib/coinbase/asset.rb
218
232
  - lib/coinbase/authenticator.rb
233
+ - lib/coinbase/balance.rb
219
234
  - lib/coinbase/balance_map.rb
220
235
  - lib/coinbase/client.rb
221
236
  - lib/coinbase/client/api/addresses_api.rb