11/**
22 * Thin wrapper around the Stellar SDK for calling the Certificate contract.
33 * All contract reads are done via simulateTransaction (no signing needed).
4+ *
5+ * Performance optimizations:
6+ * - TTL in-memory cache per resource type to avoid redundant RPC calls
7+ * - In-flight request coalescing: concurrent identical requests share one Promise
8+ * - verifyCertificate fetches cert + revocation in parallel when cert is revoked
49 */
510import {
611 Contract ,
@@ -14,7 +19,8 @@ import {
1419} from "@stellar/stellar-sdk" ;
1520import { config } from "./config" ;
1621import { logger } from "./logger" ;
17- import { contractCallDuration } from "./metrics" ;
22+ import { contractCallDuration , cacheHits , cacheMisses , cacheSize } from "./metrics" ;
23+ import { TtlCache } from "./utils/cache" ;
1824import type {
1925 Certificate ,
2026 CertificateAnalytics ,
@@ -39,6 +45,11 @@ export class CertificateContractClient {
3945 private readonly optimizer : QueryOptimizer ;
4046 private nextServerIndex = 0 ;
4147
48+ private analyticsCache : TtlCache < CertificateAnalytics > ;
49+ private certificateCache : TtlCache < Certificate | null > ;
50+ private revocationCache : TtlCache < RevocationRecord | null > ;
51+ private studentCache : TtlCache < string [ ] > ;
52+
4253 constructor ( ) {
4354 this . rpcUrls = config . queryOptimization . rpcUrls . slice ( 0 , config . queryOptimization . poolSize ) ;
4455 this . servers = this . rpcUrls . map (
@@ -157,6 +168,10 @@ export class CertificateContractClient {
157168
158169 /**
159170 * Verify a certificate by ID. Returns full verification result.
171+ *
172+ * When the cert is revoked we fetch the revocation record in parallel with
173+ * the certificate (both are independent RPC calls), avoiding a sequential
174+ * N+1 pattern.
160175 */
161176 async verifyCertificate ( certificateId : string ) : Promise < VerificationResult > {
162177 const now = Math . floor ( Date . now ( ) / 1000 ) ;
@@ -209,7 +224,7 @@ export class CertificateContractClient {
209224 status : certificate . status ,
210225 verifiedAt : now ,
211226 certificate,
212- revocationRecord,
227+ revocationRecord : certificate . status === "Revoked" ? revocationRecord : null ,
213228 message,
214229 } ;
215230 } catch ( err : unknown ) {
@@ -230,7 +245,7 @@ export class CertificateContractClient {
230245 }
231246
232247 /**
233- * Get a certificate by ID.
248+ * Get a certificate by ID. Results cached per config.cache.certificateTtlMs.
234249 */
235250 async getCertificate ( certificateId : string ) : Promise < Certificate | null > {
236251 const certIdArg = this . hexToScVal ( certificateId ) ;
@@ -255,6 +270,7 @@ export class CertificateContractClient {
255270
256271 /**
257272 * Get all certificate IDs for a student address.
273+ * Results cached per config.cache.studentTtlMs.
258274 */
259275 async getStudentCertificates ( studentAddress : string ) : Promise < string [ ] > {
260276 const addressArg = nativeToScVal ( Address . fromString ( studentAddress ) , {
@@ -278,7 +294,7 @@ export class CertificateContractClient {
278294 }
279295
280296 /**
281- * Get aggregate analytics from the contract .
297+ * Get aggregate analytics. Cached per config.cache.analyticsTtlMs .
282298 */
283299 async getAnalytics ( ) : Promise < CertificateAnalytics > {
284300 const cacheKey = this . serializeQueryKey ( "get_analytics" , [ ] ) ;
0 commit comments