coinbase-sdk 0.0.1 → 0.0.2

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