DhanHQ 2.6.1 → 2.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b6e949dc3b147daabce77b5d808d5dcdeb4953e9751322eb8cd671d70201c57
4
- data.tar.gz: 12327daca50d50836d724f4eeb5cf664abee766767c7bdec31f51b07b78ddc00
3
+ metadata.gz: 4469f87602b1825aa53ac5d53ef3493fb3a6fd40b00fd58199f23ba3c4a3e129
4
+ data.tar.gz: 7b5c65ab06cf2f945f488af2ab63499eb9a5fe2cea6ec04579db12a227ac121e
5
5
  SHA512:
6
- metadata.gz: 0ba1d351594f83a0824961dff0e8cfb28ce9b6fd6e65daa9124bf0ceab651c49aaefd00674cdbfdd78a48f9e7c2032395b4981b0b613bfccbee49b7c46ba319d
7
- data.tar.gz: 2e5ca5a81905c9e4fa861296f247eaa2411bf548f84cd94e348c51a079b406280ae9247838c013173552e6564c625ded0fd4504284d097b27892b55ecb9380ca
6
+ metadata.gz: 13683dc64df357c014a4427bbf3ee9981f3c52ad12795cd7f08d02e3d2cab3597efdb3668f63521f3bd2d43d616ac5ba577b11e104b304738c1b1c0769438bd2
7
+ data.tar.gz: cc2992611f9d3d4c785be89edc8bb3c33bffb6170a2575a20c532c8ff4ee9838bc1bf0021cdec6327349ff008fa7ae278b8e56d7cc2286310df3d0f4cd604292
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [2.6.2] - 2026-03-07
2
+
3
+ ### Changed
4
+
5
+ - **Release from main** after merging add-sandbox-support. Includes sandbox REST base URL, `ensure_configuration!`, payload non-mutation, Rakefile/VCR fixes, and docs. Full feature list is under [2.6.0](#260---2026-03-06); 2.6.0 and 2.6.1 are already on RubyGems.
6
+
7
+ ---
8
+
1
9
  ## [2.6.1] - 2026-03-07
2
10
 
3
11
  ### Changed
@@ -8,6 +16,22 @@
8
16
 
9
17
  ## [2.6.0] - 2026-03-06
10
18
 
19
+ ### Sandbox & configuration
20
+
21
+ - **Sandbox environment**: `DhanHQ.configuration.sandbox` (or `ENV["DHAN_SANDBOX"]=true`) switches REST base URL to `https://sandbox.dhan.co/v2`. Only `GET /v2/profile` and `GET /v2/fundlimit` are verified on sandbox; other REST endpoints are unverified. See `docs/ENDPOINTS_AND_SANDBOX.md`.
22
+ - **WebSocket**: Sandbox does **not** support WebSocket. Order updates, market feed, and market depth always use production URLs regardless of `sandbox`; no sandbox WS URLs are published.
23
+ - **Env-only bootstrap**: `DhanHQ.ensure_configuration!` ensures configuration exists (from ENV when nil). Called automatically in `Client#initialize` so apps using only `DHAN_CLIENT_ID` / `DHAN_ACCESS_TOKEN` work without calling `configure_with_env`.
24
+
25
+ ### Fixed
26
+
27
+ - **Payload mutation**: `prepare_payload` no longer mutates the caller's hash when injecting `dhanClientId` for DATA APIs; uses a duplicate so frozen or reused hashes are safe.
28
+ - **VCR**: Removed erroneous `/v2/v2/` market feed cassette entries (404 responses).
29
+ - **Rakefile**: Single RuboCop task; removed redundant `rubocop:fix` / `rubocop:fix_all` and deprecated `--auto-correct-all` flag.
30
+
31
+ ### Added
32
+
33
+ - **docs/ENDPOINTS_AND_SANDBOX.md**: Lists all REST/WebSocket endpoints, sandbox behavior, and endpoints verified vs not supported on sandbox.
34
+
11
35
  ### Fixed (API docs alignment)
12
36
 
13
37
  - **Kill Switch**: Manage API now uses query parameter per [dhanhq.co/docs/v2/traders-control](https://dhanhq.co/docs/v2/traders-control/). `Resources::KillSwitch#update(status)` sends `POST /v2/killswitch?killSwitchStatus=ACTIVATE` (or `DEACTIVATE`) with no body. `Models::KillSwitch.update("ACTIVATE")` / `.activate` / `.deactivate` unchanged.
data/Rakefile CHANGED
@@ -7,6 +7,8 @@ RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  require "rubocop/rake_task"
9
9
 
10
- RuboCop::RakeTask.new
10
+ # Single RuboCop task; the gem also registers rubocop:autocorrect and rubocop:autocorrect_all.
11
+ desc "Run RuboCop"
12
+ RuboCop::RakeTask.new(:rubocop)
11
13
 
12
14
  task default: %i[spec rubocop]
@@ -0,0 +1,103 @@
1
+ # DhanHQ gem — Endpoints and sandbox support
2
+
3
+ ## Sandbox behavior
4
+
5
+ - **REST:** When `DhanHQ.configuration.sandbox == true` (or `ENV["DHAN_SANDBOX"]=true`), the client uses `https://sandbox.dhan.co/v2` as the base URL for **all** requests that go through `DhanHQ::Client`. Every wrapped REST endpoint listed below is therefore **sent** to the sandbox host when sandbox is enabled.
6
+ - **Sandbox does NOT support WebSocket.** Order updates, market feed, and market depth are **production-only**. The gem always uses production WebSocket URLs regardless of the `sandbox` setting. There are no sandbox WebSocket endpoints in the Dhan v2 API; do not rely on sandbox for real-time streams.
7
+ - **Auth endpoints** (`DhanHQ::Auth`) do **not** use sandbox. They always call:
8
+ - `https://auth.dhan.co` — token generation
9
+ - `https://api.dhan.co/v2` — token renewal
10
+ So token generation/renewal always hit production; only data/order REST calls follow the sandbox flag.
11
+
12
+ ---
13
+
14
+ ## Sandbox: verified vs not working / unverified
15
+
16
+ | Status | Endpoints |
17
+ |-----------|-----------|
18
+ | **Verified on sandbox** (gem specs) | `GET /v2/profile`, `GET /v2/fundlimit` |
19
+ | **Sandbox connectivity spec** | `spec/dhan_hq/sandbox_connectivity_spec.rb` — uses VCR `record: :new_episodes`. In CI, use committed cassettes or skip without sandbox credentials; locally, run with `VCR_RECORD=new_episodes` and sandbox credentials to record. |
20
+ | **Not supported in sandbox** | All WebSocket endpoints (order updates, market feed, market depth). Use production only. |
21
+ | **Not verified on sandbox** | All other REST endpoints below. They may fail, return differently, or be unavailable in sandbox. Use Dhan documentation or manual testing before relying on them in sandbox. |
22
+
23
+ **REST endpoints not verified / may not work on sandbox** (only profile and funds are verified):
24
+
25
+ - `/v2/ledger`, `/v2/trades/{from}/{to}/{page}` (statements)
26
+ - `/v2/orders` (all order CRUD, slicing, external)
27
+ - `/v2/positions`, `/v2/positions/convert`
28
+ - `/v2/holdings`
29
+ - `/v2/trades`, `/v2/trades/{order_id}`
30
+ - `/v2/forever/orders` (all)
31
+ - `/v2/super/orders` (all)
32
+ - `/v2/killswitch`
33
+ - `/trader-control`
34
+ - `/ip/getIP`, `/ip/setIP`, `/ip/modifyIP`
35
+ - `/edis/tpin`, `/edis/form`, `/edis/bulkform`, `/edis/inquire/{isin}`
36
+ - `/alerts/orders`
37
+ - `/v2/pnlExit`
38
+ - `/v2/margincalculator`, `/v2/margincalculator/multi`
39
+ - `/v2/instrument/{segment}`
40
+ - `/v2/marketfeed/ltp`, `/v2/marketfeed/ohlc`, `/v2/marketfeed/quote`
41
+ - `/v2/optionchain`, `/v2/optionchain/expirylist`
42
+ - `/v2/charts/historical`, `/v2/charts/intraday`, `/v2/charts/rollingoption`
43
+
44
+ ---
45
+
46
+ ## REST endpoints integrated in the gem
47
+
48
+ Paths are as built by the gem (HTTP_PATH + endpoint). Base URL is either `https://api.dhan.co/v2` (production) or `https://sandbox.dhan.co/v2` (sandbox).
49
+
50
+ | Resource / model | Path(s) | Methods | API type |
51
+ |---------------------------|----------------------------------------------|-----------|-----------------|
52
+ | **Profile** | `/v2/profile` | GET | non_trading_api |
53
+ | **Funds** | `/v2/fundlimit` | GET | non_trading_api |
54
+ | **Statements** | `/v2/ledger`, `/v2/trades/{from}/{to}/{page}`| GET | non_trading_api |
55
+ | **Orders** | `/v2/orders`, `/v2/orders/{id}`, `/v2/orders/external/{correlation_id}`, `/v2/orders/slicing` | GET, POST, PUT, DELETE | order_api |
56
+ | **Positions** | `/v2/positions`, `/v2/positions/convert` | GET, POST, DELETE | order_api |
57
+ | **Holdings** | `/v2/holdings` | GET | order_api |
58
+ | **Trades** | `/v2/trades`, `/v2/trades/{order_id}` | GET | order_api |
59
+ | **Forever orders** | `/v2/forever/orders`, `/v2/forever/orders/{id}` | GET, POST, PUT, DELETE | order_api |
60
+ | **Super orders** | `/v2/super/orders`, `/v2/super/orders/{id}`, leg delete | GET, POST, PUT, DELETE | order_api |
61
+ | **Kill switch** | `/v2/killswitch` | GET, POST | order_api |
62
+ | **Trader control** | `/trader-control` | GET, POST | order_api |
63
+ | **IP setup** | `/ip/getIP`, `/ip/setIP`, `/ip/modifyIP` | GET, POST, PUT | order_api |
64
+ | **EDIS** | `/edis/tpin`, `/edis/form`, `/edis/bulkform`, `/edis/inquire/{isin}` | GET, POST | order_api |
65
+ | **Alert orders** | `/alerts/orders` | GET, POST, PUT, DELETE | order_api |
66
+ | **PnL exit** | `/v2/pnlExit` | GET, POST, DELETE | order_api |
67
+ | **Margin calculator** | `/v2/margincalculator`, `/v2/margincalculator/multi` | POST | order_api |
68
+ | **Instruments** | `/v2/instrument/{segment}` (redirect to CSV) | GET | data_api |
69
+ | **Market feed** | `/v2/marketfeed/ltp`, `/v2/marketfeed/ohlc`, `/v2/marketfeed/quote` | POST | data_api / quote_api |
70
+ | **Option chain** | `/v2/optionchain`, `/v2/optionchain/expirylist` | POST | data_api |
71
+ | **Historical data** | `/v2/charts/historical`, `/v2/charts/intraday` | POST | data_api |
72
+ | **Expired options data** | `/v2/charts/rollingoption` | POST | data_api |
73
+
74
+ ---
75
+
76
+ ## Auth (outside main client base URL)
77
+
78
+ | Purpose | URL / path | Method |
79
+ |-------------------|--------------------------------|--------|
80
+ | Generate token | `https://auth.dhan.co/app/generateAccessToken` | POST |
81
+ | Renew token | `https://api.dhan.co/v2/RenewToken` | POST |
82
+
83
+ These are **not** sandbox-aware; they always use the URLs above.
84
+
85
+ ---
86
+
87
+ ## WebSocket endpoints (production only; sandbox not supported)
88
+
89
+ | Purpose | URL |
90
+ |----------------|-----|
91
+ | Order updates | `wss://api-order-update.dhan.co` |
92
+ | Market feed | `wss://api-feed.dhan.co` |
93
+ | Market depth | `wss://depth-api-feed.dhan.co/twentydepth` |
94
+
95
+ **Sandbox:** Dhan sandbox does **not** provide WebSocket services. These endpoints are production-only. The gem never switches WS URLs based on `sandbox`; you can still override via `DhanHQ.configuration.ws_order_url`, `ws_market_feed_url`, `ws_market_depth_url`, or env vars `DHAN_WS_ORDER_URL`, `DHAN_WS_MARKET_FEED_URL`, `DHAN_WS_MARKET_DEPTH_URL` if you have a different production URL.
96
+
97
+ ---
98
+
99
+ ## Summary
100
+
101
+ - **REST:** When `sandbox` is true, all REST calls go to `https://sandbox.dhan.co/v2`. Only `GET /v2/profile` and `GET /v2/fundlimit` are verified working on sandbox; other REST endpoints are not verified — see "Sandbox: verified vs not working / unverified" above.
102
+ - **Auth:** Token generation and renewal always use production hosts.
103
+ - **WebSockets:** Sandbox does **not** support WebSocket. Order updates, market feed, and market depth always use production URLs; the gem does not publish or use any sandbox WebSocket URLs.
data/lib/DhanHQ/client.rb CHANGED
@@ -40,29 +40,15 @@ module DhanHQ
40
40
  # @return [DhanHQ::Client] A new client instance.
41
41
  # @raise [DhanHQ::Error] If configuration is invalid or rate limiter initialization fails
42
42
  def initialize(api_type:)
43
- # Configure from ENV if DHAN_CLIENT_ID is present (backward compatible behavior)
44
- # Validation happens at request time in build_headers, not here
45
- DhanHQ.configure_with_env if ENV.fetch("DHAN_CLIENT_ID", nil)
46
-
43
+ DhanHQ.ensure_configuration!
47
44
  # Use shared rate limiter instance per API type to ensure proper coordination
48
45
  @rate_limiter = RateLimiter.for(api_type)
49
46
 
50
47
  raise DhanHQ::Error, "RateLimiter initialization failed" unless @rate_limiter
51
48
 
52
- # Get timeout values from configuration or environment, with sensible defaults
53
- connect_timeout = ENV.fetch("DHAN_CONNECT_TIMEOUT", 10).to_i
54
- read_timeout = ENV.fetch("DHAN_READ_TIMEOUT", 30).to_i
55
- write_timeout = ENV.fetch("DHAN_WRITE_TIMEOUT", 30).to_i
56
-
57
- @connection = Faraday.new(url: DhanHQ.configuration.base_url) do |conn|
58
- conn.request :json, parser_options: { symbolize_names: true }
59
- conn.response :json, content_type: /\bjson$/
60
- conn.response :logger if ENV["DHAN_DEBUG"] == "true"
61
- conn.options.timeout = read_timeout
62
- conn.options.open_timeout = connect_timeout
63
- conn.options.write_timeout = write_timeout
64
- conn.adapter Faraday.default_adapter
65
- end
49
+ # Store initial URL to detect changes
50
+ @last_base_url = DhanHQ.configuration.base_url
51
+ @connection = build_connection(@last_base_url)
66
52
  end
67
53
 
68
54
  # Sends an HTTP request to the API with automatic retry for transient errors.
@@ -77,13 +63,15 @@ module DhanHQ
77
63
  @token_manager&.ensure_valid_token!
78
64
  @rate_limiter.throttle! # **Ensure we don't hit rate limit before calling API**
79
65
 
66
+ # Ensure connection matches current configuration (e.g. sandbox toggle)
67
+ refresh_connection!
68
+
80
69
  attempt = 0
81
70
  auth_retry_done = false
82
71
  begin
83
- response = connection.send(method) do |req|
84
- req.url path
72
+ response = connection.send(method, path) do |req|
85
73
  req.headers.merge!(build_headers(path))
86
- prepare_payload(req, payload, method)
74
+ prepare_payload(req, payload, method, path)
87
75
  end
88
76
 
89
77
  handle_response(response)
@@ -201,6 +189,31 @@ module DhanHQ
201
189
 
202
190
  private
203
191
 
192
+ def refresh_connection!
193
+ current_url = DhanHQ.configuration.base_url
194
+ return if @last_base_url == current_url
195
+
196
+ @last_base_url = current_url
197
+ @connection = build_connection(current_url)
198
+ end
199
+
200
+ def build_connection(url)
201
+ # Get timeout values from configuration or environment, with sensible defaults
202
+ connect_timeout = ENV.fetch("DHAN_CONNECT_TIMEOUT", 10).to_i
203
+ read_timeout = ENV.fetch("DHAN_READ_TIMEOUT", 30).to_i
204
+ write_timeout = ENV.fetch("DHAN_WRITE_TIMEOUT", 30).to_i
205
+
206
+ Faraday.new(url: url) do |conn|
207
+ conn.request :json, parser_options: { symbolize_names: true }
208
+ conn.response :json, content_type: /\bjson$/
209
+ conn.response :logger if ENV["DHAN_DEBUG"] == "true"
210
+ conn.options.timeout = read_timeout
211
+ conn.options.open_timeout = connect_timeout
212
+ conn.options.write_timeout = write_timeout
213
+ conn.adapter Faraday.default_adapter
214
+ end
215
+ end
216
+
204
217
  # Calculates exponential backoff time
205
218
  #
206
219
  # @param attempt [Integer] Current attempt number (1-based)
@@ -12,6 +12,10 @@ module DhanHQ
12
12
  #
13
13
  # @return [String]
14
14
  BASE_URL = "https://api.dhan.co/v2"
15
+
16
+ # Default Sandbox API host.
17
+ # @return [String]
18
+ SANDBOX_URL = "https://sandbox.dhan.co/v2"
15
19
  # The client ID for API authentication.
16
20
  # @return [String, nil] The client ID or `nil` if not set.
17
21
  attr_accessor :client_id
@@ -32,8 +36,11 @@ module DhanHQ
32
36
  attr_accessor :on_token_expired
33
37
 
34
38
  # The base URL for API requests.
35
- # @return [String] The base URL for the DhanHQ API.
36
- attr_accessor :base_url
39
+ # @return [String]
40
+ attr_writer :base_url
41
+
42
+ # Whether to use the sandbox environment.
43
+ attr_accessor :sandbox
37
44
 
38
45
  # URL for the compact CSV format of instruments.
39
46
  # @return [String] URL for compact CSV.
@@ -48,21 +55,33 @@ module DhanHQ
48
55
  attr_accessor :ws_version
49
56
 
50
57
  # Websocket order updates endpoint.
58
+ # Sandbox does not support WebSocket; always returns production URL unless overridden.
51
59
  # @return [String]
52
- attr_accessor :ws_order_url
60
+ def ws_order_url
61
+ @ws_order_url || "wss://api-order-update.dhan.co"
62
+ end
53
63
 
54
64
  # Websocket market feed endpoint.
65
+ # Sandbox does not support WebSocket; always returns production URL unless overridden.
55
66
  # @return [String]
56
- attr_accessor :ws_market_feed_url
67
+ def ws_market_feed_url
68
+ @ws_market_feed_url || "wss://api-feed.dhan.co"
69
+ end
57
70
 
58
71
  # Websocket market depth endpoint.
72
+ # Sandbox does not support WebSocket; always returns production URL unless overridden.
59
73
  # @return [String]
60
- attr_accessor :ws_market_depth_url
74
+ def ws_market_depth_url
75
+ @ws_market_depth_url || "wss://depth-api-feed.dhan.co/twentydepth"
76
+ end
61
77
 
62
78
  # Market depth level (20 or 200).
63
79
  # @return [Integer]
64
80
  attr_accessor :market_depth_level
65
81
 
82
+ # Setters for websocket URLs
83
+ attr_writer :ws_order_url, :ws_market_feed_url, :ws_market_depth_url
84
+
66
85
  # Websocket user type for order updates.
67
86
  # @return [String] "SELF" or "PARTNER".
68
87
  attr_accessor :ws_user_type
@@ -91,6 +110,21 @@ module DhanHQ
91
110
  end
92
111
  end
93
112
 
113
+ # Returns the base URL to use. If {#sandbox} is true and {#base_url}
114
+ # is nil or the default production URL, returns {SANDBOX_URL}.
115
+ # @return [String]
116
+ def base_url
117
+ if sandbox? && (@base_url.nil? || @base_url == BASE_URL)
118
+ SANDBOX_URL
119
+ else
120
+ @base_url || BASE_URL
121
+ end
122
+ end
123
+
124
+ def sandbox?
125
+ @sandbox == true
126
+ end
127
+
94
128
  # Initializes a new configuration instance with default values.
95
129
  #
96
130
  # @example
@@ -100,11 +134,12 @@ module DhanHQ
100
134
  def initialize
101
135
  @client_id = ENV.fetch("DHAN_CLIENT_ID", nil)
102
136
  @access_token = ENV.fetch("DHAN_ACCESS_TOKEN", nil)
103
- @base_url = ENV.fetch("DHAN_BASE_URL", "https://api.dhan.co/v2")
137
+ @sandbox = ENV.fetch("DHAN_SANDBOX", "false").to_s.casecmp("true").zero?
138
+ @base_url = ENV.fetch("DHAN_BASE_URL", nil)
104
139
  @ws_version = ENV.fetch("DHAN_WS_VERSION", 2).to_i
105
- @ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL", "wss://api-order-update.dhan.co")
106
- @ws_market_feed_url = ENV.fetch("DHAN_WS_MARKET_FEED_URL", "wss://api-feed.dhan.co")
107
- @ws_market_depth_url = ENV.fetch("DHAN_WS_MARKET_DEPTH_URL", "wss://depth-api-feed.dhan.co/twentydepth")
140
+ @ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL", nil)
141
+ @ws_market_feed_url = ENV.fetch("DHAN_WS_MARKET_FEED_URL", nil)
142
+ @ws_market_depth_url = ENV.fetch("DHAN_WS_MARKET_DEPTH_URL", nil)
108
143
  @market_depth_level = ENV.fetch("DHAN_MARKET_DEPTH_LEVEL", "20").to_i
109
144
  @ws_user_type = ENV.fetch("DHAN_WS_USER_TYPE", "SELF")
110
145
  @partner_id = ENV.fetch("DHAN_PARTNER_ID", nil)
@@ -417,7 +417,11 @@ module DhanHQ
417
417
  DATA_API_PREFIXES = [
418
418
  "/v2/marketfeed/",
419
419
  "/v2/optionchain",
420
- "/v2/instrument/"
420
+ "/v2/instrument/",
421
+ "/v2/charts",
422
+ "/v2/margincalculator",
423
+ "/v2/profile",
424
+ "/v2/fundlimit"
421
425
  ].freeze
422
426
 
423
427
  # Mapping of exchange and segment combinations to canonical exchange segment names.
@@ -88,9 +88,10 @@ module DhanHQ
88
88
 
89
89
  # Format parameters based on API endpoint
90
90
  def format_params(endpoint, params)
91
- return params if marketfeed_api?(endpoint) || params.empty?
91
+ full_path = build_path(endpoint)
92
+ return params if marketfeed_api?(full_path) || params.empty?
92
93
 
93
- optionchain_api?(endpoint) ? titleize_keys(params) : camelize_keys(params)
94
+ optionchain_api?(full_path) ? titleize_keys(params) : camelize_keys(params)
94
95
  end
95
96
 
96
97
  # Determines if the API endpoint is for Option Chain
@@ -39,7 +39,7 @@ module DhanHQ
39
39
  "access-token" => token
40
40
  }
41
41
 
42
- # Add client-id for DATA APIs
42
+ # Add client-id for DATA APIs (now including sandbox profile/funds)
43
43
  if data_api?(path)
44
44
  client_id = DhanHQ.configuration&.client_id
45
45
  unless client_id
@@ -70,18 +70,31 @@ module DhanHQ
70
70
  # @param req [Faraday::Request] The request object.
71
71
  # @param payload [Hash] The request payload.
72
72
  # @param method [Symbol] The HTTP method.
73
- def prepare_payload(req, payload, method)
74
- return if payload.nil? || payload.empty?
73
+ def prepare_payload(req, payload, method, path = nil)
74
+ return if payload.nil? || (payload.empty? && (path.nil? || !data_api?(path)))
75
75
 
76
76
  unless payload.is_a?(Hash)
77
77
  raise DhanHQ::InputExceptionError,
78
78
  "Invalid payload: Expected a Hash, got #{payload.class}"
79
79
  end
80
80
 
81
+ out = payload
82
+ if path && data_api?(path) && %i[post put patch].include?(method)
83
+ client_id = DhanHQ.configuration&.client_id
84
+ if client_id && !payload.key?(:dhanClientId) && !payload.key?("dhanClientId")
85
+ out = payload.dup
86
+ if out.keys.any?(String)
87
+ out["dhanClientId"] = client_id
88
+ else
89
+ out[:dhanClientId] = client_id
90
+ end
91
+ end
92
+ end
93
+
81
94
  case method
82
95
  when :delete then req.params = {}
83
- when :get then req.params = payload
84
- else req.body = payload.to_json
96
+ when :get then req.params = out
97
+ else req.body = out.to_json
85
98
  end
86
99
  end
87
100
  end
@@ -218,7 +218,8 @@ module DhanHQ
218
218
  def create(params)
219
219
  # Normalize params and auto-inject dhan_client_id from configuration if not provided
220
220
  normalized_params = snake_case(params)
221
- normalized_params[:dhan_client_id] ||= DhanHQ.configuration.client_id if DhanHQ.configuration.client_id
221
+ config = DhanHQ.configuration
222
+ normalized_params[:dhan_client_id] ||= config.client_id if config&.client_id
222
223
  response = resource.create(normalized_params)
223
224
  return nil unless response.is_a?(Hash) && response["orderId"]
224
225
 
@@ -207,9 +207,9 @@ module DhanHQ
207
207
  # )
208
208
  #
209
209
  def create(params)
210
- # Normalize params and auto-inject dhan_client_id from configuration if not provided
211
210
  normalized_params = snake_case(params)
212
- normalized_params[:dhan_client_id] ||= DhanHQ.configuration.client_id if DhanHQ.configuration.client_id
211
+ config = DhanHQ.configuration
212
+ normalized_params[:dhan_client_id] ||= config.client_id if config&.client_id
213
213
  response = resource.create(normalized_params)
214
214
  return nil unless response.is_a?(Hash) && response["orderId"]
215
215
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "2.6.1"
5
+ VERSION = "2.6.2"
6
6
  end
@@ -32,7 +32,8 @@ module DhanHQ
32
32
 
33
33
  cid = DhanHQ.configuration.client_id or raise "DhanHQ.client_id not set"
34
34
  ver = (DhanHQ.configuration.respond_to?(:ws_version) && DhanHQ.configuration.ws_version) || 2
35
- @url = url || "wss://api-feed.dhan.co?version=#{ver}&token=#{token}&clientId=#{cid}&authType=2"
35
+ base = url || DhanHQ.configuration.ws_market_feed_url
36
+ @url = base.include?("?") ? base : "#{base}?version=#{ver}&token=#{token}&clientId=#{cid}&authType=2"
36
37
  end
37
38
 
38
39
  # Starts the WebSocket connection and event loop.
@@ -86,11 +86,12 @@ module DhanHQ
86
86
  cid = config.client_id or raise "DhanHQ.client_id not set"
87
87
  depth_level = config.market_depth_level || 20 # Default to 20 level depth
88
88
 
89
- if depth_level == 200
90
- "wss://full-depth-api.dhan.co/twohundreddepth?token=#{token}&clientId=#{cid}&authType=2"
91
- else
92
- "wss://depth-api-feed.dhan.co/twentydepth?token=#{token}&clientId=#{cid}&authType=2"
93
- end
89
+ base = if depth_level == 200
90
+ "wss://full-depth-api.dhan.co/twohundreddepth"
91
+ else
92
+ config.ws_market_depth_url
93
+ end
94
+ base.include?("?") ? base : "#{base}?token=#{token}&clientId=#{cid}&authType=2"
94
95
  end
95
96
 
96
97
  ##
data/lib/dhan_hq.rb CHANGED
@@ -4,6 +4,7 @@ require "json"
4
4
  require "logger"
5
5
  require "zeitwerk"
6
6
  require "dotenv/load"
7
+ require "faraday"
7
8
  # Minimal eager requires for backward-compatible constants.
8
9
  # These are widely referenced (e.g. `DhanHQ::BaseAPI`) and should not depend on
9
10
  # the autoloader being fully configured.
@@ -12,6 +13,7 @@ require_relative "DhanHQ/helpers/attribute_helper"
12
13
  require_relative "DhanHQ/helpers/validation_helper"
13
14
  require_relative "DhanHQ/helpers/request_helper"
14
15
  require_relative "DhanHQ/helpers/response_helper"
16
+ require_relative "DhanHQ/errors"
15
17
  require_relative "DhanHQ/core/base_api"
16
18
  require_relative "DhanHQ/core/base_model"
17
19
  require_relative "DhanHQ/core/base_resource"
@@ -89,15 +91,21 @@ module DhanHQ
89
91
  #
90
92
  # @return [void]
91
93
  def configure_with_env
94
+ self.configuration = Configuration.new
95
+ end
96
+
97
+ # Ensures configuration exists, bootstrapping from ENV when nil.
98
+ # Called automatically when building a Client so env-only integrations work without
99
+ # an explicit configure/configure_with_env call. Idempotent when configuration is already set.
100
+ #
101
+ # @return [DhanHQ::Configuration]
102
+ def ensure_configuration!
92
103
  self.configuration ||= Configuration.new
93
- configuration.access_token = ENV.fetch("DHAN_ACCESS_TOKEN", nil)
94
- configuration.client_id = ENV.fetch("DHAN_CLIENT_ID", nil)
95
- configuration.base_url = ENV.fetch("DHAN_BASE_URL", BASE_URL)
96
- configuration.ws_version = ENV.fetch("DHAN_WS_VERSION", configuration.ws_version || 2).to_i
97
- configuration.ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL", configuration.ws_order_url)
98
- configuration.ws_user_type = ENV.fetch("DHAN_WS_USER_TYPE", configuration.ws_user_type)
99
- configuration.partner_id = ENV.fetch("DHAN_PARTNER_ID", configuration.partner_id)
100
- configuration.partner_secret = ENV.fetch("DHAN_PARTNER_SECRET", configuration.partner_secret)
104
+ end
105
+
106
+ # Resets the configuration to nil.
107
+ def reset_configuration!
108
+ self.configuration = nil
101
109
  end
102
110
 
103
111
  # Configures the DhanHQ client by fetching credentials from a token endpoint.
@@ -120,12 +128,12 @@ module DhanHQ
120
128
  base_url ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BASE_URL", nil)
121
129
  bearer_token ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BEARER", nil)
122
130
 
123
- raise TokenEndpointError, "base_url and bearer_token (or ENV DHAN_TOKEN_ENDPOINT_*) are required" if base_url.to_s.empty? || bearer_token.to_s.empty?
131
+ raise DhanHQ::TokenEndpointError, "base_url and bearer_token (or ENV DHAN_TOKEN_ENDPOINT_*) are required" if base_url.to_s.empty? || bearer_token.to_s.empty?
124
132
 
125
133
  url = "#{base_url.to_s.chomp("/")}/auth/dhan/token"
126
- conn = Faraday.new(url: url) do |c|
134
+ conn = ::Faraday.new(url: url) do |c|
127
135
  c.response :json, content_type: /\bjson$/
128
- c.adapter Faraday.default_adapter
136
+ c.adapter ::Faraday.default_adapter
129
137
  end
130
138
 
131
139
  response = conn.get("") do |req|
@@ -144,7 +152,7 @@ module DhanHQ
144
152
  end
145
153
  end
146
154
  msg = body["error"] || body["message"] || body["errorMessage"] || response.body.to_s
147
- raise TokenEndpointError, "Token endpoint returned #{response.status}: #{msg}"
155
+ raise DhanHQ::TokenEndpointError, "Token endpoint returned #{response.status}: #{msg}"
148
156
  end
149
157
 
150
158
  data = if response.body.is_a?(Hash)
@@ -160,7 +168,7 @@ module DhanHQ
160
168
 
161
169
  access_token = data["access_token"] || data[:access_token]
162
170
  client_id = data["client_id"] || data[:client_id]
163
- raise TokenEndpointError, "Token endpoint response missing access_token or client_id" if access_token.to_s.empty? || client_id.to_s.empty?
171
+ raise DhanHQ::TokenEndpointError, "Token endpoint response missing access_token or client_id" if access_token.to_s.empty? || client_id.to_s.empty?
164
172
 
165
173
  self.configuration ||= Configuration.new
166
174
  configuration.access_token = access_token.to_s
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: DhanHQ
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.1
4
+ version: 2.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shubham Taywade
@@ -214,6 +214,7 @@ files:
214
214
  - docs/CONFIGURATION.md
215
215
  - docs/CONSTANTS_REFERENCE.md
216
216
  - docs/DATA_API_PARAMETERS.md
217
+ - docs/ENDPOINTS_AND_SANDBOX.md
217
218
  - docs/LIVE_ORDER_UPDATES.md
218
219
  - docs/RAILS_INTEGRATION.md
219
220
  - docs/RAILS_WEBSOCKET_INTEGRATION.md