coinbase-sdk 0.0.3 → 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: 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