diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6bcc1a56..134d1779 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,24 @@ 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; + implementationRecommendations: JSX.Element; + acceptanceTests: [AcceptanceTest, ...AcceptanceTest[]]; +} + export interface BestPracticeAbstract< BestPracticeT extends BestPracticeType, AppliesToT extends BestPracticeTarget, @@ -212,16 +245,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 +257,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", @@ -263,21 +340,26 @@ export interface BestPracticeCategory { ### 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 +371,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..3dfb0d23 --- /dev/null +++ b/ensawards.org/data/acceptance-tests/utils.ts @@ -0,0 +1,90 @@ +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"; + +/** 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}. */ +export const getAcceptanceTestBenchmarksByApp = ( + appSlug: AppSlug, +): (AcceptanceTestBenchmark | undefined)[] => { + const appBenchmarks = getAppBenchmarks(appSlug); + + return Object.values(appBenchmarks).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/1inch-defi-app/benchmarks/index.tsx b/ensawards.org/data/apps/1inch-defi-app/benchmarks/index.tsx new file mode 100644 index 00000000..ac426f26 --- /dev/null +++ b/ensawards.org/data/apps/1inch-defi-app/benchmarks/index.tsx @@ -0,0 +1,20 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying app benchmarks + +import OneInchDeFiApp from "data/apps/1inch-defi-app"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types"; + +// TODO; Add proper benchmarks +const benchmarks: BestPracticeBenchmarks = { + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": undefined, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": undefined, + }, +}; + +defineAppBenchmarks(OneInchDeFiApp, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/1inch-defi-app/icon.tsx b/ensawards.org/data/apps/1inch-defi-app/icon.tsx new file mode 100644 index 00000000..8f716c6d --- /dev/null +++ b/ensawards.org/data/apps/1inch-defi-app/icon.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +const Icon = ({ className, ...props }: React.SVGProps) => ( + + + + +); + +export default Icon; diff --git a/ensawards.org/data/apps/1inch-defi-app/index.ts b/ensawards.org/data/apps/1inch-defi-app/index.ts new file mode 100644 index 00000000..1eeb75d5 --- /dev/null +++ b/ensawards.org/data/apps/1inch-defi-app/index.ts @@ -0,0 +1,30 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying apps + +import OneInchProject from "data/projects/1inch/index.ts"; +import { asInterpretedName } from "enssdk"; + +import { defineApp } from "../registry.ts"; +import { type App, AppTypes } from "../types.ts"; +import OneInchIcon from "./icon.tsx"; + +const OneInchDeFiApp: App = { + id: "1inch-defi-app", + appSlug: "1inch-defi-app", + type: AppTypes.DeFi, + project: OneInchProject, + name: '1inch"', + description: + "A DeFi aggregator app for swapping tokens across 13+ chains with the only native connection between Solana and EVM networks, with true self-custody and built-in security.", + socials: { + website: new URL("https://1inch.com/swap"), + twitter: new URL("https://x.com/1inch"), + ens: asInterpretedName("1inch.eth"), + }, + icon: OneInchIcon, // TODO: The icon is not perfect for dark bgs. Same case as with Ledger. Brainstorm fix ideas (but no hacky ifs in the code) + // TODO: Add OG images +}; + +defineApp(OneInchDeFiApp); + +export default OneInchDeFiApp; diff --git a/ensawards.org/data/apps/aave-defi-app/benchmarks/index.tsx b/ensawards.org/data/apps/aave-defi-app/benchmarks/index.tsx new file mode 100644 index 00000000..2cb88001 --- /dev/null +++ b/ensawards.org/data/apps/aave-defi-app/benchmarks/index.tsx @@ -0,0 +1,20 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying app benchmarks + +import AaveDeFiApp from "data/apps/aave-defi-app"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types"; + +// TODO; Add proper benchmarks +const benchmarks: BestPracticeBenchmarks = { + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": undefined, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": undefined, + }, +}; + +defineAppBenchmarks(AaveDeFiApp, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/aave-defi-app/icon.tsx b/ensawards.org/data/apps/aave-defi-app/icon.tsx new file mode 100644 index 00000000..5c5873d2 --- /dev/null +++ b/ensawards.org/data/apps/aave-defi-app/icon.tsx @@ -0,0 +1,33 @@ +import React from "react"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +const Icon = ({ className, ...props }: React.SVGProps) => ( + + + + + +); + +export default Icon; diff --git a/ensawards.org/data/apps/aave-defi-app/index.ts b/ensawards.org/data/apps/aave-defi-app/index.ts new file mode 100644 index 00000000..a801ade2 --- /dev/null +++ b/ensawards.org/data/apps/aave-defi-app/index.ts @@ -0,0 +1,30 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying apps + +import AaveProject from "data/projects/aave/index.ts"; +import { asInterpretedName } from "enssdk"; + +import { defineApp } from "../registry.ts"; +import { type App, AppTypes } from "../types.ts"; +import AaveIcon from "./icon.tsx"; + +const AaveDeFiApp: App = { + id: "aave-defi-app", + appSlug: "aave-defi-app", + type: AppTypes.DeFi, + project: AaveProject, + name: "Aave", + description: + "A decentralized lending app where users can supply assets to earn yield and borrow against their collateral across Ethereum and multiple L2 networks.", + socials: { + website: new URL("https://aave.com/app"), + twitter: new URL("https://x.com/aave"), + ens: asInterpretedName("aave.eth"), + }, + icon: AaveIcon, + // TODO: Add OG images +}; + +defineApp(AaveDeFiApp); + +export default AaveDeFiApp; 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/index.tsx b/ensawards.org/data/apps/ambire-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..99f859c8 --- /dev/null +++ b/ensawards.org/data/apps/ambire-wallet/benchmarks/index.tsx @@ -0,0 +1,47 @@ +// 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"; + +const benchmarks = { + "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: ( + // TODO: Enhance the 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: ( + // TODO: Enhance the notes +
+

Benchmark placeholder notes

+
+ ), + } 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/index.tsx b/ensawards.org/data/apps/blockscout-explorer/benchmarks/index.tsx new file mode 100644 index 00000000..a5c2dbe8 --- /dev/null +++ b/ensawards.org/data/apps/blockscout-explorer/benchmarks/index.tsx @@ -0,0 +1,67 @@ +// 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"; + +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": { + "mainnet-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Pass, + contributions: [ + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:28:32.410Z") }, + ], + notes: ( + // TODO: Enhance the 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: Enhance the notes +
+

TODO: Add real benchmark notes

+ example proof +
+ ), + } 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/index.tsx b/ensawards.org/data/apps/coinbase-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..75c88c18 --- /dev/null +++ b/ensawards.org/data/apps/coinbase-wallet/benchmarks/index.tsx @@ -0,0 +1,67 @@ +// 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"; + +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": { + "mainnet-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Fail, + contributions: [ + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:22:55.716Z") }, + ], + notes: ( + // TODO: Enhance the 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: Enhance the notes +
+

TODO: Add real benchmark notes

+ example proof +
+ ), + } 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/index.tsx b/ensawards.org/data/apps/etherscan-explorer/benchmarks/index.tsx new file mode 100644 index 00000000..3056160b --- /dev/null +++ b/ensawards.org/data/apps/etherscan-explorer/benchmarks/index.tsx @@ -0,0 +1,67 @@ +// 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"; + +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": { + "mainnet-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Pass, + contributions: [ + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:26:20.566Z") }, + ], + notes: ( + // TODO: Enhance the 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: Enhance the notes +
+

TODO: Add real benchmark notes

+ example proof +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, +} as const satisfies BestPracticeBenchmarks; + +defineAppBenchmarks(EtherscanExplorer, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/ledger-wallet/benchmarks/index.tsx b/ensawards.org/data/apps/ledger-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..09cfe52d --- /dev/null +++ b/ensawards.org/data/apps/ledger-wallet/benchmarks/index.tsx @@ -0,0 +1,20 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying app benchmarks + +import LedgerWallet from "data/apps/ledger-wallet"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types"; + +// TODO; Add proper benchmarks +const benchmarks: BestPracticeBenchmarks = { + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": undefined, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": undefined, + }, +}; + +defineAppBenchmarks(LedgerWallet, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/ledger-wallet/icon.tsx b/ensawards.org/data/apps/ledger-wallet/icon.tsx new file mode 100644 index 00000000..b2781a74 --- /dev/null +++ b/ensawards.org/data/apps/ledger-wallet/icon.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +// TODO: Think how we can enhance the display of this icon on dark backgrounds (without hacky if-ology in the component) +const Icon = ({ className, ...props }: React.SVGProps) => ( + + + +); + +export default Icon; diff --git a/ensawards.org/data/apps/ledger-wallet/index.ts b/ensawards.org/data/apps/ledger-wallet/index.ts new file mode 100644 index 00000000..b36f6d66 --- /dev/null +++ b/ensawards.org/data/apps/ledger-wallet/index.ts @@ -0,0 +1,28 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying apps + +import LedgerProject from "data/projects/ledger/index.ts"; + +import { defineApp } from "../registry.ts"; +import { type App, AppTypes } from "../types.ts"; +import LedgerIcon from "./icon.tsx"; + +const LedgerWallet: App = { + id: "ledger-wallet", + appSlug: "ledger-wallet", + type: AppTypes.Wallet, + project: LedgerProject, + name: "Ledger Wallet", + description: + "A self-custody software wallet (formerly Ledger Live) for managing, swapping, and staking crypto and NFTs across chains, with optional pairing to Ledger hardware devices.", + socials: { + website: new URL("https://shop.ledger.com/pages/ledger-wallet"), + twitter: new URL("https://x.com/Ledger"), + }, + icon: LedgerIcon, + // TODO: Add OG images +}; + +defineApp(LedgerWallet); + +export default LedgerWallet; 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/index.tsx b/ensawards.org/data/apps/metamask-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..9b23fcfa --- /dev/null +++ b/ensawards.org/data/apps/metamask-wallet/benchmarks/index.tsx @@ -0,0 +1,53 @@ +// 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"; + +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": { + "mainnet-interactions-display-named-smart-contracts": { + result: BenchmarkResults.Pass, + contributions: [ + { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:24:39.561Z") }, + ], + notes: ( + // TODO: Enhance the 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: ( + // TODO: Enhance the notes +
+

Benchmark placeholder notes

+
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, +} as const satisfies BestPracticeBenchmarks; + +defineAppBenchmarks(MetaMaskWallet, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/rabby-wallet/benchmarks/index.tsx b/ensawards.org/data/apps/rabby-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..4f1bdcb4 --- /dev/null +++ b/ensawards.org/data/apps/rabby-wallet/benchmarks/index.tsx @@ -0,0 +1,20 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying app benchmarks + +import RabbyWallet from "data/apps/rabby-wallet"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types"; + +// TODO; Add proper benchmarks +const benchmarks: BestPracticeBenchmarks = { + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": undefined, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": undefined, + }, +}; + +defineAppBenchmarks(RabbyWallet, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/rabby-wallet/icon.tsx b/ensawards.org/data/apps/rabby-wallet/icon.tsx new file mode 100644 index 00000000..9470a499 --- /dev/null +++ b/ensawards.org/data/apps/rabby-wallet/icon.tsx @@ -0,0 +1,81 @@ +import React from "react"; + +const Icon = (props: React.SVGProps) => ( + + + + + + + + + + + + + + + + + + + + + + + + +); + +export default Icon; diff --git a/ensawards.org/data/apps/rabby-wallet/index.ts b/ensawards.org/data/apps/rabby-wallet/index.ts new file mode 100644 index 00000000..339a62e2 --- /dev/null +++ b/ensawards.org/data/apps/rabby-wallet/index.ts @@ -0,0 +1,28 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying apps + +import RabbyProject from "data/projects/rabby/index.ts"; + +import { defineApp } from "../registry.ts"; +import { type App, AppTypes } from "../types.ts"; +import RabbyIcon from "./icon.tsx"; + +const RabbyWallet: App = { + id: "rabby-wallet", + appSlug: "rabby-wallet", + type: AppTypes.Wallet, + project: RabbyProject, + name: "Rabby Wallet", + description: + "An open-source, security-focused browser wallet built for DeFi on Ethereum and every EVM chain.", + icon: RabbyIcon, + socials: { + website: new URL("https://rabby.io/"), + twitter: new URL("https://x.com/Rabby_io"), + }, + // TODO: Add OG images +}; + +defineApp(RabbyWallet); + +export default RabbyWallet; 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/index.tsx b/ensawards.org/data/apps/rainbow-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..ac8aad50 --- /dev/null +++ b/ensawards.org/data/apps/rainbow-wallet/benchmarks/index.tsx @@ -0,0 +1,68 @@ +// 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"; + +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": { + "mainnet-interactions-display-named-smart-contracts": undefined, // TODO: roll back to real benchmark before merging. + // { + // result: BenchmarkResults.Pass, + // contributions: [ + // { from: contributors.stevedylan, lastUpdated: parseTimestamp("2025-12-08T18:19:28.672Z") }, + // ], + // notes: ( + // // TODO: Enhance the 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: ( + // TODO: Enhance the notes +
+

Benchmark placeholder notes

+ example proof +
+ ), + } as const satisfies AcceptanceTestBenchmark, + }, +} as const satisfies BestPracticeBenchmarks; + +defineAppBenchmarks(RainbowWallet, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/readyx-wallet/benchmarks/index.tsx b/ensawards.org/data/apps/readyx-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..c03d6e81 --- /dev/null +++ b/ensawards.org/data/apps/readyx-wallet/benchmarks/index.tsx @@ -0,0 +1,20 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying app benchmarks + +import ReadyXWallet from "data/apps/readyx-wallet"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types"; + +// TODO; Add proper benchmarks +const benchmarks: BestPracticeBenchmarks = { + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": undefined, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": undefined, + }, +}; + +defineAppBenchmarks(ReadyXWallet, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/readyx-wallet/icon.tsx b/ensawards.org/data/apps/readyx-wallet/icon.tsx new file mode 100644 index 00000000..4b8211ab --- /dev/null +++ b/ensawards.org/data/apps/readyx-wallet/icon.tsx @@ -0,0 +1,21 @@ +import { cn } from "@/utils/tailwindClassConcatenation"; + +const Icon = ({ className, ...props }: React.SVGProps) => ( + + + +); + +export default Icon; diff --git a/ensawards.org/data/apps/readyx-wallet/index.ts b/ensawards.org/data/apps/readyx-wallet/index.ts new file mode 100644 index 00000000..28bb5c52 --- /dev/null +++ b/ensawards.org/data/apps/readyx-wallet/index.ts @@ -0,0 +1,28 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying apps + +import ReadyProject from "data/projects/ready/index.ts"; + +import { defineApp } from "../registry.ts"; +import { type App, AppTypes } from "../types.ts"; +import ReadyIcon from "./icon.tsx"; + +const ReadyXWallet: App = { + id: "readyx-wallet", + appSlug: "readyx-wallet", + type: AppTypes.Wallet, + project: ReadyProject, + name: "Ready X", + description: + "Self-custody crypto wallet for swapping tokens and earning DeFi rewards, on both browser and mobile.", + socials: { + website: new URL("https://www.ready.co/ready-x"), + twitter: new URL("https://x.com/ready_co"), + }, + icon: ReadyIcon, + // TODO: Add OG images +}; + +defineApp(ReadyXWallet); + +export default ReadyXWallet; diff --git a/ensawards.org/data/apps/safe-wallet/benchmarks/index.tsx b/ensawards.org/data/apps/safe-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..b9a9a583 --- /dev/null +++ b/ensawards.org/data/apps/safe-wallet/benchmarks/index.tsx @@ -0,0 +1,20 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying app benchmarks + +import SafeWallet from "data/apps/safe-wallet"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types"; + +// TODO; Add proper benchmarks +const benchmarks: BestPracticeBenchmarks = { + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": undefined, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": undefined, + }, +}; + +defineAppBenchmarks(SafeWallet, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/safe-wallet/icon.tsx b/ensawards.org/data/apps/safe-wallet/icon.tsx new file mode 100644 index 00000000..628ce3cc --- /dev/null +++ b/ensawards.org/data/apps/safe-wallet/icon.tsx @@ -0,0 +1,33 @@ +import React from "react"; + +const Icon = (props: React.SVGProps) => ( + + + + + +); + +export default Icon; diff --git a/ensawards.org/data/apps/safe-wallet/index.ts b/ensawards.org/data/apps/safe-wallet/index.ts new file mode 100644 index 00000000..06af5380 --- /dev/null +++ b/ensawards.org/data/apps/safe-wallet/index.ts @@ -0,0 +1,30 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying apps + +import SafeProject from "data/projects/safe"; +import { asInterpretedName } from "enssdk"; + +import { defineApp } from "../registry.ts"; +import { type App, AppTypes } from "../types.ts"; +import SafeIcon from "./icon.tsx"; + +const SafeWallet: App = { + id: "safe-wallet", + appSlug: "safe-wallet", + type: AppTypes.Wallet, + project: SafeProject, + name: "Safe{Wallet}", + description: + "A multisignature smart wallet with transaction simulation, spending limits, and role-based access across multiple chains.", + socials: { + website: new URL("https://safe.global/"), + twitter: new URL("https://x.com/SafeLabs_"), + ens: asInterpretedName("safe.eth"), + }, + icon: SafeIcon, + // TODO: Add OG images +}; + +defineApp(SafeWallet); + +export default SafeWallet; diff --git a/ensawards.org/data/apps/status-wallet/benchmarks/index.tsx b/ensawards.org/data/apps/status-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..f05fdefa --- /dev/null +++ b/ensawards.org/data/apps/status-wallet/benchmarks/index.tsx @@ -0,0 +1,20 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying app benchmarks + +import StatusWallet from "data/apps/status-wallet"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types"; + +// TODO; Add proper benchmarks +const benchmarks: BestPracticeBenchmarks = { + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": undefined, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": undefined, + }, +}; + +defineAppBenchmarks(StatusWallet, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/status-wallet/icon.tsx b/ensawards.org/data/apps/status-wallet/icon.tsx new file mode 100644 index 00000000..9e4e453b --- /dev/null +++ b/ensawards.org/data/apps/status-wallet/icon.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +const Icon = (props: React.SVGProps) => ( + + + +); + +export default Icon; diff --git a/ensawards.org/data/apps/status-wallet/index.ts b/ensawards.org/data/apps/status-wallet/index.ts new file mode 100644 index 00000000..61c258ef --- /dev/null +++ b/ensawards.org/data/apps/status-wallet/index.ts @@ -0,0 +1,28 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying apps + +import StatusProject from "data/projects/status/index.ts"; + +import { defineApp } from "../registry.ts"; +import { type App, AppTypes } from "../types.ts"; +import StatusIcon from "./icon.tsx"; + +const StatusWallet: App = { + id: "status-wallet", + appSlug: "status-wallet", + type: AppTypes.Wallet, + project: StatusProject, + name: "Status", + description: + "A self-custody multichain wallet built into Status's private messenger and Web3 browser, supporting Ethereum and EVM chains.", + socials: { + website: new URL("https://status.app/"), + twitter: new URL("https://x.com/ethstatus"), + }, + icon: StatusIcon, + // TODO: Add OG images +}; + +defineApp(StatusWallet); + +export default StatusWallet; diff --git a/ensawards.org/data/apps/types.ts b/ensawards.org/data/apps/types.ts index 6d05410a..8e7cd445 100644 --- a/ensawards.org/data/apps/types.ts +++ b/ensawards.org/data/apps/types.ts @@ -10,6 +10,7 @@ import type { Project } from "../projects/types.ts"; export const AppTypes = { Wallet: "wallet", Explorer: "explorer", + DeFi: "defi-app", } as const; /** diff --git a/ensawards.org/data/apps/uniswap-defi-app/benchmarks/index.tsx b/ensawards.org/data/apps/uniswap-defi-app/benchmarks/index.tsx new file mode 100644 index 00000000..b73aa988 --- /dev/null +++ b/ensawards.org/data/apps/uniswap-defi-app/benchmarks/index.tsx @@ -0,0 +1,20 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying app benchmarks + +import UniswapDeFiApp from "data/apps/uniswap-defi-app"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types"; + +// TODO; Add proper benchmarks +const benchmarks: BestPracticeBenchmarks = { + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": undefined, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": undefined, + }, +}; + +defineAppBenchmarks(UniswapDeFiApp, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/uniswap-defi-app/icon.tsx b/ensawards.org/data/apps/uniswap-defi-app/icon.tsx new file mode 100644 index 00000000..2db804fd --- /dev/null +++ b/ensawards.org/data/apps/uniswap-defi-app/icon.tsx @@ -0,0 +1,55 @@ +import React from "react"; + +const Icon = (props: React.SVGProps) => ( + + + + + + + + + + + +); + +export default Icon; diff --git a/ensawards.org/data/apps/uniswap-defi-app/index.ts b/ensawards.org/data/apps/uniswap-defi-app/index.ts new file mode 100644 index 00000000..a07026cc --- /dev/null +++ b/ensawards.org/data/apps/uniswap-defi-app/index.ts @@ -0,0 +1,30 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying apps + +import UniswapProject from "data/projects/uniswap/index.ts"; +import { asInterpretedName } from "enssdk"; + +import { defineApp } from "../registry.ts"; +import { type App, AppTypes } from "../types.ts"; +import UniswapIcon from "./icon.tsx"; + +const UniswapDeFiApp: App = { + id: "uniswap-defi-app", + appSlug: "uniswap-defi-app", + type: AppTypes.DeFi, + project: UniswapProject, + name: "Uniswap", + description: + "A decentralized exchange app for swapping tokens and providing liquidity through automated market makers across Ethereum and 12+ other chains.", + socials: { + website: new URL("https://app.uniswap.org"), + twitter: new URL("https://x.com/Uniswap"), + ens: asInterpretedName("uniswap.eth"), + }, + icon: UniswapIcon, + // TODO: Add OG images +}; + +defineApp(UniswapDeFiApp); + +export default UniswapDeFiApp; diff --git a/ensawards.org/data/apps/utils.test.ts b/ensawards.org/data/apps/utils.test.ts index 569c3f59..2e3f41aa 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,7 +11,7 @@ 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(() => ({ @@ -33,6 +33,8 @@ vi.mock("data/benchmarks/utils.ts", () => ({ getAppBenchmarks: (slug: AppSlug) => mockBenchmarks[slug], })); +import type { AcceptanceTestBenchmark } from "data/acceptance-tests/types.ts"; + import { appliesToAllApps, asAppType, @@ -51,7 +53,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; @@ -117,16 +119,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 +140,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 +161,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 +174,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( @@ -175,7 +187,7 @@ describe("App utils", () => { describe("appliesToAllApps", () => { it("Should return true when all app types are included in targets", () => { - expect(appliesToAllApps([AppTypes.Wallet, AppTypes.Explorer])).toEqual(true); + expect(appliesToAllApps([AppTypes.Wallet, AppTypes.Explorer, AppTypes.DeFi])).toEqual(true); }); it("Should return false when not all app types are included in targets", () => { @@ -186,36 +198,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..2ce7133d 100644 --- a/ensawards.org/data/apps/utils.ts +++ b/ensawards.org/data/apps/utils.ts @@ -1,3 +1,4 @@ +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"; @@ -26,6 +27,10 @@ export const asAppType = (maybeAppType: string): AppType => { return AppTypes.Wallet; case "explorer": return AppTypes.Explorer; + + case "defi-app": + return AppTypes.DeFi; + default: throw new Error(`Invalid AppType value: ${maybeAppType}`); } @@ -58,21 +63,19 @@ export const getAppByName = (appName: string): App | undefined => { * @returns undefined - if no benchmarks are completed. */ 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); }; @@ -125,11 +128,16 @@ export const formatAppType = ( formattedType = plural ? "Explorers" : "Explorer"; break; + case AppTypes.DeFi: + formattedType = plural ? "DeFi apps" : "DeFi app"; + break; + default: const _exhaustive: never = appType; throw new Error(`Unsupported AppType: ${_exhaustive}`); } + // TODO: Should we keep the "DeFi" formatting even for the lowercase option selected? if (lowercase) { formattedType = formattedType.toLowerCase(); } 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/index.tsx b/ensawards.org/data/apps/walletchan-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..4bc2b14f --- /dev/null +++ b/ensawards.org/data/apps/walletchan-wallet/benchmarks/index.tsx @@ -0,0 +1,66 @@ +// 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 namedSmartContractsOnMainnetProofImage from "./named-smart-contracts-on-mainnet-proof.png"; + +const benchmarks: BestPracticeBenchmarks = { + "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: ( + // TODO: Enhance the 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: Enhance the notes +
+

TODO: Add correct Benchmark notes

+ example proof +
+ ), + } 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/apps/worldapp-wallet/benchmarks/index.tsx b/ensawards.org/data/apps/worldapp-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..624dc6c4 --- /dev/null +++ b/ensawards.org/data/apps/worldapp-wallet/benchmarks/index.tsx @@ -0,0 +1,20 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying app benchmarks + +import WorldWallet from "data/apps/worldapp-wallet"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types"; + +// TODO; Add proper benchmarks +const benchmarks: BestPracticeBenchmarks = { + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": undefined, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": undefined, + }, +}; + +defineAppBenchmarks(WorldWallet, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/worldapp-wallet/icon.tsx b/ensawards.org/data/apps/worldapp-wallet/icon.tsx new file mode 100644 index 00000000..f405a8c1 --- /dev/null +++ b/ensawards.org/data/apps/worldapp-wallet/icon.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +const Icon = ({ className, ...props }: React.SVGProps) => ( + + + +); + +export default Icon; diff --git a/ensawards.org/data/apps/worldapp-wallet/index.ts b/ensawards.org/data/apps/worldapp-wallet/index.ts new file mode 100644 index 00000000..98705371 --- /dev/null +++ b/ensawards.org/data/apps/worldapp-wallet/index.ts @@ -0,0 +1,28 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying apps + +import WorldProject from "data/projects/world/index.ts"; + +import { defineApp } from "../registry.ts"; +import { type App, AppTypes } from "../types.ts"; +import WorldIcon from "./icon.tsx"; + +const WorldWallet: App = { + id: "worldapp-wallet", + appSlug: "worldapp-wallet", + type: AppTypes.Wallet, + project: WorldProject, + name: "World App", + description: + "World App is a financial super app with a self-custodial wallet built into it that allows verified humans to save, send, and earn yield on USDC, WLD, and local stablecoins on Optimism.", + socials: { + website: new URL("https://world.org/world-app"), + twitter: new URL("https://x.com/worldnetwork"), + }, + icon: WorldIcon, + // TODO: Add OG images +}; + +defineApp(WorldWallet); + +export default WorldWallet; diff --git a/ensawards.org/data/apps/zerion-wallet/benchmarks/index.tsx b/ensawards.org/data/apps/zerion-wallet/benchmarks/index.tsx new file mode 100644 index 00000000..e815331b --- /dev/null +++ b/ensawards.org/data/apps/zerion-wallet/benchmarks/index.tsx @@ -0,0 +1,20 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying app benchmarks + +import ZerionWallet from "data/apps/zerion-wallet"; +import { defineAppBenchmarks } from "data/benchmarks/registry"; +import type { BestPracticeBenchmarks } from "data/ens-best-practices/types"; + +// TODO; Add proper benchmarks +const benchmarks: BestPracticeBenchmarks = { + "display-named-smart-contracts-mainnet": { + "mainnet-interactions-display-named-smart-contracts": undefined, + }, + "display-named-smart-contracts-l2-chains": { + "l2-chain-interactions-display-named-smart-contracts": undefined, + }, +}; + +defineAppBenchmarks(ZerionWallet, benchmarks); + +export default benchmarks; diff --git a/ensawards.org/data/apps/zerion-wallet/icon.tsx b/ensawards.org/data/apps/zerion-wallet/icon.tsx new file mode 100644 index 00000000..8a9292ef --- /dev/null +++ b/ensawards.org/data/apps/zerion-wallet/icon.tsx @@ -0,0 +1,32 @@ +import React from "react"; + +const Icon = (props: React.SVGProps) => ( + + + + + + + + + + + +); + +export default Icon; diff --git a/ensawards.org/data/apps/zerion-wallet/index.ts b/ensawards.org/data/apps/zerion-wallet/index.ts new file mode 100644 index 00000000..89f5aefc --- /dev/null +++ b/ensawards.org/data/apps/zerion-wallet/index.ts @@ -0,0 +1,28 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying apps + +import ZerionProject from "data/projects/zerion/index.ts"; + +import { defineApp } from "../registry.ts"; +import { type App, AppTypes } from "../types.ts"; +import ZerionIcon from "./icon.tsx"; + +const ZerionWallet: App = { + id: "zerion-wallet", + appSlug: "zerion-wallet", + type: AppTypes.Wallet, + project: ZerionProject, + name: "Zerion", + description: + "A non-custodial wallet built to trade any token on EVM and Solana, with NFT and DeFi portfolio management built in.", + socials: { + website: new URL("https://zerion.io/"), + twitter: new URL("https://x.com/zerion"), + }, + icon: ZerionIcon, + // TODO: Add OG images +}; + +defineApp(ZerionWallet); + +export default ZerionWallet; 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..466aa9e0 100644 --- a/ensawards.org/data/benchmarks/utils.test.ts +++ b/ensawards.org/data/benchmarks/utils.test.ts @@ -2,16 +2,16 @@ import { calcBestPracticeCategoryScore, calcEnsAwardsPoints, groupBenchmarksByCategory, - sortBenchmarks, + sortAcceptanceTestBenchmarks, } from "data/benchmarks/utils.ts"; import type { BestPracticeBenchmarks, BestPracticeSlug } from "data/ens-best-practices/types.ts"; import { - createMockBenchmark, + createMockAcceptanceTestBenchmark, mockDisplayProfilesBestPractice, mockForwardResolutionBestPractice, mockNormalizeNamesBestPractice, mockReverseResolutionBestPractice, -} from "data/shared/test-utils.ts"; +} from "data/shared/test-utils"; import { describe, expect, it, vi } from "vitest"; import { BenchmarkResults } from "./types.ts"; @@ -20,24 +20,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 +62,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 +89,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,8 +106,19 @@ 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, + }, + }, ]); }); }); @@ -99,9 +126,15 @@ describe("benchmarks-utils", () => { describe("calcBestPracticeCategoryScore", () => { it("Should return undefined for no completed benchmarks", () => { const benchmarks: BestPracticeBenchmarks = { - [mockReverseResolutionBestPractice.bestPracticeSlug]: undefined, - [mockDisplayProfilesBestPractice.bestPracticeSlug]: undefined, - [mockForwardResolutionBestPractice.bestPracticeSlug]: undefined, + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: undefined, + }, + [mockDisplayProfilesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug2: undefined, + }, + [mockForwardResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug3: undefined, + }, }; expect( @@ -112,15 +145,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, - ), + [mockReverseResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug1: createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + }, + [mockDisplayProfilesBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug2: createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + }, + [mockForwardResolutionBestPractice.bestPracticeSlug]: { + mockAcceptanceTestSlug3: createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + }, } as const satisfies BestPracticeBenchmarks; const result = calcBestPracticeCategoryScore(validCategoryBenchmarks); @@ -132,25 +165,25 @@ describe("benchmarks-utils", () => { }); }); - 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 = [ - createMockBenchmark(BenchmarkResults.Pass), - createMockBenchmark(BenchmarkResults.PartialPass), - createMockBenchmark(BenchmarkResults.Fail), - createMockBenchmark(BenchmarkResults.Fail), + createMockAcceptanceTestBenchmark(BenchmarkResults.Pass), + createMockAcceptanceTestBenchmark(BenchmarkResults.PartialPass), + createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), + createMockAcceptanceTestBenchmark(BenchmarkResults.Fail), undefined, ]; - const result = input.sort((a, b) => sortBenchmarks(a, b)); + const result = input.sort((a, b) => sortAcceptanceTestBenchmarks(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..b4fbb1a0 100644 --- a/ensawards.org/data/benchmarks/utils.ts +++ b/ensawards.org/data/benchmarks/utils.ts @@ -1,53 +1,87 @@ +import type { AcceptanceTestBenchmark } from "data/acceptance-tests/types.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 BestPracticeCategorySlug, BestPracticeTypes } 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); - for (const appBenchmarks of Object.values(APP_BENCHMARKS)) { - benchmarks.push(appBenchmarks[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]; + + 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 +92,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 +107,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}`); } }; @@ -112,9 +148,16 @@ export const groupBenchmarksByCategory = ( export const calcBestPracticeCategoryScore = ( benchmarks: BestPracticeBenchmarks, ): EnsAwardsScore | undefined => { - const completedBenchmarks = Object.values(benchmarks).filter( - (benchmark) => benchmark !== 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,10 +176,10 @@ 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 // and should be sorted after completed benchmarks @@ -148,7 +191,9 @@ export const sortBenchmarks = ( return resultDiff; }; -export const getBenchmarkLastUpdateTimestamp = (benchmark: AppBenchmark): UnixTimestamp => { +export const getAcceptanceTestBenchmarkLastUpdateTimestamp = ( + benchmark: AcceptanceTestBenchmark, +): UnixTimestamp => { const contributionTimestamps = benchmark.contributions.map( (contribution) => contribution.lastUpdated, ); @@ -157,16 +202,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 +222,7 @@ export const formatBenchmarkResult = ( return lowercase ? "failed" : "Failed"; default: - const _exhaustive: never = benchmark.result; + const _exhaustive: never = benchmarkResult; throw new Error(`Unsupported BenchmarkResult: ${_exhaustive}`); } }; 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 9095ae1f..00000000 --- a/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-l2-chains.ts +++ /dev/null @@ -1,61 +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: -- [ensnode-sdk](https://github.com/namehash/ensnode/blob/main/packages/ensnode-sdk/README.md) (v1.0.0+) -- [ensnode-react](https://github.com/namehash/ensnode/blob/main/packages/ensnode-react/README.md) (v1.0.0+) -- [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..34fa09ab --- /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, AppTypes.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(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 95b385f8..00000000 --- a/ensawards.org/data/ens-best-practices/contract-naming/display-named-smart-contracts-mainnet.ts +++ /dev/null @@ -1,54 +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: -- [ensnode-sdk](https://github.com/namehash/ensnode/blob/main/packages/ensnode-sdk/README.md) (v1.0.0+) -- [ensnode-react](https://github.com/namehash/ensnode/blob/main/packages/ensnode-react/README.md) (v1.0.0+) -- [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..97d0ad44 --- /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, AppTypes.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(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/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/index.ts b/ensawards.org/data/ens-best-practices/index.ts index 8df45258..2c5248c2 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)), 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..dcb0c7b7 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,13 @@ export type BestPracticeTarget = AppType | ProtocolType; */ export type BestPracticeSlug = string; +export interface BestPracticeTechnicalDetails { + useCaseSummary: JSX.Element; + benefitFromUsingEns: JSX.Element; + implementationRecommendations: JSX.Element; + acceptanceTests: [AcceptanceTest, ...AcceptanceTest[]]; +} + export interface BestPracticeAbstract< BestPracticeT extends BestPracticeType, AppliesToT extends BestPracticeTarget, @@ -31,16 +40,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 +53,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", 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..1dd08b77 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"; @@ -95,7 +97,8 @@ 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}. @@ -103,52 +106,30 @@ export const formatBestPracticeType = ( export const calcBestPracticeScore = ( bestPractice: BestPracticeApp, ): EnsAwardsScore | undefined => { - let benchmarkedApps = 0; + 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 }, @@ -164,6 +145,10 @@ export const formatBestPracticeTarget = ( formattedTarget = plural ? "Wallets" : "Wallet"; break; + case AppTypes.DeFi: + formattedTarget = plural ? "DeFi apps" : "DeFi app"; + break; + case ProtocolTypes.DAO: formattedTarget = plural ? "DAOs" : "DAO"; break; diff --git a/ensawards.org/data/projects/1inch/icon.tsx b/ensawards.org/data/projects/1inch/icon.tsx new file mode 100644 index 00000000..8f716c6d --- /dev/null +++ b/ensawards.org/data/projects/1inch/icon.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +const Icon = ({ className, ...props }: React.SVGProps) => ( + + + + +); + +export default Icon; diff --git a/ensawards.org/data/projects/1inch/index.ts b/ensawards.org/data/projects/1inch/index.ts new file mode 100644 index 00000000..5ddaef39 --- /dev/null +++ b/ensawards.org/data/projects/1inch/index.ts @@ -0,0 +1,23 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + +import { defineProject } from "../registry.ts"; +import type { Project } from "../types.ts"; +import { ProjectIds } from "../types.ts"; +import OneInchIcon from "./icon.tsx"; + +const OneInchProject: Project = { + id: ProjectIds.OneInch, + name: "1inch", + description: + "A DeFi ecosystem integrating 13+ chains with the only native connection between Solana and EVM networks, helping users trade, hold, and track digital assets with true self-custody.", + icon: OneInchIcon, + socials: { + website: new URL("https://1inch.com/"), + twitter: new URL("https://x.com/1inch"), + }, +}; + +defineProject(OneInchProject); + +export default OneInchProject; diff --git a/ensawards.org/data/projects/aave/index.ts b/ensawards.org/data/projects/aave/index.ts index d1b2ca2c..015c2c73 100644 --- a/ensawards.org/data/projects/aave/index.ts +++ b/ensawards.org/data/projects/aave/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/ambire/index.ts b/ensawards.org/data/projects/ambire/index.ts index 4c7c73bb..33a39523 100644 --- a/ensawards.org/data/projects/ambire/index.ts +++ b/ensawards.org/data/projects/ambire/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/arbitrum/index.ts b/ensawards.org/data/projects/arbitrum/index.ts index c1b6bf0c..65691ab5 100644 --- a/ensawards.org/data/projects/arbitrum/index.ts +++ b/ensawards.org/data/projects/arbitrum/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/blockscout/index.ts b/ensawards.org/data/projects/blockscout/index.ts index c5ab4b89..ed6a9b96 100644 --- a/ensawards.org/data/projects/blockscout/index.ts +++ b/ensawards.org/data/projects/blockscout/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/coinbase/index.ts b/ensawards.org/data/projects/coinbase/index.ts index c6c061c2..9a26939a 100644 --- a/ensawards.org/data/projects/coinbase/index.ts +++ b/ensawards.org/data/projects/coinbase/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/cork/index.ts b/ensawards.org/data/projects/cork/index.ts index d3590ae1..e3b8be5c 100644 --- a/ensawards.org/data/projects/cork/index.ts +++ b/ensawards.org/data/projects/cork/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/ens/index.ts b/ensawards.org/data/projects/ens/index.ts index 986e8d27..b77bb536 100644 --- a/ensawards.org/data/projects/ens/index.ts +++ b/ensawards.org/data/projects/ens/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import { type Project, ProjectIds } from "../types.ts"; import EnsIcon from "./icon.tsx"; diff --git a/ensawards.org/data/projects/etherscan/index.ts b/ensawards.org/data/projects/etherscan/index.ts index 60c05d92..5b92e08d 100644 --- a/ensawards.org/data/projects/etherscan/index.ts +++ b/ensawards.org/data/projects/etherscan/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/giveth/index.ts b/ensawards.org/data/projects/giveth/index.ts index c24a1b76..c3b4f387 100644 --- a/ensawards.org/data/projects/giveth/index.ts +++ b/ensawards.org/data/projects/giveth/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/ledger/icon.tsx b/ensawards.org/data/projects/ledger/icon.tsx new file mode 100644 index 00000000..dc4fe9c7 --- /dev/null +++ b/ensawards.org/data/projects/ledger/icon.tsx @@ -0,0 +1,25 @@ +import React from "react"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +const Icon = ({ className, ...props }: React.SVGProps) => ( + + + +); + +export default Icon; diff --git a/ensawards.org/data/projects/ledger/index.ts b/ensawards.org/data/projects/ledger/index.ts new file mode 100644 index 00000000..c031e97f --- /dev/null +++ b/ensawards.org/data/projects/ledger/index.ts @@ -0,0 +1,23 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + +import { defineProject } from "data/projects/registry"; +import { type Project, ProjectIds } from "data/projects/types"; + +import LedgerIcon from "./icon.tsx"; + +const LedgerProject: Project = { + id: ProjectIds.Ledger, + name: "Ledger", + description: + "Ledger is a self-custody platform built around hardware wallets that secure digital assets offline, paired with a companion app for managing crypto and Web3.", + icon: LedgerIcon, + socials: { + website: new URL("https://ledger.com"), + twitter: new URL("https://x.com/Ledger"), + }, +}; + +defineProject(LedgerProject); + +export default LedgerProject; diff --git a/ensawards.org/data/projects/liquity/index.ts b/ensawards.org/data/projects/liquity/index.ts index fa1fd01b..3433b312 100644 --- a/ensawards.org/data/projects/liquity/index.ts +++ b/ensawards.org/data/projects/liquity/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/metamask/index.ts b/ensawards.org/data/projects/metamask/index.ts index 06849cbe..47d0a48f 100644 --- a/ensawards.org/data/projects/metamask/index.ts +++ b/ensawards.org/data/projects/metamask/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/nouns/index.ts b/ensawards.org/data/projects/nouns/index.ts index 6c90a24e..ce48cd53 100644 --- a/ensawards.org/data/projects/nouns/index.ts +++ b/ensawards.org/data/projects/nouns/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/rabby/icon.tsx b/ensawards.org/data/projects/rabby/icon.tsx new file mode 100644 index 00000000..7d1dd3ac --- /dev/null +++ b/ensawards.org/data/projects/rabby/icon.tsx @@ -0,0 +1,81 @@ +import React from "react"; + +const Icon = (props: React.SVGProps) => ( + + + + + + + + + + + + + + + + + + + + + + + + +); + +export default Icon; diff --git a/ensawards.org/data/projects/rabby/index.ts b/ensawards.org/data/projects/rabby/index.ts new file mode 100644 index 00000000..d741b65b --- /dev/null +++ b/ensawards.org/data/projects/rabby/index.ts @@ -0,0 +1,23 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + +import { defineProject } from "data/projects/registry"; +import { type Project, ProjectIds } from "data/projects/types"; + +import RabbyIcon from "./icon.tsx"; + +const RabbyProject: Project = { + id: ProjectIds.Rabby, + name: "Rabby", + description: + "An open-source, security-focused browser wallet built for DeFi on Ethereum and every EVM chain.", + icon: RabbyIcon, + socials: { + website: new URL("https://rabby.io/"), + twitter: new URL("https://x.com/Rabby_io"), + }, +}; + +defineProject(RabbyProject); + +export default RabbyProject; diff --git a/ensawards.org/data/projects/rainbow/index.ts b/ensawards.org/data/projects/rainbow/index.ts index d279e5e7..ef369c14 100644 --- a/ensawards.org/data/projects/rainbow/index.ts +++ b/ensawards.org/data/projects/rainbow/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/ready/icon.tsx b/ensawards.org/data/projects/ready/icon.tsx new file mode 100644 index 00000000..f9038398 --- /dev/null +++ b/ensawards.org/data/projects/ready/icon.tsx @@ -0,0 +1,18 @@ +const Icon = (props: React.SVGProps) => ( + + + +); + +export default Icon; diff --git a/ensawards.org/data/projects/ready/index.ts b/ensawards.org/data/projects/ready/index.ts new file mode 100644 index 00000000..3145167b --- /dev/null +++ b/ensawards.org/data/projects/ready/index.ts @@ -0,0 +1,23 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + +import { defineProject } from "data/projects/registry"; +import { type Project, ProjectIds } from "data/projects/types"; + +import ReadyIcon from "./icon.tsx"; + +const ReadyProject: Project = { + id: ProjectIds.Ready, + name: "Ready", + description: + "Ready is an onchain alternative to traditional banking, combining a self-custody wallet, debit card, and crypto yield in one app.", + icon: ReadyIcon, + socials: { + website: new URL("https://www.ready.co/"), + twitter: new URL("https://x.com/ready_co"), + }, +}; + +defineProject(ReadyProject); + +export default ReadyProject; diff --git a/ensawards.org/data/projects/safe/icon.tsx b/ensawards.org/data/projects/safe/icon.tsx new file mode 100644 index 00000000..628ce3cc --- /dev/null +++ b/ensawards.org/data/projects/safe/icon.tsx @@ -0,0 +1,33 @@ +import React from "react"; + +const Icon = (props: React.SVGProps) => ( + + + + + +); + +export default Icon; diff --git a/ensawards.org/data/projects/safe/index.ts b/ensawards.org/data/projects/safe/index.ts new file mode 100644 index 00000000..69c4b189 --- /dev/null +++ b/ensawards.org/data/projects/safe/index.ts @@ -0,0 +1,23 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + +import { defineProject } from "data/projects/registry"; +import { type Project, ProjectIds } from "data/projects/types"; + +import SafeIcon from "./icon.tsx"; + +const SafeProject: Project = { + id: ProjectIds.Safe, + name: "Safe", + description: + "Safe is modular smart account infrastructure securing onchain assets for individuals, DAOs, and organizations.", + icon: SafeIcon, + socials: { + website: new URL("https://safe.global/"), + twitter: new URL("https://x.com/SafeLabs_"), + }, +}; + +defineProject(SafeProject); + +export default SafeProject; diff --git a/ensawards.org/data/projects/ssvnetwork/index.ts b/ensawards.org/data/projects/ssvnetwork/index.ts index 4998fa28..454f00c0 100644 --- a/ensawards.org/data/projects/ssvnetwork/index.ts +++ b/ensawards.org/data/projects/ssvnetwork/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/status/icon.tsx b/ensawards.org/data/projects/status/icon.tsx new file mode 100644 index 00000000..9e4e453b --- /dev/null +++ b/ensawards.org/data/projects/status/icon.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +const Icon = (props: React.SVGProps) => ( + + + +); + +export default Icon; diff --git a/ensawards.org/data/projects/status/index.ts b/ensawards.org/data/projects/status/index.ts new file mode 100644 index 00000000..103e2581 --- /dev/null +++ b/ensawards.org/data/projects/status/index.ts @@ -0,0 +1,23 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + +import { defineProject } from "data/projects/registry"; +import { type Project, ProjectIds } from "data/projects/types"; + +import StatusIcon from "./icon.tsx"; + +const StatusProject: Project = { + id: ProjectIds.Status, + name: "Status", + description: + "Status is an open-source, decentralized super app combining a private messenger, self-custody Web3 wallet, and dApp browser on Ethereum.", + icon: StatusIcon, + socials: { + website: new URL("https://status.app/"), + twitter: new URL("https://x.com/ethstatus"), + }, +}; + +defineProject(StatusProject); + +export default StatusProject; diff --git a/ensawards.org/data/projects/taiko/index.ts b/ensawards.org/data/projects/taiko/index.ts index ad272abb..cd39c9cb 100644 --- a/ensawards.org/data/projects/taiko/index.ts +++ b/ensawards.org/data/projects/taiko/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/types.ts b/ensawards.org/data/projects/types.ts index dbcf20ce..dce2904b 100644 --- a/ensawards.org/data/projects/types.ts +++ b/ensawards.org/data/projects/types.ts @@ -18,6 +18,14 @@ export const ProjectIds = { SSVNetwork: "project-ssvnetwork", WalletChan: "project-walletchan", Ambire: "project-ambire", + Safe: "project-safe", + Rabby: "project-rabby", + Ready: "project-ready", + Zerion: "project-zerion", + Status: "project-status", + Ledger: "project-ledger", + World: "project-world", + OneInch: "project-1inch", } as const; /** diff --git a/ensawards.org/data/projects/uniswap/index.ts b/ensawards.org/data/projects/uniswap/index.ts index 2b4be327..e4c43026 100644 --- a/ensawards.org/data/projects/uniswap/index.ts +++ b/ensawards.org/data/projects/uniswap/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/walletchan/index.ts b/ensawards.org/data/projects/walletchan/index.ts index 6913e2f0..1b4f5345 100644 --- a/ensawards.org/data/projects/walletchan/index.ts +++ b/ensawards.org/data/projects/walletchan/index.ts @@ -1,3 +1,6 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + import { defineProject } from "../registry.ts"; import type { Project } from "../types.ts"; import { ProjectIds } from "../types.ts"; diff --git a/ensawards.org/data/projects/world/icon.tsx b/ensawards.org/data/projects/world/icon.tsx new file mode 100644 index 00000000..6d2591ae --- /dev/null +++ b/ensawards.org/data/projects/world/icon.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +import { cn } from "@/utils/tailwindClassConcatenation"; + +const Icon = ({ className, ...props }: React.SVGProps) => ( + + + +); + +export default Icon; diff --git a/ensawards.org/data/projects/world/index.ts b/ensawards.org/data/projects/world/index.ts new file mode 100644 index 00000000..9fc32cd8 --- /dev/null +++ b/ensawards.org/data/projects/world/index.ts @@ -0,0 +1,23 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + +import { defineProject } from "data/projects/registry"; +import { type Project, ProjectIds } from "data/projects/types"; + +import WorldIcon from "./icon.tsx"; + +const WorldProject: Project = { + id: ProjectIds.World, + name: "World", + description: + "World is a new standard of trust for the internet - private proof of human, powerful financial infrastructure & the human-first experiences they enable.", + icon: WorldIcon, + socials: { + website: new URL("https://world.org/"), + twitter: new URL("https://x.com/worldnetwork"), + }, +}; + +defineProject(WorldProject); + +export default WorldProject; diff --git a/ensawards.org/data/projects/zerion/icon.tsx b/ensawards.org/data/projects/zerion/icon.tsx new file mode 100644 index 00000000..8a9292ef --- /dev/null +++ b/ensawards.org/data/projects/zerion/icon.tsx @@ -0,0 +1,32 @@ +import React from "react"; + +const Icon = (props: React.SVGProps) => ( + + + + + + + + + + + +); + +export default Icon; diff --git a/ensawards.org/data/projects/zerion/index.ts b/ensawards.org/data/projects/zerion/index.ts new file mode 100644 index 00000000..a8cc1241 --- /dev/null +++ b/ensawards.org/data/projects/zerion/index.ts @@ -0,0 +1,23 @@ +// Read https://github.com/namehash/ensawards/blob/main/CONTRIBUTING.md +// for additional advice on adding and modifying projects + +import { defineProject } from "data/projects/registry"; +import { type Project, ProjectIds } from "data/projects/types"; + +import ZerionIcon from "./icon.tsx"; + +const ZerionProject: Project = { + id: ProjectIds.Zerion, + name: "Zerion", + description: + "Zerion is an all-in-one Web3 platform combining a self-custody wallet, portfolio tracker, and APIs powering some of the most popular apps in crypto.", + icon: ZerionIcon, + socials: { + website: new URL("https://zerion.io/"), + twitter: new URL("https://x.com/zerion"), + }, +}; + +defineProject(ZerionProject); + +export default ZerionProject; diff --git a/ensawards.org/data/protocols/types.ts b/ensawards.org/data/protocols/types.ts index 453380c6..8acff1d9 100644 --- a/ensawards.org/data/protocols/types.ts +++ b/ensawards.org/data/protocols/types.ts @@ -47,7 +47,7 @@ export type ProtocolId = (typeof ProtocolIds)[keyof typeof ProtocolIds]; export const ProtocolTypes = { DAO: "dao", - DeFi: "defi", + DeFi: "defi-protocol", } as const; export type ProtocolType = (typeof ProtocolTypes)[keyof typeof ProtocolTypes]; diff --git a/ensawards.org/data/protocols/utils.ts b/ensawards.org/data/protocols/utils.ts index 25d43514..84edb291 100644 --- a/ensawards.org/data/protocols/utils.ts +++ b/ensawards.org/data/protocols/utils.ts @@ -84,7 +84,7 @@ export const asProtocolType = (maybeProtocolType: string): ProtocolType => { case "dao": return ProtocolTypes.DAO; - case "defi": + case "defi-protocol": return ProtocolTypes.DeFi; default: @@ -135,7 +135,7 @@ export const formatProtocolType = ( switch (protocolType) { case ProtocolTypes.DeFi: - return lowercase ? "defi" : "DeFi"; + return lowercase ? "defi protocol" : "DeFi protocol"; case ProtocolTypes.DAO: return lowercase ? "dao" : "DAO"; 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..7a02f6a6 100644 --- a/ensawards.org/src/components/atoms/cards/AppSummaryCard.tsx +++ b/ensawards.org/src/components/atoms/cards/AppSummaryCard.tsx @@ -1,18 +1,18 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; +import type { AcceptanceTestBenchmark } from "data/acceptance-tests/types.ts"; +import { generalizeAcceptanceTestBenchmarks } from "data/acceptance-tests/utils.ts"; 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, + sortAcceptanceTestBenchmarks, } from "data/benchmarks/utils.ts"; import type { BestPracticeBenchmarks, BestPracticeCategorySlug, - BestPracticeSlug, } from "data/ens-best-practices/types.ts"; import { getBestPracticeBySlug, @@ -51,6 +51,43 @@ function BenchmarkCategorySection({ throw new Error(`Invariant(CategorySlug): Category with slug ${categorySlug} not found`); } + const appBenchmarksData = [...Object.entries(benchmarksInCategory)] + .map(([bestPracticeSlug, bestPracticeBenchmarks]) => { + const bestPractice = getBestPracticeBySlug(bestPracticeSlug); + + if (bestPractice === undefined) { + throw new Error( + `Invariant(BestPracticeSlug): Best practice with slug ${bestPracticeSlug} not found`, + ); + } + + const referenceAcceptanceTestBenchmark = Object.values(bestPracticeBenchmarks).find( + (benchmark) => benchmark !== undefined, + ); + + if (referenceAcceptanceTestBenchmark === undefined) { + return { bestPractice, generalizedBenchmark: undefined }; + } + + const generalizedBenchmarkResult = generalizeAcceptanceTestBenchmarks(bestPracticeBenchmarks); + const generalizedBenchmark = + generalizedBenchmarkResult === undefined + ? undefined + : ({ + result: generalizedBenchmarkResult, + // take contributions and notes from the first defined acceptance test benchmark as a reference + // (guaranteed to exist if generalizedBenchmarkResult is not undefined) + contributions: referenceAcceptanceTestBenchmark.contributions, + notes: referenceAcceptanceTestBenchmark.notes, + } as AcceptanceTestBenchmark); + + return { + bestPractice, + generalizedBenchmark, + }; + }) + .sort((a, b) => sortAcceptanceTestBenchmarks(a.generalizedBenchmark, b.generalizedBenchmark)); + return (
)}
diff --git a/ensawards.org/src/components/atoms/cards/AwardsCard.astro b/ensawards.org/src/components/atoms/cards/AwardsCard.astro index 542c589a..bc7967e5 100644 --- a/ensawards.org/src/components/atoms/cards/AwardsCard.astro +++ b/ensawards.org/src/components/atoms/cards/AwardsCard.astro @@ -32,10 +32,10 @@ const entityAwards =

- 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 efeb7ace..35df6878 100644 --- a/ensawards.org/src/components/atoms/cards/ContributorsCard.tsx +++ b/ensawards.org/src/components/atoms/cards/ContributorsCard.tsx @@ -2,14 +2,13 @@ import { ResolveAndDisplayIdentity } from "@namehash/namehash-ui"; 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 { createEnsNodeProviderOptions, EnsNodeProvider } from "@ensnode/ensnode-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"; @@ -47,6 +46,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", @@ -111,84 +131,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 +
@@ -223,7 +206,7 @@ const ContributorTooltipContent = ({ contributor, identity }: ContributorTooltip /> View ENS Advocate profile @@ -232,6 +215,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, @@ -254,7 +271,12 @@ export const ContributorsCardLoading = ({ -
+
@@ -263,21 +285,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..e1e270be --- /dev/null +++ b/ensawards.org/src/components/molecules/technicalDetails/benchmark/acceptanceTestResultDetails.tsx @@ -0,0 +1,73 @@ +import { RelativeTime } from "@namehash/namehash-ui"; +import type { AcceptanceTestBenchmark } from "data/acceptance-tests/types"; +import { useMemo } from "react"; + +import { createEnsNodeProviderOptions, EnsNodeProvider } from "@ensnode/ensnode-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..057aa10e --- /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 { + DesiredOutcome, + 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..d0e0cc44 --- /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, + DesiredOutcome, + 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..ea579d26 --- /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 DesiredOutcome = ({ bestPractice }: { bestPractice: BestPractice }) => ( +
+

+ 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 +131,58 @@ if (app !== undefined) { ); } + const benchmarksOnBestPractice = [ + ...Object.entries(benchmarksInCategory), + ] + .map(([bestPracticeSlug, bestPracticeBenchmarks]) => { + const bestPractice = + getBestPracticeBySlug(bestPracticeSlug); + + if (bestPractice === undefined) { + throw new Error( + `Invariant(BestPracticeSlug): Best practice with slug ${bestPracticeSlug} not found`, + ); + } + + const referenceAcceptanceTestBenchmark = Object.values( + bestPracticeBenchmarks, + ).find((benchmark) => benchmark !== undefined); + + if (referenceAcceptanceTestBenchmark === undefined) { + return { + bestPractice, + generalizedBenchmark: undefined, + }; + } + + const generalizedBenchmarkResult = + generalizeAcceptanceTestBenchmarks( + bestPracticeBenchmarks, + ); + const generalizedBenchmark = + generalizedBenchmarkResult === undefined + ? undefined + : ({ + result: generalizedBenchmarkResult, + // take contributions and notes from the first defined acceptance test benchmark as a reference + // (guaranteed to exist if generalizedBenchmarkResult is not undefined) + contributions: + referenceAcceptanceTestBenchmark.contributions, + notes: referenceAcceptanceTestBenchmark.notes, + } as AcceptanceTestBenchmark); + + return { + bestPractice, + generalizedBenchmark, + }; + }) + .sort((a, b) => + sortAcceptanceTestBenchmarks( + a.generalizedBenchmark, + b.generalizedBenchmark, + ), + ); + return (
@@ -138,40 +191,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, generalizedBenchmark }) => { + return ( + + + + ); + }, + )}
); })} @@ -182,7 +219,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> + viewDetailsText={`${formatProtocolType(protocolType)} report`}> {`${idx ({ diff --git a/ensawards.org/src/pages/leaderboards/[protocolType].astro b/ensawards.org/src/pages/leaderboards/[protocolType].astro index 4b8ac03e..c9aceed0 100644 --- a/ensawards.org/src/pages/leaderboards/[protocolType].astro +++ b/ensawards.org/src/pages/leaderboards/[protocolType].astro @@ -88,7 +88,9 @@ const protocolContractDataContributions: Contribution[] = Object.keys(protocolSc name={protocol.name} ensAwardsScore={value} viewDetailsHref={`/protocol/${protocol.protocolSlug}`} - viewDetailsText={`${formatProtocolType(protocolType, { lowercase: false })}${protocolType !== ProtocolTypes.DAO ? " protocol" : ""} report`}> + viewDetailsText={formatProtocolType(protocolType, { + lowercase: false, + })}> {idx < 3 ? ( {`${idx 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/package.json b/package.json index 18b3e978..7f274285 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "picomatch@>=4.0.0 <4.0.4": ">=4.0.4 <5.0.0", "postcss@<8.5.10": ">=8.5.10 <9.0.0", "smol-toml@<1.6.1": ">=1.6.1 <2.0.0", - "brace-expansion@<5.0.5": ">=5.0.5 <6.0.0" + "brace-expansion@<5.0.5": ">=5.0.5 <6.0.0", + "fast-uri@<3.1.2": ">=3.1.2 <4.0.0" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 484e49df..a530a82b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,7 @@ overrides: postcss@<8.5.10: '>=8.5.10 <9.0.0' smol-toml@<1.6.1: '>=1.6.1 <2.0.0' brace-expansion@<5.0.5: '>=5.0.5 <6.0.0' + fast-uri@<3.1.2: '>=3.1.2 <4.0.0' importers: @@ -2176,8 +2177,8 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -5098,7 +5099,7 @@ snapshots: ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 + fast-uri: 3.1.2 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -5499,7 +5500,7 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-uri@3.1.0: {} + fast-uri@3.1.2: {} fdir@6.5.0(picomatch@4.0.4): optionalDependencies: