From 15844994845076df957ed6846eccb4484045bfb4 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Fri, 28 Jun 2024 13:46:54 -0600 Subject: [PATCH] feat: add cache to api endpoints --- api/package-lock.json | 12 +-- api/package.json | 2 +- api/src/api/init.ts | 33 +------ api/src/api/routes/address.ts | 9 +- api/src/api/routes/etchings.ts | 21 ++-- api/src/api/schemas.ts | 25 ++--- api/src/api/util/cache.ts | 153 +++--------------------------- api/src/api/util/helpers.ts | 2 + api/src/index.ts | 2 +- api/src/pg/pg-store.ts | 7 ++ api/src/pg/types.ts | 2 + migrations/V1__runes.sql | 10 +- migrations/V2__ledger.sql | 1 + src/db/cache/index_cache.rs | 13 ++- src/db/cache/transaction_cache.rs | 18 ++++ src/db/index.rs | 3 +- src/db/mod.rs | 13 ++- src/db/models/db_ledger_entry.rs | 4 + src/db/models/db_rune.rs | 7 ++ src/db/types/pg_numeric_u128.rs | 2 +- 20 files changed, 118 insertions(+), 221 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index 080d6f3..d307902 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -14,7 +14,7 @@ "@fastify/multipart": "^7.1.0", "@fastify/swagger": "^8.3.1", "@fastify/type-provider-typebox": "3.2.0", - "@hirosystems/api-toolkit": "^1.5.0", + "@hirosystems/api-toolkit": "^1.6.0", "@types/node": "^18.13.0", "bignumber.js": "^9.1.2", "env-schema": "^5.2.1", @@ -1346,13 +1346,14 @@ } }, "node_modules/@hirosystems/api-toolkit": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@hirosystems/api-toolkit/-/api-toolkit-1.5.0.tgz", - "integrity": "sha512-f7rL2Bct+tW5gtYEZwCFQYQnkEIgGH+yoBYe807c+/gYItfWa9bPdY8KAFo+5AD1TbvP1bECrUClhK2TCCc1tA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@hirosystems/api-toolkit/-/api-toolkit-1.6.0.tgz", + "integrity": "sha512-59IMo4lsq3ASvXKeL5gVXyaMlvblOANDQaxi4lDAja/gbVsNp4sPyrVvGEhNU17zJEiaKVf54AKCyd/ywwjFng==", "dependencies": { "@fastify/cors": "^8.0.0", "@fastify/swagger": "^8.3.1", "@fastify/type-provider-typebox": "^3.2.0", + "@sinclair/typebox": "^0.28.20", "fastify": "^4.3.0", "fastify-metrics": "^10.2.0", "node-pg-migrate": "^6.2.2", @@ -3371,8 +3372,7 @@ "node_modules/@sinclair/typebox": { "version": "0.28.20", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.28.20.tgz", - "integrity": "sha512-QCF3BGfacwD+3CKhGsMeixnwOmX4AWgm61nKkNdRStyLVu0mpVFYlDSY8gVBOOED1oSwzbJauIWl/+REj8K5+w==", - "peer": true + "integrity": "sha512-QCF3BGfacwD+3CKhGsMeixnwOmX4AWgm61nKkNdRStyLVu0mpVFYlDSY8gVBOOED1oSwzbJauIWl/+REj8K5+w==" }, "node_modules/@sindresorhus/is": { "version": "4.6.0", diff --git a/api/package.json b/api/package.json index 07828b2..43ab750 100644 --- a/api/package.json +++ b/api/package.json @@ -48,7 +48,7 @@ "@fastify/multipart": "^7.1.0", "@fastify/swagger": "^8.3.1", "@fastify/type-provider-typebox": "3.2.0", - "@hirosystems/api-toolkit": "^1.5.0", + "@hirosystems/api-toolkit": "^1.6.0", "@types/node": "^18.13.0", "bignumber.js": "^9.1.2", "env-schema": "^5.2.1", diff --git a/api/src/api/init.ts b/api/src/api/init.ts index 1f7861c..3c9d182 100644 --- a/api/src/api/init.ts +++ b/api/src/api/init.ts @@ -1,8 +1,6 @@ -import FastifyCors from '@fastify/cors'; import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; -import { PINO_LOGGER_CONFIG, isProdEnv } from '@hirosystems/api-toolkit'; -import Fastify, { FastifyPluginAsync } from 'fastify'; -import FastifyMetrics, { IFastifyMetrics } from 'fastify-metrics'; +import { buildFastifyApiServer } from '@hirosystems/api-toolkit'; +import { FastifyPluginAsync } from 'fastify'; import { Server } from 'http'; import { PgStore } from '../pg/pg-store'; import { EtchingRoutes } from './routes/etchings'; @@ -18,36 +16,11 @@ export const Api: FastifyPluginAsync< }; export async function buildApiServer(args: { db: PgStore }) { - const fastify = Fastify({ - trustProxy: true, - logger: PINO_LOGGER_CONFIG, - }).withTypeProvider(); + const fastify = await buildFastifyApiServer(); fastify.decorate('db', args.db); - if (isProdEnv) { - await fastify.register(FastifyMetrics, { endpoint: null }); - } - await fastify.register(FastifyCors); await fastify.register(Api, { prefix: '/runes/v1' }); await fastify.register(Api, { prefix: '/runes' }); return fastify; } - -export async function buildPromServer(args: { metrics: IFastifyMetrics }) { - const promServer = Fastify({ - trustProxy: true, - logger: PINO_LOGGER_CONFIG, - }); - - promServer.route({ - url: '/metrics', - method: 'GET', - logLevel: 'info', - handler: async (_, reply) => { - await reply.type('text/plain').send(await args.metrics.client.register.metrics()); - }, - }); - - return promServer; -} diff --git a/api/src/api/routes/address.ts b/api/src/api/routes/address.ts index 4da12a3..6683ca6 100644 --- a/api/src/api/routes/address.ts +++ b/api/src/api/routes/address.ts @@ -6,17 +6,18 @@ import { AddressParamSchema, LimitParamSchema, OffsetParamSchema, - PaginatedResponse, BalanceResponseSchema, } from '../schemas'; import { parseBalanceResponse } from '../util/helpers'; +import { Optional, PaginatedResponse } from '@hirosystems/api-toolkit'; +import { handleCache } from '../util/cache'; export const AddressRoutes: FastifyPluginCallback< Record, Server, TypeBoxTypeProvider > = (fastify, options, done) => { - // fastify.addHook('preHandler', handleInscriptionTransfersCache); + fastify.addHook('preHandler', handleCache); fastify.get( '/address/:address/balances', @@ -30,8 +31,8 @@ export const AddressRoutes: FastifyPluginCallback< address: AddressParamSchema, }), querystring: Type.Object({ - offset: Type.Optional(OffsetParamSchema), - limit: Type.Optional(LimitParamSchema), + offset: Optional(OffsetParamSchema), + limit: Optional(LimitParamSchema), }), response: { 200: PaginatedResponse(BalanceResponseSchema, 'Paginated balances response'), diff --git a/api/src/api/routes/etchings.ts b/api/src/api/routes/etchings.ts index 57fc37a..36bfad8 100644 --- a/api/src/api/routes/etchings.ts +++ b/api/src/api/routes/etchings.ts @@ -10,7 +10,6 @@ import { LimitParamSchema, NotFoundResponse, OffsetParamSchema, - PaginatedResponse, SimpleBalanceResponseSchema, SimpleActivityResponseSchema, } from '../schemas'; @@ -19,13 +18,15 @@ import { parseEtchingActivityResponse, parseEtchingResponse, } from '../util/helpers'; +import { Optional, PaginatedResponse } from '@hirosystems/api-toolkit'; +import { handleCache } from '../util/cache'; export const EtchingRoutes: FastifyPluginCallback< Record, Server, TypeBoxTypeProvider > = (fastify, options, done) => { - // fastify.addHook('preHandler', handleInscriptionTransfersCache); + fastify.addHook('preHandler', handleCache); fastify.get( '/etchings', @@ -36,8 +37,8 @@ export const EtchingRoutes: FastifyPluginCallback< description: 'Retrieves a paginated list of rune etchings', tags: ['Runes'], querystring: Type.Object({ - offset: Type.Optional(OffsetParamSchema), - limit: Type.Optional(LimitParamSchema), + offset: Optional(OffsetParamSchema), + limit: Optional(LimitParamSchema), }), response: { 200: PaginatedResponse(EtchingResponseSchema, 'Paginated etchings response'), @@ -96,8 +97,8 @@ export const EtchingRoutes: FastifyPluginCallback< etching: EtchingParamSchema, }), querystring: Type.Object({ - offset: Type.Optional(OffsetParamSchema), - limit: Type.Optional(LimitParamSchema), + offset: Optional(OffsetParamSchema), + limit: Optional(LimitParamSchema), }), response: { 200: PaginatedResponse(SimpleActivityResponseSchema, 'Paginated activity response'), @@ -130,8 +131,8 @@ export const EtchingRoutes: FastifyPluginCallback< address: AddressParamSchema, }), querystring: Type.Object({ - offset: Type.Optional(OffsetParamSchema), - limit: Type.Optional(LimitParamSchema), + offset: Optional(OffsetParamSchema), + limit: Optional(LimitParamSchema), }), response: { 200: PaginatedResponse(SimpleActivityResponseSchema, 'Paginated activity response'), @@ -168,8 +169,8 @@ export const EtchingRoutes: FastifyPluginCallback< etching: EtchingParamSchema, }), querystring: Type.Object({ - offset: Type.Optional(OffsetParamSchema), - limit: Type.Optional(LimitParamSchema), + offset: Optional(OffsetParamSchema), + limit: Optional(LimitParamSchema), }), response: { 200: PaginatedResponse(SimpleBalanceResponseSchema, 'Paginated holders response'), diff --git a/api/src/api/schemas.ts b/api/src/api/schemas.ts index 5f5cf85..0e984b0 100644 --- a/api/src/api/schemas.ts +++ b/api/src/api/schemas.ts @@ -1,5 +1,5 @@ import { SwaggerOptions } from '@fastify/swagger'; -import { SERVER_VERSION } from '@hirosystems/api-toolkit'; +import { Nullable, Optional, SERVER_VERSION } from '@hirosystems/api-toolkit'; import { Static, TSchema, Type } from '@sinclair/typebox'; import { TypeCompiler } from '@sinclair/typebox/compiler'; @@ -29,8 +29,6 @@ export const OpenApiSchemaOptions: SwaggerOptions = { }, }; -const Nullable = (type: T) => Type.Union([type, Type.Null()]); - // ========================== // Parameters // ========================== @@ -66,22 +64,14 @@ export type AddressParam = Static; // Responses // ========================== -export const PaginatedResponse = (type: T, title: string) => - Type.Object( - { - limit: Type.Integer({ examples: [20] }), - offset: Type.Integer({ examples: [0] }), - total: Type.Integer({ examples: [1] }), - results: Type.Array(type), - }, - { title } - ); - export const EtchingResponseSchema = Type.Object({ id: Type.String({ examples: ['840000:1'] }), name: Type.String({ examples: ['ZZZZZFEHUZZZZZ'] }), spaced_name: Type.String({ examples: ['Z•Z•Z•Z•Z•FEHU•Z•Z•Z•Z•Z'] }), number: Type.Integer({ examples: [1] }), + block_hash: Type.String({ + examples: ['00000000000000000000c9787573a1f1775a2b56b403a2d0c7957e9a5bc754bb'], + }), block_height: Type.Integer({ examples: [840000] }), tx_index: Type.Integer({ examples: [1] }), tx_id: Type.String({ @@ -116,6 +106,9 @@ const RuneDetailResponseSchema = Type.Object({ }); export const SimpleActivityResponseSchema = Type.Object({ + block_hash: Type.String({ + examples: ['00000000000000000000c9787573a1f1775a2b56b403a2d0c7957e9a5bc754bb'], + }), block_height: Type.Integer({ examples: [840000] }), tx_index: Type.Integer({ examples: [1] }), tx_id: Type.String({ @@ -125,7 +118,7 @@ export const SimpleActivityResponseSchema = Type.Object({ output: Type.String({ examples: ['2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e:100'], }), - address: Type.Optional(Type.String({ examples: ['bc1q7jd477wc5s88hsvenr0ddtatsw282hfjzg59wz'] })), + address: Optional(Type.String({ examples: ['bc1q7jd477wc5s88hsvenr0ddtatsw282hfjzg59wz'] })), receiver_address: Type.Optional( Type.String({ examples: ['bc1pgdrveee2v4ez95szaakw5gkd8eennv2dddf9rjdrlt6ch56lzrrsxgvazt'] }) ), @@ -147,7 +140,7 @@ export const ActivityResponseSchema = Type.Intersect([ export type ActivityResponse = Static; export const SimpleBalanceResponseSchema = Type.Object({ - address: Type.Optional(Type.String({ examples: ['bc1q7jd477wc5s88hsvenr0ddtatsw282hfjzg59wz'] })), + address: Optional(Type.String({ examples: ['bc1q7jd477wc5s88hsvenr0ddtatsw282hfjzg59wz'] })), balance: Type.String({ examples: ['11000000000'] }), }); export type SimpleBalanceResponse = Static; diff --git a/api/src/api/util/cache.ts b/api/src/api/util/cache.ts index 224244d..d6fbfdc 100644 --- a/api/src/api/util/cache.ts +++ b/api/src/api/util/cache.ts @@ -1,139 +1,14 @@ -// import { FastifyReply, FastifyRequest } from 'fastify'; -// import { InscriptionIdParamCType, InscriptionNumberParamCType } from '../schemas'; -// import { logger } from '@hirosystems/api-toolkit'; - -// enum ETagType { -// inscriptionsIndex, -// inscription, -// inscriptionsPerBlock, -// } - -// /** -// * A `Cache-Control` header used for re-validation based caching. -// * * `public` == allow proxies/CDNs to cache as opposed to only local browsers. -// * * `no-cache` == clients can cache a resource but should revalidate each time before using it. -// * * `must-revalidate` == somewhat redundant directive to assert that cache must be revalidated, required by some CDNs -// */ -// const CACHE_CONTROL_MUST_REVALIDATE = 'public, no-cache, must-revalidate'; - -// export async function handleInscriptionCache(request: FastifyRequest, reply: FastifyReply) { -// return handleCache(ETagType.inscription, request, reply); -// } - -// export async function handleInscriptionTransfersCache( -// request: FastifyRequest, -// reply: FastifyReply -// ) { -// return handleCache(ETagType.inscriptionsIndex, request, reply); -// } - -// export async function handleInscriptionsPerBlockCache( -// request: FastifyRequest, -// reply: FastifyReply -// ) { -// return handleCache(ETagType.inscriptionsPerBlock, request, reply); -// } - -// async function handleCache(type: ETagType, request: FastifyRequest, reply: FastifyReply) { -// const ifNoneMatch = parseIfNoneMatchHeader(request.headers['if-none-match']); -// let etag: string | undefined; -// switch (type) { -// case ETagType.inscription: -// etag = await getInscriptionLocationEtag(request); -// break; -// case ETagType.inscriptionsIndex: -// etag = await getInscriptionsIndexEtag(request); -// break; -// case ETagType.inscriptionsPerBlock: -// etag = await request.server.db.getInscriptionsPerBlockETag(); -// break; -// } -// if (etag) { -// if (ifNoneMatch && ifNoneMatch.includes(etag)) { -// await reply.header('Cache-Control', CACHE_CONTROL_MUST_REVALIDATE).code(304).send(); -// } else { -// void reply.headers({ 'Cache-Control': CACHE_CONTROL_MUST_REVALIDATE, ETag: `"${etag}"` }); -// } -// } -// } - -// /** -// * Retrieve the inscriptions's location timestamp as a UNIX epoch so we can use it as the response -// * ETag. -// * @param request - Fastify request -// * @returns Etag string -// */ -// async function getInscriptionLocationEtag(request: FastifyRequest): Promise { -// try { -// const components = request.url.split('/'); -// do { -// const lastElement = components.pop(); -// if (lastElement && lastElement.length) { -// if (InscriptionIdParamCType.Check(lastElement)) { -// return await request.server.db.getInscriptionETag({ genesis_id: lastElement }); -// } else if (InscriptionNumberParamCType.Check(parseInt(lastElement))) { -// return await request.server.db.getInscriptionETag({ number: lastElement }); -// } -// } -// } while (components.length); -// } catch (error) { -// return; -// } -// } - -// /** -// * Get an ETag based on the last state of all inscriptions. -// * @param request - Fastify request -// * @returns ETag string -// */ -// async function getInscriptionsIndexEtag(request: FastifyRequest): Promise { -// try { -// return await request.server.db.getInscriptionsIndexETag(); -// } catch (error) { -// return; -// } -// } - -// /** -// * Parses the etag values from a raw `If-None-Match` request header value. -// * The wrapping double quotes (if any) and validation prefix (if any) are stripped. -// * The parsing is permissive to account for commonly non-spec-compliant clients, proxies, CDNs, etc. -// * E.g. the value: -// * ```js -// * `"a", W/"b", c,d, "e", "f"` -// * ``` -// * Would be parsed and returned as: -// * ```js -// * ['a', 'b', 'c', 'd', 'e', 'f'] -// * ``` -// * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#syntax -// * ``` -// * If-None-Match: "etag_value" -// * If-None-Match: "etag_value", "etag_value", ... -// * If-None-Match: * -// * ``` -// * @param ifNoneMatchHeaderValue - raw header value -// * @returns an array of etag values -// */ -// function parseIfNoneMatchHeader(ifNoneMatchHeaderValue: string | undefined): string[] | undefined { -// if (!ifNoneMatchHeaderValue) { -// return undefined; -// } -// // Strip wrapping double quotes like `"hello"` and the ETag validation-prefix like `W/"hello"`. -// // The API returns compliant, strong-validation ETags (double quoted ASCII), but can't control what -// // clients, proxies, CDNs, etc may provide. -// const normalized = /^(?:"|W\/")?(.*?)"?$/gi.exec(ifNoneMatchHeaderValue.trim())?.[1]; -// if (!normalized) { -// // This should never happen unless handling a buggy request with something like `If-None-Match: ""`, -// // or if there's a flaw in the above code. Log warning for now. -// logger.warn(`Normalized If-None-Match header is falsy: ${ifNoneMatchHeaderValue}`); -// return undefined; -// } else if (normalized.includes(',')) { -// // Multiple etag values provided, likely irrelevant extra values added by a proxy/CDN. -// // Split on comma, also stripping quotes, weak-validation prefixes, and extra whitespace. -// return normalized.split(/(?:W\/"|")?(?:\s*),(?:\s*)(?:W\/"|")?/gi); -// } else { -// // Single value provided (the typical case) -// return [normalized]; -// } -// } +import { CACHE_CONTROL_MUST_REVALIDATE, parseIfNoneMatchHeader } from '@hirosystems/api-toolkit'; +import { FastifyReply, FastifyRequest } from 'fastify'; + +export async function handleCache(request: FastifyRequest, reply: FastifyReply) { + const ifNoneMatch = parseIfNoneMatchHeader(request.headers['if-none-match']); + const etag = await request.server.db.getChainTipEtag(); + if (etag) { + if (ifNoneMatch && ifNoneMatch.includes(etag)) { + await reply.header('Cache-Control', CACHE_CONTROL_MUST_REVALIDATE).code(304).send(); + } else { + void reply.headers({ 'Cache-Control': CACHE_CONTROL_MUST_REVALIDATE, ETag: `"${etag}"` }); + } + } +} diff --git a/api/src/api/util/helpers.ts b/api/src/api/util/helpers.ts index e46d4b3..257520d 100644 --- a/api/src/api/util/helpers.ts +++ b/api/src/api/util/helpers.ts @@ -12,6 +12,7 @@ export function parseEtchingResponse(rune: DbRune): EtchingResponse { number: rune.number, name: rune.name, spaced_name: rune.spaced_name, + block_hash: rune.block_hash, block_height: rune.block_height, tx_index: rune.tx_index, tx_id: rune.tx_id, @@ -44,6 +45,7 @@ export function parseEtchingActivityResponse( name: entry.name, spaced_name: entry.spaced_name, }, + block_hash: entry.block_hash, block_height: entry.block_height, tx_index: entry.tx_index, tx_id: entry.tx_id, diff --git a/api/src/index.ts b/api/src/index.ts index 3b22938..33592ba 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -1,5 +1,5 @@ import { isProdEnv, logger, registerShutdownConfig } from '@hirosystems/api-toolkit'; -import { buildApiServer, buildPromServer } from './api/init'; +import { buildApiServer } from './api/init'; import { ENV } from './env'; import { PgStore } from './pg/pg-store'; // import { ApiMetrics } from './metrics/metrics'; diff --git a/api/src/pg/pg-store.ts b/api/src/pg/pg-store.ts index 9e504c0..8bd1255 100644 --- a/api/src/pg/pg-store.ts +++ b/api/src/pg/pg-store.ts @@ -60,6 +60,13 @@ export class PgStore extends BasePgStore { super(sql); } + async getChainTipEtag(): Promise { + const result = await this.sql<{ etag: string }[]>` + SELECT block_hash AS etag FROM ledger ORDER BY block_height DESC LIMIT 1 + `; + return result[0]?.etag; + } + async getEtching(id: EtchingParam): Promise { const result = await this.sql` SELECT * FROM runes WHERE ${getEtchingIdWhereCondition(this.sql, id)} diff --git a/api/src/pg/types.ts b/api/src/pg/types.ts index 7718985..299667c 100644 --- a/api/src/pg/types.ts +++ b/api/src/pg/types.ts @@ -10,6 +10,7 @@ export type DbRune = { number: number; name: string; spaced_name: string; + block_hash: string; block_height: number; tx_index: number; tx_id: string; @@ -35,6 +36,7 @@ type DbLedgerOperation = 'mint' | 'burn' | 'send' | 'receive'; export type DbLedgerEntry = { rune_id: string; + block_hash: string; block_height: number; tx_index: number; tx_id: string; diff --git a/migrations/V1__runes.sql b/migrations/V1__runes.sql index 6a1164a..1a7f88f 100644 --- a/migrations/V1__runes.sql +++ b/migrations/V1__runes.sql @@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS runes ( number BIGINT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE, spaced_name TEXT NOT NULL, + block_hash TEXT NOT NULL, block_height NUMERIC NOT NULL, tx_index BIGINT NOT NULL, tx_id TEXT NOT NULL, @@ -27,10 +28,11 @@ CREATE INDEX runes_block_height_tx_index_index ON runes (block_height, tx_index) -- Insert default 'UNCOMMON•GOODS' INSERT INTO runes ( - id, number, name, spaced_name, block_height, tx_index, tx_id, symbol, terms_amount, terms_cap, - terms_height_start, terms_height_end, timestamp + id, number, name, spaced_name, block_hash, block_height, tx_index, tx_id, symbol, terms_amount, + terms_cap, terms_height_start, terms_height_end, timestamp ) VALUES ( - '1:0', 0, 'UNCOMMONGOODS', 'UNCOMMON•GOODS', 840000, 0, '', '⧉', 1, '340282366920938463463374607431768211455', 840000, - 1050000, 0 + '1:0', 0, 'UNCOMMONGOODS', 'UNCOMMON•GOODS', + '0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5', 840000, 0, '', '⧉', 1, + '340282366920938463463374607431768211455', 840000, 1050000, 0 ); diff --git a/migrations/V2__ledger.sql b/migrations/V2__ledger.sql index 003b349..8c94972 100644 --- a/migrations/V2__ledger.sql +++ b/migrations/V2__ledger.sql @@ -2,6 +2,7 @@ CREATE TYPE ledger_operation AS ENUM ('mint', 'burn', 'send', 'receive'); CREATE TABLE IF NOT EXISTS ledger ( rune_id TEXT NOT NULL, + block_hash TEXT NOT NULL, block_height NUMERIC NOT NULL, tx_index BIGINT NOT NULL, event_index BIGINT NOT NULL, diff --git a/src/db/cache/index_cache.rs b/src/db/cache/index_cache.rs index 2017d94..a683b39 100644 --- a/src/db/cache/index_cache.rs +++ b/src/db/cache/index_cache.rs @@ -50,7 +50,7 @@ impl IndexCache { next_rune_number: max_rune_number + 1, rune_cache: LruCache::new(cap), output_cache: LruCache::new(cap), - tx_cache: TransactionCache::new(network, 1, 0, &"".to_string(), 0), + tx_cache: TransactionCache::new(network, &"".to_string(), 1, 0, &"".to_string(), 0), db_cache: DbCache::new(), } } @@ -58,13 +58,20 @@ impl IndexCache { /// Creates a fresh transaction index cache. pub async fn begin_transaction( &mut self, + block_hash: &String, block_height: u64, tx_index: u32, tx_id: &String, timestamp: u32, ) { - self.tx_cache = - TransactionCache::new(self.network, block_height, tx_index, tx_id, timestamp); + self.tx_cache = TransactionCache::new( + self.network, + block_hash, + block_height, + tx_index, + tx_id, + timestamp, + ); } /// Finalizes the current transaction index cache. diff --git a/src/db/cache/transaction_cache.rs b/src/db/cache/transaction_cache.rs index 0ad3ad2..e5a7453 100644 --- a/src/db/cache/transaction_cache.rs +++ b/src/db/cache/transaction_cache.rs @@ -22,6 +22,7 @@ pub struct InputRuneBalance { /// Holds cached data relevant to a single transaction during indexing. pub struct TransactionCache { network: Network, + pub block_hash: String, pub block_height: u64, pub tx_index: u32, pub tx_id: String, @@ -44,6 +45,7 @@ pub struct TransactionCache { impl TransactionCache { pub fn new( network: Network, + block_hash: &String, block_height: u64, tx_index: u32, tx_id: &String, @@ -51,6 +53,7 @@ impl TransactionCache { ) -> Self { TransactionCache { network, + block_hash: block_hash.clone(), block_height, tx_index, next_event_index: 0, @@ -121,6 +124,7 @@ impl TransactionCache { results.push(new_ledger_entry( balance.amount, *rune_id, + &self.block_hash, self.block_height, self.tx_index, &self.tx_id, @@ -144,6 +148,7 @@ impl TransactionCache { for (rune_id, unallocated) in self.input_runes.iter_mut() { results.extend(move_rune_balance_to_output( self.network, + &self.block_hash, self.block_height, &self.tx_id, self.tx_index, @@ -169,6 +174,7 @@ impl TransactionCache { let db_rune = DbRune::from_etching( etching, number, + &self.block_hash, self.block_height, self.tx_index, &self.tx_id, @@ -197,6 +203,7 @@ impl TransactionCache { let db_rune = DbRune::from_cenotaph_etching( rune, number, + &self.block_hash, self.block_height, self.tx_index, &self.tx_id, @@ -219,6 +226,7 @@ impl TransactionCache { new_ledger_entry( mint_amount.0, rune_id.clone(), + &self.block_hash, self.block_height, self.tx_index, &self.tx_id, @@ -238,6 +246,7 @@ impl TransactionCache { new_ledger_entry( mint_amount.0, rune_id.clone(), + &self.block_hash, self.block_height, self.tx_index, &self.tx_id, @@ -288,6 +297,7 @@ impl TransactionCache { ); results.extend(move_rune_balance_to_output( self.network, + &self.block_hash, self.block_height, &self.tx_id, self.tx_index, @@ -322,6 +332,7 @@ impl TransactionCache { } results.extend(move_rune_balance_to_output( self.network, + &self.block_hash, self.block_height, &self.tx_id, self.tx_index, @@ -341,6 +352,7 @@ impl TransactionCache { let amount = edict.amount.min(unallocated); results.extend(move_rune_balance_to_output( self.network, + &self.block_hash, self.block_height, &self.tx_id, self.tx_index, @@ -364,6 +376,7 @@ impl TransactionCache { } results.extend(move_rune_balance_to_output( self.network, + &self.block_hash, self.block_height, &self.tx_id, self.tx_index, @@ -406,6 +419,7 @@ impl TransactionCache { fn new_ledger_entry( amount: u128, rune_id: RuneId, + block_hash: &String, block_height: u64, tx_index: u32, tx_id: &String, @@ -419,6 +433,7 @@ fn new_ledger_entry( let entry = DbLedgerEntry::from_values( amount, rune_id, + block_hash, block_height, tx_index, *next_event_index, @@ -438,6 +453,7 @@ fn new_ledger_entry( /// transferred. If `output` is `None`, the runes will be burnt. fn move_rune_balance_to_output( network: Network, + block_hash: &String, block_height: u64, tx_id: &String, tx_index: u32, @@ -497,6 +513,7 @@ fn move_rune_balance_to_output( results.push(new_ledger_entry( balance_taken, *rune_id, + block_hash, block_height, tx_index, tx_id, @@ -527,6 +544,7 @@ fn move_rune_balance_to_output( results.push(new_ledger_entry( total_sent, *rune_id, + block_hash, block_height, tx_index, &tx_id, diff --git a/src/db/index.rs b/src/db/index.rs index 6e17f3f..c1f5118 100644 --- a/src/db/index.rs +++ b/src/db/index.rs @@ -68,6 +68,7 @@ pub async fn index_block( ctx: &Context, ) { let stopwatch = std::time::Instant::now(); + let block_hash = &block.block_identifier.hash; let block_height = block.block_identifier.index; info!(ctx.expect_logger(), "Indexing block {}...", block_height); let mut db_tx = pg_client @@ -79,7 +80,7 @@ pub async fn index_block( let tx_index = tx.metadata.index; let tx_id = &tx.transaction_identifier.hash; index_cache - .begin_transaction(block_height, tx_index, tx_id, block.timestamp) + .begin_transaction(block_hash, block_height, tx_index, tx_id, block.timestamp) .await; if let Some(artifact) = Runestone::decipher(&transaction) { match artifact { diff --git a/src/db/mod.rs b/src/db/mod.rs index f2813a6..54f6fdb 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -99,9 +99,9 @@ pub async fn pg_insert_rune_rows( ) -> Result { let stmt = db_tx.prepare( "INSERT INTO runes - (id, number, name, spaced_name, block_height, tx_index, tx_id, divisibility, premine, symbol, terms_amount, terms_cap, - terms_height_start, terms_height_end, terms_offset_start, terms_offset_end, turbo, timestamp) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) + (id, number, name, spaced_name, block_hash, block_height, tx_index, tx_id, divisibility, premine, symbol, terms_amount, + terms_cap, terms_height_start, terms_height_end, terms_offset_start, terms_offset_end, turbo, timestamp) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) ON CONFLICT (name) DO NOTHING" ).await.expect("Unable to prepare statement"); for row in rows.iter() { @@ -113,6 +113,7 @@ pub async fn pg_insert_rune_rows( &row.number, &row.name, &row.spaced_name, + &row.block_hash, &row.block_height, &row.tx_index, &row.tx_id, @@ -243,8 +244,9 @@ pub async fn pg_insert_ledger_entries( let stmt = db_tx .prepare( "INSERT INTO ledger - (rune_id, block_height, tx_index, event_index, tx_id, output, address, receiver_address, amount, operation, timestamp) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", + (rune_id, block_hash, block_height, tx_index, event_index, tx_id, output, address, receiver_address, amount, operation, + timestamp) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", ) .await .expect("Unable to prepare statement"); @@ -254,6 +256,7 @@ pub async fn pg_insert_ledger_entries( &stmt, &[ &row.rune_id, + &row.block_hash, &row.block_height, &row.tx_index, &row.event_index, diff --git a/src/db/models/db_ledger_entry.rs b/src/db/models/db_ledger_entry.rs index 6986918..917b8d7 100644 --- a/src/db/models/db_ledger_entry.rs +++ b/src/db/models/db_ledger_entry.rs @@ -11,6 +11,7 @@ use super::db_ledger_operation::DbLedgerOperation; #[derive(Debug, Clone)] pub struct DbLedgerEntry { pub rune_id: String, + pub block_hash: String, pub block_height: PgNumericU64, pub tx_index: PgBigIntU32, pub event_index: PgBigIntU32, @@ -27,6 +28,7 @@ impl DbLedgerEntry { pub fn from_values( amount: u128, rune_id: RuneId, + block_hash: &String, block_height: u64, tx_index: u32, event_index: u32, @@ -39,6 +41,7 @@ impl DbLedgerEntry { ) -> Self { DbLedgerEntry { rune_id: rune_id.to_string(), + block_hash: block_hash[2..].to_string(), block_height: PgNumericU64(block_height), tx_index: PgBigIntU32(tx_index), event_index: PgBigIntU32(event_index), @@ -55,6 +58,7 @@ impl DbLedgerEntry { pub fn from_pg_row(row: &Row) -> Self { DbLedgerEntry { rune_id: row.get("rune_id"), + block_hash: row.get("block_hash"), block_height: row.get("block_height"), tx_index: row.get("tx_index"), event_index: row.get("event_index"), diff --git a/src/db/models/db_rune.rs b/src/db/models/db_rune.rs index 0c17044..4db44ef 100644 --- a/src/db/models/db_rune.rs +++ b/src/db/models/db_rune.rs @@ -13,6 +13,7 @@ pub struct DbRune { pub number: PgBigIntU32, pub name: String, pub spaced_name: String, + pub block_hash: String, pub block_height: PgNumericU64, pub tx_index: PgBigIntU32, pub tx_id: String, @@ -38,6 +39,7 @@ impl DbRune { pub fn from_etching( etching: &Etching, number: u32, + block_hash: &String, block_height: u64, tx_index: u32, tx_id: &String, @@ -72,6 +74,7 @@ impl DbRune { number: PgBigIntU32(number), name, spaced_name, + block_hash: block_hash[2..].to_string(), block_height: PgNumericU64(block_height), tx_index: PgBigIntU32(tx_index), tx_id: tx_id[2..].to_string(), @@ -106,6 +109,7 @@ impl DbRune { pub fn from_cenotaph_etching( rune: &Rune, number: u32, + block_hash: &String, block_height: u64, tx_index: u32, tx_id: &String, @@ -116,6 +120,7 @@ impl DbRune { name: rune.to_string(), spaced_name: rune.to_string(), number: PgBigIntU32(number), + block_hash: block_hash[2..].to_string(), block_height: PgNumericU64(block_height), tx_index: PgBigIntU32(tx_index), tx_id: tx_id[2..].to_string(), @@ -144,6 +149,7 @@ impl DbRune { number: row.get("number"), name: row.get("name"), spaced_name: row.get("spaced_name"), + block_hash: row.get("block_hash"), block_height: row.get("block_height"), tx_index: row.get("tx_index"), tx_id: row.get("tx_id"), @@ -201,6 +207,7 @@ mod test { turbo: false, }, 0, + &"00000000000000000000d2845e9e48d356e89fd3b2e1f3da668ffc04c7dfe298".to_string(), 1, 0, &"14e87956a6bb0f50df1515e85f1dcc4625a7e2ebeb08ab6db7d9211c7cf64fa3".to_string(), diff --git a/src/db/types/pg_numeric_u128.rs b/src/db/types/pg_numeric_u128.rs index 44a35e8..5e839ba 100644 --- a/src/db/types/pg_numeric_u128.rs +++ b/src/db/types/pg_numeric_u128.rs @@ -1,7 +1,7 @@ use std::{ error::Error, io::{Cursor, Read}, - ops::{Add, AddAssign}, + ops::AddAssign, }; use bytes::{BufMut, BytesMut};