Skip to content

Feat/Deposit Account Pool Service (Stellar Keypair Management for Deposit-Based Payments) - #37

Merged
codebestia merged 6 commits into
ShadeProtocol:mainfrom
DioChuks:feat/deposit-pool-service
Jul 29, 2026
Merged

Feat/Deposit Account Pool Service (Stellar Keypair Management for Deposit-Based Payments)#37
codebestia merged 6 commits into
ShadeProtocol:mainfrom
DioChuks:feat/deposit-pool-service

Conversation

@DioChuks

@DioChuks DioChuks commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Deposit Account Pool Service Summary

The Deposit Account Pool Service manages a pool of Stellar deposit keypairs for deposit-based invoice payments.

Key Changes Made

1. Encryption Utility

  • Created encryption.ts using AES-256-GCM.
  • Keyed from DEPOSIT_ACCOUNT_ENCRYPTION_KEY env variable (32-byte hex-encoded string). Enforces strict length checking and fails fast if missing or invalid.
  • Exports encrypt(plaintext) and decrypt(ciphertext) using ${ivHex}:${authTagHex}:${ciphertextHex} format.

2. Environment Configuration

  • Added STELLAR_HORIZON_URL and DEPOSIT_ACCOUNT_ENCRYPTION_KEY to .env.example.
  • Added stellarHorizonUrl and depositAccountEncryptionKey to environment.ts.

3. Database Schema Migration

  • Created DepositAccount model in schema.prisma:
    • id: UUID Primary Key
    • address: Unique Stellar public key (G...)
    • encryptedSecret: AES-256-GCM ciphertext
    • invoiceId: Unique nullable FK to Invoice
    • inUse: Boolean flag (default false)
    • lastUsedAt: Nullable DateTime
    • createdAt & updatedAt: Timestamps
  • Updated Invoice model with relation field depositAccount DepositAccount?.
  • Ran Prisma migration 20260728064714_add_deposit_account.

4. Deposit Account Service

  • Created class-based service deposit-account.service.ts constructed with Horizon.Server:
    • createAccount(): Generates Keypair.random(), encrypts secret seed, persists record, returns secret-free summary. No on-chain transactions submitted.
    • getAllAccounts(): Returns all deposit account summaries. Secrets are never exposed.
    • getAccountBalance(address): Queries horizon.loadAccount(address). Gracefully returns empty array [] when account is 404 (not yet funded/created on-chain).
    • getAvailableAccounts(): Returns deposit accounts where inUse is false.
    • assignAccount(invoiceId): Safe concurrent account allocation using conditional updateMany({ where: { id: candidateId, inUse: false } }) with retry loop.
    • releaseAccount(accountId): Clears invoiceId to null and resets inUse to false, allowing re-assignment to future invoices.

5. Automated Tests

  • Created encryption.test.ts covering AES-256-GCM round-trip encryption/decryption, key validation, and malformed inputs.
  • Created deposit-account.service.test.ts covering account creation, secret exclusion, 404 account balance fallback, concurrency-safe assignment, and account releasing/re-use.

Verification Results

Automated Tests

  • deposit-account.service.test.ts: Passed 11/11 tests.
  • encryption.test.ts: Passed 7/7 tests.
  • Full Jest Test Suite: 30 test suites passed, 240 tests passed cleanly.

Type Check & Formatting

  • npx tsc --noEmit: Executed cleanly with 0 type errors.
  • npm run format: Prettier formatting applied to all files.

Closes #27

Summary by CodeRabbit

  • New Features

    • Added support for creating, storing, assigning, releasing, and listing Stellar deposit accounts.
    • Added encrypted credential handling to protect deposit account secrets.
    • Added Stellar balance lookup with graceful handling for accounts not yet funded.
    • Added invoice associations and payment confirmation tracking for deposit accounts.
  • Configuration

    • Added settings for the Stellar Horizon endpoint and deposit account encryption key.
  • Tests

    • Added coverage for deposit account management, balance retrieval, assignment conflicts, and encryption behavior.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@DioChuks, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 581cb972-b232-440a-86b9-d3f5aca8b127

📥 Commits

Reviewing files that changed from the base of the PR and between 3533981 and 7d16f96.

📒 Files selected for processing (2)
  • prisma/migrations/20260728064714_add_deposit_account/migration.sql
  • prisma/schema.prisma
📝 Walkthrough

Walkthrough

Adds encrypted Stellar deposit-account persistence and a class-based service for creating, listing, balancing, assigning, and releasing accounts. Updates Prisma schema and migration, environment configuration, encryption utilities, TypeScript/ESLint settings, and unit tests.

Changes

Deposit account pool

Layer / File(s) Summary
Deposit account schema and configuration
prisma/schema.prisma, prisma/migrations/..., .env.example, src/config/environment.ts
Adds the DepositAccount model, its Invoice relation, migration constraints, and Stellar/encryption environment variables.
Encrypted secret storage
src/utils/encryption.ts, tests/unit/encryption.test.ts
Adds AES-256-GCM encryption, decryption, key validation, and tests for round trips, malformed ciphertext, and tampering.
Deposit account lifecycle
src/services/deposit-account.service.ts
Adds account generation, sanitized retrieval, Horizon balance lookup, available-account selection, conditional assignment retries, and release behavior.
Service validation and tooling
tests/unit/deposit-account.service.test.ts, eslint.config.cjs, tsconfig.json
Tests account lifecycle behavior and updates lint exclusions and TypeScript declaration checking configuration.

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

Suggested reviewers: codebestia

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant DepositAccountService
  participant Prisma
  participant Horizon
  Caller->>DepositAccountService: assignAccount(invoiceId)
  DepositAccountService->>Prisma: select unused account
  Prisma-->>DepositAccountService: candidate accounts
  DepositAccountService->>Prisma: conditionally claim candidate
  Prisma-->>DepositAccountService: update count
  DepositAccountService-->>Caller: sanitized account
  Caller->>DepositAccountService: getAccountBalance(address)
  DepositAccountService->>Horizon: loadAccount(address)
  Horizon-->>DepositAccountService: balances or not-found
  DepositAccountService-->>Caller: balances or empty list
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes unrelated lint and TypeScript config changes that are outside the deposit-account service scope. Remove the ESLint and tsconfig tweaks unless they are required by the issue, and keep the PR focused on the deposit-account service work.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the new Stellar deposit account pool service.
Linked Issues check ✅ Passed The changes implement the requested service, encryption, schema, and concurrency-safe account assignment while avoiding HTTP routes or sweep logic.
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

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.

@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 (7)
tests/unit/encryption.test.ts (1)

51-51: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Avoid a hardcoded key-shaped literal as test plaintext.

This string is shaped like a real Stellar secret key, which is why the secret scanner (Betterleaks) flags it. Generate it dynamically instead (as already done for validKeyHex on line 5) to avoid CI secret-scan noise and remove any ambiguity about whether it's a real credential.

🔧 Suggested fix
-      const secret = 'SDORW56POGIXY3NZS24EPRP36Y4QUTYJ2E2MVRKVKZ27TXL5N7227W4G';
+      const secret = `S${crypto.randomBytes(28).toString('hex').toUpperCase()}`;
🤖 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 `@tests/unit/encryption.test.ts` at line 51, Replace the hardcoded key-shaped
plaintext assigned to secret with a dynamically generated non-secret value,
following the existing validKeyHex generation pattern at the top of the test
file; keep the test’s plaintext semantics unchanged while ensuring secret
scanners cannot interpret it as a real credential.

Source: Linters/SAST tools

prisma/schema.prisma (1)

248-261: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add an index to back the assignAccount query pattern.

assignAccount() (in the dependent service layer) queries where: { inUse: false } ordered by createdAt on every assignment attempt. Without a matching index this becomes a full table scan as the account pool grows.

♻️ Suggested index
 model DepositAccount {
   id              String    `@id` `@default`(uuid())
   address         String    `@unique`
   encryptedSecret String
   invoiceId       String?   `@unique`
   inUse           Boolean   `@default`(false)
   lastUsedAt      DateTime?
   createdAt       DateTime  `@default`(now())
   updatedAt       DateTime  `@updatedAt`

   invoice         Invoice?  `@relation`(fields: [invoiceId], references: [id])
+
+  @@index([inUse, createdAt])
 }
🤖 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 `@prisma/schema.prisma` around lines 248 - 261, Add a composite index on
DepositAccount covering inUse and createdAt to support the assignAccount query’s
filtering and ordering. Keep the existing fields, constraints, and relation
unchanged.
src/utils/encryption.ts (2)

1-47: 🔒 Security & Privacy | 🔵 Trivial

No key-rotation strategy for DEPOSIT_ACCOUNT_ENCRYPTION_KEY.

Rotating this key would make all previously-encrypted DepositAccount.encryptedSecret values undecryptable, since decrypt() only ever tries the current key. Worth considering a key-version prefix in the ciphertext (or a small keyring) before this reaches production scale, given it protects live Stellar secret keys.

🤖 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/utils/encryption.ts` around lines 1 - 47, Update getEncryptionKey,
encrypt, and decrypt to support key rotation through a small keyring or
versioned ciphertext format. Ensure newly encrypted values identify the active
key version, while decrypt selects the corresponding current or legacy key so
existing DepositAccount.encryptedSecret values remain readable during rotation.
Preserve authentication and reject unknown or unavailable key versions clearly.

7-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reading process.env directly bypasses the environment config module.

environment.depositAccountEncryptionKey already centralizes this value; reading process.env.DEPOSIT_ACCOUNT_ENCRYPTION_KEY again here (rather than relying solely on environment) duplicates parsing logic and creates two sources of truth that can drift if the config module's handling changes later. This appears aimed at letting tests mutate process.env after the environment module snapshot was taken — consider instead resetting Jest's module registry (jest.resetModules()) between tests so the config module remains the single source of truth.

🤖 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/utils/encryption.ts` around lines 7 - 19, Update getEncryptionKey to use
only environment.depositAccountEncryptionKey and remove the direct
process.env.DEPOSIT_ACCOUNT_ENCRYPTION_KEY fallback, preserving the existing
validation and Buffer conversion. If tests need to change the configured key
between cases, reset the Jest module registry and reload the environment
configuration rather than mutating around its snapshot.
tests/unit/deposit-account.service.test.ts (1)

167-242: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing test for the P2002 (409) conflict path in assignAccount.

The success/race-retry/empty-pool cases are covered, but the unique-constraint conflict branch (service.ts lines 111-117, returning AppError(409, 'Invoice already has an assigned deposit account')) has no corresponding test — it's a concurrency-safety guarantee called out in the PR objectives.

🧪 Suggested test addition
it('throws 409 when invoiceId already has an assigned deposit account (unique constraint)', async () => {
  const candidate = { id: 'acc-1', address: 'GAAA1', inUse: false };
  prismaMock.depositAccount.findMany.mockResolvedValue([candidate]);
  prismaMock.depositAccount.updateMany.mockRejectedValue({ code: 'P2002' });

  await expect(service.assignAccount('inv-dup')).rejects.toMatchObject({
    statusCode: 409,
    message: 'Invoice already has an assigned deposit account',
  });
});

Want me to add this test?

🤖 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 `@tests/unit/deposit-account.service.test.ts` around lines 167 - 242, Add a
test in the assignAccount suite covering the unique-constraint conflict path:
mock findMany with an available candidate, make updateMany reject with a Prisma
P2002 error, and assert assignAccount rejects with statusCode 409 and message
“Invoice already has an assigned deposit account.”
eslint.config.cjs (1)

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

Blanket-ignoring tests/** removes lint coverage for all test code.

Excluding the whole directory means unused vars, unsafe any/type issues, etc. in test files will never be caught going forward. If the motivation is conflicts with patterns like top-level as any casts or dynamic ESM imports in test files, a scoped override (relaxing only the offending rules for tests/**) preserves more of the safety net than a full ignore.

🤖 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 `@eslint.config.cjs` at line 30, Remove the blanket tests/** entry from the
ignore configuration and add a scoped ESLint override for tests/** that relaxes
only the specific rules causing test-file conflicts, such as top-level as any
casts or dynamic ESM imports, while retaining the remaining lint and type-safety
checks.
src/services/deposit-account.service.ts (1)

78-120: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid the extra findUnique round trip on the success path.

After a winning updateMany (result.count === 1), the new row is fully derivable from candidate plus the values just written — no other field is mutated by this service, so re-querying the DB is an avoidable round trip on the account-assignment hot path.

♻️ Proposed simplification
           if (result.count === 1) {
-            const updated = await prisma.depositAccount.findUnique({
-              where: { id: candidate.id },
-            });
-            return sanitizeDepositAccount(updated!);
+            return sanitizeDepositAccount({
+              ...candidate,
+              inUse: true,
+              invoiceId,
+              lastUsedAt: now,
+            });
           }

Note: this also removes the updated! non-null assertion, which currently has no fallback if the row were ever missing.

🤖 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/services/deposit-account.service.ts` around lines 78 - 120, In
assignAccount, remove the successful-path findUnique call after updateMany
returns count 1. Construct the sanitized result directly from candidate,
applying the assigned invoiceId, inUse, and lastUsedAt values written by the
update, and eliminate the updated non-null assertion while preserving the
existing conflict and retry behavior.
🤖 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 `@prisma/schema.prisma`:
- Line 259: The DepositAccount.invoice relation currently allows invoice
deletion to null invoiceId without releasing the account. In
prisma/schema.prisma:259, add an explicit restrictive onDelete strategy to the
invoice relation, then regenerate the corresponding foreign-key constraint in
prisma/migrations/20260728064714_add_deposit_account/migration.sql:40-41 so the
database matches the Prisma schema.

---

Nitpick comments:
In `@eslint.config.cjs`:
- Line 30: Remove the blanket tests/** entry from the ignore configuration and
add a scoped ESLint override for tests/** that relaxes only the specific rules
causing test-file conflicts, such as top-level as any casts or dynamic ESM
imports, while retaining the remaining lint and type-safety checks.

In `@prisma/schema.prisma`:
- Around line 248-261: Add a composite index on DepositAccount covering inUse
and createdAt to support the assignAccount query’s filtering and ordering. Keep
the existing fields, constraints, and relation unchanged.

In `@src/services/deposit-account.service.ts`:
- Around line 78-120: In assignAccount, remove the successful-path findUnique
call after updateMany returns count 1. Construct the sanitized result directly
from candidate, applying the assigned invoiceId, inUse, and lastUsedAt values
written by the update, and eliminate the updated non-null assertion while
preserving the existing conflict and retry behavior.

In `@src/utils/encryption.ts`:
- Around line 1-47: Update getEncryptionKey, encrypt, and decrypt to support key
rotation through a small keyring or versioned ciphertext format. Ensure newly
encrypted values identify the active key version, while decrypt selects the
corresponding current or legacy key so existing DepositAccount.encryptedSecret
values remain readable during rotation. Preserve authentication and reject
unknown or unavailable key versions clearly.
- Around line 7-19: Update getEncryptionKey to use only
environment.depositAccountEncryptionKey and remove the direct
process.env.DEPOSIT_ACCOUNT_ENCRYPTION_KEY fallback, preserving the existing
validation and Buffer conversion. If tests need to change the configured key
between cases, reset the Jest module registry and reload the environment
configuration rather than mutating around its snapshot.

In `@tests/unit/deposit-account.service.test.ts`:
- Around line 167-242: Add a test in the assignAccount suite covering the
unique-constraint conflict path: mock findMany with an available candidate, make
updateMany reject with a Prisma P2002 error, and assert assignAccount rejects
with statusCode 409 and message “Invoice already has an assigned deposit
account.”

In `@tests/unit/encryption.test.ts`:
- Line 51: Replace the hardcoded key-shaped plaintext assigned to secret with a
dynamically generated non-secret value, following the existing validKeyHex
generation pattern at the top of the test file; keep the test’s plaintext
semantics unchanged while ensuring secret scanners cannot interpret it as a real
credential.
🪄 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 Plus

Run ID: 1436cfd3-87e8-4fae-a005-3ba82a722a53

📥 Commits

Reviewing files that changed from the base of the PR and between cfb68da and 3533981.

📒 Files selected for processing (10)
  • .env.example
  • eslint.config.cjs
  • prisma/migrations/20260728064714_add_deposit_account/migration.sql
  • prisma/schema.prisma
  • src/config/environment.ts
  • src/services/deposit-account.service.ts
  • src/utils/encryption.ts
  • tests/unit/deposit-account.service.test.ts
  • tests/unit/encryption.test.ts
  • tsconfig.json

Comment thread prisma/schema.prisma Outdated
@DioChuks

Copy link
Copy Markdown
Contributor Author

@codebestia I've resolve the minor data integrity & behavior concern, pls check it out, thanks.

@codebestia codebestia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!
Nice Implementation.
Thank you for your contribution.

@codebestia
codebestia merged commit 747b19c into ShadeProtocol:main Jul 29, 2026
3 checks passed
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.

Deposit Account Pool Service (Stellar Keypair Management for Deposit-Based Payments)

2 participants