diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6bcc1a56..d297b756 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,16 +46,22 @@ This is our way to show appreciation to the people who contributed to our cause. ![Social credit for contributors](./ensawards.org/src/assets/contributorsDocsSocialCredit.png) To add yourself: -1. Add your `AccountId` to the `contributors` collection in [ensawards.org/data/contributors/index.ts](ensawards.org/data/contributors/index.ts) (if this is your first contribution). +1. Add your `AccountId` to the `contributors` collection in +[ensawards.org/data/contributors/index.ts](ensawards.org/data/contributors/index.ts) +(if this is your first contribution). 2. Reference yourself in the contributions array of the entity you are updating. If you update the same entity multiple times, update the `lastUpdated` field for each change with the timestamp of your most recent update to that entity. -For reference see [ensawards.org/data/apps/metamask-wallet/benchmarks.ts](ensawards.org/data/apps/metamask-wallet/benchmarks.ts). +For reference see +[ensawards.org/data/apps/metamask-wallet/benchmarks/index.tsx](ensawards.org/data/apps/metamask-wallet/benchmarks/index.tsx). ### Adding a new `Project` 1. Create a new subdirectory in the [ensawards.org/data/projects/](ensawards.org/data/projects) named after the project you want to add. The directory name should be the lowercase project name. If the name contains multiple words, join them with hyphens ("-"). -2. Inside the new directory, create an `index.ts` file and default-export the project definition. Make sure to also call `defineProject()` on it. For reference see [ensawards.org/data/projects/aave/index.ts](ensawards.org/data/projects/aave/index.ts). -3. Follow its data model that you can look up in the [ensawards.org/data/projects/types.ts](ensawards.org/data/projects/types.ts) file. You can also have a quick glance at it below. +2. Inside the new directory, create an `index.ts` file and default-export the project definition. Make sure to also call `defineProject()` on it. For reference see +[ensawards.org/data/projects/aave/index.ts](ensawards.org/data/projects/aave/index.ts). +3. Follow its data model that you can look up in the +[ensawards.org/data/projects/types.ts](ensawards.org/data/projects/types.ts) file. +You can also have a quick glance at it below. ```typescript export interface Project { @@ -69,7 +75,8 @@ export interface Project { }; } ``` -4. Add an SVG icon as a React component in the created directory (`icon.tsx`). For reference, see [ensawards.org/data/projects/aave/icon.tsx](ensawards.org/data/projects/aave/icon.tsx). +4. Add an SVG icon as a React component in the created directory (`icon.tsx`). For reference, see +[ensawards.org/data/projects/aave/icon.tsx](ensawards.org/data/projects/aave/icon.tsx). 5. You are welcome to propose updates to already added projects using the same approach. ### Relationship between `Projects`, `Protocols` and `Apps` @@ -85,8 +92,13 @@ For this reason, every new `App` or `Protocol` must be associated with a corresp ### Adding a new `Protocol` 1. Create a new subdirectory in the [ensawards.org/data/protocols/](ensawards.org/data/protocols) named after the protocol you want to add. The directory name should be the same as the slug of the protocol (for reference see [ensawards.org/data/protocols/ens-dao/index.ts](ensawards.org/data/protocols/ens-dao/index.ts)). -2. Inside the new directory, create an `index.ts` file and default-export the protocol definition. Make sure to also call `defineProtocol()` on it. For reference see [ensawards.org/data/protocols/ens-dao/index.ts](ensawards.org/data/protocols/ens-dao/index.ts). -3. Make sure to follow its data model that you can look up in the [ensawards.org/data/protocols/types.ts](ensawards.org/data/protocols/types.ts) file. Remember that the `Protocol` can represent either a `DAO` or a `DeFi protocol`. Below you can see its most important interface and type: +2. Inside the new directory, create an `index.ts` file and default-export the protocol definition. +Make sure to also call `defineProtocol()` on it. For reference see +[ensawards.org/data/protocols/ens-dao/index.ts](ensawards.org/data/protocols/ens-dao/index.ts). +3. Make sure to follow its data model that you can look up in the +[ensawards.org/data/protocols/types.ts](ensawards.org/data/protocols/types.ts) file. +Remember that the `Protocol` can represent either a `DAO` or a `DeFi protocol`. +Below you can see its most important interface and type: ```typescript export interface ProtocolAbstract { @@ -129,14 +141,17 @@ export type Protocol = DAOProtocol | DeFiProtocol; ``` -4. Add an SVG icon as a React component in the created directory (`icon.tsx`). For reference, see [ensawards.org/data/protocols/ens-dao/icon.tsx](ensawards.org/data/protocols/ens-dao/icon.tsx). +4. Add an SVG icon as a React component in the created directory (`icon.tsx`). +For reference, see [ensawards.org/data/protocols/ens-dao/icon.tsx](ensawards.org/data/protocols/ens-dao/icon.tsx). 5. In your PR describe your reasoning for adding this `Protocol`. 6. You are welcome to propose updates to existing protocols using the same approach. ### Adding a new `Contract` -1. Add the new contract object to the `contracts` array in the [ensawards.org/data/protocols/[protocol-directory]/contracts.ts](ensawards.org/data/protocols/aave-dao/contracts.ts) file where `[protocol-directory]` matches the protocol’s `Protocol.protocolSlug` value. -2. Make sure to follow the data model defined in the [ensawards.org/data/protocols/contracts-types.ts](ensawards.org/data/protocols/contracts-types.ts) file. +1. Add the new contract object to the `contracts` array in the +[ensawards.org/data/protocols/[protocol-directory]/contracts.ts](ensawards.org/data/protocols/aave-dao/contracts.ts) file where `[protocol-directory]` matches the protocol’s `Protocol.protocolSlug` value. +2. Make sure to follow the data model defined in the +[ensawards.org/data/protocols/contracts-types.ts](ensawards.org/data/protocols/contracts-types.ts) file. ```typescript export interface Contract { protocol: Protocol; @@ -149,9 +164,12 @@ export interface Contract { ### Adding a new `App` -1. Create a new subdirectory in the [ensawards.org/data/apps/](ensawards.org/data/apps) named after the app you want to add. The directory name should be the same as the slug of the app (for reference see [ensawards.org/data/apps/metamask-wallet/index.ts](ensawards.org/data/apps/metamask-wallet/index.ts)). -2. Inside the new directory, create an `index.ts` file and default-export the app definition. Make sure to also call `defineApp()` on it. For reference see [ensawards.org/data/apps/rainbow-wallet/index.ts](ensawards.org/data/apps/rainbow-wallet/index.ts). -3. Follow the corresponding data model available in the [ensawards.org/data/apps/types.ts](ensawards.org/data/apps/types.ts) file. +1. Create a new subdirectory in the [ensawards.org/data/apps/](ensawards.org/data/apps) named after the app you want to add. The directory name should be the same as the slug of the app (for reference see +[ensawards.org/data/apps/metamask-wallet/index.ts](ensawards.org/data/apps/metamask-wallet/index.ts)). +2. Inside the new directory, create an `index.ts` file and default-export the app definition. Make sure to also call `defineApp()` on it. +For reference see [ensawards.org/data/apps/rainbow-wallet/index.ts](ensawards.org/data/apps/rainbow-wallet/index.ts). +3. Follow the corresponding data model available in the +[ensawards.org/data/apps/types.ts](ensawards.org/data/apps/types.ts) file. ```typescript export interface App { id: string; @@ -186,7 +204,9 @@ export interface App { } ``` -4. Add an SVG icon as a React component in the created directory (`icon.tsx`). For reference, see [ensawards.org/data/apps/blockscout-explorer/icon.tsx](ensawards.org/data/apps/blockscout-explorer/icon.tsx). +4. Add an SVG icon as a React component in the created directory (`icon.tsx`). +For reference, see +[ensawards.org/data/apps/blockscout-explorer/icon.tsx](ensawards.org/data/apps/blockscout-explorer/icon.tsx). 5. In your PR describe your reasoning for adding that new `App`. 6. You are welcome to propose updates to already added apps using the same approach. @@ -196,11 +216,26 @@ Best practices are structured hierarchically and can be added on two levels: #### `BestPractice` -Defines a specific requirement that an app or protocol must meet to pass a benchmark test. They are grouped into categories. +Defines a specific requirement that an app or protocol must meet to pass a benchmark test. +They are grouped into categories. -1. Create a new `BestPractice` default-export and its definition in `ensawards.org/data/ens-best-practices/[category]/[bestPractice].ts`, where `[category]` is the category slug and `[bestPractice]` is the practice slug. Make sure to also call `defineBestPractice()` on it. For reference see [ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts.ts](ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts.ts). -2. Make sure to follow its data model defined in the [ensawards.org/data/ens-best-practices/types.ts](ensawards.org/data/ens-best-practices/types.ts) file. +1. Create a new `BestPractice` default-export and its +definition in `ensawards.org/data/ens-best-practices/[category]/[bestPractice]/index.ts`, +where `[category]` is the category slug and `[bestPractice]` is the practice slug. +Make sure to also call `defineBestPractice()` on it. +For reference see [ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts/index.ts](ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts/index.ts). +2. Make sure to follow its data model defined in the +[ensawards.org/data/ens-best-practices/types.ts](ensawards.org/data/ens-best-practices/types.ts) file. ```typescript +export interface BestPracticeTechnicalDetails { + useCaseSummary: JSX.Element; + benefitFromUsingEns: JSX.Element; + /** Title for the benefit card. Defaults to "Benefit from using ENS" when omitted. */ + benefitFromUsingEnsTitle?: string; + implementationRecommendations: JSX.Element; + acceptanceTests: [AcceptanceTest, ...AcceptanceTest[]]; +} + export interface BestPracticeAbstract< BestPracticeT extends BestPracticeType, AppliesToT extends BestPracticeTarget, @@ -212,16 +247,7 @@ export interface BestPracticeAbstract< description: string; category: BestPracticeCategory; // each best practice belongs to exactly one best practice category appliesTo: AppliesToT[]; - technicalDetails: { - main: { - header: string; - content: string; - }; - sides: { - header: string; - content: string; - }[]; - }; + technicalDetails: BestPracticeTechnicalDetails; contributions: [Contribution, ...Contribution[]]; // Remember to add yourself as a contributor } @@ -233,16 +259,69 @@ export interface BestPracticeApp export type BestPractice = BestPracticeProtocol | BestPracticeApp; ``` -3. In your PR describe your reasoning for adding it. -4. If you want your best practice to be a part of a new category, learn how to add one below. +3. Add technical details as simple JSX elements in the created directory (`technicalDetails.tsx`). +For reference, see [ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts/technicalDetails.tsx](ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts/technicalDetails.tsx). + * Make sure to follow the `AcceptanceTest` data model available in the + [ensawards.org/data/acceptance-tests/types.ts](ensawards.org/data/acceptance-tests/types.ts) file. + ```typescript + /** A unique identifier for an acceptance test. + * + * @invariant Must be unique across all acceptance tests. + * @invariant Must match {@link ENSAWARDS_SLUG_PATTERN}. + */ + export type AcceptanceTestSlug = string; + + /** + * Represents an acceptance test that an app can be evaluated against. + */ + export interface AcceptanceTest { + /** + * Unique identifier for the acceptance test. + */ + acceptanceTestSlug: AcceptanceTestSlug; + + /** + * Description of the acceptance test, + * which should provide clear and detailed information + * about the criteria and requirements that + * an {@link App} must meet to pass the test. + * This may include specific functionalities to be tested, + * user interactions to be evaluated, + * and any relevant technical details or considerations. + * + * @note The description should not include examples of passed or failed benchmarks, + * there are dedicated fields for that + * (see {@link AcceptanceTest.examplePass}, + * {@link AcceptanceTest.examplePartialPass}, + * or {@link AcceptanceTest.exampleFail}). + */ + description: JSX.Element; + + /** + * Examples of benchmark results that illustrate + * what a passing, partially passing, or failing result + * looks like for this acceptance test. + */ + examplePass: AcceptanceTestBenchmarkPass; + examplePartialPass?: AcceptanceTestBenchmarkPartialPass; + exampleFail?: AcceptanceTestBenchmarkFail; + } + ``` +4. In your PR describe your reasoning for adding it. +5. If you want your best practice to be a part of a new category, learn how to add one below. #### `BestPracticeCategory` Categories sort best practices into topic-related groups based on their characteristics. They are a level above the basic `BestPractice` objects in the "Best Practices" hierarchy. -1. To add a new category, create a new subdirectory in the [ensawards.org/data/ens-best-practices/](ensawards.org/data/ens-best-practices/) named after the best practice category you want to add. The directory name should be the same as the slug of the category (for reference see [ensawards.org/data/ens-best-practices/contract-naming/index.ts](ensawards.org/data/ens-best-practices/contract-naming/index.ts)). -2. Inside the new directory, create an `index.ts` file and default-export the category definition. Make sure to also call `defineBestPracticeCategory()` on it. For reference see [ensawards.org/data/ens-best-practices/contract-naming/index.ts](ensawards.org/data/ens-best-practices/contract-naming/index.ts). -3. Follow its data model available in the [ensawards.org/data/ens-best-practices/types.ts](ensawards.org/data/ens-best-practices/types.ts) file. +1. To add a new category, create a new subdirectory in the +[ensawards.org/data/ens-best-practices/](ensawards.org/data/ens-best-practices/) +named after the best practice category you want to add. The directory name should be the same as the slug of the category (for reference see +[ensawards.org/data/ens-best-practices/contract-naming/index.ts](ensawards.org/data/ens-best-practices/contract-naming/index.ts)). +2. Inside the new directory, create an `index.ts` file and default-export the category definition. Make sure to also call `defineBestPracticeCategory()` on it. For reference see +[ensawards.org/data/ens-best-practices/contract-naming/index.ts](ensawards.org/data/ens-best-practices/contract-naming/index.ts). +3. Follow its data model available in the +[ensawards.org/data/ens-best-practices/types.ts](ensawards.org/data/ens-best-practices/types.ts) file. ```typescript export const CategoryStatuses = { ComingSoon: "coming-soon", @@ -257,27 +336,34 @@ export interface BestPracticeCategory { name: string; description: string; status: CategoryStatus; + /** Sort priority within a status group; lower comes first, undefined sorts last (then by name). */ + order?: number; } ``` 4. In your PR describe your reasoning for adding it. ### Suggest a `benchmark update` -1. To suggest a benchmark update for an existing app, modify its `benchmarks` record in the [ensawards.org/data/apps/[app-directory]/benchmarks.ts](ensawards.org/data/apps/rainbow-wallet/benchmarks.ts) file where `[app-directory]` represents the slug of the relevant app. These records define relationships between benchmarks and best practices. Their data model is available at [ensawards.org/data/ens-best-practices/types.ts](ensawards.org/data/ens-best-practices/types.ts). +1. To suggest a benchmark update for an existing app, modify its `benchmarks` record in the +[ensawards.org/data/apps/[app-directory]/benchmarks/index.tsx](ensawards.org/data/apps/rainbow-wallet/benchmarks/index.tsx) file +where `[app-directory]` represents the slug of the relevant app. These records define relationships between benchmarks and best practices. Their data model is available at +[ensawards.org/data/ens-best-practices/types.ts](ensawards.org/data/ens-best-practices/types.ts). ```typescript /** - * Defines relations between {@link BestPracticeSlug} and {@link AppBenchmark} - * for the related {@link BestPractice}. + * Defines relations between {@link BestPracticeSlug} and the {@link AcceptanceTestBenchmarks} + * of the related {@link BestPractice} for a given app. * - * @invariant An explicit key for each `BestPracticeSlug` should be added to this `Record` for each available {@link BestPractice}. - * If an app doesn't have a benchmark completed for a `BestPractice` then the benchmark should be explicitly set to `undefined`. - * Otherwise, the value should be an `AppBenchmark` describing how the related app performed for the `BestPractice`. + * @invariant An explicit key for each `BestPracticeSlug` should be added to this `Record` + * for each applicable {@link BestPractice}. + * The value should be the related {@link AcceptanceTestBenchmarks}. */ -export type BestPracticeBenchmarks = Record; +export type BestPracticeBenchmarks = Record; ``` -For reference see [ensawards.org/data/apps/blockscout-explorer/benchmarks.ts](ensawards.org/data/apps/blockscout-explorer/benchmarks.ts) for an example benchmarks record. +For reference see +[ensawards.org/data/apps/blockscout-explorer/benchmarks/index.tsx](ensawards.org/data/apps/blockscout-explorer/benchmarks/index.tsx) +for an example benchmarks record. 2. Make sure to follow the benchmark data model. It is available in the [ensawards.org/data/benchmarks/types.ts](ensawards.org/data/benchmarks/types.ts) file. ```typescript @@ -289,19 +375,84 @@ export const BenchmarkResults = { export type BenchmarkResult = (typeof BenchmarkResults)[keyof typeof BenchmarkResults]; -export interface AppBenchmark { - result: BenchmarkResult; +/** + * Represents the benchmarks of an {@link App} against {@link AcceptanceTest}s of {@link BestPractice}s. + * + * @invariant + * An explicit key for each {@link AcceptanceTestSlug} should be added to this `Record` + * for each {@link AcceptanceTest} available on a given best practice. + * + * If an app doesn't have a benchmark completed for an `AcceptanceTest` + * then the benchmark should be explicitly set to `undefined`. + * + * Otherwise, the value should be an `AcceptanceTestBenchmark` + * describing how the related app performed for the `AcceptanceTest`. + */ +export type AcceptanceTestBenchmarks = Record< + AcceptanceTestSlug, + AcceptanceTestBenchmark | undefined +>; +``` + +Where `AcceptanceTestBenchmark` represents the benchmark performed on a single acceptance test. Its data model is available in the [ensawards.org/data/acceptance-tests/types.ts](ensawards.org/data/acceptance-tests/types.ts) file. + +```typescript +/** + * Represents the benchmark of an {@link AcceptanceTest} on an {@link App} against a {@link BestPractice}. + */ +export interface AcceptanceTestBenchmarkAbstract { + /** The result of the benchmark */ + result: BenchmarkResultT; - /** A record of all contributors involved in the addition or maintenance of the benchmark's data. + /** A record of all contributors involved + * in the addition or maintenance of the benchmark's data. * - * @invariant Multiple {@link Contribution} from the same contributor on the same app benchmark are not allowed. - * When a contributor makes updates to their existing contribution, + * @invariant Multiple {@link Contribution} from the same contributor + * on the same app benchmark are not allowed. + * When a contributor makes updates to their existing contribution, * they should update the `lastUpdated` timestamp of their existing `Contribution`. */ contributions: [Contribution, ...Contribution[]]; + + /** + * Notes about how the benchmark result was determined, + * which may include details about the testing process, + * any challenges encountered, and explanations, + * as well as a visual proof, for the final result. + */ + notes: JSX.Element; } + +/** + * Represents a benchmark of an {@link AcceptanceTest} on an {@link App} against a {@link BestPractice}, + * that has fully passed the acceptance test. + */ +export interface AcceptanceTestBenchmarkPass + extends AcceptanceTestBenchmarkAbstract {} + +/** + * Represents a benchmark of an {@link AcceptanceTest} on an {@link App} against a {@link BestPractice}, + * that has partially passed the acceptance test. + */ +export interface AcceptanceTestBenchmarkPartialPass + extends AcceptanceTestBenchmarkAbstract {} + +/** + * Represents a benchmark of an {@link AcceptanceTest} on an {@link App} against a {@link BestPractice}, + * that hasn't passed the acceptance test. + */ +export interface AcceptanceTestBenchmarkFail + extends AcceptanceTestBenchmarkAbstract {} + +export type AcceptanceTestBenchmark = + | AcceptanceTestBenchmarkPass + | AcceptanceTestBenchmarkPartialPass + | AcceptanceTestBenchmarkFail; ``` +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 +[ensawards.org/data/apps/walletchan-wallet/benchmarks/index.tsx](ensawards.org/data/apps/walletchan-wallet/benchmarks/index.tsx). + ## Using `Biome` and `Prettier` together diff --git a/ensawards.org/data/acceptance-tests/index.test.ts b/ensawards.org/data/acceptance-tests/index.test.ts new file mode 100644 index 00000000..48b4911a --- /dev/null +++ b/ensawards.org/data/acceptance-tests/index.test.ts @@ -0,0 +1,22 @@ +import { ACCEPTANCE_TESTS } from "data/acceptance-tests"; +import { isValidSlug } from "data/shared/slugs"; +import { describe, expect, it } from "vitest"; + +import { areStringsUnique } from "@/utils"; + +describe("Acceptance test data", () => { + it("Should have valid and unique slugs", () => { + const slugArray: string[] = []; + + ACCEPTANCE_TESTS.forEach((acceptanceTest) => { + expect( + isValidSlug(acceptanceTest.acceptanceTestSlug), + `Slug={${acceptanceTest.acceptanceTestSlug}} is not valid`, + ).toEqual(true); + + slugArray.push(acceptanceTest.acceptanceTestSlug); + }); + + expect(areStringsUnique(slugArray), `Slugs for Acceptance Tests are not unique`).toEqual(true); + }); +}); diff --git a/ensawards.org/data/acceptance-tests/index.ts b/ensawards.org/data/acceptance-tests/index.ts new file mode 100644 index 00000000..02011d49 --- /dev/null +++ b/ensawards.org/data/acceptance-tests/index.ts @@ -0,0 +1,5 @@ +import { ENS_BEST_PRACTICES } from "data/ens-best-practices"; + +export const ACCEPTANCE_TESTS = [ + ...ENS_BEST_PRACTICES.flatMap((bestPractice) => bestPractice.technicalDetails.acceptanceTests), +]; diff --git a/ensawards.org/data/acceptance-tests/types.ts b/ensawards.org/data/acceptance-tests/types.ts new file mode 100644 index 00000000..b41ac673 --- /dev/null +++ b/ensawards.org/data/acceptance-tests/types.ts @@ -0,0 +1,98 @@ +import { type BenchmarkResult, BenchmarkResults } from "data/benchmarks/types"; +import type { Contribution } from "data/contributors/types"; +import type { JSX } from "react"; + +/** A unique identifier for an acceptance test. + * + * @invariant Must be unique across all acceptance tests. + * @invariant Must match {@link ENSAWARDS_SLUG_PATTERN}. + */ +export type AcceptanceTestSlug = string; + +/** + * Represents an acceptance test that an app can be evaluated against. + */ +export interface AcceptanceTest { + /** + * Unique identifier for the acceptance test. + */ + acceptanceTestSlug: AcceptanceTestSlug; + + /** + * Description of the acceptance test, + * which should provide clear and detailed information + * about the criteria and requirements that + * an {@link App} must meet to pass the test. + * This may include specific functionalities to be tested, + * user interactions to be evaluated, + * and any relevant technical details or considerations. + * + * @note The description should not include examples of passed or failed benchmarks, + * there are dedicated fields for that + * (see {@link AcceptanceTest.examplePass}, + * {@link AcceptanceTest.examplePartialPass}, + * or {@link AcceptanceTest.exampleFail}). + */ + description: JSX.Element; + + /** + * Examples of benchmark results that illustrate + * what a passing, partially passing, or failing result + * looks like for this acceptance test. + */ + examplePass: AcceptanceTestBenchmarkPass; + examplePartialPass?: AcceptanceTestBenchmarkPartialPass; + exampleFail?: AcceptanceTestBenchmarkFail; +} + +/** + * Represents the benchmark of an {@link AcceptanceTest} on an {@link App} against a {@link BestPractice}. + */ +export interface AcceptanceTestBenchmarkAbstract { + /** The result of the benchmark */ + result: BenchmarkResultT; + + /** A record of all contributors involved + * in the addition or maintenance of the benchmark's data. + * + * @invariant Multiple {@link Contribution} from the same contributor + * on the same app benchmark are not allowed. + * When a contributor makes updates to their existing contribution, + * they should update the `lastUpdated` timestamp of their existing `Contribution`. + */ + contributions: [Contribution, ...Contribution[]]; + + /** + * Notes about how the benchmark result was determined, + * which may include details about the testing process, + * any challenges encountered, and explanations, + * as well as a visual proof, for the final result. + */ + notes: JSX.Element; +} + +/** + * Represents a benchmark of an {@link AcceptanceTest} on an {@link App} against a {@link BestPractice}, + * that has fully passed the acceptance test. + */ +export interface AcceptanceTestBenchmarkPass + extends AcceptanceTestBenchmarkAbstract {} + +/** + * Represents a benchmark of an {@link AcceptanceTest} on an {@link App} against a {@link BestPractice}, + * that has partially passed the acceptance test. + */ +export interface AcceptanceTestBenchmarkPartialPass + extends AcceptanceTestBenchmarkAbstract {} + +/** + * Represents a benchmark of an {@link AcceptanceTest} on an {@link App} against a {@link BestPractice}, + * that hasn't passed the acceptance test. + */ +export interface AcceptanceTestBenchmarkFail + extends AcceptanceTestBenchmarkAbstract {} + +export type AcceptanceTestBenchmark = + | AcceptanceTestBenchmarkPass + | AcceptanceTestBenchmarkPartialPass + | AcceptanceTestBenchmarkFail; diff --git a/ensawards.org/data/acceptance-tests/utils.test.ts b/ensawards.org/data/acceptance-tests/utils.test.ts new file mode 100644 index 00000000..36761478 --- /dev/null +++ b/ensawards.org/data/acceptance-tests/utils.test.ts @@ -0,0 +1,95 @@ +import { generalizeAcceptanceTestBenchmarks } from "data/acceptance-tests/utils"; +import { type AcceptanceTestBenchmarks, BenchmarkResults } from "data/benchmarks/types"; +import { createMockAcceptanceTestBenchmark } from "data/shared/test-utils"; +import { describe, expect, it } from "vitest"; + +describe("Acceptance test utils", () => { + describe("generalizeAcceptanceTestBenchmarks", () => { + it( + "Returns `BenchmarkResults.Pass` if in all defined benchmarks there is at least one `BenchmarkResults.Pass`" + + " and all others are `BenchmarkResults.Pass` or `BenchmarkResults.PartialPass`", + () => { + const expectedResult = BenchmarkResults.Pass; + + const inputBenchmarks = { + "mock-acceptance-test-1": createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + "mock-acceptance-test-2": createMockAcceptanceTestBenchmark(BenchmarkResults.PartialPass), + "mock-acceptance-test-3": undefined, + // pending benchmarks should be ignored in this case of the generalization + } as const satisfies AcceptanceTestBenchmarks; + + expect( + generalizeAcceptanceTestBenchmarks(inputBenchmarks), + "generalizeAcceptanceTestBenchmarks should return `BenchmarkResults.Pass`", + ).toEqual(expectedResult); + }, + ); + + it("Returns `BenchmarkResults.Fail` if all defined benchmarks are `BenchmarkResults.Fail`", () => { + 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; + + 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`", + () => { + const expectedResult = BenchmarkResults.PartialPass; + + const inputBenchmarks = { + "mock-acceptance-test-1": createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + "mock-acceptance-test-2": createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + "mock-acceptance-test-3": createMockAcceptanceTestBenchmark(BenchmarkResults.PartialPass), + "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.PartialPass`", + ).toEqual(expectedResult); + }, + ); + + it("Returns `BenchmarkResults.PartialPass` if all defined benchmarks are `BenchmarkResults.PartialPass`", () => { + const expectedResult = BenchmarkResults.PartialPass; + + const inputBenchmarks = { + "mock-acceptance-test-1": createMockAcceptanceTestBenchmark(BenchmarkResults.PartialPass), + "mock-acceptance-test-2": createMockAcceptanceTestBenchmark(BenchmarkResults.PartialPass), + "mock-acceptance-test-3": undefined, + // pending benchmarks should be ignored in this case of the generalization + } as const satisfies AcceptanceTestBenchmarks; + + expect( + generalizeAcceptanceTestBenchmarks(inputBenchmarks), + "generalizeAcceptanceTestBenchmarks should return `BenchmarkResults.PartialPass`", + ).toEqual(expectedResult); + }); + + it("Returns `undefined` if all benchmarks are `undefined` (pending)", () => { + const expectedResult = undefined; + + const inputBenchmarks = { + "mock-acceptance-test-1": undefined, + "mock-acceptance-test-2": undefined, + } as const satisfies AcceptanceTestBenchmarks; + + expect( + generalizeAcceptanceTestBenchmarks(inputBenchmarks), + "generalizeAcceptanceTestBenchmarks should return `undefined` for all pending benchmarks", + ).toEqual(expectedResult); + }); + }); +}); diff --git a/ensawards.org/data/acceptance-tests/utils.ts b/ensawards.org/data/acceptance-tests/utils.ts new file mode 100644 index 00000000..e3b414ce --- /dev/null +++ b/ensawards.org/data/acceptance-tests/utils.ts @@ -0,0 +1,109 @@ +import { ACCEPTANCE_TESTS } from "data/acceptance-tests"; +import type { + AcceptanceTest, + AcceptanceTestBenchmark, + AcceptanceTestSlug, +} from "data/acceptance-tests/types"; +import type { AppSlug } from "data/apps/types"; +import { + type AcceptanceTestBenchmarks, + type BenchmarkResult, + BenchmarkResults, +} from "data/benchmarks/types"; +import { getAppBenchmarks } from "data/benchmarks/utils.ts"; +import { CategoryStatuses } from "data/ens-best-practices/types"; +import { getBestPracticeBySlug } from "data/ens-best-practices/utils"; + +/** Returns an {@link AcceptanceTest} by {@link AcceptanceTestSlug}. */ +export const getAcceptanceTestBySlug = (slug: AcceptanceTestSlug): AcceptanceTest | undefined => { + return ACCEPTANCE_TESTS.find((acceptanceTest) => acceptanceTest.acceptanceTestSlug === slug); +}; + +/** + * Returns all {@link AcceptanceTestBenchmark}s of an `App` by {@link AppSlug} + * for all {@link BestPractice}s belonging to an `Active` {@link BestPracticeCategory}. + */ +export const getAcceptanceTestBenchmarksByApp = ( + appSlug: AppSlug, +): (AcceptanceTestBenchmark | undefined)[] => { + const appBenchmarks = getAppBenchmarks(appSlug); + + const benchmarksInActiveBestPracticeCategories = Object.entries(appBenchmarks).filter( + ([bestPracticeSlug, _]) => { + const bestPractice = getBestPracticeBySlug(bestPracticeSlug); + + if (bestPractice === undefined) { + throw new Error( + `Invariant(BestPracticeSlug): Best practice with slug ${bestPracticeSlug} is not defined`, + ); + } + + return bestPractice.category.status === CategoryStatuses.Active; + }, + ); + + return benchmarksInActiveBestPracticeCategories.flatMap(([_, acceptanceTestBenchmarks]) => + Object.values(acceptanceTestBenchmarks), + ); +}; + +/** + * Generalizes multiple `AcceptanceTestBenchmark`s into a single `BenchmarkResult` + * based on the following rules: + * - Returns {@link BenchmarkResults.Pass} if: + * - in all defined benchmarks there is at least one + * {@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.PartialPass} if: + * - at least one defined benchmark is {@link BenchmarkResults.Fail} + * and at least one defined benchmark is + * {@link BenchmarkResults.Pass} or {@link BenchmarkResults.PartialPass}, + * - or all defined benchmarks are {@link BenchmarkResults.PartialPass}, + * + * - Returns `undefined` if all benchmarks are `undefined` (pending). + */ +export const generalizeAcceptanceTestBenchmarks = ( + acceptanceTestBenchmarks: AcceptanceTestBenchmarks, +): BenchmarkResult | undefined => { + const benchmarkResults = Object.values(acceptanceTestBenchmarks).map( + (benchmark) => benchmark?.result, + ); + + const definedBenchmarkResults = benchmarkResults.filter((result) => result !== undefined); + + if (definedBenchmarkResults.length === 0) { + return undefined; + } + + const allDefinedBenchmarksPassPartially = definedBenchmarkResults.every( + (result) => result === BenchmarkResults.PartialPass, + ); + + if (allDefinedBenchmarksPassPartially) { + return BenchmarkResults.PartialPass; + } + + // For now, we'll explicitly treat pass and partial pass equally + // (For cases where not all benchmarks are partial pass) + const allDefinedBenchmarksPass = definedBenchmarkResults.every( + (result) => result === BenchmarkResults.Pass || result === BenchmarkResults.PartialPass, + ); + + if (allDefinedBenchmarksPass) { + return BenchmarkResults.Pass; + } + + const allDefinedBenchmarksFail = definedBenchmarkResults.every( + (result) => result === BenchmarkResults.Fail, + ); + + if (allDefinedBenchmarksFail) { + return BenchmarkResults.Fail; + } + + return BenchmarkResults.PartialPass; +}; diff --git a/ensawards.org/data/apps/ambire-wallet/benchmarks.ts b/ensawards.org/data/apps/ambire-wallet/benchmarks.ts deleted file mode 100644 index 3d865472..00000000 --- a/ensawards.org/data/apps/ambire-wallet/benchmarks.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice -// on adding and modifying app benchmarks - -import AmbireWallet from "data/apps/ambire-wallet"; -import { defineAppBenchmarks } from "data/benchmarks/registry"; -import { BenchmarkResults } from "data/benchmarks/types"; -import type { BestPracticeBenchmarks } from "data/ens-best-practices/types.ts"; - -import { parseTimestamp } from "@ensnode/ensnode-sdk"; - -import contributors from "../../contributors"; - -const benchmarks = { - "display-named-smart-contracts-mainnet": { - result: BenchmarkResults.PartialPass, - contributions: [ - { from: contributors.caldonia, lastUpdated: parseTimestamp("2026-04-23T19:50:24Z") }, - ], - }, - "display-named-smart-contracts-l2-chains": { - result: BenchmarkResults.PartialPass, - contributions: [ - { from: contributors.caldonia, lastUpdated: parseTimestamp("2026-04-23T19:50:24Z") }, - ], - }, -} as const satisfies BestPracticeBenchmarks; - -defineAppBenchmarks(AmbireWallet, benchmarks); - -export default benchmarks; diff --git a/ensawards.org/data/apps/ambire-wallet/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png b/ensawards.org/data/apps/ambire-wallet/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png new file mode 100644 index 00000000..ac393602 Binary files /dev/null and b/ensawards.org/data/apps/ambire-wallet/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png differ diff --git a/ensawards.org/data/apps/ambire-wallet/benchmarks/index.tsx b/ensawards.org/data/apps/ambire-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..7a2c6b20 --- /dev/null +++ b/ensawards.org/data/apps/ambire-wallet/benchmarks/index.tsx @@ -0,0 +1,74 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice +// on adding and modifying app benchmarks + +import type { AcceptanceTestBenchmark } from "data/acceptance-tests/types"; +import AmbireWallet from "data/apps/ambire-wallet"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import { BenchmarkResults } from "data/benchmarks/types"; +import contributors from "data/contributors"; +import { acceptanceTestDetailsContainerStyles } from "data/ens-best-practices/styles"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types.ts"; + +import { parseTimestamp } from "@ensnode/ensnode-sdk"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +import correctlyResolveEnsv2TestNameAddressProofImage from "./correctly-resolve-ensv2-test-name-address-proof.png"; + +const benchmarks = { + // TODO: `Contract Naming` category is temporarily hidden due to unfit content, + // and so are all benchmarks belonging to it. + // We aim to fix it as soon as we have the capacity. + // See: https://github.com/namehash/ensawards/issues/222 + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": { + result: BenchmarkResults.PartialPass, + contributions: [ + { from: contributors.caldonia, lastUpdated: parseTimestamp("2026-04-23T19:50:24Z") }, + ], + notes: ( +
+

Benchmark placeholder notes

+
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": { + result: BenchmarkResults.PartialPass, + contributions: [ + { from: contributors.caldonia, lastUpdated: parseTimestamp("2026-04-23T19:50:24Z") }, + ], + notes: ( +
+

Benchmark placeholder notes

+
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, + "ensv2-ready-resolution": { + "correctly-resolve-ensv2-test-name-address": { + result: BenchmarkResults.Pass, + contributions: [ + { from: contributors.y3drk, lastUpdated: parseTimestamp("2026-06-05T17:29:00Z") }, + ], + notes: ( +
+

+ ENSv2 ready resolution was tested using the "send" flow. The resolved address + is correct. +

+ Ambire Wallet correctly resolves the name for ENSv2 +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, +} as const satisfies BestPracticeBenchmarks; + +defineAppBenchmarks(AmbireWallet, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/blockscout-explorer/benchmarks.ts b/ensawards.org/data/apps/blockscout-explorer/benchmarks.ts deleted file mode 100644 index 23de1906..00000000 --- a/ensawards.org/data/apps/blockscout-explorer/benchmarks.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice -// on adding and modifying app benchmarks - -import BlockscoutExplorer from "data/apps/blockscout-explorer"; -import { defineAppBenchmarks } from "data/benchmarks/registry"; -import { BenchmarkResults } from "data/benchmarks/types"; -import type { BestPracticeBenchmarks } from "data/ens-best-practices/types.ts"; - -import { parseTimestamp } from "@ensnode/ensnode-sdk"; - -import contributors from "../../contributors"; - -const benchmarks = { - // "recognize-all-ens-names": { - // result: BenchmarkResults.Pass, - // contributions: [ - // { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-03T14:00:00Z") }, - // ], - // }, - "display-named-smart-contracts-mainnet": { - result: BenchmarkResults.Pass, - contributions: [ - { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:28:32.410Z") }, - ], - }, - "display-named-smart-contracts-l2-chains": { - result: BenchmarkResults.Fail, - contributions: [ - { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:28:32.410Z") }, - ], - }, -} as const satisfies BestPracticeBenchmarks; - -defineAppBenchmarks(BlockscoutExplorer, benchmarks); - -export default benchmarks; diff --git a/ensawards.org/data/apps/blockscout-explorer/benchmarks/acceptance-test-benchmark-proof-example.png b/ensawards.org/data/apps/blockscout-explorer/benchmarks/acceptance-test-benchmark-proof-example.png new file mode 100644 index 00000000..30bb7663 Binary files /dev/null and b/ensawards.org/data/apps/blockscout-explorer/benchmarks/acceptance-test-benchmark-proof-example.png differ diff --git a/ensawards.org/data/apps/blockscout-explorer/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png b/ensawards.org/data/apps/blockscout-explorer/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png new file mode 100644 index 00000000..2cbed9ba Binary files /dev/null and b/ensawards.org/data/apps/blockscout-explorer/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png differ diff --git a/ensawards.org/data/apps/blockscout-explorer/benchmarks/index.tsx b/ensawards.org/data/apps/blockscout-explorer/benchmarks/index.tsx new file mode 100644 index 00000000..cb6770de --- /dev/null +++ b/ensawards.org/data/apps/blockscout-explorer/benchmarks/index.tsx @@ -0,0 +1,92 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice +// on adding and modifying app benchmarks + +import type { AcceptanceTestBenchmark } from "data/acceptance-tests/types"; +import BlockscoutExplorer from "data/apps/blockscout-explorer"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import { BenchmarkResults } from "data/benchmarks/types"; +import contributors from "data/contributors"; +import { acceptanceTestDetailsContainerStyles } from "data/ens-best-practices/styles"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types.ts"; + +import { parseTimestamp } from "@ensnode/ensnode-sdk"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +import exampleProofImage from "./acceptance-test-benchmark-proof-example.png"; +import correctlyResolveEnsv2TestNameAddressProofImage from "./correctly-resolve-ensv2-test-name-address-proof.png"; + +const benchmarks = { + // "recognize-all-ens-names": { + // result: BenchmarkResults.Pass, + // contributions: [ + // { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-03T14:00:00Z") }, + // ], + // }, + + // TODO: `Contract Naming` category is temporarily hidden due to unfit content, + // and so are all benchmarks belonging to it. + // We aim to fix it as soon as we have the capacity. + // See: https://github.com/namehash/ensawards/issues/222 + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Pass, + contributions: [ + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:28:32.410Z") }, + ], + notes: ( +
+

TODO: Add real benchmark notes

+ example proof +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Fail, + contributions: [ + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:28:32.410Z") }, + ], + notes: ( +
+

TODO: Add real benchmark notes

+ example proof +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, + "ensv2-ready-resolution": { + "correctly-resolve-ensv2-test-name-address": { + result: BenchmarkResults.Fail, + contributions: [ + { from: contributors.y3drk, lastUpdated: parseTimestamp("2026-06-05T14:55:00Z") }, + ], + notes: ( +
+

+ ENSv2 ready resolution was tested using the "search" flow. The resolved + address is NOT correct. +

+ Blockscout fails to resolve the name for ENSv2 +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, +} as const satisfies BestPracticeBenchmarks; + +defineAppBenchmarks(BlockscoutExplorer, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/coinbase-wallet/benchmarks.ts b/ensawards.org/data/apps/coinbase-wallet/benchmarks.ts deleted file mode 100644 index ad4acaf6..00000000 --- a/ensawards.org/data/apps/coinbase-wallet/benchmarks.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice -// on adding and modifying app benchmarks - -import CoinbaseWallet from "data/apps/coinbase-wallet"; -import { defineAppBenchmarks } from "data/benchmarks/registry"; -import { BenchmarkResults } from "data/benchmarks/types"; -import type { BestPracticeBenchmarks } from "data/ens-best-practices/types.ts"; - -import { parseTimestamp } from "@ensnode/ensnode-sdk"; - -import contributors from "../../contributors"; - -const benchmarks = { - // "recognize-all-ens-names": { - // result: BenchmarkResults.Pass, - // contributions: [ - // { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-03T14:00:00Z") }, - // ], - // }, - "display-named-smart-contracts-mainnet": { - result: BenchmarkResults.Fail, - contributions: [ - { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:22:55.716Z") }, - ], - }, - "display-named-smart-contracts-l2-chains": { - result: BenchmarkResults.Fail, - contributions: [ - { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:22:55.716Z") }, - ], - }, -} as const satisfies BestPracticeBenchmarks; - -defineAppBenchmarks(CoinbaseWallet, benchmarks); - -export default benchmarks; diff --git a/ensawards.org/data/apps/coinbase-wallet/benchmarks/acceptance-test-benchmark-proof-example.png b/ensawards.org/data/apps/coinbase-wallet/benchmarks/acceptance-test-benchmark-proof-example.png new file mode 100644 index 00000000..30bb7663 Binary files /dev/null and b/ensawards.org/data/apps/coinbase-wallet/benchmarks/acceptance-test-benchmark-proof-example.png differ diff --git a/ensawards.org/data/apps/coinbase-wallet/benchmarks/correctly-resolve-ensv2-test-name-address-proof.gif b/ensawards.org/data/apps/coinbase-wallet/benchmarks/correctly-resolve-ensv2-test-name-address-proof.gif new file mode 100644 index 00000000..9c9cb888 Binary files /dev/null and b/ensawards.org/data/apps/coinbase-wallet/benchmarks/correctly-resolve-ensv2-test-name-address-proof.gif differ diff --git a/ensawards.org/data/apps/coinbase-wallet/benchmarks/index.tsx b/ensawards.org/data/apps/coinbase-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..66d1ced8 --- /dev/null +++ b/ensawards.org/data/apps/coinbase-wallet/benchmarks/index.tsx @@ -0,0 +1,92 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice +// on adding and modifying app benchmarks + +import type { AcceptanceTestBenchmark } from "data/acceptance-tests/types"; +import CoinbaseWallet from "data/apps/coinbase-wallet"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import { BenchmarkResults } from "data/benchmarks/types"; +import contributors from "data/contributors"; +import { acceptanceTestDetailsContainerStyles } from "data/ens-best-practices/styles"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types.ts"; + +import { parseTimestamp } from "@ensnode/ensnode-sdk"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +import exampleProofImage from "./acceptance-test-benchmark-proof-example.png"; +import correctlyResolveEnsv2TestNameAddressProofImage from "./correctly-resolve-ensv2-test-name-address-proof.gif"; + +const benchmarks = { + // "recognize-all-ens-names": { + // result: BenchmarkResults.Pass, + // contributions: [ + // { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-03T14:00:00Z") }, + // ], + // }, + + // TODO: `Contract Naming` category is temporarily hidden due to unfit content, + // and so are all benchmarks belonging to it. + // We aim to fix it as soon as we have the capacity. + // See: https://github.com/namehash/ensawards/issues/222 + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Fail, + contributions: [ + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:22:55.716Z") }, + ], + notes: ( +
+

TODO: Add real benchmark notes

+ example proof +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Fail, + contributions: [ + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:22:55.716Z") }, + ], + notes: ( +
+

TODO: Add real benchmark notes

+ example proof +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, + "ensv2-ready-resolution": { + "correctly-resolve-ensv2-test-name-address": { + result: BenchmarkResults.Fail, + contributions: [ + { from: contributors.y3drk, lastUpdated: parseTimestamp("2026-06-05T15:12:00Z") }, + ], + notes: ( +
+

+ ENSv2 ready resolution was tested using the "send" flow. The resolved address + is NOT correct. +

+ Coinbase Wallet fails to resolve the name for ENSv2 +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, +} as const satisfies BestPracticeBenchmarks; + +defineAppBenchmarks(CoinbaseWallet, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/etherscan-explorer/benchmarks.ts b/ensawards.org/data/apps/etherscan-explorer/benchmarks.ts deleted file mode 100644 index 25ff326f..00000000 --- a/ensawards.org/data/apps/etherscan-explorer/benchmarks.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice -// on adding and modifying app benchmarks - -import EtherscanExplorer from "data/apps/etherscan-explorer"; -import { defineAppBenchmarks } from "data/benchmarks/registry"; -import { BenchmarkResults } from "data/benchmarks/types"; -import type { BestPracticeBenchmarks } from "data/ens-best-practices/types.ts"; - -import { parseTimestamp } from "@ensnode/ensnode-sdk"; - -import contributors from "../../contributors"; - -const benchmarks = { - // "recognize-all-ens-names": { - // result: BenchmarkResults.Pass, - // contributions: [ - // { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-03T14:00:00Z") }, - // ], - // }, - "display-named-smart-contracts-mainnet": { - result: BenchmarkResults.Pass, - contributions: [ - { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:26:20.566Z") }, - ], - }, - "display-named-smart-contracts-l2-chains": { - result: BenchmarkResults.Fail, - contributions: [ - { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:26:20.566Z") }, - ], - }, -} as const satisfies BestPracticeBenchmarks; - -defineAppBenchmarks(EtherscanExplorer, benchmarks); - -export default benchmarks; diff --git a/ensawards.org/data/apps/etherscan-explorer/benchmarks/acceptance-test-benchmark-proof-example.png b/ensawards.org/data/apps/etherscan-explorer/benchmarks/acceptance-test-benchmark-proof-example.png new file mode 100644 index 00000000..30bb7663 Binary files /dev/null and b/ensawards.org/data/apps/etherscan-explorer/benchmarks/acceptance-test-benchmark-proof-example.png differ diff --git a/ensawards.org/data/apps/etherscan-explorer/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png b/ensawards.org/data/apps/etherscan-explorer/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png new file mode 100644 index 00000000..666c40d3 Binary files /dev/null and b/ensawards.org/data/apps/etherscan-explorer/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png differ diff --git a/ensawards.org/data/apps/etherscan-explorer/benchmarks/index.tsx b/ensawards.org/data/apps/etherscan-explorer/benchmarks/index.tsx new file mode 100644 index 00000000..4bcc9896 --- /dev/null +++ b/ensawards.org/data/apps/etherscan-explorer/benchmarks/index.tsx @@ -0,0 +1,92 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice +// on adding and modifying app benchmarks + +import type { AcceptanceTestBenchmark } from "data/acceptance-tests/types"; +import EtherscanExplorer from "data/apps/etherscan-explorer"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import { BenchmarkResults } from "data/benchmarks/types"; +import contributors from "data/contributors"; +import { acceptanceTestDetailsContainerStyles } from "data/ens-best-practices/styles"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types.ts"; + +import { parseTimestamp } from "@ensnode/ensnode-sdk"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +import exampleProofImage from "./acceptance-test-benchmark-proof-example.png"; +import correctlyResolveEnsv2TestNameAddressProofImage from "./correctly-resolve-ensv2-test-name-address-proof.png"; + +const benchmarks = { + // "recognize-all-ens-names": { + // result: BenchmarkResults.Pass, + // contributions: [ + // { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-03T14:00:00Z") }, + // ], + // }, + + // TODO: `Contract Naming` category is temporarily hidden due to unfit content, + // and so are all benchmarks belonging to it. + // We aim to fix it as soon as we have the capacity. + // See: https://github.com/namehash/ensawards/issues/222 + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Pass, + contributions: [ + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:26:20.566Z") }, + ], + notes: ( +
+

TODO: Add real benchmark notes

+ example proof +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Fail, + contributions: [ + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:26:20.566Z") }, + ], + notes: ( +
+

TODO: Add real benchmark notes

+ example proof +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, + "ensv2-ready-resolution": { + "correctly-resolve-ensv2-test-name-address": { + result: BenchmarkResults.Fail, + contributions: [ + { from: contributors.y3drk, lastUpdated: parseTimestamp("2026-06-05T14:45:00Z") }, + ], + notes: ( +
+

+ ENSv2 ready resolution was tested using the "search" flow. The resolved + address is NOT correct. +

+ Etherscan fails to resolve the name for ENSv2 +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, +} as const satisfies BestPracticeBenchmarks; + +defineAppBenchmarks(EtherscanExplorer, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/metamask-wallet/benchmarks.ts b/ensawards.org/data/apps/metamask-wallet/benchmarks.ts deleted file mode 100644 index 0ff47b69..00000000 --- a/ensawards.org/data/apps/metamask-wallet/benchmarks.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice -// on adding and modifying app benchmarks - -import MetaMaskWallet from "data/apps/metamask-wallet"; -import { defineAppBenchmarks } from "data/benchmarks/registry"; -import { BenchmarkResults } from "data/benchmarks/types"; -import type { BestPracticeBenchmarks } from "data/ens-best-practices/types.ts"; - -import { parseTimestamp } from "@ensnode/ensnode-sdk"; - -import contributors from "../../contributors"; - -const benchmarks = { - // "recognize-all-ens-names": { - // result: BenchmarkResults.Pass, - // contributions: [ - // { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-03T14:00:00Z") }, - // ], - // }, - "display-named-smart-contracts-mainnet": { - result: BenchmarkResults.Pass, - contributions: [ - { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:24:39.561Z") }, - ], - }, - "display-named-smart-contracts-l2-chains": { - result: BenchmarkResults.Fail, - contributions: [ - { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:24:39.561Z") }, - ], - }, -} as const satisfies BestPracticeBenchmarks; - -defineAppBenchmarks(MetaMaskWallet, benchmarks); - -export default benchmarks; diff --git a/ensawards.org/data/apps/metamask-wallet/benchmarks/acceptance-test-benchmark-proof-example.png b/ensawards.org/data/apps/metamask-wallet/benchmarks/acceptance-test-benchmark-proof-example.png new file mode 100644 index 00000000..30bb7663 Binary files /dev/null and b/ensawards.org/data/apps/metamask-wallet/benchmarks/acceptance-test-benchmark-proof-example.png differ diff --git a/ensawards.org/data/apps/metamask-wallet/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png b/ensawards.org/data/apps/metamask-wallet/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png new file mode 100644 index 00000000..da93b13b Binary files /dev/null and b/ensawards.org/data/apps/metamask-wallet/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png differ diff --git a/ensawards.org/data/apps/metamask-wallet/benchmarks/index.tsx b/ensawards.org/data/apps/metamask-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..61d652f7 --- /dev/null +++ b/ensawards.org/data/apps/metamask-wallet/benchmarks/index.tsx @@ -0,0 +1,81 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice +// on adding and modifying app benchmarks + +import type { AcceptanceTestBenchmark } from "data/acceptance-tests/types"; +import MetaMaskWallet from "data/apps/metamask-wallet"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import { BenchmarkResults } from "data/benchmarks/types"; +import contributors from "data/contributors"; +import { acceptanceTestDetailsContainerStyles } from "data/ens-best-practices/styles"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types.ts"; + +import { parseTimestamp } from "@ensnode/ensnode-sdk"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +import correctlyResolveEnsv2TestNameAddressProofImage from "./correctly-resolve-ensv2-test-name-address-proof.png"; + +const benchmarks = { + // "recognize-all-ens-names": { + // result: BenchmarkResults.Pass, + // contributions: [ + // { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-03T14:00:00Z") }, + // ], + // }, + + // TODO: `Contract Naming` category is temporarily hidden due to unfit content, + // and so are all benchmarks belonging to it. + // We aim to fix it as soon as we have the capacity. + // See: https://github.com/namehash/ensawards/issues/222 + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Pass, + contributions: [ + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:24:39.561Z") }, + ], + notes: ( +
+

Benchmark placeholder notes

+
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Fail, + contributions: [ + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:24:39.561Z") }, + ], + notes: ( +
+

Benchmark placeholder notes

+
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, + "ensv2-ready-resolution": { + "correctly-resolve-ensv2-test-name-address": { + result: BenchmarkResults.Pass, + contributions: [ + { from: contributors.y3drk, lastUpdated: parseTimestamp("2026-06-05T15:34:00Z") }, + ], + notes: ( +
+

+ ENSv2 ready resolution was tested using the "send" flow. The resolved address + is correct. +

+ MetaMask Wallet correctly resolves the name for ENSv2 +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, +} as const satisfies BestPracticeBenchmarks; + +defineAppBenchmarks(MetaMaskWallet, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/rainbow-wallet/benchmarks.ts b/ensawards.org/data/apps/rainbow-wallet/benchmarks.ts deleted file mode 100644 index 16b316e8..00000000 --- a/ensawards.org/data/apps/rainbow-wallet/benchmarks.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice -// on adding and modifying app benchmarks - -import RainbowWallet from "data/apps/rainbow-wallet"; -import { defineAppBenchmarks } from "data/benchmarks/registry"; -import { BenchmarkResults } from "data/benchmarks/types"; -import type { BestPracticeBenchmarks } from "data/ens-best-practices/types.ts"; - -import { parseTimestamp } from "@ensnode/ensnode-sdk"; - -import contributors from "../../contributors"; - -const benchmarks = { - // "recognize-all-ens-names": { - // result: BenchmarkResults.Pass, - // contributions: [ - // { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-03T14:00:00Z") }, - // ], - // }, - "display-named-smart-contracts-mainnet": { - result: BenchmarkResults.Pass, - contributions: [ - { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:19:28.672Z") }, - ], - }, - "display-named-smart-contracts-l2-chains": { - result: BenchmarkResults.Fail, - contributions: [ - { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:19:28.672Z") }, - ], - }, -} as const satisfies BestPracticeBenchmarks; - -defineAppBenchmarks(RainbowWallet, benchmarks); - -export default benchmarks; diff --git a/ensawards.org/data/apps/rainbow-wallet/benchmarks/acceptance-test-benchmark-proof-example.png b/ensawards.org/data/apps/rainbow-wallet/benchmarks/acceptance-test-benchmark-proof-example.png new file mode 100644 index 00000000..30bb7663 Binary files /dev/null and b/ensawards.org/data/apps/rainbow-wallet/benchmarks/acceptance-test-benchmark-proof-example.png differ diff --git a/ensawards.org/data/apps/rainbow-wallet/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png b/ensawards.org/data/apps/rainbow-wallet/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png new file mode 100644 index 00000000..c5ad4fe2 Binary files /dev/null and b/ensawards.org/data/apps/rainbow-wallet/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png differ diff --git a/ensawards.org/data/apps/rainbow-wallet/benchmarks/index.tsx b/ensawards.org/data/apps/rainbow-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..7ae5f55c --- /dev/null +++ b/ensawards.org/data/apps/rainbow-wallet/benchmarks/index.tsx @@ -0,0 +1,92 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice +// on adding and modifying app benchmarks + +import type { AcceptanceTestBenchmark } from "data/acceptance-tests/types"; +import RainbowWallet from "data/apps/rainbow-wallet"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import { BenchmarkResults } from "data/benchmarks/types"; +import contributors from "data/contributors"; +import { acceptanceTestDetailsContainerStyles } from "data/ens-best-practices/styles"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types.ts"; + +import { parseTimestamp } from "@ensnode/ensnode-sdk"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +import exampleProofImage from "./acceptance-test-benchmark-proof-example.png"; +import correctlyResolveEnsv2TestNameAddressProofImage from "./correctly-resolve-ensv2-test-name-address-proof.png"; + +const benchmarks = { + // "recognize-all-ens-names": { + // result: BenchmarkResults.Pass, + // contributions: [ + // { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-03T14:00:00Z") }, + // ], + // }, + + // TODO: `Contract Naming` category is temporarily hidden due to unfit content, + // and so are all benchmarks belonging to it. + // We aim to fix it as soon as we have the capacity. + // See: https://github.com/namehash/ensawards/issues/222 + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Pass, + contributions: [ + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:19:28.672Z") }, + ], + notes: ( +
+

Benchmark placeholder notes

+ example proof +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Fail, + contributions: [ + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:19:28.672Z") }, + ], + notes: ( +
+

Benchmark placeholder notes

+ example proof +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, + "ensv2-ready-resolution": { + "correctly-resolve-ensv2-test-name-address": { + result: BenchmarkResults.Fail, + contributions: [ + { from: contributors.y3drk, lastUpdated: parseTimestamp("2026-06-05T15:12:00Z") }, + ], + notes: ( +
+

+ ENSv2 ready resolution was tested using the "send" flow. The resolved address + is NOT correct. +

+ Rainbow Wallet fails to resolve the name for ENSv2 +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, +} as const satisfies BestPracticeBenchmarks; + +defineAppBenchmarks(RainbowWallet, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/utils.test.ts b/ensawards.org/data/apps/utils.test.ts index 569c3f59..8304819b 100644 --- a/ensawards.org/data/apps/utils.test.ts +++ b/ensawards.org/data/apps/utils.test.ts @@ -1,8 +1,8 @@ -import { type AppBenchmark, BenchmarkResults } from "data/benchmarks/types.ts"; +import { BenchmarkResults } from "data/benchmarks/types.ts"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { - createMockBenchmark, + createMockAcceptanceTestBenchmark, mockCoinbaseWalletApp, mockDisplayProfilesBestPractice, mockEtherscanApp, @@ -11,14 +11,16 @@ import { mockNormalizeNamesBestPractice, mockRainbowApp, mockReverseResolutionBestPractice, -} from "../shared/test-utils.ts"; +} from "../shared/test-utils"; import { type App, type AppBenchmarks, type AppSlug, AppTypes } from "./types.ts"; -const { mockApps, mockEnsAwardsPoints, mockBenchmarks } = vi.hoisted(() => ({ - mockApps: [] as App[], - mockEnsAwardsPoints: vi.fn(), - mockBenchmarks: {} as AppBenchmarks, -})); +const { mockApps, mockEnsAwardsPoints, mockBenchmarks, mockGetAcceptanceTestBenchmarksByApp } = + vi.hoisted(() => ({ + mockApps: [] as App[], + mockEnsAwardsPoints: vi.fn(), + mockBenchmarks: {} as AppBenchmarks, + mockGetAcceptanceTestBenchmarksByApp: vi.fn(), + })); vi.mock("./index.ts", () => ({ APPS: mockApps, @@ -33,6 +35,12 @@ vi.mock("data/benchmarks/utils.ts", () => ({ getAppBenchmarks: (slug: AppSlug) => mockBenchmarks[slug], })); +vi.mock("data/acceptance-tests/utils.ts", () => ({ + getAcceptanceTestBenchmarksByApp: mockGetAcceptanceTestBenchmarksByApp, +})); + +import type { AcceptanceTestBenchmark } from "data/acceptance-tests/types.ts"; + import { appliesToAllApps, asAppType, @@ -51,7 +59,7 @@ describe("App utils", () => { beforeEach(() => { mockApps.splice(0, mockApps.length); mockEnsAwardsPoints.mockReset(); - mockEnsAwardsPoints.mockImplementation((benchmark: AppBenchmark) => { + mockEnsAwardsPoints.mockImplementation((benchmark: AcceptanceTestBenchmark) => { switch (benchmark.result) { case BenchmarkResults.Pass: return 1; @@ -62,6 +70,26 @@ describe("App utils", () => { return 0; } }); + + mockGetAcceptanceTestBenchmarksByApp.mockReset(); + mockGetAcceptanceTestBenchmarksByApp.mockImplementation((appSlug: AppSlug) => { + switch (appSlug) { + case mockCoinbaseWalletApp.appSlug: + return Object.values(mockBenchmarks[mockCoinbaseWalletApp.appSlug]).flatMap( + (bestPracticeBenchmarks) => Object.values(bestPracticeBenchmarks), + ); + case mockRainbowApp.appSlug: + return Object.values(mockBenchmarks[mockRainbowApp.appSlug]).flatMap( + (bestPracticeBenchmarks) => Object.values(bestPracticeBenchmarks), + ); + case mockMetamaskApp.appSlug: + return Object.values(mockBenchmarks[mockMetamaskApp.appSlug]).flatMap( + (bestPracticeBenchmarks) => Object.values(bestPracticeBenchmarks), + ); + default: + throw new Error(`No benchmarks defined for app with slug ${appSlug}`); + } + }); }); describe("validateAppType", () => { @@ -117,16 +145,18 @@ describe("App utils", () => { it("Should return the rounded ENSAwards score for an app with benchmarks", () => { mockBenchmarks[mockCoinbaseWalletApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Pass, - ), - [mockDisplayProfilesBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Pass, - ), - [mockForwardResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Fail, - ), - [mockNormalizeNamesBestPractice.bestPracticeSlug]: undefined, + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + }, + [mockDisplayProfilesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug2: createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + }, + [mockForwardResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug3: createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + }, + [mockNormalizeNamesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug4: undefined, + }, }; const result = calcAppScore(mockCoinbaseWalletApp); @@ -136,10 +166,18 @@ describe("App utils", () => { it("Should return undefined when the app has no defined benchmarks", () => { mockBenchmarks[mockCoinbaseWalletApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: undefined, - [mockDisplayProfilesBestPractice.bestPracticeSlug]: undefined, - [mockForwardResolutionBestPractice.bestPracticeSlug]: undefined, - [mockNormalizeNamesBestPractice.bestPracticeSlug]: undefined, + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: undefined, + }, + [mockDisplayProfilesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug2: undefined, + }, + [mockForwardResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug3: undefined, + }, + [mockNormalizeNamesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug4: undefined, + }, }; const result = calcAppScore(mockCoinbaseWalletApp); @@ -149,9 +187,9 @@ describe("App utils", () => { it("Should throw when the calculated score is greater than 100", () => { mockEnsAwardsPoints.mockReturnValue(2); mockBenchmarks[mockCoinbaseWalletApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Pass, - ), + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + }, }; expect(() => calcAppScore(mockCoinbaseWalletApp)).toThrow( @@ -162,9 +200,9 @@ describe("App utils", () => { it("Should throw when the calculated score is less than 0", () => { mockEnsAwardsPoints.mockReturnValue(-1); mockBenchmarks[mockCoinbaseWalletApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Pass, - ), + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + }, }; expect(() => calcAppScore(mockCoinbaseWalletApp)).toThrow( @@ -186,36 +224,48 @@ describe("App utils", () => { describe("sortApps", () => { it("Should sort apps in descending order of their scores", () => { mockBenchmarks[mockCoinbaseWalletApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Pass, - ), - [mockDisplayProfilesBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Pass, - ), - [mockForwardResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Fail, - ), - [mockNormalizeNamesBestPractice.bestPracticeSlug]: undefined, + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + }, + [mockDisplayProfilesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug2: createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + }, + [mockForwardResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug3: createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + }, + [mockNormalizeNamesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug4: undefined, + }, }; mockBenchmarks[mockRainbowApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Pass, - ), - [mockDisplayProfilesBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Fail, - ), - [mockForwardResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Fail, - ), - [mockNormalizeNamesBestPractice.bestPracticeSlug]: undefined, + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + }, + [mockDisplayProfilesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug2: createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + }, + [mockForwardResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug3: createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + }, + [mockNormalizeNamesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug4: undefined, + }, }; mockBenchmarks[mockMetamaskApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: undefined, - [mockDisplayProfilesBestPractice.bestPracticeSlug]: undefined, - [mockForwardResolutionBestPractice.bestPracticeSlug]: undefined, - [mockNormalizeNamesBestPractice.bestPracticeSlug]: undefined, + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: undefined, + }, + [mockDisplayProfilesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug2: undefined, + }, + [mockForwardResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug3: undefined, + }, + [mockNormalizeNamesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug4: undefined, + }, }; const apps = [mockMetamaskApp, mockRainbowApp, mockCoinbaseWalletApp]; diff --git a/ensawards.org/data/apps/utils.ts b/ensawards.org/data/apps/utils.ts index aabb1043..72ff4622 100644 --- a/ensawards.org/data/apps/utils.ts +++ b/ensawards.org/data/apps/utils.ts @@ -1,6 +1,7 @@ +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, getAppBenchmarks } from "data/benchmarks/utils.ts"; +import { calcEnsAwardsPoints } from "data/benchmarks/utils.ts"; import { EntityMetadataTypes } from "data/entity-metadata/types.ts"; import { asEnsAwardsScore, @@ -55,24 +56,24 @@ export const getAppByName = (appName: string): App | undefined => { /** * Calculates {@link EnsAwardsScore} for an app. * - * @returns undefined - if no benchmarks are completed. + * @returns undefined - if no benchmarks are completed or + * if all completed benchmarks belong to a `BestPracticeCategory` + * with status other than `Active`. */ export const calcAppScore = (app: App): EnsAwardsScore | undefined => { - const appBenchmarks = getAppBenchmarks(app.appSlug); - - const completedBenchmarks = Object.values(appBenchmarks).filter( - (benchmark) => benchmark !== undefined, + const completedAcceptanceTestBenchmarks = getAcceptanceTestBenchmarksByApp(app.appSlug).filter( + (acceptanceTestBenchmark) => acceptanceTestBenchmark !== undefined, ); - if (completedBenchmarks.length === 0) return undefined; + if (completedAcceptanceTestBenchmarks.length === 0) return undefined; - const totalPoints: EnsAwardsPoints = completedBenchmarks.reduce( + const totalPoints: EnsAwardsPoints = completedAcceptanceTestBenchmarks.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) / completedBenchmarks.length); + const score = Math.round((totalPoints * 100) / completedAcceptanceTestBenchmarks.length); return asEnsAwardsScore(score); }; diff --git a/ensawards.org/data/apps/walletchan-wallet/benchmarks.ts b/ensawards.org/data/apps/walletchan-wallet/benchmarks.ts deleted file mode 100644 index 5c54f5f5..00000000 --- a/ensawards.org/data/apps/walletchan-wallet/benchmarks.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice -// on adding and modifying app benchmarks - -import WalletChanWallet from "data/apps/walletchan-wallet"; -import { defineAppBenchmarks } from "data/benchmarks/registry"; -import { BenchmarkResults } from "data/benchmarks/types.ts"; -import contributors from "data/contributors"; -import type { BestPracticeBenchmarks } from "data/ens-best-practices/types"; - -import { parseTimestamp } from "@ensnode/ensnode-sdk"; - -const benchmarks: BestPracticeBenchmarks = { - "display-named-smart-contracts-mainnet": { - result: BenchmarkResults.Pass, - contributions: [ - { from: contributors.apoorvlathey, lastUpdated: parseTimestamp("2026-04-18T00:00:00Z") }, - ], - }, - "display-named-smart-contracts-l2-chains": { - result: BenchmarkResults.Pass, - contributions: [ - { - from: contributors.apoorvlathey, - lastUpdated: parseTimestamp("2026-04-18T00:00:00Z"), - }, - ], - }, -} as const satisfies BestPracticeBenchmarks; - -defineAppBenchmarks(WalletChanWallet, benchmarks); - -export default benchmarks; diff --git a/ensawards.org/data/apps/walletchan-wallet/benchmarks/acceptance-test-benchmark-proof-example.png b/ensawards.org/data/apps/walletchan-wallet/benchmarks/acceptance-test-benchmark-proof-example.png new file mode 100644 index 00000000..30bb7663 Binary files /dev/null and b/ensawards.org/data/apps/walletchan-wallet/benchmarks/acceptance-test-benchmark-proof-example.png differ diff --git a/ensawards.org/data/apps/walletchan-wallet/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png b/ensawards.org/data/apps/walletchan-wallet/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png new file mode 100644 index 00000000..27ae1224 Binary files /dev/null and b/ensawards.org/data/apps/walletchan-wallet/benchmarks/correctly-resolve-ensv2-test-name-address-proof.png differ diff --git a/ensawards.org/data/apps/walletchan-wallet/benchmarks/index.tsx b/ensawards.org/data/apps/walletchan-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..0c49281b --- /dev/null +++ b/ensawards.org/data/apps/walletchan-wallet/benchmarks/index.tsx @@ -0,0 +1,90 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice +// on adding and modifying app benchmarks + +import type { AcceptanceTestBenchmark } from "data/acceptance-tests/types"; +import WalletChanWallet from "data/apps/walletchan-wallet"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import { BenchmarkResults } from "data/benchmarks/types.ts"; +import contributors from "data/contributors"; +import { acceptanceTestDetailsContainerStyles } from "data/ens-best-practices/styles"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types"; + +import { parseTimestamp } from "@ensnode/ensnode-sdk"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +import exampleProofImage from "./acceptance-test-benchmark-proof-example.png"; +import correctlyResolveEnsv2TestNameAddressProofImage from "./correctly-resolve-ensv2-test-name-address-proof.png"; +import namedSmartContractsOnMainnetProofImage from "./named-smart-contracts-on-mainnet-proof.png"; + +const benchmarks: BestPracticeBenchmarks = { + // TODO: `Contract Naming` category is temporarily hidden due to unfit content, + // and so are all benchmarks belonging to it. + // We aim to fix it as soon as we have the capacity. + // See: https://github.com/namehash/ensawards/issues/222 + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Pass, + contributions: [ + { from: contributors.apoorvlathey, lastUpdated: parseTimestamp("2026-04-18T00:00:00Z") }, + ], + notes: ( +
+

+ Screenshot presents a transaction interaction on mainnet (setting name's records using + ENS public resolver contract) where the user is interacting with a contract that has an + ENS name, and the wallet is correctly displaying the ENS name of the contract. +

+ proof image +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Pass, + contributions: [ + { from: contributors.apoorvlathey, lastUpdated: parseTimestamp("2026-04-18T00:00:00Z") }, + ], + notes: ( +
+

TODO: Add correct Benchmark notes

+ example proof +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, + "ensv2-ready-resolution": { + "correctly-resolve-ensv2-test-name-address": { + result: BenchmarkResults.Fail, + contributions: [ + { from: contributors.y3drk, lastUpdated: parseTimestamp("2026-06-05T16:07:00Z") }, + ], + notes: ( +
+

+ The resolution was tested using the "send" flow. The app failed to resolve the + address for the test name, which we interpret as a failure. +

+ WalletChan failed to resolve the test name +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, +} as const satisfies BestPracticeBenchmarks; + +defineAppBenchmarks(WalletChanWallet, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/walletchan-wallet/benchmarks/named-smart-contracts-on-mainnet-proof.png b/ensawards.org/data/apps/walletchan-wallet/benchmarks/named-smart-contracts-on-mainnet-proof.png new file mode 100644 index 00000000..3dfe96b5 Binary files /dev/null and b/ensawards.org/data/apps/walletchan-wallet/benchmarks/named-smart-contracts-on-mainnet-proof.png differ diff --git a/ensawards.org/data/benchmarks/index.test.ts b/ensawards.org/data/benchmarks/index.test.ts index f908c185..7619cba1 100644 --- a/ensawards.org/data/benchmarks/index.test.ts +++ b/ensawards.org/data/benchmarks/index.test.ts @@ -1,11 +1,9 @@ +import type { AcceptanceTestBenchmark, AcceptanceTestSlug } from "data/acceptance-tests/types"; import { APPS } from "data/apps"; -import type { AppBenchmark } from "data/benchmarks/types"; +import { type AppSlug } from "data/apps/types"; +import { getAppBySlug } from "data/apps/utils"; import { ENS_BEST_PRACTICES } from "data/ens-best-practices"; -import { - type BestPracticeApp, - type BestPracticeSlug, - BestPracticeTypes, -} from "data/ens-best-practices/types"; +import { type BestPracticeSlug, BestPracticeTypes } from "data/ens-best-practices/types"; import { stringifyAccountId } from "enssdk"; import { describe, expect, it } from "vitest"; @@ -26,45 +24,107 @@ describe("Benchmarks registry", () => { }); describe("All BestPracticeBenchmarks in APP_BENCHMARKS", () => { - it("Should have an explicit entry for all app-related best practices", () => { - const bestPracticeSlugs = ENS_BEST_PRACTICES.filter( - (bestPractice): bestPractice is BestPracticeApp => - bestPractice.type === BestPracticeTypes.App, - ).map((bp) => bp.bestPracticeSlug); - - Object.values(data).forEach((appBenchmarks) => { - bestPracticeSlugs.forEach((slug) => { + const getApplicableBestPracticesSlugsForApp = (appSlug: AppSlug): BestPracticeSlug[] => { + const app = getAppBySlug(appSlug); + + if (app === undefined) { + throw new Error(`Invariant(AppSlug): App with slug={${appSlug}} is not defined`); + } + + const applicableBestPracticesSlugs = ENS_BEST_PRACTICES.filter((bestPractice) => { + if (bestPractice.type !== BestPracticeTypes.App) { + return false; + } + + return bestPractice.appliesTo.includes(app.type); + }).map((bp) => bp.bestPracticeSlug); + + return applicableBestPracticesSlugs; + }; + + it("Should have an explicit entry for all applicable best practices", () => { + Object.entries(data).forEach(([appSlug, appBenchmarks]) => { + getApplicableBestPracticesSlugsForApp(appSlug).forEach((slug) => { expect( appBenchmarks, - `Missing benchmark entry for best practice slug={${slug}}`, + `Missing benchmark entry for best practice slug={${slug}} for app slug={${appSlug}}`, ).toHaveProperty(slug); }); }); }); + it("Shouldn't have benchmark entries for non-applicable best practices", () => { + Object.entries(data).forEach(([appSlug, appBenchmarks]) => { + const applicableBestPracticesSlugs = getApplicableBestPracticesSlugsForApp(appSlug); + + Object.keys(appBenchmarks).forEach((bestPracticeSlug) => { + expect( + applicableBestPracticesSlugs, + `Benchmark entry for best practice slug={${bestPracticeSlug}} exists for app slug={${appSlug}} but is not applicable for that app`, + ).toContain(bestPracticeSlug); + }); + }); + }); + }); + + describe("All AcceptanceTestBenchmarks entries in all BestPracticeBenchmarks", () => { + it("Should have an explicit entry for all acceptance tests defined for a given best practice", () => { + const acceptanceTestSlugsByBestPractice: Map = + new Map(); + + ENS_BEST_PRACTICES.forEach((bestPractice) => { + const acceptanceTestSlugs = bestPractice.technicalDetails.acceptanceTests.map( + (acceptanceTest) => acceptanceTest.acceptanceTestSlug, + ); + acceptanceTestSlugsByBestPractice.set(bestPractice.bestPracticeSlug, acceptanceTestSlugs); + }); + + Object.entries(data).forEach(([appSlug, appBenchmarks]) => { + Object.entries(appBenchmarks).forEach(([bestPracticeSlug, appBenchmark]) => { + const acceptanceTestSlugs = Object.keys(appBenchmark).sort(); + const expectedSlugs = acceptanceTestSlugsByBestPractice.get(bestPracticeSlug); + + expect( + acceptanceTestSlugs, + `Missing acceptance test entries for best practice slug={${bestPracticeSlug}}`, + ).toEqual(expectedSlugs?.sort()); + }); + }); + }); + it("Should have valid data structure for defined benchmarks", () => { Object.entries(data).forEach(([appSlug, appBenchmarks]) => { - Object.entries(appBenchmarks) - .filter((entry): entry is [BestPracticeSlug, AppBenchmark] => entry[1] !== undefined) - .forEach(([bestPracticeSlug, benchmark]) => { - expect( - benchmark.result, - `Benchmark for ${appSlug} on ${bestPracticeSlug} missing result`, - ).toBeDefined(); - expect( - benchmark.contributions.length, - `Benchmark for ${appSlug} on ${bestPracticeSlug} missing contributions`, - ).toBeGreaterThan(0); - - const contributorsList = benchmark.contributions.map((contribution) => - stringifyAccountId(contribution.from), - ); - const uniqueContributors = new Set(contributorsList); - expect( - uniqueContributors.size, - `Benchmark for ${appSlug} on ${bestPracticeSlug} has duplicate contributors`, - ).toEqual(contributorsList.length); - }); + Object.entries(appBenchmarks).forEach(([bestPracticeSlug, appBenchmark]) => { + Object.entries(appBenchmark) + .filter( + (entry): entry is [AcceptanceTestSlug, AcceptanceTestBenchmark] => + entry[1] !== undefined, + ) + .forEach(([acceptanceTestSlug, benchmark]) => { + expect( + benchmark.result, + `Benchmark for ${appSlug} on ${bestPracticeSlug} | ${acceptanceTestSlug} missing result`, + ).toBeDefined(); + expect( + benchmark.contributions.length, + `Benchmark for ${appSlug} on ${bestPracticeSlug} | ${acceptanceTestSlug} missing contributions`, + ).toBeGreaterThan(0); + + const contributorsList = benchmark.contributions.map((contribution) => + stringifyAccountId(contribution.from), + ); + const uniqueContributors = new Set(contributorsList); + expect( + uniqueContributors.size, + `Benchmark for ${appSlug} on ${bestPracticeSlug} | ${acceptanceTestSlug} has duplicate contributors`, + ).toEqual(contributorsList.length); + + expect( + benchmark.notes, + `Benchmark for ${appSlug} on ${bestPracticeSlug} | ${acceptanceTestSlug} missing notes`, + ).toBeDefined(); + }); + }); }); }); }); diff --git a/ensawards.org/data/benchmarks/index.ts b/ensawards.org/data/benchmarks/index.ts index ecd31dbe..35def1a1 100644 --- a/ensawards.org/data/benchmarks/index.ts +++ b/ensawards.org/data/benchmarks/index.ts @@ -1,5 +1,5 @@ import { getAppBenchmarks } from "data/benchmarks/registry"; -import.meta.glob("../apps/*/benchmarks.ts", { eager: true }); +import.meta.glob("../apps/*/benchmarks/index.tsx", { eager: true }); export const APP_BENCHMARKS = getAppBenchmarks(); diff --git a/ensawards.org/data/benchmarks/types.ts b/ensawards.org/data/benchmarks/types.ts index f1cd664f..51fc57b2 100644 --- a/ensawards.org/data/benchmarks/types.ts +++ b/ensawards.org/data/benchmarks/types.ts @@ -1,4 +1,4 @@ -import type { Contribution } from "data/contributors/types.ts"; +import type { AcceptanceTestBenchmark, AcceptanceTestSlug } from "data/acceptance-tests/types"; export const BenchmarkResults = { Pass: "passed", @@ -9,17 +9,19 @@ export const BenchmarkResults = { export type BenchmarkResult = (typeof BenchmarkResults)[keyof typeof BenchmarkResults]; /** - * Represents the benchmark of an {@link App} against a {@link BestPractice}. + * Represents the benchmarks of an {@link App} against {@link AcceptanceTest}s of {@link BestPractice}s. + * + * @invariant + * An explicit key for each {@link AcceptanceTestSlug} should be added to this `Record` + * for each {@link AcceptanceTest} available on a given best practice. + * + * If an app doesn't have a benchmark completed for an `AcceptanceTest` + * then the benchmark should be explicitly set to `undefined`. + * + * Otherwise, the value should be an `AcceptanceTestBenchmark` + * describing how the related app performed for the `AcceptanceTest`. */ -export interface AppBenchmark { - /** The result of the benchmark */ - result: BenchmarkResult; - - /** A record of all contributors involved in the addition or maintenance of the benchmark's data. - * - * @invariant Multiple {@link Contribution} from the same contributor on the same app benchmark are not allowed. - * When a contributor makes updates to their existing contribution, - * they should update the `lastUpdated` timestamp of their existing `Contribution`. - */ - contributions: [Contribution, ...Contribution[]]; -} +export type AcceptanceTestBenchmarks = Record< + AcceptanceTestSlug, + AcceptanceTestBenchmark | undefined +>; diff --git a/ensawards.org/data/benchmarks/utils.test.ts b/ensawards.org/data/benchmarks/utils.test.ts index c8fdb1d3..c5b04466 100644 --- a/ensawards.org/data/benchmarks/utils.test.ts +++ b/ensawards.org/data/benchmarks/utils.test.ts @@ -2,17 +2,19 @@ import { calcBestPracticeCategoryScore, calcEnsAwardsPoints, groupBenchmarksByCategory, - sortBenchmarks, + sortAcceptanceTestBenchmarks, + sortBenchmarkResults, } from "data/benchmarks/utils.ts"; import type { BestPracticeBenchmarks, BestPracticeSlug } from "data/ens-best-practices/types.ts"; import { - createMockBenchmark, + createMockAcceptanceTestBenchmark, + createMockBestPractice, mockDisplayProfilesBestPractice, mockForwardResolutionBestPractice, mockNormalizeNamesBestPractice, mockReverseResolutionBestPractice, -} from "data/shared/test-utils.ts"; -import { describe, expect, it, vi } from "vitest"; +} from "data/shared/test-utils"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { BenchmarkResults } from "./types.ts"; @@ -20,24 +22,28 @@ const { mockGetBestPracticeBySlug } = vi.hoisted(() => ({ mockGetBestPracticeBySlug: vi.fn(), })); -vi.mock("data/ens-best-practices/utils.ts", () => ({ - getBestPracticeBySlug: mockGetBestPracticeBySlug, -})); +vi.mock(import("data/ens-best-practices/utils.ts"), async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getBestPracticeBySlug: mockGetBestPracticeBySlug, + }; +}); describe("benchmarks-utils", () => { describe("calcEnsAwardsPoints", () => { it("Should return correct points for each benchmark result type", () => { const benchmarkCases = [ { - benchmark: createMockBenchmark(BenchmarkResults.Pass), + benchmark: createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), expectedPoints: 1, }, { - benchmark: createMockBenchmark(BenchmarkResults.PartialPass), + benchmark: createMockAcceptanceTestBenchmark(BenchmarkResults.PartialPass), expectedPoints: 0.5, }, { - benchmark: createMockBenchmark(BenchmarkResults.Fail), + benchmark: createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), expectedPoints: 0, }, ]; @@ -58,9 +64,15 @@ describe("benchmarks-utils", () => { }); const benchmarks: BestPracticeBenchmarks = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: undefined, - [mockDisplayProfilesBestPractice.bestPracticeSlug]: undefined, - [mockForwardResolutionBestPractice.bestPracticeSlug]: undefined, + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: undefined, + }, + [mockDisplayProfilesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: undefined, + }, + [mockForwardResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: undefined, + }, } as const satisfies BestPracticeBenchmarks; expect( @@ -79,9 +91,15 @@ describe("benchmarks-utils", () => { }); const benchmarks: BestPracticeBenchmarks = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: undefined, - [mockDisplayProfilesBestPractice.bestPracticeSlug]: undefined, - [mockForwardResolutionBestPractice.bestPracticeSlug]: undefined, + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: undefined, + }, + [mockDisplayProfilesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug2: undefined, + }, + [mockForwardResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug3: undefined, + }, } as const satisfies BestPracticeBenchmarks; const result = groupBenchmarksByCategory(benchmarks); @@ -90,18 +108,85 @@ describe("benchmarks-utils", () => { Array.from(result.values()), "Expected groupBenchmarksByCategory to split benchmarks into separate category groups", ).toEqual([ - { "set-primary-name": undefined }, - { "display-avatar": undefined, "forward-resolution": undefined }, + { + "set-primary-name": { + mockAcceptanceTestSlug1: undefined, + }, + }, + { + "display-avatar": { + mockAcceptanceTestSlug2: undefined, + }, + "forward-resolution": { + mockAcceptanceTestSlug3: undefined, + }, + }, ]); }); }); describe("calcBestPracticeCategoryScore", () => { + const mockBestPracticeCategoryDetails = { + categoryId: "mock-category", + categoryName: "Mock Category", + categorySlug: "mock-category", + }; + + const mockBestPractice1 = createMockBestPractice({ + id: "mock-best-practice-1", + name: "Mock Best Practice 1", + bestPracticeSlug: "mock-best-practice-1", + ...mockBestPracticeCategoryDetails, + }); + + const mockBestPractice2 = createMockBestPractice({ + id: "mock-best-practice-2", + name: "Mock Best Practice 2", + bestPracticeSlug: "mock-best-practice-2", + ...mockBestPracticeCategoryDetails, + }); + + const mockBestPractice3 = createMockBestPractice({ + id: "mock-best-practice-3", + name: "Mock Best Practice 3", + bestPracticeSlug: "mock-best-practice-3", + ...mockBestPracticeCategoryDetails, + }); + + beforeEach(() => { + mockGetBestPracticeBySlug.mockReset(); + + mockGetBestPracticeBySlug.mockImplementation((slug: BestPracticeSlug) => { + switch (slug) { + case mockBestPractice1.bestPracticeSlug: + return mockBestPractice1; + + case mockBestPractice2.bestPracticeSlug: + return mockBestPractice2; + + case mockBestPractice3.bestPracticeSlug: + return mockBestPractice3; + + case mockReverseResolutionBestPractice.bestPracticeSlug: + return mockReverseResolutionBestPractice; + + default: + throw new Error(`No best practice found for slug: ${slug}`); + } + }); + }); + it("Should return undefined for no completed benchmarks", () => { const benchmarks: BestPracticeBenchmarks = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: undefined, - [mockDisplayProfilesBestPractice.bestPracticeSlug]: undefined, - [mockForwardResolutionBestPractice.bestPracticeSlug]: undefined, + [mockBestPractice1.bestPracticeSlug]: { + mockAcceptanceTestSlug1: undefined, + }, + [mockBestPractice2.bestPracticeSlug]: { + mockAcceptanceTestSlug2: undefined, + }, + [mockBestPractice3.bestPracticeSlug]: { + mockAcceptanceTestSlug3: undefined, + }, }; expect( @@ -112,15 +197,15 @@ describe("benchmarks-utils", () => { it("Should return the rounded category score for valid benchmarks", () => { const validCategoryBenchmarks = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Pass, - ), - [mockDisplayProfilesBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Fail, - ), - [mockForwardResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Fail, - ), + [mockBestPractice1.bestPracticeSlug]: { + mockAcceptanceTestSlug1: createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + }, + [mockBestPractice2.bestPracticeSlug]: { + mockAcceptanceTestSlug2: createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + }, + [mockBestPractice3.bestPracticeSlug]: { + mockAcceptanceTestSlug3: createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + }, } as const satisfies BestPracticeBenchmarks; const result = calcBestPracticeCategoryScore(validCategoryBenchmarks); @@ -130,27 +215,73 @@ describe("benchmarks-utils", () => { `Expected calcBestPracticeCategoryScore to return the rounded percentage for valid category benchmarks, got ${result} instead`, ).toEqual(33); }); + + it("Should throw an error if benchmarks belong to different categories", () => { + const benchmarks: BestPracticeBenchmarks = { + [mockBestPractice1.bestPracticeSlug]: { + mockAcceptanceTestSlug1: undefined, + }, + [mockBestPractice2.bestPracticeSlug]: { + mockAcceptanceTestSlug2: undefined, + }, + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug3: undefined, + }, + }; + + expect(() => calcBestPracticeCategoryScore(benchmarks)).toThrow( + "All benchmarks must belong to the same category", + ); + }); }); - describe("sortBenchmarks", () => { + describe("sortAcceptanceTestBenchmarks", () => { it("should allow correct sorting of benchmarks", () => { const input = [ undefined, - createMockBenchmark(BenchmarkResults.Fail), - createMockBenchmark(BenchmarkResults.Pass), - createMockBenchmark(BenchmarkResults.Fail), - createMockBenchmark(BenchmarkResults.PartialPass), + createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + createMockAcceptanceTestBenchmark(BenchmarkResults.PartialPass), + ]; + + const expectedOutput = [ + createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + createMockAcceptanceTestBenchmark(BenchmarkResults.PartialPass), + createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + undefined, + ]; + + const result = input.sort((a, b) => sortAcceptanceTestBenchmarks(a, b)); + + expectedOutput.forEach((benchmark, index) => + expect(benchmark, `Expected sorted benchmark at index ${index} to match`).toEqual( + result[index], + ), + ); + }); + }); + + describe("sortBenchmarkResults", () => { + it("should allow correct sorting of benchmark results", () => { + const input = [ + undefined, + BenchmarkResults.Fail, + BenchmarkResults.Pass, + BenchmarkResults.Fail, + BenchmarkResults.PartialPass, ]; const expectedOutput = [ - createMockBenchmark(BenchmarkResults.Pass), - createMockBenchmark(BenchmarkResults.PartialPass), - createMockBenchmark(BenchmarkResults.Fail), - createMockBenchmark(BenchmarkResults.Fail), + BenchmarkResults.Pass, + BenchmarkResults.PartialPass, + BenchmarkResults.Fail, + BenchmarkResults.Fail, undefined, ]; - const result = input.sort((a, b) => sortBenchmarks(a, b)); + const result = input.sort((a, b) => sortBenchmarkResults(a, b)); expectedOutput.forEach((benchmark, index) => expect(benchmark, `Expected sorted benchmark at index ${index} to match`).toEqual( diff --git a/ensawards.org/data/benchmarks/utils.ts b/ensawards.org/data/benchmarks/utils.ts index aa62ce62..409ace16 100644 --- a/ensawards.org/data/benchmarks/utils.ts +++ b/ensawards.org/data/benchmarks/utils.ts @@ -1,53 +1,97 @@ +import type { AcceptanceTestBenchmark } 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"; import { getBestPracticeBySlug } from "data/ens-best-practices/utils.ts"; import type { FormatTypeOptions } from "data/shared/format-type-options.ts"; import type { UnixTimestamp } from "enssdk"; -import type { BestPracticeBenchmarks, BestPracticeSlug } from "../ens-best-practices/types.ts"; -import { type BestPracticeCategorySlug } from "../ens-best-practices/types.ts"; +import type { + BestPractice, + BestPracticeBenchmarks, + BestPracticeSlug, +} from "../ens-best-practices/types.ts"; +import { + type BestPracticeCategory, + type BestPracticeCategorySlug, + BestPracticeTypes, + CategoryStatuses, +} from "../ens-best-practices/types.ts"; import { asEnsAwardsScore, type EnsAwardsPoints, type EnsAwardsScore, } from "../shared/ens-awards-score.ts"; import { APP_BENCHMARKS } from "."; -import { type AppBenchmark, type BenchmarkResult, BenchmarkResults } from "./types.ts"; +import { type AcceptanceTestBenchmarks, type BenchmarkResult, BenchmarkResults } from "./types.ts"; /** Returns all benchmarks of an {@link App} by its {@link AppSlug}. */ export function getAppBenchmarks(slug: AppSlug): BestPracticeBenchmarks { - return APP_BENCHMARKS[slug]; + const appBenchmarks = APP_BENCHMARKS[slug]; + + if (appBenchmarks === undefined) { + throw new Error( + `Invariant(AppBenchmarks): Benchmarks for app with slug ${slug} are not defined`, + ); + } + return appBenchmarks; } -/** Returns all benchmarks of a {@link BestPractice} by its {@link BestPracticeSlug}. - * - * If the related {@link App} doesn't have a benchmark completed for the specified {@link BestPractice} - * then this function will return `undefined` as a representation of a pending benchmark for that app. - * - * Otherwise, the value will be an `AppBenchmark` - * describing how the related {@link App} performed for the {@link BestPractice}. +/** + * Returns all {@link AcceptanceTestBenchmarks} of a {@link BestPractice} by its {@link BestPracticeSlug}. */ -export function getAppBenchmarksByBestPractice( +export function getAcceptanceTestBenchmarksByBestPractice( slug: BestPracticeSlug, -): (AppBenchmark | undefined)[] { - const benchmarks: (AppBenchmark | undefined)[] = []; +): AcceptanceTestBenchmarks[] { + const bestPractice = getBestPracticeBySlug(slug); + + if (bestPractice === undefined) { + throw new Error(`Invariant(BestPracticeSlug): BestPractice with slug ${slug} is not defined`); + } + + const benchmarks: AcceptanceTestBenchmarks[] = []; + + for (const [appSlug, appBenchmarks] of Object.entries(APP_BENCHMARKS)) { + const app = getAppBySlug(appSlug); + + if (app === undefined) { + throw new Error(`Invariant(AppSlug): App with slug ${appSlug} is not defined`); + } + + if (bestPractice.type === BestPracticeTypes.App && bestPractice.appliesTo.includes(app.type)) { + const acceptanceTestBenchmarks = appBenchmarks[slug]; - for (const appBenchmarks of Object.values(APP_BENCHMARKS)) { - benchmarks.push(appBenchmarks[slug]); + if (acceptanceTestBenchmarks === undefined) { + throw new Error( + `Invariant(BestPracticeBenchmarks): Required benchmarks for app-${appSlug} on best practice-${slug} are not defined`, + ); + } + + benchmarks.push(acceptanceTestBenchmarks); + } } return benchmarks; } -/** Returns a single benchmark of an {@link App} on a specific {@link BestPractice} +/** Returns the {@link AcceptanceTestBenchmarks} of an {@link App} on a specific {@link BestPractice} * decided by the {@link AppSlug} and {@link BestPracticeSlug}. * */ -export function getAppBenchmark( +export function getAppAcceptanceTestBenchmarks( appSlug: AppSlug, bestPracticeSlug: BestPracticeSlug, -): AppBenchmark | undefined { +): AcceptanceTestBenchmarks { const appBenchmarks = getAppBenchmarks(appSlug); - return appBenchmarks[bestPracticeSlug]; + const acceptanceTestBenchmarks = appBenchmarks[bestPracticeSlug]; + + if (acceptanceTestBenchmarks === undefined) { + throw new Error( + `Invariant(BestPracticeBenchmarks): Required benchmarks for app-${appSlug} on best practice-${bestPracticeSlug} are not defined`, + ); + } + + return acceptanceTestBenchmarks; } /** @@ -58,8 +102,10 @@ export function getAppBenchmark( * {@link BenchmarkResults.PartialPass} = 0.5 * {@link BenchmarkResults.Fail} = 0.0 */ -export const calcEnsAwardsPoints = (benchmark: AppBenchmark): EnsAwardsPoints => { - switch (benchmark.result) { +export const calcEnsAwardsPoints = (benchmark: AcceptanceTestBenchmark): EnsAwardsPoints => { + const benchmarkResult = benchmark.result; + + switch (benchmarkResult) { case BenchmarkResults.Pass: return 1; @@ -71,7 +117,7 @@ export const calcEnsAwardsPoints = (benchmark: AppBenchmark): EnsAwardsPoints => return 0; default: - const _exhaustive: never = benchmark.result; + const _exhaustive: never = benchmarkResult; throw new Error(`Unsupported BenchmarkResult: ${_exhaustive}`); } }; @@ -104,17 +150,53 @@ 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`. + * undefined - if no benchmarks are completed for the `BestPracticeCategory` + * or the category status is not `Active`. * Otherwise, an {@link EnsAwardsScore} calculation for the `BestPracticeCategory` * * @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 => { - const completedBenchmarks = Object.values(benchmarks).filter( - (benchmark) => benchmark !== undefined, - ); + let bestPracticeCategory: undefined | BestPracticeCategory = undefined; + + for (const bestPracticeSlug of Object.keys(benchmarks)) { + const bestPractice = getBestPracticeBySlug(bestPracticeSlug); + + if (!bestPractice) { + throw new Error( + `Invariant(BestPracticeSlug): BestPractice with slug ${bestPracticeSlug} is not defined`, + ); + } + + if (bestPracticeCategory === undefined) { + bestPracticeCategory = bestPractice.category; + } + + if (bestPractice.category.categorySlug !== bestPracticeCategory.categorySlug) { + throw new Error(`All benchmarks must belong to the same category`); + } + } + + if ( + bestPracticeCategory === undefined || + bestPracticeCategory.status !== CategoryStatuses.Active + ) { + return undefined; + } + + const completedBenchmarks: AcceptanceTestBenchmark[] = []; + + for (const acceptanceTestBenchmarks of Object.values(benchmarks)) { + for (const acceptanceTestBenchmark of Object.values(acceptanceTestBenchmarks)) { + if (acceptanceTestBenchmark !== undefined) { + completedBenchmarks.push(acceptanceTestBenchmark); + } + } + } + if (completedBenchmarks.length === 0) return undefined; const score = Math.round( @@ -133,12 +215,12 @@ const resultOrder = { [BenchmarkResults.Fail]: 2, } as const satisfies Record; -/** Sorts two {@link AppBenchmark}s relative to each other. */ -export const sortBenchmarks = ( - a: AppBenchmark | undefined, - b: AppBenchmark | undefined, +/** Sorts two {@link AcceptanceTestBenchmark}s relative to each other. */ +export const sortAcceptanceTestBenchmarks = ( + a: AcceptanceTestBenchmark | undefined, + b: AcceptanceTestBenchmark | undefined, ): number => { - // All undefined benchmarks are interpreted as pending + // All undefined acceptance test benchmarks are interpreted as pending // and should be sorted after completed benchmarks if (!a && !b) return 0; if (!a) return 1; @@ -148,7 +230,24 @@ export const sortBenchmarks = ( return resultDiff; }; -export const getBenchmarkLastUpdateTimestamp = (benchmark: AppBenchmark): UnixTimestamp => { +/** Sorts two {@link BenchmarkResult}s relative to each other. */ +export const sortBenchmarkResults = ( + a: BenchmarkResult | undefined, + b: BenchmarkResult | undefined, +): number => { + // All undefined benchmark results are interpreted as pending + // and should be sorted after completed benchmark results + if (!a && !b) return 0; + if (!a) return 1; + if (!b) return -1; + + const resultDiff = resultOrder[a] - resultOrder[b]; + return resultDiff; +}; + +export const getAcceptanceTestBenchmarkLastUpdateTimestamp = ( + benchmark: AcceptanceTestBenchmark, +): UnixTimestamp => { const contributionTimestamps = benchmark.contributions.map( (contribution) => contribution.lastUpdated, ); @@ -157,16 +256,16 @@ export const getBenchmarkLastUpdateTimestamp = (benchmark: AppBenchmark): UnixTi }; export const formatBenchmarkResult = ( - benchmark?: AppBenchmark, + benchmarkResult?: BenchmarkResult, options: Omit = { lowercase: false }, ): string => { const { lowercase } = options; - if (!benchmark) { + if (!benchmarkResult) { return lowercase ? "pending" : "Pending"; } - switch (benchmark.result) { + switch (benchmarkResult) { case BenchmarkResults.Pass: return lowercase ? "passed" : "Passed"; @@ -177,7 +276,35 @@ export const formatBenchmarkResult = ( return lowercase ? "failed" : "Failed"; default: - const _exhaustive: never = benchmark.result; + const _exhaustive: never = benchmarkResult; throw new Error(`Unsupported BenchmarkResult: ${_exhaustive}`); } }; + +/* + * Summarizes all acceptance test benchmarks of an `App` related to a `BestPractice` into a single `BenchmarkResult`. + * + * @returns an object containing the best practice and the summarized benchmark result for that best practice, + * if any belonging benchmarks exist. Otherwise, returns the best practice with an undefined benchmark result. + * + * @throws Error if `BestPracticeSlug` invariant is violated + */ +export const summarizeAppsAcceptanceTestBenchmarks = ( + bestPracticeSlug: BestPracticeSlug, + bestPracticeBenchmarks: AcceptanceTestBenchmarks, +): { bestPractice: BestPractice; generalizedBenchmarkResult?: BenchmarkResult } => { + const bestPractice = getBestPracticeBySlug(bestPracticeSlug); + + if (bestPractice === undefined) { + throw new Error( + `Invariant(BestPracticeSlug): Best practice with slug ${bestPracticeSlug} not found`, + ); + } + + const generalizedBenchmarkResult = generalizeAcceptanceTestBenchmarks(bestPracticeBenchmarks); + + return { + bestPractice, + generalizedBenchmarkResult, + }; +}; diff --git a/ensawards.org/data/contributors/index.ts b/ensawards.org/data/contributors/index.ts index 1826c8d6..7d4fbcb6 100644 --- a/ensawards.org/data/contributors/index.ts +++ b/ensawards.org/data/contributors/index.ts @@ -34,7 +34,11 @@ const contributors = { }, caldonia: { chainId: mainnet.id, - address: "0x9a41c5549bcc7d3f8d80e639714a4823de559134", + address: asNormalizedAddress("0x9a41c5549bcc7d3f8d80e639714a4823de559134"), + }, + shrugs: { + chainId: mainnet.id, + address: asNormalizedAddress("0xbd4cd9ae5bff533bf4ee926eeddc3dca5e2cdb4c"), }, } as const satisfies Record; diff --git a/ensawards.org/data/contributors/utils.ts b/ensawards.org/data/contributors/utils.ts index dca33a54..37f921dc 100644 --- a/ensawards.org/data/contributors/utils.ts +++ b/ensawards.org/data/contributors/utils.ts @@ -1,4 +1,4 @@ -import { getAppBenchmarks } from "data/benchmarks/utils.ts"; +import { getAcceptanceTestBenchmarksByApp } from "data/acceptance-tests/utils.ts"; import { stringifyAccountId } from "enssdk"; import { type App } from "../apps/types.ts"; @@ -24,7 +24,7 @@ export const countContributorAppearances = ( }; export const getAppContributions = (app: App): Contribution[] => - Object.values(getAppBenchmarks(app.appSlug)) + getAcceptanceTestBenchmarksByApp(app.appSlug) .filter((benchmark) => benchmark !== undefined) .flatMap((benchmark) => benchmark.contributions); diff --git a/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-l2-chains.ts b/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-l2-chains.ts deleted file mode 100644 index 2af3bc6e..00000000 --- a/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-l2-chains.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice -// on adding and modifying best practices - -import contributors from "data/contributors/index.ts"; - -import { parseTimestamp } from "@ensnode/ensnode-sdk"; - -import { AppTypes } from "../../apps/types.ts"; -import { defineBestPractice } from "../registry.ts"; -import { type BestPracticeApp, BestPracticeTypes } from "../types.ts"; -import ContractNamingCategory from "."; - -const technicalDetailsMainContent = `When users interact with a contract on an L2 chain, use the -[ENSIP-19](https://docs.ens.domains/ensip/19) standard to lookup the primary name of the contract. -ENSIP-19 provides chain-specific primary names for L2 networks (including Optimism, Arbitrum, Base, -Linea, and Scroll), with an automatic fallback to a default primary name (defined on mainnet) if no -chain-specific primary name is defined. There are several libraries to choose from that support -ENSIP-19 and all ENS best practices: -- [Viem](https://viem.sh/docs/ens/actions/getEnsName#chain-specific-resolution) (v2.35.0+) -- [Wagmi](https://wagmi.sh/react/api/hooks/useEnsName#chainid) (v2.18.0+) -Libraries and tools for additional languages or frameworks can be found in the [ENS documentation](https://docs.ens.domains/web/libraries).`; - -const technicalDetailsAdditionalContent = `ENSIP-19 enables primary names to be set on any chain. -Contracts deployed to an L2 chain benefit from this, as the contract can then configure its name -directly on the chain it is deployed to without any need to update state on mainnet. If a contract -has an ENS name, you can use the contract's ENS profile to power additional UX improvements such as -displaying the contract's avatar, metadata, audit information, and more. More information can be found -at [this ENSIP](https://discuss.ens.domains/t/ensip-proposal-contract-metadata-standard-and-text-records/21397).`; - -const displayNamedSmartContractsL2: BestPracticeApp = { - type: BestPracticeTypes.App, - id: "display-named-smart-contracts-l2-chains", - bestPracticeSlug: "display-named-smart-contracts-l2-chains", - name: "Display named smart contracts on L2 chains", - description: - "Display ENS names instead of addresses when users interact with named smart contracts on L2 chains.", - category: ContractNamingCategory, - appliesTo: [AppTypes.Wallet, AppTypes.Explorer], - technicalDetails: { - main: { - header: "ENS best practice details", - content: technicalDetailsMainContent, - }, - sides: [ - { - header: "Additional Details", - content: technicalDetailsAdditionalContent, - }, - ], - }, - contributions: [ - { from: contributors.lightwalker, lastUpdated: parseTimestamp("2025-11-03T11:25:00.000Z") }, - { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-12T14:51:00.000Z") }, - ], -}; - -defineBestPractice(displayNamedSmartContractsL2); - -export default displayNamedSmartContractsL2; diff --git a/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-l2-chains/images/mock-l2-chains-interactions-display-named-smart-contracts-example.png b/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-l2-chains/images/mock-l2-chains-interactions-display-named-smart-contracts-example.png new file mode 100644 index 00000000..0e3511b4 Binary files /dev/null and b/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-l2-chains/images/mock-l2-chains-interactions-display-named-smart-contracts-example.png differ diff --git a/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-l2-chains/index.ts b/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-l2-chains/index.ts new file mode 100644 index 00000000..fa2f8df4 --- /dev/null +++ b/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-l2-chains/index.ts @@ -0,0 +1,32 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice +// on adding and modifying best practices + +import { AppTypes } from "data/apps/types.ts"; +import contributors from "data/contributors/index.ts"; +import { defineBestPractice } from "data/ens-best-practices/registry.ts"; +import { type BestPracticeApp, BestPracticeTypes } from "data/ens-best-practices/types.ts"; + +import { parseTimestamp } from "@ensnode/ensnode-sdk"; + +import ContractNamingCategory from "../index.ts"; +import technicalDetails from "./technicalDetails.tsx"; + +const displayNamedSmartContractsL2: BestPracticeApp = { + type: BestPracticeTypes.App, + id: "display-named-smart-contracts-l2-chains", + bestPracticeSlug: "display-named-smart-contracts-l2-chains", + name: "Display named smart contracts on L2 chains", + description: + "Display ENS names instead of addresses when users interact with named smart contracts on L2 chains.", + category: ContractNamingCategory, + appliesTo: [AppTypes.Wallet, AppTypes.Explorer], + technicalDetails: technicalDetails, + contributions: [ + { from: contributors.lightwalker, lastUpdated: parseTimestamp("2025-11-03T11:25:00.000Z") }, + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-12T14:51:00.000Z") }, + ], +}; + +defineBestPractice(displayNamedSmartContractsL2); + +export default displayNamedSmartContractsL2; diff --git a/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-l2-chains/technicalDetails.tsx b/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-l2-chains/technicalDetails.tsx new file mode 100644 index 00000000..e8f4164e --- /dev/null +++ b/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-l2-chains/technicalDetails.tsx @@ -0,0 +1,159 @@ +import type { AcceptanceTest, AcceptanceTestBenchmarkPass } from "data/acceptance-tests/types"; +import { BenchmarkResults } from "data/benchmarks/types"; +import contributors from "data/contributors"; +import { + acceptanceTestDetailsContainerStyles, + bestPracticeTechnicalDetailsLinkStyles, +} from "data/ens-best-practices/styles"; +import type { BestPracticeTechnicalDetails } from "data/ens-best-practices/types"; + +import { parseTimestamp } from "@ensnode/ensnode-sdk"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +import l2ChainsInteractionsDisplayNamedSmartContractsImage from "./images/mock-l2-chains-interactions-display-named-smart-contracts-example.png"; + +// TODO: The content isn't fully curated for now. +const implementationRecommendations = ( +
+

+ When users interact with a contract on an L2 chain, use the{" "} + + ENSIP-19 + {" "} + standard to lookup the primary name of the contract. ENSIP-19 provides chain-specific primary + names for L2 networks (including Optimism, Arbitrum, Base, Linea, and Scroll), with an + automatic fallback to a default primary name (defined on mainnet) if no chain-specific primary + name is defined. There are several libraries to choose from that support ENSIP-19 and all ENS + best practices: +

+
+ +
+

+ Libraries and tools for additional languages or frameworks can be found in the{" "} + + ENS documentation + + . +

+
+); + +const benefitFromUsingEns = ( +

+ Users can easily identify and interact with named smart contracts on an L2 chain, improving + security and user experience. +

+); + +const useCaseSummary = ( +

+ ENSIP-19 enables primary names to be set on any chain. Contracts deployed to an L2 chain benefit + from this, as the contract can then configure its name directly on the chain it is deployed to + without any need to update state on mainnet. If a contract has an ENS name, you can use the + contract's ENS profile to power additional UX improvements such as displaying the contract's + avatar, metadata, audit information, and more. More information can be found at this{" "} + + ENSIP Proposal + + . +

+); + +const acceptanceTest1 = { + acceptanceTestSlug: "l2-chain-interactions-display-named-smart-contracts", + description: ( +
+

+ When users interact with named smart contracts on an L2 chain the app should display the + contract's name as its primary identifier. +

+
+ ), + examplePass: { + result: BenchmarkResults.Pass, + contributions: [ + { from: contributors.y3drk, lastUpdated: parseTimestamp("2026-05-11T10:17:00Z") }, + ], + notes: ( +
+

TODO: Find a suitable example for L2 chain interactions

+ l2-chain-interactions-display-named-smart-contracts acceptance test +
+ ), + } as const satisfies AcceptanceTestBenchmarkPass, +} as const satisfies AcceptanceTest; + +const technicalDetails = { + useCaseSummary, + benefitFromUsingEns, + implementationRecommendations, + acceptanceTests: [acceptanceTest1], +} as const satisfies BestPracticeTechnicalDetails; + +export default technicalDetails; diff --git a/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-mainnet.ts b/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-mainnet.ts deleted file mode 100644 index 323f052a..00000000 --- a/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-mainnet.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice -// on adding and modifying best practices - -import contributors from "data/contributors/index.ts"; - -import { parseTimestamp } from "@ensnode/ensnode-sdk"; - -import { AppTypes } from "../../apps/types.ts"; -import { defineBestPractice } from "../registry.ts"; -import { type BestPracticeApp, BestPracticeTypes } from "../types.ts"; -import ContractNamingCategory from "."; - -const technicalDetailsMainContent = `Looking up the name of a smart contract on Ethereum mainnet uses the same process as looking up the name of any other account. -There are a variety of libraries capable of looking up the [primary name](https://docs.ens.domains/web/reverse) of a contract address according to all ENS best practices: -- [Viem](https://viem.sh/docs/ens/actions/getEnsName#chain-specific-resolution) (v2.35.0+) -- [Wagmi](https://wagmi.sh/react/api/hooks/useEnsName#chainid) (v2.18.0+) -Libraries and tools for additional languages or frameworks can be found in the [ENS documentation](https://docs.ens.domains/web/libraries).`; - -const technicalDetailsAdditionalContent = `If a contract has an ENS name, you can use its ENS profile to power additional UX improvements -such as avatars, metadata, audit information, and more. More information can be found at the -[ENSIP Proposal](https://discuss.ens.domains/t/ensip-proposal-contract-metadata-standard-and-text-records/21397).`; - -const displayNamedSmartContractsMainnet: BestPracticeApp = { - type: BestPracticeTypes.App, - id: "display-named-smart-contracts-mainnet", - bestPracticeSlug: "display-named-smart-contracts-mainnet", - name: "Display named smart contracts on Ethereum mainnet", - description: - "Display ENS names instead of addresses when users interact with named smart contracts on Ethereum mainnet.", - category: ContractNamingCategory, - appliesTo: [AppTypes.Wallet, AppTypes.Explorer], - technicalDetails: { - main: { - header: "ENS best practice details", - content: technicalDetailsMainContent, - }, - sides: [ - { - header: "Additional Details", - content: technicalDetailsAdditionalContent, - }, - ], - }, - contributions: [ - { from: contributors.lightwalker, lastUpdated: parseTimestamp("2025-11-03T11:25:00.000Z") }, - { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-12T14:51:00.000Z") }, - ], -}; - -defineBestPractice(displayNamedSmartContractsMainnet); - -export default displayNamedSmartContractsMainnet; diff --git a/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-mainnet/images/mainnet-interactions-display-named-smart-contracts-example.png b/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-mainnet/images/mainnet-interactions-display-named-smart-contracts-example.png new file mode 100644 index 00000000..3dfe96b5 Binary files /dev/null and b/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-mainnet/images/mainnet-interactions-display-named-smart-contracts-example.png differ diff --git a/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-mainnet/index.ts b/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-mainnet/index.ts new file mode 100644 index 00000000..9b918bef --- /dev/null +++ b/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-mainnet/index.ts @@ -0,0 +1,32 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice +// on adding and modifying best practices + +import { AppTypes } from "data/apps/types.ts"; +import contributors from "data/contributors/index.ts"; +import { defineBestPractice } from "data/ens-best-practices/registry.ts"; +import { type BestPracticeApp, BestPracticeTypes } from "data/ens-best-practices/types.ts"; + +import { parseTimestamp } from "@ensnode/ensnode-sdk"; + +import ContractNamingCategory from "../index.ts"; +import technicalDetails from "./technicalDetails.tsx"; + +const displayNamedSmartContractsMainnet: BestPracticeApp = { + type: BestPracticeTypes.App, + id: "display-named-smart-contracts-mainnet", + bestPracticeSlug: "display-named-smart-contracts-mainnet", + name: "Display named smart contracts on Ethereum mainnet", + description: + "Display ENS names instead of addresses when users interact with named smart contracts on Ethereum mainnet.", + category: ContractNamingCategory, + appliesTo: [AppTypes.Wallet, AppTypes.Explorer], + technicalDetails: technicalDetails, + contributions: [ + { from: contributors.lightwalker, lastUpdated: parseTimestamp("2025-11-03T11:25:00.000Z") }, + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-12T14:51:00.000Z") }, + ], +}; + +defineBestPractice(displayNamedSmartContractsMainnet); + +export default displayNamedSmartContractsMainnet; diff --git a/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-mainnet/technicalDetails.tsx b/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-mainnet/technicalDetails.tsx new file mode 100644 index 00000000..e75aad28 --- /dev/null +++ b/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-mainnet/technicalDetails.tsx @@ -0,0 +1,160 @@ +import type { AcceptanceTest, AcceptanceTestBenchmarkPass } from "data/acceptance-tests/types"; +import { BenchmarkResults } from "data/benchmarks/types"; +import contributors from "data/contributors"; +import { + acceptanceTestDetailsContainerStyles, + bestPracticeTechnicalDetailsLinkStyles, +} from "data/ens-best-practices/styles"; +import type { BestPracticeTechnicalDetails } from "data/ens-best-practices/types"; + +import { parseTimestamp } from "@ensnode/ensnode-sdk"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +import mainnetInteractionsDisplayNamedSmartContractsImage from "./images/mainnet-interactions-display-named-smart-contracts-example.png"; + +// TODO: The content isn't fully curated for now. +const implementationRecommendations = ( +
+

+ Looking up the name of a smart contract on Ethereum mainnet uses the same process as looking + up the name of any other account. There are a variety of libraries capable of looking up the{" "} + + primary name + {" "} + of a contract address according to all ENS best practices: +

+
+ +
+

+ Libraries and tools for additional languages or frameworks can be found in the{" "} + + ENS documentation + + . +

+
+); + +const benefitFromUsingEns = ( +

+ Users can easily identify and interact with named smart contracts on Ethereum mainnet, improving + security and user experience. +

+); + +const useCaseSummary = ( +

+ If a contract has an ENS name, you can use its ENS profile to power additional UX improvements + such as avatars, metadata, audit information, and more. More information can be found at the{" "} + + ENSIP Proposal + + . +

+); + +const acceptanceTest1 = { + acceptanceTestSlug: "mainnet-interactions-display-named-smart-contracts", + description: ( +
+

+ When users interact with named smart contracts on Ethereum mainnet the app should display + the contract's name as its primary identifier. +

+
+ ), + examplePass: { + result: BenchmarkResults.Pass, + contributions: [ + { from: contributors.y3drk, lastUpdated: parseTimestamp("2026-05-11T10:17:00Z") }, + ], + notes: ( +
+

+ Below you can see how{" "} + + WalletChan + {" "} + displays the name of the contract it interacts with in addition to its address. +

+ WalletChan displaying a smart contract ENS name next to its address during interaction +

That allows the user to easily identify the contract.

+
+ ), + } as const satisfies AcceptanceTestBenchmarkPass, +} as const satisfies AcceptanceTest; + +const technicalDetails = { + useCaseSummary, + benefitFromUsingEns, + implementationRecommendations, + acceptanceTests: [acceptanceTest1], +} as const satisfies BestPracticeTechnicalDetails; + +export default technicalDetails; diff --git a/ensawards.org/data/ens-best-practices/contract-naming/index.ts b/ensawards.org/data/ens-best-practices/contract-naming/index.ts index 8e147079..f405f709 100644 --- a/ensawards.org/data/ens-best-practices/contract-naming/index.ts +++ b/ensawards.org/data/ens-best-practices/contract-naming/index.ts @@ -10,7 +10,10 @@ const ContractNamingCategory: BestPracticeCategory = { categorySlug: "contract-naming", name: "Contract naming", description: "Improve the UX and security of smart contract interactions.", - status: CategoryStatuses.Active, + status: CategoryStatuses.ComingSoon, + // TODO: This category is temporarily hidden due to unfit content. + // We aim to fix it as soon as we have the capacity. + // See: https://github.com/namehash/ensawards/issues/222 }; defineBestPracticeCategory(ContractNamingCategory); diff --git a/ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts.ts b/ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts.ts deleted file mode 100644 index eb555a93..00000000 --- a/ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice -// on adding and modifying best practices - -import contributors from "data/contributors/index.ts"; -import { ProtocolTypes } from "data/protocols/types.ts"; - -import { parseTimestamp } from "@ensnode/ensnode-sdk"; - -import { defineBestPractice } from "../registry.ts"; -import { type BestPracticeProtocol, BestPracticeTypes } from "../types.ts"; -import ContractNamingCategory from "."; - -const technicalDetailsMainContent = `Contracts should be [assigned ENS names](https://docs.ens.domains/web/naming-contracts) during or after deployment. -For deployment-time naming, see the [Enscribe documentation](https://www.enscribe.xyz/docs/introduction/naming-contracts) which goes into greater detail on the process. -For post-deployment naming we recommend using the [Enscribe App](https://app.enscribe.xyz). -Note that setting a [primary name](https://docs.ens.domains/web/reverse) (reverse resolution) requires the contract to implement -[Ownable](https://docs.openzeppelin.com/contracts/5.x/access-control#ownership-and-ownable). -Contracts without Ownable can still have forward resolution configured (\`name → address\`) but cannot set a reverse record (\`address → name\`).`; - -const technicalDetailsAdditionalContent = `Assigning ENS names to contracts gives users confidence they're interacting with the correct contract. -This is particularly valuable for security, as users can confirm \`uniswap.eth\` instead of validating a long hexadecimal address. -If you've deployed contracts without Ownable, you can still improve UX by configuring forward resolution, though full naming (with reverse resolution) provides the best user experience.`; - -const nameYourSmartContracts: BestPracticeProtocol = { - type: BestPracticeTypes.Protocol, - id: "name-your-smart-contracts", - bestPracticeSlug: "name-your-smart-contracts", - name: "Name your smart contracts", - description: "Upgrade security and UX when users interact with your smart contracts.", - category: ContractNamingCategory, - appliesTo: [ProtocolTypes.DAO, ProtocolTypes.DeFi], - technicalDetails: { - main: { - header: "ENS best practice details", - content: technicalDetailsMainContent, - }, - sides: [ - { - header: "Additional Details", - content: technicalDetailsAdditionalContent, - }, - ], - }, - contributions: [ - { from: contributors.lightwalker, lastUpdated: parseTimestamp("2025-11-03T11:25:00.000Z") }, - { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-12T14:51:00.000Z") }, - ], -}; - -defineBestPractice(nameYourSmartContracts); - -export default nameYourSmartContracts; diff --git a/ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts/images/enscribe-lookup-example.png b/ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts/images/enscribe-lookup-example.png new file mode 100644 index 00000000..c7612f3c Binary files /dev/null and b/ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts/images/enscribe-lookup-example.png differ diff --git a/ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts/index.ts b/ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts/index.ts new file mode 100644 index 00000000..aa46326f --- /dev/null +++ b/ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts/index.ts @@ -0,0 +1,31 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice +// on adding and modifying best practices + +import contributors from "data/contributors/index.ts"; +import { defineBestPractice } from "data/ens-best-practices/registry.ts"; +import { type BestPracticeProtocol, BestPracticeTypes } from "data/ens-best-practices/types.ts"; +import { ProtocolTypes } from "data/protocols/types.ts"; + +import { parseTimestamp } from "@ensnode/ensnode-sdk"; + +import ContractNamingCategory from "../index.ts"; +import technicalDetails from "./technicalDetails.tsx"; + +const nameYourSmartContracts: BestPracticeProtocol = { + type: BestPracticeTypes.Protocol, + id: "name-your-smart-contracts", + bestPracticeSlug: "name-your-smart-contracts", + name: "Name your smart contracts", + description: "Upgrade security and UX when users interact with your smart contracts.", + category: ContractNamingCategory, + appliesTo: [ProtocolTypes.DAO, ProtocolTypes.DeFi], + technicalDetails: technicalDetails, + contributions: [ + { from: contributors.lightwalker, lastUpdated: parseTimestamp("2025-11-03T11:25:00.000Z") }, + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-12T14:51:00.000Z") }, + ], +}; + +defineBestPractice(nameYourSmartContracts); + +export default nameYourSmartContracts; diff --git a/ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts/technicalDetails.tsx b/ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts/technicalDetails.tsx new file mode 100644 index 00000000..b00a0a5b --- /dev/null +++ b/ensawards.org/data/ens-best-practices/contract-naming/name-your-smart-contracts/technicalDetails.tsx @@ -0,0 +1,158 @@ +import { type AcceptanceTest, type AcceptanceTestBenchmarkPass } from "data/acceptance-tests/types"; +import { BenchmarkResults } from "data/benchmarks/types"; +import contributors from "data/contributors"; +import { + acceptanceTestDetailsContainerStyles, + bestPracticeTechnicalDetailsLinkStyles, +} from "data/ens-best-practices/styles"; +import type { BestPracticeTechnicalDetails } from "data/ens-best-practices/types"; + +import { parseTimestamp } from "@ensnode/ensnode-sdk"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +import enscribelookupAcceptanceTestImage from "./images/enscribe-lookup-example.png"; + +// TODO: The content isn't fully curated for now. + +const useCaseSummary = ( +
+ Contracts should be{" "} + + assigned ENS names + {" "} + during or after deployment. For deployment-time naming, see the{" "} + + Enscribe documentation + {" "} + which goes into greater detail on the process. For post-deployment naming we recommend using the{" "} + + Enscribe App + + . Note that setting a{" "} + + primary name + {" "} + (reverse resolution) requires the contract to implement{" "} + + Ownable + + . Contracts without Ownable can still have forward resolution configured ( + + name → address + + ) but cannot set a reverse record ( + + address → name + + ). +
+); + +const implementationRecommendations = ( +

+ Assigning ENS names to contracts gives users confidence they're interacting with the correct + contract. This is particularly valuable for security, as users can confirm{" "} + + uniswap.eth + {" "} + instead of validating a long hexadecimal address. If you've deployed contracts without Ownable, + you can still improve UX by configuring forward resolution, though full naming (with reverse + resolution) provides the best user experience. +

+); + +const benefitFromUsingEns = ( +

+ The users of your protocol can easily verify they're interacting with the correct contracts, + improving security and user confidence. +

+); + +const acceptanceTest1 = { + acceptanceTestSlug: "contract-has-primary-name", + description: ( +
+

+ When the contract's ENS name or address is looked up in the{" "} + + Enscribe App + {" "} + the result should display the contract's name with an appropriate icon. +

+
+ ), + examplePass: { + result: BenchmarkResults.Pass, + contributions: [ + { from: contributors.y3drk, lastUpdated: parseTimestamp("2026-05-11T10:17:00Z") }, + ], + notes: ( +
+

+ Below you can see the verification on the{" "} + + Enscribe App + {" "} + how{" "} + + Nouns DAO + {" "} + has named one of its smart contracts. +

+ enscribe-lookup acceptance test +

+ This is the first (but vital) step in ensuring users can easily identify the contract. +

+
+ ), + } as const satisfies AcceptanceTestBenchmarkPass, +} as const satisfies AcceptanceTest; + +const technicalDetails = { + useCaseSummary, + benefitFromUsingEns, + implementationRecommendations, + acceptanceTests: [acceptanceTest1], +} as const satisfies BestPracticeTechnicalDetails; + +export default technicalDetails; diff --git a/ensawards.org/data/ens-best-practices/ensv2-readiness/ensv2-ready-resolution/images/correctly-resolve-ensv2-test-name-address-fail-example.png b/ensawards.org/data/ens-best-practices/ensv2-readiness/ensv2-ready-resolution/images/correctly-resolve-ensv2-test-name-address-fail-example.png new file mode 100644 index 00000000..c5ad4fe2 Binary files /dev/null and b/ensawards.org/data/ens-best-practices/ensv2-readiness/ensv2-ready-resolution/images/correctly-resolve-ensv2-test-name-address-fail-example.png differ diff --git a/ensawards.org/data/ens-best-practices/ensv2-readiness/ensv2-ready-resolution/images/correctly-resolve-ensv2-test-name-address-pass-example.png b/ensawards.org/data/ens-best-practices/ensv2-readiness/ensv2-ready-resolution/images/correctly-resolve-ensv2-test-name-address-pass-example.png new file mode 100644 index 00000000..da93b13b Binary files /dev/null and b/ensawards.org/data/ens-best-practices/ensv2-readiness/ensv2-ready-resolution/images/correctly-resolve-ensv2-test-name-address-pass-example.png differ diff --git a/ensawards.org/data/ens-best-practices/ensv2-readiness/ensv2-ready-resolution/index.ts b/ensawards.org/data/ens-best-practices/ensv2-readiness/ensv2-ready-resolution/index.ts new file mode 100644 index 00000000..08b18795 --- /dev/null +++ b/ensawards.org/data/ens-best-practices/ensv2-readiness/ensv2-ready-resolution/index.ts @@ -0,0 +1,32 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice +// on adding and modifying best practices + +import { AppTypes } from "data/apps/types.ts"; +import contributors from "data/contributors/index.ts"; +import { defineBestPractice } from "data/ens-best-practices/registry.ts"; +import { type BestPracticeApp, BestPracticeTypes } from "data/ens-best-practices/types.ts"; + +import { parseTimestamp } from "@ensnode/ensnode-sdk"; + +import ENSv2ReadinessCategory from "../index.ts"; +import technicalDetails from "./technicalDetails.tsx"; + +const ensv2ReadyResolution: BestPracticeApp = { + type: BestPracticeTypes.App, + id: "ensv2-ready-resolution", + bestPracticeSlug: "ensv2-ready-resolution", + name: "Correctly Resolve All Names For ENSv2", + description: "Make sure your app keeps resolving ENS names correctly once ENSv2 launches.", + category: ENSv2ReadinessCategory, + appliesTo: [AppTypes.Wallet, AppTypes.Explorer], + technicalDetails: technicalDetails, + contributions: [ + { from: contributors.y3drk, lastUpdated: parseTimestamp("2026-06-05T13:01:00.000Z") }, + { from: contributors.shrugs, lastUpdated: parseTimestamp("2026-06-06T17:14:00.000Z") }, + { from: contributors.lightwalker, lastUpdated: parseTimestamp("2026-06-07T17:20:00.000Z") }, + ], +}; + +defineBestPractice(ensv2ReadyResolution); + +export default ensv2ReadyResolution; diff --git a/ensawards.org/data/ens-best-practices/ensv2-readiness/ensv2-ready-resolution/technicalDetails.tsx b/ensawards.org/data/ens-best-practices/ensv2-readiness/ensv2-ready-resolution/technicalDetails.tsx new file mode 100644 index 00000000..2141c9c4 --- /dev/null +++ b/ensawards.org/data/ens-best-practices/ensv2-readiness/ensv2-ready-resolution/technicalDetails.tsx @@ -0,0 +1,269 @@ +import type { + AcceptanceTest, + AcceptanceTestBenchmarkFail, + AcceptanceTestBenchmarkPass, +} from "data/acceptance-tests/types"; +import { BenchmarkResults } from "data/benchmarks/types"; +import contributors from "data/contributors"; +import { + acceptanceTestDetailsContainerStyles, + bestPracticeTechnicalDetailsLinkStyles, +} from "data/ens-best-practices/styles"; +import type { BestPracticeTechnicalDetails } from "data/ens-best-practices/types"; + +import { parseTimestamp } from "@ensnode/ensnode-sdk"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +import correctlyResolveEnsv2TestNameAddressExampleFailImage from "./images/correctly-resolve-ensv2-test-name-address-fail-example.png"; +import correctlyResolveEnsv2TestNameAddressExamplePassImage from "./images/correctly-resolve-ensv2-test-name-address-pass-example.png"; + +const useCaseSummary = ( +
+

+ ENS is upgrading to{" "} + + ENSv2 + {" "} + and the way ENS names get resolved is changing. Apps that don't upgrade their ENS resolution + logic before ENSv2 launches will quietly start giving people the wrong names and addresses as + soon as ENSv2 launches — a silent failure that would erode user trust and could even send + funds to the wrong place. +

+

+ This best practice checks that your app will keep resolving every ENS name correctly through + the ENSv2 transition, with no disruption for your users. +

+

+ The simplest way to get this right—and to continuously and automatically fetch data from ENS + correctly even as ENS keeps evolving— is to use the{" "} + + ENS Omnigraph API + {" "} + (powered by{" "} + + ENSNode + + ) which handles ENSv2-ready resolution for you. The implementation recommendations below cover + how your engineering team can adopt it, or other supported options. +

+
+); + +const benefitFromUsingEnsTitle = "Why this matters"; +const benefitFromUsingEns = ( +

+ Get this right now so that your users keep seeing the correct names and addresses before, + during, and after the ENSv2 launch—no broken names, no funds sent to the wrong place, and no + last-minute scramble to migrate. +

+); + +const implementationRecommendations = ( +
+

+ There are a few ways to make sure your app's ENS resolution is ENSv2-ready. Options below are + ordered from the easiest to the most hands-on. Use the first option that fits your stack. +

+
+

+ 1. Use the ENS Omnigraph API (recommended). +

+

+ Fetch your ENS data through the{" "} + + ENS Omnigraph API + {" "} + (powered by{" "} + + ENSNode + + ) and ENSv2-ready resolution just works — no RPCs or contracts to track, no upgrades to chase. + It's a single typed GraphQL API over both ENSv1 and ENSv2 for every chain and offchain names + too. It also handles the indexed ENS data your app reads (e.g. the names an address owns, + profiles, histories, etc.), with resolutions accelerated by{" "} + + Protocol Acceleration + + . Drop it in with{" "} + + enssdk + {" "} + (TypeScript) or{" "} + + enskit + {" "} + (React). +

+
+

+ 2. Using viem or wagmi? Update your version. +

+

+ If you resolve names using ENS helper functions from viem or wagmi and aren't ready to adopt + the Omnigraph yet, make sure you're on{" "} + + viem (v2.35.0+) + + . On wagmi, pin the viem version yourself — wagmi accepts any viem 2.x, so upgrading wagmi + alone won't get you there. +

+
+

+ 3. Writing raw RPC calls yourself? +

+

+ If your app makes its own low-level RPC calls instead of using the Omnigraph or a helper + library like viem, point your resolution RPC calls at ENS's new stable Universal Resolver + proxy ( + + 0xeeeeeeee14d718c2b47d9923deab1335e144eeee + + ) rather than hardcoding a specific Universal Resolver implementation. Because ENS controls + the proxy, it transparently upgrades to the ENSv2-compatible implementation when ENSv2 + launches, so apps pointed at it stay correct with no code changes. For additional guidance, + see the{" "} + + ENSv2 readiness guide + + . +

+
+); + +const acceptanceTest1 = { + acceptanceTestSlug: "correctly-resolve-ensv2-test-name-address", + description: ( +
+

+ To check whether an app has ENSv2 ready resolution, resolve the name{" "} + + ur.integration-tests.eth + {" "} + and verify it resolves to the correct address,{" "} + + 0x2222222222222222222222222222222222222222 + + . +

+
+ ), + examplePass: { + result: BenchmarkResults.Pass, + contributions: [ + { from: contributors.y3drk, lastUpdated: parseTimestamp("2026-06-05T15:34:00Z") }, + ], + notes: ( +
+

+ An ENSv2-ready app such as{" "} + + MetaMask + {" "} + resolves the name{" "} + + ur.integration-tests.eth + {" "} + to the correct address: + + 0x2222222222222222222222222222222222222222 + + . +

+ MetaMask correctly resolves the name for ENSv2 +
+ ), + } as const satisfies AcceptanceTestBenchmarkPass, + exampleFail: { + result: BenchmarkResults.Fail, + contributions: [ + { from: contributors.y3drk, lastUpdated: parseTimestamp("2026-06-05T15:34:00Z") }, + ], + notes: ( +
+

+ An app that is NOT ENSv2 ready resolves the name{" "} + + ur.integration-tests.eth + {" "} + to an invalid address such as:{" "} + + 0x1111111111111111111111111111111111111111 + + , or fails to resolve the name at all.
+
+ Apps failing this test require an urgent update! 🚨 +

+ Rainbow Wallet fails to resolve the name for ENSv2 +
+ ), + } as const satisfies AcceptanceTestBenchmarkFail, +} as const satisfies AcceptanceTest; + +const technicalDetails = { + useCaseSummary, + benefitFromUsingEnsTitle, + benefitFromUsingEns, + implementationRecommendations, + acceptanceTests: [acceptanceTest1], +} as const satisfies BestPracticeTechnicalDetails; + +export default technicalDetails; diff --git a/ensawards.org/data/ens-best-practices/ensv2-readiness/index.ts b/ensawards.org/data/ens-best-practices/ensv2-readiness/index.ts new file mode 100644 index 00000000..b5029557 --- /dev/null +++ b/ensawards.org/data/ens-best-practices/ensv2-readiness/index.ts @@ -0,0 +1,20 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md for additional advice +// on adding and modifying best practice categories + +import { defineBestPracticeCategory } from "../registry.ts"; +import type { BestPracticeCategory } from "../types.ts"; +import { CategoryStatuses } from "../types.ts"; + +const ENSv2ReadinessCategory: BestPracticeCategory = { + id: "ensv2-readiness", + categorySlug: "ensv2-readiness", + name: "ENSv2 Readiness", + description: + "Be ready for ENSv2 — keep resolving and reading ENS data correctly through the upgrade and beyond.", + status: CategoryStatuses.Active, + order: 0, +}; + +defineBestPracticeCategory(ENSv2ReadinessCategory); + +export default ENSv2ReadinessCategory; diff --git a/ensawards.org/data/ens-best-practices/index.ts b/ensawards.org/data/ens-best-practices/index.ts index 8df45258..880cbe2d 100644 --- a/ensawards.org/data/ens-best-practices/index.ts +++ b/ensawards.org/data/ens-best-practices/index.ts @@ -4,7 +4,8 @@ import { getDefinedBestPracticeCategories, getDefinedBestPractices } from "./registry.ts"; import { type BestPractice, type BestPracticeCategory, CategoryStatuses } from "./types.ts"; -import.meta.glob("./*/*.ts", { eager: true }); +import.meta.glob("./*/index.ts", { eager: true }); +import.meta.glob("./*/*/index.ts", { eager: true }); export const ENS_BEST_PRACTICES: BestPractice[] = [ ...getDefinedBestPractices().sort((a, b) => a.name.localeCompare(b.name)), @@ -12,10 +13,14 @@ export const ENS_BEST_PRACTICES: BestPractice[] = [ export const BEST_PRACTICE_CATEGORIES: BestPracticeCategory[] = [ ...getDefinedBestPracticeCategories().sort((a, b) => { - if (a.status === b.status) return a.name.localeCompare(b.name); + if (a.status !== b.status) { + return a.status === CategoryStatuses.Active ? -1 : 1; + } - if (a.status === CategoryStatuses.Active) return -1; + const aOrder = a.order ?? Number.POSITIVE_INFINITY; + const bOrder = b.order ?? Number.POSITIVE_INFINITY; + if (aOrder !== bOrder) return aOrder - bOrder; - return 1; + return a.name.localeCompare(b.name); }), ]; diff --git a/ensawards.org/data/ens-best-practices/styles.ts b/ensawards.org/data/ens-best-practices/styles.ts new file mode 100644 index 00000000..701ff3fd --- /dev/null +++ b/ensawards.org/data/ens-best-practices/styles.ts @@ -0,0 +1,15 @@ +// NOTE: These styles are part of the `data/ens-best-practices` directory +// solely because this data directory includes content defined as `JSX` elements. +// +// The only purpose of these Tailwind classes is to help style those elements. +// Neither should be used anywhere else across the codebase, besides: +// 1. The definitions of best practice details (`data/ens-best-practices/*/*/technicalDetails.tsx`) +// 2. Acceptance tests (`data/ens-best-practices/*/*/technicalDetails.tsx`) and their benchmarks (`data/apps/*/benchmarks/index.tsx`) + +// Separated from the other utils to avoid circular dependency issues. + +export const bestPracticeTechnicalDetailsLinkStyles = + "text-black underline underline-offset-[25%] decoration-black/40 hover:decoration-black transition-all duration-200"; + +export const acceptanceTestDetailsContainerStyles = + "flex flex-col justify-start items-center gap-3"; diff --git a/ensawards.org/data/ens-best-practices/types.ts b/ensawards.org/data/ens-best-practices/types.ts index 558dca76..76db5ac9 100644 --- a/ensawards.org/data/ens-best-practices/types.ts +++ b/ensawards.org/data/ens-best-practices/types.ts @@ -1,5 +1,7 @@ -import type { AppBenchmark } from "data/benchmarks/types.ts"; +import type { AcceptanceTest } from "data/acceptance-tests/types.ts"; +import type { AcceptanceTestBenchmarks } from "data/benchmarks/types.ts"; import type { Contribution } from "data/contributors/types.ts"; +import type { JSX } from "react"; import type { AppType } from "../apps/types.ts"; import type { ProtocolType } from "../protocols/types.ts"; @@ -20,6 +22,15 @@ export type BestPracticeTarget = AppType | ProtocolType; */ export type BestPracticeSlug = string; +export interface BestPracticeTechnicalDetails { + useCaseSummary: JSX.Element; + benefitFromUsingEns: JSX.Element; + /** Title for the benefit card. Defaults to "Benefit from using ENS" when omitted. */ + benefitFromUsingEnsTitle?: string; + implementationRecommendations: JSX.Element; + acceptanceTests: [AcceptanceTest, ...AcceptanceTest[]]; +} + export interface BestPracticeAbstract< BestPracticeT extends BestPracticeType, AppliesToT extends BestPracticeTarget, @@ -31,16 +42,7 @@ export interface BestPracticeAbstract< description: string; category: BestPracticeCategory; // each best practice belongs to exactly one best practice category appliesTo: AppliesToT[]; - technicalDetails: { - main: { - header: string; - content: string; - }; - sides: { - header: string; - content: string; - }[]; - }; + technicalDetails: BestPracticeTechnicalDetails; contributions: [Contribution, ...Contribution[]]; } @@ -53,14 +55,14 @@ export interface BestPracticeApp export type BestPractice = BestPracticeProtocol | BestPracticeApp; /** - * Defines relations between {@link BestPracticeSlug} and {@link AppBenchmark} - * for the related {@link BestPractice}. + * Defines relations between {@link BestPracticeSlug} and the {@link AcceptanceTestBenchmarks} + * of the related {@link BestPractice} for a given app. * - * @invariant An explicit key for each `BestPracticeSlug` should be added to this `Record` for each available {@link BestPractice}. - * If an app doesn't have a benchmark completed for a `BestPractice` then the benchmark should be explicitly set to `undefined`. - * Otherwise, the value should be an `AppBenchmark` describing how the related app performed for the `BestPractice`. + * @invariant An explicit key for each `BestPracticeSlug` should be added to this `Record` + * for each applicable {@link BestPractice}. + * The value should be the related {@link AcceptanceTestBenchmarks}. */ -export type BestPracticeBenchmarks = Record; +export type BestPracticeBenchmarks = Record; export const CategoryStatuses = { ComingSoon: "coming-soon", @@ -82,4 +84,6 @@ export interface BestPracticeCategory { name: string; description: string; status: CategoryStatus; + /** Sort priority within a status group; lower comes first, undefined sorts last (then by name). */ + order?: number; } diff --git a/ensawards.org/data/ens-best-practices/utils.test.ts b/ensawards.org/data/ens-best-practices/utils.test.ts index bde64d25..979be922 100644 --- a/ensawards.org/data/ens-best-practices/utils.test.ts +++ b/ensawards.org/data/ens-best-practices/utils.test.ts @@ -1,8 +1,8 @@ import { type AppBenchmarks } from "data/apps/types"; -import { type AppBenchmark, BenchmarkResults } from "data/benchmarks/types"; -import { calcAppsPassed, calcBestPracticeScore } from "data/ens-best-practices/utils"; +import { type AcceptanceTestBenchmarks, BenchmarkResults } from "data/benchmarks/types"; +import { calcBestPracticeScore } from "data/ens-best-practices/utils"; import { - createMockBenchmark, + createMockAcceptanceTestBenchmark, mockCoinbaseWalletApp, mockMetamaskApp, mockRainbowApp, @@ -22,8 +22,8 @@ vi.mock(import("data/benchmarks/utils.ts"), async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - getAppBenchmarksByBestPractice: (bestPracticeSlug: string) => { - const benchmarks: (AppBenchmark | undefined)[] = []; + getAcceptanceTestBenchmarksByBestPractice: (bestPracticeSlug: string) => { + const benchmarks: AcceptanceTestBenchmarks[] = []; for (const appBenchmarks of Object.values(mockAppBenchmarks)) { benchmarks.push(appBenchmarks[bestPracticeSlug]); @@ -40,50 +40,24 @@ describe("BestPractice and BestPracticeCategory Utils", () => { mockAppBenchmarks[mockRainbowApp.appSlug] = {}; mockAppBenchmarks[mockMetamaskApp.appSlug] = {}; }); - describe("calcAppsPassed", () => { - it("Should return the number of apps that passed or partially passed the best practice benchmark", () => { - mockAppBenchmarks[mockCoinbaseWalletApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Pass, - ), - }; - mockAppBenchmarks[mockMetamaskApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.PartialPass, - ), - }; - - expect(calcAppsPassed(mockReverseResolutionBestPractice)).toEqual(2); - }); - - it("Should exclude all pending benchmarks from the calculation", () => { - mockAppBenchmarks[mockCoinbaseWalletApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Pass, - ), - }; - mockAppBenchmarks[mockMetamaskApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: undefined, - }; - - expect( - calcAppsPassed(mockReverseResolutionBestPractice), - "calcAppsPassed doesn't exclude pending benchmarks", - ).toEqual(1); - }); - }); describe("calcBestPracticeScore", () => { it("Should return the EnsAwardsScore for benchmarked apps", () => { mockAppBenchmarks[mockCoinbaseWalletApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Pass, - ), + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + }, + }; + + mockAppBenchmarks[mockRainbowApp.appSlug] = { + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: createMockAcceptanceTestBenchmark(BenchmarkResults.PartialPass), + }, }; mockAppBenchmarks[mockMetamaskApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Fail, - ), + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + }, }; const result = calcBestPracticeScore(mockReverseResolutionBestPractice); @@ -93,10 +67,21 @@ describe("BestPractice and BestPracticeCategory Utils", () => { it("Should return undefined when no apps are benchmarked for the best practice", () => { mockAppBenchmarks[mockCoinbaseWalletApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: undefined, + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: undefined, + }, }; + + mockAppBenchmarks[mockRainbowApp.appSlug] = { + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: undefined, + }, + }; + mockAppBenchmarks[mockMetamaskApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: undefined, + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: undefined, + }, }; const result = calcBestPracticeScore(mockReverseResolutionBestPractice); @@ -109,12 +94,20 @@ describe("BestPractice and BestPracticeCategory Utils", () => { it("Should exclude pending benchmarks from the calculation", () => { mockAppBenchmarks[mockCoinbaseWalletApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: createMockBenchmark( - BenchmarkResults.Pass, - ), + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + }, }; + mockAppBenchmarks[mockRainbowApp.appSlug] = { + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: undefined, + }, + }; + mockAppBenchmarks[mockMetamaskApp.appSlug] = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: undefined, + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: undefined, + }, }; const result = calcBestPracticeScore(mockReverseResolutionBestPractice); diff --git a/ensawards.org/data/ens-best-practices/utils.ts b/ensawards.org/data/ens-best-practices/utils.ts index f1d22e82..c41712bd 100644 --- a/ensawards.org/data/ens-best-practices/utils.ts +++ b/ensawards.org/data/ens-best-practices/utils.ts @@ -1,6 +1,8 @@ import { AppTypes } from "data/apps/types.ts"; -import { BenchmarkResults } from "data/benchmarks/types.ts"; -import { calcEnsAwardsPoints, getAppBenchmarksByBestPractice } from "data/benchmarks/utils.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 type { FormatTypeOptions } from "data/shared/format-type-options.ts"; @@ -15,6 +17,7 @@ import { type BestPracticeTarget, type BestPracticeType, BestPracticeTypes, + CategoryStatuses, } from "./types.ts"; /** @@ -95,60 +98,44 @@ export const formatBestPracticeType = ( /** * Calculates an {@link EnsAwardsScore} for a {@link BestPractice}, - * by calculating the average score of all apps that were benchmarked on this best practice. + * 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, - * otherwise returns the {@link EnsAwardsScore}. + * or the best practice belongs to a category with status other than `Active`. + * Otherwise returns the {@link EnsAwardsScore}. */ export const calcBestPracticeScore = ( bestPractice: BestPracticeApp, ): EnsAwardsScore | undefined => { - let benchmarkedApps = 0; + if (bestPractice.category.status !== CategoryStatuses.Active) { + return undefined; + } + + let benchmarkedAcceptanceTests = 0; let bestPracticePoints = 0; - const bestPracticeBenchmarks = getAppBenchmarksByBestPractice(bestPractice.bestPracticeSlug); + const bestPracticeBenchmarks = getAcceptanceTestBenchmarksByBestPractice( + bestPractice.bestPracticeSlug, + ).flatMap((appBenchmark) => Object.values(appBenchmark)); - for (const benchmark of bestPracticeBenchmarks) { - if (benchmark === undefined) { + for (const acceptanceTestBenchmark of bestPracticeBenchmarks) { + if (acceptanceTestBenchmark === undefined) { continue; } - benchmarkedApps += 1; + benchmarkedAcceptanceTests += 1; - bestPracticePoints += calcEnsAwardsPoints(benchmark); + bestPracticePoints += calcEnsAwardsPoints(acceptanceTestBenchmark); } - if (benchmarkedApps === 0) return undefined; + if (benchmarkedAcceptanceTests === 0) return undefined; - const score = Math.round((bestPracticePoints * 100) / benchmarkedApps); + const score = Math.round((bestPracticePoints * 100) / benchmarkedAcceptanceTests); return asEnsAwardsScore(score); }; -/** - * Calculates how many apps passed our benchmark on this {@link BestPractice}. - * - * For now, both {@link BenchmarkResults.Pass} and {@link BenchmarkResults.PartialPass} are treated as a pass. - */ -export const calcAppsPassed = (bestPractice: BestPracticeApp): number => { - let appsPassed = 0; - - const bestPracticeBenchmarks = getAppBenchmarksByBestPractice(bestPractice.bestPracticeSlug); - - bestPracticeBenchmarks.forEach((benchmark) => { - // Explicit acceptance of Pass & Partial Pass results - if ( - benchmark !== undefined && - (benchmark.result === BenchmarkResults.Pass || - benchmark.result === BenchmarkResults.PartialPass) - ) { - appsPassed += 1; - } - }); - - return appsPassed; -}; - export const formatBestPracticeTarget = ( target: BestPracticeTarget, options: FormatTypeOptions = { lowercase: false, plural: false }, diff --git a/ensawards.org/data/shared/test-utils.ts b/ensawards.org/data/shared/test-utils.tsx similarity index 78% rename from ensawards.org/data/shared/test-utils.ts rename to ensawards.org/data/shared/test-utils.tsx index d3dc80b3..78145810 100644 --- a/ensawards.org/data/shared/test-utils.ts +++ b/ensawards.org/data/shared/test-utils.tsx @@ -1,5 +1,6 @@ -import type { AppBenchmark } from "data/benchmarks/types.ts"; -import { type BenchmarkResult } from "data/benchmarks/types.ts"; +import type { AcceptanceTestBenchmark, AcceptanceTestSlug } from "data/acceptance-tests/types.ts"; +import type { AcceptanceTestBenchmarks } from "data/benchmarks/types.ts"; +import { type BenchmarkResult, BenchmarkResults } from "data/benchmarks/types.ts"; import { zeroAddress } from "viem"; import { mainnet } from "viem/chains"; @@ -48,11 +49,25 @@ export const createMockBestPractice = (overrides: { }, appliesTo: [AppTypes.Wallet], technicalDetails: { - main: { - header: "Main details", - content: "Main content", - }, - sides: [], + useCaseSummary:

Mock use case summary

, + implementationRecommendations:

Mock implementation recommendations

, + benefitFromUsingEns:

Mock benefit from using ENS

, + acceptanceTests: [ + { + acceptanceTestSlug: "mock-acceptance-test-slug-1", + description:

Mock acceptance test description 1

, + examplePass: { + result: BenchmarkResults.Pass, + contributions: [ + { + from: { address: zeroAddress, chainId: mainnet.id }, + lastUpdated: parseTimestamp("1970-01-01T00:00:00.000Z"), + }, + ], + notes:

Mock notes for acceptance test 1

, + }, + }, + ], }, contributions: [ { @@ -62,7 +77,9 @@ export const createMockBestPractice = (overrides: { ], }); -export const createMockBenchmark = (result: BenchmarkResult): AppBenchmark => ({ +export const createMockAcceptanceTestBenchmark = ( + result: BenchmarkResult, +): AcceptanceTestBenchmark => ({ result, contributions: [ { @@ -70,6 +87,7 @@ export const createMockBenchmark = (result: BenchmarkResult): AppBenchmark => ({ lastUpdated: parseTimestamp("1970-01-01T00:00:00.000Z"), }, ], + notes:

Mock notes

, }); export const createMockApp = (overrides: { diff --git a/ensawards.org/public/contract-naming-season_og_image.png b/ensawards.org/public/contract-naming-season_og_image.png index 33a5c10e..a8e35420 100644 Binary files a/ensawards.org/public/contract-naming-season_og_image.png and b/ensawards.org/public/contract-naming-season_og_image.png differ diff --git a/ensawards.org/public/contract-naming-season_twitter_og_image.png b/ensawards.org/public/contract-naming-season_twitter_og_image.png index 8d12d336..2b420f5b 100644 Binary files a/ensawards.org/public/contract-naming-season_twitter_og_image.png and b/ensawards.org/public/contract-naming-season_twitter_og_image.png differ diff --git a/ensawards.org/src/components/atoms/badges/BenchmarkResultBadge.tsx b/ensawards.org/src/components/atoms/badges/BenchmarkResultBadge.tsx index af05c717..671979bb 100644 --- a/ensawards.org/src/components/atoms/badges/BenchmarkResultBadge.tsx +++ b/ensawards.org/src/components/atoms/badges/BenchmarkResultBadge.tsx @@ -1,4 +1,4 @@ -import { type AppBenchmark, BenchmarkResults } from "data/benchmarks/types.ts"; +import { type BenchmarkResult, BenchmarkResults } from "data/benchmarks/types.ts"; import { formatBenchmarkResult } from "data/benchmarks/utils.ts"; import { X as FailIcon, @@ -10,15 +10,15 @@ import { import { cn } from "@/utils/tailwindClassConcatenation"; export interface BenchmarkResultBadgeProps { - benchmark?: AppBenchmark; + benchmarkResult?: BenchmarkResult; className?: string; } -export const benchmarkResultToBadgeStyles = (benchmark?: AppBenchmark) => { - if (!benchmark) { +export const benchmarkResultToBadgeStyles = (benchmarkResult?: BenchmarkResult) => { + if (!benchmarkResult) { return "text-muted-foreground bg-black/8"; } - switch (benchmark.result) { + switch (benchmarkResult) { case BenchmarkResults.PartialPass: return "text-orange-600 bg-orange-100"; case BenchmarkResults.Pass: @@ -27,16 +27,16 @@ export const benchmarkResultToBadgeStyles = (benchmark?: AppBenchmark) => { return "text-red-600 bg-[rgba(220,38,38,0.1)]"; default: - const _exhaustive: never = benchmark.result; + const _exhaustive: never = benchmarkResult; throw new Error(`Unsupported BenchmarkResult: ${_exhaustive}`); } }; -export const getBenchmarkIcon = (benchmark?: AppBenchmark, className?: string) => { - if (!benchmark) { +export const getBenchmarkIcon = (benchmarkResult?: BenchmarkResult, className?: string) => { + if (!benchmarkResult) { return ; } - switch (benchmark.result) { + switch (benchmarkResult) { case BenchmarkResults.Pass: return ; case BenchmarkResults.PartialPass: @@ -45,24 +45,24 @@ export const getBenchmarkIcon = (benchmark?: AppBenchmark, className?: string) = return ; default: - const _exhaustive: never = benchmark.result; + const _exhaustive: never = benchmarkResult; throw new Error(`Unsupported BenchmarkResult: ${_exhaustive}`); } }; -export function BenchmarkResultBadge({ benchmark, className }: BenchmarkResultBadgeProps) { - const BenchmarkIcon = getBenchmarkIcon(benchmark, "w-4 h-4 shrink-0"); +export function BenchmarkResultBadge({ benchmarkResult, className }: BenchmarkResultBadgeProps) { + const BenchmarkIcon = getBenchmarkIcon(benchmarkResult, "w-4 h-4 shrink-0"); return ( {BenchmarkIcon} - {formatBenchmarkResult(benchmark, { lowercase: false })} + {formatBenchmarkResult(benchmarkResult, { lowercase: false })} ); } diff --git a/ensawards.org/src/components/atoms/badges/BenchmarkResultHeroBadge.astro b/ensawards.org/src/components/atoms/badges/BenchmarkResultHeroBadge.astro index 49613383..46a5fed0 100644 --- a/ensawards.org/src/components/atoms/badges/BenchmarkResultHeroBadge.astro +++ b/ensawards.org/src/components/atoms/badges/BenchmarkResultHeroBadge.astro @@ -6,23 +6,23 @@ 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 { BenchmarkResults, type AppBenchmark } from "data/benchmarks/types.ts"; +import { BenchmarkResults, type BenchmarkResult } from "data/benchmarks/types.ts"; import { cn } from "@/utils/tailwindClassConcatenation"; import { type BenchmarkResultBadgeProps } from "./BenchmarkResultBadge.tsx"; import { formatBenchmarkResult } from "data/benchmarks/utils.ts"; -const { benchmark, className }: BenchmarkResultBadgeProps = Astro.props; +const { benchmarkResult, className }: BenchmarkResultBadgeProps = Astro.props; const benchmarkResultContent = ( - benchmark?: AppBenchmark, + benchmarkResult?: BenchmarkResult, ): { symbol: ImageMetadata; symbolAlt: string } => { - if (benchmark === undefined) { + if (benchmarkResult === undefined) { return { symbol: benchmarkPendingSymbol, symbolAlt: "Pending review benchmark indicator", }; } - switch (benchmark.result) { + switch (benchmarkResult) { case BenchmarkResults.PartialPass: return { symbol: benchmarkResultPartialPassSymbol, @@ -40,12 +40,12 @@ const benchmarkResultContent = ( }; default: - const _exhaustive: never = benchmark.result; + const _exhaustive: never = benchmarkResult; throw new Error(`Unsupported BenchmarkResult: ${_exhaustive}`); } }; -const { symbol, symbolAlt } = benchmarkResultContent(benchmark); +const { symbol, symbolAlt } = benchmarkResultContent(benchmarkResult); ---

- {formatBenchmarkResult(benchmark, { lowercase: false })} + {formatBenchmarkResult(benchmarkResult, { lowercase: false })}

diff --git a/ensawards.org/src/components/atoms/banners/ContractNamingBanner.astro b/ensawards.org/src/components/atoms/banners/ContractNamingBanner.astro index 52bbb64a..2b3198d1 100644 --- a/ensawards.org/src/components/atoms/banners/ContractNamingBanner.astro +++ b/ensawards.org/src/components/atoms/banners/ContractNamingBanner.astro @@ -7,7 +7,7 @@ import DollarEnsTokenIcon from "@/components/atoms/icons/DollarEnsTokenIcon.astr
-

- Up to 10,000 $ENS in awards + class="text-xs sm:text-sm text-white/50 font-normal leading-6 sm:leading-7"> + Elevate your protocol’s onchain identity by naming your smart contracts.

@@ -39,6 +36,6 @@ import DollarEnsTokenIcon from "@/components/atoms/icons/DollarEnsTokenIcon.astr )}>Learn more
diff --git a/ensawards.org/src/components/atoms/banners/PendingAcceptanceTestResultCTA.tsx b/ensawards.org/src/components/atoms/banners/PendingAcceptanceTestResultCTA.tsx new file mode 100644 index 00000000..d512f12e --- /dev/null +++ b/ensawards.org/src/components/atoms/banners/PendingAcceptanceTestResultCTA.tsx @@ -0,0 +1,38 @@ +import { type App } from "data/apps/types.ts"; + +import { GitHubOutlineIcon } from "@/components/atoms/icons/GitHubOutlineIcon"; +import { shadcnButtonVariants } from "@/components/ui/shadcnButtonStyles"; +import { cn } from "@/utils/tailwindClassConcatenation"; + +export interface PendingAcceptanceTestResultCTAProps { + app: App; +} + +export const PendingAcceptanceTestResultCTA = ({ app }: PendingAcceptanceTestResultCTAProps) => ( +
+

+ Waiting for a community submission of the result of this acceptance test on {app.name}. +

+
+
+

+ Contributions are open +

+ + + Submit result on GitHub + +
+
+); diff --git a/ensawards.org/src/components/atoms/cards/AppSummaryCard.tsx b/ensawards.org/src/components/atoms/cards/AppSummaryCard.tsx index 28eccd6d..c828fb19 100644 --- a/ensawards.org/src/components/atoms/cards/AppSummaryCard.tsx +++ b/ensawards.org/src/components/atoms/cards/AppSummaryCard.tsx @@ -1,23 +1,21 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import type { App } from "data/apps/types.ts"; import { calcAppScore, getAppById } from "data/apps/utils.ts"; -import type { AppBenchmark } from "data/benchmarks/types.ts"; import { calcBestPracticeCategoryScore, formatBenchmarkResult, getAppBenchmarks, groupBenchmarksByCategory, - sortBenchmarks, + sortBenchmarkResults, + summarizeAppsAcceptanceTestBenchmarks, } from "data/benchmarks/utils.ts"; -import type { - BestPracticeBenchmarks, - BestPracticeCategorySlug, - BestPracticeSlug, -} from "data/ens-best-practices/types.ts"; +import { BEST_PRACTICE_CATEGORIES } from "data/ens-best-practices"; import { - getBestPracticeBySlug, - getBestPracticeCategoryBySlug, -} from "data/ens-best-practices/utils.ts"; + type BestPracticeBenchmarks, + type BestPracticeCategorySlug, + CategoryStatuses, +} from "data/ens-best-practices/types.ts"; +import { getBestPracticeCategoryBySlug } from "data/ens-best-practices/utils.ts"; import { ChevronRight } from "lucide-react"; import { useState } from "react"; @@ -51,6 +49,14 @@ function BenchmarkCategorySection({ throw new Error(`Invariant(CategorySlug): Category with slug ${categorySlug} not found`); } + const appBenchmarksData = [...Object.entries(benchmarksInCategory)] + .map(([bestPracticeSlug, bestPracticeBenchmarks]) => + summarizeAppsAcceptanceTestBenchmarks(bestPracticeSlug, bestPracticeBenchmarks), + ) + .sort((a, b) => + sortBenchmarkResults(a.generalizedBenchmarkResult, b.generalizedBenchmarkResult), + ); + return (
)}
@@ -129,6 +131,36 @@ export function AppSummaryCard({ app }: AppSummaryCardProps) { const appScore = calcAppScore(resolvedApp); const benchmarksByCategory = groupBenchmarksByCategory(getAppBenchmarks(resolvedApp.appSlug)); + + // Remove inactive categories from the summary + const benchmarksInActiveCategories = [...benchmarksByCategory].filter( + ([categorySlug, _bestPracticeBenchmarks]) => { + const category = getBestPracticeCategoryBySlug(categorySlug); + + if (category === undefined) { + throw new Error( + `Invariant(BestPracticeCategorySlug): Category with slug ${categorySlug} not defined`, + ); + } + + return category.status === CategoryStatuses.Active; + }, + ); + + // Precompute the best practice category --> index mapping to + // optimize time complexity of sorting categories + const categorySlugToIndex: Record = {}; + BEST_PRACTICE_CATEGORIES.forEach((category, index) => { + categorySlugToIndex[category.categorySlug] = index; + }); + + // Sort categories based on the BestPracticeCategory.order field + // (used to initially order BEST_PRACTICE_CATEGORIES array) + const benchmarksByCategorySorted = [...benchmarksInActiveCategories].sort( + ([aCategorySlug, _a], [bCategorySlug, _b]) => + categorySlugToIndex[aCategorySlug] - categorySlugToIndex[bCategorySlug], + ); + const AppIcon = resolvedApp.icon; return ( @@ -143,7 +175,7 @@ export function AppSummaryCard({ app }: AppSummaryCardProps) { - {[...benchmarksByCategory.entries()].map(([categorySlug, benchmarksInCategory], index) => { + {benchmarksByCategorySorted.map(([categorySlug, benchmarksInCategory], index) => { return (

- Awards + Special ENSAwards

- List of all {entity.name}'s awards + Recognitions granted to {entity.name}

diff --git a/ensawards.org/src/components/atoms/cards/BenchmarkSummaryCard.astro b/ensawards.org/src/components/atoms/cards/BenchmarkSummaryCard.astro index b61c79a8..9f154a4a 100644 --- a/ensawards.org/src/components/atoms/cards/BenchmarkSummaryCard.astro +++ b/ensawards.org/src/components/atoms/cards/BenchmarkSummaryCard.astro @@ -1,18 +1,23 @@ --- import { RelativeTime } from "@namehash/namehash-ui"; -import { type AppBenchmark } from "data/benchmarks/types.ts"; +import { type AcceptanceTestBenchmarks } from "data/benchmarks/types.ts"; import { type BestPracticeApp } from "data/ens-best-practices/types.ts"; import { cn } from "@/utils/tailwindClassConcatenation"; -import { getBenchmarkLastUpdateTimestamp } from "data/benchmarks/utils.ts"; -import { BenchmarkResultBadge, benchmarkResultToBadgeStyles } from "../badges/BenchmarkResultBadge"; +import { getAcceptanceTestBenchmarkLastUpdateTimestamp } from "data/benchmarks/utils.ts"; +import { BenchmarkResultBadge } from "../badges/BenchmarkResultBadge"; +import { generalizeAcceptanceTestBenchmarks } from "data/acceptance-tests/utils.ts"; export interface BenchmarkSummaryCardProps { bestPractice: BestPracticeApp; - benchmark?: AppBenchmark; + acceptanceTestsBenchmarks: AcceptanceTestBenchmarks; } -const { bestPractice, benchmark }: BenchmarkSummaryCardProps = Astro.props; +const { bestPractice, acceptanceTestsBenchmarks }: BenchmarkSummaryCardProps = Astro.props; + +const benchmarksUpdateTimes = Object.values(acceptanceTestsBenchmarks) + .filter((benchmark) => benchmark !== undefined) + .map((benchmark) => getAcceptanceTestBenchmarkLastUpdateTimestamp(benchmark)); const bestPracticeHref = `/ens-best-practices/${bestPractice.category.categorySlug}/${bestPractice.bestPracticeSlug}`; @@ -22,6 +27,9 @@ const detailValueStyles = cn( "underline decoration-black/40 decoration-from-font underline-offset-[25%]", "transition-all duration-200 hover:decoration-black", ); + +const generalizedAcceptanceTestsBenchmarkResult = + generalizeAcceptanceTestBenchmarks(acceptanceTestsBenchmarks); ---
-

Benchmark result

- +

Acceptance test results

+
- { - benchmark !== undefined && ( + benchmarksUpdateTimes.length > 0 && (

Last updated

diff --git a/ensawards.org/src/components/atoms/cards/BenchmarksPerAppTypeCard.tsx b/ensawards.org/src/components/atoms/cards/BenchmarksPerAppTypeCard.tsx index eeb844be..fa0b13a1 100644 --- a/ensawards.org/src/components/atoms/cards/BenchmarksPerAppTypeCard.tsx +++ b/ensawards.org/src/components/atoms/cards/BenchmarksPerAppTypeCard.tsx @@ -1,6 +1,7 @@ +import { generalizeAcceptanceTestBenchmarks } from "data/acceptance-tests/utils"; import type { App } from "data/apps/types.ts"; import { formatAppType, getAppById } from "data/apps/utils.ts"; -import type { AppBenchmark } from "data/benchmarks/types"; +import type { AcceptanceTestBenchmarks } from "data/benchmarks/types"; import type { BestPracticeApp } from "data/ens-best-practices/types"; import { Fragment } from "react"; @@ -9,7 +10,7 @@ import { BenchmarkResultBadge } from "@/components/atoms/badges/BenchmarkResultB export interface AppWithBenchmark { app: App; bestPractice: BestPracticeApp; - benchmark?: AppBenchmark; + acceptanceTestBenchmarks: AcceptanceTestBenchmarks; } export interface BenchmarksPerAppTypeCardProps { @@ -18,11 +19,13 @@ export interface BenchmarksPerAppTypeCardProps { export function BenchmarksPerAppTypeCard({ appsWithBenchmark }: BenchmarksPerAppTypeCardProps) { // A necessary step due to Astro Island's inner serialization logic - const resolvedAppsWithBenchmark = appsWithBenchmark.map(({ app, bestPractice, benchmark }) => ({ - app: getAppById(app.id) ?? app, - bestPractice, - benchmark, - })); + const resolvedAppsWithBenchmark = appsWithBenchmark.map( + ({ app, bestPractice, acceptanceTestBenchmarks }) => ({ + app: getAppById(app.id) ?? app, + bestPractice, + acceptanceTestBenchmarks, + }), + ); if (resolvedAppsWithBenchmark.length === 0) return null; @@ -39,8 +42,10 @@ export function BenchmarksPerAppTypeCard({ appsWithBenchmark }: BenchmarksPerApp {formatAppType(firstApp.type, { plural: false, lowercase: false })} benchmarks
- {resolvedAppsWithBenchmark.map(({ app, bestPractice, benchmark }, index) => { + {resolvedAppsWithBenchmark.map(({ app, bestPractice, acceptanceTestBenchmarks }, index) => { const AppIcon = app.icon; + const generalizedBenchmarkResult = + generalizeAcceptanceTestBenchmarks(acceptanceTestBenchmarks); return ( @@ -52,7 +57,10 @@ export function BenchmarksPerAppTypeCard({ appsWithBenchmark }: BenchmarksPerApp {app.name} - + {index < appsWithBenchmark.length - 1 && (
@@ -64,5 +72,3 @@ export function BenchmarksPerAppTypeCard({ appsWithBenchmark }: BenchmarksPerApp
); } - -export default BenchmarksPerAppTypeCard; diff --git a/ensawards.org/src/components/atoms/cards/ContributorsCard.tsx b/ensawards.org/src/components/atoms/cards/ContributorsCard.tsx index 81cec6b2..78082da2 100644 --- a/ensawards.org/src/components/atoms/cards/ContributorsCard.tsx +++ b/ensawards.org/src/components/atoms/cards/ContributorsCard.tsx @@ -6,13 +6,12 @@ import { import type { Contribution, Contributor } from "data/contributors/types"; import { countContributorAppearances } from "data/contributors/utils"; import { User as NoContributionsIcon } from "lucide-react"; -import { useMemo } from "react"; -import { getAddress } from "viem"; +import { type ReactElement, useMemo } from "react"; import { buildUnresolvedIdentity, type UnresolvedIdentity } from "@ensnode/ensnode-sdk"; -import GitHubOutlineIcon from "@/assets/githubOutlineIcon.svg"; import { GenericTooltip } from "@/components/atoms/GenericTooltip"; +import { GitHubOutlineIcon } from "@/components/atoms/icons/GitHubOutlineIcon"; import { Skeleton } from "@/components/ui/skeleton.tsx"; import { TooltipProvider } from "@/components/ui/tooltip"; import { getEnsAdvocateDetailsRelativePath, getEnsAwardsBaseUrl } from "@/utils"; @@ -50,6 +49,27 @@ const formatContributionType = (contributionType: ContributionType): string => { } }; +const formatNoContributionsMessage = (contributionType: ContributionType): ReactElement => { + switch (contributionType) { + case ContributionTypes.App: + case ContributionTypes.Protocol: + return No leaderboard contributions yet; + + case ContributionTypes.Contract: + return No smart contract contributions yet; + + case ContributionTypes.BestPractice: + return No ENS best practice contributions yet; + + case ContributionTypes.BenchmarkResult: + return No acceptance test results contributed yet; + + default: + const _exhaustive: never = contributionType; + throw new Error(`Unsupported ContributionType: ${_exhaustive}`); + } +}; + export const ContributionTypes = { App: "app", BestPractice: "best-practice", @@ -114,84 +134,47 @@ export const ContributorsCard = ({
-
+
{orderedContributorProfiles.length === 0 ? (
-

- No contributions yet +

+ {formatNoContributionsMessage(contributionType)}

) : ( - orderedContributorProfiles.map((profile) => { - const identity = buildUnresolvedIdentity( - profile.address, - DEFAULT_ENS_NAMESPACE, - profile.chainId, - ); - - return ( - - } - tooltipOffset={1} - > -
- -
-
- ); - }) + orderedContributorProfiles.map((profile) => ( + + )) )}
-
-

- Submit a contribution -

- -
+ + Submit a contribution on GitHub +
@@ -226,7 +209,7 @@ const ContributorTooltipContent = ({ contributor, identity }: ContributorTooltip /> View ENS Advocate profile @@ -235,6 +218,40 @@ const ContributorTooltipContent = ({ contributor, identity }: ContributorTooltip ); }; +export const DisplayContributorIdentity = ({ contributor }: { contributor: Contributor }) => { + const identity = buildUnresolvedIdentity( + contributor.address, + DEFAULT_ENS_NAMESPACE, + contributor.chainId, + ); + + return ( + } + tooltipOffset={1} + > +
+ +
+
+ ); +}; + export const ContributorsCardLoading = ({ contributionType, sidebarVariant = false, @@ -257,7 +274,12 @@ export const ContributorsCardLoading = ({ -
+
@@ -266,21 +288,7 @@ export const ContributorsCardLoading = ({
-
-

- Submit a contribution -

-
- -
-
+
); diff --git a/ensawards.org/src/assets/githubOutlineIcon.svg b/ensawards.org/src/components/atoms/icons/GitHubOutlineIcon.tsx similarity index 54% rename from ensawards.org/src/assets/githubOutlineIcon.svg rename to ensawards.org/src/components/atoms/icons/GitHubOutlineIcon.tsx index c01202e0..729b01ac 100644 --- a/ensawards.org/src/assets/githubOutlineIcon.svg +++ b/ensawards.org/src/components/atoms/icons/GitHubOutlineIcon.tsx @@ -1,3 +1,18 @@ - - - +export const GitHubOutlineIcon = (props: React.SVGProps) => ( + + + +); diff --git a/ensawards.org/src/components/contract-naming-season/ContractNamingHero.astro b/ensawards.org/src/components/contract-naming-season/ContractNamingHero.astro index 28d96960..4c0e763a 100644 --- a/ensawards.org/src/components/contract-naming-season/ContractNamingHero.astro +++ b/ensawards.org/src/components/contract-naming-season/ContractNamingHero.astro @@ -44,11 +44,11 @@ import { shadcnButtonVariants } from "../ui/shadcnButtonStyles";

- ENS Contract Naming Season is now live + ENS Contract Naming Season

- Elevate your protocol’s onchain identity! This 6-month ENS DAO-backed + Elevate your protocol’s onchain identity! This ENS DAO-backed initiative rewards DAOs, protocols, and dApps who name their smart contracts.

diff --git a/ensawards.org/src/components/molecules/BestPracticeTechnicalDetails.astro b/ensawards.org/src/components/molecules/BestPracticeTechnicalDetails.astro deleted file mode 100644 index 3426379b..00000000 --- a/ensawards.org/src/components/molecules/BestPracticeTechnicalDetails.astro +++ /dev/null @@ -1,37 +0,0 @@ ---- -import type { BestPractice } from "data/ens-best-practices/types.ts"; -import { markdownToHtml } from "@/utils/markdown"; - -export interface BestPracticeTechnicalDetailsProps { - bestPractice: BestPractice; -} - -const { bestPractice }: BestPracticeTechnicalDetailsProps = Astro.props; - -const technicalSectionContentStyles = "w-fit text-base font-normal leading-7 text-gray-500"; -const technicalSectionMinorHeaderStyles = "text-lg leading-normal font-semibold text-black"; ---- - -
-

- {bestPractice.technicalDetails.main.header} -

-

- { - bestPractice.technicalDetails.sides.map((additionalContent) => ( - <> -

- {additionalContent.header} -

-

- - )) - } -

diff --git a/ensawards.org/src/components/molecules/technicalDetails/benchmark/acceptanceTestResultDetails.tsx b/ensawards.org/src/components/molecules/technicalDetails/benchmark/acceptanceTestResultDetails.tsx new file mode 100644 index 00000000..42dd01a7 --- /dev/null +++ b/ensawards.org/src/components/molecules/technicalDetails/benchmark/acceptanceTestResultDetails.tsx @@ -0,0 +1,71 @@ +import { createEnsNodeProviderOptions, EnsNodeProvider, RelativeTime } from "@namehash/namehash-ui"; +import type { AcceptanceTestBenchmark } from "data/acceptance-tests/types"; +import { useMemo } from "react"; + +import { DisplayContributorIdentity } from "@/components/atoms/cards/ContributorsCard"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { getENSNodeUrl } from "@/utils/env"; +import { cn } from "@/utils/tailwindClassConcatenation"; + +export interface AcceptanceTestResultFooterProps { + acceptanceTestBenchmark: AcceptanceTestBenchmark; +} + +export const AcceptanceTestResultFooter = ({ + acceptanceTestBenchmark, +}: AcceptanceTestResultFooterProps) => { + const lastBenchmarkUpdate = acceptanceTestBenchmark.contributions.reduce( + (latest, contribution) => { + return contribution.lastUpdated > latest.lastUpdated ? contribution : latest; + }, + acceptanceTestBenchmark.contributions[0], + ); + + const ensNodeProviderOptions = useMemo( + () => + createEnsNodeProviderOptions({ + url: getENSNodeUrl(), + }), + [], + ); + + return ( + + +
+

+ Test completed{" "} + + + +

+
+ Contributed by + +
+
+
+
+ ); +}; + +export const AcceptanceTestResultFooterLoading = () => { + const loadingStateStyles = "animate-pulse bg-gray-200 rounded-sm"; + return ( +
+
+ Test completed
+
+
+ Contributed by +
+
+
+ ); +}; + +export const AcceptanceTestResultNotes = ({ + acceptanceTestBenchmark, +}: { + acceptanceTestBenchmark: AcceptanceTestBenchmark; +}) => <>{acceptanceTestBenchmark.notes}; diff --git a/ensawards.org/src/components/molecules/technicalDetails/benchmark/index.astro b/ensawards.org/src/components/molecules/technicalDetails/benchmark/index.astro new file mode 100644 index 00000000..49da85fb --- /dev/null +++ b/ensawards.org/src/components/molecules/technicalDetails/benchmark/index.astro @@ -0,0 +1,91 @@ +--- +import type { App } from "data/apps/types.ts"; +import type { AcceptanceTestBenchmarks } from "data/benchmarks/types"; +import type { BestPractice } from "data/ens-best-practices/types.ts"; + +import { + BenefitFromUsingEns, + technicalSectionContainerStyles, + technicalSectionHeaderStyles, + ImplementationRecommendations, + UseCaseSummary, + AcceptanceTestDetailsContainer, +} from "../shared"; +import { cn } from "@/utils/tailwindClassConcatenation"; +import { BenchmarkResultBadge } from "@/components/atoms/badges/BenchmarkResultBadge.tsx"; +import { PendingAcceptanceTestResultCTA } from "@/components/atoms/banners/PendingAcceptanceTestResultCTA.tsx"; +import { + AcceptanceTestResultNotes, + AcceptanceTestResultFooter, + AcceptanceTestResultFooterLoading, +} from "./acceptanceTestResultDetails.tsx"; + +interface BenchmarkTechnicalDetailsProps { + bestPractice: BestPractice; + acceptanceTestBenchmarks: AcceptanceTestBenchmarks; + benchmarkedApp: App; +} + +const { bestPractice, acceptanceTestBenchmarks, benchmarkedApp }: BenchmarkTechnicalDetailsProps = + Astro.props; + +// Sort acceptance tests by slug (alphabetically) to ensure consistent display order across best practice details and benchmark details pages. +const sortedAcceptanceTestBenchmarks = [...Object.entries(acceptanceTestBenchmarks)].sort( + ([aAcceptanceTestSlug, _a], [bAcceptanceTestSlug, _b]) => + aAcceptanceTestSlug.localeCompare(bAcceptanceTestSlug), +); +--- + +
+
+

+ ENS best practice benchmark report +

+ + +
+ { + sortedAcceptanceTestBenchmarks.map( + ([acceptanceTestSlug, acceptanceTestBenchmark], index) => ( + +
+

+ {`Test ${index + 1} Result`} + +

+ {acceptanceTestBenchmark === undefined ? ( + + ) : ( + <> + +
+ + + + + )} +
+ + ), + ) + } + +
diff --git a/ensawards.org/src/components/molecules/technicalDetails/bestPractice/index.tsx b/ensawards.org/src/components/molecules/technicalDetails/bestPractice/index.tsx new file mode 100644 index 00000000..8254bc42 --- /dev/null +++ b/ensawards.org/src/components/molecules/technicalDetails/bestPractice/index.tsx @@ -0,0 +1,100 @@ +import { BenchmarkResults } from "data/benchmarks/types"; +import type { BestPractice } from "data/ens-best-practices/types.ts"; + +import { BenchmarkResultBadge } from "@/components/atoms/badges/BenchmarkResultBadge"; +import { cn } from "@/utils/tailwindClassConcatenation"; + +import { + AcceptanceTestDetailsContainer, + BenefitFromUsingEns, + ImplementationRecommendations, + technicalSectionContainerStyles, + technicalSectionHeaderStyles, + UseCaseSummary, +} from "../shared"; + +export interface BestPracticeTechnicalDetailsProps { + bestPractice: BestPractice; +} + +export const BestPracticeTechnicalDetails = ({ + bestPractice, +}: BestPracticeTechnicalDetailsProps) => { + // Sort acceptance tests by slug (alphabetically) to ensure consistent display order across best practice details and benchmark details pages. + const sortedAcceptanceTests = [...bestPractice.technicalDetails.acceptanceTests].sort((a, b) => + a.acceptanceTestSlug.localeCompare(b.acceptanceTestSlug), + ); + + return ( +
+
+

ENS best practice overview

+ + +
+ {sortedAcceptanceTests.map((acceptanceTest, index) => ( + + <> +
+

+ Example test result + +

+ {acceptanceTest.examplePass.notes} +
+ {acceptanceTest.examplePartialPass !== undefined && ( +
+

+ Example test result + +

+ {acceptanceTest.examplePartialPass.notes} +
+ )} + {acceptanceTest.exampleFail !== undefined && ( +
+

+ Example test result + +

+ {acceptanceTest.exampleFail.notes} +
+ )} + +
+ ))} + +
+ ); +}; diff --git a/ensawards.org/src/components/molecules/technicalDetails/shared.tsx b/ensawards.org/src/components/molecules/technicalDetails/shared.tsx new file mode 100644 index 00000000..fd467cea --- /dev/null +++ b/ensawards.org/src/components/molecules/technicalDetails/shared.tsx @@ -0,0 +1,64 @@ +import { EnsIcon } from "@namehash/namehash-ui"; +import type { AcceptanceTestSlug } from "data/acceptance-tests/types"; +import { getAcceptanceTestBySlug } from "data/acceptance-tests/utils"; +import type { BestPractice } from "data/ens-best-practices/types"; +import type { PropsWithChildren } from "react"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +export const technicalSectionContainerStyles = + "flex flex-col justify-start items-start gap-3 text-base leading-7 font-normal text-muted-foreground"; + +export const technicalSectionHeaderStyles = "text-2xl leading-none font-semibold text-black"; + +export const BenefitFromUsingEns = ({ bestPractice }: { bestPractice: BestPractice }) => ( +
+

+ {bestPractice.technicalDetails.benefitFromUsingEnsTitle ?? "Benefit from using ENS"} + +

+ {bestPractice.technicalDetails.benefitFromUsingEns} +
+); + +export const ImplementationRecommendations = ({ bestPractice }: { bestPractice: BestPractice }) => ( +
+

Implementation recommendations

+ {bestPractice.technicalDetails.implementationRecommendations} +
+); + +export const UseCaseSummary = ({ bestPractice }: { bestPractice: BestPractice }) => ( + <>{bestPractice.technicalDetails.useCaseSummary} +); + +export interface AcceptanceTestDetailsProps { + acceptanceTestSlug: AcceptanceTestSlug; + testIndex: number; +} + +export const AcceptanceTestDetailsContainer = ({ + acceptanceTestSlug, + testIndex, + children, +}: PropsWithChildren) => { + const acceptanceTest = getAcceptanceTestBySlug(acceptanceTestSlug); + + if (acceptanceTest === undefined) { + throw new Error( + `Invariant(AcceptanceTestSlug): Acceptance test with slug ${acceptanceTestSlug} is not defined`, + ); + } + return ( +
+

{`Acceptance Test ${testIndex + 1}`}

+ {acceptanceTest.description} + {children} +
+ ); +}; diff --git a/ensawards.org/src/components/organisms/AppBestPracticeDetails.astro b/ensawards.org/src/components/organisms/AppBestPracticeDetails.astro index 57dff726..064debe9 100644 --- a/ensawards.org/src/components/organisms/AppBestPracticeDetails.astro +++ b/ensawards.org/src/components/organisms/AppBestPracticeDetails.astro @@ -4,13 +4,13 @@ import type { BestPracticeApp } from "data/ens-best-practices/types.ts"; import { cn } from "@/utils/tailwindClassConcatenation"; import SubpageHero from "../molecules/SubpageHero.astro"; import { calcBestPracticeScore } from "data/ens-best-practices/utils.ts"; -import { sortBenchmarks, getAppBenchmarks } from "data/benchmarks/utils.ts"; +import { sortAcceptanceTestBenchmarks, getAppBenchmarks } from "data/benchmarks/utils.ts"; import { ContributionTypes, ContributorsCard, ContributorsCardLoading, } from "../atoms/cards/ContributorsCard.tsx"; -import BestPracticeTechnicalDetails from "@/components/molecules/BestPracticeTechnicalDetails.astro"; +import { BestPracticeTechnicalDetails } from "@/components/molecules/technicalDetails/bestPractice"; import { BenchmarksPerAppTypeCard } from "@/components/atoms/cards/BenchmarksPerAppTypeCard.tsx"; import { ScoreBar } from "@/components/atoms/ens-awards-score/bar.tsx"; import { type Contribution } from "data/contributors/types"; @@ -23,20 +23,39 @@ export interface AppBestPracticeDetailsProps { const { bestPractice }: AppBestPracticeDetailsProps = Astro.props; // Get apps that have benchmarks for this specific best practice -const appsWithBenchmarks = APPS.map((app) => { - const matchingBenchmark = getAppBenchmarks(app.appSlug)[bestPractice.bestPracticeSlug]; - - return { - app, - bestPractice, - benchmark: matchingBenchmark, - }; -}); +const appsWithBenchmarks = APPS.filter((app) => bestPractice.appliesTo.includes(app.type)).map( + (app) => { + const matchingBenchmark = getAppBenchmarks(app.appSlug)[bestPractice.bestPracticeSlug]; + + if (matchingBenchmark === undefined) { + throw new Error( + `Invariant(BestPracticeBenchmarks): Required benchmarks for app-${app.name} on best practice-${bestPractice.name} are not defined`, + ); + } + + return { + app, + bestPractice, + acceptanceTestBenchmarks: matchingBenchmark, + }; + }, +); // Sort benchmark results const sortedAppsByBenchmark = appsWithBenchmarks.sort((a, b) => { // First try to differentiate by benchmarks - const benchmarksDiff = sortBenchmarks(a.benchmark, b.benchmark); + + let benchmarksDiff = 0; + + const acceptanceTestSlugs = Object.keys(a.acceptanceTestBenchmarks); + + for (let idx = 0; idx < acceptanceTestSlugs.length; idx++) { + const acceptanceTestSlug = acceptanceTestSlugs[idx]; + const aBenchmark = a.acceptanceTestBenchmarks[acceptanceTestSlug]; + const bBenchmark = b.acceptanceTestBenchmarks[acceptanceTestSlug]; + + benchmarksDiff += sortAcceptanceTestBenchmarks(aBenchmark, bBenchmark); + } if (benchmarksDiff !== 0) return benchmarksDiff; @@ -46,8 +65,11 @@ const sortedAppsByBenchmark = appsWithBenchmarks.sort((a, b) => { const bestPracticeScore = calcBestPracticeScore(bestPractice); -const benchmarkContributions: Contribution[] = appsWithBenchmarks.flatMap(({ benchmark }) => - benchmark ? benchmark.contributions : [], +const benchmarkContributions: Contribution[] = appsWithBenchmarks.flatMap( + ({ acceptanceTestBenchmarks }) => + Object.values(acceptanceTestBenchmarks).flatMap( + (benchmarkResult) => benchmarkResult?.contributions ?? [], + ), ); const allContributions: Contribution[] = [...bestPractice.contributions, ...benchmarkContributions]; @@ -104,7 +126,7 @@ const additionalInfoContainerStyles = sidebarVariant contributionType={ContributionTypes.BestPractice} existingContributions={allContributions} - gitHubTargetHref={`https://github.com/namehash/ensawards/blob/main/ensawards.org/data/ens-best-practices/${bestPractice.category.categorySlug}/${bestPractice.bestPracticeSlug}.ts`}> + gitHubTargetHref={`https://github.com/namehash/ensawards/blob/main/ensawards.org/data/ens-best-practices/${bestPractice.category.categorySlug}/${bestPractice.bestPracticeSlug}/index.ts`}>
-
-

- {bestPractice.technicalDetails.main.header} -

-

- { - bestPractice.technicalDetails.sides.map((additionalContent) => ( - <> -

- {additionalContent.header} -

-

- - )) - } -

+
@@ -99,11 +111,15 @@ if (app !== undefined && bestPractice !== undefined) {
- +
+ acceptanceTestsBenchmarks={appAcceptanceTestBenchmarks}>
+ existingContributions={allContributions} + gitHubTargetHref={`https://github.com/namehash/ensawards/blob/main/ensawards.org/data/apps/${app.appSlug}/benchmarks/index.tsx`}> ({ params: { appSlug: app.appSlug } })); @@ -130,6 +128,26 @@ if (app !== undefined) { ); } + if (category.status !== CategoryStatuses.Active) { + return null; + } + + const benchmarksOnBestPractice = [ + ...Object.entries(benchmarksInCategory), + ] + .map(([bestPracticeSlug, bestPracticeBenchmarks]) => + summarizeAppsAcceptanceTestBenchmarks( + bestPracticeSlug, + bestPracticeBenchmarks, + ), + ) + .sort((a, b) => + sortBenchmarkResults( + a.generalizedBenchmarkResult, + b.generalizedBenchmarkResult, + ), + ); + return (
@@ -138,40 +156,24 @@ if (app !== undefined) { {category.name}
- {[...Object.entries(benchmarksInCategory)] - .sort(([_a, aBenchmark], [_b, bBenchmark]) => - sortBenchmarks(aBenchmark, bBenchmark), - ) - .map( - ([bestPracticeSlug, benchmark]: [ - BestPracticeSlug, - AppBenchmark | undefined, - ]) => { - const bestPractice = - getBestPracticeBySlug(bestPracticeSlug); - - if (bestPractice === undefined) { - throw new Error( - `Invariant(BestPracticeSlug): Best practice with slug ${bestPracticeSlug} not found`, - ); - } - - return ( - - - - ); - }, - )} + {benchmarksOnBestPractice.map( + ({ bestPractice, generalizedBenchmarkResult }) => { + return ( + + + + ); + }, + )}
); })} @@ -182,7 +184,7 @@ if (app !== undefined) { client:only="react" contributionType={ContributionTypes.BenchmarkResult} existingContributions={appContributions} - gitHubTargetHref={`https://github.com/namehash/ensawards/blob/main/ensawards.org/data/apps/${app.appSlug}/benchmarks.ts`} + gitHubTargetHref={`https://github.com/namehash/ensawards/blob/main/ensawards.org/data/apps/${app.appSlug}/benchmarks/index.tsx`} sidebarVariant> ({ diff --git a/ensawards.org/src/utils/markdown.ts b/ensawards.org/src/utils/markdown.ts deleted file mode 100644 index 2e56c58c..00000000 --- a/ensawards.org/src/utils/markdown.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Converts simple markdown to HTML. - * Supports: - * - links [text](url) - * - inline code `code` - * - unordered lists (- item) - * - newlines as line breaks - * Can be extended to support more markdown features as needed. - * - * NOTE: Translating JSON strings into markdown is not the ideal setup, but for the current context - * of this repo it is the simplest solution. If markdown needs increase then we will want to find a - * more robust solution. - * - */ -export function markdownToHtml(markdown: string): string { - let result = markdown; - - // First, process inline elements (code and links) before processing block elements - - // Convert inline code `code` to HTML tags - result = result.replace( - /`([^`]+)`/g, - '$1', - ); - - // Convert markdown links [text](url) to HTML
tags - result = result.replace( - /\[([^\]]+)\]\(([^)]+)\)/g, - '$1', - ); - - // Convert bullet points to HTML lists - // Match sequences of lines starting with "- " (unordered list items) - result = result.replace(/(?:^|\n)((?:- .+(?:\n|$))+)/g, (match, listContent) => { - // Extract individual list items - const items = listContent - .split("\n") - .filter((line: string) => line.trim().startsWith("- ")) - .map((line: string) => line.trim().substring(2).trim()) - .filter((item: string) => item.length > 0); - - // Build the
    with
  • items - const listItems = items - .map((item: string) => `
  • ${item}
  • `) - .join(""); - - return `

      ${listItems}

    `; - }); - - return result; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2c37fbb..5050e68d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,8 +10,8 @@ catalogs: specifier: ^5.7.3 version: 5.9.3 vitest: - specifier: ^3.2.4 - version: 3.2.4 + specifier: ^4.1.8 + version: 4.1.8 overrides: lodash@<4.18.0: '>=4.18.0 <5.0.0' @@ -45,7 +45,7 @@ importers: version: 5.9.3 vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) + version: 4.1.8(@types/node@24.12.0)(vite@7.3.2(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)) ensawards.org: dependencies: @@ -57,13 +57,13 @@ importers: version: 3.7.2 '@astrojs/vercel': specifier: ^10.0.3 - version: 10.0.3(astro@6.3.3(@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.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) '@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.50.4(typescript@5.9.3)(zod@4.3.6)) + 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)) '@ensnode/ensnode-sdk': 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.50.4(typescript@5.9.3)(zod@4.3.6)) + 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)) '@formkit/auto-animate': specifier: ^0.9.0 version: 0.9.0 @@ -75,13 +75,13 @@ importers: version: 2.2.0(react@19.2.0) '@lucide/astro': specifier: ^1.7.0 - version: 1.7.0(astro@6.3.3(@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.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)) '@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.50.4(typescript@5.9.3)(zod@4.3.6)) + 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)) '@namehash/namehash-ui': specifier: 1.14.0 - version: 1.14.0(@tanstack/react-query@5.90.11(react@19.2.0))(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sonner@2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(tailwindcss@4.1.17)(typescript@5.9.3)(viem@2.50.4(typescript@5.9.3)(zod@4.3.6)) + version: 1.14.0(@tanstack/react-query@5.90.11(react@19.2.0))(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sonner@2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(tailwindcss@4.1.17)(typescript@5.9.3)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) '@radix-ui/react-avatar': specifier: ^1.1.10 version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -114,7 +114,7 @@ importers: version: 19.2.3(@types/react@19.2.6) astro: specifier: ^6.1.10 - version: 6.3.3(@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: 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-font: specifier: ^1.0.0 version: 1.1.0 @@ -135,7 +135,7 @@ importers: version: 4.1.0 enssdk: 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)(viem@2.50.4(typescript@5.9.3)(zod@4.3.6)) + version: 1.14.0(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) lucide-react: specifier: ^0.534.0 version: 0.534.0(react@19.2.0) @@ -159,7 +159,7 @@ importers: version: 1.0.7(tailwindcss@4.1.17) viem: specifier: ^2.50.4 - version: 2.50.4(typescript@5.9.3)(zod@4.3.6) + version: 2.52.2(typescript@5.9.3)(zod@4.3.6) yup: specifier: ^1.7.0 version: 1.7.1 @@ -193,7 +193,7 @@ importers: version: 5.1.4(typescript@5.9.3)(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) + version: 4.1.8(@types/node@22.19.1)(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)) packages: @@ -230,12 +230,12 @@ packages: '@astrojs/compiler@4.0.0': resolution: {integrity: sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==} + '@astrojs/internal-helpers@0.10.0': + resolution: {integrity: sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw==} + '@astrojs/internal-helpers@0.8.0': resolution: {integrity: sha512-J56GrhEiV+4dmrGLPNOl2pZjpHXAndWVyiVDYGDuw6MWKpBSEMLdFxHzeM/6sqaknw9M+HFfHZAcvi3OfT3D/w==} - '@astrojs/internal-helpers@0.9.1': - resolution: {integrity: sha512-1pWuARqYom/TzuU3+0ZugsTrKlUydWKuULmDqSMTuonY+9IRDUEGKX/8PXQ1nBxRq3w85uGtd9q9SXfqEldMIQ==} - '@astrojs/language-server@2.16.3': resolution: {integrity: sha512-yO5K7RYCMXUfeDlnU6UnmtnoXzpuQc0yhlaCNZ67k1C/MiwwwvMZz+LGa+H35c49w5QBfvtr4w4Zcf5PcH8uYA==} hasBin: true @@ -248,8 +248,8 @@ packages: prettier-plugin-astro: optional: true - '@astrojs/markdown-remark@7.1.2': - resolution: {integrity: sha512-caXZ4Dc2St2dW8luEg22GlP0gupLdztCTQE4EzZOxW1pqWXz9mbeJEuHUkgDYcKWW8tjIHkydYDhWLVoxJ327Q==} + '@astrojs/markdown-remark@7.2.0': + resolution: {integrity: sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw==} '@astrojs/prism@4.0.2': resolution: {integrity: sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==} @@ -1486,6 +1486,9 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} @@ -1738,34 +1741,34 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@vitest/expect@3.2.4': - resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/expect@4.1.8': + resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} - '@vitest/mocker@3.2.4': - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + '@vitest/mocker@4.1.8': + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@3.2.4': - resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@4.1.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} - '@vitest/runner@3.2.4': - resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + '@vitest/runner@4.1.8': + resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} - '@vitest/snapshot@3.2.4': - resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + '@vitest/snapshot@4.1.8': + resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} - '@vitest/spy@3.2.4': - resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@4.1.8': + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} - '@vitest/utils@3.2.4': - resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} '@volar/kit@2.4.27': resolution: {integrity: sha512-ilZoQDMLzqmSsImJRWx4YiZ4FcvvPrPnFVmL6hSsIWB6Bn3qc7k88J9yP32dagrs5Y8EXIlvvD/mAFaiuEOACQ==} @@ -1879,8 +1882,8 @@ packages: astro-seo@1.1.0: resolution: {integrity: sha512-G6LDNDyga30o+52v58jU7C35n3cORUzk7RSuY+xf/ZRbW6JiNplyaOO1VoYVKO+xzYNaBcxqNln2RFRR/wgJNQ==} - astro@6.3.3: - resolution: {integrity: sha512-wvLIZQYbBZt6U8gyflBW4SLBypaqdwLZUH93rT3oT53cmQ0bTGubvMAGjqBRoheOYzYcTJZtW6czztzbu4kQ5g==} + astro@6.4.4: + resolution: {integrity: sha512-hVe8tq3lqt/Dr0UyB//yUmQSlHMTU8scTiF/vQddQVahLE4TTaSdH5H0nb7OvRcwo0UmlAO8DWYar4jNaS7H+A==} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true @@ -1923,10 +1926,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - caip@1.1.1: resolution: {integrity: sha512-a3v5lteUUOoyRI0U6qe5ayCCGkF2mCmJ5zQMDnOD2vRjgRg6sm9p8TsRC2h4D4beyqRN9RYniphAPnj/+jQC6g==} @@ -1936,8 +1935,8 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chai@5.3.3: - resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} character-entities-html4@2.1.0: @@ -1949,10 +1948,6 @@ packages: character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} - chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -2052,10 +2047,6 @@ packages: decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} @@ -2131,9 +2122,6 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} @@ -2159,8 +2147,8 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - expect-type@1.2.2: - resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} extend@3.0.2: @@ -2327,9 +2315,6 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -2437,9 +2422,6 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lru-cache@11.2.4: resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} @@ -2686,8 +2668,8 @@ packages: oniguruma-to-es@4.3.4: resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} - ox@0.14.22: - resolution: {integrity: sha512-nb5msL8qWbPglhIfZbGJAfw3cqiJjFMiWmACt7kgyWtLib12tcctbHufMT9Hb0Lr6Pt4k9I3dbpueTpbhvbqvA==} + ox@0.14.29: + resolution: {integrity: sha512-M5j87Ec4V99MQdRct/g09eWXW60g6zhHTUs1lr4deUtrPDnezBdCJTgKd7pxqTpSZBFveV0ALi9jMMuT1qKyNg==} peerDependencies: typescript: '>=5.4.0' peerDependenciesMeta: @@ -2728,10 +2710,6 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} - piccolore@0.1.3: resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} @@ -2974,8 +2952,8 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} stream-replace-string@2.0.0: resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==} @@ -2991,9 +2969,6 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - suf-log@2.5.3: resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==} @@ -3037,9 +3012,6 @@ packages: resolution: {integrity: sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA==} engines: {node: ^16.14.0 || >= 17.3.0} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.0.4: resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} @@ -3048,16 +3020,8 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - - tinyspy@4.0.4: - resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} toposort@2.0.2: @@ -3253,19 +3217,14 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - viem@2.50.4: - resolution: {integrity: sha512-rf98F4s3Vlb+uJZEKfay3IbBw3CNCbVtx5Y3UIljlO2tSX420g/J0WQSYsjzBSasUFgxgsXabji14O9kGbiqgg==} + viem@2.52.2: + resolution: {integrity: sha512-HSU12p5aD/kAPZfrlbCUqdiP4P/c6hQ9AhfTS51VbLUQIjkWd1d5EjrCx/SCxZ0zhZVRn4Iv5X5WDqXPG8Ubew==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: typescript: optional: true - vite-node@3.2.4: - resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - vite-tsconfig-paths@5.1.4: resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} peerDependencies: @@ -3322,26 +3281,39 @@ packages: vite: optional: true - vitest@3.2.4: - resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + vitest@4.1.8: + resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.4 - '@vitest/ui': 3.2.4 + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.8 + '@vitest/browser-preview': 4.1.8 + '@vitest/browser-webdriverio': 4.1.8 + '@vitest/coverage-istanbul': 4.1.8 + '@vitest/coverage-v8': 4.1.8 + '@vitest/ui': 4.1.8 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true - '@types/debug': + '@opentelemetry/api': optional: true '@types/node': optional: true - '@vitest/browser': + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': optional: true '@vitest/ui': optional: true @@ -3555,11 +3527,18 @@ snapshots: '@astrojs/compiler@4.0.0': {} - '@astrojs/internal-helpers@0.8.0': + '@astrojs/internal-helpers@0.10.0': dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + js-yaml: 4.1.1 picomatch: 4.0.4 + retext-smartypants: 6.2.0 + shiki: 4.0.2 + smol-toml: 1.6.1 + unified: 11.0.5 - '@astrojs/internal-helpers@0.9.1': + '@astrojs/internal-helpers@0.8.0': dependencies: picomatch: 4.0.4 @@ -3589,14 +3568,13 @@ snapshots: transitivePeerDependencies: - typescript - '@astrojs/markdown-remark@7.1.2': + '@astrojs/markdown-remark@7.2.0': dependencies: - '@astrojs/internal-helpers': 0.9.1 + '@astrojs/internal-helpers': 0.10.0 '@astrojs/prism': 4.0.2 github-slugger: 2.0.0 hast-util-from-html: 2.0.3 hast-util-to-text: 4.0.2 - js-yaml: 4.1.1 mdast-util-definitions: 6.0.0 rehype-raw: 7.0.0 rehype-stringify: 10.0.1 @@ -3604,9 +3582,6 @@ snapshots: remark-parse: 11.0.0 remark-rehype: 11.1.2 remark-smartypants: 3.0.2 - retext-smartypants: 6.2.0 - shiki: 4.0.2 - smol-toml: 1.6.1 unified: 11.0.5 unist-util-remove-position: 5.0.0 unist-util-visit: 5.1.0 @@ -3658,14 +3633,14 @@ snapshots: is-wsl: 3.1.1 which-pm-runs: 1.1.0 - '@astrojs/vercel@10.0.3(astro@6.3.3(@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.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)': 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.3.3(@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.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) esbuild: 0.27.3 tinyglobby: 0.2.15 transitivePeerDependencies: @@ -3890,24 +3865,24 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 - '@ensnode/datasources@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.50.4(typescript@5.9.3)(zod@4.3.6))': + '@ensnode/datasources@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))': dependencies: - '@ponder/utils': 0.2.18(typescript@5.9.3)(viem@2.50.4(typescript@5.9.3)(zod@4.3.6)) - enssdk: 1.14.0(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(viem@2.50.4(typescript@5.9.3)(zod@4.3.6)) - viem: 2.50.4(typescript@5.9.3)(zod@4.3.6) + '@ponder/utils': 0.2.18(typescript@5.9.3)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) + enssdk: 1.14.0(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) + viem: 2.52.2(typescript@5.9.3)(zod@4.3.6) transitivePeerDependencies: - gql.tada - graphql - typescript - '@ensnode/ensnode-sdk@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.50.4(typescript@5.9.3)(zod@4.3.6))': + '@ensnode/ensnode-sdk@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))': dependencies: '@ensdomains/address-encoder': 1.1.4 - '@ensnode/datasources': 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.50.4(typescript@5.9.3)(zod@4.3.6)) + '@ensnode/datasources': 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)) caip: 1.1.1 date-fns: 4.1.0 - enssdk: 1.14.0(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(viem@2.50.4(typescript@5.9.3)(zod@4.3.6)) - viem: 2.50.4(typescript@5.9.3)(zod@4.3.6) + enssdk: 1.14.0(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) + viem: 2.52.2(typescript@5.9.3)(zod@4.3.6) zod: 4.3.6 transitivePeerDependencies: - gql.tada @@ -4165,9 +4140,9 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@lucide/astro@1.7.0(astro@6.3.3(@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.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))': dependencies: - astro: 6.3.3(@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.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) '@mapbox/node-pre-gyp@2.0.3': dependencies: @@ -4182,21 +4157,21 @@ snapshots: - encoding - supports-color - '@namehash/ens-referrals@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.50.4(typescript@5.9.3)(zod@4.3.6))': + '@namehash/ens-referrals@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))': dependencies: - '@ensnode/ensnode-sdk': 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.50.4(typescript@5.9.3)(zod@4.3.6)) - enssdk: 1.14.0(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(viem@2.50.4(typescript@5.9.3)(zod@4.3.6)) - viem: 2.50.4(typescript@5.9.3)(zod@4.3.6) + '@ensnode/ensnode-sdk': 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)) + enssdk: 1.14.0(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) + viem: 2.52.2(typescript@5.9.3)(zod@4.3.6) zod: 4.3.6 transitivePeerDependencies: - gql.tada - graphql - typescript - '@namehash/namehash-ui@1.14.0(@tanstack/react-query@5.90.11(react@19.2.0))(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sonner@2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(tailwindcss@4.1.17)(typescript@5.9.3)(viem@2.50.4(typescript@5.9.3)(zod@4.3.6))': + '@namehash/namehash-ui@1.14.0(@tanstack/react-query@5.90.11(react@19.2.0))(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sonner@2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(tailwindcss@4.1.17)(typescript@5.9.3)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6))': dependencies: - '@ensnode/datasources': 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.50.4(typescript@5.9.3)(zod@4.3.6)) - '@ensnode/ensnode-sdk': 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.50.4(typescript@5.9.3)(zod@4.3.6)) + '@ensnode/datasources': 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)) + '@ensnode/ensnode-sdk': 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)) '@radix-ui/react-avatar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@radix-ui/react-slot': 1.2.4(@types/react@19.2.6)(react@19.2.0) '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -4205,7 +4180,7 @@ snapshots: class-variance-authority: 0.7.1 clsx: 2.1.1 date-fns: 4.1.0 - enssdk: 1.14.0(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(viem@2.50.4(typescript@5.9.3)(zod@4.3.6)) + enssdk: 1.14.0(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) lucide-react: 0.548.0(react@19.2.0) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) @@ -4236,9 +4211,9 @@ snapshots: '@oslojs/encoding@1.1.0': {} - '@ponder/utils@0.2.18(typescript@5.9.3)(viem@2.50.4(typescript@5.9.3)(zod@4.3.6))': + '@ponder/utils@0.2.18(typescript@5.9.3)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6))': dependencies: - viem: 2.50.4(typescript@5.9.3)(zod@4.3.6) + viem: 2.52.2(typescript@5.9.3)(zod@4.3.6) optionalDependencies: typescript: 5.9.3 @@ -4728,6 +4703,8 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@standard-schema/spec@1.1.0': {} + '@swc/helpers@0.5.17': dependencies: tslib: 2.8.1 @@ -4966,55 +4943,54 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/expect@3.2.4': + '@vitest/expect@4.1.8': dependencies: + '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - tinyrainbow: 2.0.0 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + chai: 6.2.2 + tinyrainbow: 3.1.0 - '@vitest/mocker@3.2.4(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))': + '@vitest/mocker@4.1.8(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))': dependencies: - '@vitest/spy': 3.2.4 + '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) - '@vitest/mocker@3.2.4(vite@7.3.2(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))': + '@vitest/mocker@4.1.8(vite@7.3.2(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))': dependencies: - '@vitest/spy': 3.2.4 + '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 7.3.2(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) - '@vitest/pretty-format@3.2.4': + '@vitest/pretty-format@4.1.8': dependencies: - tinyrainbow: 2.0.0 + tinyrainbow: 3.1.0 - '@vitest/runner@3.2.4': + '@vitest/runner@4.1.8': dependencies: - '@vitest/utils': 3.2.4 + '@vitest/utils': 4.1.8 pathe: 2.0.3 - strip-literal: 3.1.0 - '@vitest/snapshot@3.2.4': + '@vitest/snapshot@4.1.8': dependencies: - '@vitest/pretty-format': 3.2.4 + '@vitest/pretty-format': 4.1.8 + '@vitest/utils': 4.1.8 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@3.2.4': - dependencies: - tinyspy: 4.0.4 + '@vitest/spy@4.1.8': {} - '@vitest/utils@3.2.4': + '@vitest/utils@4.1.8': dependencies: - '@vitest/pretty-format': 3.2.4 - loupe: 3.2.1 - tinyrainbow: 2.0.0 + '@vitest/pretty-format': 4.1.8 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 '@volar/kit@2.4.27(typescript@5.9.3)': dependencies: @@ -5133,11 +5109,11 @@ snapshots: - prettier-plugin-astro - typescript - astro@6.3.3(@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.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): dependencies: '@astrojs/compiler': 4.0.0 - '@astrojs/internal-helpers': 0.9.1 - '@astrojs/markdown-remark': 7.1.2 + '@astrojs/internal-helpers': 0.10.0 + '@astrojs/markdown-remark': 7.2.0 '@astrojs/telemetry': 3.3.2 '@capsizecss/unpack': 4.0.0 '@clack/prompts': 1.1.0 @@ -5259,21 +5235,13 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.1.4(browserslist@4.28.0) - cac@6.7.14: {} - caip@1.1.1: {} caniuse-lite@1.0.30001756: {} ccount@2.0.1: {} - chai@5.3.3: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.1 - deep-eql: 5.0.2 - loupe: 3.2.1 - pathval: 2.0.1 + chai@6.2.2: {} character-entities-html4@2.1.0: {} @@ -5281,8 +5249,6 @@ snapshots: character-entities@2.0.2: {} - check-error@2.1.1: {} - chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -5367,8 +5333,6 @@ snapshots: dependencies: character-entities: 2.0.2 - deep-eql@5.0.2: {} - defu@6.1.7: {} dequal@2.0.3: {} @@ -5423,21 +5387,19 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 - enssdk@1.14.0(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(viem@2.50.4(typescript@5.9.3)(zod@4.3.6)): + enssdk@1.14.0(gql.tada@1.9.2(graphql@16.13.2)(typescript@5.9.3))(graphql@16.13.2)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)): dependencies: '@adraffy/ens-normalize': 1.11.1 '@ensdomains/address-encoder': 1.1.4 caip: 1.1.1 gql.tada: 1.9.2(graphql@16.13.2)(typescript@5.9.3) graphql: 16.13.2 - viem: 2.50.4(typescript@5.9.3)(zod@4.3.6) + viem: 2.52.2(typescript@5.9.3)(zod@4.3.6) entities@4.5.0: {} entities@6.0.1: {} - es-module-lexer@1.7.0: {} - es-module-lexer@2.0.0: {} esbuild@0.27.3: @@ -5481,7 +5443,7 @@ snapshots: eventemitter3@5.0.1: {} - expect-type@1.2.2: {} + expect-type@1.3.0: {} extend@3.0.2: {} @@ -5682,8 +5644,6 @@ snapshots: js-tokens@4.0.0: {} - js-tokens@9.0.1: {} - js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -5753,8 +5713,6 @@ snapshots: longest-streak@3.1.0: {} - loupe@3.2.1: {} - lru-cache@11.2.4: {} lru-cache@11.3.5: {} @@ -6164,7 +6122,7 @@ snapshots: regex: 6.0.1 regex-recursion: 6.0.2 - ox@0.14.22(typescript@5.9.3)(zod@4.3.6): + ox@0.14.29(typescript@5.9.3)(zod@4.3.6): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/ciphers': 1.3.0 @@ -6216,8 +6174,6 @@ snapshots: pathe@2.0.3: {} - pathval@2.0.1: {} - piccolore@0.1.3: {} picocolors@1.1.1: {} @@ -6524,7 +6480,7 @@ snapshots: stackback@0.0.2: {} - std-env@3.10.0: {} + std-env@4.1.0: {} stream-replace-string@2.0.0: {} @@ -6543,10 +6499,6 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-literal@3.1.0: - dependencies: - js-tokens: 9.0.1 - suf-log@2.5.3: dependencies: s.color: 0.0.15 @@ -6589,8 +6541,6 @@ snapshots: tinyclip@0.1.12: {} - tinyexec@0.3.2: {} - tinyexec@1.0.4: {} tinyglobby@0.2.15: @@ -6598,11 +6548,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - tinypool@1.1.1: {} - - tinyrainbow@2.0.0: {} - - tinyspy@4.0.4: {} + tinyrainbow@3.1.0: {} toposort@2.0.2: {} @@ -6751,7 +6697,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - viem@2.50.4(typescript@5.9.3)(zod@4.3.6): + viem@2.52.2(typescript@5.9.3)(zod@4.3.6): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 @@ -6759,7 +6705,7 @@ snapshots: '@scure/bip39': 1.6.0 abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) isows: 1.0.7(ws@8.20.1) - ox: 0.14.22(typescript@5.9.3)(zod@4.3.6) + ox: 0.14.29(typescript@5.9.3)(zod@4.3.6) ws: 8.20.1 optionalDependencies: typescript: 5.9.3 @@ -6768,48 +6714,6 @@ snapshots: - utf-8-validate - zod - vite-node@3.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite-node@3.2.4(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.3.2(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)): dependencies: debug: 4.4.3 @@ -6855,89 +6759,59 @@ snapshots: optionalDependencies: vite: 7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3): + vitest@4.1.8(@types/node@22.19.1)(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)): dependencies: - '@types/chai': 5.2.3 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - debug: 4.4.3 - expect-type: 1.2.2 + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 magic-string: 0.30.21 + obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.4 - std-env: 3.10.0 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 0.3.2 + tinyexec: 1.0.4 tinyglobby: 0.2.15 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 + tinyrainbow: 3.1.0 vite: 7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) - vite-node: 3.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: - '@types/debug': 4.1.12 '@types/node': 22.19.1 transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3): + vitest@4.1.8(@types/node@24.12.0)(vite@7.3.2(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)): dependencies: - '@types/chai': 5.2.3 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.2(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - debug: 4.4.3 - expect-type: 1.2.2 + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@7.3.2(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 magic-string: 0.30.21 + obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.4 - std-env: 3.10.0 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 0.3.2 + tinyexec: 1.0.4 tinyglobby: 0.2.15 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 + tinyrainbow: 3.1.0 vite: 7.3.2(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) - vite-node: 3.2.4(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: - '@types/debug': 4.1.12 '@types/node': 24.12.0 transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml volar-service-css@0.0.68(@volar/language-service@2.4.27): dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 55444128..e78daa8f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,4 +3,4 @@ packages: catalog: typescript: ^5.7.3 - "vitest": ^3.2.4 + "vitest": ^4.1.8