Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4d47c1e
fix: reject ssl verification
0xkenj1 Jan 24, 2025
49ae81c
fix: remove admin secret header on envio indexer client
0xkenj1 Jan 24, 2025
fe17f98
fix: db connection
0xkenj1 Jan 30, 2025
9992af7
feat: improve pricing and metadata
0xkenj1 Feb 3, 2025
2d114f0
fix: dependencies
0xkenj1 Feb 4, 2025
5491be8
feat: improve-bootstrap-scripts
0xkenj1 Feb 4, 2025
50ef5b4
fix: metadata and pricing issues
0xkenj1 Feb 4, 2025
adf1bf2
fix: issues
0xkenj1 Feb 4, 2025
ad6c803
fix: exception issue
0xkenj1 Feb 4, 2025
04d481c
Merge branch 'feat/metadata-pricing-improvements' into feat/bootstrap…
0xkenj1 Feb 4, 2025
f0c25ec
Merge remote-tracking branch 'origin/dev' into feat/bootstrap-scripts
0xkenj1 Feb 5, 2025
37c63ae
feat: add optimizations and fix caching
0xkenj1 Feb 5, 2025
1f07ae8
test: envio indexer client
0xkenj1 Feb 11, 2025
227e2a7
fix: metadata tests and smol fixes
0xkenj1 Feb 11, 2025
632cb94
fix: dataflow tests and smol fixes
0xkenj1 Feb 11, 2025
2f7c02c
fix: pricing tests and console logs
0xkenj1 Feb 11, 2025
dbac0f9
fix: pricing tests and console logs
0xkenj1 Feb 11, 2025
a413df9
fix: pr comments
0xkenj1 Feb 11, 2025
45ca767
Merge remote-tracking branch 'origin/dev' into feat/bootstrap-scripts
0xkenj1 Feb 11, 2025
34d7f96
fix: pricing bootstrap script
0xkenj1 Feb 11, 2025
74a3bb5
fix: tests
0xkenj1 Feb 11, 2025
17bcc8b
fix: lint
0xkenj1 Feb 11, 2025
230e42c
fix: pr comments
0xkenj1 Feb 12, 2025
f07d951
fix: yacomments
0xkenj1 Feb 12, 2025
5b4612d
fix: metadata tests
0xkenj1 Feb 12, 2025
218e933
fix: local environment
0xkenj1 Feb 13, 2025
d41c4fb
fix: env example
0xkenj1 Feb 13, 2025
d5a42c0
fix: metadata script
0xkenj1 Feb 13, 2025
43534b3
fix: examples
0xkenj1 Feb 13, 2025
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
2 changes: 0 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ HASURA_GRAPHQL_UNAUTHORIZED_ROLE=public
RPC_URLS=["https://optimism.llamarpc.com","https://rpc.ankr.com/optimism","https://optimism.gateway.tenderly.co","https://optimism.blockpi.network/v1/rpc/public","https://mainnet.optimism.io","https://opt-mainnet.g.alchemy.com/v2/demo"]
CHAIN_ID=10

FETCH_LIMIT=1000
FETCH_DELAY_MS=3000

DATABASE_URL=postgresql://postgres:testing@datalayer-postgres-db:5432/datalayer-postgres-db
DATABASE_SCHEMA=chain_data_schema_1
Expand Down
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate

FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run -r build
RUN pnpm deploy --filter=./apps/processing --prod /prod/processing
RUN pnpm deploy --filter=./apps/processing /prod/processing

FROM base AS processing
COPY --from=build /prod/processing /prod/processing
WORKDIR /prod/processing
CMD [ "pnpm", "start" ]
CMD [ "pnpm", "start" ]
40 changes: 34 additions & 6 deletions apps/processing/src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const chainSchema = z.object({
});

const baseSchema = z.object({
NODE_ENV: z.enum(["development", "staging", "production"]).default("development"),
CHAINS: stringToJSONSchema.pipe(z.array(chainSchema).nonempty()).refine((chains) => {
const ids = chains.map((chain) => chain.id);
const uniqueIds = new Set(ids);
Expand All @@ -31,14 +32,12 @@ const baseSchema = z.object({
DATABASE_URL: z.string(),
DATABASE_SCHEMA: z.string().default("public"),
INDEXER_GRAPHQL_URL: z.string().url(),
INDEXER_ADMIN_SECRET: z.string(),
INDEXER_ADMIN_SECRET: z.string().optional(),
PRICING_SOURCE: z.enum(["dummy", "coingecko"]).default("coingecko"),
IPFS_GATEWAYS_URL: stringToJSONSchema
.pipe(z.array(z.string().url()))
.default('["https://ipfs.io"]'),
METADATA_SOURCE: z.enum(["dummy", "public-gateway"]).default("dummy"),
RETRY_MAX_ATTEMPTS: z.coerce.number().int().min(1).default(3),
RETRY_BASE_DELAY_MS: z.coerce.number().int().min(1).default(3000), // 3 seconds
RETRY_FACTOR: z.coerce.number().int().min(1).default(2),
RETRY_FACTOR: z.coerce.number().min(1).default(2),
RETRY_MAX_DELAY_MS: z.coerce.number().int().min(1).optional(), // 5 minute
});

Expand All @@ -53,6 +52,15 @@ const coingeckoPricingSchema = baseSchema.extend({
COINGECKO_API_TYPE: z.enum(["demo", "pro"]).default("pro"),
});

const dummyMetadataSchema = baseSchema.extend({
METADATA_SOURCE: z.literal("dummy"),
});

const publicGatewayMetadataSchema = baseSchema.extend({
METADATA_SOURCE: z.literal("public-gateway"),
PUBLIC_GATEWAY_URLS: stringToJSONSchema.pipe(z.array(z.string().url())),
});

const validationSchema = z
.discriminatedUnion("PRICING_SOURCE", [dummyPricingSchema, coingeckoPricingSchema])
.transform((val) => {
Expand All @@ -66,7 +74,27 @@ const validationSchema = z
apiType: val.COINGECKO_API_TYPE,
...val,
};
});
})
.and(
z
.discriminatedUnion("METADATA_SOURCE", [
dummyMetadataSchema,
publicGatewayMetadataSchema,
])
.transform((val) => {
if (val.METADATA_SOURCE === "dummy") {
return { metadataSource: val.METADATA_SOURCE, ...val };
}
if (val.METADATA_SOURCE === "public-gateway") {
return {
metadataSource: val.METADATA_SOURCE,
gateways: val.PUBLIC_GATEWAY_URLS,
...val,
};
}
throw new Error("Invalid metadata source");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something to have in mind that could avoid throwing errors:
https://github.com/colinhacks/zod?tab=readme-ov-file#validating-during-transform

}),
);
Comment on lines +83 to +97
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Feb 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Simplify chained discriminated unions for maintainability.

The combination of two .discriminatedUnion(...) calls using .and(...) can be tricky to maintain. If you add or remove a source, you must carefully update each chain of transforms. Consider extracting smaller schema parts or adopting a single union structure with refined branching.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be interesting to see what solution or apporach you suggest on this?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0xnigir1 Here's a suggested approach to simplify the schema using a single discriminated union:

// Define all possible combinations of sources
const sourceSchema = z.object({
  sourceType: z.enum([
    'dummy-all',
    'dummy-pricing-coingecko-metadata',
    'coingecko-pricing-dummy-metadata',
    'coingecko-pricing-pinata-metadata',
    'coingecko-pricing-public-gateway-metadata',
  ]),
});

// Define source-specific configurations
const sourcesConfig = {
  'dummy-all': baseSchema.extend({
    sourceType: z.literal('dummy-all'),
    DUMMY_PRICE: z.coerce.number().optional().default(1),
  }),
  'dummy-pricing-coingecko-metadata': baseSchema.extend({
    sourceType: z.literal('dummy-pricing-coingecko-metadata'),
    DUMMY_PRICE: z.coerce.number().optional().default(1),
    COINGECKO_API_KEY: z.string().min(1),
    COINGECKO_API_TYPE: z.enum(['demo', 'pro']).default('pro'),
  }),
  // ... other combinations
};

const validationSchema = z
  .discriminatedUnion('sourceType', Object.values(sourcesConfig))
  .transform((val) => {
    const config: Record<string, unknown> = {
      pricingSource: val.sourceType.includes('dummy-pricing') ? 'dummy' : 'coingecko',
      metadataSource: val.sourceType.includes('dummy-metadata') ? 'dummy' : 
                      val.sourceType.includes('pinata') ? 'pinata' : 
                      val.sourceType.includes('public-gateway') ? 'public-gateway' : 'dummy',
    };

    // Add source-specific configurations
    if (config.pricingSource === 'dummy') {
      config.dummyPrice = val.DUMMY_PRICE;
    }
    if (config.pricingSource === 'coingecko') {
      config.apiKey = val.COINGECKO_API_KEY;
      config.apiType = val.COINGECKO_API_TYPE;
    }
    // ... handle metadata sources similarly

    return { ...val, ...config };
  });

This approach:

  1. Uses a single sourceType discriminator that combines both pricing and metadata sources
  2. Defines all possible valid combinations upfront
  3. Simplifies the transform logic by deriving configurations from the source type
  4. Makes it easier to add new source combinations by just extending the sourcesConfig

Let me know if you'd like me to explain any part in more detail!


const env = validationSchema.safeParse(process.env);

Expand Down
9 changes: 7 additions & 2 deletions apps/processing/src/services/sharedDependencies.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CoreDependencies } from "@grants-stack-indexer/data-flow";
import { EnvioIndexerClient } from "@grants-stack-indexer/indexer-client";
import { CachingMetadataProvider, IpfsProvider } from "@grants-stack-indexer/metadata";
import { CachingMetadataProvider, MetadataProviderFactory } from "@grants-stack-indexer/metadata";
import { CachingPricingProvider, PricingProviderFactory } from "@grants-stack-indexer/pricing";
import {
createKyselyDatabase,
Expand Down Expand Up @@ -52,6 +52,7 @@ export class SharedDependenciesService {
const kyselyDatabase = createKyselyDatabase(
{
connectionString: env.DATABASE_URL,
isProduction: env.NODE_ENV === "production" || env.NODE_ENV === "staging",
},
logger,
);
Expand Down Expand Up @@ -89,7 +90,11 @@ export class SharedDependenciesService {
);

const metadataRepository = new KyselyMetadataCache(kyselyDatabase, env.DATABASE_SCHEMA);
const internalMetadataProvider = new IpfsProvider(env.IPFS_GATEWAYS_URL, logger);

const internalMetadataProvider = MetadataProviderFactory.create(env, {
logger,
});

const dbCachedMetadataProvider = new CachingMetadataProvider(
internalMetadataProvider,
metadataRepository,
Expand Down
17 changes: 12 additions & 5 deletions apps/processing/test/unit/sharedDependencies.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from "vitest";

import { EnvioIndexerClient } from "@grants-stack-indexer/indexer-client";
import { IpfsProvider } from "@grants-stack-indexer/metadata";
import { MetadataProviderFactory } from "@grants-stack-indexer/metadata";
import { PricingProviderFactory } from "@grants-stack-indexer/pricing";
import { createKyselyDatabase } from "@grants-stack-indexer/repository";

Expand Down Expand Up @@ -59,8 +59,10 @@ vi.mock("@grants-stack-indexer/pricing", () => ({
}));

vi.mock("@grants-stack-indexer/metadata", () => ({
IpfsProvider: vi.fn(),
CachingMetadataProvider: vi.fn(),
MetadataProviderFactory: {
create: vi.fn(),
},
}));

vi.mock("@grants-stack-indexer/indexer-client", () => ({
Expand Down Expand Up @@ -103,17 +105,19 @@ describe("SharedDependenciesService", () => {
Environment,
| "DATABASE_URL"
| "DATABASE_SCHEMA"
| "IPFS_GATEWAYS_URL"
| "INDEXER_GRAPHQL_URL"
| "INDEXER_ADMIN_SECRET"
| "PRICING_SOURCE"
| "METADATA_SOURCE"
| "NODE_ENV"
> = {
DATABASE_URL: "postgresql://localhost:5432/test",
DATABASE_SCHEMA: "public",
IPFS_GATEWAYS_URL: ["https://ipfs.io"],
INDEXER_GRAPHQL_URL: "http://localhost:8080",
INDEXER_ADMIN_SECRET: "secret",
PRICING_SOURCE: "dummy",
METADATA_SOURCE: "public-gateway",
NODE_ENV: "development",
};

it("initializes all dependencies correctly", async () => {
Expand All @@ -123,6 +127,7 @@ describe("SharedDependenciesService", () => {
expect(createKyselyDatabase).toHaveBeenCalledWith(
{
connectionString: mockEnv.DATABASE_URL,
isProduction: mockEnv.NODE_ENV === "production",
},
mocks.logger,
);
Expand All @@ -131,8 +136,10 @@ describe("SharedDependenciesService", () => {
expect(PricingProviderFactory.create).toHaveBeenCalledWith(mockEnv, {
logger: mocks.logger,
});
expect(IpfsProvider).toHaveBeenCalledWith(mockEnv.IPFS_GATEWAYS_URL, mocks.logger);

expect(MetadataProviderFactory.create).toHaveBeenCalledWith(mockEnv, {
logger: mocks.logger,
});
// Verify indexer client initialization
expect(EnvioIndexerClient).toHaveBeenCalledWith(
mockEnv.INDEXER_GRAPHQL_URL,
Expand Down
15 changes: 13 additions & 2 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ services:
HASURA_GRAPHQL_ENABLED_LOG_TYPES: "startup, http-log, webhook-log, websocket-log, query-log"
HASURA_GRAPHQL_ADMIN_INTERNAL_ERRORS: ${DATALAYER_HASURA_ADMIN_INTERNAL_ERRORS:-true}
## uncomment next line to run console offline (i.e load console assets from server instead of CDN)
HASURA_GRAPHQL_CONSOLE_ASSETS_DIR: null
# HASURA_GRAPHQL_CONSOLE_ASSETS_DIR: null
depends_on:
datalayer-postgres-db:
condition: service_healthy
Expand All @@ -55,6 +55,16 @@ services:
start_period: 5s
networks:
- datalayer
processing:
build:
context: .
dockerfile: Dockerfile
restart: always
depends_on:
datalayer-postgres-db:
condition: service_healthy
env_file:
- .env
datalayer-graphql-config:
build:
context: .
Expand Down Expand Up @@ -108,6 +118,8 @@ services:
start_period: 5s
networks:
- indexer
- datalayer

indexer:
build:
context: ./apps/indexer
Expand All @@ -122,7 +134,6 @@ services:
- .env
networks:
- indexer
- datalayer
volumes:
db_data:
ganache-data:
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
"type": "module",
"scripts": {
"api:configure": "pnpm run --filter @grants-stack-indexer/hasura-config-scripts api:configure",
"bootstrap:metadata": "pnpm run --filter @grants-stack-indexer/bootstrap bootstrap:metadata",
"bootstrap:pricing": "pnpm run --filter @grants-stack-indexer/bootstrap bootstrap:pricing",
"build": "turbo run build",
"check-types": "turbo run check-types",
"clean": "turbo run clean",
"db:cache:migrate": "pnpm run --filter @grants-stack-indexer/migrations db:cache:migrate",
"db:cache:reset": "pnpm run --filter @grants-stack-indexer/migrations db:cache:reset",
"db:migrate": "pnpm run --filter @grants-stack-indexer/migrations db:migrate",
"db:reset": "pnpm run --filter @grants-stack-indexer/migrations db:reset",
"dev": "turbo run dev",
Expand Down Expand Up @@ -49,7 +53,7 @@
"typescript": "5.5.4",
"vitest": "2.0.5"
},
"packageManager": "pnpm@9.7.1+sha256.46f1bbc8f8020aa9869568c387198f1a813f21fb44c82f400e7d1dbde6c70b40",
"packageManager": "pnpm@9.12.0",
"engines": {
"node": "20"
}
Expand Down
2 changes: 2 additions & 0 deletions packages/data-flow/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"@grants-stack-indexer/processors": "workspace:*",
"@grants-stack-indexer/repository": "workspace:*",
"@grants-stack-indexer/shared": "workspace:*",
"p-map": "7.0.3",
"ts-retry": "5.0.1",
"viem": "2.21.19"
}
}
19 changes: 19 additions & 0 deletions packages/data-flow/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Maximum number of retries for bulk fetching metadata.
*/
export const MAX_BULK_FETCH_METADATA_RETRIES = 10;

/**
* Base delay in milliseconds for bulk fetching metadata retries.
*/
export const METADATA_BULK_FETCH_BASE_DELAY_MS = 1000;

/**
* Backoff factor for bulk fetching metadata retries.
*/
export const METADATA_BULK_FETCH_BACKOFF_FACTOR = 1.5;

/**
* Maximum concurrency for bulk fetching metadata.
*/
export const MAX_BULK_FETCH_METADATA_CONCURRENCY = 10;
1 change: 1 addition & 0 deletions packages/data-flow/src/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export {
DatabaseEventRegistry,
DatabaseStrategyRegistry,
Orchestrator,
getMetadataCidsFromEvents,
} from "./internal.js";

export type { IEventsRegistry, IStrategyRegistry, IDataLoader } from "./internal.js";
Expand Down
44 changes: 44 additions & 0 deletions packages/data-flow/src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
decodeDGApplicationData,
decodeDVMDApplicationData,
decodeDVMDExtendedApplicationData,
} from "@grants-stack-indexer/processors";
import { AnyIndexerFetchedEvent, ILogger } from "@grants-stack-indexer/shared";

/**
* Extracts unique metadata ids from the events batch.
* @param events - Array of indexer fetched events to process
* @returns Array of unique metadata ids found in the events
*/
export const getMetadataCidsFromEvents = (
events: AnyIndexerFetchedEvent[],
logger: ILogger,
): string[] => {
const ids = new Set<string>();

for (const event of events) {
if ("metadata" in event.params) {
ids.add(event.params.metadata[1]);
} else if ("data" in event.params) {
try {
const decoded = decodeDGApplicationData(event.params.data);
ids.add(decoded.metadata.pointer);
} catch (error) {}
Comment on lines +23 to +26
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

Add error logging in catch blocks.

Empty catch blocks silently swallow errors, making it difficult to debug issues with metadata decoding. Consider logging these errors or implementing proper error handling.

 try {
     const decoded = decodeDGApplicationData(event.params.data);
     ids.add(decoded.metadata.pointer);
-} catch (error) {}
+} catch (error) {
+    console.warn(`Failed to decode DG application data: ${error}`);
+}

Also applies to: 24-27, 28-31

try {
const decoded = decodeDVMDApplicationData(event.params.data);
ids.add(decoded.metadata.pointer);
} catch (error) {}
try {
const decoded = decodeDVMDExtendedApplicationData(event.params.data);
ids.add(decoded.metadata.pointer);
} catch (error) {
logger.warn("Failed to decode Metadata CID from event data", {
error,
event,
});
}
}
Comment on lines +23 to +40
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now im thinking, we could write a decode method that doesn't throw but instead returns [boolean, result | undefined] so we don't have this nested try/catch blocks. i think it would make it a bit cleaner

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would leave it as it is, it doesn't bother imo

}

return Array.from(ids);
};
1 change: 1 addition & 0 deletions packages/data-flow/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from "./registries/index.js";
export * from "./eventsProcessor.js";
export * from "./orchestrator.js";
export * from "./retroactiveProcessor.js";
export * from "./helpers/index.js";
Loading
Loading