Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dvmd direct transfer allocated handler #29

Merged
merged 4 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions apps/processing/src/services/sharedDependencies.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CoingeckoProvider } from "@grants-stack-indexer/pricing";
import {
createKyselyDatabase,
KyselyApplicationRepository,
KyselyDonationRepository,
KyselyProjectRepository,
KyselyRoundRepository,
} from "@grants-stack-indexer/repository";
Expand Down Expand Up @@ -45,6 +46,10 @@ export class SharedDependenciesService {
kyselyDatabase,
env.DATABASE_SCHEMA,
);
const donationRepository = new KyselyDonationRepository(
kyselyDatabase,
env.DATABASE_SCHEMA,
);
const pricingProvider = new CoingeckoProvider(
{
apiKey: env.COINGECKO_API_KEY,
Expand All @@ -71,6 +76,7 @@ export class SharedDependenciesService {
roundRepository,
applicationRepository,
pricingProvider,
donationRepository,
metadataProvider,
},
registries: {
Expand Down
4 changes: 4 additions & 0 deletions packages/data-flow/src/data-loader/dataLoader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Changeset,
IApplicationRepository,
IDonationRepository,
IProjectRepository,
IRoundRepository,
} from "@grants-stack-indexer/repository";
Expand All @@ -9,6 +10,7 @@ import { ILogger, stringify } from "@grants-stack-indexer/shared";
import { ExecutionResult, IDataLoader, InvalidChangeset } from "../internal.js";
import {
createApplicationHandlers,
createDonationHandlers,
createProjectHandlers,
createRoundHandlers,
} from "./handlers/index.js";
Expand All @@ -35,13 +37,15 @@ export class DataLoader implements IDataLoader {
project: IProjectRepository;
round: IRoundRepository;
application: IApplicationRepository;
donation: IDonationRepository;
},
private readonly logger: ILogger,
) {
this.handlers = {
...createProjectHandlers(repositories.project),
...createRoundHandlers(repositories.round),
...createApplicationHandlers(repositories.application),
...createDonationHandlers(repositories.donation),
};
}

Expand Down
27 changes: 27 additions & 0 deletions packages/data-flow/src/data-loader/handlers/donation.handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { DonationChangeset, IDonationRepository } from "@grants-stack-indexer/repository";

import { ChangesetHandler } from "../types/index.js";

/**
* Collection of handlers for application-related operations.
* Each handler corresponds to a specific Application changeset type.
*/
export type DonationHandlers = {
[K in DonationChangeset["type"]]: ChangesetHandler<K>;
};

/**
* Creates handlers for managing application-related operations.
*
* @param repository - The application repository instance used for database operations
* @returns An object containing all application-related handlers
*/
export const createDonationHandlers = (repository: IDonationRepository): DonationHandlers => ({
InsertDonation: (async (changeset): Promise<void> => {
await repository.insertDonation(changeset.args.donation);
}) satisfies ChangesetHandler<"InsertDonation">,

InsertManyDonations: (async (changeset): Promise<void> => {
await repository.insertManyDonations(changeset.args.donations);
}) satisfies ChangesetHandler<"InsertManyDonations">,
});
1 change: 1 addition & 0 deletions packages/data-flow/src/data-loader/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./application.handlers.js";
export * from "./project.handlers.js";
export * from "./round.handlers.js";
export * from "./donation.handlers.js";
1 change: 1 addition & 0 deletions packages/data-flow/src/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export class Orchestrator {
project: this.dependencies.projectRepository,
round: this.dependencies.roundRepository,
application: this.dependencies.applicationRepository,
donation: this.dependencies.donationRepository,
},
this.logger,
);
Expand Down
2 changes: 2 additions & 0 deletions packages/data-flow/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ProcessorDependencies } from "@grants-stack-indexer/processors";
import {
Changeset,
IApplicationRepository,
IDonationRepository,
IProjectRepository,
IRoundRepository,
} from "@grants-stack-indexer/repository";
Expand Down Expand Up @@ -31,4 +32,5 @@ export type CoreDependencies = Pick<
roundRepository: IRoundRepository;
projectRepository: IProjectRepository;
applicationRepository: IApplicationRepository;
donationRepository: IDonationRepository;
};
7 changes: 7 additions & 0 deletions packages/data-flow/test/data-loader/dataLoader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import {
Changeset,
IApplicationRepository,
IDonationRepository,
IProjectRepository,
IRoundRepository,
} from "@grants-stack-indexer/repository";
Expand All @@ -27,6 +28,11 @@ describe("DataLoader", () => {
updateApplication: vi.fn(),
} as unknown as IApplicationRepository;

const mockDonationRepository = {
insertDonation: vi.fn(),
insertManyDonations: vi.fn(),
} as IDonationRepository;

const logger: ILogger = {
debug: vi.fn(),
error: vi.fn(),
Expand All @@ -39,6 +45,7 @@ describe("DataLoader", () => {
project: mockProjectRepository,
round: mockRoundRepository,
application: mockApplicationRepository,
donation: mockDonationRepository,
},
logger,
);
Expand Down
12 changes: 8 additions & 4 deletions packages/data-flow/test/unit/eventsFetcher.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, Mocked, vi } from "vitest";

import { IIndexerClient } from "@grants-stack-indexer/indexer-client";
import { AnyIndexerFetchedEvent, ChainId } from "@grants-stack-indexer/shared";
import { AnyIndexerFetchedEvent, ChainId, PoolCreatedParams } from "@grants-stack-indexer/shared";

import { EventsFetcher } from "../../src/eventsFetcher.js";

Expand All @@ -27,7 +27,11 @@ describe("EventsFetcher", () => {
eventName: "PoolCreated",
srcAddress: "0x1234567890123456789012345678901234567890",
logIndex: 0,
params: { contractAddress: "0x1234", tokenAddress: "0x1234", amount: 1000 },
params: {
contractAddress: "0x1234",
tokenAddress: "0x1234",
amount: 1000n,
} as unknown as PoolCreatedParams,
transactionFields: { hash: "0x1234", transactionIndex: 0 },
},
{
Expand All @@ -41,8 +45,8 @@ describe("EventsFetcher", () => {
params: {
contractAddress: "0x1234",
tokenAddress: "0x1234",
amount: 1000,
},
amount: 1000n,
} as unknown as PoolCreatedParams,
transactionFields: { hash: "0x1234", transactionIndex: 1 },
},
];
Expand Down
6 changes: 6 additions & 0 deletions packages/data-flow/test/unit/orchestrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { UnsupportedStrategy } from "@grants-stack-indexer/processors";
import {
Changeset,
IApplicationRepository,
IDonationRepository,
IProjectRepository,
IRoundRepository,
} from "@grants-stack-indexer/repository";
Expand Down Expand Up @@ -92,6 +93,7 @@ describe("Orchestrator", { sequential: true }, () => {
projectRepository: {} as unknown as IProjectRepository,
roundRepository: {} as unknown as IRoundRepository,
applicationRepository: {} as unknown as IApplicationRepository,
donationRepository: {} as unknown as IDonationRepository,
pricingProvider: {
getTokenPrice: vi.fn(),
},
Expand Down Expand Up @@ -269,6 +271,10 @@ describe("Orchestrator", { sequential: true }, () => {
RegisteredWithData: "",
DistributedWithData: "",
DistributedWithFlowRate: "",
AllocatedWithOrigin: "",
AllocatedWithData: "",
AllocatedWithVotes: "",
AllocatedWithStatus: "",
};

for (const event of Object.keys(strategyEvents) as StrategyEvent[]) {
Expand Down
26 changes: 21 additions & 5 deletions packages/indexer-client/test/unit/envioIndexerClient.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { GraphQLClient, RequestDocument, RequestOptions } from "graphql-request";
import { afterEach, beforeEach, describe, expect, it, Mocked, vi } from "vitest";

import { AnyIndexerFetchedEvent, ChainId } from "@grants-stack-indexer/shared";
import { AnyIndexerFetchedEvent, ChainId, PoolCreatedParams } from "@grants-stack-indexer/shared";

import { IndexerClientError, InvalidIndexerResponse } from "../../src/exceptions/index.js";
import { EnvioIndexerClient } from "../../src/providers/envioIndexerClient.js";
Expand Down Expand Up @@ -32,7 +32,11 @@ describe("EnvioIndexerClient", () => {
eventName: "PoolCreated",
srcAddress: "0x1234",
logIndex: 1,
params: { contractAddress: "0x1234", tokenAddress: "0x1234", amount: 1000 },
params: {
contractAddress: "0x1234",
tokenAddress: "0x1234",
amount: 1000n,
} as unknown as PoolCreatedParams,
transactionFields: { hash: "0x123", transactionIndex: 1 },
},
{
Expand All @@ -43,7 +47,11 @@ describe("EnvioIndexerClient", () => {
eventName: "PoolCreated",
srcAddress: "0x1234",
logIndex: 3,
params: { contractAddress: "0x1234", tokenAddress: "0x1234", amount: 1000 },
params: {
contractAddress: "0x1234",
tokenAddress: "0x1234",
amount: 1000n,
} as unknown as PoolCreatedParams,
transactionFields: { hash: "0x123", transactionIndex: 1 },
},
{
Expand All @@ -54,7 +62,11 @@ describe("EnvioIndexerClient", () => {
eventName: "PoolCreated",
srcAddress: "0x1234",
logIndex: 1,
params: { contractAddress: "0x1234", tokenAddress: "0x1234", amount: 1000 },
params: {
contractAddress: "0x1234",
tokenAddress: "0x1234",
amount: 1000n,
} as unknown as PoolCreatedParams,
transactionFields: { hash: "0x123", transactionIndex: 1 },
},
{
Expand All @@ -65,7 +77,11 @@ describe("EnvioIndexerClient", () => {
eventName: "PoolCreated",
srcAddress: "0x1234",
logIndex: 1,
params: { contractAddress: "0x1234", tokenAddress: "0x1234", amount: 1000 },
params: {
contractAddress: "0x1234",
tokenAddress: "0x1234",
amount: 1000n,
} as unknown as PoolCreatedParams,
transactionFields: { hash: "0x123", transactionIndex: 1 },
},
];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ChainId } from "@grants-stack-indexer/shared";

export class ApplicationNotFound extends Error {
constructor(chainId: ChainId, roundId: string, recipientId: string) {
super(
`Application not found on chain ${chainId} for round ${roundId} and recipient ${recipientId}`,
);
}
}
2 changes: 2 additions & 0 deletions packages/processors/src/exceptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export * from "./invalidArgument.exception.js";
export * from "./unsupportedStrategy.exception.js";
export * from "./projectNotFound.exception.js";
export * from "./roundNotFound.exception.js";
export * from "./applicationNotFound.exception.js";
export * from "./unknownToken.exception.js";
7 changes: 7 additions & 0 deletions packages/processors/src/exceptions/unknownToken.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ChainId } from "@grants-stack-indexer/shared";

export class UnknownToken extends Error {
constructor(tokenAddress: string, chainId?: ChainId) {
super(`Unknown token: ${tokenAddress} ${chainId ? `on chain ${chainId}` : ""}`);
}
}
1 change: 1 addition & 0 deletions packages/processors/src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./roles.js";
export * from "./utils.js";
export * from "./tokenMath.js";
export * from "./pricing.js";
71 changes: 71 additions & 0 deletions packages/processors/src/helpers/pricing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { IPricingProvider } from "@grants-stack-indexer/pricing";
import { Token } from "@grants-stack-indexer/shared";

import { TokenPriceNotFoundError } from "../internal.js";
import { calculateAmountInToken, calculateAmountInUsd } from "./tokenMath.js";
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
import { calculateAmountInToken, calculateAmountInUsd } from "./tokenMath.js";
import { calculateAmountInToken, calculateAmountInUsd } from "./index.js";

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

how to make autoimport work magically with this? maybe it's easier to have one single file called index.js with all the code ?)

Copy link
Collaborator

Choose a reason for hiding this comment

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

i know that is annoying but, it save us some future headaches 🤣


// sometimes coingecko returns no prices for 1 hour range, 2 hours works better
const TIMESTAMP_DELTA_RANGE = 2 * 60 * 60 * 1000;
Copy link
Collaborator

Choose a reason for hiding this comment

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

do you think this might be better as an env variable to avoid a code change if their api behavior changes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

there isn't a specific api behaviour here actually, this comment is smth Gitcoin team found out experimenting,
i think for now is not needed an env variable but now im thinking if maybe this can be the default value on Coingecko provider instead of API caller passing timestamp.now() + DELTA_RANGE everytime. i leave it for reviewing later 🫡

Copy link
Collaborator

Choose a reason for hiding this comment

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

agree with you nigiri, lets have this value as default ( within the CoingeckoProvider) and make the param optional , wdyt ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

will add a task in linear to refactor in another PR

Copy link
Collaborator Author

Choose a reason for hiding this comment

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


/**
* Get the amount in USD for a given amount in the token
* @param pricingProvider - The pricing provider to use
* @param token - The token to get the amount in
* @param amount - The amount in the token
* @param timestamp - The timestamp to get the price at
* @returns The amount in USD
* @throws TokenPriceNotFoundError if the price is not found
*/
export const getTokenAmountInUsd = async (
pricingProvider: IPricingProvider,
token: Token,
amount: bigint,
timestamp: number,
): Promise<{ amountInUsd: string; timestamp: number }> => {
const tokenPrice = await pricingProvider.getTokenPrice(
token.priceSourceCode,
timestamp,
timestamp + TIMESTAMP_DELTA_RANGE,
Copy link
Collaborator

Choose a reason for hiding this comment

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

in addition to prev comment, instead timestamp + TIMESTAMP_DELTA_RANGE as param , just delta

);

if (!tokenPrice) {
throw new TokenPriceNotFoundError(token.address, timestamp);
}

return {
amountInUsd: calculateAmountInUsd(amount, tokenPrice.priceUsd, token.decimals),
timestamp: tokenPrice.timestampMs,
};
};

/**
* Get the amount in the token for a given amount in USD
* @param pricingProvider - The pricing provider to use
* @param token - The token to get the amount in
* @param amountInUSD - The amount in USD
* @param timestamp - The timestamp to get the price at
* @returns The amount in the token
* @throws TokenPriceNotFoundError if the price is not found
*/
export const getUsdInTokenAmount = async (
pricingProvider: IPricingProvider,
token: Token,
amountInUSD: string,
timestamp: number,
): Promise<{ amount: bigint; price: number; timestamp: Date }> => {
const closestPrice = await pricingProvider.getTokenPrice(
token.priceSourceCode,
timestamp,
timestamp + TIMESTAMP_DELTA_RANGE,
Copy link
Collaborator

Choose a reason for hiding this comment

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

ditto

);

if (!closestPrice) {
throw new TokenPriceNotFoundError(token.address, timestamp);
}

return {
amount: calculateAmountInToken(amountInUSD, closestPrice.priceUsd, token.decimals),
timestamp: new Date(closestPrice.timestampMs),
price: 1 / closestPrice.priceUsd, // price is the token price in USD, we return the inverse
};
};
19 changes: 19 additions & 0 deletions packages/processors/src/helpers/tokenMath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,22 @@ export const calculateAmountInUsd = (

return amountInUsd.toString();
};

/**
* Calculates the amount in token
* @param amountInUSD - The amount in USD
* @param tokenPriceInUsd - The price of the token in USD
* @param tokenDecimals - The number of decimals the token has
* @returns The amount in token
*/
export const calculateAmountInToken = (
amountInUSD: string,
tokenPriceInUsd: string | number,
Copy link
Collaborator

Choose a reason for hiding this comment

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

should this always be string ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

i left it open because pricing provider returns number because coingecko returns a number (anyways, it doesn't affect the purpose of the function)

Copy link
Collaborator

Choose a reason for hiding this comment

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

oh, got it , now i remember

tokenDecimals: number,
): bigint => {
const amountInUsdBN = new BigNumber(amountInUSD);
const tokenPriceInUsdBN = new BigNumber(tokenPriceInUsd);
const scaleFactor = new BigNumber(10).pow(tokenDecimals);

return BigInt(amountInUsdBN.multipliedBy(scaleFactor).dividedBy(tokenPriceInUsdBN).toFixed(0));
Copy link
Collaborator

Choose a reason for hiding this comment

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

might need some protection against division by zero (and possibly invalid/NaN values, not sure all the responses coingecko can return)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

i was relying on the exception that bignumber.js throws for ZeroDivision, i'll clarify in natssec that it throws an exception in that case)

};
12 changes: 12 additions & 0 deletions packages/processors/src/schemas/applicationMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { z } from "zod";

export const ApplicationMetadataSchema = z
.object({
application: z.object({
round: z.string(),
recipient: z.string(),
}),
})
.transform((data) => ({ type: "application" as const, ...data }));

export type ApplicationMetadata = z.infer<typeof ApplicationMetadataSchema>;
Loading