diff --git a/.gitignore b/.gitignore index 1605919..c7b34fe 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules dist coverage -.envrc \ No newline at end of file +.envrc +.history diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..58a4133 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +16.13.0 diff --git a/src/helpers/calculateNumberOfPrizesForIndex.ts b/src/helpers/calculateNumberOfPrizesForIndex.ts index eafa462..04f909c 100644 --- a/src/helpers/calculateNumberOfPrizesForIndex.ts +++ b/src/helpers/calculateNumberOfPrizesForIndex.ts @@ -5,9 +5,8 @@ export function calculateNumberOfPrizesForIndex( let bitRangeDecimal = 2 ** bitRangeSize; let numberOfPrizesForIndex = bitRangeDecimal ** prizeDistributionIndex; - while (prizeDistributionIndex > 0) { + if (prizeDistributionIndex > 0) { numberOfPrizesForIndex -= bitRangeDecimal ** (prizeDistributionIndex - 1); - prizeDistributionIndex--; } return numberOfPrizesForIndex; diff --git a/src/utils/computePrizeFromAbsolutePrizes.ts b/src/utils/computePrizeFromAbsolutePrizes.ts new file mode 100644 index 0000000..392334f --- /dev/null +++ b/src/utils/computePrizeFromAbsolutePrizes.ts @@ -0,0 +1,26 @@ +import { BigNumber, ethers } from "ethers"; +import { calculateNumberOfPrizesForIndex } from "../helpers/calculateNumberOfPrizesForIndex"; + +/** + * Calculates the total prize payout for tiers defined absolutely. + * + * @remarks + * Given tier prizes of [2500, 800], that means there is 1 $2500 prize and 3 $800 prizes. This function + * will add them all up. + * + * @param bitRangeSize The bit range to use. Determines the number of prizes per tier + * @param tierPrizes The prize tiers defined in terms of absolute prize value. + */ +export function computePrizeFromAbsolutePrizes( + bitRangeSize: number, + tierPrizes: BigNumber[] +): BigNumber { + return tierPrizes.reduce( + (tot: BigNumber, tierPrize: BigNumber, index: number): BigNumber => { + return tot.add( + tierPrize.mul(calculateNumberOfPrizesForIndex(bitRangeSize, index)) + ); + }, + ethers.BigNumber.from("0") + ); +} diff --git a/src/utils/computeTiersFromAbsolutePrizes.ts b/src/utils/computeTiersFromAbsolutePrizes.ts new file mode 100644 index 0000000..1465fb0 --- /dev/null +++ b/src/utils/computeTiersFromAbsolutePrizes.ts @@ -0,0 +1,36 @@ +import { BigNumber, ethers } from "ethers"; +import { calculateNumberOfPrizesForIndex } from "../helpers/calculateNumberOfPrizesForIndex"; +import { computePrizeFromAbsolutePrizes } from "./computePrizeFromAbsolutePrizes"; + +/** + * Converts a list of absolute prizes to a fraction array. + * + * @remarks + * Given an array of prizes, this function returns a corresponding array of fixed point 9 fractions for the prize tiers. + * + * For example, if the tierPrizes array is [2500, 800] then this function will assume: + * + * There is 1 $2500 prize, + * There are 3 $800 prizes + * + * The total prize is 2500 + 3 * 800 = 4900. + * + * The result will then be `[2500*1e9/4900, (3*800*1e9)/4900]` or [ 510204081, 489795918 ] + * + * @param bitRangeSize The bit range that determines the count for each tier of prizes + * @param tierPrizes The tiers of prizes represented as absolute amounts. + */ +export function computeTiersFromAbsolutePrizes( + bitRangeSize: number, + tierPrizes: BigNumber[] +): BigNumber[] { + const totalPrizes = computePrizeFromAbsolutePrizes(bitRangeSize, tierPrizes); + + return tierPrizes.map( + (tierPrize: BigNumber, index: number): BigNumber => + tierPrize + .mul(calculateNumberOfPrizesForIndex(bitRangeSize, index)) + .mul(ethers.utils.parseUnits("1", 9)) + .div(totalPrizes) + ); +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 982cb2e..d262cd3 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,3 +5,5 @@ export * from "./isDrawStructSet"; export * from "./isPrizeDistributionStructSet"; export * from "./sanityCheckPrizeDistribution"; export * from "./sumBigNumbers"; +export * from "./computePrizeFromAbsolutePrizes"; +export * from "./computeTiersFromAbsolutePrizes"; diff --git a/test/batchCalculateDrawResults.test.ts b/test/batchCalculateDrawResults.test.ts deleted file mode 100644 index 4ac995c..0000000 --- a/test/batchCalculateDrawResults.test.ts +++ /dev/null @@ -1,359 +0,0 @@ -import { BigNumber, ethers, utils } from "ethers"; -import { - Claim, - Draw, - DrawResults, - PrizeDistribution, - User, -} from "../src/types"; -import { batchCalculateDrawResults } from "../src/batchCalculateDrawResults"; -import { prepareClaims } from "../src/prepareClaims"; -import { calculateFractionOfPrize } from "../src/helpers/calculateFractionOfPrize"; -import { calculatePrizeAmount } from "../src/helpers/calculatePrizeAmount"; -import { findBitMatchesAtIndex } from "../src/helpers/findBitMatchesAtIndex"; -import { calculatePrizeForDistributionIndex } from "../src/helpers/calculatePrizeForDistributionIndex"; -import { formatTierToBasePercentage } from "../src/utils/formatTierToBasePercentage"; -const debug = require("debug")("v4-js-core:test"); - -describe.only("batchCalculateDrawResults()", () => { - it("Single DrawCalculator run 1 matches", async () => { - const exampleDrawSettings: PrizeDistribution = { - tiers: [ - formatTierToBasePercentage("0.3"), - formatTierToBasePercentage("0.2"), - formatTierToBasePercentage("0.1"), - ], - numberOfPicks: BigNumber.from(10), - matchCardinality: 3, - bitRangeSize: 4, - prize: BigNumber.from(utils.parseEther("100")), - maxPicksPerUser: 100, - expiryDuration: 0, - }; - - const exampleDraw: Draw = { - drawId: 1, - winningRandomNumber: BigNumber.from( - "8781184742215173699638593792190316559257409652205547100981219837421219359728" - ), - timestamp: 1, - beaconPeriodStartedAt: 1, - beaconPeriodSeconds: 1, - }; - - const exampleUser: User = { - address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - normalizedBalances: [ethers.utils.parseEther("0.2")], - }; - - const results = batchCalculateDrawResults( - [exampleDrawSettings], - [exampleDraw], - exampleUser - ); - const expectedPrize = BigNumber.from("0x94a62bef705e30"); // const prizeReceived = utils.parseEther("0.041666666666666667") - expect(results[0].totalValue).toStrictEqual(expectedPrize); - }); - - it("all matches", async () => { - const exampleUser: User = { - address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - normalizedBalances: [ethers.utils.parseEther("0.2")], - }; - - const winningNumber = utils.solidityKeccak256( - ["address"], - [exampleUser.address] - ); - const winningRandomNumber = utils.solidityKeccak256( - ["bytes32", "uint256"], - [winningNumber, 1] - ); - - debug("winning number ", winningRandomNumber); - const exampleDrawSettings: PrizeDistribution = { - tiers: [ - formatTierToBasePercentage("0.4"), - formatTierToBasePercentage("0.2"), - formatTierToBasePercentage("0.1"), - formatTierToBasePercentage("0.1"), - ], - numberOfPicks: BigNumber.from(10), - matchCardinality: 4, - bitRangeSize: 4, - prize: BigNumber.from(utils.parseEther("100")), - maxPicksPerUser: 100, - expiryDuration: 0, - }; - - const exampleDraw: Draw = { - drawId: 1, - winningRandomNumber: BigNumber.from(winningRandomNumber), - timestamp: 0, - beaconPeriodStartedAt: 0, - beaconPeriodSeconds: 0, - }; - - const results = batchCalculateDrawResults( - [exampleDrawSettings], - [exampleDraw], - exampleUser - ); - const prizeReceived = utils.parseEther("40"); - expect(results[0].totalValue).toStrictEqual(prizeReceived); - }); -}); - -describe("calculatePrizeAmount()", () => { - it("Can calculate the prize given the draw settings and number of matches", async () => { - const exampleDrawSettings: PrizeDistribution = { - tiers: [ - formatTierToBasePercentage("0.3"), - formatTierToBasePercentage("0.2"), - formatTierToBasePercentage("0.1"), - ], - numberOfPicks: BigNumber.from(10), - matchCardinality: 3, - bitRangeSize: 4, - prize: BigNumber.from(utils.parseEther("100")), - maxPicksPerUser: 100, - expiryDuration: 0, - }; - - const result = calculatePrizeAmount(exampleDrawSettings, 2); - const prizeReceived = utils.parseEther("1.25"); - expect(result!.amount).toStrictEqual(prizeReceived); - expect(result!.distributionIndex).toStrictEqual(1); - }); - it("Can calculate the prize given the draw settings and number of matches", async () => { - const exampleDrawSettings: PrizeDistribution = { - tiers: [ - formatTierToBasePercentage("0.3"), - formatTierToBasePercentage("0.2"), - formatTierToBasePercentage("0.1"), - ], - numberOfPicks: BigNumber.from(10), - matchCardinality: 3, - bitRangeSize: 4, - prize: BigNumber.from(utils.parseEther("100")), - maxPicksPerUser: 100, - expiryDuration: 0, - }; - - const result = calculatePrizeAmount(exampleDrawSettings, 3); - const prizeReceived = utils.parseEther("1.25"); - expect(result!.amount).toStrictEqual(prizeReceived); - }); -}); - -describe("findBitMatchesAtIndex()", () => { - it("Can findBitMatchesAtIndex", async () => { - const result = findBitMatchesAtIndex( - BigNumber.from(61676), - BigNumber.from(61612), - 1, - 8 - ); - expect(result).toBeTruthy(); - }); - - it("Can NOT findBitMatchesAtIndex", async () => { - const result = findBitMatchesAtIndex( - BigNumber.from(61676), - BigNumber.from(61612), - 1, - 6 - ); - expect(result).toBeFalsy(); - }); - - it("Can findBitMatchesAtIndex", async () => { - const result = findBitMatchesAtIndex( - BigNumber.from( - "24703804328475188150699190457572086651745971796997325887553663750514688469872" - ), - BigNumber.from( - "8781184742215173699638593792190316559257409652205547100981219837421219359728" - ), - 1, - 8 - ); - expect(result).toBeTruthy(); - }); - - it("Can NOT findBitMatchesAtIndex", async () => { - const result = findBitMatchesAtIndex( - BigNumber.from( - "24703804328475188150699190457572086651745971796997325887553663750514688469872" - ), - BigNumber.from( - "8781184742215173699638593792190316559257409652205547100981219837421219359728" - ), - 2, - 8 - ); - expect(result).toBeFalsy(); - }); -}); - -describe("calculatePrizeForPrizeDistributionIndex()", () => { - it("can calculate the prize awardable for the prize distribution and prize", async () => { - const exampleDrawSettings: PrizeDistribution = { - tiers: [ - formatTierToBasePercentage("0.3"), - formatTierToBasePercentage("0.2"), - formatTierToBasePercentage("0.1"), - ], - numberOfPicks: BigNumber.from(10), - matchCardinality: 3, - bitRangeSize: 4, - prize: BigNumber.from(utils.parseEther("100")), - maxPicksPerUser: 100, - expiryDuration: 0, - }; - - const prizeReceivable = calculatePrizeForDistributionIndex( - 1, - exampleDrawSettings - ); - const prize = utils.parseEther("1.25"); - expect(prizeReceivable).toStrictEqual(prize); - }); -}); - -describe("calculateFractionOfPrize()", () => { - it("can calculate the fraction for the prize distribution", async () => { - const exampleDrawSettings: PrizeDistribution = { - tiers: [ - formatTierToBasePercentage("0.3"), - formatTierToBasePercentage("0.2"), - formatTierToBasePercentage("0.1"), - ], - numberOfPicks: BigNumber.from(10), - matchCardinality: 3, - bitRangeSize: 4, - prize: BigNumber.from(utils.parseEther("100")), - maxPicksPerUser: 100, - expiryDuration: 0, - }; - const fraction = calculateFractionOfPrize(1, exampleDrawSettings); - const expectedFraction = utils.parseEther("0.0125"); - expect(fraction).toStrictEqual(expectedFraction); - }); -}); - -describe("prepareClaimForUserFromDrawResult()", () => { - it("returns correct claim struct for user", async () => { - const exampleDrawSettings: PrizeDistribution = { - tiers: [ - formatTierToBasePercentage("0.3"), - formatTierToBasePercentage("0.2"), - formatTierToBasePercentage("0.1"), - ], - numberOfPicks: BigNumber.from(10), - matchCardinality: 3, - bitRangeSize: 4, - prize: BigNumber.from(utils.parseEther("100")), - maxPicksPerUser: 100, - expiryDuration: 0, - }; - - const drawId = 2; - const winningPickIndices = BigNumber.from(1); - - const exampleDraw: Draw = { - drawId, - winningRandomNumber: BigNumber.from( - "8781184742215173699638593792190316559257409652205547100981219837421219359728" - ), - timestamp: 0, - beaconPeriodStartedAt: 0, - beaconPeriodSeconds: 0, - }; - - const exampleUser: User = { - address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - normalizedBalances: [ethers.utils.parseEther("10")], - }; - - const drawResult = batchCalculateDrawResults( - [exampleDrawSettings], - [exampleDraw], - exampleUser - ); - - const claimResult: Claim = prepareClaims(exampleUser, drawResult); - expect(claimResult.drawIds).toStrictEqual([drawId]); - expect(claimResult.data).toStrictEqual([[winningPickIndices]]); - }); -}); - -describe("prepareClaimsForUserFromDrawResults()", () => { - it("returns correct claim struct for user", async () => { - const exampleDrawSettings1: PrizeDistribution = { - tiers: [ - formatTierToBasePercentage("0.3"), - formatTierToBasePercentage("0.2"), - formatTierToBasePercentage("0.1"), - ], - numberOfPicks: BigNumber.from(10), - matchCardinality: 3, - bitRangeSize: 4, - prize: BigNumber.from(utils.parseEther("100")), - maxPicksPerUser: 100, - expiryDuration: 0, - }; - const exampleDrawSettings2: PrizeDistribution = { - tiers: [ - formatTierToBasePercentage("0.3"), - formatTierToBasePercentage("0.2"), - formatTierToBasePercentage("0.1"), - ], - numberOfPicks: BigNumber.from(10), - matchCardinality: 3, - bitRangeSize: 4, - prize: BigNumber.from(utils.parseEther("100")), - maxPicksPerUser: 100, - expiryDuration: 0, - }; - const drawIds = [2, 3]; - const winningPickIndices = BigNumber.from(1); - - const exampleDraw1: Draw = { - drawId: drawIds[0], - winningRandomNumber: BigNumber.from( - "8781184742215173699638593792190316559257409652205547100981219837421219359728" - ), - timestamp: 0, - beaconPeriodStartedAt: 0, - beaconPeriodSeconds: 0, - }; - const exampleDraw2: Draw = { - drawId: drawIds[1], - winningRandomNumber: BigNumber.from( - "8781184742215173699638593792190316559257409652205547100981219837421219359728" - ), - timestamp: 0, - beaconPeriodStartedAt: 0, - beaconPeriodSeconds: 0, - }; - - const exampleUser: User = { - address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - normalizedBalances: [ethers.utils.parseEther("10")], - }; - - const drawResults: DrawResults[] = batchCalculateDrawResults( - [exampleDrawSettings1, exampleDrawSettings2], - [exampleDraw1, exampleDraw2], - exampleUser - ); - - expect(drawResults.length).toEqual(1); // only wins exampleDraw1 - - const claimResult: Claim = prepareClaims(exampleUser, drawResults); - - expect(claimResult.drawIds).toStrictEqual([drawIds[0]]); - expect(claimResult.data).toStrictEqual([[winningPickIndices]]); - }); -}); diff --git a/test/helpers/calculateNumberOfPrizesForIndex.test.ts b/test/helpers/calculateNumberOfPrizesForIndex.test.ts index ea0a689..f03beaf 100644 --- a/test/helpers/calculateNumberOfPrizesForIndex.test.ts +++ b/test/helpers/calculateNumberOfPrizesForIndex.test.ts @@ -1,5 +1,9 @@ -describe("calculateNumberOfPrizesForIndex", () => { - it("should", () => { - expect(1 + 1).toEqual(2); - }); +import { calculateNumberOfPrizesForIndex } from "../../src/helpers"; + +describe("calculateNumberOfPrizesForIndex.test", () => { + it("0", () => expect(calculateNumberOfPrizesForIndex(2, 0)).toEqual(1)); + it("1", () => expect(calculateNumberOfPrizesForIndex(2, 1)).toEqual(3)); + it("2", () => expect(calculateNumberOfPrizesForIndex(2, 2)).toEqual(12)); + it("3", () => expect(calculateNumberOfPrizesForIndex(2, 3)).toEqual(48)); + it("4", () => expect(calculateNumberOfPrizesForIndex(2, 4)).toEqual(192)); }); diff --git a/test/utils/computePrizeFromAbsolutePrizes.test.ts b/test/utils/computePrizeFromAbsolutePrizes.test.ts new file mode 100644 index 0000000..017be22 --- /dev/null +++ b/test/utils/computePrizeFromAbsolutePrizes.test.ts @@ -0,0 +1,36 @@ +import { BigNumber } from "ethers"; +import { computePrizeFromAbsolutePrizes } from "../../src/utils"; + +describe("computePrizeFromAbsolutePrizes.test", () => { + it("should correctly sum 2 tier prizes with bit range 1", () => { + expect( + computePrizeFromAbsolutePrizes(1, [ + BigNumber.from("2500"), + BigNumber.from("800"), + ]) + ).toEqual(BigNumber.from("3300")); + }); + + it("should correctly sum 2 tier prizes with bit range 2", () => { + expect( + computePrizeFromAbsolutePrizes(2, [ + BigNumber.from("2500"), + BigNumber.from("800"), + ]) + ).toEqual(BigNumber.from("4900")); + }); + + it("should correctly sum 5 tiers prizes", () => { + expect( + computePrizeFromAbsolutePrizes(2, [ + BigNumber.from("100"), // 1 + BigNumber.from("100"), // 3 + BigNumber.from("100"), // 12 + BigNumber.from("100"), // 48 + BigNumber.from("100"), // 192 + BigNumber.from("100"), // 768 + BigNumber.from("100"), // 3072 + ]).toString() + ).toEqual(BigNumber.from("409600").toString()); + }); +}); diff --git a/test/utils/computeTiersFromAbsolutePrizes.test.ts b/test/utils/computeTiersFromAbsolutePrizes.test.ts new file mode 100644 index 0000000..ae7686a --- /dev/null +++ b/test/utils/computeTiersFromAbsolutePrizes.test.ts @@ -0,0 +1,13 @@ +import { BigNumber } from "ethers"; +import { computeTiersFromAbsolutePrizes } from "../../src/utils"; + +describe("computeTiersFromAbsolutePrizes.test", () => { + it("should correctly sum 2 tier prizes", () => { + expect( + computeTiersFromAbsolutePrizes(2, [ + BigNumber.from("2500"), + BigNumber.from("800"), + ]) + ).toEqual([BigNumber.from("510204081"), BigNumber.from("489795918")]); + }); +});