From 11c9bbe2b6a4894c0811112e13b6e79351e14b4c Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:53:51 -0300 Subject: [PATCH] feat: add bignumberjs for money ops --- .../processors/src/allo/allo.processor.ts | 7 +-- .../src/allo/handlers/poolCreated.handler.ts | 8 ++-- packages/processors/src/helpers/tokenMath.ts | 23 +++++----- .../test/allo/allo.processor.spec.ts | 11 +++-- .../allo/handlers/poolCreated.handler.spec.ts | 12 ++--- .../processors/test/helpers/tokenMath.spec.ts | 44 ++++++++++++++----- .../interfaces/roundRepository.interface.ts | 2 +- .../repositories/kysely/round.repository.ts | 2 +- .../repository/src/types/changeset.types.ts | 4 +- packages/repository/src/types/round.types.ts | 6 +-- packages/shared/package.json | 1 + packages/shared/src/external.ts | 3 ++ packages/shared/src/internal.ts | 1 + packages/shared/src/math/bignumber.ts | 4 ++ pnpm-lock.yaml | 11 +++++ 15 files changed, 86 insertions(+), 53 deletions(-) create mode 100644 packages/shared/src/math/bignumber.ts diff --git a/packages/processors/src/allo/allo.processor.ts b/packages/processors/src/allo/allo.processor.ts index 88e93a1..76f3e41 100644 --- a/packages/processors/src/allo/allo.processor.ts +++ b/packages/processors/src/allo/allo.processor.ts @@ -13,12 +13,7 @@ export class AlloProcessor implements IProcessor<"Allo", AlloEvent> { async process(event: ProtocolEvent<"Allo", AlloEvent>): Promise { switch (event.eventName) { case "PoolCreated": - return new PoolCreatedHandler(event, this.chainId, { - evmProvider: this.dependencies.evmProvider, - pricingProvider: this.dependencies.pricingProvider, - metadataProvider: this.dependencies.metadataProvider, - roundRepository: this.dependencies.roundRepository, - }).handle(); + return new PoolCreatedHandler(event, this.chainId, this.dependencies).handle(); default: throw new Error(`Unknown event name: ${event.eventName}`); } diff --git a/packages/processors/src/allo/handlers/poolCreated.handler.ts b/packages/processors/src/allo/handlers/poolCreated.handler.ts index a07a449..392b587 100644 --- a/packages/processors/src/allo/handlers/poolCreated.handler.ts +++ b/packages/processors/src/allo/handlers/poolCreated.handler.ts @@ -78,7 +78,7 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> }; let matchAmount = 0n; - let matchAmountInUsd = 0; + let matchAmountInUsd = "0"; if (strategy) { strategyTimings = await getStrategyTimings(evmProvider, strategy, strategyAddress); @@ -102,7 +102,7 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> } } - let fundedAmountInUsd = 0; + let fundedAmountInUsd = "0"; if (token !== null && fundedAmount > 0n) { fundedAmountInUsd = await this.getTokenAmountInUsd( @@ -122,7 +122,7 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> id: poolId.toString(), tags: ["allo-v2", ...(parsedRoundMetadata.success ? ["grants-stack"] : [])], totalDonationsCount: 0, - totalAmountDonatedInUsd: 0, + totalAmountDonatedInUsd: "0", uniqueDonorsCount: 0, matchTokenAddress, matchAmount, @@ -209,7 +209,7 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> token: { address: Address; decimals: number }, amount: bigint, timestamp: number, - ): Promise { + ): Promise { const { pricingProvider } = this.dependencies; const tokenPrice = await pricingProvider.getTokenPrice( this.chainId, diff --git a/packages/processors/src/helpers/tokenMath.ts b/packages/processors/src/helpers/tokenMath.ts index 4d32849..037e1c2 100644 --- a/packages/processors/src/helpers/tokenMath.ts +++ b/packages/processors/src/helpers/tokenMath.ts @@ -1,4 +1,4 @@ -import { formatUnits, parseUnits } from "viem"; +import { BigNumber } from "@grants-stack-indexer/shared"; /** * Calculates the amount in USD @@ -10,23 +10,22 @@ import { formatUnits, parseUnits } from "viem"; */ export const calculateAmountInUsd = ( amount: bigint, - tokenPriceInUsd: number, + tokenPriceInUsd: string | number, tokenDecimals: number, truncateDecimals?: number, -): number => { - const amountInUsd = Number( - formatUnits( - amount * parseUnits(tokenPriceInUsd.toString(), tokenDecimals), - tokenDecimals * 2, - ), - ); +): string => { + const amountBN = new BigNumber(amount.toString()); + const tokenPriceBN = new BigNumber(tokenPriceInUsd.toString()); + const scaleFactor = new BigNumber(10).pow(tokenDecimals); - if (truncateDecimals) { + let amountInUsd = amountBN.multipliedBy(tokenPriceBN).dividedBy(scaleFactor); + + if (truncateDecimals !== undefined) { if (truncateDecimals < 0 || truncateDecimals > 18) { throw new Error("Truncate decimals must be between 0 and 18"); } - return Number(amountInUsd.toFixed(truncateDecimals)); + amountInUsd = amountInUsd.decimalPlaces(truncateDecimals); } - return amountInUsd; + return amountInUsd.toString(); }; diff --git a/packages/processors/test/allo/allo.processor.spec.ts b/packages/processors/test/allo/allo.processor.spec.ts index 7d0967b..5650eeb 100644 --- a/packages/processors/test/allo/allo.processor.spec.ts +++ b/packages/processors/test/allo/allo.processor.spec.ts @@ -58,12 +58,11 @@ describe("AlloProcessor", () => { await processor.process(mockEvent); - expect(PoolCreatedHandler).toHaveBeenCalledWith(mockEvent, mockChainId, { - evmProvider: mockEvmProvider, - pricingProvider: mockPricingProvider, - metadataProvider: mockMetadataProvider, - roundRepository: mockRoundRepository, - }); + expect(PoolCreatedHandler).toHaveBeenCalledWith( + mockEvent, + mockChainId, + processor["dependencies"], + ); expect(PoolCreatedHandler.prototype.handle).toHaveBeenCalled(); }); diff --git a/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts b/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts index 1332cbd..49b3d43 100644 --- a/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts +++ b/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts @@ -94,7 +94,7 @@ describe("PoolCreatedHandler", () => { expect(changeset.type).toBe("InsertRound"); expect(changeset.args.round).toMatchObject({ fundedAmount: fundedAmount, - fundedAmountInUsd: 1000, + fundedAmountInUsd: "1000", }); expect(mockPricingProvider.getTokenPrice).toHaveBeenCalled(); expect(mockMetadataProvider.getMetadata).toHaveBeenCalled(); @@ -188,13 +188,13 @@ describe("PoolCreatedHandler", () => { id: "10", tags: ["allo-v2", "grants-stack"], totalDonationsCount: 0, - totalAmountDonatedInUsd: 0, + totalAmountDonatedInUsd: "0", uniqueDonorsCount: 0, matchTokenAddress: mockEvent.params.token, matchAmount: parseUnits("1", 18), - matchAmountInUsd: 100, + matchAmountInUsd: "100", fundedAmount: 0n, - fundedAmountInUsd: 0, + fundedAmountInUsd: "0", applicationMetadataCid: "bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", applicationMetadata: { version: "1.0.0", @@ -291,9 +291,9 @@ describe("PoolCreatedHandler", () => { id: "10", tags: ["allo-v2"], matchAmount: 0n, - matchAmountInUsd: 0, + matchAmountInUsd: "0", fundedAmount: 0n, - fundedAmountInUsd: 0, + fundedAmountInUsd: "0", applicationMetadataCid: "bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", applicationMetadata: {}, roundMetadataCid: "bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", diff --git a/packages/processors/test/helpers/tokenMath.spec.ts b/packages/processors/test/helpers/tokenMath.spec.ts index 5702e52..f15500d 100644 --- a/packages/processors/test/helpers/tokenMath.spec.ts +++ b/packages/processors/test/helpers/tokenMath.spec.ts @@ -10,7 +10,7 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 18; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); - expect(result).toBe(100); + expect(result).toBe("100"); }); it("calculate USD amount for 18 decimal token with float price", () => { @@ -19,7 +19,7 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 18; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); - expect(result).toBeCloseTo(41.025, 5); + expect(result).toBe("41.025"); }); it("calculate USD amount for 8 decimal token with integer price", () => { @@ -28,7 +28,7 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 8; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); - expect(result).toBe(50); + expect(result).toBe("50"); }); // Test case for 8 decimal token with float price @@ -38,7 +38,7 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 8; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); - expect(result).toBeCloseTo(19.125, 5); + expect(result).toBe("19.125"); }); it("correctly calculate USD amount for 1gwei token amount", () => { @@ -47,7 +47,7 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 18; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); - expect(result).toBe(0.000001); + expect(result).toBe("0.000001"); }); it("correctly truncate decimals when specified", () => { @@ -56,7 +56,27 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 18; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals, 4); - expect(result).toBe(1.5185); + expect(result).toBe("1.5185"); + }); + + it("handle token price with 19 decimal digits", () => { + const amount = 1000000000000000000n; // 1 token + const tokenPriceInUsd = 1e-19; // 19 decimal places + const tokenDecimals = 18; + + const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); + + expect(result).toBe("0.0000000000000000001"); + }); + + it("handle scientific notation token price with interspersed non-zero digits in result", () => { + const amount = 123456789012345678n; // 0.123456789012345678 tokens + const tokenPriceInUsd = 1.23e-15; // 0.00000000000000123 + const tokenDecimals = 18; + + const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); + + expect(result).toBe("0.00000000000000015185"); }); it("return zero for zero token amount", () => { @@ -65,7 +85,7 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 18; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); - expect(result).toBe(0); + expect(result).toBe("0"); }); it("should return zero for zero token price", () => { @@ -74,7 +94,7 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 18; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); - expect(result).toBe(0); + expect(result).toBe("0"); }); it("throw an error for invalid truncate decimals", () => { @@ -87,12 +107,12 @@ describe("calculateAmountInUsd", () => { }); test("migrated cases", () => { - expect(calculateAmountInUsd(3400000000000000000n, 1, 18, 8)).toBe(3.4); + expect(calculateAmountInUsd(3400000000000000000n, 1, 18, 8)).toBe("3.4"); - expect(calculateAmountInUsd(50000000000n, 1, 18, 8)).toBe(0.00000005); + expect(calculateAmountInUsd(50000000000n, 1, 18, 8)).toBe("0.00000005"); - expect(calculateAmountInUsd(3400000000000000000n, 0.5, 18, 8)).toBe(1.7); + expect(calculateAmountInUsd(3400000000000000000n, 0.5, 18, 8)).toBe("1.7"); - expect(calculateAmountInUsd(3400000000000000000n, 2, 18, 8)).toBe(6.8); + expect(calculateAmountInUsd(3400000000000000000n, 2, 18, 8)).toBe("6.8"); }); }); diff --git a/packages/repository/src/interfaces/roundRepository.interface.ts b/packages/repository/src/interfaces/roundRepository.interface.ts index 5e7b1b3..8cc2792 100644 --- a/packages/repository/src/interfaces/roundRepository.interface.ts +++ b/packages/repository/src/interfaces/roundRepository.interface.ts @@ -106,7 +106,7 @@ export interface IRoundRepository extends IRoundReadRepository { roundId: string; }, amount: bigint, - amountInUsd: number, + amountInUsd: string, ): Promise; /** diff --git a/packages/repository/src/repositories/kysely/round.repository.ts b/packages/repository/src/repositories/kysely/round.repository.ts index decc0b0..66d409b 100644 --- a/packages/repository/src/repositories/kysely/round.repository.ts +++ b/packages/repository/src/repositories/kysely/round.repository.ts @@ -111,7 +111,7 @@ export class KyselyRoundRepository implements IRoundRepository { roundId: string; }, amount: bigint, - amountInUsd: number, + amountInUsd: string, ): Promise { await this.db .withSchema(this.schemaName) diff --git a/packages/repository/src/types/changeset.types.ts b/packages/repository/src/types/changeset.types.ts index ee012e7..526332c 100644 --- a/packages/repository/src/types/changeset.types.ts +++ b/packages/repository/src/types/changeset.types.ts @@ -88,7 +88,7 @@ export type Changeset = chainId: ChainId; roundId: string; fundedAmount: bigint; - fundedAmountInUsd: number; + fundedAmountInUsd: string; }; } | { @@ -96,7 +96,7 @@ export type Changeset = args: { chainId: ChainId; roundId: Address; - amountInUsd: number; + amountInUsd: string; }; } | { diff --git a/packages/repository/src/types/round.types.ts b/packages/repository/src/types/round.types.ts index 09d8bff..a458421 100644 --- a/packages/repository/src/types/round.types.ts +++ b/packages/repository/src/types/round.types.ts @@ -16,9 +16,9 @@ export type Round = { chainId: ChainId; matchAmount: bigint; matchTokenAddress: Address; - matchAmountInUsd: number; + matchAmountInUsd: string; fundedAmount: bigint; - fundedAmountInUsd: number; + fundedAmountInUsd: string; applicationMetadataCid: string; applicationMetadata: unknown | null; roundMetadataCid: string | null; @@ -30,7 +30,7 @@ export type Round = { createdByAddress: Address; createdAtBlock: bigint; updatedAtBlock: bigint; - totalAmountDonatedInUsd: number; + totalAmountDonatedInUsd: string; totalDonationsCount: number; totalDistributed: bigint; uniqueDonorsCount: number; diff --git a/packages/shared/package.json b/packages/shared/package.json index 2faaa5b..cb4b1f7 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -28,6 +28,7 @@ "test:cov": "vitest run --config vitest.config.ts --coverage" }, "dependencies": { + "bignumber.js": "9.1.2", "viem": "2.21.19", "winston": "3.15.0" } diff --git a/packages/shared/src/external.ts b/packages/shared/src/external.ts index d2aa32f..9b1b0af 100644 --- a/packages/shared/src/external.ts +++ b/packages/shared/src/external.ts @@ -10,3 +10,6 @@ export { export type { DeepPartial } from "./utils/testing.js"; export { mergeDeep } from "./utils/testing.js"; export type { ILogger, Logger } from "./internal.js"; + +export { BigNumber } from "./internal.js"; +export type { BigNumberType } from "./internal.js"; diff --git a/packages/shared/src/internal.ts b/packages/shared/src/internal.ts index 6462239..a6a96fb 100644 --- a/packages/shared/src/internal.ts +++ b/packages/shared/src/internal.ts @@ -1,4 +1,5 @@ export type { Address } from "viem"; +export * from "./math/bignumber.js"; export * from "./types/index.js"; export * from "./constants/index.js"; export * from "./utils/testing.js"; diff --git a/packages/shared/src/math/bignumber.ts b/packages/shared/src/math/bignumber.ts new file mode 100644 index 0000000..f95f80f --- /dev/null +++ b/packages/shared/src/math/bignumber.ts @@ -0,0 +1,4 @@ +import * as b from "bignumber.js"; + +export const BigNumber = b.BigNumber.clone({ EXPONENTIAL_AT: 32 }); +export type BigNumberType = typeof BigNumber; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50f9985..36e40e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,6 +200,9 @@ importers: packages/shared: dependencies: + bignumber.js: + specifier: 9.1.2 + version: 9.1.2 viem: specifier: 2.21.19 version: 2.21.19(typescript@5.5.4)(zod@3.23.8) @@ -1524,6 +1527,12 @@ packages: integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, } + bignumber.js@9.1.2: + resolution: + { + integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==, + } + binary-extensions@2.3.0: resolution: { @@ -5360,6 +5369,8 @@ snapshots: base64-js@1.5.1: {} + bignumber.js@9.1.2: {} + binary-extensions@2.3.0: {} brace-expansion@1.1.11: