@@ -2,17 +2,13 @@ import * as prom from 'prom-client';
22import { normalizeHashString } from '../../helpers' ;
33import { PgStore } from '../../datastore/pg-store' ;
44import { logger } from '../../logger' ;
5- import { sha256 } from '@hirosystems/api-toolkit' ;
5+ import {
6+ CACHE_CONTROL_MUST_REVALIDATE ,
7+ parseIfNoneMatchHeader ,
8+ sha256 ,
9+ } from '@hirosystems/api-toolkit' ;
610import { FastifyReply , FastifyRequest } from 'fastify' ;
711
8- /**
9- * A `Cache-Control` header used for re-validation based caching.
10- * * `public` == allow proxies/CDNs to cache as opposed to only local browsers.
11- * * `no-cache` == clients can cache a resource but should revalidate each time before using it.
12- * * `must-revalidate` == somewhat redundant directive to assert that cache must be revalidated, required by some CDNs
13- */
14- const CACHE_CONTROL_MUST_REVALIDATE = 'public, no-cache, must-revalidate' ;
15-
1612/**
1713 * Describes a key-value to be saved into a request's locals, representing the current
1814 * state of the chain depending on the type of information being requested by the endpoint.
@@ -25,6 +21,8 @@ enum ETagType {
2521 mempool = 'mempool' ,
2622 /** ETag based on the status of a single transaction across the mempool or canonical chain. */
2723 transaction = 'transaction' ,
24+ /** Etag based on the confirmed balance of a single principal (STX address or contract id) */
25+ principal = 'principal' ,
2826}
2927
3028/** Value that means the ETag did get calculated but it is empty. */
@@ -75,52 +73,6 @@ function getETagMetrics(): ETagCacheMetrics {
7573 return _eTagMetrics ;
7674}
7775
78- /**
79- * Parses the etag values from a raw `If-None-Match` request header value.
80- * The wrapping double quotes (if any) and validation prefix (if any) are stripped.
81- * The parsing is permissive to account for commonly non-spec-compliant clients, proxies, CDNs, etc.
82- * E.g. the value:
83- * ```js
84- * `"a", W/"b", c,d, "e", "f"`
85- * ```
86- * Would be parsed and returned as:
87- * ```js
88- * ['a', 'b', 'c', 'd', 'e', 'f']
89- * ```
90- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#syntax
91- * ```
92- * If-None-Match: "etag_value"
93- * If-None-Match: "etag_value", "etag_value", ...
94- * If-None-Match: *
95- * ```
96- * @param ifNoneMatchHeaderValue - raw header value
97- * @returns an array of etag values
98- */
99- export function parseIfNoneMatchHeader (
100- ifNoneMatchHeaderValue : string | undefined
101- ) : string [ ] | undefined {
102- if ( ! ifNoneMatchHeaderValue ) {
103- return undefined ;
104- }
105- // Strip wrapping double quotes like `"hello"` and the ETag validation-prefix like `W/"hello"`.
106- // The API returns compliant, strong-validation ETags (double quoted ASCII), but can't control what
107- // clients, proxies, CDNs, etc may provide.
108- const normalized = / ^ (?: " | W \/ " ) ? ( .* ?) " ? $ / gi. exec ( ifNoneMatchHeaderValue . trim ( ) ) ?. [ 1 ] ;
109- if ( ! normalized ) {
110- // This should never happen unless handling a buggy request with something like `If-None-Match: ""`,
111- // or if there's a flaw in the above code. Log warning for now.
112- logger . warn ( `Normalized If-None-Match header is falsy: ${ ifNoneMatchHeaderValue } ` ) ;
113- return undefined ;
114- } else if ( normalized . includes ( ',' ) ) {
115- // Multiple etag values provided, likely irrelevant extra values added by a proxy/CDN.
116- // Split on comma, also stripping quotes, weak-validation prefixes, and extra whitespace.
117- return normalized . split ( / (?: W \/ " | " ) ? (?: \s * ) , (?: \s * ) (?: W \/ " | " ) ? / gi) ;
118- } else {
119- // Single value provided (the typical case)
120- return [ normalized ] ;
121- }
122- }
123-
12476async function calculateETag (
12577 db : PgStore ,
12678 etagType : ETagType ,
@@ -155,7 +107,7 @@ async function calculateETag(
155107 }
156108 return digest . result . digest ;
157109 } catch ( error ) {
158- logger . error ( error , 'Unable to calculate mempool' ) ;
110+ logger . error ( error , 'Unable to calculate mempool etag ' ) ;
159111 return ;
160112 }
161113
@@ -178,7 +130,20 @@ async function calculateETag(
178130 ] ;
179131 return sha256 ( elements . join ( ':' ) ) ;
180132 } catch ( error ) {
181- logger . error ( error , 'Unable to calculate transaction' ) ;
133+ logger . error ( error , 'Unable to calculate transaction etag' ) ;
134+ return ;
135+ }
136+
137+ case ETagType . principal :
138+ try {
139+ const params = req . params as { address ?: string ; principal ?: string } ;
140+ const principal = params . address ?? params . principal ;
141+ if ( ! principal ) return ETAG_EMPTY ;
142+ const activity = await db . getPrincipalLastActivityTxIds ( principal ) ;
143+ const text = `${ activity . stx_tx_id } :${ activity . ft_tx_id } :${ activity . nft_tx_id } ` ;
144+ return sha256 ( text ) ;
145+ } catch ( error ) {
146+ logger . error ( error , 'Unable to calculate principal etag' ) ;
182147 return ;
183148 }
184149 }
@@ -224,3 +189,7 @@ export async function handleMempoolCache(request: FastifyRequest, reply: Fastify
224189export async function handleTransactionCache ( request : FastifyRequest , reply : FastifyReply ) {
225190 return handleCache ( ETagType . transaction , request , reply ) ;
226191}
192+
193+ export async function handlePrincipalCache ( request : FastifyRequest , reply : FastifyReply ) {
194+ return handleCache ( ETagType . principal , request , reply ) ;
195+ }
0 commit comments