coinbase-advanced 0.1.1

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.
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coinbase
4
+ module Advanced
5
+ module REST
6
+ module Resources
7
+ module Futures
8
+ def get_futures_balance_summary # rubocop:disable Naming/AccessorMethodName
9
+ get("cfm/balance_summary")
10
+ end
11
+
12
+ def get_intraday_margin_setting # rubocop:disable Naming/AccessorMethodName
13
+ get("cfm/intraday/margin_setting")
14
+ end
15
+
16
+ def set_intraday_margin_setting(params = {})
17
+ post("cfm/intraday/margin_setting", params)
18
+ end
19
+
20
+ def get_current_margin_window(params = {})
21
+ get("cfm/intraday/current_margin_window", params)
22
+ end
23
+
24
+ def list_futures_positions
25
+ get("cfm/positions")
26
+ end
27
+
28
+ def get_futures_position(params = {})
29
+ raise ArgumentError, "Missing :product_id" unless params.key? :product_id
30
+
31
+ get("cfm/positions/#{params[:position_id]}")
32
+ end
33
+
34
+ def schedule_futures_sweep(params = {})
35
+ post("cfm/sweeps/schedule", params)
36
+ end
37
+
38
+ def list_futures_sweeps
39
+ get("cfm/sweeps")
40
+ end
41
+
42
+ def cancel_pending_futures_sweep
43
+ delete("cfm/sweeps")
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coinbase
4
+ module Advanced
5
+ module REST
6
+ module Resources
7
+ # https://docs.cdp.coinbase.com/advanced-trade/reference/retailbrokerageapi_postorder
8
+ module Orders
9
+ def create_order(params = {})
10
+ raise ArgumentError, "Missing client_order_id" unless params.key? :client_order_id
11
+ raise ArgumentError, "Missing product_id" unless params.key? :product_id
12
+ raise ArgumentError, "Missing side" unless params.key? :side
13
+ raise ArgumentError, "Missing order_configuration" unless params.key? :order_configuration
14
+
15
+ post("orders", params)
16
+ end
17
+
18
+ def cancel_orders(params = {})
19
+ raise ArgumentError, "Missing order_ids" unless params.key? :order_ids
20
+
21
+ post("orders/batch_cancel", params)
22
+ end
23
+
24
+ def edit_order(params = {})
25
+ raise ArgumentError, "Missing order_id" unless params.key? :order_id
26
+ raise ArgumentError, "Missing price" unless params.key? :price
27
+ raise ArgumentError, "Missing size" unless params.key? :size
28
+
29
+ post("orders/edit", params)
30
+ end
31
+
32
+ def edit_order_preview(params = {})
33
+ raise ArgumentError, "Missing order_id" unless params.key? :order_id
34
+ raise ArgumentError, "Missing price" unless params.key? :price
35
+ raise ArgumentError, "Missing size" unless params.key? :size
36
+
37
+ post("orders/edit_preview", params)
38
+ end
39
+
40
+ def get_order(params = {})
41
+ raise ArgumentError, "Missing order_id" unless params.key? :order_id
42
+
43
+ order_id = params.delete(:order_id)
44
+ get("orders/historical/#{order_id}", params)
45
+ end
46
+
47
+ def list_orders(params = {})
48
+ get("orders/historical/batch", params)
49
+ end
50
+
51
+ def list_fills(params = {})
52
+ get("orders/historical/fills", params)
53
+ end
54
+
55
+ def preview_order(params = {})
56
+ raise ArgumentError, "Missing product_id" unless params.key? :product_id
57
+ raise ArgumentError, "Missing side" unless params.key? :side
58
+ raise ArgumentError, "Missing order_configuration" unless params.key? :order_configuration
59
+
60
+ post("orders/preview", params)
61
+ end
62
+
63
+ def close_position(params = {})
64
+ raise ArgumentError, "Missing client_order_id" unless params.key? :client_order_id
65
+ raise ArgumentError, "Missing product_id" unless params.key? :product_id
66
+
67
+ post("orders/close_position", params)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coinbase
4
+ module Advanced
5
+ module REST
6
+ module Resources
7
+ module PaymentMethods
8
+ def list_payment_methods
9
+ get("/payment_methods")
10
+ end
11
+
12
+ def get_payment_method(params = {})
13
+ raise ArgumentError, "Missing :payment_method_id" unless params.key? :payment_method_id
14
+
15
+ get("payment_methods/#{params[:payment_method_id]}")
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coinbase
4
+ module Advanced
5
+ module REST
6
+ module Resources
7
+ module Perpetuals
8
+ def allocate_portfolio(params = {})
9
+ raise ArgumentError, "Missing :portfolio_uuid" unless params.key? :portfolio_uuid
10
+ raise ArgumentError, "Missing :symbol" unless params.key? :symbol
11
+ raise ArgumentError, "Missing :amount" unless params.key? :amount
12
+ raise ArgumentError, "Missing :currency" unless params.key? :currency
13
+
14
+ post("intx/allocate", params)
15
+ end
16
+
17
+ def get_perpetuals_portfolio_summary(params = {})
18
+ raise ArgumentError, "Missing :portfolio_uuid" unless params.key? :portfolio_uuid
19
+
20
+ get("intx/portfolio/#{params[:portfolio_uuid]}")
21
+ end
22
+
23
+ def list_perpetuals_positions(params = {})
24
+ raise ArgumentError, "Missing :portfolio_uuid" unless params.key? :portfolio_uuid
25
+
26
+ get("intx/positions/#{params[:portfolio_uuid]}")
27
+ end
28
+
29
+ def get_perpetuals_position(params = {})
30
+ raise ArgumentError, "Missing :portfolio_uuid" unless params.key? :portfolio_uuid
31
+ raise ArgumentError, "Missing :symbol" unless params.key? :symbol
32
+
33
+ get("intx/positions/#{params[:portfolio_uuid]}/#{params[:symbol]}")
34
+ end
35
+
36
+ def get_portfolios_balances(params = {})
37
+ raise ArgumentError, "Missing :portfolio_uuid" unless params.key? :portfolio_uuid
38
+
39
+ get("intx/balances/#{params[:portfolio_uuid]}")
40
+ end
41
+
42
+ def opt_in_or_out_of_multi_asset_collateral(params = {})
43
+ post("intx/multi_asset_collateral", params)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coinbase
4
+ module Advanced
5
+ module REST
6
+ module Resources
7
+ module Portfolios
8
+ def list_portfolios(params = {})
9
+ get("portfolios", params)
10
+ end
11
+
12
+ def create_portfolio(params = {})
13
+ raise ArgumentError, "Missing :name" unless params.key? :name
14
+
15
+ post("portfolios", params)
16
+ end
17
+
18
+ def move_portfolio_funds(params = {})
19
+ raise ArgumentError, "Missing :funds" unless params.key? :funds
20
+ raise ArgumentError, "Missing :funds[:value]" unless params[:funds].key? :value
21
+ raise ArgumentError, "Missing :funds[:currency]" unless params[:funds].key? :currency
22
+ raise ArgumentError, "Missing :source_portfolio_uuid" unless params.key? :source_portfolio_uuid
23
+ raise ArgumentError, "Missing :target_portfolio_uuid" unless params.key? :target_portfolio_uuid
24
+
25
+ post("portfolios/move_funds", params)
26
+ end
27
+
28
+ def get_portfolio_funds(params = {})
29
+ raise ArgumentError, "Missing :portfolio_uuid" unless params.key? :portfolio_uuid
30
+
31
+ portfolio_uuid = params.delete(:portfolio_uuid)
32
+ get("portfolios/#{portfolio_uuid}", params)
33
+ end
34
+
35
+ def delete_portfolio(params = {})
36
+ raise ArgumentError, "Missing :portfolio_uuid" unless params.key? :portfolio_uuid
37
+
38
+ delete("portfolios/#{params[:portfolio_uuid]}")
39
+ end
40
+
41
+ def edit_portfolio(params = {})
42
+ raise ArgumentError, "Missing :portfolio_uuid" unless params.key? :portfolio_uuid
43
+ raise ArgumentError, "Missing :name" unless params.key? :name
44
+
45
+ portfolio_uuid = params.delete(:portfolio_uuid)
46
+ put("portfolios/#{portfolio_uuid}", params)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coinbase
4
+ module Advanced
5
+ module REST
6
+ module Resources
7
+ module Products
8
+ def get_best_bid_ask(params = {})
9
+ get("best_bid_ask", params)
10
+ end
11
+
12
+ def get_product_book(params = {})
13
+ raise ArgumentError, "Missing :product_id" unless params.key? :product_id
14
+
15
+ get("product_book", params)
16
+ end
17
+
18
+ def list_products(params = {})
19
+ get("products", params)
20
+ end
21
+
22
+ def get_product(params = {})
23
+ raise ArgumentError, "Missing :product_id" unless params.key? :product_id
24
+
25
+ product_id = params.delete(:product_id)
26
+ get("products/#{product_id}", params)
27
+ end
28
+
29
+ def get_product_candles(params = {})
30
+ raise ArgumentError, "Missing :product_id" unless params.key? :product_id
31
+ raise ArgumentError, "Missing :start" unless params.key? :start
32
+ raise ArgumentError, "Missing :end" unless params.key? :end
33
+ raise ArgumentError, "Missing :granularity" unless params.key? :granularity
34
+
35
+ product_id = params.delete(:product_id)
36
+ get("products/#{product_id}/candles", params)
37
+ end
38
+
39
+ def get_market_trades(params = {})
40
+ raise ArgumentError, "Missing :product_id" unless params.key? :product_id
41
+ raise ArgumentError, "Missing :limit" unless params.key? :limit
42
+
43
+ product_id = params.delete(:product_id)
44
+ get("products/#{product_id}/ticker", params)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coinbase
4
+ module Advanced
5
+ module REST
6
+ module Resources
7
+ module Public
8
+ def get_server_time # rubocop:disable Naming/AccessorMethodName
9
+ get("time", {}, auth_required: false)
10
+ end
11
+
12
+ def get_public_product_book(params = {})
13
+ raise ArgumentError, "Missing :product_id" unless params.key? :product_id
14
+
15
+ get("market/product_book", params, auth_required: false)
16
+ end
17
+
18
+ def list_public_products(params = {})
19
+ get("market/products", params, auth_required: false)
20
+ end
21
+
22
+ def get_public_product(params = {})
23
+ raise ArgumentError, "Missing :product_id" unless params.key? :product_id
24
+
25
+ product_id = params.delete(:product_id)
26
+ get("market/products/#{product_id}", params, auth_required: false)
27
+ end
28
+
29
+ def get_public_product_candles(params = {})
30
+ raise ArgumentError, "Missing :product_id" unless params.key? :product_id
31
+ raise ArgumentError, "Missing :start" unless params.key? :start
32
+ raise ArgumentError, "Missing :end" unless params.key? :end
33
+ raise ArgumentError, "Missing :granularity" unless params.key? :granularity
34
+
35
+ product_id = params.delete(:product_id)
36
+ get("market/products/#{product_id}/candles", params, auth_required: false)
37
+ end
38
+
39
+ def get_public_market_trades(params = {})
40
+ raise ArgumentError, "Missing :product_id" unless params.key? :product_id
41
+ raise ArgumentError, "Missing :limit" unless params.key? :limit
42
+
43
+ product_id = params.delete(:product_id)
44
+ get("market/products/#{product_id}/ticker", params, auth_required: false)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coinbase
4
+ module Advanced
5
+ module REST
6
+ # RESTBase sets up, executes, and appropriately handles errors strictly within context of HTTP + REST communication
7
+ class RESTBase < APIBase
8
+ attr_reader :session
9
+
10
+ include Resources::Accounts
11
+ include Resources::Products
12
+ include Resources::Orders
13
+ include Resources::Portfolios
14
+ include Resources::Futures
15
+ include Resources::Perpetuals
16
+ include Resources::Converts
17
+ include Resources::Fees
18
+ include Resources::Public
19
+ include Resources::PaymentMethods
20
+ include Resources::DataAPI
21
+
22
+ AUTH_ERROR_MESSAGE = <<~HEREDOC
23
+ Unauthenticated request to private endpoint. If you wish to access private endpoints, you must \
24
+ provide your API key and secret when initializing the RESTClient.
25
+ HEREDOC
26
+
27
+ def initialize(config = Coinbase::Advanced.config)
28
+ super
29
+ @session = Faraday.new
30
+ end
31
+
32
+ def set_headers(method, path)
33
+ @session.headers = {
34
+ "User-Agent" => Coinbase::Advanced::USER_AGENT,
35
+ "Content-Type" => "application/json"
36
+ }
37
+
38
+ return unless @authenticated
39
+
40
+ uri = "#{method} #{@base_url}#{path}"
41
+ @session.headers["Authorization"] = "Bearer #{build_jwt(uri)}"
42
+ end
43
+
44
+ # copied rb sample from https://docs.cdp.coinbase.com/advanced-trade/docs/ws-auth#code-samples
45
+ # See README if confused why this method is here
46
+ def build_jwt(uri = nil)
47
+ header = {
48
+ typ: "JWT",
49
+ kid: @api_key,
50
+ nonce: SecureRandom.hex(16)
51
+ }
52
+
53
+ claims = {
54
+ sub: @api_key,
55
+ iss: "cdp",
56
+ aud: ["cdp_service"],
57
+ nbf: Time.now.to_i,
58
+ exp: Time.now.to_i + 120 # Expiration time: 2 minute from now.
59
+ }
60
+
61
+ claims[:uri] = uri if uri
62
+
63
+ private_key = OpenSSL::PKey.read(@api_secret)
64
+ JWT.encode(claims, private_key, "ES256", header)
65
+ end
66
+
67
+ def prepare_and_send_request(http_method, url_path, params = {}, data = {}, auth_required: true)
68
+ raise AuthenticationError, AUTH_ERROR_MESSAGE if auth_required && !@authenticated
69
+
70
+ url_path = "#{@api_prefix}/#{url_path.dup}"
71
+
72
+ set_headers(http_method, url_path)
73
+ @session.params = params unless params.empty?
74
+ url_path.insert(0, "/") if url_path[0] != "/"
75
+
76
+ send_request(http_method, url_path, data)
77
+ end
78
+
79
+ # def send_request(http_method, url_path, params, headers, data = None) - py method signature, lot of redundant shit
80
+ def send_request(http_method, url_path, data = {})
81
+ url = URI::HTTPS.build(host: @base_url, path: url_path).to_s
82
+
83
+ @config.log("Sending #{http_method.to_s.upcase}:#{url} with body #{data.to_json}") if @verbose
84
+
85
+ response = case http_method
86
+ when :get then @session.get(url)
87
+ when :post then @session.post(url, data.to_json)
88
+ when :put then @session.put(url, data.to_json)
89
+ when :delete then @session.delete(url, data.to_json)
90
+ else
91
+ raise URI::BadURIError, "Unknown HTTP method / verb: '#{http_method}'"
92
+ end
93
+
94
+ @config.log("Response object:\n\n#{response.pretty_inspect}") if @verbose
95
+
96
+ BaseResponse.new(JSON.parse(response.body))
97
+ end
98
+
99
+ def get(url_path, params = {}, auth_required: true)
100
+ prepare_and_send_request(:get, url_path, params, {}, auth_required: auth_required)
101
+ end
102
+
103
+ def post(url_path, params = {}, data = {}, auth_required: true)
104
+ prepare_and_send_request(:post, url_path, params, data, auth_required: auth_required)
105
+ end
106
+
107
+ def put(url_path, params = {}, data = {}, auth_required: true)
108
+ prepare_and_send_request(:put, url_path, params, data, auth_required: auth_required)
109
+ end
110
+
111
+ def delete(url_path, params = {}, data = {}, auth_required: true)
112
+ prepare_and_send_request(:delete, url_path, params, data, auth_required: auth_required)
113
+ end
114
+ end
115
+
116
+ class AuthenticationError < StandardError
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coinbase
4
+ module Advanced
5
+ VERSION = "0.1.1"
6
+ end
7
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "faraday"
5
+
6
+ require "jwt"
7
+ require "openssl"
8
+ require "time"
9
+ require "securerandom"
10
+
11
+ require_relative "advanced/concerns/filter_param"
12
+
13
+ require_relative "advanced/constants"
14
+ require_relative "advanced/api_base"
15
+ require_relative "advanced/version"
16
+
17
+ require_relative "advanced/rest/resources/accounts"
18
+ require_relative "advanced/rest/resources/products"
19
+ require_relative "advanced/rest/resources/orders"
20
+ require_relative "advanced/rest/resources/portfolios"
21
+ require_relative "advanced/rest/resources/futures"
22
+ require_relative "advanced/rest/resources/perpetuals"
23
+ require_relative "advanced/rest/resources/converts"
24
+ require_relative "advanced/rest/resources/fees"
25
+ require_relative "advanced/rest/resources/public"
26
+ require_relative "advanced/rest/resources/payment_methods"
27
+ require_relative "advanced/rest/resources/data_api"
28
+
29
+ require_relative "advanced/rest/base_response"
30
+ require_relative "advanced/rest/rest_base"
31
+
32
+ class Configuration
33
+ DEFAULT_API_BASE_URL = "api.coinbase.com"
34
+ DEFAULT_API_PREFIX = "/api/v3/brokerage"
35
+
36
+ DEFAULT_LOGGER = Logger.new($stdout)
37
+ DEFAULT_LOG_LEVEL = Logger::INFO
38
+
39
+ DEFAULT_SENSITIVE_PARAMS = %i[@api_key @api_secret].freeze
40
+
41
+ include Coinbase::Advanced::Concerns::FilterParam
42
+
43
+ attr_accessor :base_url, :api_prefix, :key_file, :api_key, :api_secret,
44
+ :timeout, :verbose, :filter_params
45
+
46
+ attr_reader :logger, :log_level
47
+
48
+ def initialize
49
+ @base_url = DEFAULT_API_BASE_URL
50
+ @api_prefix = DEFAULT_API_PREFIX
51
+ @timeout = 30
52
+
53
+ @key_file = ENV.fetch("COINBASE_JSON_KEY_FILE", nil)
54
+ @api_key = ENV.fetch("COINBASE_API_KEY", nil)
55
+ @api_secret = ENV.fetch("COINBASE_API_SECRET", nil)&.gsub('\n', "\n")
56
+
57
+ @logger = DEFAULT_LOGGER
58
+ @logger.level = DEFAULT_LOG_LEVEL
59
+ @verbose = false
60
+ @filter_params = DEFAULT_SENSITIVE_PARAMS
61
+ end
62
+
63
+ def setup
64
+ yield(self) if block_given?
65
+ end
66
+
67
+ # Currently only supporting default logger
68
+ def logger=(logger)
69
+ raise ArgumentError, "Only default logger supported" unless logger.is_a?(Logger)
70
+
71
+ @logger = logger
72
+ end
73
+
74
+ def log_level=(level)
75
+ valid_levels = %i[debug info warn error fatal]
76
+ raise ArgumentError, "Invalid log level" unless valid_levels.include?(level)
77
+
78
+ @log_level = level
79
+ end
80
+
81
+ def log(message, level = :info)
82
+ return unless @logger
83
+
84
+ @logger.send(level, message)
85
+ end
86
+ end
87
+
88
+ # inspired by https://github.com/stripe/stripe-ruby/blob/master/lib/stripe.rb
89
+ # TODO: this probably is redundant because:
90
+ # A. configuration class is defined right above
91
+ # B. def_delegators here are equivalent to attr_accessor
92
+ # Keeping it here for time being until a Stripe engineer gets back to me
93
+ module Coinbase
94
+ module Advanced
95
+ @config = Configuration.new
96
+
97
+ class << self
98
+ extend Forwardable
99
+
100
+ attr_reader :config
101
+
102
+ # User configurable options
103
+ def_delegators :@config, :base_url, :base_url=
104
+ def_delegators :@config, :api_prefix, :api_prefix=
105
+ def_delegators :@config, :key_file, :key_file=
106
+ def_delegators :@config, :api_key, :api_key=
107
+ def_delegators :@config, :api_secret, :api_secret=
108
+ def_delegators :@config, :timeout, :timeout=
109
+ def_delegators :@config, :verbose, :verbose=
110
+ def_delegators :@config, :logger, :log_level=
111
+ def_delegators :@config, :log_level, :log_level=
112
+ def_delegators :@config, :filter_params, :filter_params=
113
+
114
+ def configure
115
+ yield(@config) if block_given?
116
+ end
117
+ end
118
+ end
119
+ end