coinbase 0.0.1 → 4.0.7

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of coinbase might be problematic. Click here for more details.

Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +611 -0
  6. data/Rakefile +7 -0
  7. data/bin/console +14 -0
  8. data/bin/setup +7 -0
  9. data/coinbase.gemspec +27 -0
  10. data/lib/coinbase/wallet/adapters/em_http.rb +78 -0
  11. data/lib/coinbase/wallet/adapters/net_http.rb +68 -0
  12. data/lib/coinbase/wallet/api_client.rb +692 -0
  13. data/lib/coinbase/wallet/api_errors.rb +120 -0
  14. data/lib/coinbase/wallet/api_response.rb +41 -0
  15. data/lib/coinbase/wallet/ca-coinbase.crt +629 -0
  16. data/lib/coinbase/wallet/client.rb +101 -0
  17. data/lib/coinbase/wallet/models/account.rb +187 -0
  18. data/lib/coinbase/wallet/models/address.rb +12 -0
  19. data/lib/coinbase/wallet/models/api_object.rb +46 -0
  20. data/lib/coinbase/wallet/models/checkout.rb +19 -0
  21. data/lib/coinbase/wallet/models/order.rb +12 -0
  22. data/lib/coinbase/wallet/models/transaction.rb +21 -0
  23. data/lib/coinbase/wallet/models/transfer.rb +13 -0
  24. data/lib/coinbase/wallet/models/user.rb +15 -0
  25. data/lib/coinbase/wallet/version.rb +5 -0
  26. data/lib/coinbase/wallet.rb +23 -156
  27. data/spec/account_spec.rb +193 -0
  28. data/spec/clients/client_spec.rb +34 -0
  29. data/spec/clients/oauth_client_spec.rb +56 -0
  30. data/spec/endpoints_spec.rb +352 -0
  31. data/spec/error_spec.rb +137 -0
  32. data/spec/models/address_spec.rb +26 -0
  33. data/spec/models/api_object_spec.rb +63 -0
  34. data/spec/models/checkout_spec.rb +52 -0
  35. data/spec/models/current_user_spec.rb +29 -0
  36. data/spec/models/order_spec.rb +52 -0
  37. data/spec/models/request_spec.rb +47 -0
  38. data/spec/models/transfer_spec.rb +64 -0
  39. data/spec/models/user_spec.rb +24 -0
  40. data/spec/spec_helper.rb +13 -0
  41. metadata +62 -98
  42. data/lib/coinbase/address.rb +0 -127
  43. data/lib/coinbase/asset.rb +0 -20
  44. data/lib/coinbase/balance_map.rb +0 -48
  45. data/lib/coinbase/constants.rb +0 -38
  46. data/lib/coinbase/network.rb +0 -55
  47. data/lib/coinbase/transfer.rb +0 -153
  48. data/lib/coinbase.rb +0 -18
@@ -0,0 +1,101 @@
1
+ module Coinbase
2
+ module Wallet
3
+ BASE_API_URL = "https://api.coinbase.com"
4
+ API_VERSION = '2015-06-16'
5
+
6
+ class Client < NetHTTPClient
7
+ def initialize(options={})
8
+ [ :api_key, :api_secret ].each do |opt|
9
+ raise unless options.has_key? opt
10
+ end
11
+ @api_key = options[:api_key]
12
+ @api_secret = options[:api_secret]
13
+ @api_uri = URI.parse(options[:api_url] || BASE_API_URL)
14
+ super(@api_uri, options)
15
+ end
16
+
17
+ def auth_headers(method, path, body)
18
+ ts = Time.now.to_i.to_s
19
+ signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'),
20
+ @api_secret,
21
+ ts + method + path + body.to_s)
22
+ { 'CB-ACCESS-KEY' => @api_key,
23
+ 'CB-ACCESS-SIGN' => signature,
24
+ 'CB-ACCESS-TIMESTAMP' => ts,
25
+ 'CB-VERSION' => API_VERSION }
26
+ end
27
+ end
28
+
29
+ class OAuthClient < NetHTTPClient
30
+ attr_accessor :access_token, :refresh_token
31
+
32
+ def initialize(options={})
33
+ raise unless options.has_key? :access_token
34
+ @access_token = options[:access_token]
35
+ @refresh_token = options[:refresh_token]
36
+ @oauth_uri = URI.parse(options[:api_url] || BASE_API_URL)
37
+ super(@oauth_uri, options)
38
+ end
39
+
40
+ def auth_headers(method, path, body)
41
+ { 'Authorization' => "Bearer #{@access_token}",
42
+ 'CB-VERSION' => API_VERSION }
43
+ end
44
+
45
+ def authorize!(redirect_url, params = {})
46
+ raise NotImplementedError
47
+ end
48
+
49
+ def revoke!(params = {})
50
+ params[:token] ||= @access_token
51
+
52
+ out = nil
53
+ post("/oauth/revoke", params) do |resp|
54
+ out = APIObject.new(self, resp.body)
55
+ yield(out, resp) if block_given?
56
+ end
57
+ out
58
+ end
59
+
60
+ def refresh!(params = {})
61
+ params[:grant_type] = 'refresh_token'
62
+ params[:refresh_token] ||= @refresh_token
63
+
64
+ raise "Missing Parameter: refresh_token" unless params.has_key?(:refresh_token)
65
+
66
+ out = nil
67
+ post("/oauth/token", params) do |resp|
68
+ out = APIObject.new(self, resp.body)
69
+ # Update tokens to current instance
70
+ # Developer should always persist them
71
+ @access_token = out.access_token
72
+ @refresh_token = out.refresh_token
73
+ yield(out, resp) if block_given?
74
+ end
75
+ out
76
+ end
77
+ end
78
+
79
+ class AsyncClient < EMHTTPClient
80
+ def initialize(options={})
81
+ [ :api_key, :api_secret ].each do |opt|
82
+ raise unless options.has_key? opt
83
+ end
84
+ @api_key = options[:api_key]
85
+ @api_secret = options[:api_secret]
86
+ @api_uri = URI.parse(options[:api_url] || BASE_API_URL)
87
+ end
88
+
89
+ def auth_headers(method, path, body)
90
+ ts = Time.now.to_i.to_s
91
+ signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'),
92
+ @api_secret,
93
+ ts + method + path + body.to_s)
94
+ { 'CB-ACCESS-KEY' => @api_key,
95
+ 'CB-ACCESS-SIGN' => signature,
96
+ 'CB-ACCESS-TIMESTAMP' => ts,
97
+ 'CB-VERSION' => API_VERSION }
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,187 @@
1
+ module Coinbase
2
+ module Wallet
3
+ class Account < APIObject
4
+ def update!(params = {})
5
+ @client.update_account(self['id'], params) do |data, resp|
6
+ update(data)
7
+ yield(data, resp) if block_given?
8
+ end
9
+ end
10
+
11
+ def make_primary!(params = {})
12
+ @client.set_primary_account(self['id'], params) do |data, resp|
13
+ update(data)
14
+ yield(data, resp) if block_given?
15
+ end
16
+ end
17
+
18
+ def delete!(params = {})
19
+ @client.delete_account(self['id'], params) do |data, resp|
20
+ yield(data, resp) if block_given?
21
+ end
22
+ end
23
+
24
+ #
25
+ # Addresses
26
+ #
27
+ def addresses(params = {})
28
+ @client.addresses(self['id'], params) do |data, resp|
29
+ yield(data, resp) if block_given?
30
+ end
31
+ end
32
+
33
+ def address(address_id, params = {})
34
+ @client.address(self['id'], address_id, params) do |data, resp|
35
+ yield(data, resp) if block_given?
36
+ end
37
+ end
38
+
39
+ def create_address(params = {})
40
+ @client.create_address(self['id']) do |data, resp|
41
+ yield(data, resp) if block_given?
42
+ end
43
+ end
44
+
45
+ #
46
+ # Transactions
47
+ #
48
+ def transactions(params = {})
49
+ @client.transactions(self['id'], params) do |data, resp|
50
+ yield(data, resp) if block_given?
51
+ end
52
+ end
53
+
54
+ def transaction(transaction_id, params = {})
55
+ @client.transaction(self['id'], transaction_id, params) do |data, resp|
56
+ yield(data, resp) if block_given?
57
+ end
58
+ end
59
+
60
+ def send(params = {})
61
+ @client.send(self['id'], params) do |data, resp|
62
+ yield(data, resp) if block_given?
63
+ end
64
+ end
65
+
66
+ def transfer(params = {})
67
+ @client.transfer(self['id'], params) do |data, resp|
68
+ yield(data, resp) if block_given?
69
+ end
70
+ end
71
+
72
+ def request(params = {})
73
+ @client.request(self['id'], params) do |data, resp|
74
+ yield(data, resp) if block_given?
75
+ end
76
+ end
77
+
78
+ #
79
+ # Buys
80
+ #
81
+ def list_buys(params = {})
82
+ @client.list_buys(self['id'], params) do |data, resp|
83
+ yield(data, resp) if block_given?
84
+ end
85
+ end
86
+
87
+ def list_buy(transaction_id, params = {})
88
+ @client.list_buy(self['id'], transaction_id, params) do |data, resp|
89
+ yield(data, resp) if block_given?
90
+ end
91
+ end
92
+
93
+ def buy(params = {})
94
+ @client.buy(self['id'], params) do |data, resp|
95
+ yield(data, resp) if block_given?
96
+ end
97
+ end
98
+
99
+ def commit_buy(transaction_id, params = {})
100
+ @client.commit_buy(self['id'], transaction_id, params) do |data, resp|
101
+ yield(data, resp) if block_given?
102
+ end
103
+ end
104
+
105
+ #
106
+ # Sells
107
+ #
108
+ def list_sells(params = {})
109
+ @client.list_sells(self['id'], params) do |data, resp|
110
+ yield(data, resp) if block_given?
111
+ end
112
+ end
113
+
114
+ def list_sell(transaction_id, params = {})
115
+ @client.list_sell(self['id'], transaction_id, params) do |data, resp|
116
+ yield(data, resp) if block_given?
117
+ end
118
+ end
119
+
120
+ def sell(params = {})
121
+ @client.sell(self['id'], params) do |data, resp|
122
+ yield(data, resp) if block_given?
123
+ end
124
+ end
125
+
126
+ def commit_sell(transaction_id, params = {})
127
+ @client.commit_sell(self['id'], transaction_id, params) do |data, resp|
128
+ yield(data, resp) if block_given?
129
+ end
130
+ end
131
+
132
+ #
133
+ # Deposit
134
+ #
135
+ def list_deposits(params = {})
136
+ @client.list_deposits(self['id'], params) do |data, resp|
137
+ yield(data, resp) if block_given?
138
+ end
139
+ end
140
+
141
+ def list_deposit(transaction_id, params = {})
142
+ @client.list_deposit(self['id'], transaction_id, params) do |data, resp|
143
+ yield(data, resp) if block_given?
144
+ end
145
+ end
146
+
147
+ def deposit(params = {})
148
+ @client.deposit(self['id'], params) do |data, resp|
149
+ yield(data, resp) if block_given?
150
+ end
151
+ end
152
+
153
+ def commit_deposit(transaction_id, params = {})
154
+ @client.commit_deposit(self['id'], transaction_id, params) do |data, resp|
155
+ yield(data, resp) if block_given?
156
+ end
157
+ end
158
+
159
+ #
160
+ # Withdrawals
161
+ #
162
+ def list_withdrawals(params = {})
163
+ @client.list_withdrawals(self['id'], params) do |data, resp|
164
+ yield(data, resp) if block_given?
165
+ end
166
+ end
167
+
168
+ def list_withdrawal(transaction_id, params = {})
169
+ @client.list_withdrawal(self['id'], transaction_id, params) do |data, resp|
170
+ yield(data, resp) if block_given?
171
+ end
172
+ end
173
+
174
+ def withdraw(params = {})
175
+ @client.withdraw(self['id'], params) do |data, resp|
176
+ yield(data, resp) if block_given?
177
+ end
178
+ end
179
+
180
+ def commit_withdrawal(transaction_id, params = {})
181
+ @client.commit_withdrawal(self['id'], transaction_id, params) do |data, resp|
182
+ yield(data, resp) if block_given?
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,12 @@
1
+ module Coinbase
2
+ module Wallet
3
+ class Address < APIObject
4
+ def transactions(params = {})
5
+ @client.get("#{self['resource_path']}/transactions", params) do |resp|
6
+ out = resp.data.map { |item| Transaction.new(self, item) }
7
+ yield(out, resp) if block_given?
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,46 @@
1
+ module Coinbase
2
+ module Wallet
3
+ # Response item abstract model
4
+ class APIObject < Hash
5
+ def initialize(client, data)
6
+ super()
7
+ update(data)
8
+ @client = client
9
+ end
10
+
11
+ def refresh!(params = {})
12
+ @client.get(self['resource_path'], params) do |resp|
13
+ update(resp.data)
14
+ yield(resp.data, resp) if block_given?
15
+ end
16
+ end
17
+
18
+ def update(data)
19
+ return if data.nil?
20
+ data.each { |key, val| self[key] = val } if data.is_a?(Hash)
21
+ end
22
+
23
+ def format(key, val)
24
+ return if val.nil?
25
+ # Looks like a number or currency
26
+ if val.class == Hash
27
+ APIObject.new(@client, val)
28
+ elsif key =~ /_at$/ && (Time.iso8601(val) rescue nil)
29
+ Time.parse(val)
30
+ elsif key == "amount" && val =~ /^.{0,1}\s*[0-9,]*\.{0,1}[0-9]*$/
31
+ BigDecimal(val.gsub(/[^0-9\.]/, ''))
32
+ else
33
+ val
34
+ end
35
+ end
36
+
37
+ def method_missing(method, *args, &blk)
38
+ format(method.to_s, self[method.to_s]) || super
39
+ end
40
+
41
+ def respond_to_missing?(method, include_all = false)
42
+ self.key?(method.to_s) || super
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,19 @@
1
+ module Coinbase
2
+ module Wallet
3
+ class Checkout < APIObject
4
+ def orders(params = {})
5
+ @client.get("#{self['resource_path']}/orders", params) do |resp|
6
+ out = resp.data.map { |item| Order.new(self, item) }
7
+ yield(out, resp) if block_given?
8
+ end
9
+ end
10
+
11
+ def create_order(params = {})
12
+ @client.post("#{self['resource_path']}/orders", params) do |resp|
13
+ out = Order.new(self, resp.data)
14
+ yield(out, resp) if block_given?
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ module Coinbase
2
+ module Wallet
3
+ class Order < APIObject
4
+ def refund!(params = {})
5
+ @client.post("#{self['resource_path']}/refund", params) do |resp|
6
+ update(resp.data)
7
+ yield(resp.data, resp) if block_given?
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ module Coinbase
2
+ module Wallet
3
+ class Transaction < APIObject
4
+ end
5
+
6
+ class Request < Transaction
7
+ def resend!(params = {})
8
+ @client.post("#{self['resource_path']}/resend", params) do |resp|
9
+ yield(resp.data, resp) if block_given?
10
+ end
11
+ end
12
+
13
+ def cancel!(params = {})
14
+ @client.delete("#{self['resource_path']}", params) do |resp|
15
+ yield(resp.data, resp) if block_given?
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,13 @@
1
+ module Coinbase
2
+ module Wallet
3
+ class Transfer < APIObject
4
+ def commit!(params = {})
5
+ @client.post("#{self['resource_path']}/commit", params) do |resp|
6
+ update(resp.data)
7
+ yield(resp.data, resp) if block_given?
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+
@@ -0,0 +1,15 @@
1
+ module Coinbase
2
+ module Wallet
3
+ class User < APIObject
4
+ end
5
+
6
+ class CurrentUser < User
7
+ def update!(params = {})
8
+ @client.update_current_user(params) do |data, resp|
9
+ update(resp.data)
10
+ yield(resp.data, resp) if block_given?
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module Coinbase
2
+ module Wallet
3
+ VERSION = "4.0.7"
4
+ end
5
+ end
@@ -1,160 +1,27 @@
1
- # frozen_string_literal: true
2
-
3
- require 'jimson'
4
- require 'money-tree'
5
- require 'securerandom'
1
+ require "coinbase/wallet/version"
2
+
3
+ require "base64"
4
+ require "bigdecimal"
5
+ require "json"
6
+ require "uri"
7
+ require "net/https"
8
+
9
+ require "coinbase/wallet/api_errors"
10
+ require "coinbase/wallet/api_response"
11
+ require "coinbase/wallet/api_client"
12
+ require "coinbase/wallet/adapters/net_http.rb"
13
+ require "coinbase/wallet/adapters/em_http.rb"
14
+ require "coinbase/wallet/models/api_object"
15
+ require "coinbase/wallet/models/account"
16
+ require "coinbase/wallet/models/address"
17
+ require "coinbase/wallet/models/user"
18
+ require "coinbase/wallet/models/transaction"
19
+ require "coinbase/wallet/models/transfer"
20
+ require "coinbase/wallet/models/order"
21
+ require "coinbase/wallet/models/checkout"
22
+ require "coinbase/wallet/client"
6
23
 
7
24
  module Coinbase
8
- # A representation of a Wallet. Wallets come with a single default Address, but can expand to have a set of Addresses,
9
- # 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.
11
- 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.
19
- # @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(ENV.fetch('BASE_SEPOLIA_RPC_URL', nil)))
21
- 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
23
-
24
- @master = seed.nil? ? MoneyTree::Master.new : MoneyTree::Master.new(seed_hex: seed)
25
-
26
- @wallet_id = SecureRandom.uuid
27
- # TODO: Make Network an argument to the constructor.
28
- @network_id = :base_sepolia
29
- @addresses = []
30
-
31
- # TODO: Adjust derivation path prefix based on network protocol.
32
- @address_path_prefix = "m/44'/60'/0'/0"
33
- @address_index = 0
34
-
35
- @client = client
36
-
37
- address_count.times { create_address }
38
- end
39
-
40
- # Creates a new Address in the Wallet.
41
- # @return [Address] The new Address
42
- 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
51
- end
52
-
53
- # Returns the default address of the Wallet.
54
- # @return [Address] The default address
55
- def default_address
56
- @addresses.first
57
- end
58
-
59
- # Returns the Address with the given ID.
60
- # @param address_id [String] The ID of the Address to retrieve
61
- # @return [Address] The Address
62
- def get_address(address_id)
63
- @addresses.find { |address| address.address_id == address_id }
64
- end
65
-
66
- # Returns the list of Addresses in the Wallet.
67
- # @return [Array<Address>] The list of Addresses
68
- def list_addresses
69
- # TODO: Register with server.
70
- @addresses
71
- end
72
-
73
- # Returns the list of balances of this Wallet. Balances are aggregated across all Addresses in the Wallet.
74
- # @return [BalanceMap] The list of balances. The key is the Asset ID, and the value is the balance.
75
- 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
88
- end
89
-
90
- # Returns the balance of the provided Asset. Balances are aggregated across all Addresses in the Wallet.
91
- # @param asset_id [Symbol] The ID of the Asset to retrieve the balance for
92
- # @return [BigDecimal] The balance of the Asset
93
- def get_balance(asset_id)
94
- normalized_asset_id = if %i[wei gwei].include?(asset_id)
95
- :eth
96
- else
97
- asset_id
98
- end
99
-
100
- eth_balance = list_balances[normalized_asset_id] || BigDecimal(0)
101
-
102
- case asset_id
103
- when :eth
104
- eth_balance
105
- when :gwei
106
- eth_balance * Coinbase::GWEI_PER_ETHER
107
- when :wei
108
- eth_balance * Coinbase::WEI_PER_ETHER
109
- else
110
- BigDecimal(0)
111
- end
112
- end
113
-
114
- # Transfers the given amount of the given Asset to the given address. Only same-Network Transfers are supported.
115
- # Currently only the default_address is used to source the Transfer.
116
- # @param amount [Integer, Float, BigDecimal] The amount of the Asset to send
117
- # @param asset_id [Symbol] The ID of the Asset to send
118
- # @param destination [Wallet | Address | String] The destination of the transfer. If a Wallet, sends to the Wallet's
119
- # default address. If a String, interprets it as the address ID.
120
- # @return [Transfer] The hash of the Transfer transaction.
121
- def transfer(amount, asset_id, destination)
122
- if destination.is_a?(Wallet)
123
- raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != @network_id
124
-
125
- destination = destination.default_address.address_id
126
- elsif destination.is_a?(Address)
127
- raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != @network_id
128
-
129
- destination = destination.address_id
130
- end
131
-
132
- default_address.transfer(amount, asset_id, destination)
133
- end
134
-
135
- # Exports the Wallet's data to a WalletData object.
136
- # @return [WalletData] The Wallet data
137
- def export
138
- WalletData.new(@master.seed_hex, @addresses.length)
139
- end
140
-
141
- # The data required to recreate a Wallet.
142
- class WalletData
143
- attr_reader :seed, :address_count
144
-
145
- # Returns a new WalletData object.
146
- # @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)
149
- @seed = seed
150
- @address_count = address_count
151
- end
152
- end
153
-
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)
158
- end
25
+ module Wallet
159
26
  end
160
27
  end