Skip to content

Commit

Permalink
feat: dvmd direct transfer allocated handler (#29)
Browse files Browse the repository at this point in the history
# 🤖 Linear

Closes GIT-140 GIT-142

## Description
- `Allocated` event handler for DVMD Direct Transfer strategy
- add `AllocatedWithX` events from Envio
- adds `DonationRepository`

## Checklist before requesting a review

-   [x] I have conducted a self-review of my code.
-   [x] I have conducted a QA.
-   [x] If it is a core feature, I have included comprehensive tests.
  • Loading branch information
0xnigir1 authored Nov 12, 2024
1 parent b6b112e commit 658bd7b
Show file tree
Hide file tree
Showing 40 changed files with 1,009 additions and 30 deletions.
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}`,
);
}
}
3 changes: 3 additions & 0 deletions packages/processors/src/exceptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ 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";
export * from "./metadataParsingFailed.exception.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class MetadataParsingFailed extends Error {
constructor(additionalInfo?: string) {
super(`Failed to parse application metadata: ${additionalInfo}`);
}
}
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 "./index.js";

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

/**
* 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,
);

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,
);

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
};
};
25 changes: 25 additions & 0 deletions packages/processors/src/helpers/tokenMath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,28 @@ 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
* @throws Error if tokenPriceInUsd is 0 (division by zero)
*/
export const calculateAmountInToken = (
amountInUSD: string,
tokenPriceInUsd: string | number,
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, BigNumber.ROUND_FLOOR),
);
};
Loading

0 comments on commit 658bd7b

Please sign in to comment.