Skip to content

feat: integrate DCA bot with configuration and deposit notification#64

Merged
xbuddhi merged 3 commits into
mainfrom
scoopa/auto-reset-retry-counts
Jun 12, 2026
Merged

feat: integrate DCA bot with configuration and deposit notification#64
xbuddhi merged 3 commits into
mainfrom
scoopa/auto-reset-retry-counts

Conversation

@helloscoopa

@helloscoopa helloscoopa commented Jun 10, 2026

Copy link
Copy Markdown

Add DCA backend integration for retry-count reset on deposits

Summary

  • Adds new dca config section (wallet_username, api_url) for integrating with the BitcoinDeepa DCA backend.
  • Introduces internal/dca package with NotifyDeposit(toUsername), which calls POST {api_url}/transaction/reset-retry-counts whenever
    sats are deposited into the configured DCA wallet account.
  • Wires NotifyDeposit into both deposit paths:
    • internal/lnbits/webhook/webhook.go — fires for external incoming Lightning payments.
    • internal/telegram/transaction.go — fires for successful internal sends/tips.

Why

The DCA bot needs to know when its wallet receives new funds so it can reset its retry counters and resume scheduled purchases.

Config

dca:
wallet_username: "" # Telegram username of the DCA wallet account
api_url: "https://bitcoindeepa-dca-be-production.up.railway.app"

Test plan

  • Set dca.wallet_username to the DCA account's username and confirm an external deposit (LNbits webhook) triggers the
    reset-retry-counts call (check logs for [DCA] reset-retry-counts called for ...).
  • Confirm an internal tip/send to the DCA account triggers the same call.
  • Confirm deposits to other accounts do NOT trigger the call.
  • Confirm behavior is a no-op when dca.wallet_username is unset.

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@helloscoopa, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 58 minutes and 46 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1c5c4d5c-fd68-4055-8a83-485788c3f48b

📥 Commits

Reviewing files that changed from the base of the PR and between d1c7e68 and f3abfd6.

📒 Files selected for processing (1)
  • internal/dca/dca.go
📝 Walkthrough

Walkthrough

This PR integrates a DCA bot service that receives deposit notifications via HTTP POST. Configuration defines the bot's wallet username and API endpoint. A new NotifyDeposit function matches incoming usernames against configuration and sends reset-retry-counts requests. Integration points in webhook and transaction handlers trigger notifications on deposit events.

Changes

DCA Deposit Notification Integration

Layer / File(s) Summary
Configuration schema and example
internal/config.go, config.yaml.example
DCAConfiguration struct with wallet_username and api_url YAML fields added to global Configuration. Example configuration block inserted into config.yaml.example.
DCA notification service implementation
internal/dca/dca.go
New package exports NotifyDeposit(toUsername string) that validates username match, constructs and POSTs a reset-retry-counts request to the configured DCA API endpoint with 10-second timeout, and logs request creation/execution errors and success responses.
Webhook event handler integration
internal/lnbits/webhook/webhook.go
After successfully resolving webhook user by wallet_id, calls dca.NotifyDeposit with the Telegram user identifier to notify the DCA service of the incoming deposit.
Transaction completion handler integration
internal/telegram/transaction.go
On successful transaction completion, spawns a goroutine that calls dca.NotifyDeposit with the destination user to asynchronously notify the DCA service.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

Review effort 4/5

🐰 A deposit notification hops,
To DCA's API it bops,
With retry counts reset,
Users vest and invest,
No dollar-cost averaging stops! 🚀

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: integrating DCA bot functionality with configuration and deposit notification capabilities across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch scoopa/auto-reset-retry-counts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
internal/config.go (1)

25-28: ⚖️ Poor tradeoff

Add URL validation for ApiUrl in init().

Other URL configuration fields (LNURLServer, WebhookServer, etc.) are parsed and validated in the init() function (lines 145-154), but DCA.ApiUrl is not. The URL is later concatenated as a string in internal/dca/dca.go line 18, which could produce malformed URLs if the configuration value is invalid.

🔍 Proposed validation addition

Add validation in the init() function after line 157:

 	Configuration.Bot.LNURLHostUrl = hostname
 	checkLnbitsConfiguration()
 	setAPISendDefaults()
 	setAPIAnalyticsDefaults()
+	validateDCAConfiguration()
 }
+
+// validateDCAConfiguration validates the DCA configuration
+func validateDCAConfiguration() {
+	if Configuration.DCA.ApiUrl == "" {
+		return // DCA integration is optional
+	}
+	_, err := url.Parse(Configuration.DCA.ApiUrl)
+	if err != nil {
+		log.Warnf("Invalid DCA api_url '%s': %v. DCA notifications will be disabled.", Configuration.DCA.ApiUrl, err)
+		Configuration.DCA.ApiUrl = "" // Disable on invalid URL
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/config.go` around lines 25 - 28, The DCAConfiguration.ApiUrl isn't
validated in init(), so add URL parsing/validation there: locate the init()
function and after the other server URL validations validate
DCAConfiguration.ApiUrl by parsing it (use url.Parse or equivalent), ensure a
non-empty scheme (http/https) and host, and return/log an error if invalid;
reference the DCAConfiguration struct and its ApiUrl field and ensure this
prevents malformed concatenation later in the DCA client code (referenced by
internal/dca/dca.go using ApiUrl).
config.yaml.example (1)

49-49: ⚡ Quick win

Use a placeholder URL in the example configuration.

The example configuration file contains a production Railway URL (https://bitcoindeepa-dca-be-production.up.railway.app). Example/template config files should use placeholder URLs (e.g., https://your-dca-bot-api.example.com) to prevent accidental API calls to production systems during development and testing.

📝 Proposed change
-  api_url: "https://bitcoindeepa-dca-be-production.up.railway.app"
+  api_url: "https://your-dca-bot-api.example.com"  # Replace with your DCA bot API URL
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config.yaml.example` at line 49, Replace the hard-coded production Railway
URL in the example config by changing the api_url value (the "api_url" key) to a
clearly marked placeholder (e.g., "https://your-dca-bot-api.example.com") so the
example config cannot accidentally target production; update the api_url entry
in config.yaml.example accordingly and ensure any accompanying documentation or
comments reflect that this is a placeholder for users to fill in.
internal/dca/dca.go (1)

26-27: ⚡ Quick win

Reuse HTTP client instead of creating one per request.

A new http.Client is created on every NotifyDeposit call. HTTP clients manage connection pooling and should be reused. Creating a new client per request incurs unnecessary overhead and prevents connection reuse.

♻️ Proposed refactor

Define a package-level client:

+var httpClient = &http.Client{
+	Timeout: 10 * time.Second,
+}
+
 // NotifyDeposit checks whether the given recipient username matches the configured
 // DCA wallet username and, if so, asks the DCA backend to reset its retry counts.
 func NotifyDeposit(toUsername string) {

Then use it:

-	client := &http.Client{Timeout: 10 * time.Second}
-	resp, err := client.Do(req)
+	resp, err := httpClient.Do(req)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/dca/dca.go` around lines 26 - 27, The NotifyDeposit function
currently creates a new http.Client for each call (client :=
&http.Client{Timeout: 10 * time.Second}), which prevents connection reuse;
refactor by adding a package-level reusable client (e.g., var httpClient =
&http.Client{Timeout: 10 * time.Second}) and replace the local client creation
in NotifyDeposit with that httpClient so all requests reuse the same client and
connection pool; ensure you reference the httpClient variable in NotifyDeposit
and remove the per-call instantiation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/dca/dca.go`:
- Around line 19-24: The request is setting req.Header.Set("Content-Type",
"application/json") while creating the POST with a nil body; remove the
incorrect header or set it only when a non-nil body is provided. Update the code
around the http.NewRequest(...) call in internal/dca/dca.go so that the
Content-Type header is not added when body is nil (e.g., only call
req.Header.Set("Content-Type", "application/json") when the body variable is
non-nil), keeping the rest of the error handling and logging for req creation
intact.
- Line 32: The deferred call defer resp.Body.Close() ignores the error return;
update the function in internal/dca/dca.go to capture and handle the error from
resp.Body.Close() (e.g., assign err := resp.Body.Close() or check the returned
error inside the deferred closure) so any Close() failure is logged or returned;
locate the resp variable and replace the bare defer resp.Body.Close() with a
deferred closure that checks the error and uses the function's logger or error
return to report it.
- Line 18: The code builds the endpoint by concatenating
internal.Configuration.DCA.ApiUrl with "/transaction/reset-retry-counts" which
can produce malformed URLs if ApiUrl has a trailing slash; replace the string
concatenation that assigns url with a proper join using net/url (e.g.,
url.JoinPath) to combine internal.Configuration.DCA.ApiUrl and the endpoint
path, add "net/url" to imports, and update any variable or error handling around
the resulting url variable to use the joined value.

---

Nitpick comments:
In `@config.yaml.example`:
- Line 49: Replace the hard-coded production Railway URL in the example config
by changing the api_url value (the "api_url" key) to a clearly marked
placeholder (e.g., "https://your-dca-bot-api.example.com") so the example config
cannot accidentally target production; update the api_url entry in
config.yaml.example accordingly and ensure any accompanying documentation or
comments reflect that this is a placeholder for users to fill in.

In `@internal/config.go`:
- Around line 25-28: The DCAConfiguration.ApiUrl isn't validated in init(), so
add URL parsing/validation there: locate the init() function and after the other
server URL validations validate DCAConfiguration.ApiUrl by parsing it (use
url.Parse or equivalent), ensure a non-empty scheme (http/https) and host, and
return/log an error if invalid; reference the DCAConfiguration struct and its
ApiUrl field and ensure this prevents malformed concatenation later in the DCA
client code (referenced by internal/dca/dca.go using ApiUrl).

In `@internal/dca/dca.go`:
- Around line 26-27: The NotifyDeposit function currently creates a new
http.Client for each call (client := &http.Client{Timeout: 10 * time.Second}),
which prevents connection reuse; refactor by adding a package-level reusable
client (e.g., var httpClient = &http.Client{Timeout: 10 * time.Second}) and
replace the local client creation in NotifyDeposit with that httpClient so all
requests reuse the same client and connection pool; ensure you reference the
httpClient variable in NotifyDeposit and remove the per-call instantiation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a4ee353b-1e1d-41b5-aacc-5fa47cea6f5f

📥 Commits

Reviewing files that changed from the base of the PR and between 396682d and d1c7e68.

📒 Files selected for processing (5)
  • config.yaml.example
  • internal/config.go
  • internal/dca/dca.go
  • internal/lnbits/webhook/webhook.go
  • internal/telegram/transaction.go

Comment thread internal/dca/dca.go Outdated
return
}

url := internal.Configuration.DCA.ApiUrl + "/transaction/reset-retry-counts"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use proper URL joining instead of string concatenation.

Directly concatenating the API URL with the endpoint path can produce malformed URLs if Configuration.DCA.ApiUrl has a trailing slash (resulting in https://api.com//transaction/...) or is otherwise malformed.

🔗 Proposed fix using url.JoinPath
-	url := internal.Configuration.DCA.ApiUrl + "/transaction/reset-retry-counts"
-	req, err := http.NewRequest(http.MethodPost, url, nil)
+	baseURL := internal.Configuration.DCA.ApiUrl
+	fullURL, err := url.JoinPath(baseURL, "/transaction/reset-retry-counts")
+	if err != nil {
+		log.Errorf("[DCA] Error constructing URL: %s", err.Error())
+		return
+	}
+	req, err := http.NewRequest(http.MethodPost, fullURL, nil)

Note: Add "net/url" to imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/dca/dca.go` at line 18, The code builds the endpoint by
concatenating internal.Configuration.DCA.ApiUrl with
"/transaction/reset-retry-counts" which can produce malformed URLs if ApiUrl has
a trailing slash; replace the string concatenation that assigns url with a
proper join using net/url (e.g., url.JoinPath) to combine
internal.Configuration.DCA.ApiUrl and the endpoint path, add "net/url" to
imports, and update any variable or error handling around the resulting url
variable to use the joined value.

Comment thread internal/dca/dca.go
Comment on lines +19 to +24
req, err := http.NewRequest(http.MethodPost, url, nil)
if err != nil {
log.Errorf("[DCA] Error creating reset-retry-counts request: %s", err.Error())
return
}
req.Header.Set("Content-Type", "application/json")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove Content-Type header when sending no body.

The request sets Content-Type: application/json (line 24) but the request body is nil (line 19). This is inconsistent. If the DCA API endpoint does not require a request body, the Content-Type header should be omitted.

🧹 Proposed fix
 	req, err := http.NewRequest(http.MethodPost, url, nil)
 	if err != nil {
 		log.Errorf("[DCA] Error creating reset-retry-counts request: %s", err.Error())
 		return
 	}
-	req.Header.Set("Content-Type", "application/json")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
req, err := http.NewRequest(http.MethodPost, url, nil)
if err != nil {
log.Errorf("[DCA] Error creating reset-retry-counts request: %s", err.Error())
return
}
req.Header.Set("Content-Type", "application/json")
req, err := http.NewRequest(http.MethodPost, url, nil)
if err != nil {
log.Errorf("[DCA] Error creating reset-retry-counts request: %s", err.Error())
return
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/dca/dca.go` around lines 19 - 24, The request is setting
req.Header.Set("Content-Type", "application/json") while creating the POST with
a nil body; remove the incorrect header or set it only when a non-nil body is
provided. Update the code around the http.NewRequest(...) call in
internal/dca/dca.go so that the Content-Type header is not added when body is
nil (e.g., only call req.Header.Set("Content-Type", "application/json") when the
body variable is non-nil), keeping the rest of the error handling and logging
for req creation intact.

Comment thread internal/dca/dca.go
log.Errorf("[DCA] Error calling reset-retry-counts: %s", err.Error())
return
}
defer resp.Body.Close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Check error return from resp.Body.Close().

The deferred resp.Body.Close() call does not check for errors. While rare, Close() can fail and may indicate issues with connection cleanup or incomplete response reads.

🔧 Proposed fix
-	defer resp.Body.Close()
+	defer func() {
+		if err := resp.Body.Close(); err != nil {
+			log.Errorf("[DCA] Error closing response body: %s", err.Error())
+		}
+	}()

Based on learnings from static analysis tool errcheck.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
defer resp.Body.Close()
defer func() {
if err := resp.Body.Close(); err != nil {
log.Errorf("[DCA] Error closing response body: %s", err.Error())
}
}()
🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 32-32: Error return value of resp.Body.Close is not checked

(errcheck)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/dca/dca.go` at line 32, The deferred call defer resp.Body.Close()
ignores the error return; update the function in internal/dca/dca.go to capture
and handle the error from resp.Body.Close() (e.g., assign err :=
resp.Body.Close() or check the returned error inside the deferred closure) so
any Close() failure is logged or returned; locate the resp variable and replace
the bare defer resp.Body.Close() with a deferred closure that checks the error
and uses the function's logger or error return to report it.

Source: Linters/SAST tools

@xbuddhi xbuddhi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @helloscoopa — nice, focused change. The wiring is correct: SendTransaction is only ever called from Transaction.Send(), and every internal send/tip/faucet/shop/batch path routes through Send(), so the single hook there covers all internal transfers. No import cycle, and the goroutine keeps deposit handling non-blocking.

There's one correctness issue that will silently break the feature, plus a couple of robustness items — could you take a look before we merge?

1. Username @-prefix mismatch (blocking). Both call sites pass GetUserStr(), which returns @username (see internal/telegram/users.go:30). So wallet_username only matches if the operator includes the leading @. The config comment and empty example give no hint, so a value like BitcoinDeepaDCA silently never fires with no error to debug. Please normalize both sides (e.g. strings.TrimPrefix(x, "@")) so it works with or without the @.

2. No guard on empty api_url — if wallet_username is set but api_url is empty, every matching deposit POSTs to a relative path and fails. A cheap early-return makes misconfig obvious.

3. Unauthenticated callreset-retry-counts is POSTed with no auth token. If the backend endpoint is meant to be protected, add a shared secret; otherwise confirm it's intentionally open.

Minor (non-blocking): no trailing-slash handling on api_url, and failures are best-effort (logged, no retry) — fine for this design, just noting it. A small unit test on the match/no-op logic with httptest.Server would also catch #1.

Comment thread internal/dca/dca.go Outdated
Comment thread internal/dca/dca.go Outdated
Comment thread config.yaml.example

@xbuddhi xbuddhi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @helloscoopa — all three points are resolved, verified against f3abfd6:

  1. Username @ mismatchstrings.TrimPrefix on both the configured wallet and the incoming username, so it now matches with or without the leading @. This also makes the config comment a non-issue.
  2. Empty api_url guard — early-return with a clear log line, plus TrimSuffix on the trailing slash (also resolves CodeRabbit's URL-joining note).
  3. Auth — acknowledged as an intentionally-open, non-sensitive endpoint. Fine.

LGTM 👍

@xbuddhi xbuddhi merged commit 76e2bfb into main Jun 12, 2026
4 checks passed
@xbuddhi xbuddhi deleted the scoopa/auto-reset-retry-counts branch June 12, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants