coinbase-sdk 0.0.1 → 0.0.2

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/coinbase/address.rb +88 -49
  3. data/lib/coinbase/asset.rb +2 -1
  4. data/lib/coinbase/authenticator.rb +52 -0
  5. data/lib/coinbase/balance_map.rb +1 -1
  6. data/lib/coinbase/client/api/addresses_api.rb +385 -0
  7. data/lib/coinbase/client/api/transfers_api.rb +256 -0
  8. data/lib/coinbase/client/api/users_api.rb +79 -0
  9. data/lib/coinbase/client/api/wallets_api.rb +348 -0
  10. data/lib/coinbase/client/api_client.rb +431 -0
  11. data/lib/coinbase/client/api_error.rb +58 -0
  12. data/lib/coinbase/client/configuration.rb +375 -0
  13. data/lib/coinbase/client/models/address.rb +273 -0
  14. data/lib/coinbase/client/models/address_balance_list.rb +275 -0
  15. data/lib/coinbase/client/models/address_list.rb +275 -0
  16. data/lib/coinbase/client/models/asset.rb +260 -0
  17. data/lib/coinbase/client/models/balance.rb +239 -0
  18. data/lib/coinbase/client/models/create_address_request.rb +239 -0
  19. data/lib/coinbase/client/models/create_transfer_request.rb +273 -0
  20. data/lib/coinbase/client/models/create_wallet_request.rb +221 -0
  21. data/lib/coinbase/client/models/error.rb +278 -0
  22. data/lib/coinbase/client/models/transfer.rb +393 -0
  23. data/lib/coinbase/client/models/transfer_list.rb +275 -0
  24. data/lib/coinbase/client/models/user.rb +231 -0
  25. data/lib/coinbase/client/models/wallet.rb +241 -0
  26. data/lib/coinbase/client/models/wallet_list.rb +275 -0
  27. data/lib/coinbase/client/version.rb +15 -0
  28. data/lib/coinbase/client.rb +57 -0
  29. data/lib/coinbase/constants.rb +5 -1
  30. data/lib/coinbase/middleware.rb +21 -0
  31. data/lib/coinbase/network.rb +2 -2
  32. data/lib/coinbase/transfer.rb +69 -54
  33. data/lib/coinbase/user.rb +64 -0
  34. data/lib/coinbase/wallet.rb +146 -54
  35. data/lib/coinbase.rb +73 -9
  36. metadata +74 -6
@@ -1,29 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'digest'
3
4
  require 'jimson'
5
+ require 'json'
4
6
  require 'money-tree'
5
7
  require 'securerandom'
6
8
 
7
9
  module Coinbase
8
10
  # A representation of a Wallet. Wallets come with a single default Address, but can expand to have a set of Addresses,
9
11
  # each of which can hold a balance of one or more Assets. Wallets can create new Addresses, list their addresses,
10
- # list their balances, and transfer Assets to other Addresses.
12
+ # list their balances, and transfer Assets to other Addresses. Wallets should be created through User#create_wallet or
13
+ # User#import_wallet.
11
14
  class Wallet
12
- attr_reader :wallet_id, :network_id
13
-
14
- # Returns a new Wallet object.
15
- # @param seed [Integer] (Optional) The seed to use for the Wallet. Expects a 32-byte hexadecimal. If not provided,
16
- # a new seed will be generated.
17
- # @param address_count [Integer] (Optional) The number of addresses to generate for the Wallet. If not provided,
18
- # a single address will be generated.
15
+ # Returns a new Wallet object. Do not use this method directly. Instead, use User#create_wallet or
16
+ # User#import_wallet.
17
+ # @param model [Coinbase::Client::Wallet] The underlying Wallet object
18
+ # @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.
19
21
  # @param client [Jimson::Client] (Optional) The JSON RPC client to use for interacting with the Network
20
- def initialize(seed: nil, address_count: 1, client: Jimson::Client.new(Coinbase.base_sepolia_rpc_url))
22
+ def initialize(model, seed: nil, address_count: 0)
21
23
  raise ArgumentError, 'Seed must be 32 bytes' if !seed.nil? && seed.length != 64
22
- raise ArgumentError, 'Address count must be positive' if address_count < 1
24
+
25
+ @model = model
23
26
 
24
27
  @master = seed.nil? ? MoneyTree::Master.new : MoneyTree::Master.new(seed_hex: seed)
25
28
 
26
- @wallet_id = SecureRandom.uuid
27
29
  # TODO: Make Network an argument to the constructor.
28
30
  @network_id = :base_sepolia
29
31
  @addresses = []
@@ -32,28 +34,49 @@ module Coinbase
32
34
  @address_path_prefix = "m/44'/60'/0'/0"
33
35
  @address_index = 0
34
36
 
35
- @client = client
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
44
+ end
36
45
 
37
- address_count.times { create_address }
46
+ # Returns the Wallet ID.
47
+ # @return [String] The Wallet ID
48
+ def wallet_id
49
+ @model.id
50
+ end
51
+
52
+ # Returns the Network ID of the Wallet.
53
+ # @return [Symbol] The Network ID
54
+ def network_id
55
+ Coinbase.to_sym(@model.network_id)
38
56
  end
39
57
 
40
58
  # Creates a new Address in the Wallet.
41
59
  # @return [Address] The new Address
42
60
  def create_address
43
- # TODO: Register with server.
44
- path = "#{@address_path_prefix}/#{@address_index}"
45
- private_key = @master.node_for_path(path).private_key.to_hex
46
- key = Eth::Key.new(priv: private_key)
47
- address = Address.new(@network_id, key.address.address, @wallet_id, key, client: @client)
48
- @addresses << address
49
- @address_index += 1
50
- address
61
+ key = derive_key
62
+ attestation = create_attestation(key)
63
+ public_key = key.public_key.compressed.unpack1('H*')
64
+
65
+ opts = {
66
+ create_address_request: {
67
+ public_key: public_key,
68
+ attestation: attestation
69
+ }
70
+ }
71
+ address_model = addresses_api.create_address(wallet_id, opts)
72
+
73
+ cache_address(address_model, key)
51
74
  end
52
75
 
53
76
  # Returns the default address of the Wallet.
54
77
  # @return [Address] The default address
55
78
  def default_address
56
- @addresses.first
79
+ @addresses.find { |address| address.address_id == @model.default_address.address_id }
57
80
  end
58
81
 
59
82
  # Returns the Address with the given ID.
@@ -66,25 +89,14 @@ module Coinbase
66
89
  # Returns the list of Addresses in the Wallet.
67
90
  # @return [Array<Address>] The list of Addresses
68
91
  def list_addresses
69
- # TODO: Register with server.
70
92
  @addresses
71
93
  end
72
94
 
73
95
  # Returns the list of balances of this Wallet. Balances are aggregated across all Addresses in the Wallet.
74
96
  # @return [BalanceMap] The list of balances. The key is the Asset ID, and the value is the balance.
75
97
  def list_balances
76
- balance_map = BalanceMap.new
77
-
78
- @addresses.each do |address|
79
- address.list_balances.each do |asset_id, balance|
80
- balance_map[asset_id] ||= BigDecimal(0)
81
- current_balance = balance_map[asset_id]
82
- new_balance = balance + current_balance
83
- balance_map[asset_id] = new_balance
84
- end
85
- end
86
-
87
- balance_map
98
+ response = wallets_api.list_wallet_balances(wallet_id)
99
+ Coinbase.to_balance_map(response)
88
100
  end
89
101
 
90
102
  # Returns the balance of the provided Asset. Balances are aggregated across all Addresses in the Wallet.
@@ -97,17 +109,21 @@ module Coinbase
97
109
  asset_id
98
110
  end
99
111
 
100
- eth_balance = list_balances[normalized_asset_id] || BigDecimal(0)
112
+ response = wallets_api.get_wallet_balance(wallet_id, normalized_asset_id.to_s)
113
+
114
+ return BigDecimal('0') if response.nil?
115
+
116
+ amount = BigDecimal(response.amount)
101
117
 
102
118
  case asset_id
103
119
  when :eth
104
- eth_balance
120
+ amount / BigDecimal(Coinbase::WEI_PER_ETHER.to_s)
105
121
  when :gwei
106
- eth_balance * Coinbase::GWEI_PER_ETHER
107
- when :wei
108
- eth_balance * Coinbase::WEI_PER_ETHER
122
+ amount / BigDecimal(Coinbase::GWEI_PER_ETHER.to_s)
123
+ when :usdc
124
+ amount / BigDecimal(Coinbase::ATOMIC_UNITS_PER_USDC.to_s)
109
125
  else
110
- BigDecimal(0)
126
+ amount
111
127
  end
112
128
  end
113
129
 
@@ -132,29 +148,105 @@ module Coinbase
132
148
  default_address.transfer(amount, asset_id, destination)
133
149
  end
134
150
 
135
- # Exports the Wallet's data to a WalletData object.
136
- # @return [WalletData] The Wallet data
151
+ # Exports the Wallet's data to a Data object.
152
+ # @return [Data] The Wallet data
137
153
  def export
138
- WalletData.new(@master.seed_hex, @addresses.length)
154
+ Data.new(wallet_id: wallet_id, seed: @master.seed_hex)
139
155
  end
140
156
 
141
157
  # The data required to recreate a Wallet.
142
- class WalletData
143
- attr_reader :seed, :address_count
158
+ class Data
159
+ attr_reader :wallet_id, :seed
144
160
 
145
- # Returns a new WalletData object.
161
+ # Returns a new Data object.
162
+ # @param wallet_id [String] The ID of the Wallet
146
163
  # @param seed [String] The seed of the Wallet
147
- # @param address_count [Integer] The number of addresses in the Wallet
148
- def initialize(seed, address_count)
164
+ def initialize(wallet_id:, seed:)
165
+ @wallet_id = wallet_id
149
166
  @seed = seed
150
- @address_count = address_count
151
167
  end
168
+
169
+ # Converts the Data object to a Hash.
170
+ # @return [Hash] The Hash representation of the Data object
171
+ def to_hash
172
+ { wallet_id: wallet_id, seed: seed }
173
+ end
174
+
175
+ # Creates a Data object from the given Hash.
176
+ # @param data [Hash] The Hash to create the Data object from
177
+ # @return [Data] The new Data object
178
+ def self.from_hash(data)
179
+ Data.new(wallet_id: data['wallet_id'], seed: data['seed'])
180
+ end
181
+ end
182
+
183
+ private
184
+
185
+ # Derives an already registered Address in the Wallet.
186
+ # @return [Address] The new Address
187
+ def derive_address
188
+ key = derive_key
189
+
190
+ address_id = key.address.to_s
191
+ address_model = addresses_api.get_address(wallet_id, address_id)
192
+
193
+ cache_address(address_model, key)
194
+ end
195
+
196
+ # Derives a key for an already registered Address in the Wallet.
197
+ # @return [Eth::Key] The new key
198
+ def derive_key
199
+ path = "#{@address_path_prefix}/#{@address_index}"
200
+ private_key = @master.node_for_path(path).private_key.to_hex
201
+ Eth::Key.new(priv: private_key)
202
+ end
203
+
204
+ # Caches an Address on the client-side and increments the address index.
205
+ # @param address_model [Coinbase::Client::Address] The Address model
206
+ # @param key [Eth::Key] The private key of the Address
207
+ # @return [Address] The new Address
208
+ def cache_address(address_model, key)
209
+ address = Address.new(address_model, key)
210
+ @addresses << address
211
+ @address_index += 1
212
+ address
213
+ end
214
+
215
+ # Creates an attestation for the Address currently being created.
216
+ # @param key [Eth::Key] The private key of the Address
217
+ # @return [String] The attestation
218
+ def create_attestation(key)
219
+ public_key = key.public_key.compressed.unpack1('H*')
220
+ payload = {
221
+ wallet_id: wallet_id,
222
+ public_key: public_key
223
+ }.to_json
224
+ hashed_payload = Digest::SHA256.digest(payload)
225
+ signature = key.sign(hashed_payload)
226
+
227
+ # The secp256k1 library serializes the signature as R, S, V.
228
+ # The server expects the signature as V, R, S in the format:
229
+ # <(byte of 27+public key solution)+4 if compressed >< padded bytes for signature R><padded bytes for signature S>
230
+ # Ruby gem does not add 4 to the recovery byte, so we need to add it here.
231
+ # Take the last byte (V) and add 4 to it to show signature is compressed.
232
+ signature_bytes = [signature].pack('H*').unpack('C*')
233
+ last_byte = signature_bytes.last
234
+ compressed_last_byte = last_byte + 4
235
+ new_signature_bytes = [compressed_last_byte] + signature_bytes[0..-2]
236
+ new_signature_bytes.pack('C*').unpack1('H*')
237
+ end
238
+
239
+ # Updates the Wallet model with the latest data.
240
+ def update_model
241
+ @model = wallets_api.get_wallet(wallet_id)
242
+ end
243
+
244
+ def addresses_api
245
+ @addresses_api ||= Coinbase::Client::AddressesApi.new(Coinbase.configuration.api_client)
152
246
  end
153
247
 
154
- # Returns the data required to recreate the Wallet.
155
- # @return [WalletData] The Wallet data
156
- def to_data
157
- WalletData.new(@master.seed_hex, @addresses.length)
248
+ def wallets_api
249
+ @wallets_api ||= Coinbase::Client::WalletsApi.new(Coinbase.configuration.api_client)
158
250
  end
159
251
  end
160
252
  end
data/lib/coinbase.rb CHANGED
@@ -2,25 +2,89 @@
2
2
 
3
3
  require_relative 'coinbase/address'
4
4
  require_relative 'coinbase/asset'
5
+ require_relative 'coinbase/authenticator'
5
6
  require_relative 'coinbase/balance_map'
7
+ require_relative 'coinbase/client'
6
8
  require_relative 'coinbase/constants'
9
+ require_relative 'coinbase/middleware'
7
10
  require_relative 'coinbase/network'
8
11
  require_relative 'coinbase/transfer'
12
+ require_relative 'coinbase/user'
9
13
  require_relative 'coinbase/wallet'
10
14
 
11
15
  # The Coinbase SDK.
12
16
  module Coinbase
13
- @base_sepolia_rpc_url = 'https://sepolia.base.org'
17
+ class InvalidConfiguration < StandardError; end
14
18
 
15
- # Returns the Base Sepolia RPC URL.
16
- # @return [String] the Base Sepolia RPC URL
17
- def self.base_sepolia_rpc_url
18
- @base_sepolia_rpc_url
19
+ def self.configuration
20
+ @configuration ||= Configuration.new
19
21
  end
20
22
 
21
- # Sets the Base Sepolia RPC URL.
22
- # @param value [String] the Base Sepolia RPC URL
23
- def self.base_sepolia_rpc_url=(value)
24
- @base_sepolia_rpc_url = value
23
+ def self.configure
24
+ yield(configuration)
25
+
26
+ raise InvalidConfiguration, 'API key private key is not set' unless configuration.api_key_private_key
27
+ raise InvalidConfiguration, 'API key name is not set' unless configuration.api_key_name
28
+ end
29
+
30
+ # Configuration object for the Coinbase SDK
31
+ class Configuration
32
+ attr_reader :base_sepolia_rpc_url, :base_sepolia_client
33
+ attr_accessor :api_url, :api_key_name, :api_key_private_key
34
+
35
+ def initialize
36
+ @base_sepolia_rpc_url = 'https://sepolia.base.org'
37
+ @base_sepolia_client = Jimson::Client.new(@base_sepolia_rpc_url)
38
+ @api_url = 'https://api.cdp.coinbase.com'
39
+ end
40
+
41
+ def base_sepolia_rpc_url=(new_base_sepolia_rpc_url)
42
+ @base_sepolia_rpc_url = new_base_sepolia_rpc_url
43
+ @base_sepolia_client = Jimson::Client.new(@base_sepolia_rpc_url)
44
+ end
45
+
46
+ def api_client
47
+ @api_client ||= Coinbase::Client::ApiClient.new(Middleware.config)
48
+ end
49
+ end
50
+
51
+ # Returns the default user.
52
+ # @return [Coinbase::User] the default user
53
+ def self.default_user
54
+ @default_user ||= load_default_user
55
+ end
56
+
57
+ # Converts a string to a symbol, replacing hyphens with underscores.
58
+ # @param string [String] the string to convert
59
+ # @return [Symbol] the converted symbol
60
+ def self.to_sym(value)
61
+ value.to_s.gsub('-', '_').to_sym
62
+ end
63
+
64
+ # Converts a Coinbase::Client::AddressBalanceList to a BalanceMap.
65
+ # @param address_balance_list [Coinbase::Client::AddressBalanceList] The AddressBalanceList to convert
66
+ # @return [BalanceMap] The converted BalanceMap
67
+ def self.to_balance_map(address_balance_list)
68
+ balances = {}
69
+
70
+ address_balance_list.data.each do |balance|
71
+ asset_id = Coinbase.to_sym(balance.asset.asset_id.downcase)
72
+ amount = if asset_id == :eth
73
+ BigDecimal(balance.amount) / BigDecimal(Coinbase::WEI_PER_ETHER)
74
+ elsif asset_id == :usdc
75
+ BigDecimal(balance.amount) / BigDecimal(Coinbase::ATOMIC_UNITS_PER_USDC)
76
+ else
77
+ BigDecimal(balance.amount)
78
+ end
79
+ balances[asset_id] = amount
80
+ end
81
+
82
+ BalanceMap.new(balances)
83
+ end
84
+
85
+ def self.load_default_user
86
+ users_api = Coinbase::Client::UsersApi.new(configuration.api_client)
87
+ user_model = users_api.get_current_user
88
+ Coinbase::User.new(user_model)
25
89
  end
26
90
  end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coinbase-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
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-19 00:00:00.000000000 Z
11
+ date: 2024-04-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bigdecimal
14
+ name: eth
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -25,7 +25,21 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: eth
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday-multipart
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - ">="
@@ -53,7 +67,7 @@ dependencies:
53
67
  - !ruby/object:Gem::Version
54
68
  version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
- name: money-tree
70
+ name: jwt
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - ">="
@@ -67,7 +81,7 @@ dependencies:
67
81
  - !ruby/object:Gem::Version
68
82
  version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
- name: securerandom
84
+ name: marcel
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
87
  - - ">="
@@ -80,6 +94,34 @@ dependencies:
80
94
  - - ">="
81
95
  - !ruby/object:Gem::Version
82
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: money-tree
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: dotenv
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
83
125
  - !ruby/object:Gem::Dependency
84
126
  name: pry
85
127
  requirement: !ruby/object:Gem::Requirement
@@ -159,10 +201,36 @@ files:
159
201
  - lib/coinbase.rb
160
202
  - lib/coinbase/address.rb
161
203
  - lib/coinbase/asset.rb
204
+ - lib/coinbase/authenticator.rb
162
205
  - lib/coinbase/balance_map.rb
206
+ - lib/coinbase/client.rb
207
+ - lib/coinbase/client/api/addresses_api.rb
208
+ - lib/coinbase/client/api/transfers_api.rb
209
+ - lib/coinbase/client/api/users_api.rb
210
+ - lib/coinbase/client/api/wallets_api.rb
211
+ - lib/coinbase/client/api_client.rb
212
+ - lib/coinbase/client/api_error.rb
213
+ - lib/coinbase/client/configuration.rb
214
+ - lib/coinbase/client/models/address.rb
215
+ - lib/coinbase/client/models/address_balance_list.rb
216
+ - lib/coinbase/client/models/address_list.rb
217
+ - lib/coinbase/client/models/asset.rb
218
+ - lib/coinbase/client/models/balance.rb
219
+ - lib/coinbase/client/models/create_address_request.rb
220
+ - lib/coinbase/client/models/create_transfer_request.rb
221
+ - lib/coinbase/client/models/create_wallet_request.rb
222
+ - lib/coinbase/client/models/error.rb
223
+ - lib/coinbase/client/models/transfer.rb
224
+ - lib/coinbase/client/models/transfer_list.rb
225
+ - lib/coinbase/client/models/user.rb
226
+ - lib/coinbase/client/models/wallet.rb
227
+ - lib/coinbase/client/models/wallet_list.rb
228
+ - lib/coinbase/client/version.rb
163
229
  - lib/coinbase/constants.rb
230
+ - lib/coinbase/middleware.rb
164
231
  - lib/coinbase/network.rb
165
232
  - lib/coinbase/transfer.rb
233
+ - lib/coinbase/user.rb
166
234
  - lib/coinbase/wallet.rb
167
235
  homepage: https://github.com/coinbase/coinbase-sdk-ruby
168
236
  licenses: