diff --git a/ensawards.org/data/awards/types.ts b/ensawards.org/data/awards/types.ts index 0c970b40..93f39bf8 100644 --- a/ensawards.org/data/awards/types.ts +++ b/ensawards.org/data/awards/types.ts @@ -2,10 +2,11 @@ import type { App } from "data/apps/types.ts"; import type { EntityMetadata } from "data/entity-metadata/types.ts"; import type { IncentiveProgram, IncentiveProgramSlug } from "data/incentive-programs/types.ts"; import type { Protocol } from "data/protocols/types.ts"; -import { type EnsTokens } from "data/shared/ensTokens.ts"; import type { TransactionRef } from "data/shared/transactionRef.ts"; import type { AccountId, UnixTimestamp } from "enssdk"; +import type { Price } from "@ensnode/ensnode-sdk"; + export const AwardTypes = { FinancialAward: "financial-award", RecognitionAward: "recognition-award", @@ -53,11 +54,11 @@ export interface AwardAbstract { export interface AwardRecognition extends AwardAbstract {} export interface AwardFinancial extends AwardAbstract { - /** Amount of the award in $ENS. + /** Financial award in any currency supported by {@link Price}. * * @invariant Award amount must be finite and greater than 0. */ - price: EnsTokens; // should be replaced with `Price` from enssdk when Issue#1941 is completed. + price: Price; /** * Details of the transaction associated with the distribution of this award. diff --git a/ensawards.org/data/awards/utils.test.ts b/ensawards.org/data/awards/utils.test.ts index 4b7a4c32..19561872 100644 --- a/ensawards.org/data/awards/utils.test.ts +++ b/ensawards.org/data/awards/utils.test.ts @@ -2,12 +2,72 @@ import type { ChainId } from "enssdk"; import { zeroAddress, zeroHash } from "viem"; import { describe, expect, it } from "vitest"; +import { CurrencyIds, parseEnsTokens, parseUsdc } from "@ensnode/ensnode-sdk"; + import { type AwardFinancial, AwardTypes } from "./types.ts"; -import { sortFinancialAwardsByPrice } from "./utils.ts"; +import { isValidAwardValue, sortFinancialAwardsByPrice } from "./utils.ts"; describe("awards utils", () => { + describe("isValidAwardValue", () => { + it("should return true for valid award values", () => { + expect( + isValidAwardValue(parseUsdc("1000")), + "Should return true for positive finite amounts", + ).toBe(true); + }); + + it("should return false for invalid award values", () => { + expect(isValidAwardValue(parseUsdc("0")), "Should return false for zero amount").toBe(false); + + // Not possible to have negative amounts with parseX function (it will throw instead), + // but we want to ensure the validation function + // behaves correctly if it receives such input + expect( + isValidAwardValue({ currency: CurrencyIds.USDC, amount: -10n }), + "Should return false for negative amount", + ).toBe(false); + }); + }); + describe("sortFinancialAwardsByPrice", () => { const placeholderChainId: ChainId = 1; + + it("Should throw an error if awards have different currencies", () => { + const awardA: AwardFinancial = { + associatedIncentiveProgramSlug: "placeholder-incentive-program", + type: AwardTypes.FinancialAward, + awardedTo: { + chainId: placeholderChainId, + address: zeroAddress, + }, + price: parseUsdc("100"), + awardedAt: 1, + transaction: { + chainId: placeholderChainId, + transactionHash: zeroHash, + }, + }; + + const awardB: AwardFinancial = { + associatedIncentiveProgramSlug: "placeholder-incentive-program", + type: AwardTypes.FinancialAward, + awardedTo: { + chainId: placeholderChainId, + address: zeroAddress, + }, + price: parseEnsTokens("100"), + awardedAt: 1, + transaction: { + chainId: placeholderChainId, + transactionHash: zeroHash, + }, + }; + + expect(() => sortFinancialAwardsByPrice(awardA, awardB)).toThrow( + "Cannot compare awards with `price` in different currencies: USDC vs ENSTokens", + ); + }); + it("should sort awards by award value in descending order", () => { const awards: AwardFinancial[] = [ { @@ -17,7 +77,7 @@ describe("awards utils", () => { chainId: placeholderChainId, address: zeroAddress, }, - price: 100, + price: parseEnsTokens("100"), awardedAt: 1, transaction: { chainId: placeholderChainId, @@ -31,7 +91,7 @@ describe("awards utils", () => { chainId: placeholderChainId, address: zeroAddress, }, - price: 200, + price: parseEnsTokens("200"), awardedAt: 2, transaction: { chainId: placeholderChainId, @@ -45,7 +105,7 @@ describe("awards utils", () => { chainId: placeholderChainId, address: zeroAddress, }, - price: 150, + price: parseEnsTokens("150"), awardedAt: 0, transaction: { chainId: placeholderChainId, @@ -62,7 +122,7 @@ describe("awards utils", () => { chainId: placeholderChainId, address: zeroAddress, }, - price: 200, + price: parseEnsTokens("200"), awardedAt: 2, transaction: { chainId: placeholderChainId, @@ -76,7 +136,7 @@ describe("awards utils", () => { chainId: placeholderChainId, address: zeroAddress, }, - price: 150, + price: parseEnsTokens("150"), awardedAt: 0, transaction: { chainId: placeholderChainId, @@ -90,7 +150,7 @@ describe("awards utils", () => { chainId: placeholderChainId, address: zeroAddress, }, - price: 100, + price: parseEnsTokens("100"), awardedAt: 1, transaction: { chainId: placeholderChainId, @@ -114,7 +174,7 @@ describe("awards utils", () => { chainId: placeholderChainId, address: zeroAddress, }, - price: 100, + price: parseEnsTokens("100"), awardedAt: 2, transaction: { chainId: placeholderChainId, @@ -128,7 +188,7 @@ describe("awards utils", () => { chainId: placeholderChainId, address: zeroAddress, }, - price: 100, + price: parseEnsTokens("100"), awardedAt: 1, transaction: { chainId: placeholderChainId, @@ -142,7 +202,7 @@ describe("awards utils", () => { chainId: placeholderChainId, address: zeroAddress, }, - price: 100, + price: parseEnsTokens("100"), awardedAt: 3, transaction: { chainId: placeholderChainId, @@ -159,7 +219,7 @@ describe("awards utils", () => { chainId: placeholderChainId, address: zeroAddress, }, - price: 100, + price: parseEnsTokens("100"), awardedAt: 1, transaction: { chainId: placeholderChainId, @@ -173,7 +233,7 @@ describe("awards utils", () => { chainId: placeholderChainId, address: zeroAddress, }, - price: 100, + price: parseEnsTokens("100"), awardedAt: 2, transaction: { chainId: placeholderChainId, @@ -187,7 +247,7 @@ describe("awards utils", () => { chainId: placeholderChainId, address: zeroAddress, }, - price: 100, + price: parseEnsTokens("100"), awardedAt: 3, transaction: { chainId: placeholderChainId, diff --git a/ensawards.org/data/awards/utils.ts b/ensawards.org/data/awards/utils.ts index 91c991f3..7be4f3cb 100644 --- a/ensawards.org/data/awards/utils.ts +++ b/ensawards.org/data/awards/utils.ts @@ -1,16 +1,27 @@ -import { type EnsTokens } from "data/shared/ensTokens"; +import { interpretCurrency } from "data/shared/currencies"; -import { type Award, type AwardFinancial } from "./types"; +import type { Price } from "@ensnode/ensnode-sdk"; + +import { type AwardFinancial } from "./types"; /** - * Sorts {@link Award}s of {@link AwardFinancial} type. + * Sorts {@link AwardFinancial}s. * - * Prioritizes awards with higher {@link AwardFinancial.price} and, + * Prioritizes awards with higher {@link AwardFinancial.price.amount} and, * in case of a tie, earlier {@link AwardFinancial.awardedAt} date. + * + * @throws + * If the two awards have different {@link AwardFinancial.price.currency} values. */ export const sortFinancialAwardsByPrice = (a: AwardFinancial, b: AwardFinancial): number => { - if (a.price > b.price) return -1; - if (a.price < b.price) return 1; + if (a.price.currency !== b.price.currency) { + throw new Error( + `Cannot compare awards with \`price\` in different currencies: ${a.price.currency} vs ${b.price.currency}`, + ); + } + + if (a.price.amount > b.price.amount) return -1; + if (a.price.amount < b.price.amount) return 1; return a.awardedAt - b.awardedAt; }; @@ -19,6 +30,13 @@ export const sortFinancialAwardsByPrice = (a: AwardFinancial, b: AwardFinancial) * Checks if a given award value is valid * according to the invariants defined in {@link AwardFinancial}. */ -export const isValidAwardValue = (awardValue: EnsTokens): boolean => { - return Number.isFinite(awardValue) && awardValue > 0; +export const isValidAwardValue = (awardPrice: Price): boolean => { + // Check the raw `Price.amount` value first + if (awardPrice.amount <= 0n) { + return false; + } + + // Then check the user-facing interpreted value + const interpretedAmount = interpretCurrency(awardPrice); + return Number.isFinite(interpretedAmount) && interpretedAmount > 0; }; diff --git a/ensawards.org/data/incentive-programs/ens-contract-naming-season/awards.ts b/ensawards.org/data/incentive-programs/ens-contract-naming-season/awards.ts index 876c0e80..0b3523c2 100644 --- a/ensawards.org/data/incentive-programs/ens-contract-naming-season/awards.ts +++ b/ensawards.org/data/incentive-programs/ens-contract-naming-season/awards.ts @@ -12,7 +12,7 @@ import Superfluid from "data/protocols/superfluid-defi"; import { asNormalizedAddress } from "enssdk"; import { mainnet } from "viem/chains"; -import { parseTimestamp } from "@ensnode/ensnode-sdk"; +import { parseEnsTokens, parseTimestamp } from "@ensnode/ensnode-sdk"; const marchBulkDistribution = { timestamp: parseTimestamp("2026-03-06T11:21:23Z"), @@ -42,7 +42,7 @@ const ensContractNamingSeasonAwards: Award[] = [ type: EntityMetadataTypes.Protocol, protocol: NounsDao, }, - price: 500, + price: parseEnsTokens("500"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Medium project", @@ -58,7 +58,7 @@ const ensContractNamingSeasonAwards: Award[] = [ type: EntityMetadataTypes.Protocol, protocol: LiquityDeFi, }, - price: 500, + price: parseEnsTokens("500"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Medium project", @@ -74,7 +74,7 @@ const ensContractNamingSeasonAwards: Award[] = [ type: EntityMetadataTypes.Protocol, protocol: CorkDeFi, }, - price: 500, + price: parseEnsTokens("500"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Medium project", @@ -90,7 +90,7 @@ const ensContractNamingSeasonAwards: Award[] = [ type: EntityMetadataTypes.Protocol, protocol: GivethDeFi, }, - price: 500, + price: parseEnsTokens("500"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Medium project", @@ -107,7 +107,7 @@ const ensContractNamingSeasonAwards: Award[] = [ name: "Based Nouns", link: new URL("https://nouns.build/"), }, - price: 100, + price: parseEnsTokens("100"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Small project", @@ -123,7 +123,7 @@ const ensContractNamingSeasonAwards: Award[] = [ type: EntityMetadataTypes.App, app: BlockscoutExplorer, }, - price: 1195, + price: parseEnsTokens("1195"), awardedAt: parseTimestamp("2026-01-30T17:46:59Z"), transaction: { chainId: mainnet.id, @@ -138,7 +138,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x843819e77947e2ca4f198dfa9c32cf49b598ef4b"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -150,7 +150,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0xd1da830e7d175ec8a51103bcfbbbe32a9362a6b2"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -162,7 +162,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x1208a26faa0f4ac65b42098419eb4daa5e580ac6"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -179,7 +179,7 @@ const ensContractNamingSeasonAwards: Award[] = [ name: "Bittrees", link: new URL("https://bittrees.org/"), }, - price: 100, + price: parseEnsTokens("100"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Small project", @@ -191,7 +191,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x42204dc2efcfcb0f148d94f97348edb11b1e3f85"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -203,7 +203,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x66a5699dc1882a9ca9e7270ff5e7b49d412c2f4f"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -220,7 +220,7 @@ const ensContractNamingSeasonAwards: Award[] = [ name: "ENSWheel", link: new URL("https://x.com/enswheel"), }, - price: 100, + price: parseEnsTokens("100"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Small project", @@ -232,7 +232,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0xc0d86456f6f2930b892f3dad007cdbe32c081fe6"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -244,7 +244,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x6fef965fe935ebe7c569176c857e1b5e5ee8c34b"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -256,7 +256,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0xd804edb35d367b1a877b3544e85b16c96681a775"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -268,7 +268,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x364056980867d1655897299889bca4e7d465b395"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -280,7 +280,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0xae7e2aa76f463ca04812906f9c8c3d870704bd8d"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -292,7 +292,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x5629aea7c12097bb4af9920577dcd5fc33d3f77e"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -309,7 +309,7 @@ const ensContractNamingSeasonAwards: Award[] = [ name: "SimpleX Network", link: new URL("https://simplex.chat/"), }, - price: 100, + price: parseEnsTokens("100"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Small project", @@ -321,7 +321,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x51050ec063d393217b436747617ad1c2285aeeee"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -333,7 +333,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x7e5a0b21cc3c606f8d488a194e705ed74c0acb95"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -350,7 +350,7 @@ const ensContractNamingSeasonAwards: Award[] = [ name: "EthereumPhunks", link: new URL("https://ethereumphunks.com/"), }, - price: 100, + price: parseEnsTokens("100"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Small project", @@ -362,7 +362,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x995f47734ec1b19baad65944504d71362a502daa"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -374,7 +374,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0xf1c7c037891525e360c59f708739ac09a7670c59"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -386,7 +386,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0xcf42f35a7db4b37769b8519b323202a32520e673"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -398,7 +398,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0xd1177f978a5535eba843bdd817e730df1c42c476"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Individual", @@ -420,7 +420,7 @@ const ensContractNamingSeasonAwards: Award[] = [ name: "Gramajo", link: new URL("https://0xgramajo.xyz/"), }, - price: 100, + price: parseEnsTokens("100"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Contributions and collaborations", @@ -437,7 +437,7 @@ const ensContractNamingSeasonAwards: Award[] = [ name: "Dev Tools Guild", link: new URL("https://devtoolsguild.xyz/"), }, - price: 500, + price: parseEnsTokens("500"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Medium project", @@ -454,7 +454,7 @@ const ensContractNamingSeasonAwards: Award[] = [ name: "Proof Of Groove", link: new URL("https://www.proofofgroove.xyz/"), }, - price: 100, + price: parseEnsTokens("100"), awardedAt: marchBulkDistribution.timestamp, transaction: marchBulkDistribution.transaction, reason: "Small project", @@ -470,7 +470,7 @@ const ensContractNamingSeasonAwards: Award[] = [ type: EntityMetadataTypes.Protocol, protocol: Superfluid, }, - price: 500, + price: parseEnsTokens("500"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Medium project", @@ -486,7 +486,7 @@ const ensContractNamingSeasonAwards: Award[] = [ type: EntityMetadataTypes.Protocol, protocol: Kleros, }, - price: 500, + price: parseEnsTokens("500"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Medium project", @@ -502,7 +502,7 @@ const ensContractNamingSeasonAwards: Award[] = [ type: EntityMetadataTypes.Protocol, protocol: SSVNetworkDao, }, - price: 1000, + price: parseEnsTokens("1000"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Large project", @@ -519,7 +519,7 @@ const ensContractNamingSeasonAwards: Award[] = [ name: "ENSFall", link: new URL("https://ensfall.xyz/"), }, - price: 100, + price: parseEnsTokens("100"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Small project", @@ -556,7 +556,7 @@ const ensContractNamingSeasonAwards: Award[] = [ name: "Elimu", link: new URL("https://elimu.ai/"), }, - price: 200, + price: parseEnsTokens("200"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Small project, named twice", @@ -573,7 +573,7 @@ const ensContractNamingSeasonAwards: Award[] = [ name: "Liaison Capital", link: new URL("https://opensea.io/collection/liaisoncapital"), }, - price: 100, + price: parseEnsTokens("100"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Small project", @@ -585,7 +585,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x02b60f47eecadc7b34591ffb9add33e3014b3c83"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Individual", @@ -597,7 +597,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x8562e4199743671322652dd7955920d7bb0fd372"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Individual", @@ -609,7 +609,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x07f62f48709b2c427e54e102531d07d9e21ad99e"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Individual", @@ -621,7 +621,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x733455e2f63f5b1c812a00a93da46d504f7b52a4"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Individual", @@ -633,7 +633,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0xe2eedd9ab6522f0fbef92e3483208708a2e3352f"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Individual", @@ -645,7 +645,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x89c828be2e60624710423f1e6eaff34f7d4c8ae3"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Individual", @@ -657,7 +657,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0xf462e5dac4ee5ee9d6bfd5a5c633a5104612fd59"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Individual", @@ -669,7 +669,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x05592957fb56bd230f8fa41515ed902a1d3e94d0"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Individual", @@ -681,7 +681,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x5ab137b17c3584a9debba742964f09f84a4a5a7c"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Individual", @@ -693,7 +693,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0xb19918e6fcd9a5373a1e967aaa38c24f7c667f3b"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Individual", @@ -705,7 +705,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x69eab63384c37d44a18401fd765e4c54df2b4185"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Individual", @@ -717,7 +717,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x926a19d7429f9ad47b2cb2b0e5c46a9e69f05a3e"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Individual", @@ -729,7 +729,7 @@ const ensContractNamingSeasonAwards: Award[] = [ chainId: mainnet.id, address: asNormalizedAddress("0x5560650bf336dafc2aab23e6c2bcc78028877725"), }, - price: 10, + price: parseEnsTokens("10"), awardedAt: mayBulkDistribution.timestamp, transaction: mayBulkDistribution.transaction, reason: "Individual", diff --git a/ensawards.org/data/incentive-programs/ens-contract-naming-season/index.ts b/ensawards.org/data/incentive-programs/ens-contract-naming-season/index.ts index 105c1114..a11d0957 100644 --- a/ensawards.org/data/incentive-programs/ens-contract-naming-season/index.ts +++ b/ensawards.org/data/incentive-programs/ens-contract-naming-season/index.ts @@ -1,13 +1,15 @@ import { defineIncentiveProgram } from "data/incentive-programs/registry"; import { type IncentiveProgram, IncentiveProgramTypes } from "data/incentive-programs/types"; +import { parseEnsTokens } from "@ensnode/ensnode-sdk"; + const EnsContractNamingSeason: IncentiveProgram = { type: IncentiveProgramTypes.AwardPool, incentiveProgramSlug: "ens-contract-naming-season", displayName: "ENS Contract Naming Season", description: "Elevate your protocol’s onchain identity! This 6-month ENS DAO-backed initiative rewards DAOs, protocols, and dApps who name their smart contracts.", - totalAwardPool: 10000, // $ENS Tokens + totalAwardPool: parseEnsTokens("10000"), }; defineIncentiveProgram(EnsContractNamingSeason); diff --git a/ensawards.org/data/incentive-programs/index.test.ts b/ensawards.org/data/incentive-programs/index.test.ts index 474f4dbc..463c9e6e 100644 --- a/ensawards.org/data/incentive-programs/index.test.ts +++ b/ensawards.org/data/incentive-programs/index.test.ts @@ -1,4 +1,7 @@ -import type { IncentiveProgramSlug } from "data/incentive-programs/types"; +import { AwardTypes } from "data/awards/types"; +import { isValidAwardValue } from "data/awards/utils"; +import { type IncentiveProgramSlug, IncentiveProgramTypes } from "data/incentive-programs/types"; +import { getAwardsByIncentiveProgramSlug } from "data/incentive-programs/utils"; import { isValidSlug } from "data/shared/slugs"; import { describe, expect, it } from "vitest"; @@ -25,4 +28,32 @@ describe("Incentive Program data", () => { true, ); }); + + it("For the AwardPool type, all of program's awards should be of the same currency as the program's totalAwardPool", () => { + data.forEach((incentiveProgram) => { + if (incentiveProgram.type === IncentiveProgramTypes.AwardPool) { + const programCurrency = incentiveProgram.totalAwardPool.currency; + + getAwardsByIncentiveProgramSlug(incentiveProgram.incentiveProgramSlug) + .filter((award) => award.type === AwardTypes.FinancialAward) + .forEach((financialAward) => { + expect( + financialAward.price.currency, + `Financial award with transaction hash ${financialAward.transaction.transactionHash} has currency ${financialAward.price.currency} which does not match the program's totalAwardPool currency ${programCurrency}`, + ).toEqual(programCurrency); + }); + } + }); + }); + + it("For the AwardPool type, the program's `totalAwardPool` should be valid", () => { + data.forEach((incentiveProgram) => { + if (incentiveProgram.type === IncentiveProgramTypes.AwardPool) { + expect( + isValidAwardValue(incentiveProgram.totalAwardPool), + `Incentive program with slug ${incentiveProgram.incentiveProgramSlug} has invalid totalAwardPool value ${incentiveProgram.totalAwardPool.amount}`, + ).toEqual(true); + } + }); + }); }); diff --git a/ensawards.org/data/incentive-programs/types.ts b/ensawards.org/data/incentive-programs/types.ts index 0c4f19c0..db5fe0fc 100644 --- a/ensawards.org/data/incentive-programs/types.ts +++ b/ensawards.org/data/incentive-programs/types.ts @@ -1,6 +1,7 @@ -import { type EnsTokens } from "data/shared/ensTokens"; import { ENSAWARDS_SLUG_PATTERN } from "data/shared/slugs"; +import type { Price } from "@ensnode/ensnode-sdk"; + /** A unique identifier for an {@link IncentiveProgram}. * * @invariant Must be unique across all incentive programs. @@ -30,8 +31,16 @@ export interface IncentiveProgramAwardPool { /** * The total award pool allocated for this incentive program. + * + * @invariant + * Award pool amount must be finite and greater than 0. + * + * @invariant + * All financial awards associated with this incentive program + * must have a {@link Price.currency} that matches the + * {@link totalAwardPool.currency} of this `totalAwardPool`. */ - totalAwardPool: EnsTokens; // TODO: Should be of type Price once Issue#1941 in ensnode is resolved + totalAwardPool: Price; } export type IncentiveProgram = IncentiveProgramAwardPool; diff --git a/ensawards.org/data/incentive-programs/utils.ts b/ensawards.org/data/incentive-programs/utils.ts index 9d975f68..f5c5aa7e 100644 --- a/ensawards.org/data/incentive-programs/utils.ts +++ b/ensawards.org/data/incentive-programs/utils.ts @@ -2,6 +2,7 @@ import { AWARDS } from "data/awards"; import { type Award, AwardTypes } from "data/awards/types"; import { INCENTIVE_PROGRAMS } from "data/incentive-programs"; import type { IncentiveProgram, IncentiveProgramSlug } from "data/incentive-programs/types"; +import { interpretCurrency } from "data/shared/currencies"; export const getIncentiveProgramBySlug = ( incentiveProgramSlug: IncentiveProgramSlug, @@ -29,7 +30,16 @@ export const sumIncentiveProgramFinancialAwards = ( incentiveProgramSlug, ).filter((award) => award.type === AwardTypes.FinancialAward); - return incentiveProgramFinancialAwards.reduce((acc, award) => acc + award.price, 0); + if (incentiveProgramFinancialAwards.length === 0) { + return 0; + } + + const totalFinancialAwards = incentiveProgramFinancialAwards.reduce( + (acc, award) => ({ currency: acc.currency, amount: acc.amount + award.price.amount }), + { currency: incentiveProgramFinancialAwards[0].price.currency, amount: 0n }, + ); + + return interpretCurrency(totalFinancialAwards); }; /** @@ -50,7 +60,8 @@ export const calcIncentiveProgramRemainingAwardPool = ( } const remainingAwardPool = - incentiveProgram.totalAwardPool - sumIncentiveProgramFinancialAwards(incentiveProgramSlug); + interpretCurrency(incentiveProgram.totalAwardPool) - + sumIncentiveProgramFinancialAwards(incentiveProgramSlug); if (remainingAwardPool < 0) { throw new Error( diff --git a/ensawards.org/data/shared/currencies.ts b/ensawards.org/data/shared/currencies.ts new file mode 100644 index 00000000..92176ae4 --- /dev/null +++ b/ensawards.org/data/shared/currencies.ts @@ -0,0 +1,22 @@ +import { getCurrencyInfo, type Price } from "@ensnode/ensnode-sdk"; + +//TODO: See https://github.com/namehash/ensawards/issues/151 +/** + * Converts the parsed currency representation from the {@link Price} form in its smallest unit + * back to its original numeric value. + * + * **Note** For large values this conversion may lead to loss of precision + * + * @param value - a {@link Price} object with the amount in the smallest unit + * @returns A number representing the actual amount of the given currency + * + * @example + * Based on the USDC currency + * interpretCurrency({ currency: "USDC", amount: 123456780n }) // returns 123.45678 + * interpretCurrency({ currency: "USDC", amount: 1000000n }) // returns 1 + * interpretCurrency({ currency: "USDC", amount: 1000n }) // returns 0.001 + */ +export const interpretCurrency = (value: Price): number => { + const currencyInfo = getCurrencyInfo(value.currency); + return Number(value.amount) / Math.pow(10, currencyInfo.decimals); +}; diff --git a/ensawards.org/data/shared/ensTokens.ts b/ensawards.org/data/shared/ensTokens.ts index 94c2af07..a5205d6c 100644 --- a/ensawards.org/data/shared/ensTokens.ts +++ b/ensawards.org/data/shared/ensTokens.ts @@ -1,8 +1,3 @@ -/** An amount in units of $ENS */ -export type EnsTokens = number; -// TODO: Use an import from ensnode-sdk when the support for $ENS is implemented. -// See https://github.com/namehash/ensnode/issues/1941 - // TODO: Add dynamic currency conversion rate lookups export const ENS_TOKENS_TO_USDC_CONVERSION_RATE = 5.72; diff --git a/ensawards.org/src/components/atoms/cards/contractNamingSeasonAwardCard/index.tsx b/ensawards.org/src/components/atoms/cards/contractNamingSeasonAwardCard/index.tsx index 90ef7d86..060970f8 100644 --- a/ensawards.org/src/components/atoms/cards/contractNamingSeasonAwardCard/index.tsx +++ b/ensawards.org/src/components/atoms/cards/contractNamingSeasonAwardCard/index.tsx @@ -6,6 +6,7 @@ import { useNow, } from "@namehash/namehash-ui"; import { type AwardFinancial } from "data/awards/types"; +import { interpretCurrency } from "data/shared/currencies"; import { ENS_TOKENS_TO_USDC_CONVERSION_RATE, ensTokenFormatter } from "data/shared/ensTokens"; import { secondsInMinute } from "date-fns/constants"; import { useCallback, useMemo } from "react"; @@ -38,7 +39,7 @@ export const ContractNamingSeasonAwardCard = ({ award }: ContractNamingSeasonAwa ); const awardedToDetailsUrl = getAdvocateDetailsUrl(award.awardedTo.address); - const estimatedValueUSD = award.price * ENS_TOKENS_TO_USDC_CONVERSION_RATE; + const estimatedValueUSD = interpretCurrency(award.price) * ENS_TOKENS_TO_USDC_CONVERSION_RATE; const awardedContentWrapper = useCallback( ({ children }: React.PropsWithChildren): React.ReactNode => @@ -99,7 +100,7 @@ export const ContractNamingSeasonAwardCard = ({ award }: ContractNamingSeasonAwa Award

- {ensTokenFormatter.format(award.price)} $ENS + {ensTokenFormatter.format(interpretCurrency(award.price))} $ENS

diff --git a/ensawards.org/src/components/atoms/cards/referralProgramEditionCard/rev-share/hero.tsx b/ensawards.org/src/components/atoms/cards/referralProgramEditionCard/rev-share/hero.tsx index c76d5483..b3498ead 100644 --- a/ensawards.org/src/components/atoms/cards/referralProgramEditionCard/rev-share/hero.tsx +++ b/ensawards.org/src/components/atoms/cards/referralProgramEditionCard/rev-share/hero.tsx @@ -1,5 +1,6 @@ import { ReferralProgramAwardModels } from "@namehash/ens-referrals"; import { useNow } from "@namehash/namehash-ui"; +import { interpretCurrency } from "data/shared/currencies"; import { secondsInMinute } from "date-fns/constants"; import { useEffect, useState } from "react"; @@ -10,10 +11,7 @@ import { ReferralProgramEditionRules, ReferralProgramEditionTimePeriod, } from "@/components/atoms/cards/referralProgramEditionCard/shared"; -import { - getReferralProgramEditionSummaryBySlug, - parseReferralProgramCurrency, -} from "@/utils/referralProgram"; +import { getReferralProgramEditionSummaryBySlug } from "@/utils/referralProgram"; import { cn } from "@/utils/tailwindClassConcatenation.ts"; import { usdFormatter } from "@/utils/textModifications"; @@ -94,7 +92,7 @@ export const ReferralProgramEditionHeroCardRevShareCap = ({

{referralProgramEditionSummaryData.awardModel === ReferralProgramAwardModels.RevShareCap - ? `${usdFormatter.format(parseReferralProgramCurrency(referralProgramEditionSummaryData.rules.minBaseRevenueContribution))} USD` + ? `${usdFormatter.format(interpretCurrency(referralProgramEditionSummaryData.rules.minBaseRevenueContribution))} USD` : "-"}

diff --git a/ensawards.org/src/components/atoms/cards/referralProgramEditionCard/shared.tsx b/ensawards.org/src/components/atoms/cards/referralProgramEditionCard/shared.tsx index f804aa70..b83d1332 100644 --- a/ensawards.org/src/components/atoms/cards/referralProgramEditionCard/shared.tsx +++ b/ensawards.org/src/components/atoms/cards/referralProgramEditionCard/shared.tsx @@ -1,4 +1,5 @@ import type { ReferralProgramEditionSummary } from "@namehash/ens-referrals"; +import { interpretCurrency } from "data/shared/currencies"; import { type UnixTimestamp } from "enssdk"; import { ChevronRightIcon } from "lucide-react"; import type { PropsWithChildren } from "react"; @@ -8,7 +9,6 @@ import type { PriceUsdc } from "@ensnode/ensnode-sdk"; import { ReferralProgramPeriodDate } from "@/components/atoms/ReferralProgramPeriodDate"; import { Skeleton } from "@/components/ui/skeleton.tsx"; import { TooltipProvider } from "@/components/ui/tooltip"; -import { parseReferralProgramCurrency } from "@/utils/referralProgram"; import { cn } from "@/utils/tailwindClassConcatenation.ts"; import { usdFormatter } from "@/utils/textModifications"; @@ -96,7 +96,7 @@ export const ReferralProgramEditionAwardPool = ({ "text-sm leading-normal font-medium text-black max-sm:text-right cursor-default", )} > - {usdFormatter.format(parseReferralProgramCurrency(totalAwardPoolValue))} USD + {usdFormatter.format(interpretCurrency(totalAwardPoolValue))} USD

); diff --git a/ensawards.org/src/components/atoms/cards/referrerCard/pie-split/index.tsx b/ensawards.org/src/components/atoms/cards/referrerCard/pie-split/index.tsx index 3ade5ef5..2bdddda3 100644 --- a/ensawards.org/src/components/atoms/cards/referrerCard/pie-split/index.tsx +++ b/ensawards.org/src/components/atoms/cards/referrerCard/pie-split/index.tsx @@ -4,10 +4,10 @@ import { ReferralProgramEditionStatuses, type ReferralProgramEditionStatusId, } from "@namehash/ens-referrals"; +import { interpretCurrency } from "data/shared/currencies"; import { memo } from "react"; import { GenericTooltip } from "@/components/atoms/GenericTooltip.tsx"; -import { parseReferralProgramCurrency } from "@/utils/referralProgram.ts"; import { cn } from "@/utils/tailwindClassConcatenation.ts"; import { usdFormatter } from "@/utils/textModifications.ts"; @@ -131,9 +131,7 @@ function ReferrerCardPieSplit({ )} > {referrer.isQualified ? ( - <> - US {usdFormatter.format(parseReferralProgramCurrency(referrer.awardPoolApproxValue))} - + <>US {usdFormatter.format(interpretCurrency(referrer.awardPoolApproxValue))} ) : ( "-" )} diff --git a/ensawards.org/src/components/atoms/cards/referrerCard/rev-share/index.tsx b/ensawards.org/src/components/atoms/cards/referrerCard/rev-share/index.tsx index 49764245..5578a66a 100644 --- a/ensawards.org/src/components/atoms/cards/referrerCard/rev-share/index.tsx +++ b/ensawards.org/src/components/atoms/cards/referrerCard/rev-share/index.tsx @@ -10,6 +10,7 @@ import { ReferralProgramEditionStatuses, SECONDS_PER_YEAR, } from "@namehash/ens-referrals"; +import { interpretCurrency } from "data/shared/currencies"; import { TriangleAlert as RulesBreachIcon } from "lucide-react"; import { memo } from "react"; @@ -19,7 +20,6 @@ import { ReferrerCardHeader, } from "@/components/atoms/cards/referrerCard/shared"; import { GenericTooltip } from "@/components/atoms/GenericTooltip.tsx"; -import { parseReferralProgramCurrency } from "@/utils/referralProgram.ts"; import { cn } from "@/utils/tailwindClassConcatenation.ts"; import { usdFormatter } from "@/utils/textModifications"; @@ -81,7 +81,7 @@ export const TotalRevenueContributionField = ({

- Ξ {ethFormatter.format(parseReferralProgramCurrency(referrer.totalRevenueContribution))} + Ξ {ethFormatter.format(interpretCurrency(referrer.totalRevenueContribution))}

); @@ -96,7 +96,7 @@ export const BaseRevenueContributionField = ({ ); const totalBaseRevenueContributionInUSD = usdFormatter.format( - parseReferralProgramCurrency(referrer.totalBaseRevenueContribution), + interpretCurrency(referrer.totalBaseRevenueContribution), ); const userFacingTotalBaseRevenueContribution = @@ -109,10 +109,8 @@ export const BaseRevenueContributionField = ({ content={

Measured as US{" "} - {usdFormatter.format( - parseReferralProgramCurrency(editionRules.baseAnnualRevenueContribution), - )}{" "} - × {referralYears} years of registrations and renewals referred by this referrer during + {usdFormatter.format(interpretCurrency(editionRules.baseAnnualRevenueContribution))} ×{" "} + {referralYears} years of registrations and renewals referred by this referrer during this edition. Excludes premium revenue sources.

} @@ -144,12 +142,12 @@ export const RevenueShareField = ({ editionRules, }: Omit) => { const minQualifiedRevenueContributionInUSD = usdFormatter.format( - parseReferralProgramCurrency(editionRules.minBaseRevenueContribution), + interpretCurrency(editionRules.minBaseRevenueContribution), ); const qualifiedRevenueSharePercentage = `${Math.round(editionRules.maxBaseRevenueShare * 100)}%`; const additionalRevenueRequiredInUSD = usdFormatter.format( - parseReferralProgramCurrency({ + interpretCurrency({ currency: editionRules.minBaseRevenueContribution.currency, amount: editionRules.minBaseRevenueContribution.amount - @@ -256,7 +254,7 @@ export const TentativeAwardsField = ({ )} > {referrer.isQualified ? ( - <>US {usdFormatter.format(parseReferralProgramCurrency(referrer.cappedAward))} + <>US {usdFormatter.format(interpretCurrency(referrer.cappedAward))} ) : ( "-" )} diff --git a/ensawards.org/src/components/molecules/ReferralProgramEditionInfo.tsx b/ensawards.org/src/components/molecules/ReferralProgramEditionInfo.tsx index b4dbfc5a..5381143b 100644 --- a/ensawards.org/src/components/molecules/ReferralProgramEditionInfo.tsx +++ b/ensawards.org/src/components/molecules/ReferralProgramEditionInfo.tsx @@ -3,6 +3,7 @@ import { type ReferralProgramEditionSummaryPieSplit, type ReferralProgramEditionSummaryRevShareCap, } from "@namehash/ens-referrals"; +import { interpretCurrency } from "data/shared/currencies"; import { ReferralProgramStatusBadge } from "@/components/atoms/badges/ReferralProgramStatusBadge"; import { @@ -11,7 +12,6 @@ import { ReferralProgramEditionRules, ReferralProgramEditionTimePeriod, } from "@/components/atoms/cards/referralProgramEditionCard/shared.tsx"; -import { parseReferralProgramCurrency } from "@/utils/referralProgram.ts"; import { usdFormatter } from "@/utils/textModifications.ts"; interface ReferralProgramEditionInfoProps { @@ -69,7 +69,7 @@ export const ReferralProgramEditionInfo = ({

{usdFormatter.format( - parseReferralProgramCurrency(referralProgramEditionSummary.awardPoolRemaining), + interpretCurrency(referralProgramEditionSummary.awardPoolRemaining), )}{" "} USD

diff --git a/ensawards.org/src/utils/referralProgram.ts b/ensawards.org/src/utils/referralProgram.ts index 0599cc74..10dafe93 100644 --- a/ensawards.org/src/utils/referralProgram.ts +++ b/ensawards.org/src/utils/referralProgram.ts @@ -11,8 +11,6 @@ import { } from "@namehash/ens-referrals"; import { isValidSlug } from "data/shared/slugs"; -import { getCurrencyInfo, type Price } from "@ensnode/ensnode-sdk"; - import { getENSNodeUrl } from "@/utils/env"; export const filterOutUnrecognizedEditions = ( @@ -73,23 +71,3 @@ export async function fetchReferralProgramEditionSummaries(): Promise< ); } } - -//TODO: See https://github.com/namehash/ensawards/issues/151 -/** - * Converts the parsed currency representation in its smallest unit back to its original value. - * - * **Note** For large values this parsing may lead to loss of precision - * - * @param value - a {@link Price} object with the amount in the smallest unit - * @returns A number representing the actual amount of the given currency - * - * @example - * Based on the USDC currency - * parseReferralProgramCurrency({ currency: "USDC", amount: 123456780n }) // returns 123.45678 - * parseReferralProgramCurrency({ currency: "USDC", amount: 1000000n }) // returns 1 - * parseReferralProgramCurrency({ currency: "USDC", amount: 1000n }) // returns 0.001 - */ -export const parseReferralProgramCurrency = (value: Price): number => { - const currencyInfo = getCurrencyInfo(value.currency); - return Number(value.amount) / Math.pow(10, currencyInfo.decimals); -};