Skip to content

feat(backend): add deactivateServiceOnChain and POST /api/services/:id/deactivate route#192

Open
leocagli wants to merge 1 commit into
Stellar-Ecosystem:mainfrom
leocagli:feat/deactivate-service-route
Open

feat(backend): add deactivateServiceOnChain and POST /api/services/:id/deactivate route#192
leocagli wants to merge 1 commit into
Stellar-Ecosystem:mainfrom
leocagli:feat/deactivate-service-route

Conversation

@leocagli

@leocagli leocagli commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #14

Problem

The Soroban registry contract exposes deactivate_service(provider, id) with provider.require_auth() authorization checks, but no backend route called it. Providers using the server keypair (services registered via registerServiceOnChain / seed scripts / update-endpoints.js) had no API path to deactivate listings — only direct Soroban CLI invocation worked.

Changes

backend/src/lib/contract.js — new deactivateServiceOnChain(id):

export async function deactivateServiceOnChain(id) {
  const op = contract.call(
    'deactivate_service',
    nativeToScVal(providerAddress, { type: 'address' }),
    nativeToScVal(BigInt(id), { type: 'u64' })
  );
  await simulateAndSubmit(op);
}

backend/src/routes/registry.js — new POST /api/services/:id/deactivate:

Condition Response
Invalid ID 400 INVALID_ID
Service not found 404 NOT_FOUND
Already inactive 409 ALREADY_DEACTIVATED
Chain error 500 DEACTIVATE_ERROR
Success 200 { success: true, id }

The route is rate-limited via writeRateLimiter() to match the other write endpoints.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added service deactivation capability with validation checks to ensure services exist and are currently active before processing deactivation requests.

…d/deactivate

Closes Stellar-Ecosystem#14

The Soroban registry contract exposes deactivate_service(provider, id) with
provider.require_auth() and provider == entry.provider authorization, but no
backend route exposed it — providers using the server keypair had no API path
to deactivate their listings after registration.

Added:
- deactivateServiceOnChain(id) in backend/src/lib/contract.js: builds and
  submits the deactivate_service call signed by the server keypair (which is
  the provider for services registered via registerServiceOnChain / seed scripts)
- POST /api/services/:id/deactivate in backend/src/routes/registry.js:
  validates the ID, checks the service exists and is active, then calls
  deactivateServiceOnChain; returns 404 if not found, 409 if already inactive,
  500 on chain error
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

A new deactivateServiceOnChain(id) helper is added to the contract library, building and submitting a deactivate_service on-chain call. A new POST /services/:id/deactivate route consumes this helper after validating the service id, existence, and active status, returning typed error codes for each failure case.

Changes

Service Deactivation Feature

Layer / File(s) Summary
deactivateServiceOnChain contract helper
backend/src/lib/contract.js
Adds an exported async function that builds a deactivate_service contract call with the server keypair's public key as provider and the given id as u64, submits it via the existing simulate+submit flow, and logs/throws on failure.
POST /services/:id/deactivate route
backend/src/routes/registry.js
Imports deactivateServiceOnChain and adds a POST endpoint that parses and validates :id, checks the service exists and is active (returning 400, 404, or 409 with specific error codes), invokes the contract helper, logs success, and returns { success: true, id } or 500 DEACTIVATE_ERROR.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested reviewers

  • ritik4ever

Poem

🐇 Hop hop, the service sleeps,
A chain call made, no longer keeps.
The route checks twice — alive or gone?
Then signs the tx and carries on.
One helper, one endpoint, all neatly done! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements both required coding objectives: deactivateServiceOnChain helper function and POST /api/services/:id/deactivate route with validation and error handling. Add or update automated test coverage for the new route and function as specified in the acceptance criteria for maintainability.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main changes: adding deactivateServiceOnChain function and the new deactivate route.
Out of Scope Changes check ✅ Passed All changes are directly aligned with Issue #14 requirements: the new deactivateServiceOnChain function and POST deactivate route with appropriate error handling.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/deactivate-service-route

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: 2

🤖 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 `@backend/src/routes/registry.js`:
- Around line 153-156: The current code at Line 154 returns a 404 status for any
falsy value from getService(id), but getService() returns null for both missing
services and contract read errors. Modify the logic to distinguish between these
two cases: either update getService() to return a distinct value or error object
for read failures, or enhance the route handler to catch exceptions from
getService(id) separately and map those failures to a 5xx status code, while
reserving 404 only for cases where the service genuinely does not exist.
- Around line 145-163: The deactivate service route handler currently only
applies writeRateLimiter() middleware, allowing any unauthenticated user to
deactivate services. Add authentication middleware before or after the
writeRateLimiter() call in the router.post("/services/:id/deactivate", ...)
handler to verify the caller's identity. Additionally, after retrieving the
service with getService(id), add authorization logic to verify that the
authenticated caller is the provider/owner of that service before allowing the
deactivateServiceOnChain(id) operation to proceed. Return a 403 Forbidden status
if the caller is not authorized to deactivate that particular service.
🪄 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: fd6f4284-032c-4c56-925d-b4557fd5f01d

📥 Commits

Reviewing files that changed from the base of the PR and between 6fb6da9 and 3145887.

📒 Files selected for processing (2)
  • backend/src/lib/contract.js
  • backend/src/routes/registry.js

Comment on lines +145 to +163
router.post("/services/:id/deactivate", writeRateLimiter(), async (req, res) => {
let id;
try {
id = parseInt(req.params.id, 10);
if (isNaN(id) || id < 1) {
return res.status(400).json({ error: "Invalid service ID", code: "INVALID_ID" });
}

const service = await getService(id);
if (!service) {
return res.status(404).json({ error: "Service not found", code: "NOT_FOUND" });
}
if (!service.active) {
return res.status(409).json({ error: "Service is already deactivated", code: "ALREADY_DEACTIVATED" });
}

await deactivateServiceOnChain(id);
logger.info({ id }, "Service deactivated");
res.json({ success: true, id });

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 | 🔴 Critical | ⚡ Quick win

Add provider authentication/authorization to this write route.

At Line 145, only writeRateLimiter() is enforced. This allows unauthenticated callers to invoke deactivation, and because the backend signs with the server keypair, callers can deactivate server-owned listings without proving provider identity.

🤖 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 `@backend/src/routes/registry.js` around lines 145 - 163, The deactivate
service route handler currently only applies writeRateLimiter() middleware,
allowing any unauthenticated user to deactivate services. Add authentication
middleware before or after the writeRateLimiter() call in the
router.post("/services/:id/deactivate", ...) handler to verify the caller's
identity. Additionally, after retrieving the service with getService(id), add
authorization logic to verify that the authenticated caller is the
provider/owner of that service before allowing the deactivateServiceOnChain(id)
operation to proceed. Return a 403 Forbidden status if the caller is not
authorized to deactivate that particular service.

Comment on lines +153 to +156
const service = await getService(id);
if (!service) {
return res.status(404).json({ error: "Service not found", code: "NOT_FOUND" });
}

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 | 🏗️ Heavy lift

Don’t return NOT_FOUND for upstream read failures.

At Line 153, getService(id) is used as an existence check, but backend/src/lib/contract.js returns null on read exceptions too. That makes Line 154 emit 404 during RPC/contract failures, masking outages as missing services. Please split “service not found” from “contract read failed” and map read failures to 5xx.

🤖 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 `@backend/src/routes/registry.js` around lines 153 - 156, The current code at
Line 154 returns a 404 status for any falsy value from getService(id), but
getService() returns null for both missing services and contract read errors.
Modify the logic to distinguish between these two cases: either update
getService() to return a distinct value or error object for read failures, or
enhance the route handler to catch exceptions from getService(id) separately and
map those failures to a 5xx status code, while reserving 404 only for cases
where the service genuinely does not exist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant