Skip to content

feat(stellar): add fee-bump sponsorship - #50

Open
Dayz-tech-co wants to merge 2 commits into
Deen-Bridge:devfrom
Dayz-tech-co:feat/30-fee-sponsorship
Open

feat(stellar): add fee-bump sponsorship#50
Dayz-tech-co wants to merge 2 commits into
Deen-Bridge:devfrom
Dayz-tech-co:feat/30-fee-sponsorship

Conversation

@Dayz-tech-co

@Dayz-tech-co Dayz-tech-co commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add opt-in Stellar fee-bump sponsorship for purchase and donation submissions
  • strictly validate buyer signature, initialized transaction hash, memo, source, USDC asset, destinations, amounts, and operation count before sponsorship
  • enforce per-transaction, platform daily-spend, and per-user daily limits with atomic MongoDB reservations
  • preserve the normal payment path when disabled and return fallback-safe 4xx responses without failing pending rows
  • persist sponsored fee, inner hash, and fee-bump hash; add an authenticated sponsor status endpoint
  • document and validate all sponsorship environment settings

Testing

  • test/feeSponsor.test.js: 11 passed
  • non-database regression suites: 37 passed across 5 suites
  • full app suite requires the CI MongoDB service (local run reached a connection timeout only)

Closes #30

Summary by CodeRabbit

  • New Features
    • Added optional Stellar fee sponsorship (fee-bumps) for eligible donations and payments, including “request sponsorship” control.
    • Added sponsorship status endpoint to report availability, balance, and spending/cap limits.
    • Sponsored transactions now retain sponsorship and fee details, and transaction submission now returns feeCharged.
  • Bug Fixes
    • Improved handling of sponsorship eligibility/declines with clearer “can submit normally” guidance.
  • Tests
    • Added coverage for fee sponsorship validation, fee-bump fee caps, and daily/per-user spending limit enforcement.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9eae8634-9457-42e7-a142-56994fb393b6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Fee sponsorship is added for Stellar payments and donations through validated fee-bump transactions, configurable spend limits, persisted sponsorship metadata, and an authenticated sponsorship status endpoint.

Changes

Fee sponsorship flow

Layer / File(s) Summary
Sponsorship configuration, accounting, and validation
.env.example, src/config/validateEnv.js, src/models/FeeSponsorDailySpend.js, src/services/stellar/feeSponsorService.js, test/feeSponsor.test.js
Adds optional sponsorship settings, production secret validation, daily and per-user spend tracking, signed inner-transaction validation, fee-bump construction, reservation reconciliation, status reporting, and focused tests.
Payment and donation submission integration
src/models/Transaction.js, src/controllers/stellar/paymentController.js, src/controllers/stellar/donationController.js
Accepts requestSponsorship, reconciles prior attempts, submits through the sponsorship service when requested, returns normal-submission fallback details for retry-safe errors, and stores sponsorship metadata.
Sponsorship status endpoint
src/controllers/stellar/paymentController.js, src/routes/stellar/paymentRoutes.js
Adds an authenticated status endpoint backed by getFeeSponsorStatus, with mapped sponsorship errors.
Transaction fee result metadata
src/services/stellar/stellarService.js
Exposes charged fees from Stellar submission responses for sponsorship accounting and persistence.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PaymentController
  participant FeeSponsorService
  participant FeeSponsorDailySpend
  participant StellarServer
  participant Transaction
  Client->>PaymentController: Submit signed XDR with requestSponsorship
  PaymentController->>FeeSponsorService: Validate and prepare fee bump
  FeeSponsorService->>FeeSponsorDailySpend: Reserve sponsorship stroops
  FeeSponsorService->>StellarServer: Submit signed fee-bump transaction
  StellarServer-->>FeeSponsorService: Return transaction hash and charged fee
  FeeSponsorService->>FeeSponsorDailySpend: Reconcile reservation
  FeeSponsorService-->>PaymentController: Return sponsorship result
  PaymentController->>Transaction: Persist sponsorship metadata
  PaymentController-->>Client: Return submission response
Loading

Possibly related PRs

Suggested reviewers: abimbolaalabi

🚥 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 matches the main change: adding Stellar fee-bump sponsorship.
Linked Issues check ✅ Passed The changes align with #30: config, sponsorship service, controller flags, caps, persistence, status endpoint, and tests are all present.
Out of Scope Changes check ✅ Passed The PR stays focused on fee-bump sponsorship and related validation, persistence, routes, and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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: 5

🧹 Nitpick comments (2)
src/models/FeeSponsorDailySpend.js (1)

3-10: 🚀 Performance & Scalability | 🔵 Trivial

Schema looks solid and the fields map exactly to the atomic findOneAndUpdate reservation in feeSponsorService.js (dateKey, totalStroops, perUser.<userKey>). The dateKey unique index correctly supports the daily lookup/reservation path, and I appreciate that you deliberately avoided a TTL here — these accounting rows shouldn't be auto-deleted mid-cap-window.

One operational note for later: since there's (rightly) no TTL, one document accrues per UTC day forever. That's tiny data, but a periodic archival/cleanup job for rows older than your retention window keeps the collection tidy without risking active-day records.

🤖 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 `@src/models/FeeSponsorDailySpend.js` around lines 3 - 10, No code change is
required for this review; retain the current feeSponsorDailySpendSchema design,
including the unique dateKey index and absence of a TTL.
src/config/validateEnv.js (1)

31-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a fail-fast check for enabled-but-unconfigured sponsorship.

Treating all five as optional is right — the feature is opt-in. But there's a gap: if FEE_SPONSOR_ENABLED="true" while FEE_SPONSOR_SECRET is unset, validation still passes (the production-only warning at Line 79 is easy to miss), and the failure only surfaces later as a runtime 503 sponsor_unavailable from sponsorKeypair(). Failing fast at boot makes the misconfiguration obvious to operators instead of silently disabling sponsorship in prod.

♻️ Suggested conditional validation (add inside validateEnv, after the required-vars check)
if (process.env.FEE_SPONSOR_ENABLED === "true" && !process.env.FEE_SPONSOR_SECRET) {
  logger.error(
    "❌ FEE_SPONSOR_ENABLED is 'true' but FEE_SPONSOR_SECRET is not set."
  );
  process.exit(1);
}
🤖 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 `@src/config/validateEnv.js` around lines 31 - 35, Update validateEnv after the
required-vars check to fail fast when FEE_SPONSOR_ENABLED is exactly "true" but
FEE_SPONSOR_SECRET is unset: log a clear configuration error through logger and
exit with status 1. Keep the five sponsorship variables optional when
sponsorship is disabled.
🤖 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 `@src/controllers/stellar/paymentController.js`:
- Around line 444-450: Standardize the sponsorship responses across all listed
sites to the { success, message, data } envelope: in paymentController.js lines
444-450 and donationController.js lines 172-178, move code and canSubmitNormally
into data; in paymentController.js lines 641-645, place the sponsorship status
under data and provide a consistent message.
- Around line 436-464: Update the sponsored submission flow around
submitSponsoredTransaction in src/controllers/stellar/paymentController.js
(lines 436-464) to durably persist a sponsorship attempt/idempotency key before
the external call, reconcile ambiguous outcomes by the inner transaction hash
before allowing fallback, and handle subsequent database failures without
leaving the payment unrecoverable. Apply the same durable-attempt persistence
and unknown-outcome reconciliation to the corresponding sponsored submission
flow in src/controllers/stellar/donationController.js (lines 166-192).
- Line 410: Validate requestSponsorship with an explicit boolean check before
selecting the sponsorship flow in both
src/controllers/stellar/paymentController.js at lines 410-410 and
src/controllers/stellar/donationController.js at lines 139-139; return a 400
response for any non-boolean value while preserving normal handling for true and
false.

In `@src/models/Transaction.js`:
- Around line 16-19: Update the sponsorFeeStroops field in the Transaction model
to represent non-negative integer stroops safely, using the project’s validated
decimal-string/BigInt-compatible convention rather than an unrestricted Number
with only min: 0. Propagate the same representation and validation through
related services and controllers, ensuring all Stellar amount calculations use
integer stroops or BigInt.

In `@src/services/stellar/feeSponsorService.js`:
- Around line 40-43: Update sameAmount to parse both Stellar amount values into
exact integer stroops, using BigInt or the existing precise amount parser, and
compare those integers directly. Remove the Number(...).toFixed(7) normalization
so distinct high-value payments cannot round to the same comparison value.

---

Nitpick comments:
In `@src/config/validateEnv.js`:
- Around line 31-35: Update validateEnv after the required-vars check to fail
fast when FEE_SPONSOR_ENABLED is exactly "true" but FEE_SPONSOR_SECRET is unset:
log a clear configuration error through logger and exit with status 1. Keep the
five sponsorship variables optional when sponsorship is disabled.

In `@src/models/FeeSponsorDailySpend.js`:
- Around line 3-10: No code change is required for this review; retain the
current feeSponsorDailySpendSchema design, including the unique dateKey index
and absence of a TTL.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dad3ce6c-ad5b-4dd8-a053-0f0df837d19b

📥 Commits

Reviewing files that changed from the base of the PR and between 2866f7b and f9b98fd.

📒 Files selected for processing (10)
  • .env.example
  • src/config/validateEnv.js
  • src/controllers/stellar/donationController.js
  • src/controllers/stellar/paymentController.js
  • src/models/FeeSponsorDailySpend.js
  • src/models/Transaction.js
  • src/routes/stellar/paymentRoutes.js
  • src/services/stellar/feeSponsorService.js
  • src/services/stellar/stellarService.js
  • test/feeSponsor.test.js

Comment thread src/controllers/stellar/paymentController.js
Comment thread src/controllers/stellar/paymentController.js Outdated
Comment thread src/controllers/stellar/paymentController.js Outdated
Comment thread src/models/Transaction.js
Comment thread src/services/stellar/feeSponsorService.js
@Dayz-tech-co

Copy link
Copy Markdown
Contributor Author

Addressed all five actionable findings in commit 9568232:

  • reject non-boolean requestSponsorship values on both endpoints
  • use the { success, message, data } envelope for sponsorship responses
  • compare amounts as exact BigInt stroops and persist charged fees as validated integer strings
  • fail fast when sponsorship is enabled without a secret
  • durably persist the inner-hash attempt before Horizon submission, reconcile unknown outcomes by inner hash, and block unsafe normal fallback while an outcome is ambiguous

Validation: 38 tests passed across 5 suites; syntax and diff checks are clean.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

@Dayz-tech-co Thanks for the thorough follow-up. I’ll review PR #50 with particular attention to the fee-sponsorship safety invariants and the reported fallback protections.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 1

🧹 Nitpick comments (2)
src/models/Transaction.js (1)

26-30: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Optional: make sponsorshipAttemptKey a partial unique index to harden idempotency.

The staged submission flow uses sponsorshipAttemptKey (the inner tx hash) as the idempotency key, but today the guarantee lives only in controller logic. A DB-level partial unique index would prevent two rows from ever claiming the same sponsored inner hash even under a race, which is the kind of belt-and-suspenders worth having on a payment path. Keep it partial/sparse since most documents leave this field unset.

transactionSchema.index(
  { sponsorshipAttemptKey: 1 },
  { unique: true, partialFilterExpression: { sponsorshipAttemptKey: { $type: "string" } } }
);
🤖 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 `@src/models/Transaction.js` around lines 26 - 30, Update the transaction
schema around sponsorshipAttemptKey to add a partial unique index on
sponsorshipAttemptKey, applying uniqueness only when the field contains a
string. Preserve the existing field definition and ensure documents without
sponsorshipAttemptKey remain allowed.

Source: Path instructions

src/controllers/stellar/paymentController.js (1)

448-464: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

Avoid holding the MongoDB transaction open across the Horizon reconcile call.

The else branch was carefully written to commitTransaction() before the external submitPreparedSponsoredTransaction call — good, because MongoDB multi-document transactions have a transactionLifetimeLimitSeconds (default 60s) and hold locks while open. The reconcile-prior branch breaks that discipline: it calls reconcileSponsoredTransaction(...) (a Horizon HTTP fetch of up to 200 records) while the transaction started at the top of the handler is still open, and then keeps that same transaction open through the later verifyPaymentOperations network call up to the final commit. Under slow Horizon responses this can trip the transaction lifetime limit and abort a legitimate sponsored recovery, or hold locks longer than necessary.

Recommend committing/ending the read transaction before the external reconcile call and re-opening a fresh transaction afterward (as the else branch already does), so external I/O never runs inside an open DB transaction.

  • src/controllers/stellar/paymentController.js#L448-L464: commit/end the transaction before reconcileSponsoredTransaction(transaction.sponsorshipAttemptKey), then re-startTransaction() before continuing to the submitted/verify steps.
  • src/controllers/stellar/donationController.js#L178-L194: apply the same restructuring around reconcileSponsoredTransaction(donation.sponsorshipAttemptKey).
🤖 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 `@src/controllers/stellar/paymentController.js` around lines 448 - 464, In
src/controllers/stellar/paymentController.js lines 448-464, commit/end the
current read transaction before calling reconcileSponsoredTransaction, then
start a fresh transaction before the subsequent submitted/verify flow. Apply the
same restructuring around reconcileSponsoredTransaction in
src/controllers/stellar/donationController.js lines 178-194; ensure no external
Horizon or verification network call runs while a MongoDB transaction is open.
🤖 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 `@src/controllers/stellar/paymentController.js`:
- Around line 730-742: Restrict getSponsorshipStatus to administrators by
applying the existing admin authorization guard in the route configuration
before this handler runs. Keep protect for authentication, ensure non-admin
users are rejected, and preserve the handler’s current response behavior for
authorized requests.

---

Nitpick comments:
In `@src/controllers/stellar/paymentController.js`:
- Around line 448-464: In src/controllers/stellar/paymentController.js lines
448-464, commit/end the current read transaction before calling
reconcileSponsoredTransaction, then start a fresh transaction before the
subsequent submitted/verify flow. Apply the same restructuring around
reconcileSponsoredTransaction in src/controllers/stellar/donationController.js
lines 178-194; ensure no external Horizon or verification network call runs
while a MongoDB transaction is open.

In `@src/models/Transaction.js`:
- Around line 26-30: Update the transaction schema around sponsorshipAttemptKey
to add a partial unique index on sponsorshipAttemptKey, applying uniqueness only
when the field contains a string. Preserve the existing field definition and
ensure documents without sponsorshipAttemptKey remain allowed.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 48ab9f53-13b9-4de3-b8b8-a63eba18258d

📥 Commits

Reviewing files that changed from the base of the PR and between f9b98fd and 9568232.

📒 Files selected for processing (6)
  • src/config/validateEnv.js
  • src/controllers/stellar/donationController.js
  • src/controllers/stellar/paymentController.js
  • src/models/Transaction.js
  • src/services/stellar/feeSponsorService.js
  • test/feeSponsor.test.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/config/validateEnv.js
  • test/feeSponsor.test.js
  • src/services/stellar/feeSponsorService.js

Comment on lines +730 to +742
export const getSponsorshipStatus = async (req, res) => {
try {
res.status(200).json({
success: true,
message: "Fee sponsorship status retrieved",
data: { sponsorship: await getFeeSponsorStatus() },
});
} catch (error) {
logger.error("Fee sponsorship status error:", error);
const status = error instanceof FeeSponsorshipError ? error.status : 500;
res.status(status).json({ success: false, message: error.message, data: null });
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how the sponsorship status route is protected
fd paymentRoutes.js --exec cat -n {}

Repository: Deen-Bridge/dnb-backend

Length of output: 1219


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the auth middleware and the route mount chain
printf '\n### authMiddleware.js\n'
fd authMiddleware.js --exec sh -c 'cat -n "$1"' sh {}

printf '\n### routes mounting references\n'
rg -n "paymentRoutes|/stellar|sponsorship/status|router\.use\(protect\)|admin" src --glob '!**/node_modules/**'

Repository: Deen-Bridge/dnb-backend

Length of output: 5859


Restrict /sponsorship/status to admins
router.use(protect) only authenticates users; it doesn’t add any role check, so any logged-in user can read the sponsor account public key, XLM balance, and daily spend. Add an admin guard before exposing this treasury data.

🤖 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 `@src/controllers/stellar/paymentController.js` around lines 730 - 742,
Restrict getSponsorshipStatus to administrators by applying the existing admin
authorization guard in the route configuration before this handler runs. Keep
protect for authentication, ensure non-admin users are rejected, and preserve
the handler’s current response behavior for authorized requests.

Source: Path instructions

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.

1 participant