Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 37 additions & 16 deletions backend/src/lib/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@ const {
rpc,
} = pkg;
import config from '../config.js';
import { getStellarServer, getNetworkPassphrase } from './stellar.js';
import { getStellarServer, getNetworkPassphrase, getCurrentLedgerSequence } from './stellar.js';
import logger from './logger.js';


const TIMEOUT = 30;

// Soroban persistent storage MAX_TTL constant (mirrors contract/src/lib.rs)
const MAX_TTL_LEDGERS = 3_110_400;
// Warn when within 10% of expiry (~18 days at 5s/ledger)
const TTL_WARNING_THRESHOLD = Math.floor(MAX_TTL_LEDGERS * 0.1);

function getContract() {
return new Contract(config.contract.id);
}
Expand Down Expand Up @@ -113,6 +118,10 @@ async function simulateRead(operation) {
}


function hasTtlWarning(registeredAt, currentLedger) {
return currentLedger >= registeredAt + MAX_TTL_LEDGERS - TTL_WARNING_THRESHOLD;
}

export async function listServices({ category, page = 0, pageSize = 20 } = {}) {
try {
const contract = getContract();
Expand All @@ -127,24 +136,31 @@ export async function listServices({ category, page = 0, pageSize = 20 } = {}) {
nativeToScVal(pageSize, { type: 'u32' }),
optionArg,
);
const retval = await simulateRead(callOp);
const [retval, currentLedger] = await Promise.all([
simulateRead(callOp),
getCurrentLedgerSequence().catch(() => 0),
]);
Comment on lines +139 to +142

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

Ledger fallback is silent and won’t trigger on hangs.

At Line 141 and Line 176, .catch(() => 0) suppresses RPC failures with no log, and it does not protect against a hanging ledger call (no rejection), so Promise.all can still stall both endpoints.

Suggested fix
 const TIMEOUT = 30;
+const LEDGER_FETCH_TIMEOUT_MS = TIMEOUT * 1000;
+
+async function getCurrentLedgerSequenceSafe(context) {
+  try {
+    return await Promise.race([
+      getCurrentLedgerSequence(),
+      new Promise((_, reject) =>
+        setTimeout(() => reject(new Error('getCurrentLedgerSequence timeout')), LEDGER_FETCH_TIMEOUT_MS),
+      ),
+    ]);
+  } catch (err) {
+    logger.warn({ err, context }, 'Ledger fetch failed; ttl_warning disabled for this response');
+    return 0;
+  }
+}
 ...
-    const [retval, currentLedger] = await Promise.all([
+    const [retval, currentLedger] = await Promise.all([
       simulateRead(callOp),
-      getCurrentLedgerSequence().catch(() => 0),
+      getCurrentLedgerSequenceSafe('listServices'),
     ]);
 ...
-    const [retval, currentLedger] = await Promise.all([
+    const [retval, currentLedger] = await Promise.all([
       simulateRead(op),
-      getCurrentLedgerSequence().catch(() => 0),
+      getCurrentLedgerSequenceSafe('getService'),
     ]);

Also applies to: 174-177

🤖 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/lib/contract.js` around lines 139 - 142, The
getCurrentLedgerSequence() call wrapped with .catch(() => 0) silently suppresses
RPC failures without logging, and provides no protection against hanging calls
that never reject, allowing Promise.all to stall indefinitely. Add logging to
the catch handler to record the error when it occurs, and wrap the
getCurrentLedgerSequence() call with a timeout mechanism to protect against
non-rejecting promises that hang. Apply this fix to both occurrences of the
pattern (around line 141 and line 176) to ensure both endpoints are protected
from stalling.

if (!retval) return [];

const vec = scValToNative(retval);
if (!Array.isArray(vec)) return [];

return vec.map((item) => ({
id: Number(item.id),
name: item.name,
description: item.description,
endpoint: item.endpoint,
price_usdc: item.price_usdc,
category: item.category,
provider: item.provider?.toString() ?? item.provider,
reputation: Number(item.reputation),
active: item.active,
registered_at: Number(item.registered_at),
}));
return vec.map((item) => {
const registeredAt = Number(item.registered_at);
return {
id: Number(item.id),
name: item.name,
description: item.description,
endpoint: item.endpoint,
price_usdc: item.price_usdc,
category: item.category,
provider: item.provider?.toString() ?? item.provider,
reputation: Number(item.reputation),
active: item.active,
registered_at: registeredAt,
ttl_warning: hasTtlWarning(registeredAt, currentLedger),
};
});
} catch (err) {
logger.error({ err }, 'listServices failed');
throw err;
Expand All @@ -155,9 +171,13 @@ export async function getService(id) {
try {
const contract = getContract();
const op = contract.call('get_service', nativeToScVal(BigInt(id), { type: 'u64' }));
const retval = await simulateRead(op);
const [retval, currentLedger] = await Promise.all([
simulateRead(op),
getCurrentLedgerSequence().catch(() => 0),
]);
if (!retval) return null;
const native = scValToNative(retval);
const registeredAt = Number(native.registered_at);
return {
id: Number(native.id),
name: native.name,
Expand All @@ -168,7 +188,8 @@ export async function getService(id) {
provider: native.provider?.toString() ?? native.provider,
reputation: Number(native.reputation),
active: native.active,
registered_at: Number(native.registered_at),
registered_at: registeredAt,
ttl_warning: hasTtlWarning(registeredAt, currentLedger),
};
} catch (err) {
logger.error({ err, id }, 'getService failed');
Expand Down
6 changes: 6 additions & 0 deletions backend/src/lib/stellar.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export function getUSDCContractId() {
return config.stellar.usdcContractId;
}

export async function getCurrentLedgerSequence() {
const server = getStellarServer();
const ledger = await server.getLatestLedger();
return ledger.sequence;
}

/**
* Check RPC server connectivity and contract reachability.
* Returns a health status object with connection and contract status.
Expand Down