DhanHQ 2.2.0 → 2.2.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 +4 -4
- data/CHANGELOG.md +47 -0
- data/GUIDE.md +5 -1
- data/README.md +2 -0
- data/docs/AUTHENTICATION.md +50 -3
- data/docs/TESTING_GUIDE.md +2 -0
- data/docs/live_order_updates.md +1 -1
- data/docs/rails_integration.md +2 -0
- data/docs/rails_websocket_integration.md +2 -0
- data/docs/standalone_ruby_websocket_integration.md +2 -0
- data/docs/websocket_integration.md +1 -1
- data/lib/DhanHQ/auth.rb +53 -0
- data/lib/DhanHQ/configuration.rb +1 -0
- data/lib/DhanHQ/contracts/expired_options_data_contract.rb +9 -1
- data/lib/DhanHQ/contracts/historical_data_contract.rb +30 -3
- data/lib/DhanHQ/contracts/trade_contract.rb +10 -3
- data/lib/DhanHQ/errors.rb +2 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/client.rb +1 -0
- data/lib/DhanHQ/ws/market_depth/client.rb +1 -0
- data/lib/DhanHQ/ws/orders/connection.rb +1 -0
- data/lib/dhan_hq.rb +71 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '09510cd95013213714d5ba4878e87dfa6bd5968cb90ea4e0a539ddb0d05bc2b0'
|
|
4
|
+
data.tar.gz: 459c316d0f2c68ad8b0556518dfd70762e460e5c68ef063439e73f68ca33ba02
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d54cf0cc58e82d1566f525303ea25fddd051b1c838152da39f12509435a16e4005f55417d5516c52f6222b4ba6286c421202d0a148f743be7ca4127b7ef03bae
|
|
7
|
+
data.tar.gz: ea522dfeb54054d8162d00ead357efb4c444c3dd0c4c182c4d688a2bd9a484d44af7b2bab221476c93fded44511bfff7e8cf4b75d6e962fc8191486bee5440b1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [2.2.2] - 2026-01-31
|
|
4
|
+
|
|
5
|
+
### Contracts (date validation)
|
|
6
|
+
|
|
7
|
+
- **from_date / to_date**: `from_date` must be strictly before `to_date` and must be a valid trading date (no weekend). `to_date` may be any date after `from_date` (format YYYY-MM-DD). Applied in `HistoricalDataContract`, `TradeHistoryContract`, and `ExpiredOptionsDataContract`.
|
|
8
|
+
- **HistoricalDataContract**: Added trading-day check for `from_date` and `from_date < to_date`; inherits `BaseContract`.
|
|
9
|
+
|
|
10
|
+
### Specs & tooling
|
|
11
|
+
|
|
12
|
+
- **Specs**: Base model, expired options, trade, and historical data specs updated to use weekday dates and expect `from_date must be before to_date`; VCR cassette `trade_history.yml` updated for new date.
|
|
13
|
+
- **RuboCop**: RSpec/ExampleLength in expired options contract spec fixed via `next_weekday` helper.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## [2.2.1] - 2026-01-31
|
|
18
|
+
|
|
19
|
+
### Authentication
|
|
20
|
+
|
|
21
|
+
- **RenewToken API**: Added `DhanHQ::Auth.renew_token(access_token, client_id, base_url: nil)` to refresh web-generated access tokens (24h validity). Calls GET `/v2/RenewToken` with `access-token` and `dhanClientId` headers; returns response hash with indifferent access (e.g. `accessToken`, `expiryTime`). Use in `access_token_provider` or `on_token_expired` to refresh and store the new token. Only valid for tokens generated from Dhan Web (not API key flow).
|
|
22
|
+
- **Dhan auth scope**: Documented that the gem does **not** implement API key/secret consent or Partner consent flows; apps obtain tokens via Dhan Web, API key OAuth, or Partner flow and pass them to the gem. See [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md).
|
|
23
|
+
|
|
24
|
+
### Documentation
|
|
25
|
+
|
|
26
|
+
- **docs/AUTHENTICATION.md**: Added “How you get the token (Dhan’s responsibility)” (Individual: Web token, RenewToken, API key; Partner: consent flow) and “RenewToken (web-generated tokens only)” with `DhanHQ::Auth.renew_token` usage and example. “See also” updated for GUIDE, rails_integration, TESTING_GUIDE, CHANGELOG 2.2.0/2.2.1.
|
|
27
|
+
- **README.md**: Note under Dynamic access token for RenewToken via `DhanHQ::Auth.renew_token` and that API key/Partner flows are implemented in the app.
|
|
28
|
+
- **GUIDE.md**: “Dynamic access token” section extended with RenewToken (`DhanHQ::Auth.renew_token`) and note that API key/Partner flows are in the app.
|
|
29
|
+
- **docs/TESTING_GUIDE.md**: Optional config comment for RenewToken and pointer to AUTHENTICATION.md (API key/Partner in app).
|
|
30
|
+
- **docs/rails_integration.md**: “Dynamic access token” section extended with RenewToken (web-generated tokens) and link to AUTHENTICATION.md.
|
|
31
|
+
- **docs/websocket_integration.md**, **docs/live_order_updates.md**: Notes updated for dynamic token, RenewToken, and API key/Partner in app.
|
|
32
|
+
- **docs/standalone_ruby_websocket_integration.md**, **docs/rails_websocket_integration.md**: Configuration section updated with RenewToken and AUTHENTICATION.md link.
|
|
33
|
+
|
|
34
|
+
### CI / Release
|
|
35
|
+
|
|
36
|
+
- **Release workflow**: Aligned with ollama-client: tag-based release (`on: push: tags: v*`), validate tag vs gem version, use `GEM_HOST_API_KEY` for RubyGems push (no credentials file), single retry with OTP. Removed GitHub Release step.
|
|
37
|
+
- **RELEASING.md**, **docs/RELEASE_GUIDE.md**: Updated to describe tag-only publish and `GEM_HOST_API_KEY`; removed references to “Create GitHub Release” and “Run tests” in release job.
|
|
38
|
+
|
|
39
|
+
### Fixes
|
|
40
|
+
|
|
41
|
+
- **RuboCop**: Layout/EmptyLineAfterGuardClause — added blank line after guard clauses in Configuration, WS client, market depth client, orders connection. Style/NilLambda — `-> { nil }` → `-> {}` in configuration_spec. RSpec/InstanceVariable — replaced `@token_call_count` and `@hook_called`/`@hook_error` with `let(:token_call_count)`, `let(:token_provider)`, `let(:hook_state)` in client_spec auth-failure examples.
|
|
42
|
+
- **CI**: Gemfile.lock updated for path gem version (DhanHQ 2.2.1) so `bundle install` in frozen mode succeeds.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **lib/DhanHQ/auth.rb**: New module with `Auth.renew_token` for Dhan RenewToken API.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
3
50
|
## [2.2.0] - 2026-01-31
|
|
4
51
|
|
|
5
52
|
### Authentication & token handling
|
data/GUIDE.md
CHANGED
|
@@ -71,7 +71,11 @@ Set any of the following environment variables _before_ calling
|
|
|
71
71
|
|
|
72
72
|
**Dynamic access token**
|
|
73
73
|
|
|
74
|
-
For token rotation without restarting the app, set `access_token_provider` (Proc/lambda) so the token is resolved at request time. When the API returns 401 or token-expired (error code 807) and the provider is set, the client retries the request once with a fresh token. Optional `on_token_expired` is called before that retry.
|
|
74
|
+
For token rotation without restarting the app, set `access_token_provider` (Proc/lambda) so the token is resolved at request time. When the API returns 401 or token-expired (error code 807) and the provider is set, the client retries the request once with a fresh token. Optional `on_token_expired` is called before that retry.
|
|
75
|
+
|
|
76
|
+
**RenewToken (web-generated tokens):** If the token was generated from Dhan Web (24h validity), use `DhanHQ::Auth.renew_token(access_token, client_id)` to refresh it; use the returned token in your provider or store. The gem does **not** implement API key/secret or Partner consent flows—implement those in your app and pass the token to the gem.
|
|
77
|
+
|
|
78
|
+
See [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md) and README “Dynamic access token”.
|
|
75
79
|
|
|
76
80
|
---
|
|
77
81
|
|
data/README.md
CHANGED
|
@@ -114,6 +114,8 @@ end
|
|
|
114
114
|
|
|
115
115
|
If the API returns **401** or error code **807** (token expired) and `access_token_provider` is set, the client retries the request **once** with a fresh token from the provider. Otherwise it raises `DhanHQ::InvalidAuthenticationError` or `DhanHQ::TokenExpiredError`. Missing or nil token from config raises `DhanHQ::AuthenticationError`.
|
|
116
116
|
|
|
117
|
+
**RenewToken (web-generated tokens):** For tokens generated from Dhan Web (24h validity), use `DhanHQ::Auth.renew_token(access_token, client_id)` to refresh; use the returned token in your provider or store. The gem does **not** implement API key/secret or Partner consent flows—implement those in your app and pass the token to the gem. See [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md).
|
|
118
|
+
|
|
117
119
|
### Logging
|
|
118
120
|
|
|
119
121
|
```ruby
|
data/docs/AUTHENTICATION.md
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
# Authentication & token handling
|
|
2
2
|
|
|
3
|
-
This document describes how the gem handles access tokens,
|
|
3
|
+
This document describes how the gem handles access tokens, dynamic resolution, retry-on-401, and how that fits with Dhan’s authentication methods.
|
|
4
|
+
|
|
5
|
+
## How you get the token (Dhan’s responsibility)
|
|
6
|
+
|
|
7
|
+
Dhan supports several ways to obtain an access token. **The gem does not implement these flows**; your app or the user obtains the token, and the gem uses it.
|
|
8
|
+
|
|
9
|
+
| User type | Method | Where it happens |
|
|
10
|
+
| --------- |--------|------------------|
|
|
11
|
+
| **Individual** | **Access token from Dhan Web** | User logs in at web.dhan.co → My Profile → Access DhanHQ APIs → Generate token (24h). You can refresh it with **RenewToken** (see below); the gem can call that for you. |
|
|
12
|
+
| **Individual** | **API key & secret (OAuth)** | User creates API key/secret at web.dhan.co. Your app does: (1) Generate consent, (2) Browser login, (3) Consume consent to get `accessToken` and `expiryTime`. Implement this flow in your app; then pass the token to the gem via `access_token` or `access_token_provider`. |
|
|
13
|
+
| **Partner** | **Partner consent flow** | You have `partner_id` and `partner_secret`. Your app does: (1) Generate consent, (2) User logs in on browser, (3) Consume consent to get `accessToken`. Implement in your app; pass the token to the gem. |
|
|
14
|
+
|
|
15
|
+
**What the gem provides:** It accepts a token (static or from a provider), sends it on every request, and can retry once on 401 when you use `access_token_provider`. It also provides **`DhanHQ::Auth.renew_token`** for refreshing **web-generated** tokens (RenewToken API). It does **not** implement API key/secret consent or Partner consent; use Dhan’s docs and your own HTTP client for those.
|
|
16
|
+
|
|
17
|
+
For full details and curl examples, see [DhanHQ API docs](https://dhanhq.co/docs) (Authentication).
|
|
4
18
|
|
|
5
19
|
## Static token (default)
|
|
6
20
|
|
|
@@ -34,6 +48,37 @@ end
|
|
|
34
48
|
|
|
35
49
|
REST and WebSocket clients both use `config.resolved_access_token`, which calls the provider when set or falls back to `access_token`.
|
|
36
50
|
|
|
51
|
+
## RenewToken (web-generated tokens only)
|
|
52
|
+
|
|
53
|
+
If the token was **generated from Dhan Web** (not API key flow), you can refresh it (24h validity) using Dhan’s RenewToken API. The gem provides a helper:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
# Returns a hash with API response (e.g. accessToken, expiryTime). Use the new token for subsequent requests.
|
|
57
|
+
response = DhanHQ::Auth.renew_token(current_access_token, client_id)
|
|
58
|
+
new_token = response["accessToken"] || response[:accessToken]
|
|
59
|
+
# Optional: response may include "expiryTime"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Use this inside `access_token_provider` or in `on_token_expired` to refresh and then return the new token (e.g. store it and return it from the provider on the next request).
|
|
63
|
+
|
|
64
|
+
Example: refresh in provider and cache the result until near expiry:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# Pseudocode: store current token + expiry; in provider, refresh if expired or near expiry
|
|
68
|
+
config.access_token_provider = lambda do
|
|
69
|
+
stored = YourTokenStore.current
|
|
70
|
+
if stored.nil? || stored.expired_soon?
|
|
71
|
+
response = DhanHQ::Auth.renew_token(stored&.access_token || ENV["ACCESS_TOKEN"], config.client_id)
|
|
72
|
+
YourTokenStore.update!(response["accessToken"], response["expiryTime"])
|
|
73
|
+
stored = YourTokenStore.current
|
|
74
|
+
end
|
|
75
|
+
raise "Token missing" unless stored
|
|
76
|
+
stored.access_token
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Note:** RenewToken is only for tokens generated from Dhan Web. For API key or Partner flows, obtain a new token using Dhan’s consent APIs in your app.
|
|
81
|
+
|
|
37
82
|
## Retry-on-401
|
|
38
83
|
|
|
39
84
|
When the API returns **401** or a token-expired error (e.g. error code **807**), and `access_token_provider` is set:
|
|
@@ -59,5 +104,7 @@ Rescue `AuthenticationError` for local config/token resolution failures; rescue
|
|
|
59
104
|
## See also
|
|
60
105
|
|
|
61
106
|
- [README.md](../README.md) — Configuration and “Dynamic access token”
|
|
62
|
-
- [
|
|
63
|
-
- [
|
|
107
|
+
- [GUIDE.md](../GUIDE.md) — Dynamic access token and RenewToken
|
|
108
|
+
- [rails_integration.md](rails_integration.md) — Rails initializer with optional `access_token_provider` and RenewToken
|
|
109
|
+
- [TESTING_GUIDE.md](TESTING_GUIDE.md) — Config examples and RenewToken
|
|
110
|
+
- [CHANGELOG.md](../CHANGELOG.md) — 2.2.0 and 2.2.1 auth and token changes
|
data/docs/TESTING_GUIDE.md
CHANGED
|
@@ -56,6 +56,8 @@ end
|
|
|
56
56
|
# config.access_token_provider = -> { YourTokenStore.active_token }
|
|
57
57
|
# config.on_token_expired = ->(err) { YourTokenStore.refresh! }
|
|
58
58
|
# end
|
|
59
|
+
# Optional: refresh web-generated tokens with DhanHQ::Auth.renew_token(current_token, client_id)
|
|
60
|
+
# See docs/AUTHENTICATION.md (API key/Partner flows are implemented in your app, not in the gem)
|
|
59
61
|
|
|
60
62
|
# Set log level for debugging
|
|
61
63
|
DhanHQ.logger.level = Logger::DEBUG
|
data/docs/live_order_updates.md
CHANGED
|
@@ -18,7 +18,7 @@ The DhanHQ Ruby client provides comprehensive real-time order update functionali
|
|
|
18
18
|
```ruby
|
|
19
19
|
require 'dhan_hq'
|
|
20
20
|
|
|
21
|
-
# Configure credentials
|
|
21
|
+
# Configure credentials. For dynamic token use config.access_token_provider; for web tokens refresh with DhanHQ::Auth.renew_token. API key/Partner: implement in app. See docs/AUTHENTICATION.md.
|
|
22
22
|
DhanHQ.configure do |config|
|
|
23
23
|
config.client_id = "your_client_id"
|
|
24
24
|
config.access_token = "your_access_token"
|
data/docs/rails_integration.md
CHANGED
|
@@ -122,6 +122,8 @@ end
|
|
|
122
122
|
|
|
123
123
|
When the API returns 401 or token-expired (error code 807) and `access_token_provider` is set, the client retries the request **once** with a fresh token from the provider. `on_token_expired` is called before that retry so you can refresh your store if needed.
|
|
124
124
|
|
|
125
|
+
**RenewToken (web-generated tokens only):** If the token was generated from Dhan Web (24h validity), you can refresh it with `DhanHQ::Auth.renew_token(access_token, client_id)` and store the result; use it in your provider or call it from `on_token_expired`. The gem does **not** implement API key/secret or Partner consent flows—implement those in your app and pass the token to the gem. See [docs/AUTHENTICATION.md](AUTHENTICATION.md).
|
|
126
|
+
|
|
125
127
|
## 3. Build service objects for REST flows
|
|
126
128
|
|
|
127
129
|
Wrap trading actions in plain-old Ruby objects so controllers and jobs stay thin:
|
|
@@ -99,6 +99,8 @@ end
|
|
|
99
99
|
|
|
100
100
|
## Configuration
|
|
101
101
|
|
|
102
|
+
For dynamic token use `config.access_token_provider`. For web-generated tokens refresh with `DhanHQ::Auth.renew_token(access_token, client_id)`. API key/Partner flows: implement in your app. See [AUTHENTICATION.md](AUTHENTICATION.md).
|
|
103
|
+
|
|
102
104
|
### Environment-Specific Configuration
|
|
103
105
|
|
|
104
106
|
```ruby
|
|
@@ -68,6 +68,8 @@ ruby market_feed_script.rb
|
|
|
68
68
|
|
|
69
69
|
## Configuration
|
|
70
70
|
|
|
71
|
+
For dynamic token at request time use `config.access_token_provider`. For web-generated tokens (24h) refresh with `DhanHQ::Auth.renew_token(access_token, client_id)`. API key/Partner flows: implement in your app and pass the token to the gem. See [AUTHENTICATION.md](AUTHENTICATION.md).
|
|
72
|
+
|
|
71
73
|
### Environment Variables
|
|
72
74
|
|
|
73
75
|
```bash
|
|
@@ -32,7 +32,7 @@ DhanHQ.configure do |config|
|
|
|
32
32
|
config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
|
|
33
33
|
config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
|
|
34
34
|
end
|
|
35
|
-
# For dynamic token
|
|
35
|
+
# For dynamic token: use config.access_token_provider. For web-generated tokens, refresh with DhanHQ::Auth.renew_token. API key/Partner flows: implement in your app. See docs/AUTHENTICATION.md.
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
### 2. Market Feed WebSocket (Recommended for Beginners)
|
data/lib/DhanHQ/auth.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module DhanHQ
|
|
7
|
+
# Helpers for Dhan authentication APIs.
|
|
8
|
+
# The gem does not implement API key/secret consent flows or Partner consent;
|
|
9
|
+
# use Dhan Web or your own OAuth flow to obtain tokens, then pass them via
|
|
10
|
+
# configuration. This module supports refreshing web-generated tokens only.
|
|
11
|
+
module Auth
|
|
12
|
+
# Refreshes a web-generated access token (24h validity).
|
|
13
|
+
# Calls POST /v2/RenewToken; expires the current token and returns a new one.
|
|
14
|
+
# Only valid for tokens generated from Dhan Web (not API key flow).
|
|
15
|
+
#
|
|
16
|
+
# @param access_token [String] Current JWT from Dhan Web
|
|
17
|
+
# @param client_id [String] Dhan client ID (dhanClientId)
|
|
18
|
+
# @param base_url [String, nil] API base URL (default: DhanHQ.configuration.base_url)
|
|
19
|
+
# @return [HashWithIndifferentAccess] Response with :access_token and :expiry_time (if present)
|
|
20
|
+
# @raise [DhanHQ::Error] On HTTP or API error
|
|
21
|
+
def self.renew_token(access_token, client_id, base_url: nil)
|
|
22
|
+
base_url ||= DhanHQ.configuration&.base_url || Configuration::BASE_URL
|
|
23
|
+
url = "#{base_url.chomp("/")}/RenewToken"
|
|
24
|
+
|
|
25
|
+
conn = Faraday.new(url: url) do |c|
|
|
26
|
+
c.request :json
|
|
27
|
+
c.response :json, content_type: /\bjson$/
|
|
28
|
+
c.adapter Faraday.default_adapter
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
response = conn.get("") do |req|
|
|
32
|
+
req.headers["access-token"] = access_token
|
|
33
|
+
req.headers["dhanClientId"] = client_id
|
|
34
|
+
req.headers["Accept"] = "application/json"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
unless response.success?
|
|
38
|
+
body = begin
|
|
39
|
+
JSON.parse(response.body.to_s)
|
|
40
|
+
rescue JSON::ParserError
|
|
41
|
+
{}
|
|
42
|
+
end
|
|
43
|
+
error_message = body["errorMessage"] || body["message"] || response.body.to_s
|
|
44
|
+
raise DhanHQ::InvalidAuthenticationError, "RenewToken failed: #{response.status} #{error_message}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
data = response.body
|
|
48
|
+
data = JSON.parse(data) if data.is_a?(String)
|
|
49
|
+
result = data.is_a?(Hash) ? data : {}
|
|
50
|
+
result.with_indifferent_access
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/DhanHQ/configuration.rb
CHANGED
|
@@ -69,7 +69,8 @@ module DhanHQ
|
|
|
69
69
|
from_date = Date.parse(values[:from_date])
|
|
70
70
|
to_date = Date.parse(values[:to_date])
|
|
71
71
|
|
|
72
|
-
key.failure("from_date must be
|
|
72
|
+
key.failure("from_date must be before to_date") if from_date >= to_date
|
|
73
|
+
key.failure("from_date must be a valid trading date (no weekend dates)") unless trading_day?(from_date)
|
|
73
74
|
|
|
74
75
|
# Check if date range is not too large (max 31 days; to_date is non-inclusive)
|
|
75
76
|
key.failure("date range cannot exceed 31 days") if (to_date - from_date).to_i > 31
|
|
@@ -98,6 +99,13 @@ module DhanHQ
|
|
|
98
99
|
false
|
|
99
100
|
end
|
|
100
101
|
end
|
|
102
|
+
|
|
103
|
+
def trading_day?(date)
|
|
104
|
+
return false unless date.is_a?(Date)
|
|
105
|
+
|
|
106
|
+
# Sunday = 0, Saturday = 6; trading days are Monday (1) through Friday (5)
|
|
107
|
+
(1..5).cover?(date.wday)
|
|
108
|
+
end
|
|
101
109
|
end
|
|
102
110
|
end
|
|
103
111
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
3
5
|
module DhanHQ
|
|
4
6
|
module Contracts
|
|
5
7
|
# Validates payloads for the historical data endpoints.
|
|
6
|
-
class HistoricalDataContract <
|
|
7
|
-
include DhanHQ::Constants
|
|
8
|
-
|
|
8
|
+
class HistoricalDataContract < BaseContract
|
|
9
9
|
params do
|
|
10
10
|
# Common required fields
|
|
11
11
|
required(:security_id).filled(:string)
|
|
@@ -23,6 +23,33 @@ module DhanHQ
|
|
|
23
23
|
# (valid: 1, 5, 15, 25, 60)
|
|
24
24
|
optional(:interval).maybe(:string, included_in?: %w[1 5 15 25 60])
|
|
25
25
|
end
|
|
26
|
+
|
|
27
|
+
rule(:from_date) do
|
|
28
|
+
next unless value.is_a?(String) && value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
29
|
+
|
|
30
|
+
d = Date.parse(value)
|
|
31
|
+
key.failure("must be a valid trading date (no weekend dates)") unless trading_day?(d)
|
|
32
|
+
rescue Date::Error
|
|
33
|
+
key.failure("invalid date format")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
rule(:from_date, :to_date) do
|
|
37
|
+
next unless values[:from_date].match?(/\A\d{4}-\d{2}-\d{2}\z/) && values[:to_date].match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
38
|
+
|
|
39
|
+
from_date = Date.parse(values[:from_date])
|
|
40
|
+
to_date = Date.parse(values[:to_date])
|
|
41
|
+
key.failure("from_date must be before to_date") if from_date >= to_date
|
|
42
|
+
rescue Date::Error
|
|
43
|
+
key.failure("invalid date format")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def trading_day?(date)
|
|
49
|
+
return false unless date.is_a?(Date)
|
|
50
|
+
|
|
51
|
+
(1..5).cover?(date.wday)
|
|
52
|
+
end
|
|
26
53
|
end
|
|
27
54
|
end
|
|
28
55
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
3
5
|
module DhanHQ
|
|
4
6
|
module Contracts
|
|
5
7
|
##
|
|
@@ -20,6 +22,7 @@ module DhanHQ
|
|
|
20
22
|
|
|
21
23
|
rule(:from_date) do
|
|
22
24
|
key.failure("must be in YYYY-MM-DD format (e.g., 2024-01-15)") unless valid_date_format?(value)
|
|
25
|
+
key.failure("must be a valid trading date (no weekend dates)") if valid_date_format?(value) && !trading_day?(Date.parse(value))
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
rule(:to_date) do
|
|
@@ -35,9 +38,8 @@ module DhanHQ
|
|
|
35
38
|
from_date = Date.parse(values[:from_date])
|
|
36
39
|
to_date = Date.parse(values[:to_date])
|
|
37
40
|
|
|
38
|
-
key.failure("from_date must be before
|
|
41
|
+
key.failure("from_date must be before to_date") if from_date >= to_date
|
|
39
42
|
rescue Date::Error
|
|
40
|
-
# This shouldn't happen since we already validated format, but just in case
|
|
41
43
|
key.failure("invalid date format")
|
|
42
44
|
end
|
|
43
45
|
end
|
|
@@ -49,7 +51,6 @@ module DhanHQ
|
|
|
49
51
|
return false unless date_string.is_a?(String)
|
|
50
52
|
return false unless date_string.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
51
53
|
|
|
52
|
-
# Additional check to ensure it's a valid date
|
|
53
54
|
begin
|
|
54
55
|
Date.parse(date_string)
|
|
55
56
|
true
|
|
@@ -57,6 +58,12 @@ module DhanHQ
|
|
|
57
58
|
false
|
|
58
59
|
end
|
|
59
60
|
end
|
|
61
|
+
|
|
62
|
+
def trading_day?(date)
|
|
63
|
+
return false unless date.is_a?(Date)
|
|
64
|
+
|
|
65
|
+
(1..5).cover?(date.wday)
|
|
66
|
+
end
|
|
60
67
|
end
|
|
61
68
|
|
|
62
69
|
##
|
data/lib/DhanHQ/errors.rb
CHANGED
|
@@ -21,6 +21,8 @@ module DhanHQ
|
|
|
21
21
|
class InvalidTokenError < Error; end
|
|
22
22
|
# DH-810
|
|
23
23
|
class InvalidClientIDError < Error; end
|
|
24
|
+
# Raised when fetching credentials from a token endpoint fails (HTTP error or invalid response).
|
|
25
|
+
class TokenEndpointError < Error; end
|
|
24
26
|
|
|
25
27
|
# Rate limits and input validation errors
|
|
26
28
|
# DH-904, 805
|
data/lib/DhanHQ/version.rb
CHANGED
data/lib/DhanHQ/ws/client.rb
CHANGED
|
@@ -29,6 +29,7 @@ module DhanHQ
|
|
|
29
29
|
|
|
30
30
|
token = DhanHQ.configuration.resolved_access_token
|
|
31
31
|
raise DhanHQ::AuthenticationError, "Missing access token" if token.nil? || token.empty?
|
|
32
|
+
|
|
32
33
|
cid = DhanHQ.configuration.client_id or raise "DhanHQ.client_id not set"
|
|
33
34
|
ver = (DhanHQ.configuration.respond_to?(:ws_version) && DhanHQ.configuration.ws_version) || 2
|
|
34
35
|
@url = url || "wss://api-feed.dhan.co?version=#{ver}&token=#{token}&clientId=#{cid}&authType=2"
|
|
@@ -82,6 +82,7 @@ module DhanHQ
|
|
|
82
82
|
def build_market_depth_url(config)
|
|
83
83
|
token = config.resolved_access_token
|
|
84
84
|
raise DhanHQ::AuthenticationError, "Missing access token" if token.nil? || token.empty?
|
|
85
|
+
|
|
85
86
|
cid = config.client_id or raise "DhanHQ.client_id not set"
|
|
86
87
|
depth_level = config.market_depth_level || 20 # Default to 20 level depth
|
|
87
88
|
|
|
@@ -88,6 +88,7 @@ module DhanHQ
|
|
|
88
88
|
else
|
|
89
89
|
token = cfg.resolved_access_token
|
|
90
90
|
raise DhanHQ::AuthenticationError, "Missing access token" if token.nil? || token.empty?
|
|
91
|
+
|
|
91
92
|
cid = cfg.client_id or raise "DhanHQ.client_id not set"
|
|
92
93
|
payload = {
|
|
93
94
|
LoginReq: { MsgCode: 42, ClientId: cid, Token: token },
|
data/lib/dhan_hq.rb
CHANGED
|
@@ -23,6 +23,7 @@ require_relative "DhanHQ/error_object"
|
|
|
23
23
|
require_relative "DhanHQ/client"
|
|
24
24
|
require_relative "DhanHQ/configuration"
|
|
25
25
|
require_relative "DhanHQ/rate_limiter"
|
|
26
|
+
require_relative "DhanHQ/auth"
|
|
26
27
|
|
|
27
28
|
# Contracts
|
|
28
29
|
require_relative "DhanHQ/contracts/base_contract"
|
|
@@ -144,5 +145,75 @@ module DhanHQ
|
|
|
144
145
|
configuration.partner_id = ENV.fetch("DHAN_PARTNER_ID", configuration.partner_id)
|
|
145
146
|
configuration.partner_secret = ENV.fetch("DHAN_PARTNER_SECRET", configuration.partner_secret)
|
|
146
147
|
end
|
|
148
|
+
|
|
149
|
+
# Configures the DhanHQ client by fetching credentials from a token endpoint.
|
|
150
|
+
#
|
|
151
|
+
# Performs GET <base_url>/auth/dhan/token with Authorization: Bearer <bearer_token>.
|
|
152
|
+
# Expects JSON with at least +access_token+ and +client_id+. Optional +base_url+ in
|
|
153
|
+
# the response overrides the Dhan API base URL.
|
|
154
|
+
#
|
|
155
|
+
# @param base_url [String, nil] Base URL of your app (e.g. https://myapp.com). If nil, uses ENV["DHAN_TOKEN_ENDPOINT_BASE_URL"].
|
|
156
|
+
# @param bearer_token [String, nil] Secret token for the endpoint. If nil, uses ENV["DHAN_TOKEN_ENDPOINT_BEARER"].
|
|
157
|
+
# @return [DhanHQ::Configuration] The configured configuration.
|
|
158
|
+
# @raise [DhanHQ::TokenEndpointError] On HTTP error or when response lacks access_token/client_id.
|
|
159
|
+
#
|
|
160
|
+
# @example Explicit
|
|
161
|
+
# DhanHQ.configure_from_token_endpoint(base_url: "https://myapp.com", bearer_token: "secret-token")
|
|
162
|
+
#
|
|
163
|
+
# @example From ENV (DHAN_TOKEN_ENDPOINT_BASE_URL and DHAN_TOKEN_ENDPOINT_BEARER set)
|
|
164
|
+
# DhanHQ.configure_from_token_endpoint
|
|
165
|
+
def configure_from_token_endpoint(base_url: nil, bearer_token: nil)
|
|
166
|
+
base_url ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BASE_URL", nil)
|
|
167
|
+
bearer_token ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BEARER", nil)
|
|
168
|
+
|
|
169
|
+
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?
|
|
170
|
+
|
|
171
|
+
url = "#{base_url.to_s.chomp("/")}/auth/dhan/token"
|
|
172
|
+
conn = Faraday.new(url: url) do |c|
|
|
173
|
+
c.response :json, content_type: /\bjson$/
|
|
174
|
+
c.adapter Faraday.default_adapter
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
response = conn.get("") do |req|
|
|
178
|
+
req.headers["Authorization"] = "Bearer #{bearer_token}"
|
|
179
|
+
req.headers["Accept"] = "application/json"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
unless response.success?
|
|
183
|
+
body = if response.body.is_a?(Hash)
|
|
184
|
+
response.body
|
|
185
|
+
else
|
|
186
|
+
begin
|
|
187
|
+
JSON.parse(response.body.to_s)
|
|
188
|
+
rescue StandardError
|
|
189
|
+
{}
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
msg = body["error"] || body["message"] || body["errorMessage"] || response.body.to_s
|
|
193
|
+
raise TokenEndpointError, "Token endpoint returned #{response.status}: #{msg}"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
data = if response.body.is_a?(Hash)
|
|
197
|
+
response.body
|
|
198
|
+
else
|
|
199
|
+
begin
|
|
200
|
+
JSON.parse(response.body.to_s)
|
|
201
|
+
rescue StandardError
|
|
202
|
+
{}
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
206
|
+
|
|
207
|
+
access_token = data["access_token"] || data[:access_token]
|
|
208
|
+
client_id = data["client_id"] || data[:client_id]
|
|
209
|
+
raise TokenEndpointError, "Token endpoint response missing access_token or client_id" if access_token.to_s.empty? || client_id.to_s.empty?
|
|
210
|
+
|
|
211
|
+
self.configuration ||= Configuration.new
|
|
212
|
+
configuration.access_token = access_token.to_s
|
|
213
|
+
configuration.client_id = client_id.to_s
|
|
214
|
+
dhan_base = data["base_url"] || data[:base_url]
|
|
215
|
+
configuration.base_url = dhan_base.to_s if dhan_base.to_s != ""
|
|
216
|
+
configuration
|
|
217
|
+
end
|
|
147
218
|
end
|
|
148
219
|
end
|
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.2.
|
|
4
|
+
version: 2.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shubham Taywade
|
|
@@ -202,6 +202,7 @@ files:
|
|
|
202
202
|
- examples/order_update_example.rb
|
|
203
203
|
- examples/trading_fields_example.rb
|
|
204
204
|
- exe/DhanHQ
|
|
205
|
+
- lib/DhanHQ/auth.rb
|
|
205
206
|
- lib/DhanHQ/client.rb
|
|
206
207
|
- lib/DhanHQ/config.rb
|
|
207
208
|
- lib/DhanHQ/configuration.rb
|