DhanHQ 2.6.3 → 2.7.0

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: ef2616285615327c5e0ad08a85440d91617ea66818add0a55e4cd6c5e65512aa
4
- data.tar.gz: 4232cfcd4c9626683e3bf993f75aa0a8add2f8c83702435c66edb2c58a3e252e
3
+ metadata.gz: df325d70c7c2e3c13493ad199ca8e11bb7f5a9df1d62a775452e4fb1eeed5842
4
+ data.tar.gz: 7cfde3f38fa073311ad50992ef497ba7391927b78136346e7549c135e7a4d7cc
5
5
  SHA512:
6
- metadata.gz: c0ba9cd2119899ed6c9eaefe27601b142e75abbc541538f241058712b82bccdc5fab6af888489c5ac01423c1bfb387e42cdfcee22487bbafce6d32c7ce6f54b2
7
- data.tar.gz: 4107fc9f9a5ced70692469c21a88f2ed6979739af7e3ef5444d1506e5ba574ba4124f2f7f264747a147157164610606ff22408dcd3612649cb19b9f1e558ca8a
6
+ metadata.gz: 945e4a71c15f01e33f7f05a3ed2453e7558c46d9c9bb5067a6225e68e87b4c3aac4a903ff08da384b488f9b8d36764f9f1f47103a8a8db4d5b90d1ea97e09252
7
+ data.tar.gz: 1b805c79a999c6604cd58d8df163c339233b09732d77c1911010dfa3d5ea32e12ae27d166b1484621338ba378acace69003328e9e6d4ec3654eb646df753c2c0
data/AGENTS.md ADDED
@@ -0,0 +1,23 @@
1
+ # AGENTS.md
2
+
3
+ ## Cursor Cloud specific instructions
4
+
5
+ This is a pure-Ruby gem (no Rails, no running servers). Development commands are documented in `CLAUDE.md`.
6
+
7
+ ### Quick reference
8
+
9
+ | Task | Command |
10
+ |------|---------|
11
+ | Install deps | `bundle install` |
12
+ | Tests | `bundle exec rspec` |
13
+ | Lint | `bundle exec rubocop` |
14
+ | Both | `bundle exec rake` |
15
+ | Single spec | `bundle exec rspec spec/path/to_spec.rb` |
16
+
17
+ ### Non-obvious caveats
18
+
19
+ - **Bundler 4.x required**: The lockfile was bundled with Bundler 4.0.6. Install with `sudo gem install bundler -v 4.0.6` if missing.
20
+ - **`libyaml-dev` must be installed**: The `psych` gem (transitive dep) needs `yaml.h`. Without `libyaml-dev`, `bundle install` fails on native extension build.
21
+ - **`vendor/bundle` path**: Gems are installed to `./vendor/bundle` via `.bundle/config` to avoid needing root write access to `/var/lib/gems`.
22
+ - **No real API calls in tests**: All HTTP interactions are stubbed with WebMock/VCR. No `DHAN_CLIENT_ID` or `DHAN_ACCESS_TOKEN` secrets are needed to run the test suite.
23
+ - **No services to start**: This is a library gem. There is no web server, database, or background worker. Testing is purely `bundle exec rspec` / `bundle exec rubocop`.
data/ARCHITECTURE.md CHANGED
@@ -48,6 +48,8 @@ This document describes the architecture of the DhanHQ v2 API client gem: layers
48
48
  | `resources/` | REST wrappers | One class per API surface (Orders, Positions, MarketFeed, OptionChain, …). Set `HTTP_PATH`, `API_TYPE`; implement get/post/put/delete via BaseAPI. |
49
49
  | `contracts/` | Request/response validation | Dry::Validation contracts (PlaceOrderContract, ModifyOrderContract, OptionChainContract, etc.). BaseContract provides shared macros (e.g. lot_size, tick_size). |
50
50
  | `auth/` | Token lifecycle | Token generator/renewal/manager for dynamic tokens. |
51
+ | `concerns/` | Shared behavior | Modules included across layers (e.g. `OrderAudit` for live trading guard + audit logging, included in all order resources). |
52
+ | `utils/` | Utilities | Cross-cutting utilities not tied to a single layer (e.g. `NetworkInspector` for IP/hostname/env lookup used by order audit logging). |
51
53
  | `ws/` | WebSocket | Connection, packets, decoder, market depth, orders client — isolated from REST. |
52
54
 
53
55
  ---
data/CHANGELOG.md CHANGED
@@ -1,3 +1,37 @@
1
+ ## [2.7.0] - 2026-03-17
2
+
3
+ ### Added
4
+
5
+ - **Order audit logging across all order types**: Every order submission (regular, super, forever/GTT, and alert orders) now emits a WARN-level structured JSON log line capturing: event type, public IPv4/IPv6, hostname, runtime environment, `security_id`, `correlation_id`, and UTC timestamp. Log output example:
6
+ ```json
7
+ {"event":"DHAN_ORDER_ATTEMPT","hostname":"DESKTOP-SHUBHAM","env":"production","ipv4":"122.171.22.40","ipv6":"2401:4900:...","security_id":"11536","correlation_id":"SCALPER_7af1","timestamp":"2026-03-17T06:45:22Z"}
8
+ ```
9
+ Events logged: `DHAN_ORDER_ATTEMPT`, `DHAN_ORDER_MODIFY_ATTEMPT`, `DHAN_ORDER_SLICING_ATTEMPT`, `DHAN_SUPER_ORDER_ATTEMPT`, `DHAN_SUPER_ORDER_MODIFY_ATTEMPT`, `DHAN_SUPER_ORDER_CANCEL_ATTEMPT`, `DHAN_FOREVER_ORDER_ATTEMPT`, `DHAN_FOREVER_ORDER_MODIFY_ATTEMPT`, `DHAN_FOREVER_ORDER_CANCEL_ATTEMPT`, `DHAN_ALERT_ORDER_ATTEMPT`, `DHAN_ALERT_ORDER_MODIFY_ATTEMPT`, `DHAN_ALERT_ORDER_DELETE_ATTEMPT`, `DHAN_PNL_EXIT_CONFIGURE_ATTEMPT`, `DHAN_PNL_EXIT_STOP_ATTEMPT`.
10
+ - **`DhanHQ::Concerns::OrderAudit`**: Shared concern providing `log_order_context` and `ensure_live_trading!` — included in `Resources::Orders`, `Resources::SuperOrders`, `Resources::ForeverOrders`, `Resources::AlertOrders`, and `Resources::PnlExit`.
11
+ - **`DhanHQ::Utils::NetworkInspector`**: New utility class that resolves the machine's public IPv4 (via api.ipify.org), IPv6 (via api64.ipify.org), hostname (`Socket.gethostname`), and runtime environment (`RAILS_ENV` / `RACK_ENV` / `APP_ENV`). IP lookups are memoized for the process lifetime; call `NetworkInspector.reset_cache!` to refresh.
12
+ - **Live trading guard**: All mutating order/trader-control calls require `ENV["LIVE_TRADING"]="true"`. Guarded methods:
13
+ - `Resources::Orders#create`, `#update`, `#slicing`, `#cancel`
14
+ - `Resources::SuperOrders#create`, `#update`, `#cancel`
15
+ - `Resources::ForeverOrders#create`, `#update`, `#cancel`
16
+ - `Resources::AlertOrders#create`, `#update`, `#delete`
17
+ - `Resources::PnlExit#configure`, `#stop`
18
+ - **`DhanHQ::LiveTradingDisabledError`**: New error class raised when the live trading guard fires.
19
+ - **Correlation ID prefix convention**: Recommended per-app correlation ID prefixes for instant source identification in the Dhan orderbook (e.g. `SCALPER_7af1`, `TRADER_3bc9`). See README.
20
+
21
+ ### Changed
22
+
23
+ - **`Resources::Orders`**: `#create`, `#update`, `#slicing`, `#cancel` log order context and enforce the live trading guard.
24
+ - **`Resources::SuperOrders`**: `#create`, `#update`, `#cancel` log order context and enforce the live trading guard.
25
+ - **`Resources::ForeverOrders`**: `#create`, `#update`, `#cancel` log order context and enforce the live trading guard.
26
+ - **`Resources::AlertOrders`**: `#create`, `#update`, `#delete` log order context and enforce the live trading guard; `#delete` added.
27
+ - **`Resources::PnlExit`**: `#configure` and `#stop` use `OrderAudit` (guard + audit log).
28
+
29
+ ### Breaking
30
+
31
+ - **`ENV["LIVE_TRADING"]` required for all order/trader-control mutations**: Any call that creates, updates, cancels, or deletes orders (or configures/stops PnL exit) now raises `LiveTradingDisabledError` unless `ENV["LIVE_TRADING"]="true"`. Affects `Resources::Orders`, `Resources::SuperOrders`, `Resources::ForeverOrders`, `Resources::AlertOrders`, `Resources::PnlExit`, and the corresponding model wrappers. Set `LIVE_TRADING=true` in production and `LIVE_TRADING=false` (or omit) in development/test.
32
+
33
+ ---
34
+
1
35
  ## [2.6.3] - 2026-03-14
2
36
 
3
37
  ### Added
data/README.md CHANGED
@@ -53,6 +53,8 @@ You could wire up Faraday and parse JSON yourself. Here's why you shouldn't:
53
53
  - **Secure logging** — automatic token sanitization in all log output
54
54
  - **Super Orders** — entry + stop-loss + target + trailing jump in one request
55
55
  - **Instrument convenience methods** — `.ltp`, `.ohlc`, `.option_chain` directly on instruments
56
+ - **Order audit logging** — every order attempt logs machine, IP, environment, and correlation ID as structured JSON
57
+ - **Live trading guard** — prevents accidental order placement unless `ENV["LIVE_TRADING"]="true"`
56
58
  - **Full REST coverage** — Orders, Trades, Forever Orders, Super Orders, Positions, Holdings, Funds, HistoricalData, OptionChain, MarketFeed, EDIS, Kill Switch, P&L Exit, Alert Orders, Margin Calculator
57
59
  - **P&L Based Exit** — automatic position exit on profit/loss thresholds
58
60
  - **Postback parser** — parse Dhan webhook payloads with `Postback.parse` and status predicates
@@ -120,6 +122,57 @@ When the API returns 401, the client retries **once** with a fresh token from yo
120
122
 
121
123
  ---
122
124
 
125
+ ## Order Safety
126
+
127
+ ### Live Trading Guard
128
+
129
+ Order placement (`create`, `slicing`) is blocked unless you explicitly enable it:
130
+
131
+ ```bash
132
+ # Production (Render, VPS, etc.)
133
+ LIVE_TRADING=true
134
+
135
+ # Development / Test (default — orders are blocked)
136
+ LIVE_TRADING=false # or simply omit
137
+ ```
138
+
139
+ Attempting to place an order without `LIVE_TRADING=true` raises `DhanHQ::LiveTradingDisabledError`.
140
+
141
+ ### Order Audit Logging
142
+
143
+ Every order attempt (place, modify, slice) automatically logs a structured JSON line at WARN level:
144
+
145
+ ```json
146
+ {
147
+ "event": "DHAN_ORDER_ATTEMPT",
148
+ "hostname": "DESKTOP-SHUBHAM",
149
+ "env": "production",
150
+ "ipv4": "122.171.22.40",
151
+ "ipv6": "2401:4900:894c:8448:1da9:27f1:48e7:61be",
152
+ "security_id": "11536",
153
+ "correlation_id": "SCALPER_7af1",
154
+ "timestamp": "2026-03-17T06:45:22Z"
155
+ }
156
+ ```
157
+
158
+ This tells you instantly which machine, app, IP, and environment placed the order.
159
+
160
+ ### Correlation ID Prefixes
161
+
162
+ Use per-app prefixes for instant source identification in the Dhan orderbook:
163
+
164
+ ```ruby
165
+ # algo_scalper_api
166
+ correlation_id: "SCALPER_#{SecureRandom.hex(4)}"
167
+
168
+ # algo_trader_api
169
+ correlation_id: "TRADER_#{SecureRandom.hex(4)}"
170
+ ```
171
+
172
+ The Dhan orderbook will show `SCALPER_7af1` or `TRADER_3bc9`, making the source obvious.
173
+
174
+ ---
175
+
123
176
  ## REST API
124
177
 
125
178
  ### Orders — Place, Modify, Cancel
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../utils/network_inspector"
4
+
5
+ module DhanHQ
6
+ module Concerns
7
+ # Shared behavior for order audit logging and live trading safety.
8
+ #
9
+ # Include this module in any Resource that submits, modifies, or cancels
10
+ # orders on the Dhan API. It provides:
11
+ #
12
+ # - {#log_order_context}: emits a structured JSON log line (WARN level)
13
+ # capturing hostname, public IP, environment, security_id, correlation_id,
14
+ # and a UTC timestamp.
15
+ #
16
+ # - {#ensure_live_trading!}: raises {DhanHQ::LiveTradingDisabledError}
17
+ # unless +ENV["LIVE_TRADING"]+ is +"true"+, preventing accidental order
18
+ # placement from development machines.
19
+ #
20
+ # @example Including in a resource
21
+ # class MyOrders < BaseAPI
22
+ # include DhanHQ::Concerns::OrderAudit
23
+ #
24
+ # def create(params)
25
+ # ensure_live_trading!
26
+ # log_order_context("MY_ORDER_ATTEMPT", params)
27
+ # post("", params: params)
28
+ # end
29
+ # end
30
+ module OrderAudit
31
+ private
32
+
33
+ # Raises an error if LIVE_TRADING is not explicitly enabled.
34
+ # Set ENV["LIVE_TRADING"]="true" in production to allow order submission.
35
+ def ensure_live_trading!
36
+ return if ENV["LIVE_TRADING"] == "true"
37
+
38
+ raise DhanHQ::LiveTradingDisabledError,
39
+ "Live trading is disabled. Set ENV[\"LIVE_TRADING\"]=\"true\" to enable order placement."
40
+ end
41
+
42
+ # Emits a structured JSON log line with machine/network/correlation context.
43
+ # Uses WARN level so it appears even when INFO is silenced.
44
+ def log_order_context(event, params = {})
45
+ inspector = DhanHQ::Utils::NetworkInspector
46
+ entry = {
47
+ event: event,
48
+ hostname: inspector.hostname,
49
+ env: inspector.environment,
50
+ ipv4: inspector.public_ipv4,
51
+ ipv6: inspector.public_ipv6,
52
+ security_id: extract_param(params, :securityId, :security_id),
53
+ correlation_id: extract_param(params, :correlationId, :correlation_id),
54
+ order_id: extract_param(params, :orderId, :order_id),
55
+ timestamp: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
56
+ }.compact
57
+
58
+ DhanHQ.logger&.warn(JSON.generate(entry))
59
+ end
60
+
61
+ # Extracts a value from params trying both camelCase and snake_case keys,
62
+ # as well as both symbol and string key types.
63
+ def extract_param(params, camel_key, snake_key)
64
+ p = params || {}
65
+ p[camel_key] || p[camel_key.to_s] || p[snake_key] || p[snake_key.to_s]
66
+ end
67
+ end
68
+ end
69
+ end
data/lib/DhanHQ/errors.rb CHANGED
@@ -48,6 +48,8 @@ module DhanHQ
48
48
  # Raised when the 25-modifications-per-order API cap would be exceeded.
49
49
  # Count is tracked per Order instance in this process only (see Order#modify).
50
50
  class ModificationLimitError < Error; end
51
+ # Raised when an order placement is attempted but ENV["LIVE_TRADING"] != "true".
52
+ class LiveTradingDisabledError < Error; end
51
53
  # Raised when the API signals an issue with the requested data payload.
52
54
  class DataError < Error; end
53
55
 
@@ -1,11 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../concerns/order_audit"
4
+
3
5
  module DhanHQ
4
6
  module Resources
5
7
  # Resource for alert/conditional orders per API docs: /alerts/orders (GET/POST/PUT/DELETE).
6
8
  class AlertOrders < BaseResource
9
+ include DhanHQ::Concerns::OrderAudit
10
+
7
11
  API_TYPE = :order_api
8
12
  HTTP_PATH = "/v2/alerts/orders"
13
+
14
+ def create(params)
15
+ ensure_live_trading!
16
+ log_order_context("DHAN_ALERT_ORDER_ATTEMPT", params)
17
+ super
18
+ end
19
+
20
+ def update(id, params)
21
+ ensure_live_trading!
22
+ log_order_context("DHAN_ALERT_ORDER_MODIFY_ATTEMPT", params.merge(alert_id: id))
23
+ super
24
+ end
25
+
26
+ def delete(id)
27
+ ensure_live_trading!
28
+ log_order_context("DHAN_ALERT_ORDER_DELETE_ATTEMPT", { order_id: id })
29
+ super
30
+ end
9
31
  end
10
32
  end
11
33
  end
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../concerns/order_audit"
4
+
3
5
  module DhanHQ
4
6
  module Resources
5
7
  # Resource client for GTT/forever order management.
6
8
  class ForeverOrders < BaseAPI
9
+ include DhanHQ::Concerns::OrderAudit
10
+
7
11
  # Uses the trading API tier.
8
12
  API_TYPE = :order_api
9
13
  # Root path for forever order operations.
@@ -21,6 +25,8 @@ module DhanHQ
21
25
  # @param params [Hash]
22
26
  # @return [Hash]
23
27
  def create(params)
28
+ ensure_live_trading!
29
+ log_order_context("DHAN_FOREVER_ORDER_ATTEMPT", params)
24
30
  post("/orders", params: params)
25
31
  end
26
32
 
@@ -38,6 +44,8 @@ module DhanHQ
38
44
  # @param params [Hash]
39
45
  # @return [Hash]
40
46
  def update(order_id, params)
47
+ ensure_live_trading!
48
+ log_order_context("DHAN_FOREVER_ORDER_MODIFY_ATTEMPT", params.merge(order_id: order_id))
41
49
  put("/orders/#{order_id}", params: params)
42
50
  end
43
51
 
@@ -46,6 +54,8 @@ module DhanHQ
46
54
  # @param order_id [String]
47
55
  # @return [Hash]
48
56
  def cancel(order_id)
57
+ ensure_live_trading!
58
+ log_order_context("DHAN_FOREVER_ORDER_CANCEL_ATTEMPT", { order_id: order_id })
49
59
  delete("/orders/#{order_id}")
50
60
  end
51
61
  end
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../concerns/order_audit"
4
+
3
5
  module DhanHQ
4
6
  module Resources
5
7
  # Handles order placement, modification, and cancellation
6
8
  class Orders < BaseAPI
9
+ include DhanHQ::Concerns::OrderAudit
10
+
7
11
  API_TYPE = :order_api
8
12
  HTTP_PATH = "/v2/orders"
9
13
 
@@ -12,21 +16,29 @@ module DhanHQ
12
16
  # --------------------------------------------------
13
17
 
14
18
  def create(params)
19
+ ensure_live_trading!
20
+ log_order_context("DHAN_ORDER_ATTEMPT", params)
15
21
  validate_place_order!(params)
16
22
  post("", params: params)
17
23
  end
18
24
 
19
25
  def update(order_id, params)
26
+ ensure_live_trading!
27
+ log_order_context("DHAN_ORDER_MODIFY_ATTEMPT", params.merge(order_id: order_id))
20
28
  validate_modify_order!(params.merge(order_id: order_id))
21
29
  put("/#{order_id}", params: params)
22
30
  end
23
31
 
24
32
  def slicing(params)
33
+ ensure_live_trading!
34
+ log_order_context("DHAN_ORDER_SLICING_ATTEMPT", params)
25
35
  validate_place_order!(params)
26
36
  post("/slicing", params: params)
27
37
  end
28
38
 
29
39
  def cancel(order_id)
40
+ ensure_live_trading!
41
+ log_order_context("DHAN_ORDER_CANCEL_ATTEMPT", { order_id: order_id })
30
42
  delete("/#{order_id}")
31
43
  end
32
44
 
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../concerns/order_audit"
4
+
3
5
  module DhanHQ
4
6
  module Resources
5
7
  # Resource for P&L Based Exit endpoints per https://dhanhq.co/docs/v2/traders-control/
6
8
  # POST /v2/pnlExit — configure, DELETE /v2/pnlExit — stop, GET /v2/pnlExit — status.
7
9
  class PnlExit < BaseAPI
10
+ include DhanHQ::Concerns::OrderAudit
11
+
8
12
  API_TYPE = :order_api
9
13
  HTTP_PATH = "/v2/pnlExit"
10
14
 
@@ -14,6 +18,8 @@ module DhanHQ
14
18
  # @param params [Hash] Request body with profitValue, lossValue, productType, enableKillSwitch.
15
19
  # @return [Hash] API response containing pnlExitStatus and message.
16
20
  def configure(params)
21
+ ensure_live_trading!
22
+ log_order_context("DHAN_PNL_EXIT_CONFIGURE_ATTEMPT", params)
17
23
  post("", params: params)
18
24
  end
19
25
 
@@ -22,6 +28,8 @@ module DhanHQ
22
28
  #
23
29
  # @return [Hash] API response containing pnlExitStatus and message.
24
30
  def stop
31
+ ensure_live_trading!
32
+ log_order_context("DHAN_PNL_EXIT_STOP_ATTEMPT")
25
33
  delete("")
26
34
  end
27
35
 
@@ -1,14 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../concerns/order_audit"
4
+
3
5
  module DhanHQ
4
6
  module Resources
5
7
  # Resource client for multi-leg super orders.
6
8
  class SuperOrders < BaseAPI
9
+ include DhanHQ::Concerns::OrderAudit
10
+
7
11
  # Super orders are executed via the trading API.
8
12
  API_TYPE = :order_api
9
13
  # Base path for super order endpoints.
10
14
  HTTP_PATH = "/v2/super/orders"
11
15
 
16
+ SUPER_ORDER_LEGS = %w[ENTRY_LEG STOP_LOSS_LEG TARGET_LEG].freeze
17
+
12
18
  # Lists all configured super orders.
13
19
  #
14
20
  # @return [Array<Hash>]
@@ -21,6 +27,8 @@ module DhanHQ
21
27
  # @param params [Hash]
22
28
  # @return [Hash]
23
29
  def create(params)
30
+ ensure_live_trading!
31
+ log_order_context("DHAN_SUPER_ORDER_ATTEMPT", params)
24
32
  post("", params: params)
25
33
  end
26
34
 
@@ -30,11 +38,11 @@ module DhanHQ
30
38
  # @param params [Hash]
31
39
  # @return [Hash]
32
40
  def update(order_id, params)
41
+ ensure_live_trading!
42
+ log_order_context("DHAN_SUPER_ORDER_MODIFY_ATTEMPT", params.merge(order_id: order_id))
33
43
  put("/#{order_id}", params: params)
34
44
  end
35
45
 
36
- SUPER_ORDER_LEGS = %w[ENTRY_LEG STOP_LOSS_LEG TARGET_LEG].freeze
37
-
38
46
  # Cancels a specific leg from a super order.
39
47
  #
40
48
  # @param order_id [String]
@@ -43,8 +51,13 @@ module DhanHQ
43
51
  # @raise [DhanHQ::ValidationError] if leg_name is not a valid leg
44
52
  def cancel(order_id, leg_name)
45
53
  normalized = leg_name.to_s.upcase.strip
46
- raise DhanHQ::ValidationError, "leg_name must be one of: #{SUPER_ORDER_LEGS.join(", ")}" unless SUPER_ORDER_LEGS.include?(normalized)
54
+ unless SUPER_ORDER_LEGS.include?(normalized)
55
+ raise DhanHQ::ValidationError,
56
+ "leg_name must be one of: #{SUPER_ORDER_LEGS.join(", ")}"
57
+ end
47
58
 
59
+ ensure_live_trading!
60
+ log_order_context("DHAN_SUPER_ORDER_CANCEL_ATTEMPT", { order_id: order_id, leg_name: normalized })
48
61
  delete("/#{order_id}/#{normalized}")
49
62
  end
50
63
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "socket"
5
+
6
+ module DhanHQ
7
+ module Utils
8
+ # Collects network and environment metadata for order audit logging.
9
+ #
10
+ # Results are memoized at class level so that repeated calls within a single
11
+ # process do not incur additional HTTP round-trips to the IP-lookup services.
12
+ #
13
+ # @example
14
+ # DhanHQ::Utils::NetworkInspector.public_ipv4 # => "122.171.22.40"
15
+ # DhanHQ::Utils::NetworkInspector.hostname # => "DESKTOP-SHUBHAM"
16
+ # DhanHQ::Utils::NetworkInspector.environment # => "production"
17
+ class NetworkInspector
18
+ IPV4_URI = URI("https://api.ipify.org")
19
+ IPV6_URI = URI("https://api64.ipify.org")
20
+
21
+ class << self
22
+ # Returns the public IPv4 address of this machine.
23
+ # Cached after the first successful lookup. Returns "unknown" on failure.
24
+ #
25
+ # @return [String]
26
+ def public_ipv4
27
+ @public_ipv4 ||= fetch_ip(IPV4_URI)
28
+ end
29
+
30
+ # Returns the public IPv6 address of this machine.
31
+ # Cached after the first successful lookup. Returns "unknown" on failure.
32
+ #
33
+ # @return [String]
34
+ def public_ipv6
35
+ @public_ipv6 ||= fetch_ip(IPV6_URI)
36
+ end
37
+
38
+ # Returns the system hostname.
39
+ #
40
+ # @return [String]
41
+ def hostname
42
+ Socket.gethostname
43
+ end
44
+
45
+ # Returns the current runtime environment name.
46
+ # Checks RAILS_ENV, RACK_ENV, APP_ENV in order; falls back to "unknown".
47
+ #
48
+ # @return [String]
49
+ def environment
50
+ ENV["RAILS_ENV"] || ENV["RACK_ENV"] || ENV["APP_ENV"] || "unknown"
51
+ end
52
+
53
+ # Clears the memoized IP cache (useful in tests or when the IP may change).
54
+ #
55
+ # @return [void]
56
+ def reset_cache!
57
+ @public_ipv4 = nil
58
+ @public_ipv6 = nil
59
+ end
60
+
61
+ private
62
+
63
+ def fetch_ip(uri)
64
+ Net::HTTP.get(uri).strip
65
+ rescue StandardError
66
+ "unknown"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "2.6.3"
5
+ VERSION = "2.7.0"
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: DhanHQ
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.3
4
+ version: 2.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shubham Taywade
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-14 00:00:00.000000000 Z
11
+ date: 2026-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -191,6 +191,7 @@ files:
191
191
  - ".rspec"
192
192
  - ".rubocop.yml"
193
193
  - ".rubocop_todo.yml"
194
+ - AGENTS.md
194
195
  - ARCHITECTURE.md
195
196
  - CHANGELOG.md
196
197
  - CODE_OF_CONDUCT.md
@@ -226,6 +227,7 @@ files:
226
227
  - lib/DhanHQ/auth/token_manager.rb
227
228
  - lib/DhanHQ/auth/token_renewal.rb
228
229
  - lib/DhanHQ/client.rb
230
+ - lib/DhanHQ/concerns/order_audit.rb
229
231
  - lib/DhanHQ/configuration.rb
230
232
  - lib/DhanHQ/constants.rb
231
233
  - lib/DhanHQ/contracts/alert_order_contract.rb
@@ -313,6 +315,7 @@ files:
313
315
  - lib/DhanHQ/resources/super_orders.rb
314
316
  - lib/DhanHQ/resources/trader_control.rb
315
317
  - lib/DhanHQ/resources/trades.rb
318
+ - lib/DhanHQ/utils/network_inspector.rb
316
319
  - lib/DhanHQ/version.rb
317
320
  - lib/DhanHQ/ws.rb
318
321
  - lib/DhanHQ/ws/base_connection.rb