coinbase-sdk 0.0.2 → 0.0.3

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.
data/lib/coinbase/user.rb CHANGED
@@ -30,7 +30,9 @@ module Coinbase
30
30
  }
31
31
  opts = { create_wallet_request: create_wallet_request }
32
32
 
33
- model = wallets_api.create_wallet(opts)
33
+ model = Coinbase.call_api do
34
+ wallets_api.create_wallet(opts)
35
+ end
34
36
 
35
37
  Wallet.new(model)
36
38
  end
@@ -39,18 +41,115 @@ module Coinbase
39
41
  # @param data [Coinbase::Wallet::Data] the Wallet data to import
40
42
  # @return [Coinbase::Wallet] the imported Wallet
41
43
  def import_wallet(data)
42
- model = wallets_api.get_wallet(data.wallet_id)
43
- address_count = addresses_api.list_addresses(model.id).total_count
44
+ model = Coinbase.call_api do
45
+ wallets_api.get_wallet(data.wallet_id)
46
+ end
47
+
48
+ address_count = Coinbase.call_api do
49
+ addresses_api.list_addresses(model.id).total_count
50
+ end
51
+
44
52
  Wallet.new(model, seed: data.seed, address_count: address_count)
45
53
  end
46
54
 
47
55
  # Lists the IDs of the Wallets belonging to the User.
48
56
  # @return [Array<String>] the IDs of the Wallets belonging to the User
49
57
  def list_wallet_ids
50
- wallets = wallets_api.list_wallets
58
+ wallets = Coinbase.call_api do
59
+ wallets_api.list_wallets
60
+ end
61
+
51
62
  wallets.data.map(&:id)
52
63
  end
53
64
 
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.
69
+ # The default backup_file is `seeds.json` in the root folder. It can be configured by changing
70
+ # Coinbase.configuration.backup_file_path.
71
+ #
72
+ # @param wallet [Coinbase::Wallet] The wallet model to save.
73
+ # @param encrypt [bool] (Optional) Boolean representing whether the backup persisted to local file system should be
74
+ # encrypted or not. Data is unencrypted by default.
75
+ # @return [Coinbase::Wallet] the saved wallet.
76
+ def save_wallet(wallet, encrypt: false)
77
+ existing_seeds_in_store = existing_seeds
78
+ data = wallet.export
79
+ seed_to_store = data.seed
80
+ auth_tag = ''
81
+ iv = ''
82
+ if encrypt
83
+ shared_secret = store_encryption_key
84
+ cipher = OpenSSL::Cipher.new('aes-256-gcm').encrypt
85
+ cipher.key = OpenSSL::Digest.digest('SHA256', shared_secret)
86
+ iv = cipher.random_iv
87
+ cipher.iv = iv
88
+ cipher.auth_data = ''
89
+ encrypted_data = cipher.update(data.seed) + cipher.final
90
+ auth_tag = cipher.auth_tag.unpack1('H*')
91
+ iv = iv.unpack1('H*')
92
+ seed_to_store = encrypted_data.unpack1('H*')
93
+ end
94
+
95
+ existing_seeds_in_store[data.wallet_id] = {
96
+ seed: seed_to_store,
97
+ encrypted: encrypt,
98
+ auth_tag: auth_tag,
99
+ iv: iv
100
+ }
101
+
102
+ File.open(Coinbase.configuration.backup_file_path, 'w') do |file|
103
+ file.write(JSON.pretty_generate(existing_seeds_in_store))
104
+ end
105
+ wallet
106
+ end
107
+
108
+ # Loads all wallets belonging to the User with backup persisted to the local file system.
109
+ # @return [Map<String>Coinbase::Wallet] the map of wallet_ids to the wallets.
110
+ def load_wallets
111
+ existing_seeds_in_store = existing_seeds
112
+ raise ArgumentError, 'Backup file not found' if existing_seeds_in_store == {}
113
+
114
+ wallets = {}
115
+ existing_seeds_in_store.each do |wallet_id, seed_data|
116
+ seed = seed_data['seed']
117
+ raise ArgumentError, 'Malformed backup data' if seed.nil? || seed == ''
118
+
119
+ if seed_data['encrypted']
120
+ shared_secret = store_encryption_key
121
+ raise ArgumentError, 'Malformed encrypted seed data' if seed_data['iv'] == '' ||
122
+ seed_data['auth_tag'] == ''
123
+
124
+ cipher = OpenSSL::Cipher.new('aes-256-gcm').decrypt
125
+ cipher.key = OpenSSL::Digest.digest('SHA256', shared_secret)
126
+ iv = [seed_data['iv']].pack('H*')
127
+ cipher.iv = iv
128
+ auth_tag = [seed_data['auth_tag']].pack('H*')
129
+ cipher.auth_tag = auth_tag
130
+ cipher.auth_data = ''
131
+ hex_decoded_data = [seed_data['seed']].pack('H*')
132
+ seed = cipher.update(hex_decoded_data) + cipher.final
133
+ end
134
+
135
+ data = Coinbase::Wallet::Data.new(wallet_id: wallet_id, seed: seed)
136
+ wallets[wallet_id] = import_wallet(data)
137
+ end
138
+ wallets
139
+ end
140
+
141
+ # Returns a string representation of the User.
142
+ # @return [String] a string representation of the User
143
+ def to_s
144
+ "Coinbase::User{user_id: '#{user_id}'}"
145
+ end
146
+
147
+ # Same as to_s.
148
+ # @return [String] a string representation of the User
149
+ def inspect
150
+ to_s
151
+ end
152
+
54
153
  private
55
154
 
56
155
  def addresses_api
@@ -60,5 +159,22 @@ module Coinbase
60
159
  def wallets_api
61
160
  @wallets_api ||= Coinbase::Client::WalletsApi.new(Coinbase.configuration.api_client)
62
161
  end
162
+
163
+ def existing_seeds
164
+ existing_seed_data = '{}'
165
+ file_path = Coinbase.configuration.backup_file_path
166
+ existing_seed_data = File.read(file_path) if File.exist?(file_path)
167
+ output = JSON.parse(existing_seed_data)
168
+
169
+ raise ArgumentError, 'Malformed backup data' unless output.is_a?(Hash)
170
+
171
+ output
172
+ end
173
+
174
+ def store_encryption_key
175
+ pk = OpenSSL::PKey.read(Coinbase.configuration.api_key_private_key)
176
+ public_key = pk.public_key # use own public key to generate the shared secret.
177
+ pk.dh_compute_key(public_key)
178
+ end
63
179
  end
64
180
  end
@@ -68,7 +68,9 @@ module Coinbase
68
68
  attestation: attestation
69
69
  }
70
70
  }
71
- address_model = addresses_api.create_address(wallet_id, opts)
71
+ address_model = Coinbase.call_api do
72
+ addresses_api.create_address(wallet_id, opts)
73
+ end
72
74
 
73
75
  cache_address(address_model, key)
74
76
  end
@@ -95,7 +97,10 @@ module Coinbase
95
97
  # Returns the list of balances of this Wallet. Balances are aggregated across all Addresses in the Wallet.
96
98
  # @return [BalanceMap] The list of balances. The key is the Asset ID, and the value is the balance.
97
99
  def list_balances
98
- response = wallets_api.list_wallet_balances(wallet_id)
100
+ response = Coinbase.call_api do
101
+ wallets_api.list_wallet_balances(wallet_id)
102
+ end
103
+
99
104
  Coinbase.to_balance_map(response)
100
105
  end
101
106
 
@@ -109,7 +114,9 @@ module Coinbase
109
114
  asset_id
110
115
  end
111
116
 
112
- response = wallets_api.get_wallet_balance(wallet_id, normalized_asset_id.to_s)
117
+ response = Coinbase.call_api do
118
+ wallets_api.get_wallet_balance(wallet_id, normalized_asset_id.to_s)
119
+ end
113
120
 
114
121
  return BigDecimal('0') if response.nil?
115
122
 
@@ -154,6 +161,19 @@ module Coinbase
154
161
  Data.new(wallet_id: wallet_id, seed: @master.seed_hex)
155
162
  end
156
163
 
164
+ # Returns a String representation of the Wallet.
165
+ # @return [String] a String representation of the Wallet
166
+ def to_s
167
+ "Coinbase::Wallet{wallet_id: '#{wallet_id}', network_id: '#{network_id}', " \
168
+ "default_address: '#{default_address.address_id}'}"
169
+ end
170
+
171
+ # Same as to_s.
172
+ # @return [String] a String representation of the Wallet
173
+ def inspect
174
+ to_s
175
+ end
176
+
157
177
  # The data required to recreate a Wallet.
158
178
  class Data
159
179
  attr_reader :wallet_id, :seed
@@ -188,7 +208,9 @@ module Coinbase
188
208
  key = derive_key
189
209
 
190
210
  address_id = key.address.to_s
191
- address_model = addresses_api.get_address(wallet_id, address_id)
211
+ address_model = Coinbase.call_api do
212
+ addresses_api.get_address(wallet_id, address_id)
213
+ end
192
214
 
193
215
  cache_address(address_model, key)
194
216
  end
@@ -238,7 +260,9 @@ module Coinbase
238
260
 
239
261
  # Updates the Wallet model with the latest data.
240
262
  def update_model
241
- @model = wallets_api.get_wallet(wallet_id)
263
+ @model = Coinbase.call_api do
264
+ wallets_api.get_wallet(wallet_id)
265
+ end
242
266
  end
243
267
 
244
268
  def addresses_api
data/lib/coinbase.rb CHANGED
@@ -6,20 +6,27 @@ require_relative 'coinbase/authenticator'
6
6
  require_relative 'coinbase/balance_map'
7
7
  require_relative 'coinbase/client'
8
8
  require_relative 'coinbase/constants'
9
+ require_relative 'coinbase/errors'
10
+ require_relative 'coinbase/faucet_transaction'
9
11
  require_relative 'coinbase/middleware'
10
12
  require_relative 'coinbase/network'
11
13
  require_relative 'coinbase/transfer'
12
14
  require_relative 'coinbase/user'
13
15
  require_relative 'coinbase/wallet'
16
+ require 'json'
14
17
 
15
18
  # The Coinbase SDK.
16
19
  module Coinbase
17
20
  class InvalidConfiguration < StandardError; end
21
+ class FaucetLimitReached < StandardError; end
18
22
 
23
+ # Returns the configuration object.
24
+ # @return [Configuration] the configuration object
19
25
  def self.configuration
20
26
  @configuration ||= Configuration.new
21
27
  end
22
28
 
29
+ # Configures the Coinbase SDK.
23
30
  def self.configure
24
31
  yield(configuration)
25
32
 
@@ -27,22 +34,54 @@ module Coinbase
27
34
  raise InvalidConfiguration, 'API key name is not set' unless configuration.api_key_name
28
35
  end
29
36
 
30
- # Configuration object for the Coinbase SDK
37
+ # Configures the Coinbase SDK from the given CDP API Key JSON file.
38
+ # @param file_path [String] (Optional) the path to the CDP API Key JSON file
39
+ # file in the root directory by default.
40
+ def self.configure_from_json(file_path = 'coinbase_cloud_api_key.json')
41
+ configuration.from_json(file_path)
42
+
43
+ raise InvalidConfiguration, 'API key private key is not set' unless configuration.api_key_private_key
44
+ raise InvalidConfiguration, 'API key name is not set' unless configuration.api_key_name
45
+ end
46
+
47
+ # Configuration object for the Coinbase SDK.
31
48
  class Configuration
32
49
  attr_reader :base_sepolia_rpc_url, :base_sepolia_client
33
- attr_accessor :api_url, :api_key_name, :api_key_private_key
50
+ attr_accessor :api_url, :api_key_name, :api_key_private_key, :debug_api, :backup_file_path
34
51
 
52
+ # Initializes the configuration object.
35
53
  def initialize
36
54
  @base_sepolia_rpc_url = 'https://sepolia.base.org'
37
55
  @base_sepolia_client = Jimson::Client.new(@base_sepolia_rpc_url)
38
56
  @api_url = 'https://api.cdp.coinbase.com'
57
+ @debug_api = false
58
+ @backup_file_path = 'seeds.json'
39
59
  end
40
60
 
61
+ # Sets configuration values based on the provided CDP API Key JSON file.
62
+ # @param file_path [String] (Optional) the path to the CDP API Key JSON file
63
+ # file in the root directory by default.
64
+ def from_json(file_path = 'coinbase_cloud_api_key.json')
65
+ # Expand paths to respect shortcuts like ~.
66
+ file_path = File.expand_path(file_path)
67
+
68
+ raise InvalidConfiguration, 'Invalid configuration file type' unless file_path.end_with?('.json')
69
+
70
+ file = File.read(file_path)
71
+ data = JSON.parse(file)
72
+ @api_key_name = data['name']
73
+ @api_key_private_key = data['privateKey']
74
+ end
75
+
76
+ # Sets the Base Sepolia RPC URL.
77
+ # @param new_base_sepolia_rpc_url [String] the new Base Sepolia RPC URL
41
78
  def base_sepolia_rpc_url=(new_base_sepolia_rpc_url)
42
79
  @base_sepolia_rpc_url = new_base_sepolia_rpc_url
43
80
  @base_sepolia_client = Jimson::Client.new(@base_sepolia_rpc_url)
44
81
  end
45
82
 
83
+ # Returns the API client.
84
+ # @return [Coinbase::Client::ApiClient] the API client
46
85
  def api_client
47
86
  @api_client ||= Coinbase::Client::ApiClient.new(Middleware.config)
48
87
  end
@@ -69,10 +108,13 @@ module Coinbase
69
108
 
70
109
  address_balance_list.data.each do |balance|
71
110
  asset_id = Coinbase.to_sym(balance.asset.asset_id.downcase)
72
- amount = if asset_id == :eth
111
+ amount = case asset_id
112
+ when :eth
73
113
  BigDecimal(balance.amount) / BigDecimal(Coinbase::WEI_PER_ETHER)
74
- elsif asset_id == :usdc
114
+ when :usdc
75
115
  BigDecimal(balance.amount) / BigDecimal(Coinbase::ATOMIC_UNITS_PER_USDC)
116
+ when :weth
117
+ BigDecimal(balance.amount) / BigDecimal(Coinbase::WEI_PER_ETHER)
76
118
  else
77
119
  BigDecimal(balance.amount)
78
120
  end
@@ -82,9 +124,21 @@ module Coinbase
82
124
  BalanceMap.new(balances)
83
125
  end
84
126
 
127
+ # Loads the default user.
128
+ # @return [Coinbase::User] the default user
85
129
  def self.load_default_user
86
130
  users_api = Coinbase::Client::UsersApi.new(configuration.api_client)
87
131
  user_model = users_api.get_current_user
88
132
  Coinbase::User.new(user_model)
89
133
  end
134
+
135
+ # Wraps a call to the Platform API to ensure that the error is caught and
136
+ # wrapped as an APIError.
137
+ def self.call_api
138
+ yield
139
+ rescue Coinbase::Client::ApiError => e
140
+ raise Coinbase::APIError.from_error(e)
141
+ rescue StandardError => e
142
+ raise e
143
+ end
90
144
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coinbase-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
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-04-30 00:00:00.000000000 Z
11
+ date: 2024-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: eth
@@ -192,6 +192,20 @@ dependencies:
192
192
  - - '='
193
193
  - !ruby/object:Gem::Version
194
194
  version: 0.9.36
195
+ - !ruby/object:Gem::Dependency
196
+ name: yard-markdown
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
195
209
  description: Coinbase Ruby SDK for accessing Coinbase Platform APIs
196
210
  email: yuga.cohler@coinbase.com
197
211
  executables: []
@@ -216,10 +230,12 @@ files:
216
230
  - lib/coinbase/client/models/address_list.rb
217
231
  - lib/coinbase/client/models/asset.rb
218
232
  - lib/coinbase/client/models/balance.rb
233
+ - lib/coinbase/client/models/broadcast_transfer_request.rb
219
234
  - lib/coinbase/client/models/create_address_request.rb
220
235
  - lib/coinbase/client/models/create_transfer_request.rb
221
236
  - lib/coinbase/client/models/create_wallet_request.rb
222
237
  - lib/coinbase/client/models/error.rb
238
+ - lib/coinbase/client/models/faucet_transaction.rb
223
239
  - lib/coinbase/client/models/transfer.rb
224
240
  - lib/coinbase/client/models/transfer_list.rb
225
241
  - lib/coinbase/client/models/user.rb
@@ -227,6 +243,8 @@ files:
227
243
  - lib/coinbase/client/models/wallet_list.rb
228
244
  - lib/coinbase/client/version.rb
229
245
  - lib/coinbase/constants.rb
246
+ - lib/coinbase/errors.rb
247
+ - lib/coinbase/faucet_transaction.rb
230
248
  - lib/coinbase/middleware.rb
231
249
  - lib/coinbase/network.rb
232
250
  - lib/coinbase/transfer.rb