diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d297b756..3c52b28d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -289,22 +289,26 @@ For reference, see [ensawards.org/data/ens-best-practices/contract-naming/name-y * user interactions to be evaluated, * and any relevant technical details or considerations. * - * @note The description should not include examples of passed or failed benchmarks, + * @note The description should not include examples of + * passed, partially passed, failed, or not applicable benchmarks, * there are dedicated fields for that * (see {@link AcceptanceTest.examplePass}, * {@link AcceptanceTest.examplePartialPass}, - * or {@link AcceptanceTest.exampleFail}). + * {@link AcceptanceTest.exampleFail}, + * or {@link AcceptanceTest.exampleNotApplicable}). */ description: JSX.Element; /** * Examples of benchmark results that illustrate - * what a passing, partially passing, or failing result + * what a passing, partially passing, failing, + * or not applicable result * looks like for this acceptance test. */ examplePass: AcceptanceTestBenchmarkPass; examplePartialPass?: AcceptanceTestBenchmarkPartialPass; exampleFail?: AcceptanceTestBenchmarkFail; + exampleNotApplicable?: NotApplicableAcceptanceTestBenchmark; } ``` 4. In your PR describe your reasoning for adding it. @@ -371,6 +375,7 @@ export const BenchmarkResults = { Pass: "passed", PartialPass: "partially-passed", Fail: "failed", + NotApplicable: "not-applicable", } as const; export type BenchmarkResult = (typeof BenchmarkResults)[keyof typeof BenchmarkResults]; @@ -444,10 +449,23 @@ export interface AcceptanceTestBenchmarkPartialPass export interface AcceptanceTestBenchmarkFail extends AcceptanceTestBenchmarkAbstract {} -export type AcceptanceTestBenchmark = +/** + * Represents a benchmark of an {@link AcceptanceTest} on an {@link App} against a {@link BestPractice}, + * that is not applicable to the acceptance test scenario. + * Most often, this is because the app doesn't use ENS at all, + * in places where it should. + */ +export interface NotApplicableAcceptanceTestBenchmark + extends AcceptanceTestBenchmarkAbstract {} + +export type ApplicableAcceptanceTestBenchmark = | AcceptanceTestBenchmarkPass | AcceptanceTestBenchmarkPartialPass | AcceptanceTestBenchmarkFail; + +export type AcceptanceTestBenchmark = + | ApplicableAcceptanceTestBenchmark + | NotApplicableAcceptanceTestBenchmark; ``` 3. Add notes made during the benchmarking process in the form of a simple JSX element that is a part of the new item in the `benchmarks` record. For reference, see diff --git a/ensawards.org/data/acceptance-tests/types.ts b/ensawards.org/data/acceptance-tests/types.ts index b41ac673..96256a02 100644 --- a/ensawards.org/data/acceptance-tests/types.ts +++ b/ensawards.org/data/acceptance-tests/types.ts @@ -27,22 +27,26 @@ export interface AcceptanceTest { * user interactions to be evaluated, * and any relevant technical details or considerations. * - * @note The description should not include examples of passed or failed benchmarks, + * @note The description should not include examples of + * passed, partially passed, failed, or not applicable benchmarks, * there are dedicated fields for that * (see {@link AcceptanceTest.examplePass}, * {@link AcceptanceTest.examplePartialPass}, - * or {@link AcceptanceTest.exampleFail}). + * {@link AcceptanceTest.exampleFail}, + * or {@link AcceptanceTest.exampleNotApplicable}). */ description: JSX.Element; /** * Examples of benchmark results that illustrate - * what a passing, partially passing, or failing result + * what a passing, partially passing, failing, + * or not applicable result * looks like for this acceptance test. */ examplePass: AcceptanceTestBenchmarkPass; examplePartialPass?: AcceptanceTestBenchmarkPartialPass; exampleFail?: AcceptanceTestBenchmarkFail; + exampleNotApplicable?: AcceptanceTestBenchmarkNotApplicable; } /** @@ -92,7 +96,20 @@ export interface AcceptanceTestBenchmarkPartialPass export interface AcceptanceTestBenchmarkFail extends AcceptanceTestBenchmarkAbstract {} -export type AcceptanceTestBenchmark = +/** + * Represents a benchmark of an {@link AcceptanceTest} on an {@link App} against a {@link BestPractice}, + * that is not applicable to the acceptance test scenario. + * Most often, this is because the app doesn't use ENS at all, + * in places where it should. + */ +export interface AcceptanceTestBenchmarkNotApplicable + extends AcceptanceTestBenchmarkAbstract {} + +export type AcceptanceTestBenchmarkApplicable = | AcceptanceTestBenchmarkPass | AcceptanceTestBenchmarkPartialPass | AcceptanceTestBenchmarkFail; + +export type AcceptanceTestBenchmark = + | AcceptanceTestBenchmarkApplicable + | AcceptanceTestBenchmarkNotApplicable; diff --git a/ensawards.org/data/acceptance-tests/utils.test.ts b/ensawards.org/data/acceptance-tests/utils.test.ts index 36761478..b07c1744 100644 --- a/ensawards.org/data/acceptance-tests/utils.test.ts +++ b/ensawards.org/data/acceptance-tests/utils.test.ts @@ -25,25 +25,32 @@ describe("Acceptance test utils", () => { }, ); - it("Returns `BenchmarkResults.Fail` if all defined benchmarks are `BenchmarkResults.Fail`", () => { - const expectedResult = BenchmarkResults.Fail; + it( + "Returns `BenchmarkResults.Fail` if in all defined benchmarks there is at least one `BenchmarkResults.Fail`" + + " and all others are `BenchmarkResults.Fail` or `BenchmarkResults.NotApplicable`", + () => { + const expectedResult = BenchmarkResults.Fail; - const inputBenchmarks = { - "mock-acceptance-test-1": createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), - "mock-acceptance-test-2": createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), - "mock-acceptance-test-3": undefined, - // pending benchmarks should be ignored in this case of the generalization - } as const satisfies AcceptanceTestBenchmarks; + const inputBenchmarks = { + "mock-acceptance-test-1": createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + "mock-acceptance-test-2": createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + "mock-acceptance-test-3": createMockAcceptanceTestBenchmark( + BenchmarkResults.NotApplicable, + ), + "mock-acceptance-test-4": undefined, + // pending benchmarks should be ignored in this case of the generalization + } as const satisfies AcceptanceTestBenchmarks; - expect( - generalizeAcceptanceTestBenchmarks(inputBenchmarks), - "generalizeAcceptanceTestBenchmarks should return `BenchmarkResults.Fail`", - ).toEqual(expectedResult); - }); + expect( + generalizeAcceptanceTestBenchmarks(inputBenchmarks), + "generalizeAcceptanceTestBenchmarks should return `BenchmarkResults.Fail`", + ).toEqual(expectedResult); + }, + ); it( "Returns `BenchmarkResults.PartialPass` if at least one defined benchmark is `BenchmarkResults.Fail`" + - " and at least one defined benchmark is `BenchmarkResults.Pass`", + " and at least one defined benchmark is `BenchmarkResults.Pass` or `BenchmarkResults.PartialPass`", () => { const expectedResult = BenchmarkResults.PartialPass; @@ -78,18 +85,44 @@ describe("Acceptance test utils", () => { ).toEqual(expectedResult); }); - it("Returns `undefined` if all benchmarks are `undefined` (pending)", () => { - const expectedResult = undefined; + it("Returns `BenchmarkResults.NotApplicable` if all benchmarks are defined and `BenchmarkResults.NotApplicable`", () => { + const expectedResult = BenchmarkResults.NotApplicable; const inputBenchmarks = { + "mock-acceptance-test-1": createMockAcceptanceTestBenchmark(BenchmarkResults.NotApplicable), + "mock-acceptance-test-2": createMockAcceptanceTestBenchmark(BenchmarkResults.NotApplicable), + "mock-acceptance-test-3": createMockAcceptanceTestBenchmark(BenchmarkResults.NotApplicable), + } as const satisfies AcceptanceTestBenchmarks; + + expect( + generalizeAcceptanceTestBenchmarks(inputBenchmarks), + "generalizeAcceptanceTestBenchmarks should return `BenchmarkResults.NotApplicable`", + ).toEqual(expectedResult); + }); + + it("Returns `undefined` if all benchmarks are `undefined` (pending) or all defined benchmarks are `BenchmarkResults.NotApplicable`", () => { + const expectedResult = undefined; + + const inputBenchmarks1 = { "mock-acceptance-test-1": undefined, "mock-acceptance-test-2": undefined, } as const satisfies AcceptanceTestBenchmarks; + const inputBenchmarks2 = { + "mock-acceptance-test-1": createMockAcceptanceTestBenchmark(BenchmarkResults.NotApplicable), + "mock-acceptance-test-2": createMockAcceptanceTestBenchmark(BenchmarkResults.NotApplicable), + "mock-acceptance-test-3": undefined, + } as const satisfies AcceptanceTestBenchmarks; + expect( - generalizeAcceptanceTestBenchmarks(inputBenchmarks), + generalizeAcceptanceTestBenchmarks(inputBenchmarks1), "generalizeAcceptanceTestBenchmarks should return `undefined` for all pending benchmarks", ).toEqual(expectedResult); + + expect( + generalizeAcceptanceTestBenchmarks(inputBenchmarks2), + "generalizeAcceptanceTestBenchmarks should return `undefined` for all defined benchmarks being `NotApplicable`", + ).toEqual(expectedResult); }); }); }); diff --git a/ensawards.org/data/acceptance-tests/utils.ts b/ensawards.org/data/acceptance-tests/utils.ts index e3b414ce..4f7101c0 100644 --- a/ensawards.org/data/acceptance-tests/utils.ts +++ b/ensawards.org/data/acceptance-tests/utils.ts @@ -55,8 +55,10 @@ export const getAcceptanceTestBenchmarksByApp = ( * {@link BenchmarkResults.Pass} * - and all others are {@link BenchmarkResults.Pass} or {@link BenchmarkResults.PartialPass} * - * - Returns {@link BenchmarkResults.Fail} if all defined benchmarks - * are {@link BenchmarkResults.Fail}, + * - Returns {@link BenchmarkResults.Fail} if: + * - in all defined benchmarks there is at least one + * {@link BenchmarkResults.Fail} + * - and all others are {@link BenchmarkResults.Fail} or {@link BenchmarkResults.NotApplicable} * * - Returns {@link BenchmarkResults.PartialPass} if: * - at least one defined benchmark is {@link BenchmarkResults.Fail} @@ -64,7 +66,11 @@ export const getAcceptanceTestBenchmarksByApp = ( * {@link BenchmarkResults.Pass} or {@link BenchmarkResults.PartialPass}, * - or all defined benchmarks are {@link BenchmarkResults.PartialPass}, * - * - Returns `undefined` if all benchmarks are `undefined` (pending). + * - Returns {@link BenchmarkResults.NotApplicable} if: + * - all benchmarks are **defined** and {@link BenchmarkResults.NotApplicable} + * + * - Returns `undefined` if all benchmarks are `undefined` (pending) + * or all defined benchmarks are {@link BenchmarkResults.NotApplicable}. */ export const generalizeAcceptanceTestBenchmarks = ( acceptanceTestBenchmarks: AcceptanceTestBenchmarks, @@ -75,7 +81,19 @@ export const generalizeAcceptanceTestBenchmarks = ( const definedBenchmarkResults = benchmarkResults.filter((result) => result !== undefined); - if (definedBenchmarkResults.length === 0) { + const allBenchmarksNotApplicable = definedBenchmarkResults.every( + (result) => result === BenchmarkResults.NotApplicable, + ); + + // We want to be very strict about returning NotApplicable, + // so we only return it if all benchmarks are defined and `NotApplicable`. + if (allBenchmarksNotApplicable && definedBenchmarkResults.length === benchmarkResults.length) { + return BenchmarkResults.NotApplicable; + } + + // And for all possible mixes of pending and NotApplicable, + // we want to return undefined (pending). + if (definedBenchmarkResults.length === 0 || allBenchmarksNotApplicable) { return undefined; } @@ -97,8 +115,10 @@ export const generalizeAcceptanceTestBenchmarks = ( return BenchmarkResults.Pass; } + // For now, we'll explicitly treat fail and not applicable equally + // (For cases where not all benchmarks are not applicable) const allDefinedBenchmarksFail = definedBenchmarkResults.every( - (result) => result === BenchmarkResults.Fail, + (result) => result === BenchmarkResults.Fail || result === BenchmarkResults.NotApplicable, ); if (allDefinedBenchmarksFail) { diff --git a/ensawards.org/data/apps/binance-exchange/benchmarks/index.tsx b/ensawards.org/data/apps/binance-exchange/benchmarks/index.tsx index 2026f882..9ce7bff5 100644 --- a/ensawards.org/data/apps/binance-exchange/benchmarks/index.tsx +++ b/ensawards.org/data/apps/binance-exchange/benchmarks/index.tsx @@ -19,7 +19,7 @@ import correctlyResolveEnsv2TestNameAddressProofImageWithdrawal from "./correctl const benchmarks: BestPracticeBenchmarks = { "ensv2-ready-resolution": { "correctly-resolve-ensv2-test-name-address": { - result: BenchmarkResults.Fail, + result: BenchmarkResults.NotApplicable, contributions: [ { from: contributors.y3drk, lastUpdated: parseTimestamp("2026-06-11T07:30:06Z") }, ], @@ -27,18 +27,29 @@ const benchmarks: BestPracticeBenchmarks = {

ENSv2 ready resolution was tested using the search tool in the "copy-trading" - flow. The app either doesn't allow using ENS name as the trader identifier or fails to - resolve it, both of which we interpret as a failure. + flow. The app doesn't support the use of ENS names at all as the trader identifier. +
+
+ While that's a key issue that this app is encouraged to improve, this best practice is + applicable specifically to apps that already have an existing ENS integration and making + sure existing integrations are ENSv2 compatible. Therefore, for this best practice we + apply a rating of not applicable.

Binance exchange doesn't allow ENS name as trader in the copy-trading flow +

The ENSv2 ready resolution was also tested using the "withdrawal" flow. The - app doesn't allow using ENS name as the recipient identifier, which we interpret as a - failure. + app doesn't support the use of ENS names at all as the recipient identifier. +
+
+ While that's a key issue that this app is encouraged to improve, this best practice is + applicable specifically to apps that already have an existing ENS integration and making + sure existing integrations are ENSv2 compatible. Therefore, for this best practice we + apply a rating of not applicable.

Binance exchange doesn't allow ENS name as recipient in the withdrawal flow

ENSv2 ready resolution was tested using the "send" flow. The wallet doesn't - allow using ENS name as the recipient identifier, which we interpret as a failure. + support the use of ENS names at all as the recipient identifier. +
+
+ While that's a key issue that this app is encouraged to improve, this best practice is + applicable specifically to apps that already have an existing ENS integration and making + sure existing integrations are ENSv2 compatible. Therefore, for this best practice we + apply a rating of not applicable.

Binance Wallet doesn't allow ENS name as recipient in the send flow

ENSv2 ready resolution was tested using the "withdrawal" flow. The app doesn't - allow using ENS name as the recipient identifier, which we interpret as a failure. + support the use of ENS names at all as the recipient identifier. +
+
+ While that's a key issue that this app is encouraged to improve, this best practice is + applicable specifically to apps that already have an existing ENS integration and making + sure existing integrations are ENSv2 compatible. Therefore, for this best practice we + apply a rating of not applicable.

Crypto.com exchange doesn't allow ENS name as recipient in the withdrawal flow

ENSv2 ready resolution was tested using the "withdrawal" flow. The app doesn't - allow using ENS name as the recipient identifier, which we interpret as a failure. + support the use of ENS names at all as the recipient identifier. +
+
+ While that's a key issue that this app is encouraged to improve, this best practice is + applicable specifically to apps that already have an existing ENS integration and making + sure existing integrations are ENSv2 compatible. Therefore, for this best practice we + apply a rating of not applicable.

Kraken exchange doesn't allow ENS name as recipient in the withdrawal flow

ENSv2 ready resolution was tested using the "withdrawal" flow. The app doesn't - allow using ENS name as the recipient identifier, which we interpret as a failure. + support the use of ENS names at all as the recipient identifier. +
+
+ While that's a key issue that this app is encouraged to improve, this best practice is + applicable specifically to apps that already have an existing ENS integration and making + sure existing integrations are ENSv2 compatible. Therefore, for this best practice we + apply a rating of not applicable.

OKX exchange doesn't allow ENS name as recipient in the withdrawal flow

- ENSv2 ready resolution was tested using the "send" flow. The app doesn't allow - using ENS name as the recipient identifier, which we interpret as a failure. + ENSv2 ready resolution was tested using the "send" flow. The app doesn't + support the use of ENS names at all as the recipient identifier. +
+
+ While that's a key issue that this app is encouraged to improve, this best practice is + applicable specifically to apps that already have an existing ENS integration and making + sure existing integrations are ENSv2 compatible. Therefore, for this best practice we + apply a rating of not applicable.

ReadyX doesn't allow ENS name as recipient in the send flow

- ENSv2 ready resolution was tested using the "send" flow. The app doesn't allow - using ENS name as the recipient identifier, which we interpret as a failure. + ENSv2 ready resolution was tested using the "send" flow. The app doesn't + support the use of ENS names at all as the recipient identifier. +
+
+ While that's a key issue that this app is encouraged to improve, this best practice is + applicable specifically to apps that already have an existing ENS integration and making + sure existing integrations are ENSv2 compatible. Therefore, for this best practice we + apply a rating of not applicable.

ENSv2 ready resolution was tested using the "send" flow. The wallet doesn't - allow using ENS name as the recipient identifier, which we interpret as a failure. + support the use of ENS names at all as the recipient identifier. +
+
+ While that's a key issue that this app is encouraged to improve, this best practice is + applicable specifically to apps that already have an existing ENS integration and making + sure existing integrations are ENSv2 compatible. Therefore, for this best practice we + apply a rating of not applicable.

Robinhood Wallet doesn't allow ENS name as recipient in the send flow ({ - mockApps: [] as App[], - mockEnsAwardsPoints: vi.fn(), - mockBenchmarks: {} as AppBenchmarks, - mockGetAcceptanceTestBenchmarksByApp: vi.fn(), - })); +const { + mockApps, + mockEnsAwardsPoints, + mockBenchmarks, + mockGetAcceptanceTestBenchmarksByApp, + mockGetBestPracticeBySlug, +} = vi.hoisted(() => ({ + mockApps: [] as App[], + mockEnsAwardsPoints: vi.fn(), + mockBenchmarks: {} as AppBenchmarks, + mockGetAcceptanceTestBenchmarksByApp: vi.fn(), + mockGetBestPracticeBySlug: vi.fn(), +})); vi.mock("./index.ts", () => ({ APPS: mockApps, @@ -30,16 +37,29 @@ vi.mock("data/benchmarks/index.ts", () => ({ APP_BENCHMARKS: mockBenchmarks, })); -vi.mock("data/benchmarks/utils.ts", () => ({ - calcEnsAwardsPoints: mockEnsAwardsPoints, - getAppBenchmarks: (slug: AppSlug) => mockBenchmarks[slug], -})); +vi.mock(import("data/benchmarks/utils.ts"), async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + calcEnsAwardsPoints: mockEnsAwardsPoints, + getAppBenchmarks: (slug: AppSlug) => mockBenchmarks[slug], + }; +}); vi.mock("data/acceptance-tests/utils.ts", () => ({ getAcceptanceTestBenchmarksByApp: mockGetAcceptanceTestBenchmarksByApp, })); +vi.mock(import("data/ens-best-practices/utils.ts"), async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getBestPracticeBySlug: mockGetBestPracticeBySlug, + }; +}); + import type { AcceptanceTestBenchmark } from "data/acceptance-tests/types.ts"; +import type { BestPracticeSlug } from "data/ens-best-practices/types.ts"; import { appliesToAllApps, @@ -56,6 +76,18 @@ const setMockApps = (...apps: App[]) => { }; describe("App utils", () => { + const mockBestPractice1 = createMockBestPractice({ + id: "mock-bp-1", + name: "Mock Best Practice 1", + bestPracticeSlug: "mock-best-practice-1", + categoryId: "mock-category-1", + categoryName: "Mock Category 1", + categorySlug: "mock-category-1", + }); + const mockNotApplicableBenchmarkResult = createMockAcceptanceTestBenchmark( + BenchmarkResults.NotApplicable, + ); + beforeEach(() => { mockApps.splice(0, mockApps.length); mockEnsAwardsPoints.mockReset(); @@ -86,10 +118,39 @@ describe("App utils", () => { return Object.values(mockBenchmarks[mockMetamaskApp.appSlug]).flatMap( (bestPracticeBenchmarks) => Object.values(bestPracticeBenchmarks), ); + + case mockEtherscanApp.appSlug: + return Object.values(mockBenchmarks[mockEtherscanApp.appSlug]).flatMap( + (bestPracticeBenchmarks) => Object.values(bestPracticeBenchmarks), + ); + default: throw new Error(`No benchmarks defined for app with slug ${appSlug}`); } }); + + mockGetBestPracticeBySlug.mockReset(); + mockGetBestPracticeBySlug.mockImplementation((bestPracticeSlug: BestPracticeSlug) => { + switch (bestPracticeSlug) { + case mockReverseResolutionBestPractice.bestPracticeSlug: + return mockReverseResolutionBestPractice; + + case mockDisplayProfilesBestPractice.bestPracticeSlug: + return mockDisplayProfilesBestPractice; + + case mockForwardResolutionBestPractice.bestPracticeSlug: + return mockForwardResolutionBestPractice; + + case mockNormalizeNamesBestPractice.bestPracticeSlug: + return mockNormalizeNamesBestPractice; + + case mockBestPractice1.bestPracticeSlug: + return mockBestPractice1; + + default: + throw new Error(`No best practice defined for slug ${bestPracticeSlug}`); + } + }); }); describe("validateAppType", () => { @@ -160,15 +221,39 @@ describe("App utils", () => { [mockNormalizeNamesBestPractice.bestPracticeSlug]: { mockAcceptanceTestSlug4: undefined, }, + [mockBestPractice1.bestPracticeSlug]: { + mockAcceptanceTestSlug5: mockNotApplicableBenchmarkResult, + }, }; - const result = calcAppScore(mockCoinbaseWalletApp); + const result = calcAppScore(mockCoinbaseWalletApp).score; expect(result, `Expected ENSAwards score to be 67 got ${result}`).toEqual(67); }); - it("Should return undefined when the app has no defined benchmarks", () => { + it("Should return undefined when the app has no defined benchmarks or all benchmark results are `NotApplicable`", () => { mockBenchmarks[mockCoinbaseWalletApp.appSlug] = { + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: undefined, + }, + [mockDisplayProfilesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug2: undefined, + }, + [mockForwardResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug3: undefined, + }, + [mockNormalizeNamesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug4: mockNotApplicableBenchmarkResult, + }, + }; + const resultMixed = calcAppScore(mockCoinbaseWalletApp); + + expect( + resultMixed.score, + `Expected ENSAwards score to be undefined got ${resultMixed}`, + ).toBeUndefined(); + + mockBenchmarks[mockRainbowApp.appSlug] = { [mockReverseResolutionBestPractice.bestPracticeSlug]: { mockAcceptanceTestSlug1: undefined, }, @@ -182,9 +267,12 @@ describe("App utils", () => { mockAcceptanceTestSlug4: undefined, }, }; - const result = calcAppScore(mockCoinbaseWalletApp); + const resultAllUndefined = calcAppScore(mockRainbowApp); - expect(result, `Expected ENSAwards score to be undefined got ${result}`).toBeUndefined(); + expect( + resultAllUndefined.score, + `Expected ENSAwards score to be undefined got ${resultAllUndefined.score}`, + ).toBeUndefined(); }); it("Should throw when the calculated score is greater than 100", () => { @@ -241,6 +329,9 @@ describe("App utils", () => { [mockNormalizeNamesBestPractice.bestPracticeSlug]: { mockAcceptanceTestSlug4: undefined, }, + [mockBestPractice1.bestPracticeSlug]: { + mockAcceptanceTestSlug4: mockNotApplicableBenchmarkResult, + }, }; mockBenchmarks[mockRainbowApp.appSlug] = { @@ -254,6 +345,9 @@ describe("App utils", () => { mockAcceptanceTestSlug3: createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), }, [mockNormalizeNamesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug4: mockNotApplicableBenchmarkResult, + }, + [mockBestPractice1.bestPracticeSlug]: { mockAcceptanceTestSlug4: undefined, }, }; @@ -268,13 +362,39 @@ describe("App utils", () => { [mockForwardResolutionBestPractice.bestPracticeSlug]: { mockAcceptanceTestSlug3: undefined, }, + [mockBestPractice1.bestPracticeSlug]: { + mockAcceptanceTestSlug4: undefined, + }, [mockNormalizeNamesBestPractice.bestPracticeSlug]: { mockAcceptanceTestSlug4: undefined, }, }; - const apps = [mockMetamaskApp, mockRainbowApp, mockCoinbaseWalletApp]; - const expectedOrder = [mockCoinbaseWalletApp, mockRainbowApp, mockMetamaskApp]; + mockBenchmarks[mockEtherscanApp.appSlug] = { + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: mockNotApplicableBenchmarkResult, + }, + [mockDisplayProfilesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug2: mockNotApplicableBenchmarkResult, + }, + [mockForwardResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug3: mockNotApplicableBenchmarkResult, + }, + [mockBestPractice1.bestPracticeSlug]: { + mockAcceptanceTestSlug4: mockNotApplicableBenchmarkResult, + }, + [mockNormalizeNamesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug4: mockNotApplicableBenchmarkResult, + }, + }; + + const apps = [mockMetamaskApp, mockRainbowApp, mockCoinbaseWalletApp, mockEtherscanApp]; + const expectedOrder = [ + mockCoinbaseWalletApp, + mockRainbowApp, + mockEtherscanApp, + mockMetamaskApp, + ]; const sortedApps = apps.sort(sortApps); sortedApps.forEach((app, index) => { diff --git a/ensawards.org/data/apps/utils.ts b/ensawards.org/data/apps/utils.ts index 07f8ea3c..11a95e41 100644 --- a/ensawards.org/data/apps/utils.ts +++ b/ensawards.org/data/apps/utils.ts @@ -1,18 +1,31 @@ +import type { + AcceptanceTestBenchmark, + AcceptanceTestBenchmarkApplicable, +} from "data/acceptance-tests/types.ts"; import { getAcceptanceTestBenchmarksByApp } from "data/acceptance-tests/utils.ts"; import { AWARDS } from "data/awards/index.ts"; import type { Award } from "data/awards/types.ts"; -import { calcEnsAwardsPoints } from "data/benchmarks/utils.ts"; +import { BenchmarkResults } from "data/benchmarks/types.ts"; +import { calcEnsAwardsPoints, getAppBenchmarks } from "data/benchmarks/utils.ts"; +import { getBestPracticeBySlug } from "data/ens-best-practices/utils.ts"; import { EntityMetadataTypes } from "data/entity-metadata/types.ts"; import { asEnsAwardsScore, type EnsAwardsPoints, type EnsAwardsScore, + type EnsAwardsScoreResult, + EnsAwardsScoreResultTypes, + EnsAwardsUndefinedScoreLabels, } from "data/shared/ens-awards-score.ts"; import type { FormatTypeOptions } from "data/shared/format-type-options.ts"; import { getEnsAwardsBaseUrl } from "@/utils/index.ts"; -import type { BestPractice, BestPracticeTarget } from "../ens-best-practices/types.ts"; +import { + type BestPractice, + type BestPracticeTarget, + CategoryStatuses, +} from "../ens-best-practices/types.ts"; import { APPS } from "./index.ts"; import { type App, type AppSlug, type AppType, AppTypes } from "./types.ts"; @@ -60,26 +73,58 @@ export const getAppByName = (appName: string): App | undefined => { /** * Calculates {@link EnsAwardsScore} for an app. * - * @returns undefined - if no benchmarks are completed or - * if all completed benchmarks belong to a `BestPracticeCategory` + * @returns + * An {@link EnsAwardsScoreResult} object containing the score and a label describing the result. + * The {@link EnsAwardsScoreResult.score} field is: + * - undefined - if no benchmarks are completed, all completed benchmarks returned a not applicable result, + * or if all completed benchmarks belong to a `BestPracticeCategory` * with status other than `Active`. + * - an {@link EnsAwardsScore} calculation for the `App` otherwise. */ -export const calcAppScore = (app: App): EnsAwardsScore | undefined => { +export const calcAppScore = (app: App): EnsAwardsScoreResult => { const completedAcceptanceTestBenchmarks = getAcceptanceTestBenchmarksByApp(app.appSlug).filter( (acceptanceTestBenchmark) => acceptanceTestBenchmark !== undefined, ); - if (completedAcceptanceTestBenchmarks.length === 0) return undefined; - - const totalPoints: EnsAwardsPoints = completedAcceptanceTestBenchmarks.reduce( + if (completedAcceptanceTestBenchmarks.length === 0) + return { + type: EnsAwardsScoreResultTypes.Undefined, + score: undefined, + label: EnsAwardsUndefinedScoreLabels.Pending, + }; + + const completedApplicableAcceptanceTestBenchmarks: AcceptanceTestBenchmarkApplicable[] = + completedAcceptanceTestBenchmarks.filter( + (benchmark) => + // explicitly exclude benchmarks with `NotApplicable` result + benchmark.result !== BenchmarkResults.NotApplicable, + ); + + // For an overall app if the only benchmarks we have + // are `Pending` or `Not Applicable`, + // the overall app score should still be `Pending`. + if (completedApplicableAcceptanceTestBenchmarks.length === 0) + return { + type: EnsAwardsScoreResultTypes.Undefined, + score: undefined, + label: EnsAwardsUndefinedScoreLabels.Pending, + }; + + const totalPoints: EnsAwardsPoints = completedApplicableAcceptanceTestBenchmarks.reduce( (sum, benchmark) => sum + calcEnsAwardsPoints(benchmark), 0, ); // Guarantee EnsAwardsScore type invariant by rounding the score to the nearest integer - const score = Math.round((totalPoints * 100) / completedAcceptanceTestBenchmarks.length); + const score = Math.round( + (totalPoints * 100) / completedApplicableAcceptanceTestBenchmarks.length, + ); - return asEnsAwardsScore(score); + return { + type: EnsAwardsScoreResultTypes.Defined, + score: asEnsAwardsScore(score), + label: undefined, + }; }; /** @@ -90,16 +135,23 @@ export const appliesToAllApps = (targets: BestPracticeTarget[]): boolean => /** * Sorts two {@link App}s based on their {@link EnsAwardsScore}. + * For apps with `undefined` score, + * the one with more benchmarks with `NotApplicable` result is ranked higher. */ export const sortApps = (a: App, b: App): number => { - const aScore = calcAppScore(a); - const bScore = calcAppScore(b); + const aScoreResult = calcAppScore(a); + const bScoreResult = calcAppScore(b); - if (aScore === undefined && bScore === undefined) return 0; - if (bScore === undefined) return -1; - if (aScore === undefined) return 1; + if (aScoreResult.score === undefined && bScoreResult.score === undefined) { + const aNotApplicableBenchmarks = calcNotApplicableAppBenchmarks(a); + const bNotApplicableBenchmarks = calcNotApplicableAppBenchmarks(b); + return bNotApplicableBenchmarks - aNotApplicableBenchmarks; + } + + if (bScoreResult.score === undefined) return -1; + if (aScoreResult.score === undefined) return 1; - return bScore - aScore; + return bScoreResult.score - aScoreResult.score; }; /** Builds the URL for an app's Open Graph image. @@ -166,3 +218,34 @@ export const getAwardsByAppSlug = (appSlug: AppSlug): Award[] => award.awardedEntityMetadata?.type === EntityMetadataTypes.App && award.awardedEntityMetadata.app.appSlug === appSlug, ); + +/** + * Calculates the number of benchmarks with `NotApplicable` result for a given {@link App}. + * + * Excludes the benchmarks that belong to an inactive {@link BestPracticeCategory}. + */ +const calcNotApplicableAppBenchmarks = (app: App): number => { + const benchmarksInActiveCategories: AcceptanceTestBenchmark[] = []; + + Object.entries(getAppBenchmarks(app.appSlug)).forEach( + ([bestPracticeSlug, acceptanceTestBenchmarks]) => { + const bestPractice = getBestPracticeBySlug(bestPracticeSlug); + + if (bestPractice === undefined) { + throw new Error( + `Invariant(BestPracticeSlug): Best practice with slug ${bestPracticeSlug} is not defined`, + ); + } + + if (bestPractice.category.status === CategoryStatuses.Active) { + benchmarksInActiveCategories.push( + ...Object.values(acceptanceTestBenchmarks).filter((benchmark) => benchmark !== undefined), + ); + } + }, + ); + + return benchmarksInActiveCategories.filter( + (benchmark) => benchmark.result === BenchmarkResults.NotApplicable, + ).length; +}; diff --git a/ensawards.org/data/apps/worldapp-wallet/benchmarks/index.tsx b/ensawards.org/data/apps/worldapp-wallet/benchmarks/index.tsx index 6193bcfe..0534606b 100644 --- a/ensawards.org/data/apps/worldapp-wallet/benchmarks/index.tsx +++ b/ensawards.org/data/apps/worldapp-wallet/benchmarks/index.tsx @@ -29,7 +29,7 @@ const benchmarks: BestPracticeBenchmarks = { }, "ensv2-ready-resolution": { "correctly-resolve-ensv2-test-name-address": { - result: BenchmarkResults.Fail, + result: BenchmarkResults.NotApplicable, contributions: [ { from: contributors.y3drk, lastUpdated: parseTimestamp("2026-06-09T07:49:00Z") }, ], @@ -37,7 +37,13 @@ const benchmarks: BestPracticeBenchmarks = {

ENSv2 ready resolution was tested using the "send" flow. The wallet doesn't - allow using ENS name as the recipient identifier, which we interpret as a failure. + support the use of ENS names at all as the recipient identifier. +
+
+ While that's a key issue that this app is encouraged to improve, this best practice is + applicable specifically to apps that already have an existing ENS integration and making + sure existing integrations are ENSv2 compatible. Therefore, for this best practice we + apply a rating of not applicable.

{ describe("benchmarks-utils", () => { describe("calcEnsAwardsPoints", () => { it("Should return correct points for each benchmark result type", () => { - const benchmarkCases = [ + const benchmarkCases: { + benchmark: AcceptanceTestBenchmarkApplicable; + expectedPoints: EnsAwardsPoints; + }[] = [ + // Type assertions are acceptable here since we are in a fully controlled setting { - benchmark: createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + benchmark: createMockAcceptanceTestBenchmark( + BenchmarkResults.Pass, + ) as AcceptanceTestBenchmarkApplicable, expectedPoints: 1, }, { - benchmark: createMockAcceptanceTestBenchmark(BenchmarkResults.PartialPass), + benchmark: createMockAcceptanceTestBenchmark( + BenchmarkResults.PartialPass, + ) as AcceptanceTestBenchmarkApplicable, expectedPoints: 0.5, }, { - benchmark: createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + benchmark: createMockAcceptanceTestBenchmark( + BenchmarkResults.Fail, + ) as AcceptanceTestBenchmarkApplicable, expectedPoints: 0, }, ]; @@ -153,6 +168,13 @@ describe("benchmarks-utils", () => { ...mockBestPracticeCategoryDetails, }); + const mockBestPractice4 = createMockBestPractice({ + id: "mock-best-practice-4", + name: "Mock Best Practice 4", + bestPracticeSlug: "mock-best-practice-4", + ...mockBestPracticeCategoryDetails, + }); + beforeEach(() => { mockGetBestPracticeBySlug.mockReset(); @@ -167,6 +189,9 @@ describe("benchmarks-utils", () => { case mockBestPractice3.bestPracticeSlug: return mockBestPractice3; + case mockBestPractice4.bestPracticeSlug: + return mockBestPractice4; + case mockReverseResolutionBestPractice.bestPracticeSlug: return mockReverseResolutionBestPractice; @@ -190,9 +215,44 @@ describe("benchmarks-utils", () => { }; expect( - calcBestPracticeCategoryScore(benchmarks), - "Expected calcBestPracticeCategoryScore to return undefined for an empty benchmark list", + calcBestPracticeCategoryScore(benchmarks).score, + "Expected calcBestPracticeCategoryScore to return undefined for a list of pending benchmarks", ).toEqual(undefined); + + expect( + calcBestPracticeCategoryScore(benchmarks).label, + "Expected calcBestPracticeCategoryScore to return a result with a 'pending' label for a list of pending benchmarks", + ).toEqual(EnsAwardsUndefinedScoreLabels.Pending); + }); + + it("Should return undefined for completed, but not applicable benchmarks", () => { + const benchmarks: BestPracticeBenchmarks = { + [mockBestPractice1.bestPracticeSlug]: { + mockAcceptanceTestSlug1: createMockAcceptanceTestBenchmark( + BenchmarkResults.NotApplicable, + ), + }, + [mockBestPractice2.bestPracticeSlug]: { + mockAcceptanceTestSlug2: createMockAcceptanceTestBenchmark( + BenchmarkResults.NotApplicable, + ), + }, + [mockBestPractice3.bestPracticeSlug]: { + mockAcceptanceTestSlug3: createMockAcceptanceTestBenchmark( + BenchmarkResults.NotApplicable, + ), + }, + }; + + expect( + calcBestPracticeCategoryScore(benchmarks).score, + "Expected calcBestPracticeCategoryScore to return undefined for a list of not applicable benchmarks", + ).toEqual(undefined); + + expect( + calcBestPracticeCategoryScore(benchmarks).label, + "Expected calcBestPracticeCategoryScore to return a result with a 'not-applicable' label for a list of not applicable benchmarks", + ).toEqual(EnsAwardsUndefinedScoreLabels.NotApplicable); }); it("Should return the rounded category score for valid benchmarks", () => { @@ -206,9 +266,14 @@ describe("benchmarks-utils", () => { [mockBestPractice3.bestPracticeSlug]: { mockAcceptanceTestSlug3: createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), }, + [mockBestPractice4.bestPracticeSlug]: { + mockAcceptanceTestSlug4: createMockAcceptanceTestBenchmark( + BenchmarkResults.NotApplicable, + ), + }, } as const satisfies BestPracticeBenchmarks; - const result = calcBestPracticeCategoryScore(validCategoryBenchmarks); + const result = calcBestPracticeCategoryScore(validCategoryBenchmarks).score; expect( result, @@ -241,6 +306,7 @@ describe("benchmarks-utils", () => { undefined, createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + createMockAcceptanceTestBenchmark(BenchmarkResults.NotApplicable), createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), createMockAcceptanceTestBenchmark(BenchmarkResults.PartialPass), ]; @@ -250,6 +316,7 @@ describe("benchmarks-utils", () => { createMockAcceptanceTestBenchmark(BenchmarkResults.PartialPass), createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + createMockAcceptanceTestBenchmark(BenchmarkResults.NotApplicable), undefined, ]; @@ -271,6 +338,7 @@ describe("benchmarks-utils", () => { BenchmarkResults.Pass, BenchmarkResults.Fail, BenchmarkResults.PartialPass, + BenchmarkResults.NotApplicable, ]; const expectedOutput = [ @@ -278,6 +346,7 @@ describe("benchmarks-utils", () => { BenchmarkResults.PartialPass, BenchmarkResults.Fail, BenchmarkResults.Fail, + BenchmarkResults.NotApplicable, undefined, ]; diff --git a/ensawards.org/data/benchmarks/utils.ts b/ensawards.org/data/benchmarks/utils.ts index 409ace16..c647d86a 100644 --- a/ensawards.org/data/benchmarks/utils.ts +++ b/ensawards.org/data/benchmarks/utils.ts @@ -1,4 +1,7 @@ -import type { AcceptanceTestBenchmark } from "data/acceptance-tests/types.ts"; +import type { + AcceptanceTestBenchmark, + AcceptanceTestBenchmarkApplicable, +} from "data/acceptance-tests/types.ts"; import { generalizeAcceptanceTestBenchmarks } from "data/acceptance-tests/utils.ts"; import type { AppSlug } from "data/apps/types.ts"; import { getAppBySlug } from "data/apps/utils.ts"; @@ -21,6 +24,9 @@ import { asEnsAwardsScore, type EnsAwardsPoints, type EnsAwardsScore, + type EnsAwardsScoreResult, + EnsAwardsScoreResultTypes, + EnsAwardsUndefinedScoreLabels, } from "../shared/ens-awards-score.ts"; import { APP_BENCHMARKS } from "."; import { type AcceptanceTestBenchmarks, type BenchmarkResult, BenchmarkResults } from "./types.ts"; @@ -102,7 +108,9 @@ export function getAppAcceptanceTestBenchmarks( * {@link BenchmarkResults.PartialPass} = 0.5 * {@link BenchmarkResults.Fail} = 0.0 */ -export const calcEnsAwardsPoints = (benchmark: AcceptanceTestBenchmark): EnsAwardsPoints => { +export const calcEnsAwardsPoints = ( + benchmark: AcceptanceTestBenchmarkApplicable, +): EnsAwardsPoints => { const benchmarkResult = benchmark.result; switch (benchmarkResult) { @@ -150,16 +158,19 @@ export const groupBenchmarksByCategory = ( * Calculates {@link EnsAwardsScore} for all benchmarks belonging to a single {@link BestPracticeCategory}. * * @returns - * undefined - if no benchmarks are completed for the `BestPracticeCategory` + * An {@link EnsAwardsScoreResult} object containing the score and a label describing the result. + * The {@link EnsAwardsScoreResult.score} field is: + * - undefined - if no benchmarks are completed for the `BestPracticeCategory`, + * all completed benchmarks returned a not applicable result, * or the category status is not `Active`. - * Otherwise, an {@link EnsAwardsScore} calculation for the `BestPracticeCategory` + * - an {@link EnsAwardsScore} calculation for the `BestPracticeCategory` otherwise. * * @throws if the {@link EnsAwardsScore} invariants are not satisfied * @throws if the input benchmarks do not belong to the same `BestPracticeCategory` */ export const calcBestPracticeCategoryScore = ( benchmarks: BestPracticeBenchmarks, -): EnsAwardsScore | undefined => { +): EnsAwardsScoreResult => { let bestPracticeCategory: undefined | BestPracticeCategory = undefined; for (const bestPracticeSlug of Object.keys(benchmarks)) { @@ -184,7 +195,11 @@ export const calcBestPracticeCategoryScore = ( bestPracticeCategory === undefined || bestPracticeCategory.status !== CategoryStatuses.Active ) { - return undefined; + return { + type: EnsAwardsScoreResultTypes.Undefined, + score: undefined, + label: EnsAwardsUndefinedScoreLabels.InactiveCategory, + }; } const completedBenchmarks: AcceptanceTestBenchmark[] = []; @@ -197,22 +212,50 @@ export const calcBestPracticeCategoryScore = ( } } - if (completedBenchmarks.length === 0) return undefined; + if (completedBenchmarks.length === 0) + return { + type: EnsAwardsScoreResultTypes.Undefined, + score: undefined, + label: EnsAwardsUndefinedScoreLabels.Pending, + }; + + // explicitly exclude benchmarks with `NotApplicable` result + const completedApplicableBenchmarks: AcceptanceTestBenchmarkApplicable[] = + completedBenchmarks.filter( + (benchmark): benchmark is AcceptanceTestBenchmarkApplicable => + benchmark.result !== BenchmarkResults.NotApplicable, + ); + + if (completedApplicableBenchmarks.length === 0) + return { + type: EnsAwardsScoreResultTypes.Undefined, + score: undefined, + label: EnsAwardsUndefinedScoreLabels.NotApplicable, + }; const score = Math.round( - (completedBenchmarks.reduce((sum, benchmark) => sum + calcEnsAwardsPoints(benchmark), 0) * + (completedApplicableBenchmarks.reduce( + (sum, benchmark) => sum + calcEnsAwardsPoints(benchmark), + 0, + ) * 100) / - completedBenchmarks.length, + completedApplicableBenchmarks.length, ); - return asEnsAwardsScore(score); + return { + type: EnsAwardsScoreResultTypes.Defined, + score: asEnsAwardsScore(score), + label: undefined, + }; }; -/** Declare sort order for benchmark result (Pass → Partial Pass → Fail) */ +/** Declare sort order for benchmark result + * (Pass → Partial Pass → Fail → Not Applicable) */ const resultOrder = { [BenchmarkResults.Pass]: 0, [BenchmarkResults.PartialPass]: 1, [BenchmarkResults.Fail]: 2, + [BenchmarkResults.NotApplicable]: 3, } as const satisfies Record; /** Sorts two {@link AcceptanceTestBenchmark}s relative to each other. */ @@ -275,6 +318,9 @@ export const formatBenchmarkResult = ( case BenchmarkResults.Fail: return lowercase ? "failed" : "Failed"; + case BenchmarkResults.NotApplicable: + return lowercase ? "not applicable" : "Not Applicable"; + default: const _exhaustive: never = benchmarkResult; throw new Error(`Unsupported BenchmarkResult: ${_exhaustive}`); diff --git a/ensawards.org/data/ens-best-practices/utils.test.ts b/ensawards.org/data/ens-best-practices/utils.test.ts index 979be922..e2ce0472 100644 --- a/ensawards.org/data/ens-best-practices/utils.test.ts +++ b/ensawards.org/data/ens-best-practices/utils.test.ts @@ -62,10 +62,10 @@ describe("BestPractice and BestPracticeCategory Utils", () => { const result = calcBestPracticeScore(mockReverseResolutionBestPractice); - expect(result, `Expected score to be 50, got ${result} instead`).toEqual(50); + expect(result.score, `Expected score to be 50, got ${result.score} instead`).toEqual(50); }); - it("Should return undefined when no apps are benchmarked for the best practice", () => { + it("Should return undefined score when no apps are benchmarked for the best practice or all defined benchmarks returned a not applicable result", () => { mockAppBenchmarks[mockCoinbaseWalletApp.appSlug] = { [mockReverseResolutionBestPractice.bestPracticeSlug]: { mockAcceptanceTestSlug1: undefined, @@ -80,19 +80,21 @@ describe("BestPractice and BestPracticeCategory Utils", () => { mockAppBenchmarks[mockMetamaskApp.appSlug] = { [mockReverseResolutionBestPractice.bestPracticeSlug]: { - mockAcceptanceTestSlug1: undefined, + mockAcceptanceTestSlug1: createMockAcceptanceTestBenchmark( + BenchmarkResults.NotApplicable, + ), }, }; const result = calcBestPracticeScore(mockReverseResolutionBestPractice); expect( - result, - "calcBestPracticeScore should return undefined when no apps are benchmarked", + result.score, + "calcBestPracticeScore should return undefined when no apps are benchmarked or all defined benchmarks returned a not applicable result", ).toBeUndefined(); }); - it("Should exclude pending benchmarks from the calculation", () => { + it("Should exclude pending & not applicable benchmarks from the calculation", () => { mockAppBenchmarks[mockCoinbaseWalletApp.appSlug] = { [mockReverseResolutionBestPractice.bestPracticeSlug]: { mockAcceptanceTestSlug1: createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), @@ -106,13 +108,18 @@ describe("BestPractice and BestPracticeCategory Utils", () => { mockAppBenchmarks[mockMetamaskApp.appSlug] = { [mockReverseResolutionBestPractice.bestPracticeSlug]: { - mockAcceptanceTestSlug1: undefined, + mockAcceptanceTestSlug1: createMockAcceptanceTestBenchmark( + BenchmarkResults.NotApplicable, + ), }, }; const result = calcBestPracticeScore(mockReverseResolutionBestPractice); - expect(result, "calcBestPracticeScore doesn't exclude pending benchmarks").toEqual(100); + expect( + result.score, + "calcBestPracticeScore doesn't exclude pending & not applicable benchmarks", + ).toEqual(100); }); }); }); diff --git a/ensawards.org/data/ens-best-practices/utils.ts b/ensawards.org/data/ens-best-practices/utils.ts index 0cd365cd..ae49ed6b 100644 --- a/ensawards.org/data/ens-best-practices/utils.ts +++ b/ensawards.org/data/ens-best-practices/utils.ts @@ -1,10 +1,18 @@ +import type { AcceptanceTestBenchmarkApplicable } from "data/acceptance-tests/types.ts"; import { AppTypes } from "data/apps/types.ts"; +import { BenchmarkResults } from "data/benchmarks/types.ts"; import { calcEnsAwardsPoints, getAcceptanceTestBenchmarksByBestPractice, } from "data/benchmarks/utils.ts"; import { ProtocolTypes } from "data/protocols/types.ts"; -import { asEnsAwardsScore, type EnsAwardsScore } from "data/shared/ens-awards-score.ts"; +import { + asEnsAwardsScore, + type EnsAwardsScore, + type EnsAwardsScoreResult, + EnsAwardsScoreResultTypes, + EnsAwardsUndefinedScoreLabels, +} from "data/shared/ens-awards-score.ts"; import type { FormatTypeOptions } from "data/shared/format-type-options.ts"; import { BEST_PRACTICE_CATEGORIES, ENS_BEST_PRACTICES } from "./index.ts"; @@ -101,15 +109,21 @@ export const formatBestPracticeType = ( * by calculating the total score of all apps that were benchmarked on this best practice * and dividing it by the total number of acceptance test benchmarks completed on it. * - * @returns `undefined` if no apps were benchmarked on this best practice, + * @returns + * An {@link EnsAwardsScoreResult} object containing the score and a label describing the result. + * The {@link EnsAwardsScoreResult.score} field is: + * - `undefined` if no apps were benchmarked on this best practice, + * all of its completed benchmarks returned a not applicable result, * or the best practice belongs to a category with status other than `Active`. - * Otherwise returns the {@link EnsAwardsScore}. + * - {@link EnsAwardsScore} otherwise. */ -export const calcBestPracticeScore = ( - bestPractice: BestPracticeApp, -): EnsAwardsScore | undefined => { +export const calcBestPracticeScore = (bestPractice: BestPracticeApp): EnsAwardsScoreResult => { if (bestPractice.category.status !== CategoryStatuses.Active) { - return undefined; + return { + type: EnsAwardsScoreResultTypes.Undefined, + score: undefined, + label: EnsAwardsUndefinedScoreLabels.InactiveCategory, + }; } let benchmarkedAcceptanceTests = 0; @@ -119,21 +133,43 @@ export const calcBestPracticeScore = ( bestPractice.bestPracticeSlug, ).flatMap((appBenchmark) => Object.values(appBenchmark)); - for (const acceptanceTestBenchmark of bestPracticeBenchmarks) { - if (acceptanceTestBenchmark === undefined) { - continue; - } + const completedBenchmarks = bestPracticeBenchmarks.filter((benchmark) => benchmark !== undefined); + + if (completedBenchmarks.length === 0) { + return { + type: EnsAwardsScoreResultTypes.Undefined, + score: undefined, + label: EnsAwardsUndefinedScoreLabels.Pending, + }; + } + + const completedApplicableBenchmarks: AcceptanceTestBenchmarkApplicable[] = + completedBenchmarks.filter( + // explicitly exclude benchmarks that are pending or not applicable + (benchmark) => benchmark.result !== BenchmarkResults.NotApplicable, + ); + + if (completedApplicableBenchmarks.length === 0) { + return { + type: EnsAwardsScoreResultTypes.Undefined, + score: undefined, + label: EnsAwardsUndefinedScoreLabels.NotApplicable, + }; + } + for (const acceptanceTestBenchmark of completedApplicableBenchmarks) { benchmarkedAcceptanceTests += 1; bestPracticePoints += calcEnsAwardsPoints(acceptanceTestBenchmark); } - if (benchmarkedAcceptanceTests === 0) return undefined; - const score = Math.round((bestPracticePoints * 100) / benchmarkedAcceptanceTests); - return asEnsAwardsScore(score); + return { + type: EnsAwardsScoreResultTypes.Defined, + score: asEnsAwardsScore(score), + label: undefined, + }; }; export const formatBestPracticeTarget = ( diff --git a/ensawards.org/data/shared/ens-awards-score.ts b/ensawards.org/data/shared/ens-awards-score.ts index 56595046..d83f3ec9 100644 --- a/ensawards.org/data/shared/ens-awards-score.ts +++ b/ensawards.org/data/shared/ens-awards-score.ts @@ -1,3 +1,5 @@ +import type { FormatTypeOptions } from "data/shared/format-type-options"; + /** * Points awarded for a benchmark result, where higher points indicate better benchmark results. * Used for calculating an EnsAwardsScore. @@ -17,6 +19,81 @@ export type EnsAwardsPoints = number; */ export type EnsAwardsScore = number; +export const EnsAwardsUndefinedScoreLabels = { + Pending: "pending", + NotApplicable: "not-applicable", + InactiveCategory: "inactive-category", +} as const; + +export type EnsAwardsUndefinedScoreLabel = + (typeof EnsAwardsUndefinedScoreLabels)[keyof typeof EnsAwardsUndefinedScoreLabels]; + +export const formatEnsAwardsUndefinedScoreLabel = ( + label: EnsAwardsUndefinedScoreLabel, + options: Omit = { lowercase: false }, +): string => { + const { lowercase } = options; + + switch (label) { + case EnsAwardsUndefinedScoreLabels.Pending: + return lowercase ? "pending" : "Pending"; + + case EnsAwardsUndefinedScoreLabels.NotApplicable: + return lowercase ? "not applicable" : "Not Applicable"; + + case EnsAwardsUndefinedScoreLabels.InactiveCategory: + return lowercase ? "inactive category" : "Inactive Category"; + + default: + const _exhaustive: never = label; + throw new Error(`Unsupported EnsAwardsUndefinedScoreLabel: ${_exhaustive}`); + } +}; + +export const EnsAwardsScoreResultTypes = { + Defined: "defined", + Undefined: "undefined", +} as const; + +export type EnsAwardsScoreResultType = + (typeof EnsAwardsScoreResultTypes)[keyof typeof EnsAwardsScoreResultTypes]; + +export interface EnsAwardsScoreResultAbstract< + EnsAwardsScoreResultTypeT extends EnsAwardsScoreResultType, + EnsAwardsScoreT extends EnsAwardsScore | undefined, + EnsAwardsUndefinedScoreLabelT extends EnsAwardsUndefinedScoreLabel | undefined, +> { + type: EnsAwardsScoreResultTypeT; + /** + * Calculated EnsAwardsScore for the benchmarked entity + * ({@link App}, {@link Protocol}, {@link BestPractice}, or {@link BestPracticeCategory}). + * + * Can be `undefined` if no benchmarks were completed or if all completed benchmarks returned a not applicable result. + */ + score: EnsAwardsScoreT; + + /** + * Label to display instead of the score when necessary. + */ + label: EnsAwardsUndefinedScoreLabelT; +} + +export interface EnsAwardsScoreResultDefined + extends EnsAwardsScoreResultAbstract< + typeof EnsAwardsScoreResultTypes.Defined, + EnsAwardsScore, + undefined + > {} + +export interface EnsAwardsScoreResultUndefined + extends EnsAwardsScoreResultAbstract< + typeof EnsAwardsScoreResultTypes.Undefined, + undefined, + EnsAwardsUndefinedScoreLabel + > {} + +export type EnsAwardsScoreResult = EnsAwardsScoreResultDefined | EnsAwardsScoreResultUndefined; + /** * Checks if a number is a valid {@link EnsAwardsScore}. */ diff --git a/ensawards.org/package.json b/ensawards.org/package.json index 6d062c23..c4d69917 100644 --- a/ensawards.org/package.json +++ b/ensawards.org/package.json @@ -38,7 +38,7 @@ "@tanstack/react-query": "^5.0.0", "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", - "astro": "^6.1.10", + "astro": "^6.4.8", "astro-font": "^1.0.0", "astro-seo": "^1.1.0", "boring-avatars": "^2.0.4", diff --git a/ensawards.org/src/assets/benchmarkNotApplicableSymbol.svg b/ensawards.org/src/assets/benchmarkNotApplicableSymbol.svg new file mode 100644 index 00000000..622a022f --- /dev/null +++ b/ensawards.org/src/assets/benchmarkNotApplicableSymbol.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/ensawards.org/src/components/atoms/CircularScore.astro b/ensawards.org/src/components/atoms/CircularScore.astro index c8a22868..9a9c9342 100644 --- a/ensawards.org/src/components/atoms/CircularScore.astro +++ b/ensawards.org/src/components/atoms/CircularScore.astro @@ -1,14 +1,19 @@ --- import { cn } from "../../utils/tailwindClassConcatenation"; -import { type EnsAwardsScore } from "data/shared/ens-awards-score"; +import { + type EnsAwardsScoreResult, + type EnsAwardsScore, + formatEnsAwardsUndefinedScoreLabel, + EnsAwardsScoreResultTypes, +} from "data/shared/ens-awards-score"; export interface CircularScoreProps { - score?: EnsAwardsScore; + scoreResult: EnsAwardsScoreResult; size?: number; strokeWidth?: number; } -const { score, size = 139, strokeWidth = 10 }: CircularScoreProps = Astro.props; +const { scoreResult, size = 139, strokeWidth = 10 }: CircularScoreProps = Astro.props; const gradientStartColors = ["#12D494", "#ffd230", "#fb2c36", "#d1d1d1"]; const gradientStopColors = ["#0D9A6B", "#e17100", "#c10007", "#969696"]; @@ -32,7 +37,7 @@ const getGradientColorIndex = (score?: EnsAwardsScore) => { return 0; }; -const gradientIndex = getGradientColorIndex(score); +const gradientIndex = getGradientColorIndex(scoreResult.score); const colorFrom = gradientStartColors[gradientIndex]; const colorTo = gradientStopColors[gradientIndex]; @@ -82,20 +87,20 @@ const circumference = 2 * Math.PI * radius;
{ - score === undefined ? ( -

- Pending + scoreResult.type === EnsAwardsScoreResultTypes.Undefined ? ( +

+ {formatEnsAwardsUndefinedScoreLabel(scoreResult.label)}

) : (

) } { - score !== undefined && ( + scoreResult.score !== undefined && (

%

) } diff --git a/ensawards.org/src/components/atoms/badges/BenchmarkResultBadge.tsx b/ensawards.org/src/components/atoms/badges/BenchmarkResultBadge.tsx index 671979bb..33758bf6 100644 --- a/ensawards.org/src/components/atoms/badges/BenchmarkResultBadge.tsx +++ b/ensawards.org/src/components/atoms/badges/BenchmarkResultBadge.tsx @@ -2,6 +2,7 @@ import { type BenchmarkResult, BenchmarkResults } from "data/benchmarks/types.ts import { formatBenchmarkResult } from "data/benchmarks/utils.ts"; import { X as FailIcon, + CircleOff as NotApplicableIcon, Check as PartialPassIcon, CheckCheck as PassIcon, Clock as PendingIcon, @@ -26,6 +27,9 @@ export const benchmarkResultToBadgeStyles = (benchmarkResult?: BenchmarkResult) case BenchmarkResults.Fail: return "text-red-600 bg-[rgba(220,38,38,0.1)]"; + case BenchmarkResults.NotApplicable: + return "text-muted-foreground bg-black/8"; + default: const _exhaustive: never = benchmarkResult; throw new Error(`Unsupported BenchmarkResult: ${_exhaustive}`); @@ -44,6 +48,9 @@ export const getBenchmarkIcon = (benchmarkResult?: BenchmarkResult, className?: case BenchmarkResults.Fail: return ; + case BenchmarkResults.NotApplicable: + return ; + default: const _exhaustive: never = benchmarkResult; throw new Error(`Unsupported BenchmarkResult: ${_exhaustive}`); diff --git a/ensawards.org/src/components/atoms/badges/BenchmarkResultHeroBadge.astro b/ensawards.org/src/components/atoms/badges/BenchmarkResultHeroBadge.astro index 46a5fed0..87065cbe 100644 --- a/ensawards.org/src/components/atoms/badges/BenchmarkResultHeroBadge.astro +++ b/ensawards.org/src/components/atoms/badges/BenchmarkResultHeroBadge.astro @@ -6,6 +6,7 @@ import benchmarkResultFailSymbol from "@/assets/benchmarkResultFailSymbol.svg"; import benchmarkResultPartialPassSymbol from "@/assets/benchmarkResultPartialPassSymbol.svg"; import benchmarkResultPassSymbol from "@/assets/benchmarkResultPassSymbol.svg"; import benchmarkPendingSymbol from "@/assets/benchmarkPendingSymbol.svg"; +import benchmarkNotApplicableSymbol from "@/assets/benchmarkNotApplicableSymbol.svg"; import { BenchmarkResults, type BenchmarkResult } from "data/benchmarks/types.ts"; import { cn } from "@/utils/tailwindClassConcatenation"; import { type BenchmarkResultBadgeProps } from "./BenchmarkResultBadge.tsx"; @@ -39,6 +40,12 @@ const benchmarkResultContent = ( symbolAlt: "Failed benchmark result indicator", }; + case BenchmarkResults.NotApplicable: + return { + symbol: benchmarkNotApplicableSymbol, + symbolAlt: "Not applicable review benchmark indicator", + }; + default: const _exhaustive: never = benchmarkResult; throw new Error(`Unsupported BenchmarkResult: ${_exhaustive}`); @@ -50,7 +57,7 @@ const { symbol, symbolAlt } = benchmarkResultContent(benchmarkResult);
- + {category.name} @@ -173,7 +173,7 @@ export function AppSummaryCard({ app }: AppSummaryCardProps) {

{resolvedApp.name}

- +
{benchmarksByCategorySorted.map(([categorySlug, benchmarksInCategory], index) => { return ( diff --git a/ensawards.org/src/components/atoms/cards/LeaderboardCard.astro b/ensawards.org/src/components/atoms/cards/LeaderboardCard.astro index 191867cf..e3419099 100644 --- a/ensawards.org/src/components/atoms/cards/LeaderboardCard.astro +++ b/ensawards.org/src/components/atoms/cards/LeaderboardCard.astro @@ -2,13 +2,13 @@ import { cn } from "../../../utils/tailwindClassConcatenation"; import { shadcnButtonVariants } from "../../ui/shadcnButtonStyles"; import { EnsAwardsBarScore } from "@/components/atoms/ens-awards-score/bar.tsx"; -import { type EnsAwardsScore } from "data/shared/ens-awards-score.ts"; +import { type EnsAwardsScoreResult } from "data/shared/ens-awards-score.ts"; export interface LeaderboardCardProps { name: string; viewDetailsHref: string; viewDetailsText?: string; - ensAwardsScore?: EnsAwardsScore; + ensAwardsScore: EnsAwardsScoreResult; } const { name, viewDetailsHref, viewDetailsText, ensAwardsScore }: LeaderboardCardProps = @@ -41,7 +41,7 @@ const { name, viewDetailsHref, viewDetailsText, ensAwardsScore }: LeaderboardCar
- +
); +}; diff --git a/ensawards.org/src/components/atoms/ens-awards-score/circular-small.tsx b/ensawards.org/src/components/atoms/ens-awards-score/circular-small.tsx index 166d2536..a61f5c66 100644 --- a/ensawards.org/src/components/atoms/ens-awards-score/circular-small.tsx +++ b/ensawards.org/src/components/atoms/ens-awards-score/circular-small.tsx @@ -1,15 +1,42 @@ -import { type EnsAwardsScore } from "data/shared/ens-awards-score"; +import { + type EnsAwardsScoreResult, + EnsAwardsScoreResultTypes, + EnsAwardsUndefinedScoreLabels, +} from "data/shared/ens-awards-score"; import { calcScoreBarFill, getScoreColor } from "@/components/atoms/ens-awards-score/utils"; +import { AllBenchmarksNotApplicableIcon } from "@/components/atoms/icons/AllBenchmarksNotApplicableIcon"; import { AllBenchmarksPendingIcon } from "@/components/atoms/icons/AllBenchmarksPendingIcon"; -export const EnsAwardsCircularScoreSmall = ({ score }: { score?: EnsAwardsScore }) => { - if (score === undefined) return ; +export const EnsAwardsCircularScoreSmall = ({ + scoreResult, +}: { + scoreResult: EnsAwardsScoreResult; +}) => { + if (scoreResult.type === EnsAwardsScoreResultTypes.Undefined) { + switch (scoreResult.label) { + case EnsAwardsUndefinedScoreLabels.Pending: + return ; + case EnsAwardsUndefinedScoreLabels.NotApplicable: + return ; + + case EnsAwardsUndefinedScoreLabels.InactiveCategory: + // This variant will never be publicly available, + return ( +
+

Inactive Category

+
+ ); + + default: + throw new Error("Invariant(EnsAwardsScoreResult): Unrecognized label in scoreResult."); + } + } const radius = 13; const circumference = 2 * Math.PI * radius; - const dashOffset = circumference * (1 - calcScoreBarFill(score) / 100); - const progressColorClass = `text-${getScoreColor(score)}`; + const dashOffset = circumference * (1 - calcScoreBarFill(scoreResult.score) / 100); + const progressColorClass = `text-${getScoreColor(scoreResult.score)}`; return (
@@ -28,7 +55,9 @@ export const EnsAwardsCircularScoreSmall = ({ score }: { score?: EnsAwardsScore className={progressColorClass} /> - {score} + + {scoreResult.score} +
); }; diff --git a/ensawards.org/src/components/atoms/ens-awards-score/circular.astro b/ensawards.org/src/components/atoms/ens-awards-score/circular.astro index 1081f080..a9563606 100644 --- a/ensawards.org/src/components/atoms/ens-awards-score/circular.astro +++ b/ensawards.org/src/components/atoms/ens-awards-score/circular.astro @@ -1,17 +1,17 @@ --- import CircularScore from "../CircularScore.astro"; -import { type EnsAwardsScore } from "data/shared/ens-awards-score"; +import { type EnsAwardsScoreResult } from "data/shared/ens-awards-score"; export interface EnsAwardsScoreProps { - score?: EnsAwardsScore; + scoreResult: EnsAwardsScoreResult; label: string; } -const { score, label }: EnsAwardsScoreProps = Astro.props; +const { scoreResult, label }: EnsAwardsScoreProps = Astro.props; ---
- +

{label}

diff --git a/ensawards.org/src/components/atoms/icons/AllBenchmarksNotApplicableIcon.tsx b/ensawards.org/src/components/atoms/icons/AllBenchmarksNotApplicableIcon.tsx new file mode 100644 index 00000000..eb11aaf1 --- /dev/null +++ b/ensawards.org/src/components/atoms/icons/AllBenchmarksNotApplicableIcon.tsx @@ -0,0 +1,7 @@ +import { CircleOff as NotApplicableIcon } from "lucide-react"; + +export const AllBenchmarksNotApplicableIcon = () => ( + + + +); diff --git a/ensawards.org/src/components/organisms/AppBestPracticeDetails.astro b/ensawards.org/src/components/organisms/AppBestPracticeDetails.astro index 064debe9..c5787111 100644 --- a/ensawards.org/src/components/organisms/AppBestPracticeDetails.astro +++ b/ensawards.org/src/components/organisms/AppBestPracticeDetails.astro @@ -108,9 +108,9 @@ const additionalInfoContainerStyles = )}>

App support

diff --git a/ensawards.org/src/components/organisms/ProtocolBestPracticeDetails.astro b/ensawards.org/src/components/organisms/ProtocolBestPracticeDetails.astro index 342c6836..862034ea 100644 --- a/ensawards.org/src/components/organisms/ProtocolBestPracticeDetails.astro +++ b/ensawards.org/src/components/organisms/ProtocolBestPracticeDetails.astro @@ -23,6 +23,7 @@ import { type Contribution } from "data/contributors/types"; import { CONTRACTS } from "data/protocols/contracts"; import { AppliesToBadges } from "@/components/atoms/badges/AppliesToBadges.tsx"; import { BestPracticeTechnicalDetails } from "@/components/molecules/technicalDetails/bestPractice"; +import { EnsAwardsScoreResultTypes } from "data/shared/ens-awards-score.ts"; export interface ProtocolBestPracticeDetailsProps { bestPractice: BestPracticeProtocol; @@ -147,7 +148,7 @@ const additionalInfoContainerStyles = header={`${formatProtocolType(protocolType)} Leaderboard`} containerStyles="gap-3 sm:gap-2 sm:justify-start max-sm:justify-center"> {Object.entries(protocolScores).map( - ([scoredProtocolId, value], idx) => { + ([scoredProtocolId, ensAwardsScore], idx) => { // Type casting is necessary due to `Record`'s type nature. // We are completely sure of its correctness const protocolId = scoredProtocolId as ProtocolId; @@ -157,7 +158,11 @@ const additionalInfoContainerStyles = {idx < 3 ? ( diff --git a/ensawards.org/src/pages/app/[appSlug]/index.astro b/ensawards.org/src/pages/app/[appSlug]/index.astro index 9ee32553..7f5b0bb7 100644 --- a/ensawards.org/src/pages/app/[appSlug]/index.astro +++ b/ensawards.org/src/pages/app/[appSlug]/index.astro @@ -17,7 +17,7 @@ import { ContributorsCardLoading, } from "@/components/atoms/cards/ContributorsCard.tsx"; import { type Contribution } from "data/contributors/types.ts"; -import { type EnsAwardsScore as EnsAwardsScoreType } from "data/shared/ens-awards-score.ts"; +import { type EnsAwardsScoreResult } from "data/shared/ens-awards-score.ts"; import { groupBenchmarksByCategory, calcBestPracticeCategoryScore, @@ -29,6 +29,10 @@ import { EnsAwardsCircularScoreSmall } from "@/components/atoms/ens-awards-score import { getAppContributions } from "data/contributors/utils.ts"; import type { BestPracticeCategorySlug } from "data/ens-best-practices/types"; import { getBestPracticeCategoryBySlug } from "data/ens-best-practices/utils.ts"; +import { + EnsAwardsScoreResultTypes, + EnsAwardsUndefinedScoreLabels, +} from "data/shared/ens-awards-score.ts"; import AwardsCard from "@/components/atoms/cards/AwardsCard.astro"; import { CategoryStatuses } from "data/ens-best-practices/types.ts"; import { summarizeAppsAcceptanceTestBenchmarks } from "data/benchmarks/utils.ts"; @@ -42,7 +46,11 @@ const { appSlug } = Astro.params; const app = getAppBySlug(appSlug); // The default values should never be used. Only introduced for type safety. -let appScore: EnsAwardsScoreType | undefined = undefined; +let appScore: EnsAwardsScoreResult = { + type: EnsAwardsScoreResultTypes.Undefined, + score: undefined, + label: EnsAwardsUndefinedScoreLabels.NotApplicable, +}; let AppIcon; let appContributions: Contribution[] = []; let benchmarksByCategory: Map = new Map(); @@ -107,7 +115,7 @@ if (app !== undefined) {
@@ -151,7 +159,9 @@ if (app !== undefined) { return (
- + {category.name} diff --git a/ensawards.org/src/pages/ens-best-practices/index.astro b/ensawards.org/src/pages/ens-best-practices/index.astro index aa7c57a0..ea21d4b9 100644 --- a/ensawards.org/src/pages/ens-best-practices/index.astro +++ b/ensawards.org/src/pages/ens-best-practices/index.astro @@ -6,7 +6,7 @@ import { BEST_PRACTICE_CATEGORIES } from "data/ens-best-practices"; import Layout from "@/layouts/Layout.astro"; import { CategoryStatuses } from "data/ens-best-practices/types"; import { getBestPracticesByCategory } from "data/ens-best-practices/utils"; -import BestPracticeCard from "@/components/atoms/cards/BestPracticeCard.astro"; +import BestPracticeCard from "@/components/atoms/cards/bestPracticeCard/index.astro"; --- ("/src/assets/*.{jpeg,jpg,png,gif,svg}"); const placeIcons = [ @@ -98,7 +99,7 @@ const placeIcons = [ containerStyles="gap-3 sm:gap-2 sm:justify-start max-sm:justify-center"> {[...Object.entries(protocolScores)] .slice(0, 3) - .map(([scoredProtocolId, value], idx) => { + .map(([scoredProtocolId, ensAwardsScore], idx) => { // Type casting is necessary due to `Record`'s type nature. // We are completely sure of its correctness const protocolId = scoredProtocolId as ProtocolId; @@ -108,7 +109,11 @@ const placeIcons = [ ({ @@ -76,7 +75,7 @@ const protocolContractDataContributions: Contribution[] = Object.keys(protocolSc { [...Object.entries(protocolScores)].map( - ([scoredProtocolId, value], idx) => { + ([scoredProtocolId, ensAwardsScore], idx) => { // Type casting is necessary due to `Record`'s type nature. // We are completely sure of its correctness const protocolId = scoredProtocolId as ProtocolId; @@ -86,7 +85,11 @@ const protocolContractDataContributions: Contribution[] = Object.keys(protocolSc {idx < 3 ? ( diff --git a/ensawards.org/src/pages/protocol/[protocolSlug].astro b/ensawards.org/src/pages/protocol/[protocolSlug].astro index 17969881..0f66ed52 100644 --- a/ensawards.org/src/pages/protocol/[protocolSlug].astro +++ b/ensawards.org/src/pages/protocol/[protocolSlug].astro @@ -33,7 +33,8 @@ import { } from "@/components/atoms/cards/ContributorsCard"; import { type Contribution } from "data/contributors/types"; import { getProtocolContributions } from "data/contributors/utils"; -import type { EnsAwardsScore } from "data/shared/ens-awards-score"; +import type { EnsAwardsScoreResult } from "data/shared/ens-awards-score"; +import { EnsAwardsScoreResultTypes } from "data/shared/ens-awards-score"; import AwardsCard from "@/components/atoms/cards/AwardsCard.astro"; export function getStaticPaths() { @@ -55,14 +56,22 @@ const protocol = getProtocolBySlug(protocolSlug); // The default values should never be used. Only introduced for type safety. let protocolContracts: Contract[] = []; -let protocolScore: EnsAwardsScore = 0; +let protocolScoreResult: EnsAwardsScoreResult = { + type: EnsAwardsScoreResultTypes.Defined, + score: 0, + label: undefined, +}; let ProtocolIcon: typeof GlobeIcon = GlobeIcon; let protocolContributions: Contribution[] = []; if (protocol) { protocolContracts = getAllProtocolContracts(protocol.id); ProtocolIcon = protocol.icon; - protocolScore = getContractNamingScoresByProtocolType(protocol.protocolType)[protocol.id] ?? 0; + protocolScoreResult = { + type: EnsAwardsScoreResultTypes.Defined, + score: getContractNamingScoresByProtocolType(protocol.protocolType)[protocol.id] ?? 0, + label: undefined, + }; protocolContributions = getProtocolContributions(protocol); } --- @@ -119,7 +128,7 @@ if (protocol) {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ffaf5116..d3ac6a4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,7 +62,7 @@ importers: version: 3.7.2 '@astrojs/vercel': specifier: ^10.0.3 - version: 10.0.3(astro@6.4.4(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3))(react@19.2.0)(rollup@4.59.0) + version: 10.0.3(astro@6.4.8(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3))(react@19.2.0)(rollup@4.59.0) '@ensnode/datasources': specifier: 1.14.0 version: 1.14.0(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(typescript@5.9.3)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) @@ -80,7 +80,7 @@ importers: version: 2.2.0(react@19.2.0) '@lucide/astro': specifier: ^1.7.0 - version: 1.7.0(astro@6.4.4(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3)) + version: 1.7.0(astro@6.4.8(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3)) '@namehash/ens-referrals': specifier: 1.14.0 version: 1.14.0(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(typescript@5.9.3)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) @@ -118,8 +118,8 @@ importers: specifier: ^19.1.7 version: 19.2.3(@types/react@19.2.6) astro: - specifier: ^6.1.10 - version: 6.4.4(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3) + specifier: ^6.4.8 + version: 6.4.8(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3) astro-font: specifier: ^1.0.0 version: 1.1.0 @@ -1887,8 +1887,8 @@ packages: astro-seo@1.1.0: resolution: {integrity: sha512-G6LDNDyga30o+52v58jU7C35n3cORUzk7RSuY+xf/ZRbW6JiNplyaOO1VoYVKO+xzYNaBcxqNln2RFRR/wgJNQ==} - astro@6.4.4: - resolution: {integrity: sha512-hVe8tq3lqt/Dr0UyB//yUmQSlHMTU8scTiF/vQddQVahLE4TTaSdH5H0nb7OvRcwo0UmlAO8DWYar4jNaS7H+A==} + astro@6.4.8: + resolution: {integrity: sha512-KK5lX90uU9EeVaTjINyj3sy9/NFXVa59aowaqbWBDDKLXZh4rr7GwIaCFYVetE22MJtsCNFerQXn0vlCLmpP/Q==} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true @@ -3638,14 +3638,14 @@ snapshots: is-wsl: 3.1.1 which-pm-runs: 1.1.0 - '@astrojs/vercel@10.0.3(astro@6.4.4(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3))(react@19.2.0)(rollup@4.59.0)': + '@astrojs/vercel@10.0.3(astro@6.4.8(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3))(react@19.2.0)(rollup@4.59.0)': dependencies: '@astrojs/internal-helpers': 0.8.0 '@vercel/analytics': 1.6.1(react@19.2.0) '@vercel/functions': 3.4.3 '@vercel/nft': 1.5.0(rollup@4.59.0) '@vercel/routing-utils': 5.3.3 - astro: 6.4.4(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3) + astro: 6.4.8(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3) esbuild: 0.28.1 tinyglobby: 0.2.15 transitivePeerDependencies: @@ -4145,9 +4145,9 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@lucide/astro@1.7.0(astro@6.4.4(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3))': + '@lucide/astro@1.7.0(astro@6.4.8(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3))': dependencies: - astro: 6.4.4(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3) + astro: 6.4.8(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3) '@mapbox/node-pre-gyp@2.0.3': dependencies: @@ -5114,7 +5114,7 @@ snapshots: - prettier-plugin-astro - typescript - astro@6.4.4(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3): + astro@6.4.8(@types/node@22.19.1)(@vercel/functions@3.4.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(yaml@2.8.3): dependencies: '@astrojs/compiler': 4.0.0 '@astrojs/internal-helpers': 0.10.0